@kenz1117/dsh-ui-usage-billing 1.3.0 → 1.4.1

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/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
6
6
  import { defineTool } from "@deepseek-ai/dsh-tools";
7
7
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
8
8
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
9
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
10
  import z from "@deepseek-ai/schemastery";
10
11
  import { createHash, createHmac } from "node:crypto";
11
12
  //#region lib/types/client/plan-knowledge.js
@@ -60,6 +61,7 @@ const PLAN_KNOWLEDGE = {
60
61
  "kimi-coding": { type: "code" },
61
62
  "zai-coding-cn": { type: "code" },
62
63
  "zai-coding": { type: "code" },
64
+ "commandcode": { type: "code" },
63
65
  "qwen-token-plan": { type: "code" },
64
66
  "qwen-token-plan-cn": { type: "code" },
65
67
  "xiaomi-token-plan-ams": { type: "code" },
@@ -315,7 +317,7 @@ function tierAtWithBounds(timeMs, bounds) {
315
317
  }
316
318
  /** 时刻是否落在北京时间周末(周六/周日)。 */
317
319
  function isBeijingWeekend(timeMs) {
318
- const day = new Date(timeMs + 8 * 36e5).getUTCDay();
320
+ const day = new Date(timeMs + 288e5).getUTCDay();
319
321
  return day === 0 || day === 6;
320
322
  }
321
323
  /** 峰谷切换边界(北京时间的当日分钟数):09:00 / 12:00 / 14:00 / 18:00。 */
@@ -327,7 +329,7 @@ const TIER_BOUNDARY_MINUTES = [
327
329
  ];
328
330
  /** 北京时间的当日毫秒数(0–86,400,000)。 */
329
331
  function beijingMillisOfDay(timeMs) {
330
- return ((timeMs + 8 * 36e5) % 864e5 + 864e5) % 864e5;
332
+ return ((timeMs + 288e5) % 864e5 + 864e5) % 864e5;
331
333
  }
332
334
  /**
333
335
  * Built-in catalog of current mainstream models as of 2026-08-16, priced from
@@ -444,6 +446,18 @@ const MODEL_CATALOG = [
444
446
  note: "限时 5 折"
445
447
  }
446
448
  },
449
+ {
450
+ key: "glm-5.3-flashx",
451
+ name: "GLM-5.3-FlashX",
452
+ provider: "智谱 AI",
453
+ colorVar: "dsw-static-blue-300",
454
+ price: {
455
+ currency: "CNY",
456
+ input: 2,
457
+ cacheHit: .57,
458
+ output: 7
459
+ }
460
+ },
447
461
  {
448
462
  key: "glm-4.6",
449
463
  name: "GLM-4.6",
@@ -880,6 +894,18 @@ const MODEL_CATALOG = [
880
894
  output: 4
881
895
  }
882
896
  },
897
+ {
898
+ key: "hunyuan-hy4-preview",
899
+ name: "混元 Hy4 Preview",
900
+ provider: "腾讯混元",
901
+ colorVar: "dsw-static-amber-400",
902
+ price: {
903
+ currency: "CNY",
904
+ input: 6,
905
+ cacheHit: .3,
906
+ output: 18
907
+ }
908
+ },
883
909
  {
884
910
  key: "hunyuan-t1",
885
911
  name: "混元 T1",
@@ -1478,6 +1504,8 @@ const MODEL_KEY_ALIASES = {
1478
1504
  "glm-5-turbo": "glm-5-turbo",
1479
1505
  "glm-5.1": "glm-5.1",
1480
1506
  "glm-5.3-flash": "glm-5.3-flash",
1507
+ "glm-5.3-flashx": "glm-5.3-flashx",
1508
+ "glm-5.3-flash-x": "glm-5.3-flashx",
1481
1509
  "glm-5v-turbo": "glm-5v-turbo",
1482
1510
  "glm-5v.1": "glm-5v-turbo",
1483
1511
  "claude-opus-4-6": "claude-opus-4-6",
@@ -1499,6 +1527,10 @@ const MODEL_KEY_ALIASES = {
1499
1527
  "command-r-08-2024": "command-r-08-2024",
1500
1528
  "command-r": "command-r-08-2024",
1501
1529
  "hy3": "hunyuan",
1530
+ "hy4-preview": "hunyuan-hy4-preview",
1531
+ "hy4": "hunyuan-hy4-preview",
1532
+ "hunyuan-hy4-preview": "hunyuan-hy4-preview",
1533
+ "hunyuan-hy4": "hunyuan-hy4-preview",
1502
1534
  "longcat-2.0": "longcat-2.0",
1503
1535
  "longcat-2": "longcat-2.0",
1504
1536
  "minicpm-v-4.5": "minicpm-v-4.5",
@@ -1529,6 +1561,7 @@ const MODEL_KEY_ALIASES = {
1529
1561
  "glm-5.2f": "glm-5.2-fast",
1530
1562
  "kimi-k3-fast": "kimi-k3-fast",
1531
1563
  "kimi-k3f": "kimi-k3-fast",
1564
+ "kimi-for-coding": "kimi-k2.8-preview",
1532
1565
  "kimi-k2.7-code-fast": "kimi-k2.7-code-fast",
1533
1566
  "kimi-k2.7-code-f": "kimi-k2.7-code-fast",
1534
1567
  "kimi-k2.6-fast": "kimi-k2.6-fast",
@@ -3083,8 +3116,8 @@ function createUsageAggregator(persistence, options = {}) {
3083
3116
  };
3084
3117
  const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
3085
3118
  const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
3086
- const modelKeys = new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3087
- const hourKeys = new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3119
+ const modelKeys = /* @__PURE__ */ new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3120
+ const hourKeys = /* @__PURE__ */ new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3088
3121
  const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
3089
3122
  byModel: Object.fromEntries([...modelKeys].map((model) => {
3090
3123
  const live = perfModel.get(model);
@@ -3095,7 +3128,7 @@ function createUsageAggregator(persistence, options = {}) {
3095
3128
  byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
3096
3129
  const liveModels = perfHourModel.get(hour);
3097
3130
  const digestModels = perfHourDigest.get(hour);
3098
- const modelKeysInHour = new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3131
+ const modelKeysInHour = /* @__PURE__ */ new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3099
3132
  return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
3100
3133
  const live = liveModels?.get(model);
3101
3134
  const restored = digestModels?.get(model);
@@ -3298,7 +3331,11 @@ function tc3Authorization(secretId, secretKey, payload, timestamp) {
3298
3331
  const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
3299
3332
  const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3300
3333
  const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3301
- return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
3334
+ const kDate = createHmac("sha256", date).update(secretKey).digest();
3335
+ const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
3336
+ const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
3337
+ const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3338
+ return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
3302
3339
  }
3303
3340
  /**
3304
3341
  * 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
@@ -4062,7 +4099,7 @@ const declaredGate = createCooldownGate({
4062
4099
  cooldownMs: 6e4
4063
4100
  });
4064
4101
  /** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
4065
- const FORBIDDEN_SEGMENTS = new Set([
4102
+ const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
4066
4103
  "__proto__",
4067
4104
  "constructor",
4068
4105
  "prototype"
@@ -4612,7 +4649,7 @@ function asPrice(value) {
4612
4649
  */
4613
4650
  function buildExtraModels(data) {
4614
4651
  if (data === null || typeof data !== "object") return [];
4615
- const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4652
+ const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4616
4653
  const extras = [];
4617
4654
  for (const [providerId, providerDoc] of Object.entries(data)) {
4618
4655
  if (providerDoc === null || typeof providerDoc !== "object") continue;
@@ -4694,6 +4731,8 @@ const EMPTY_SUBSCRIPTION_KEYS = {
4694
4731
  opencodeApiKey: "",
4695
4732
  minmaxApiKey: "",
4696
4733
  openrouterApiKey: "",
4734
+ anthropicApiKey: "",
4735
+ commandcodeApiKey: "",
4697
4736
  tencentCloudApi: "",
4698
4737
  zaiRegion: "global"
4699
4738
  };
@@ -4720,11 +4759,12 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
4720
4759
  "minimax-token-plan-cn": "MiniMax Token Plan(国内)",
4721
4760
  "minimax-cn": "MiniMax Token Plan(国内)",
4722
4761
  "openrouter": "OpenRouter",
4762
+ "commandcode": "CommandCode",
4723
4763
  "tencent-token-plan": "腾讯云 Token Plan",
4724
4764
  "grok": "Grok(X Premium)"
4725
4765
  };
4726
4766
  /** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
4727
- const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter|grok)", "i");
4767
+ const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter|grok|commandcode)", "i");
4728
4768
  /** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
4729
4769
  function isSubscriptionProviderId(providerId) {
4730
4770
  if (SUBSCRIPTION_ID_RE.test(providerId)) return true;
@@ -4741,6 +4781,8 @@ const SUBSCRIPTION_ADAPTERS = {
4741
4781
  "minimax-token-plan": { collect: collectMiniMax },
4742
4782
  "minimax-token-plan-cn": { collect: collectMiniMax },
4743
4783
  "openrouter": { collect: collectOpenRouter },
4784
+ "anthropic": { collect: collectAnthropic },
4785
+ "commandcode": { collect: collectCommandCode },
4744
4786
  "tencent-token-plan": { collect: collectTencentTokenPlan }
4745
4787
  };
4746
4788
  /** 有额度适配器的 provider id 集合(识别用)。 */
@@ -5237,6 +5279,131 @@ async function collectOpenRouter(keys, config, timeoutMs) {
5237
5279
  };
5238
5280
  }
5239
5281
  }
5282
+ /** Parse one Anthropic usage window (`utilization` is already 0–100). */
5283
+ function anthropicWindow(value, kind) {
5284
+ if (value === null || typeof value !== "object") return null;
5285
+ const record = value;
5286
+ const utilization = numberOrNull$1(record.utilization ?? record.used_percentage);
5287
+ if (utilization === null) return null;
5288
+ const usedPercent = round1$1(clampPercent$1(utilization) ?? 0);
5289
+ const resetsAt = toIso(record.resets_at ?? record.reset_at);
5290
+ return {
5291
+ kind,
5292
+ usedPercent,
5293
+ remainingPercent: round1$1(100 - usedPercent),
5294
+ ...resetsAt === null ? {} : { resetsAt }
5295
+ };
5296
+ }
5297
+ /**
5298
+ * 解析 Anthropic OAuth 用量响应(GET https://api.anthropic.com/api/oauth/usage)。
5299
+ * 形如 `{ five_hour: { utilization, resets_at }, seven_day: {...}, seven_day_sonnet: {...} }`:
5300
+ * `utilization` 为 0–100 百分数,`resets_at` 为 unix 秒。子配额窗口
5301
+ * (`seven_day_sonnet` / `five_hour_opus` 等单模型系列限额)只描述一个模型分支,
5302
+ * 与主窗口量纲相同但口径更窄,整体丢弃,避免面板百分比被分支配额覆盖。
5303
+ * 导出供测试:纯函数。
5304
+ * @param body - 接口响应 JSON。
5305
+ * @returns 窗口列表(5 小时 → session、7 天 → weekly);无可用窗口时为 []。
5306
+ */
5307
+ function parseAnthropicUsage(body) {
5308
+ const doc = body ?? {};
5309
+ return [anthropicWindow(doc.five_hour, "session"), anthropicWindow(doc.seven_day, "weekly")].filter((hit) => hit !== null);
5310
+ }
5311
+ /** Collect the Claude Pro/Max subscription usage via the OAuth usage endpoint. */
5312
+ async function collectAnthropic(keys, config, timeoutMs) {
5313
+ const token = keys.anthropicApiKey.trim();
5314
+ const base = config.baseUrl ?? "https://api.anthropic.com";
5315
+ const displayName = "Claude (Anthropic)";
5316
+ if (token === "") return {
5317
+ provider: config.provider,
5318
+ displayName,
5319
+ status: "not-configured",
5320
+ windows: [],
5321
+ hint: "需 Claude Code 登录态(~/.claude/.credentials.json 自动读取)或 OAuth token;普通 sk-ant- 按量 API key 查不了订阅用量"
5322
+ };
5323
+ try {
5324
+ const windows = parseAnthropicUsage(await requestJson(`${base}/api/oauth/usage`, { headers: {
5325
+ authorization: `Bearer ${token}`,
5326
+ accept: "application/json"
5327
+ } }, timeoutMs));
5328
+ return {
5329
+ provider: config.provider,
5330
+ displayName,
5331
+ status: windows.length > 0 ? "ok" : "invalid-response",
5332
+ windows
5333
+ };
5334
+ } catch (error) {
5335
+ return {
5336
+ provider: config.provider,
5337
+ displayName,
5338
+ status: statusOf$1(error),
5339
+ windows: []
5340
+ };
5341
+ }
5342
+ }
5343
+ /** Parse one CommandCode window row (`used/cap`, `resetAt` is epoch ms). */
5344
+ function commandcodeWindow(value, kind) {
5345
+ if (value === null || typeof value !== "object") return null;
5346
+ const record = value;
5347
+ const used = numberOrNull$1(record.used);
5348
+ const cap = numberOrNull$1(record.cap ?? record.limit ?? record.total);
5349
+ if (used === null || cap === null || cap <= 0 || used < 0) return null;
5350
+ const usedPercent = round1$1(clampPercent$1(used / cap * 100) ?? 0);
5351
+ const resetsAt = toIso(record.resetAt ?? record.reset_at ?? record.resetsAt);
5352
+ return {
5353
+ kind,
5354
+ usedPercent,
5355
+ remainingPercent: round1$1(100 - usedPercent),
5356
+ ...resetsAt === null ? {} : { resetsAt }
5357
+ };
5358
+ }
5359
+ /**
5360
+ * 解析 CommandCode(commandcode.ai)额度响应
5361
+ * (GET https://api.commandcode.ai/alpha/billing/credits)。形如
5362
+ * `{ windowLimits: { fiveHour: { used, cap, resetAt }, weekly: {...} }, credits: { monthlyCredits } }`:
5363
+ * 窗口按 used/cap 算已用%(resetAt 为 epoch 毫秒);monthlyCredits 是月度
5364
+ * Credits 余额池(1 credit ≈ $1 用量),无总量字段、算不出百分比,不产出窗口。
5365
+ * 导出供测试:纯函数。
5366
+ * @param body - 接口响应 JSON。
5367
+ * @returns 窗口列表(5 小时 → session、周 → weekly);无可用窗口时为 []。
5368
+ */
5369
+ function parseCommandCodeCredits(body) {
5370
+ const limits = (body ?? {}).windowLimits;
5371
+ if (limits === null || typeof limits !== "object" || Array.isArray(limits)) return [];
5372
+ const record = limits;
5373
+ return [commandcodeWindow(record.fiveHour ?? record.five_hour, "session"), commandcodeWindow(record.weekly ?? record.week, "weekly")].filter((hit) => hit !== null);
5374
+ }
5375
+ /** Collect the CommandCode quota (5h/weekly windows + monthly credits). */
5376
+ async function collectCommandCode(keys, config, timeoutMs) {
5377
+ const apiKey = keys.commandcodeApiKey.trim();
5378
+ const base = config.baseUrl ?? "https://api.commandcode.ai";
5379
+ const displayName = SUBSCRIPTION_DISPLAY_NAMES["commandcode"] ?? "CommandCode";
5380
+ if (apiKey === "") return {
5381
+ provider: config.provider,
5382
+ displayName,
5383
+ status: "not-configured",
5384
+ windows: [],
5385
+ hint: "需 commandcode.ai 的 API key(user_ 前缀);可在 llm-pi-ai 给 commandcode 路由配 apiKeyEnv"
5386
+ };
5387
+ try {
5388
+ const windows = parseCommandCodeCredits(await requestJson(`${base}/alpha/billing/credits`, { headers: {
5389
+ authorization: `Bearer ${apiKey}`,
5390
+ accept: "application/json"
5391
+ } }, timeoutMs));
5392
+ return {
5393
+ provider: config.provider,
5394
+ displayName,
5395
+ status: windows.length > 0 ? "ok" : "invalid-response",
5396
+ windows
5397
+ };
5398
+ } catch (error) {
5399
+ return {
5400
+ provider: config.provider,
5401
+ displayName,
5402
+ status: statusOf$1(error),
5403
+ windows: []
5404
+ };
5405
+ }
5406
+ }
5240
5407
  /**
5241
5408
  * 腾讯云 Token Plan(TokenHub 管控面)订阅额度。凭据是云 API 密钥对
5242
5409
  * `<SecretId>:<SecretKey>`(与余额面板同源、同格式),链路与 balance.ts 的
@@ -5332,7 +5499,7 @@ const relayGate = createCooldownGate({
5332
5499
  /** Abort a relay fetch when the upstream hangs beyond this budget. */
5333
5500
  const FETCH_TIMEOUT_MS = 8e3;
5334
5501
  /** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
5335
- const FINGERPRINT_TTL_MS = 300 * 1e3;
5502
+ const FINGERPRINT_TTL_MS = 3e5;
5336
5503
  /** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
5337
5504
  const fingerprintCache = /* @__PURE__ */ new Map();
5338
5505
  /** Number, or null when the value is not a finite number (nor numeric string). */
@@ -5468,7 +5635,7 @@ function originOf(baseURL) {
5468
5635
  * 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
5469
5636
  * 「未识别」。中转站面板只列真正的第三方中转程序。
5470
5637
  */
5471
- const OFFICIAL_HOSTS = new Set([
5638
+ const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
5472
5639
  "api.deepseek.com",
5473
5640
  "api.openai.com",
5474
5641
  "open.bigmodel.cn",
@@ -5721,10 +5888,13 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
5721
5888
  /** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
5722
5889
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
5723
5890
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
5724
- const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
5891
+ const PRICING_REFRESH_INTERVAL_MS = 216e5;
5892
+ /** 历史回放预热的延迟:等宿主启动高峰(插件加载 / 路由挂载)过去再全量折叠,
5893
+ * 避免抢启动期的 CPU;纯延迟不阻塞任何请求,面板提前打开也只会提前聚合。 */
5894
+ const WARMUP_DELAY_MS = 3e3;
5725
5895
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
5726
- const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
5727
- const BALANCE_CACHE_MS = 300 * 1e3;
5896
+ const SUBSCRIPTION_CACHE_MS = 3e5;
5897
+ const BALANCE_CACHE_MS = 3e5;
5728
5898
  /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
5729
5899
  const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5730
5900
  /**
@@ -5741,7 +5911,7 @@ const SNAPSHOT_INTERVAL_MS = 3e5;
5741
5911
  * 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
5742
5912
  const RESPONSE_BUDGET_MS = 1500;
5743
5913
  /** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
5744
- const AUTH_WARN_COOLDOWN_MS = 1800 * 1e3;
5914
+ const AUTH_WARN_COOLDOWN_MS = 18e5;
5745
5915
  /**
5746
5916
  * 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
5747
5917
  * `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
@@ -5874,6 +6044,10 @@ const SUBSCRIPTION_KEY_SOURCES = [
5874
6044
  provider: "openrouter",
5875
6045
  key: "openrouterApiKey"
5876
6046
  },
6047
+ {
6048
+ provider: "commandcode",
6049
+ key: "commandcodeApiKey"
6050
+ },
5877
6051
  {
5878
6052
  provider: "tencent-token-plan",
5879
6053
  key: "tencentCloudApi"
@@ -5981,6 +6155,19 @@ async function resolveSubscriptionKeys(settings, credentials) {
5981
6155
  }
5982
6156
  if (providers?.["zai-coding-cn"]?.apiKeyEnv !== void 0 && keys.zaiApiKey !== "") keys.zaiRegion = "bigmodel-cn";
5983
6157
  if (keys.opencodeApiKey === "") keys.opencodeApiKey = await readOpenCodeToken();
6158
+ keys.anthropicApiKey = await readClaudeOAuthToken();
6159
+ if (keys.anthropicApiKey !== "") {
6160
+ const identified = identifySubscriptionPlans(providers);
6161
+ if (!identified.some((plan) => plan.provider === "anthropic")) identified.push({
6162
+ provider: "anthropic",
6163
+ displayName: "Claude (Anthropic)",
6164
+ adapter: true
6165
+ });
6166
+ return {
6167
+ keys,
6168
+ identified
6169
+ };
6170
+ }
5984
6171
  return {
5985
6172
  keys,
5986
6173
  identified: identifySubscriptionPlans(providers)
@@ -6000,6 +6187,17 @@ async function readOpenCodeToken() {
6000
6187
  return "";
6001
6188
  }
6002
6189
  /**
6190
+ * 从本机 Claude Code 登录态自动发现 Anthropic OAuth access token;取不到返回
6191
+ * 空串(安静退回)。与 OpenCode auth.json 兜底同款姿态:只是一次便利,绝不报错。
6192
+ */
6193
+ async function readClaudeOAuthToken() {
6194
+ try {
6195
+ const token = JSON.parse(await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8"))?.claudeAiOauth?.accessToken;
6196
+ if (typeof token === "string" && token !== "") return token;
6197
+ } catch {}
6198
+ return "";
6199
+ }
6200
+ /**
6003
6201
  * 宿主 persistence 形状适配。宿主 0.1.3 起 SessionPersistence 改为
6004
6202
  * SessionHandle 模型(open(id,'read') 后经 handle.read(offset) 读,fork 边界
6005
6203
  * 挂在 handle.inheritedEventCount,list 返回 {header, revision} 快照行,
@@ -6047,9 +6245,10 @@ function adaptSessionPersistence(raw) {
6047
6245
  function apply(ctx, config = {}) {
6048
6246
  let usageSettingsScope;
6049
6247
  const cwd = process.cwd();
6050
- const snapshotPath = config.snapshotPath ?? join(homedir(), ".dsh/.dsh-usage-stats.json");
6051
- const ledgerStore = createFileUsageLedgerStore(config.ledgerPath ?? join(homedir(), ".dsh/.dsh-usage-ledger.json"));
6052
- const reconcilePath = config.reconcilePath ?? join(homedir(), ".dsh/.dsh-usage-reconcile.json");
6248
+ const dshHome = resolveDshHome();
6249
+ const snapshotPath = config.snapshotPath ?? join(dshHome, ".dsh-usage-stats.json");
6250
+ const ledgerStore = createFileUsageLedgerStore(config.ledgerPath ?? join(dshHome, ".dsh-usage-ledger.json"));
6251
+ const reconcilePath = config.reconcilePath ?? join(dshHome, ".dsh-usage-reconcile.json");
6053
6252
  let reconcileRef = null;
6054
6253
  (async () => {
6055
6254
  try {
@@ -6062,7 +6261,8 @@ function apply(ctx, config = {}) {
6062
6261
  })();
6063
6262
  const persistReconcileRef = () => {
6064
6263
  if (reconcileRef === null) return;
6065
- writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
6264
+ const payload = JSON.stringify({ ref: reconcileRef });
6265
+ writeFileAtomic(reconcilePath, payload, {
6066
6266
  mode: 384,
6067
6267
  dirMode: 448
6068
6268
  }).catch(() => {});
@@ -6080,6 +6280,16 @@ function apply(ctx, config = {}) {
6080
6280
  ctx.effect(() => () => {
6081
6281
  aggregator.flush();
6082
6282
  }, "usage-billing: ledger flush on dispose");
6283
+ const warmupTimer = setTimeout(() => {
6284
+ aggregator.aggregate().then(() => {
6285
+ console.info("[usage-billing] historical replay warmed up; ledger ready");
6286
+ }).catch((error) => {
6287
+ console.warn("[usage-billing] historical replay warmup failed; will fold on first dashboard request:", error);
6288
+ });
6289
+ }, WARMUP_DELAY_MS);
6290
+ ctx.effect(() => () => {
6291
+ clearTimeout(warmupTimer);
6292
+ }, "usage-billing: historical replay warmup timer");
6083
6293
  const candidates = [
6084
6294
  config.statsPath,
6085
6295
  process.env.DSH_USAGE_STATS,
@@ -6328,8 +6538,10 @@ function apply(ctx, config = {}) {
6328
6538
  });
6329
6539
  });
6330
6540
  let live = { source: "builtin" };
6541
+ let pricingSyncedAt = 0;
6331
6542
  const refreshPricing = async () => {
6332
6543
  live = await fetchLivePricing();
6544
+ pricingSyncedAt = Date.now();
6333
6545
  applyLivePricing(live);
6334
6546
  };
6335
6547
  refreshPricing();
@@ -6347,9 +6559,40 @@ function apply(ctx, config = {}) {
6347
6559
  handler: async (req, res) => {
6348
6560
  if (!guardLoopback(req, res)) return;
6349
6561
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6350
- res.end(JSON.stringify(live));
6562
+ res.end(JSON.stringify({
6563
+ ...live,
6564
+ syncedAt: pricingSyncedAt
6565
+ }));
6351
6566
  }
6352
6567
  }), "usage-billing: pricing route");
6568
+ ctx.effect(() => ctx.webServer.register({
6569
+ kind: "exact",
6570
+ path: "/api/billing/pricing/refresh",
6571
+ handler: async (req, res) => {
6572
+ if (!guardLoopback(req, res)) return;
6573
+ if (req.method !== "POST") {
6574
+ res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
6575
+ res.end(JSON.stringify({ error: "method not allowed" }));
6576
+ return;
6577
+ }
6578
+ if (!isLoopbackOrigin(req.headers.origin)) {
6579
+ res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
6580
+ res.end(JSON.stringify({ error: "forbidden: loopback only" }));
6581
+ return;
6582
+ }
6583
+ if (!(req.headers["content-type"] ?? "").toLowerCase().includes("application/json")) {
6584
+ res.writeHead(415, { "content-type": "application/json; charset=utf-8" });
6585
+ res.end(JSON.stringify({ error: "unsupported content-type" }));
6586
+ return;
6587
+ }
6588
+ await refreshPricing();
6589
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6590
+ res.end(JSON.stringify({
6591
+ ...live,
6592
+ syncedAt: pricingSyncedAt
6593
+ }));
6594
+ }
6595
+ }), "usage-billing: pricing refresh route");
6353
6596
  let balanceCache = {
6354
6597
  at: 0,
6355
6598
  doc: { balances: [] }
@@ -31,13 +31,14 @@ export declare function guardLoopback(req: IncomingMessage, res: ServerResponse)
31
31
  export interface UsageBillingConfig {
32
32
  /** Absolute path to a `.dsh-usage-stats.json` fallback file. */
33
33
  statsPath?: string;
34
- /** 统计快照的持久化路径;默认 `~/.dsh/.dsh-usage-stats.json`。
34
+ /** 统计快照的持久化路径;默认 `<harness home>/.dsh-usage-stats.json`
35
+ * (harness home = `DSH_HOME` 环境变量或 `~/.dsh`,见 resolveDshHome)。
35
36
  * 测试注入临时目录以隔离真实家目录(聚合失败回退与快照落盘都走此路径)。 */
36
37
  snapshotPath?: string;
37
- /** 独立持久用量账本的绝对路径;默认 `~/.dsh/.dsh-usage-ledger.json`。
38
+ /** 独立持久用量账本的绝对路径;默认 `<harness home>/.dsh-usage-ledger.json`。
38
39
  * 账本与会话日志解耦,因此永久删除会话不会抹掉已经观测到的用量。 */
39
40
  ledgerPath?: string;
40
- /** 余额差对账基准的持久化路径;默认 `~/.dsh/.dsh-usage-reconcile.json`。 */
41
+ /** 余额差对账基准的持久化路径;默认 `<harness home>/.dsh-usage-reconcile.json`。 */
41
42
  reconcilePath?: string;
42
43
  /** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
43
44
  subscriptionProviders?: string[];
@@ -27,6 +27,8 @@ export interface LivePricing {
27
27
  * 内置目录未收录」的模型也能计价并出现在费率表。
28
28
  */
29
29
  extraModels?: readonly ExtraModelPrice[];
30
+ /** 上次价格目录同步完成的本机时间戳(毫秒);0 = 尚未完成过任何一次同步。 */
31
+ syncedAt?: number;
30
32
  }
31
33
  /** models.dev 补充的目录外模型价(USD / 1M tokens)。 */
32
34
  export interface ExtraModelPrice {
@@ -24,6 +24,10 @@ export interface SubscriptionKeys {
24
24
  minmaxApiKey: string;
25
25
  /** OpenRouter API key(credits 已用%)。 */
26
26
  openrouterApiKey: string;
27
+ /** Anthropic Claude Pro/Max OAuth access token。 */
28
+ anthropicApiKey: string;
29
+ /** CommandCode API key(user_* 前缀)。 */
30
+ commandcodeApiKey: string;
27
31
  /** 腾讯云云 API 密钥对(`<SecretId>:<SecretKey>`,管控面用,非 TokenHub 推理 key)。 */
28
32
  tencentCloudApi: string;
29
33
  /** Z.ai 区域(global / bigmodel-cn)。 */
@@ -67,6 +71,28 @@ export declare function parseMiniMaxRemains(body: unknown): SubscriptionWindow[]
67
71
  * @returns 窗口列表;无有效额度时为 []。
68
72
  */
69
73
  export declare function parseOpenRouterCredits(body: unknown): SubscriptionWindow[];
74
+ /**
75
+ * 解析 Anthropic OAuth 用量响应(GET https://api.anthropic.com/api/oauth/usage)。
76
+ * 形如 `{ five_hour: { utilization, resets_at }, seven_day: {...}, seven_day_sonnet: {...} }`:
77
+ * `utilization` 为 0–100 百分数,`resets_at` 为 unix 秒。子配额窗口
78
+ * (`seven_day_sonnet` / `five_hour_opus` 等单模型系列限额)只描述一个模型分支,
79
+ * 与主窗口量纲相同但口径更窄,整体丢弃,避免面板百分比被分支配额覆盖。
80
+ * 导出供测试:纯函数。
81
+ * @param body - 接口响应 JSON。
82
+ * @returns 窗口列表(5 小时 → session、7 天 → weekly);无可用窗口时为 []。
83
+ */
84
+ export declare function parseAnthropicUsage(body: unknown): SubscriptionWindow[];
85
+ /**
86
+ * 解析 CommandCode(commandcode.ai)额度响应
87
+ * (GET https://api.commandcode.ai/alpha/billing/credits)。形如
88
+ * `{ windowLimits: { fiveHour: { used, cap, resetAt }, weekly: {...} }, credits: { monthlyCredits } }`:
89
+ * 窗口按 used/cap 算已用%(resetAt 为 epoch 毫秒);monthlyCredits 是月度
90
+ * Credits 余额池(1 credit ≈ $1 用量),无总量字段、算不出百分比,不产出窗口。
91
+ * 导出供测试:纯函数。
92
+ * @param body - 接口响应 JSON。
93
+ * @returns 窗口列表(5 小时 → session、周 → weekly);无可用窗口时为 []。
94
+ */
95
+ export declare function parseCommandCodeCredits(body: unknown): SubscriptionWindow[];
70
96
  /**
71
97
  * Collect quota for the given plans concurrently (adapter-backed plans only;
72
98
  * identified plans without an adapter are surfaced by the caller as "no
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "1.3.0",
4
+ "version": "1.4.1",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",
@@ -65,7 +65,8 @@
65
65
  "0.1.5-alpha.1": "compatible",
66
66
  "0.1.5-alpha.2": "compatible",
67
67
  "0.1.5-rc.1": "compatible",
68
- "0.1.5-rc.2": "compatible"
68
+ "0.1.5-rc.2": "compatible",
69
+ "0.1.6-alpha.1": "compatible"
69
70
  }
70
71
  }
71
72
  },
@@ -93,6 +94,7 @@
93
94
  "@deepseek-ai/dsh-client-ui-slots": "*",
94
95
  "@deepseek-ai/dsh-credentials": "*",
95
96
  "@deepseek-ai/dsh-host-webserver": "*",
97
+ "@deepseek-ai/dsh-home-paths": "*",
96
98
  "@deepseek-ai/dsh-invariants": "*",
97
99
  "@deepseek-ai/dsh-llm": "*",
98
100
  "@deepseek-ai/dsh-session": "*",
@@ -122,6 +124,7 @@
122
124
  "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
123
125
  "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
124
126
  "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
127
+ "@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
125
128
  "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
126
129
  "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
127
130
  "@deepseek-ai/dsh-scope": "0.1.2-rc.1",