feat: 添加CORS支持,优化API响应,重构工作流管理逻辑,支持本地存储设置

This commit is contained in:
iHeyTang
2025-08-11 15:24:42 +08:00
parent e3dfffad7d
commit 745319962c
5 changed files with 338 additions and 277 deletions

View File

@@ -1,6 +1,93 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// 本地存储的 key
const SETTINGS_STORAGE_KEY = "comfyui_publisher_settings";
// 获取本地设置
function getLocalSettings() {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
return stored ? JSON.parse(stored) : { host: "" };
} catch (error) {
console.error("读取本地设置失败:", error);
return { host: "" };
}
}
// 保存设置到本地
function saveLocalSettings(settings) {
try {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch (error) {
console.error("保存本地设置失败:", error);
}
}
// 自定义 fetchApi 函数,使用本地存储的 host
async function customFetchApi(endpoint, options = {}) {
try {
// 从本地存储获取设置
const settings = getLocalSettings();
// 如果没有配置 host回退到默认的 api.fetchApi
if (!settings.host) {
console.warn("未配置 host使用默认的 ComfyUI API");
return await api.fetchApi(endpoint, options);
}
// 构建完整的 URL
const host = settings.host.replace(/\/$/, ""); // 移除末尾的斜杠
const fullUrl = `${host}${endpoint}`;
console.log(`使用本地存储的 host 请求: ${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) {
@@ -34,6 +121,15 @@ class Modal {
}
}
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() {
@@ -81,25 +177,19 @@ app.registerExtension({
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 = `<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, "从服务器获取工作流", () =>
showWorkflowListDialog()
);
const btnPublish = createButton(iconPublish, "发布当前工作流", () =>
showPublishDialog()
);
const btnSettings = createButton(`⚙️`, "发布设置", () =>
showSettingsDialog()
);
const btnGet = createButton(iconGet, "从服务器获取工作流", {
onClick: () => showWorkflowListDialog(),
});
const btnPublish = createButton(iconPublish, "发布当前工作流", {
onClick: () => showPublishDialog(),
});
const btnSettings = createButton(`⚙️`, "发布设置", {
onClick: () => showSettingsDialog(),
});
const separator = document.createElement("div");
separator.style.cssText =
"border-left: 1px solid var(--border-color); height: 22px; margin: 0 4px;";
@@ -111,63 +201,17 @@ app.registerExtension({
"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);
}
}
async function showWorkflowListDialog() {
workflowListDialog.setContent(
@@ -178,23 +222,16 @@ app.registerExtension({
]);
workflowListDialog.show();
try {
const response = await api.fetchApi("/publisher/workflows");
const response = await customFetchApi("/api/workflow");
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("服务器返回的数据格式无效");
}
const workflows = parseJson(text);
const workflowKeys = Object.keys(workflows);
if (workflowKeys.length === 0) {
if (workflows.length === 0) {
workflowListDialog.setContent(
'<p style="text-align:center;">服务器上没有找到任何工作流。</p>'
);
@@ -203,80 +240,24 @@ app.registerExtension({
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 = `<span>${baseName}</span><span style="color:var(--prompt-text-color); font-size: 0.9em;">${versions.length} 个版本</span>`;
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
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" }
);
};
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";
app.handleFile(file);
}
};
accordionContainer.appendChild(header);
accordionContainer.appendChild(panel);
accordionContainer.appendChild(workflowLabel);
});
workflowListDialog.setContent("");
workflowListDialog.element
@@ -290,34 +271,23 @@ app.registerExtension({
}
}
// ... (其余函数保持不变) ...
async function publishWorkflow() {
const baseName = document
const flowName = document
.getElementById("publisher-workflow-name")
.value.trim();
if (!baseName) {
if (!flowName) {
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}]`;
const versionString = new Date().toISOString();
try {
const workflow = await app.graph.serialize();
const body = { name: finalName, workflow: workflow };
statusEl.textContent = `正在发布为 "${finalName}"...`;
const response = await api.fetchApi("/publisher/publish", {
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),
@@ -347,34 +317,15 @@ app.registerExtension({
console.error("发布失败:", error);
}
}
async function showSettingsDialog() {
try {
let settings = { api_url: "", host: "" };
await api
.fetchApi("/publisher/settings", {
cache: "no-store",
})
.then(async (resp) => {
if (!resp.ok) {
console.error(`服务器错误: ${resp.status} ${resp.statusText}`);
return;
}
const text = await resp.text();
try {
settings = text ? JSON.parse(text) : {};
} catch (parseError) {
console.error("JSON解析失败:", parseError, "原始响应:", text);
settings = { api_url: "", host: "" };
}
});
// 首先尝试从本地存储获取设置
const settings = getLocalSettings();
settingsDialog.setContent(`
<label for="publisher-host" style="display: block; margin-bottom: 5px;">Host:</label>
<input id="publisher-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: 5px; border-radius: 4px; margin-bottom: 10px;">
<label for="publisher-api-url" style="display: block; margin-bottom: 5px;">API URL:</label>
<input id="publisher-api-url" type="text" value="${settings.api_url}" placeholder="例如: /api/workflow" 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;">
`);
settingsDialog.addButtons([
{ label: "取消", callback: () => settingsDialog.hide() },
@@ -386,51 +337,57 @@ app.registerExtension({
alert(`加载设置失败: ${error.message}`);
}
}
async function saveSettings() {
const apiUrl = document.getElementById("publisher-api-url").value;
const host = document.getElementById("publisher-host").value;
// 构建新的设置对象
const newSettings = { host: host };
try {
await api.fetchApi("/publisher/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_url: apiUrl, host: host }),
});
alert("设置已保存!");
// 保存到本地存储
saveLocalSettings(newSettings);
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: "" };
// 从本地存储获取设置
let settings = getLocalSettings();
// 如果本地没有设置,尝试从服务器获取
if (!settings.host) {
try {
const settingsResp = await customFetchApi("/publisher/settings", {
cache: "no-store",
});
if (settingsResp.ok) {
const text = await settingsResp.text();
try {
const serverSettings = text ? JSON.parse(text) : {};
settings = { ...settings, ...serverSettings };
// 保存到本地存储
saveLocalSettings(settings);
} catch (parseError) {
console.error("JSON解析失败:", parseError, "原始响应:", text);
}
}
} catch (error) {
console.error("从服务器获取设置失败:", error);
}
}
// 确保设置对象有必需的字段
settings = {
api_url: settings.api_url || "",
host: settings.host || "",
...settings,
};
if (!settings.api_url) {
alert("请先点击 ⚙️ 按钮设置发布API的URL。");
return;
}
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>`
);