ComfyUI-WorkflowPublisher/js/publisher.js

406 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// 本地存储的 key
const SETTINGS_STORAGE_KEY = "comfyui_publisher_settings";
// 获取本地设置
function getLocalSettings() {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
return stored ? JSON.parse(stored) : { host: "" };
} catch (error) {
console.error("读取本地设置失败:", error);
return { host: "" };
}
}
// 保存设置到本地
function saveLocalSettings(settings) {
try {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch (error) {
console.error("保存本地设置失败:", error);
}
}
// 自定义 fetchApi 函数,使用本地存储的 host
async function customFetchApi(endpoint, options = {}) {
try {
// 从本地存储获取设置
const settings = getLocalSettings();
// 如果没有配置 host回退到默认的 api.fetchApi
if (!settings.host) {
console.warn("未配置 host使用默认的 ComfyUI API");
return await api.fetchApi(endpoint, options);
}
// 构建完整的 URL
const host = settings.host.replace(/\/$/, ""); // 移除末尾的斜杠
const fullUrl = `${host}${endpoint}`;
console.log(`使用本地存储的 host 请求: ${fullUrl}`);
// 使用 fetch 发送请求
const response = await fetch(fullUrl, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
return response;
} catch (error) {
console.error("自定义 fetchApi 失败,回退到默认 API:", error);
// 如果自定义请求失败,回退到默认的 api.fetchApi
return await api.fetchApi(endpoint, options);
}
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch (error) {
console.error("JSON解析失败:", error, "原始数据:", text);
return null;
}
}
function createElement(tagName, props) {
const element = document.createElement(tagName);
if (props.style) element.style = props.style;
if (props.className) element.className = props.className;
if (props.id) element.id = props.id;
if (props.textContent) element.textContent = props.textContent;
if (props.innerHTML) element.innerHTML = props.innerHTML;
if (props.onclick) element.onclick = props.onclick;
if (props.children) {
if (Array.isArray(props.children)) {
props.children.forEach((child) => {
element.appendChild(child);
});
} else {
element.appendChild(props.children);
}
}
return element;
}
// Modal class (保持不变)
class Modal {
constructor(id, title, full_width = false) {
this.id = id;
this.element = document.createElement("div");
this.element.id = this.id;
this.element.style.cssText = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--comfy-menu-bg); color: var(--fg-color); padding: 20px; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5); z-index: 1001; display: none; flex-direction: column; gap: 15px; min-width: ${
full_width ? "500px" : "350px"
}; max-width: 600px; max-height: 80vh;`;
this.element.innerHTML = `<h3 style="margin: 0; padding-bottom: 10px; border-bottom: 1px solid var(--border-color);">${title}</h3><div class="content" style="overflow-y: auto; padding-right: 5px;"></div><div class="buttons" style="display: flex; justify-content: flex-end; gap: 10px;"></div>`;
document.body.appendChild(this.element);
}
show() {
this.element.style.display = "flex";
}
hide() {
this.element.style.display = "none";
}
setContent(html) {
this.element.querySelector(".content").innerHTML = html;
}
addButtons(buttons) {
const container = this.element.querySelector(".buttons");
container.innerHTML = "";
buttons.forEach((btn) => {
const button = document.createElement("button");
button.textContent = btn.label;
button.onclick = btn.callback;
container.appendChild(button);
});
}
}
const createButton = (innerHTML, title, { onClick }) => {
const btn = document.createElement("button");
btn.className = "jags-button";
btn.innerHTML = innerHTML;
btn.title = title;
btn.onclick = onClick;
return btn;
};
app.registerExtension({
name: "Comfy.WorkflowPublisher",
async setup() {
const addCustomStyles = () => {
const styleId = "jags-publisher-styles";
if (document.getElementById(styleId)) return;
const style = document.createElement("style");
style.id = styleId;
style.innerHTML = `
.jags-button { background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: var(--fg-color); border-radius: 8px; padding: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease-in-out; outline: none; position: relative; }
.jags-button:hover { background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); transform: translateY(-1px); box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2); }
.jags-button:active { background-color: rgba(255, 255, 255, 0.15); transform: translateY(0px) scale(0.98); box-shadow: none; }
.jags-accordion-header { background-color: var(--comfy-input-bg); width: 100%; border: 1px solid var(--border-color); padding: 10px; text-align: left; cursor: pointer; transition: background-color 0.2s; display: flex; justify-content: space-between; align-items: center; border-radius: 4px; }
.jags-accordion-header:hover { background-color: var(--comfy-menu-bg); }
.jags-accordion-panel { padding-left: 15px; margin-top: 5px; border-left: 2px solid var(--border-color); max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out, margin-top 0.3s ease-out; }
.jags-version-item { display: flex; justify-content: space-between; align-items: center; padding: 2px 5px; border-radius: 3px; cursor: pointer; }
.jags-version-item:hover { background-color: var(--comfy-input-bg); }
.jags-version-item-label { flex-grow: 1; padding: 6px 0; }
.jags-delete-btn { background: transparent; border: none; color: var(--error-color); cursor: pointer; font-size: 1.2em; padding: 4px 8px; border-radius: 50%; line-height: 1; }
.jags-delete-btn:hover { background-color: var(--error-color); color: var(--comfy-menu-bg); }
`;
document.head.appendChild(style);
};
addCustomStyles();
const waitForElement = (selector, callback, timeout = 10000) => {
const startTime = Date.now();
const interval = setInterval(() => {
const element = document.getElementsByClassName(selector)[0];
if (element) {
clearInterval(interval);
callback(element);
} else if (Date.now() - startTime > timeout) {
clearInterval(interval);
console.error(
`Workflow Publisher: Timed out waiting for element: "${selector}"`
);
}
}, 100);
};
waitForElement("queue-button-group flex", (menu) => {
const container = document.createElement("div");
container.id = "jags-publisher-container";
container.style.display = "flex";
container.style.alignItems = "center";
container.style.gap = "8px";
const iconGet = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg>`;
const iconPublish = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" /><path d="M7 9l5-5 5 5" /><path d="M12 4v12" /></svg>`;
const btnGet = createButton(iconGet, "从服务器获取工作流", {
onClick: () => showWorkflowListDialog(),
});
const btnPublish = createButton(iconPublish, "发布当前工作流", {
onClick: () => showPublishDialog(),
});
const btnSettings = createButton(`⚙️`, "发布设置", {
onClick: () => showSettingsDialog(),
});
const separator = document.createElement("div");
separator.style.cssText =
"border-left: 1px solid var(--border-color); height: 22px; margin: 0 4px;";
container.append(btnGet, btnPublish, btnSettings, separator);
menu.insertBefore(container, menu.firstChild);
});
const settingsDialog = new Modal(
"workflow-publisher-settings-dialog",
"发布设置"
);
const publishDialog = new Modal(
"workflow-publisher-publish-dialog",
"发布工作流"
);
const workflowListDialog = new Modal(
"workflow-publisher-list-dialog",
"工作流库",
true
);
async function showWorkflowListDialog() {
workflowListDialog.setContent(
'<p style="text-align:center;">正在从服务器加载工作流...</p>'
);
workflowListDialog.addButtons([
{ label: "关闭", callback: () => workflowListDialog.hide() },
]);
workflowListDialog.show();
try {
const response = await customFetchApi("/api/workflow");
if (!response.ok) {
throw new Error(
`服务器错误: ${response.status} ${response.statusText}`
);
}
const text = await response.text();
const workflows = parseJson(text);
if (workflows.length === 0) {
workflowListDialog.setContent(
'<p style="text-align:center;">服务器上没有找到任何工作流。</p>'
);
return;
}
const accordionContainer = document.createElement("div");
accordionContainer.style.cssText =
"display: flex; flex-direction: column; gap: 8px;";
workflows.forEach((workflow) => {
const workflowLabel = document.createElement("span");
workflowLabel.textContent = workflow.name;
workflowLabel.onclick = () => {
if (
confirm(
`确定要加载工作流 "${workflow.name}" 吗?\n这将覆盖当前画布。`
)
) {
const file = new File(
[JSON.stringify(workflow.workflow)],
workflow.name,
{ type: "application/json" }
);
app.handleFile(file);
}
};
accordionContainer.appendChild(workflowLabel);
});
workflowListDialog.setContent("");
workflowListDialog.element
.querySelector(".content")
.appendChild(accordionContainer);
} catch (error) {
console.error("加载工作流失败:", error);
workflowListDialog.setContent(
`<p style="color:var(--error-color); text-align:center;">加载失败: ${error.message}</p>`
);
}
}
async function publishWorkflow() {
const flowName = document
.getElementById("publisher-workflow-name")
.value.trim();
if (!flowName) {
alert("请输入工作流的基础名称。");
return;
}
const statusEl = document.getElementById("publisher-status");
statusEl.textContent = "正在准备发布...";
statusEl.style.color = "orange";
const versionString = new Date().toISOString();
try {
const workflow = await app.graph.serialize();
const body = { name: flowName, workflow: workflow };
statusEl.textContent = `正在发布为 "${flowName}",版本号: ${versionString}...`;
const response = await customFetchApi("/api/workflow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`服务器返回错误: ${response.status} ${response.statusText}${
errorText ? ` - ${errorText}` : ""
}`
);
}
const text = await response.text();
let result;
try {
result = text ? JSON.parse(text) : {};
} catch (parseError) {
console.error("JSON解析失败:", parseError, "原始响应:", text);
result = { message: "发布成功,但服务器返回的数据格式无效" };
}
statusEl.textContent = `发布成功! ${result.message || ""}`;
statusEl.style.color = "var(--success-color)";
setTimeout(() => publishDialog.hide(), 2000);
} catch (error) {
statusEl.textContent = `发布失败: ${error.message}`;
statusEl.style.color = "var(--error-color)";
console.error("发布失败:", error);
}
}
async function showSettingsDialog() {
try {
// 首先尝试从本地存储获取设置
const settings = getLocalSettings();
settingsDialog.setContent(`
<label for="publisher-host" style="display: block; margin-bottom: 5px;">Host:</label>
<input id="publisher-host" type="text" value="${settings.host}" placeholder="例如: http://localhost:8000" style="width: 100%; box-sizing: border-box; background: var(--comfy-input-bg); color: var(--fg-color); border: 1px solid var(--border-color); padding: 5px; border-radius: 4px; margin-bottom: 10px;">
`);
settingsDialog.addButtons([
{ label: "取消", callback: () => settingsDialog.hide() },
{ label: "保存", callback: saveSettings },
]);
settingsDialog.show();
} catch (error) {
console.error("加载设置失败:", error);
alert(`加载设置失败: ${error.message}`);
}
}
async function saveSettings() {
const host = document.getElementById("publisher-host").value;
// 构建新的设置对象
const newSettings = { host: host };
try {
// 保存到本地存储
saveLocalSettings(newSettings);
alert("设置已保存到本地!");
settingsDialog.hide();
} catch (error) {
alert(`保存设置失败: ${error}`);
}
}
async function showPublishDialog() {
try {
// 从本地存储获取设置
let settings = getLocalSettings();
// 如果本地没有设置,尝试从服务器获取
if (!settings.host) {
try {
const settingsResp = await customFetchApi("/publisher/settings", {
cache: "no-store",
});
if (settingsResp.ok) {
const text = await settingsResp.text();
try {
const serverSettings = text ? JSON.parse(text) : {};
settings = { ...settings, ...serverSettings };
// 保存到本地存储
saveLocalSettings(settings);
} catch (parseError) {
console.error("JSON解析失败:", parseError, "原始响应:", text);
}
}
} catch (error) {
console.error("从服务器获取设置失败:", error);
}
}
// 确保设置对象有必需的字段
settings = {
host: settings.host || "",
...settings,
};
publishDialog.setContent(
`<label for="publisher-workflow-name" style="display: block; margin-bottom: 5px;">工作流基础名称:</label><input id="publisher-workflow-name" type="text" placeholder="例如: SDXL高级出图" style="width: 100%; box-sizing: border-box; background: var(--comfy-input-bg); color: var(--fg-color); border: 1px solid var(--border-color); padding: 5px; border-radius: 4px;"><p id="publisher-status" style="margin-top: 10px; color: var(--success-color); min-height: 1.2em;"></p>`
);
publishDialog.addButtons([
{ label: "取消", callback: () => publishDialog.hide() },
{ label: "确认发布", callback: publishWorkflow },
]);
publishDialog.show();
} catch (error) {
console.error("加载设置失败:", error);
alert(`加载设置失败: ${error.message}`);
}
}
},
});