ccus-cli 0.1.16 → 0.1.18

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 CHANGED
@@ -177,6 +177,10 @@ ccus aggregate serve --input-dir ./team-exports
177
177
  - `detail.csv`:来自 winner bundle 的 `rawEvents`(同人同天只展开最新那份的事件),`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
178
178
  - `daily.csv`:同人同天取最新导出 bundle 的 `dailySummaries`,usage 从该 bundle 当天事件重算
179
179
  - `weekly.csv`:把同人同周的各天 winner(已按天去重)上卷累加得到,usage 从该周全部 winner 天的事件重算
180
+ - `daily.csv` / `weekly.csv` 还各带一列 `sevenDayCumulativeUsagePct`:7 天额度的**累计真实使用量**,把锯齿波(涨到峰值→窗口重置归零→再涨)还原成实际消耗。计算分两层:先**去毛刺**(把持续短于 2 分钟的 stale 瞬时读数尖峰抹平,只保留真实持续的水平),再做**分段峰谷和**(按 reset 跌破段峰值一半切成上升段,每段累加「段内峰值−谷值」)。以百分比累加表达、可大于 100(用掉多于一个 7d 额度)。对 ±1 采样抖动和 stale 读数尖峰都鲁棒,不会被噪声虚增
181
+ - 该列**绕开按天 winner**,对同一个人**所有机器、所有周**的 bundle `rawEvents` 先按时间合并去重成一条账号级曲线再算,绝不分机相加(同账号 7d 额度共享)
182
+ - 区间内第一个有效样本无前值、不贡献增量,所以 **`daily.csv` 逐行相加只是对全局总量的近似**(跨天边界增量不计入单天);`weekly.csv` 在整周连续算、把跨天增量也计入,更连续,恒有 `weekly ≥ Σ 同周 daily`,想要更准的总量请看 weekly 这列
183
+ - 无有效样本写空(`null`),有样本但无净增长写 `0`;`detail.csv` 不含此列(单事件行不承载区间累计语义)
180
184
  - CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
181
185
  - 想直接查看团队多人 dashboard,可以用 `ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]`:默认监听 `127.0.0.1` 上的随机端口,启动后会自动用系统默认浏览器打开,每次请求实时读取目录里的 bundle,不写入任何文件
182
186
 
@@ -31,7 +31,7 @@ function maxOrNull(values) {
31
31
  *
32
32
  * 这里只信任 daily.csv 的口径,确保数字和 aggregate 输出的 CSV 一致。
33
33
  */
34
- function summarizePeople(dailyRows) {
34
+ function summarizePeople(dailyRows, weeklyRows = []) {
35
35
  const grouped = new Map();
36
36
  for (const row of dailyRows) {
37
37
  const items = grouped.get(row.personKey);
@@ -42,6 +42,15 @@ function summarizePeople(dailyRows) {
42
42
  grouped.set(row.personKey, [row]);
43
43
  }
44
44
  }
45
+ // 累计 7d 走 weekly.csv 同源:按 personKey 把各周 weekly 行的累计值相加(weekly ≥ Σ daily,不能用 daily 求和)。
46
+ const cumulativeByPerson = new Map();
47
+ for (const row of weeklyRows) {
48
+ if (row.sevenDayCumulativeUsagePct === null) {
49
+ continue;
50
+ }
51
+ const current = cumulativeByPerson.get(row.personKey);
52
+ cumulativeByPerson.set(row.personKey, (current ?? 0) + row.sevenDayCumulativeUsagePct);
53
+ }
45
54
  const summaries = [];
46
55
  for (const [personKey, items] of grouped.entries()) {
47
56
  const sortedByDate = [...items].sort((left, right) => left.date.localeCompare(right.date));
@@ -66,6 +75,7 @@ function summarizePeople(dailyRows) {
66
75
  fiveHourLatestUsagePct: latestRowWithUsage?.fiveHourLatestUsagePct ?? null,
67
76
  sevenDayPeakUsagePct: maxOrNull(items.map((row) => row.sevenDayPeakUsagePct)),
68
77
  sevenDayLatestUsagePct: latestRowWithSevenDay?.sevenDayLatestUsagePct ?? null,
78
+ sevenDayCumulativeUsagePct: cumulativeByPerson.get(personKey) ?? null,
69
79
  activeDays,
70
80
  firstDate: sortedByDate[0]?.date ?? null,
71
81
  lastDate: sortedByDate.at(-1)?.date ?? null,
@@ -175,13 +185,13 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
175
185
  const points = series.values
176
186
  .map((value, index) => `<circle cx="${xFor(index).toFixed(2)}" cy="${yFor(value).toFixed(2)}" r="3" fill="${color}"><title>${escapeHtml(series.person.personKey)} · ${escapeHtml(dateAxis[index])} · ${formatNumber(value)} 用户请求</title></circle>`)
177
187
  .join("");
178
- return `<g><path d="${path}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />${points}</g>`;
188
+ return `<g data-person="${escapeHtml(series.person.personKey)}"><path d="${path}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />${points}</g>`;
179
189
  })
180
190
  .join("");
181
191
  const legend = seriesData
182
192
  .map((series, seriesIndex) => {
183
193
  const color = palette[seriesIndex % palette.length];
184
- return `<span class="legend-chip"><span class="legend-dot" style="background:${color}"></span>${escapeHtml(series.person.personKey)}</span>`;
194
+ return `<span class="legend-chip legend-toggle" data-person="${escapeHtml(series.person.personKey)}"><span class="legend-dot" style="background:${color}"></span>${escapeHtml(series.person.personKey)}</span>`;
185
195
  })
186
196
  .join("");
187
197
  return `
@@ -191,7 +201,7 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
191
201
  <p class="eyebrow">Daily User Requests</p>
192
202
  <h2>每日用户请求数对比</h2>
193
203
  </div>
194
- <p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent 提示保留)。</p>
204
+ <p class="muted">基于每人 daily 汇总中的 userMessageCount(已剔除 tool_result 工具回填;sidechain 子 agent 提示保留)。点击图例人名可只高亮该人曲线。</p>
195
205
  </div>
196
206
  <div class="legend">${legend}</div>
197
207
  <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="每日用户请求数对比">
@@ -203,24 +213,26 @@ function renderDailyUserRequestChart(people, dailyIndex, dateAxis) {
203
213
  `;
204
214
  }
205
215
  /**
206
- * 用横向条形图对比每个人的「周使用量(7 天额度)峰值」。
216
+ * 用横向条形图对比每个人的「周使用量(7 天额度)累计真实使用量」。
207
217
  *
208
- * 7 天额度是 Claude 给出的滚动周额度使用率,峰值代表这段时间里每个人最接近用满周额度的程度。
209
- * 条长按固定的 100% 满刻度归一,直接反映绝对使用率水平;右侧仍标注绝对百分比。
218
+ * sevenDayCumulativeUsagePct 7 天额度的锯齿波还原成区间内累计真实使用量,比峰值更能反映
219
+ * 一段时间里实际消耗了多少额度(峰值相同但累计可相差数倍)。条长默认按 100% 满刻度归一、
220
+ * 直接反映绝对使用率;仅当有人累计超过 100% 时才放大刻度到该最大值,避免超长条溢出。右侧仍
221
+ * 标注绝对累计百分比。
210
222
  */
211
- function renderSevenDayPeakChart(people) {
223
+ function renderSevenDayCumulativeChart(people) {
212
224
  const ranked = people
213
- .filter((person) => person.sevenDayPeakUsagePct !== null)
214
- .sort((left, right) => (right.sevenDayPeakUsagePct ?? 0) - (left.sevenDayPeakUsagePct ?? 0));
225
+ .filter((person) => person.sevenDayCumulativeUsagePct !== null)
226
+ .sort((left, right) => (right.sevenDayCumulativeUsagePct ?? 0) - (left.sevenDayCumulativeUsagePct ?? 0));
215
227
  if (ranked.length === 0) {
216
228
  return `
217
229
  <section class="panel chart-panel">
218
230
  <div class="panel-header">
219
231
  <div>
220
- <p class="eyebrow">Weekly Peak Usage</p>
221
- <h2>周使用量峰值对比</h2>
232
+ <p class="eyebrow">Weekly Cumulative Usage</p>
233
+ <h2>周使用量累计对比</h2>
222
234
  </div>
223
- <p class="muted">还没有 7 天额度使用率样本。</p>
235
+ <p class="muted">还没有 7 天额度累计使用量样本。</p>
224
236
  </div>
225
237
  </section>
226
238
  `;
@@ -234,10 +246,12 @@ function renderSevenDayPeakChart(people) {
234
246
  const height = paddingTop + paddingBottom + ranked.length * rowHeight;
235
247
  const trackX = labelWidth;
236
248
  const trackWidth = width - labelWidth - valueWidth - 16;
237
- const maxValue = 100;
249
+ // 默认按 100% 满刻度,条长直接反映绝对使用率;仅当有人累计超过 100% 时才放大刻度到该最大值,
250
+ // 避免超长条溢出轨道。下限 100 同时兜底全 0 时的除零(条归零而非 NaN)。
251
+ const maxValue = Math.max(100, ...ranked.map((person) => person.sevenDayCumulativeUsagePct ?? 0));
238
252
  const bars = ranked
239
253
  .map((person, index) => {
240
- const pct = person.sevenDayPeakUsagePct ?? 0;
254
+ const pct = person.sevenDayCumulativeUsagePct ?? 0;
241
255
  const rowTop = paddingTop + index * rowHeight;
242
256
  const barHeight = 18;
243
257
  const barY = rowTop + (rowHeight - barHeight) / 2;
@@ -256,12 +270,12 @@ function renderSevenDayPeakChart(people) {
256
270
  <section class="panel chart-panel">
257
271
  <div class="panel-header">
258
272
  <div>
259
- <p class="eyebrow">Weekly Peak Usage</p>
260
- <h2>周使用量峰值对比</h2>
273
+ <p class="eyebrow">Weekly Cumulative Usage</p>
274
+ <h2>周使用量累计对比</h2>
261
275
  </div>
262
- <p class="muted">每个人 7 天额度使用率(sevenDayPeakUsagePct)的峰值,条越长越接近用满周额度。</p>
276
+ <p class="muted">每个人 7 天额度累计真实使用量(sevenDayCumulativeUsagePct),条越长累计消耗越多;默认按 100% 满刻度,仅当有人累计超过 100% 时才放大刻度到在场最大值。</p>
263
277
  </div>
264
- <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="周使用量峰值对比">
278
+ <svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="周使用量累计对比">
265
279
  ${bars}
266
280
  </svg>
267
281
  </section>
@@ -351,14 +365,14 @@ function renderFiveHourUsageChart(people, detailRows) {
351
365
  const sevenPath = entry.sevenPoints.length > 0
352
366
  ? `<path d="${pathFor(entry.sevenPoints)}" fill="none" stroke="${sevenColor}" stroke-width="2" stroke-dasharray="5 4" stroke-linecap="round" stroke-linejoin="round"><title>${escapeHtml(entry.person.personKey)} · 7d</title></path>`
353
367
  : "";
354
- return `<g>${fivePath}${sevenPath}</g>`;
368
+ return `<g data-person="${escapeHtml(entry.person.personKey)}">${fivePath}${sevenPath}</g>`;
355
369
  })
356
370
  .join("");
357
371
  const legend = series
358
372
  .map((entry) => {
359
373
  const color = CHART_PALETTE[entry.index % CHART_PALETTE.length];
360
374
  const sevenColor = SEVEN_DAY_PALETTE[entry.index % SEVEN_DAY_PALETTE.length];
361
- return `<span class="legend-chip"><span class="legend-dot" style="background:${color}"></span><span class="legend-dot" style="background:${sevenColor}"></span>${escapeHtml(entry.person.personKey)}</span>`;
375
+ return `<span class="legend-chip legend-toggle" data-person="${escapeHtml(entry.person.personKey)}"><span class="legend-dot" style="background:${color}"></span><span class="legend-dot" style="background:${sevenColor}"></span>${escapeHtml(entry.person.personKey)}</span>`;
362
376
  })
363
377
  .join("");
364
378
  return `
@@ -368,7 +382,7 @@ function renderFiveHourUsageChart(people, detailRows) {
368
382
  <p class="eyebrow">Usage Detail</p>
369
383
  <h2>5h / 7d 使用率详细曲线</h2>
370
384
  </div>
371
- <p class="muted">每条 statusline 采样的 5 小时额度(实线)与 7 天周额度(对比色虚线)使用率,按真实时间戳绘制、共用 Y 轴。</p>
385
+ <p class="muted">每条 statusline 采样的 5 小时额度(实线)与 7 天周额度(对比色虚线)使用率,按真实时间戳绘制、共用 Y 轴。点击图例人名可只高亮该人曲线。</p>
372
386
  </div>
373
387
  <div class="legend">
374
388
  ${legend}
@@ -411,6 +425,7 @@ function renderPeopleLeaderboard(people) {
411
425
  <td>${escapeHtml(statValue(person.fiveHourLatestUsagePct))}</td>
412
426
  <td>${escapeHtml(statValue(person.sevenDayPeakUsagePct))}</td>
413
427
  <td>${escapeHtml(statValue(person.sevenDayLatestUsagePct))}</td>
428
+ <td>${escapeHtml(statValue(person.sevenDayCumulativeUsagePct))}</td>
414
429
  <td>${person.activeDays}</td>
415
430
  <td class="muted-col">${formatNumber(person.apiRequestCount)}</td>
416
431
  </tr>`)
@@ -438,6 +453,7 @@ function renderPeopleLeaderboard(people) {
438
453
  <th>5h Latest</th>
439
454
  <th>7d Peak</th>
440
455
  <th>7d Latest</th>
456
+ <th>7d 累计</th>
441
457
  <th>活跃天数</th>
442
458
  <th class="muted-col">API 请求</th>
443
459
  </tr>
@@ -517,6 +533,7 @@ function renderWeeklyTable(weeklyRows) {
517
533
  <td>${escapeHtml(statValue(row.fiveHourLatestUsagePct))}</td>
518
534
  <td>${escapeHtml(statValue(row.sevenDayPeakUsagePct))}</td>
519
535
  <td>${escapeHtml(statValue(row.sevenDayLatestUsagePct))}</td>
536
+ <td>${escapeHtml(statValue(row.sevenDayCumulativeUsagePct))}</td>
520
537
  <td class="muted-col">${formatNumber(row.apiRequestCount)}</td>
521
538
  </tr>`)
522
539
  .join("");
@@ -543,6 +560,7 @@ function renderWeeklyTable(weeklyRows) {
543
560
  <th>5h Latest</th>
544
561
  <th>7d Peak</th>
545
562
  <th>7d Latest</th>
563
+ <th>7d 累计</th>
546
564
  <th class="muted-col">API 请求</th>
547
565
  </tr>
548
566
  </thead>
@@ -558,7 +576,7 @@ function renderWeeklyTable(weeklyRows) {
558
576
  * 这是一份单文件静态页面,所有数据已经内联,可直接打开或拷贝分享。
559
577
  */
560
578
  function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generatedAt = new Date()) {
561
- const people = summarizePeople(dailyRows);
579
+ const people = summarizePeople(dailyRows, weeklyRows);
562
580
  const overall = summarizeOverall(dailyRows, people);
563
581
  const dateAxis = collectDateAxis(dailyRows);
564
582
  const dailyIndex = indexDailyRows(dailyRows);
@@ -678,7 +696,14 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
678
696
  font-size: 13px;
679
697
  }
680
698
  .legend-chip { display: inline-flex; align-items: center; gap: 8px; }
699
+ .legend-toggle { cursor: pointer; user-select: none; transition: opacity 0.15s ease; }
700
+ .legend-toggle:hover { color: var(--text); }
701
+ .legend-toggle.is-active { color: var(--text); font-weight: 600; }
702
+ .legend-toggle.is-dimmed { opacity: 0.4; }
681
703
  .legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
704
+ svg [data-person] { transition: opacity 0.15s ease; }
705
+ svg [data-person].is-dimmed { opacity: 0.1; }
706
+ svg [data-person].is-active { opacity: 1; }
682
707
  .legend-line { display: inline-block; width: 22px; height: 0; border-top-width: 2px; border-top-style: solid; border-top-color: var(--muted); }
683
708
  .legend-line-dashed { border-top-style: dashed; }
684
709
  .table-panel { margin-top: 22px; }
@@ -762,12 +787,32 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
762
787
  </article>
763
788
  </section>
764
789
  ${renderPeopleLeaderboard(people)}
765
- ${renderSevenDayPeakChart(people)}
790
+ ${renderSevenDayCumulativeChart(people)}
766
791
  ${renderFiveHourUsageChart(people, detailRows)}
767
792
  ${renderDailyUserRequestChart(people, dailyIndex, dateAxis)}
768
793
  ${renderDailyMatrix(people, dailyIndex, dateAxis)}
769
794
  ${renderWeeklyTable(weeklyRows)}
770
795
  </main>
796
+ <script>
797
+ (function () {
798
+ // 点击图例里的人名:高亮该人在所有图表里的曲线,其余淡化;再次点击同名取消高亮。
799
+ var active = null;
800
+ function apply() {
801
+ document.querySelectorAll("[data-person]").forEach(function (el) {
802
+ el.classList.remove("is-active", "is-dimmed");
803
+ if (active === null) return;
804
+ el.classList.add(el.getAttribute("data-person") === active ? "is-active" : "is-dimmed");
805
+ });
806
+ }
807
+ document.querySelectorAll(".legend-toggle").forEach(function (chip) {
808
+ chip.addEventListener("click", function () {
809
+ var person = chip.getAttribute("data-person");
810
+ active = active === person ? null : person;
811
+ apply();
812
+ });
813
+ });
814
+ })();
815
+ </script>
771
816
  </body>
772
817
  </html>`;
773
818
  }
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadWeeklyExportBundles = loadWeeklyExportBundles;
7
+ exports.computeCumulativeSevenDay = computeCumulativeSevenDay;
8
+ exports.buildPersonSevenDayCurve = buildPersonSevenDayCurve;
7
9
  exports.buildAggregatedDetailRows = buildAggregatedDetailRows;
8
10
  exports.buildAggregatedDailyRows = buildAggregatedDailyRows;
9
11
  exports.buildAggregatedWeeklyRows = buildAggregatedWeeklyRows;
@@ -186,6 +188,152 @@ function bundleEventsByDate(bundle) {
186
188
  bundleEventsCache.set(bundle, byDate);
187
189
  return byDate;
188
190
  }
191
+ /**
192
+ * 7d 曲线判定额度重置(reset)的阈值:某样本跌到「当前段峰值 × 此比例」及以下时视为一次归零重置,开启新段。
193
+ *
194
+ * 真实 7d 曲线在每个档位附近会 ±1 抖动、并随滚动窗口小幅回落(aging),这些都不该被当成新使用量。
195
+ * 只有跌破段峰值一半才算真正的额度重置;0.4 / 0.5 / 0.6 对实测数值几乎不敏感,取中间值 0.5。
196
+ */
197
+ const SEVEN_DAY_RESET_RATIO = 0.5;
198
+ /**
199
+ * 7 天额度累计真实使用量:对一组事件取非 null `sevenDayUsagePct`、按时间升序,用**分段峰谷和**还原。
200
+ *
201
+ * 把曲线按 reset(样本跌破当前段峰值的 {@link SEVEN_DAY_RESET_RATIO})切成若干上升段,
202
+ * 每段贡献「段内峰值 − 段内谷值」,累计 = 各段贡献之和。等价于「正增量累加,但忽略未跌破段峰一半的小回落」。
203
+ *
204
+ * 之所以不用朴素的 `Σ max(0, uᵢ − uᵢ₋₁)`:实测 7d 信号在同一档位反复 ±1 抖动(采样毛刺),
205
+ * 朴素累加会把每次上抖都计成真实增长,导致严重高估(实测 gaoshang 102 vs 分段 50)。
206
+ * 分段峰谷和对抖动 / aging 回落鲁棒,又能正确累计「涨到峰 → 归零 → 再涨」的多段真实使用。
207
+ *
208
+ * 无有效样本返回 null(区别于 0:0 表示有样本但无净增长);单样本返回 0。
209
+ */
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;
217
+ }
218
+ 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];
223
+ if (value > segMax) {
224
+ segMax = value;
225
+ }
226
+ else if (segMax > 0 && value <= segMax * SEVEN_DAY_RESET_RATIO) {
227
+ // 跌破段峰一半:判定为额度重置,锁定上一段峰谷差,从该点开启新段。
228
+ cumulative += segMax - segMin;
229
+ segMin = value;
230
+ segMax = value;
231
+ }
232
+ else if (value < segMin) {
233
+ // 段内小回落(抖动 / aging):只更新谷值,不分段、不重复计数。
234
+ segMin = value;
235
+ }
236
+ }
237
+ cumulative += segMax - segMin;
238
+ return (0, time_1.roundNumber)(cumulative, 1);
239
+ }
240
+ /** 7d 曲线读数去毛刺的最小持续时长:短于此的中间读数段视为 stale / 瞬时异常。 */
241
+ const SEVEN_DAY_MIN_HOLD_MS = 2 * 60 * 1000;
242
+ /**
243
+ * 7d 曲线读数去毛刺:真实 7d 信号变化慢、每个档位会被高频采样连续覆盖几十上百个样本,
244
+ * 而 stale 缓存读数 / 瞬时异常只会短暂出现(秒级,如 baseline 很低时偶发跳到 30 又落回)。
245
+ *
246
+ * 把**中间**持续短于 {@link SEVEN_DAY_MIN_HOLD_MS} 的读数段(下一段起始 − 本段起始)替换为最近的已保留前值,
247
+ * 抹掉这些尖峰;首尾段无条件保留(端点缺上下文判断持续性)。这等价于人眼在曲线图上自动忽略短毛刺。
248
+ *
249
+ * 仅对密集采样的真实曲线生效:稀疏数据(每个值只有一两个样本、间隔很大)的中间段持续时长通常远超阈值,
250
+ * 不会被误删,所以 spec 的稀疏示例与单样本段不受影响。输入须按 timestamp 升序。
251
+ */
252
+ function deburrSevenDayEvents(events, minHoldMs = SEVEN_DAY_MIN_HOLD_MS) {
253
+ if (events.length <= 2) {
254
+ return events;
255
+ }
256
+ const runs = [];
257
+ let index = 0;
258
+ while (index < events.length) {
259
+ let end = index;
260
+ while (end + 1 < events.length && events[end + 1].sevenDayUsagePct === events[index].sevenDayUsagePct) {
261
+ end += 1;
262
+ }
263
+ runs.push({ value: events[index].sevenDayUsagePct, startIdx: index, endIdx: end, startTime: new Date(events[index].timestamp).getTime() });
264
+ index = end + 1;
265
+ }
266
+ const result = [...events];
267
+ let lastKept = runs[0].value;
268
+ for (let k = 0; k < runs.length; k += 1) {
269
+ 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;
271
+ if (isEdge || holdMs >= minHoldMs) {
272
+ lastKept = runs[k].value;
273
+ continue;
274
+ }
275
+ for (let idx = runs[k].startIdx; idx <= runs[k].endIdx; idx += 1) {
276
+ result[idx] = { ...result[idx], sevenDayUsagePct: lastKept };
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ /**
282
+ * 同一 personKey 的 7d 累计曲线:把该人**所有 bundle**(非仅 winner)的 rawEvents 计算成事件、
283
+ * 取非 null `sevenDayUsagePct`、按 timestamp 升序合并、对完全相同 timestamp 去重,再做读数去毛刺,
284
+ * 得到一条账号级曲线。
285
+ *
286
+ * 走全样本而非 winner,是因为累计指标是同一条共享额度曲线的密集采样:只取 winner 会漏掉非 winner
287
+ * 机器的样本(曲线稀疏、累计偏小),分机各自累计再相加又会翻倍。结果按 bundles 数组缓存复用。
288
+ */
289
+ const personSevenDayCurveCache = new WeakMap();
290
+ function buildPersonSevenDayCurve(bundles) {
291
+ const cached = personSevenDayCurveCache.get(bundles);
292
+ if (cached) {
293
+ return cached;
294
+ }
295
+ const collected = new Map();
296
+ for (const { bundle } of bundles) {
297
+ const personKey = bundlePersonKey(bundle);
298
+ for (const record of bundle.rawEvents.filter(isPersistedStatuslineEvent)) {
299
+ const event = (0, payload_1.computeStatuslineEvent)(record);
300
+ if (event.sevenDayUsagePct === null) {
301
+ continue;
302
+ }
303
+ const list = collected.get(personKey);
304
+ if (list) {
305
+ list.push(event);
306
+ }
307
+ else {
308
+ collected.set(personKey, [event]);
309
+ }
310
+ }
311
+ }
312
+ const curves = new Map();
313
+ 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));
325
+ }
326
+ personSevenDayCurveCache.set(bundles, curves);
327
+ return curves;
328
+ }
329
+ /** 从合并曲线里切出某自然日的子序列。 */
330
+ function sliceCurveByDate(curve, date) {
331
+ return curve.filter((event) => localDateKey(new Date(event.timestamp)) === date);
332
+ }
333
+ /** 从合并曲线里切出某周(周起始日 key)的子序列。 */
334
+ function sliceCurveByWeek(curve, week) {
335
+ return curve.filter((event) => weekKey(new Date(event.timestamp)) === week);
336
+ }
189
337
  /** 从一组事件按真实时间戳重算 5h / 7d 的 peak(max)与 latest(时间戳最新非空)。 */
190
338
  function recomputeUsage(events) {
191
339
  const newestFirst = [...events].sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime());
@@ -221,10 +369,13 @@ function buildAggregatedDetailRows(bundles) {
221
369
  /** 展开 daily.csv:同人同天取 winner bundle 的累加值,usage 从该 bundle 当天事件重算。 */
222
370
  function buildAggregatedDailyRows(bundles) {
223
371
  const winners = selectDailyWinners(bundles);
372
+ const curves = buildPersonSevenDayCurve(bundles);
224
373
  const rows = [];
225
374
  for (const winner of winners.values()) {
226
375
  const day = winner.day;
227
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));
228
379
  rows.push({
229
380
  personKey: winner.personKey,
230
381
  date: day.date,
@@ -238,6 +389,7 @@ function buildAggregatedDailyRows(bundles) {
238
389
  fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? day.fiveHourLatestUsagePct,
239
390
  sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? day.sevenDayPeakUsagePct,
240
391
  sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? day.sevenDayLatestUsagePct,
392
+ sevenDayCumulativeUsagePct,
241
393
  uniqueSessions: day.uniqueSessions,
242
394
  uniqueWorkspaces: day.uniqueWorkspaces,
243
395
  });
@@ -263,6 +415,7 @@ function fallbackWeeklyUsage(days) {
263
415
  */
264
416
  function buildAggregatedWeeklyRows(bundles) {
265
417
  const dailyWinners = selectDailyWinners(bundles);
418
+ const curves = buildPersonSevenDayCurve(bundles);
266
419
  const groups = new Map();
267
420
  for (const winner of dailyWinners.values()) {
268
421
  const week = weekKey(new Date(winner.bundle.range.start));
@@ -301,6 +454,8 @@ function buildAggregatedWeeklyRows(bundles) {
301
454
  for (const acc of groups.values()) {
302
455
  const usage = recomputeUsage(acc.events);
303
456
  const fallback = fallbackWeeklyUsage(acc.days);
457
+ // 整周累计:在整周合并曲线上一次性求正增量,跨天边界增量被计入,故 weekly ≥ Σ daily。
458
+ const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByWeek(curves.get(acc.personKey) ?? [], acc.week));
304
459
  rows.push({
305
460
  personKey: acc.personKey,
306
461
  week: acc.week,
@@ -314,6 +469,7 @@ function buildAggregatedWeeklyRows(bundles) {
314
469
  fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fallback.fiveHourLatestUsagePct,
315
470
  sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? fallback.sevenDayPeakUsagePct,
316
471
  sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? fallback.sevenDayLatestUsagePct,
472
+ sevenDayCumulativeUsagePct,
317
473
  uniqueSessions: acc.uniqueSessions,
318
474
  uniqueWorkspaces: acc.uniqueWorkspaces,
319
475
  });
@@ -214,6 +214,7 @@ function buildAggregatedDailyCsv(rows) {
214
214
  "fiveHourLatestUsagePct",
215
215
  "sevenDayPeakUsagePct",
216
216
  "sevenDayLatestUsagePct",
217
+ "sevenDayCumulativeUsagePct",
217
218
  "uniqueSessions",
218
219
  "uniqueWorkspaces",
219
220
  ];
@@ -230,6 +231,7 @@ function buildAggregatedDailyCsv(rows) {
230
231
  row.fiveHourLatestUsagePct,
231
232
  row.sevenDayPeakUsagePct,
232
233
  row.sevenDayLatestUsagePct,
234
+ row.sevenDayCumulativeUsagePct,
233
235
  row.uniqueSessions,
234
236
  row.uniqueWorkspaces,
235
237
  ]));
@@ -250,6 +252,7 @@ function buildAggregatedWeeklyCsv(rows) {
250
252
  "fiveHourLatestUsagePct",
251
253
  "sevenDayPeakUsagePct",
252
254
  "sevenDayLatestUsagePct",
255
+ "sevenDayCumulativeUsagePct",
253
256
  "uniqueSessions",
254
257
  "uniqueWorkspaces",
255
258
  ];
@@ -266,6 +269,7 @@ function buildAggregatedWeeklyCsv(rows) {
266
269
  row.fiveHourLatestUsagePct,
267
270
  row.sevenDayPeakUsagePct,
268
271
  row.sevenDayLatestUsagePct,
272
+ row.sevenDayCumulativeUsagePct,
269
273
  row.uniqueSessions,
270
274
  row.uniqueWorkspaces,
271
275
  ]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {