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.
- package/lib/client.js +24 -3
- package/lib/index.js +159 -19
- 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
|
-
|
|
262
|
+
} else {
|
|
263
|
+
total = computeSingleSplitCost(usage, p, multiplier);
|
|
263
264
|
}
|
|
264
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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,
|