@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 +8 -8
- package/lib/client.js +83 -18
- package/lib/index.js +267 -487
- package/lib/pricing.js +89 -146
- package/package.json +2 -3
- package/lib/pricing-remote.js +0 -428
- package/lib/pricing-store.js +0 -253
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
|
|
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
|
|
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
|
-
- **峰谷指示灯 +
|
|
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
|
-
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
* 逐分钟向前探测(上限
|
|
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 <=
|
|
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 (
|
|
255
|
+
// 判断依据只看"有没有拿到余额",不依赖宿主内部字段。
|
|
256
|
+
const load = useCallback(async () => {
|
|
209
257
|
try {
|
|
210
|
-
const res = await fetch(USAGE_PATH + "?lang=" + lang
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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) {
|