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: ["当前安装包含自定义修改,暂时无法自动更新。", "请联系技术支持处理,以免丢失现有内容。"], not_git: ["当前安装方式暂不支持在线更新。", "请联系技术支持。"], git_missing: ["当前环境暂不支持在线更新。", "请联系技术支持。"], wrong_branch: ["当前版本暂时无法自动更新。", "请联系技术支持。"], diverged: ["当前版本暂时无法自动更新。", "请联系技术支持。"], merge_blocked: ["更新被现有文件阻止。", "现有文件已保留,请联系技术支持。"], fetch_failed: ["暂时无法检查新版本。", "请检查网络连接,稍后重试。"], timeout: ["检查新版本超时。", "请检查网络连接,稍后重试。"], update_in_progress: ["更新正在进行中。", "请稍候再试。"], }; function ensureStyles() { if (document.querySelector(`#${STYLE_ID}`)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` #${DIALOG_ID} { box-sizing: border-box; width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 32px); padding: 0; overflow: auto; border: 1px solid var(--border-color, #454545); border-radius: 12px; color: var(--fg-color, #eee); background: var(--comfy-menu-bg, #202020); box-shadow: 0 24px 72px rgba(0, 0, 0, .48); font-family: inherit; } #${DIALOG_ID}::backdrop { background: rgba(0, 0, 0, .62); } #${DIALOG_ID} .o1key-update-header { display: flex; align-items: center; gap: 13px; padding: 21px 24px; border-bottom: 1px solid var(--border-color, #3b3b3b); } #${DIALOG_ID} .o1key-update-brand { display: grid; place-items: center; flex: none; width: 38px; height: 38px; border: 1px solid rgba(96, 165, 250, .35); border-radius: 10px; color: var(--p-primary-color, #60a5fa); background: rgba(96, 165, 250, .12); font-size: 17px; } #${DIALOG_ID} .o1key-update-heading { flex: 1; min-width: 0; } #${DIALOG_ID} .o1key-update-eyebrow { margin: 0 0 3px; color: var(--p-primary-color, #60a5fa); font-size: 10px; font-weight: 700; letter-spacing: .12em; } #${DIALOG_ID} h2 { margin: 0; color: var(--p-primary-color, #60a5fa); font-size: 17px; font-weight: 700; line-height: 1.35; } #${DIALOG_ID} button { font: inherit; } #${DIALOG_ID} .o1key-update-close { display: grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 7px; color: var(--fg-color, #ccc); background: transparent; cursor: pointer; opacity: .72; font-size: 22px; line-height: 1; } #${DIALOG_ID} .o1key-update-close:hover { background: rgba(127, 127, 127, .16); opacity: 1; } #${DIALOG_ID} .o1key-update-body { padding: 22px 24px 24px; } #${DIALOG_ID} .o1key-update-description { margin: 0 0 17px; color: var(--fg-color, #ddd); font-size: 13px; line-height: 1.6; opacity: .75; } #${DIALOG_ID} .o1key-update-result { display: flex; gap: 13px; min-height: 72px; padding: 15px; border: 1px solid var(--border-color, #454545); border-radius: 9px; background: var(--comfy-input-bg, #292929); } #${DIALOG_ID} .o1key-update-result[data-kind="error"] { border-color: rgba(239, 119, 119, .42); background: rgba(239, 119, 119, .08); } #${DIALOG_ID} .o1key-update-result[data-kind="success"], #${DIALOG_ID} .o1key-update-result[data-kind="available"] { border-color: rgba(96, 165, 250, .4); background: rgba(96, 165, 250, .08); } #${DIALOG_ID} .o1key-update-state-icon { display: grid; place-items: center; box-sizing: border-box; flex: 0 0 18px; width: 18px; height: 18px; margin-top: 2px; color: var(--fg-color, #bbb); font-size: 17px; opacity: .72; } #${DIALOG_ID} [data-kind="error"] .o1key-update-state-icon { color: #ef7777; opacity: 1; } #${DIALOG_ID} [data-kind="success"] .o1key-update-state-icon, #${DIALOG_ID} [data-kind="available"] .o1key-update-state-icon { color: var(--p-primary-color, #60a5fa); opacity: 1; } #${DIALOG_ID} .o1key-update-state-icon.is-loading { opacity: 1; } #${DIALOG_ID} .o1key-update-state-icon.is-loading::before { content: ""; box-sizing: border-box; width: 16px; height: 16px; border: 2px solid rgba(96, 165, 250, .25); border-top-color: var(--p-primary-color, #60a5fa); border-radius: 50%; animation: o1key-update-spin .8s linear infinite; } @keyframes o1key-update-spin { to { transform: rotate(360deg); } } #${DIALOG_ID} .o1key-update-result-copy { min-width: 0; } #${DIALOG_ID} .o1key-update-status { margin: 0; font-size: 14px; font-weight: 650; line-height: 1.5; } #${DIALOG_ID} [data-kind="busy"] .o1key-update-status, #${DIALOG_ID} [data-kind="success"] .o1key-update-status, #${DIALOG_ID} [data-kind="available"] .o1key-update-status { color: var(--p-primary-color, #60a5fa); } #${DIALOG_ID} .o1key-update-suggestion { margin: 5px 0 0; font-size: 12px; line-height: 1.55; opacity: .72; } #${DIALOG_ID} .o1key-update-actions { display: flex; justify-content: flex-end; gap: 9px; padding: 16px 24px 20px; border-top: 1px solid var(--border-color, #3b3b3b); } #${DIALOG_ID} .o1key-update-actions button { min-width: 96px; min-height: 36px; padding: 8px 16px; border-radius: 7px; font-size: 13px; font-weight: 600; cursor: pointer; transition: background .15s, border-color .15s, transform .15s; } #${DIALOG_ID} .o1key-update-secondary { border: 1px solid var(--border-color, #555); color: var(--p-primary-color, #60a5fa); background: var(--comfy-input-bg, #292929); } #${DIALOG_ID} .o1key-update-secondary:hover { border-color: var(--p-primary-color, #60a5fa); background: rgba(96, 165, 250, .1); } #${DIALOG_ID} .o1key-update-primary { border: 1px solid var(--p-primary-500, #3b82f6); color: #fff; background: var(--p-primary-500, #3b82f6); } #${DIALOG_ID} .o1key-update-primary:hover { border-color: var(--p-primary-600, #2563eb); background: var(--p-primary-600, #2563eb); } #${DIALOG_ID} button:focus-visible { outline: 2px solid var(--p-primary-color, #60a5fa); outline-offset: 2px; } #${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; } #${DIALOG_ID} .o1key-update-actions { padding: 14px 18px 18px; } #${DIALOG_ID} .o1key-update-actions button { flex: 1; } } `; document.head.appendChild(style); } function createUpdateDialog() { const dialog = document.createElement("dialog"); dialog.id = DIALOG_ID; dialog.setAttribute("aria-labelledby", "o1key-update-title"); const root = document.createElement("div"); const header = document.createElement("div"); header.className = "o1key-update-header"; const brand = document.createElement("span"); brand.className = "pi pi-download o1key-update-brand"; brand.setAttribute("aria-hidden", "true"); const heading = document.createElement("div"); heading.className = "o1key-update-heading"; const eyebrow = document.createElement("p"); eyebrow.className = "o1key-update-eyebrow"; eyebrow.textContent = "O1KEY"; const title = document.createElement("h2"); title.id = "o1key-update-title"; title.textContent = "更新 O1Key 节点包"; heading.append(eyebrow, title); const close = document.createElement("button"); close.type = "button"; close.className = "o1key-update-close"; close.textContent = "×"; close.setAttribute("aria-label", "关闭"); close.addEventListener("click", () => dialog.close()); header.append(brand, heading, close); const body = document.createElement("div"); body.className = "o1key-update-body"; const description = document.createElement("p"); description.className = "o1key-update-description"; description.textContent = "检查是否有新版本可用。"; const resultCard = document.createElement("div"); resultCard.className = "o1key-update-result"; const statusIcon = document.createElement("span"); statusIcon.className = "pi pi-info-circle o1key-update-state-icon"; statusIcon.setAttribute("aria-hidden", "true"); const resultCopy = document.createElement("div"); resultCopy.className = "o1key-update-result-copy"; const status = document.createElement("p"); status.className = "o1key-update-status"; status.setAttribute("role", "status"); status.setAttribute("aria-live", "polite"); const suggestion = document.createElement("p"); suggestion.className = "o1key-update-suggestion"; resultCopy.append(status, suggestion); resultCard.append(statusIcon, resultCopy); body.append(description, resultCard); const actions = document.createElement("div"); actions.className = "o1key-update-actions"; const secondary = document.createElement("button"); secondary.type = "button"; secondary.className = "o1key-update-secondary"; secondary.textContent = "关闭"; secondary.addEventListener("click", () => dialog.close()); const primary = document.createElement("button"); primary.type = "button"; primary.className = "o1key-update-primary"; actions.append(secondary, primary); function show(message, nextStep = "", kind = "info") { status.textContent = message; suggestion.textContent = nextStep; resultCard.dataset.kind = kind; statusIcon.className = kind === "busy" ? "o1key-update-state-icon is-loading" : `pi ${{ error: "pi-exclamation-circle", success: "pi-check-circle", available: "pi-arrow-circle-down", }[kind] || "pi-info-circle"} o1key-update-state-icon`; } function setState(state) { dialog.dataset.state = state; const busy = state === "checking" || state === "updating"; primary.disabled = busy; close.disabled = state === "updating"; secondary.disabled = state === "updating"; primary.textContent = { checking: "检查中…", available: "立即更新", updating: "更新中…", success: "完成", }[state] || "重新检查"; secondary.textContent = state === "available" ? "稍后再说" : "关闭"; } function showError(response, result) { setState("error"); if (response.status === 404) { show("更新功能尚未就绪。", "请重启 ComfyUI 并刷新页面后重试。", "error"); return; } const [message, nextStep] = UPDATE_ERRORS[result?.code] || ["暂时无法完成更新。", "请稍后重试;如果问题持续,请联系技术支持。"]; show(message, nextStep, "error"); } async function checkForUpdate() { if (dialog.dataset.state === "checking" || dialog.dataset.state === "updating") return; setState("checking"); show("正在检查新版本…", "请稍候。", "busy"); try { const checkResponse = await api.fetchApi("/o1key/update/check", { cache: "no-store" }); if (checkResponse.status === 404) return showError(checkResponse); let checkResult; try { checkResult = await checkResponse.json(); } catch { setState("error"); show("暂时无法读取检查结果。", "请刷新页面后重试。", "error"); return; } if (!checkResponse.ok) { showError(checkResponse, checkResult); return; } if (typeof checkResult.update_available !== "boolean") { setState("error"); show("暂时无法读取检查结果。", "请刷新页面后重试。", "error"); return; } if (!checkResult.update_available) { setState("current"); show("已是最新版本。", "无需更新。", "success"); return; } setState("available"); show("发现新版本。", "现在更新即可使用最新功能。", "available"); } catch { setState("error"); show("暂时无法完成操作。", "请确认 ComfyUI 正在运行,然后重试。", "error"); } } async function startUpdate() { if (dialog.dataset.state !== "available") return; setState("updating"); show("正在更新…", "请保持 ComfyUI 运行,等待完成。", "busy"); try { const response = await api.fetchApi("/o1key/update", { method: "POST", headers: { "X-O1Key-Update": "1" }, }); if (response.status === 404) return showError(response); let result; try { result = await response.json(); } catch { setState("error"); show("暂时无法读取更新结果。", "请刷新页面后重试。", "error"); return; } if (!response.ok) return showError(response, result); if (typeof result.updated !== "boolean") { setState("error"); show("暂时无法读取更新结果。", "请刷新页面后重试。", "error"); return; } if (result.updated) { setState("success"); show( "更新完成。", result.requirements_changed ? "新版本还需要维护人员完成配置,请联系技术支持后重启 ComfyUI。" : "请重启 ComfyUI,并刷新页面以使用新版本。", "success", ); } else { setState("current"); show("已是最新版本。", "无需更新。", "success"); } } catch { setState("error"); show("暂时无法完成操作。", "请确认 ComfyUI 正在运行,然后重试。", "error"); } } primary.addEventListener("click", () => { if (dialog.dataset.state === "available") return startUpdate(); if (dialog.dataset.state === "success") return dialog.close(); return checkForUpdate(); }); dialog.addEventListener("cancel", (event) => { if (dialog.dataset.state === "updating") event.preventDefault(); }); dialog.checkForUpdate = checkForUpdate; root.append(header, body, actions); dialog.append(root); document.body.appendChild(dialog); setState("idle"); show("准备检查更新。", "", "info"); return dialog; } function openUpdateDialog() { ensureStyles(); const dialog = document.querySelector(`#${DIALOG_ID}`) || createUpdateDialog(); if (!dialog.open) { dialog.showModal(); void dialog.checkForUpdate(); } } 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; let updateButton = document.querySelector(`#${BUTTON_ID}`); if (!updateButton) { updateButton = tokenButton.cloneNode(true); updateButton.id = BUTTON_ID; updateButton.type = "button"; updateButton.classList.remove("side-bar-button-selected", "o1key-token-manager-button", "o1key-restart-button"); updateButton.classList.add("o1key-update-button"); updateButton.setAttribute("aria-label", "更新"); updateButton.setAttribute("title", "更新 O1Key 节点包"); updateButton.removeAttribute("aria-pressed"); const icon = updateButton.querySelector(".side-bar-button-icon"); if (icon) icon.className = "pi pi-download side-bar-button-icon"; const label = updateButton.querySelector(".side-bar-button-label"); if (label) label.textContent = "更新"; updateButton.addEventListener("click", openUpdateDialog); } 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; } app.registerExtension({ name: "o1key.updateButton", setup() { if (mountUpdateButton() || mountObserver) return; mountObserver = new MutationObserver(() => { if (!mountUpdateButton()) return; mountObserver.disconnect(); mountObserver = null; }); mountObserver.observe(document.body, { childList: true, subtree: true }); }, });