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.
- package/README.md +8 -4
- package/dist/cli.js +7 -1
- package/dist/lib/aggregate-dashboard.js +100 -166
- package/dist/lib/aggregate.js +250 -115
- package/dist/lib/chart-assets.js +485 -0
- package/dist/lib/dashboard.js +74 -138
- 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 +4 -3
package/dist/lib/aggregate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
102
|
-
continue;
|
|
107
|
+
return { filePath, bundle: parsed };
|
|
103
108
|
}
|
|
104
|
-
|
|
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
|
-
/**
|
|
144
|
-
|
|
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
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
281
|
+
* 每个点的累计 = 已锁定的各完整段贡献 + 当前段 (segMax − segMin);段内更新 / reset 都不会让它回落,
|
|
282
|
+
* 所以曲线单调非递减、终点恒等于 {@link computeCumulativeSevenDay} 的标量返回值。输入建议先经 deburr 去毛刺。
|
|
283
|
+
* 无有效样本返回空数组;单样本返回单点 0。
|
|
209
284
|
*/
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
.
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
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 =
|
|
220
|
-
let segMax =
|
|
221
|
-
for (let index = 1; index <
|
|
222
|
-
const value =
|
|
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
|
-
|
|
238
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
463
|
+
/** 展开 detail.csv:同人同天各机器的代表 bundle 事件都列出来,token 总量随本机器当天的 daySummary 附带。 */
|
|
350
464
|
function buildAggregatedDetailRows(bundles) {
|
|
351
|
-
const
|
|
465
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
352
466
|
const rows = [];
|
|
353
|
-
for (const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* 展开 daily.csv:同人同天的不同机器数据直接叠加(计数字段相加),usage 从所有机器该天事件合并后重算。
|
|
487
|
+
* 同机器重复导出由 selectDailyRepresentatives 在分组阶段去重,不会翻倍。
|
|
488
|
+
*/
|
|
370
489
|
function buildAggregatedDailyRows(bundles) {
|
|
371
|
-
const
|
|
490
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
372
491
|
const curves = buildPersonSevenDayCurve(bundles);
|
|
373
492
|
const rows = [];
|
|
374
|
-
for (const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
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
|
|
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 ??
|
|
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
|
|
394
|
-
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
|
|
413
|
-
*
|
|
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
|
|
550
|
+
const repsMap = selectDailyRepresentatives(bundles);
|
|
418
551
|
const curves = buildPersonSevenDayCurve(bundles);
|
|
419
552
|
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
|
-
|
|
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()) {
|