From 96010c058ff012c4330a92ba1693520926c708dd Mon Sep 17 00:00:00 2001 From: Jony <951565127@qq.com> Date: Thu, 24 Sep 2026 22:10:42 +0800 Subject: [PATCH] Add update verification button to sidebar --- README.md | 1 + docs/architecture.md | 2 +- tests/test_o1key_update_button.mjs | 26 ++++++++++-- web/js/o1keyUpdateButton.js | 67 ++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a803420..5528ff8 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ O1KEY_ASYNC_API_BASE_URL=https://your-async-api-domain.com 1. 点击 ComfyUI 左侧工具栏中“令牌管理”下方的“更新”按钮。 2. 等待版本检查。如果发现新版本,确认后等待更新完成;需要时可在面板中重新检查。 3. 按完成提示重启 ComfyUI。如果面板提示需要技术支持,请联系维护人员。 +4. 刷新页面后,左侧“更新”下方会出现“检测”按钮,可用来确认本次版本已加载。 > 如果当前安装包含自定义修改,面板会停止更新并保留现有内容。此时请联系维护人员处理。 diff --git a/docs/architecture.md b/docs/architecture.md index 5d4dbf0..bc6ab8e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -109,7 +109,7 @@ 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. -`web/js/o1keyUpdateButton.js` places an Update button directly below the Token Manager button in the left toolbar, before Restart. Opening its 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, 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. 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. diff --git a/tests/test_o1key_update_button.mjs b/tests/test_o1key_update_button.mjs index 35cce4d..78dc154 100644 --- a/tests/test_o1key_update_button.mjs +++ b/tests/test_o1key_update_button.mjs @@ -96,6 +96,7 @@ let observer; const requests = []; const responses = []; let fetchFailure = false; +let dismissNotice; const reply = (body, status = 200) => ({ ok: status < 400, status, async json() { return body; } }); const app = { registerExtension(value) { extension = value; } }; const api = { @@ -110,7 +111,11 @@ class MutationObserver { observe() {} disconnect() { this.disconnected = true; } } -vm.runInNewContext(source, { app, api, document, window: {}, MutationObserver }); +const window = { + setTimeout(callback) { dismissNotice = callback; return 1; }, + clearTimeout() { dismissNotice = null; }, +}; +vm.runInNewContext(source, { app, api, document, window, MutationObserver }); extension.setup(); assert.ok(observer); @@ -130,14 +135,29 @@ toolbar.append(token, restart); observer.callback(); const update = document.querySelector("#o1key-update-button"); +const detect = document.querySelector("#o1key-detect-button"); assert.equal(toolbar.children[0], token); assert.equal(toolbar.children[1], update); -assert.equal(toolbar.children[2], restart); +assert.equal(toolbar.children[2], detect); +assert.equal(toolbar.children[3], restart); assert.equal(update.querySelector(".side-bar-button-label").textContent, "更新"); assert.match(update.querySelector(".side-bar-button-icon").className, /pi-download/); +assert.equal(detect.querySelector(".side-bar-button-label").textContent, "检测"); +assert.match(detect.querySelector(".side-bar-button-icon").className, /pi-check-circle/); assert.ok(observer.disconnected); extension.setup(); -assert.equal(toolbar.children.length, 3); +assert.equal(toolbar.children.length, 4); + +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 })); update.listeners.get("click")(); diff --git a/web/js/o1keyUpdateButton.js b/web/js/o1keyUpdateButton.js index 5732846..16cbad0 100644 --- a/web/js/o1keyUpdateButton.js +++ b/web/js/o1keyUpdateButton.js @@ -2,9 +2,12 @@ import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; 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 STYLE_ID = "o1key-update-styles"; let mountObserver = null; +let detectNoticeTimer = null; const UPDATE_ERRORS = { local_changes: ["当前安装包含自定义修改,暂时无法自动更新。", "请联系技术支持处理,以免丢失现有内容。"], @@ -204,6 +207,26 @@ function ensureStyles() { } #${DIALOG_ID} button:disabled { cursor: default; opacity: .55; } #${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) { #${DIALOG_ID} .o1key-update-header { padding: 18px; } #${DIALOG_ID} .o1key-update-body { padding: 18px; } @@ -435,6 +458,29 @@ 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() { const tokenButton = document.querySelector("#o1key-token-manager-button"); if (!tokenButton?.parentElement) return false; @@ -459,6 +505,27 @@ function mountUpdateButton() { } 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; }