Files
comfyui_o1key/web/js/o1keyUpdateButton.js
T

477 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
const BUTTON_ID = "o1key-update-button";
const DIALOG_ID = "o1key-update-dialog";
const STYLE_ID = "o1key-update-styles";
let mountObserver = 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; }
@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 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);
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 });
},
});