@lancecheney/dsh-deepseek-balance 0.1.1 → 0.1.3

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
@@ -6,6 +6,12 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) client plu
6
6
  Spent ¥0.12 | Balance ¥47.17 | Peak | ¥27.0⁺/M output
7
7
  ```
8
8
 
9
+ ![badge](https://github.com/lancecheney/dsh-plugins/raw/main/docs/screenshot.png)
10
+
11
+ Clicking the badge opens a right-side drawer with full usage stats:
12
+
13
+ ![panel](https://github.com/lancecheney/dsh-plugins/raw/main/docs/panel.png)
14
+
9
15
  ## Features
10
16
 
11
17
  - **Balance** — live DeepSeek API balance from `GET /user/balance`, proxied server-side so your API key never reaches the browser.
@@ -39,6 +45,10 @@ then restart `dsh web` and refresh the page.
39
45
 
40
46
  Requires a `DEEPSEEK_API_KEY` configured through the harness credential store (or the `DEEPSEEK_API_KEY` environment variable).
41
47
 
48
+ ## Security
49
+
50
+ The balance is proxied server-side: the browser only ever talks to the local `/api/deepseek-balance` route, which the host half forwards to DeepSeek. Your API key is resolved from the harness credential store (or the `DEEPSEEK_API_KEY` environment variable) and is only used server-side in the `Authorization: Bearer ...` header when calling DeepSeek — it never reaches the browser. The badge only shows a masked preview (`sk-8ce0e*****d899`) so you can tell which key is in use.
51
+
42
52
  ## Notes
43
53
 
44
54
  - The **Spent** figure is an estimate: all tokens in a session are priced at the current period's rate. Real billing is computed by DeepSeek.
package/README.zh.md CHANGED
@@ -6,6 +6,12 @@
6
6
  消耗 ¥0.12 | 余额 ¥47.17 | 高峰 | ¥27.0⁺/M 输出
7
7
  ```
8
8
 
9
+ ![徽章](https://github.com/lancecheney/dsh-plugins/raw/main/docs/screenshot.png)
10
+
11
+ 点开徽章会打开右侧抽屉面板,展示完整用量统计:
12
+
13
+ ![面板](https://github.com/lancecheney/dsh-plugins/raw/main/docs/panel.png)
14
+
9
15
  ## 功能
10
16
 
11
17
  - **余额** — 实时 DeepSeek API 余额(`GET /user/balance`),由服务端代理请求,浏览器不会接触你的 API Key。
@@ -39,6 +45,10 @@ dsh plugin --profile web add @lancecheney/dsh-deepseek-balance
39
45
 
40
46
  需要已在 harness 凭据存储中配置 `DEEPSEEK_API_KEY`(或设置 `DEEPSEEK_API_KEY` 环境变量)。
41
47
 
48
+ ## 安全
49
+
50
+ 余额接口走服务端代理:浏览器只请求本地 `/api/deepseek-balance` 路由,由 host 半区转发到 DeepSeek。API Key 从 harness 凭据存储(或 `DEEPSEEK_API_KEY` 环境变量)解析,仅在服务端调用 DeepSeek 时放入 `Authorization: Bearer ...` 请求头,绝不进入浏览器。界面只显示脱敏后的预览(`sk-8ce0e*****d899`),方便确认当前用的是哪个密钥。
51
+
42
52
  ## 说明
43
53
 
44
54
  - **消耗** 是估算值:会话内所有 token 都按当前时段价格计算,实际扣费以 DeepSeek 服务端为准。
package/lib/client.js CHANGED
@@ -22,18 +22,41 @@ window.__ModuleLoader__.load({
22
22
  const C = { root: "Dbg1_root", num: "Dbg1_num", sep: "Dbg1_sep", chip: "Dbg1_chip", peak: "Dbg1_peak", offPeak: "Dbg1_offPeak", legacy: "Dbg1_legacy", dimmed: "Dbg1_dimmed", plus: "Dbg1_plus", unit: "Dbg1_unit" };
23
23
  const P = { backdrop: "Dp1_backdrop", panel: "Dp1_panel", head: "Dp1_head", avatar: "Dp1_avatar", name: "Dp1_name", key: "Dp1_key", close: "Dp1_close", body: "Dp1_body", stats: "Dp1_stats", statItem: "Dp1_statItem", statValue: "Dp1_statValue", statLabel: "Dp1_statLabel", sepBar: "Dp1_sepBar", balance: "Dp1_balance", balanceBlock: "Dp1_balanceBlock", balanceBlockLabel: "Dp1_balanceBlockLabel", balanceBlockValue: "Dp1_balanceBlockValue", btn: "Dp1_btn", btnPrimary: "Dp1_btnPrimary", section: "Dp1_section", title: "Dp1_title", row: "Dp1_row", twoCol: "Dp1_twoCol", rowTitle: "Dp1_rowTitle", rowTitleGray: "Dp1_rowTitleGray", rowVal: "Dp1_rowVal", rank: "Dp1_rank", monthNav: "Dp1_monthNav", monthGrids: "Dp1_monthGrids", monthGroup: "Dp1_monthGroup", monthGroupTitle: "Dp1_monthGroupTitle", grid: "Dp1_grid", cell: "Dp1_cell", cellFlat: "Dp1_cellFlat", cellPeak: "Dp1_cellPeak", cellOff: "Dp1_cellOff", cellBlue: "Dp1_cellBlue", legend: "Dp1_legend", dot: "Dp1_dot", nameRow: "Dp1_nameRow", edit: "Dp1_edit", editRow: "Dp1_editRow", editInput: "Dp1_editInput" };
24
24
 
25
- const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-v4-flash": { label: "DeepSeek-V4-Flash" } };
25
+ const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-flash": { label: "DeepSeek-Flash" }, "deepseek-v4-flash": { label: "DeepSeek-Flash" }, "deepseek-v4-flash-vision-exp": { label: "DeepSeek-Flash (vision)" } };
26
26
  const FALLBACK_PRICING = {
27
27
  effectiveFrom: "2026-08-17T00:00:00+08:00",
28
+ weekendFrom: "2026-08-23T00:00:00+08:00",
28
29
  currencies: {
29
- CNY: { symbol: "¥", models: { "deepseek-v4-pro": { legacy: { hit: 0.025, miss: 3.0, output: 6.0 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 }, offPeak: { hit: 0.15, miss: 4.5, output: 13.5 } }, "deepseek-v4-flash": { legacy: { hit: 0.02, miss: 1.0, output: 2.0 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 }, offPeak: { hit: 0.05, miss: 1.5, output: 4.5 } } } },
30
- USD: { symbol: "$", models: { "deepseek-v4-pro": { legacy: { hit: 0.003625, miss: 0.435, output: 0.87 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 }, offPeak: { hit: 0.022, miss: 0.66, output: 1.98 } }, "deepseek-v4-flash": { legacy: { hit: 0.0028, miss: 0.14, output: 0.28 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 }, offPeak: { hit: 0.007, miss: 0.22, output: 0.66 } } } }
30
+ CNY: { symbol: "¥", models: {
31
+ "deepseek-flash": { eras: [
32
+ { from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
33
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 } },
34
+ { from: null, legacy: { hit: 0.02, miss: 1.0, output: 2.0 } }
35
+ ] },
36
+ "deepseek-v4-pro": { eras: [
37
+ { from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
38
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 } },
39
+ { from: null, legacy: { hit: 0.025, miss: 3.0, output: 6.0 } }
40
+ ] }
41
+ } },
42
+ USD: { symbol: "$", models: {
43
+ "deepseek-flash": { eras: [
44
+ { from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
45
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 } },
46
+ { from: null, legacy: { hit: 0.0028, miss: 0.14, output: 0.28 } }
47
+ ] },
48
+ "deepseek-v4-pro": { eras: [
49
+ { from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
50
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 } },
51
+ { from: null, legacy: { hit: 0.003625, miss: 0.435, output: 0.87 } }
52
+ ] }
53
+ } }
31
54
  }
32
55
  };
33
56
  const DEFAULT_MODEL = "deepseek-v4-pro";
34
57
 
35
58
  function beijingDecimalHour(now) { let h = 0, m = 0; try { const p = new Intl.DateTimeFormat("en-GB", { timeZone: "Asia/Shanghai", hour12: false, hour: "2-digit", minute: "2-digit" }).formatToParts(now); for (const x of p) { if (x.type === "hour") h = Number(x.value); if (x.type === "minute") m = Number(x.value); } } catch { h = now.getHours(); m = now.getMinutes(); } return h + m / 60; }
36
- function periodFor(now, effectiveFrom) { const eff = Date.parse(effectiveFrom); if (Number.isFinite(eff) && now.getTime() < eff) return "legacy"; const h = beijingDecimalHour(now); return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak"; }
59
+ function periodFor(now, effectiveFrom, weekendFrom) { const eff = Date.parse(effectiveFrom); if (Number.isFinite(eff) && now.getTime() < eff) return "legacy"; const wk = Date.parse(weekendFrom); const d = new Date(now.getTime() + 8 * 3600 * 1000); const day = d.getUTCDay(); if ((day === 0 || day === 6) && Number.isFinite(wk) && now.getTime() >= wk) return "offPeak"; const h = beijingDecimalHour(now); return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak"; }
37
60
  function firstBalance(data) { const infos = data && data.balance_infos; if (!Array.isArray(infos) || infos.length === 0) return null; return infos.find((r) => r.currency === "CNY") || infos[0]; }
38
61
  function fmtPrice(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : String(v); }
39
62
  function fmtMoney(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; if (n <= 0) return "0.00"; if (n < 0.01) return n.toFixed(4); if (n < 1) return n.toFixed(3); return n.toFixed(2); }
@@ -89,6 +112,7 @@ window.__ModuleLoader__.load({
89
112
 
90
113
  const pricing = state.pricing || FALLBACK_PRICING;
91
114
  const effectiveFrom = pricing.effectiveFrom || FALLBACK_PRICING.effectiveFrom;
115
+ const weekendFrom = pricing.weekendFrom || FALLBACK_PRICING.weekendFrom;
92
116
  const balanceRow = state.phase === "ok" && state.balance ? firstBalance(state.balance) : null;
93
117
  const currency = balanceRow && balanceRow.currency ? balanceRow.currency : "CNY";
94
118
  const dict = currency === "USD" ? en : zh;
@@ -102,12 +126,15 @@ window.__ModuleLoader__.load({
102
126
  const modelId = currentSelection && currentSelection.model ? currentSelection.model : DEFAULT_MODEL;
103
127
  const reasoningEffort = currentSelection ? currentSelection.reasoningEffort : void 0;
104
128
 
105
- const period = periodFor(new Date(), effectiveFrom);
129
+ const period = periodFor(new Date(), effectiveFrom, weekendFrom);
106
130
  const isPeak = period === "peak";
107
131
  const isLegacy = period === "legacy";
108
132
  const model = models[modelId] || models[DEFAULT_MODEL] || FALLBACK_PRICING.currencies.CNY.models[DEFAULT_MODEL];
109
- const p = (period === "peak" ? model.peak : period === "offPeak" ? model.offPeak : model.legacy) || model.peak;
110
- const outputPrice = fmtPrice(p.output);
133
+ const eras = Array.isArray(model.eras) ? model.eras : [];
134
+ const nowMs = Date.now();
135
+ const era = eras.find((e) => e.from != null && nowMs >= Date.parse(e.from)) || eras[eras.length - 1] || model;
136
+ const p = (period === "peak" ? era.peak : period === "offPeak" ? era.offPeak : era.legacy) || era.peak || era.offPeak || era.legacy;
137
+ const outputPrice = fmtPrice(p ? p.output : "—");
111
138
  const cost = state.usage && state.usage.spend ? state.usage.spend.total : null;
112
139
  const plus = reasoningEffort === "max" ? "++" : reasoningEffort === "off" ? "" : "+";
113
140
  const periodClass = isLegacy ? C.legacy : isPeak ? C.peak : C.offPeak;
@@ -233,7 +260,7 @@ window.__ModuleLoader__.load({
233
260
  stat(tr("stat.total"), usageData ? fmtTokens(usageData.totalTokens) : "…"), sepBar("s1"),
234
261
  stat(tr("stat.dailyPeak"), usageData ? fmtTokens(usageData.dailyPeakTokens) : "…"), sepBar("s2"),
235
262
  stat(tr("stat.longest"), usageData ? fmtDuration(usageData.longestChatMs) : "…"), sepBar("s3"),
236
- stat(tr("stat.curStreak"), usageData ? usageData.currentStreak + tr("unit.day") : "…"), sepBar("s4"),
263
+ stat(tr("stat.todayTokens"), usageData && usageData.days && usageData.days[beijingDayKey(Date.now())] ? fmtTokens(usageData.days[beijingDayKey(Date.now())].tokens) : "…"), sepBar("s4"),
237
264
  stat(tr("stat.longStreak"), usageData ? usageData.longestStreak + tr("unit.day") : "…")
238
265
  ),
239
266
  react.createElement("div", { className: P.balance },
@@ -321,7 +348,7 @@ window.__ModuleLoader__.load({
321
348
  "balance.unavailable": "余额不可用",
322
349
  "panel.stats": "统计", "panel.balance": "余额与消耗", "panel.topup": "跳转API官网", "panel.save": "保存",
323
350
  "panel.topSessions": "消耗最多的对话", "panel.month": "每日分布", "panel.monthPeriod": "每日时段分布", "panel.monthTokens": "每日 Token 消耗", "panel.peakSessions": "高峰消耗排行", "panel.modelEffort": "模型与思考强度",
324
- "stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.curStreak": "当前连续", "stat.longStreak": "最长连续", "stat.today": "今日消耗", "stat.cumulative": "累计消耗",
351
+ "stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.longStreak": "最长连续", "stat.todayTokens": "今日 Token", "stat.today": "今日消耗", "stat.cumulative": "累计消耗",
325
352
  "unit.day": " 天",
326
353
  "legend.flat": "平价", "legend.peak": "高峰", "legend.off": "空闲", "legend.none": "无数据", "legend.less": "少", "legend.more": "多"
327
354
  };
@@ -331,7 +358,7 @@ window.__ModuleLoader__.load({
331
358
  "balance.unavailable": "balance unavailable",
332
359
  "panel.stats": "Stats", "panel.balance": "Balance & Spend", "panel.topup": "API site", "panel.save": "Save",
333
360
  "panel.topSessions": "Top conversations", "panel.month": "Daily", "panel.monthPeriod": "Daily period", "panel.monthTokens": "Daily token usage", "panel.peakSessions": "Peak-hour ranking", "panel.modelEffort": "Model & effort",
334
- "stat.total": "Total tokens", "stat.dailyPeak": "Daily peak", "stat.longest": "Longest chat", "stat.curStreak": "Current streak", "stat.longStreak": "Longest streak", "stat.today": "Today", "stat.cumulative": "Total",
361
+ "stat.total": "Total tokens", "stat.dailyPeak": "Daily peak", "stat.longest": "Longest chat", "stat.longStreak": "Longest streak", "stat.todayTokens": "Today tokens", "stat.today": "Today", "stat.cumulative": "Total",
335
362
  "unit.day": "d",
336
363
  "legend.flat": "Flat", "legend.peak": "Peak", "legend.off": "Off-peak", "legend.none": "None", "legend.less": "Less", "legend.more": "More"
337
364
  };
package/lib/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
2
  import { z } from "zod";
4
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
4
  import { homedir } from "node:os";
@@ -26,43 +25,85 @@ const PRICING_CN_URL = "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/
26
25
  const PRICING_US_URL = "https://api-docs.deepseek.com/quick_start/pricing/";
27
26
  const PRICING_FETCH_TIMEOUT_MS = 15000;
28
27
 
29
- /** Last-known-good prices: legacy + the 2026-08-17 peak/off-peak table, per currency. */
28
+ /** Prices per currency. Each model carries an `eras` list (newest first): the
29
+ * currently effective peak/off-peak table plus historical tiers, so tokens are
30
+ * billed at the price in force when they were used. `from: null` is the
31
+ * pre-peak/off-peak legacy (flat) tier. */
30
32
  const FALLBACK_PRICING = {
31
33
  effectiveFrom: "2026-08-17T00:00:00+08:00",
34
+ weekendFrom: "2026-08-23T00:00:00+08:00",
32
35
  currencies: {
33
36
  CNY: {
34
37
  symbol: "¥",
35
38
  models: {
36
- "deepseek-v4-flash": {
37
- legacy: { hit: 0.02, miss: 1.0, output: 2.0 },
38
- peak: { hit: 0.10, miss: 3.0, output: 9.0 },
39
- offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }
39
+ "deepseek-flash": {
40
+ eras: [
41
+ { from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
42
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 } },
43
+ { from: null, legacy: { hit: 0.02, miss: 1.0, output: 2.0 } }
44
+ ]
40
45
  },
41
46
  "deepseek-v4-pro": {
42
- legacy: { hit: 0.025, miss: 3.0, output: 6.0 },
43
- peak: { hit: 0.30, miss: 9.0, output: 27.0 },
44
- offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }
47
+ eras: [
48
+ { from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.02, miss: 1.0, output: 4.0 }, peak: { hit: 0.04, miss: 2.0, output: 8.0 } },
49
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 } },
50
+ { from: null, legacy: { hit: 0.025, miss: 3.0, output: 6.0 } }
51
+ ]
45
52
  }
46
53
  }
47
54
  },
48
55
  USD: {
49
56
  symbol: "$",
50
57
  models: {
51
- "deepseek-v4-flash": {
52
- legacy: { hit: 0.0028, miss: 0.14, output: 0.28 },
53
- peak: { hit: 0.014, miss: 0.44, output: 1.32 },
54
- offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }
58
+ "deepseek-flash": {
59
+ eras: [
60
+ { from: "2026-09-10T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
61
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 } },
62
+ { from: null, legacy: { hit: 0.0028, miss: 0.14, output: 0.28 } }
63
+ ]
55
64
  },
56
65
  "deepseek-v4-pro": {
57
- legacy: { hit: 0.003625, miss: 0.435, output: 0.87 },
58
- peak: { hit: 0.044, miss: 1.32, output: 3.96 },
59
- offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }
66
+ eras: [
67
+ { from: "2026-09-14T12:00:00+08:00", offPeak: { hit: 0.003, miss: 0.15, output: 0.6 }, peak: { hit: 0.006, miss: 0.3, output: 1.2 } },
68
+ { from: "2026-08-17T00:00:00+08:00", offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 } },
69
+ { from: null, legacy: { hit: 0.003625, miss: 0.435, output: 0.87 } }
70
+ ]
60
71
  }
61
72
  }
62
73
  }
63
74
  }
64
75
  };
65
76
 
77
+ /** Legacy model names retired/renamed by DeepSeek; their requests are served by
78
+ * DeepSeek-V4.1-Flash and billed at the Flash price. */
79
+ const MODEL_ALIASES = {
80
+ "deepseek-v4-flash": "deepseek-flash",
81
+ "deepseek-v4-flash-vision-exp": "deepseek-flash"
82
+ };
83
+
84
+ function resolveModelId(rawModel) {
85
+ const id = typeof rawModel === "string" && rawModel.length > 0 ? rawModel : "";
86
+ return MODEL_ALIASES[id] || id;
87
+ }
88
+
89
+ /** Index of the price era in force at `time` (eras are newest-first). */
90
+ function priceEraIndex(modelId, time) {
91
+ const m = FALLBACK_PRICING.currencies.CNY.models[modelId];
92
+ if (!m || !Array.isArray(m.eras)) return 0;
93
+ for (let i = 0; i < m.eras.length; i++) {
94
+ const from = m.eras[i].from;
95
+ if (from === null || from === void 0 || time >= Date.parse(from)) return i;
96
+ }
97
+ return m.eras.length - 1;
98
+ }
99
+
100
+ /** Price era for a model at `time`, falling back to legacy/flat when unknown. */
101
+ function priceEraAt(modelId, time) {
102
+ const m = FALLBACK_PRICING.currencies.CNY.models[modelId];
103
+ if (!m || !Array.isArray(m.eras)) return null;
104
+ return m.eras[priceEraIndex(modelId, time)] || null;
105
+ }
106
+
66
107
  function json(res, status, body) {
67
108
  const payload = JSON.stringify(body);
68
109
  res.writeHead(status, {
@@ -76,7 +117,7 @@ function resolveSettingsSection(ctx) {
76
117
  try {
77
118
  const settings = ctx.get("settings");
78
119
  if (settings && typeof settings.get === "function") {
79
- return settings.get(settingsNamespace("llm-deepseek"));
120
+ return settings.get("llm-deepseek");
80
121
  }
81
122
  } catch {}
82
123
  return void 0;
@@ -136,81 +177,93 @@ const number = (match, index) => {
136
177
  return Number.isFinite(value) ? value : void 0;
137
178
  };
138
179
 
139
- /** Parse the zh-CN docs page: CNY prices + the effective date. */
180
+ /** Parse the zh-CN docs page: CNY peak/off-peak table (2 columns: flash, pro). */
140
181
  function parseCnPricing(html) {
141
182
  const text = stripTags(html);
142
- const parseModel = (id) => {
143
- const re = new RegExp(id + "\\s+空闲时段\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+高峰时段\\s+([\\d.]+)元\\s+([\\d.]+)元\\s+([\\d.]+)元");
183
+ const grab = (label) => {
184
+ const re = new RegExp(label + "\\s*空闲时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*高峰时段\\s*([\\d.]+)元\\s*([\\d.]+)元");
144
185
  const m = text.match(re);
145
186
  if (!m) return void 0;
146
187
  return {
147
- offPeak: { hit: number(m, 1), miss: number(m, 2), output: number(m, 3) },
148
- peak: { hit: number(m, 4), miss: number(m, 5), output: number(m, 6) }
188
+ offPeak: [number(m, 1), number(m, 2)],
189
+ peak: [number(m, 3), number(m, 4)]
149
190
  };
150
191
  };
151
- const flash = parseModel("deepseek-v4-flash");
152
- const pro = parseModel("deepseek-v4-pro");
153
- if (!flash || !pro) throw new Error("CNY peak/off-peak table not found");
154
-
155
- let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
156
- const dm = text.match(/北京时间\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*(\d{1,2}):(\d{2})/);
157
- if (dm) {
158
- const [y, mo, d, h, mi] = [1, 2, 3, 4, 5].map((i) => Number(dm[i]));
159
- effectiveFrom = `${y}-${String(mo).padStart(2, "0")}-${String(d).padStart(2, "0")}T${String(h).padStart(2, "0")}:${String(mi).padStart(2, "0")}:00+08:00`;
160
- }
161
-
162
- let legacy;
163
- const lm = text.match(/百万tokens输入(缓存命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输入(缓存未命中)\s*([\d.]+)元\s*([\d.]+)元\s*百万tokens输出\s*([\d.]+)元\s*([\d.]+)元/);
164
- if (lm) {
165
- legacy = {
166
- flash: { hit: number(lm, 1), miss: number(lm, 3), output: number(lm, 5) },
167
- pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
168
- };
169
- }
192
+ const hit = grab("百万tokens输入\\s*(缓存命中)");
193
+ const miss = grab("百万tokens输入\\s*(缓存未命中)");
194
+ const out = grab("百万tokens输出");
195
+ if (!hit || !miss || !out) throw new Error("CNY peak/off-peak table not found");
196
+
197
+ const mk = (i) => ({
198
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
199
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
200
+ });
170
201
 
171
202
  return {
172
- effectiveFrom,
173
203
  models: {
174
- "deepseek-v4-flash": { ...(legacy?.flash ? { legacy: legacy.flash } : {}), peak: flash.peak, offPeak: flash.offPeak },
175
- "deepseek-v4-pro": { ...(legacy?.pro ? { legacy: legacy.pro } : {}), peak: pro.peak, offPeak: pro.offPeak }
204
+ "deepseek-flash": mk(0),
205
+ "deepseek-v4-pro": mk(1)
176
206
  }
177
207
  };
178
208
  }
179
209
 
180
- /** Parse the English docs page: USD prices. */
210
+ /** Parse the English docs page: USD peak/off-peak table (2 columns: flash, pro). */
181
211
  function parseUsModels(html) {
182
212
  const text = stripTags(html);
183
- const parseModel = (id) => {
184
- const re = new RegExp(id + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
213
+ const grab = (label) => {
214
+ const re = new RegExp(label + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
185
215
  const m = text.match(re);
186
216
  if (!m) return void 0;
187
217
  return {
188
- offPeak: { hit: number(m, 1), miss: number(m, 2), output: number(m, 3) },
189
- peak: { hit: number(m, 4), miss: number(m, 5), output: number(m, 6) }
218
+ offPeak: [number(m, 1), number(m, 2)],
219
+ peak: [number(m, 3), number(m, 4)]
190
220
  };
191
221
  };
192
- const flash = parseModel("deepseek-v4-flash");
193
- const pro = parseModel("deepseek-v4-pro");
194
- if (!flash || !pro) throw new Error("USD peak/off-peak table not found");
195
-
196
- let legacy;
197
- const lm = text.match(/1M INPUT TOKENS \(CACHE HIT\)\s+\$([\d.]+)\s+\$([\d.]+)\s+1M INPUT TOKENS \(CACHE MISS\)\s+\$([\d.]+)\s+\$([\d.]+)\s+1M OUTPUT TOKENS\s+\$([\d.]+)\s+\$([\d.]+)/i);
198
- if (lm) {
199
- legacy = {
200
- flash: { hit: number(lm, 1), miss: number(lm, 3), output: number(lm, 5) },
201
- pro: { hit: number(lm, 2), miss: number(lm, 4), output: number(lm, 6) }
202
- };
203
- }
222
+ const hit = grab("1M INPUT TOKENS\\s*\\(CACHE HIT\\)");
223
+ const miss = grab("1M INPUT TOKENS\\s*\\(CACHE MISS\\)");
224
+ const out = grab("1M OUTPUT TOKENS");
225
+ if (!hit || !miss || !out) throw new Error("USD peak/off-peak table not found");
226
+
227
+ const mk = (i) => ({
228
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
229
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
230
+ });
204
231
 
205
232
  return {
206
- "deepseek-v4-flash": { ...(legacy?.flash ? { legacy: legacy.flash } : {}), peak: flash.peak, offPeak: flash.offPeak },
207
- "deepseek-v4-pro": { ...(legacy?.pro ? { legacy: legacy.pro } : {}), peak: pro.peak, offPeak: pro.offPeak }
233
+ models: {
234
+ "deepseek-flash": mk(0),
235
+ "deepseek-v4-pro": mk(1)
236
+ }
208
237
  };
209
238
  }
210
239
 
211
240
  const pricing = { data: FALLBACK_PRICING, fetchedAt: 0, source: "fallback" };
212
241
  let pricingRefresh = null;
213
242
 
243
+ /** Merge freshly parsed current prices into the era in force right now. */
244
+ function mergeLegacy(models, fallbackModels) {
245
+ const result = {};
246
+ const now = Date.now();
247
+ for (const [id, fb] of Object.entries(fallbackModels)) {
248
+ const parsed = models[id];
249
+ if (!parsed || !Array.isArray(fb.eras)) {
250
+ result[id] = fb;
251
+ continue;
252
+ }
253
+ let curIdx = fb.eras.length - 1;
254
+ for (let i = 0; i < fb.eras.length; i++) {
255
+ const from = fb.eras[i].from;
256
+ if (from !== null && from !== void 0 && now >= Date.parse(from)) {
257
+ curIdx = i;
258
+ break;
259
+ }
260
+ }
261
+ const eras = fb.eras.map((era, i) => (i === curIdx ? { ...era, peak: parsed.peak, offPeak: parsed.offPeak } : era));
262
+ result[id] = { ...fb, eras };
263
+ }
264
+ return result;
265
+ }
266
+
214
267
  async function refreshPricing(ctx) {
215
268
  if (pricingRefresh) return pricingRefresh;
216
269
  pricingRefresh = (async () => {
@@ -218,6 +271,7 @@ async function refreshPricing(ctx) {
218
271
  if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: ${message}`);
219
272
  };
220
273
  let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
274
+ let weekendFrom = FALLBACK_PRICING.weekendFrom;
221
275
  let cnModels = FALLBACK_PRICING.currencies.CNY.models;
222
276
  let usModels = FALLBACK_PRICING.currencies.USD.models;
223
277
  try {
@@ -228,8 +282,7 @@ async function refreshPricing(ctx) {
228
282
  if (cnRes.ok) {
229
283
  try {
230
284
  const parsed = parseCnPricing(await cnRes.text());
231
- effectiveFrom = parsed.effectiveFrom;
232
- cnModels = parsed.models;
285
+ cnModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.CNY.models);
233
286
  } catch (error) {
234
287
  warn(`CNY pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
235
288
  }
@@ -238,7 +291,8 @@ async function refreshPricing(ctx) {
238
291
  }
239
292
  if (usRes.ok) {
240
293
  try {
241
- usModels = parseUsModels(await usRes.text());
294
+ const parsed = parseUsModels(await usRes.text());
295
+ usModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.USD.models);
242
296
  } catch (error) {
243
297
  warn(`USD pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
244
298
  }
@@ -247,6 +301,7 @@ async function refreshPricing(ctx) {
247
301
  }
248
302
  pricing.data = {
249
303
  effectiveFrom,
304
+ weekendFrom,
250
305
  currencies: {
251
306
  CNY: { symbol: "¥", models: cnModels },
252
307
  USD: { symbol: "$", models: usModels }
@@ -333,9 +388,12 @@ function beijingDayKey(ms) {
333
388
  return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10);
334
389
  }
335
390
 
336
- function periodOfUsage(time, effectiveFromMs) {
391
+ function periodOfUsage(time, effectiveFromMs, weekendFromMs) {
337
392
  if (time < effectiveFromMs) return "flat";
338
- const hour = new Date(time + 8 * 3600 * 1000).getUTCHours();
393
+ const d = new Date(time + 8 * 3600 * 1000);
394
+ const day = d.getUTCDay();
395
+ if ((day === 0 || day === 6) && Number.isFinite(weekendFromMs) && time >= weekendFromMs) return "offPeak";
396
+ const hour = d.getUTCHours();
339
397
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18) ? "peak" : "offPeak";
340
398
  }
341
399
 
@@ -378,6 +436,7 @@ function aggregateSessions(archivedIds = []) {
378
436
  if (!existsSync(root)) return summary;
379
437
  const effectiveFromMs = Date.parse(pricing.data.effectiveFrom || FALLBACK_PRICING.effectiveFrom);
380
438
  if (!Number.isFinite(effectiveFromMs)) return summary;
439
+ const weekendFromMs = Date.parse(pricing.data.weekendFrom || FALLBACK_PRICING.weekendFrom);
381
440
 
382
441
  let wsDirs = [];
383
442
  try { wsDirs = readdirSync(root); } catch { return summary; }
@@ -440,13 +499,16 @@ function aggregateSessions(archivedIds = []) {
440
499
  else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") usage = r.data.chunk.usage;
441
500
  if (usage && time !== null && dedupe.has(r)) {
442
501
  const t = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0);
443
- const period = periodOfUsage(time, effectiveFromMs);
502
+ const modelId = resolveModelId(model);
503
+ const eraIdx = priceEraIndex(modelId, time);
504
+ const era = priceEraAt(modelId, time);
505
+ const period = era && era.legacy ? "flat" : periodOfUsage(time, effectiveFromMs, weekendFromMs);
444
506
  const dayKey = beijingDayKey(time);
445
507
  tokens += t;
446
508
  if (period === "peak") peakTokens += t;
447
509
  else if (period === "offPeak") offPeakTokens += t;
448
510
  else flatTokens += t;
449
- const mkModel = model ?? "";
511
+ const mkModel = `${modelId}@${eraIdx}`;
450
512
  const b = bucketsFrom(usage);
451
513
  dayTokens[dayKey] = dayTokens[dayKey] || {};
452
514
  dayTokens[dayKey][mkModel] = dayTokens[dayKey][mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
@@ -545,10 +607,9 @@ function computeSpend(summary, currency = "CNY") {
545
607
  pricing.data.currencies?.CNY ||
546
608
  FALLBACK_PRICING.currencies.CNY;
547
609
  const models = pricingSet.models || {};
548
- const defaultModel = models[DEFAULT_MODEL] || { legacy: null, peak: { miss: 0, hit: 0, output: 0 }, offPeak: { miss: 0, hit: 0, output: 0 } };
549
610
  const priceOf = (tokens, price) => (tokens || 0) * (price || 0) / 1e6;
550
- const bucketCost = (b, mm) => {
551
- if (!mm) return 0;
611
+ const eraCost = (b, era) => {
612
+ if (!era) return 0;
552
613
  const cost = (bucket, price) => {
553
614
  if (!price) return 0;
554
615
  return priceOf(bucket.uncachedInputTokens, price.miss)
@@ -556,22 +617,30 @@ function computeSpend(summary, currency = "CNY") {
556
617
  + priceOf(bucket.cacheReadTokens, price.hit)
557
618
  + priceOf(bucket.outputTokens, price.output);
558
619
  };
559
- return (mm.legacy ? cost(b.flat, mm.legacy) : 0) + cost(b.peak, mm.peak) + cost(b.offPeak, mm.offPeak);
620
+ if (era.legacy) return cost(b.flat, era.legacy);
621
+ return cost(b.peak, era.peak) + cost(b.offPeak, era.offPeak);
622
+ };
623
+ // Bucket keys are `${modelId}@${eraIndex}` so each token is billed at the
624
+ // price era in force when it was used.
625
+ const bucketCost = (b, key) => {
626
+ const at = typeof key === "string" ? key.lastIndexOf("@") : -1;
627
+ if (at < 0) return 0;
628
+ const modelId = key.slice(0, at);
629
+ const eraIdx = Number(key.slice(at + 1));
630
+ const mm = models[modelId];
631
+ if (!mm || !Array.isArray(mm.eras)) return 0;
632
+ const era = mm.eras[Number.isFinite(eraIdx) ? eraIdx : 0] || mm.eras[mm.eras.length - 1];
633
+ return eraCost(b, era);
560
634
  };
561
635
  let total = 0;
562
636
  for (const [k, b] of Object.entries(summary.byModel || {})) {
563
- const [m] = k.split("|");
564
- total += bucketCost(b, models[m] || defaultModel);
637
+ total += bucketCost(b, k);
565
638
  }
566
639
  const day = (summary.days || {})[beijingDayKey(Date.now())];
567
640
  let today = null;
568
641
  if (day) {
569
- if (day.byModel) {
570
- today = 0;
571
- for (const [m, b] of Object.entries(day.byModel)) today += bucketCost(b, models[m] || defaultModel);
572
- } else {
573
- today = bucketCost(day, defaultModel);
574
- }
642
+ today = 0;
643
+ for (const [k, b] of Object.entries(day.byModel || {})) today += bucketCost(b, k);
575
644
  }
576
645
  return { total, today, currency };
577
646
  }
@@ -705,7 +774,7 @@ function apply(ctx, config = {}) {
705
774
  spend: computeSpend({ byModel: s.byModel || {} }, usageCache.currency || "CNY"),
706
775
  apiKeyPreview: key,
707
776
  tokenName: resolveTokenName(config),
708
- effectiveFrom: pricing.data.effectiveFrom,
777
+ effectiveFrom: pricing.data.effectiveFrom, weekendFrom: pricing.data.weekendFrom,
709
778
  fetchedAt: usageCache.at,
710
779
  source: pricing.source
711
780
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lancecheney/dsh-deepseek-balance",
3
3
  "description": "Session-header DeepSeek API balance, per-conversation spend, and peak/off-peak pricing indicator",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {