import { app } from "../../scripts/app.js"; 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); }); } } 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 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 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 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(); } } });