balance-dsh 1.3.0 → 1.3.1

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 +24 -3
  2. package/lib/index.js +159 -19
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -254,14 +254,18 @@ window.__ModuleLoader__.load({
254
254
  * ⚠️ p 必须是**目录价**(服务端 pricing 字段即目录价),倍率由 peakMultiplier 单独给,
255
255
  * 否则会与这里的乘法重复翻倍(高峰变 ×4)。 */
256
256
  function computeSplitCost(usage, p, multiplier = 2) {
257
+ let total = 0;
257
258
  if (Array.isArray(usage.byModel) && usage.byModel.length > 0) {
258
- let total = 0;
259
259
  for (const b of usage.byModel) {
260
260
  total += typeof b.cost === "number" && Number.isFinite(b.cost) ? b.cost : computeSingleSplitCost(b, p, multiplier);
261
261
  }
262
- return total;
262
+ } else {
263
+ total = computeSingleSplitCost(usage, p, multiplier);
263
264
  }
264
- return computeSingleSplitCost(usage, p, multiplier);
265
+ // 网页搜索 / 抓取按次费用:服务端已按官方「每千次 ¥5」、且只对 2026-11-01 00:00
266
+ // (北京)之后的成功调用计价,这里直接累加,不再乘峰谷倍率。
267
+ total += typeof usage.webToolCost === "number" && Number.isFinite(usage.webToolCost) ? usage.webToolCost : 0;
268
+ return total;
265
269
  }
266
270
  function computeSingleSplitCost(usage, p, multiplier = 2) {
267
271
  const costOf = (u, factor) =>
@@ -1534,6 +1538,23 @@ function UsageCostLine({ useProjection, sessionId }) {
1534
1538
  ["子代理 tokens", "+" + formatTokens(subNewTokens) + " / 计费 " + formatTokens(subagentUsage.totalTokens)],
1535
1539
  );
1536
1540
  }
1541
+ // 网页搜索 / 抓取:官方按次计费(每千次 ¥5),2026-11-01 00:00(北京)起收费。
1542
+ // 次数来自 tool/call 与 tool/result 配对,金额由服务端算好放在 webToolCost。
1543
+ const webSearch = splitUsable ? (split.webSearchCalls ?? 0) : 0;
1544
+ const webFetch = splitUsable ? (split.webFetchCalls ?? 0) : 0;
1545
+ const webCost = splitUsable && Number.isFinite(split.webToolCost) ? split.webToolCost : 0;
1546
+ if (webSearch > 0 || webFetch > 0) {
1547
+ const parts = [];
1548
+ if (webSearch > 0) parts.push("搜索 " + webSearch + " 次");
1549
+ if (webFetch > 0) parts.push("抓取 " + webFetch + " 次");
1550
+ costDetails.push(
1551
+ ["网页工具", parts.join(" · ")],
1552
+ [
1553
+ "网页工具费用",
1554
+ webCost > 0 ? "¥" + webCost.toFixed(4) : "免费期内",
1555
+ ],
1556
+ );
1557
+ }
1537
1558
 
1538
1559
  const detailRows = (prefix, rows) =>
1539
1560
  rows.flatMap(([label, value], index) => [
package/lib/index.js CHANGED
@@ -343,7 +343,7 @@ function formatBalance(balance) {
343
343
  // | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
344
344
  // client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
345
345
  // 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
346
- const PLUGIN_VERSION = "1.3.0";
346
+ const PLUGIN_VERSION = "1.3.1";
347
347
 
348
348
  /**
349
349
  * 读取官方账号余额并归一化成与 normalizeBalance 相同的形状。
@@ -453,12 +453,75 @@ function modelAtTime(switches, time) {
453
453
  return model;
454
454
  }
455
455
 
456
+ /** 网页搜索 / 抓取工具按次计费(官方 2026-10-17 上线 Search/Fetch API 公告):
457
+ * · Search 与 Fetch 同价:**每千次 ¥5**(= ¥0.005 / 次),结果条数不影响价格;
458
+ * · 只有 Search 返回非空结果、Fetch 返回正文且 HTTP 200 才计费,失败/空结果/超时不计费;
459
+ * · **北京时间 2026-11-01 00:00 起开始收费**,此前的调用免费。
460
+ * 会话事件里 tool/call 带 { callId, name, arguments }(name = web_search / web_fetch),
461
+ * tool/result 带 { toolCallId, message: { isError, content } },按 callId 配对就能算钱。 */
462
+ const WEB_TOOL_BILLING_START = Date.parse("2026-11-01T00:00:00+08:00");
463
+ const WEB_TOOL_PRICE_PER_CALL = 5 / 1000;
464
+
465
+ /** 统计网页工具调用:次数、可计费次数与金额。
466
+ * 只认 tool/call 与 tool/result 配对成功、且 isError !== true 的调用(官方口径:失败不计费)。 */
467
+ function webToolUsage(events) {
468
+ const calls = new Map(); // callId -> { name, time }
469
+ for (const event of events ?? []) {
470
+ if (event.type !== "tool/call") continue;
471
+ const name = event.data?.name;
472
+ if (name !== "web_search" && name !== "web_fetch") continue;
473
+ const time = Number(event.time ?? 0);
474
+ if (!Number.isFinite(time)) continue;
475
+ const id = event.data?.callId;
476
+ if (id === void 0 || id === null) continue;
477
+ calls.set(String(id), { name, time });
478
+ }
479
+ let webSearchCalls = 0;
480
+ let webFetchCalls = 0;
481
+ let webSearchBillable = 0;
482
+ let webFetchBillable = 0;
483
+ let webSearchCost = 0;
484
+ let webFetchCost = 0;
485
+ const done = new Set();
486
+ for (const event of events ?? []) {
487
+ if (event.type !== "tool/result") continue;
488
+ const id = event.data?.toolCallId ?? event.data?.message?.toolCallId;
489
+ const key = id === void 0 || id === null ? "" : String(id);
490
+ const call = calls.get(key);
491
+ if (call === void 0 || done.has(key)) continue;
492
+ done.add(key);
493
+ if (event.data?.message?.isError === true) continue; // 失败不计费
494
+ if (call.name === "web_search") webSearchCalls += 1;
495
+ else webFetchCalls += 1;
496
+ if (call.time >= WEB_TOOL_BILLING_START) {
497
+ if (call.name === "web_search") {
498
+ webSearchBillable += 1;
499
+ webSearchCost += WEB_TOOL_PRICE_PER_CALL;
500
+ } else {
501
+ webFetchBillable += 1;
502
+ webFetchCost += WEB_TOOL_PRICE_PER_CALL;
503
+ }
504
+ }
505
+ }
506
+ return {
507
+ webSearchCalls,
508
+ webFetchCalls,
509
+ webSearchBillable,
510
+ webFetchBillable,
511
+ webSearchCost,
512
+ webFetchCost,
513
+ webToolCost: webSearchCost + webFetchCost,
514
+ };
515
+ }
516
+
456
517
  /** 从事件数组汇总 usage(按模型分段 + 跨时段分桶)。
457
518
  * 供 live 会话与持久化(子代理)会话复用。返回按模型拆分的 byModel,及跨模型聚合字段。 */
458
519
  function sumEventsUsage(events, config, fallbackModel = "") {
459
520
  const switches = collectModelSwitches(events);
460
521
  const starts = stepStartTimes(events);
461
522
  const fallback = fallbackModel !== "" ? fallbackModel : "deepseek-v4-flash";
523
+ // 网页搜索 / 抓取:次数 + 按次计费金额(2026-11-01 起才收费)
524
+ const webTools = webToolUsage(events);
462
525
  const latestByStep = new Map();
463
526
  for (const event of events ?? []) {
464
527
  if (event.type !== "assistant/message") continue;
@@ -518,6 +581,7 @@ function sumEventsUsage(events, config, fallbackModel = "") {
518
581
  peak,
519
582
  offpeak,
520
583
  byModel: modelList,
584
+ ...webTools,
521
585
  };
522
586
  }
523
587
 
@@ -728,6 +792,35 @@ function tokenTotal(usage) {
728
792
  return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
729
793
  }
730
794
 
795
+ /** 取(或建)某天的统计桶。 */
796
+ function ensureDay(days, key) {
797
+ let day = days.get(key);
798
+ if (day == null) {
799
+ day = {
800
+ date: key,
801
+ ...emptyTokenBucket(),
802
+ totalTokens: 0,
803
+ cost: 0,
804
+ sessionIds: new Set(),
805
+ // —— 以下为细分维度,供统计面板展示;不改动上方总额口径 ——
806
+ byModel: new Map(), // modelKey -> { model, tokens..., totalTokens, cost }
807
+ peak: emptyTokenBucket(),
808
+ offpeak: emptyTokenBucket(),
809
+ peakCost: 0,
810
+ offpeakCost: 0,
811
+ // 网页搜索 / 抓取的工具调用次数与按次费用(官方 ¥5/千次,2026-11-01 起计费)
812
+ webSearchCalls: 0,
813
+ webFetchCalls: 0,
814
+ webSearchBillable: 0,
815
+ webFetchBillable: 0,
816
+ webSearchCost: 0,
817
+ webFetchCost: 0,
818
+ };
819
+ days.set(key, day);
820
+ }
821
+ return day;
822
+ }
823
+
731
824
  /** 按北京时间把一个会话的最终 assistant/message usage 聚合到每日统计。 */
732
825
  function collectDailyUsage(events, config, fallbackModel, cutoffMs, days, sessionId) {
733
826
  const switches = collectModelSwitches(events);
@@ -743,26 +836,52 @@ function collectDailyUsage(events, config, fallbackModel, cutoffMs, days, sessio
743
836
  // 与 sumEventsUsage 同口径:按"请求发起时刻"归属日期与峰谷,拿不到才回退完成时刻
744
837
  latestByStep.set(`${turn}/${step}`, { usage, time: starts.get(`${turn}/${step}`) ?? Number(event.time ?? 0) });
745
838
  }
746
- for (const { usage, time } of latestByStep.values()) {
839
+ // 网页搜索 / 抓取:按调用发生的北京日期归日,金额按官方「每千次 ¥5」、且只在
840
+ // 2026-11-01 00:00(北京)之后计费。tool/call 与 tool/result 配对 + isError 过滤同 webToolUsage。
841
+ const webCalls = new Map(); // callId -> { name, time }
842
+ for (const event of events ?? []) {
843
+ if (event.type !== "tool/call") continue;
844
+ const name = event.data?.name;
845
+ if (name !== "web_search" && name !== "web_fetch") continue;
846
+ const time = Number(event.time ?? 0);
847
+ const id = event.data?.callId;
747
848
  if (!Number.isFinite(time) || time < cutoffMs) continue;
748
- const key = beijingDayKey(time);
749
- let day = days.get(key);
750
- if (day == null) {
751
- day = {
752
- date: key,
753
- ...emptyTokenBucket(),
754
- totalTokens: 0,
755
- cost: 0,
756
- sessionIds: new Set(),
757
- // —— 以下为细分维度,供统计面板展示;不改动上方总额口径 ——
758
- byModel: new Map(), // modelKey -> { model, tokens..., totalTokens, cost }
759
- peak: emptyTokenBucket(),
760
- offpeak: emptyTokenBucket(),
761
- peakCost: 0,
762
- offpeakCost: 0,
763
- };
764
- days.set(key, day);
849
+ if (id === void 0 || id === null) continue;
850
+ webCalls.set(String(id), { name, time });
851
+ }
852
+ if (webCalls.size > 0) {
853
+ const seen = new Set();
854
+ for (const event of events ?? []) {
855
+ if (event.type !== "tool/result") continue;
856
+ const id = event.data?.toolCallId ?? event.data?.message?.toolCallId;
857
+ const key = id === void 0 || id === null ? "" : String(id);
858
+ const call = webCalls.get(key);
859
+ if (call === void 0 || seen.has(key)) continue;
860
+ seen.add(key);
861
+ if (event.data?.message?.isError === true) continue;
862
+ const day = ensureDay(days, beijingDayKey(call.time));
863
+ const billable = call.time >= WEB_TOOL_BILLING_START;
864
+ if (call.name === "web_search") {
865
+ day.webSearchCalls += 1;
866
+ if (billable) {
867
+ day.webSearchBillable += 1;
868
+ day.webSearchCost += WEB_TOOL_PRICE_PER_CALL;
869
+ day.cost += WEB_TOOL_PRICE_PER_CALL;
870
+ }
871
+ } else {
872
+ day.webFetchCalls += 1;
873
+ if (billable) {
874
+ day.webFetchBillable += 1;
875
+ day.webFetchCost += WEB_TOOL_PRICE_PER_CALL;
876
+ day.cost += WEB_TOOL_PRICE_PER_CALL;
877
+ }
878
+ }
879
+ day.sessionIds.add(sessionId);
765
880
  }
881
+ }
882
+ for (const { usage, time } of latestByStep.values()) {
883
+ if (!Number.isFinite(time) || time < cutoffMs) continue;
884
+ const day = ensureDay(days, beijingDayKey(time));
766
885
  const model = modelAtTime(switches, time) || fallback;
767
886
  const isPeak = isPeakAt(time, config);
768
887
  // 分时段精确计费:高峰桶按高峰价(= 目录价 ×2)、空闲桶按目录价
@@ -896,6 +1015,13 @@ async function buildUsageStats(ctx, config) {
896
1015
  offpeak: day.offpeak,
897
1016
  peakCost: day.peakCost,
898
1017
  offpeakCost: day.offpeakCost,
1018
+ webSearchCalls: day.webSearchCalls,
1019
+ webFetchCalls: day.webFetchCalls,
1020
+ webSearchBillable: day.webSearchBillable,
1021
+ webFetchBillable: day.webFetchBillable,
1022
+ webSearchCost: day.webSearchCost,
1023
+ webFetchCost: day.webFetchCost,
1024
+ webToolCost: day.webSearchCost + day.webFetchCost,
899
1025
  };
900
1026
  });
901
1027
  const total = rows.reduce((acc, day) => {
@@ -910,6 +1036,13 @@ async function buildUsageStats(ctx, config) {
910
1036
  addTokenUsage(acc.offpeak, day.offpeak);
911
1037
  acc.peakCost += day.peakCost;
912
1038
  acc.offpeakCost += day.offpeakCost;
1039
+ acc.webSearchCalls += day.webSearchCalls;
1040
+ acc.webFetchCalls += day.webFetchCalls;
1041
+ acc.webSearchBillable += day.webSearchBillable;
1042
+ acc.webFetchBillable += day.webFetchBillable;
1043
+ acc.webSearchCost += day.webSearchCost;
1044
+ acc.webFetchCost += day.webFetchCost;
1045
+ acc.webToolCost += day.webToolCost;
913
1046
  for (const item of day.byModel ?? []) {
914
1047
  let entry = acc.byModel.get(item.model);
915
1048
  if (entry == null) {
@@ -930,6 +1063,13 @@ async function buildUsageStats(ctx, config) {
930
1063
  offpeak: emptyTokenBucket(),
931
1064
  peakCost: 0,
932
1065
  offpeakCost: 0,
1066
+ webSearchCalls: 0,
1067
+ webFetchCalls: 0,
1068
+ webSearchBillable: 0,
1069
+ webFetchBillable: 0,
1070
+ webSearchCost: 0,
1071
+ webFetchCost: 0,
1072
+ webToolCost: 0,
933
1073
  });
934
1074
  return {
935
1075
  days: rows,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "在 DSH 中显示 DeepSeek 账户余额和当前会话 token/费用消耗(含高峰/空闲峰谷计费)。仅支持 DeepSeek 官方模型与账号,暂不支持其他大模型。",
5
5
  "author": "Rannist",
6
6
  "type": "module",