@dougen/dsh-deepseek-usage 0.9.1 → 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/lib/pricing.js CHANGED
@@ -1,27 +1,22 @@
1
1
  /**
2
2
  * dsh-deepseek-usage — 定价表(元 / 百万 tokens)。
3
3
  *
4
- * 价格来源优先级(0.7.0 起,三层):
5
- * - remote(进程内最新):DeepSeek 官方"模型 & 价格"页(lib/pricing-remote.js 抓取,
6
- * 每日首次访问/点击卡片时刷新),解析结果经 setRemotePrices() 注入本模块——官方调价
7
- * 只需页面更新,无需改本文件。
8
- * - file(本地价格配置表):lib/pricing-store.js 维护的 pricing.json,内容是"上次同步到
9
- * 的官方价 + 用户手改的价",经 setFilePrices() 注入。进程重启后 remote 层为空,卡片
10
- * 直接用它,因此不会再退回内置表(也就不会再出现"价格未同步(用内置表)")。
11
- * - seed(内置兜底):下方 PRICE_TABLE 内置种子表——DeepSeek 官方峰谷定价快照
12
- * (2026-09-10,与当时官方价格页一致)。首次运行会拿它生成初始 pricing.json;
13
- * 只有"本地价格表缺失/损坏且官方页也拿不到"时才会真正被展示。
4
+ * 价格唯一来源:下方 PRICE_TABLE 内置表——DeepSeek 官方峰谷定价快照
5
+ * (2026-09-10 官方价格页)。官方调价时随插件版本更新本表。
14
6
  *
15
7
  * 模型改名(0.6.0 起):官方会把模型改名并把旧名保留为别名(如
16
8
  * deepseek-v4-flash / deepseek-v4-flash-vision-exp → deepseek-flash)。旧名若
17
9
  * 落到内置表就会长期显示过期价且无从察觉,因此:
18
10
  * · MODEL_ALIASES 维护"旧名 → 现名"的显式映射;
19
- * · 远端表查不到时先试别名,再试"同家族唯一匹配"(flash / pro / chat /
11
+ * · 查不到时先试别名,再试"同家族唯一匹配"(flash / pro / chat /
20
12
  * reasoner),命中即按现名计价;
21
- * · 两条路径都会在 unitAt() 的 via 字段里标出来源,供界面提示,避免静默算错。
13
+ * · 两条路径都会在 unitAt() 的 via 字段里标出来源,供排查,避免静默算错。
22
14
  *
23
- * 峰谷(官方口径):高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,
24
- * 其余(含整个周末)为空闲时段;缓存写暂不计费(cacheWrite = 0)。
15
+ * 峰谷(官方口径):高峰时段为北京时间周一至周五(**不含中国法定节假日**)
16
+ * 9:00–12:00、14:00–18:00,其余(含整个周末、法定节假日全天、以及调休上班的
17
+ * 周末)为空闲时段;缓存写暂不计费(cacheWrite = 0)。
18
+ * 因此只有"落在工作日里的法定节假日"会改变判定结果——周末无论是否调休上班都
19
+ * 是空闲,见 HOLIDAY_RANGES。
25
20
  *
26
21
  * 本文件用于卡片"当前档单价"展示、峰谷指示灯,以及 nextChange() 给出的
27
22
  * 「距下次峰谷切换倒计时」(卡片上显示为 `2d 15h` / `5h 47m` 这类英文单位时长)。
@@ -35,10 +30,8 @@
35
30
  export const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
36
31
 
37
32
  /**
38
- * 内置种子表:input(缓存未命中)/ cacheRead(缓存命中)/ output 均为
39
- * [高峰, 空闲] 二元组;cacheWrite 为常量 0。
40
- * 只用于两处:首次运行生成 pricing.json(lib/pricing-store.js),以及"价格表缺失/
41
- * 损坏且官方页也拿不到"时的最后兜底;正常运行时都会被 file / remote 层覆盖。
33
+ * 价格表:input(缓存未命中)/ cacheRead(缓存命中)/ output 均为 [高峰, 空闲]
34
+ * 二元组;cacheWrite 为常量 0。官方改价时更新本表并发布新版本。
42
35
  */
43
36
  const PRICE_TABLE = {
44
37
  'deepseek-flash': { input: [2, 1], cacheRead: [0.04, 0.02], cacheWrite: 0, output: [8, 4] },
@@ -57,17 +50,6 @@ export const MODEL_ALIASES = {
57
50
  'deepseek-v4-flash-vision-exp': 'deepseek-flash',
58
51
  };
59
52
 
60
- /** 远端官方价格(setRemotePrices 注入,按模型覆盖种子表;key 与种子一致)。 */
61
- let remoteTable = null;
62
- /** 远端同步元信息(解析策略 / 抓取时间 / 来源),供界面展示同步状态。 */
63
- let remoteMeta = null;
64
- /** 本地价格配置表(setFilePrices 注入;重启后由它接管,见 lib/pricing-store.js)。 */
65
- let fileTable = null;
66
- /** 本地价格表元信息(路径 / 上次写入时间 / 来源)。 */
67
- let fileMeta = null;
68
- /** 本地价格表里的模型别名(旧名 → 现名),与内置 MODEL_ALIASES 合并使用。 */
69
- let fileAliases = {};
70
-
71
53
  /** 默认模型(当 agentDefaultModel 不可用时的展示默认)。 */
72
54
  export const DEFAULT_MODEL = 'deepseek-flash';
73
55
 
@@ -76,18 +58,67 @@ export const PEAK_WINDOWS = [[9, 12], [14, 18]];
76
58
  /** 官方口径:高峰仅限周一至周五(北京时间),周末整天为空闲时段。 */
77
59
  export const PEAK_WEEKDAYS_ONLY = true;
78
60
  /** 高峰时段展示文案(中文)。 */
79
- export const PEAK_HOURS_TEXT = '周一至周五 9:00–12:00、14:00–18:00(北京时间)';
61
+ export const PEAK_HOURS_TEXT = '周一至周五(不含法定节假日)9:00–12:00、14:00–18:00(北京时间)';
80
62
  /** 高峰时段展示文案(英文)。 */
81
- export const PEAK_HOURS_TEXT_EN = 'Mon–Fri 9:00–12:00, 14:00–18:00 (Beijing Time)';
63
+ export const PEAK_HOURS_TEXT_EN = 'Mon–Fri (excluding public holidays) 9:00–12:00, 14:00–18:00 (Beijing Time)';
64
+
65
+ /**
66
+ * 中国法定节假日放假日期(含起止,闭区间;北京时间的日历日)。
67
+ * 来源:国务院办公厅《关于 2026 年部分节假日安排的通知》(国办发明电〔2025〕7号)。
68
+ * 官方口径:法定节假日全天、调休上班的周末均为空闲时段——周末本来就空闲,所以
69
+ * 只需列"放假日期",落在工作日里的那几天才会把判定从高峰翻成空闲。
70
+ * 下一年的安排通常在前一年 11 月发布,届时更新本表;当前年份未收录时由
71
+ * lib/index.js 在启动时告警(见 hasHolidayYear)。
72
+ */
73
+ export const HOLIDAY_RANGES = [
74
+ ['2026-01-01', '2026-01-03'], // 元旦
75
+ ['2026-02-15', '2026-02-23'], // 春节
76
+ ['2026-04-04', '2026-04-06'], // 清明节
77
+ ['2026-05-01', '2026-05-05'], // 劳动节
78
+ ['2026-06-19', '2026-06-21'], // 端午节
79
+ ['2026-09-25', '2026-09-27'], // 中秋节
80
+ ['2026-10-01', '2026-10-07'], // 国庆节
81
+ ];
82
+
83
+ /** 把 [起, 止] 闭区间展开成日期键集合(`yyyy-mm-dd`,O(1) 查表)。 */
84
+ function expandHolidayRanges(ranges) {
85
+ const set = new Set();
86
+ const DAY = 24 * 60 * 60 * 1000;
87
+ for (const [from, to] of ranges) {
88
+ const start = Date.parse(`${from}T00:00:00Z`);
89
+ const end = Date.parse(`${to}T00:00:00Z`);
90
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue;
91
+ for (let t = start; t <= end; t += DAY) set.add(new Date(t).toISOString().slice(0, 10));
92
+ }
93
+ return set;
94
+ }
95
+
96
+ const HOLIDAY_DAYS = expandHolidayRanges(HOLIDAY_RANGES);
82
97
 
83
- /** 某时刻的北京时间小时(0–23)。 */
84
- export function shanghaiHour(ms) {
85
- return new Date(ms + SHANGHAI_OFFSET_MS).getUTCHours();
98
+ /** 某时刻的北京时间日期键(`yyyy-mm-dd`)。 */
99
+ export function beijingDateKey(ms) {
100
+ return new Date(ms + SHANGHAI_OFFSET_MS).toISOString().slice(0, 10);
86
101
  }
87
102
 
88
- /** 某时刻是否为高峰时段(按北京时间;周末非工作日恒为空闲)。 */
103
+ /** 该时刻所在的北京时间日历日是否为中国法定节假日(放假日期)。 */
104
+ export function isHoliday(ms) {
105
+ return HOLIDAY_DAYS.has(beijingDateKey(ms));
106
+ }
107
+
108
+ /** 是否收录了某一年(北京时间年份)的节假日安排;未收录时界面可能把节假日当高峰。 */
109
+ export function hasHolidayYear(year) {
110
+ const prefix = `${year}-`;
111
+ for (const day of HOLIDAY_DAYS) {
112
+ if (day.startsWith(prefix)) return true;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ /** 某时刻是否为高峰时段(按北京时间;法定节假日全天与周末恒为空闲)。 */
89
118
  export function isPeak(ms) {
90
- const d = new Date(ms + SHANGHAI_OFFSET_MS);
119
+ const now = Number.isFinite(ms) ? ms : Date.now();
120
+ if (isHoliday(now)) return false; // 法定节假日:全天空闲
121
+ const d = new Date(now + SHANGHAI_OFFSET_MS);
91
122
  if (PEAK_WEEKDAYS_ONLY) {
92
123
  const dow = d.getUTCDay(); // 0 = 周日
93
124
  if (dow === 0 || dow === 6) return false;
@@ -106,9 +137,10 @@ export function isPeak(ms) {
106
137
  * · 周五 18:00 之后、整个周末 → 下一次切换是下周一 09:00(跨 2 天多)。
107
138
  * 因此倒计时必须能显示「天」。
108
139
  *
109
- * 实现:逐分钟向前探测(上限 8 天),纯算术,无时区/日历依赖。
110
- * 任何口径变更必须同时改 isPeak / PEAK_WINDOWS / PEAK_WEEKDAYS_ONLY,以及
111
- * lib/client.js 里为浏览器侧镜像的同名常量(那边无法 import 本模块)。
140
+ * 实现:逐分钟向前探测(上限 16 天:覆盖春节这类"长假最后一天 18:00 → 假期后
141
+ * 第一个工作日 09:00"的最长空档,2026 年春节为 10 天半)。纯算术,无时区/日历依赖。
142
+ * 任何口径变更必须同时改 isPeak / PEAK_WINDOWS / PEAK_WEEKDAYS_ONLY / HOLIDAY_RANGES,
143
+ * 以及 lib/client.js 里为浏览器侧镜像的同名常量(那边无法 import 本模块)。
112
144
  *
113
145
  * @returns {{ peak: boolean, at: number, ms: number }} peak=切换后所处的时段,
114
146
  * at=切换发生的时刻(ms 时间戳,新时段在此刻生效),ms=剩余毫秒(不会为负)。
@@ -116,8 +148,8 @@ export function isPeak(ms) {
116
148
  export function nextChange(ms) {
117
149
  const now = Number.isFinite(ms) ? ms : Date.now();
118
150
  const current = isPeak(now);
119
- // 逐分钟探测(上限 8 天:覆盖"周五晚 → 下周一早"的最长空档)
120
- for (let step = 1; step <= 8 * 24 * 60; step += 1) {
151
+ // 逐分钟探测(上限 16 天:覆盖"春节长假 → 假期后首个工作日早晨"的最长空档)
152
+ for (let step = 1; step <= 16 * 24 * 60; step += 1) {
121
153
  const at = now + step * 60 * 1000;
122
154
  const peak = isPeak(at);
123
155
  if (peak !== current) return { peak, at, ms: Math.max(0, at - now) };
@@ -126,73 +158,6 @@ export function nextChange(ms) {
126
158
  return { peak: !current, at: now, ms: 0 };
127
159
  }
128
160
 
129
- /** 清洗一张价格表(形状校验 + 档位回退),供 remote / file 两层共用。 */
130
- function cleanTable(table) {
131
- if (!table || typeof table !== 'object') return null;
132
- const okPair = (v) => Array.isArray(v) && v.length === 2 && v.every((n) => Number.isFinite(n) && n >= 0);
133
- const clean = {};
134
- for (const [id, e] of Object.entries(table)) {
135
- if (typeof id !== 'string' || id === '' || !e || typeof e !== 'object') continue;
136
- const input = okPair(e.input) ? e.input : null;
137
- if (!input) continue;
138
- clean[id] = {
139
- input,
140
- cacheRead: okPair(e.cacheRead) ? e.cacheRead : input,
141
- cacheWrite: 0,
142
- output: okPair(e.output) ? e.output : input,
143
- };
144
- }
145
- return Object.keys(clean).length > 0 ? clean : null;
146
- }
147
-
148
- /** 注入远端官方价格(parseOfficialPrices 的结果);传 null/空表即清除回下一层。 */
149
- export function setRemotePrices(table, meta) {
150
- remoteTable = cleanTable(table);
151
- remoteMeta = remoteTable && meta && typeof meta === 'object' ? { ...meta } : null;
152
- }
153
-
154
- /** 远端同步元信息(未同步/同步失败时为 null)。 */
155
- export function remotePriceMeta() {
156
- return remoteMeta ? { ...remoteMeta } : null;
157
- }
158
-
159
- /** 远端价格表是否可用。 */
160
- export function hasRemotePrices() {
161
- return remoteTable !== null;
162
- }
163
-
164
- /**
165
- * 注入本地价格配置表(lib/pricing-store.js 的读取结果)。
166
- * @param table 模型映射(models 字段)
167
- * @param meta 元信息(path / updatedAt / source / strategy / autoUpdate)
168
- * @param aliases 文件里的模型别名(旧名 → 现名),与内置 MODEL_ALIASES 合并
169
- */
170
- export function setFilePrices(table, meta, aliases) {
171
- fileTable = cleanTable(table);
172
- fileMeta = fileTable && meta && typeof meta === 'object' ? { ...meta } : null;
173
- fileAliases = aliases && typeof aliases === 'object' && !Array.isArray(aliases) ? { ...aliases } : {};
174
- }
175
-
176
- /** 本地价格表元信息(未载入时为 null)。 */
177
- export function filePriceMeta() {
178
- return fileMeta ? { ...fileMeta } : null;
179
- }
180
-
181
- /** 本地价格表是否可用。 */
182
- export function hasFilePrices() {
183
- return fileTable !== null;
184
- }
185
-
186
- /** 内置种子价格表快照(深拷贝;用于首次运行生成 pricing.json)。 */
187
- export function seedPrices() {
188
- return JSON.parse(JSON.stringify(PRICE_TABLE));
189
- }
190
-
191
- /** 内置模型改名映射快照(用于首次运行生成 pricing.json)。 */
192
- export function seedAliases() {
193
- return { ...MODEL_ALIASES };
194
- }
195
-
196
161
  /** 模型家族名(用于官方改名后的兜底匹配)。 */
197
162
  function familyOf(id) {
198
163
  const m = /(flash|pro|chat|reasoner)/.exec(id);
@@ -201,51 +166,38 @@ function familyOf(id) {
201
166
 
202
167
  const has = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key);
203
168
 
204
- /** 别名(旧名 → 现名):本地价格表的 aliases 覆盖内置 MODEL_ALIASES。 */
169
+ /** 别名(旧名 → 现名)。 */
205
170
  function aliasOf(id) {
206
171
  if (typeof id !== 'string' || id === '') return undefined;
207
- return has(fileAliases, id) ? fileAliases[id] : MODEL_ALIASES[id];
172
+ return MODEL_ALIASES[id];
208
173
  }
209
174
 
210
- /** 在某一层价格表里找模型:精确 → 别名 → 同家族唯一匹配。 */
211
- function layerLookup(table, tag, id) {
212
- if (!table || typeof id !== 'string' || id === '') return null;
213
- if (has(table, id)) return { id, via: tag };
175
+ /** 在价格表里找模型:精确 → 别名 → 同家族唯一匹配。 */
176
+ function lookupId(id) {
177
+ if (typeof id !== 'string' || id === '') return null;
178
+ if (has(PRICE_TABLE, id)) return { id, via: 'exact' };
214
179
  const alias = aliasOf(id);
215
- if (alias && has(table, alias)) return { id: alias, via: `${tag}-alias` };
180
+ if (alias && has(PRICE_TABLE, alias)) return { id: alias, via: 'alias' };
216
181
  const fam = familyOf(id);
217
182
  if (fam) {
218
- const hits = Object.keys(table).filter((k) => familyOf(k) === fam);
183
+ const hits = Object.keys(PRICE_TABLE).filter((k) => familyOf(k) === fam);
219
184
  // 仅在家族唯一时匹配,避免把 flash 档算成 pro 档。
220
- if (hits.length === 1) return { id: hits[0], via: `${tag}-family` };
185
+ if (hits.length === 1) return { id: hits[0], via: 'family' };
221
186
  }
222
187
  return null;
223
188
  }
224
189
 
225
190
  /**
226
- * 解析模型的计价条目(优先级:remote → file → seed → chat 兜底)。
227
- * @returns {{ price: object, id: string, via: 'remote'|'remote-alias'|'remote-family'|'file'|'file-alias'|'file-family'|'alias'|'seed'|'fallback', source: 'remote'|'file'|'seed', known: boolean }}
191
+ * 解析模型的计价条目:精确 → 别名 → 同家族唯一匹配 → chat 档兜底。
192
+ * @returns {{ price: object, id: string, via: 'exact'|'alias'|'family'|'fallback', known: boolean }}
228
193
  */
229
194
  function lookup(model) {
230
- const remote = layerLookup(remoteTable, 'remote', model);
231
- if (remote) return { price: remoteTable[remote.id], id: remote.id, via: remote.via, source: 'remote', known: true };
232
-
233
- // 进程重启后 remote 层为空 → 先用本地价格表(上次同步到的官方价 / 用户手改价)。
234
- const file = layerLookup(fileTable, 'file', model);
235
- if (file) return { price: fileTable[file.id], id: file.id, via: file.via, source: 'file', known: true };
236
-
237
- // 本地表也没有(首次运行、文件损坏、或模型确实不在表里)→ 内置种子兜底。
238
- const alias = aliasOf(model);
239
- if (alias && has(PRICE_TABLE, alias)) {
240
- return { price: PRICE_TABLE[alias], id: alias, via: 'alias', source: 'seed', known: true };
241
- }
242
- if (has(PRICE_TABLE, model)) {
243
- return { price: PRICE_TABLE[model], id: model, via: 'seed', source: 'seed', known: true };
244
- }
245
- return { price: PRICE_TABLE['deepseek-chat'], id: 'deepseek-chat', via: 'fallback', source: 'seed', known: false };
195
+ const found = lookupId(model);
196
+ if (found) return { price: PRICE_TABLE[found.id], id: found.id, via: found.via, known: true };
197
+ return { price: PRICE_TABLE['deepseek-chat'], id: 'deepseek-chat', via: 'fallback', known: false };
246
198
  }
247
199
 
248
- /** 模型是否能解析出价格(远端精确/别名/家族,或本地表/内置表)。 */
200
+ /** 模型是否能解析出价格(内置表精确命中 / 别名 / 家族匹配)。 */
249
201
  export function isKnownModel(model) {
250
202
  return lookup(model).known;
251
203
  }
@@ -253,9 +205,7 @@ export function isKnownModel(model) {
253
205
  /**
254
206
  * 某模型在某一时刻的单位价格(元/百万 tokens)。
255
207
  * 未知模型回退 chat 档;known=false 供调用方提示,避免静默算错。
256
- * via 说明取价路径,source 说明价格来自哪一层(remote 官方页本次同步 / file 本地价格表 /
257
- * seed 内置兜底);layerAt 是该层数据的时间戳(remote.fetchedAt / file.updatedAt),
258
- * 供界面说明"这份价是什么时候同步的"。
208
+ * via 说明取价路径(exact / alias / family / fallback),供排查与回归测试。
259
209
  */
260
210
  export function unitAt(model, ms) {
261
211
  const found = lookup(model);
@@ -270,12 +220,5 @@ export function unitAt(model, ms) {
270
220
  requested: typeof model === 'string' ? model : '',
271
221
  known: found.known,
272
222
  via: found.via,
273
- source: found.source,
274
- layerAt:
275
- found.source === 'remote'
276
- ? (remoteMeta && Number.isFinite(remoteMeta.fetchedAt) ? remoteMeta.fetchedAt : null)
277
- : found.source === 'file'
278
- ? (fileMeta && Number.isFinite(fileMeta.updatedAt) ? fileMeta.updatedAt : null)
279
- : null,
280
223
  };
281
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "DeepSeek 余额侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯,支持中/英文界面。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -13,8 +13,7 @@
13
13
  "url": "https://github.com/dougen/dsh-deepseek-usage/issues"
14
14
  },
15
15
  "scripts": {
16
- "test": "node --test test/",
17
- "check:pricing": "node lib/pricing-remote.js --check"
16
+ "test": "node --test test/"
18
17
  },
19
18
  "files": [
20
19
  "lib",