@kenz1117/dsh-ui-usage-billing 1.5.0-alpha.2 → 1.5.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/README.md +2 -1
- package/lib/client.js +4 -4
- package/lib/index.js +230 -77
- package/lib/types/aggregate.d.ts +52 -2
- package/lib/types/client/UsageBilling.d.ts +11 -0
- package/lib/types/client/export.d.ts +7 -1
- package/lib/types/client/live-cost.d.ts +13 -3
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/usage-billing-settings.d.ts +3 -1
- package/package.json +33 -31
package/lib/index.js
CHANGED
|
@@ -2301,7 +2301,9 @@ const BUILTIN_MODEL_KEY_ALIASES = {
|
|
|
2301
2301
|
"deepseek-v4.1-flash": "flash",
|
|
2302
2302
|
"deepseek-v4.1-flash-expires-on-0910": "flash",
|
|
2303
2303
|
"deepseek-flash": "flash",
|
|
2304
|
+
"deepseek-v4.1-flash-1017": "flash",
|
|
2304
2305
|
"deepseek/deepseek-v4.1-flash": "flash",
|
|
2306
|
+
"deepseek/deepseek-v4.1-flash-1017": "flash",
|
|
2305
2307
|
"deepseek/deepseek-flash": "flash",
|
|
2306
2308
|
"deepseek/deepseek-v4-pro": "pro",
|
|
2307
2309
|
"deepseek-v4-pro": "pro",
|
|
@@ -2733,6 +2735,57 @@ function workspaceNameOf(cwd) {
|
|
|
2733
2735
|
if (cwd === void 0 || cwd === "") return "—";
|
|
2734
2736
|
return cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? "—";
|
|
2735
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* 从会话 header 提取子智能体归并标记(直接父会话 id):`origin: "subagent"`
|
|
2740
|
+
* (宿主的产品分类)或 `delegationDepth > 0`(持久化委派深度)任一命中即视为
|
|
2741
|
+
* 子会话。子会话但 header 无 parentSession(异常数据)返回 undefined——无法
|
|
2742
|
+
* 归位就按顶层会话处理,保守不标。导出供测试。
|
|
2743
|
+
*/
|
|
2744
|
+
function subParentOf(meta) {
|
|
2745
|
+
if (meta.origin !== "subagent" && !((meta.delegationDepth ?? 0) > 0)) return void 0;
|
|
2746
|
+
const parent = meta.parentSession;
|
|
2747
|
+
return typeof parent === "string" && parent !== "" ? parent : void 0;
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* 给会话行补后代合计(subCost/subCalls):按 parentId 建子索引,对每行递归
|
|
2751
|
+
* 汇总全部后代子会话(多级委派整链计入根对话)。纯展示层归因——不改任何
|
|
2752
|
+
* 总额桶(子会话费用本就在 total/byDay/byModel 里,行级只是拆出来给人看)。
|
|
2753
|
+
* 环引用(异常数据)按 visited 跳过,不死循环。原地修改传入行。导出供测试。
|
|
2754
|
+
*/
|
|
2755
|
+
function foldSubagentFamilies(rows) {
|
|
2756
|
+
const subsOf = /* @__PURE__ */ new Map();
|
|
2757
|
+
for (const row of rows) {
|
|
2758
|
+
const parent = row.parentId;
|
|
2759
|
+
if (parent === void 0) continue;
|
|
2760
|
+
const list = subsOf.get(parent);
|
|
2761
|
+
if (list !== void 0) list.push(row);
|
|
2762
|
+
else subsOf.set(parent, [row]);
|
|
2763
|
+
}
|
|
2764
|
+
const familyOf = (id, visited) => {
|
|
2765
|
+
let cost = 0;
|
|
2766
|
+
let calls = 0;
|
|
2767
|
+
for (const child of subsOf.get(id) ?? []) {
|
|
2768
|
+
if (visited.has(child.id)) continue;
|
|
2769
|
+
visited.add(child.id);
|
|
2770
|
+
cost += child.cost;
|
|
2771
|
+
calls += child.calls;
|
|
2772
|
+
const nested = familyOf(child.id, visited);
|
|
2773
|
+
cost += nested.cost;
|
|
2774
|
+
calls += nested.calls;
|
|
2775
|
+
}
|
|
2776
|
+
return {
|
|
2777
|
+
cost,
|
|
2778
|
+
calls
|
|
2779
|
+
};
|
|
2780
|
+
};
|
|
2781
|
+
for (const row of rows) {
|
|
2782
|
+
const family = familyOf(row.id, /* @__PURE__ */ new Set([row.id]));
|
|
2783
|
+
if (family.calls > 0) {
|
|
2784
|
+
row.subCalls = family.calls;
|
|
2785
|
+
if (family.cost > 0) row.subCost = family.cost;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2736
2789
|
/** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
|
|
2737
2790
|
const PERF_SPIKE_MS = 1e4;
|
|
2738
2791
|
/** 全零摘要(合并的恒等元)。 */
|
|
@@ -2876,6 +2929,46 @@ function perfDigestsOf(samples) {
|
|
|
2876
2929
|
};
|
|
2877
2930
|
}
|
|
2878
2931
|
/**
|
|
2932
|
+
* 账本行谁更新:{@link UsageLedgerSession.foldedAt} 新者胜(缺失视为 0 =
|
|
2933
|
+
* 合并语义引入前的旧行);时刻相同比 {@link UsageLedgerSession.foldVersion}
|
|
2934
|
+
* (新算法胜);再相同视为等价,取 incoming(本进程刚折出的当前视图)。
|
|
2935
|
+
*/
|
|
2936
|
+
function ledgerRowWins(incoming, existing) {
|
|
2937
|
+
const at = incoming.foldedAt ?? 0;
|
|
2938
|
+
const bt = existing.foldedAt ?? 0;
|
|
2939
|
+
if (at !== bt) return at > bt;
|
|
2940
|
+
return (incoming.foldVersion ?? 1) > (existing.foldVersion ?? 1);
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* 多实例账本合并(自审计延期项:save 路径 last-writer-wins 丢写):同一
|
|
2944
|
+
* DSH_HOME 下多个宿主实例各自持有内存账本,整份覆盖式落盘会让后写者抹掉
|
|
2945
|
+
* 先写者刚保存的行。本进程要落的文档与磁盘现存文档按会话 id 行级合并:
|
|
2946
|
+
* 双方都有的行按「谁更新谁胜」,仅磁盘有的行(他实例写入、本进程内存没有)
|
|
2947
|
+
* 原样保留——账本行从不按 seen 剪除(见 aggregateOnce 尾部注释),合并不会
|
|
2948
|
+
* 复活已删会话。合并只发生在落盘边界:跨实例的即时可见性仍靠重启重载,
|
|
2949
|
+
* 此处保证的是磁盘上不丢数据(最终一致)。
|
|
2950
|
+
* @param document - 本进程刚聚合出的完整账本文档。
|
|
2951
|
+
* @param existing - 磁盘上现存(他实例可能写入过)的账本文档。
|
|
2952
|
+
* @returns 合并后的文档(sessions 行序:incoming 在前、existing 独有行按原序追加)。
|
|
2953
|
+
*/
|
|
2954
|
+
function mergeUsageLedgerDocuments(document, existing) {
|
|
2955
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2956
|
+
for (const row of existing.sessions) byId.set(row.id, row);
|
|
2957
|
+
const sessions = [];
|
|
2958
|
+
for (const row of document.sessions) {
|
|
2959
|
+
const prev = byId.get(row.id);
|
|
2960
|
+
sessions.push(prev !== void 0 && ledgerRowWins(prev, row) ? prev : row);
|
|
2961
|
+
byId.delete(row.id);
|
|
2962
|
+
}
|
|
2963
|
+
for (const row of byId.values()) sessions.push(row);
|
|
2964
|
+
const applied = document.appliedMigrations === void 0 && existing.appliedMigrations === void 0 ? void 0 : [.../* @__PURE__ */ new Set([...document.appliedMigrations ?? [], ...existing.appliedMigrations ?? []])];
|
|
2965
|
+
return {
|
|
2966
|
+
...document,
|
|
2967
|
+
sessions,
|
|
2968
|
+
...applied === void 0 ? {} : { appliedMigrations: applied }
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2879
2972
|
* 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
|
|
2880
2973
|
* header 归因算法);perf 摘要化迁移把 v15 及更早行的逐样本 `perf` 数组转换为
|
|
2881
2974
|
* `perfDigest`/`perfHourDigest` 摘要(日志已删的行不再被重折,迁移是它们唯一的
|
|
@@ -3236,20 +3329,20 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
|
|
|
3236
3329
|
const unverifiedChannel = siteBucket === "unknown";
|
|
3237
3330
|
const priced = !subscription && !unverifiedChannel && isPriced(modelKey);
|
|
3238
3331
|
if (!subscription && !isPriced(modelKey)) fold.unpricedModels.add(modelKey);
|
|
3239
|
-
const entry =
|
|
3240
|
-
const thisCost =
|
|
3332
|
+
const entry = modelOf(modelKey);
|
|
3333
|
+
const thisCost = priced ? computeCostAt(entry, {
|
|
3241
3334
|
input: (usage.cacheReadTokens ?? 0) + usage.inputTokens + (usage.cacheWriteTokens ?? 0),
|
|
3242
3335
|
cacheHit: usage.cacheReadTokens ?? 0,
|
|
3243
3336
|
cacheMiss: usage.inputTokens + (usage.cacheWriteTokens ?? 0),
|
|
3244
3337
|
output: usage.outputTokens
|
|
3245
|
-
}, event.time);
|
|
3338
|
+
}, event.time) : void 0;
|
|
3246
3339
|
foldUsage(fold.total, usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3247
3340
|
foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3248
3341
|
foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3249
3342
|
foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3250
3343
|
foldUsage(modelDaySiteCell(fold.byDayModelsSite, day, modelKey, siteBucket), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3251
3344
|
foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3252
|
-
foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3345
|
+
if (entry.tierSemantics !== "latency") foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official, thisCost, unverifiedChannel);
|
|
3253
3346
|
if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
|
|
3254
3347
|
if (subscription) {
|
|
3255
3348
|
const sitePlanKey = planSiteKey(day, modelKey, siteBucket);
|
|
@@ -3410,19 +3503,21 @@ function hourPerfOf(digest) {
|
|
|
3410
3503
|
* 订阅豁免),历史账本行立即失效并全量重折,配置变更即时生效(issue #37)。
|
|
3411
3504
|
* 判定函数无法序列化,统一记为 `fn`(任何函数形态互视为同一指纹)。
|
|
3412
3505
|
*/
|
|
3413
|
-
function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, modelKeyAliases = {}, registeredProviders = /* @__PURE__ */ new Set()) {
|
|
3506
|
+
function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, modelKeyAliases = {}, registeredProviders = /* @__PURE__ */ new Set(), routeOrigins = {}) {
|
|
3414
3507
|
const subs = typeof subscriptionMatcher === "function" ? "fn" : [...subscriptionMatcher].sort().join(",");
|
|
3415
3508
|
const officials = officialProviderIds === void 0 ? "" : [...officialProviderIds].sort().join(",");
|
|
3416
3509
|
const aliases = Object.keys(routeAliases).sort().map((key) => `${key}=${routeAliases[key]}`).join(",");
|
|
3417
3510
|
const modelKeys = Object.keys(modelKeyAliases).sort().map((key) => `${key}=${modelKeyAliases[key]}`).join(",");
|
|
3418
3511
|
const registered = [...registeredProviders].sort().join(",");
|
|
3512
|
+
const origins = Object.keys(routeOrigins).sort().map((key) => `${key}=${routeOrigins[key]}`).join(",");
|
|
3419
3513
|
return [
|
|
3420
3514
|
subs,
|
|
3421
3515
|
officials,
|
|
3422
3516
|
aliases,
|
|
3423
3517
|
String(searchEstimate),
|
|
3424
3518
|
modelKeys,
|
|
3425
|
-
registered
|
|
3519
|
+
registered,
|
|
3520
|
+
origins
|
|
3426
3521
|
].join("|");
|
|
3427
3522
|
}
|
|
3428
3523
|
/** 会话格式拒读(宿主 SessionFormatUnsupportedError):日志完好,仅本构建读不了。
|
|
@@ -3473,8 +3568,13 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3473
3568
|
return routes;
|
|
3474
3569
|
};
|
|
3475
3570
|
const searchEstimate = options.searchCallEstimateCny ?? .02;
|
|
3476
|
-
const
|
|
3477
|
-
|
|
3571
|
+
const routeOriginsOf = (routes) => {
|
|
3572
|
+
const origins = {};
|
|
3573
|
+
for (const [route, view] of Object.entries(routes)) if (view.baseURL !== void 0 && view.baseURL !== "") origins[route] = view.baseURL;
|
|
3574
|
+
return origins;
|
|
3575
|
+
};
|
|
3576
|
+
const foldFingerprintOf = (registered, routes) => configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, options.modelKeyAliases, registered, routeOriginsOf(routes));
|
|
3577
|
+
let foldFingerprint = foldFingerprintOf(/* @__PURE__ */ new Set(), {});
|
|
3478
3578
|
const FOLD_CHUNK_EVENTS = 8e3;
|
|
3479
3579
|
/** 惰性失效(价目表时序):账本/缓存行若以「当时不可计价」的模型落盘,
|
|
3480
3580
|
* 而价目表现在能计价了,该行必须重折——否则显示层按实时价算出的
|
|
@@ -3486,18 +3586,20 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3486
3586
|
* 价目表变化后未受影响行(无 unpriced 模型)依旧零读取复用。 */
|
|
3487
3587
|
const foldNeedsReprice = (fold) => fold.unpricedModels.some((key) => isPriced(key));
|
|
3488
3588
|
/** 把一份(全量或增量)折叠结果登记进 durable ledger;内容无变化时不落盘。 */
|
|
3489
|
-
const recordLedger = (id, cwd, stamp, fold) => {
|
|
3589
|
+
const recordLedger = (id, cwd, stamp, fold, subOf) => {
|
|
3490
3590
|
if (options.ledger === void 0) return;
|
|
3491
3591
|
const entry = {
|
|
3492
3592
|
id,
|
|
3493
3593
|
...cwd === void 0 ? {} : { cwd },
|
|
3594
|
+
...subOf === void 0 ? {} : { subOf },
|
|
3494
3595
|
...stamp === null ? {} : { stamp },
|
|
3495
|
-
foldVersion:
|
|
3596
|
+
foldVersion: 23,
|
|
3496
3597
|
fingerprint: foldFingerprint,
|
|
3598
|
+
foldedAt: Date.now(),
|
|
3497
3599
|
fold: serializeFold(fold)
|
|
3498
3600
|
};
|
|
3499
3601
|
const row = ledger.get(id);
|
|
3500
|
-
if (row === void 0 || row.stamp !== entry.stamp || row.cwd !== entry.cwd || row.foldVersion !== entry.foldVersion || row.fingerprint !== entry.fingerprint || stamp === null && JSON.stringify(row.fold) !== JSON.stringify(entry.fold) || stamp !== null && foldNeedsReprice(row.fold)) {
|
|
3602
|
+
if (row === void 0 || row.stamp !== entry.stamp || row.cwd !== entry.cwd || row.subOf !== entry.subOf || row.foldVersion !== entry.foldVersion || row.fingerprint !== entry.fingerprint || stamp === null && JSON.stringify(row.fold) !== JSON.stringify(entry.fold) || stamp !== null && foldNeedsReprice(row.fold)) {
|
|
3501
3603
|
ledger.set(id, entry);
|
|
3502
3604
|
ledgerNeedsSave = true;
|
|
3503
3605
|
}
|
|
@@ -3557,8 +3659,8 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3557
3659
|
const now = Date.now();
|
|
3558
3660
|
await ensureLedgerLoaded();
|
|
3559
3661
|
const registeredProviders = options.resolveRegisteredProviders?.() ?? /* @__PURE__ */ new Set();
|
|
3560
|
-
foldFingerprint = foldFingerprintOf(registeredProviders);
|
|
3561
3662
|
const roundRoutes = routesOf(registeredProviders);
|
|
3663
|
+
foldFingerprint = foldFingerprintOf(registeredProviders, roundRoutes);
|
|
3562
3664
|
let metas;
|
|
3563
3665
|
try {
|
|
3564
3666
|
metas = await persistence.list();
|
|
@@ -3589,12 +3691,14 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3589
3691
|
}
|
|
3590
3692
|
try {
|
|
3591
3693
|
const cwd = meta.cwd;
|
|
3694
|
+
const subOf = subParentOf(meta);
|
|
3592
3695
|
const ledgerRow = ledger.get(id);
|
|
3593
|
-
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) ===
|
|
3696
|
+
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 23 && ledgerRow.fingerprint === foldFingerprint && !foldNeedsReprice(ledgerRow.fold)) {
|
|
3594
3697
|
const fold = deserializedFoldOf(ledgerRow.fold);
|
|
3595
3698
|
folds.push({
|
|
3596
3699
|
id,
|
|
3597
3700
|
...cwd === void 0 ? {} : { cwd },
|
|
3701
|
+
...subOf === void 0 ? {} : { subOf },
|
|
3598
3702
|
fold
|
|
3599
3703
|
});
|
|
3600
3704
|
included.add(id);
|
|
@@ -3607,6 +3711,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3607
3711
|
folds.push({
|
|
3608
3712
|
id,
|
|
3609
3713
|
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
3714
|
+
...subOf === void 0 ? {} : { subOf },
|
|
3610
3715
|
fold: hit.fold
|
|
3611
3716
|
});
|
|
3612
3717
|
included.add(id);
|
|
@@ -3635,10 +3740,11 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3635
3740
|
folds.push({
|
|
3636
3741
|
id,
|
|
3637
3742
|
...cwd === void 0 ? {} : { cwd },
|
|
3743
|
+
...subOf === void 0 ? {} : { subOf },
|
|
3638
3744
|
fold: previous.fold
|
|
3639
3745
|
});
|
|
3640
3746
|
included.add(id);
|
|
3641
|
-
recordLedger(id, cwd, stamp, previous.fold);
|
|
3747
|
+
recordLedger(id, cwd, stamp, previous.fold, subOf);
|
|
3642
3748
|
continue;
|
|
3643
3749
|
}
|
|
3644
3750
|
const { events, inheritedEventCount } = await persistence.readFrom(meta.id, sessionLogOffsetOf(0));
|
|
@@ -3664,10 +3770,11 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3664
3770
|
folds.push({
|
|
3665
3771
|
id,
|
|
3666
3772
|
...cwd === void 0 ? {} : { cwd },
|
|
3773
|
+
...subOf === void 0 ? {} : { subOf },
|
|
3667
3774
|
fold
|
|
3668
3775
|
});
|
|
3669
3776
|
included.add(id);
|
|
3670
|
-
recordLedger(id, cwd, stamp, fold);
|
|
3777
|
+
recordLedger(id, cwd, stamp, fold, subOf);
|
|
3671
3778
|
} catch (error) {
|
|
3672
3779
|
skipped.push(id);
|
|
3673
3780
|
const format = isFormatUnsupportedError(error);
|
|
@@ -3691,10 +3798,11 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3691
3798
|
for (const entry of ledger.values()) {
|
|
3692
3799
|
if (included.has(entry.id)) continue;
|
|
3693
3800
|
try {
|
|
3694
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
3801
|
+
const stale = (entry.foldVersion ?? 1) < 23 || entry.fingerprint !== foldFingerprint;
|
|
3695
3802
|
folds.push({
|
|
3696
3803
|
id: entry.id,
|
|
3697
3804
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
3805
|
+
...entry.subOf === void 0 ? {} : { subOf: entry.subOf },
|
|
3698
3806
|
...stale ? { staleLedger: true } : {},
|
|
3699
3807
|
fold: deserializedFoldOf(entry.fold)
|
|
3700
3808
|
});
|
|
@@ -3731,7 +3839,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3731
3839
|
const perfHourModel = /* @__PURE__ */ new Map();
|
|
3732
3840
|
const perfModelDigest = /* @__PURE__ */ new Map();
|
|
3733
3841
|
const perfHourDigest = /* @__PURE__ */ new Map();
|
|
3734
|
-
for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
|
|
3842
|
+
for (const { id: sessionId, cwd, subOf, fold, staleLedger } of folds) {
|
|
3735
3843
|
mergeUsageInto(total, fold.total);
|
|
3736
3844
|
roles.userChars += fold.roles.userChars;
|
|
3737
3845
|
roles.toolChars += fold.roles.toolChars;
|
|
@@ -3810,12 +3918,14 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3810
3918
|
id: sessionId,
|
|
3811
3919
|
...fold.title !== void 0 ? { title: fold.title } : {},
|
|
3812
3920
|
...cwd !== void 0 ? { cwd } : {},
|
|
3921
|
+
...subOf !== void 0 ? { parentId: subOf } : {},
|
|
3813
3922
|
...staleLedger === true ? { stale: true } : {},
|
|
3814
3923
|
calls: fold.total.calls,
|
|
3815
3924
|
cost: fold.total.cost,
|
|
3816
3925
|
lastActive: fold.lastActive
|
|
3817
3926
|
});
|
|
3818
3927
|
}
|
|
3928
|
+
foldSubagentFamilies(sessionRows);
|
|
3819
3929
|
sessionRows.sort((a, b) => b.cost - a.cost || b.lastActive - a.lastActive);
|
|
3820
3930
|
turnRows.sort((a, b) => b.startedAt - a.startedAt);
|
|
3821
3931
|
const workspaces = [...workspaceMap.values()].sort((a, b) => b.cost - a.cost || b.lastActive - a.lastActive);
|
|
@@ -6933,15 +7043,21 @@ function createFileUsageLedgerStore(ledgerPath) {
|
|
|
6933
7043
|
mode: 448
|
|
6934
7044
|
});
|
|
6935
7045
|
const commit = async () => {
|
|
7046
|
+
let existing;
|
|
7047
|
+
let existingDoc;
|
|
6936
7048
|
try {
|
|
6937
|
-
|
|
6938
|
-
JSON.parse(existing);
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
}
|
|
6944
|
-
|
|
7049
|
+
existing = await readFile(ledgerPath, "utf8");
|
|
7050
|
+
const parsed = JSON.parse(existing);
|
|
7051
|
+
if (parsed.version === 1 && Array.isArray(parsed.sessions)) existingDoc = parsed;
|
|
7052
|
+
} catch {
|
|
7053
|
+
existing = void 0;
|
|
7054
|
+
}
|
|
7055
|
+
if (existing !== void 0) await writeFileAtomic(`${ledgerPath}.bak`, existing, {
|
|
7056
|
+
mode: 384,
|
|
7057
|
+
dirMode: 448
|
|
7058
|
+
});
|
|
7059
|
+
const merged = existingDoc === void 0 ? document : mergeUsageLedgerDocuments(document, existingDoc);
|
|
7060
|
+
await writeFileAtomic(ledgerPath, JSON.stringify(merged), {
|
|
6945
7061
|
mode: 384,
|
|
6946
7062
|
dirMode: 448
|
|
6947
7063
|
});
|
|
@@ -7665,69 +7781,78 @@ function apply(ctx, config = {}) {
|
|
|
7665
7781
|
at: 0,
|
|
7666
7782
|
doc: { balances: [] }
|
|
7667
7783
|
};
|
|
7784
|
+
let balanceInflight;
|
|
7785
|
+
const refreshBalance = async () => {
|
|
7786
|
+
const piProviders = await readPiAiProviders(ctx.settings);
|
|
7787
|
+
const providers = { ...piProviders };
|
|
7788
|
+
const deepseekKey = providers["deepseek"]?.apiKeyEnv;
|
|
7789
|
+
if (deepseekKey === void 0 || deepseekKey === "") providers["deepseek"] = {
|
|
7790
|
+
...providers["deepseek"],
|
|
7791
|
+
apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
|
|
7792
|
+
};
|
|
7793
|
+
const balances = await queryBalances(ctx, providers);
|
|
7794
|
+
const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
|
|
7795
|
+
const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
|
|
7796
|
+
for (const row of [
|
|
7797
|
+
...balances,
|
|
7798
|
+
...custom,
|
|
7799
|
+
...declared
|
|
7800
|
+
]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
|
|
7801
|
+
const official = balances.find((row) => row.provider === "deepseek");
|
|
7802
|
+
let reconcile;
|
|
7803
|
+
const reconcilable = official !== void 0 && official.totalBalance !== void 0 && (official.currency === void 0 || official.currency.toUpperCase() === "CNY");
|
|
7804
|
+
if (official !== void 0 && reconcilable) {
|
|
7805
|
+
const now = Date.now();
|
|
7806
|
+
const today = dayStamp(now);
|
|
7807
|
+
let todayOfficialCost = 0;
|
|
7808
|
+
try {
|
|
7809
|
+
todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
|
|
7810
|
+
} catch {}
|
|
7811
|
+
const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
|
|
7812
|
+
const refChanged = result.ref !== null && result.ref !== reconcileRef;
|
|
7813
|
+
reconcileRef = result.ref;
|
|
7814
|
+
if (result.event !== null && result.event.kind !== "flat") reconcile = {
|
|
7815
|
+
...result.event,
|
|
7816
|
+
provider: official.displayName,
|
|
7817
|
+
...official.currency === void 0 ? {} : { currency: official.currency }
|
|
7818
|
+
};
|
|
7819
|
+
if (refChanged) persistReconcileRef();
|
|
7820
|
+
}
|
|
7821
|
+
const doc = {
|
|
7822
|
+
balances: [
|
|
7823
|
+
...balances,
|
|
7824
|
+
...custom,
|
|
7825
|
+
...declared
|
|
7826
|
+
],
|
|
7827
|
+
...reconcile === void 0 ? {} : { reconcile }
|
|
7828
|
+
};
|
|
7829
|
+
balanceCache = {
|
|
7830
|
+
at: Date.now(),
|
|
7831
|
+
doc
|
|
7832
|
+
};
|
|
7833
|
+
};
|
|
7834
|
+
const refreshBalanceOnce = () => {
|
|
7835
|
+
balanceInflight ??= refreshBalance().finally(() => {
|
|
7836
|
+
balanceInflight = void 0;
|
|
7837
|
+
});
|
|
7838
|
+
return balanceInflight;
|
|
7839
|
+
};
|
|
7668
7840
|
ctx.effect(() => ctx.webServer.register({
|
|
7669
7841
|
kind: "exact",
|
|
7670
7842
|
path: "/api/billing/balance",
|
|
7671
7843
|
handler: async (req, res) => {
|
|
7672
7844
|
if (!guardLoopback(req, res, trustedHosts)) return;
|
|
7673
7845
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7674
|
-
const piProviders = await readPiAiProviders(ctx.settings);
|
|
7675
|
-
const providers = { ...piProviders };
|
|
7676
|
-
const deepseekKey = providers["deepseek"]?.apiKeyEnv;
|
|
7677
|
-
if (deepseekKey === void 0 || deepseekKey === "") providers["deepseek"] = {
|
|
7678
|
-
...providers["deepseek"],
|
|
7679
|
-
apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
|
|
7680
|
-
};
|
|
7681
7846
|
if (Date.now() - balanceCache.at < BALANCE_CACHE_MS) {
|
|
7682
7847
|
res.end(JSON.stringify(balanceCache.doc));
|
|
7683
7848
|
return;
|
|
7684
7849
|
}
|
|
7685
7850
|
try {
|
|
7686
|
-
|
|
7687
|
-
const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
|
|
7688
|
-
const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
|
|
7689
|
-
for (const row of [
|
|
7690
|
-
...balances,
|
|
7691
|
-
...custom,
|
|
7692
|
-
...declared
|
|
7693
|
-
]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
|
|
7694
|
-
const official = balances.find((row) => row.provider === "deepseek");
|
|
7695
|
-
let reconcile;
|
|
7696
|
-
const reconcilable = official !== void 0 && official.totalBalance !== void 0 && (official.currency === void 0 || official.currency.toUpperCase() === "CNY");
|
|
7697
|
-
if (official !== void 0 && reconcilable) {
|
|
7698
|
-
const now = Date.now();
|
|
7699
|
-
const today = dayStamp(now);
|
|
7700
|
-
let todayOfficialCost = 0;
|
|
7701
|
-
try {
|
|
7702
|
-
todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
|
|
7703
|
-
} catch {}
|
|
7704
|
-
const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
|
|
7705
|
-
const refChanged = result.ref !== null && result.ref !== reconcileRef;
|
|
7706
|
-
reconcileRef = result.ref;
|
|
7707
|
-
if (result.event !== null && result.event.kind !== "flat") reconcile = {
|
|
7708
|
-
...result.event,
|
|
7709
|
-
provider: official.displayName,
|
|
7710
|
-
...official.currency === void 0 ? {} : { currency: official.currency }
|
|
7711
|
-
};
|
|
7712
|
-
if (refChanged) persistReconcileRef();
|
|
7713
|
-
}
|
|
7714
|
-
const doc = {
|
|
7715
|
-
balances: [
|
|
7716
|
-
...balances,
|
|
7717
|
-
...custom,
|
|
7718
|
-
...declared
|
|
7719
|
-
],
|
|
7720
|
-
...reconcile === void 0 ? {} : { reconcile }
|
|
7721
|
-
};
|
|
7722
|
-
balanceCache = {
|
|
7723
|
-
at: Date.now(),
|
|
7724
|
-
doc
|
|
7725
|
-
};
|
|
7726
|
-
res.end(JSON.stringify(doc));
|
|
7851
|
+
await refreshBalanceOnce();
|
|
7727
7852
|
} catch (error) {
|
|
7728
7853
|
console.warn("[usage-billing] refreshBalances failed; serving cached balances:", error);
|
|
7729
|
-
res.end(JSON.stringify(balanceCache.doc));
|
|
7730
7854
|
}
|
|
7855
|
+
res.end(JSON.stringify(balanceCache.doc));
|
|
7731
7856
|
}
|
|
7732
7857
|
}), "usage-billing: balance route");
|
|
7733
7858
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -7822,6 +7947,13 @@ function apply(ctx, config = {}) {
|
|
|
7822
7947
|
quotas: rows
|
|
7823
7948
|
};
|
|
7824
7949
|
};
|
|
7950
|
+
let quotasInflight;
|
|
7951
|
+
const refreshQuotasOnce = () => {
|
|
7952
|
+
quotasInflight ??= refreshQuotas().finally(() => {
|
|
7953
|
+
quotasInflight = void 0;
|
|
7954
|
+
});
|
|
7955
|
+
return quotasInflight;
|
|
7956
|
+
};
|
|
7825
7957
|
ctx.effect(() => ctx.webServer.register({
|
|
7826
7958
|
kind: "exact",
|
|
7827
7959
|
path: "/api/billing/subscriptions",
|
|
@@ -7829,7 +7961,7 @@ function apply(ctx, config = {}) {
|
|
|
7829
7961
|
if (!guardLoopback(req, res, trustedHosts)) return;
|
|
7830
7962
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7831
7963
|
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
7832
|
-
await
|
|
7964
|
+
await refreshQuotasOnce();
|
|
7833
7965
|
} catch (error) {
|
|
7834
7966
|
console.warn("[usage-billing] refreshQuotas failed; serving cached quotas:", error);
|
|
7835
7967
|
}
|
|
@@ -7858,6 +7990,13 @@ function apply(ctx, config = {}) {
|
|
|
7858
7990
|
quotas: await queryRelayQuotas(ctx, routes)
|
|
7859
7991
|
};
|
|
7860
7992
|
};
|
|
7993
|
+
let relayInflight;
|
|
7994
|
+
const refreshRelayOnce = () => {
|
|
7995
|
+
relayInflight ??= refreshRelay().finally(() => {
|
|
7996
|
+
relayInflight = void 0;
|
|
7997
|
+
});
|
|
7998
|
+
return relayInflight;
|
|
7999
|
+
};
|
|
7861
8000
|
ctx.effect(() => ctx.webServer.register({
|
|
7862
8001
|
kind: "exact",
|
|
7863
8002
|
path: "/api/billing/relay-quotas",
|
|
@@ -7865,7 +8004,7 @@ function apply(ctx, config = {}) {
|
|
|
7865
8004
|
if (!guardLoopback(req, res, trustedHosts)) return;
|
|
7866
8005
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7867
8006
|
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
7868
|
-
await
|
|
8007
|
+
await refreshRelayOnce();
|
|
7869
8008
|
} catch (error) {
|
|
7870
8009
|
console.warn("[usage-billing] refreshRelay failed; serving cached relay quotas:", error);
|
|
7871
8010
|
}
|
|
@@ -7880,6 +8019,7 @@ function apply(ctx, config = {}) {
|
|
|
7880
8019
|
}));
|
|
7881
8020
|
}
|
|
7882
8021
|
}), "usage-billing: relay-quotas route");
|
|
8022
|
+
let statsBodyCache;
|
|
7883
8023
|
ctx.effect(() => ctx.webServer.register({
|
|
7884
8024
|
kind: "exact",
|
|
7885
8025
|
path: "/api/billing/usage-stats",
|
|
@@ -7892,6 +8032,19 @@ function apply(ctx, config = {}) {
|
|
|
7892
8032
|
...config.monthlyBudget === void 0 ? {} : { budget: config.monthlyBudget },
|
|
7893
8033
|
...config.lowBalanceThreshold === void 0 ? {} : { lowBalanceThreshold: config.lowBalanceThreshold }
|
|
7894
8034
|
});
|
|
8035
|
+
const statsBodyOf = (doc) => {
|
|
8036
|
+
const budget = config.monthlyBudget;
|
|
8037
|
+
const threshold = config.lowBalanceThreshold;
|
|
8038
|
+
if (statsBodyCache !== void 0 && statsBodyCache.doc === doc && statsBodyCache.budget === budget && statsBodyCache.threshold === threshold) return statsBodyCache.body;
|
|
8039
|
+
const body = JSON.stringify(decorate(doc));
|
|
8040
|
+
statsBodyCache = {
|
|
8041
|
+
doc,
|
|
8042
|
+
budget,
|
|
8043
|
+
threshold,
|
|
8044
|
+
body
|
|
8045
|
+
};
|
|
8046
|
+
return body;
|
|
8047
|
+
};
|
|
7895
8048
|
const aggregating = aggregator.aggregate();
|
|
7896
8049
|
aggregating.then((stats) => {
|
|
7897
8050
|
persistSnapshot(decorate(stats));
|
|
@@ -7907,7 +8060,7 @@ function apply(ctx, config = {}) {
|
|
|
7907
8060
|
console.error("[usage-billing] usage-stats aggregate failed, falling back to snapshot:", error);
|
|
7908
8061
|
}
|
|
7909
8062
|
if (fresh !== void 0) {
|
|
7910
|
-
res.end(
|
|
8063
|
+
res.end(statsBodyOf(fresh));
|
|
7911
8064
|
return;
|
|
7912
8065
|
}
|
|
7913
8066
|
for (const candidate of candidates) {
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -389,6 +389,17 @@ export interface SessionUsageRow {
|
|
|
389
389
|
title?: string;
|
|
390
390
|
/** 会话创建时的工作目录(项目路径);未知时缺失。 */
|
|
391
391
|
cwd?: string;
|
|
392
|
+
/**
|
|
393
|
+
* 直接父会话 id(仅子智能体会话携带):宿主 header 的 `origin: "subagent"`
|
|
394
|
+
* 会话经 parentSession 指向委派它的对话。fork/resume 的种子血缘
|
|
395
|
+
* (isSeeded)不满足子智能体门,不带本字段。旧宿主 header 无标记,
|
|
396
|
+
* 按顶层会话处理。
|
|
397
|
+
*/
|
|
398
|
+
parentId?: string;
|
|
399
|
+
/** 全部后代子会话的费用合计(人民币元,多级委派整链递归);无后代或后代全免费时缺失。 */
|
|
400
|
+
subCost?: number;
|
|
401
|
+
/** 全部后代子会话的调用数;无后代时缺失。 */
|
|
402
|
+
subCalls?: number;
|
|
392
403
|
calls: number;
|
|
393
404
|
cost: number;
|
|
394
405
|
/** 最后一个事件的时间戳(毫秒)。 */
|
|
@@ -396,6 +407,20 @@ export interface SessionUsageRow {
|
|
|
396
407
|
/** 数据来自旧算法折叠的持久账本行(日志已删/不可读,无法重算);UI 据此标注置信度。 */
|
|
397
408
|
stale?: boolean;
|
|
398
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* 从会话 header 提取子智能体归并标记(直接父会话 id):`origin: "subagent"`
|
|
412
|
+
* (宿主的产品分类)或 `delegationDepth > 0`(持久化委派深度)任一命中即视为
|
|
413
|
+
* 子会话。子会话但 header 无 parentSession(异常数据)返回 undefined——无法
|
|
414
|
+
* 归位就按顶层会话处理,保守不标。导出供测试。
|
|
415
|
+
*/
|
|
416
|
+
export declare function subParentOf(meta: SessionHeader): string | undefined;
|
|
417
|
+
/**
|
|
418
|
+
* 给会话行补后代合计(subCost/subCalls):按 parentId 建子索引,对每行递归
|
|
419
|
+
* 汇总全部后代子会话(多级委派整链计入根对话)。纯展示层归因——不改任何
|
|
420
|
+
* 总额桶(子会话费用本就在 total/byDay/byModel 里,行级只是拆出来给人看)。
|
|
421
|
+
* 环引用(异常数据)按 visited 跳过,不死循环。原地修改传入行。导出供测试。
|
|
422
|
+
*/
|
|
423
|
+
export declare function foldSubagentFamilies(rows: SessionUsageRow[]): void;
|
|
399
424
|
/** 每轮费用明细行:仪表盘「每轮费用」图的数据源。 */
|
|
400
425
|
export interface TurnUsageRow {
|
|
401
426
|
/** 会话 id(字符串形式):不同会话的轮次号相互独立,展示时需区分。 */
|
|
@@ -563,6 +588,12 @@ export interface SerializedSessionFold {
|
|
|
563
588
|
export interface UsageLedgerSession {
|
|
564
589
|
id: string;
|
|
565
590
|
cwd?: string;
|
|
591
|
+
/**
|
|
592
|
+
* 子智能体归并标记(直接父会话 id):仅宿主 header 带 `origin: "subagent"`
|
|
593
|
+
* 的会话行携带。日志已删、只剩账本行时仍能归位到父对话;缺失 = 顶层会话
|
|
594
|
+
* (含 fork/resume 血缘)。可选字段,旧账本行缺失按顶层处理,无需迁移。
|
|
595
|
+
*/
|
|
596
|
+
subOf?: string;
|
|
566
597
|
/** Stable log stamp (mtime + size) when the persistence backend exposes it. */
|
|
567
598
|
stamp?: string;
|
|
568
599
|
/**
|
|
@@ -575,6 +606,12 @@ export interface UsageLedgerSession {
|
|
|
575
606
|
* 更早写入的行(配置不参与复用判定);与当前指纹不一致的行不复用、全量重折。
|
|
576
607
|
*/
|
|
577
608
|
fingerprint?: string;
|
|
609
|
+
/**
|
|
610
|
+
* 该行最近一次折叠落盘的时刻(epoch 毫秒)。多实例共享同一账本文件时,
|
|
611
|
+
* save 侧按它做行级合并(后折覆盖先折),防止整份覆盖式落盘丢掉另一实例
|
|
612
|
+
* 刚写入的行;缺失 = 合并语义引入前的旧行,视为最旧。
|
|
613
|
+
*/
|
|
614
|
+
foldedAt?: number;
|
|
578
615
|
fold: SerializedSessionFold;
|
|
579
616
|
}
|
|
580
617
|
/** On-disk durable usage ledger. Versioned independently from the dashboard document. */
|
|
@@ -585,6 +622,19 @@ export interface UsageLedgerDocument {
|
|
|
585
622
|
/** 已应用的一次性配置迁移 id 列表(随文档落盘;缺省 = 尚未跑过任何迁移)。 */
|
|
586
623
|
appliedMigrations?: string[];
|
|
587
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* 多实例账本合并(自审计延期项:save 路径 last-writer-wins 丢写):同一
|
|
627
|
+
* DSH_HOME 下多个宿主实例各自持有内存账本,整份覆盖式落盘会让后写者抹掉
|
|
628
|
+
* 先写者刚保存的行。本进程要落的文档与磁盘现存文档按会话 id 行级合并:
|
|
629
|
+
* 双方都有的行按「谁更新谁胜」,仅磁盘有的行(他实例写入、本进程内存没有)
|
|
630
|
+
* 原样保留——账本行从不按 seen 剪除(见 aggregateOnce 尾部注释),合并不会
|
|
631
|
+
* 复活已删会话。合并只发生在落盘边界:跨实例的即时可见性仍靠重启重载,
|
|
632
|
+
* 此处保证的是磁盘上不丢数据(最终一致)。
|
|
633
|
+
* @param document - 本进程刚聚合出的完整账本文档。
|
|
634
|
+
* @param existing - 磁盘上现存(他实例可能写入过)的账本文档。
|
|
635
|
+
* @returns 合并后的文档(sessions 行序:incoming 在前、existing 独有行按原序追加)。
|
|
636
|
+
*/
|
|
637
|
+
export declare function mergeUsageLedgerDocuments(document: UsageLedgerDocument, existing: UsageLedgerDocument): UsageLedgerDocument;
|
|
588
638
|
/**
|
|
589
639
|
* 折叠算法版本:归账语义变化时递增。v1 = 按 request/header 归账(稀疏 header 把
|
|
590
640
|
* 两次 header 之间的用量串到上一个模型,订阅模型首当其冲,issue #14);v2 =
|
|
@@ -599,7 +649,7 @@ export interface UsageLedgerDocument {
|
|
|
599
649
|
* 会话费用只剩最近一段,issue #29)。
|
|
600
650
|
* 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
|
|
601
651
|
*/
|
|
602
|
-
export declare const FOLD_VERSION =
|
|
652
|
+
export declare const FOLD_VERSION = 23;
|
|
603
653
|
/**
|
|
604
654
|
* 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
|
|
605
655
|
* 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
|
|
@@ -695,7 +745,7 @@ export interface SessionUsageSummary {
|
|
|
695
745
|
* 订阅豁免),历史账本行立即失效并全量重折,配置变更即时生效(issue #37)。
|
|
696
746
|
* 判定函数无法序列化,统一记为 `fn`(任何函数形态互视为同一指纹)。
|
|
697
747
|
*/
|
|
698
|
-
export declare function configFingerprint(subscriptionMatcher: SubscriptionMatcher, officialProviderIds: ReadonlySet<string> | undefined, routeAliases: Readonly<Record<string, string>>, searchEstimate: number, modelKeyAliases?: Readonly<Record<string, string>>, registeredProviders?: ReadonlySet<string
|
|
748
|
+
export declare function configFingerprint(subscriptionMatcher: SubscriptionMatcher, officialProviderIds: ReadonlySet<string> | undefined, routeAliases: Readonly<Record<string, string>>, searchEstimate: number, modelKeyAliases?: Readonly<Record<string, string>>, registeredProviders?: ReadonlySet<string>, routeOrigins?: Readonly<Record<string, string>>): string;
|
|
699
749
|
/**
|
|
700
750
|
* Create the incremental usage aggregator.
|
|
701
751
|
* @param persistence - the session persistence service.
|
|
@@ -178,6 +178,15 @@ interface SessionBillingRow {
|
|
|
178
178
|
id: string;
|
|
179
179
|
title?: string;
|
|
180
180
|
cwd?: string;
|
|
181
|
+
/**
|
|
182
|
+
* 直接父会话 id(仅子智能体会话携带):宿主把子智能体落成独立会话,
|
|
183
|
+
* header 的 origin: subagent + parentSession 指向委派它的对话。
|
|
184
|
+
*/
|
|
185
|
+
parentId?: string;
|
|
186
|
+
/** 全部后代子会话费用合计(服务端递归算好);无后代时缺失。 */
|
|
187
|
+
subCost?: number;
|
|
188
|
+
/** 全部后代子会话调用数;无后代时缺失。 */
|
|
189
|
+
subCalls?: number;
|
|
181
190
|
/** 数据来自旧算法折叠的持久账本行(原始日志已删,无法重算)。 */
|
|
182
191
|
stale?: boolean;
|
|
183
192
|
calls: number;
|
|
@@ -187,6 +196,8 @@ interface SessionBillingRow {
|
|
|
187
196
|
mergedCount?: number;
|
|
188
197
|
/** 合并组的段明细(展示层专用):一级行默认收起,展开后逐段显示。合并函数需 push,故非 readonly。 */
|
|
189
198
|
children?: SessionBillingRow[];
|
|
199
|
+
/** 挂到本组下的子智能体行(展示层专用):与段明细并列展开,带子智能体徽标。 */
|
|
200
|
+
subs?: SessionBillingRow[];
|
|
190
201
|
}
|
|
191
202
|
/** Usage stats structure from `.dsh-usage-stats.json`. */
|
|
192
203
|
export interface UsageStats {
|