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

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
@@ -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 = /* @__PURE__ */ new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3066
+ const hourKeys = /* @__PURE__ */ 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 = /* @__PURE__ */ 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);
@@ -5468,8 +5715,10 @@ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5468
5715
  * 下发,供「设置 → 插件信息卡」展示。发布版 lib/index.js 相对包根解析。
5469
5716
  */
5470
5717
  const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
5471
- /** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
5472
- const SNAPSHOT_INTERVAL_MS = 3e4;
5718
+ /** 统计快照的落盘节流(毫秒):快照只服务聚合失败回退与重启首屏(展示级
5719
+ * 新鲜度即可),降到 5 分钟一次——原子写含上一版备份是两次全量 JSON 落盘,
5720
+ * 重度用户快照上百 KB,跟随前端轮询 30 秒写一次纯属写放大。 */
5721
+ const SNAPSHOT_INTERVAL_MS = 3e5;
5473
5722
  /** usage-stats 响应的等待预算(毫秒):聚合在此预算内完成就回实时数据,
5474
5723
  * 超预算立即回最近快照(stale-while-revalidate),后台折叠继续跑。
5475
5724
  * 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
@@ -6002,6 +6251,11 @@ function apply(ctx, config = {}) {
6002
6251
  }
6003
6252
  const sessionId = exec.agent?.id;
6004
6253
  if (sessionId === void 0) throw new Error("usage_stats 的 session 范围需要 agent 会话上下文");
6254
+ const summary = await aggregator.sessionUsageOf(String(sessionId));
6255
+ if (summary !== void 0) return {
6256
+ range: args.range,
6257
+ ...summary
6258
+ };
6005
6259
  let cost = 0;
6006
6260
  let calls = 0;
6007
6261
  let input = 0;