dsh-subusage 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -43
- package/lib/client.js +110 -38
- package/lib/index.js +170 -67
- package/lib/volcengine.js +2 -2
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { MimoLogin } from "./mimo-login.js";
|
|
|
11
11
|
import { parseVolcAgentPlan, parseVolcCodingPlan, parseVolcSeatAfp, parseVolcSeatCoding, parseVolcSeatIds, volcError, volcSignature } from "./volcengine.js";
|
|
12
12
|
|
|
13
13
|
const name = "dsh-subusage";
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.12.0";
|
|
15
15
|
const inject = ["llm"];
|
|
16
16
|
const PROVIDERS = {
|
|
17
17
|
"zai-coding-cn": { label: "Z.ai", envName: "ZAI_CODING_CN_API_KEY" },
|
|
@@ -64,7 +64,8 @@ const PROVIDERS = {
|
|
|
64
64
|
// 国内那两个 id 药丸仍认:Client 的 ROUTE_PROVIDER_ALIAS 把它们归并到 arkcli 路由。
|
|
65
65
|
// DeepSeek 官方:余额型(不是订阅窗口),与推理**同一把** API Key,端点与响应见官方文档
|
|
66
66
|
// https://api-docs.deepseek.com/zh-cn/api/get-user-balance
|
|
67
|
-
"deepseek": { label: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
|
|
67
|
+
"deepseek": { label: "DeepSeek API", envName: "DEEPSEEK_API_KEY" },
|
|
68
|
+
"deepseek-account": { label: "DeepSeek 账号", envName: "", managedByPlugin: true },
|
|
68
69
|
// OpenRouter:key 限额与用量走 /key(任意 key 可用),账户余额走 /credits(**只对
|
|
69
70
|
// management / provisioning key 开放**,普通推理 key 会被拒 → 静默降级为只显示限额)。
|
|
70
71
|
"openrouter": { label: "OpenRouter", envName: "OPENROUTER_API_KEY", defaultEnabled: false },
|
|
@@ -112,6 +113,7 @@ const PROVIDER_ALIASES = {
|
|
|
112
113
|
minimax: ["minimax-cn", "minimax"], "海螺": ["minimax-cn", "minimax"],
|
|
113
114
|
commandcode: ["commandcode"], "command-code": ["commandcode"], "command code": ["commandcode"],
|
|
114
115
|
supergrok: ["xai-oauth"], grok: ["xai-oauth"], xai: ["xai-oauth"],
|
|
116
|
+
"deepseek-official": ["deepseek-account"], "deepseek api": ["deepseek"], "deepseek 账号": ["deepseek-account"],
|
|
115
117
|
codex: ["openai-codex"], chatgpt: ["openai-codex"], gpt: ["openai-codex"], openai: ["openai-codex"],
|
|
116
118
|
ark: ARK_IDS, volcengine: ARK_IDS, volc: ARK_IDS, "火山": ARK_IDS, "方舟": ARK_IDS, "火山方舟": ARK_IDS, doubao: ARK_IDS, "豆包": ARK_IDS
|
|
117
119
|
};
|
|
@@ -437,7 +439,7 @@ async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
|
437
439
|
// 避免用户被误导去重登录;两个条件都加显式括号,不依赖 &&/|| 优先级。
|
|
438
440
|
if ((response.status >= 300 && response.status < 400)) throw failure("subusage/response", "服务端返回了意外的重定向响应(HTTP 3xx)");
|
|
439
441
|
if ((response.status === 401 || response.status === 403)) throw failure("subusage/auth", "登录或凭据已失效");
|
|
440
|
-
if (!response.ok) {
|
|
442
|
+
if (!response.ok && !(init.notSubscribed404 === true && response.status === 404)) {
|
|
441
443
|
const retry = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
442
444
|
const raw = response.headers?.get?.("retry-after");
|
|
443
445
|
const retryAfterMs = raw ? (/^\d+(\.\d+)?$/.test(raw) ? Number(raw) * 1000 : Math.max(0, Date.parse(raw) - Date.now())) : undefined;
|
|
@@ -453,7 +455,8 @@ async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
|
453
455
|
} catch (e) { if (e instanceof RemoteError) throw e; throw failure("subusage/network", "读取响应失败", true); }
|
|
454
456
|
let body;
|
|
455
457
|
try { body = JSON.parse(text); } catch { throw failure("subusage/response", "响应不是合法 JSON"); }
|
|
456
|
-
if (
|
|
458
|
+
if (response.status === 404 && init.notSubscribed404 === true && !/^(NotFound\.BillingType|OperationDenied\.NotSubscribed)$/.test(body?.ResponseMetadata?.Error?.Code ?? "")) throw failure("subusage/http", "HTTP 404");
|
|
459
|
+
if ([401, 403, "401", "403"].includes(body?.code)) throw failure("subusage/auth", "登录或凭据已失效");
|
|
457
460
|
return body;
|
|
458
461
|
}
|
|
459
462
|
function numeric(v) { return typeof v === "number" && Number.isFinite(v) && v >= 0; }
|
|
@@ -466,7 +469,10 @@ const RANK = { month: 0, sub: 0, period: 0, week: 1, "7d": 1, day: 2, "5h": 2, r
|
|
|
466
469
|
function cascadeRateLimited(windows) {
|
|
467
470
|
const roots = windows.filter(w => w.status === "rate-limited");
|
|
468
471
|
for (const w of windows) {
|
|
469
|
-
const blockers = roots.filter(root =>
|
|
472
|
+
const blockers = roots.filter(root => {
|
|
473
|
+
const sourceScope = root.detail?.scope ?? "all", targetScope = w.detail?.scope ?? "all";
|
|
474
|
+
return (sourceScope === "all" || sourceScope === targetScope) && (RANK[root.kind] ?? 3) < (RANK[w.kind] ?? 3);
|
|
475
|
+
});
|
|
470
476
|
if (blockers.length) { if (w.status !== "rate-limited") w.cascade = true; w.status = "rate-limited"; w.detail = { ...w.detail, blockedBy: [...new Set(blockers.map(root => root.kind))] }; }
|
|
471
477
|
}
|
|
472
478
|
return windows;
|
|
@@ -514,18 +520,25 @@ export function normalizeZai(body) {
|
|
|
514
520
|
return { windows: cascadeRateLimited(windows), level: typeof data.level === "string" ? data.level : undefined };
|
|
515
521
|
}
|
|
516
522
|
export function normalizeMimo(balBody, detailBody, usageBody) {
|
|
517
|
-
const
|
|
518
|
-
|
|
523
|
+
const rawBal = balBody?.data;
|
|
524
|
+
const bal = rawBal && typeof rawBal.balance === "string" && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(rawBal.balance) && Number.isFinite(Number(rawBal.balance)) ? rawBal : undefined;
|
|
519
525
|
const data = usageBody?.data ?? {}; const detail = detailBody?.data ?? {};
|
|
520
526
|
const pick = (group, name) => Array.isArray(group?.items) ? group.items.find(i => i?.name === name) : undefined;
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
const
|
|
527
|
+
const pools = [pick(data.usage, "plan_total_token"), pick(data.monthUsage, "month_total_token")].filter(Boolean);
|
|
528
|
+
const pool = pools.find(row => row.limit !== 0) ?? pools[0];
|
|
529
|
+
const groups = ["usage", "monthUsage"].filter(key => Object.hasOwn(data, key)).map(key => data[key]);
|
|
530
|
+
const emptyGroups = groups.length > 0 && groups.every(group => group === null || Array.isArray(group?.items) && group.items.length === 0);
|
|
531
|
+
const detailReadable = detailBody?.data === null || detailBody?.data && typeof detailBody.data === "object" && !Array.isArray(detailBody.data);
|
|
532
|
+
const noSubscription = detailReadable && [undefined, 0].includes(detailBody?.code) && [undefined, 0].includes(usageBody?.code) && !detail.planName && !detail.planCode && (emptyGroups || pools.length > 0 && pools.every(row => row.limit === 0 && row.used === 0 && row.percent === 0));
|
|
533
|
+
const poolDetail = numeric(pool?.used) && numeric(pool?.limit) && pool.limit > 0 ? { used: pool.used, limit: pool.limit, unit: "credits" } : undefined;
|
|
534
|
+
const windows = pool && pool.limit !== 0 && numeric(pool.percent) ? [windowRow("sub", pool.percent * 100, detail.currentPeriodEnd, undefined, poolDetail)] : [];
|
|
535
|
+
const currency = typeof bal?.currency === "string" && /^[A-Z]{3}$/.test(bal.currency) ? bal.currency : "";
|
|
536
|
+
const extras = bal ? [{ kind: "balance", value: `${bal.balance} ${currency}`.trim() }] : [];
|
|
537
|
+
if (!bal && !windows.length) throw failure("subusage/response", "MiMo 没有可读的余额或套餐用量");
|
|
538
|
+
if (!bal) extras.push({ kind: "note", value: "余额暂不可读;套餐用量已更新" });
|
|
526
539
|
const plan = detail.planName || detail.planCode;
|
|
527
540
|
if (typeof plan === "string") extras.push({ kind: "plan", value: plan });
|
|
528
|
-
return { windows, extras, balanceOk: true, coverage: windows.length ? "complete" : "partial" };
|
|
541
|
+
return { windows, extras, balanceOk: !!bal, ...(windows.length ? { subscribed: true } : noSubscription ? { subscribed: false } : {}), coverage: windows.length || noSubscription && !!bal ? "complete" : "partial" };
|
|
529
542
|
}
|
|
530
543
|
export function normalizeOpencodeGo(body) {
|
|
531
544
|
const src = body?.usage ?? body;
|
|
@@ -558,7 +571,12 @@ export function normalizeNanoGpt(body, balanceBody) {
|
|
|
558
571
|
const extras = [{ kind: "plan", value: body.state }];
|
|
559
572
|
const usd = typeof balanceBody?.usd_balance === "string" && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(balanceBody.usd_balance.trim()) ? balanceBody.usd_balance.trim() : undefined;
|
|
560
573
|
if (usd) extras.push({ kind: "balance", value: `${usd} USD` });
|
|
561
|
-
|
|
574
|
+
const routing = body?.routing;
|
|
575
|
+
const mode = routing?.recommendedMode;
|
|
576
|
+
const availability = mode === "unavailable" ? { availability: "blocked", availabilityReason: "routing-policy" }
|
|
577
|
+
: mode === "subscription" ? { availability: routing.subscriptionQuotaAvailable === true ? "included" : "unknown", availabilityReason: "routing-advice" }
|
|
578
|
+
: mode === "paygo" ? { availability: usd !== undefined && Number.isFinite(Number(usd)) && Number(usd) > 0 && routing.paidSpendPolicyAllowsBalance !== false ? "paygo" : "unknown", availabilityReason: "routing-advice" } : {};
|
|
579
|
+
return { windows: cascadeRateLimited(windows), extras, coverage: (windows.length || usd !== undefined || mode === "unavailable") && !partial ? "complete" : "partial", ...availability };
|
|
562
580
|
}
|
|
563
581
|
// 官方 CLI 的计数消歧规则:有 remaining_percent 时按它校准;旧响应 usage_count 是剩余。
|
|
564
582
|
// https://github.com/MiniMax-AI/cli/blob/main/src/utils/quota.ts
|
|
@@ -602,7 +620,8 @@ export function normalizeMinimax(body) {
|
|
|
602
620
|
const reset = numeric(endTime) && endTime > 0 && Number.isFinite(new Date(endTime).getTime()) ? new Date(endTime).toISOString() : undefined;
|
|
603
621
|
windows.push(windowRow(kind, usedPercent, reset, undefined, detail));
|
|
604
622
|
}
|
|
605
|
-
|
|
623
|
+
const unlimited = !windows.length && row.current_interval_status === 3 && row.current_weekly_status === 3;
|
|
624
|
+
return { windows: cascadeRateLimited(windows), extras: unlimited ? [{ kind: "quota-status", value: "unlimited" }] : [], coverage: windows.length || [row.current_interval_status, row.current_weekly_status].includes(3) ? "complete" : "partial" };
|
|
606
625
|
}
|
|
607
626
|
export function normalizeCommandCode(body, subscriptionBody) {
|
|
608
627
|
// 输入是 Command Code 计费接口(/alpha/billing/credits)的原始响应体。
|
|
@@ -642,7 +661,11 @@ export function normalizeCommandCode(body, subscriptionBody) {
|
|
|
642
661
|
if (reported.length) extras.push({ kind: "balance", value: `${Math.round(reported.reduce((sum, v) => sum + Math.max(0, v), 0) * 100) / 100} credits` });
|
|
643
662
|
// 月池耗尽并不证明已购/赠送池或短窗口也不可用;只级联接口的周/5小时限制。
|
|
644
663
|
cascadeRateLimited(windows.filter(w => w.kind !== "month"));
|
|
645
|
-
|
|
664
|
+
const exhausted = windows.some(w => w.status === "rate-limited");
|
|
665
|
+
const purchased = credits.purchasedCredits;
|
|
666
|
+
const availability = exhausted && numeric(purchased) && purchased > 0 ? { availability: "paygo", availabilityReason: "paid-credits" }
|
|
667
|
+
: exhausted ? { availability: "blocked", availabilityReason: "quota-exhausted" } : {};
|
|
668
|
+
return { windows, extras, coverage: hasMonthlyBalance || windows.length || reported.length ? "complete" : "partial", ...availability };
|
|
646
669
|
}
|
|
647
670
|
// SuperGrok / X Premium 统一用量池:GetGrokCreditsConfig 响应(官方 grok CLI 的
|
|
648
671
|
// /v1/billing?format=credits)。creditUsagePercent 是“已用”百分比(0-100),剩余由
|
|
@@ -688,11 +711,14 @@ export function normalizeSiliconFlow(body) {
|
|
|
688
711
|
}
|
|
689
712
|
// Codex / ChatGPT 订阅用量:rate_limit.primary_window 与 secondary_window 各含 used_percent 与
|
|
690
713
|
// resets_at。免费档的次窗口是 30 天,所以有 window_seconds 时按它判窗口名,缺失时按位置回退 5h / 7d。
|
|
691
|
-
// credits 只在真正报告了可用余额时显示:has_credits=false、
|
|
714
|
+
// credits 只在真正报告了可用余额时显示:has_credits=false、balance="0" 不画余额;unlimited 单独保留状态,
|
|
692
715
|
// 不把「无额度」画成 0。
|
|
693
716
|
// https://raw.githubusercontent.com/steipete/CodexBar/main/docs/codex.md
|
|
694
717
|
export function normalizeCodexUsage(body) {
|
|
695
718
|
const windows = [];
|
|
719
|
+
const plan = typeof body?.plan_type === "string" ? body.plan_type.trim() : "";
|
|
720
|
+
// 当前 Pro 三档无短窗;未知档位的缺失时长不按主窗口位置猜成 5h。
|
|
721
|
+
const weeklyOnly = ["pro", "prolite", "promax"].includes(plan.toLowerCase());
|
|
696
722
|
const windowKind = (row, fallback) => {
|
|
697
723
|
const seconds = row?.window_seconds ?? row?.limit_window_seconds;
|
|
698
724
|
if (!Number.isFinite(seconds) || seconds <= 0) return fallback;
|
|
@@ -709,22 +735,48 @@ export function normalizeCodexUsage(body) {
|
|
|
709
735
|
windows.push(windowRow(windowKind(row, fallback), row.used_percent, reset));
|
|
710
736
|
};
|
|
711
737
|
const limits = body?.rate_limit && typeof body.rate_limit === "object" && !Array.isArray(body.rate_limit) ? body.rate_limit : {};
|
|
712
|
-
|
|
713
|
-
push(limits.
|
|
714
|
-
|
|
715
|
-
if (!windows.length) return { windows: [], extras: [{ kind: "plan", value: "未检测到订阅额度窗口" }], coverage: "partial" };
|
|
738
|
+
const positional = !plan || ["plus", "team", "business"].includes(plan.toLowerCase());
|
|
739
|
+
push(limits.primary_window, weeklyOnly ? "week" : positional ? "5h" : "rolling");
|
|
740
|
+
push(limits.secondary_window, weeklyOnly ? "week" : positional ? "7d" : "rolling");
|
|
716
741
|
const extras = [];
|
|
717
|
-
// 实测:plan_type 给套餐档位(如 "plus"),值得显示。
|
|
718
|
-
const plan = typeof body?.plan_type === "string" && body.plan_type.trim() ? body.plan_type.trim() : "";
|
|
719
742
|
if (plan) extras.push({ kind: "plan", value: plan });
|
|
720
743
|
const credits = body?.credits && typeof body.credits === "object" && !Array.isArray(body.credits) ? body.credits : {};
|
|
721
744
|
const balance = typeof credits.balance === "string" && credits.balance.trim() ? credits.balance.trim() : typeof credits.balance === "number" ? String(credits.balance) : "";
|
|
722
|
-
|
|
723
|
-
|
|
745
|
+
const creditBalance = balance && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(balance) && Number.isFinite(Number(balance)) ? Number(balance) : undefined;
|
|
746
|
+
if (credits.has_credits === true && credits.unlimited !== true && creditBalance !== undefined) extras.push({ kind: "balance", value: balance });
|
|
747
|
+
if (credits.has_credits === true && credits.unlimited === true) extras.push({ kind: "credit-status", value: "unlimited" });
|
|
748
|
+
const control = body?.spend_control;
|
|
749
|
+
const member = control?.individual_limit;
|
|
750
|
+
const memberLimit = Number(member?.limit), memberUsed = member?.used == null ? NaN : Number(member.used);
|
|
751
|
+
if (member && Number.isFinite(memberLimit) && memberLimit > 0 && Number.isFinite(memberUsed) && memberUsed >= 0) {
|
|
752
|
+
const reset = numeric(member.reset_at) && member.reset_at > 0 ? new Date(member.reset_at * 1000).toISOString() : undefined;
|
|
753
|
+
windows.push({ ...windowRow("period", memberUsed / memberLimit * 100, reset, undefined, { used: memberUsed, limit: memberLimit, unit: "credits", scope: "member-budget" }), groupLabel: "Member budget" });
|
|
754
|
+
}
|
|
755
|
+
const reached = body?.rate_limit_reached_type?.kind;
|
|
756
|
+
const restricted = control?.reached === true || ["workspace_owner_credits_depleted", "workspace_member_credits_depleted", "workspace_owner_usage_limit_reached", "workspace_member_usage_limit_reached"].includes(reached);
|
|
757
|
+
const exhausted = windows.some(w => w.status === "rate-limited") || limits.limit_reached === true || limits.allowed === false;
|
|
758
|
+
const creditFallback = credits.has_credits === true && (credits.unlimited === true || creditBalance > 0);
|
|
759
|
+
const availability = restricted ? { availability: "blocked", availabilityReason: "spend-control" }
|
|
760
|
+
: exhausted && creditFallback ? { availability: "unknown", availabilityReason: "credit-eligibility" }
|
|
761
|
+
: exhausted ? { availability: "blocked", availabilityReason: "quota-exhausted" } : {};
|
|
762
|
+
if (!windows.length && !extras.some(e => e.kind !== "plan") && !restricted) extras.push({ kind: "note", value: "未检测到订阅额度窗口" });
|
|
763
|
+
const reported = windows.length || extras.some(e => e.kind !== "plan" && e.kind !== "note") || restricted;
|
|
764
|
+
// 额外特性池未按所选模型映射前,不声称已完整覆盖它们。
|
|
765
|
+
const partial = Array.isArray(body?.additional_rate_limits) && body.additional_rate_limits.length > 0;
|
|
766
|
+
return { windows: cascadeRateLimited(windows), extras, coverage: reported && !partial ? "complete" : "partial", ...availability };
|
|
724
767
|
}
|
|
725
768
|
// DeepSeek 官方余额(余额型):GET https://api.deepseek.com/user/balance,Bearer 与推理同一把 Key。
|
|
726
769
|
// 响应 { is_available, balance_infos: [{ currency, total_balance, granted_balance, topped_up_balance }] },
|
|
727
770
|
// 金额是**字符串十进制**。余额没有上限也就没有百分比:不产出窗口,只产出 extras。
|
|
771
|
+
// DSH 账号服务已完成鉴权与余额响应投影;本插件不读取账号 grant/token。
|
|
772
|
+
export function normalizeDeepseekAccountBalance(body) {
|
|
773
|
+
const decimal = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
774
|
+
if (body?.status !== "ready" || !Array.isArray(body.value)) throw failure("subusage/response", "本机 DeepSeek 账号余额读取失败,请检查 DSH 的账号与余额页面", true);
|
|
775
|
+
const wallets = body.value.filter(row => ["CNY", "USD"].includes(row?.currency) && typeof row.balance === "string" && decimal.test(row.balance));
|
|
776
|
+
if (!wallets.length || wallets.length !== body.value.length) throw failure("subusage/response", "本机 DeepSeek 账号没有有效的充值余额信息");
|
|
777
|
+
const row = wallets.find(row => row.currency === "CNY") ?? wallets[0];
|
|
778
|
+
return { windows: [], extras: [{ kind: "balance", value: `${row.balance} ${row.currency}` }], coverage: "complete" };
|
|
779
|
+
}
|
|
728
780
|
export function normalizeDeepseekBalance(body) {
|
|
729
781
|
if (typeof body?.is_available !== "boolean") throw failure("subusage/response", "Invalid DeepSeek balance response");
|
|
730
782
|
const infos = Array.isArray(body.balance_infos) ? body.balance_infos : [];
|
|
@@ -761,14 +813,17 @@ export function normalizeOpenRouter(keyBody, creditBody) {
|
|
|
761
813
|
const total = num(credits?.total_credits), spent = num(credits?.total_usage);
|
|
762
814
|
if (total !== undefined && spent !== undefined) extras.push({ kind: "balance", value: `${Math.round(Math.max(0, total - spent) * 100) / 100} USD` });
|
|
763
815
|
if (data.is_free_tier === true) extras.push({ kind: "plan", value: "Free tier" });
|
|
764
|
-
if (!windows.length &&
|
|
765
|
-
|
|
816
|
+
if (!windows.length && num(data.usage) !== undefined) extras.push({ kind: "spend", value: `${Math.round(data.usage * 100) / 100} USD` });
|
|
817
|
+
if (!windows.length && !extras.length) throw failure("subusage/response", "OpenRouter 响应没有可用的限额、余额或用量");
|
|
818
|
+
return { windows: cascadeRateLimited(windows), extras, coverage: windows.length || extras.some(e => e.kind !== "plan") ? "complete" : "partial" };
|
|
766
819
|
}
|
|
767
820
|
// ── 余额型聚合商(各家单位与形状都不同,逐个显式换算,不做通用折算)────────────────
|
|
768
821
|
/** Novita:金额单位是 1/10000 USD(10000 = $1.00)。 */
|
|
769
822
|
export function normalizeNovitaBalance(body) {
|
|
770
|
-
|
|
771
|
-
const
|
|
823
|
+
// 官方金额为十进制字符串;不接受空串、指数或十六进制等非金额写法。
|
|
824
|
+
const raw = body?.availableBalance;
|
|
825
|
+
const available = typeof raw === "number" && Number.isFinite(raw) ? raw
|
|
826
|
+
: typeof raw === "string" && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(raw.trim()) && Number.isFinite(Number(raw)) ? Number(raw) : undefined;
|
|
772
827
|
if (available === undefined) throw failure("subusage/response", "Invalid Novita balance response");
|
|
773
828
|
return { windows: [], extras: [{ kind: "balance", value: `${Math.round(available / 100) / 100} USD` }], coverage: "complete" };
|
|
774
829
|
}
|
|
@@ -809,7 +864,7 @@ export function normalizeOllamaCloudUsage(body) {
|
|
|
809
864
|
windows.push(windowRow(kind, Math.min(100, usage * 100)));
|
|
810
865
|
}
|
|
811
866
|
if (!windows.length) throw failure("subusage/response", "Ollama Cloud 响应没有可用的额度窗口");
|
|
812
|
-
return { windows: cascadeRateLimited(windows), extras: [], coverage:
|
|
867
|
+
return { windows: cascadeRateLimited(windows), extras: [], coverage: "complete" };
|
|
813
868
|
}
|
|
814
869
|
/** Vercel AI Gateway:balance / total_used 是十进制字符串;没有限额也没有重置,只显示金额。 */
|
|
815
870
|
export function normalizeVercelCredits(body) {
|
|
@@ -826,12 +881,14 @@ export function normalizeZenmux(subscriptionBody, balanceBody) {
|
|
|
826
881
|
const num = value => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
827
882
|
const windows = [], extras = [];
|
|
828
883
|
const data = subscriptionBody?.data && typeof subscriptionBody.data === "object" && !Array.isArray(subscriptionBody.data) ? subscriptionBody.data : {};
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
884
|
+
for (const [field, kind] of [["quota_5_hour", "5h"], ["quota_7_day", "week"]]) {
|
|
885
|
+
const quota = data[field];
|
|
886
|
+
if (quota === undefined || quota === null) continue;
|
|
887
|
+
if (typeof quota !== "object" || Array.isArray(quota) || num(quota.usage_percentage) === undefined) throw failure("subusage/response", "Invalid ZenMux quota window");
|
|
832
888
|
const detail = num(quota.used_flows) !== undefined && num(quota.max_flows) !== undefined ? { used: quota.used_flows, limit: quota.max_flows, unit: "flows" } : undefined;
|
|
833
|
-
windows.push(windowRow(
|
|
889
|
+
windows.push(windowRow(kind, quota.usage_percentage * 100, typeof quota.resets_at === "string" ? quota.resets_at : undefined, undefined, detail));
|
|
834
890
|
}
|
|
891
|
+
// quota_monthly 只有上限,未报告实时用量;不从短窗拼造月百分比。
|
|
835
892
|
const tier = data.plan && typeof data.plan.tier === "string" ? data.plan.tier.trim() : "";
|
|
836
893
|
if (tier) extras.push({ kind: "plan", value: tier });
|
|
837
894
|
const balance = balanceBody?.data && typeof balanceBody.data === "object" && !Array.isArray(balanceBody.data) ? balanceBody.data : {};
|
|
@@ -841,7 +898,7 @@ export function normalizeZenmux(subscriptionBody, balanceBody) {
|
|
|
841
898
|
extras.push({ kind: "balance", value: `${Math.round(total * 100) / 100} ${currency}`.trim() });
|
|
842
899
|
}
|
|
843
900
|
if (!windows.length && !extras.length) throw failure("subusage/response", "ZenMux 响应没有可用的配额或余额");
|
|
844
|
-
return { windows: cascadeRateLimited(windows), extras, coverage: windows.length ? "complete" : "partial" };
|
|
901
|
+
return { windows: cascadeRateLimited(windows), extras, coverage: windows.length || total !== undefined ? "complete" : "partial", ...(["suspended", "banned"].includes(data.account_status) ? { availability: "blocked", availabilityReason: "account-restricted" } : {}) };
|
|
845
902
|
}
|
|
846
903
|
/**
|
|
847
904
|
* LiteLLM 的管理端点在 **proxy 根**(不是 /v1):去掉尾部斜杠与常见的 /v1 后缀,
|
|
@@ -858,7 +915,7 @@ export function litellmRoot(baseUrl) {
|
|
|
858
915
|
}
|
|
859
916
|
/**
|
|
860
917
|
* LiteLLM:`/key/info` 给 key 自身的预算与花费,`/user/info`|`/team/info` 给更完整的视图。
|
|
861
|
-
*
|
|
918
|
+
* 各预算同时生效,分别保留;**没有上限时不编造百分比**,只如实显示已用金额。
|
|
862
919
|
* 接口不给重置时刻时不显示重置(别把金额渲染成倒计时)。
|
|
863
920
|
* https://docs.litellm.ai/docs/proxy/virtual_keys
|
|
864
921
|
*/
|
|
@@ -868,20 +925,28 @@ export function normalizeLitellm(keyBody, budgetBody) {
|
|
|
868
925
|
// 真实响应是 `{ key, info: {...} }`;也接受直接把字段放在顶层。
|
|
869
926
|
const keyInfo = pick(keyBody?.info) ?? pick(keyBody);
|
|
870
927
|
if (!keyInfo) throw failure("subusage/response", "Invalid LiteLLM key info response");
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
928
|
+
const scopes = [["key", keyInfo]];
|
|
929
|
+
for (const body of Array.isArray(budgetBody) ? budgetBody : [budgetBody]) {
|
|
930
|
+
if (pick(body?.team_info)) scopes.push(["team", body.team_info]);
|
|
931
|
+
else if (pick(body?.user_info)) scopes.push(["user", body.user_info]);
|
|
932
|
+
else if (pick(body)) scopes.push(["scope", body]);
|
|
933
|
+
}
|
|
934
|
+
const windows = [], extras = [];
|
|
875
935
|
const round = value => Math.round(value * 100) / 100;
|
|
876
|
-
|
|
877
|
-
|
|
936
|
+
for (const [scope, row] of scopes) {
|
|
937
|
+
const spend = num(row.spend), maxBudget = num(row.max_budget);
|
|
938
|
+
if (spend === undefined || spend < 0) continue;
|
|
939
|
+
if (maxBudget !== undefined && maxBudget > 0) {
|
|
940
|
+
const resetAt = typeof row.budget_reset_at === "string" ? row.budget_reset_at : undefined;
|
|
941
|
+
windows.push({ ...windowRow("period", spend / maxBudget * 100, resetAt, undefined, { used: round(spend), limit: round(maxBudget), unit: "USD" }), ...(scopes.length > 1 ? { budgetScope: scope } : {}) });
|
|
942
|
+
} else extras.push({ kind: scope === "key" ? "spend" : `${scope}-spend`, value: `${round(spend)} USD` });
|
|
878
943
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
944
|
+
if (!windows.length && !extras.length) throw failure("subusage/response", "LiteLLM 响应没有可用的预算或花费");
|
|
945
|
+
// 各预算同时约束,不用更宽松的上级覆盖 Key。
|
|
946
|
+
return { windows, extras, coverage: "complete" };
|
|
882
947
|
}
|
|
883
948
|
function detectConfigured(ctx) {
|
|
884
|
-
try { const ids = new Set(ctx.llm.listProviders().map(p => p.id)); return Object.fromEntries(IDS.map(id => [id, ids.has(id)])); }
|
|
949
|
+
try { const ids = new Set(ctx.llm.listProviders().map(p => p.id)); return Object.fromEntries(IDS.map(id => [id, ids.has(id) || id === "deepseek-account" && ids.has("deepseek-official")])); }
|
|
885
950
|
catch { return undefined; }
|
|
886
951
|
}
|
|
887
952
|
const codec = (type, parse) => ({ mode: "strict", typeSymbol: `dsh-subusage#${type}`, schema: { parse }, create: () => ({ parse }) });
|
|
@@ -930,6 +995,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
930
995
|
// identity 解析(凭据服务/登录文件读取)与 HTTP 同量级的超时,默认 10s(对齐 fetchJson 的
|
|
931
996
|
// AbortSignal.timeout);经 options 注入以便测试用极短值,不必真等 10 秒。
|
|
932
997
|
this.identityTimeoutMs = options.identityTimeoutMs ?? 10000;
|
|
998
|
+
this.accountBalanceTimeoutMs = options.accountBalanceTimeoutMs ?? 10000;
|
|
999
|
+
this.deepseekAccount = options.resolveDeepseekAccount ?? (() => ctx.get?.("deepseekAccount"));
|
|
933
1000
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
934
1001
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
935
1002
|
// 火山方舟的 AK/SK 不是路由的推理 Key,按固定变量名各自解析,两端都命中才算配对。
|
|
@@ -988,7 +1055,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
988
1055
|
return cred?.key ? { key: cred.key, source: "auth-file", ...(cred.accountId ? { accountId: cred.accountId } : {}) } : { key: undefined, source: "none" };
|
|
989
1056
|
}
|
|
990
1057
|
if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
|
|
991
|
-
if (id === "
|
|
1058
|
+
if (id === "deepseek-account") {
|
|
1059
|
+
const service = this.deepseekAccount();
|
|
1060
|
+
const ready = typeof service?.getState === "function" && typeof service?.getBalance === "function" && (await service.getState())?.status === "credential-stored";
|
|
1061
|
+
return { key: undefined, source: ready ? "account-service" : "none" };
|
|
1062
|
+
}
|
|
1063
|
+
if (id === "commandcode" && account && account !== "default") {
|
|
992
1064
|
// 额外账户:id 即凭据引用名(apiKeyEnv)。引用名不合法(不可能来自提供方
|
|
993
1065
|
// 插件的 slots,但 client 输入不可信)按未配置处理,不让 credentialRef 抛错。
|
|
994
1066
|
if (!isCredentialRefName(account)) return { key: undefined, source: "none" };
|
|
@@ -1055,7 +1127,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1055
1127
|
const call = async (action, payload) => {
|
|
1056
1128
|
const body = payload === undefined ? "{}" : JSON.stringify(payload);
|
|
1057
1129
|
const signed = volcSignature({ accessKeyId: identity.key, secretAccessKey: identity.secret, action, host: spec.volcHost, region: spec.volcRegion, body });
|
|
1058
|
-
const response = await get(signed.url, signed.headers, { method: "POST", body });
|
|
1130
|
+
const response = await get(signed.url, signed.headers, { method: "POST", body, notSubscribed404: true });
|
|
1059
1131
|
const apiError = volcError(response);
|
|
1060
1132
|
// 「这个账号没有这份套餐」不是故障,是答案:实测没有 BytePlus Coding Plan 的账号会返回
|
|
1061
1133
|
// HTTP 404 `NotFound.BillingType`(coding plan config is not exist),`OperationDenied.NotSubscribed`
|
|
@@ -1128,6 +1200,13 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1128
1200
|
if (id === "minimax" || id === "minimax-cn") return normalizeMinimax(await get(PROVIDERS[id].usageUrl, headers));
|
|
1129
1201
|
if (id === "siliconflow") return normalizeSiliconFlow(await get("https://api.siliconflow.cn/v1/user/info", headers));
|
|
1130
1202
|
if (id === "deepseek") return normalizeDeepseekBalance(await get("https://api.deepseek.com/user/balance", headers));
|
|
1203
|
+
if (id === "deepseek-account") {
|
|
1204
|
+
const service = this.deepseekAccount();
|
|
1205
|
+
if (typeof service?.getBalance !== "function") throw failure("subusage/response", "本机 DeepSeek 账号余额服务不可用", true);
|
|
1206
|
+
const body = await withTimeout(service.getBalance({ version: VERSION, locale: "zh-CN", timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 }), this.accountBalanceTimeoutMs, () => failure("subusage/network", "本机 DeepSeek 账号余额读取超时", true));
|
|
1207
|
+
if (body === null) throw failure("subusage/auth", "本机 DeepSeek 账号未登录或登录已失效,请在 DSH 账号与余额页面确认");
|
|
1208
|
+
return normalizeDeepseekAccountBalance(body);
|
|
1209
|
+
}
|
|
1131
1210
|
if (id === "openrouter") {
|
|
1132
1211
|
const keyBody = await get("https://openrouter.ai/api/v1/key", headers);
|
|
1133
1212
|
// 余额端点对普通推理 key 会 403;这一次失败不影响 key 限额与用量的展示。
|
|
@@ -1148,7 +1227,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1148
1227
|
get("https://zenmux.ai/api/v1/management/subscription/detail", headers).catch(() => undefined),
|
|
1149
1228
|
get("https://zenmux.ai/api/v1/management/payg/balance", headers).catch(() => undefined)
|
|
1150
1229
|
]);
|
|
1151
|
-
|
|
1230
|
+
const data = normalizeZenmux(subscription, balance);
|
|
1231
|
+
if (subscription === undefined) {
|
|
1232
|
+
data.coverage = "partial";
|
|
1233
|
+
data.extras.push({ kind: "note", value: "余额已读取;套餐状态暂不可确认" });
|
|
1234
|
+
}
|
|
1235
|
+
return data;
|
|
1152
1236
|
}
|
|
1153
1237
|
if (id === "litellm") {
|
|
1154
1238
|
// 地址是用户自己的 proxy;缺地址算「凭据不完整」,与缺 Key 一样给出可行动的指引。
|
|
@@ -1157,10 +1241,14 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1157
1241
|
const keyInfo = await get(`${root}/key/info`, headers);
|
|
1158
1242
|
const own = keyInfo?.info && typeof keyInfo.info === "object" && !Array.isArray(keyInfo.info) ? keyInfo.info : {};
|
|
1159
1243
|
// 更完整的预算视图在 user/team info;拿不到就退回 key 自身的 info。
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1244
|
+
const scopes = [["team", own.team_id], ["user", own.user_id]].filter(([, value]) => typeof value === "string" && value.trim());
|
|
1245
|
+
const budgets = await Promise.all(scopes.map(async ([scope, value]) => {
|
|
1246
|
+
const body = await get(`${root}/${scope}/info?${scope}_id=${encodeURIComponent(value.trim())}`, headers).catch(() => undefined);
|
|
1247
|
+
return body ? body[`${scope}_info`] ? body : { [`${scope}_info`]: body } : undefined;
|
|
1248
|
+
}));
|
|
1249
|
+
const data = normalizeLitellm(keyInfo, budgets);
|
|
1250
|
+
if (budgets.some(body => body === undefined)) data.coverage = "partial";
|
|
1251
|
+
return data;
|
|
1164
1252
|
}
|
|
1165
1253
|
if (id === "commandcode") {
|
|
1166
1254
|
// 与提供方插件同一凭据来源,但直连计费接口并行拉取:其 report() 会对每个账户
|
|
@@ -1170,7 +1258,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1170
1258
|
get(`${COMMAND_CODE_API_BASE}/alpha/billing/credits`, billingHeaders),
|
|
1171
1259
|
get(`${COMMAND_CODE_API_BASE}/alpha/billing/subscriptions`, billingHeaders).catch(() => undefined)
|
|
1172
1260
|
]);
|
|
1173
|
-
|
|
1261
|
+
const data = normalizeCommandCode(credits, subscription);
|
|
1262
|
+
if (subscription === undefined) data.coverage = "partial";
|
|
1263
|
+
return data;
|
|
1174
1264
|
}
|
|
1175
1265
|
if (id === "xai-oauth") {
|
|
1176
1266
|
// 登录文件里的 access token 短命(实测 6 小时量级),而 grok-kit 的自动刷新是懒触发:
|
|
@@ -1200,12 +1290,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1200
1290
|
}
|
|
1201
1291
|
if (id === "openai-codex") {
|
|
1202
1292
|
// ChatGPT 后端私有接口:Bearer 是 Codex CLI 的 OAuth access token,不是 OPENAI_API_KEY。
|
|
1203
|
-
return normalizeCodexUsage(await get(`${CODEX_API_BASE}/wham/usage`, headers));
|
|
1293
|
+
return normalizeCodexUsage(await get(`${CODEX_API_BASE}/wham/usage`, { ...headers, ...(identity.accountId ? { "ChatGPT-Account-Id": identity.accountId } : {}) }));
|
|
1204
1294
|
}
|
|
1205
1295
|
const base = "https://platform.xiaomimimo.com/api/v1";
|
|
1206
1296
|
const h = { cookie: identity.key, origin: "https://platform.xiaomimimo.com", referer: "https://platform.xiaomimimo.com/#/console/balance", "x-timezone": "UTC+08:00" };
|
|
1207
1297
|
let usageError;
|
|
1208
|
-
const [bal, detail, usage] = await Promise.all([get(`${base}/balance`, h), get(`${base}/tokenPlan/detail`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/usage`, h).catch(e => { if (e.code === "subusage/auth") throw e; usageError = e; return undefined; })]);
|
|
1298
|
+
const [bal, detail, usage] = await Promise.all([get(`${base}/balance`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/detail`, h).catch(e => { if (e.code === "subusage/auth") throw e; return undefined; }), get(`${base}/tokenPlan/usage`, h).catch(e => { if (e.code === "subusage/auth") throw e; usageError = e; return undefined; })]);
|
|
1209
1299
|
// 普通刷新与登录验证都经过这里:服务端字段不可信,缓存前去除 Cookie 回显。
|
|
1210
1300
|
const data = redactMimo(normalizeMimo(bal, detail, usage), identity.key);
|
|
1211
1301
|
if (usageError) data.usageError = usageError;
|
|
@@ -1276,12 +1366,16 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1276
1366
|
const entry = entries[index];
|
|
1277
1367
|
const windows = Array.isArray(entry?.windows) ? entry.windows : [];
|
|
1278
1368
|
const ok = entry?.state === "ok";
|
|
1369
|
+
const paid = entry?.availability === "paygo";
|
|
1370
|
+
const limited = entry?.availability === "blocked" || windows.some(w => w.status === "rate-limited");
|
|
1371
|
+
const known = paid || entry?.coverage !== "partial" && windows.length > 0;
|
|
1279
1372
|
return {
|
|
1280
1373
|
id: account.id, label: account.label, active: account.active === true,
|
|
1281
1374
|
state: entry?.state ?? "error", errorCode: entry?.errorCode, windows,
|
|
1282
1375
|
// usable:读到了且没有任何限额窗口;blocked:读到了但有窗口限额(容量降级,仍可能成功)。
|
|
1283
|
-
usable: ok &&
|
|
1284
|
-
|
|
1376
|
+
usable: ok && (paid || known && !limited),
|
|
1377
|
+
...(ok && !known && !limited ? { availability: "unknown" } : entry?.availability ? { availability: entry.availability } : {}),
|
|
1378
|
+
blocked: ok && !paid && limited
|
|
1285
1379
|
};
|
|
1286
1380
|
});
|
|
1287
1381
|
// 「有效容量」= usable + blocked(真的有凭据且读到了额度的账户);no-key/error/disabled 不算。
|
|
@@ -1289,7 +1383,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1289
1383
|
const available = accounts.filter(a => a.usable).length;
|
|
1290
1384
|
const blockedCount = accounts.filter(a => a.blocked).length;
|
|
1291
1385
|
const unusableCount = accounts.length - available - blockedCount;
|
|
1292
|
-
const exhausted = capable.length > 0 && capable.every(a => a.blocked);
|
|
1386
|
+
const exhausted = capable.length > 0 && capable.every(a => a.blocked) && !accounts.some(a => a.state === "error" || a.availability === "unknown");
|
|
1293
1387
|
return { mode: "auto", total: accounts.length, available, blockedCount, unusableCount, exhausted, accounts };
|
|
1294
1388
|
}
|
|
1295
1389
|
/** 按 id 失效一家 provider 的**全部账户**缓存:凭据/开关变化不能只影响默认账户那一条。 */
|
|
@@ -1326,17 +1420,18 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1326
1420
|
if (startedGeneration !== (this.generations.get(cacheKey) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
1327
1421
|
if (!providerEnabled(settings, id)) {
|
|
1328
1422
|
this.cache.delete(cacheKey); this.inflight.delete(cacheKey);
|
|
1329
|
-
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
1423
|
+
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key || identity.source === "account-service", windows: [], extras: [] };
|
|
1330
1424
|
}
|
|
1331
1425
|
// fingerprint 计入账户选择:切换账户后同 id 缓存/inflight 不串账(不同账户 key 必不同);
|
|
1332
1426
|
// 空串与 "default" 同为默认账户,归一后共享缓存。
|
|
1333
|
-
const fingerprint = hash([identity.key ?? "", identity.secret ?? "", identity.source, settings.keyModes[id], id === "zai-coding-cn" ? settings.zai : null, id === "commandcode" ? (account && account !== "default" ? account : "default") : null]);
|
|
1427
|
+
const fingerprint = hash([identity.key ?? "", identity.secret ?? "", identity.source, settings.keyModes[id], id === "zai-coding-cn" ? settings.zai : null, id === "commandcode" ? (account && account !== "default" ? account : "default") : id === "openai-codex" ? identity.accountId ?? null : null]);
|
|
1334
1428
|
if (this.fingerprints.has(cacheKey) && this.fingerprints.get(cacheKey) !== fingerprint) this.generations.set(cacheKey, (this.generations.get(cacheKey) ?? 0) + 1);
|
|
1335
1429
|
this.fingerprints.set(cacheKey, fingerprint);
|
|
1336
1430
|
const generation = this.generations.get(cacheKey) ?? 0;
|
|
1337
1431
|
const cached = this.cache.get(cacheKey);
|
|
1338
1432
|
if (cached && cached.fingerprint !== fingerprint) this.cache.delete(cacheKey);
|
|
1339
|
-
|
|
1433
|
+
// 账号服务不提供会话指纹,也不读取其 token:每次查询复用其实时余额链,不跨账号复用本插件缓存。
|
|
1434
|
+
const previous = identity.source !== "account-service" && cached?.fingerprint === fingerprint ? cached : undefined;
|
|
1340
1435
|
const active = this.inflight.get(cacheKey);
|
|
1341
1436
|
if (active?.fingerprint === fingerprint && active.generation === generation) return active.promise;
|
|
1342
1437
|
const now = this.now();
|
|
@@ -1344,7 +1439,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1344
1439
|
if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
|
|
1345
1440
|
const promise = (async () => {
|
|
1346
1441
|
const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
1347
|
-
if (!identity.key) entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
1442
|
+
if (!identity.key && identity.source !== "account-service") entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : id === "deepseek-account" ? "未检测到本机 DeepSeek 账号登录:请在 DSH 设置 → 账号与余额登录;此入口不使用 API Key" : id === "xai-oauth" ? "未检测到 Grok 登录:可在 设置 → Grok Kit(xAI OAuth)登录、运行 grok login(共享登录文件 ~/.grok/auth.json)" : id === "openai-codex" ? "未检测到 Codex 订阅登录:运行 codex login 完成 ChatGPT 登录(登录文件需为 $CODEX_HOME/auth.json 或 ~/.codex/auth.json,且 auth_mode 为 chatgpt;API Key 模式取不到订阅额度)" : PROVIDERS[id].credentialKind === "volc" ? "未配置火山方舟 AK/SK:额度查询要 IAM Access Key(VOLC_ACCESSKEY / VOLC_SECRETKEY,或在本页填写),不是推理用的方舟 API Key" : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
1348
1443
|
else try {
|
|
1349
1444
|
const data = await this.fetchProvider(id, identity, settings);
|
|
1350
1445
|
const usageError = data.usageError; delete data.usageError;
|
|
@@ -1415,6 +1510,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1415
1510
|
// 只输出真正有值的字段:视图里带 `undefined` 的键会被调用方的 schema 校验判成型别错误
|
|
1416
1511
|
// (DSH 就会),而且「字段缺席」本来就该表示「这家没报告」,不是「报告了空」。
|
|
1417
1512
|
...(typeof entry.coverage === "string" ? { coverage: entry.coverage } : {}),
|
|
1513
|
+
...(typeof entry.availability === "string" ? { availability: entry.availability } : {}),
|
|
1514
|
+
...(typeof entry.availabilityReason === "string" ? { availabilityReason: entry.availabilityReason } : {}),
|
|
1418
1515
|
...(typeof entry.freshness === "string" ? { freshness: entry.freshness } : {}),
|
|
1419
1516
|
...(typeof entry.lastSuccessAt === "string" ? { lastSuccessAt: entry.lastSuccessAt } : {}),
|
|
1420
1517
|
// 对外视图里不能出现内部凭据引用名:no-key 文案嵌了它,这里换成账户标签
|
|
@@ -1535,7 +1632,8 @@ function formatQuotaText(value) {
|
|
|
1535
1632
|
if (entry.state !== "ok") return `${head}:${entry.error || "读取失败"}`;
|
|
1536
1633
|
const parts = [];
|
|
1537
1634
|
for (const window of entry.windows || []) {
|
|
1538
|
-
|
|
1635
|
+
const scope = window.budgetScope ? `${window.budgetScope} 预算` : ({ text: "文本/向量", "daily-capabilities": "日额度适用能力", all: "全部能力共享" }[window.detail?.scope] || window.groupLabel);
|
|
1636
|
+
parts.push(`${scope ? scope + " · " : ""}${window.kind} ${window.status === "rate-limited" ? "已达限额" : `${Math.round(Math.max(0, 100 - window.percent) * 10) / 10}% 剩余`}${window.resetsAt ? `(重置 ${window.resetsAt})` : ""}`);
|
|
1539
1637
|
}
|
|
1540
1638
|
// 账户名已经在标题行里了;再打一遍只会把额度挤出视野。
|
|
1541
1639
|
// **只对多账户行跳过**(顶层 account 字段 = Command Code 的账户标注):火山方舟的席位标注
|
|
@@ -1543,7 +1641,9 @@ function formatQuotaText(value) {
|
|
|
1543
1641
|
// 信息,跟着一起丢就等于把额度说成不属于任何席位。
|
|
1544
1642
|
const skipAccountExtra = typeof entry.account === "string" && entry.account !== "";
|
|
1545
1643
|
for (const extra of entry.extras || []) { if (skipAccountExtra && extra.kind === "account") continue; parts.push(`${extra.kind}: ${extra.value}`); }
|
|
1546
|
-
|
|
1644
|
+
const advice = { included: "接口仍允许订阅续用(基础额度可能已用尽)", paygo: "包含额度用尽时可按量续用,实际余额与服务端为准", blocked: "账户/预算受限或包含额度已用尽", unknown: "续用条件待确认,不保证可调用" };
|
|
1645
|
+
if (advice[entry.availability]) parts.push(advice[entry.availability]);
|
|
1646
|
+
if (entry.freshness === "stale") parts.push("缓存数据");
|
|
1547
1647
|
return `${head}:${parts.length ? parts.join(";") : "暂无额度数据"}`;
|
|
1548
1648
|
});
|
|
1549
1649
|
// 一条额度都没读到时,末尾补一句汇总原因:模型据此才能告诉用户下一步做什么。
|
|
@@ -1578,7 +1678,8 @@ function hasQuotaData(entry) {
|
|
|
1578
1678
|
if (!entry) return false;
|
|
1579
1679
|
if (entry.state === "error") return true;
|
|
1580
1680
|
if (entry.state !== "ok") return false;
|
|
1581
|
-
if (
|
|
1681
|
+
if (typeof entry.availability === "string") return true;
|
|
1682
|
+
if (Array.isArray(entry.windows) && entry.windows.length) return true;
|
|
1582
1683
|
const metadata = typeof entry.account === "string" && entry.account !== "" ? ["account", "plan"] : ["plan"];
|
|
1583
1684
|
return (Array.isArray(entry.extras) ? entry.extras : []).some(item => !metadata.includes(item?.kind));
|
|
1584
1685
|
}
|
|
@@ -1642,7 +1743,9 @@ function registerQuotaTool(ctx) {
|
|
|
1642
1743
|
value: { type: "string", required: true }
|
|
1643
1744
|
} }
|
|
1644
1745
|
},
|
|
1645
|
-
coverage: { type: "string", description: "complete | partial" },
|
|
1746
|
+
coverage: { type: "string", description: "complete | partial — completeness of applicable reported quota facts, not a count of windows." },
|
|
1747
|
+
availability: { type: "string", description: "Optional advisory: included | paygo | blocked | unknown. Window exhaustion alone need not block paid usage; absent means no separate billing-route evidence." },
|
|
1748
|
+
availabilityReason: { type: "string", description: "Machine-readable basis for availability; no credentials or raw upstream objects." },
|
|
1646
1749
|
freshness: { type: "string", description: "fresh | stale" },
|
|
1647
1750
|
lastSuccessAt: { type: "string" },
|
|
1648
1751
|
account: { type: "string", description: "Account label this entry reports (Command Code only, when the provider has several accounts)." },
|
package/lib/volcengine.js
CHANGED
|
@@ -157,7 +157,7 @@ export function parseVolcAgentPlan(body) {
|
|
|
157
157
|
const quota = amount(row.Quota), used = amount(row.Used);
|
|
158
158
|
if (quota === undefined || used === undefined) throw new Error("Volcengine AFP window is invalid");
|
|
159
159
|
if (quota <= 0) continue;
|
|
160
|
-
windows.push({ kind, percent: Math.min(100, used / quota * 100), resetsAt: fromEpoch(row.ResetTime, "milliseconds"), groupLabel, detail: { used, limit: quota, unit: "AFP" } });
|
|
160
|
+
windows.push({ kind, percent: Math.min(100, used / quota * 100), resetsAt: fromEpoch(row.ResetTime, "milliseconds"), groupLabel, detail: { used, limit: quota, unit: "AFP", scope: kind === "month" ? "all" : kind === "day" ? "daily-capabilities" : "text" } });
|
|
161
161
|
}
|
|
162
162
|
const plan = typeof result.PlanType === "string" && result.PlanType.trim() ? result.PlanType.trim() : undefined;
|
|
163
163
|
return { subscribed: windows.length > 0 || reported, windows, plan };
|
|
@@ -188,7 +188,7 @@ export function parseVolcSeatAfp(body) {
|
|
|
188
188
|
const quota = amount(window.Quota), used = amount(window.Used);
|
|
189
189
|
if (quota === undefined || used === undefined) throw new Error("Volcengine seat AFP window is invalid");
|
|
190
190
|
if (quota <= 0) continue;
|
|
191
|
-
windows.push({ kind, percent: Math.min(100, used / quota * 100), resetsAt: fromEpoch(window.ResetTime, "milliseconds"), groupLabel, detail: { used, limit: quota, unit: "AFP" } });
|
|
191
|
+
windows.push({ kind, percent: Math.min(100, used / quota * 100), resetsAt: fromEpoch(window.ResetTime, "milliseconds"), groupLabel, detail: { used, limit: quota, unit: "AFP", scope: kind === "month" ? "all" : kind === "day" ? "daily-capabilities" : "text" } });
|
|
192
192
|
}
|
|
193
193
|
const plan = typeof row.PlanType === "string" && row.PlanType.trim() ? row.PlanType.trim() : undefined;
|
|
194
194
|
const seatId = typeof row.SeatID === "string" && row.SeatID.trim() ? row.SeatID.trim() : undefined;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"displayName": "订阅用量显示",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "显示独立 DeepSeek API Key 与 DSH 本机账号余额(账号入口默认开启)、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
8
8
|
"dsh-plugin",
|