@dougen/dsh-deepseek-usage 0.10.2 → 0.11.1

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
@@ -12,7 +12,16 @@ 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
+ - 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
+ ## DSH compatibility
18
+
19
+ | DSH | Plugin version |
20
+ | --- | --- |
21
+ | `0.2.x` (`0.2.0-rc.1` and later) | `0.11.0` and later |
22
+ | `0.1.x` (`0.1.0-rc.6` and later) | `0.10.x`, or `0.11.0` and later |
23
+
24
+ `dsh --version` prints your DSH version. Installing without a version picks the newest compatible release; name an exact version (`@dougen/dsh-deepseek-usage@0.10.2`) when the newest does not match your DSH.
16
25
 
17
26
  ## Install, uninstall, common errors
18
27
 
@@ -42,12 +51,12 @@ The card then shows the reason instead of numbers:
42
51
 
43
52
  - 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.
44
53
  - If the active model is not in the table, the card falls back to `deepseek-chat` pricing.
45
- - 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
+ - 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.
46
55
 
47
56
  ## How it works
48
57
 
49
58
  - 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.
50
- - 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.
59
+ - 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.
51
60
 
52
61
  ---
53
62
 
@@ -60,7 +69,16 @@ The card then shows the reason instead of numbers:
60
69
  - **余额**:侧边栏底部显示 DeepSeek 账号余额(符号随账号币种 `¥` / `$`)。
61
70
  - **当前模型的档单价**:输入 / 输入缓存 / 输出,按此刻生效的计费时段显示;切换模型、跨过峰谷切换点时会跟着变。
62
71
  - **峰谷指示灯 + 倒计时**:高峰(周一至周五、不含中国法定节假日,9:00–12:00、14:00–18:00 北京时间)橙色,其余时间绿色——周末、法定节假日全天,以及调休上班的周末都是空闲;指示灯旁给出距下次切换的剩余时间(`2d 15h`、`5h 47m`、`23m`)。
63
- - 每 5 分钟自动刷新;点击卡片或切换模型时立即刷新。跟随应用明暗主题与 DSH 语言(中文 / English)。
72
+ - 每 30 秒自动刷新;点击卡片或切换模型时立即刷新。跟随应用明暗主题与 DSH 语言(中文 / English)。
73
+
74
+ ### DSH 版本对应关系
75
+
76
+ | DSH | 插件版本 |
77
+ | --- | --- |
78
+ | `0.2.x`(`0.2.0-rc.1` 起) | `0.11.0` 及以后 |
79
+ | `0.1.x`(`0.1.0-rc.6` 起) | `0.10.x`,或 `0.11.0` 及以后 |
80
+
81
+ `dsh --version` 查看当前 DSH 版本。不带版本号安装会取最新的兼容版本;最新版与你的 DSH 不匹配时,用 `@dougen/dsh-deepseek-usage@0.10.2` 这样的完整版本号指定。
64
82
 
65
83
  ### 安装、卸载与常见错误
66
84
 
@@ -88,12 +106,12 @@ dsh plugin --profile web remove @dougen/dsh-deepseek-usage
88
106
 
89
107
  - 价格是 `lib/pricing.js` 的内置表 `PRICE_TABLE`(官方价格页的快照),只在发布新版本时更新——没有本地价格文件,也没有后台同步。
90
108
  - 当前模型不在价格表里时,按 `deepseek-chat` 的价格显示。
91
- - 放假日期维护在 `lib/pricing.js` 的 `HOLIDAY_RANGES`(`lib/client.js` 有一份镜像,两处要一起改)。国务院办公厅通常在前一年 11 月发布次年安排,届时更新;未更新前,节假日会按普通工作日处理,白天显示高峰价。
109
+ - 放假日期维护在 `lib/pricing.js` 的 `HOLIDAY_RANGES`。国务院办公厅通常在前一年 11 月发布次年安排,届时更新;未更新前,节假日会按普通工作日处理,白天显示高峰价。
92
110
 
93
111
  ### 简单的工作原理
94
112
 
95
113
  - **宿主侧**(`lib/index.js`、`lib/pricing.js`)注册 `GET /api/deepseek-usage`:每次请求读取 DSH 凭据里的 API Key、调用官方余额接口、从内置价格表解析当前模型档单价,返回余额、档单价、峰谷状态与下次切换时刻;`GET /api/deepseek-usage/holidays` 把同一份法定节假日表交给浏览器侧。
96
- - **浏览器侧**(`lib/client.js`)把这份响应渲染进 `sidebar.footer.action` 槽位,每 5 分钟轮询一次,点击卡片或模型变化时立即刷新;峰谷状态与倒计时同样来自这次响应,界面自身不做计价逻辑。
114
+ - **浏览器侧**(`lib/client.js`)把这份响应渲染进 `sidebar.footer.action` 槽位,每 30 秒重新读一次,点击卡片或模型变化时立即刷新;峰谷状态与倒计时同样来自这次响应,界面自身不做计价逻辑。
97
115
 
98
116
  ## License
99
117
 
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
  // (输入/输入缓存/输出,图标 + 数字,位于卡片右下角)+ 峰谷指示灯(右上角,
@@ -27,7 +28,9 @@ window.__ModuleLoader__.load({
27
28
  const h = react.createElement;
28
29
 
29
30
  // ---- constants -------------------------------------------------
30
- const POLL_MS = 5 * 60 * 1000;
31
+ // 卡片心跳:每 30 秒重算一次倒计时并重新取一次数(余额 + 当前档单价)。
32
+ // 峰谷切换也因此不需要额外的检测逻辑——每次心跳都重新取数,切换后最多 30 秒翻正。
33
+ const POLL_MS = 30 * 1000;
31
34
  const USAGE_PATH = "/api/deepseek-usage";
32
35
  // 放假日期:宿主路由(见 lib/index.js),浏览器启动时读一次,覆盖内置镜像表;
33
36
  // 读不到就继续用内置表(两边都是同一份国务院办公厅日期表)。
@@ -38,7 +41,7 @@ window.__ModuleLoader__.load({
38
41
  // 异常态把卡片置空时用它兜住同一个高度,侧边栏高度不跳动。
39
42
  const CARD_MIN_HEIGHT = 54;
40
43
  // 卡片还没拿到余额时的快速重试节奏(宿主刚重启、接口刚起来、或本次请求失败):
41
- // 否则要等满 5 分钟轮询才会从空卡片变成真实数字。
44
+ // 让空卡片在几秒内收敛,而不必等满下一次心跳。
42
45
  const QUICK_RETRY_MS = [4000, 8000, 15000, 30000];
43
46
 
44
47
  // ---- i18n(DSH locale 服务;zh 为兜底)----
@@ -69,6 +72,8 @@ window.__ModuleLoader__.load({
69
72
  const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
70
73
  const PEAK_WINDOWS = [[9, 12], [14, 18]];
71
74
  const PEAK_WEEKDAYS_ONLY = true;
75
+ /** 可能的切换时刻(北京时间整点):高峰窗口的起止整点,nextChange 逐日枚举它们。 */
76
+ const BOUNDARY_HOURS = [...new Set(PEAK_WINDOWS.flat())].sort((a, b) => a - b);
72
77
  // 放假日期(含起止,闭区间):国务院办公厅 2026 年节假日安排。
73
78
  const HOLIDAY_RANGES = [
74
79
  ['2026-01-01', '2026-01-03'],
@@ -116,15 +121,23 @@ window.__ModuleLoader__.load({
116
121
 
117
122
  /**
118
123
  * 距下一个计费时段边界的剩余时间;与 lib/pricing.js 的 nextChange 等价。
119
- * 逐分钟向前探测(上限 16 天,覆盖春节长假 → 假期后首个工作日早晨)。
124
+ * 候选时刻是 PEAK_WINDOWS 起止整点落在各天的那些点,逐日枚举取第一个让 isPeak
125
+ * 翻转的,因此返回精确时刻(上限 32 天,覆盖春节长假 → 假期后首个工作日早晨)。
120
126
  */
121
127
  function nextChange(ms) {
122
128
  const now = Number.isFinite(ms) ? ms : Date.now();
123
129
  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) };
130
+ const DAY = 24 * 60 * 60 * 1000;
131
+ const hourMs = 60 * 60 * 1000;
132
+ const dayStart = Date.parse(new Date(now + SHANGHAI_OFFSET_MS).toISOString().slice(0, 10) + 'T00:00:00Z') - SHANGHAI_OFFSET_MS;
133
+ for (let day = 0; day < 32; day += 1) {
134
+ const base = dayStart + day * DAY;
135
+ for (const hour of BOUNDARY_HOURS) {
136
+ const at = base + hour * hourMs;
137
+ if (at <= now) continue;
138
+ const peak = isPeak(at);
139
+ if (peak !== current) return { peak, at, ms: at - now };
140
+ }
128
141
  }
129
142
  return { peak: !current, at: now, ms: 0 };
130
143
  }
@@ -250,7 +263,7 @@ window.__ModuleLoader__.load({
250
263
  };
251
264
 
252
265
  // 宿主还在后台忙(重启后首次余额、接口刚起来)时按 QUICK_RETRY_MS 阶梯补几次请求,
253
- // 让卡片几秒内收敛;封顶后回到 5 分钟轮询。失败同样按此节奏重试。
266
+ // 让卡片几秒内收敛,不必等下一次心跳(POLL_MS)。失败同样按此节奏重试。
254
267
  // 判断依据只看"有没有拿到余额",不依赖宿主内部字段。
255
268
  const load = useCallback(async () => {
256
269
  try {
@@ -293,7 +306,6 @@ window.__ModuleLoader__.load({
293
306
  useEffect(() => {
294
307
  mounted.current = true;
295
308
  load();
296
- const timer = setInterval(load, POLL_MS);
297
309
  // 模型切换(写入 settings 触发 document-updated)时立即刷新,单价跟着变。
298
310
  let off = null;
299
311
  try {
@@ -307,7 +319,6 @@ window.__ModuleLoader__.load({
307
319
  }
308
320
  return () => {
309
321
  mounted.current = false;
310
- clearInterval(timer);
311
322
  if (retryTimer.current !== null) {
312
323
  clearTimeout(retryTimer.current);
313
324
  retryTimer.current = null;
@@ -335,12 +346,16 @@ window.__ModuleLoader__.load({
335
346
  return () => { cancelled = true; };
336
347
  }, []);
337
348
 
338
- // 倒计时走字:每 30 秒重算一次渲染(跨过切换瞬间时,不依赖下一次 5 分钟轮询)。
339
- // 30s 而非 60s 是为了保证"剩余不足 1 分钟"的窗口不会被整分钟跳过。
349
+ // 心跳:每 POLL_MS 重算一次倒计时(setTick 触发重渲染)并重新取一次数。
350
+ // 取数会带回最新的余额、峰谷状态与档单价,所以跨过峰谷切换点后单价与指示灯
351
+ // 最多滞后一次心跳——不需要单独检测切换时刻。
340
352
  useEffect(() => {
341
- const timer = setInterval(() => setTick((v) => v + 1), 30 * 1000);
353
+ const timer = setInterval(() => {
354
+ setTick((v) => v + 1);
355
+ load(); // 失败已在 load 内部转成卡片文案并按 QUICK_RETRY_MS 重试
356
+ }, POLL_MS);
342
357
  return () => clearInterval(timer);
343
- }, []);
358
+ }, [load]);
344
359
 
345
360
  const balance = data && data.balance ? data.balance : null;
346
361
  const tier = data && data.tier ? data.tier : null;
@@ -351,8 +366,7 @@ window.__ModuleLoader__.load({
351
366
  // 把卡片置空并说明原因——否则余额是空的,用户无从判断。
352
367
  const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
353
368
  const blanked = errs.length > 0;
354
- // 每次渲染都以"当前时刻"求剩余时间;tick 每 30 秒 bump 一次触发这次重算,
355
- // 所以跨过切换瞬间时不需要等下一次 5 分钟轮询。
369
+ // 每次渲染都以"当前时刻"求剩余时间;tick 每 30 秒 bump 一次触发这次重算。
356
370
  // nextAt 优先用宿主给的 next.at(避免宿主与浏览器时钟不一致导致数字跳变),
357
371
  // 宿主没给(旧版本 / 字段缺失)时用同一套规则在本地推算。
358
372
  const nowMs = Date.now();
package/lib/pricing.js CHANGED
@@ -127,6 +127,12 @@ export function isPeak(ms) {
127
127
  return PEAK_WINDOWS.some(([start, end]) => h >= start && h < end);
128
128
  }
129
129
 
130
+ /**
131
+ * 可能的切换时刻(北京时间的整点):高峰窗口的起止整点。
132
+ * 翻转只可能发生在这些整点上——窗口外的整点不改变 isPeak,工作日之外则整天不变。
133
+ */
134
+ const BOUNDARY_HOURS = [...new Set(PEAK_WINDOWS.flat())].sort((a, b) => a - b);
135
+
130
136
  /**
131
137
  * 距下一个计费时段边界的剩余时间(供卡片显示倒计时)。
132
138
  *
@@ -137,8 +143,11 @@ export function isPeak(ms) {
137
143
  * · 周五 18:00 之后、整个周末 → 下一次切换是下周一 09:00(跨 2 天多)。
138
144
  * 因此倒计时必须能显示「天」。
139
145
  *
140
- * 实现:逐分钟向前探测(上限 16 天:覆盖春节这类"长假最后一天 18:00 → 假期后
141
- * 第一个工作日 09:00"的最长空档,2026 年春节为 10 天半)。纯算术,无时区/日历依赖。
146
+ * 实现:候选时刻就是 BOUNDARY_HOURS 落在各天的那些整点,从今天起逐日枚举、取第一个
147
+ * 让 isPeak 翻转的,因此返回的是**精确**时刻。纯算术,无时区/日历依赖。
148
+ * (旧实现逐分钟探测,返回的 at 最多晚 59.75 秒,倒计时会因此多显示 1m 或
149
+ * 提前进入 switching。)
150
+ *
142
151
  * 任何口径变更必须同时改 isPeak / PEAK_WINDOWS / PEAK_WEEKDAYS_ONLY / HOLIDAY_RANGES,
143
152
  * 以及 lib/client.js 里为浏览器侧镜像的同名常量(那边无法 import 本模块)。
144
153
  *
@@ -148,13 +157,22 @@ export function isPeak(ms) {
148
157
  export function nextChange(ms) {
149
158
  const now = Number.isFinite(ms) ? ms : Date.now();
150
159
  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) };
160
+ const DAY = 24 * 60 * 60 * 1000;
161
+ const hourMs = 60 * 60 * 1000;
162
+ // 北京时间的今天零点
163
+ const dayStart = Date.parse(`${beijingDateKey(now)}T00:00:00Z`) - SHANGHAI_OFFSET_MS;
164
+ // 上限 32 天:远超"长假最后一天 18:00 → 假期后第一个工作日 09:00"的最长空档
165
+ // (2026 年春节为 11 天)
166
+ for (let day = 0; day < 32; day += 1) {
167
+ const base = dayStart + day * DAY;
168
+ for (const hour of BOUNDARY_HOURS) {
169
+ const at = base + hour * hourMs;
170
+ if (at <= now) continue;
171
+ const peak = isPeak(at);
172
+ if (peak !== current) return { peak, at, ms: at - now };
173
+ }
156
174
  }
157
- // 理论上不可达(不变量:一周内必有边界);兜底给"下一天 09:00"方向的最小值
175
+ // 理论上不可达(不变量:一个月内必有工作日整点边界)
158
176
  return { peak: !current, at: now, ms: 0 };
159
177
  }
160
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
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)、当前模型档单价与峰谷时段指示灯,界面支持中英文。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -38,8 +38,8 @@
38
38
  }
39
39
  },
40
40
  "peerDependencies": {
41
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
42
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6"
41
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6 || ^0.2.0-rc.1",
42
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6 || ^0.2.0-rc.1"
43
43
  },
44
44
  "keywords": [
45
45
  "dsh",