Restart ComfyUI automatically after plugin updates
This commit is contained in:
@@ -106,8 +106,9 @@ O1KEY_ASYNC_API_BASE_URL=https://your-async-api-domain.com
|
|||||||
|
|
||||||
1. 点击 ComfyUI 左侧工具栏中“令牌管理”下方的“更新”按钮。
|
1. 点击 ComfyUI 左侧工具栏中“令牌管理”下方的“更新”按钮。
|
||||||
2. 等待版本检查。如果发现新版本,确认后等待更新完成;需要时可在面板中重新检查。
|
2. 等待版本检查。如果发现新版本,确认后等待更新完成;需要时可在面板中重新检查。
|
||||||
3. 按完成提示重启 ComfyUI。如果面板提示需要技术支持,请联系维护人员。
|
3. 更新完成后,ComfyUI 会自动重启并刷新页面。如果面板提示需要技术支持,请联系维护人员完成配置后再重启。
|
||||||
4. 刷新页面后,左侧“更新”下方会出现“检测”按钮,可用来确认本次版本已加载。
|
|
||||||
|
> 首次从旧版本更新到支持自动重启的版本时,请按旧版面板提示手动重启一次;之后的更新会自动重启。
|
||||||
|
|
||||||
> 如果当前安装包含自定义修改,面板会停止更新并保留现有内容。此时请联系维护人员处理。
|
> 如果当前安装包含自定义修改,面板会停止更新并保留现有内容。此时请联系维护人员处理。
|
||||||
|
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ Like `clients`, the `utils` package uses lazy exports to reduce startup work.
|
|||||||
|
|
||||||
Every JavaScript file in `web/` is served as a ComfyUI extension. Major responsibilities include settings, chat, cases, notes, element management, workflow migration, upload helpers, previews, painting, trimming, and the panel-style image generator.
|
Every JavaScript file in `web/` is served as a ComfyUI extension. Major responsibilities include settings, chat, cases, notes, element management, workflow migration, upload helpers, previews, painting, trimming, and the panel-style image generator.
|
||||||
|
|
||||||
`web/js/o1keyUpdateButton.js` places an Update button directly below the Token Manager button in the left toolbar, then a temporary Detect button before Restart. Detect displays a short local notice to confirm the new frontend loaded. Opening the Update dialog immediately calls `GET /o1key/update/check` and asks for confirmation only when a newer version is available; confirmation calls `POST /o1key/update`. Both routes delegate to `utils/updater.py` and share a lock. The updater fetches the public `main` branch from `https://git.o1key.com/publisher/comfyui_o1key.git` without changing the user's `origin`. It reports an already current installation before checking local modifications, and only fast-forwards a clean local Git `main` when an update exists. Local tracked changes, divergent history, and file collisions receive structured error codes; the UI maps those codes to customer-facing messages without exposing the repository or Git details. It never resets or cleans the worktree. A completed update requires a ComfyUI restart to load new Python and JavaScript code.
|
`web/js/o1keyUpdateButton.js` places an Update button directly below the Token Manager button in the left toolbar, before Restart. Opening the Update dialog immediately calls `GET /o1key/update/check` and asks for confirmation only when a newer version is available; confirmation calls `POST /o1key/update`. Both routes delegate to `utils/updater.py` and share a lock. The updater fetches the public `main` branch from `https://git.o1key.com/publisher/comfyui_o1key.git` without changing the user's `origin`. It reports an already current installation before checking local modifications, and only fast-forwards a clean local Git `main` when an update exists. Local tracked changes, divergent history, and file collisions receive structured error codes; the UI maps those codes to customer-facing messages without exposing the repository or Git details. It never resets or cleans the worktree. After a successful update with unchanged requirements, the frontend invokes the shared `web/js/o1keyRestart.js` flow, waits for a new process boot ID and a ready system endpoint, then reloads the page. A requirements change still asks for maintenance before restart; an automatic restart failure leaves the manual Restart button available.
|
||||||
|
|
||||||
The updater dialog keeps the check result, update confirmation, progress, and retry actions in one ComfyUI-styled modal. Opening it runs the check; a newer version changes the primary action to “立即更新”, while “稍后再说” closes without updating.
|
The updater dialog keeps the check result, update confirmation, progress, and retry actions in one ComfyUI-styled modal. Opening it runs the check; a newer version changes the primary action to “立即更新”, while “稍后再说” closes without updating.
|
||||||
|
Because an update runs in the old frontend and server process, installations upgrading from a version without automatic restart must restart manually once to load this behavior.
|
||||||
|
|
||||||
Because the directory is auto-loaded, unused or experimental JavaScript must not be left here.
|
Because the directory is auto-loaded, unused or experimental JavaScript must not be left here.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
const source = fs.readFileSync(new URL("../web/js/o1keyRestart.js", import.meta.url), "utf8")
|
||||||
|
.replace(/^export /gm, "");
|
||||||
|
const requests = [];
|
||||||
|
const responses = [];
|
||||||
|
const reloads = [];
|
||||||
|
const attributes = new Map();
|
||||||
|
const icon = { className: "pi pi-refresh side-bar-button-icon" };
|
||||||
|
const label = { textContent: "重启" };
|
||||||
|
const button = {
|
||||||
|
disabled: false,
|
||||||
|
setAttribute(name, value) { attributes.set(name, value); },
|
||||||
|
querySelector(selector) {
|
||||||
|
return selector === ".side-bar-button-icon" ? icon : label;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const document = {
|
||||||
|
querySelector(selector) {
|
||||||
|
assert.equal(selector, "#o1key-restart-button");
|
||||||
|
return button;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const window = {
|
||||||
|
location: {
|
||||||
|
href: "http://127.0.0.1:8188/?workflow=test",
|
||||||
|
replace(url) { reloads.push(url); },
|
||||||
|
},
|
||||||
|
setTimeout() { return 1; },
|
||||||
|
clearTimeout() {},
|
||||||
|
};
|
||||||
|
const reply = (data, status = 200) => ({
|
||||||
|
ok: status < 400,
|
||||||
|
status,
|
||||||
|
async json() { return data; },
|
||||||
|
});
|
||||||
|
async function fetch(url, options) {
|
||||||
|
requests.push({ url, options });
|
||||||
|
const next = responses.shift();
|
||||||
|
assert.ok(next, `Unexpected request: ${url}`);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
const context = vm.createContext({ document, window, fetch, AbortController, URL, Date });
|
||||||
|
vm.runInContext(source, context);
|
||||||
|
|
||||||
|
responses.push(
|
||||||
|
reply({ ready: true, boot_id: "old" }),
|
||||||
|
reply({ error: "restart rejected" }, 500),
|
||||||
|
);
|
||||||
|
await assert.rejects(context.startComfyUIRestart(), /restart rejected/);
|
||||||
|
assert.equal(reloads.length, 0);
|
||||||
|
assert.equal(button.disabled, false);
|
||||||
|
assert.equal(attributes.get("aria-busy"), "false");
|
||||||
|
assert.equal(label.textContent, "重启");
|
||||||
|
assert.match(icon.className, /pi-refresh/);
|
||||||
|
requests.length = 0;
|
||||||
|
|
||||||
|
responses.push(
|
||||||
|
reply({ ready: true, boot_id: "old" }),
|
||||||
|
reply({ success: true, boot_id: "old" }),
|
||||||
|
reply({ ready: true, boot_id: "new" }),
|
||||||
|
reply({}),
|
||||||
|
);
|
||||||
|
await context.startComfyUIRestart();
|
||||||
|
assert.equal(requests.length, 4);
|
||||||
|
assert.match(requests[0].url, /^\/o1key\/restart\/status\?/);
|
||||||
|
assert.equal(requests[1].url, "/o1key/restart");
|
||||||
|
assert.equal(requests[1].options.method, "POST");
|
||||||
|
assert.equal(requests[1].options.body, "{}");
|
||||||
|
assert.match(requests[2].url, /^\/o1key\/restart\/status\?/);
|
||||||
|
assert.match(requests[3].url, /^\/api\/system_stats\?/);
|
||||||
|
assert.equal(reloads.length, 1);
|
||||||
|
const reloadedUrl = new URL(reloads[0]);
|
||||||
|
assert.equal(reloadedUrl.searchParams.get("workflow"), "test");
|
||||||
|
assert.ok(reloadedUrl.searchParams.has("o1key_restart"));
|
||||||
|
assert.equal(button.disabled, true);
|
||||||
|
assert.equal(attributes.get("aria-busy"), "true");
|
||||||
|
assert.equal(label.textContent, "重启中");
|
||||||
|
await assert.rejects(context.startComfyUIRestart(), /正在重启/);
|
||||||
|
assert.equal(requests.length, 4);
|
||||||
|
assert.equal(reloads.length, 1);
|
||||||
@@ -96,7 +96,8 @@ let observer;
|
|||||||
const requests = [];
|
const requests = [];
|
||||||
const responses = [];
|
const responses = [];
|
||||||
let fetchFailure = false;
|
let fetchFailure = false;
|
||||||
let dismissNotice;
|
let restartCalls = 0;
|
||||||
|
let restartFailure = false;
|
||||||
const reply = (body, status = 200) => ({ ok: status < 400, status, async json() { return body; } });
|
const reply = (body, status = 200) => ({ ok: status < 400, status, async json() { return body; } });
|
||||||
const app = { registerExtension(value) { extension = value; } };
|
const app = { registerExtension(value) { extension = value; } };
|
||||||
const api = {
|
const api = {
|
||||||
@@ -111,11 +112,11 @@ class MutationObserver {
|
|||||||
observe() {}
|
observe() {}
|
||||||
disconnect() { this.disconnected = true; }
|
disconnect() { this.disconnected = true; }
|
||||||
}
|
}
|
||||||
const window = {
|
async function startComfyUIRestart() {
|
||||||
setTimeout(callback) { dismissNotice = callback; return 1; },
|
restartCalls++;
|
||||||
clearTimeout() { dismissNotice = null; },
|
if (restartFailure) throw new Error("restart failed");
|
||||||
};
|
}
|
||||||
vm.runInNewContext(source, { app, api, document, window, MutationObserver });
|
vm.runInNewContext(source, { app, api, document, startComfyUIRestart, MutationObserver });
|
||||||
|
|
||||||
extension.setup();
|
extension.setup();
|
||||||
assert.ok(observer);
|
assert.ok(observer);
|
||||||
@@ -135,29 +136,15 @@ toolbar.append(token, restart);
|
|||||||
observer.callback();
|
observer.callback();
|
||||||
|
|
||||||
const update = document.querySelector("#o1key-update-button");
|
const update = document.querySelector("#o1key-update-button");
|
||||||
const detect = document.querySelector("#o1key-detect-button");
|
|
||||||
assert.equal(toolbar.children[0], token);
|
assert.equal(toolbar.children[0], token);
|
||||||
assert.equal(toolbar.children[1], update);
|
assert.equal(toolbar.children[1], update);
|
||||||
assert.equal(toolbar.children[2], detect);
|
assert.equal(toolbar.children[2], restart);
|
||||||
assert.equal(toolbar.children[3], restart);
|
|
||||||
assert.equal(update.querySelector(".side-bar-button-label").textContent, "更新");
|
assert.equal(update.querySelector(".side-bar-button-label").textContent, "更新");
|
||||||
assert.match(update.querySelector(".side-bar-button-icon").className, /pi-download/);
|
assert.match(update.querySelector(".side-bar-button-icon").className, /pi-download/);
|
||||||
assert.equal(detect.querySelector(".side-bar-button-label").textContent, "检测");
|
assert.equal(document.querySelector("#o1key-detect-button"), null);
|
||||||
assert.match(detect.querySelector(".side-bar-button-icon").className, /pi-check-circle/);
|
|
||||||
assert.ok(observer.disconnected);
|
assert.ok(observer.disconnected);
|
||||||
extension.setup();
|
extension.setup();
|
||||||
assert.equal(toolbar.children.length, 4);
|
assert.equal(toolbar.children.length, 3);
|
||||||
|
|
||||||
detect.listeners.get("click")();
|
|
||||||
const notice = document.querySelector("#o1key-detect-notice");
|
|
||||||
assert.equal(notice.children[1].textContent, "检测按钮已加载");
|
|
||||||
assert.equal(notice.hidden, false);
|
|
||||||
assert.equal(requests.length, 0);
|
|
||||||
dismissNotice();
|
|
||||||
assert.equal(notice.hidden, true);
|
|
||||||
detect.listeners.get("click")();
|
|
||||||
assert.equal(notice.hidden, false);
|
|
||||||
assert.equal(document.body.children.filter((child) => child.id === "o1key-detect-notice").length, 1);
|
|
||||||
|
|
||||||
responses.push(reply({ update_available: false }));
|
responses.push(reply({ update_available: false }));
|
||||||
update.listeners.get("click")();
|
update.listeners.get("click")();
|
||||||
@@ -212,22 +199,34 @@ await new Promise(setImmediate);
|
|||||||
assert.equal(dialog.open, true);
|
assert.equal(dialog.open, true);
|
||||||
assert.equal(dialog.dataset.state, "available");
|
assert.equal(dialog.dataset.state, "available");
|
||||||
|
|
||||||
responses.push(reply({ updated: true, version: "abc1234", requirements_changed: false }));
|
responses.push(reply({ updated: true, version: "abc1234", requirements_changed: true }));
|
||||||
await primary.listeners.get("click")();
|
await primary.listeners.get("click")();
|
||||||
assert.equal(requests.at(-1).path, "/o1key/update");
|
assert.equal(requests.at(-1).path, "/o1key/update");
|
||||||
assert.equal(requests.at(-1).options.method, "POST");
|
assert.equal(requests.at(-1).options.method, "POST");
|
||||||
assert.equal(requests.at(-1).options.headers["X-O1Key-Update"], "1");
|
assert.equal(requests.at(-1).options.headers["X-O1Key-Update"], "1");
|
||||||
assert.match(status.textContent, /更新完成/);
|
assert.match(status.textContent, /更新完成/);
|
||||||
assert.match(suggestion.textContent, /重启 ComfyUI/);
|
assert.match(suggestion.textContent, /联系技术支持/);
|
||||||
|
assert.doesNotMatch(suggestion.textContent, /requirements\.txt/);
|
||||||
assert.equal(primary.textContent, "完成");
|
assert.equal(primary.textContent, "完成");
|
||||||
assert.equal(dialog.dataset.state, "success");
|
assert.equal(dialog.dataset.state, "success");
|
||||||
|
assert.equal(restartCalls, 0);
|
||||||
|
|
||||||
responses.push(reply({ update_available: true }));
|
responses.push(reply({ update_available: true }));
|
||||||
await dialog.checkForUpdate();
|
await dialog.checkForUpdate();
|
||||||
responses.push(reply({ updated: true, requirements_changed: true }));
|
restartFailure = true;
|
||||||
|
responses.push(reply({ updated: true, requirements_changed: false }));
|
||||||
await primary.listeners.get("click")();
|
await primary.listeners.get("click")();
|
||||||
assert.match(suggestion.textContent, /联系技术支持/);
|
assert.equal(restartCalls, 1);
|
||||||
assert.doesNotMatch(suggestion.textContent, /requirements\.txt/);
|
assert.equal(dialog.dataset.state, "restart_failed");
|
||||||
|
assert.match(status.textContent, /自动重启失败/);
|
||||||
|
assert.match(suggestion.textContent, /左侧“重启”按钮/);
|
||||||
|
assert.equal(primary.textContent, "关闭");
|
||||||
|
await primary.listeners.get("click")();
|
||||||
|
assert.equal(dialog.open, false);
|
||||||
|
responses.push(reply({ update_available: true }));
|
||||||
|
update.listeners.get("click")();
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
assert.equal(dialog.dataset.state, "available");
|
||||||
|
|
||||||
const requestsBeforeError = requests.length;
|
const requestsBeforeError = requests.length;
|
||||||
responses.push(reply({ code: "local_changes", error: "插件目录有未提交的代码修改。", suggestion: "请先提交" }, 409));
|
responses.push(reply({ code: "local_changes", error: "插件目录有未提交的代码修改。", suggestion: "请先提交" }, 409));
|
||||||
@@ -254,3 +253,19 @@ assert.equal(primary.disabled, false);
|
|||||||
assert.equal(head.children.length, 1);
|
assert.equal(head.children.length, 1);
|
||||||
header.children[2].listeners.get("click")();
|
header.children[2].listeners.get("click")();
|
||||||
assert.equal(dialog.open, false);
|
assert.equal(dialog.open, false);
|
||||||
|
|
||||||
|
fetchFailure = false;
|
||||||
|
restartFailure = false;
|
||||||
|
responses.push(reply({ update_available: true }));
|
||||||
|
update.listeners.get("click")();
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
responses.push(reply({ updated: true, requirements_changed: false }));
|
||||||
|
await primary.listeners.get("click")();
|
||||||
|
assert.equal(restartCalls, 2);
|
||||||
|
assert.equal(dialog.dataset.state, "restarting");
|
||||||
|
assert.match(status.textContent, /正在重启 ComfyUI/);
|
||||||
|
assert.match(suggestion.textContent, /页面会自动刷新/);
|
||||||
|
assert.equal(primary.disabled, true);
|
||||||
|
assert.equal(secondary.disabled, true);
|
||||||
|
assert.equal(header.children[2].disabled, true);
|
||||||
|
assert.equal(primary.textContent, "重启中…");
|
||||||
|
|||||||
+3
-100
@@ -1,10 +1,8 @@
|
|||||||
import { app } from "../../../scripts/app.js";
|
import { app } from "../../../scripts/app.js";
|
||||||
|
import { startComfyUIRestart } from "./o1keyRestart.js";
|
||||||
|
|
||||||
const DIALOG_ID = "o1key-api-settings-dialog";
|
const DIALOG_ID = "o1key-api-settings-dialog";
|
||||||
const STYLE_ID = "o1key-api-settings-styles";
|
const STYLE_ID = "o1key-api-settings-styles";
|
||||||
const RESTART_STATUS_URL = "/o1key/restart/status";
|
|
||||||
const RESTART_TIMEOUT_MS = 120_000;
|
|
||||||
let restartInProgress = false;
|
|
||||||
|
|
||||||
function ensureStyles() {
|
function ensureStyles() {
|
||||||
if (document.querySelector(`#${STYLE_ID}`)) return;
|
if (document.querySelector(`#${STYLE_ID}`)) return;
|
||||||
@@ -98,106 +96,11 @@ async function readJson(response) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function delay(milliseconds) {
|
async function restartComfyUI() {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(url, options = {}, timeout = 2_500) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = window.setTimeout(() => controller.abort(), timeout);
|
|
||||||
try {
|
|
||||||
return await fetch(url, {
|
|
||||||
...options,
|
|
||||||
cache: "no-store",
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
window.clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRestartStatus() {
|
|
||||||
const separator = RESTART_STATUS_URL.includes("?") ? "&" : "?";
|
|
||||||
const response = await fetchWithTimeout(
|
|
||||||
`${RESTART_STATUS_URL}${separator}_=${Date.now()}`
|
|
||||||
);
|
|
||||||
return readJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRestartButtonState(button, busy) {
|
|
||||||
button.disabled = busy;
|
|
||||||
button.setAttribute("aria-busy", String(busy));
|
|
||||||
button.setAttribute("aria-label", busy ? "ComfyUI 正在重启" : "重启");
|
|
||||||
button.setAttribute("title", busy ? "ComfyUI 正在重启" : "重启");
|
|
||||||
|
|
||||||
const icon = button.querySelector(".side-bar-button-icon");
|
|
||||||
if (icon) {
|
|
||||||
icon.className = busy
|
|
||||||
? "pi pi-spinner pi-spin side-bar-button-icon"
|
|
||||||
: "pi pi-refresh side-bar-button-icon";
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = button.querySelector(".side-bar-button-label");
|
|
||||||
if (label) label.textContent = busy ? "重启中" : "重启";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForRestart(oldBootId) {
|
|
||||||
const deadline = Date.now() + RESTART_TIMEOUT_MS;
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
try {
|
|
||||||
const status = await getRestartStatus();
|
|
||||||
if (status.ready && status.boot_id && status.boot_id !== oldBootId) {
|
|
||||||
const systemStatus = await fetchWithTimeout(
|
|
||||||
`/api/system_stats?_=${Date.now()}`
|
|
||||||
);
|
|
||||||
if (systemStatus.ok) return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 旧进程退出到新进程监听端口之间,接口暂时不可用是正常状态。
|
|
||||||
}
|
|
||||||
await delay(1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("等待 ComfyUI 重启超时,请查看终端中的错误信息。");
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadAfterRestart() {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set("o1key_restart", Date.now().toString());
|
|
||||||
window.location.replace(url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function restartComfyUI(event) {
|
|
||||||
if (restartInProgress) return;
|
|
||||||
if (!window.confirm("确定要重启 ComfyUI 吗?当前正在执行的任务会被中断。")) return;
|
if (!window.confirm("确定要重启 ComfyUI 吗?当前正在执行的任务会被中断。")) return;
|
||||||
|
|
||||||
const button = event.currentTarget;
|
|
||||||
restartInProgress = true;
|
|
||||||
setRestartButtonState(button, true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let currentStatus = null;
|
await startComfyUIRestart();
|
||||||
try {
|
|
||||||
currentStatus = await getRestartStatus();
|
|
||||||
} catch {
|
|
||||||
// POST 响应还会返回旧进程的 boot_id,因此预检失败不阻断重启。
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchWithTimeout("/o1key/restart", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: "{}",
|
|
||||||
}, 5_000);
|
|
||||||
const result = await readJson(response);
|
|
||||||
const oldBootId = result.boot_id || currentStatus?.boot_id;
|
|
||||||
if (!oldBootId) throw new Error("无法确认当前 ComfyUI 进程标识。");
|
|
||||||
|
|
||||||
await waitForRestart(oldBootId);
|
|
||||||
reloadAfterRestart();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
restartInProgress = false;
|
|
||||||
setRestartButtonState(button, false);
|
|
||||||
window.alert(error?.message || "ComfyUI 重启失败,请查看终端中的错误信息。");
|
window.alert(error?.message || "ComfyUI 重启失败,请查看终端中的错误信息。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
const RESTART_STATUS_URL = "/o1key/restart/status";
|
||||||
|
const RESTART_TIMEOUT_MS = 120_000;
|
||||||
|
let restartInProgress = false;
|
||||||
|
|
||||||
|
async function readJson(response) {
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(milliseconds) {
|
||||||
|
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithTimeout(url, options = {}, timeout = 2_500) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = window.setTimeout(() => controller.abort(), timeout);
|
||||||
|
try {
|
||||||
|
return await fetch(url, {
|
||||||
|
...options,
|
||||||
|
cache: "no-store",
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRestartStatus() {
|
||||||
|
const separator = RESTART_STATUS_URL.includes("?") ? "&" : "?";
|
||||||
|
const response = await fetchWithTimeout(
|
||||||
|
`${RESTART_STATUS_URL}${separator}_=${Date.now()}`
|
||||||
|
);
|
||||||
|
return readJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRestartButtonState(button, busy) {
|
||||||
|
if (!button) return;
|
||||||
|
button.disabled = busy;
|
||||||
|
button.setAttribute("aria-busy", String(busy));
|
||||||
|
button.setAttribute("aria-label", busy ? "ComfyUI 正在重启" : "重启");
|
||||||
|
button.setAttribute("title", busy ? "ComfyUI 正在重启" : "重启");
|
||||||
|
|
||||||
|
const icon = button.querySelector(".side-bar-button-icon");
|
||||||
|
if (icon) {
|
||||||
|
icon.className = busy
|
||||||
|
? "pi pi-spinner pi-spin side-bar-button-icon"
|
||||||
|
: "pi pi-refresh side-bar-button-icon";
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = button.querySelector(".side-bar-button-label");
|
||||||
|
if (label) label.textContent = busy ? "重启中" : "重启";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRestart(oldBootId) {
|
||||||
|
const deadline = Date.now() + RESTART_TIMEOUT_MS;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
const status = await getRestartStatus();
|
||||||
|
if (status.ready && status.boot_id && status.boot_id !== oldBootId) {
|
||||||
|
const systemStatus = await fetchWithTimeout(
|
||||||
|
`/api/system_stats?_=${Date.now()}`
|
||||||
|
);
|
||||||
|
if (systemStatus.ok) return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 旧进程退出到新进程监听端口之间,接口暂时不可用是正常状态。
|
||||||
|
}
|
||||||
|
await delay(1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("等待 ComfyUI 重启超时,请查看终端中的错误信息。");
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadAfterRestart() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("o1key_restart", Date.now().toString());
|
||||||
|
window.location.replace(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startComfyUIRestart() {
|
||||||
|
if (restartInProgress) throw new Error("ComfyUI 正在重启,请稍候。");
|
||||||
|
restartInProgress = true;
|
||||||
|
let reloadRequested = false;
|
||||||
|
const button = document.querySelector("#o1key-restart-button");
|
||||||
|
setRestartButtonState(button, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let currentStatus = null;
|
||||||
|
try {
|
||||||
|
currentStatus = await getRestartStatus();
|
||||||
|
} catch {
|
||||||
|
// POST 响应还会返回旧进程的 boot_id,因此预检失败不阻断重启。
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchWithTimeout("/o1key/restart", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: "{}",
|
||||||
|
}, 5_000);
|
||||||
|
const result = await readJson(response);
|
||||||
|
const oldBootId = result.boot_id || currentStatus?.boot_id;
|
||||||
|
if (!oldBootId) throw new Error("无法确认当前 ComfyUI 进程标识。");
|
||||||
|
|
||||||
|
await waitForRestart(oldBootId);
|
||||||
|
reloadAfterRestart();
|
||||||
|
reloadRequested = true;
|
||||||
|
} catch (error) {
|
||||||
|
setRestartButtonState(button, false);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (!reloadRequested) restartInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-78
@@ -1,13 +1,11 @@
|
|||||||
import { app } from "../../../scripts/app.js";
|
import { app } from "../../../scripts/app.js";
|
||||||
import { api } from "../../../scripts/api.js";
|
import { api } from "../../../scripts/api.js";
|
||||||
|
import { startComfyUIRestart } from "./o1keyRestart.js";
|
||||||
|
|
||||||
const BUTTON_ID = "o1key-update-button";
|
const BUTTON_ID = "o1key-update-button";
|
||||||
const DETECT_BUTTON_ID = "o1key-detect-button";
|
|
||||||
const DETECT_NOTICE_ID = "o1key-detect-notice";
|
|
||||||
const DIALOG_ID = "o1key-update-dialog";
|
const DIALOG_ID = "o1key-update-dialog";
|
||||||
const STYLE_ID = "o1key-update-styles";
|
const STYLE_ID = "o1key-update-styles";
|
||||||
let mountObserver = null;
|
let mountObserver = null;
|
||||||
let detectNoticeTimer = null;
|
|
||||||
|
|
||||||
const UPDATE_ERRORS = {
|
const UPDATE_ERRORS = {
|
||||||
local_changes: ["当前安装包含自定义修改,暂时无法自动更新。", "请联系技术支持处理,以免丢失现有内容。"],
|
local_changes: ["当前安装包含自定义修改,暂时无法自动更新。", "请联系技术支持处理,以免丢失现有内容。"],
|
||||||
@@ -207,26 +205,6 @@ function ensureStyles() {
|
|||||||
}
|
}
|
||||||
#${DIALOG_ID} button:disabled { cursor: default; opacity: .55; }
|
#${DIALOG_ID} button:disabled { cursor: default; opacity: .55; }
|
||||||
#${DIALOG_ID} button:disabled:hover { transform: none; }
|
#${DIALOG_ID} button:disabled:hover { transform: none; }
|
||||||
#${DETECT_NOTICE_ID} {
|
|
||||||
position: fixed;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 20px;
|
|
||||||
z-index: 100000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid rgba(96, 165, 250, .45);
|
|
||||||
border-radius: 9px;
|
|
||||||
color: var(--p-primary-color, #60a5fa);
|
|
||||||
background: var(--comfy-menu-bg, #202020);
|
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, .32);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
#${DETECT_NOTICE_ID}[hidden] { display: none; }
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
#${DIALOG_ID} .o1key-update-header { padding: 18px; }
|
#${DIALOG_ID} .o1key-update-header { padding: 18px; }
|
||||||
#${DIALOG_ID} .o1key-update-body { padding: 18px; }
|
#${DIALOG_ID} .o1key-update-body { padding: 18px; }
|
||||||
@@ -328,15 +306,17 @@ function createUpdateDialog() {
|
|||||||
|
|
||||||
function setState(state) {
|
function setState(state) {
|
||||||
dialog.dataset.state = state;
|
dialog.dataset.state = state;
|
||||||
const busy = state === "checking" || state === "updating";
|
const busy = state === "checking" || state === "updating" || state === "restarting";
|
||||||
primary.disabled = busy;
|
primary.disabled = busy;
|
||||||
close.disabled = state === "updating";
|
close.disabled = state === "updating" || state === "restarting";
|
||||||
secondary.disabled = state === "updating";
|
secondary.disabled = state === "updating" || state === "restarting";
|
||||||
primary.textContent = {
|
primary.textContent = {
|
||||||
checking: "检查中…",
|
checking: "检查中…",
|
||||||
available: "立即更新",
|
available: "立即更新",
|
||||||
updating: "更新中…",
|
updating: "更新中…",
|
||||||
|
restarting: "重启中…",
|
||||||
success: "完成",
|
success: "完成",
|
||||||
|
restart_failed: "关闭",
|
||||||
}[state] || "重新检查";
|
}[state] || "重新检查";
|
||||||
secondary.textContent = state === "available" ? "稍后再说" : "关闭";
|
secondary.textContent = state === "available" ? "稍后再说" : "关闭";
|
||||||
}
|
}
|
||||||
@@ -414,14 +394,19 @@ function createUpdateDialog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
|
if (!result.requirements_changed) {
|
||||||
|
setState("restarting");
|
||||||
|
show("更新完成,正在重启 ComfyUI…", "重启完成后页面会自动刷新,请稍候。", "busy");
|
||||||
|
try {
|
||||||
|
await startComfyUIRestart();
|
||||||
|
} catch {
|
||||||
|
setState("restart_failed");
|
||||||
|
show("更新完成,但自动重启失败。", "请点击左侧“重启”按钮;如果问题持续,请联系技术支持。", "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState("success");
|
setState("success");
|
||||||
show(
|
show("更新完成。", "新版本还需要维护人员完成配置,请联系技术支持后重启 ComfyUI。", "success");
|
||||||
"更新完成。",
|
|
||||||
result.requirements_changed
|
|
||||||
? "新版本还需要维护人员完成配置,请联系技术支持后重启 ComfyUI。"
|
|
||||||
: "请重启 ComfyUI,并刷新页面以使用新版本。",
|
|
||||||
"success",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setState("current");
|
setState("current");
|
||||||
show("已是最新版本。", "无需更新。", "success");
|
show("已是最新版本。", "无需更新。", "success");
|
||||||
@@ -433,11 +418,11 @@ function createUpdateDialog() {
|
|||||||
}
|
}
|
||||||
primary.addEventListener("click", () => {
|
primary.addEventListener("click", () => {
|
||||||
if (dialog.dataset.state === "available") return startUpdate();
|
if (dialog.dataset.state === "available") return startUpdate();
|
||||||
if (dialog.dataset.state === "success") return dialog.close();
|
if (dialog.dataset.state === "success" || dialog.dataset.state === "restart_failed") return dialog.close();
|
||||||
return checkForUpdate();
|
return checkForUpdate();
|
||||||
});
|
});
|
||||||
dialog.addEventListener("cancel", (event) => {
|
dialog.addEventListener("cancel", (event) => {
|
||||||
if (dialog.dataset.state === "updating") event.preventDefault();
|
if (dialog.dataset.state === "updating" || dialog.dataset.state === "restarting") event.preventDefault();
|
||||||
});
|
});
|
||||||
dialog.checkForUpdate = checkForUpdate;
|
dialog.checkForUpdate = checkForUpdate;
|
||||||
|
|
||||||
@@ -458,29 +443,6 @@ function openUpdateDialog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDetectNotice() {
|
|
||||||
ensureStyles();
|
|
||||||
let notice = document.querySelector(`#${DETECT_NOTICE_ID}`);
|
|
||||||
if (!notice) {
|
|
||||||
notice = document.createElement("div");
|
|
||||||
notice.id = DETECT_NOTICE_ID;
|
|
||||||
notice.setAttribute("role", "status");
|
|
||||||
const icon = document.createElement("span");
|
|
||||||
icon.className = "pi pi-check-circle";
|
|
||||||
icon.setAttribute("aria-hidden", "true");
|
|
||||||
const label = document.createElement("span");
|
|
||||||
label.textContent = "检测按钮已加载";
|
|
||||||
notice.append(icon, label);
|
|
||||||
document.body.appendChild(notice);
|
|
||||||
}
|
|
||||||
notice.hidden = false;
|
|
||||||
if (detectNoticeTimer !== null) window.clearTimeout(detectNoticeTimer);
|
|
||||||
detectNoticeTimer = window.setTimeout(() => {
|
|
||||||
notice.hidden = true;
|
|
||||||
detectNoticeTimer = null;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountUpdateButton() {
|
function mountUpdateButton() {
|
||||||
const tokenButton = document.querySelector("#o1key-token-manager-button");
|
const tokenButton = document.querySelector("#o1key-token-manager-button");
|
||||||
if (!tokenButton?.parentElement) return false;
|
if (!tokenButton?.parentElement) return false;
|
||||||
@@ -506,26 +468,6 @@ function mountUpdateButton() {
|
|||||||
|
|
||||||
if (tokenButton.nextElementSibling !== updateButton) tokenButton.after(updateButton);
|
if (tokenButton.nextElementSibling !== updateButton) tokenButton.after(updateButton);
|
||||||
|
|
||||||
let detectButton = document.querySelector(`#${DETECT_BUTTON_ID}`);
|
|
||||||
if (!detectButton) {
|
|
||||||
detectButton = tokenButton.cloneNode(true);
|
|
||||||
detectButton.id = DETECT_BUTTON_ID;
|
|
||||||
detectButton.type = "button";
|
|
||||||
detectButton.classList.remove("side-bar-button-selected", "o1key-token-manager-button", "o1key-restart-button");
|
|
||||||
detectButton.classList.add("o1key-detect-button");
|
|
||||||
detectButton.setAttribute("aria-label", "检测");
|
|
||||||
detectButton.setAttribute("title", "检测按钮");
|
|
||||||
detectButton.removeAttribute("aria-pressed");
|
|
||||||
|
|
||||||
const icon = detectButton.querySelector(".side-bar-button-icon");
|
|
||||||
if (icon) icon.className = "pi pi-check-circle side-bar-button-icon";
|
|
||||||
const label = detectButton.querySelector(".side-bar-button-label");
|
|
||||||
if (label) label.textContent = "检测";
|
|
||||||
|
|
||||||
detectButton.addEventListener("click", showDetectNotice);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateButton.nextElementSibling !== detectButton) updateButton.after(detectButton);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user