ccus-cli 0.1.18 → 0.1.21
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 +6 -2
- package/dist/cli.js +7 -1
- package/dist/lib/aggregate-dashboard.js +100 -166
- package/dist/lib/aggregate.js +174 -84
- package/dist/lib/chart-assets.js +466 -0
- package/dist/lib/dashboard.js +49 -134
- package/dist/vendor/uplot.LICENSE +21 -0
- package/dist/vendor/uplot.iife.min.js +2 -0
- package/dist/vendor/uplot.min.css +1 -0
- package/package.json +5 -4
package/dist/lib/aggregate.js
CHANGED
|
@@ -16,6 +16,10 @@ const node_zlib_1 = require("node:zlib");
|
|
|
16
16
|
const payload_1 = require("./payload");
|
|
17
17
|
const time_1 = require("./time");
|
|
18
18
|
const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
|
|
19
|
+
function maxOrNull(values) {
|
|
20
|
+
const numbers = values.filter((v) => v !== null);
|
|
21
|
+
return numbers.length > 0 ? Math.max(...numbers) : null;
|
|
22
|
+
}
|
|
19
23
|
function isRecord(value) {
|
|
20
24
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
25
|
}
|
|
@@ -91,20 +95,29 @@ async function readBundleFileContent(filePath) {
|
|
|
91
95
|
/** 读取目录里的 export bundle json 文件。 */
|
|
92
96
|
async function loadWeeklyExportBundles(inputDir) {
|
|
93
97
|
const files = await collectBundleJsonFiles(inputDir);
|
|
94
|
-
const bundles = [];
|
|
95
98
|
const invalidFiles = [];
|
|
96
|
-
|
|
99
|
+
const results = await Promise.all(files.map(async (filePath) => {
|
|
97
100
|
try {
|
|
98
101
|
const content = await readBundleFileContent(filePath);
|
|
99
102
|
const parsed = JSON.parse(content);
|
|
100
103
|
if (isWeeklyExportBundle(parsed)) {
|
|
101
|
-
|
|
102
|
-
continue;
|
|
104
|
+
return { filePath, bundle: parsed };
|
|
103
105
|
}
|
|
104
|
-
|
|
106
|
+
return { filePath, bundle: null };
|
|
105
107
|
}
|
|
106
108
|
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
const bundles = [];
|
|
113
|
+
for (const result of results) {
|
|
114
|
+
if (result === null)
|
|
107
115
|
continue;
|
|
116
|
+
if (result.bundle === null) {
|
|
117
|
+
invalidFiles.push(result.filePath);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
bundles.push({ filePath: result.filePath, bundle: result.bundle });
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
123
|
if (invalidFiles.length > 0) {
|
|
@@ -140,31 +153,87 @@ function dayDataTier(day) {
|
|
|
140
153
|
return 1;
|
|
141
154
|
return 0;
|
|
142
155
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
156
|
+
/**
|
|
157
|
+
* winner 比较:数据质量等级高优先;同 tier=2 時消息数多优先(避免"仅 generatedAt 更新"的低活跃机器
|
|
158
|
+
* 覆盖同一天高活跃机器的数据);同 count 内 generatedAt 较新优先;最后用 filePath 做稳定 tie-break。
|
|
159
|
+
*/
|
|
160
|
+
function isBetterCandidate(nextTier, nextMsgCount, nextApiCount, nextGeneratedAt, nextFilePath, currentTier, currentMsgCount, currentApiCount, currentGeneratedAt, currentFilePath) {
|
|
145
161
|
if (nextTier !== currentTier) {
|
|
146
162
|
return nextTier > currentTier;
|
|
147
163
|
}
|
|
164
|
+
if (nextTier === 2) {
|
|
165
|
+
if (nextMsgCount !== currentMsgCount) {
|
|
166
|
+
return nextMsgCount > currentMsgCount;
|
|
167
|
+
}
|
|
168
|
+
if (nextApiCount !== currentApiCount) {
|
|
169
|
+
return nextApiCount > currentApiCount;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
148
172
|
if (nextGeneratedAt !== currentGeneratedAt) {
|
|
149
173
|
return nextGeneratedAt > currentGeneratedAt;
|
|
150
174
|
}
|
|
151
175
|
return nextFilePath > currentFilePath;
|
|
152
176
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
/**
|
|
178
|
+
* 同一 (personKey, date) 的所有 bundle,按 rawEvents sessionId 集合的交集分组:
|
|
179
|
+
* - 有交集的视为同机器重复导出(同一账号同一天的会话在两份 bundle 里均存在),只取最优 winner
|
|
180
|
+
* - 无交集的视为不同机器的独立数据,分别保留,后续叠加
|
|
181
|
+
*
|
|
182
|
+
* sessionId 集合为空的候选(该天没有 statusline 事件)不参与交集判断,单独成组。
|
|
183
|
+
*/
|
|
184
|
+
function selectDailyRepresentatives(bundles) {
|
|
185
|
+
// 收集每个 (personKey, date) 的所有候选
|
|
186
|
+
const candidatesByKey = new Map();
|
|
156
187
|
for (const { filePath, bundle } of bundles) {
|
|
157
188
|
const personKey = bundlePersonKey(bundle);
|
|
158
189
|
const generatedAt = bundle.generatedAt ?? "";
|
|
190
|
+
const eventsByDate = bundleEventsByDate(bundle);
|
|
159
191
|
for (const day of bundle.dailySummaries) {
|
|
160
192
|
const key = `${personKey}|${day.date}`;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
const events = eventsByDate.get(day.date) ?? [];
|
|
194
|
+
const sessionIds = new Set(events.map((e) => e.sessionId).filter((s) => s !== null));
|
|
195
|
+
const list = candidatesByKey.get(key) ?? [];
|
|
196
|
+
list.push({ day, bundle, generatedAt, filePath, sessionIds });
|
|
197
|
+
candidatesByKey.set(key, list);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const result = new Map();
|
|
201
|
+
for (const [key, candidates] of candidatesByKey.entries()) {
|
|
202
|
+
const barIdx = key.indexOf("|");
|
|
203
|
+
const personKey = key.slice(0, barIdx);
|
|
204
|
+
const date = key.slice(barIdx + 1);
|
|
205
|
+
// 贪心分组:候选有 sessionId 且与某组内任意候选的 sessionId 有交集,则并入该组;否则新建组
|
|
206
|
+
const groups = [];
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
let added = false;
|
|
209
|
+
if (candidate.sessionIds.size > 0) {
|
|
210
|
+
for (const group of groups) {
|
|
211
|
+
const hasOverlap = group.some((c) => c.sessionIds.size > 0 && [...candidate.sessionIds].some((s) => c.sessionIds.has(s)));
|
|
212
|
+
if (hasOverlap) {
|
|
213
|
+
group.push(candidate);
|
|
214
|
+
added = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!added) {
|
|
220
|
+
groups.push([candidate]);
|
|
164
221
|
}
|
|
165
222
|
}
|
|
223
|
+
// 每组取最优代表(同机器多次导出只保留一份)
|
|
224
|
+
const reps = groups.map((group) => {
|
|
225
|
+
let best = group[0];
|
|
226
|
+
for (let i = 1; i < group.length; i++) {
|
|
227
|
+
const c = group[i];
|
|
228
|
+
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)) {
|
|
229
|
+
best = c;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { personKey, date, day: best.day, bundle: best.bundle };
|
|
233
|
+
});
|
|
234
|
+
result.set(key, reps);
|
|
166
235
|
}
|
|
167
|
-
return
|
|
236
|
+
return result;
|
|
168
237
|
}
|
|
169
238
|
/** 把 bundle 的 rawEvents 计算成 StatuslineEvent 并按本地自然日分组,结果做缓存复用。 */
|
|
170
239
|
const bundleEventsCache = new WeakMap();
|
|
@@ -346,52 +415,72 @@ function recomputeUsage(events) {
|
|
|
346
415
|
sevenDayLatestUsagePct: newestFirst.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null,
|
|
347
416
|
};
|
|
348
417
|
}
|
|
349
|
-
/**
|
|
418
|
+
/** 展开 detail.csv:同人同天各机器的代表 bundle 事件都列出来,token 总量随本机器当天的 daySummary 附带。 */
|
|
350
419
|
function buildAggregatedDetailRows(bundles) {
|
|
351
|
-
const
|
|
420
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
352
421
|
const rows = [];
|
|
353
|
-
for (const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
422
|
+
for (const reps of repsMap.values()) {
|
|
423
|
+
for (const rep of reps) {
|
|
424
|
+
const events = bundleEventsByDate(rep.bundle).get(rep.date) ?? [];
|
|
425
|
+
for (const event of events) {
|
|
426
|
+
rows.push({
|
|
427
|
+
...event,
|
|
428
|
+
personKey: rep.personKey,
|
|
429
|
+
weekKey: weekKey(new Date(event.timestamp)),
|
|
430
|
+
dateKey: rep.date,
|
|
431
|
+
inputTokens: rep.day.inputTokens,
|
|
432
|
+
outputTokens: rep.day.outputTokens,
|
|
433
|
+
cacheReadInputTokens: rep.day.cacheReadInputTokens,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
365
436
|
}
|
|
366
437
|
}
|
|
367
438
|
return rows.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
368
439
|
}
|
|
369
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* 展开 daily.csv:同人同天的不同机器数据直接叠加(计数字段相加),usage 从所有机器该天事件合并后重算。
|
|
442
|
+
* 同机器重复导出由 selectDailyRepresentatives 在分组阶段去重,不会翻倍。
|
|
443
|
+
*/
|
|
370
444
|
function buildAggregatedDailyRows(bundles) {
|
|
371
|
-
const
|
|
445
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
372
446
|
const curves = buildPersonSevenDayCurve(bundles);
|
|
373
447
|
const rows = [];
|
|
374
|
-
for (const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
448
|
+
for (const reps of repsMap.values()) {
|
|
449
|
+
const { personKey, date } = reps[0];
|
|
450
|
+
// 不同机器的独立数据直接叠加
|
|
451
|
+
const userMessageCount = reps.reduce((sum, r) => sum + r.day.userMessageCount, 0);
|
|
452
|
+
const apiRequestCount = reps.reduce((sum, r) => sum + r.day.apiRequestCount, 0);
|
|
453
|
+
const inputTokens = reps.reduce((sum, r) => sum + r.day.inputTokens, 0);
|
|
454
|
+
const outputTokens = reps.reduce((sum, r) => sum + r.day.outputTokens, 0);
|
|
455
|
+
const cacheReadInputTokens = reps.reduce((sum, r) => sum + r.day.cacheReadInputTokens, 0);
|
|
456
|
+
const sampleCount = reps.reduce((sum, r) => sum + r.day.sampleCount, 0);
|
|
457
|
+
const uniqueSessions = reps.reduce((sum, r) => sum + r.day.uniqueSessions, 0);
|
|
458
|
+
const uniqueWorkspaces = reps.reduce((sum, r) => sum + r.day.uniqueWorkspaces, 0);
|
|
459
|
+
// usage 从所有机器该天事件合并后重算;rawEvents 缺失时用各代表 daySummary 回退
|
|
460
|
+
const allEvents = reps.flatMap((r) => bundleEventsByDate(r.bundle).get(date) ?? []);
|
|
461
|
+
const usage = recomputeUsage(allEvents);
|
|
462
|
+
const fiveHourPeakFallback = maxOrNull(reps.map((r) => r.day.fiveHourPeakUsagePct));
|
|
463
|
+
const sevenDayPeakFallback = maxOrNull(reps.map((r) => r.day.sevenDayPeakUsagePct));
|
|
464
|
+
const fiveHourLatestFallback = reps.find((r) => r.day.fiveHourLatestUsagePct !== null)?.day.fiveHourLatestUsagePct ?? null;
|
|
465
|
+
const sevenDayLatestFallback = reps.find((r) => r.day.sevenDayLatestUsagePct !== null)?.day.sevenDayLatestUsagePct ?? null;
|
|
466
|
+
// 累计指标走全样本合并曲线,不走单机的 recomputeUsage,避免漏掉另一台机器的样本。
|
|
467
|
+
const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByDate(curves.get(personKey) ?? [], date));
|
|
379
468
|
rows.push({
|
|
380
|
-
personKey
|
|
381
|
-
date
|
|
382
|
-
userMessageCount
|
|
383
|
-
apiRequestCount
|
|
384
|
-
inputTokens
|
|
385
|
-
outputTokens
|
|
386
|
-
cacheReadInputTokens
|
|
387
|
-
sampleCount
|
|
388
|
-
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ??
|
|
389
|
-
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ??
|
|
390
|
-
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ??
|
|
391
|
-
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ??
|
|
469
|
+
personKey,
|
|
470
|
+
date,
|
|
471
|
+
userMessageCount,
|
|
472
|
+
apiRequestCount,
|
|
473
|
+
inputTokens,
|
|
474
|
+
outputTokens,
|
|
475
|
+
cacheReadInputTokens,
|
|
476
|
+
sampleCount,
|
|
477
|
+
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? fiveHourPeakFallback,
|
|
478
|
+
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fiveHourLatestFallback,
|
|
479
|
+
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? sevenDayPeakFallback,
|
|
480
|
+
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? sevenDayLatestFallback,
|
|
392
481
|
sevenDayCumulativeUsagePct,
|
|
393
|
-
uniqueSessions
|
|
394
|
-
uniqueWorkspaces
|
|
482
|
+
uniqueSessions,
|
|
483
|
+
uniqueWorkspaces,
|
|
395
484
|
});
|
|
396
485
|
}
|
|
397
486
|
return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
|
|
@@ -409,46 +498,47 @@ function fallbackWeeklyUsage(days) {
|
|
|
409
498
|
};
|
|
410
499
|
}
|
|
411
500
|
/**
|
|
412
|
-
* 展开 weekly.csv
|
|
413
|
-
*
|
|
414
|
-
* usage 从该周所有 winner 天的事件重算(peak 取 max、latest 取时间戳最新),缺失时回退到 daySummary 自带值。
|
|
501
|
+
* 展开 weekly.csv:不同机器的同人同天数据已在 selectDailyRepresentatives 层按 sessionId 去重分组,
|
|
502
|
+
* 这里直接按 (person, 周) 把所有代表的 token / 计数累加上卷,usage 从该周所有代表事件重算。
|
|
415
503
|
*/
|
|
416
504
|
function buildAggregatedWeeklyRows(bundles) {
|
|
417
|
-
const
|
|
505
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
418
506
|
const curves = buildPersonSevenDayCurve(bundles);
|
|
419
507
|
const groups = new Map();
|
|
420
|
-
for (const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
acc
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
508
|
+
for (const reps of repsMap.values()) {
|
|
509
|
+
for (const rep of reps) {
|
|
510
|
+
const week = weekKey(new Date(rep.bundle.range.start));
|
|
511
|
+
const key = `${rep.personKey}|${week}`;
|
|
512
|
+
let acc = groups.get(key);
|
|
513
|
+
if (!acc) {
|
|
514
|
+
acc = {
|
|
515
|
+
personKey: rep.personKey,
|
|
516
|
+
week,
|
|
517
|
+
userMessageCount: 0,
|
|
518
|
+
apiRequestCount: 0,
|
|
519
|
+
inputTokens: 0,
|
|
520
|
+
outputTokens: 0,
|
|
521
|
+
cacheReadInputTokens: 0,
|
|
522
|
+
sampleCount: 0,
|
|
523
|
+
uniqueSessions: 0,
|
|
524
|
+
uniqueWorkspaces: 0,
|
|
525
|
+
days: [],
|
|
526
|
+
events: [],
|
|
527
|
+
};
|
|
528
|
+
groups.set(key, acc);
|
|
529
|
+
}
|
|
530
|
+
const day = rep.day;
|
|
531
|
+
acc.userMessageCount += day.userMessageCount;
|
|
532
|
+
acc.apiRequestCount += day.apiRequestCount;
|
|
533
|
+
acc.inputTokens += day.inputTokens;
|
|
534
|
+
acc.outputTokens += day.outputTokens;
|
|
535
|
+
acc.cacheReadInputTokens += day.cacheReadInputTokens;
|
|
536
|
+
acc.sampleCount += day.sampleCount;
|
|
537
|
+
acc.uniqueSessions += day.uniqueSessions;
|
|
538
|
+
acc.uniqueWorkspaces += day.uniqueWorkspaces;
|
|
539
|
+
acc.days.push(day);
|
|
540
|
+
acc.events.push(...(bundleEventsByDate(rep.bundle).get(rep.date) ?? []));
|
|
440
541
|
}
|
|
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
542
|
}
|
|
453
543
|
const rows = [];
|
|
454
544
|
for (const acc of groups.values()) {
|