feat: 添加CORS支持,优化API响应,重构工作流管理逻辑,支持本地存储设置
This commit is contained in:
153
__init__.py
153
__init__.py
@@ -13,7 +13,6 @@ CONFIG_FILE = os.path.join(NODE_DIR, "config.json")
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"api_url": "",
|
||||
"host": ""
|
||||
}
|
||||
|
||||
@@ -75,7 +74,11 @@ class WorkflowPublisherNode:
|
||||
async def get_publisher_settings(request):
|
||||
"""获取发布器设置"""
|
||||
config = load_config()
|
||||
return web.json_response(config)
|
||||
response = web.json_response(config)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/publisher/settings")
|
||||
@@ -83,15 +86,21 @@ 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"})
|
||||
response = web.json_response({"status": "success", "message": "Settings saved"})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
response = web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/publisher/publish")
|
||||
@@ -103,22 +112,24 @@ async def publish_workflow_handler(request):
|
||||
workflow_name = data.get("name")
|
||||
|
||||
if not workflow or not workflow_name:
|
||||
return web.json_response({"status": "error", "message": "Missing workflow data or name"}, status=400)
|
||||
response = web.json_response({"status": "error", "message": "Missing workflow data or name"}, status=400)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
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)
|
||||
if not host:
|
||||
response = web.json_response({"status": "error", "message": "Host not configured"}, status=400)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
# 构建完整的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
|
||||
# 构建完整的URL - 直接使用 host + /api/workflow
|
||||
full_url = host.rstrip('/') + '/api/workflow'
|
||||
|
||||
# 准备要发送到目标API的数据
|
||||
payload = {
|
||||
@@ -133,19 +144,31 @@ async def publish_workflow_handler(request):
|
||||
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(
|
||||
result_response = web.json_response(
|
||||
{"status": "success", "message": "Workflow published successfully!", "details": response_text})
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
else:
|
||||
return web.json_response({
|
||||
result_response = web.json_response({
|
||||
"status": "error",
|
||||
"message": f"Failed to publish workflow. Target API returned status {response.status}",
|
||||
"details": response_text
|
||||
}, status=500)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response = web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/publisher/workflows")
|
||||
@@ -153,29 +176,31 @@ 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)
|
||||
if not host:
|
||||
response = web.json_response({"status": "error", "message": "Host not configured"}, status=400)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
# 构建完整的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
|
||||
# 构建完整的URL - 直接使用 host + /api/workflow
|
||||
full_url = host.rstrip('/') + '/api/workflow'
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(full_url) as response:
|
||||
if response.status != 200:
|
||||
response_text = await response.text()
|
||||
return web.json_response({
|
||||
result_response = web.json_response({
|
||||
"status": "error",
|
||||
"message": f"Failed to fetch workflows. Target API returned status {response.status}",
|
||||
"details": response_text
|
||||
}, status=500)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
workflows = await response.json()
|
||||
|
||||
@@ -206,12 +231,20 @@ async def get_workflows_from_server(request):
|
||||
for base_name in grouped_workflows:
|
||||
grouped_workflows[base_name].sort(key=lambda x: x['version'], reverse=True)
|
||||
|
||||
return web.json_response(grouped_workflows)
|
||||
result_response = web.json_response(grouped_workflows)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response = web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/publisher/workflow/delete")
|
||||
@@ -221,22 +254,24 @@ async def delete_workflow_version(request):
|
||||
data = await request.json()
|
||||
workflow_name = data.get("name")
|
||||
if not workflow_name:
|
||||
return web.json_response({"status": "error", "message": "Missing workflow name"}, status=400)
|
||||
response = web.json_response({"status": "error", "message": "Missing workflow name"}, status=400)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
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)
|
||||
if not host:
|
||||
response = web.json_response({"status": "error", "message": "Host not configured"}, status=400)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return response
|
||||
|
||||
# 构建完整的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 - 直接使用 host + /api/workflow
|
||||
base_url = host.rstrip('/') + '/api/workflow'
|
||||
|
||||
# 构建目标URL,需要对工作流名称进行URL编码
|
||||
delete_url = f"{base_url}/{urllib.parse.quote(workflow_name)}"
|
||||
@@ -244,16 +279,40 @@ async def delete_workflow_version(request):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.delete(delete_url) as response:
|
||||
if response.status == 200:
|
||||
return web.json_response({"status": "success", "message": "Workflow version deleted"})
|
||||
result_response = web.json_response({"status": "success", "message": "Workflow version deleted"})
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
else:
|
||||
details = await response.text()
|
||||
return web.json_response({
|
||||
result_response = web.json_response({
|
||||
"status": "error",
|
||||
"message": f"Target API failed to delete. Status: {response.status}",
|
||||
"details": details
|
||||
}, status=response.status)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response = web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
result_response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
result_response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
result_response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
return result_response
|
||||
|
||||
|
||||
# 添加OPTIONS请求处理,用于CORS预检请求
|
||||
@PromptServer.instance.routes.options("/publisher/{path:.*}")
|
||||
async def handle_options(request):
|
||||
"""处理CORS预检请求"""
|
||||
response = web.Response()
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
response.headers['Access-Control-Max-Age'] = '86400' # 24小时
|
||||
return response
|
||||
|
||||
# -----------------
|
||||
# ComfyUI 注册
|
||||
|
||||
381
js/publisher.js
381
js/publisher.js
@@ -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>`
|
||||
);
|
||||
|
||||
17
run_service.py
Normal file
17
run_service.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
启动脚本 - 从项目根目录运行 workflow_service
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将项目根目录添加到Python路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 现在可以正常导入
|
||||
from workflow_service.main import web_app
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(web_app, host="0.0.0.0", port=18000)
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
import aiosqlite
|
||||
import json
|
||||
import re
|
||||
@@ -20,23 +21,28 @@ async def init_db():
|
||||
print(f"数据库 '{DATABASE_FILE}' 已准备就绪。")
|
||||
|
||||
async def save_workflow(name: str, workflow_json: str):
|
||||
# 正则表达式修正为匹配更精确的版本格式 [YYYYMMDDHHMMSS]
|
||||
version_match = re.search(r"\[(\d{14})\]$", name)
|
||||
if not version_match:
|
||||
raise ValueError("Workflow name must have a version suffix like [YYYYMMDDHHMMSS]")
|
||||
version = version_match.group(1)
|
||||
base_name = re.sub(r"\s*\[\d{14}\]$", "", name).strip()
|
||||
version = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
async with aiosqlite.connect(DATABASE_FILE) as db:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO workflows (name, base_name, version, workflow_json) VALUES (?, ?, ?, ?)",
|
||||
(name, base_name, version, workflow_json)
|
||||
(f"{name} [{version}]", name, version, workflow_json)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def get_all_workflows() -> list[dict]:
|
||||
async with aiosqlite.connect(DATABASE_FILE) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("SELECT name, workflow_json FROM workflows")
|
||||
# 按 base_name 分组,获取每个基础名称的最新版本记录
|
||||
cursor = await db.execute("""
|
||||
SELECT w1.name, w1.workflow_json, w1.base_name, w1.version
|
||||
FROM workflows w1
|
||||
INNER JOIN (
|
||||
SELECT base_name, MAX(version) as max_version
|
||||
FROM workflows
|
||||
GROUP BY base_name
|
||||
) w2 ON w1.base_name = w2.base_name AND w1.version = w2.max_version
|
||||
ORDER BY w1.base_name
|
||||
""")
|
||||
rows = await cursor.fetchall()
|
||||
return [{"name": row["name"], "workflow": json.loads(row["workflow_json"])} for row in rows]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import aiohttp
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, HTTPException, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
from workflow_service import comfyui_client
|
||||
@@ -22,6 +23,15 @@ settings = Settings()
|
||||
|
||||
web_app = FastAPI(title="ComfyUI Workflow Service & Management API")
|
||||
|
||||
# 配置跨域支持
|
||||
web_app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 允许所有来源,生产环境中建议限制具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # 允许所有HTTP方法
|
||||
allow_headers=["*"], # 允许所有请求头
|
||||
)
|
||||
|
||||
|
||||
# --- Pydantic Response Models for New Endpoints ---
|
||||
|
||||
@@ -72,11 +82,11 @@ async def startup_event():
|
||||
|
||||
|
||||
# --- Section 1: 工作流管理API ---
|
||||
BASE_MANAGEMENT_PATH = "/api/workflow"
|
||||
|
||||
|
||||
@web_app.post(BASE_MANAGEMENT_PATH, status_code=201)
|
||||
@web_app.post("/api/workflow", status_code=201)
|
||||
async def publish_workflow_endpoint(request: Request):
|
||||
"""
|
||||
发布工作流
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
name, wf_json = data.get("name"), data.get("workflow")
|
||||
@@ -92,8 +102,11 @@ async def publish_workflow_endpoint(request: Request):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to save workflow: {e}")
|
||||
|
||||
|
||||
@web_app.get(BASE_MANAGEMENT_PATH, response_model=List[dict])
|
||||
@web_app.get("/api/workflow", response_model=List[dict])
|
||||
async def get_all_workflows_endpoint():
|
||||
"""
|
||||
获取所有工作流
|
||||
"""
|
||||
try:
|
||||
return await database.get_all_workflows()
|
||||
except Exception as e:
|
||||
@@ -101,9 +114,12 @@ async def get_all_workflows_endpoint():
|
||||
|
||||
|
||||
# [BUG修复] 修正API路径,使其与其他管理端点保持一致
|
||||
@web_app.delete(f"{BASE_MANAGEMENT_PATH}/{{workflow_name:path}}")
|
||||
@web_app.delete(f"/api/workflow/{{workflow_name:path}}")
|
||||
async def delete_workflow_endpoint(workflow_name: str = Path(...,
|
||||
description="The full, unique name of the workflow to delete, e.g., 'my_workflow [20250101120000]'")):
|
||||
"""
|
||||
删除工作流
|
||||
"""
|
||||
try:
|
||||
success = await database.delete_workflow(workflow_name)
|
||||
if success:
|
||||
@@ -142,6 +158,9 @@ async def handle_file_upload(file_path: str, base_name: str) -> str:
|
||||
|
||||
@web_app.post("/api/run/")
|
||||
async def execute_workflow_endpoint(base_name: str, request_data_raw: Dict[str, Any], version: Optional[str] = None):
|
||||
"""
|
||||
执行工作流
|
||||
"""
|
||||
cleanup_paths = []
|
||||
try:
|
||||
# 1. 获取工作流定义
|
||||
@@ -261,6 +280,9 @@ async def execute_workflow_endpoint(base_name: str, request_data_raw: Dict[str,
|
||||
# --- Section 3: 工作流元数据/规范API ---
|
||||
@web_app.get("/api/spec/")
|
||||
async def get_workflow_spec_endpoint(base_name: str, version: Optional[str] = None):
|
||||
"""
|
||||
获取工作流规范
|
||||
"""
|
||||
if version:
|
||||
workflow_data = await database.get_workflow_by_version(base_name, version)
|
||||
else:
|
||||
@@ -373,7 +395,7 @@ def read_root():
|
||||
"step": 1,
|
||||
"action": "发布一个工作流 (Publish a Workflow)",
|
||||
"description": "从 ComfyUI 保存你的工作流 (API格式),然后使用一个唯一的名称将其发布到服务中。名称中必须包含一个版本号,格式为 `[YYYYMMDDHHMMSS]`。",
|
||||
"endpoint": f"POST {BASE_MANAGEMENT_PATH}",
|
||||
"endpoint": f"/api/workflow",
|
||||
"example_curl": "curl -X POST http://127.0.0.1:18000/api/workflow -H 'Content-Type: application/json' -d '{\n \"name\": \"my_t2i [20250801120000]\",\n \"workflow\": { ... your_workflow_api_json ... }\n}'"
|
||||
},
|
||||
{
|
||||
@@ -404,11 +426,11 @@ def read_root():
|
||||
"action": "管理工作流 (Manage Workflows)",
|
||||
"description": "你也可以列出所有已发布的工作流或删除不再需要的工作流。",
|
||||
"endpoints": [
|
||||
{"method": "GET", "path": f"{BASE_MANAGEMENT_PATH}", "description": "列出所有工作流。"},
|
||||
{"method": "DELETE", "path": f"{BASE_MANAGEMENT_PATH}/{{workflow_name}}",
|
||||
{"method": "GET", "path": f"/api/workflow", "description": "列出所有工作流。"},
|
||||
{"method": "DELETE", "path": f"/api/workflow/{{workflow_name}}",
|
||||
"description": "按完整名称删除一个工作流。"}
|
||||
],
|
||||
"example_curl_list": f"curl -X GET http://127.0.0.1:18000{BASE_MANAGEMENT_PATH}",
|
||||
"example_curl_list": f"curl -X GET http://127.0.0.1:18000/api/workflow",
|
||||
"example_curl_delete": f"curl -X DELETE 'http://127.0.0.1:18000/api/workflow/my_t2i%20[20250801120000]'"
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user