@dougen/dsh-deepseek-usage 0.4.3 → 0.5.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
@@ -10,6 +10,7 @@ DeepSeek **余额侧边栏插件**(DeepSeek Harness Web UI):在 DSH **网
10
10
 
11
11
  - **Balance**: `¥ 110.00` large display (symbol follows account currency: `¥` CNY / `$` USD)
12
12
  - **Unit pricing**: `→|1.5 ⚡0.05 |→4.5` (input / input cache / output, bottom-right of the card; switches with the active model and peak/off-peak window — **updates instantly when you switch models**)
13
+ - **Auto price sync**: unit prices are re-fetched from the official DeepSeek pricing page on first use each day and when you click the card — DeepSeek price changes no longer require a plugin update
13
14
  - **Peak indicator**: top-right dot, orange during peak hours (Mon–Fri 9:00–12:00, 14:00–18:00 Beijing Time), green otherwise
14
15
  - **Auto refresh**: every 5 minutes; click the card to refresh instantly
15
16
  - **i18n**: UI copy follows the DSH locale (Chinese / English)
@@ -17,6 +18,7 @@ DeepSeek **余额侧边栏插件**(DeepSeek Harness Web UI):在 DSH **网
17
18
 
18
19
  - **余额**:`¥ 110.00` 大字显示(符号随账号币种自动切换:人民币 `¥` / 美元 `$`)
19
20
  - **档单价**:`→|1.5 ⚡0.05 |→4.5`(输入 / 输入缓存 / 输出,位于卡片右下角;随当前启用模型与峰谷时段自动切换,**切换模型时立即同步**)
21
+ - **单价自动同步**:每日首次使用或点击卡片时,自动从官方价格页拉取最新档单价;官方调价无需更新插件
20
22
  - **峰谷指示灯**:卡片右上角,高峰(周一至周五 9:00–12:00、14:00–18:00 北京时间)橙色,空闲绿色
21
23
  - **自动刷新**:每 5 分钟一次;点击卡片任意位置立即刷新
22
24
  - **多语言**:界面文案跟随 DSH 语言(中文 / English)
@@ -47,12 +49,12 @@ Restart the web app (`dsh web`), open http://127.0.0.1:3080 , and the usage card
47
49
  | Data | Source |
48
50
  |---|---|
49
51
  | Balance | Official DeepSeek API `GET /user/balance` (uses `DEEPSEEK_API_KEY` configured in DSH) |
50
- | Unit pricing / peak window | Built-in price table (DeepSeek official peak/off-peak pricing snapshot, effective 2026-08-21) |
52
+ | Unit pricing / peak window | Auto-synced from the official pricing page (fetched on first daily use and on card click: [api-docs.deepseek.com/zh-cn/quick_start/pricing](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)); built-in table (official snapshot) as offline fallback |
51
53
 
52
54
  | 数据 | 来源 |
53
55
  |---|---|
54
56
  | 余额 | DeepSeek 官方 API `GET /user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`) |
55
- | 档单价 / 峰谷时段 | 内置价格表(DeepSeek 官方峰谷定价快照,2026-08-21 起) |
57
+ | 档单价 / 峰谷时段 | 自动从官方价格页同步(每日首次使用或点击卡片时拉取 [api-docs.deepseek.com/zh-cn/quick_start/pricing](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/));内置价格表仅作离线兜底 |
56
58
 
57
59
  Make sure `DEEPSEEK_API_KEY` is configured in DSH (Model settings page) before use.
58
60
 
@@ -70,9 +72,9 @@ Make sure `DEEPSEEK_API_KEY` is configured in DSH (Model settings page) before u
70
72
 
71
73
  **Price differs from the official one? / 单价与官方价格不一致?**
72
74
 
73
- - The unit price switches with the **active model** and the **peak/off-peak window**; if the active model is not in the built-in price table, `deepseek-chat` pricing is shown with a notice.
75
+ - The unit price switches with the **active model** and the **peak/off-peak window**; prices are re-synced from the official pricing page daily (first use) or on card click. If the active model is not on the official page nor in the built-in table, `deepseek-chat` pricing is shown with a notice.
74
76
 
75
- - 单价按**当前启用模型**与**峰谷时段**自动切换;若当前模型不在内置价格表,会按 `deepseek-chat` 档展示并给出提示。
77
+ - 单价按**当前启用模型**与**峰谷时段**自动切换;单价每日(首次使用)或点击卡片时从官方价格页同步。若当前模型不在官方价格页也不在内置表,会按 `deepseek-chat` 档展示并给出提示。
76
78
 
77
79
  ## Uninstall / 卸载
78
80
 
package/lib/client.js CHANGED
@@ -113,9 +113,9 @@ window.__ModuleLoader__.load({
113
113
  return ZH[key] || key;
114
114
  };
115
115
 
116
- const load = useCallback(async () => {
116
+ const load = useCallback(async (force) => {
117
117
  try {
118
- const res = await fetch(USAGE_PATH + "?lang=" + lang, { cache: "no-store" });
118
+ const res = await fetch(USAGE_PATH + "?lang=" + lang + (force ? "&refresh=1" : ""), { cache: "no-store" });
119
119
  let body = null;
120
120
  try {
121
121
  body = await res.json();
@@ -168,8 +168,9 @@ window.__ModuleLoader__.load({
168
168
  background: data && data.peak ? C.peak : C.idle,
169
169
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
170
170
  };
171
- // 指示灯 hover 文案:仅状态文字,不附加具体时段(与空闲一致)。
172
- const peakTitle = data && data.peak ? t("peak.peak") : t("peak.off");
171
+ const peakTitle = data && data.peak
172
+ ? t("peak.peak") + (data.peakHours ? " " + data.peakHours : "")
173
+ : t("peak.off");
173
174
 
174
175
  if (!wide) {
175
176
  return h("div", {
@@ -188,7 +189,7 @@ window.__ModuleLoader__.load({
188
189
  gap: 5,
189
190
  userSelect: "none",
190
191
  },
191
- onClick: load,
192
+ onClick: () => load(true),
192
193
  },
193
194
  h("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 3 } },
194
195
  h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, symbol),
@@ -221,7 +222,7 @@ window.__ModuleLoader__.load({
221
222
  fontSize: "12px",
222
223
  lineHeight: "15px",
223
224
  },
224
- onClick: load,
225
+ onClick: () => load(true),
225
226
  },
226
227
  // 第一行:余额(24px,符号与数字 3px 间距)
227
228
  h("div", { style: { display: "flex", alignItems: "baseline", minWidth: 0, paddingRight: 14, gap: 6 } },
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 注册一个 HTTP 路由:
5
5
  *
6
- * GET /api/deepseek-usage[?lang=zh|en]
6
+ * GET /api/deepseek-usage[?lang=zh|en][&refresh=1]
7
7
  *
8
8
  * 返回(JSON):
9
9
  * {
@@ -17,22 +17,31 @@
17
17
  * `lang` 参数(zh 默认)控制错误消息与峰谷文案的语言;UI 其余文案由
18
18
  * client 端(DSH locale 服务)本地化。
19
19
  *
20
+ * 单价数据源(0.5.0 起):
21
+ * - 默认从 DeepSeek 官方"模型 & 价格"页自动同步(lib/pricing-remote.js):
22
+ * 每天首次访问该路由时后台拉取一次;请求带 refresh=1(点击卡片)时立即
23
+ * 重拉并等待结果,便于官方刚调价后手动跟新。
24
+ * - 拉取/解析失败不打扰用户:记录日志并继续使用上次成功数据或内置种子表
25
+ * (lib/pricing.js)。峰谷时段(周一至周五 9–12、14–18,北京时间)见
26
+ * lib/pricing.js。官方调价后只需页面更新,无需更新本插件。
27
+ *
20
28
  * 数据源:余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,
21
- * 与 llm-deepseek 同一引用,不出本机)。单价与峰谷时段来自内置价格表
22
- * (lib/pricing.js,DeepSeek 官方峰谷定价快照)。本插件仅支持 DeepSeek
23
- * 官方平台余额显示。
29
+ * 与 llm-deepseek 同一引用,不出本机)。本插件仅支持 DeepSeek 官方平台余额显示。
24
30
  *
25
31
  * 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
26
32
  * rebinding 与 0.0.0.0 绑定下的局域网访问。
27
33
  */
28
34
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
29
35
  import {
36
+ SHANGHAI_OFFSET_MS,
30
37
  DEFAULT_MODEL,
31
38
  PEAK_HOURS_TEXT,
32
39
  PEAK_HOURS_TEXT_EN,
33
40
  isPeak,
41
+ setRemotePrices,
34
42
  unitAt,
35
43
  } from "./pricing.js";
44
+ import { fetchOfficialPrices } from "./pricing-remote.js";
36
45
 
37
46
  const name = "dsh-deepseek-usage";
38
47
  const inject = ["credentials", "webServer"];
@@ -45,6 +54,11 @@ const BALANCE_PATH = "/user/balance";
45
54
  const ROUTE_PATH = "/api/deepseek-usage";
46
55
  const TIMEOUT_MS = 15000;
47
56
 
57
+ /** 官方价格页抓取超时。 */
58
+ const REMOTE_TIMEOUT_MS = 12000;
59
+ /** 手动(点击)重拉的最小间隔,避免连点打爆官方文档站。 */
60
+ const FORCED_GAP_MS = 30 * 1000;
61
+
48
62
  const JSON_HEADERS = {
49
63
  "content-type": "application/json; charset=utf-8",
50
64
  "cache-control": "no-store",
@@ -66,6 +80,50 @@ const MESSAGES = {
66
80
  },
67
81
  };
68
82
 
83
+ // ---- 官方价格自动同步状态(进程内缓存;每日首次访问拉取一次,点击可强制) ----
84
+ let remoteInFlight = null; // 进行中的拉取 promise(合并并发)
85
+ let remoteDay = ""; // 已尝试拉取的北京日期键(成功/失败都记,离线时避免反复重试)
86
+ let lastForcedAt = 0; // 上次手动重拉时刻
87
+
88
+ /** 北京时间日期键(yyyy-mm-dd)。 */
89
+ function beijingDayKey(ms) {
90
+ const d = new Date(ms + SHANGHAI_OFFSET_MS);
91
+ const m = `${d.getUTCMonth() + 1}`.padStart(2, "0");
92
+ const day = `${d.getUTCDate()}`.padStart(2, "0");
93
+ return `${d.getUTCFullYear()}-${m}-${day}`;
94
+ }
95
+
96
+ /**
97
+ * 保证"今天"的价格已尝试同步;force(点击卡片 refresh=1)时立即重拉并等待。
98
+ * 拉取成功 → pricing.setRemotePrices() 注入;失败 → 记日志,继续用内置/上次数据。
99
+ */
100
+ async function refreshRemote(ctx, force) {
101
+ const now = Date.now();
102
+ if (!force && remoteDay === beijingDayKey(now)) return; // 今天已拉取过
103
+ if (force && now - lastForcedAt < FORCED_GAP_MS) return; // 防连点
104
+ if (force) lastForcedAt = now;
105
+ if (remoteInFlight) {
106
+ if (force) { try { await remoteInFlight; } catch { /* 已吞错 */ } }
107
+ return;
108
+ }
109
+ const task = (async () => {
110
+ try {
111
+ const { models, meta } = await fetchOfficialPrices({ timeoutMs: REMOTE_TIMEOUT_MS });
112
+ setRemotePrices(models);
113
+ ctx.logger.info(`dsh-deepseek-usage: 官方价格已同步(${Object.keys(models).length} 个模型,来源 ${meta.source})`);
114
+ } catch (error) {
115
+ ctx.logger.warn(`dsh-deepseek-usage: 官方价格同步失败,沿用内置/上次价格: ${error instanceof Error ? error.message : String(error)}`);
116
+ } finally {
117
+ remoteDay = beijingDayKey(Date.now()); // 无论成败,当天不再自动重试(可点击手动刷新)
118
+ remoteInFlight = null;
119
+ }
120
+ })();
121
+ remoteInFlight = task;
122
+ if (force) {
123
+ try { await task; } catch { /* 已吞错 */ }
124
+ }
125
+ }
126
+
69
127
  function sendJson(res, status, body) {
70
128
  res.writeHead(status, JSON_HEADERS);
71
129
  res.end(JSON.stringify(body));
@@ -102,15 +160,18 @@ function isLocalHost(req) {
102
160
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
103
161
  }
104
162
 
105
- /** 解析 ?lang= 参数(zh 默认;仅识别 en 为英文,其余中文)。 */
106
- function resolveLang(req) {
163
+ /** 解析 ?lang= / ?refresh= 参数。 */
164
+ function resolveQuery(req) {
107
165
  const url = req.url ?? "";
108
166
  const q = url.indexOf("?");
109
- if (q === -1) return "zh";
110
- for (const kv of url.slice(q + 1).split("&")) {
111
- if (kv === "lang=en") return "en";
167
+ const kv = q === -1 ? [] : url.slice(q + 1).split("&");
168
+ let lang = "zh";
169
+ let refresh = false;
170
+ for (const item of kv) {
171
+ if (item === "lang=en") lang = "en";
172
+ else if (item === "refresh=1") refresh = true;
112
173
  }
113
- return "zh";
174
+ return { lang, refresh };
114
175
  }
115
176
 
116
177
  function apply(ctx) {
@@ -125,9 +186,12 @@ function apply(ctx) {
125
186
  return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
126
187
  }
127
188
 
128
- const lang = resolveLang(req);
189
+ const { lang, refresh } = resolveQuery(req);
129
190
  const msg = MESSAGES[lang] || MESSAGES.zh;
130
191
 
192
+ // ---- 官方价格自动同步(每日首次后台拉取;点击卡片强制并等待)----
193
+ await refreshRemote(ctx, refresh);
194
+
131
195
  // ---- 当前模型当前档单价(展示用)----
132
196
  const now = Date.now();
133
197
  let model = DEFAULT_MODEL;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * dsh-deepseek-usage — 官方"模型 & 价格"页抓取与解析(lib/pricing.js 的远端数据源)。
3
+ *
4
+ * 从 DeepSeek 官方价格页拉取当前生效价目:
5
+ * https://api-docs.deepseek.com/zh-cn/quick_start/pricing/
6
+ *
7
+ * 该页为静态站点、服务端直出 HTML:无需登录、无需浏览器执行 JS,价格以
8
+ * "元 / 百万 tokens"按模型列出(输入缓存命中 / 未命中 / 输出 × 空闲 / 高峰)。
9
+ * 官方调整价格后页面随之更新,插件每日首次访问或点击卡片时重拉即可跟随,
10
+ * 无需随官方改价更新内置价格表。
11
+ *
12
+ * 本文件不含运行时状态:fetchOfficialPrices() 取 HTML,
13
+ * parseOfficialPrices() 解析为 { models, meta };"每日一次"的节流与注入由
14
+ * lib/index.js 负责,解析结果经 pricing.setRemotePrices() 注入。
15
+ * 解析基于"行主序文本顺序 + 标签紧跟价格"的宽容规则(对表格排布方式不敏感);
16
+ * 任何异常(无模型列、维度缺失、数值不合理)一律返回 null,由调用方回退内置表。
17
+ */
18
+
19
+ /** 官方价格页(中文,人民币计价)。 */
20
+ const DEFAULT_URL = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/';
21
+ /** 可选环境变量覆盖(供镜像/代理场景)。 */
22
+ const URL_ENV = 'DEEPSEEK_USAGE_PRICE_URL';
23
+ const USER_AGENT = 'Mozilla/5.0 (compatible; dsh-deepseek-usage/0.5; official pricing auto-sync)';
24
+
25
+ const ENTITY_MAP = {
26
+ '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>',
27
+ '&quot;': '"', '&#39;': "'", '&apos;': "'",
28
+ };
29
+
30
+ /** 官方价格页 URL(可用环境变量 DEEPSEEK_USAGE_PRICE_URL 覆盖)。 */
31
+ export function officialPriceUrl() {
32
+ const v = process.env[URL_ENV];
33
+ return typeof v === 'string' && v.trim() !== '' ? v.trim() : DEFAULT_URL;
34
+ }
35
+
36
+ /** 常见 HTML 实体解码(含数字实体)。 */
37
+ function decodeEntities(s) {
38
+ return s.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, e) => {
39
+ if (e[0] === '#') {
40
+ const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
41
+ if (Number.isFinite(code)) {
42
+ try { return String.fromCodePoint(code); } catch {}
43
+ }
44
+ return m;
45
+ }
46
+ return Object.prototype.hasOwnProperty.call(ENTITY_MAP, e.toLowerCase()) ? ENTITY_MAP[e.toLowerCase()] : m;
47
+ });
48
+ }
49
+
50
+ /** 把 HTML 拍平成"行主序纯文本":去脚本/样式/标签,连续空白收敛为单个空格。 */
51
+ function flattenHtml(html) {
52
+ return decodeEntities(html)
53
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
54
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
55
+ .replace(/<[^>]+>/g, ' ')
56
+ .replace(/\s+/g, ' ')
57
+ .trim();
58
+ }
59
+
60
+ /**
61
+ * 解析官方价格页 HTML。
62
+ * @returns {{ models: Record<string, {input:number[],cacheRead:number[],cacheWrite:0,output:number[]}>, meta:{source:string,weekdayPeakOnly:boolean} } | null}
63
+ */
64
+ export function parseOfficialPrices(html) {
65
+ const flat = flattenHtml(html);
66
+
67
+ // 定位价格表区域:模型细节标题之后、并发限制行之前(排除导航/脚注噪音)。
68
+ const start = flat.indexOf('模型细节');
69
+ const end = start === -1 ? -1 : flat.indexOf('并发限制', start);
70
+ const region = start !== -1 && end > start ? flat.slice(start, end) : flat;
71
+
72
+ // 模型 id:区域内按首次出现顺序收集(表头在前,脚注/重复被去重)。
73
+ const ids = [];
74
+ for (const m of region.matchAll(/deepseek-[a-z0-9-]+/g)) {
75
+ if (!ids.includes(m[0])) ids.push(m[0]);
76
+ }
77
+ if (ids.length === 0) return null;
78
+
79
+ // 结构化 token 游走:metric 标签 →(空闲/高峰)× 紧随其后的价格数字。
80
+ // 对表格排布不敏感:无论"整行按档位分组"还是"逐模型交替",按序分配都对齐模型列。
81
+ const TOKEN = /缓存命中|缓存未命中|空闲时段|高峰时段|输出(?=\s)|(\d+(?:\.\d+)?)\s*元/g;
82
+ const slots = {
83
+ input: { idle: [], peak: [] },
84
+ cacheRead: { idle: [], peak: [] },
85
+ output: { idle: [], peak: [] },
86
+ };
87
+ let metric = null;
88
+ let tier = null;
89
+ for (const m of region.matchAll(TOKEN)) {
90
+ const t = m[0];
91
+ if (t === '缓存命中') { metric = 'cacheRead'; tier = null; continue; }
92
+ if (t === '缓存未命中') { metric = 'input'; tier = null; continue; }
93
+ if (t === '输出') { metric = 'output'; tier = null; continue; }
94
+ if (t === '空闲时段') { tier = 'idle'; continue; }
95
+ if (t === '高峰时段') { tier = 'peak'; continue; }
96
+ // 仅当处于某 (metric, tier) 上下文时才收数字,否则忽略(如并发数、上下文长度)。
97
+ if (m[1] !== undefined && metric && tier) {
98
+ const n = Number(m[1]);
99
+ if (Number.isFinite(n) && n > 0) slots[metric][tier].push(n);
100
+ }
101
+ }
102
+
103
+ // 校验:三个维度都要有"恰好每模型一格"的空闲与高峰价。
104
+ const n = ids.length;
105
+ const complete = (slot) => slot.length === n;
106
+ const ok = ['input', 'cacheRead', 'output'].every(
107
+ (k) => complete(slots[k].idle) && complete(slots[k].peak),
108
+ );
109
+ if (!ok) return null;
110
+
111
+ const models = {};
112
+ ids.forEach((id, i) => {
113
+ models[id] = {
114
+ input: [slots.input.peak[i], slots.input.idle[i]],
115
+ cacheRead: [slots.cacheRead.peak[i], slots.cacheRead.idle[i]],
116
+ cacheWrite: 0,
117
+ output: [slots.output.peak[i], slots.output.idle[i]],
118
+ };
119
+ });
120
+
121
+ // 保守数值校验:价格 > 0,高峰 ≥ 空闲,且高峰/空闲比值不超合理范围(当前官档为 ×2)。
122
+ for (const e of Object.values(models)) {
123
+ for (const key of ['input', 'cacheRead', 'output']) {
124
+ const [peak, idle] = e[key];
125
+ if (!(peak > 0 && idle > 0 && peak >= idle && peak / idle <= 4)) return null;
126
+ }
127
+ }
128
+
129
+ // 页面脚注若仍写"周一至周五",记入 meta(峰谷窗口本身以内置配置为准;
130
+ // 脚注位于价格表之后,因此扫整页文本)。
131
+ return {
132
+ models,
133
+ meta: { source: officialPriceUrl(), weekdayPeakOnly: /周一至周五/.test(flat) },
134
+ };
135
+ }
136
+
137
+ /**
138
+ * 拉取并解析官方价格页。
139
+ * @returns {{ models, meta: {source, weekdayPeakOnly, fetchedAt} }}
140
+ * @throws 网络失败 / HTTP 非 2xx / 解析失败(无可用的完整价格表)
141
+ */
142
+ export async function fetchOfficialPrices(opts = {}) {
143
+ const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 12000;
144
+ const url = officialPriceUrl();
145
+ const res = await fetch(url, {
146
+ headers: { 'user-agent': USER_AGENT, accept: 'text/html,application/xhtml+xml' },
147
+ redirect: 'follow',
148
+ signal: AbortSignal.timeout(timeoutMs),
149
+ });
150
+ if (!res.ok) throw new Error(`pricing page HTTP ${res.status}`);
151
+ const html = await res.text();
152
+ const parsed = parseOfficialPrices(html);
153
+ if (!parsed) throw new Error('pricing page parse failed (official page layout changed?)');
154
+ parsed.meta.fetchedAt = Date.now();
155
+ return parsed;
156
+ }
package/lib/pricing.js CHANGED
@@ -1,23 +1,33 @@
1
1
  /**
2
2
  * dsh-deepseek-usage — 定价表(元 / 百万 tokens)。
3
3
  *
4
- * DeepSeek 官方价格快照(2026-08-21 起 V4 系列峰谷定价,北京时间):
5
- * 高峰时段为周一至周五 9:00–12:00 与 14:00–18:00(北京时间),
6
- * 周六周日与其余时间均为空闲时段(空闲=高峰半价)。
7
- * 缓存写暂不计费(cacheWrite = 0);未知模型回退 chat 档,并通过 unitAt().known
8
- * 与 isKnownModel() 让调用方提示用户,而不是静默算错。
9
- * 价格调整时只需改 PRICE_TABLE 与 PEAK_WINDOWS。
4
+ * 价格来源(0.5.0 起):
5
+ * - 首选:DeepSeek 官方"模型 & 价格"页(lib/pricing-remote.js 抓取,每日首次
6
+ * 访问/点击卡片时刷新),解析结果经 setRemotePrices() 注入本模块,作为 LIVE
7
+ * 层按模型覆盖内置种子——官方调价只需页面更新,无需再改本文件。
8
+ * - 兜底:下方 PRICE_TABLE 内置种子表——DeepSeek 官方峰谷定价快照
9
+ * (2026-08-17,与当时官方价格页一致),仅当官方页抓取/解析失败时生效;
10
+ * deepseek-chat / deepseek-reasoner 为历史档(官方页已不单列),未知模型
11
+ * 回退 chat 档,并通过 unitAt().known 与 isKnownModel() 让调用方提示用户,
12
+ * 而不是静默算错。
13
+ *
14
+ * 峰谷(官方口径):高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,
15
+ * 其余(含整个周末)为空闲时段;缓存写暂不计费(cacheWrite = 0)。
10
16
  *
11
17
  * 本文件用于卡片"当前档单价"展示与峰谷指示灯。
12
18
  *
13
- * 时区:峰谷判定按北京时间(Asia/Shanghai,固定 UTC+8,无夏令时)计算,
14
- * 不随服务器本地时区变化——在任何时区部署的 DSH 上显示一致。
19
+ * 时区(0.1.7 起):峰谷判定与"每日"边界均按北京时间(Asia/Shanghai,固定
20
+ * UTC+8,无夏令时)计算,不随服务器本地时区变化——在任何时区部署的 DSH 上
21
+ * 显示一致。
15
22
  */
16
23
 
17
24
  /** 北京时间固定偏移(上海无夏令时)。 */
18
- const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
25
+ export const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
19
26
 
20
- /** 每模型价格:input/cacheRead/output 均为 [高峰, 空闲] 二元组;cacheWrite 为常量 0。 */
27
+ /**
28
+ * 内置种子表:input(缓存未命中)/ cacheRead(缓存命中)/ output 均为
29
+ * [高峰, 空闲] 二元组;cacheWrite 为常量 0。
30
+ */
21
31
  const PRICE_TABLE = {
22
32
  'deepseek-v4-flash': { input: [3.0, 1.5], cacheRead: [0.1, 0.05], cacheWrite: 0, output: [9.0, 4.5] },
23
33
  'deepseek-v4-flash-vision-exp': { input: [3.0, 1.5], cacheRead: [0.1, 0.05], cacheWrite: 0, output: [9.0, 4.5] },
@@ -26,11 +36,16 @@ const PRICE_TABLE = {
26
36
  'deepseek-reasoner': { input: [4, 4], cacheRead: [1, 1], cacheWrite: 0, output: [16, 16] },
27
37
  };
28
38
 
39
+ /** 远端官方价格(setRemotePrices 注入,按模型覆盖种子表;key 与种子一致)。 */
40
+ let remoteTable = null;
41
+
29
42
  /** 默认模型(当 agentDefaultModel 不可用时的展示默认)。 */
30
43
  export const DEFAULT_MODEL = 'deepseek-v4-flash';
31
44
 
32
- /** 高峰时段窗口(北京时间,含起点不含终点;仅周一至周五生效)。 */
45
+ /** 高峰时段窗口(北京时间,含起点不含终点)。 */
33
46
  export const PEAK_WINDOWS = [[9, 12], [14, 18]];
47
+ /** 官方口径:高峰仅限周一至周五(北京时间),周末整天为空闲时段。 */
48
+ export const PEAK_WEEKDAYS_ONLY = true;
34
49
  /** 高峰时段展示文案(中文)。 */
35
50
  export const PEAK_HOURS_TEXT = '周一至周五 9:00–12:00、14:00–18:00(北京时间)';
36
51
  /** 高峰时段展示文案(英文)。 */
@@ -41,22 +56,53 @@ export function shanghaiHour(ms) {
41
56
  return new Date(ms + SHANGHAI_OFFSET_MS).getUTCHours();
42
57
  }
43
58
 
44
- /** 某时刻是否为北京时间的工作日(周一至周五,0=周日,6=周六)。 */
45
- export function isWeekday(ms) {
46
- const day = new Date(ms + SHANGHAI_OFFSET_MS).getUTCDay();
47
- return day >= 1 && day <= 5;
48
- }
49
-
50
- /** 某时刻是否为高峰时段(北京时间,周一至周五)。 */
59
+ /** 某时刻是否为高峰时段(按北京时间;周末非工作日恒为空闲)。 */
51
60
  export function isPeak(ms) {
52
- if (!isWeekday(ms)) return false;
53
- const h = shanghaiHour(ms);
61
+ const d = new Date(ms + SHANGHAI_OFFSET_MS);
62
+ if (PEAK_WEEKDAYS_ONLY) {
63
+ const dow = d.getUTCDay(); // 0 = 周日
64
+ if (dow === 0 || dow === 6) return false;
65
+ }
66
+ const h = d.getUTCHours();
54
67
  return PEAK_WINDOWS.some(([start, end]) => h >= start && h < end);
55
68
  }
56
69
 
57
- /** 模型是否在价格表中。 */
70
+ /** 注入远端官方价格(parseOfficialPrices 的结果);传 null/空表即清除回种子。 */
71
+ export function setRemotePrices(table) {
72
+ if (!table || typeof table !== 'object') {
73
+ remoteTable = null;
74
+ return;
75
+ }
76
+ const clean = {};
77
+ const okPair = (v) => Array.isArray(v) && v.length === 2 && v.every((n) => Number.isFinite(n) && n >= 0);
78
+ for (const [id, e] of Object.entries(table)) {
79
+ if (typeof id !== 'string' || id === '' || !e || typeof e !== 'object') continue;
80
+ const input = okPair(e.input) ? e.input : null;
81
+ if (!input) continue;
82
+ clean[id] = {
83
+ input,
84
+ cacheRead: okPair(e.cacheRead) ? e.cacheRead : input,
85
+ cacheWrite: 0,
86
+ output: okPair(e.output) ? e.output : input,
87
+ };
88
+ }
89
+ remoteTable = Object.keys(clean).length > 0 ? clean : null;
90
+ }
91
+
92
+ /** 模型是否在价格表中(种子表或远端表)。 */
58
93
  export function isKnownModel(model) {
59
- return typeof model === 'string' && Object.prototype.hasOwnProperty.call(PRICE_TABLE, model);
94
+ if (typeof model !== 'string' || model === '') return false;
95
+ if (Object.prototype.hasOwnProperty.call(PRICE_TABLE, model)) return true;
96
+ return !!remoteTable && Object.prototype.hasOwnProperty.call(remoteTable, model);
97
+ }
98
+
99
+ /** 取某模型价格条目:远端 > 种子 > chat 档。 */
100
+ function entryFor(model) {
101
+ if (typeof model === 'string') {
102
+ if (remoteTable && Object.prototype.hasOwnProperty.call(remoteTable, model)) return remoteTable[model];
103
+ if (Object.prototype.hasOwnProperty.call(PRICE_TABLE, model)) return PRICE_TABLE[model];
104
+ }
105
+ return PRICE_TABLE['deepseek-chat'];
60
106
  }
61
107
 
62
108
  /**
@@ -65,7 +111,7 @@ export function isKnownModel(model) {
65
111
  */
66
112
  export function unitAt(model, ms) {
67
113
  const known = isKnownModel(model);
68
- const p = PRICE_TABLE[model] || PRICE_TABLE['deepseek-chat'];
114
+ const p = entryFor(model);
69
115
  const tier = isPeak(ms) ? 0 : 1;
70
116
  return {
71
117
  input: p.input[tier],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "DeepSeek 余额侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯,支持中/英文界面。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",