balance-dsh 1.0.9 → 1.1.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.
Files changed (3) hide show
  1. package/lib/client.js +12 -8
  2. package/lib/index.js +116 -33
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -111,12 +111,13 @@ window.__ModuleLoader__.load({
111
111
  }
112
112
 
113
113
  // ─── 价格(与服务端默认保持一致;服务端 /balance-api 返回后会覆盖) ───
114
- // 2026-08-17 生效的 DeepSeek V4-Flash 空闲时段价(元/百万 tokens)
114
+ // 北京时间 2026-09-10 12:00 起生效的 DeepSeek V4-Flash 空闲时段价(元/百万 tokens)
115
+ // 注:历史消耗由服务端按当时价格回传 cost;这里的默认值只用于拉不到服务端分桶时的本地估算。
115
116
  const DEFAULT_PRICING = {
116
- inputCnyPerMillion: 1.5, // 输入(缓存未命中)
117
- cacheReadCnyPerMillion: 0.05, // 输入(缓存命中)
117
+ inputCnyPerMillion: 1, // 输入(缓存未命中)
118
+ cacheReadCnyPerMillion: 0.02, // 输入(缓存命中)
118
119
  cacheWriteCnyPerMillion: 0, // DeepSeek 无缓存写入计费
119
- outputCnyPerMillion: 4.5, // 输出
120
+ outputCnyPerMillion: 4, // 输出
120
121
  };
121
122
 
122
123
  let pricing = DEFAULT_PRICING;
@@ -808,9 +809,12 @@ function UsageCostLine({ useProjection, sessionId }) {
808
809
 
809
810
  // 金额:优先用服务端分时段 usage(高峰桶×2+空闲桶);未就绪时按当前时段价估算
810
811
  const split = splitReady && splitUsage ? splitUsage : null;
811
- const cost = split ? computeSplitCost(split, p) : computeCost(usage, periodAdjustedPricing(p));
812
- const peakTokens = split ? split.peak.inputTokens + split.peak.cacheReadTokens + split.peak.cacheWriteTokens + split.peak.outputTokens : 0;
813
- const offpeakTokens = split ? split.offpeak.inputTokens + split.offpeak.cacheReadTokens + split.offpeak.cacheWriteTokens + split.offpeak.outputTokens : 0;
812
+ // 服务端分桶可能"非 null 但为空"(拿不到该会话事件),此时必须回退本地投影估算,否则金额会显示 ¥0.0000
813
+ const splitUsable =
814
+ split != null && ((Array.isArray(split.byModel) && split.byModel.length > 0) || (split.totalTokens ?? 0) > 0);
815
+ const cost = splitUsable ? computeSplitCost(split, p) : computeCost(usage, periodAdjustedPricing(p));
816
+ const peakTokens = splitUsable ? split.peak.inputTokens + split.peak.cacheReadTokens + split.peak.cacheWriteTokens + split.peak.outputTokens : 0;
817
+ const offpeakTokens = splitUsable ? split.offpeak.inputTokens + split.offpeak.cacheReadTokens + split.offpeak.cacheWriteTokens + split.offpeak.outputTokens : 0;
814
818
 
815
819
  // 子代理消耗(与主会话分开,同样跨时段精确计费)
816
820
  const hasSubagent = splitReady && subagentUsage && subagentUsage.totalTokens > 0;
@@ -825,7 +829,7 @@ function UsageCostLine({ useProjection, sessionId }) {
825
829
  "新增:" + formatTokens(newTokens) + " tokens(未缓存输入 " + formatTokens(uncachedInput) + " + 输出 " + formatTokens(output) + ")",
826
830
  "计费:" + formatTokens(billedTotal) + " tokens(含缓存重读 " + formatTokens(cacheRead) + "、缓存写入 " + formatTokens(cacheWrite) + ")",
827
831
  ];
828
- if (split) {
832
+ if (splitUsable) {
829
833
  detailLines.push(
830
834
  "时段拆分:高峰 " + formatTokens(peakTokens) + " tokens + 空闲 " + formatTokens(offpeakTokens) + " tokens",
831
835
  "估算金额:¥" + cost.toFixed(4) + "(高峰桶按目录价×2、空闲桶按目录价)",
package/lib/index.js CHANGED
@@ -55,10 +55,10 @@ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
55
55
  // DeepSeek 目录价(人民币/百万 tokens),按模型区分。默认取 2026-08-17 生效的
56
56
  // V4 峰谷定价之空闲时段价;高峰时段(每日 9:00 / 14:00)价格为下表 2 倍,
57
57
  // 可在 config.peak: true 时统一翻倍,或在 config.price 中逐项覆盖。
58
- // V4-Flash 空闲:输入未命中 1.5 / 命中 0.05 / 输出 4.5
59
- // V4-Pro 空闲:输入未命中 4.5 / 命中 0.15 / 输出 13.5
58
+ // V4-Flash 空闲:输入未命中 1 / 命中 0.02 / 输出 4(北京时间 2026-09-10 12:00 起的新价)
59
+ // V4-Pro 空闲:输入未命中 4.5 / 命中 0.15 / 输出 13.5(官方 2026-09-14 后继续提供,计费不变)
60
60
  const MODEL_PRICING = {
61
- "deepseek-v4-flash": { inputCnyPerMillion: 1.5, cacheReadCnyPerMillion: 0.05, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4.5 },
61
+ "deepseek-v4-flash": { inputCnyPerMillion: 1, cacheReadCnyPerMillion: 0.02, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4 },
62
62
  "deepseek-v4-pro": { inputCnyPerMillion: 4.5, cacheReadCnyPerMillion: 0.15, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 13.5 },
63
63
  // 旧模型兜底(V3.2 时代目录价)
64
64
  "deepseek-chat": { inputCnyPerMillion: 2, cacheReadCnyPerMillion: 0.5, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 3 },
@@ -66,6 +66,18 @@ const MODEL_PRICING = {
66
66
  };
67
67
  const FALLBACK_PRICING = MODEL_PRICING["deepseek-v4-flash"];
68
68
 
69
+ // 定价历史:官方自北京时间 2026-09-10 12:00 起下调 flash 系列价格。
70
+ // 调价前产生的 token 仍按当时旧价计算(历史消耗金额不因后续调价而改变),
71
+ // 之后的消耗用 MODEL_PRICING(新价)。按每条 usage 的事件时间戳自动选择。
72
+ const FLASH_REPRICED_AT = Date.parse("2026-09-10T12:00:00+08:00");
73
+ const LEGACY_MODEL_PRICING = {
74
+ "deepseek-v4-flash": { inputCnyPerMillion: 1.5, cacheReadCnyPerMillion: 0.05, cacheWriteCnyPerMillion: 0, outputCnyPerMillion: 4.5 },
75
+ };
76
+ /** 事件时间在调价前返回 "legacy"(旧价),之后返回 "current"(新价)。 */
77
+ function pricingEraAt(time) {
78
+ return Number(time) < FLASH_REPRICED_AT ? "legacy" : "current";
79
+ }
80
+
69
81
  // 高峰/空闲时段(V4 峰谷定价,2026-08-17 生效):高峰价 = 空闲价 × 2。
70
82
  // 官方高峰窗口(北京时间)为 周一至周五 09:00-12:00、14:00-18:00;周末全天空闲。
71
83
  // 可用 config.peakWindows 覆盖小时窗口(如 [{start:9,end:12},{start:14,end:18}]),
@@ -91,19 +103,23 @@ function isPeakAt(timeMs, config) {
91
103
  return weekdays.includes(weekday) && windows.some((w) => hour >= w.start && hour < w.end);
92
104
  }
93
105
 
94
- /** 把模型字符串规整到定价表键;未识别返回 ""(用 Flash 价兜底)。 */
106
+ /** 把模型字符串规整到定价表键;未识别返回 ""(用 Flash 价兜底)。
107
+ * 注意:官方已把 Flash 主名改为 `deepseek-flash`,旧名 `deepseek-v4-flash` /
108
+ * `deepseek-v4-flash-vision-exp` 虽已退役但仍按 Flash 价计费,因此统一按 "flash" 匹配。 */
95
109
  function modelKey(model) {
96
110
  const m = String(model || "").toLowerCase();
97
- if (m.includes("v4-pro") || m.includes("v4pro")) return "deepseek-v4-pro";
98
- if (m.includes("v4-flash") || m.includes("v4flash")) return "deepseek-v4-flash";
111
+ if (m.includes("v4-pro") || m.includes("v4pro") || m.includes("-pro")) return "deepseek-v4-pro";
112
+ if (m.includes("flash")) return "deepseek-v4-flash";
99
113
  if (m.includes("reasoner")) return "deepseek-reasoner";
100
114
  if (m.includes("chat")) return "deepseek-chat";
101
115
  return "";
102
116
  }
103
117
 
104
- /** 解析价格:模型基价 + config.price 覆盖,config.peak: true 时按高峰价翻倍。 */
105
- function resolvePricing(config, model = "") {
106
- const base = MODEL_PRICING[modelKey(model)] ?? FALLBACK_PRICING;
118
+ /** 解析价格:模型基价 + config.price 覆盖,config.peak: true 时按高峰价翻倍。
119
+ * era = "legacy" 时取调价前的历史价(目前仅 flash 系列与现价不同)。 */
120
+ function resolvePricing(config, model = "", era = "current") {
121
+ const key = modelKey(model);
122
+ const base = (era === "legacy" && LEGACY_MODEL_PRICING[key]) || MODEL_PRICING[key] || FALLBACK_PRICING;
107
123
  const price = config?.price ?? {};
108
124
  const multiplier = config?.peak === true ? 2 : 1;
109
125
  const pick = (name, fallback) => Number(price[name] ?? fallback) * multiplier;
@@ -115,9 +131,25 @@ function resolvePricing(config, model = "") {
115
131
  };
116
132
  }
117
133
 
134
+ /** 取会话事件数组。DSH 2.0.4 起 live session **不再有 `.events` 属性**,改用 `snapshotEvents()` 方法
135
+ * (签名 `snapshotEvents(fromSeq = 0, toSeqExclusive = seq)`,可无参调用取全部事件);
136
+ * 旧版仍读 `.events` 属性。两者都拿不到时返回空数组(由调用方降级)。 */
137
+ function sessionEvents(session) {
138
+ if (!session) return [];
139
+ if (typeof session.snapshotEvents === "function") {
140
+ try {
141
+ const events = session.snapshotEvents();
142
+ if (Array.isArray(events)) return events;
143
+ } catch {
144
+ // 落到旧属性兜底
145
+ }
146
+ }
147
+ return Array.isArray(session.events) ? session.events : [];
148
+ }
149
+
118
150
  /** 从会话中识别实际使用的模型。
119
151
  * 优先用 DSH 会话的 requestHeader()/requestContext() 折叠结果(即官方 session.models 取 current 的同一来源),
120
- * 取不到再回退遍历 session.events 找 request/header / request/context 事件。 */
152
+ * 取不到再回退遍历会话事件找 request/header / request/context 事件。 */
121
153
  function detectModel(session) {
122
154
  if (!session) return "";
123
155
  // 1) DSH 官方折叠 API:requestHeader().config.model 或 requestContext().model
@@ -133,7 +165,7 @@ function detectModel(session) {
133
165
  }
134
166
  // 2) 兜底:遍历事件
135
167
  let model = "";
136
- for (const event of session?.events ?? []) {
168
+ for (const event of sessionEvents(session)) {
137
169
  if (event.type === "request/header") {
138
170
  const m = event.data?.header?.config?.model;
139
171
  if (typeof m === "string" && m !== "") model = m;
@@ -272,11 +304,14 @@ function sumEventsUsage(events, config, fallbackModel = "") {
272
304
  bucket.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
273
305
  };
274
306
  const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
275
- const byModel = new Map(); // model -> { model, peak, offpeak }
307
+ const byModel = new Map(); // `${model}\0${era}` -> { model, era, peak, offpeak }
276
308
  for (const { usage, time } of latestByStep.values()) {
277
309
  const model = modelAtTime(switches, time) || fallback;
278
- let e = byModel.get(model);
279
- if (!e) { e = { model, peak: empty(), offpeak: empty() }; byModel.set(model, e); }
310
+ // 同一模型在调价前后的 token 分开分桶,各自用当时的价格计算(历史金额不变)。
311
+ const era = pricingEraAt(time);
312
+ const key = `${model}\u0000${era}`;
313
+ let e = byModel.get(key);
314
+ if (!e) { e = { model, era, peak: empty(), offpeak: empty() }; byModel.set(key, e); }
280
315
  add(isPeakAt(time, config) ? e.peak : e.offpeak, usage);
281
316
  }
282
317
  // 聚合 + 转换 byModel(按金额未在前面算,这里只分桶 + 汇总字段)
@@ -284,7 +319,7 @@ function sumEventsUsage(events, config, fallbackModel = "") {
284
319
  const offpeak = empty();
285
320
  const modelList = [];
286
321
  for (const e of byModel.values()) {
287
- modelList.push({ model: e.model, peak: e.peak, offpeak: e.offpeak });
322
+ modelList.push({ model: e.model, era: e.era, peak: e.peak, offpeak: e.offpeak, cost: estimateCost(e, resolvePricing(config, e.model, e.era)) });
288
323
  peak.inputTokens += e.peak.inputTokens;
289
324
  peak.outputTokens += e.peak.outputTokens;
290
325
  peak.cacheReadTokens += e.peak.cacheReadTokens;
@@ -315,7 +350,7 @@ function sumEventsUsage(events, config, fallbackModel = "") {
315
350
  function sumSessionUsage(session, config) {
316
351
  if (session == null) return null;
317
352
  // fallbackModel = detectModel(session):当 message 早于所有模型切换点时兜底,避免落到默认 flash 价。
318
- return sumEventsUsage(session.events ?? [], config, detectModel(session));
353
+ return sumEventsUsage(sessionEvents(session), config, detectModel(session));
319
354
  }
320
355
 
321
356
  function estimateCost(usage, pricing) {
@@ -385,7 +420,7 @@ function collectDailyUsage(events, config, fallbackModel, cutoffMs, days, sessio
385
420
  const model = modelAtTime(switches, time) || fallback;
386
421
  const bucket = { peak: emptyTokenBucket(), offpeak: emptyTokenBucket() };
387
422
  addTokenUsage(isPeakAt(time, config) ? bucket.peak : bucket.offpeak, usage);
388
- day.cost += estimateCost(bucket, resolvePricing(config, model));
423
+ day.cost += estimateCost(bucket, resolvePricing(config, model, pricingEraAt(time)));
389
424
  }
390
425
  }
391
426
 
@@ -401,16 +436,35 @@ function recentDayKeys(days, now = Date.now()) {
401
436
  }
402
437
 
403
438
  async function buildUsageStats(ctx, config) {
404
- const persistence = typeof ctx.get === "function" ? ctx.get("sessionPersistence") : void 0;
405
- if (persistence == null || typeof persistence.list !== "function" || typeof persistence.inspect !== "function") {
406
- throw new Error("会话持久化服务不可用");
407
- }
439
+ const getService = typeof ctx.get === "function" ? ctx.get.bind(ctx) : null;
440
+ const controller = getService?.("sessionController");
441
+ const persistence = getService?.("sessionPersistence");
408
442
  const now = Date.now();
409
443
  const cutoffMs = now - (STATS_DAYS - 1) * 24 * 60 * 60 * 1000;
410
- const headers = await persistence.list();
411
- const selected = [...headers]
412
- .filter((header) => header && typeof header.id === "string")
413
- .sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))
444
+ // 列举会话要跨 DSH 版本:新版 sessionController.list() 返回 { items:[{ sessionId, updatedAt }] }
445
+ // (已按活动降序,字段叫 sessionId);旧版 sessionPersistence.list() 返回 [{ id, createdAt }]
446
+ // (字段叫 id)。旧版 persistence 还提供 inspect(),新版改成 handle-based 的 open(),
447
+ // 所以读取一律走已做三级兼容的 inspectPersistedSession,不再直接调 persistence.inspect。
448
+ let sessionIds = [];
449
+ let availableSessions = 0;
450
+ if (typeof controller?.list === "function") {
451
+ const listed = await controller.list({});
452
+ const items = Array.isArray(listed) ? listed : (listed?.items ?? []);
453
+ sessionIds = items.map((item) => (item && typeof item === "object" ? item.sessionId ?? item.id : void 0));
454
+ availableSessions = sessionIds.length;
455
+ } else if (typeof persistence?.list === "function") {
456
+ const headers = await persistence.list();
457
+ const list = Array.isArray(headers) ? headers : [];
458
+ availableSessions = list.length;
459
+ sessionIds = [...list]
460
+ .filter((header) => header && typeof header.id === "string")
461
+ .sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))
462
+ .map((header) => header.id);
463
+ } else {
464
+ throw new Error("会话持久化服务不可用");
465
+ }
466
+ const selected = sessionIds
467
+ .filter((id) => typeof id === "string" && id !== "")
414
468
  .slice(0, STATS_MAX_SESSIONS);
415
469
  const daily = new Map();
416
470
  let cursor = 0;
@@ -418,16 +472,16 @@ async function buildUsageStats(ctx, config) {
418
472
  let failedSessions = 0;
419
473
  const worker = async () => {
420
474
  while (cursor < selected.length) {
421
- const header = selected[cursor++];
475
+ const sessionId = selected[cursor++];
422
476
  try {
423
- const inspected = await persistence.inspect(header.id);
477
+ const inspected = await inspectPersistedSession(ctx, sessionId);
424
478
  collectDailyUsage(
425
479
  inspected?.events ?? [],
426
480
  config,
427
481
  detectModel({ requestHeader: () => void 0, requestContext: () => void 0, events: inspected?.events ?? [] }),
428
482
  cutoffMs,
429
483
  daily,
430
- header.id,
484
+ sessionId,
431
485
  );
432
486
  scannedSessions++;
433
487
  } catch {
@@ -468,8 +522,8 @@ async function buildUsageStats(ctx, config) {
468
522
  rangeDays: STATS_DAYS,
469
523
  scannedSessions,
470
524
  failedSessions,
471
- availableSessions: headers.length,
472
- limited: headers.length > selected.length,
525
+ availableSessions,
526
+ limited: availableSessions > selected.length,
473
527
  generatedAt: now,
474
528
  };
475
529
  }
@@ -564,7 +618,15 @@ async function sumSubagentUsage(ctx, sessionId, config) {
564
618
  }
565
619
 
566
620
  function formatUsage(usage, pricing, model = "") {
567
- const cost = estimateCost(usage, pricing);
621
+ // 优先按 byModel 逐段累加(每段用各自模型 + 价格时代的价格,历史消耗不因调价而变);
622
+ // 无 byModel 时回退单一价格估算(兼容旧数据)。
623
+ const cost =
624
+ Array.isArray(usage.byModel) && usage.byModel.length > 0
625
+ ? usage.byModel.reduce(
626
+ (sum, b) => sum + (typeof b.cost === "number" && Number.isFinite(b.cost) ? b.cost : estimateCost(b, pricing)),
627
+ 0,
628
+ )
629
+ : estimateCost(usage, pricing);
568
630
  const newTokens = usage.inputTokens + usage.outputTokens;
569
631
  const peakTokens = usage.peak.inputTokens + usage.peak.cacheReadTokens + usage.peak.cacheWriteTokens + usage.peak.outputTokens;
570
632
  const offpeakTokens = usage.offpeak.inputTokens + usage.offpeak.cacheReadTokens + usage.offpeak.cacheWriteTokens + usage.offpeak.outputTokens;
@@ -710,6 +772,27 @@ export function apply(ctx, config = {}) {
710
772
  usage = sumSessionUsage(session, config);
711
773
  detectedModel = detectModel(session);
712
774
  }
775
+ // DSH 2.0.4 起 live session(sessions.get)可能拿不到 events,导致 usage 全 0。
776
+ // 此时回退从持久化读事件,避免客户端拿到"空但非 null"的 usage 而把金额显示成 ¥0.0000。
777
+ if (usage == null || usage.totalTokens === 0) {
778
+ try {
779
+ const persisted = await inspectPersistedSession(ctx, sessionParam);
780
+ if (Array.isArray(persisted?.events) && persisted.events.length > 0) {
781
+ const persistedUsage = sumEventsUsage(persisted.events, config, detectedModel);
782
+ if (persistedUsage.totalTokens > 0) {
783
+ usage = persistedUsage;
784
+ if (detectedModel === "") {
785
+ const switches = collectModelSwitches(persisted.events);
786
+ if (switches.length > 0) detectedModel = switches[switches.length - 1].model;
787
+ }
788
+ }
789
+ }
790
+ } catch {
791
+ // 持久化读取不可用:忽略,走下方统一降级
792
+ }
793
+ }
794
+ // 仍无任何 token 就返回 null,让客户端回退本地投影估算(而不是当成 0 消耗显示 ¥0.0000)
795
+ if (usage != null && usage.totalTokens === 0) usage = null;
713
796
  // 子代理枚举走持久化目录(不依赖 live 会话),即使父会话不在内存也能拿到
714
797
  const subRes = await sumSubagentUsage(httpCtx, sessionParam, config);
715
798
  subagentUsage = subRes.usage;
@@ -722,10 +805,10 @@ export function apply(ctx, config = {}) {
722
805
  const effectiveModel = model || detectedModel;
723
806
  // 逐模型标注 cost(高峰期×2、空闲期×1),客户端直接按各模型 cost 累加,无需自己定价。
724
807
  if (usage && Array.isArray(usage.byModel)) {
725
- usage.byModel = usage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
808
+ usage.byModel = usage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model, b.era)) }));
726
809
  }
727
810
  if (subagentUsage && Array.isArray(subagentUsage.byModel)) {
728
- subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
811
+ subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model, b.era)) }));
729
812
  }
730
813
  try {
731
814
  const data = await fetchBalance(apiKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "查询 DeepSeek 余额并显示当前会话 token 消耗。DeepSeek 账户余额 + 会话 token/费用显示,含高峰/空闲峰谷计费。",
5
5
  "author": "Rannist",
6
6
  "type": "module",