Files
ComfyUI-WorkflowPublisher/js/publisher.js

750 lines
28 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 COMFY_SERVER_REGISTRATION_KEY = "comfyui_server_registration";
// ComfyUI服务器注册配置
const DEFAULT_SERVER_CONFIG = {
name: "comfyui_node_1",
http_url: "http://127.0.0.1:8188",
workflow_service_host: "http://localhost:8000",
};
// 获取ComfyUI服务器注册配置
function getComfyServerConfig() {
try {
const stored = localStorage.getItem(COMFY_SERVER_REGISTRATION_KEY);
return stored ? JSON.parse(stored) : { ...DEFAULT_SERVER_CONFIG };
} catch (error) {
console.error("读取ComfyUI服务器配置失败:", error);
return { ...DEFAULT_SERVER_CONFIG };
}
}
// 保存ComfyUI服务器注册配置
function saveComfyServerConfig(config) {
try {
localStorage.setItem(COMFY_SERVER_REGISTRATION_KEY, JSON.stringify(config));
} catch (error) {
console.error("保存ComfyUI服务器配置失败:", error);
}
}
// 获取本地设置
function getLocalSettings() {
try {
const stored = localStorage.getItem("workflow_publisher_settings");
return stored ? JSON.parse(stored) : { host: "" };
} catch (error) {
console.error("读取本地设置失败:", error);
return { host: "" };
}
}
// 保存本地设置
function saveLocalSettings(settings) {
try {
localStorage.setItem(
"workflow_publisher_settings",
JSON.stringify(settings)
);
} catch (error) {
console.error("保存本地设置失败:", error);
}
}
// ComfyUI服务器注册管理器
class ComfyServerManager {
constructor() {
this.registrationInterval = null;
this.heartbeatInterval = null;
this.isRegistered = false;
this.serverConfig = getComfyServerConfig();
}
// 注册服务器到工作流服务
async registerServer() {
if (!this.serverConfig.workflow_service_host) {
console.warn("未设置工作流服务地址,无法注册");
return false;
}
try {
const response = await fetch(
`${this.serverConfig.workflow_service_host}/api/comfy/register`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.serverConfig.name,
http_url: this.serverConfig.http_url,
ws_url: this.getWebSocketUrl(),
}),
}
);
if (response.ok) {
const result = await response.json();
console.log("ComfyUI服务器注册成功:", result);
this.isRegistered = true;
this.startHeartbeat();
return true;
} else {
const errorText = await response.text();
console.error("ComfyUI服务器注册失败:", response.status, errorText);
return false;
}
} catch (error) {
console.error("注册ComfyUI服务器时发生错误:", error);
return false;
}
}
// 注销服务器
async unregisterServer() {
if (!this.serverConfig.workflow_service_host || !this.isRegistered) {
return false;
}
try {
const response = await fetch(
`${this.serverConfig.workflow_service_host}/api/comfy/unregister`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: this.serverConfig.name }),
}
);
if (response.ok) {
console.log("ComfyUI服务器注销成功");
this.isRegistered = false;
this.stopHeartbeat();
return true;
} else {
console.error("ComfyUI服务器注销失败:", response.status);
return false;
}
} catch (error) {
console.error("注销ComfyUI服务器时发生错误:", error);
return false;
}
}
// 发送心跳
async sendHeartbeat() {
if (!this.serverConfig.workflow_service_host || !this.isRegistered) {
return false;
}
try {
const response = await fetch(
`${this.serverConfig.workflow_service_host}/api/comfy/heartbeat`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: this.serverConfig.name }),
}
);
if (response.ok) {
console.debug("心跳发送成功");
return true;
} else {
console.warn("心跳发送失败:", response.status);
// 如果心跳失败,尝试重新注册
if (response.status === 404) {
console.log("服务器不存在,尝试重新注册...");
this.isRegistered = false;
await this.registerServer();
}
return false;
}
} catch (error) {
console.error("发送心跳时发生错误:", error);
// 如果网络错误,尝试重新注册
this.isRegistered = false;
await this.registerServer();
return false;
}
}
// 启动心跳
startHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
// 每30秒发送一次心跳
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, 30000);
console.log("ComfyUI服务器心跳已启动");
}
// 停止心跳
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
console.log("ComfyUI服务器心跳已停止");
}
// 更新服务器配置
updateConfig(newConfig) {
this.serverConfig = { ...this.serverConfig, ...newConfig };
saveComfyServerConfig(this.serverConfig);
// 如果已注册,需要重新注册以更新配置
if (this.isRegistered) {
this.unregisterServer().then(() => {
setTimeout(() => this.registerServer(), 1000);
});
}
}
// 设置工作流服务地址
setWorkflowServiceHost(host) {
this.serverConfig.workflow_service_host = host;
saveComfyServerConfig(this.serverConfig);
}
// 获取WebSocket URL自动从HTTP URL生成
getWebSocketUrl() {
if (!this.serverConfig.http_url) return "";
const url = new URL(this.serverConfig.http_url);
// 根据协议自动选择ws或wss
const protocol = url.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${url.host}/ws`;
}
// 获取服务器状态
async getServerStatus() {
if (!this.serverConfig.workflow_service_host) {
return null;
}
try {
const response = await fetch(
`${this.serverConfig.workflow_service_host}/api/comfy/status/${this.serverConfig.name}`
);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error("获取服务器状态失败:", error);
}
return null;
}
// 获取系统健康状态
async getSystemHealth() {
if (!this.serverConfig.workflow_service_host) {
return null;
}
try {
const response = await fetch(
`${this.serverConfig.workflow_service_host}/api/comfy/health`
);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error("获取系统健康状态失败:", error);
}
return null;
}
}
// 自定义 fetchApi 函数使用ComfyUI服务器配置中的工作流服务地址
async function customFetchApi(endpoint, options = {}) {
try {
// 从ComfyUI服务器配置获取工作流服务地址
const serverConfig = getComfyServerConfig();
// 如果没有配置工作流服务地址,回退到默认的 api.fetchApi
if (!serverConfig.workflow_service_host) {
console.warn("未配置工作流服务地址,使用默认的 ComfyUI API");
return await api.fetchApi(endpoint, options);
}
// 构建完整的 URL
const host = serverConfig.workflow_service_host.replace(/\/$/, ""); // 移除末尾的斜杠
const fullUrl = `${host}${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 = `<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 btnComfyServer = createButton(`🖥️`, "ComfyUI服务器管理", {
onClick: () => showComfyServerDialog(),
});
const separator = document.createElement("div");
separator.style.cssText =
"border-left: 1px solid var(--border-color); height: 22px; margin: 0 4px;";
container.append(btnGet, btnPublish, btnComfyServer, separator);
menu.insertBefore(container, menu.firstChild);
});
const publishDialog = new Modal(
"workflow-publisher-publish-dialog",
"发布工作流"
);
const workflowListDialog = new Modal(
"workflow-publisher-list-dialog",
"工作流库",
true
);
const comfyServerDialog = new Modal(
"comfy-server-dialog",
"ComfyUI服务器管理",
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 showPublishDialog() {
try {
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}`);
}
}
// ComfyUI 服务器管理对话框
async function showComfyServerDialog() {
try {
const serverConfig = getComfyServerConfig();
const settings = getLocalSettings();
// 创建服务器配置表单
const configForm = `
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="server-name" style="display: block; margin-bottom: 5px;">当前服务节点名称:</label>
<input id="server-name" type="text" value="${serverConfig.name}" placeholder="例如: comfyui_node_1" style="width: 100%; box-sizing: border-box; background: var(--comfy-input-bg); color: var(--fg-color); border: 1px solid var(--border-color); padding: 8px; border-radius: 4px;">
</div>
<div>
<label for="server-http-url" style="display: block; margin-bottom: 5px;">当前服务节点地址:</label>
<input id="server-http-url" type="text" value="${serverConfig.http_url}" placeholder="例如: http://127.0.0.1:8188" style="width: 100%; box-sizing: border-box; background: var(--comfy-input-bg); color: var(--fg-color); border: 1px solid var(--border-color); padding: 8px; border-radius: 4px;">
<small style="color: var(--border-color); font-size: 0.9em;">WebSocket地址将自动生成为: <span id="ws-url-preview">${serverConfig.http_url ? serverConfig.http_url.replace(/^https?:\/\//, 'ws://') + '/ws' : ''}</span></small>
</div>
<div>
<label for="workflow-service-host" style="display: block; margin-bottom: 5px;">工作流服务地址 (统一管理地址):</label>
<input id="workflow-service-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: 8px; border-radius: 4px;">
</div>
<div id="server-status" style="padding: 10px; border-radius: 4px; background: var(--comfy-input-bg); border: 1px solid var(--border-color);">
<strong>服务器状态:</strong> <span id="status-text">未连接</span>
</div>
<div id="server-actions" style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="btn-register" class="jags-button" style="flex: 1; min-width: 120px;">注册服务器</button>
<button id="btn-unregister" class="jags-button" style="flex: 1; min-width: 120px;">注销服务器</button>
<button id="btn-refresh-status" class="jags-button" style="flex: 1; min-width: 120px;">刷新状态</button>
</div>
</div>
`;
comfyServerDialog.setContent(configForm);
comfyServerDialog.addButtons([
{ label: "关闭", callback: () => comfyServerDialog.hide() },
]);
comfyServerDialog.show();
// 初始化 ComfyServerManager
const serverManager = new ComfyServerManager();
if (settings.host) {
serverManager.setWorkflowServiceHost(settings.host);
}
// 绑定事件
const btnRegister = document.getElementById("btn-register");
const btnUnregister = document.getElementById("btn-unregister");
const btnRefreshStatus = document.getElementById("btn-refresh-status");
const statusText = document.getElementById("status-text");
// 添加HTTP地址输入框的实时预览功能
const httpUrlInput = document.getElementById("server-http-url");
const wsUrlPreview = document.getElementById("ws-url-preview");
httpUrlInput.addEventListener("input", function() {
const httpUrl = this.value;
if (httpUrl) {
try {
const url = new URL(httpUrl);
// 根据协议自动选择ws或wss
const protocol = url.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${url.host}/ws`;
wsUrlPreview.textContent = wsUrl;
} catch (error) {
wsUrlPreview.textContent = "无效的URL格式";
}
} else {
wsUrlPreview.textContent = "";
}
});
// 注册服务器
btnRegister.onclick = async () => {
const newConfig = {
name: document.getElementById("server-name").value,
http_url: document.getElementById("server-http-url").value,
};
const workflowHost = document.getElementById(
"workflow-service-host"
).value;
if (workflowHost) {
serverManager.setWorkflowServiceHost(workflowHost);
// 保存到本地设置
saveLocalSettings({ host: workflowHost });
}
serverManager.updateConfig(newConfig);
btnRegister.disabled = true;
btnRegister.textContent = "注册中...";
const success = await serverManager.registerServer();
if (success) {
statusText.textContent = "已注册";
statusText.style.color = "var(--success-color)";
btnRegister.textContent = "已注册";
btnRegister.disabled = true;
btnUnregister.disabled = false;
} else {
statusText.textContent = "注册失败";
statusText.style.color = "var(--error-color)";
btnRegister.textContent = "注册服务器";
btnRegister.disabled = false;
}
};
// 注销服务器
btnUnregister.onclick = async () => {
btnUnregister.disabled = true;
btnUnregister.textContent = "注销中...";
const success = await serverManager.unregisterServer();
if (success) {
statusText.textContent = "已注销";
statusText.style.color = "orange";
btnRegister.disabled = false;
btnRegister.textContent = "注册服务器";
btnUnregister.disabled = true;
btnUnregister.textContent = "注销服务器";
} else {
statusText.textContent = "注销失败";
statusText.style.color = "var(--error-color)";
btnUnregister.disabled = false;
btnUnregister.textContent = "注销服务器";
}
};
// 刷新状态
btnRefreshStatus.onclick = async () => {
if (settings.host) {
serverManager.setWorkflowServiceHost(settings.host);
const status = await serverManager.getServerStatus();
if (status) {
statusText.textContent = `在线 - 最后心跳: ${new Date(
status.last_heartbeat
).toLocaleString()}`;
statusText.style.color = "var(--success-color)";
btnRegister.disabled = true;
btnRegister.textContent = "已注册";
btnUnregister.disabled = false;
} else {
statusText.textContent = "未注册";
statusText.style.color = "orange";
btnRegister.disabled = false;
btnRegister.textContent = "注册服务器";
btnUnregister.disabled = true;
}
}
};
// 初始状态检查
if (settings.host) {
btnRefreshStatus.click();
}
} catch (error) {
console.error("显示ComfyUI服务器管理对话框失败:", error);
alert(`显示对话框失败: ${error.message}`);
}
}
},
});