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}`); } } }, });