@dougen/dsh-deepseek-usage 0.9.2 → 0.10.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
@@ -11,8 +11,7 @@ A sidebar plugin for the **DeepSeek Harness (DSH) Web UI** that answers two ques
11
11
 
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
- - **Peak / off-peak dot with a countdown**: orange during peak hours (Mon–Fri 9:00–12:00 and 14:00–18:00 Beijing Time), green otherwise; next to it, the time left until the next switch (`2d 15h`, `5h 47m`, `23m`).
15
- - **Collapsed sidebar** (56 px icon rail): only the dot stays, so the rail keeps its shape.
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`).
16
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).
17
16
 
18
17
  ## Install, uninstall, common errors
@@ -41,12 +40,13 @@ The card then shows the reason instead of numbers:
41
40
 
42
41
  ### Prices differ from the official page
43
42
 
44
- - Prices come from the local price table `<DSH_HOME>/dsh-deepseek-usage/pricing.json`. The plugin refreshes that file from the official pricing page on the first request of each day and when you click the card, then reads it back — a hand-edited table works after a restart, no code change needed.
43
+ - 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.
45
44
  - 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.
46
46
 
47
47
  ## How it works
48
48
 
49
- - 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 price table (syncing it from the official pricing page at most once a day, or right away when you click the card), and answers with balance, tier prices, the peak flag and the next switch time.
49
+ - 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
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.
51
51
 
52
52
  ---
@@ -59,8 +59,7 @@ The card then shows the reason instead of numbers:
59
59
 
60
60
  - **余额**:侧边栏底部显示 DeepSeek 账号余额(符号随账号币种 `¥` / `$`)。
61
61
  - **当前模型的档单价**:输入 / 输入缓存 / 输出,按此刻生效的计费时段显示;切换模型、跨过峰谷切换点时会跟着变。
62
- - **峰谷指示灯 + 倒计时**:高峰(周一至周五 9:00–12:00、14:00–18:00 北京时间)橙色,其余时间绿色;指示灯旁给出距下次切换的剩余时间(`2d 15h`、`5h 47m`、`23m`)。
63
- - **侧边栏折叠时**只剩这颗指示灯,图标栏保持原样。
62
+ - **峰谷指示灯 + 倒计时**:高峰(周一至周五、不含中国法定节假日,9:00–12:00、14:00–18:00 北京时间)橙色,其余时间绿色——周末、法定节假日全天,以及调休上班的周末都是空闲;指示灯旁给出距下次切换的剩余时间(`2d 15h`、`5h 47m`、`23m`)。
64
63
  - 每 5 分钟自动刷新;点击卡片或切换模型时立即刷新。跟随应用明暗主题与 DSH 语言(中文 / English)。
65
64
 
66
65
  ### 安装、卸载与常见错误
@@ -87,12 +86,13 @@ dsh plugin --profile web remove @dougen/dsh-deepseek-usage
87
86
 
88
87
  价格与官方不一致时:
89
88
 
90
- - 价格来自本地价格表 `<DSH_HOME>/dsh-deepseek-usage/pricing.json`;插件在每日首次请求和点击卡片时从官方价格页刷新并回写该文件,手改这份表重启后即生效,不用改代码。
89
+ - 价格是 `lib/pricing.js` 的内置表 `PRICE_TABLE`(官方价格页的快照),只在发布新版本时更新——没有本地价格文件,也没有后台同步。
91
90
  - 当前模型不在价格表里时,按 `deepseek-chat` 的价格显示。
91
+ - 放假日期维护在 `lib/pricing.js` 的 `HOLIDAY_RANGES`(`lib/client.js` 有一份镜像,两处要一起改)。国务院办公厅通常在前一年 11 月发布次年安排,届时更新;未更新前,节假日会按普通工作日处理,白天显示高峰价。
92
92
 
93
93
  ### 简单的工作原理
94
94
 
95
- - **宿主侧**(`lib/index.js`、`lib/pricing*.js`)注册 `GET /api/deepseek-usage`:每次请求读取 DSH 凭据里的 API Key、调用官方余额接口、从价格表解析当前模型档单价(每天最多同步一次官方价格页,点击卡片则立即同步),返回余额、档单价、峰谷状态与下次切换时刻。
95
+ - **宿主侧**(`lib/index.js`、`lib/pricing.js`)注册 `GET /api/deepseek-usage`:每次请求读取 DSH 凭据里的 API Key、调用官方余额接口、从内置价格表解析当前模型档单价,返回余额、档单价、峰谷状态与下次切换时刻;`GET /api/deepseek-usage/holidays` 把同一份法定节假日表交给浏览器侧。
96
96
  - **浏览器侧**(`lib/client.js`)把这份响应渲染进 `sidebar.footer.action` 槽位,每 5 分钟轮询一次,点击卡片或模型变化时立即刷新;峰谷状态与倒计时同样来自这次响应,界面自身不做计价逻辑。
97
97
 
98
98
  ## License
package/lib/client.js CHANGED
@@ -11,8 +11,8 @@
11
11
  // · 侧边栏折叠——36px 图标栏里只有峰谷指示灯(见下方 `if (!wide)` 分支)。
12
12
  //
13
13
  // 设计约束(用户只关心"我还能花多少、这会儿什么价"):
14
- // · 插件的内部信息——计价来源、同步状态、价格表文件名与路径、模型改名——一律
15
- // 不出现在 UI 上(历史上圆点的 title 里塞过这些,已删除)。
14
+ // · 插件的内部信息——取价路径、模型改名、放假日期来源——一律不出现在 UI 上
15
+ // (历史上圆点的 title 里塞过这些,已删除)。
16
16
  // · 唯一的交互是「点击卡片刷新」;没有可切换的提示态。
17
17
  // · 只有余额类错误(未配置 Key / 余额接口或网络失败)才置空卡片说明原因。
18
18
  // 样式仅使用 `--dsw-*` 主题变量(含兜底值),跟随明暗主题。
@@ -29,6 +29,10 @@ window.__ModuleLoader__.load({
29
29
  // ---- constants -------------------------------------------------
30
30
  const POLL_MS = 5 * 60 * 1000;
31
31
  const USAGE_PATH = "/api/deepseek-usage";
32
+ // 放假日期:宿主路由(见 lib/index.js),浏览器启动时读一次、并把“未来第 8 段”
33
+ // 的起始日期写回去(宿主随后自动续跑后续周期、自动合并)。浏览器比宿主更可能
34
+ // 处于新年份,因此由它来推进这份日期表。
35
+ const HOLIDAYS_PATH = "/api/deepseek-usage/holidays";
32
36
  // 常态卡片高度:上下 padding 8+16 + 金额行 minHeight 28 + 边框 2 = 54。
33
37
  // 底部多留的 8px 是为了让倒计时那一行与下方单价行之间有呼吸感。
34
38
  // 倒计时与圆点都在金额行内定位,不额外增高(无头 Chromium 实测 cardH=54)。
@@ -41,7 +45,7 @@ window.__ModuleLoader__.load({
41
45
  // ---- i18n(DSH locale 服务;zh 为兜底)----
42
46
  // 注意:卡片上只出现「输入 / 输入缓存 / 输出」这三个标签。峰谷灯没有文字、
43
47
  // 没有 hover;倒计时统一用英文单位(d/h/m)且不带前缀,中英界面完全一致——
44
- // 插件内部的同步状态、计价来源、价格表位置等一概不进 UI。
48
+ // 插件内部的取价路径等一概不进 UI。
45
49
  const NS = "dsh-deepseek-usage";
46
50
  const ZH = {
47
51
  "price.input": "输入",
@@ -60,19 +64,49 @@ window.__ModuleLoader__.load({
60
64
  const SWITCHING = "switching";
61
65
 
62
66
  // ---- 峰谷规则(必须与 lib/pricing.js 的 isPeak / PEAK_WINDOWS /
63
- // PEAK_WEEKDAYS_ONLY 保持一致;浏览器侧无法 import 宿主模块,只能镜像)----
67
+ // PEAK_WEEKDAYS_ONLY / HOLIDAY_RANGES 保持一致;浏览器侧无法 import 宿主模块,
68
+ // 只能镜像。2026 年起官方口径:法定节假日全天、调休上班的周末都是空闲时段,
69
+ // 所以放假日期必须一起镜像,否则中秋/国庆白天会被误显示成高峰价)----
64
70
  const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
65
71
  const PEAK_WINDOWS = [[9, 12], [14, 18]];
66
72
  const PEAK_WEEKDAYS_ONLY = true;
73
+ // 放假日期(含起止,闭区间):国务院办公厅 2026 年节假日安排。
74
+ const HOLIDAY_RANGES = [
75
+ ['2026-01-01', '2026-01-03'],
76
+ ['2026-02-15', '2026-02-23'],
77
+ ['2026-04-04', '2026-04-06'],
78
+ ['2026-05-01', '2026-05-05'],
79
+ ['2026-06-19', '2026-06-21'],
80
+ ['2026-09-25', '2026-09-27'],
81
+ ['2026-10-01', '2026-10-07'],
82
+ ];
83
+ const HOLIDAY_DAYS = (() => {
84
+ const set = new Set();
85
+ const DAY = 24 * 60 * 60 * 1000;
86
+ for (const [from, to] of HOLIDAY_RANGES) {
87
+ const start = Date.parse(from + 'T00:00:00Z');
88
+ const end = Date.parse(to + 'T00:00:00Z');
89
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue;
90
+ for (let t = start; t <= end; t += DAY) set.add(new Date(t).toISOString().slice(0, 10));
91
+ }
92
+ return set;
93
+ })();
67
94
 
68
95
  /** 某时刻的北京时间小时(0–23)。 */
69
96
  function shanghaiHour(ms) {
70
97
  return new Date(ms + SHANGHAI_OFFSET_MS).getUTCHours();
71
98
  }
72
99
 
73
- /** 某时刻是否为高峰时段(按北京时间;周末恒为空闲)。与 lib/pricing.js 的 isPeak 等价。 */
100
+ /** 该时刻所在北京时间日历日是否为法定节假日(放假日期)。 */
101
+ function isHoliday(ms) {
102
+ return HOLIDAY_DAYS.has(new Date(ms + SHANGHAI_OFFSET_MS).toISOString().slice(0, 10));
103
+ }
104
+
105
+ /** 某时刻是否为高峰时段(按北京时间;节假日全天与周末恒为空闲)。与 lib/pricing.js 的 isPeak 等价。 */
74
106
  function isPeak(ms) {
75
- const d = new Date(ms + SHANGHAI_OFFSET_MS);
107
+ const now = Number.isFinite(ms) ? ms : Date.now();
108
+ if (isHoliday(now)) return false;
109
+ const d = new Date(now + SHANGHAI_OFFSET_MS);
76
110
  if (PEAK_WEEKDAYS_ONLY) {
77
111
  const dow = d.getUTCDay();
78
112
  if (dow === 0 || dow === 6) return false;
@@ -83,12 +117,12 @@ window.__ModuleLoader__.load({
83
117
 
84
118
  /**
85
119
  * 距下一个计费时段边界的剩余时间;与 lib/pricing.js 的 nextChange 等价。
86
- * 逐分钟向前探测(上限 8 天,覆盖"周五晚 → 下周一早")。
120
+ * 逐分钟向前探测(上限 16 天,覆盖春节长假 → 假期后首个工作日早晨)。
87
121
  */
88
122
  function nextChange(ms) {
89
123
  const now = Number.isFinite(ms) ? ms : Date.now();
90
124
  const current = isPeak(now);
91
- for (let step = 1; step <= 8 * 24 * 60; step += 1) {
125
+ for (let step = 1; step <= 16 * 24 * 60; step += 1) {
92
126
  const at = now + step * 60 * 1000;
93
127
  const peak = isPeak(at);
94
128
  if (peak !== current) return { peak, at, ms: Math.max(0, at - now) };
@@ -178,8 +212,22 @@ window.__ModuleLoader__.load({
178
212
  const [error, setError] = useState("");
179
213
  // 倒计时走字用的心跳:每 30 秒 bump 一次,渲染时按当前时刻重算剩余时间。
180
214
  const [tick, setTick] = useState(0);
215
+ const [holidayRanges, setHolidayRanges] = useState(() => HOLIDAY_RANGES);
216
+ // 放假日期表变化时重算(与宿主口径一致)。
217
+ useEffect(() => {
218
+ const set = new Set();
219
+ const DAY = 24 * 60 * 60 * 1000;
220
+ for (const [from, to] of holidayRanges) {
221
+ const start = Date.parse(String(from) + 'T00:00:00Z');
222
+ const end = Date.parse(String(to) + 'T00:00:00Z');
223
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue;
224
+ for (let t = start; t <= end; t += DAY) set.add(new Date(t).toISOString().slice(0, 10));
225
+ }
226
+ HOLIDAY_DAYS.clear();
227
+ for (const day of set) HOLIDAY_DAYS.add(day);
228
+ }, [holidayRanges]);
181
229
  const mounted = useRef(true);
182
- // 快速重试:阶梯下标 + 定时器(宿主正在后台同步时,几秒后自己再看一眼)。
230
+ // 快速重试:阶梯下标 + 定时器(宿主刚起来、余额接口还没返回时,几秒后自己再看一眼)。
183
231
  const retryStep = useRef(0);
184
232
  const retryTimer = useRef(null);
185
233
  const locale = ctx && ctx.locale;
@@ -204,10 +252,10 @@ window.__ModuleLoader__.load({
204
252
 
205
253
  // 宿主还在后台忙(重启后首次余额、接口刚起来)时按 QUICK_RETRY_MS 阶梯补几次请求,
206
254
  // 让卡片几秒内收敛;封顶后回到 5 分钟轮询。失败同样按此节奏重试。
207
- // 判断依据只看"有没有拿到余额",不再依赖宿主内部的同步状态字段。
208
- const load = useCallback(async (force) => {
255
+ // 判断依据只看"有没有拿到余额",不依赖宿主内部字段。
256
+ const load = useCallback(async () => {
209
257
  try {
210
- const res = await fetch(USAGE_PATH + "?lang=" + lang + (force ? "&refresh=1" : ""), { cache: "no-store" });
258
+ const res = await fetch(USAGE_PATH + "?lang=" + lang, { cache: "no-store" });
211
259
  let body = null;
212
260
  try {
213
261
  body = await res.json();
@@ -247,7 +295,7 @@ window.__ModuleLoader__.load({
247
295
  mounted.current = true;
248
296
  load();
249
297
  const timer = setInterval(load, POLL_MS);
250
- // 模型切换(写入 settings 触发 document-updated)时立即刷新,价格同步。
298
+ // 模型切换(写入 settings 触发 document-updated)时立即刷新,单价跟着变。
251
299
  let off = null;
252
300
  try {
253
301
  if (ctx && ctx.remote && typeof ctx.remote.$on === "function") {
@@ -271,6 +319,23 @@ window.__ModuleLoader__.load({
271
319
  };
272
320
  }, [load, ctx]);
273
321
 
322
+ // ---- 放假日期表(宿主是唯一权威:启动时拉一次,只用于本地兜底计算)----
323
+ // 浏览器侧必须镜像峰谷规则(无法 import 宿主模块),放假日期也就不该各写一份,
324
+ // 否则放假那几天两边会算出不同结果。
325
+ useEffect(() => {
326
+ let cancelled = false;
327
+ (async () => {
328
+ try {
329
+ const res = await fetch(HOLIDAYS_PATH, { cache: "no-store" });
330
+ let body = null;
331
+ try { body = await res.json(); } catch {}
332
+ if (cancelled || !res.ok || !body || body.ok !== true) return;
333
+ if (Array.isArray(body.ranges)) setHolidayRanges(body.ranges);
334
+ } catch { /* 读不到就用内置表继续 */ }
335
+ })();
336
+ return () => { cancelled = true; };
337
+ }, []);
338
+
274
339
  // 倒计时走字:每 30 秒重算一次渲染(跨过切换瞬间时,不依赖下一次 5 分钟轮询)。
275
340
  // 30s 而非 60s 是为了保证"剩余不足 1 分钟"的窗口不会被整分钟跳过。
276
341
  useEffect(() => {
@@ -282,7 +347,7 @@ window.__ModuleLoader__.load({
282
347
  const tier = data && data.tier ? data.tier : null;
283
348
  const symbol = balance && balance.currency === "USD" ? "$" : "¥";
284
349
  // 卡片只显示"用户要的数字":余额、当前档单价、状态圆点、距下次切换的倒计时。
285
- // 插件的内部信息(计价来源、同步状态、价格表文件名/路径、模型改名)一律不进 UI,
350
+ // 插件的内部信息(取价路径、模型改名)一律不进 UI,
286
351
  // 唯一的交互是点击卡片刷新。只有"余额类错误"(未配置 Key / 接口或网络失败)才
287
352
  // 把卡片置空并说明原因——否则余额是空的,用户无从判断。
288
353
  const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
@@ -308,12 +373,12 @@ window.__ModuleLoader__.load({
308
373
  background: data && data.peak ? C.peak : C.idle,
309
374
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
310
375
  };
311
- // 圆点自身没有任何 hover(历史上的 title 里塞了同步时间、价格表路径等内部信息,
376
+ // 圆点自身没有任何 hover(历史上的 title 里塞了价格表路径等内部信息,
312
377
  // 对用户毫无意义,已删除);它只表达"现在是高峰还是空闲",切换时刻由旁边的
313
378
  // 倒计时承担。
314
379
  // 点击卡片:唯一动作 = 刷新。
315
380
  const onCardClick = () => {
316
- load(true);
381
+ load();
317
382
  };
318
383
 
319
384
  // 折叠态(图标栏):内容宽 36px(侧边栏 56px − 折叠态左右各 10px 内边距),
@@ -331,14 +396,14 @@ window.__ModuleLoader__.load({
331
396
  alignItems: "center",
332
397
  userSelect: "none",
333
398
  },
334
- onClick: () => load(true),
399
+ onClick: () => load(),
335
400
  }, h("span", { style: dot }));
336
401
  }
337
402
 
338
403
  // 只有"余额类错误"(未配置 Key / 余额接口或网络失败)才走到这里:卡片置空、
339
404
  // 只显示原因文字,不画余额/单价/圆点/倒计时。minHeight 兜住常态卡片高度
340
405
  // (CARD_MIN_HEIGHT),侧边栏不跳动;文字多行时卡片自然变高。
341
- // 注意:这里不显示任何插件内部信息(来源、同步时间、价格表路径),历史上那批
406
+ // 注意:这里不显示任何插件内部信息(取价路径、放假日期来源),历史上那批
342
407
  // 良性提示已随"去技术化"一起删除。
343
408
  // 折叠态(!wide)早在上面就返回了:它只有峰谷指示灯,没有文字空间。
344
409
  if (blanked) {