feat: 增加配置文件支持主机地址,优化配置加载和保存逻辑
This commit is contained in:
18
.env
18
.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"
|
||||
|
||||
61
__init__.py
61
__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:
|
||||
|
||||
517
js/publisher.js
517
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 = `<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); }); }
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = `<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 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 = `<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 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('<p style="text-align:center;">正在从服务器加载工作流...</p>');
|
||||
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('<p style="text-align:center;">服务器上没有找到任何工作流。</p>'); 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 = `<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); };
|
||||
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(`<p style="color:var(--error-color); text-align:center;">加载失败: ${error.message}</p>`); }
|
||||
async function showWorkflowListDialog() {
|
||||
workflowListDialog.setContent(
|
||||
'<p style="text-align:center;">正在从服务器加载工作流...</p>'
|
||||
);
|
||||
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(`<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 || ''}" 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() },{ 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(`<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(); }
|
||||
const workflowKeys = Object.keys(workflows);
|
||||
if (workflowKeys.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;";
|
||||
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
|
||||
);
|
||||
};
|
||||
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(
|
||||
`<p style="color:var(--error-color); text-align:center;">加载失败: ${error.message}</p>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ... (其余函数保持不变) ...
|
||||
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(`
|
||||
<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() },
|
||||
{ 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(
|
||||
`<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}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user