feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片

基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。
详见 README.CUSTOM.md。
This commit is contained in:
dsh-custom committed 2026-10-10 13:59:09 +08:00
commit a44227b69a
14 files changed
+5517

No files matched your search

+430
View File
@@ -0,0 +1,430 @@
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { setTimeout as delay } from "node:timers/promises";
export const WORKBUDDY_SESSION_REF = "WORKBUDDY_LOGIN_SESSION";
export const WORKBUDDY_SESSIONS_REF = "WORKBUDDY_LOGIN_SESSIONS";
export const WORKBUDDY_SESSION_ROUTING_REF = "WORKBUDDY_SESSION_ROUTING";
export const WORKBUDDY_API_KEYS_REF = "WORKBUDDY_API_KEYS";
export const LEGACY_SESSION_REF = "CODEBUDDY_LOGIN_SESSION";
export const LEGACY_SESSIONS_REF = "CODEBUDDY_LOGIN_SESSIONS";
export const LEGACY_API_KEYS_REF = "CODEBUDDY_API_KEYS";
function normalizeWorkBuddySessionBinding(binding) {
if (!binding || typeof binding !== "object") return undefined;
if (binding.mode === "token" && typeof binding.accountId === "string" && binding.accountId.trim()) {
return { mode: "token", accountId: binding.accountId.trim() };
}
if (binding.mode === "api-key" && typeof binding.apiKeyRef === "string" && binding.apiKeyRef.trim()) {
return { mode: "api-key", apiKeyRef: binding.apiKeyRef.trim() };
}
return undefined;
}
export function createWorkBuddySessionRoutingState(enabled = false, bindings = {}, lastUsed) {
const normalized = {};
if (bindings && typeof bindings === "object" && !Array.isArray(bindings)) {
for (const [sessionId, binding] of Object.entries(bindings)) {
if (typeof sessionId !== "string" || !sessionId.trim() || !binding || typeof binding !== "object") continue;
const value = normalizeWorkBuddySessionBinding(binding);
if (value) normalized[sessionId] = value;
}
}
const recent = normalizeWorkBuddySessionBinding(lastUsed);
return {
version: 1,
enabled: enabled === true,
bindings: normalized,
...(recent ? { lastUsed: recent } : {}),
};
}
export function serializeWorkBuddySessionRouting(state) {
return JSON.stringify(createWorkBuddySessionRoutingState(state?.enabled, state?.bindings, state?.lastUsed));
}
export function parseWorkBuddySessionRouting(value) {
let parsed;
try {
parsed = JSON.parse(value);
} catch (error) {
throw new Error("WorkBuddy 会话级认证配置已损坏,请重新设置", { cause: error });
}
return createWorkBuddySessionRoutingState(parsed?.enabled, parsed?.bindings, parsed?.lastUsed);
}
const BASE_URL = "https://copilot.tencent.com/v2/plugin";
const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
const REQUEST_HEADERS = {
accept: "application/json",
"content-type": "application/json",
"user-agent": USER_AGENT,
"x-product": "SaaS",
};
const NO_ACCOUNT_HEADERS = {
"X-No-Authorization": "true",
"X-No-User-Id": "true",
"X-No-Enterprise-Id": "true",
"X-No-Department-Info": "true",
};
const NO_ID_HEADERS = {
"X-No-User-Id": "true",
"X-No-Enterprise-Id": "true",
"X-No-Department-Info": "true",
};
function calculateExpiresAt(auth, now = Date.now()) {
const result = { ...auth };
if (!result.expiresAt && Number.isFinite(result.expiresIn)) result.expiresAt = now + result.expiresIn * 1000;
if (!result.refreshExpiresAt && Number.isFinite(result.refreshExpiresIn)) result.refreshExpiresAt = now + result.refreshExpiresIn * 1000;
return result;
}
async function responseBody(response, action) {
try {
return await response.json();
} catch (error) {
throw new Error(`${action}返回了无法解析的数据`, { cause: error });
}
}
async function request(path, options, action) {
let response;
try {
response = await fetch(`${BASE_URL}${path}`, options);
} catch (error) {
if (options.signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
throw new Error(`${action}无法连接 WorkBuddy 中国站`, { cause: error });
}
const body = await responseBody(response, action);
if (!response.ok || body?.code !== 0) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
return body.data;
}
function enterpriseHeaders(session) {
const enterpriseId = session.account?.enterpriseId;
return {
...(enterpriseId ? { "X-Enterprise-Id": enterpriseId, "X-Tenant-Id": enterpriseId } : {}),
...(session.auth?.domain ? { "X-Domain": session.auth.domain } : {}),
};
}
async function poll(path, headers, action, timeoutMs, signal) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await delay(1000, undefined, { signal });
let response;
try {
response = await fetch(`${BASE_URL}${path}`, { headers: { ...REQUEST_HEADERS, ...headers }, signal });
} catch (error) {
if (signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
continue;
}
const body = await responseBody(response, action);
if (response.ok && body?.code === 0 && body.data) return body.data;
if (response.status === 401 || response.status === 403) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
}
throw new Error(`${action}超时`);
}
function openBrowser(url) {
const [command, args] = process.platform === "win32"
? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
: process.platform === "darwin"
? ["open", [url]]
: ["xdg-open", [url]];
return new Promise((resolve, reject) => {
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
child.once("spawn", () => {
child.unref();
resolve();
});
child.once("error", reject);
});
}
function textValue(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
if (typeof value === "number" && Number.isFinite(value)) return String(value);
}
return undefined;
}
function tokenClaims(token) {
if (typeof token !== "string") return {};
try {
const payload = token.split(".")[1];
if (!payload) return {};
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function normalizeApiKeyEntry(entry, now = Date.now()) {
const ref = textValue(entry?.ref);
if (!ref || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(ref)) throw new Error("WorkBuddy API Key 引用无效");
const id = textValue(entry?.id) ?? `dsh:${ref}`;
return {
id,
ref,
label: textValue(entry?.label) ?? "DSH 保存的 API Key",
createdAt: Number.isFinite(entry?.createdAt) ? entry.createdAt : now,
updatedAt: Number.isFinite(entry?.updatedAt) ? entry.updatedAt : now,
};
}
export function createWorkBuddyApiKeyStore(entries = [], activeId) {
const keys = [];
for (const entry of entries) {
const normalized = normalizeApiKeyEntry(entry);
if (!keys.some((item) => item.id === normalized.id || item.ref === normalized.ref)) keys.push(normalized);
}
const selected = activeId === null
? null
: typeof activeId === "string" && keys.some((entry) => entry.id === activeId) ? activeId : keys[0]?.id;
return { version: 1, activeId: selected, entries: keys };
}
export function upsertWorkBuddyApiKey(store, entry, now = Date.now()) {
const current = createWorkBuddyApiKeyStore(store?.entries ?? [], store?.activeId);
const incoming = normalizeApiKeyEntry({ ...entry, updatedAt: now }, now);
const index = current.entries.findIndex((item) => item.id === incoming.id || item.ref === incoming.ref);
if (index >= 0) incoming.createdAt = current.entries[index].createdAt;
const entries = index >= 0
? current.entries.map((item, position) => position === index ? incoming : item)
: [...current.entries, incoming];
return { version: 1, activeId: incoming.id, entries };
}
export function serializeWorkBuddyApiKeys(store) {
const normalized = createWorkBuddyApiKeyStore(store?.entries ?? [], store?.activeId);
return JSON.stringify(normalized);
}
export function parseWorkBuddyApiKeys(value) {
let parsed;
try {
parsed = JSON.parse(value);
} catch (error) {
throw new Error("WorkBuddy API Key 列表已损坏,请重新配置", { cause: error });
}
if (!Array.isArray(parsed?.entries)) throw new Error("WorkBuddy API Key 列表格式无效,请重新配置");
return createWorkBuddyApiKeyStore(parsed.entries, parsed.activeId);
}
export function workBuddyApiKeyEntries(store) {
return (store?.entries ?? []).map(({ id, ref, label, createdAt, updatedAt }) => ({ id, ref, label, createdAt, updatedAt }));
}
function normalizeAccount(account, auth) {
// The account endpoint has returned both a flat object and wrapped objects
// across WorkBuddy versions. Keep all known wrappers in the lookup so a newly
// added account gets the same display metadata as an imported account.
const sources = [
account,
account?.account,
account?.user,
account?.userInfo,
account?.profile,
account?.data,
].filter((source) => source && typeof source === "object");
const read = (...keys) => textValue(...sources.flatMap((source) => keys.map((key) => source[key])));
const claims = tokenClaims(auth?.accessToken);
const userId = read("userId", "uid", "user_id", "id") ?? textValue(claims.userId, claims.uid, claims.user_id, claims.sub);
const enterpriseId = read("enterpriseId", "tenantId", "enterprise_id", "tenant_id")
?? textValue(claims.enterpriseId, claims.tenantId, claims.enterprise_id, claims.tenant_id);
const email = read("email", "mail", "emailAddress") ?? textValue(claims.email, claims.mail);
const uin = read("uin", "phoneNumber", "phone", "mobile", "mobilePhone")
?? textValue(claims.uin, claims.phoneNumber, claims.phone_number, claims.mobile);
const type = read("type", "accountType", "account_type");
const displayName = read("displayName", "name", "nickname", "username", "accountName")
?? uin
?? textValue(claims.displayName, claims.name, claims.nickname, claims.username, claims.preferred_username)
?? email;
return {
...(userId ? { userId } : {}),
...(enterpriseId ? { enterpriseId } : {}),
...(email ? { email } : {}),
...(uin ? { uin } : {}),
...(type ? { type } : {}),
...(displayName ? { displayName } : {}),
};
}
export function workBuddySessionId(session) {
const account = normalizeAccount(session?.account, session?.auth);
if (account.userId) return `user:${account.userId}`;
if (account.email) return `email:${account.email}`;
if (account.enterpriseId) return `enterprise:${account.enterpriseId}`;
const refreshToken = session?.auth?.refreshToken ?? session?.auth?.accessToken;
if (!refreshToken) throw new Error("WorkBuddy 登录会话缺少账号标识和令牌");
return `token:${createHash("sha256").update(refreshToken).digest("hex")}`;
}
export function workBuddySessionLabel(session) {
const account = normalizeAccount(session?.account, session?.auth);
return account.displayName ?? account.email ?? account.userId ?? account.enterpriseId ?? `账号 ${workBuddySessionId(session).slice(-8)}`;
}
export function normalizeWorkBuddySessionEntry(session, now = Date.now()) {
const normalized = {
auth: calculateExpiresAt(session?.auth),
account: normalizeAccount(session?.account, session?.auth),
};
if (!normalized.auth.accessToken || !normalized.auth.refreshToken) throw new Error("WorkBuddy 登录会话无效");
const id = typeof session?.id === "string" && session.id.trim() ? session.id : workBuddySessionId(normalized);
const createdAt = Number.isFinite(session?.createdAt) ? session.createdAt : now;
return {
id,
label: typeof session?.label === "string" && session.label.trim() ? session.label : workBuddySessionLabel(normalized),
createdAt,
updatedAt: Number.isFinite(session?.updatedAt) ? session.updatedAt : now,
...normalized,
};
}
export function createWorkBuddySessionStore(entries = [], activeId) {
const sessions = [];
for (const entry of entries) {
const normalized = normalizeWorkBuddySessionEntry(entry);
if (!sessions.some((item) => item.id === normalized.id)) sessions.push(normalized);
}
const selected = typeof activeId === "string" && sessions.some((entry) => entry.id === activeId) ? activeId : sessions[0]?.id;
return { version: 1, activeId: selected, sessions };
}
export function upsertWorkBuddySession(store, session, now = Date.now()) {
const current = createWorkBuddySessionStore(store?.sessions ?? [], store?.activeId);
const incoming = normalizeWorkBuddySessionEntry({ ...session, updatedAt: now }, now);
const index = current.sessions.findIndex((entry) => entry.id === incoming.id);
if (index >= 0) incoming.createdAt = current.sessions[index].createdAt;
const sessions = index >= 0
? current.sessions.map((entry, position) => position === index ? incoming : entry)
: [...current.sessions, incoming];
return { version: 1, activeId: incoming.id, sessions };
}
export function activeWorkBuddySession(store) {
return store?.sessions?.find((entry) => entry.id === store.activeId) ?? store?.sessions?.[0];
}
export function workBuddySessionAccounts(store) {
return (store?.sessions ?? []).map(({ id, label, account, createdAt, updatedAt }) => ({
id,
label,
accountName: label,
userId: account?.userId ?? null,
account,
createdAt,
updatedAt,
}));
}
export async function loginWorkBuddy(onAuthUrl, signal) {
const state = await request("/auth/state?platform=CLI", {
method: "POST",
headers: { ...REQUEST_HEADERS, ...NO_ACCOUNT_HEADERS },
body: "{}",
signal,
}, "创建 WorkBuddy 登录会话");
if (!state?.state || !state?.authUrl) throw new Error("WorkBuddy 登录接口没有返回登录地址");
try {
await openBrowser(state.authUrl);
onAuthUrl?.(state.authUrl, true);
} catch {
onAuthUrl?.(state.authUrl, false);
}
const auth = calculateExpiresAt(await poll(
`/auth/token?state=${encodeURIComponent(state.state)}`,
NO_ACCOUNT_HEADERS,
"等待 WorkBuddy 登录",
10 * 60_000,
signal,
));
if (!auth.accessToken || !auth.refreshToken) throw new Error("WorkBuddy 登录接口没有返回完整令牌");
const account = await poll(
`/login/account?state=${encodeURIComponent(state.state)}`,
{ ...enterpriseHeaders({ auth }), authorization: `Bearer ${auth.accessToken}`, ...NO_ID_HEADERS },
"获取 WorkBuddy 账号",
60_000,
signal,
);
return { auth, account: normalizeAccount(account, auth) };
}
export async function refreshWorkBuddySession(session, signal) {
if (!session?.auth?.refreshToken) throw new Error("WorkBuddy 登录会话缺少刷新令牌,请重新登录");
const auth = await request("/auth/token/refresh", {
method: "POST",
headers: {
...REQUEST_HEADERS,
...enterpriseHeaders(session),
"X-Refresh-Token": session.auth.refreshToken,
"X-Auth-Refresh-Source": "plugin",
},
body: "{}",
signal,
}, "刷新 WorkBuddy 登录令牌");
const fresh = calculateExpiresAt(auth);
const merged = { ...session.auth, ...fresh, refreshToken: fresh?.refreshToken ?? session.auth.refreshToken };
if (!merged.accessToken) throw new Error("WorkBuddy 刷新接口没有返回访问令牌");
return { auth: merged, account: normalizeAccount(session.account, merged) };
}
export function serializeWorkBuddySession(session) {
if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("WorkBuddy 登录会话无效");
return JSON.stringify({ auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account, session.auth) });
}
export function serializeWorkBuddySessions(store) {
const normalized = createWorkBuddySessionStore(store?.sessions ?? [], store?.activeId);
if (normalized.sessions.length === 0) throw new Error("WorkBuddy 登录账号列表为空");
return JSON.stringify(normalized);
}
export function parseWorkBuddySession(value) {
let session;
try {
session = JSON.parse(value);
} catch (error) {
throw new Error("WorkBuddy 登录凭据已损坏,请重新登录", { cause: error });
}
if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("WorkBuddy 登录凭据不完整,请重新登录");
return { auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account, session.auth) };
}
export function parseWorkBuddySessions(value) {
let parsed;
try {
parsed = JSON.parse(value);
} catch (error) {
throw new Error("WorkBuddy 登录账号列表已损坏,请重新登录", { cause: error });
}
if (Array.isArray(parsed?.sessions)) return createWorkBuddySessionStore(parsed.sessions, parsed.activeId);
if (parsed?.auth) {
const session = parseWorkBuddySession(value);
return createWorkBuddySessionStore([session], workBuddySessionId(session));
}
throw new Error("WorkBuddy 登录账号列表格式无效,请重新登录");
}
export function sessionNeedsRefresh(session, now = Date.now()) {
const expiresAt = Number(session?.auth?.expiresAt);
if (Number.isFinite(expiresAt)) return expiresAt <= now + 2 * 60_000;
try {
const payload = JSON.parse(Buffer.from(session.auth.accessToken.split(".")[1], "base64url").toString("utf8"));
return Number.isFinite(payload.exp) ? payload.exp * 1000 <= now + 2 * 60_000 : true;
} catch {
return true;
}
}
export function sessionCacheDeadline(session, now = Date.now()) {
const expiresAt = Number(session?.auth?.expiresAt);
return Number.isFinite(expiresAt)
? Math.max(now, Math.min(expiresAt - 2 * 60_000, now + 30 * 60_000))
: now + 5 * 60_000;
}