ccus-cli 0.1.19 → 0.1.22

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.
@@ -4,7 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadWeeklyExportBundles = loadWeeklyExportBundles;
7
+ exports.computeCumulativeSevenDayCurve = computeCumulativeSevenDayCurve;
7
8
  exports.computeCumulativeSevenDay = computeCumulativeSevenDay;
9
+ exports.deburrSevenDayEvents = deburrSevenDayEvents;
10
+ exports.buildSevenDayCurveFromEvents = buildSevenDayCurveFromEvents;
8
11
  exports.buildPersonSevenDayCurve = buildPersonSevenDayCurve;
9
12
  exports.buildAggregatedDetailRows = buildAggregatedDetailRows;
10
13
  exports.buildAggregatedDailyRows = buildAggregatedDailyRows;
@@ -16,6 +19,10 @@ const node_zlib_1 = require("node:zlib");
16
19
  const payload_1 = require("./payload");
17
20
  const time_1 = require("./time");
18
21
  const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
22
+ function maxOrNull(values) {
23
+ const numbers = values.filter((v) => v !== null);
24
+ return numbers.length > 0 ? Math.max(...numbers) : null;
25
+ }
19
26
  function isRecord(value) {
20
27
  return typeof value === "object" && value !== null && !Array.isArray(value);
21
28
  }
@@ -91,20 +98,29 @@ async function readBundleFileContent(filePath) {
91
98
  /** 读取目录里的 export bundle json 文件。 */
92
99
  async function loadWeeklyExportBundles(inputDir) {
93
100
  const files = await collectBundleJsonFiles(inputDir);
94
- const bundles = [];
95
101
  const invalidFiles = [];
96
- for (const filePath of files) {
102
+ const results = await Promise.all(files.map(async (filePath) => {
97
103
  try {
98
104
  const content = await readBundleFileContent(filePath);
99
105
  const parsed = JSON.parse(content);
100
106
  if (isWeeklyExportBundle(parsed)) {
101
- bundles.push({ filePath, bundle: parsed });
102
- continue;
107
+ return { filePath, bundle: parsed };
103
108
  }
104
- invalidFiles.push(filePath);
109
+ return { filePath, bundle: null };
105
110
  }
106
111
  catch {
112
+ return null;
113
+ }
114
+ }));
115
+ const bundles = [];
116
+ for (const result of results) {
117
+ if (result === null)
107
118
  continue;
119
+ if (result.bundle === null) {
120
+ invalidFiles.push(result.filePath);
121
+ }
122
+ else {
123
+ bundles.push({ filePath: result.filePath, bundle: result.bundle });
108
124
  }
109
125
  }
110
126
  if (invalidFiles.length > 0) {
@@ -140,31 +156,87 @@ function dayDataTier(day) {
140
156
  return 1;
141
157
  return 0;
142
158
  }
143
- /** winner 比较:数据质量等级高优先,同级别内 generatedAt 较新优先,最后用 filePath 做稳定 tie-break。 */
144
- function isBetterCandidate(nextTier, nextGeneratedAt, nextFilePath, currentTier, currentGeneratedAt, currentFilePath) {
159
+ /**
160
+ * winner 比较:数据质量等级高优先;同 tier=2 時消息数多优先(避免"仅 generatedAt 更新"的低活跃机器
161
+ * 覆盖同一天高活跃机器的数据);同 count 内 generatedAt 较新优先;最后用 filePath 做稳定 tie-break。
162
+ */
163
+ function isBetterCandidate(nextTier, nextMsgCount, nextApiCount, nextGeneratedAt, nextFilePath, currentTier, currentMsgCount, currentApiCount, currentGeneratedAt, currentFilePath) {
145
164
  if (nextTier !== currentTier) {
146
165
  return nextTier > currentTier;
147
166
  }
167
+ if (nextTier === 2) {
168
+ if (nextMsgCount !== currentMsgCount) {
169
+ return nextMsgCount > currentMsgCount;
170
+ }
171
+ if (nextApiCount !== currentApiCount) {
172
+ return nextApiCount > currentApiCount;
173
+ }
174
+ }
148
175
  if (nextGeneratedAt !== currentGeneratedAt) {
149
176
  return nextGeneratedAt > currentGeneratedAt;
150
177
  }
151
178
  return nextFilePath > currentFilePath;
152
179
  }
153
- /** 对每个 (personKey, date) 选出 generatedAt 最新、且尽量有数据的那份 bundle 当天数据。 */
154
- function selectDailyWinners(bundles) {
155
- const winners = new Map();
180
+ /**
181
+ * 同一 (personKey, date) 的所有 bundle,按 rawEvents sessionId 集合的交集分组:
182
+ * - 有交集的视为同机器重复导出(同一账号同一天的会话在两份 bundle 里均存在),只取最优 winner
183
+ * - 无交集的视为不同机器的独立数据,分别保留,后续叠加
184
+ *
185
+ * sessionId 集合为空的候选(该天没有 statusline 事件)不参与交集判断,单独成组。
186
+ */
187
+ function selectDailyRepresentatives(bundles) {
188
+ // 收集每个 (personKey, date) 的所有候选
189
+ const candidatesByKey = new Map();
156
190
  for (const { filePath, bundle } of bundles) {
157
191
  const personKey = bundlePersonKey(bundle);
158
192
  const generatedAt = bundle.generatedAt ?? "";
193
+ const eventsByDate = bundleEventsByDate(bundle);
159
194
  for (const day of bundle.dailySummaries) {
160
195
  const key = `${personKey}|${day.date}`;
161
- const current = winners.get(key);
162
- if (!current || isBetterCandidate(dayDataTier(day), generatedAt, filePath, dayDataTier(current.day), current.generatedAt, current.filePath)) {
163
- winners.set(key, { personKey, date: day.date, day, bundle, generatedAt, filePath });
196
+ const events = eventsByDate.get(day.date) ?? [];
197
+ const sessionIds = new Set(events.map((e) => e.sessionId).filter((s) => s !== null));
198
+ const list = candidatesByKey.get(key) ?? [];
199
+ list.push({ day, bundle, generatedAt, filePath, sessionIds });
200
+ candidatesByKey.set(key, list);
201
+ }
202
+ }
203
+ const result = new Map();
204
+ for (const [key, candidates] of candidatesByKey.entries()) {
205
+ const barIdx = key.indexOf("|");
206
+ const personKey = key.slice(0, barIdx);
207
+ const date = key.slice(barIdx + 1);
208
+ // 贪心分组:候选有 sessionId 且与某组内任意候选的 sessionId 有交集,则并入该组;否则新建组
209
+ const groups = [];
210
+ for (const candidate of candidates) {
211
+ let added = false;
212
+ if (candidate.sessionIds.size > 0) {
213
+ for (const group of groups) {
214
+ const hasOverlap = group.some((c) => c.sessionIds.size > 0 && [...candidate.sessionIds].some((s) => c.sessionIds.has(s)));
215
+ if (hasOverlap) {
216
+ group.push(candidate);
217
+ added = true;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ if (!added) {
223
+ groups.push([candidate]);
164
224
  }
165
225
  }
226
+ // 每组取最优代表(同机器多次导出只保留一份)
227
+ const reps = groups.map((group) => {
228
+ let best = group[0];
229
+ for (let i = 1; i < group.length; i++) {
230
+ const c = group[i];
231
+ if (isBetterCandidate(dayDataTier(c.day), c.day.userMessageCount, c.day.apiRequestCount, c.generatedAt, c.filePath, dayDataTier(best.day), best.day.userMessageCount, best.day.apiRequestCount, best.generatedAt, best.filePath)) {
232
+ best = c;
233
+ }
234
+ }
235
+ return { personKey, date, day: best.day, bundle: best.bundle };
236
+ });
237
+ result.set(key, reps);
166
238
  }
167
- return winners;
239
+ return result;
168
240
  }
169
241
  /** 把 bundle 的 rawEvents 计算成 StatuslineEvent 并按本地自然日分组,结果做缓存复用。 */
170
242
  const bundleEventsCache = new WeakMap();
@@ -196,7 +268,8 @@ function bundleEventsByDate(bundle) {
196
268
  */
197
269
  const SEVEN_DAY_RESET_RATIO = 0.5;
198
270
  /**
199
- * 7 天额度累计真实使用量:对一组事件取非 null `sevenDayUsagePct`、按时间升序,用**分段峰谷和**还原。
271
+ * 7 天额度累计真实使用量的**逐点曲线**:对一组事件取非 null `sevenDayUsagePct`、按时间升序,用**分段峰谷和**
272
+ * 还原,并在每个样本处输出「截至此刻的累计值」,得到一条单调非递减曲线。
200
273
  *
201
274
  * 把曲线按 reset(样本跌破当前段峰值的 {@link SEVEN_DAY_RESET_RATIO})切成若干上升段,
202
275
  * 每段贡献「段内峰值 − 段内谷值」,累计 = 各段贡献之和。等价于「正增量累加,但忽略未跌破段峰一半的小回落」。
@@ -205,21 +278,23 @@ const SEVEN_DAY_RESET_RATIO = 0.5;
205
278
  * 朴素累加会把每次上抖都计成真实增长,导致严重高估(实测 gaoshang 102 vs 分段 50)。
206
279
  * 分段峰谷和对抖动 / aging 回落鲁棒,又能正确累计「涨到峰 → 归零 → 再涨」的多段真实使用。
207
280
  *
208
- * 无有效样本返回 null(区别于 0:0 表示有样本但无净增长);单样本返回 0。
281
+ * 每个点的累计 = 已锁定的各完整段贡献 + 当前段 (segMax − segMin);段内更新 / reset 都不会让它回落,
282
+ * 所以曲线单调非递减、终点恒等于 {@link computeCumulativeSevenDay} 的标量返回值。输入建议先经 deburr 去毛刺。
283
+ * 无有效样本返回空数组;单样本返回单点 0。
209
284
  */
210
- function computeCumulativeSevenDay(events) {
211
- const values = [...events]
212
- .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime())
213
- .map((event) => event.sevenDayUsagePct)
214
- .filter((value) => value !== null);
215
- if (values.length === 0) {
216
- return null;
285
+ function computeCumulativeSevenDayCurve(events) {
286
+ const sorted = [...events]
287
+ .filter((event) => event.sevenDayUsagePct !== null)
288
+ .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
289
+ if (sorted.length === 0) {
290
+ return [];
217
291
  }
292
+ const points = [{ timestamp: sorted[0].timestamp, cumulative: 0 }];
218
293
  let cumulative = 0;
219
- let segMin = values[0];
220
- let segMax = values[0];
221
- for (let index = 1; index < values.length; index += 1) {
222
- const value = values[index];
294
+ let segMin = sorted[0].sevenDayUsagePct;
295
+ let segMax = sorted[0].sevenDayUsagePct;
296
+ for (let index = 1; index < sorted.length; index += 1) {
297
+ const value = sorted[index].sevenDayUsagePct;
223
298
  if (value > segMax) {
224
299
  segMax = value;
225
300
  }
@@ -233,9 +308,18 @@ function computeCumulativeSevenDay(events) {
233
308
  // 段内小回落(抖动 / aging):只更新谷值,不分段、不重复计数。
234
309
  segMin = value;
235
310
  }
311
+ points.push({ timestamp: sorted[index].timestamp, cumulative: (0, time_1.roundNumber)(cumulative + (segMax - segMin), 1) ?? 0 });
236
312
  }
237
- cumulative += segMax - segMin;
238
- return (0, time_1.roundNumber)(cumulative, 1);
313
+ return points;
314
+ }
315
+ /**
316
+ * 7 天额度累计真实使用量(标量):取 {@link computeCumulativeSevenDayCurve} 曲线终点。
317
+ *
318
+ * 无有效样本返回 null(区别于 0:0 表示有样本但无净增长);单样本返回 0。
319
+ */
320
+ function computeCumulativeSevenDay(events) {
321
+ const curve = computeCumulativeSevenDayCurve(events);
322
+ return curve.length === 0 ? null : curve[curve.length - 1].cumulative;
239
323
  }
240
324
  /** 7d 曲线读数去毛刺的最小持续时长:短于此的中间读数段视为 stale / 瞬时异常。 */
241
325
  const SEVEN_DAY_MIN_HOLD_MS = 2 * 60 * 1000;
@@ -243,10 +327,14 @@ const SEVEN_DAY_MIN_HOLD_MS = 2 * 60 * 1000;
243
327
  * 7d 曲线读数去毛刺:真实 7d 信号变化慢、每个档位会被高频采样连续覆盖几十上百个样本,
244
328
  * 而 stale 缓存读数 / 瞬时异常只会短暂出现(秒级,如 baseline 很低时偶发跳到 30 又落回)。
245
329
  *
246
- * 把**中间**持续短于 {@link SEVEN_DAY_MIN_HOLD_MS} 的读数段(下一段起始 − 本段起始)替换为最近的已保留前值,
247
- * 抹掉这些尖峰;首尾段无条件保留(端点缺上下文判断持续性)。这等价于人眼在曲线图上自动忽略短毛刺。
330
+ * 把**中间**持续短于 {@link SEVEN_DAY_MIN_HOLD_MS} 的读数段替换为最近的已保留前值,抹掉这些尖峰;
331
+ * 首尾段无条件保留(端点缺上下文判断持续性)。这等价于人眼在曲线图上自动忽略短毛刺。
332
+ *
333
+ * 持续时长默认按「下一段起始 − 本段起始」度量(密集采样下约等于本段实际持续);但当**多样本**段后面紧跟一段
334
+ * 比阈值还大的**采集间隙**时,该度量会把间隙也算进去、把只持续几十秒的短尖峰“撑”过阈值而漏抹,
335
+ * 此时改用**段内真实跨度(最后样本 − 第一样本)**判定。单样本段无段内跨度、无法据此判断,仍走原度量。
248
336
  *
249
- * 仅对密集采样的真实曲线生效:稀疏数据(每个值只有一两个样本、间隔很大)的中间段持续时长通常远超阈值,
337
+ * 仅对密集采样的真实曲线生效:稀疏数据(每个值只有一两个样本、间隔很大)的单样本中间段仍按原度量保留,
250
338
  * 不会被误删,所以 spec 的稀疏示例与单样本段不受影响。输入须按 timestamp 升序。
251
339
  */
252
340
  function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
@@ -260,14 +348,28 @@ function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
260
348
  while (end + 1 < events.length && events[end + 1].sevenDayUsagePct === events[index].sevenDayUsagePct) {
261
349
  end += 1;
262
350
  }
263
- runs.push({ value: events[index].sevenDayUsagePct, startIdx: index, endIdx: end, startTime: new Date(events[index].timestamp).getTime() });
351
+ runs.push({
352
+ value: events[index].sevenDayUsagePct,
353
+ startIdx: index,
354
+ endIdx: end,
355
+ startTime: new Date(events[index].timestamp).getTime(),
356
+ endTime: new Date(events[end].timestamp).getTime(),
357
+ });
264
358
  index = end + 1;
265
359
  }
266
360
  const result = [...events];
267
361
  let lastKept = runs[0].value;
268
362
  for (let k = 0; k < runs.length; k += 1) {
269
363
  const isEdge = k === 0 || k === runs.length - 1;
270
- const holdMs = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].startTime : Number.POSITIVE_INFINITY;
364
+ // 默认持续时长 = 下一段起始 本段起始(密集采样下约等于本段实际持续)。
365
+ const toNext = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].startTime : Number.POSITIVE_INFINITY;
366
+ // 本段最后样本到下一段起始的采集间隙:密集采样下很小,遇到数据采集中断时会变大。
367
+ const gapAfter = k + 1 < runs.length ? runs[k + 1].startTime - runs[k].endTime : Number.POSITIVE_INFINITY;
368
+ // 多样本段(段内本身有跨度)后面若紧跟一段比阈值还大的采集间隙,说明 toNext 把间隙也算进了持续时长,
369
+ // 会把本只持续几十秒的短尖峰“撑”过阈值而漏抹;此时改用段内真实跨度判定,正确识别这类 stale 尖峰。
370
+ // 单样本段(无段内跨度)无法据此判断真实持续,仍走 toNext,避免误伤稀疏单样本数据。
371
+ const multiSample = runs[k].endIdx > runs[k].startIdx;
372
+ const holdMs = multiSample && gapAfter > minHoldMs ? runs[k].endTime - runs[k].startTime : toNext;
271
373
  if (isEdge || holdMs >= minHoldMs) {
272
374
  lastKept = runs[k].value;
273
375
  continue;
@@ -278,6 +380,28 @@ function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
278
380
  }
279
381
  return result;
280
382
  }
383
+ /**
384
+ * 把一组事件整理成一条可用于累计计算的 7d 曲线:取非 null `sevenDayUsagePct`、按时间升序、
385
+ * 对完全相同 timestamp 去重,再做读数去毛刺。
386
+ *
387
+ * 单台机器的本地日志(dashboard 个人看板)与多机合并(aggregate)都走这同一套整理逻辑,
388
+ * 保证两边的累计口径不会漂移。
389
+ */
390
+ function buildSevenDayCurveFromEvents(events) {
391
+ const sorted = [...events]
392
+ .filter((event) => event.sevenDayUsagePct !== null)
393
+ .sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
394
+ const seen = new Set();
395
+ const deduped = [];
396
+ for (const event of sorted) {
397
+ if (seen.has(event.timestamp)) {
398
+ continue;
399
+ }
400
+ seen.add(event.timestamp);
401
+ deduped.push(event);
402
+ }
403
+ return deburrSevenDayEvents(deduped);
404
+ }
281
405
  /**
282
406
  * 同一 personKey 的 7d 累计曲线:把该人**所有 bundle**(非仅 winner)的 rawEvents 计算成事件、
283
407
  * 取非 null `sevenDayUsagePct`、按 timestamp 升序合并、对完全相同 timestamp 去重,再做读数去毛刺,
@@ -311,17 +435,7 @@ function buildPersonSevenDayCurve(bundles) {
311
435
  }
312
436
  const curves = new Map();
313
437
  for (const [personKey, events] of collected.entries()) {
314
- const sorted = [...events].sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
315
- const seen = new Set();
316
- const deduped = [];
317
- for (const event of sorted) {
318
- if (seen.has(event.timestamp)) {
319
- continue;
320
- }
321
- seen.add(event.timestamp);
322
- deduped.push(event);
323
- }
324
- curves.set(personKey, deburrSevenDayEvents(deduped));
438
+ curves.set(personKey, buildSevenDayCurveFromEvents(events));
325
439
  }
326
440
  personSevenDayCurveCache.set(bundles, curves);
327
441
  return curves;
@@ -346,52 +460,72 @@ function recomputeUsage(events) {
346
460
  sevenDayLatestUsagePct: newestFirst.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null,
347
461
  };
348
462
  }
349
- /** winner bundle 的事件展开 detail.csv,同人同天只保留 winner,那份的当天 token 总量随行附带。 */
463
+ /** 展开 detail.csv:同人同天各机器的代表 bundle 事件都列出来,token 总量随本机器当天的 daySummary 附带。 */
350
464
  function buildAggregatedDetailRows(bundles) {
351
- const winners = selectDailyWinners(bundles);
465
+ const repsMap = selectDailyRepresentatives(bundles);
352
466
  const rows = [];
353
- for (const winner of winners.values()) {
354
- const events = bundleEventsByDate(winner.bundle).get(winner.date) ?? [];
355
- for (const event of events) {
356
- rows.push({
357
- ...event,
358
- personKey: winner.personKey,
359
- weekKey: weekKey(new Date(event.timestamp)),
360
- dateKey: winner.date,
361
- inputTokens: winner.day.inputTokens,
362
- outputTokens: winner.day.outputTokens,
363
- cacheReadInputTokens: winner.day.cacheReadInputTokens,
364
- });
467
+ for (const reps of repsMap.values()) {
468
+ for (const rep of reps) {
469
+ const events = bundleEventsByDate(rep.bundle).get(rep.date) ?? [];
470
+ for (const event of events) {
471
+ rows.push({
472
+ ...event,
473
+ personKey: rep.personKey,
474
+ weekKey: weekKey(new Date(event.timestamp)),
475
+ dateKey: rep.date,
476
+ inputTokens: rep.day.inputTokens,
477
+ outputTokens: rep.day.outputTokens,
478
+ cacheReadInputTokens: rep.day.cacheReadInputTokens,
479
+ });
480
+ }
365
481
  }
366
482
  }
367
483
  return rows.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
368
484
  }
369
- /** 展开 daily.csv:同人同天取 winner bundle 的累加值,usage 从该 bundle 当天事件重算。 */
485
+ /**
486
+ * 展开 daily.csv:同人同天的不同机器数据直接叠加(计数字段相加),usage 从所有机器该天事件合并后重算。
487
+ * 同机器重复导出由 selectDailyRepresentatives 在分组阶段去重,不会翻倍。
488
+ */
370
489
  function buildAggregatedDailyRows(bundles) {
371
- const winners = selectDailyWinners(bundles);
490
+ const repsMap = selectDailyRepresentatives(bundles);
372
491
  const curves = buildPersonSevenDayCurve(bundles);
373
492
  const rows = [];
374
- for (const winner of winners.values()) {
375
- const day = winner.day;
376
- const usage = recomputeUsage(bundleEventsByDate(winner.bundle).get(winner.date) ?? []);
377
- // 累计指标走全样本合并曲线,不走 winner recomputeUsage,避免漏掉非 winner 机器的样本。
378
- const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByDate(curves.get(winner.personKey) ?? [], winner.date));
493
+ for (const reps of repsMap.values()) {
494
+ const { personKey, date } = reps[0];
495
+ // 不同机器的独立数据直接叠加
496
+ const userMessageCount = reps.reduce((sum, r) => sum + r.day.userMessageCount, 0);
497
+ const apiRequestCount = reps.reduce((sum, r) => sum + r.day.apiRequestCount, 0);
498
+ const inputTokens = reps.reduce((sum, r) => sum + r.day.inputTokens, 0);
499
+ const outputTokens = reps.reduce((sum, r) => sum + r.day.outputTokens, 0);
500
+ const cacheReadInputTokens = reps.reduce((sum, r) => sum + r.day.cacheReadInputTokens, 0);
501
+ const sampleCount = reps.reduce((sum, r) => sum + r.day.sampleCount, 0);
502
+ const uniqueSessions = reps.reduce((sum, r) => sum + r.day.uniqueSessions, 0);
503
+ const uniqueWorkspaces = reps.reduce((sum, r) => sum + r.day.uniqueWorkspaces, 0);
504
+ // usage 从所有机器该天事件合并后重算;rawEvents 缺失时用各代表 daySummary 回退
505
+ const allEvents = reps.flatMap((r) => bundleEventsByDate(r.bundle).get(date) ?? []);
506
+ const usage = recomputeUsage(allEvents);
507
+ const fiveHourPeakFallback = maxOrNull(reps.map((r) => r.day.fiveHourPeakUsagePct));
508
+ const sevenDayPeakFallback = maxOrNull(reps.map((r) => r.day.sevenDayPeakUsagePct));
509
+ const fiveHourLatestFallback = reps.find((r) => r.day.fiveHourLatestUsagePct !== null)?.day.fiveHourLatestUsagePct ?? null;
510
+ const sevenDayLatestFallback = reps.find((r) => r.day.sevenDayLatestUsagePct !== null)?.day.sevenDayLatestUsagePct ?? null;
511
+ // 累计指标走全样本合并曲线,不走单机的 recomputeUsage,避免漏掉另一台机器的样本。
512
+ const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByDate(curves.get(personKey) ?? [], date));
379
513
  rows.push({
380
- personKey: winner.personKey,
381
- date: day.date,
382
- userMessageCount: day.userMessageCount,
383
- apiRequestCount: day.apiRequestCount,
384
- inputTokens: day.inputTokens,
385
- outputTokens: day.outputTokens,
386
- cacheReadInputTokens: day.cacheReadInputTokens,
387
- sampleCount: day.sampleCount,
388
- fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? day.fiveHourPeakUsagePct,
389
- fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? day.fiveHourLatestUsagePct,
390
- sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? day.sevenDayPeakUsagePct,
391
- sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? day.sevenDayLatestUsagePct,
514
+ personKey,
515
+ date,
516
+ userMessageCount,
517
+ apiRequestCount,
518
+ inputTokens,
519
+ outputTokens,
520
+ cacheReadInputTokens,
521
+ sampleCount,
522
+ fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? fiveHourPeakFallback,
523
+ fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fiveHourLatestFallback,
524
+ sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? sevenDayPeakFallback,
525
+ sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? sevenDayLatestFallback,
392
526
  sevenDayCumulativeUsagePct,
393
- uniqueSessions: day.uniqueSessions,
394
- uniqueWorkspaces: day.uniqueWorkspaces,
527
+ uniqueSessions,
528
+ uniqueWorkspaces,
395
529
  });
396
530
  }
397
531
  return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
@@ -409,46 +543,47 @@ function fallbackWeeklyUsage(days) {
409
543
  };
410
544
  }
411
545
  /**
412
- * 展开 weekly.csv:一个人同一周有多份 bundle(多台电脑各导出)时,不取单独一份的整周汇总,
413
- * 而是复用按天去重后的 daily winner(每天选有数据的那份),按 (person, 周) 把每天的 token / 计数累加上卷,
414
- * usage 从该周所有 winner 天的事件重算(peak 取 max、latest 取时间戳最新),缺失时回退到 daySummary 自带值。
546
+ * 展开 weekly.csv:不同机器的同人同天数据已在 selectDailyRepresentatives 层按 sessionId 去重分组,
547
+ * 这里直接按 (person, 周) 把所有代表的 token / 计数累加上卷,usage 从该周所有代表事件重算。
415
548
  */
416
549
  function buildAggregatedWeeklyRows(bundles) {
417
- const dailyWinners = selectDailyWinners(bundles);
550
+ const repsMap = selectDailyRepresentatives(bundles);
418
551
  const curves = buildPersonSevenDayCurve(bundles);
419
552
  const groups = new Map();
420
- for (const winner of dailyWinners.values()) {
421
- const week = weekKey(new Date(winner.bundle.range.start));
422
- const key = `${winner.personKey}|${week}`;
423
- let acc = groups.get(key);
424
- if (!acc) {
425
- acc = {
426
- personKey: winner.personKey,
427
- week,
428
- userMessageCount: 0,
429
- apiRequestCount: 0,
430
- inputTokens: 0,
431
- outputTokens: 0,
432
- cacheReadInputTokens: 0,
433
- sampleCount: 0,
434
- uniqueSessions: 0,
435
- uniqueWorkspaces: 0,
436
- days: [],
437
- events: [],
438
- };
439
- groups.set(key, acc);
553
+ for (const reps of repsMap.values()) {
554
+ for (const rep of reps) {
555
+ const week = weekKey(new Date(rep.bundle.range.start));
556
+ const key = `${rep.personKey}|${week}`;
557
+ let acc = groups.get(key);
558
+ if (!acc) {
559
+ acc = {
560
+ personKey: rep.personKey,
561
+ week,
562
+ userMessageCount: 0,
563
+ apiRequestCount: 0,
564
+ inputTokens: 0,
565
+ outputTokens: 0,
566
+ cacheReadInputTokens: 0,
567
+ sampleCount: 0,
568
+ uniqueSessions: 0,
569
+ uniqueWorkspaces: 0,
570
+ days: [],
571
+ events: [],
572
+ };
573
+ groups.set(key, acc);
574
+ }
575
+ const day = rep.day;
576
+ acc.userMessageCount += day.userMessageCount;
577
+ acc.apiRequestCount += day.apiRequestCount;
578
+ acc.inputTokens += day.inputTokens;
579
+ acc.outputTokens += day.outputTokens;
580
+ acc.cacheReadInputTokens += day.cacheReadInputTokens;
581
+ acc.sampleCount += day.sampleCount;
582
+ acc.uniqueSessions += day.uniqueSessions;
583
+ acc.uniqueWorkspaces += day.uniqueWorkspaces;
584
+ acc.days.push(day);
585
+ acc.events.push(...(bundleEventsByDate(rep.bundle).get(rep.date) ?? []));
440
586
  }
441
- const day = winner.day;
442
- acc.userMessageCount += day.userMessageCount;
443
- acc.apiRequestCount += day.apiRequestCount;
444
- acc.inputTokens += day.inputTokens;
445
- acc.outputTokens += day.outputTokens;
446
- acc.cacheReadInputTokens += day.cacheReadInputTokens;
447
- acc.sampleCount += day.sampleCount;
448
- acc.uniqueSessions += day.uniqueSessions;
449
- acc.uniqueWorkspaces += day.uniqueWorkspaces;
450
- acc.days.push(day);
451
- acc.events.push(...(bundleEventsByDate(winner.bundle).get(winner.date) ?? []));
452
587
  }
453
588
  const rows = [];
454
589
  for (const acc of groups.values()) {