@kenz1117/dsh-ui-usage-billing 1.2.9 → 1.4.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/lib/index.js CHANGED
@@ -60,6 +60,7 @@ const PLAN_KNOWLEDGE = {
60
60
  "kimi-coding": { type: "code" },
61
61
  "zai-coding-cn": { type: "code" },
62
62
  "zai-coding": { type: "code" },
63
+ "commandcode": { type: "code" },
63
64
  "qwen-token-plan": { type: "code" },
64
65
  "qwen-token-plan-cn": { type: "code" },
65
66
  "xiaomi-token-plan-ams": { type: "code" },
@@ -300,14 +301,35 @@ function isPeakHour(beijingHour) {
300
301
  */
301
302
  function tierAt(timeMs) {
302
303
  if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return "peak";
304
+ return tierAtWithBounds(timeMs, TIER_BOUNDARY_MINUTES);
305
+ }
306
+ /** 按给定峰段边界判档:周末全天低谷;工作日分钟数落在任一 [b(i), b(i+1)) 峰段即为高峰。 */
307
+ function tierAtWithBounds(timeMs, bounds) {
303
308
  if (isBeijingWeekend(timeMs)) return "offPeak";
304
- return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
309
+ const minute = Math.floor(beijingMillisOfDay(timeMs) / 6e4);
310
+ for (let i = 0; i + 1 < bounds.length; i += 2) {
311
+ const start = bounds[i] ?? 0;
312
+ const end = bounds[i + 1] ?? 0;
313
+ if (minute >= start && minute < end) return "peak";
314
+ }
315
+ return "offPeak";
305
316
  }
306
317
  /** 时刻是否落在北京时间周末(周六/周日)。 */
307
318
  function isBeijingWeekend(timeMs) {
308
319
  const day = new Date(timeMs + 8 * 36e5).getUTCDay();
309
320
  return day === 0 || day === 6;
310
321
  }
322
+ /** 峰谷切换边界(北京时间的当日分钟数):09:00 / 12:00 / 14:00 / 18:00。 */
323
+ const TIER_BOUNDARY_MINUTES = [
324
+ 540,
325
+ 720,
326
+ 840,
327
+ 1080
328
+ ];
329
+ /** 北京时间的当日毫秒数(0–86,400,000)。 */
330
+ function beijingMillisOfDay(timeMs) {
331
+ return ((timeMs + 8 * 36e5) % 864e5 + 864e5) % 864e5;
332
+ }
311
333
  /**
312
334
  * Built-in catalog of current mainstream models as of 2026-08-16, priced from
313
335
  * each provider's official price page. Domestic providers are OpenAI-API
@@ -4673,6 +4695,8 @@ const EMPTY_SUBSCRIPTION_KEYS = {
4673
4695
  opencodeApiKey: "",
4674
4696
  minmaxApiKey: "",
4675
4697
  openrouterApiKey: "",
4698
+ anthropicApiKey: "",
4699
+ commandcodeApiKey: "",
4676
4700
  tencentCloudApi: "",
4677
4701
  zaiRegion: "global"
4678
4702
  };
@@ -4699,11 +4723,12 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
4699
4723
  "minimax-token-plan-cn": "MiniMax Token Plan(国内)",
4700
4724
  "minimax-cn": "MiniMax Token Plan(国内)",
4701
4725
  "openrouter": "OpenRouter",
4726
+ "commandcode": "CommandCode",
4702
4727
  "tencent-token-plan": "腾讯云 Token Plan",
4703
4728
  "grok": "Grok(X Premium)"
4704
4729
  };
4705
4730
  /** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
4706
- 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");
4731
+ 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");
4707
4732
  /** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
4708
4733
  function isSubscriptionProviderId(providerId) {
4709
4734
  if (SUBSCRIPTION_ID_RE.test(providerId)) return true;
@@ -4720,6 +4745,8 @@ const SUBSCRIPTION_ADAPTERS = {
4720
4745
  "minimax-token-plan": { collect: collectMiniMax },
4721
4746
  "minimax-token-plan-cn": { collect: collectMiniMax },
4722
4747
  "openrouter": { collect: collectOpenRouter },
4748
+ "anthropic": { collect: collectAnthropic },
4749
+ "commandcode": { collect: collectCommandCode },
4723
4750
  "tencent-token-plan": { collect: collectTencentTokenPlan }
4724
4751
  };
4725
4752
  /** 有额度适配器的 provider id 集合(识别用)。 */
@@ -5216,6 +5243,131 @@ async function collectOpenRouter(keys, config, timeoutMs) {
5216
5243
  };
5217
5244
  }
5218
5245
  }
5246
+ /** Parse one Anthropic usage window (`utilization` is already 0–100). */
5247
+ function anthropicWindow(value, kind) {
5248
+ if (value === null || typeof value !== "object") return null;
5249
+ const record = value;
5250
+ const utilization = numberOrNull$1(record.utilization ?? record.used_percentage);
5251
+ if (utilization === null) return null;
5252
+ const usedPercent = round1$1(clampPercent$1(utilization) ?? 0);
5253
+ const resetsAt = toIso(record.resets_at ?? record.reset_at);
5254
+ return {
5255
+ kind,
5256
+ usedPercent,
5257
+ remainingPercent: round1$1(100 - usedPercent),
5258
+ ...resetsAt === null ? {} : { resetsAt }
5259
+ };
5260
+ }
5261
+ /**
5262
+ * 解析 Anthropic OAuth 用量响应(GET https://api.anthropic.com/api/oauth/usage)。
5263
+ * 形如 `{ five_hour: { utilization, resets_at }, seven_day: {...}, seven_day_sonnet: {...} }`:
5264
+ * `utilization` 为 0–100 百分数,`resets_at` 为 unix 秒。子配额窗口
5265
+ * (`seven_day_sonnet` / `five_hour_opus` 等单模型系列限额)只描述一个模型分支,
5266
+ * 与主窗口量纲相同但口径更窄,整体丢弃,避免面板百分比被分支配额覆盖。
5267
+ * 导出供测试:纯函数。
5268
+ * @param body - 接口响应 JSON。
5269
+ * @returns 窗口列表(5 小时 → session、7 天 → weekly);无可用窗口时为 []。
5270
+ */
5271
+ function parseAnthropicUsage(body) {
5272
+ const doc = body ?? {};
5273
+ return [anthropicWindow(doc.five_hour, "session"), anthropicWindow(doc.seven_day, "weekly")].filter((hit) => hit !== null);
5274
+ }
5275
+ /** Collect the Claude Pro/Max subscription usage via the OAuth usage endpoint. */
5276
+ async function collectAnthropic(keys, config, timeoutMs) {
5277
+ const token = keys.anthropicApiKey.trim();
5278
+ const base = config.baseUrl ?? "https://api.anthropic.com";
5279
+ const displayName = "Claude (Anthropic)";
5280
+ if (token === "") return {
5281
+ provider: config.provider,
5282
+ displayName,
5283
+ status: "not-configured",
5284
+ windows: [],
5285
+ hint: "需 Claude Code 登录态(~/.claude/.credentials.json 自动读取)或 OAuth token;普通 sk-ant- 按量 API key 查不了订阅用量"
5286
+ };
5287
+ try {
5288
+ const windows = parseAnthropicUsage(await requestJson(`${base}/api/oauth/usage`, { headers: {
5289
+ authorization: `Bearer ${token}`,
5290
+ accept: "application/json"
5291
+ } }, timeoutMs));
5292
+ return {
5293
+ provider: config.provider,
5294
+ displayName,
5295
+ status: windows.length > 0 ? "ok" : "invalid-response",
5296
+ windows
5297
+ };
5298
+ } catch (error) {
5299
+ return {
5300
+ provider: config.provider,
5301
+ displayName,
5302
+ status: statusOf$1(error),
5303
+ windows: []
5304
+ };
5305
+ }
5306
+ }
5307
+ /** Parse one CommandCode window row (`used/cap`, `resetAt` is epoch ms). */
5308
+ function commandcodeWindow(value, kind) {
5309
+ if (value === null || typeof value !== "object") return null;
5310
+ const record = value;
5311
+ const used = numberOrNull$1(record.used);
5312
+ const cap = numberOrNull$1(record.cap ?? record.limit ?? record.total);
5313
+ if (used === null || cap === null || cap <= 0 || used < 0) return null;
5314
+ const usedPercent = round1$1(clampPercent$1(used / cap * 100) ?? 0);
5315
+ const resetsAt = toIso(record.resetAt ?? record.reset_at ?? record.resetsAt);
5316
+ return {
5317
+ kind,
5318
+ usedPercent,
5319
+ remainingPercent: round1$1(100 - usedPercent),
5320
+ ...resetsAt === null ? {} : { resetsAt }
5321
+ };
5322
+ }
5323
+ /**
5324
+ * 解析 CommandCode(commandcode.ai)额度响应
5325
+ * (GET https://api.commandcode.ai/alpha/billing/credits)。形如
5326
+ * `{ windowLimits: { fiveHour: { used, cap, resetAt }, weekly: {...} }, credits: { monthlyCredits } }`:
5327
+ * 窗口按 used/cap 算已用%(resetAt 为 epoch 毫秒);monthlyCredits 是月度
5328
+ * Credits 余额池(1 credit ≈ $1 用量),无总量字段、算不出百分比,不产出窗口。
5329
+ * 导出供测试:纯函数。
5330
+ * @param body - 接口响应 JSON。
5331
+ * @returns 窗口列表(5 小时 → session、周 → weekly);无可用窗口时为 []。
5332
+ */
5333
+ function parseCommandCodeCredits(body) {
5334
+ const limits = (body ?? {}).windowLimits;
5335
+ if (limits === null || typeof limits !== "object" || Array.isArray(limits)) return [];
5336
+ const record = limits;
5337
+ return [commandcodeWindow(record.fiveHour ?? record.five_hour, "session"), commandcodeWindow(record.weekly ?? record.week, "weekly")].filter((hit) => hit !== null);
5338
+ }
5339
+ /** Collect the CommandCode quota (5h/weekly windows + monthly credits). */
5340
+ async function collectCommandCode(keys, config, timeoutMs) {
5341
+ const apiKey = keys.commandcodeApiKey.trim();
5342
+ const base = config.baseUrl ?? "https://api.commandcode.ai";
5343
+ const displayName = SUBSCRIPTION_DISPLAY_NAMES["commandcode"] ?? "CommandCode";
5344
+ if (apiKey === "") return {
5345
+ provider: config.provider,
5346
+ displayName,
5347
+ status: "not-configured",
5348
+ windows: [],
5349
+ hint: "需 commandcode.ai 的 API key(user_ 前缀);可在 llm-pi-ai 给 commandcode 路由配 apiKeyEnv"
5350
+ };
5351
+ try {
5352
+ const windows = parseCommandCodeCredits(await requestJson(`${base}/alpha/billing/credits`, { headers: {
5353
+ authorization: `Bearer ${apiKey}`,
5354
+ accept: "application/json"
5355
+ } }, timeoutMs));
5356
+ return {
5357
+ provider: config.provider,
5358
+ displayName,
5359
+ status: windows.length > 0 ? "ok" : "invalid-response",
5360
+ windows
5361
+ };
5362
+ } catch (error) {
5363
+ return {
5364
+ provider: config.provider,
5365
+ displayName,
5366
+ status: statusOf$1(error),
5367
+ windows: []
5368
+ };
5369
+ }
5370
+ }
5219
5371
  /**
5220
5372
  * 腾讯云 Token Plan(TokenHub 管控面)订阅额度。凭据是云 API 密钥对
5221
5373
  * `<SecretId>:<SecretKey>`(与余额面板同源、同格式),链路与 balance.ts 的
@@ -5701,6 +5853,9 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
5701
5853
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
5702
5854
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
5703
5855
  const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
5856
+ /** 历史回放预热的延迟:等宿主启动高峰(插件加载 / 路由挂载)过去再全量折叠,
5857
+ * 避免抢启动期的 CPU;纯延迟不阻塞任何请求,面板提前打开也只会提前聚合。 */
5858
+ const WARMUP_DELAY_MS = 3e3;
5704
5859
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
5705
5860
  const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
5706
5861
  const BALANCE_CACHE_MS = 300 * 1e3;
@@ -5853,6 +6008,10 @@ const SUBSCRIPTION_KEY_SOURCES = [
5853
6008
  provider: "openrouter",
5854
6009
  key: "openrouterApiKey"
5855
6010
  },
6011
+ {
6012
+ provider: "commandcode",
6013
+ key: "commandcodeApiKey"
6014
+ },
5856
6015
  {
5857
6016
  provider: "tencent-token-plan",
5858
6017
  key: "tencentCloudApi"
@@ -5960,6 +6119,19 @@ async function resolveSubscriptionKeys(settings, credentials) {
5960
6119
  }
5961
6120
  if (providers?.["zai-coding-cn"]?.apiKeyEnv !== void 0 && keys.zaiApiKey !== "") keys.zaiRegion = "bigmodel-cn";
5962
6121
  if (keys.opencodeApiKey === "") keys.opencodeApiKey = await readOpenCodeToken();
6122
+ keys.anthropicApiKey = await readClaudeOAuthToken();
6123
+ if (keys.anthropicApiKey !== "") {
6124
+ const identified = identifySubscriptionPlans(providers);
6125
+ if (!identified.some((plan) => plan.provider === "anthropic")) identified.push({
6126
+ provider: "anthropic",
6127
+ displayName: "Claude (Anthropic)",
6128
+ adapter: true
6129
+ });
6130
+ return {
6131
+ keys,
6132
+ identified
6133
+ };
6134
+ }
5963
6135
  return {
5964
6136
  keys,
5965
6137
  identified: identifySubscriptionPlans(providers)
@@ -5979,6 +6151,17 @@ async function readOpenCodeToken() {
5979
6151
  return "";
5980
6152
  }
5981
6153
  /**
6154
+ * 从本机 Claude Code 登录态自动发现 Anthropic OAuth access token;取不到返回
6155
+ * 空串(安静退回)。与 OpenCode auth.json 兜底同款姿态:只是一次便利,绝不报错。
6156
+ */
6157
+ async function readClaudeOAuthToken() {
6158
+ try {
6159
+ const token = JSON.parse(await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8"))?.claudeAiOauth?.accessToken;
6160
+ if (typeof token === "string" && token !== "") return token;
6161
+ } catch {}
6162
+ return "";
6163
+ }
6164
+ /**
5982
6165
  * 宿主 persistence 形状适配。宿主 0.1.3 起 SessionPersistence 改为
5983
6166
  * SessionHandle 模型(open(id,'read') 后经 handle.read(offset) 读,fork 边界
5984
6167
  * 挂在 handle.inheritedEventCount,list 返回 {header, revision} 快照行,
@@ -6059,6 +6242,16 @@ function apply(ctx, config = {}) {
6059
6242
  ctx.effect(() => () => {
6060
6243
  aggregator.flush();
6061
6244
  }, "usage-billing: ledger flush on dispose");
6245
+ const warmupTimer = setTimeout(() => {
6246
+ aggregator.aggregate().then(() => {
6247
+ console.info("[usage-billing] historical replay warmed up; ledger ready");
6248
+ }).catch((error) => {
6249
+ console.warn("[usage-billing] historical replay warmup failed; will fold on first dashboard request:", error);
6250
+ });
6251
+ }, WARMUP_DELAY_MS);
6252
+ ctx.effect(() => () => {
6253
+ clearTimeout(warmupTimer);
6254
+ }, "usage-billing: historical replay warmup timer");
6062
6255
  const candidates = [
6063
6256
  config.statsPath,
6064
6257
  process.env.DSH_USAGE_STATS,
@@ -6307,8 +6500,10 @@ function apply(ctx, config = {}) {
6307
6500
  });
6308
6501
  });
6309
6502
  let live = { source: "builtin" };
6503
+ let pricingSyncedAt = 0;
6310
6504
  const refreshPricing = async () => {
6311
6505
  live = await fetchLivePricing();
6506
+ pricingSyncedAt = Date.now();
6312
6507
  applyLivePricing(live);
6313
6508
  };
6314
6509
  refreshPricing();
@@ -6326,9 +6521,40 @@ function apply(ctx, config = {}) {
6326
6521
  handler: async (req, res) => {
6327
6522
  if (!guardLoopback(req, res)) return;
6328
6523
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6329
- res.end(JSON.stringify(live));
6524
+ res.end(JSON.stringify({
6525
+ ...live,
6526
+ syncedAt: pricingSyncedAt
6527
+ }));
6330
6528
  }
6331
6529
  }), "usage-billing: pricing route");
6530
+ ctx.effect(() => ctx.webServer.register({
6531
+ kind: "exact",
6532
+ path: "/api/billing/pricing/refresh",
6533
+ handler: async (req, res) => {
6534
+ if (!guardLoopback(req, res)) return;
6535
+ if (req.method !== "POST") {
6536
+ res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
6537
+ res.end(JSON.stringify({ error: "method not allowed" }));
6538
+ return;
6539
+ }
6540
+ if (!isLoopbackOrigin(req.headers.origin)) {
6541
+ res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
6542
+ res.end(JSON.stringify({ error: "forbidden: loopback only" }));
6543
+ return;
6544
+ }
6545
+ if (!(req.headers["content-type"] ?? "").toLowerCase().includes("application/json")) {
6546
+ res.writeHead(415, { "content-type": "application/json; charset=utf-8" });
6547
+ res.end(JSON.stringify({ error: "unsupported content-type" }));
6548
+ return;
6549
+ }
6550
+ await refreshPricing();
6551
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6552
+ res.end(JSON.stringify({
6553
+ ...live,
6554
+ syncedAt: pricingSyncedAt
6555
+ }));
6556
+ }
6557
+ }), "usage-billing: pricing refresh route");
6332
6558
  let balanceCache = {
6333
6559
  at: 0,
6334
6560
  doc: { balances: [] }
@@ -26,6 +26,7 @@ export interface LiveStats {
26
26
  byTurn?: readonly {
27
27
  sessionId: string;
28
28
  turn: number;
29
+ model?: string;
29
30
  cost: number;
30
31
  }[];
31
32
  }
@@ -66,6 +67,14 @@ export declare function lowQuotaChips(quotas: readonly QuotaSlice[], threshold?:
66
67
  kind: string;
67
68
  pct: number;
68
69
  }[];
70
+ /**
71
+ * 当前会话最近一轮的模型(峰谷窗口跟随它):byTurn 里该会话轮次号最大的 model。
72
+ * 导出供测试:纯函数。
73
+ * @param stats - 薄统计切片。
74
+ * @param sessionId - 当前会话 id。
75
+ * @returns 该会话最近一轮的归因模型键;无轮次或轮行未带模型时 undefined。
76
+ */
77
+ export declare function sessionModelOf(stats: LiveStats | null, sessionId: string | undefined): string | undefined;
69
78
  /** Props: the framework's session identity plus the owning dock's locale seat. */
70
79
  export interface LiveCostBarProps {
71
80
  /** Current Session identity supplied by the session-scope standard kit. */
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'trendEmpty' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapMonth' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown';
2
+ export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'trendEmpty' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapMonth' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -6,7 +6,7 @@
6
6
  * (默认关闭,用户到面板设置开启);「同一切换点只提醒一次」由 budget store 的
7
7
  * `lastTierSwitchAt` 承担(与原系统通知共用一份去重,避免一条切换提醒弹两次)。
8
8
  */
9
- import { type PriceTierId } from './pricing.ts';
9
+ import { type PriceTierId, type RateChannel } from './pricing.ts';
10
10
  /** 提醒模式:只提醒进入峰时 / 只提醒进入平价 / 峰与谷都提醒。 */
11
11
  export type PeakAlertMode = 'peak' | 'offPeak' | 'both';
12
12
  /** 浮层位置:右下角 / 屏幕居中。 */
@@ -28,22 +28,26 @@ export interface PeakAlertConfig {
28
28
  export declare const PEAK_ALERT_KEY = "dsh-billing-peak-alert-v1";
29
29
  /** 默认配置:关、2 分钟提前、右下角、开系统通知、峰与谷都提醒。 */
30
30
  export declare const DEFAULT_PEAK_ALERT_CONFIG: PeakAlertConfig;
31
- /** 一次命中:即将进入的档位与该切换时刻。 */
31
+ /** 一次命中:即将进入的档位、该切换时刻与命中的计费通道(决定文案口径)。 */
32
32
  export interface PeakAlertHit {
33
33
  entering: PriceTierId;
34
34
  atMs: number;
35
+ channel: RateChannel;
35
36
  }
36
37
  /** 读取本地偏好(缺失/损坏回退默认,字段宽松校验)。 */
37
38
  export declare function loadPeakAlertConfig(): PeakAlertConfig;
38
39
  /** 保存偏好;存储失败静默(降级为关闭,不影响其它能力)。 */
39
40
  export declare function savePeakAlertConfig(config: PeakAlertConfig): void;
40
41
  /**
41
- * 计算是否需要提醒:已启用、距切换不足提前量、按模式过滤、且该切换点未提醒过。
42
+ * 计算是否需要提醒:已启用、当前计费通道有峰谷窗口、距切换不足提前量、按模式过滤、
43
+ * 且该切换点未提醒过。通道由调用方按「当前会话最近一轮的模型 + 订阅状态」推导
44
+ * (rateChannelOf);none(模型不涉及峰谷)恒不提醒。
42
45
  * 导出供测试:纯函数。
43
46
  * @param nowMs - 当前时刻(epoch 毫秒)。
44
47
  * @param config - 峰谷提醒偏好。
45
48
  * @param lastAlertedAt - 上次提醒过的切换点时刻(budget store 的 lastTierSwitchAt);同点跳过。
46
- * @returns 命中(含即将进入的档位与切换时刻),否则 null。
49
+ * @param channel - 当前会话模型的计费通道;none 直接返回 null。
50
+ * @returns 命中(含即将进入的档位、切换时刻与通道),否则 null。
47
51
  */
48
- export declare function computePeakAlert(nowMs: number, config: PeakAlertConfig, lastAlertedAt: number): PeakAlertHit | null;
52
+ export declare function computePeakAlert(nowMs: number, config: PeakAlertConfig, lastAlertedAt: number, channel: RateChannel): PeakAlertHit | null;
49
53
  //# sourceMappingURL=peak-alert.d.ts.map
@@ -204,6 +204,28 @@ export declare function upcomingTierSwitch(nowMs: number, leadMs: number): {
204
204
  entering: PriceTierId;
205
205
  atMs: number;
206
206
  } | null;
207
+ /** 计费通道的峰谷窗口:无窗口 / DeepSeek 按量分时 / 智谱 Coding Plan 积分分时。 */
208
+ export type RateChannel = 'none' | 'deepseek-metered' | 'zhipu-coding-plan';
209
+ /**
210
+ * 由会话当前模型与订阅状态推断峰谷窗口。DeepSeek 目录模型恒为按量分时;
211
+ * 智谱模型仅在持有 Z.ai Coding Plan(status ok)时适用积分分时——智谱按量价
212
+ * 全天统一,不涉及峰谷。其余模型(含未收录)返回 none:不显示档位、不提醒,
213
+ * 峰谷提示严格跟随当前对话实际使用的模型而非全局规则。
214
+ * @param modelKey - 当前会话最近一轮的计费目录键(归因模型 key);无轮次时 undefined。
215
+ * @param hasZhipuPlan - 是否存在状态正常的 Z.ai Coding Plan 订阅。
216
+ * @returns 命中的峰谷窗口种类。
217
+ */
218
+ export declare function rateChannelOf(modelKey: string | undefined, hasZhipuPlan: boolean): RateChannel;
219
+ /** 按计费通道的峰谷倒计时:none 返回 null(调用方据此隐藏档位 UI 与切换预告)。 */
220
+ export declare function channelCountdown(nowMs: number, channel: RateChannel): {
221
+ tier: PriceTierId;
222
+ nextSwitchInMs: number;
223
+ } | null;
224
+ /** 按计费通道的切换预告:语义同 upcomingTierSwitch,窗口取自通道;none 恒 null。 */
225
+ export declare function channelUpcomingSwitch(nowMs: number, channel: RateChannel, leadMs: number): {
226
+ entering: PriceTierId;
227
+ atMs: number;
228
+ } | null;
207
229
  /**
208
230
  * 切换倒计时短格式:`1h23m` / `45m` / `3m`。导出供测试:纯函数。
209
231
  * @param ms - 剩余毫秒数。
@@ -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.2.9",
4
+ "version": "1.4.0",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",