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

+448
View File
@@ -0,0 +1,448 @@
const DEFAULT_BILLING_HOST = "https://www.codebuddy.cn";
const BROWSER_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36";
const ENTERPRISE_EDITIONS = new Set(["ultimate", "exclusive"]);
const REMAINING_FIELDS = [
"SlicePeriodCapacityRemainPrecise",
"SlicePeriodCapacityRemain",
"CycleCapacityRemainPrecise",
"CycleCapacityRemain",
"CapacityRemainPrecise",
"CapacityRemain",
"RemainPrecise",
"Remain",
"Remaining",
"Balance",
];
const TOTAL_FIELDS = [
"SlicePeriodCapacitySizePrecise",
"SlicePeriodCapacitySize",
"CycleCapacitySizePrecise",
"CycleCapacitySize",
"CycleCapacityPrecise",
"CycleCapacity",
"CapacityPrecise",
"Capacity",
"TotalCapacityPrecise",
"TotalCapacity",
"PackageCapacity",
"Quota",
"Amount",
];
const EXPIRY_FIELDS = [
"DeductionEndTime",
"ExpiredTime",
"SlicePeriodEndTime",
"PackageEndTime",
"EndTime",
"CycleEndTime",
"ExpireTime",
"ExpirationTime",
"ValidEndTime",
"ValidPeriodEndTime",
"EndAt",
"ExpireAt",
];
const LABEL_FIELDS = ["PackageName", "PackageTypeName", "AccountName", "ProductName", "Name", "RuleName", "Description"];
const MAX_USAGE_PAGES = 100;
const PAGE_SIZE = 100;
function normalizeHost(value) {
const fallback = new URL(DEFAULT_BILLING_HOST);
if (typeof value !== "string" || !value.trim()) return fallback.origin;
try {
const candidate = new URL(value.includes("://") ? value : `https://${value}`);
if (candidate.protocol !== "https:") return fallback.origin;
const host = candidate.hostname.toLowerCase();
if (!["codebuddy.cn", "www.codebuddy.cn", "workbuddy.cn", "www.workbuddy.cn"].includes(host)) return fallback.origin;
return candidate.origin;
} catch {
return fallback.origin;
}
}
function billingHost(session) {
return normalizeHost(session?.auth?.domain);
}
function authToken(session) {
const token = typeof session?.auth?.accessToken === "string" ? session.auth.accessToken.trim() : "";
if (!token) throw new Error("WorkBuddy 登录令牌为空");
return token;
}
function billingHeaders(session, host, enterpriseId) {
const headers = {
accept: "application/json, text/plain, */*",
"content-type": "application/json",
"x-client-platform": "web",
origin: host,
referer: `${host}/profile/plans-usage`,
authorization: `Bearer ${authToken(session)}`,
"user-agent": BROWSER_USER_AGENT,
};
const domain = typeof session?.auth?.domain === "string" ? session.auth.domain.trim() : "";
if (domain) headers["x-domain"] = domain;
const userId = typeof session?.account?.userId === "string" ? session.account.userId.trim() : "";
if (userId) headers["x-user-id"] = userId;
if (enterpriseId) {
headers["x-enterprise-id"] = String(enterpriseId);
headers["x-tenant-id"] = String(enterpriseId);
}
return headers;
}
function timeoutSignal(timeoutMs, externalSignal) {
if (externalSignal) return externalSignal;
if (typeof AbortSignal?.timeout === "function") return AbortSignal.timeout(timeoutMs);
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs).unref?.();
return controller.signal;
}
async function readJson(response, action) {
const raw = await response.text();
if (!raw.trim()) throw new Error(`${action}返回空响应(HTTP ${response.status})`);
let payload;
try {
payload = JSON.parse(raw);
} catch (error) {
throw new Error(`${action}返回了无法解析的数据`, { cause: error });
}
if (!response.ok) throw new Error(`${action} HTTP ${response.status}: ${raw.slice(0, 160)}`);
if (payload?.code !== undefined && payload.code !== null && payload.code !== 0) {
throw new Error(`${action}失败(${payload.msg ?? payload.message ?? `code=${payload.code}`})`);
}
return payload;
}
function firstNumber(value, fields) {
for (const field of fields) {
const raw = value?.[field];
if (raw === undefined || raw === null || raw === "") continue;
const number = Number(raw);
if (Number.isFinite(number)) return number;
}
return null;
}
function parseTimestamp(value) {
if (value === undefined || value === null || value === "") return null;
if (typeof value === "number" || /^\d+(?:\.\d+)?$/.test(String(value).trim())) {
const number = Number(value);
if (!Number.isFinite(number)) return null;
return number < 1e12 ? Math.round(number * 1000) : Math.round(number);
}
const parsed = Date.parse(String(value).replace(/^(\d{4}-\d\d-\d\d)\s+/, "$1T"));
return Number.isFinite(parsed) ? parsed : null;
}
function firstTimestamp(value, fields) {
for (const field of fields) {
const parsed = parseTimestamp(value?.[field]);
if (parsed !== null) return parsed;
}
return null;
}
function firstText(value, fields) {
for (const field of fields) {
const text = value?.[field];
if (typeof text === "string" && text.trim()) return text.trim();
}
return "";
}
function extractAccounts(payload) {
const candidates = [
payload?.data?.Response?.Data?.Accounts,
payload?.data?.data?.Response?.Data?.Accounts,
payload?.data?.accounts,
payload?.data?.data?.accounts,
payload?.Response?.Data?.Accounts,
];
return candidates.find(Array.isArray) ?? [];
}
function extractCreditSegments(accounts, source = "积分") {
return (Array.isArray(accounts) ? accounts : [])
.flatMap((account) => {
const details = Array.isArray(account?.SlicePeriodUsageDetails) && account.SlicePeriodUsageDetails.length
? account.SlicePeriodUsageDetails.map((detail) => ({ ...account, ...detail }))
: [account];
return details.map((item) => {
const remaining = firstNumber(item, REMAINING_FIELDS);
if (remaining === null || remaining <= 0) return null;
const total = firstNumber(item, TOTAL_FIELDS);
return {
remaining: Number(remaining.toFixed(2)),
total: Number((total === null ? remaining : Math.max(total, remaining)).toFixed(2)),
expiresAt: firstTimestamp(item, EXPIRY_FIELDS),
source: firstText(item, LABEL_FIELDS) || source,
packageCode: item?.PackageCode ? String(item.PackageCode) : "",
};
});
})
.filter(Boolean);
}
function sortSegments(segments) {
return (Array.isArray(segments) ? segments : [])
.filter((segment) => segment && Number(segment.remaining) > 0)
.map((segment) => ({
remaining: Number(Number(segment.remaining).toFixed(2)),
total: Number(Number(segment.total || segment.remaining).toFixed(2)),
expiresAt: segment.expiresAt === null || segment.expiresAt === undefined ? null : Number(segment.expiresAt),
source: String(segment.source || "积分"),
packageCode: String(segment.packageCode || ""),
}))
.sort((left, right) => {
if (left.expiresAt === null && right.expiresAt !== null) return 1;
if (left.expiresAt !== null && right.expiresAt === null) return -1;
return (left.expiresAt || 0) - (right.expiresAt || 0);
});
}
function mergeSegments(segments) {
const merged = new Map();
for (const segment of Array.isArray(segments) ? segments : []) {
if (!segment || Number(segment.remaining) <= 0) continue;
const key = [segment.packageCode || segment.source || "积分", segment.expiresAt ?? "unknown"].join("|");
const previous = merged.get(key);
if (previous) {
previous.remaining += Number(segment.remaining) || 0;
previous.total += Number(segment.total || segment.remaining) || 0;
} else {
merged.set(key, {
remaining: Number(segment.remaining) || 0,
total: Number(segment.total || segment.remaining) || 0,
expiresAt: segment.expiresAt === undefined ? null : segment.expiresAt,
source: String(segment.source || "积分"),
packageCode: String(segment.packageCode || ""),
});
}
}
return sortSegments(Array.from(merged.values()));
}
function buildCreditResourceBody(now = new Date()) {
const end = new Date(now.getTime());
end.setFullYear(end.getFullYear() + 101);
const format = (date) => {
const pad = (value) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
return {
PageNumber: 1,
PageSize: 100,
ProductCode: "p_tcaca",
Status: [0, 3],
PackageEndTimeRangeBegin: format(now),
PackageEndTimeRangeEnd: format(end),
};
}
async function postJson(url, session, body, action, options = {}) {
const response = await (options.fetchImpl || globalThis.fetch)(url, {
method: "POST",
headers: billingHeaders(session, options.host, options.enterpriseId),
body: JSON.stringify(body),
signal: timeoutSignal(options.timeoutMs ?? 12_000, options.signal),
});
return readJson(response, action);
}
function enterpriseUsage(payload) {
const candidates = [payload?.data, payload?.data?.data, payload?.data?.Response?.Data, payload];
const data = candidates.find((value) => value && typeof value === "object" && ("limitNum" in value || "LimitNum" in value));
if (!data) return null;
const limitNum = Number(data.limitNum ?? data.LimitNum);
if (!Number.isFinite(limitNum)) return null;
const reset = firstTimestamp(data, ["cycleResetTime", "CycleResetTime", "CycleResetTimeMs"]);
if (limitNum === -1) return { unlimited: true, credits: null, total: null, count: 0, segments: [], cycleResetTime: reset };
const credit = Number(data.credit ?? data.Credit);
const used = Number.isFinite(credit) ? credit : 0;
const credits = Math.max(0, limitNum - used);
return {
unlimited: false,
credits: Number(credits.toFixed(2)),
total: Number(limitNum.toFixed(2)),
count: 1,
segments: sortSegments([{ remaining: credits, total: limitNum, expiresAt: reset, source: "企业配额" }]),
cycleResetTime: reset,
};
}
async function queryPersonalCredits(session, host, options = {}) {
const payload = await postJson(`${host}/v2/billing/meter/get-user-resource`, session, buildCreditResourceBody(), "WorkBuddy 积分接口", { ...options, host });
const accounts = extractAccounts(payload);
let credits = 0;
for (const account of accounts) {
const remaining = firstNumber(account, [
"CycleCapacityRemainPrecise",
"CycleCapacityRemain",
"CapacityRemainPrecise",
"CapacityRemain",
]);
if (remaining !== null) credits += remaining;
}
// [自用定制 2026-10-10] 提取会员套餐:账单里 CapacityType=2 是套餐/会员类,
// CapacityType=1 是签到/活动/兑换码赠送的积分包。
const planCandidates = accounts.filter((account) => String(account?.CapacityType) === "2");
const plan = planCandidates.find((account) => /高级版|标准版|旗舰版|专业版/.test(String(account?.PackageName || "")))
?? planCandidates[0];
return {
credits: Number(credits.toFixed(2)),
count: accounts.length,
totalDosage: payload?.data?.Response?.Data?.TotalDosage ?? payload?.data?.data?.Response?.Data?.TotalDosage ?? null,
segments: mergeSegments(extractCreditSegments(accounts)),
unlimited: false,
cycleResetTime: null,
planName: plan?.PackageName ?? null,
planCycleEnd: plan?.CycleEndTime ?? null,
};
}
async function resolveEnterpriseId(session, host, options = {}) {
const uid = session?.account?.userId;
if (!uid) return "";
const response = await (options.fetchImpl || globalThis.fetch)(`${host}/console/accounts`, {
method: "GET",
headers: billingHeaders(session, host),
signal: timeoutSignal(8_000, options.signal),
});
const payload = await readJson(response, "WorkBuddy 企业账号接口");
const accounts = payload?.data?.accounts;
const first = Array.isArray(accounts) ? accounts[0] : undefined;
return typeof first?.enterpriseId === "string" ? first.enterpriseId.trim() : "";
}
async function queryEnterpriseCredits(session, host, enterpriseId, options = {}) {
const payload = await postJson(`${host}/v2/billing/meter/get-enterprise-user-usage`, session, {}, "WorkBuddy 企业积分接口", {
...options,
host,
enterpriseId,
});
const parsed = enterpriseUsage(payload);
if (!parsed) throw new Error("WorkBuddy 企业积分接口返回数据无法解析");
return parsed;
}
function pad2(value) {
return String(value).padStart(2, "0");
}
function formatLocalDateTime(date) {
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
}
function localDateString(date) {
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
}
function parseUsageTime(value) {
return parseTimestamp(value);
}
function usageRows(payload) {
const data = payload?.data;
const rows = data && Array.isArray(data.data) ? data.data : data && Array.isArray(data.rows) ? data.rows : [];
const total = Number(data?.total);
return { rows, total: Number.isSafeInteger(total) && total >= 0 ? total : rows.length };
}
async function queryTodayUsage(session, host, options = {}) {
const now = new Date();
const start = new Date(now.getTime());
start.setHours(0, 0, 0, 0);
const fetchImpl = options.fetchImpl || globalThis.fetch;
const records = [];
let expectedTotal = null;
let fetched = 0;
for (let pageNum = 1; pageNum <= MAX_USAGE_PAGES; pageNum += 1) {
const payload = await postJson(`${host}/billing/meter/get-user-request-usage`, session, {
startTime: formatLocalDateTime(start),
endTime: formatLocalDateTime(now),
pageNum,
pageSize: PAGE_SIZE,
}, "WorkBuddy 今日请求量接口", { ...options, host, fetchImpl, timeoutMs: options.usageTimeoutMs ?? 8_000 });
const page = usageRows(payload);
if (expectedTotal === null) expectedTotal = page.total;
if (!page.rows.length) break;
for (const row of page.rows) {
const requestTime = parseUsageTime(row?.requestTime ?? row?.RequestTime ?? row?.createdAt);
const credit = Number(row?.credit ?? row?.Credit ?? 0);
if (requestTime === null || !Number.isFinite(credit) || credit < 0) continue;
records.push({ requestTime, credit });
}
fetched += page.rows.length;
if (fetched >= expectedTotal || page.rows.length < PAGE_SIZE) break;
}
const used = records.reduce((sum, record) => sum + record.credit, 0);
return {
date: localDateString(now),
used: Number(used.toFixed(2)),
count: records.length,
synced: true,
};
}
async function retry(task, attempts = 2) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
}
}
throw lastError;
}
export async function fetchWorkBuddyCredits(session, options = {}) {
const host = billingHost(session);
const account = session?.account && typeof session.account === "object" ? session.account : {};
let creditResult;
let creditError = null;
try {
const enterpriseId = typeof account.enterpriseId === "string" ? account.enterpriseId.trim() : "";
const enterpriseEdition = typeof account.type === "string" && ENTERPRISE_EDITIONS.has(account.type.toLowerCase());
if (enterpriseId || enterpriseEdition) {
const resolvedId = enterpriseId || await retry(() => resolveEnterpriseId(session, host, options));
if (!resolvedId) throw new Error("企业账号缺少 enterpriseId");
creditResult = await retry(() => queryEnterpriseCredits(session, host, resolvedId, options));
} else {
creditResult = await retry(() => queryPersonalCredits(session, host, options));
}
} catch (error) {
creditError = error instanceof Error ? error.message : String(error);
creditResult = { credits: null, count: 0, totalDosage: null, segments: [], unlimited: false, cycleResetTime: null };
}
let todayUsage;
let todayUsageError = null;
try {
todayUsage = await retry(() => queryTodayUsage(session, host, options));
} catch (error) {
todayUsageError = error instanceof Error ? error.message : String(error);
}
return {
...creditResult,
creditError,
todayUsage: todayUsage ?? null,
todayUsageError,
};
}
export const __testing = Object.freeze({
billingHost,
buildCreditResourceBody,
enterpriseUsage,
extractAccounts,
extractCreditSegments,
formatLocalDateTime,
mergeSegments,
normalizeHost,
queryTodayUsage,
sortSegments,
});