Files
comfyui_o1key/web/js/o1keyApiSettings.js

320 lines
12 KiB
JavaScript
Raw Permalink 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 { startComfyUIRestart } from "./o1keyRestart.js";
const DIALOG_ID = "o1key-api-settings-dialog";
const STYLE_ID = "o1key-api-settings-styles";
function ensureStyles() {
if (document.querySelector(`#${STYLE_ID}`)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${DIALOG_ID} {
width: min(520px, calc(100vw - 32px));
padding: 0;
border: 1px solid var(--border-color, #444);
border-radius: 12px;
color: var(--fg-color, #eee);
background: var(--comfy-menu-bg, #202020);
box-shadow: 0 20px 60px rgba(0, 0, 0, .45);
}
#${DIALOG_ID}::backdrop { background: rgba(0, 0, 0, .58); }
#${DIALOG_ID} .o1key-api-form { padding: 22px; }
#${DIALOG_ID} .o1key-api-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
#${DIALOG_ID} h2 { margin: 0; font-size: 18px; }
#${DIALOG_ID} .o1key-close {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
color: inherit;
background: transparent;
cursor: pointer;
}
#${DIALOG_ID} .o1key-close:hover { background: rgba(127, 127, 127, .18); }
#${DIALOG_ID} label {
display: block;
margin: 14px 0 6px;
font-size: 13px;
color: var(--fg-color, #ddd);
}
#${DIALOG_ID} input,
#${DIALOG_ID} select {
box-sizing: border-box;
width: 100%;
min-height: 38px;
padding: 8px 10px;
border: 1px solid var(--border-color, #555);
border-radius: 7px;
color: inherit;
background: var(--comfy-input-bg, #111);
}
#${DIALOG_ID} .o1key-hint,
#${DIALOG_ID} .o1key-status { margin-top: 7px; font-size: 12px; color: #aaa; }
#${DIALOG_ID} .o1key-status[data-kind="success"] { color: #55c98b; }
#${DIALOG_ID} .o1key-status[data-kind="error"] { color: #ef7777; }
#${DIALOG_ID} .o1key-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
#${DIALOG_ID} .o1key-actions button {
min-height: 34px;
padding: 7px 14px;
border: 1px solid var(--border-color, #555);
border-radius: 7px;
color: var(--fg-color, #eee);
background: var(--comfy-input-bg, #292929);
cursor: pointer;
}
#${DIALOG_ID} .o1key-actions button:hover { filter: brightness(1.12); }
#${DIALOG_ID} .o1key-primary { border-color: #2f8d64; background: #287653; }
#${DIALOG_ID} .o1key-danger { margin-right: auto; border-color: #8b4444; background: #703838; }
#${DIALOG_ID} button:disabled { cursor: wait; opacity: .6; }
`;
document.head.appendChild(style);
}
function setStatus(dialog, message, kind = "info") {
const status = dialog.querySelector(".o1key-status");
status.textContent = message;
status.dataset.kind = kind;
}
async function readJson(response) {
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`);
return data;
}
async function restartComfyUI() {
if (!window.confirm("确定要重启 ComfyUI 吗?当前正在执行的任务会被中断。")) return;
try {
await startComfyUIRestart();
} catch (error) {
window.alert(error?.message || "ComfyUI 重启失败,请查看终端中的错误信息。");
}
}
function createDialog() {
const dialog = document.createElement("dialog");
dialog.id = DIALOG_ID;
dialog.innerHTML = `
<form class="o1key-api-form">
<div class="o1key-api-header">
<h2>令牌管理</h2>
<button class="o1key-close" type="button" aria-label="关闭" title="关闭">✕</button>
</div>
<label for="o1key-api-key-input">API Key</label>
<input id="o1key-api-key-input" type="password" autocomplete="new-password"
placeholder="留空则保留当前 API Key" spellcheck="false" />
<div class="o1key-hint o1key-key-hint">正在读取当前配置…</div>
<label for="o1key-network-route-select">网络线路</label>
<select id="o1key-network-route-select"></select>
<div class="o1key-hint">保存后,所有 O1Key 节点从下一次执行开始使用新配置,无需重启。</div>
<div class="o1key-status" role="status" aria-live="polite"></div>
<div class="o1key-actions">
<button class="o1key-danger" type="button" data-action="clear">清除 Key</button>
<button type="button" data-action="test">测试连接</button>
<button class="o1key-primary" type="button" data-action="save">保存并立即生效</button>
</div>
</form>
`;
dialog.querySelector("form").addEventListener("submit", (event) => event.preventDefault());
dialog.querySelector(".o1key-close").addEventListener("click", () => dialog.close());
dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close();
});
const runAction = async (button, action) => {
const buttons = dialog.querySelectorAll("button[data-action]");
buttons.forEach((item) => { item.disabled = true; });
try {
await action();
} catch (error) {
setStatus(dialog, error.message || "操作失败", "error");
} finally {
buttons.forEach((item) => { item.disabled = false; });
button.focus();
}
};
dialog.querySelector('[data-action="save"]').addEventListener("click", (event) => {
runAction(event.currentTarget, async () => {
setStatus(dialog, "正在保存…");
const apiKey = dialog.querySelector("#o1key-api-key-input").value.trim();
const networkRoute = dialog.querySelector("#o1key-network-route-select").value;
const data = await readJson(await fetch("/o1key/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey || undefined, network_route: networkRoute }),
}));
dialog.querySelector("#o1key-api-key-input").value = "";
dialog.querySelector(".o1key-key-hint").textContent = data.has_key
? `当前 Key${data.masked}`
: "当前未配置 API Key";
setStatus(dialog, "已保存,下一次执行立即使用新配置。", "success");
});
});
dialog.querySelector('[data-action="test"]').addEventListener("click", (event) => {
runAction(event.currentTarget, async () => {
setStatus(dialog, "正在测试连接…");
const apiKey = dialog.querySelector("#o1key-api-key-input").value.trim();
const networkRoute = dialog.querySelector("#o1key-network-route-select").value;
const data = await readJson(await fetch("/o1key/test_key", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey || undefined, network_route: networkRoute }),
}));
if (!data.valid) throw new Error(data.error || "连接测试失败");
setStatus(dialog, "连接正常。测试不会自动保存,请按“保存并立即生效”。", "success");
});
});
dialog.querySelector('[data-action="clear"]').addEventListener("click", (event) => {
if (!window.confirm("确定清除已保存的 O1Key API Key 吗?")) return;
runAction(event.currentTarget, async () => {
await readJson(await fetch("/o1key/api_key", { method: "DELETE" }));
dialog.querySelector("#o1key-api-key-input").value = "";
dialog.querySelector(".o1key-key-hint").textContent = "当前未配置 API Key";
setStatus(dialog, "API Key 已清除。", "success");
});
});
document.body.appendChild(dialog);
return dialog;
}
async function refreshDialog(dialog) {
setStatus(dialog, "正在读取配置…");
const data = await readJson(await fetch("/o1key/api_key", { cache: "no-store" }));
const select = dialog.querySelector("#o1key-network-route-select");
select.replaceChildren();
for (const route of data.network_route_options || []) {
const option = document.createElement("option");
option.value = route;
option.textContent = route;
select.appendChild(option);
}
select.value = data.network_route || "全球加速";
dialog.querySelector(".o1key-key-hint").textContent = data.has_key
? `当前 Key${data.masked}`
: "当前未配置 API Key";
setStatus(dialog, "");
}
async function openApiSettings() {
ensureStyles();
const dialog = document.querySelector(`#${DIALOG_ID}`) || createDialog();
if (!dialog.open) dialog.showModal();
try {
await refreshDialog(dialog);
} catch (error) {
setStatus(dialog, error.message || "读取配置失败", "error");
}
}
function createSidebarButton(sourceButton, { id, className, iconClass, label, onClick }) {
const button = sourceButton.cloneNode(true);
button.id = id;
button.type = "button";
button.classList.remove("side-bar-button-selected", "o1key-token-manager-button", "o1key-restart-button");
button.classList.add(className);
button.setAttribute("aria-label", label);
button.setAttribute("title", label);
button.removeAttribute("aria-pressed");
const icon = button.querySelector(".side-bar-button-icon");
if (icon) icon.className = `${iconClass} side-bar-button-icon`;
const buttonLabel = button.querySelector(".side-bar-button-label");
if (buttonLabel) buttonLabel.textContent = label;
button.addEventListener("click", onClick);
return button;
}
function mountSidebarButtons() {
let tokenButton = document.querySelector("#o1key-token-manager-button");
const consoleIcon = document.querySelector(
'.side-tool-bar-container [class~="icon-[ph--terminal-bold]"]'
);
const consoleButton = consoleIcon?.closest("button");
if (!consoleButton) return false;
if (!tokenButton) {
tokenButton = createSidebarButton(consoleButton, {
id: "o1key-token-manager-button",
className: "o1key-token-manager-button",
iconClass: "pi pi-key",
label: "令牌管理",
onClick: openApiSettings,
});
consoleButton.before(tokenButton);
}
if (!document.querySelector("#o1key-restart-button")) {
const restartButton = createSidebarButton(tokenButton, {
id: "o1key-restart-button",
className: "o1key-restart-button",
iconClass: "pi pi-refresh",
label: "重启",
onClick: restartComfyUI,
});
tokenButton.after(restartButton);
}
return true;
}
function mountSidebarButtonsWhenReady() {
if (mountSidebarButtons()) return;
// ComfyUI mounts the Vue sidebar after extension setup on some versions.
// Observe only during that initial mount and disconnect immediately once
// the button has been inserted. This is event-driven, not periodic polling.
const observer = new MutationObserver(() => {
if (mountSidebarButtons()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
app.registerExtension({
name: "o1key.apiSettings",
commands: [
{
id: "o1key.OpenApiSettings",
icon: "pi pi-key",
label: "令牌管理",
function: openApiSettings,
},
],
setup() {
ensureStyles();
// 重启完成后的地址参数只用于绕过浏览器缓存,进入新页面后立即清理。
const pageUrl = new URL(window.location.href);
if (pageUrl.searchParams.has("o1key_restart")) {
pageUrl.searchParams.delete("o1key_restart");
window.history.replaceState(window.history.state, "", pageUrl.toString());
}
mountSidebarButtonsWhenReady();
},
});