From 039e3fc0bb38d30fc069bf62fe7ec27affb963c9 Mon Sep 17 00:00:00 2001 From: iHeyTang Date: Mon, 11 Aug 2025 10:25:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=94=AF=E6=8C=81=E4=B8=BB=E6=9C=BA=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=EF=BC=8C=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=92=8C=E4=BF=9D=E5=AD=98=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 18 +- __init__.py | 61 +++++- js/publisher.js | 519 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 483 insertions(+), 115 deletions(-) diff --git a/.env b/.env index 801663a..50e540a 100644 --- a/.env +++ b/.env @@ -2,30 +2,30 @@ COMFYUI_SERVERS_JSON='[ { "http_url": "https://bowongai-dev--comfyui-for-waas-ui-1.modal.run", "ws_url": "wss://bowongai-dev--comfyui-for-waas-ui-1.modal.run/ws", - "input_dir": "/waas/comfyui_1/input", - "output_dir": "/waas/comfyui_1/output" + "input_dir": "waas/comfyui_1/input", + "output_dir": "waas/comfyui_1/output" }, { "http_url": "https://bowongai-dev--comfyui-for-waas-ui-2.modal.run", "ws_url": "wss://bowongai-dev--comfyui-for-waas-ui-2.modal.run/ws", - "input_dir": "/waas/comfyui_2/input", - "output_dir": "/waas/comfyui_2/output" + "input_dir": "waas/comfyui_2/input", + "output_dir": "waas/comfyui_2/output" }, { "http_url": "https://bowongai-dev--comfyui-for-waas-ui-3.modal.run", "ws_url": "wss://bowongai-dev--comfyui-for-waas-ui-3.modal.run/ws", - "input_dir": "/waas/comfyui_3/input", - "output_dir": "/waas/comfyui_3/output" + "input_dir": "waas/comfyui_3/input", + "output_dir": "waas/comfyui_3/output" }, { "http_url": "https://bowongai-dev--comfyui-for-waas-ui-4.modal.run", "ws_url": "wss://bowongai-dev--comfyui-for-waas-ui-4.modal.run/ws", - "input_dir": "/waas/comfyui_4/input", - "output_dir": "/waas/comfyui_4/output" + "input_dir": "waas/comfyui_4/input", + "output_dir": "waas/comfyui_4/output" } ]' -DB_FILE="/db/workflows_service.sqlite" +DB_FILE="workflows_service.sqlite" # AWS S3 配置 S3_BUCKET_NAME="modal-media-cache" diff --git a/__init__.py b/__init__.py index 54212e4..a0e16fd 100644 --- a/__init__.py +++ b/__init__.py @@ -13,24 +13,36 @@ CONFIG_FILE = os.path.join(NODE_DIR, "config.json") # 默认配置 DEFAULT_CONFIG = { - "api_url": "" + "api_url": "", + "host": "" } def load_config(): """加载配置文件,如果文件不存在则创建""" if not os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, 'w') as f: - json.dump(DEFAULT_CONFIG, f) + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False) + return DEFAULT_CONFIG + try: + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + # 确保所有必需的字段都存在 + for key in DEFAULT_CONFIG: + if key not in config: + config[key] = DEFAULT_CONFIG[key] + return config + except (json.JSONDecodeError, FileNotFoundError): + # 如果配置文件损坏或不存在,重新创建 + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False) return DEFAULT_CONFIG - with open(CONFIG_FILE, 'r') as f: - return json.load(f) def save_config(config_data): """保存配置到文件""" - with open(CONFIG_FILE, 'w') as f: - json.dump(config_data, f, indent=4) + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=4, ensure_ascii=False) # 创建一个虚拟节点类,虽然它不会出现在图表中,但这是ComfyUI加载自定义节点的标准方式 @@ -72,8 +84,10 @@ async def save_publisher_settings(request): try: data = await request.json() api_url = data.get("api_url", "") + host = data.get("host", "") config = load_config() config["api_url"] = api_url + config["host"] = host save_config(config) return web.json_response({"status": "success", "message": "Settings saved"}) except Exception as e: @@ -93,10 +107,19 @@ async def publish_workflow_handler(request): config = load_config() target_api_url = config.get("api_url") + host = config.get("host", "") if not target_api_url: return web.json_response({"status": "error", "message": "API URL not configured"}, status=400) + # 构建完整的URL + if host and not target_api_url.startswith(('http://', 'https://')): + # 如果提供了host且api_url不是完整URL,则组合它们 + full_url = host.rstrip('/') + '/' + target_api_url.lstrip('/') + else: + # 否则直接使用api_url + full_url = target_api_url + # 准备要发送到目标API的数据 payload = { "name": workflow_name, @@ -107,7 +130,7 @@ async def publish_workflow_handler(request): # 使用 aiohttp 发送异步请求 async with aiohttp.ClientSession() as session: - async with session.post(target_api_url, json=payload, headers=headers) as response: + async with session.post(full_url, json=payload, headers=headers) as response: response_text = await response.text() if response.status == 200 or response.status == 201: return web.json_response( @@ -131,12 +154,21 @@ async def get_workflows_from_server(request): try: config = load_config() target_api_url = config.get("api_url") + host = config.get("host", "") if not target_api_url: return web.json_response({"status": "error", "message": "API URL not configured"}, status=400) + # 构建完整的URL + if host and not target_api_url.startswith(('http://', 'https://')): + # 如果提供了host且api_url不是完整URL,则组合它们 + full_url = host.rstrip('/') + '/' + target_api_url.lstrip('/') + else: + # 否则直接使用api_url + full_url = target_api_url + async with aiohttp.ClientSession() as session: - async with session.get(target_api_url) as response: + async with session.get(full_url) as response: if response.status != 200: response_text = await response.text() return web.json_response({ @@ -193,12 +225,21 @@ async def delete_workflow_version(request): config = load_config() target_api_url = config.get("api_url") + host = config.get("host", "") if not target_api_url: return web.json_response({"status": "error", "message": "API URL not configured"}, status=400) + # 构建完整的URL + if host and not target_api_url.startswith(('http://', 'https://')): + # 如果提供了host且api_url不是完整URL,则组合它们 + base_url = host.rstrip('/') + '/' + target_api_url.lstrip('/') + else: + # 否则直接使用api_url + base_url = target_api_url + # 构建目标URL,需要对工作流名称进行URL编码 - delete_url = f"{target_api_url}/{urllib.parse.quote(workflow_name)}" + delete_url = f"{base_url}/{urllib.parse.quote(workflow_name)}" async with aiohttp.ClientSession() as session: async with session.delete(delete_url) as response: diff --git a/js/publisher.js b/js/publisher.js index 5b5a761..abfbc10 100644 --- a/js/publisher.js +++ b/js/publisher.js @@ -3,27 +3,46 @@ import { api } from "../../scripts/api.js"; // 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); }); } + 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); + }); + } } - 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 = ` + 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; } @@ -36,88 +55,396 @@ app.registerExtension({ .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(); + 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); - }; + 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 createButton = (innerHTML, title, onClick) => { const btn = document.createElement("button"); btn.className = "jags-button"; btn.innerHTML = innerHTML; btn.title = title; btn.onclick = onClick; return btn; }; - const iconGet = ``; - const iconPublish = ``; - const btnGet = createButton(iconGet, "从服务器获取工作流", () => showWorkflowListDialog()); - const btnPublish = createButton(iconPublish, "发布当前工作流", () => showPublishDialog()); - const btnSettings = createButton(`⚙️`, "发布设置", () => 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); + 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 createButton = (innerHTML, title, onClick) => { + const btn = document.createElement("button"); + btn.className = "jags-button"; + btn.innerHTML = innerHTML; + btn.title = title; + btn.onclick = onClick; + return btn; + }; + const iconGet = ``; + const iconPublish = ``; + const btnGet = createButton(iconGet, "从服务器获取工作流", () => + showWorkflowListDialog() + ); + const btnPublish = createButton(iconPublish, "发布当前工作流", () => + showPublishDialog() + ); + const btnSettings = createButton(`⚙️`, "发布设置", () => + 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 deleteWorkflowVersion( + baseName, + version, + fullVersionedName, + itemElement + ) { + if ( + !confirm( + `确定要永久删除工作流 "${baseName}" 的版本 [${version}] 吗?\n此操作无法撤销!` + ) + ) { + return; + } + try { + const response = await api.fetchApi("/publisher/workflow/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: fullVersionedName }), }); + if (!response.ok) { + const errorText = await response.text(); + let err; + try { + err = errorText ? JSON.parse(errorText) : {}; + } catch (parseError) { + console.error("JSON解析失败:", parseError, "原始响应:", errorText); + err = { message: "服务器删除失败" }; + } + throw new Error(err.message || "服务器删除失败"); + } + const panel = itemElement.parentElement; + itemElement.remove(); + const remainingItems = panel.querySelectorAll(".jags-version-item"); + const header = panel.previousElementSibling; + if (header.matches(".jags-accordion-header")) { + const countSpan = header.querySelector("span:last-child"); + if (remainingItems.length > 0) { + countSpan.textContent = `${remainingItems.length} 个版本`; + } else { + header.remove(); + panel.remove(); + } + } + } catch (error) { + alert(`删除失败: ${error.message}`); + console.error("Delete failed:", error); + } + } - 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 deleteWorkflowVersion(baseName, version, fullVersionedName, itemElement) { if (!confirm(`确定要永久删除工作流 "${baseName}" 的版本 [${version}] 吗?\n此操作无法撤销!`)) { return; } try { const response = await api.fetchApi("/publisher/workflow/delete", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: fullVersionedName }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.message || "服务器删除失败"); } const panel = itemElement.parentElement; itemElement.remove(); const remainingItems = panel.querySelectorAll('.jags-version-item'); const header = panel.previousElementSibling; if (header.matches('.jags-accordion-header')) { const countSpan = header.querySelector('span:last-child'); if(remainingItems.length > 0) { countSpan.textContent = `${remainingItems.length} 个版本`; } else { header.remove(); panel.remove(); } } } catch (error) { alert(`删除失败: ${error.message}`); console.error("Delete failed:", error); } } - - async function showWorkflowListDialog() { - workflowListDialog.setContent('

正在从服务器加载工作流...

'); - workflowListDialog.addButtons([{ label: '关闭', callback: () => workflowListDialog.hide() }]); - workflowListDialog.show(); - try { - const response = await api.fetchApi("/publisher/workflows"); - if (!response.ok) throw new Error(`服务器错误: ${response.statusText}`); - const workflows = await response.json(); - const workflowKeys = Object.keys(workflows); - if (workflowKeys.length === 0) { workflowListDialog.setContent('

服务器上没有找到任何工作流。

'); return; } - const accordionContainer = document.createElement('div'); accordionContainer.style.cssText = 'display: flex; flex-direction: column; gap: 8px;'; - workflowKeys.sort().forEach(baseName => { - const versions = workflows[baseName]; - const header = document.createElement('button'); header.className = 'jags-accordion-header'; - header.innerHTML = `${baseName}${versions.length} 个版本`; - const panel = document.createElement('div'); panel.className = 'jags-accordion-panel'; - versions.forEach(versionData => { - const fullVersionedName = versionData.version === "N/A" ? baseName : `${baseName} [${versionData.version}]`; - const item = document.createElement('div'); item.className = 'jags-version-item'; - const label = document.createElement('span'); label.className = 'jags-version-item-label'; - label.textContent = `版本: ${versionData.version}`; - - // --- [核心升级] 使用 app.handleFile 加载工作流 --- - label.onclick = () => { - if (confirm(`确定要加载工作流 "${baseName}" 的版本 [${versionData.version}] 吗?\n这将覆盖当前画布。`)) { - // 1. 将工作流JSON对象转换为字符串 - const jsonString = JSON.stringify(versionData.workflow, null, 2); // 格式化JSON - - // 2. 创建一个虚拟的File对象 - const file = new File([jsonString], fullVersionedName, { - type: "application/json", - }); - - // 3. 调用ComfyUI的官方文件处理函数 - app.handleFile(file); - - // 4. 关闭对话框 - workflowListDialog.hide(); - } - }; - - const deleteBtn = document.createElement('button'); deleteBtn.className = 'jags-delete-btn'; deleteBtn.innerHTML = '✖'; deleteBtn.title = `删除此版本`; - deleteBtn.onclick = (e) => { e.stopPropagation(); deleteWorkflowVersion(baseName, versionData.version, fullVersionedName, item); }; - item.appendChild(label); item.appendChild(deleteBtn); panel.appendChild(item); - }); - header.onclick = () => { header.classList.toggle('active'); if (panel.style.maxHeight) { panel.style.maxHeight = null; panel.style.marginTop = '0px'; } else { panel.style.marginTop = '5px'; panel.style.maxHeight = panel.scrollHeight + "px"; }}; - accordionContainer.appendChild(header); accordionContainer.appendChild(panel); - }); - workflowListDialog.setContent(''); workflowListDialog.element.querySelector('.content').appendChild(accordionContainer); - } catch (error) { workflowListDialog.setContent(`

加载失败: ${error.message}

`); } + async function showWorkflowListDialog() { + workflowListDialog.setContent( + '

正在从服务器加载工作流...

' + ); + workflowListDialog.addButtons([ + { label: "关闭", callback: () => workflowListDialog.hide() }, + ]); + workflowListDialog.show(); + try { + const response = await api.fetchApi("/publisher/workflows"); + if (!response.ok) { + throw new Error( + `服务器错误: ${response.status} ${response.statusText}` + ); + } + const text = await response.text(); + let workflows; + try { + workflows = text ? JSON.parse(text) : {}; + } catch (parseError) { + console.error("JSON解析失败:", parseError, "原始响应:", text); + throw new Error("服务器返回的数据格式无效"); } - // ... (其余函数保持不变) ... - async function publishWorkflow() { const baseName = document.getElementById('publisher-workflow-name').value.trim(); if (!baseName) { alert("请输入工作流的基础名称。"); return; } const statusEl = document.getElementById('publisher-status'); statusEl.textContent = "正在准备发布..."; statusEl.style.color = "orange"; const d = new Date(); const versionString = `${d.getFullYear()}${(d.getMonth()+1).toString().padStart(2,'0')}${d.getDate().toString().padStart(2,'0')}${d.getHours().toString().padStart(2,'0')}${d.getMinutes().toString().padStart(2,'0')}${d.getSeconds().toString().padStart(2,'0')}`; const finalName = `${baseName} [${versionString}]`; try { const workflow = await app.graph.serialize(); const body = { name: finalName, workflow: workflow }; statusEl.textContent = `正在发布为 "${finalName}"...`; const response = await api.fetchApi("/publisher/publish", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) throw new Error(`服务器返回错误: ${response.status}`); const result = await response.json(); 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 resp = await api.fetchApi("/publisher/settings", { cache: "no-store" }); const settings = await resp.json(); settingsDialog.setContent(``); settingsDialog.addButtons([{ label: '取消', callback: () => settingsDialog.hide() },{ label: '保存', callback: saveSettings }]); settingsDialog.show(); } catch (error) { alert(`加载设置失败: ${error}`); } } - async function saveSettings() { const apiUrl = document.getElementById('publisher-api-url').value; try { await api.fetchApi("/publisher/settings", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_url: apiUrl }) }); alert("设置已保存!"); settingsDialog.hide(); } catch (error) { alert(`保存设置失败: ${error}`); } } - async function showPublishDialog() { const settingsResp = await api.fetchApi("/publisher/settings", { cache: "no-store" }); const settings = await settingsResp.json(); if (!settings.api_url) { alert("请先点击 ⚙️ 按钮设置发布API的URL。"); return; } publishDialog.setContent(`

`); publishDialog.addButtons([{ label: '取消', callback: () => publishDialog.hide() },{ label: '确认发布', callback: publishWorkflow }]); publishDialog.show(); } + const workflowKeys = Object.keys(workflows); + if (workflowKeys.length === 0) { + workflowListDialog.setContent( + '

服务器上没有找到任何工作流。

' + ); + return; + } + const accordionContainer = document.createElement("div"); + accordionContainer.style.cssText = + "display: flex; flex-direction: column; gap: 8px;"; + workflowKeys.sort().forEach((baseName) => { + const versions = workflows[baseName]; + const header = document.createElement("button"); + header.className = "jags-accordion-header"; + header.innerHTML = `${baseName}${versions.length} 个版本`; + const panel = document.createElement("div"); + panel.className = "jags-accordion-panel"; + versions.forEach((versionData) => { + const fullVersionedName = + versionData.version === "N/A" + ? baseName + : `${baseName} [${versionData.version}]`; + const item = document.createElement("div"); + item.className = "jags-version-item"; + const label = document.createElement("span"); + label.className = "jags-version-item-label"; + label.textContent = `版本: ${versionData.version}`; + + // --- [核心升级] 使用 app.handleFile 加载工作流 --- + label.onclick = () => { + if ( + confirm( + `确定要加载工作流 "${baseName}" 的版本 [${versionData.version}] 吗?\n这将覆盖当前画布。` + ) + ) { + // 1. 将工作流JSON对象转换为字符串 + const jsonString = JSON.stringify( + versionData.workflow, + null, + 2 + ); // 格式化JSON + + // 2. 创建一个虚拟的File对象 + const file = new File([jsonString], fullVersionedName, { + type: "application/json", + }); + + // 3. 调用ComfyUI的官方文件处理函数 + app.handleFile(file); + + // 4. 关闭对话框 + workflowListDialog.hide(); + } + }; + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "jags-delete-btn"; + deleteBtn.innerHTML = "✖"; + deleteBtn.title = `删除此版本`; + deleteBtn.onclick = (e) => { + e.stopPropagation(); + deleteWorkflowVersion( + baseName, + versionData.version, + fullVersionedName, + item + ); + }; + item.appendChild(label); + item.appendChild(deleteBtn); + panel.appendChild(item); + }); + header.onclick = () => { + header.classList.toggle("active"); + if (panel.style.maxHeight) { + panel.style.maxHeight = null; + panel.style.marginTop = "0px"; + } else { + panel.style.marginTop = "5px"; + panel.style.maxHeight = panel.scrollHeight + "px"; + } + }; + accordionContainer.appendChild(header); + accordionContainer.appendChild(panel); + }); + workflowListDialog.setContent(""); + workflowListDialog.element + .querySelector(".content") + .appendChild(accordionContainer); + } catch (error) { + console.error("加载工作流失败:", error); + workflowListDialog.setContent( + `

加载失败: ${error.message}

` + ); + } } -}); \ No newline at end of file + + // ... (其余函数保持不变) ... + async function publishWorkflow() { + const baseName = document + .getElementById("publisher-workflow-name") + .value.trim(); + if (!baseName) { + alert("请输入工作流的基础名称。"); + return; + } + const statusEl = document.getElementById("publisher-status"); + statusEl.textContent = "正在准备发布..."; + statusEl.style.color = "orange"; + const d = new Date(); + const versionString = `${d.getFullYear()}${(d.getMonth() + 1) + .toString() + .padStart(2, "0")}${d.getDate().toString().padStart(2, "0")}${d + .getHours() + .toString() + .padStart(2, "0")}${d.getMinutes().toString().padStart(2, "0")}${d + .getSeconds() + .toString() + .padStart(2, "0")}`; + const finalName = `${baseName} [${versionString}]`; + try { + const workflow = await app.graph.serialize(); + const body = { name: finalName, workflow: workflow }; + statusEl.textContent = `正在发布为 "${finalName}"...`; + const response = await api.fetchApi("/publisher/publish", { + 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 resp = await api.fetchApi("/publisher/settings", { + cache: "no-store", + }); + if (!resp.ok) { + throw new Error(`服务器错误: ${resp.status} ${resp.statusText}`); + } + const text = await resp.text(); + let settings; + try { + settings = text ? JSON.parse(text) : {}; + } catch (parseError) { + console.error("JSON解析失败:", parseError, "原始响应:", text); + settings = { api_url: "", host: "" }; + } + + // 确保设置对象有必需的字段 + settings = { + api_url: settings.api_url || "", + host: settings.host || "", + ...settings, + }; + + settingsDialog.setContent(` + + + + + + `); + settingsDialog.addButtons([ + { label: "取消", callback: () => settingsDialog.hide() }, + { label: "保存", callback: saveSettings }, + ]); + settingsDialog.show(); + } catch (error) { + console.error("加载设置失败:", error); + alert(`加载设置失败: ${error.message}`); + } + } + async function saveSettings() { + const apiUrl = document.getElementById("publisher-api-url").value; + const host = document.getElementById("publisher-host").value; + try { + await api.fetchApi("/publisher/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ api_url: apiUrl, host: host }), + }); + alert("设置已保存!"); + settingsDialog.hide(); + } catch (error) { + alert(`保存设置失败: ${error}`); + } + } + async function showPublishDialog() { + try { + const settingsResp = await api.fetchApi("/publisher/settings", { + cache: "no-store", + }); + if (!settingsResp.ok) { + throw new Error( + `服务器错误: ${settingsResp.status} ${settingsResp.statusText}` + ); + } + const text = await settingsResp.text(); + let settings; + try { + settings = text ? JSON.parse(text) : {}; + } catch (parseError) { + console.error("JSON解析失败:", parseError, "原始响应:", text); + settings = { api_url: "", host: "" }; + } + + // 确保设置对象有必需的字段 + settings = { + api_url: settings.api_url || "", + host: settings.host || "", + ...settings, + }; + + if (!settings.api_url) { + alert("请先点击 ⚙️ 按钮设置发布API的URL。"); + return; + } + publishDialog.setContent( + `

` + ); + publishDialog.addButtons([ + { label: "取消", callback: () => publishDialog.hide() }, + { label: "确认发布", callback: publishWorkflow }, + ]); + publishDialog.show(); + } catch (error) { + console.error("加载设置失败:", error); + alert(`加载设置失败: ${error.message}`); + } + } + }, +});