import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// 本地存储的 key
const WORKFLOW_SERVICE_HOST_KEY = "workflow_publisher_service_host";
// 默认工作流服务地址
const DEFAULT_WORKFLOW_SERVICE_HOST = "http://localhost:8000";
// 获取工作流服务地址
function getWorkflowServiceHost() {
try {
const stored = localStorage.getItem(WORKFLOW_SERVICE_HOST_KEY);
return stored || DEFAULT_WORKFLOW_SERVICE_HOST;
} catch (error) {
console.error("读取工作流服务地址失败:", error);
return DEFAULT_WORKFLOW_SERVICE_HOST;
}
}
// 保存工作流服务地址
function saveWorkflowServiceHost(host) {
try {
localStorage.setItem(WORKFLOW_SERVICE_HOST_KEY, host);
} catch (error) {
console.error("保存工作流服务地址失败:", error);
}
}
// 自定义 fetchApi 函数,使用配置的工作流服务地址
async function customFetchApi(endpoint, options = {}) {
try {
const host = getWorkflowServiceHost();
if (!host) {
console.warn("未配置工作流服务地址,使用默认的 ComfyUI API");
return await api.fetchApi(endpoint, options);
}
// 构建完整的 URL
const fullHost = host.replace(/\/$/, ""); // 移除末尾的斜杠
const fullUrl = `${fullHost}${endpoint}`;
console.log(`使用工作流服务地址请求: ${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 = `
${title}
`;
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 = ``;
const iconPublish = ``;
const iconSettings = ``;
const btnGet = createButton(iconGet, "从服务器获取工作流", {
onClick: () => showWorkflowListDialog(),
});
const btnPublish = createButton(iconPublish, "发布当前工作流", {
onClick: () => showPublishDialog(),
});
const btnSettings = createButton(iconSettings, "设置工作流服务地址", {
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 publishDialog = new Modal(
"workflow-publisher-publish-dialog",
"发布工作流"
);
const workflowListDialog = new Modal(
"workflow-publisher-list-dialog",
"工作流库",
true
);
const settingsDialog = new Modal(
"workflow-publisher-settings-dialog",
"设置工作流服务地址"
);
async function showWorkflowListDialog() {
workflowListDialog.setContent(
'正在从服务器加载工作流...
'
);
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(
'服务器上没有找到任何工作流。
'
);
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(
`加载失败: ${error.message}
`
);
}
}
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 showPublishDialog() {
try {
publishDialog.setContent(
``
);
publishDialog.addButtons([
{ label: "取消", callback: () => publishDialog.hide() },
{ label: "确认发布", callback: publishWorkflow },
]);
publishDialog.show();
} catch (error) {
console.error("显示发布对话框失败:", error);
alert(`显示对话框失败: ${error.message}`);
}
}
// 设置对话框
async function showSettingsDialog() {
try {
const currentHost = getWorkflowServiceHost();
const settingsForm = `
这是用于获取和发布工作流的服务地址
连接状态: 未测试
`;
settingsDialog.setContent(settingsForm);
settingsDialog.addButtons([
{ label: "关闭", callback: () => settingsDialog.hide() },
]);
settingsDialog.show();
// 绑定事件
const btnTestConnection = document.getElementById(
"btn-test-connection"
);
const btnSaveSettings = document.getElementById("btn-save-settings");
const statusText = document.getElementById("status-text");
const hostInput = document.getElementById("workflow-service-host");
// 测试连接
btnTestConnection.onclick = async () => {
btnTestConnection.disabled = true;
btnTestConnection.textContent = "测试中...";
statusText.textContent = "正在测试连接...";
statusText.style.color = "orange";
try {
const testHost = hostInput.value.trim();
if (!testHost) {
throw new Error("请输入工作流服务地址");
}
const response = await fetch(`${testHost}/api/workflow`);
if (response.ok) {
statusText.textContent = "连接成功";
statusText.style.color = "var(--success-color)";
} else {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
} catch (error) {
statusText.textContent = `连接失败: ${error.message}`;
statusText.style.color = "var(--error-color)";
} finally {
btnTestConnection.disabled = false;
btnTestConnection.textContent = "测试连接";
}
};
// 保存设置
btnSaveSettings.onclick = () => {
const newHost = hostInput.value.trim();
if (!newHost) {
alert("请输入工作流服务地址");
return;
}
saveWorkflowServiceHost(newHost);
statusText.textContent = "设置已保存";
statusText.style.color = "var(--success-color)";
setTimeout(() => {
settingsDialog.hide();
}, 1000);
};
} catch (error) {
console.error("显示设置对话框失败:", error);
alert(`显示对话框失败: ${error.message}`);
}
}
},
});