@dougen/dsh-deepseek-usage 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <!-- 渲染器会给标题 id 加 `user-content-` 前缀(npm 页面、GitHub 都是),跳转链接要用带前缀的锚点 -->
4
4
  [中文文档](#user-content-中文文档)
5
5
 
6
- A sidebar plugin for the **DeepSeek Harness (DSH) Web UI** that answers two questions without leaving the app: **how much balance is left**, and **what the model costs right now**.
6
+ A sidebar plugin for the **DeepSeek Harness (DSH) Web UI** that answers two questions without leaving the app: **how much balance is left**, and **what the model costs right now** — the second one for official DeepSeek models, whose prices the built-in table and DeepSeek's peak/off-peak windows cover.
7
7
 
8
8
  ![DeepSeek usage card / 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
9
9
 
@@ -12,12 +12,11 @@ A sidebar plugin for the **DeepSeek Harness (DSH) Web UI** that answers two ques
12
12
  - **Balance** in the sidebar footer: your DeepSeek account balance (the symbol follows the account currency: `¥` / `$`).
13
13
  - **Unit prices of the active model**: input / input cache / output for the billing window in effect right now. They change when you switch models and when the peak/off-peak window flips.
14
14
  - **Peak / off-peak dot with a countdown**: orange during peak hours (Mon–Fri, excluding Chinese public holidays, 9:00–12:00 and 14:00–18:00 Beijing Time), green otherwise — weekends, public holidays and the make-up workdays that fall on a weekend are all off-peak; next to it, the time left until the next switch (`2d 15h`, `5h 47m`, `23m`).
15
- - Refreshes every 5 minutes, and immediately when you click the card or switch models. Follows the app light/dark theme and the DSH language (Chinese / English).
15
+ - **Unit prices and the peak/off-peak dot are for official DeepSeek models only.** When the active model is not one of them (a third-party provider's model, or a router that reports the provider name as the model), the card shows the balance alone: the built-in price table and DeepSeek's peak/off-peak windows do not apply, so any figure derived from them would not match your bill.
16
+ - Refreshes every 30 seconds, and immediately when you click the card or switch models. Follows the app light/dark theme and the DSH language (Chinese / English).
16
17
 
17
18
  ## DSH compatibility
18
19
 
19
- Before installing, match the plugin version to your DSH line. DSH compares this package's `peerDependencies` on `@deepseek-ai/dsh-credentials` and `@deepseek-ai/dsh-host-webserver` against its own version and refuses to load the plugin when they disagree.
20
-
21
20
  | DSH | Plugin version |
22
21
  | --- | --- |
23
22
  | `0.2.x` (`0.2.0-rc.1` and later) | `0.11.0` and later |
@@ -52,31 +51,31 @@ The card then shows the reason instead of numbers:
52
51
  ### Prices differ from the official page
53
52
 
54
53
  - Prices are the built-in table `PRICE_TABLE` in `lib/pricing.js`, a snapshot of the official pricing page. It changes only when a new version of the plugin ships — there is no local price file and no background sync.
55
- - If the active model is not in the table, the card falls back to `deepseek-chat` pricing.
56
- - Off-peak days come from the holiday table `HOLIDAY_RANGES` in `lib/pricing.js` (and its mirror in `lib/client.js`). The State Council usually publishes next year's arrangement in November; update both lists then. Until you do, holidays are treated as ordinary workdays and the card shows peak prices during them.
54
+ - The card only shows unit prices and the peak/off-peak dot when the active model is an official DeepSeek model (`deepseek-flash`, `deepseek-v4-pro`, `deepseek-chat`, `deepseek-reasoner` and the legacy names). Any other model — a third-party provider's, or a router reporting the provider name as the model — has no official peak/off-peak pricing, so the card hides both instead of showing a figure that would not match your bill.
55
+ - An official model newer than this plugin release is not in the table either, and the card hides prices for it until the table is updated.
56
+ - Off-peak days come from the holiday table `HOLIDAY_RANGES` in `lib/pricing.js`. The State Council usually publishes next year's arrangement in November; update it then. Until you do, holidays are treated as ordinary workdays and the card shows peak prices during them.
57
57
 
58
58
  ## How it works
59
59
 
60
60
  - The **host half** (`lib/index.js`, `lib/pricing.js`) registers `GET /api/deepseek-usage`. Per request it reads the API key from DSH credentials, asks the official balance API, resolves the active model's price from the built-in price table, and answers with balance, tier prices, the peak flag and the next switch time. `GET /api/deepseek-usage/holidays` hands the browser half the same public-holiday table.
61
- - The **browser half** (`lib/client.js`) renders that answer into the `sidebar.footer.action` slot, polls every 5 minutes, and refreshes on click or on a model change. The peak state and the countdown come from the same answer, so the card itself holds no pricing logic.
61
+ - The **browser half** (`lib/client.js`) renders that answer into the `sidebar.footer.action` slot, re-reads it every 30 seconds, and refreshes on click or on a model change. The peak state and the countdown come from the same answer, so the card itself holds no pricing logic.
62
62
 
63
63
  ---
64
64
 
65
65
  ## 中文文档
66
66
 
67
- 一个 **DeepSeek Harness(DSH)网页界面**的侧边栏插件:不离开 DSH 就能知道**账号还剩多少钱**、**当前的模型现在什么价**。
67
+ 一个 **DeepSeek Harness(DSH)网页界面**的侧边栏插件:不离开 DSH 就能知道**账号还剩多少钱**;当前用的是 DeepSeek 官方模型时,还能看到**它现在什么价**——内置价格表与官方峰谷时段覆盖的正是这些模型。
68
68
 
69
69
  ### 这个插件做什么
70
70
 
71
71
  - **余额**:侧边栏底部显示 DeepSeek 账号余额(符号随账号币种 `¥` / `$`)。
72
72
  - **当前模型的档单价**:输入 / 输入缓存 / 输出,按此刻生效的计费时段显示;切换模型、跨过峰谷切换点时会跟着变。
73
73
  - **峰谷指示灯 + 倒计时**:高峰(周一至周五、不含中国法定节假日,9:00–12:00、14:00–18:00 北京时间)橙色,其余时间绿色——周末、法定节假日全天,以及调休上班的周末都是空闲;指示灯旁给出距下次切换的剩余时间(`2d 15h`、`5h 47m`、`23m`)。
74
- - 每 5 分钟自动刷新;点击卡片或切换模型时立即刷新。跟随应用明暗主题与 DSH 语言(中文 / English)。
74
+ - **档单价与峰谷指示灯只对 DeepSeek 官方模型显示**。当前模型不是官方模型时(第三方供应商的模型,或路由器把供应商名当模型名上报),卡片只显示余额:内置价格表与官方峰谷时段对它不成立,据此算出的数字与实际账单不符。
75
+ - 每 30 秒自动刷新;点击卡片或切换模型时立即刷新。跟随应用明暗主题与 DSH 语言(中文 / English)。
75
76
 
76
77
  ### DSH 版本对应关系
77
78
 
78
- 安装前先按 DSH 版本选插件版本:DSH 会用本包的 `peerDependencies`(`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-host-webserver`)与自身版本比对,不一致就拒绝加载插件。
79
-
80
79
  | DSH | 插件版本 |
81
80
  | --- | --- |
82
81
  | `0.2.x`(`0.2.0-rc.1` 起) | `0.11.0` 及以后 |
@@ -109,13 +108,14 @@ dsh plugin --profile web remove @dougen/dsh-deepseek-usage
109
108
  价格与官方不一致时:
110
109
 
111
110
  - 价格是 `lib/pricing.js` 的内置表 `PRICE_TABLE`(官方价格页的快照),只在发布新版本时更新——没有本地价格文件,也没有后台同步。
112
- - 当前模型不在价格表里时,按 `deepseek-chat` 的价格显示。
113
- - 放假日期维护在 `lib/pricing.js` 的 `HOLIDAY_RANGES`(`lib/client.js` 有一份镜像,两处要一起改)。国务院办公厅通常在前一年 11 月发布次年安排,届时更新;未更新前,节假日会按普通工作日处理,白天显示高峰价。
111
+ - 只有当前模型是 DeepSeek 官方模型(`deepseek-flash`、`deepseek-v4-pro`、`deepseek-chat`、`deepseek-reasoner` 及旧名)时,卡片才显示档单价与峰谷指示灯;其他模型(第三方供应商的模型、路由器把供应商名当模型名上报)没有官方峰谷定价,卡片把这两项都隐藏,而不是显示一个与实际账单不符的数字。
112
+ - 比本插件版本更新的官方模型同样不在表里,卡片会隐藏价格,直到价格表更新。
113
+ - 放假日期维护在 `lib/pricing.js` 的 `HOLIDAY_RANGES`。国务院办公厅通常在前一年 11 月发布次年安排,届时更新;未更新前,节假日会按普通工作日处理,白天显示高峰价。
114
114
 
115
115
  ### 简单的工作原理
116
116
 
117
117
  - **宿主侧**(`lib/index.js`、`lib/pricing.js`)注册 `GET /api/deepseek-usage`:每次请求读取 DSH 凭据里的 API Key、调用官方余额接口、从内置价格表解析当前模型档单价,返回余额、档单价、峰谷状态与下次切换时刻;`GET /api/deepseek-usage/holidays` 把同一份法定节假日表交给浏览器侧。
118
- - **浏览器侧**(`lib/client.js`)把这份响应渲染进 `sidebar.footer.action` 槽位,每 5 分钟轮询一次,点击卡片或模型变化时立即刷新;峰谷状态与倒计时同样来自这次响应,界面自身不做计价逻辑。
118
+ - **浏览器侧**(`lib/client.js`)把这份响应渲染进 `sidebar.footer.action` 槽位,每 30 秒重新读一次,点击卡片或模型变化时立即刷新;峰谷状态与倒计时同样来自这次响应,界面自身不做计价逻辑。
119
119
 
120
120
  ## License
121
121
 
package/lib/client.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // dsh-deepseek-usage — browser half.
2
2
  //
3
3
  // 侧边栏底部用量卡片(注册进 `sidebar.footer.action` 槽位,独占一行,位于
4
- // Cordis 面板按钮下方)。轮询宿主路由 `/api/deepseek-usage`(见 lib/index.js)
5
- // 每 5 分钟一次,点击卡片任意位置可立即刷新;还没拿到余额(宿主刚重启/接口刚起来)
6
- // 时按 QUICK_RETRY_MS 阶梯快速补几次请求,卡片几秒内收敛。
4
+ // Cordis 面板按钮下方)。每 30 秒心跳一次:重算倒计时并重新拉取宿主路由
5
+ // `/api/deepseek-usage`(见 lib/index.js),因此余额、档单价与峰谷指示灯都随心跳更新,
6
+ // 跨过峰谷切换点后最多滞后一次心跳;点击卡片任意位置可立即刷新;还没拿到余额
7
+ // (宿主刚重启/接口刚起来)时按 QUICK_RETRY_MS 阶梯快速补几次请求,卡片几秒内收敛。
7
8
  // 两种形态:
8
9
  // · 侧边栏展开——完整卡片:余额(放大显示,符号随币种 ¥/$)+ 当前模型档单价
9
10
  // (输入/输入缓存/输出,图标 + 数字,位于卡片右下角)+ 峰谷指示灯(右上角,
@@ -15,6 +16,8 @@
15
16
  // (历史上圆点的 title 里塞过这些,已删除)。
16
17
  // · 唯一的交互是「点击卡片刷新」;没有可切换的提示态。
17
18
  // · 只有余额类错误(未配置 Key / 余额接口或网络失败)才置空卡片说明原因。
19
+ // · 档单价与峰谷指示灯只对 DeepSeek 官方模型展示(内置价格表命中);其他模型
20
+ // (第三方供应商、路由组合名)不适用官方峰谷定价,整组隐藏,卡片只留余额。
18
21
  // 样式仅使用 `--dsw-*` 主题变量(含兜底值),跟随明暗主题。
19
22
  window.__ModuleLoader__.load({
20
23
  id: "@dougen/dsh-deepseek-usage",
@@ -27,7 +30,9 @@ window.__ModuleLoader__.load({
27
30
  const h = react.createElement;
28
31
 
29
32
  // ---- constants -------------------------------------------------
30
- const POLL_MS = 5 * 60 * 1000;
33
+ // 卡片心跳:每 30 秒重算一次倒计时并重新取一次数(余额 + 当前档单价)。
34
+ // 峰谷切换也因此不需要额外的检测逻辑——每次心跳都重新取数,切换后最多 30 秒翻正。
35
+ const POLL_MS = 30 * 1000;
31
36
  const USAGE_PATH = "/api/deepseek-usage";
32
37
  // 放假日期:宿主路由(见 lib/index.js),浏览器启动时读一次,覆盖内置镜像表;
33
38
  // 读不到就继续用内置表(两边都是同一份国务院办公厅日期表)。
@@ -38,7 +43,7 @@ window.__ModuleLoader__.load({
38
43
  // 异常态把卡片置空时用它兜住同一个高度,侧边栏高度不跳动。
39
44
  const CARD_MIN_HEIGHT = 54;
40
45
  // 卡片还没拿到余额时的快速重试节奏(宿主刚重启、接口刚起来、或本次请求失败):
41
- // 否则要等满 5 分钟轮询才会从空卡片变成真实数字。
46
+ // 让空卡片在几秒内收敛,而不必等满下一次心跳。
42
47
  const QUICK_RETRY_MS = [4000, 8000, 15000, 30000];
43
48
 
44
49
  // ---- i18n(DSH locale 服务;zh 为兜底)----
@@ -69,6 +74,8 @@ window.__ModuleLoader__.load({
69
74
  const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
70
75
  const PEAK_WINDOWS = [[9, 12], [14, 18]];
71
76
  const PEAK_WEEKDAYS_ONLY = true;
77
+ /** 可能的切换时刻(北京时间整点):高峰窗口的起止整点,nextChange 逐日枚举它们。 */
78
+ const BOUNDARY_HOURS = [...new Set(PEAK_WINDOWS.flat())].sort((a, b) => a - b);
72
79
  // 放假日期(含起止,闭区间):国务院办公厅 2026 年节假日安排。
73
80
  const HOLIDAY_RANGES = [
74
81
  ['2026-01-01', '2026-01-03'],
@@ -116,15 +123,23 @@ window.__ModuleLoader__.load({
116
123
 
117
124
  /**
118
125
  * 距下一个计费时段边界的剩余时间;与 lib/pricing.js 的 nextChange 等价。
119
- * 逐分钟向前探测(上限 16 天,覆盖春节长假 → 假期后首个工作日早晨)。
126
+ * 候选时刻是 PEAK_WINDOWS 起止整点落在各天的那些点,逐日枚举取第一个让 isPeak
127
+ * 翻转的,因此返回精确时刻(上限 32 天,覆盖春节长假 → 假期后首个工作日早晨)。
120
128
  */
121
129
  function nextChange(ms) {
122
130
  const now = Number.isFinite(ms) ? ms : Date.now();
123
131
  const current = isPeak(now);
124
- for (let step = 1; step <= 16 * 24 * 60; step += 1) {
125
- const at = now + step * 60 * 1000;
126
- const peak = isPeak(at);
127
- if (peak !== current) return { peak, at, ms: Math.max(0, at - now) };
132
+ const DAY = 24 * 60 * 60 * 1000;
133
+ const hourMs = 60 * 60 * 1000;
134
+ const dayStart = Date.parse(new Date(now + SHANGHAI_OFFSET_MS).toISOString().slice(0, 10) + 'T00:00:00Z') - SHANGHAI_OFFSET_MS;
135
+ for (let day = 0; day < 32; day += 1) {
136
+ const base = dayStart + day * DAY;
137
+ for (const hour of BOUNDARY_HOURS) {
138
+ const at = base + hour * hourMs;
139
+ if (at <= now) continue;
140
+ const peak = isPeak(at);
141
+ if (peak !== current) return { peak, at, ms: at - now };
142
+ }
128
143
  }
129
144
  return { peak: !current, at: now, ms: 0 };
130
145
  }
@@ -250,7 +265,7 @@ window.__ModuleLoader__.load({
250
265
  };
251
266
 
252
267
  // 宿主还在后台忙(重启后首次余额、接口刚起来)时按 QUICK_RETRY_MS 阶梯补几次请求,
253
- // 让卡片几秒内收敛;封顶后回到 5 分钟轮询。失败同样按此节奏重试。
268
+ // 让卡片几秒内收敛,不必等下一次心跳(POLL_MS)。失败同样按此节奏重试。
254
269
  // 判断依据只看"有没有拿到余额",不依赖宿主内部字段。
255
270
  const load = useCallback(async () => {
256
271
  try {
@@ -293,7 +308,6 @@ window.__ModuleLoader__.load({
293
308
  useEffect(() => {
294
309
  mounted.current = true;
295
310
  load();
296
- const timer = setInterval(load, POLL_MS);
297
311
  // 模型切换(写入 settings 触发 document-updated)时立即刷新,单价跟着变。
298
312
  let off = null;
299
313
  try {
@@ -307,7 +321,6 @@ window.__ModuleLoader__.load({
307
321
  }
308
322
  return () => {
309
323
  mounted.current = false;
310
- clearInterval(timer);
311
324
  if (retryTimer.current !== null) {
312
325
  clearTimeout(retryTimer.current);
313
326
  retryTimer.current = null;
@@ -335,24 +348,35 @@ window.__ModuleLoader__.load({
335
348
  return () => { cancelled = true; };
336
349
  }, []);
337
350
 
338
- // 倒计时走字:每 30 秒重算一次渲染(跨过切换瞬间时,不依赖下一次 5 分钟轮询)。
339
- // 30s 而非 60s 是为了保证"剩余不足 1 分钟"的窗口不会被整分钟跳过。
351
+ // 心跳:每 POLL_MS 重算一次倒计时(setTick 触发重渲染)并重新取一次数。
352
+ // 取数会带回最新的余额、峰谷状态与档单价,所以跨过峰谷切换点后单价与指示灯
353
+ // 最多滞后一次心跳——不需要单独检测切换时刻。
340
354
  useEffect(() => {
341
- const timer = setInterval(() => setTick((v) => v + 1), 30 * 1000);
355
+ const timer = setInterval(() => {
356
+ setTick((v) => v + 1);
357
+ load(); // 失败已在 load 内部转成卡片文案并按 QUICK_RETRY_MS 重试
358
+ }, POLL_MS);
342
359
  return () => clearInterval(timer);
343
- }, []);
360
+ }, [load]);
344
361
 
345
362
  const balance = data && data.balance ? data.balance : null;
346
363
  const tier = data && data.tier ? data.tier : null;
347
364
  const symbol = balance && balance.currency === "USD" ? "$" : "¥";
365
+ // 档单价与峰谷指示灯只对 DeepSeek 官方模型有意义:官方价格表与峰谷时段不适用于
366
+ // 第三方供应商的模型(gpt-5-pro 这类),也不适用于路由器把供应商名当模型传上来的
367
+ // 情况(如 codebuddy、codebuddy,deepseek-v4.1-flash)——那种情况下内置表算出的
368
+ // 数字与用户实际账单无关,显示出来就是错的。
369
+ // 宿主用 tier.known 表达这件事(见 lib/pricing.js 的 isOfficialModel / lookup):
370
+ // known=false 就是"这个模型的价格插件不知道",档单价与峰谷都无从展示。
371
+ // 拿不到 tier(宿主刚重启、请求失败)时按已知处理,避免卡片在收敛过程中闪成半张脸。
372
+ const priced = tier ? tier.known !== false : true;
348
373
  // 卡片只显示"用户要的数字":余额、当前档单价、状态圆点、距下次切换的倒计时。
349
374
  // 插件的内部信息(取价路径、模型改名)一律不进 UI,
350
375
  // 唯一的交互是点击卡片刷新。只有"余额类错误"(未配置 Key / 接口或网络失败)才
351
376
  // 把卡片置空并说明原因——否则余额是空的,用户无从判断。
352
377
  const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
353
378
  const blanked = errs.length > 0;
354
- // 每次渲染都以"当前时刻"求剩余时间;tick 每 30 秒 bump 一次触发这次重算,
355
- // 所以跨过切换瞬间时不需要等下一次 5 分钟轮询。
379
+ // 每次渲染都以"当前时刻"求剩余时间;tick 每 30 秒 bump 一次触发这次重算。
356
380
  // nextAt 优先用宿主给的 next.at(避免宿主与浏览器时钟不一致导致数字跳变),
357
381
  // 宿主没给(旧版本 / 字段缺失)时用同一套规则在本地推算。
358
382
  const nowMs = Date.now();
@@ -383,7 +407,9 @@ window.__ModuleLoader__.load({
383
407
  // 折叠态(图标栏):内容宽 36px(侧边栏 56px − 折叠态左右各 10px 内边距),
384
408
  // 只容得下峰谷指示灯,没有外框也没有底色(直接坐在侧边栏背景上)。
385
409
  // 指示灯仍是"点击刷新"的落点。
410
+ // 非官方模型时这个栏里没有任何有意义的内容(余额在折叠态放不下),整块不渲染。
386
411
  if (!wide) {
412
+ if (!priced) return null;
387
413
  return h("div", {
388
414
  style: {
389
415
  boxSizing: "border-box",
@@ -477,7 +503,8 @@ window.__ModuleLoader__.load({
477
503
  // 负 margin 只挪动整组,不改变金额行高度(卡片高度由内边距决定)。
478
504
  // 实测:marginTop -4 与 marginRight -3 时,圆点距卡片顶、右各 8px
479
505
  // (右侧不能按"内边距 10 − 8"去推,内边距本就含在行宽里)。
480
- h("span", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0, marginLeft: "auto", flex: "0 1 auto", alignSelf: "flex-start", marginTop: -4, marginRight: -3 } },
506
+ // 非官方模型时整组不渲染:没有官方峰谷时段,倒计时与指示灯都无从谈起。
507
+ priced && h("span", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0, marginLeft: "auto", flex: "0 1 auto", alignSelf: "flex-start", marginTop: -4, marginRight: -3 } },
481
508
  // 倒计时:距下次峰谷切换的剩余时间(`2d 15h` / `5h 47m` / `23m` / `switching`)。
482
509
  // 纯英文单位、无前缀,中英界面一致。
483
510
  // 字号/字重刻意与右下角单价数字对齐(继承卡片正文 12px、fontWeight 600),
@@ -494,12 +521,13 @@ window.__ModuleLoader__.load({
494
521
  }, countdown),
495
522
  // 峰谷指示灯:只有颜色(橙=高峰 / 绿=空闲),没有 title、没有其他文字。
496
523
  h("span", { style: dot }))),
497
- // 底部行:只剩当前档单价,恒靠右下角。
524
+ // 底部行:只剩当前档单价,恒靠右下角。非官方模型时整行不渲染
525
+ // (内置价格表算出的数字与它们的实际账单无关)。
498
526
  // 这组 bottom:2 + 卡片下内边距 10 是用户看过多组对比后选定的观感(价格略微下移、
499
527
  // 下方不再空一大块)。注意绝对定位的行不参与卡片高度计算,改内边距时两者要一起看。
500
528
  // 用 flex-end 而不是 space-between:容器只有一个子元素时 space-between 会把它
501
529
  // 推到最左侧(0.6.0 的回归点)。
502
- h("span", {
530
+ priced && h("span", {
503
531
  style: { position: "absolute", left: 9, right: 9, bottom: 2, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 6 },
504
532
  },
505
533
  h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, flex: "none" } },
package/lib/pricing.js CHANGED
@@ -12,6 +12,10 @@
12
12
  * reasoner),命中即按现名计价;
13
13
  * · 两条路径都会在 unitAt() 的 via 字段里标出来源,供排查,避免静默算错。
14
14
  *
15
+ * 只对 DeepSeek 官方模型计价(isOfficialModel):第三方供应商的模型与路由器把供应商名
16
+ * 当模型名上报的情况(codebuddy、codebuddy,deepseek-v4.1-flash)一律 known=false,
17
+ * 界面据此隐藏档单价与峰谷指示灯——官方价格表与峰谷时段对它们不成立。
18
+ *
15
19
  * 峰谷(官方口径):高峰时段为北京时间周一至周五(**不含中国法定节假日**)
16
20
  * 9:00–12:00、14:00–18:00,其余(含整个周末、法定节假日全天、以及调休上班的
17
21
  * 周末)为空闲时段;缓存写暂不计费(cacheWrite = 0)。
@@ -127,6 +131,12 @@ export function isPeak(ms) {
127
131
  return PEAK_WINDOWS.some(([start, end]) => h >= start && h < end);
128
132
  }
129
133
 
134
+ /**
135
+ * 可能的切换时刻(北京时间的整点):高峰窗口的起止整点。
136
+ * 翻转只可能发生在这些整点上——窗口外的整点不改变 isPeak,工作日之外则整天不变。
137
+ */
138
+ const BOUNDARY_HOURS = [...new Set(PEAK_WINDOWS.flat())].sort((a, b) => a - b);
139
+
130
140
  /**
131
141
  * 距下一个计费时段边界的剩余时间(供卡片显示倒计时)。
132
142
  *
@@ -137,8 +147,11 @@ export function isPeak(ms) {
137
147
  * · 周五 18:00 之后、整个周末 → 下一次切换是下周一 09:00(跨 2 天多)。
138
148
  * 因此倒计时必须能显示「天」。
139
149
  *
140
- * 实现:逐分钟向前探测(上限 16 天:覆盖春节这类"长假最后一天 18:00 → 假期后
141
- * 第一个工作日 09:00"的最长空档,2026 年春节为 10 天半)。纯算术,无时区/日历依赖。
150
+ * 实现:候选时刻就是 BOUNDARY_HOURS 落在各天的那些整点,从今天起逐日枚举、取第一个
151
+ * 让 isPeak 翻转的,因此返回的是**精确**时刻。纯算术,无时区/日历依赖。
152
+ * (旧实现逐分钟探测,返回的 at 最多晚 59.75 秒,倒计时会因此多显示 1m 或
153
+ * 提前进入 switching。)
154
+ *
142
155
  * 任何口径变更必须同时改 isPeak / PEAK_WINDOWS / PEAK_WEEKDAYS_ONLY / HOLIDAY_RANGES,
143
156
  * 以及 lib/client.js 里为浏览器侧镜像的同名常量(那边无法 import 本模块)。
144
157
  *
@@ -148,13 +161,22 @@ export function isPeak(ms) {
148
161
  export function nextChange(ms) {
149
162
  const now = Number.isFinite(ms) ? ms : Date.now();
150
163
  const current = isPeak(now);
151
- // 逐分钟探测(上限 16 天:覆盖"春节长假 → 假期后首个工作日早晨"的最长空档)
152
- for (let step = 1; step <= 16 * 24 * 60; step += 1) {
153
- const at = now + step * 60 * 1000;
154
- const peak = isPeak(at);
155
- if (peak !== current) return { peak, at, ms: Math.max(0, at - now) };
164
+ const DAY = 24 * 60 * 60 * 1000;
165
+ const hourMs = 60 * 60 * 1000;
166
+ // 北京时间的今天零点
167
+ const dayStart = Date.parse(`${beijingDateKey(now)}T00:00:00Z`) - SHANGHAI_OFFSET_MS;
168
+ // 上限 32 天:远超"长假最后一天 18:00 → 假期后第一个工作日 09:00"的最长空档
169
+ // (2026 年春节为 11 天)
170
+ for (let day = 0; day < 32; day += 1) {
171
+ const base = dayStart + day * DAY;
172
+ for (const hour of BOUNDARY_HOURS) {
173
+ const at = base + hour * hourMs;
174
+ if (at <= now) continue;
175
+ const peak = isPeak(at);
176
+ if (peak !== current) return { peak, at, ms: at - now };
177
+ }
156
178
  }
157
- // 理论上不可达(不变量:一周内必有边界);兜底给"下一天 09:00"方向的最小值
179
+ // 理论上不可达(不变量:一个月内必有工作日整点边界)
158
180
  return { peak: !current, at: now, ms: 0 };
159
181
  }
160
182
 
@@ -164,6 +186,22 @@ function familyOf(id) {
164
186
  return m ? m[1] : null;
165
187
  }
166
188
 
189
+ /**
190
+ * 是否是 DeepSeek 官方模型(官方价格表与峰谷时段只对它们成立)。
191
+ *
192
+ * 只看模型名:官方模型的 id 一律以 `deepseek-` / `deepseek.` 开头(deepseek-flash、
193
+ * deepseek-v4-pro、deepseek-chat、deepseek-reasoner 及其旧名)。第三方供应商的模型
194
+ * (gpt-5-pro 这类)与路由器把供应商名当模型名传进来的情况(codebuddy、
195
+ * codebuddy,deepseek-v4.1-flash)都不匹配——它们的计费口径与官方峰谷价无关,
196
+ * 按内置表算出来的数字会与用户实际账单不一致。
197
+ *
198
+ * 不用"家族匹配"(familyOf)判断:familyOf('gpt-5-pro') 也命中 pro,会把第三方
199
+ * 模型误判成官方 V4 Pro。家族匹配只用于官方模型改名后的取价兜底。
200
+ */
201
+ export function isOfficialModel(model) {
202
+ return typeof model === 'string' && /^deepseek[-.]/i.test(model.trim());
203
+ }
204
+
167
205
  const has = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key);
168
206
 
169
207
  /** 别名(旧名 → 现名)。 */
@@ -189,9 +227,14 @@ function lookupId(id) {
189
227
 
190
228
  /**
191
229
  * 解析模型的计价条目:精确 → 别名 → 同家族唯一匹配 → chat 档兜底。
230
+ * 非官方模型(见 isOfficialModel)不查表:即便名字里带 chat/pro 等家族词,也直接按
231
+ * known=false 返回,由调用方隐藏档单价与峰谷指示灯。
192
232
  * @returns {{ price: object, id: string, via: 'exact'|'alias'|'family'|'fallback', known: boolean }}
193
233
  */
194
234
  function lookup(model) {
235
+ if (!isOfficialModel(model)) {
236
+ return { price: PRICE_TABLE['deepseek-chat'], id: 'deepseek-chat', via: 'fallback', known: false };
237
+ }
195
238
  const found = lookupId(model);
196
239
  if (found) return { price: PRICE_TABLE[found.id], id: found.id, via: found.via, known: true };
197
240
  return { price: PRICE_TABLE['deepseek-chat'], id: 'deepseek-chat', via: 'fallback', known: false };
@@ -204,7 +247,8 @@ export function isKnownModel(model) {
204
247
 
205
248
  /**
206
249
  * 某模型在某一时刻的单位价格(元/百万 tokens)。
207
- * 未知模型回退 chat 档;known=false 供调用方提示,避免静默算错。
250
+ * 非官方模型(见 isOfficialModel)与不在表里的官方模型都会走 chat 档兜底,但
251
+ * known=false 会把这件事告诉调用方:那份兜底价与它们的实际账单无关,界面应隐藏。
208
252
  * via 说明取价路径(exact / alias / family / fallback),供排查与回归测试。
209
253
  */
210
254
  export function unitAt(model, ms) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.11.0",
4
- "description": "A DeepSeek Harness sidebar plugin: your balance (official API), the active model's unit prices and a peak/off-peak indicator, in a Chinese/English UI. / DeepSeek Harness 侧边栏插件:余额(官方 API)、当前模型档单价与峰谷时段指示灯,界面支持中英文。",
3
+ "version": "0.12.0",
4
+ "description": "A DeepSeek Harness sidebar plugin: your balance (official API), and for official DeepSeek models the active model's unit prices and a peak/off-peak indicator, in a Chinese/English UI. / DeepSeek Harness 侧边栏插件:余额(官方 API);当前模型为 DeepSeek 官方模型时,另显示档单价与峰谷时段指示灯。界面支持中英文。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "repository": {