@kenz1117/dsh-ui-usage-billing 1.2.6 → 1.2.8

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { mkdir, readFile, stat } from "node:fs/promises";
2
+ import { lstat, mkdir, readFile, rm, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
@@ -305,7 +305,7 @@ function tierAt(timeMs) {
305
305
  }
306
306
  /** 时刻是否落在北京时间周末(周六/周日)。 */
307
307
  function isBeijingWeekend(timeMs) {
308
- const day = new Date(timeMs + 288e5).getUTCDay();
308
+ const day = new Date(timeMs + 8 * 36e5).getUTCDay();
309
309
  return day === 0 || day === 6;
310
310
  }
311
311
  /**
@@ -331,7 +331,7 @@ const MODEL_CATALOG = [
331
331
  key: "flash",
332
332
  name: "DeepSeek V4.1 Flash",
333
333
  provider: "DeepSeek",
334
- colorVar: "dsw-static-blue-500",
334
+ colorVar: "ds-blue",
335
335
  price: {
336
336
  currency: "CNY",
337
337
  input: 2,
@@ -349,7 +349,7 @@ const MODEL_CATALOG = [
349
349
  key: "flash-vision-exp",
350
350
  name: "DeepSeek V4 Flash Vision (Exp)",
351
351
  provider: "DeepSeek",
352
- colorVar: "dsw-static-blue-500",
352
+ colorVar: "ds-blue",
353
353
  retired: true,
354
354
  price: {
355
355
  currency: "CNY",
@@ -398,7 +398,7 @@ const MODEL_CATALOG = [
398
398
  key: "glm-5.3",
399
399
  name: "GLM-5.3",
400
400
  provider: "智谱 AI",
401
- colorVar: "dsw-static-blue-500",
401
+ colorVar: "ds-blue",
402
402
  price: {
403
403
  currency: "CNY",
404
404
  input: 8,
@@ -464,7 +464,7 @@ const MODEL_CATALOG = [
464
464
  key: "glm-5-turbo",
465
465
  name: "GLM-5-Turbo",
466
466
  provider: "智谱 AI",
467
- colorVar: "dsw-static-blue-500",
467
+ colorVar: "ds-blue",
468
468
  price: {
469
469
  currency: "CNY",
470
470
  input: 5,
@@ -606,7 +606,7 @@ const MODEL_CATALOG = [
606
606
  key: "qwen-plus",
607
607
  name: "Qwen3.5-Plus",
608
608
  provider: "阿里通义",
609
- colorVar: "dsw-static-blue-500",
609
+ colorVar: "ds-blue",
610
610
  price: {
611
611
  currency: "CNY",
612
612
  input: .8,
@@ -659,7 +659,7 @@ const MODEL_CATALOG = [
659
659
  key: "doubao",
660
660
  name: "Doubao Seed-2.0 Pro",
661
661
  provider: "字节豆包",
662
- colorVar: "dsw-static-red-500",
662
+ colorVar: "ds-red",
663
663
  price: {
664
664
  currency: "CNY",
665
665
  input: 3.2,
@@ -695,7 +695,7 @@ const MODEL_CATALOG = [
695
695
  key: "doubao-seed-evolving",
696
696
  name: "Doubao-Seed-Evolving",
697
697
  provider: "字节豆包",
698
- colorVar: "dsw-static-red-500",
698
+ colorVar: "ds-red",
699
699
  price: {
700
700
  currency: "CNY",
701
701
  input: 6,
@@ -803,7 +803,7 @@ const MODEL_CATALOG = [
803
803
  key: "minimax",
804
804
  name: "MiniMax-M3",
805
805
  provider: "MiniMax",
806
- colorVar: "dsw-static-amber-500",
806
+ colorVar: "ds-amber",
807
807
  price: {
808
808
  currency: "CNY",
809
809
  input: 2.1,
@@ -827,7 +827,7 @@ const MODEL_CATALOG = [
827
827
  key: "minimax-m2.7-highspeed",
828
828
  name: "MiniMax-M2.7-highspeed",
829
829
  provider: "MiniMax",
830
- colorVar: "dsw-static-amber-500",
830
+ colorVar: "ds-amber",
831
831
  price: {
832
832
  currency: "CNY",
833
833
  input: 4.2,
@@ -875,7 +875,7 @@ const MODEL_CATALOG = [
875
875
  key: "yi",
876
876
  name: "Yi-Lightning",
877
877
  provider: "零一万物",
878
- colorVar: "dsw-static-green-500",
878
+ colorVar: "ds-green",
879
879
  price: {
880
880
  currency: "CNY",
881
881
  input: .99,
@@ -937,7 +937,7 @@ const MODEL_CATALOG = [
937
937
  key: "gpt-6-astra",
938
938
  name: "GPT-6 Astra",
939
939
  provider: "OpenAI",
940
- colorVar: "dsw-static-green-500",
940
+ colorVar: "ds-green",
941
941
  price: {
942
942
  currency: "USD",
943
943
  input: 10,
@@ -949,7 +949,7 @@ const MODEL_CATALOG = [
949
949
  key: "gpt-5.6-sol",
950
950
  name: "GPT-5.6 Sol",
951
951
  provider: "OpenAI",
952
- colorVar: "dsw-static-green-500",
952
+ colorVar: "ds-green",
953
953
  price: {
954
954
  currency: "USD",
955
955
  input: 5,
@@ -979,7 +979,7 @@ const MODEL_CATALOG = [
979
979
  key: "gpt-5.6-luna",
980
980
  name: "GPT-5.6 Luna",
981
981
  provider: "OpenAI",
982
- colorVar: "dsw-static-green-500",
982
+ colorVar: "ds-green",
983
983
  price: {
984
984
  currency: "USD",
985
985
  input: .2,
@@ -1053,7 +1053,7 @@ const MODEL_CATALOG = [
1053
1053
  key: "llama",
1054
1054
  name: "Llama 4 Maverick",
1055
1055
  provider: "Meta",
1056
- colorVar: "dsw-static-red-500",
1056
+ colorVar: "ds-red",
1057
1057
  price: {
1058
1058
  currency: "USD",
1059
1059
  input: .2,
@@ -1077,7 +1077,7 @@ const MODEL_CATALOG = [
1077
1077
  key: "claude-opus-4-6",
1078
1078
  name: "Claude Opus 4.6",
1079
1079
  provider: "Anthropic",
1080
- colorVar: "dsw-static-red-500",
1080
+ colorVar: "ds-red",
1081
1081
  price: {
1082
1082
  currency: "USD",
1083
1083
  input: 5,
@@ -1113,7 +1113,7 @@ const MODEL_CATALOG = [
1113
1113
  key: "claude-opus-5",
1114
1114
  name: "Claude Opus 5",
1115
1115
  provider: "Anthropic",
1116
- colorVar: "dsw-static-red-500",
1116
+ colorVar: "ds-red",
1117
1117
  price: {
1118
1118
  currency: "USD",
1119
1119
  input: 5,
@@ -1137,7 +1137,7 @@ const MODEL_CATALOG = [
1137
1137
  key: "mistral-large-2512",
1138
1138
  name: "Mistral Large 3",
1139
1139
  provider: "Mistral AI",
1140
- colorVar: "dsw-static-violet-500",
1140
+ colorVar: "ds-violet",
1141
1141
  price: {
1142
1142
  currency: "USD",
1143
1143
  input: .5,
@@ -1173,7 +1173,7 @@ const MODEL_CATALOG = [
1173
1173
  key: "command-a-03-2025",
1174
1174
  name: "Command A",
1175
1175
  provider: "Cohere",
1176
- colorVar: "dsw-static-cyan-500",
1176
+ colorVar: "ds-cyan",
1177
1177
  price: {
1178
1178
  currency: "USD",
1179
1179
  input: 2.5,
@@ -1197,7 +1197,7 @@ const MODEL_CATALOG = [
1197
1197
  key: "longcat-2.0",
1198
1198
  name: "LongCat 2.0",
1199
1199
  provider: "美团",
1200
- colorVar: "dsw-static-amber-500",
1200
+ colorVar: "ds-amber",
1201
1201
  price: {
1202
1202
  currency: "CNY",
1203
1203
  input: 4,
@@ -1210,7 +1210,7 @@ const MODEL_CATALOG = [
1210
1210
  key: "minicpm-v-4.5",
1211
1211
  name: "MiniCPM-V 4.5",
1212
1212
  provider: "面壁智能",
1213
- colorVar: "dsw-static-green-500",
1213
+ colorVar: "ds-green",
1214
1214
  price: {
1215
1215
  currency: "CNY",
1216
1216
  input: 1,
@@ -1236,7 +1236,7 @@ const MODEL_CATALOG = [
1236
1236
  key: "dots-3-note-preview",
1237
1237
  name: "Dots3-Note Preview",
1238
1238
  provider: "小红书",
1239
- colorVar: "dsw-static-red-500",
1239
+ colorVar: "ds-red",
1240
1240
  price: {
1241
1241
  currency: "CNY",
1242
1242
  input: 2,
@@ -1403,7 +1403,7 @@ const MODEL_CATALOG = [
1403
1403
  key: "doubao-seed-2.0-code",
1404
1404
  name: "Doubao Seed-2.0 Code",
1405
1405
  provider: "字节豆包",
1406
- colorVar: "dsw-static-red-500",
1406
+ colorVar: "ds-red",
1407
1407
  price: {
1408
1408
  currency: "CNY",
1409
1409
  input: 3.2,
@@ -2047,9 +2047,151 @@ function workspaceNameOf(cwd) {
2047
2047
  }
2048
2048
  /** TTFT 尖峰阈值(毫秒):超过计为一次尖峰样本,用于定位服务端抖动。 */
2049
2049
  const PERF_SPIKE_MS = 1e4;
2050
+ /** 全零摘要(合并的恒等元)。 */
2051
+ const EMPTY_MODEL_DIGEST = {
2052
+ samples: 0,
2053
+ ttftSum: 0,
2054
+ ttftMax: 0,
2055
+ ttftSpikes: 0,
2056
+ ttftP50: 0,
2057
+ ttftP90: 0,
2058
+ tpsCount: 0,
2059
+ tpsSum: 0,
2060
+ latencyCount: 0,
2061
+ latencySum: 0,
2062
+ estimated: 0
2063
+ };
2064
+ /** 从样本集构建模型级摘要(样本侧精确:P50/P90 为精确插值)。 */
2065
+ function modelDigestOf(samples) {
2066
+ const ttfts = [];
2067
+ let ttftSum = 0;
2068
+ let ttftMax = 0;
2069
+ let ttftSpikes = 0;
2070
+ let tpsCount = 0;
2071
+ let tpsSum = 0;
2072
+ let latencyCount = 0;
2073
+ let latencySum = 0;
2074
+ let estimated = 0;
2075
+ for (const sample of samples) {
2076
+ ttfts.push(sample.ttftMs);
2077
+ ttftSum += sample.ttftMs;
2078
+ if (sample.ttftMs > ttftMax) ttftMax = sample.ttftMs;
2079
+ if (sample.ttftMs > 1e4) ttftSpikes += 1;
2080
+ if (sample.tps !== void 0) {
2081
+ tpsCount += 1;
2082
+ tpsSum += sample.tps;
2083
+ }
2084
+ if (sample.latencyMs !== void 0) {
2085
+ latencyCount += 1;
2086
+ latencySum += sample.latencyMs;
2087
+ }
2088
+ if (sample.estimated) estimated += 1;
2089
+ }
2090
+ return {
2091
+ samples: samples.length,
2092
+ ttftSum,
2093
+ ttftMax,
2094
+ ttftSpikes,
2095
+ ttftP50: percentile(ttfts, .5),
2096
+ ttftP90: percentile(ttfts, .9),
2097
+ tpsCount,
2098
+ tpsSum,
2099
+ latencyCount,
2100
+ latencySum,
2101
+ estimated
2102
+ };
2103
+ }
2104
+ /** 合并两个模型级摘要:计数/求和/极值精确;分位数按样本数加权。 */
2105
+ function mergeModelDigest(a, b) {
2106
+ const n = a.samples + b.samples;
2107
+ const weighted = (x, y) => n === 0 ? 0 : (x * a.samples + y * b.samples) / n;
2108
+ return {
2109
+ samples: n,
2110
+ ttftSum: a.ttftSum + b.ttftSum,
2111
+ ttftMax: Math.max(a.ttftMax, b.ttftMax),
2112
+ ttftSpikes: a.ttftSpikes + b.ttftSpikes,
2113
+ ttftP50: weighted(a.ttftP50, b.ttftP50),
2114
+ ttftP90: weighted(a.ttftP90, b.ttftP90),
2115
+ tpsCount: a.tpsCount + b.tpsCount,
2116
+ tpsSum: a.tpsSum + b.tpsSum,
2117
+ latencyCount: a.latencyCount + b.latencyCount,
2118
+ latencySum: a.latencySum + b.latencySum,
2119
+ estimated: a.estimated + b.estimated
2120
+ };
2121
+ }
2122
+ /** 从样本集构建小时级摘要(均值可无损重建)。 */
2123
+ function hourDigestOf(samples) {
2124
+ let ttftSum = 0;
2125
+ let tpsCount = 0;
2126
+ let tpsSum = 0;
2127
+ for (const sample of samples) {
2128
+ ttftSum += sample.ttftMs;
2129
+ if (sample.tps !== void 0) {
2130
+ tpsCount += 1;
2131
+ tpsSum += sample.tps;
2132
+ }
2133
+ }
2134
+ return {
2135
+ samples: samples.length,
2136
+ ttftSum,
2137
+ tpsCount,
2138
+ tpsSum
2139
+ };
2140
+ }
2141
+ /** 全零小时摘要(合并的恒等元)。 */
2142
+ const EMPTY_HOUR_DIGEST = {
2143
+ samples: 0,
2144
+ ttftSum: 0,
2145
+ tpsCount: 0,
2146
+ tpsSum: 0
2147
+ };
2148
+ /** 合并两个小时级摘要(纯字段加法,无损)。 */
2149
+ function mergeHourDigest(a, b) {
2150
+ return {
2151
+ samples: a.samples + b.samples,
2152
+ ttftSum: a.ttftSum + b.ttftSum,
2153
+ tpsCount: a.tpsCount + b.tpsCount,
2154
+ tpsSum: a.tpsSum + b.tpsSum
2155
+ };
2156
+ }
2157
+ /**
2158
+ * 样本集 → 账本摘要形态(模型 → 摘要;小时 → 模型 → 摘要)。serializeFold
2159
+ * 与账本迁移共用同一转换,两处口径不会漂移。
2160
+ * @param samples - 一个会话的全部性能样本。
2161
+ * @returns `{ byModel, byHourModel }` 的 JSON-safe 记录。
2162
+ */
2163
+ function perfDigestsOf(samples) {
2164
+ const byModelSamples = /* @__PURE__ */ new Map();
2165
+ const byHourModelSamples = /* @__PURE__ */ new Map();
2166
+ for (const sample of samples) {
2167
+ let modelSamples = byModelSamples.get(sample.model);
2168
+ if (modelSamples === void 0) {
2169
+ modelSamples = [];
2170
+ byModelSamples.set(sample.model, modelSamples);
2171
+ }
2172
+ modelSamples.push(sample);
2173
+ let hourModels = byHourModelSamples.get(sample.hour);
2174
+ if (hourModels === void 0) {
2175
+ hourModels = /* @__PURE__ */ new Map();
2176
+ byHourModelSamples.set(sample.hour, hourModels);
2177
+ }
2178
+ let hourModelSamples = hourModels.get(sample.model);
2179
+ if (hourModelSamples === void 0) {
2180
+ hourModelSamples = [];
2181
+ hourModels.set(sample.model, hourModelSamples);
2182
+ }
2183
+ hourModelSamples.push(sample);
2184
+ }
2185
+ return {
2186
+ byModel: Object.fromEntries([...byModelSamples].map(([model, list]) => [model, modelDigestOf(list)])),
2187
+ byHourModel: Object.fromEntries([...byHourModelSamples].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, list]) => [model, hourDigestOf(list)]))]))
2188
+ };
2189
+ }
2050
2190
  /**
2051
2191
  * 账本迁移注册表。首条迁移给 1.0.6 及更早的行回填 foldVersion = 1(它们全部出自
2052
- * header 归因算法);此后新写入的行总带当前 {@link FOLD_VERSION}。
2192
+ * header 归因算法);perf 摘要化迁移把 v15 及更早行的逐样本 `perf` 数组转换为
2193
+ * `perfDigest`/`perfHourDigest` 摘要(日志已删的行不再被重折,迁移是它们唯一的
2194
+ * 收缩机会);此后新写入的行总带当前 {@link FOLD_VERSION} 与摘要形态。
2053
2195
  */
2054
2196
  const LEDGER_MIGRATIONS = [{
2055
2197
  id: "fold-version-backfill",
@@ -2061,6 +2203,26 @@ const LEDGER_MIGRATIONS = [{
2061
2203
  }
2062
2204
  return changed;
2063
2205
  }
2206
+ }, {
2207
+ id: "perf-sample-to-digest",
2208
+ apply(document) {
2209
+ let changed = false;
2210
+ for (const session of document.sessions) {
2211
+ const fold = session.fold;
2212
+ if (Array.isArray(fold.perf)) {
2213
+ const { byModel, byHourModel } = perfDigestsOf(fold.perf);
2214
+ fold.perfDigest = byModel;
2215
+ fold.perfHourDigest = byHourModel;
2216
+ delete fold.perf;
2217
+ changed = true;
2218
+ } else if (fold.perfDigest === void 0 || fold.perfHourDigest === void 0) {
2219
+ fold.perfDigest ??= {};
2220
+ fold.perfHourDigest ??= {};
2221
+ changed = true;
2222
+ }
2223
+ }
2224
+ return changed;
2225
+ }
2064
2226
  }];
2065
2227
  /**
2066
2228
  * 在加载边界对账本文档应用未执行的迁移,并记录已应用 id 供写回。
@@ -2084,6 +2246,7 @@ function runLedgerMigrations(document, migrations = LEDGER_MIGRATIONS) {
2084
2246
  }
2085
2247
  /** Serialize Map/Set-heavy fold state into a JSON-safe ledger entry. */
2086
2248
  function serializeFold(fold) {
2249
+ const { byModel: perfDigest, byHourModel: perfHourDigest } = perfDigestsOf(fold.perf);
2087
2250
  return {
2088
2251
  total: fold.total,
2089
2252
  byModel: Object.fromEntries(fold.byModel),
@@ -2096,7 +2259,8 @@ function serializeFold(fold) {
2096
2259
  unpricedModels: [...fold.unpricedModels],
2097
2260
  planCalls: Object.fromEntries(fold.planCalls),
2098
2261
  turns: fold.turns,
2099
- perf: fold.perf,
2262
+ perfDigest,
2263
+ perfHourDigest,
2100
2264
  roles: fold.roles,
2101
2265
  lastActive: fold.lastActive,
2102
2266
  ...fold.title !== void 0 ? { title: fold.title } : {}
@@ -2116,7 +2280,9 @@ function deserializeFold(fold) {
2116
2280
  unpricedModels: new Set(fold.unpricedModels),
2117
2281
  planCalls: new Map(Object.entries(fold.planCalls)),
2118
2282
  turns: fold.turns,
2119
- perf: fold.perf,
2283
+ perf: [],
2284
+ perfDigest: new Map(Object.entries(fold.perfDigest)),
2285
+ perfHourDigest: new Map(Object.entries(fold.perfHourDigest).map(([hour, models]) => [hour, new Map(Object.entries(models))])),
2120
2286
  roles: fold.roles,
2121
2287
  lastActive: fold.lastActive,
2122
2288
  ...fold.title !== void 0 ? { title: fold.title } : {}
@@ -2132,7 +2298,7 @@ function ledgerSessionsOf(value) {
2132
2298
  const row = entry;
2133
2299
  if (typeof row.id !== "string" || row.id === "" || row.fold === null || typeof row.fold !== "object") return false;
2134
2300
  const fold = row.fold;
2135
- return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) && Array.isArray(fold.perf) && fold.roles !== void 0 && typeof fold.lastActive === "number";
2301
+ return fold.total !== void 0 && fold.byModel !== void 0 && fold.byDay !== void 0 && fold.byDayModels !== void 0 && fold.bySite !== void 0 && Array.isArray(fold.unpricedModels) && fold.planCalls !== void 0 && Array.isArray(fold.turns) && fold.perfDigest !== void 0 && fold.perfHourDigest !== void 0 && fold.roles !== void 0 && typeof fold.lastActive === "number";
2136
2302
  });
2137
2303
  }
2138
2304
  /**
@@ -2227,6 +2393,8 @@ function freshFold() {
2227
2393
  planCalls: /* @__PURE__ */ new Map(),
2228
2394
  turns: [],
2229
2395
  perf: [],
2396
+ perfDigest: /* @__PURE__ */ new Map(),
2397
+ perfHourDigest: /* @__PURE__ */ new Map(),
2230
2398
  roles: {
2231
2399
  userChars: 0,
2232
2400
  toolChars: 0,
@@ -2468,11 +2636,6 @@ function mergeUsageInto(acc, cell) {
2468
2636
  acc.officialCost += cell.officialCost;
2469
2637
  if (cell.searchCalls !== void 0) acc.searchCalls = (acc.searchCalls ?? 0) + cell.searchCalls;
2470
2638
  }
2471
- /** 均值(数组非空时调用;空数组按 0 兜底)。 */
2472
- function mean(values) {
2473
- if (values.length === 0) return 0;
2474
- return values.reduce((sum, value) => sum + value, 0) / values.length;
2475
- }
2476
2639
  /** 分位数(0..1):先拷贝排序,再线性插值;空数组返回 0。 */
2477
2640
  function percentile(values, p) {
2478
2641
  if (values.length === 0) return 0;
@@ -2489,6 +2652,53 @@ function percentile(values, p) {
2489
2652
  if (a === void 0 || b === void 0) return 0;
2490
2653
  return a + (b - a) * (idx - lo);
2491
2654
  }
2655
+ /** 活样本累加器 → 模型级摘要(跨会话合并前先归一到摘要形态)。 */
2656
+ function accumToModelDigest(acc) {
2657
+ return {
2658
+ samples: acc.ttfts.length,
2659
+ ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
2660
+ ttftMax: acc.ttfts.length === 0 ? 0 : Math.max(...acc.ttfts),
2661
+ ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2662
+ ttftP50: percentile(acc.ttfts, .5),
2663
+ ttftP90: percentile(acc.ttfts, .9),
2664
+ tpsCount: acc.tps.length,
2665
+ tpsSum: acc.tps.reduce((sum, value) => sum + value, 0),
2666
+ latencyCount: acc.latencies.length,
2667
+ latencySum: acc.latencies.reduce((sum, value) => sum + value, 0),
2668
+ estimated: acc.estimated
2669
+ };
2670
+ }
2671
+ /** 活样本小时累加器 → 小时级摘要。 */
2672
+ function accumToHourDigest(acc) {
2673
+ return {
2674
+ samples: acc.ttfts.length,
2675
+ ttftSum: acc.ttfts.reduce((sum, value) => sum + value, 0),
2676
+ tpsCount: acc.tps.length,
2677
+ tpsSum: acc.tps.reduce((sum, value) => sum + value, 0)
2678
+ };
2679
+ }
2680
+ /** 模型级摘要 → 面板行(均值由求和/计数派生)。 */
2681
+ function modelPerfOf(digest) {
2682
+ return {
2683
+ samples: digest.samples,
2684
+ ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
2685
+ ttftP50: digest.ttftP50,
2686
+ ttftP90: digest.ttftP90,
2687
+ ttftMax: digest.ttftMax,
2688
+ ttftSpikes: digest.ttftSpikes,
2689
+ ...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount },
2690
+ latencyAvg: digest.latencyCount === 0 ? 0 : digest.latencySum / digest.latencyCount,
2691
+ estimatedSamples: digest.estimated
2692
+ };
2693
+ }
2694
+ /** 小时级摘要 → 面板行(均值由求和/计数派生)。 */
2695
+ function hourPerfOf(digest) {
2696
+ return {
2697
+ samples: digest.samples,
2698
+ ttftAvg: digest.samples === 0 ? 0 : digest.ttftSum / digest.samples,
2699
+ ...digest.tpsCount === 0 ? {} : { tpsAvg: digest.tpsSum / digest.tpsCount }
2700
+ };
2701
+ }
2492
2702
  /**
2493
2703
  * 聚合配置指纹:影响折叠语义的全部配置(订阅豁免、官方名单、路由别名、搜索估值)
2494
2704
  * 的稳定序列化。账本行的复用判定携带该指纹——用户改配置后(如为 grok build 加
@@ -2536,7 +2746,7 @@ function createUsageAggregator(persistence, options = {}) {
2536
2746
  id,
2537
2747
  ...cwd === void 0 ? {} : { cwd },
2538
2748
  ...stamp === null ? {} : { stamp },
2539
- foldVersion: 15,
2749
+ foldVersion: 16,
2540
2750
  fingerprint: foldFingerprint,
2541
2751
  fold: serializeFold(fold)
2542
2752
  };
@@ -2633,7 +2843,7 @@ function createUsageAggregator(persistence, options = {}) {
2633
2843
  try {
2634
2844
  const cwd = meta.cwd;
2635
2845
  const ledgerRow = ledger.get(id);
2636
- if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 15 && ledgerRow.fingerprint === foldFingerprint) {
2846
+ if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 16 && ledgerRow.fingerprint === foldFingerprint) {
2637
2847
  const fold = deserializeFold(ledgerRow.fold);
2638
2848
  folds.push({
2639
2849
  id,
@@ -2715,7 +2925,7 @@ function createUsageAggregator(persistence, options = {}) {
2715
2925
  for (const entry of ledger.values()) {
2716
2926
  if (included.has(entry.id)) continue;
2717
2927
  try {
2718
- const stale = (entry.foldVersion ?? 1) < 15 || entry.fingerprint !== foldFingerprint;
2928
+ const stale = (entry.foldVersion ?? 1) < 16 || entry.fingerprint !== foldFingerprint;
2719
2929
  folds.push({
2720
2930
  id: entry.id,
2721
2931
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -2752,6 +2962,8 @@ function createUsageAggregator(persistence, options = {}) {
2752
2962
  };
2753
2963
  const perfModel = /* @__PURE__ */ new Map();
2754
2964
  const perfHourModel = /* @__PURE__ */ new Map();
2965
+ const perfModelDigest = /* @__PURE__ */ new Map();
2966
+ const perfHourDigest = /* @__PURE__ */ new Map();
2755
2967
  for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
2756
2968
  mergeUsageInto(total, fold.total);
2757
2969
  roles.userChars += fold.roles.userChars;
@@ -2798,6 +3010,15 @@ function createUsageAggregator(persistence, options = {}) {
2798
3010
  hourAccum.ttfts.push(sample.ttftMs);
2799
3011
  if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
2800
3012
  }
3013
+ for (const [model, digest] of fold.perfDigest) perfModelDigest.set(model, mergeModelDigest(perfModelDigest.get(model) ?? EMPTY_MODEL_DIGEST, digest));
3014
+ for (const [hour, models] of fold.perfHourDigest) {
3015
+ let hourModels = perfHourDigest.get(hour);
3016
+ if (hourModels === void 0) {
3017
+ hourModels = /* @__PURE__ */ new Map();
3018
+ perfHourDigest.set(hour, hourModels);
3019
+ }
3020
+ for (const [model, digest] of models) hourModels.set(model, mergeHourDigest(hourModels.get(model) ?? EMPTY_HOUR_DIGEST, digest));
3021
+ }
2801
3022
  for (const row of fold.turns) turnRows.push({
2802
3023
  sessionId,
2803
3024
  ...row
@@ -2841,23 +3062,26 @@ function createUsageAggregator(persistence, options = {}) {
2841
3062
  };
2842
3063
  const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
2843
3064
  const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
2844
- const perf = perfModel.size === 0 ? void 0 : {
2845
- byModel: Object.fromEntries([...perfModel].map(([model, acc]) => [model, {
2846
- samples: acc.ttfts.length,
2847
- ttftAvg: mean(acc.ttfts),
2848
- ttftP50: percentile(acc.ttfts, .5),
2849
- ttftP90: percentile(acc.ttfts, .9),
2850
- ttftMax: Math.max(...acc.ttfts),
2851
- ttftSpikes: acc.ttfts.filter((ttft) => ttft > PERF_SPIKE_MS).length,
2852
- ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) },
2853
- latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2854
- estimatedSamples: acc.estimated
2855
- }])),
2856
- byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
2857
- samples: acc.ttfts.length,
2858
- ttftAvg: mean(acc.ttfts),
2859
- ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
2860
- }]))]))
3065
+ const modelKeys = new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3066
+ const hourKeys = new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3067
+ const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
3068
+ byModel: Object.fromEntries([...modelKeys].map((model) => {
3069
+ const live = perfModel.get(model);
3070
+ const restored = perfModelDigest.get(model);
3071
+ const digest = live === void 0 ? restored : mergeModelDigest(accumToModelDigest(live), restored ?? EMPTY_MODEL_DIGEST);
3072
+ return digest === void 0 ? null : [model, modelPerfOf(digest)];
3073
+ }).filter((entry) => entry !== null)),
3074
+ byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
3075
+ const liveModels = perfHourModel.get(hour);
3076
+ const digestModels = perfHourDigest.get(hour);
3077
+ const modelKeysInHour = new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3078
+ return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
3079
+ const live = liveModels?.get(model);
3080
+ const restored = digestModels?.get(model);
3081
+ const digest = live === void 0 ? restored : mergeHourDigest(accumToHourDigest(live), restored ?? EMPTY_HOUR_DIGEST);
3082
+ return digest === void 0 ? null : [model, hourPerfOf(digest)];
3083
+ }).filter((entry) => entry !== null))];
3084
+ }))
2861
3085
  };
2862
3086
  lastDoc = {
2863
3087
  version: 4,
@@ -2903,6 +3127,29 @@ function createUsageAggregator(persistence, options = {}) {
2903
3127
  });
2904
3128
  return inflight;
2905
3129
  },
3130
+ async sessionUsageOf(sessionId) {
3131
+ await this.aggregate();
3132
+ const hit = cache.get(sessionId);
3133
+ if (hit !== void 0) return {
3134
+ calls: hit.fold.total.calls,
3135
+ cost: hit.fold.total.cost,
3136
+ input: hit.fold.total.input,
3137
+ output: hit.fold.total.output
3138
+ };
3139
+ const row = ledger.get(sessionId);
3140
+ if (row === void 0) return void 0;
3141
+ try {
3142
+ const fold = deserializeFold(row.fold);
3143
+ return {
3144
+ calls: fold.total.calls,
3145
+ cost: fold.total.cost,
3146
+ input: fold.total.input,
3147
+ output: fold.total.output
3148
+ };
3149
+ } catch {
3150
+ return;
3151
+ }
3152
+ },
2906
3153
  async flush() {
2907
3154
  if (inflight !== void 0) await inflight.catch(() => {});
2908
3155
  await saveLedger(true);
@@ -3030,11 +3277,7 @@ function tc3Authorization(secretId, secretKey, payload, timestamp) {
3030
3277
  const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
3031
3278
  const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3032
3279
  const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3033
- const kDate = createHmac("sha256", date).update(secretKey).digest();
3034
- const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
3035
- const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
3036
- const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3037
- return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
3280
+ return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
3038
3281
  }
3039
3282
  /**
3040
3283
  * 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
@@ -3798,7 +4041,7 @@ const declaredGate = createCooldownGate({
3798
4041
  cooldownMs: 6e4
3799
4042
  });
3800
4043
  /** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
3801
- const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
4044
+ const FORBIDDEN_SEGMENTS = new Set([
3802
4045
  "__proto__",
3803
4046
  "constructor",
3804
4047
  "prototype"
@@ -4348,7 +4591,7 @@ function asPrice(value) {
4348
4591
  */
4349
4592
  function buildExtraModels(data) {
4350
4593
  if (data === null || typeof data !== "object") return [];
4351
- const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4594
+ const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4352
4595
  const extras = [];
4353
4596
  for (const [providerId, providerDoc] of Object.entries(data)) {
4354
4597
  if (providerDoc === null || typeof providerDoc !== "object") continue;
@@ -5068,7 +5311,7 @@ const relayGate = createCooldownGate({
5068
5311
  /** Abort a relay fetch when the upstream hangs beyond this budget. */
5069
5312
  const FETCH_TIMEOUT_MS = 8e3;
5070
5313
  /** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
5071
- const FINGERPRINT_TTL_MS = 3e5;
5314
+ const FINGERPRINT_TTL_MS = 300 * 1e3;
5072
5315
  /** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
5073
5316
  const fingerprintCache = /* @__PURE__ */ new Map();
5074
5317
  /** Number, or null when the value is not a finite number (nor numeric string). */
@@ -5204,7 +5447,7 @@ function originOf(baseURL) {
5204
5447
  * 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
5205
5448
  * 「未识别」。中转站面板只列真正的第三方中转程序。
5206
5449
  */
5207
- const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
5450
+ const OFFICIAL_HOSTS = new Set([
5208
5451
  "api.deepseek.com",
5209
5452
  "api.openai.com",
5210
5453
  "open.bigmodel.cn",
@@ -5457,10 +5700,10 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
5457
5700
  /** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
5458
5701
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
5459
5702
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
5460
- const PRICING_REFRESH_INTERVAL_MS = 216e5;
5703
+ const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
5461
5704
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
5462
- const SUBSCRIPTION_CACHE_MS = 3e5;
5463
- const BALANCE_CACHE_MS = 3e5;
5705
+ const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
5706
+ const BALANCE_CACHE_MS = 300 * 1e3;
5464
5707
  /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
5465
5708
  const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5466
5709
  /**
@@ -5468,14 +5711,16 @@ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5468
5711
  * 下发,供「设置 → 插件信息卡」展示。发布版 lib/index.js 相对包根解析。
5469
5712
  */
5470
5713
  const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
5471
- /** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
5472
- const SNAPSHOT_INTERVAL_MS = 3e4;
5714
+ /** 统计快照的落盘节流(毫秒):快照只服务聚合失败回退与重启首屏(展示级
5715
+ * 新鲜度即可),降到 5 分钟一次——原子写含上一版备份是两次全量 JSON 落盘,
5716
+ * 重度用户快照上百 KB,跟随前端轮询 30 秒写一次纯属写放大。 */
5717
+ const SNAPSHOT_INTERVAL_MS = 3e5;
5473
5718
  /** usage-stats 响应的等待预算(毫秒):聚合在此预算内完成就回实时数据,
5474
5719
  * 超预算立即回最近快照(stale-while-revalidate),后台折叠继续跑。
5475
5720
  * 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
5476
5721
  const RESPONSE_BUDGET_MS = 1500;
5477
5722
  /** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
5478
- const AUTH_WARN_COOLDOWN_MS = 18e5;
5723
+ const AUTH_WARN_COOLDOWN_MS = 1800 * 1e3;
5479
5724
  /**
5480
5725
  * 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
5481
5726
  * `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
@@ -5489,6 +5734,32 @@ function warnAuthOnce(source, provider, displayName) {
5489
5734
  authWarnedAt.set(key, now);
5490
5735
  console.warn(`[usage-billing] ${displayName}(${provider})鉴权失败:请检查 llm-pi-ai 设置中该 provider 的凭据环境变量是否正确/有效。`);
5491
5736
  }
5737
+ /** 锁等待上限:账本提交是毫秒级 read-render-commit,2s 已数倍于正常持锁时长。 */
5738
+ const LEDGER_LOCK_WAIT_MS = 2e3;
5739
+ /**
5740
+ * 锁年龄超过该值判为崩溃残留的孤儿锁。正常持锁是毫秒级文件提交,没有活跃
5741
+ * writer 会合法持有 60s;库层刻意不做年龄判定(它无法假设调用方的操作时长),
5742
+ * 本调用方知道自己操作的上界,可以给出有依据的清理阈值。
5743
+ */
5744
+ const STALE_LEDGER_LOCK_MS = 6e4;
5745
+ /**
5746
+ * 锁等待超时后的孤儿锁自愈(issue #48):仅当锁文件年龄超过
5747
+ * STALE_LEDGER_LOCK_MS 才清理,避免误删慢 writer 的活跃锁。
5748
+ * @param ledgerPath - 账本文件路径(锁为其 `.lock` 兄弟文件)。
5749
+ * @returns 是否清理了孤儿锁;false = 锁已消失或太新鲜,调用方上抛原错误。
5750
+ */
5751
+ async function reclaimStaleLedgerLock(ledgerPath) {
5752
+ let ageMs;
5753
+ try {
5754
+ ageMs = Date.now() - (await lstat(`${ledgerPath}.lock`)).mtimeMs;
5755
+ } catch {
5756
+ return false;
5757
+ }
5758
+ if (ageMs < STALE_LEDGER_LOCK_MS) return false;
5759
+ console.warn(`[usage-billing] 账本锁已存在 ${Math.round(ageMs / 1e3)}s(崩溃残留),清理后重试:${ledgerPath}.lock`);
5760
+ await rm(`${ledgerPath}.lock`, { force: true });
5761
+ return true;
5762
+ }
5492
5763
  /**
5493
5764
  * Create the atomic file-backed durable-ledger store. The previous complete file
5494
5765
  * is retained as `.bak`; a malformed/missing main file falls back to that backup.
@@ -5509,7 +5780,7 @@ function createFileUsageLedgerStore(ledgerPath) {
5509
5780
  recursive: true,
5510
5781
  mode: 448
5511
5782
  });
5512
- await withFileLock(ledgerPath, async () => {
5783
+ const commit = async () => {
5513
5784
  try {
5514
5785
  const existing = await readFile(ledgerPath, "utf8");
5515
5786
  JSON.parse(existing);
@@ -5522,7 +5793,14 @@ function createFileUsageLedgerStore(ledgerPath) {
5522
5793
  mode: 384,
5523
5794
  dirMode: 448
5524
5795
  });
5525
- });
5796
+ };
5797
+ try {
5798
+ await withFileLock(ledgerPath, commit, { waitMs: LEDGER_LOCK_WAIT_MS });
5799
+ } catch (error) {
5800
+ if (!(error instanceof Error) || !error.message.includes("timed out waiting for the writer lock")) throw error;
5801
+ if (!await reclaimStaleLedgerLock(ledgerPath)) throw error;
5802
+ await withFileLock(ledgerPath, commit, { waitMs: LEDGER_LOCK_WAIT_MS });
5803
+ }
5526
5804
  }
5527
5805
  };
5528
5806
  }
@@ -5763,8 +6041,7 @@ function apply(ctx, config = {}) {
5763
6041
  })();
5764
6042
  const persistReconcileRef = () => {
5765
6043
  if (reconcileRef === null) return;
5766
- const payload = JSON.stringify({ ref: reconcileRef });
5767
- writeFileAtomic(reconcilePath, payload, {
6044
+ writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
5768
6045
  mode: 384,
5769
6046
  dirMode: 448
5770
6047
  }).catch(() => {});
@@ -6002,6 +6279,11 @@ function apply(ctx, config = {}) {
6002
6279
  }
6003
6280
  const sessionId = exec.agent?.id;
6004
6281
  if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
6282
+ const summary = await aggregator.sessionUsageOf(String(sessionId));
6283
+ if (summary !== void 0) return {
6284
+ range: args.range,
6285
+ ...summary
6286
+ };
6005
6287
  let cost = 0;
6006
6288
  let calls = 0;
6007
6289
  let input = 0;