320 lines
12 KiB
JavaScript
320 lines
12 KiB
JavaScript
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();
|
||
},
|
||
});
|