Files
dsh-llm-workbuddy/client.js
T
dsh-custom a44227b69a feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片
基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。
详见 README.CUSTOM.md。
2026-10-10 13:59:09 +08:00

1428 lines
64 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.
window.__ModuleLoader__.load({
id: "@axiaohungry/dsh-llm-workbuddy",
factory: (require) => {
const ROUTE = "/dsh-llm-workbuddy/auth";
const MARKER = "data-workbuddy-auth-switch";
const AUTH_STATE_EVENT = "dsh-llm-workbuddy:auth-state";
const PENDING_MARKER = "data-workbuddy-new-session-selector";
const WORKBUDDY_PROVIDER_PATTERN = /(?:^|-)(?:work-?buddy|code-?buddy)(?:-|$)/;
const React = require("react");
const { createElement, useEffect, useRef, useState } = React;
function isWorkBuddyProvider(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized.length > 0 && WORKBUDDY_PROVIDER_PATTERN.test(normalized);
}
function isModLensWorkBuddyProvider(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return /^modlens-(?:work-?buddy|code-?buddy)(?:-|$)/.test(normalized);
}
function button(text) {
const element = document.createElement("button");
element.type = "button";
element.textContent = text;
Object.assign(element.style, {
minHeight: "44px",
padding: "0 12px",
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
borderRadius: "8px",
background: "var(--dsw-surface-subtle, transparent)",
color: "inherit",
cursor: "pointer",
whiteSpace: "nowrap",
boxSizing: "border-box",
font: "inherit",
fontSize: "13px",
fontWeight: "500",
transition: "background-color 120ms ease, border-color 120ms ease, opacity 120ms ease",
});
return element;
}
function accountPicker() {
const element = document.createElement("select");
element.setAttribute("aria-label", "WorkBuddy 令牌账号");
Object.assign(element.style, {
minHeight: "44px",
minWidth: "180px",
maxWidth: "260px",
flex: "1 1 220px",
boxSizing: "border-box",
padding: "0 10px",
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
borderRadius: "8px",
background: "var(--dsw-surface-subtle, transparent)",
color: "inherit",
font: "inherit",
fontSize: "13px",
});
return element;
}
function textInput(type, placeholder, ariaLabel) {
const element = document.createElement("input");
element.type = type;
element.placeholder = placeholder;
element.setAttribute("aria-label", ariaLabel);
element.autocomplete = type === "password" ? "new-password" : "off";
Object.assign(element.style, {
minHeight: "44px",
minWidth: "0",
flex: "1 1 200px",
boxSizing: "border-box",
padding: "0 12px",
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
borderRadius: "8px",
background: "var(--dsw-surface-subtle, transparent)",
color: "inherit",
font: "inherit",
fontSize: "13px",
});
return element;
}
function fieldLabel(text) {
const element = document.createElement("span");
element.textContent = text;
Object.assign(element.style, {
flex: "0 0 auto",
minWidth: "92px",
fontSize: "13px",
lineHeight: "20px",
color: "var(--dsw-text-secondary, #667085)",
});
return element;
}
function row() {
const element = document.createElement("div");
Object.assign(element.style, {
display: "flex",
alignItems: "center",
gap: "8px",
flexWrap: "wrap",
width: "100%",
});
return element;
}
function section() {
const element = document.createElement("div");
Object.assign(element.style, {
display: "flex",
flexDirection: "column",
gap: "8px",
width: "100%",
boxSizing: "border-box",
padding: "10px 12px",
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
borderRadius: "10px",
background: "var(--dsw-surface-subtle, rgba(0, 0, 0, 0.02))",
});
return element;
}
function setVisible(element, visible, display = "flex") {
element.hidden = !visible;
element.style.setProperty("display", visible ? display : "none", "important");
}
function isWorkBuddy(input) {
const editor = input.parentElement?.parentElement;
if (!editor) return false;
if (isWorkBuddyProvider(editor.textContent)) return true;
const provider = editor.parentElement?.querySelector('select[aria-label="提供方"]')?.value;
return isWorkBuddyProvider(provider);
}
function accountText(account) {
const label = account?.accountName || account?.label || account?.account?.displayName || account?.account?.email || account?.account?.userId || "未命名账号";
const userId = account?.userId || account?.account?.userId;
return userId && userId !== label ? `${label} · ${String(userId).slice(0, 8)}` : label;
}
function accountLabel(account) {
return account?.accountName || account?.label || account?.account?.displayName || account?.account?.email || account?.account?.userId || "未命名账号";
}
function apiKeyText(key) {
const label = key?.label || (key?.kind === "environment" ? `环境变量 ${key.ref}` : "DSH 保存的 API Key");
const suffix = key?.masked ? ` · ${key.masked}` : "";
return key?.configured === false ? `${label}(不可用)` : `${label}${suffix}`;
}
function apiKeyLabel(key) {
return key?.label || (key?.kind === "environment" ? `环境变量 ${key.ref}` : "DSH 保存的 API Key");
}
function formatCredits(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toLocaleString("zh-CN", { maximumFractionDigits: 2 }) : "—";
}
async function authRequest(path, body, sessionId) {
const options = {
method: body === undefined ? "GET" : "POST",
cache: "no-store",
};
if (body !== undefined) {
options.headers = { "content-type": "application/json" };
options.body = JSON.stringify({ ...body, ...(sessionId && body.sessionId === undefined ? { sessionId: String(sessionId) } : {}) });
}
const query = sessionId && body === undefined ? `?sessionId=${encodeURIComponent(String(sessionId))}` : "";
const response = await fetch(`${ROUTE}/${path}${query}`, options);
const text = await response.text();
let result = {};
try {
result = text ? JSON.parse(text) : {};
} catch {
result = {};
}
if (!response.ok || result.ok === false) {
const error = new Error(result.message || `请求失败(${response.status})`);
error.status = response.status;
throw error;
}
return result;
}
function notifyAuthState() {
if (typeof window !== "undefined") window.dispatchEvent(new Event(AUTH_STATE_EVENT));
if (typeof document !== "undefined") {
for (const panel of document.querySelectorAll(`[${PENDING_MARKER}]`)) {
delete panel.dataset.loaded;
mountPendingSelector();
}
}
}
function selectedProvider(selection) {
return selection?.next?.provider ?? selection?.lastUsed?.provider ?? selection?.provider;
}
function pendingCredentialOptions(status, mode) {
return mode === "token"
? (Array.isArray(status.accounts) ? status.accounts : []).map((account) => ({ id: account.id, label: accountText(account) }))
: (Array.isArray(status.apiKeys) ? status.apiKeys : []).map((key) => ({ id: key.id, label: apiKeyText(key), disabled: key.configured === false }));
}
function createPendingSelector() {
const panel = document.createElement("div");
panel.setAttribute(PENDING_MARKER, "");
panel.setAttribute("role", "group");
panel.setAttribute("aria-label", "WorkBuddy 新会话凭证");
Object.assign(panel.style, {
boxSizing: "border-box",
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
flexWrap: "wrap",
gap: "6px",
width: "100%",
maxWidth: "var(--dsh-composer-card-max-width, 720px)",
margin: "0 auto 8px",
padding: "0 8px",
color: "var(--dsw-text-tertiary, #98a2b3)",
fontSize: "12px",
lineHeight: "20px",
});
const label = document.createElement("span");
label.textContent = "新会话默认凭证";
const mode = document.createElement("select");
mode.setAttribute("aria-label", "新会话认证模式");
const credential = document.createElement("select");
credential.setAttribute("aria-label", "新会话 WorkBuddy 凭证");
for (const element of [mode, credential]) Object.assign(element.style, {
boxSizing: "border-box",
minWidth: "0",
maxWidth: "min(240px, 100%)",
height: "28px",
padding: "0 6px",
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
borderRadius: "7px",
background: "var(--dsw-surface-subtle, transparent)",
color: "inherit",
font: "inherit",
fontSize: "12px",
});
const hint = document.createElement("span");
hint.textContent = "发送时自动绑定所选凭证";
hint.style.color = "var(--dsw-text-tertiary, #98a2b3)";
panel.append(label, mode, credential, hint);
panel._workbuddyMode = mode;
panel._workbuddyCredential = credential;
panel._workbuddyHint = hint;
const apply = async () => {
if (panel.dataset.busy === "1") return;
const value = credential.value;
if (!value || credential.selectedOptions[0]?.disabled) return;
panel.dataset.busy = "1";
mode.disabled = true;
credential.disabled = true;
hint.textContent = "正在保存新会话默认凭证…";
try {
const next = mode.value === "token"
? await authRequest("token", { accountId: value })
: await authRequest("api-key", { keyId: value });
renderPendingSelector(panel, next);
notifyAuthState();
} catch (error) {
hint.textContent = error instanceof Error ? error.message : "保存新会话凭证失败";
hint.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
delete panel.dataset.busy;
mode.disabled = false;
credential.disabled = false;
}
};
mode.addEventListener("change", () => {
renderPendingOptions(panel, panel._workbuddyStatus, mode.value);
void apply();
});
credential.addEventListener("change", () => void apply());
return panel;
}
function renderPendingOptions(panel, status, preferredMode) {
if (!status) return;
const tokenOptions = pendingCredentialOptions(status, "token");
const apiOptions = pendingCredentialOptions(status, "api-key");
const tokenAvailable = tokenOptions.length > 0;
const apiAvailable = apiOptions.some((option) => !option.disabled);
if (!status.routingEnabled || (!tokenAvailable && !apiAvailable)) {
panel.hidden = true;
panel.style.setProperty("display", "none", "important");
return;
}
panel.hidden = false;
panel.style.setProperty("display", "flex", "important");
const suggestedMode = status.suggestedBinding?.mode;
const modeAvailable = (value) => value === "token" ? tokenAvailable : value === "api-key" && apiAvailable;
const mode = modeAvailable(preferredMode)
? preferredMode
: modeAvailable(suggestedMode)
? suggestedMode
: status.mode === "token" && tokenAvailable ? "token" : "api-key";
const modeSelect = panel._workbuddyMode;
const credentialSelect = panel._workbuddyCredential;
modeSelect.replaceChildren();
for (const [value, label, disabled] of [["token", "令牌", !tokenAvailable], ["api-key", "API Key", !apiAvailable]]) {
const option = document.createElement("option");
option.value = value;
option.textContent = label;
option.disabled = disabled;
modeSelect.append(option);
}
modeSelect.value = mode;
const options = mode === "token" ? tokenOptions : apiOptions;
credentialSelect.replaceChildren(...options.map((entry) => {
const option = document.createElement("option");
option.value = entry.id;
option.textContent = entry.label;
option.disabled = entry.disabled === true;
return option;
}));
const activeId = mode === "token"
? status.suggestedAccountId ?? status.activeAccountId
: status.suggestedApiKeyId ?? status.activeApiKeyId;
if (activeId && options.some((entry) => entry.id === activeId && !entry.disabled)) credentialSelect.value = activeId;
panel._workbuddyHint.textContent = "发送时自动绑定所选凭证";
panel._workbuddyHint.style.color = "var(--dsw-text-tertiary, #98a2b3)";
}
function renderPendingSelector(panel, status) {
panel._workbuddyStatus = status;
renderPendingOptions(panel, status, panel._workbuddyMode.value);
}
function mountPendingSelector() {
if (typeof document === "undefined") return;
// [自用定制 2026-10-10] 移除「新会话默认凭证」选择器:先摘掉已挂载的面板,再直接返回。
for (const panel of document.querySelectorAll(`[${PENDING_MARKER}]`)) panel.remove();
return;
const seats = Array.from(document.querySelectorAll("[data-composer-seat]"));
const seat = seats.find((candidate) => candidate.closest('[data-phase="hero"]'));
const panels = Array.from(document.querySelectorAll(`[${PENDING_MARKER}]`));
if (!seat) {
for (const panel of panels) panel.remove();
return;
}
for (const panel of panels) {
if (panel.parentElement !== seat) panel.remove();
}
const panel = seat.querySelector(`[${PENDING_MARKER}]`) ?? createPendingSelector();
if (!panel.parentElement) seat.append(panel);
if (panel.dataset.loading === "1" || panel.dataset.loaded === "1") return;
panel.dataset.loading = "1";
authRequest("status")
.then((status) => {
panel.dataset.loaded = "1";
renderPendingSelector(panel, status);
})
.catch(() => {
panel.hidden = true;
panel.style.setProperty("display", "none", "important");
})
.finally(() => {
delete panel.dataset.loading;
});
}
function WorkBuddyCreditsDock({ useProjection, sessionId }) {
let selection;
try {
selection = typeof useProjection === "function" ? useProjection("modelSelection") : undefined;
} catch {
selection = undefined;
}
const provider = selectedProvider(selection);
const selected = isWorkBuddyProvider(provider);
const modlens = isModLensWorkBuddyProvider(provider);
const [state, setState] = useState(null);
useEffect(() => {
let disposed = false;
let requestId = 0;
const load = async () => {
const currentRequestId = ++requestId;
if (!selected) {
setState(null);
return;
}
setState({ mode: "token", creditLoading: true, credits: undefined, todayUsage: null, creditError: null, todayUsageError: null });
let status;
try {
status = await authRequest("status", undefined, sessionId);
} catch {
if (!disposed && currentRequestId === requestId) setState(null);
return;
}
if (disposed || currentRequestId !== requestId) return;
// [自用定制 2026-10-10] 原来在 API Key 模式下直接跳过积分查询;现在继续查。
if (status.mode === "token" && !status.activeAccountId) {
setState({ ...status, creditLoading: false });
return;
}
try {
const result = await authRequest("credits", { accountId: status.activeAccountId }, sessionId);
if (!disposed && currentRequestId === requestId) setState({ ...status, ...result, creditLoading: false });
} catch (error) {
if (!disposed && currentRequestId === requestId) {
const message = error instanceof Error ? error.message : "查询 WorkBuddy 积分失败";
setState({
...status,
credits: null,
creditLoading: false,
creditError: message,
todayUsage: null,
todayUsageError: "查询 WorkBuddy 今日请求量失败",
});
}
}
};
load();
const onAuthState = () => load();
window.addEventListener(AUTH_STATE_EVENT, onAuthState);
const timer = window.setInterval(load, 60_000);
return () => {
disposed = true;
window.removeEventListener(AUTH_STATE_EVENT, onAuthState);
window.clearInterval(timer);
};
}, [provider, selected, sessionId]);
// [自用定制 2026-10-10] A+ 方案:积分项自带悬浮卡片,不接管官方统计行。
const [cardOpen, setCardOpen] = useState(false);
const cardRootRef = useRef(null);
useEffect(() => {
if (!cardOpen || typeof document === "undefined") return undefined;
const onDocPointerDown = (event) => {
if (cardRootRef.current && !cardRootRef.current.contains(event.target)) setCardOpen(false);
};
const onKeyDown = (event) => {
if (event.key === "Escape") setCardOpen(false);
};
document.addEventListener("pointerdown", onDocPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("pointerdown", onDocPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, [cardOpen]);
const hasTokenAccount = state?.mode === "token" && Boolean(state.activeAccountId);
// [自用定制 2026-10-10] 放宽门槛:API Key 模式下同样渲染积分行。
const hasApiKey = state?.mode !== "token" && Boolean(state?.activeApiKeyId || state?.apiKeyConfigured);
if (!selected || !state || (!state.routingEnabled && !hasTokenAccount && !hasApiKey) || (!sessionId && !hasTokenAccount && !hasApiKey)) return null;
const creditsText = state.creditLoading
? "剩余积分:读取中…"
: state.unlimited
? "剩余积分:不限量"
: typeof state.credits === "number" && Number.isFinite(state.credits)
? `剩余积分:${formatCredits(state.credits)}`
: state.creditError
? "剩余积分:暂不可用"
: "剩余积分:—";
const today = state.todayUsage;
const usageText = today?.synced === true
? `今日请求:${Number.isFinite(Number(today.count)) ? Number(today.count) : 0} 次 · 用量 ${formatCredits(today.used)} 积分`
: state.creditLoading
? "今日请求:读取中…"
: state.todayUsageError
? "今日请求:暂不可用"
: "今日请求:—";
const modlensHint = modlens ? createElement("span", {
"data-workbuddy-modlens-hint": true,
title: "ModLens 会先处理会话中的历史图片,首次响应可能较慢;若超时、无响应或返回 429,请检查视觉引擎登录、网络和额度。纯文本任务可切换 WorkBuddy 直连。",
style: {
flex: "1 1 100%",
minWidth: 0,
color: "var(--dsw-text-tertiary, #98a2b3)",
fontSize: "11px",
lineHeight: "16px",
textAlign: "right",
},
}, "ModLens:历史图片会先处理,响应较慢或出现 429 时请检查视觉引擎与额度;纯文本可切换 WorkBuddy 直连") : null;
const activeAccount = Array.isArray(state.accounts) ? state.accounts.find((account) => account.id === state.activeAccountId) : undefined;
const credentialOptions = state.mode === "token"
? (Array.isArray(state.accounts) ? state.accounts.map((account) => createElement("option", { key: account.id, value: account.id }, accountText(account))) : [])
: (Array.isArray(state.apiKeys) ? state.apiKeys.map((key) => createElement("option", { key: key.id, value: key.id, disabled: key.configured === false }, apiKeyText(key))) : []);
const confirmBindingChange = (message) => typeof window === "undefined" || window.confirm(message);
const applyBindingResult = async (result) => {
setState(result);
if (result.mode === "token" && result.activeAccountId) {
try {
const credits = await authRequest("credits", { accountId: result.activeAccountId }, sessionId);
setState({ ...result, ...credits, creditLoading: false });
} catch (error) {
setState({
...result,
credits: null,
creditLoading: false,
creditError: error instanceof Error ? error.message : "查询 WorkBuddy 积分失败",
todayUsage: null,
todayUsageError: "查询 WorkBuddy 今日请求量失败",
});
}
}
};
const bindCredential = async (mode, value, failureMessage) => {
try {
const result = mode === "token"
? await authRequest("token", { accountId: value }, sessionId)
: await authRequest("api-key", { keyId: value }, sessionId);
await applyBindingResult(result);
} catch (error) {
setState({ ...state, creditError: error instanceof Error ? error.message : failureMessage });
}
};
const onSessionCredentialChange = async (event) => {
if (!state.routingEnabled || !sessionId) return;
const value = event.target.value;
if (!confirmBindingChange("切换后,当前已发出的请求继续使用原凭证;后续模型调用将使用新凭证。确定切换吗?")) {
event.target.value = state.mode === "token" ? state.activeAccountId ?? "" : state.activeApiKeyId ?? "";
return;
}
await bindCredential(state.mode, value, "切换会话凭证失败");
};
const onSessionModeChange = async (event) => {
if (!state.routingEnabled || !sessionId) return;
const mode = event.target.value;
const value = mode === "token" ? state.accounts?.[0]?.id : state.apiKeys?.find((key) => key.configured)?.id;
if (!value) return;
if (!confirmBindingChange("切换后,当前已发出的请求继续使用原凭证;后续模型调用将使用新凭证。确定切换认证模式吗?")) {
event.target.value = state.mode;
return;
}
await bindCredential(mode, value, "切换会话认证模式失败");
};
const onUnbind = async () => {
if (!state.routingEnabled || !sessionId || !state.sessionBinding) return;
if (!confirmBindingChange("解除后,当前会话将在下一次模型调用时绑定当前默认凭证。确定解除吗?")) return;
try {
await applyBindingResult(await authRequest("unbind", {}, sessionId));
} catch (error) {
setState({ ...state, creditError: error instanceof Error ? error.message : "解除会话凭证失败" });
}
};
const actionStyle = { border: "0", padding: "0 2px", background: "transparent", color: "inherit", font: "inherit", fontSize: "12px", cursor: "pointer", textDecoration: "underline" };
const sessionControls = state.routingEnabled && sessionId ? createElement(
"span",
{ "data-workbuddy-session-controls": true, style: { display: "inline-flex", alignItems: "center", justifyContent: "flex-end", flex: "1 1 100%", flexWrap: "wrap", gap: "4px", minWidth: 0, maxWidth: "100%" } },
createElement("span", { title: state.sessionBinding ? "当前会话已使用独立凭证" : "当前会话未绑定,下次发送将固化当前默认凭证" }, state.sessionBinding ? "已绑定" : "未绑定"),
createElement("select", { value: state.mode, onChange: onSessionModeChange, "aria-label": "当前会话认证模式", style: { border: "0", background: "transparent", color: "inherit", font: "inherit", fontSize: "12px", minWidth: 0, maxWidth: "110px" } },
createElement("option", { value: "token" }, "令牌"),
createElement("option", { value: "api-key" }, "API Key"),
),
createElement("select", { value: state.mode === "token" ? state.activeAccountId ?? "" : state.activeApiKeyId ?? "", onChange: onSessionCredentialChange, "aria-label": "当前会话凭证", style: { border: "0", background: "transparent", color: "inherit", font: "inherit", fontSize: "12px", minWidth: 0, maxWidth: "190px" } }, credentialOptions),
state.sessionBinding ? createElement("button", { type: "button", onClick: onUnbind, title: "解除当前绑定,下次发送时重新绑定默认凭证", style: actionStyle }, "解除绑定") : null,
createElement("span", { "aria-hidden": true, style: { opacity: 0.55, padding: "0 2px" } }, "·"),
) : null;
// [自用定制 2026-10-10] A+ 方案:药丸式积分项 + 点击弹出信息卡片;官方统计行保持原样。
const creditValue = state.creditLoading
? "读取中…"
: state.unlimited
? "不限量"
: typeof state.credits === "number" && Number.isFinite(state.credits)
? formatCredits(state.credits)
: state.creditError ? "暂不可用" : "—";
const formatStamp = (value) => {
const matched = String(value ?? "").match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
return matched ? `${matched[1]}-${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}` : String(value ?? "");
};
const cardRows = [
["剩余积分", creditValue],
state.planName ? ["套餐", state.planName] : null,
state.planCycleEnd ? ["下次重置", formatStamp(state.planCycleEnd)] : null,
].filter(Boolean);
return createElement(
"div",
{
ref: cardRootRef,
className: "dsh-workbuddy-credits",
"data-workbuddy-credits": true,
"data-workbuddy-session-aware": state.routingEnabled ? true : undefined,
role: "status",
"aria-live": "polite",
style: {
boxSizing: "border-box",
position: "relative",
minWidth: 0,
width: "100%",
maxWidth: "100%",
minHeight: "20px",
padding: "0",
display: "inline-flex",
flexWrap: "wrap",
gap: "2px 8px",
flex: "0 1 auto",
justifyContent: "flex-end",
alignItems: "center",
color: "var(--dsw-text-tertiary, #98a2b3)",
fontSize: "12px",
lineHeight: "18px",
fontVariantNumeric: "tabular-nums",
whiteSpace: "normal",
overflow: "visible",
textOverflow: "clip",
},
},
createElement(
"button",
{
type: "button",
"data-workbuddy-credit-pill": true,
"aria-expanded": cardOpen ? "true" : "false",
title: [state.creditError, state.todayUsageError].filter(Boolean).join(";") || "点击查看 WorkBuddy 用量",
onClick: (event) => {
event.stopPropagation();
setCardOpen((open) => !open);
},
style: {
border: "0",
margin: "0",
padding: "1px 4px",
background: "transparent",
color: "inherit",
font: "inherit",
fontSize: "12px",
lineHeight: "18px",
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap",
cursor: "pointer",
borderRadius: "4px",
},
},
`剩余积分 ${creditValue}`,
),
cardOpen && cardRows.length > 0
? createElement(
"div",
{
"data-workbuddy-credit-card": true,
role: "dialog",
style: {
position: "absolute",
right: "0",
bottom: "calc(100% + 8px)",
zIndex: 80,
minWidth: "216px",
padding: "10px 12px",
borderRadius: "10px",
background: "var(--dsw-surface-primary, #ffffff)",
border: "1px solid var(--dsw-border-tertiary, rgba(0,0,0,.12))",
boxShadow: "0 10px 28px rgba(0,0,0,.16)",
color: "var(--dsw-text-primary, #1f2329)",
fontSize: "12px",
lineHeight: "20px",
textAlign: "left",
whiteSpace: "normal",
},
},
createElement("div", { style: { fontWeight: 600, marginBottom: "6px" } }, "WorkBuddy"),
...cardRows.map(([label, value]) => createElement(
"div",
{ style: { display: "flex", justifyContent: "space-between", gap: "16px" } },
createElement("span", { style: { color: "var(--dsw-text-tertiary, #98a2b3)" } }, label),
createElement("span", { style: { fontVariantNumeric: "tabular-nums" } }, value),
)),
)
: null,
modlensHint,
);
}
function installComposerDockLayout() {
if (typeof document === "undefined" || document.querySelector('style[data-plugin-css="dsh-llm-workbuddy-composer-dock"]')) return;
const style = document.createElement("style");
style.dataset.plugin = "@axiaohungry/dsh-llm-workbuddy";
style.dataset.pluginCss = "dsh-llm-workbuddy-composer-dock";
style.textContent = `
[data-slot="conversation.composer.dock"]:has(> [data-composer-stats]),
[data-slot="conversation.composer.dock"]:has(> [data-workbuddy-credits]) {
box-sizing: border-box;
width: 100%;
min-width: 0;
display: grid !important;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
padding: 0 4px 4px;
overflow: visible;
}
[data-slot="conversation.composer.dock"] > [data-composer-stats] {
grid-column: 2;
width: auto !important;
max-width: 100%;
min-width: 0;
margin: 0 !important;
}
[data-slot="conversation.composer.dock"] > [data-workbuddy-credits] {
grid-column: 3;
justify-self: end;
width: 100% !important;
max-width: 100%;
min-width: 0;
margin: 0 !important;
}
[data-slot="conversation.composer.dock"] > [data-workbuddy-credits] > span {
min-width: 0;
}
[data-workbuddy-credit-pill] {
transition: background-color 0.12s ease;
}
[data-workbuddy-credit-pill]:hover,
[data-workbuddy-credit-pill]:focus-visible {
background: var(--dsw-surface-subtle, rgba(127, 127, 127, 0.14)) !important;
}
[data-workbuddy-credit-card] {
pointer-events: auto;
}
[data-slot="conversation.composer.dock"] > [data-workbuddy-credits] > [data-workbuddy-session-controls] {
flex-basis: 100%;
width: 100%;
}
@media (max-width: 760px) {
[data-slot="conversation.composer.dock"]:has(> [data-composer-stats]),
[data-slot="conversation.composer.dock"]:has(> [data-workbuddy-credits]) {
display: flex !important;
flex-wrap: wrap;
justify-content: center;
}
[data-slot="conversation.composer.dock"] > [data-composer-stats],
[data-slot="conversation.composer.dock"] > [data-workbuddy-credits] {
flex: 0 1 auto;
}
}
`;
document.head.appendChild(style);
}
function applyCreditStatus(stats, status, activeAccount) {
const visible = status.mode === "token" && Boolean(activeAccount);
setVisible(stats, visible);
if (!visible) return;
const credit = stats.querySelector('[data-workbuddy-stat="credits"]');
const usage = stats.querySelector('[data-workbuddy-stat="usage"]');
credit.title = status.creditError || "";
usage.title = status.todayUsageError || "";
if (status.creditLoading) credit.textContent = "剩余积分:读取中…";
else if (status.unlimited) credit.textContent = "剩余积分:不限量";
else if (typeof status.credits === "number" && Number.isFinite(status.credits)) credit.textContent = `剩余积分:${formatCredits(status.credits)}`;
else credit.textContent = status.creditError ? "剩余积分:暂不可用" : "剩余积分:—";
const today = status.todayUsage;
if (today && today.synced === true) {
usage.textContent = `今日请求:${Number.isFinite(Number(today.count)) ? Number(today.count) : 0} 次 · 用量 ${formatCredits(today.used)} 积分`;
} else {
usage.textContent = status.creditLoading ? "今日请求:读取中…" : status.todayUsageError ? "今日请求:暂不可用" : "今日请求:—";
}
}
function applyMode(input, keyButton, tokenButton, keySection, keySourceRow, keySelect, keyHint, newKeyInput, newKeyLabelInput, saveKeyButton, removeKeyButton, tokenSection, accountRow, accountSelect, accountName, addButton, removeButton, accountStats, routingRow, routingToggle, message, status) {
const token = status.mode === "token";
const apiKeys = Array.isArray(status.apiKeys) ? status.apiKeys : [];
const activeApiKeyId = status.activeApiKeyId ?? apiKeys[0]?.id;
const activeApiKey = apiKeys.find((key) => key.id === activeApiKeyId);
const accounts = Array.isArray(status.accounts) ? status.accounts : [];
const activeAccountId = status.activeAccountId ?? accounts[0]?.id;
input.disabled = token;
input.placeholder = token ? "当前使用 WorkBuddy 账号令牌" : "输入新的 API Key(保存到 DSH)";
newKeyInput.disabled = token;
newKeyLabelInput.disabled = token;
keyButton.setAttribute("aria-pressed", String(!token));
tokenButton.setAttribute("aria-pressed", String(token));
keyButton.style.background = !token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
tokenButton.style.background = token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
setVisible(keySection, !token);
// [自用定制 2026-10-10] 自用只保留一个 Key:隐藏「当前 API Key」多 Key 来源下拉
setVisible(keySourceRow, false);
keySelect.replaceChildren(...apiKeys.map((key) => {
const option = document.createElement("option");
option.value = key.id;
option.textContent = apiKeyText(key);
option.title = apiKeyLabel(key);
return option;
}));
if (activeApiKeyId) keySelect.value = activeApiKeyId;
// [自用定制 2026-10-10] 不回显正在使用的 API Key
keyHint.textContent = token ? "" : (activeApiKey ? "" : "未检测到可用 API Key,可输入 Key 保存");
keyHint.title = "";
saveKeyButton.textContent = "添加并使用";
removeKeyButton.hidden = !activeApiKey || activeApiKey.kind !== "dsh";
setVisible(tokenSection, token);
tokenButton.textContent = token ? "令牌模式" : "令牌登录";
addButton.textContent = accounts.length ? "添加账号" : "登录 WorkBuddy";
setVisible(accountRow, token && accounts.length > 0);
accountSelect.replaceChildren(...accounts.map((account) => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = accountText(account);
option.title = accountLabel(account);
return option;
}));
if (activeAccountId) accountSelect.value = activeAccountId;
const activeAccount = accounts.find((account) => account.id === activeAccountId);
accountName.textContent = activeAccount ? `当前账号:${accountText(activeAccount)}` : "";
accountName.title = activeAccount ? accountLabel(activeAccount) : "";
removeButton.hidden = !token || !activeAccount;
setVisible(accountStats, token && Boolean(activeAccount));
applyCreditStatus(accountStats, status, activeAccount);
routingToggle.checked = status.routingEnabled === true;
routingToggle.disabled = false;
routingRow.title = status.routingEnabled ? "已启用:每个会话可单独选择 WorkBuddy 账号或 API Key" : "关闭时使用原有的全局账号或 API Key";
message.textContent = token
? status.authenticated ? `令牌已登录:${activeAccount ? accountText(activeAccount) : "当前账号"}` : "令牌缺失,请重新登录"
: status.apiKeyConfigured ? `API Key 已就绪:${activeApiKey ? apiKeyText(activeApiKey) : "当前来源"}` : "未配置 API Key,请输入后保存";
message.style.color = token
? status.authenticated ? "var(--dsw-text-success, #2e7d32)" : "var(--dsw-text-danger, #c62828)"
: status.apiKeyConfigured ? "var(--dsw-text-success, #2e7d32)" : "var(--dsw-text-danger, #c62828)";
}
async function request(path, body) {
const options = { method: "POST" };
if (body !== undefined) {
options.headers = { "content-type": "application/json" };
options.body = JSON.stringify(body);
}
const response = await fetch(`${ROUTE}/${path}`, options);
const result = await response.json();
if (!response.ok || !result.ok) {
const error = new Error(result.message || `请求失败(${response.status})`);
error.status = response.status;
throw error;
}
return result;
}
function mount(input) {
const field = input.parentElement;
if (!field || field.querySelector(`[${MARKER}]`)) return;
field.setAttribute("data-workbuddy-auth-field", "");
input.dataset.workbuddyPlaceholder = input.placeholder;
for (const nativeNode of field.children) nativeNode.hidden = true;
Object.assign(field.style, { display: "block", width: "100%", boxSizing: "border-box" });
const controls = document.createElement("div");
controls.setAttribute(MARKER, "");
controls.setAttribute("role", "group");
controls.setAttribute("aria-label", "WorkBuddy 认证方式");
Object.assign(controls.style, {
display: "flex",
flexDirection: "column",
alignItems: "stretch",
gap: "8px",
width: "100%",
maxWidth: "100%",
paddingTop: "4px",
boxSizing: "border-box",
color: "var(--dsw-text-primary, inherit)",
});
const modeRow = row();
const keySection = section();
const keySourceRow = row();
const keyAddRow = row();
const tokenSection = section();
const accountRow = row();
const tokenActionRow = row();
const keyButton = button("API Key");
const tokenButton = button("令牌登录");
const keySelect = accountPicker();
keySelect.setAttribute("aria-label", "WorkBuddy API Key 来源");
const keySourceLabel = fieldLabel("当前 API Key");
const newKeyLabel = fieldLabel("新增 API Key");
const newKeyInput = textInput("password", "粘贴新的 API Key", "新增 WorkBuddy API Key");
const newKeyLabelInput = textInput("text", "名称(可选)", "API Key 名称");
const keyHint = document.createElement("span");
Object.assign(keyHint.style, {
display: "block",
width: "100%",
minWidth: "0",
overflowWrap: "anywhere",
whiteSpace: "normal",
fontSize: "12px",
lineHeight: "18px",
color: "var(--dsw-text-secondary, #667085)",
});
const saveKeyButton = button("添加并使用");
const removeKeyButton = button("删除");
const accountSelect = accountPicker();
const accountName = document.createElement("span");
Object.assign(accountName.style, {
flex: "1 1 220px",
minWidth: "0",
overflowWrap: "anywhere",
whiteSpace: "normal",
fontSize: "13px",
color: "var(--dsw-text-secondary, #667085)",
});
const addButton = button("令牌登录");
const removeButton = button("删除账号");
const accountLabel = fieldLabel("令牌账号");
const accountStats = document.createElement("div");
accountStats.setAttribute("role", "status");
accountStats.setAttribute("aria-live", "polite");
Object.assign(accountStats.style, {
display: "flex",
alignItems: "center",
gap: "8px",
flexWrap: "wrap",
width: "100%",
minHeight: "24px",
fontSize: "13px",
color: "var(--dsw-text-secondary, #667085)",
});
const tokenHint = document.createElement("span");
tokenHint.textContent = "令牌登录后可切换账号,并查看积分与今日请求量。";
Object.assign(tokenHint.style, {
display: "block",
width: "100%",
fontSize: "12px",
lineHeight: "18px",
color: "var(--dsw-text-secondary, #667085)",
});
const creditStat = document.createElement("span");
creditStat.dataset.workbuddyStat = "credits";
const usageStat = document.createElement("span");
usageStat.dataset.workbuddyStat = "usage";
accountStats.append(creditStat, usageStat);
const routingRow = row();
const routingToggle = document.createElement("input");
routingToggle.type = "checkbox";
routingToggle.setAttribute("aria-label", "启用会话级账号/API Key");
routingToggle.style.accentColor = "var(--dsw-accent, #2563eb)";
const routingLabel = document.createElement("label");
routingLabel.textContent = "会话级账号/API Key";
routingLabel.style.fontSize = "13px";
routingLabel.style.color = "var(--dsw-text-secondary, #667085)";
const routingHint = document.createElement("span");
routingHint.textContent = "关闭时保持原有全局认证方式";
routingHint.style.fontSize = "12px";
routingHint.style.color = "var(--dsw-text-tertiary, #98a2b3)";
routingRow.append(routingToggle, routingLabel, routingHint);
const message = document.createElement("span");
message.setAttribute("role", "status");
message.setAttribute("aria-live", "polite");
Object.assign(message.style, { fontSize: "12px", minHeight: "18px", lineHeight: "18px" });
modeRow.append(keyButton, tokenButton);
keySourceRow.append(keySourceLabel, keySelect, removeKeyButton);
keyAddRow.append(newKeyLabel, newKeyInput, newKeyLabelInput, saveKeyButton);
keySection.append(keySourceRow, keyAddRow, keyHint);
accountRow.append(accountLabel, accountSelect, accountName, removeButton);
tokenActionRow.append(addButton);
tokenSection.append(tokenHint, accountRow, accountStats, tokenActionRow);
controls.append(modeRow, routingRow, keySection, tokenSection, message);
field.append(controls);
let current = { mode: "api-key", authenticated: false, accounts: [], apiKeys: [], credits: undefined, creditLoading: false, creditError: null, todayUsage: null, todayUsageError: null };
const render = (status) => {
const previousMode = current.mode;
const previousAccountId = current.activeAccountId;
const previousApiKeyId = current.activeApiKeyId;
const previousAccounts = current.accounts;
const previousApiKeys = current.apiKeys;
const previousRouting = current.routingEnabled;
current = { ...current, ...status };
applyMode(input, keyButton, tokenButton, keySection, keySourceRow, keySelect, keyHint, newKeyInput, newKeyLabelInput, saveKeyButton, removeKeyButton, tokenSection, accountRow, accountSelect, accountName, addButton, removeButton, accountStats, routingRow, routingToggle, message, current);
if (previousMode !== current.mode || previousAccountId !== current.activeAccountId || previousApiKeyId !== current.activeApiKeyId || previousAccounts !== current.accounts || previousApiKeys !== current.apiKeys || previousRouting !== current.routingEnabled) notifyAuthState();
};
render(current);
let creditRequestId = 0;
const loadCredits = async (accountId) => {
const requestId = ++creditRequestId;
if (current.mode !== "token" || !accountId) {
render({ credits: undefined, creditLoading: false, creditError: null, todayUsage: null, todayUsageError: null });
return;
}
render({ credits: undefined, creditLoading: true, creditError: null, todayUsage: null, todayUsageError: null });
try {
const result = await request("credits", { accountId });
if (requestId !== creditRequestId || current.activeAccountId !== accountId) return;
render({ ...result, creditLoading: false });
} catch (error) {
if (requestId !== creditRequestId || current.activeAccountId !== accountId) return;
render({ credits: null, creditLoading: false, creditError: error instanceof Error ? error.message : "查询 WorkBuddy 积分失败", todayUsage: null, todayUsageError: "查询 WorkBuddy 今日请求量失败" });
}
};
const setBusy = (busy) => {
keyButton.disabled = busy;
tokenButton.disabled = busy;
keySelect.disabled = busy;
newKeyInput.disabled = busy || current.mode === "token";
newKeyLabelInput.disabled = busy || current.mode === "token";
saveKeyButton.disabled = busy;
removeKeyButton.disabled = busy;
accountSelect.disabled = busy;
addButton.disabled = busy;
removeButton.disabled = busy;
routingToggle.disabled = busy;
keyButton.style.cursor = busy ? "progress" : "pointer";
tokenButton.style.cursor = busy ? "progress" : "pointer";
saveKeyButton.style.cursor = busy ? "progress" : "pointer";
removeKeyButton.style.cursor = busy ? "progress" : "pointer";
addButton.style.cursor = busy ? "progress" : "pointer";
removeButton.style.cursor = busy ? "progress" : "pointer";
};
keyButton.addEventListener("click", async () => {
setBusy(true);
message.textContent = "正在切换…";
try {
render(await request("api-key", current.activeApiKeyId ? { keyId: current.activeApiKeyId } : undefined));
newKeyInput.focus();
} catch (error) {
message.textContent = error instanceof Error ? error.message : "切换失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
keySelect.addEventListener("change", async () => {
setBusy(true);
message.textContent = "正在切换 API Key…";
try {
render(await request("api-key", { keyId: keySelect.value }));
} catch (error) {
message.textContent = error instanceof Error ? error.message : "切换失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
saveKeyButton.addEventListener("click", async () => {
const value = newKeyInput.value.trim();
if (!value) {
message.textContent = "请输入新的 API Key";
message.style.color = "var(--dsw-text-danger, #c62828)";
newKeyInput.focus();
return;
}
setBusy(true);
saveKeyButton.textContent = "添加中…";
message.textContent = "正在保存 API Key…";
try {
const label = newKeyLabelInput.value.trim();
const next = await request("api-key/add", { key: value, ...(label ? { label } : {}) });
newKeyInput.value = "";
newKeyLabelInput.value = "";
render(next);
} catch (error) {
message.textContent = error instanceof Error ? error.message : "保存失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
newKeyInput.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !saveKeyButton.disabled) {
event.preventDefault();
saveKeyButton.click();
}
});
removeKeyButton.addEventListener("click", async () => {
if (!current.activeApiKeyId || !window.confirm("确定删除当前 DSH 保存的 API Key 吗?")) return;
setBusy(true);
message.textContent = "正在删除 API Key…";
try {
render(await request("api-key/remove", { keyId: current.activeApiKeyId }));
} catch (error) {
message.textContent = error instanceof Error ? error.message : "删除失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
tokenButton.addEventListener("click", async () => {
setBusy(true);
tokenButton.textContent = "切换中…";
message.textContent = current.accounts?.length ? "正在切换令牌账号…" : "请在浏览器中完成 WorkBuddy 中国站登录";
try {
const next = await request(current.accounts?.length ? "token" : "login");
render(next);
await loadCredits(next.activeAccountId);
} catch (error) {
message.textContent = error instanceof Error ? error.message : "登录失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
addButton.addEventListener("click", async () => {
setBusy(true);
addButton.textContent = "等待浏览器登录…";
message.textContent = "请在浏览器中完成 WorkBuddy 中国站登录";
try {
const next = await request("login");
render(next);
await loadCredits(next.activeAccountId);
} catch (error) {
message.textContent = error instanceof Error ? error.message : "登录失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
accountSelect.addEventListener("change", async () => {
setBusy(true);
message.textContent = "正在切换令牌账号…";
try {
const next = await request("token", { accountId: accountSelect.value });
render(next);
await loadCredits(next.activeAccountId);
} catch (error) {
message.textContent = error instanceof Error ? error.message : "切换失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
removeButton.addEventListener("click", async () => {
if (!current.activeAccountId || !window.confirm("确定删除这个 WorkBuddy 登录账号吗?令牌将从 DSH 凭据中移除。")) return;
setBusy(true);
message.textContent = "正在删除账号…";
try {
const next = await request("remove", { accountId: current.activeAccountId });
render(next);
await loadCredits(next.activeAccountId);
} catch (error) {
message.textContent = error instanceof Error ? error.message : "删除失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
routingToggle.addEventListener("change", async () => {
setBusy(true);
message.textContent = routingToggle.checked ? "正在启用会话级认证…" : "正在关闭会话级认证…";
try {
render(await authRequest("routing", { enabled: routingToggle.checked }));
} catch (error) {
routingToggle.checked = !routingToggle.checked;
message.textContent = error instanceof Error ? error.message : "切换会话级认证失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
} finally {
setBusy(false);
}
});
fetch(`${ROUTE}/status`, { cache: "no-store" })
.then((response) => response.json())
.then((status) => {
render(status);
return loadCredits(status.activeAccountId);
})
.catch(() => {
message.textContent = "认证状态读取失败";
message.style.color = "var(--dsw-text-danger, #c62828)";
});
}
function mountModelEditor(host) {
const heading = document.createElement("strong");
heading.textContent = "模型目录";
const actions = row();
const fetchButton = button("获取可用模型");
const addButton = button("添加模型");
const resetButton = button("恢复在线目录");
const saveButton = button("保存模型");
const list = document.createElement("div");
const candidates = document.createElement("div");
const message = document.createElement("span");
message.setAttribute("role", "status");
message.setAttribute("aria-live", "polite");
Object.assign(host.style, { display: "flex", flexDirection: "column", gap: "10px", width: "100%" });
Object.assign(list.style, { display: "flex", flexDirection: "column", gap: "8px" });
Object.assign(message.style, { fontSize: "12px", minHeight: "18px" });
actions.append(fetchButton, addButton, resetButton, saveButton);
host.append(heading, actions, list, candidates, message);
let models = null;
let revision;
let busy = false;
const invalidReasoning = new Set();
const say = (value, error = false) => {
message.textContent = value;
message.style.color = error ? "var(--dsw-text-danger, #c62828)" : "var(--dsw-text-secondary, #667085)";
};
const setBusy = (value) => {
busy = value;
for (const control of [fetchButton, addButton, resetButton, saveButton]) control.disabled = value;
};
const makeInput = (value, label, onChange, type = "text") => {
const input = textInput(type, label, label);
input.value = value ?? "";
input.title = label;
input.addEventListener("input", () => onChange(input.value));
return input;
};
const render = () => {
list.replaceChildren();
if (models === null) {
const hint = document.createElement("span");
hint.textContent = "使用当前账号的在线模型目录;获取后可选择模型进行自定义。";
list.append(hint);
return;
}
for (const [index, model] of models.entries()) {
const item = section();
const fields = row();
const id = makeInput(model.id, "模型 ID", (value) => { model.id = value.trim(); });
const name = makeInput(model.name, "显示名称", (value) => { model.name = value; });
const context = makeInput(model.contextWindow, "上下文窗口", (value) => {
if (value) model.contextWindow = Number(value);
else delete model.contextWindow;
}, "number");
const output = makeInput(model.maxTokens, "最大输出 Token", (value) => {
if (value) model.maxTokens = Number(value);
else delete model.maxTokens;
}, "number");
for (const input of [id, name, context, output]) {
input.style.flex = "1 1 140px";
fields.append(input);
}
const imageLabel = document.createElement("label");
const images = document.createElement("input");
images.type = "checkbox";
images.checked = model.input?.includes("image") === true;
images.addEventListener("change", () => { model.input = images.checked ? ["text", "image"] : ["text"]; });
imageLabel.append(images, " 图片输入");
const remove = button("删除");
remove.addEventListener("click", () => { invalidReasoning.delete(model); models.splice(index, 1); render(); });
fields.append(imageLabel, remove);
item.append(fields);
if (model.reasoningEfforts !== undefined) {
const thinking = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = "模型思考档位";
const editor = document.createElement("textarea");
editor.setAttribute("aria-label", `${model.id} 思考档位 JSON`);
editor.value = JSON.stringify(model.reasoningEfforts);
editor.style.width = "100%";
editor.addEventListener("change", () => {
try {
const value = JSON.parse(editor.value);
if (value !== false && (!value || typeof value !== "object" || Array.isArray(value))) throw new Error();
model.reasoningEfforts = value;
invalidReasoning.delete(model);
say("");
} catch { invalidReasoning.add(model); say("思考档位 JSON 格式无效", true); }
});
thinking.append(summary, editor);
item.append(thinking);
}
list.append(item);
}
};
fetch(`${ROUTE}/models`, { cache: "no-store" })
.then((response) => response.json())
.then((state) => {
if (!state.ok) throw new Error(state.message);
models = state.models?.map((model) => ({ ...model })) ?? null;
revision = state.revision;
render();
})
.catch((error) => say(error instanceof Error ? error.message : "读取模型失败", true));
addButton.addEventListener("click", () => {
models ??= [];
models.push({ id: "", name: "" });
render();
list.lastElementChild?.querySelector('input[aria-label="模型 ID"]')?.focus();
});
fetchButton.addEventListener("click", async () => {
if (busy) return;
setBusy(true);
say("正在获取模型…");
try {
const result = await request("models", { action: "discover" });
candidates.replaceChildren();
const choices = result.models.filter((model) => !models?.some((existing) => existing.id === model.id));
if (!choices.length) return say("没有新的可用模型");
const selected = new Set();
for (const model of choices) {
const label = document.createElement("label");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.addEventListener("change", () => checkbox.checked ? selected.add(model.id) : selected.delete(model.id));
label.append(checkbox, ` ${model.name || model.id} (${model.id})`);
Object.assign(label.style, { display: "block", fontSize: "13px" });
candidates.append(label);
}
const addSelected = button("添加选中模型");
addSelected.addEventListener("click", () => {
models ??= [];
models.push(...choices.filter((model) => selected.has(model.id)).map((model) => ({ ...model })));
candidates.replaceChildren();
render();
});
candidates.append(addSelected);
say(`获取到 ${result.models.length} 个模型,请选择后保存`);
} catch (error) { say(error instanceof Error ? error.message : "获取模型失败", true); }
finally { setBusy(false); }
});
saveButton.addEventListener("click", async () => {
if (busy || models === null) return;
if (invalidReasoning.size) return say("请先修正模型思考档位 JSON", true);
if (models.some((model) => !model.id || !Number.isSafeInteger(model.contextWindow ?? 1)
|| !Number.isSafeInteger(model.maxTokens ?? 1))) return say("请检查模型 ID 和 Token 数值", true);
setBusy(true);
try {
const result = await request("models", { action: "save", models, revision });
revision = result.revision;
say("模型已保存");
} catch (error) { say(error instanceof Error ? error.message : "保存模型失败", true); }
finally { setBusy(false); }
});
resetButton.addEventListener("click", async () => {
if (busy || !window.confirm("恢复在线模型目录?自定义模型列表会被移除。")) return;
setBusy(true);
try {
const result = await request("models", { action: "reset", revision });
models = null;
revision = result.revision;
candidates.replaceChildren();
render();
say("已恢复在线模型目录");
} catch (error) { say(error instanceof Error ? error.message : "恢复失败", true); }
finally { setBusy(false); }
});
render();
}
function WorkBuddySettingsCard({ provider, embedded = false }) {
const root = useRef(null);
useEffect(() => {
if (provider?.provider !== "workbuddy-cn" || !root.current) return;
const host = root.current;
const field = section();
const input = document.createElement("input");
input.setAttribute("aria-label", "API 密钥");
field.append(input);
host.append(field);
mount(input);
const models = section();
host.append(models);
mountModelEditor(models);
const scope = embedded ? null : host.closest("li") ?? host.closest('[role="tabpanel"]') ?? host.closest('[class*="_addPanel"]');
const hidden = new Map();
const hideNative = () => {
if (!scope) return;
for (const node of scope.querySelectorAll('[class*="_editor"], [class*="_rowActions"]')) {
if (node.contains(host) || host.contains(node) || hidden.has(node)) continue;
hidden.set(node, {
hidden: node.hidden,
display: node.style.getPropertyValue("display"),
priority: node.style.getPropertyPriority("display"),
});
node.hidden = true;
node.style.setProperty("display", "none", "important");
}
};
const observer = scope ? new MutationObserver(hideNative) : undefined;
observer?.observe(scope, { childList: true, subtree: true });
hideNative();
return () => {
observer?.disconnect();
for (const [node, previous] of hidden) {
node.hidden = previous.hidden;
if (previous.display) node.style.setProperty("display", previous.display, previous.priority);
else node.style.removeProperty("display");
}
};
}, [provider?.provider]);
return provider?.provider === "workbuddy-cn"
? createElement("div", { ref: root, "data-workbuddy-settings-card": "" })
: null;
}
function WorkBuddyPluginDetails({ subject }) {
return subject?.kind === "bundle" && subject.pkg?.name === "@axiaohungry/dsh-llm-workbuddy"
? createElement(WorkBuddySettingsCard, { provider: { provider: "workbuddy-cn" }, embedded: true })
: null;
}
function enhance() {
for (const input of document.querySelectorAll('input[aria-label="API 密钥"]')) {
if (isWorkBuddy(input)) mount(input);
}
mountPendingSelector();
}
function apply(ctx) {
installComposerDockLayout();
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
name: "settings.models.provider-card",
key: "llm-workbuddy",
}, WorkBuddySettingsCard));
// [自用定制 2026-10-10] 按用户要求移除「插件详情页」里的内置面板。
// 需要还原:见同目录 client.js.bak-20261010
// ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
// name: "plugins.detail.section",
// id: "llm-workbuddy",
// }, WorkBuddyPluginDetails));
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
name: "conversation.composer.dock",
id: "workbuddy-credits",
order: 100,
label: "WorkBuddy 用量",
}, WorkBuddyCreditsDock));
ctx.effect(() => {
const observer = new MutationObserver(enhance);
observer.observe(document.body, { childList: true, subtree: true });
document.addEventListener("change", enhance, true);
enhance();
return () => {
observer.disconnect();
document.removeEventListener("change", enhance, true);
};
}, "llm-workbuddy: auth switch");
}
return { name: "dsh-llm-workbuddy-client", inject: ["slots"], apply };
},
});