@kenz1117/dsh-ui-usage-billing 0.2.2 → 0.2.4

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 CHANGED
@@ -14,6 +14,11 @@ DeepSeek Harness 计费仪表盘插件。从持久化会话日志实时聚合模
14
14
  - **真实用量**:服务端从会话日志实时聚合,无需手工维护统计文件。
15
15
  - **模型健康探测**:模型行的圆点反映各厂商接入状态(正常绿 / 异常红 / 未接入灰)。
16
16
  - **订阅计划豁免**:走 coding/token 套餐的模型照常统计 token,费用记 0。
17
+ - **余额查询**:模型计费明细按厂商显示「余额」列,DeepSeek 通过官方余额 API 实时查询;未配置 / Key 无效 / 服务不可达均有状态提示,扩展点可接更多厂商。
18
+ - **实时汇率与定价**:启动时自动拉取腾讯财经行情(USD→CNY)与 OpenRouter 官方模型价,失败自动降级内置默认值;此后每 6 小时自动刷新,单价表标注「今日汇率」与实时 / 内置徽标。
19
+ - **动态刷新**:侧边栏入口与仪表盘每 30 秒自动更新,无需重启或手动刷新。
20
+ - **更新时间**:模型计费明细表头显示最近一次统计的更新时间,精确到时分秒。
21
+ - **北京时间统一**:统计聚合与仪表盘日期一律按北京时间归天,跨零点不漂移。
17
22
  - **离线自包含**:无图表库、无外部 CDN,全部使用设计令牌,适配深色/浅色主题。
18
23
 
19
24
  ## 截图
@@ -52,21 +57,23 @@ npm install @kenz1117/dsh-ui-usage-billing
52
57
  ├─ GET /api/billing/usage-stats ────────▶ ├─ sessionPersistence 遍历持久化会话日志
53
58
  │ ├─ 按 request/header 归属模型
54
59
  │ ├─ token 按缓存命中 / 未命中分桶
55
- │ └─ 按单价表估算费用(人民币)
60
+ │ └─ 按实时单价表估算费用(人民币)
61
+ ├─ GET /api/billing/pricing ────────────▶ ├─ 腾讯财经 / OpenRouter 实时汇率与模型价
62
+ ├─ GET /api/billing/balance ────────────▶ ├─ DeepSeek 官方余额 API(凭据 seam 取 key)
56
63
  ├─ llm.models 健康探测 ─────────────────▶ └─ 返回聚合统计 JSON
57
64
  └─ 渲染仪表盘
58
65
  ```
59
66
 
60
- - **服务端**(`src/index.ts`):注入 `webServer` 与 `sessionPersistence`,注册 `GET /api/billing/usage-stats`。每次调用折叠全部持久化日志:一次 LLM 调用归属到其前置 `request/header` 记录的模型,token 拆分到缓存命中 / 未命中桶,日期按本机时区归天。聚合逻辑见 `src/aggregate.ts`。
67
+ - **服务端**(`src/index.ts`):注入 `webServer`、`sessionPersistence` 与 `credentials`,注册 `GET /api/billing/usage-stats`、`/api/billing/pricing`、`/api/billing/balance`。每次调用折叠全部持久化日志:一次 LLM 调用归属到其前置 `request/header` 记录的模型,token 拆分到缓存命中 / 未命中桶,日期按本机时区归天。聚合逻辑见 `src/aggregate.ts`。
61
68
  - **浏览器端**(`src/client/`):请求上述接口渲染仪表盘,通过 `llm.models` 探测各厂商连接状态。真实数据到达前显示全零空快照,不展示伪造样本。
62
69
 
63
70
  ## 计费引擎
64
71
 
65
- 单价表(`src/client/pricing.ts`)采用**原生币种**存储:国内厂商直接录入人民币价格,国外厂商录入美元价格。费用统一以人民币计算与展示——仅美元计价模型经过汇率(CFETS 中间价 6.79,2026-08-14),国内模型全程不经过汇率换算。
72
+ 单价表(`src/client/pricing.ts`)采用**原生币种**存储:国内厂商直接录入人民币价格,国外厂商录入美元价格。费用统一以人民币计算与展示——美元模型按**实时汇率**折算,国内模型全程不经过汇率换算。启动时服务端拉取实时汇率与模型价(`src/pricing-fetch.ts`):USD→CNY 优先腾讯财经行情(免 key、国内可达),失败依次降级 open.er-api 与内置默认值;之后每 6 小时后台刷新,单价表弹窗标注「今日汇率」与实时 / 内置徽标。
66
73
 
67
74
  ```
68
75
  cost(CNY)= (missInput × p_input + cacheHit × p_cacheHit + output × p_output) / 10⁶
69
- —— 价格为原生币种;美元模型按 USD × 6.79 折算
76
+ —— 价格为原生币种;美元模型按实时 USD CNY 汇率折算
70
77
  ```
71
78
 
72
79
  统计中的 `input` 为总输入(cacheHit + cacheMiss),估算按命中 / 未命中分拆计价,避免重复计费。支持双档计费的模型按 `DEFAULT_PEAK_SHARE`(默认 0.5)混合高峰与低谷档。
@@ -100,6 +107,45 @@ cost(CNY)= (missInput × p_input + cacheHit × p_cacheHit + output × p_outp
100
107
 
101
108
  ## HTTP API
102
109
 
110
+ ### `GET /api/billing/pricing`
111
+
112
+ 实时定价文档(汇率 + 模型价,6 小时后台刷新),浏览器端单价表数据源:
113
+
114
+ ```json
115
+ {
116
+ "source": "live",
117
+ "rate": 7.11,
118
+ "rateTime": "2026-08-16T12:00:00+08:00",
119
+ "models": {
120
+ "deepseek-chat": { "input": 0.5, "output": 2, "cacheHit": 0.1 }
121
+ }
122
+ }
123
+ ```
124
+
125
+ `source` 为 `live`(腾讯财经 / OpenRouter 拉到)或 `builtin`(全部降级内置默认值);`rate` 为 USD→CNY 实时汇率。
126
+
127
+ ### `GET /api/billing/balance`
128
+
129
+ 各接入厂商账户余额(凭据 seam 按 `balanceApiKeyEnv` 取 key):
130
+
131
+ ```json
132
+ {
133
+ "balances": [
134
+ {
135
+ "provider": "deepseek",
136
+ "displayName": "DeepSeek",
137
+ "ok": true,
138
+ "currency": "CNY",
139
+ "total": 12.34,
140
+ "available": 10.56,
141
+ "granted": 1.78
142
+ }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ 查询失败时对应条目带 `error`(`unconfigured` / `unauthorized` / `unreachable`),表格按此渲染状态提示。
148
+
103
149
  ### `GET /api/billing/usage-stats`
104
150
 
105
151
  聚合统计文档,浏览器端数据源:
@@ -135,6 +181,8 @@ cost(CNY)= (missInput × p_input + cacheHit × p_cacheHit + output × p_outp
135
181
  | 字段 | 默认 | 说明 |
136
182
  |---|---|---|
137
183
  | `statsPath` | 未设置 | 回退统计文件 `.dsh-usage-stats.json` 的绝对路径(`sessionPersistence` 不可用时生效) |
184
+ | `balanceApiKeyEnv` | `DEEPSEEK_API_KEY` | 余额查询使用的 DeepSeek 凭据引用(环境变量名),经 `ctx.credentials` 解析 |
185
+ | `subscriptionProviders` | `kimi-coding`、`xiaomi-token-plan-cn` | 订阅制(coding / token 套餐)provider id 列表,照常统计 token、费用记 0 |
138
186
 
139
187
  ## 开发
140
188
 
package/lib/client.js CHANGED
@@ -33,93 +33,93 @@ window.__ModuleLoader__.load({
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var UsageBilling_module_css_default = {
36
- "costCol": "VWh0dG_costCol",
36
+ "panelTitle": "VWh0dG_panelTitle",
37
+ "chartLegendLine": "VWh0dG_chartLegendLine",
38
+ "dashboardModal": "VWh0dG_dashboardModal",
39
+ "emptyRow": "VWh0dG_emptyRow",
40
+ "heroValue": "VWh0dG_heroValue",
41
+ "modelDot": "VWh0dG_modelDot",
42
+ "chartLegendBar": "VWh0dG_chartLegendBar",
37
43
  "rateBadge": "VWh0dG_rateBadge",
38
- "pricingToggleText": "VWh0dG_pricingToggleText",
39
44
  "bandTagOff": "VWh0dG_bandTagOff",
40
- "healthDot": "VWh0dG_healthDot",
41
- "kpiTile": "VWh0dG_kpiTile",
45
+ "heroSideLabel": "VWh0dG_heroSideLabel",
46
+ "triggerAmountSub": "VWh0dG_triggerAmountSub",
47
+ "healthIdle": "VWh0dG_healthIdle",
48
+ "chartGrid": "VWh0dG_chartGrid",
42
49
  "deltaUp": "VWh0dG_deltaUp",
43
- "delta": "VWh0dG_delta",
44
- "panelHead": "VWh0dG_panelHead",
45
- "panelTitle": "VWh0dG_panelTitle",
50
+ "healthBadgeOk": "VWh0dG_healthBadgeOk",
51
+ "kpiValue": "VWh0dG_kpiValue",
52
+ "kpiGrid": "VWh0dG_kpiGrid",
53
+ "heroSide": "VWh0dG_heroSide",
54
+ "chartDot": "VWh0dG_chartDot",
55
+ "heroSideItem": "VWh0dG_heroSideItem",
56
+ "chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
57
+ "deltaDown": "VWh0dG_deltaDown",
58
+ "rateBadgeLive": "VWh0dG_rateBadgeLive",
59
+ "pricingChevron": "VWh0dG_pricingChevron",
60
+ "heroLabel": "VWh0dG_heroLabel",
61
+ "trigger": "VWh0dG_trigger",
62
+ "chartAxisLabel": "VWh0dG_chartAxisLabel",
63
+ "modelTable": "VWh0dG_modelTable",
46
64
  "chartBar": "VWh0dG_chartBar",
65
+ "modelProvider": "VWh0dG_modelProvider",
66
+ "dashboardBody": "VWh0dG_dashboardBody",
67
+ "chartCrosshair": "VWh0dG_chartCrosshair",
68
+ "triggerAmount": "VWh0dG_triggerAmount",
69
+ "chartTooltip": "VWh0dG_chartTooltip",
70
+ "pricingToggleText": "VWh0dG_pricingToggleText",
71
+ "closeButton": "VWh0dG_closeButton",
72
+ "costCol": "VWh0dG_costCol",
73
+ "bandPriceOff": "VWh0dG_bandPriceOff",
47
74
  "hero": "VWh0dG_hero",
48
- "emptyRow": "VWh0dG_emptyRow",
49
- "dashboardModal": "VWh0dG_dashboardModal",
50
- "chartLegendBar": "VWh0dG_chartLegendBar",
51
- "tableScroll": "VWh0dG_tableScroll",
52
75
  "heroMain": "VWh0dG_heroMain",
53
- "heroSide": "VWh0dG_heroSide",
54
- "pricingTable": "VWh0dG_pricingTable",
76
+ "chartEmpty": "VWh0dG_chartEmpty",
77
+ "triggerMeta": "VWh0dG_triggerMeta",
55
78
  "kpiDetail": "VWh0dG_kpiDetail",
56
- "healthBadgeOk": "VWh0dG_healthBadgeOk",
57
- "triggerDivider": "VWh0dG_triggerDivider",
58
- "heroLabel": "VWh0dG_heroLabel",
79
+ "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
80
+ "dashboardTitle": "VWh0dG_dashboardTitle",
81
+ "healthDot": "VWh0dG_healthDot",
82
+ "planTag": "VWh0dG_planTag",
83
+ "heroSideValue": "VWh0dG_heroSideValue",
59
84
  "pricingToggle": "VWh0dG_pricingToggle",
60
- "dashboardRight": "VWh0dG_dashboardRight",
61
- "railButton": "VWh0dG_railButton",
62
- "kpiLabel": "VWh0dG_kpiLabel",
63
- "healthBadge": "VWh0dG_healthBadge",
64
- "triggerMonth": "VWh0dG_triggerMonth",
65
- "triggerToday": "VWh0dG_triggerToday",
66
- "rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
67
- "dashboardSubtitle": "VWh0dG_dashboardSubtitle",
85
+ "triggerDivider": "VWh0dG_triggerDivider",
86
+ "panelHint": "VWh0dG_panelHint",
87
+ "modelCell": "VWh0dG_modelCell",
88
+ "healthOk": "VWh0dG_healthOk",
89
+ "chartWrap": "VWh0dG_chartWrap",
90
+ "bandTag": "VWh0dG_bandTag",
91
+ "kpiGreen": "VWh0dG_kpiGreen",
68
92
  "flatTag": "VWh0dG_flatTag",
93
+ "dashboardHead": "VWh0dG_dashboardHead",
94
+ "pricingTable": "VWh0dG_pricingTable",
95
+ "kpiLabel": "VWh0dG_kpiLabel",
96
+ "numCol": "VWh0dG_numCol",
97
+ "tableScroll": "VWh0dG_tableScroll",
98
+ "chartStack": "VWh0dG_chartStack",
99
+ "modelName": "VWh0dG_modelName",
69
100
  "dashboard": "VWh0dG_dashboard",
70
- "chartWrap": "VWh0dG_chartWrap",
101
+ "rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
102
+ "kpiTile": "VWh0dG_kpiTile",
71
103
  "panel": "VWh0dG_panel",
72
- "chartLegendLine": "VWh0dG_chartLegendLine",
73
- "deltaDown": "VWh0dG_deltaDown",
74
- "kpiGrid": "VWh0dG_kpiGrid",
75
- "chartSvg": "VWh0dG_chartSvg",
76
- "closeButton": "VWh0dG_closeButton",
104
+ "bandPrice": "VWh0dG_bandPrice",
105
+ "healthBad": "VWh0dG_healthBad",
77
106
  "chartLegend": "VWh0dG_chartLegend",
78
- "modelName": "VWh0dG_modelName",
79
- "pricingChevron": "VWh0dG_pricingChevron",
80
- "na": "VWh0dG_na",
81
- "healthBadgeBad": "VWh0dG_healthBadgeBad",
82
- "chartStack": "VWh0dG_chartStack",
107
+ "chartLine": "VWh0dG_chartLine",
83
108
  "chartTooltipRow": "VWh0dG_chartTooltipRow",
84
- "modelTable": "VWh0dG_modelTable",
85
- "chartCrosshair": "VWh0dG_chartCrosshair",
86
- "bandTag": "VWh0dG_bandTag",
87
- "heroValue": "VWh0dG_heroValue",
109
+ "triggerMonth": "VWh0dG_triggerMonth",
110
+ "healthBadgeBad": "VWh0dG_healthBadgeBad",
111
+ "railButton": "VWh0dG_railButton",
112
+ "triggerIcon": "VWh0dG_triggerIcon",
113
+ "healthBadge": "VWh0dG_healthBadge",
114
+ "chartSvg": "VWh0dG_chartSvg",
115
+ "panelHead": "VWh0dG_panelHead",
88
116
  "heroMeta": "VWh0dG_heroMeta",
89
- "healthOk": "VWh0dG_healthOk",
90
- "kpiValue": "VWh0dG_kpiValue",
91
- "planTag": "VWh0dG_planTag",
92
- "heroSideLabel": "VWh0dG_heroSideLabel",
93
- "chartDot": "VWh0dG_chartDot",
94
- "healthBad": "VWh0dG_healthBad",
95
- "kpiGreen": "VWh0dG_kpiGreen",
96
- "heroSideItem": "VWh0dG_heroSideItem",
97
- "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
98
- "dashboardBody": "VWh0dG_dashboardBody",
99
- "panelHint": "VWh0dG_panelHint",
117
+ "delta": "VWh0dG_delta",
100
118
  "chartTooltipDate": "VWh0dG_chartTooltipDate",
101
- "heroSideValue": "VWh0dG_heroSideValue",
102
- "triggerAmount": "VWh0dG_triggerAmount",
103
- "trigger": "VWh0dG_trigger",
104
- "chartAxisLabel": "VWh0dG_chartAxisLabel",
105
- "modelProvider": "VWh0dG_modelProvider",
106
- "chartGrid": "VWh0dG_chartGrid",
107
- "healthIdle": "VWh0dG_healthIdle",
108
- "chartLine": "VWh0dG_chartLine",
109
- "numCol": "VWh0dG_numCol",
110
- "rateBadgeLive": "VWh0dG_rateBadgeLive",
111
- "triggerAmountSub": "VWh0dG_triggerAmountSub",
112
- "bandPrice": "VWh0dG_bandPrice",
113
- "dashboardTitle": "VWh0dG_dashboardTitle",
114
- "triggerIcon": "VWh0dG_triggerIcon",
115
- "dashboardHead": "VWh0dG_dashboardHead",
116
- "bandPriceOff": "VWh0dG_bandPriceOff",
117
- "chartTooltip": "VWh0dG_chartTooltip",
118
- "chartEmpty": "VWh0dG_chartEmpty",
119
- "modelCell": "VWh0dG_modelCell",
120
- "modelDot": "VWh0dG_modelDot",
121
- "chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
122
- "triggerMeta": "VWh0dG_triggerMeta"
119
+ "triggerToday": "VWh0dG_triggerToday",
120
+ "dashboardRight": "VWh0dG_dashboardRight",
121
+ "dashboardSubtitle": "VWh0dG_dashboardSubtitle",
122
+ "na": "VWh0dG_na"
123
123
  };
124
124
  /** 运行时实时覆盖:undefined = 用内置目录与内置汇率(默认值降级)。 */
125
125
  let liveRate;
@@ -665,6 +665,12 @@ window.__ModuleLoader__.load({
665
665
  * Estimate the CNY cost of one model's token usage, mixing the peak and
666
666
  * off-peak bands by the given peak share (flat-priced models cost the same in
667
667
  * both bands).
668
+ *
669
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
670
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
671
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
672
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
673
+ * 时段只能按比例估算,而非逐请求判定。
668
674
  * @param entry - the catalog entry whose prices apply.
669
675
  * @param buckets - token usage counts.
670
676
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
@@ -1062,6 +1068,41 @@ window.__ModuleLoader__.load({
1062
1068
  const USAGE_STATS_PATH = "/api/billing/usage-stats";
1063
1069
  /** Path to the live-pricing endpoint served by this plugin's node half. */
1064
1070
  const PRICING_PATH = "/api/billing/pricing";
1071
+ /** Path to the account-balance endpoint served by this plugin's node half. */
1072
+ const BALANCE_PATH = "/api/billing/balance";
1073
+ /** 弹窗打开期间统计与定价的自动刷新间隔(毫秒)。 */
1074
+ const STATS_REFRESH_INTERVAL_MS = 3e4;
1075
+ /**
1076
+ * 本地时区(北京时间)日期戳:与服务端聚合的 dayStamp 一致。不要用
1077
+ * `toISOString()`——那是 UTC,北京时间的凌晨 0-8 点会取到前一天。
1078
+ */
1079
+ function localDayStamp(time = Date.now()) {
1080
+ const date = new Date(time);
1081
+ const pad = (n) => String(n).padStart(2, "0");
1082
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1083
+ }
1084
+ /** 本地时区时钟:`HH:MM:SS`。 */
1085
+ function formatClock(time) {
1086
+ const date = new Date(time);
1087
+ const pad = (n) => String(n).padStart(2, "0");
1088
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
1089
+ }
1090
+ /**
1091
+ * 高区分度图表色板:趋势图柱、图例与计费表圆点按模型分配。不用模型品牌色
1092
+ * (目录里多为蓝色系,视觉上几乎分不开),保证每个模型一眼可辨。
1093
+ */
1094
+ const CHART_PALETTE = [
1095
+ "#3b82f6",
1096
+ "#06b6d4",
1097
+ "#8b5cf6",
1098
+ "#f59e0b",
1099
+ "#10b981",
1100
+ "#ef4444",
1101
+ "#ec4899",
1102
+ "#6366f1",
1103
+ "#f97316",
1104
+ "#14b8a6"
1105
+ ];
1065
1106
  /** Empty snapshot: shown before (or without) real host data — zeros, never fabricated samples. */
1066
1107
  const EMPTY_STATS = {
1067
1108
  total: {
@@ -1089,7 +1130,8 @@ window.__ModuleLoader__.load({
1089
1130
  total: candidate.total ?? EMPTY_STATS.total,
1090
1131
  byModel: candidate.byModel ?? {},
1091
1132
  byDay: candidate.byDay ?? {},
1092
- ...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {}
1133
+ ...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {},
1134
+ ...candidate.updatedAt !== void 0 ? { updatedAt: candidate.updatedAt } : {}
1093
1135
  };
1094
1136
  } catch {
1095
1137
  return null;
@@ -1122,6 +1164,22 @@ window.__ModuleLoader__.load({
1122
1164
  } catch {}
1123
1165
  }
1124
1166
  /**
1167
+ * 拉取各提供方账户余额(供模型计费明细表的余额列);失败返回空列表。
1168
+ * @returns the balance rows, or an empty list on any failure.
1169
+ */
1170
+ async function fetchBalances() {
1171
+ try {
1172
+ const response = await fetch(BALANCE_PATH);
1173
+ if (!response.ok) return [];
1174
+ const text = await response.text();
1175
+ const parsed = JSON.parse(text);
1176
+ if (parsed !== null && typeof parsed === "object" && "balances" in parsed) return parsed.balances;
1177
+ return [];
1178
+ } catch {
1179
+ return [];
1180
+ }
1181
+ }
1182
+ /**
1125
1183
  * Sidebar footer trigger: compact pill in wide mode, icon in rail mode.
1126
1184
  * @param props - framework props plus `wide` column state.
1127
1185
  */
@@ -1193,15 +1251,30 @@ window.__ModuleLoader__.load({
1193
1251
  }
1194
1252
  /**
1195
1253
  * The centered billing dashboard modal.
1196
- * @param props - stats, locale function, close handler, and model health.
1254
+ * @param props - stats, locale function, close handler, model health, balances.
1197
1255
  */
1198
- function BillingDashboard({ stats, t, onClose, health }) {
1256
+ function BillingDashboard({ stats, t, onClose, health, balances }) {
1199
1257
  const { total, byModel, byDay } = stats;
1200
1258
  const [pricingOpen, setPricingOpen] = (0, react.useState)(false);
1201
1259
  const rateInfo = getRateInfo();
1260
+ const balanceFor = (provider) => balances.find((balance) => normalizeProvider(balance.provider) === normalizeProvider(provider));
1261
+ const renderBalance = (balance) => {
1262
+ if (balance === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1263
+ className: UsageBilling_module_css_default.na,
1264
+ children: "—"
1265
+ });
1266
+ if (balance.error === "unconfigured") return t("billing.balanceUnconfigured");
1267
+ if (balance.error === "unauthorized") return t("billing.balanceUnauthorized");
1268
+ if (balance.error === "unreachable") return t("billing.balanceUnreachable");
1269
+ if (balance.totalBalance === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1270
+ className: UsageBilling_module_css_default.na,
1271
+ children: "—"
1272
+ });
1273
+ return balance.currency === "USD" ? `$${balance.totalBalance.toFixed(2)}` : formatMoney(balance.totalBalance);
1274
+ };
1202
1275
  const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
1203
1276
  const dates = Object.keys(byDay).sort();
1204
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1277
+ const today = localDayStamp();
1205
1278
  const todayCost = byDay[today]?.cost ?? 0;
1206
1279
  const monthPrefix = today.slice(0, 7);
1207
1280
  const yearPrefix = today.slice(0, 4);
@@ -1213,7 +1286,7 @@ window.__ModuleLoader__.load({
1213
1286
  for (let offset = 6; offset >= 0; offset -= 1) {
1214
1287
  const day = /* @__PURE__ */ new Date();
1215
1288
  day.setDate(day.getDate() - offset);
1216
- out.push(day.toISOString().slice(0, 10));
1289
+ out.push(localDayStamp(day.getTime()));
1217
1290
  }
1218
1291
  return out;
1219
1292
  }, []);
@@ -1248,7 +1321,6 @@ window.__ModuleLoader__.load({
1248
1321
  key,
1249
1322
  name: entry.name,
1250
1323
  provider: entry.provider,
1251
- color: resolveToken(entry.colorVar),
1252
1324
  calls: data.calls,
1253
1325
  input: data.input,
1254
1326
  output: data.output,
@@ -1257,7 +1329,10 @@ window.__ModuleLoader__.load({
1257
1329
  plan: isSubscriptionPlan(key),
1258
1330
  ...data.cost > 0 ? { actual: data.cost } : {}
1259
1331
  };
1260
- }).sort((a, b) => (b.actual ?? b.estimated) - (a.actual ?? a.estimated)), [byModel]);
1332
+ }).sort((a, b) => (b.actual ?? b.estimated) - (a.actual ?? a.estimated)).map((row, index) => ({
1333
+ ...row,
1334
+ color: CHART_PALETTE[index % CHART_PALETTE.length] ?? "#8b95a3"
1335
+ })), [byModel]);
1261
1336
  const estimatedTotal = modelRows.reduce((sum, row) => sum + row.estimated, 0);
1262
1337
  const displayTotal = total.cost > 0 ? total.cost : estimatedTotal;
1263
1338
  const avgPerCall = total.calls > 0 ? displayTotal / total.calls : 0;
@@ -1482,13 +1557,9 @@ window.__ModuleLoader__.load({
1482
1557
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1483
1558
  className: UsageBilling_module_css_default.panelTitle,
1484
1559
  children: t("billing.models")
1485
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1560
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1486
1561
  className: UsageBilling_module_css_default.panelHint,
1487
- children: [
1488
- t("billing.estimated"),
1489
- " · ",
1490
- t("billing.pricePerM")
1491
- ]
1562
+ children: stats.updatedAt !== void 0 ? `${t("billing.lastUpdated")} ${formatClock(stats.updatedAt)}` : ""
1492
1563
  })]
1493
1564
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1494
1565
  className: UsageBilling_module_css_default.tableScroll,
@@ -1514,11 +1585,11 @@ window.__ModuleLoader__.load({
1514
1585
  }),
1515
1586
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1516
1587
  className: UsageBilling_module_css_default.numCol,
1517
- children: t("billing.estimated")
1588
+ children: t("billing.actual")
1518
1589
  }),
1519
1590
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1520
1591
  className: UsageBilling_module_css_default.numCol,
1521
- children: t("billing.actual")
1592
+ children: t("billing.balance")
1522
1593
  })
1523
1594
  ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [modelRows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1524
1595
  colSpan: 7,
@@ -1554,13 +1625,6 @@ window.__ModuleLoader__.load({
1554
1625
  className: UsageBilling_module_css_default.numCol,
1555
1626
  children: formatPercent(row.cacheHitRate)
1556
1627
  }),
1557
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1558
- className: clsx(UsageBilling_module_css_default.numCol, UsageBilling_module_css_default.costCol),
1559
- children: row.plan ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1560
- className: UsageBilling_module_css_default.planTag,
1561
- children: "订阅包含"
1562
- }) : formatMoney(row.estimated)
1563
- }),
1564
1628
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1565
1629
  className: UsageBilling_module_css_default.numCol,
1566
1630
  children: row.plan ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1570,6 +1634,10 @@ window.__ModuleLoader__.load({
1570
1634
  className: UsageBilling_module_css_default.na,
1571
1635
  children: "—"
1572
1636
  })
1637
+ }),
1638
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1639
+ className: UsageBilling_module_css_default.numCol,
1640
+ children: renderBalance(balanceFor(row.provider))
1573
1641
  })
1574
1642
  ] }, row.key))] })]
1575
1643
  })
@@ -1703,23 +1771,37 @@ window.__ModuleLoader__.load({
1703
1771
  const { t, checkModels } = props;
1704
1772
  const [stats, setStats] = (0, react.useState)(EMPTY_STATS);
1705
1773
  const [health, setHealth] = (0, react.useState)(IDLE_HEALTH);
1774
+ const [balances, setBalances] = (0, react.useState)([]);
1706
1775
  const [open, setOpen] = (0, react.useState)(false);
1707
1776
  const close = (0, react.useCallback)(() => {
1708
1777
  setOpen(false);
1709
1778
  }, []);
1779
+ const reloadStats = (0, react.useCallback)(() => {
1780
+ loadUsageStats().then((data) => {
1781
+ if (data !== null) setStats(data);
1782
+ });
1783
+ fetchBalances().then((list) => {
1784
+ if (list.length > 0) setBalances(list);
1785
+ });
1786
+ }, []);
1710
1787
  const openDashboard = (0, react.useCallback)(() => {
1788
+ reloadStats();
1789
+ loadLivePricing();
1711
1790
  setOpen(true);
1712
- }, []);
1791
+ }, [reloadStats]);
1713
1792
  (0, react.useEffect)(() => {
1714
- let mounted = true;
1715
- loadUsageStats().then((data) => {
1716
- if (mounted && data !== null) setStats(data);
1717
- });
1793
+ reloadStats();
1718
1794
  loadLivePricing();
1795
+ }, [reloadStats]);
1796
+ (0, react.useEffect)(() => {
1797
+ const timer = setInterval(() => {
1798
+ reloadStats();
1799
+ loadLivePricing();
1800
+ }, STATS_REFRESH_INTERVAL_MS);
1719
1801
  return () => {
1720
- mounted = false;
1802
+ clearInterval(timer);
1721
1803
  };
1722
- }, []);
1804
+ }, [reloadStats]);
1723
1805
  (0, react.useEffect)(() => {
1724
1806
  let mounted = true;
1725
1807
  checkModels().then((result) => {
@@ -1729,7 +1811,7 @@ window.__ModuleLoader__.load({
1729
1811
  mounted = false;
1730
1812
  };
1731
1813
  }, [checkModels]);
1732
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1814
+ const today = localDayStamp();
1733
1815
  const monthCost = Object.entries(stats.byDay).filter(([date]) => date.startsWith(today.slice(0, 7))).reduce((sum, [, day]) => sum + day.cost, 0);
1734
1816
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBillingTrigger, {
1735
1817
  ...props,
@@ -1740,7 +1822,8 @@ window.__ModuleLoader__.load({
1740
1822
  stats,
1741
1823
  t,
1742
1824
  onClose: close,
1743
- health
1825
+ health,
1826
+ balances
1744
1827
  })] });
1745
1828
  }
1746
1829
  //#endregion
@@ -1783,7 +1866,11 @@ window.__ModuleLoader__.load({
1783
1866
  "billing.noData": "暂无计费数据",
1784
1867
  "billing.todayRate": "今日汇率",
1785
1868
  "billing.rateLive": "实时",
1786
- "billing.rateBuiltin": "内置"
1869
+ "billing.rateBuiltin": "内置",
1870
+ "billing.balance": "余额",
1871
+ "billing.balanceUnconfigured": "未配置",
1872
+ "billing.balanceUnauthorized": "密钥无效",
1873
+ "billing.balanceUnreachable": "查询失败"
1787
1874
  };
1788
1875
  const en = {
1789
1876
  "billing.title": "Usage",
@@ -1822,7 +1909,11 @@ window.__ModuleLoader__.load({
1822
1909
  "billing.noData": "No billing data yet",
1823
1910
  "billing.todayRate": "Today rate",
1824
1911
  "billing.rateLive": "Live",
1825
- "billing.rateBuiltin": "Built-in"
1912
+ "billing.rateBuiltin": "Built-in",
1913
+ "billing.balance": "Balance",
1914
+ "billing.balanceUnconfigured": "Not set",
1915
+ "billing.balanceUnauthorized": "Bad key",
1916
+ "billing.balanceUnreachable": "Unavailable"
1826
1917
  };
1827
1918
  //#endregion
1828
1919
  //#region src/client/apply.ts
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
5
  /** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
5
6
  function currentRate() {
6
7
  return 6.79;
@@ -507,6 +508,12 @@ function priceBandCost(band, buckets, currency) {
507
508
  * Estimate the CNY cost of one model's token usage, mixing the peak and
508
509
  * off-peak bands by the given peak share (flat-priced models cost the same in
509
510
  * both bands).
511
+ *
512
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
513
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
514
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
515
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
516
+ * 时段只能按比例估算,而非逐请求判定。
510
517
  * @param entry - the catalog entry whose prices apply.
511
518
  * @param buckets - token usage counts.
512
519
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
@@ -656,6 +663,101 @@ async function aggregateUsage(persistence, options = {}) {
656
663
  };
657
664
  }
658
665
  //#endregion
666
+ //#region lib/types/balance.js
667
+ /**
668
+ * Account-balance queries for the billing dashboard.
669
+ *
670
+ * Only providers with a public balance endpoint can report one. Today that is
671
+ * DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
672
+ * other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
673
+ * balance API, so their rows in the model table show an unavailable state.
674
+ * The lookup map below is the extension point for future providers.
675
+ */
676
+ /** Abort a balance fetch when the upstream hangs beyond this budget. */
677
+ const FETCH_TIMEOUT_MS$1 = 8e3;
678
+ /** DeepSeek 官方余额接口(官方文档 api-docs.deepseek.com/api/get-user-balance)。 */
679
+ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
680
+ /** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
681
+ function toNumber(value) {
682
+ if (typeof value === "number" && Number.isFinite(value)) return value;
683
+ if (typeof value === "string") {
684
+ const parsed = Number(value);
685
+ return Number.isFinite(parsed) ? parsed : void 0;
686
+ }
687
+ }
688
+ /**
689
+ * Query the DeepSeek account balance through the configured credential.
690
+ * @param ctx - host context carrying the credentials seam.
691
+ * @param apiKeyEnv - credential reference resolving the DeepSeek API key.
692
+ * @returns the balance row, or an error row when the key/endpoint misbehaves.
693
+ */
694
+ async function queryDeepSeek(ctx, apiKeyEnv) {
695
+ const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
696
+ if (hit === void 0) return {
697
+ provider: "deepseek",
698
+ displayName: "DeepSeek",
699
+ error: "unconfigured"
700
+ };
701
+ const controller = new AbortController();
702
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$1);
703
+ try {
704
+ const response = await fetch(DEEPSEEK_BALANCE_URL, {
705
+ headers: {
706
+ accept: "application/json",
707
+ authorization: `Bearer ${hit.value}`
708
+ },
709
+ signal: controller.signal
710
+ });
711
+ if (response.status === 401 || response.status === 403) return {
712
+ provider: "deepseek",
713
+ displayName: "DeepSeek",
714
+ error: "unauthorized"
715
+ };
716
+ if (!response.ok) return {
717
+ provider: "deepseek",
718
+ displayName: "DeepSeek",
719
+ error: "unreachable"
720
+ };
721
+ const data = await response.json();
722
+ const info = (Array.isArray(data.balance_infos) ? data.balance_infos : [])[0];
723
+ const currency = typeof info?.currency === "string" ? info.currency : void 0;
724
+ const totalBalance = toNumber(info?.total_balance);
725
+ const grantedBalance = toNumber(info?.granted_balance);
726
+ const toppedUpBalance = toNumber(info?.topped_up_balance);
727
+ const isAvailable = typeof data.is_available === "boolean" ? data.is_available : void 0;
728
+ return {
729
+ provider: "deepseek",
730
+ displayName: "DeepSeek",
731
+ ...currency !== void 0 ? { currency } : {},
732
+ ...totalBalance !== void 0 ? { totalBalance } : {},
733
+ ...grantedBalance !== void 0 ? { grantedBalance } : {},
734
+ ...toppedUpBalance !== void 0 ? { toppedUpBalance } : {},
735
+ ...isAvailable !== void 0 ? { isAvailable } : {}
736
+ };
737
+ } catch {
738
+ return {
739
+ provider: "deepseek",
740
+ displayName: "DeepSeek",
741
+ error: "unreachable"
742
+ };
743
+ } finally {
744
+ clearTimeout(timer);
745
+ }
746
+ }
747
+ const QUERIERS = [{
748
+ provider: "deepseek",
749
+ querier: queryDeepSeek
750
+ }];
751
+ /**
752
+ * Query every configured provider's account balance.
753
+ * @param ctx - host context carrying the credentials seam.
754
+ * @param balanceApiKeyEnv - credential reference for the DeepSeek key.
755
+ * @returns the balance rows (one per provider).
756
+ */
757
+ async function queryBalances(ctx, balanceApiKeyEnv) {
758
+ return await Promise.all(QUERIERS.map(({ querier }) => querier(ctx, balanceApiKeyEnv)));
759
+ }
760
+ //#endregion
659
761
  //#region lib/types/pricing-fetch.js
660
762
  /**
661
763
  * One-shot live pricing refresh for the billing dashboard.
@@ -820,8 +922,16 @@ async function fetchLivePricing() {
820
922
  * missing file answers `{ error }` so the dashboard shows zeros, never
821
923
  * fabricated samples.
822
924
  */
925
+ /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
926
+ const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
927
+ /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
928
+ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
823
929
  /** Required services: the web server and the persisted session log store. */
824
- const inject = ["webServer", "sessionPersistence"];
930
+ const inject = [
931
+ "webServer",
932
+ "sessionPersistence",
933
+ "credentials"
934
+ ];
825
935
  /**
826
936
  * Host plugin body: serve real aggregated usage to the browser dashboard.
827
937
  * @param ctx - host context carrying webServer and sessionPersistence.
@@ -836,9 +946,18 @@ function apply(ctx, config = {}) {
836
946
  join(homedir(), ".dsh/.dsh-usage-stats.json")
837
947
  ].filter((path) => typeof path === "string" && path.length > 0);
838
948
  let live = { source: "builtin" };
839
- fetchLivePricing().then((result) => {
840
- live = result;
841
- });
949
+ const refreshPricing = async () => {
950
+ live = await fetchLivePricing();
951
+ };
952
+ refreshPricing();
953
+ ctx.effect(() => {
954
+ const timer = setInterval(() => {
955
+ refreshPricing();
956
+ }, PRICING_REFRESH_INTERVAL_MS);
957
+ return () => {
958
+ clearInterval(timer);
959
+ };
960
+ }, "usage-billing: pricing refresh timer");
842
961
  ctx.effect(() => ctx.webServer.register({
843
962
  kind: "exact",
844
963
  path: "/api/billing/pricing",
@@ -847,6 +966,15 @@ function apply(ctx, config = {}) {
847
966
  res.end(JSON.stringify(live));
848
967
  }
849
968
  }), "usage-billing: pricing route");
969
+ ctx.effect(() => ctx.webServer.register({
970
+ kind: "exact",
971
+ path: "/api/billing/balance",
972
+ handler: async (_req, res) => {
973
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
974
+ const balances = await queryBalances(ctx, config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV);
975
+ res.end(JSON.stringify({ balances }));
976
+ }
977
+ }), "usage-billing: balance route");
850
978
  ctx.effect(() => ctx.webServer.register({
851
979
  kind: "exact",
852
980
  path: "/api/billing/usage-stats",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Account-balance queries for the billing dashboard.
3
+ *
4
+ * Only providers with a public balance endpoint can report one. Today that is
5
+ * DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
6
+ * other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
7
+ * balance API, so their rows in the model table show an unavailable state.
8
+ * The lookup map below is the extension point for future providers.
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import type { ProviderBalance } from './pricing-shared.ts';
12
+ /**
13
+ * Query every configured provider's account balance.
14
+ * @param ctx - host context carrying the credentials seam.
15
+ * @param balanceApiKeyEnv - credential reference for the DeepSeek key.
16
+ * @returns the balance rows (one per provider).
17
+ */
18
+ export declare function queryBalances(ctx: Context, balanceApiKeyEnv: string): Promise<readonly ProviderBalance[]>;
19
+ //# sourceMappingURL=balance.d.ts.map
@@ -1,9 +1,11 @@
1
1
  /**
2
- * TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
3
- * Each day's column stacks every model's share in its brand color, so the
4
- * total trend and the per-model composition are visible at once. A hover
5
- * crosshair shows the day's model breakdown. No chart library the surface
6
- * stays self-contained and offline.
2
+ * TrendChart: dependency-free SVG chart of daily cost + calls.
3
+ *
4
+ * The columns are GROUPED per model one bar per model per day, each in its
5
+ * brand color, so per-model cost is directly comparable. The blue line is the
6
+ * total call volume across all models, plotted on its own right-hand axis.
7
+ * A hover crosshair shows the day's model breakdown. No chart library — the
8
+ * surface stays self-contained and offline.
7
9
  */
8
10
  /** One model's legend identity: key, display name, and brand color. */
9
11
  export interface TrendSeriesModel {
@@ -11,7 +13,7 @@ export interface TrendSeriesModel {
11
13
  key: string;
12
14
  /** Human-readable model name. */
13
15
  name: string;
14
- /** Resolved brand color for the stack segment and legend swatch. */
16
+ /** Resolved brand color for the bar and legend swatch (empty = single-color fallback). */
15
17
  color: string;
16
18
  }
17
19
  /** One day row fed to the chart. */
@@ -20,15 +22,15 @@ export interface TrendPoint {
20
22
  date: string;
21
23
  /** Total cost that day. */
22
24
  cost: number;
23
- /** API calls that day. */
25
+ /** API calls that day (total across models). */
24
26
  calls: number;
25
- /** Per-model cost that day (stats key → CNY); absent entries stack zero. */
27
+ /** Per-model cost that day (stats key → CNY); absent entries plot zero. */
26
28
  byModel?: Readonly<Record<string, number>>;
27
29
  }
28
30
  /**
29
- * Render the daily per-model stacked cost chart.
31
+ * Render the daily grouped cost bars plus the total-calls line.
30
32
  * @param props.data - sorted daily rows (ascending date).
31
- * @param props.models - the model legend, in stack order (bottom first).
33
+ * @param props.models - the model legend, in bar order.
32
34
  */
33
35
  export declare function TrendChart({ data, models }: {
34
36
  data: readonly TrendPoint[];
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable';
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>;
@@ -115,6 +115,12 @@ export declare function resolveToken(name: string): string;
115
115
  * Estimate the CNY cost of one model's token usage, mixing the peak and
116
116
  * off-peak bands by the given peak share (flat-priced models cost the same in
117
117
  * both bands).
118
+ *
119
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
120
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
121
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
122
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
123
+ * 时段只能按比例估算,而非逐请求判定。
118
124
  * @param entry - the catalog entry whose prices apply.
119
125
  * @param buckets - token usage counts.
120
126
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
@@ -16,6 +16,8 @@ export interface UsageBillingConfig {
16
16
  statsPath?: string;
17
17
  /** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
18
18
  subscriptionProviders?: string[];
19
+ /** 余额查询用的 DeepSeek 凭据引用(环境变量名);默认 DEEPSEEK_API_KEY。 */
20
+ balanceApiKeyEnv?: string;
19
21
  }
20
22
  /** Required services: the web server and the persisted session log store. */
21
23
  export declare const inject: string[];
@@ -22,4 +22,29 @@ export interface LivePricing {
22
22
  /** Overrides keyed by built-in catalog key (present when router matches succeeded). */
23
23
  prices?: Record<string, LivePrice>;
24
24
  }
25
+ /** 余额查询失败的原因,前端据此显示文案。 */
26
+ export type BalanceError = 'unconfigured' | 'unauthorized' | 'unreachable';
27
+ /** 一个提供方的账户余额(`/api/billing/balance` 的一行)。 */
28
+ export interface ProviderBalance {
29
+ /** 提供方 id(小写,如 `deepseek`),与模型表 provider 匹配用。 */
30
+ provider: string;
31
+ /** 显示名(如 `DeepSeek`)。 */
32
+ displayName: string;
33
+ /** 余额币种(CNY / USD)。 */
34
+ currency?: string;
35
+ /** 总可用余额(含赠金与充值)。 */
36
+ totalBalance?: number;
37
+ /** 未过期赠金余额。 */
38
+ grantedBalance?: number;
39
+ /** 充值余额。 */
40
+ toppedUpBalance?: number;
41
+ /** 余额是否足以继续调用。 */
42
+ isAvailable?: boolean;
43
+ /** 未配置/鉴权失败/网络不可达等失败原因;缺省 = 查询成功。 */
44
+ error?: BalanceError;
45
+ }
46
+ /** Response of `/api/billing/balance` consumed by the dashboard. */
47
+ export interface BalanceResponse {
48
+ balances: readonly ProviderBalance[];
49
+ }
25
50
  //# sourceMappingURL=pricing-shared.d.ts.map
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": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },