@lancecheney/dsh-deepseek-balance 0.1.1 → 0.1.2

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
@@ -25,15 +25,16 @@ window.__ModuleLoader__.load({
25
25
  const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-v4-flash": { label: "DeepSeek-V4-Flash" } };
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: { "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 } }, "deepseek-v4-flash-vision-exp": { 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 } } } },
31
+ 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 } }, "deepseek-v4-flash-vision-exp": { 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 } } } }
31
32
  }
32
33
  };
33
34
  const DEFAULT_MODEL = "deepseek-v4-pro";
34
35
 
35
36
  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"; }
37
+ 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
38
  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
39
  function fmtPrice(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : String(v); }
39
40
  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 +90,7 @@ window.__ModuleLoader__.load({
89
90
 
90
91
  const pricing = state.pricing || FALLBACK_PRICING;
91
92
  const effectiveFrom = pricing.effectiveFrom || FALLBACK_PRICING.effectiveFrom;
93
+ const weekendFrom = pricing.weekendFrom || FALLBACK_PRICING.weekendFrom;
92
94
  const balanceRow = state.phase === "ok" && state.balance ? firstBalance(state.balance) : null;
93
95
  const currency = balanceRow && balanceRow.currency ? balanceRow.currency : "CNY";
94
96
  const dict = currency === "USD" ? en : zh;
@@ -102,7 +104,7 @@ window.__ModuleLoader__.load({
102
104
  const modelId = currentSelection && currentSelection.model ? currentSelection.model : DEFAULT_MODEL;
103
105
  const reasoningEffort = currentSelection ? currentSelection.reasoningEffort : void 0;
104
106
 
105
- const period = periodFor(new Date(), effectiveFrom);
107
+ const period = periodFor(new Date(), effectiveFrom, weekendFrom);
106
108
  const isPeak = period === "peak";
107
109
  const isLegacy = period === "legacy";
108
110
  const model = models[modelId] || models[DEFAULT_MODEL] || FALLBACK_PRICING.currencies.CNY.models[DEFAULT_MODEL];
@@ -233,7 +235,7 @@ window.__ModuleLoader__.load({
233
235
  stat(tr("stat.total"), usageData ? fmtTokens(usageData.totalTokens) : "…"), sepBar("s1"),
234
236
  stat(tr("stat.dailyPeak"), usageData ? fmtTokens(usageData.dailyPeakTokens) : "…"), sepBar("s2"),
235
237
  stat(tr("stat.longest"), usageData ? fmtDuration(usageData.longestChatMs) : "…"), sepBar("s3"),
236
- stat(tr("stat.curStreak"), usageData ? usageData.currentStreak + tr("unit.day") : "…"), sepBar("s4"),
238
+ stat(tr("stat.todayTokens"), usageData && usageData.days && usageData.days[beijingDayKey(Date.now())] ? fmtTokens(usageData.days[beijingDayKey(Date.now())].tokens) : "…"), sepBar("s4"),
237
239
  stat(tr("stat.longStreak"), usageData ? usageData.longestStreak + tr("unit.day") : "…")
238
240
  ),
239
241
  react.createElement("div", { className: P.balance },
@@ -321,7 +323,7 @@ window.__ModuleLoader__.load({
321
323
  "balance.unavailable": "余额不可用",
322
324
  "panel.stats": "统计", "panel.balance": "余额与消耗", "panel.topup": "跳转API官网", "panel.save": "保存",
323
325
  "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": "累计消耗",
326
+ "stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.longStreak": "最长连续", "stat.todayTokens": "今日 Token", "stat.today": "今日消耗", "stat.cumulative": "累计消耗",
325
327
  "unit.day": " 天",
326
328
  "legend.flat": "平价", "legend.peak": "高峰", "legend.off": "空闲", "legend.none": "无数据", "legend.less": "少", "legend.more": "多"
327
329
  };
@@ -331,7 +333,7 @@ window.__ModuleLoader__.load({
331
333
  "balance.unavailable": "balance unavailable",
332
334
  "panel.stats": "Stats", "panel.balance": "Balance & Spend", "panel.topup": "API site", "panel.save": "Save",
333
335
  "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",
336
+ "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
337
  "unit.day": "d",
336
338
  "legend.flat": "Flat", "legend.peak": "Peak", "legend.off": "Off-peak", "legend.none": "None", "legend.less": "Less", "legend.more": "More"
337
339
  };
package/lib/index.js CHANGED
@@ -29,6 +29,7 @@ const PRICING_FETCH_TIMEOUT_MS = 15000;
29
29
  /** Last-known-good prices: legacy + the 2026-08-17 peak/off-peak table, per currency. */
30
30
  const FALLBACK_PRICING = {
31
31
  effectiveFrom: "2026-08-17T00:00:00+08:00",
32
+ weekendFrom: "2026-08-23T00:00:00+08:00",
32
33
  currencies: {
33
34
  CNY: {
34
35
  symbol: "¥",
@@ -42,6 +43,11 @@ const FALLBACK_PRICING = {
42
43
  legacy: { hit: 0.025, miss: 3.0, output: 6.0 },
43
44
  peak: { hit: 0.30, miss: 9.0, output: 27.0 },
44
45
  offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }
46
+ },
47
+ "deepseek-v4-flash-vision-exp": {
48
+ legacy: { hit: 0.02, miss: 1.0, output: 2.0 },
49
+ peak: { hit: 0.10, miss: 3.0, output: 9.0 },
50
+ offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }
45
51
  }
46
52
  }
47
53
  },
@@ -57,6 +63,11 @@ const FALLBACK_PRICING = {
57
63
  legacy: { hit: 0.003625, miss: 0.435, output: 0.87 },
58
64
  peak: { hit: 0.044, miss: 1.32, output: 3.96 },
59
65
  offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }
66
+ },
67
+ "deepseek-v4-flash-vision-exp": {
68
+ legacy: { hit: 0.0028, miss: 0.14, output: 0.28 },
69
+ peak: { hit: 0.014, miss: 0.44, output: 1.32 },
70
+ offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }
60
71
  }
61
72
  }
62
73
  }
@@ -136,81 +147,84 @@ const number = (match, index) => {
136
147
  return Number.isFinite(value) ? value : void 0;
137
148
  };
138
149
 
139
- /** Parse the zh-CN docs page: CNY prices + the effective date. */
150
+ /** Parse the zh-CN docs page: CNY peak/off-peak table (3 columns). */
140
151
  function parseCnPricing(html) {
141
152
  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.]+)元");
153
+ const grab = (label) => {
154
+ const re = new RegExp(label + "\\s*空闲时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*高峰时段\\s*([\\d.]+)元\\s*([\\d.]+)元\\s*([\\d.]+)元");
144
155
  const m = text.match(re);
145
156
  if (!m) return void 0;
146
157
  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) }
158
+ offPeak: [number(m, 1), number(m, 2), number(m, 3)],
159
+ peak: [number(m, 4), number(m, 5), number(m, 6)]
149
160
  };
150
161
  };
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
- }
162
+ const hit = grab("百万tokens输入\\s*(缓存命中)");
163
+ const miss = grab("百万tokens输入\\s*(缓存未命中)");
164
+ const out = grab("百万tokens输出");
165
+ if (!hit || !miss || !out) throw new Error("CNY peak/off-peak table not found");
166
+
167
+ const mk = (i) => ({
168
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
169
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
170
+ });
170
171
 
171
172
  return {
172
- effectiveFrom,
173
173
  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 }
174
+ "deepseek-v4-flash": mk(0),
175
+ "deepseek-v4-pro": mk(1),
176
+ "deepseek-v4-flash-vision-exp": mk(2)
176
177
  }
177
178
  };
178
179
  }
179
180
 
180
- /** Parse the English docs page: USD prices. */
181
+ /** Parse the English docs page: USD peak/off-peak table (3 columns). */
181
182
  function parseUsModels(html) {
182
183
  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");
184
+ const grab = (label) => {
185
+ const re = new RegExp(label + "\\s+OFF-PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+PEAK\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)\\s+\\$([\\d.]+)", "i");
185
186
  const m = text.match(re);
186
187
  if (!m) return void 0;
187
188
  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) }
189
+ offPeak: [number(m, 1), number(m, 2), number(m, 3)],
190
+ peak: [number(m, 4), number(m, 5), number(m, 6)]
190
191
  };
191
192
  };
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
- }
193
+ const hit = grab("1M INPUT TOKENS\\s*\\(CACHE HIT\\)");
194
+ const miss = grab("1M INPUT TOKENS\\s*\\(CACHE MISS\\)");
195
+ const out = grab("1M OUTPUT TOKENS");
196
+ if (!hit || !miss || !out) throw new Error("USD peak/off-peak table not found");
197
+
198
+ const mk = (i) => ({
199
+ offPeak: { hit: hit.offPeak[i], miss: miss.offPeak[i], output: out.offPeak[i] },
200
+ peak: { hit: hit.peak[i], miss: miss.peak[i], output: out.peak[i] }
201
+ });
204
202
 
205
203
  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 }
204
+ models: {
205
+ "deepseek-v4-flash": mk(0),
206
+ "deepseek-v4-pro": mk(1),
207
+ "deepseek-v4-flash-vision-exp": mk(2)
208
+ }
208
209
  };
209
210
  }
210
211
 
211
212
  const pricing = { data: FALLBACK_PRICING, fetchedAt: 0, source: "fallback" };
212
213
  let pricingRefresh = null;
213
214
 
215
+ function mergeLegacy(models, fallbackModels) {
216
+ const result = {};
217
+ for (const [id, m] of Object.entries(models)) {
218
+ const fb = fallbackModels[id];
219
+ result[id] = {
220
+ ...(fb && fb.legacy ? { legacy: fb.legacy } : {}),
221
+ peak: m.peak,
222
+ offPeak: m.offPeak
223
+ };
224
+ }
225
+ return result;
226
+ }
227
+
214
228
  async function refreshPricing(ctx) {
215
229
  if (pricingRefresh) return pricingRefresh;
216
230
  pricingRefresh = (async () => {
@@ -218,6 +232,7 @@ async function refreshPricing(ctx) {
218
232
  if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: ${message}`);
219
233
  };
220
234
  let effectiveFrom = FALLBACK_PRICING.effectiveFrom;
235
+ let weekendFrom = FALLBACK_PRICING.weekendFrom;
221
236
  let cnModels = FALLBACK_PRICING.currencies.CNY.models;
222
237
  let usModels = FALLBACK_PRICING.currencies.USD.models;
223
238
  try {
@@ -228,8 +243,7 @@ async function refreshPricing(ctx) {
228
243
  if (cnRes.ok) {
229
244
  try {
230
245
  const parsed = parseCnPricing(await cnRes.text());
231
- effectiveFrom = parsed.effectiveFrom;
232
- cnModels = parsed.models;
246
+ cnModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.CNY.models);
233
247
  } catch (error) {
234
248
  warn(`CNY pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
235
249
  }
@@ -238,7 +252,8 @@ async function refreshPricing(ctx) {
238
252
  }
239
253
  if (usRes.ok) {
240
254
  try {
241
- usModels = parseUsModels(await usRes.text());
255
+ const parsed = parseUsModels(await usRes.text());
256
+ usModels = mergeLegacy(parsed.models, FALLBACK_PRICING.currencies.USD.models);
242
257
  } catch (error) {
243
258
  warn(`USD pricing parse failed: ${error instanceof Error ? error.message : String(error)}`);
244
259
  }
@@ -247,6 +262,7 @@ async function refreshPricing(ctx) {
247
262
  }
248
263
  pricing.data = {
249
264
  effectiveFrom,
265
+ weekendFrom,
250
266
  currencies: {
251
267
  CNY: { symbol: "¥", models: cnModels },
252
268
  USD: { symbol: "$", models: usModels }
@@ -333,9 +349,12 @@ function beijingDayKey(ms) {
333
349
  return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10);
334
350
  }
335
351
 
336
- function periodOfUsage(time, effectiveFromMs) {
352
+ function periodOfUsage(time, effectiveFromMs, weekendFromMs) {
337
353
  if (time < effectiveFromMs) return "flat";
338
- const hour = new Date(time + 8 * 3600 * 1000).getUTCHours();
354
+ const d = new Date(time + 8 * 3600 * 1000);
355
+ const day = d.getUTCDay();
356
+ if ((day === 0 || day === 6) && Number.isFinite(weekendFromMs) && time >= weekendFromMs) return "offPeak";
357
+ const hour = d.getUTCHours();
339
358
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18) ? "peak" : "offPeak";
340
359
  }
341
360
 
@@ -378,6 +397,7 @@ function aggregateSessions(archivedIds = []) {
378
397
  if (!existsSync(root)) return summary;
379
398
  const effectiveFromMs = Date.parse(pricing.data.effectiveFrom || FALLBACK_PRICING.effectiveFrom);
380
399
  if (!Number.isFinite(effectiveFromMs)) return summary;
400
+ const weekendFromMs = Date.parse(pricing.data.weekendFrom || FALLBACK_PRICING.weekendFrom);
381
401
 
382
402
  let wsDirs = [];
383
403
  try { wsDirs = readdirSync(root); } catch { return summary; }
@@ -440,7 +460,7 @@ function aggregateSessions(archivedIds = []) {
440
460
  else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") usage = r.data.chunk.usage;
441
461
  if (usage && time !== null && dedupe.has(r)) {
442
462
  const t = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0);
443
- const period = periodOfUsage(time, effectiveFromMs);
463
+ const period = periodOfUsage(time, effectiveFromMs, weekendFromMs);
444
464
  const dayKey = beijingDayKey(time);
445
465
  tokens += t;
446
466
  if (period === "peak") peakTokens += t;
@@ -705,7 +725,7 @@ function apply(ctx, config = {}) {
705
725
  spend: computeSpend({ byModel: s.byModel || {} }, usageCache.currency || "CNY"),
706
726
  apiKeyPreview: key,
707
727
  tokenName: resolveTokenName(config),
708
- effectiveFrom: pricing.data.effectiveFrom,
728
+ effectiveFrom: pricing.data.effectiveFrom, weekendFrom: pricing.data.weekendFrom,
709
729
  fetchedAt: usageCache.at,
710
730
  source: pricing.source
711
731
  });
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.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {