ccus-cli 0.1.12 → 0.1.14
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 +2 -2
- package/dist/lib/aggregate.js +89 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,12 +171,12 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
171
171
|
|
|
172
172
|
- 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
|
|
173
173
|
- `aggregate` 目前只接受 `schemaVersion: 6` 的 bundle;旧导出请先用当前版本重新 `ccus export`
|
|
174
|
-
- 同一个人在多台电脑上各自导出 bundle
|
|
174
|
+
- 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d)从选中事件按真实时间戳重算(peak 取 max、latest 取最新)
|
|
175
175
|
- `ccus aggregate --input-dir DIR --out-dir DIR`
|
|
176
176
|
- 输出三个文件:
|
|
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
|
-
- `weekly.csv
|
|
179
|
+
- `weekly.csv`:把同人同周的各天 winner(已按天去重)上卷累加得到,usage 从该周全部 winner 天的事件重算
|
|
180
180
|
- CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
|
|
181
181
|
- 想直接查看团队多人 dashboard,可以用 `ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]`:默认监听 `127.0.0.1` 上的随机端口,启动后会自动用系统默认浏览器打开,每次请求实时读取目录里的 bundle,不写入任何文件
|
|
182
182
|
|
package/dist/lib/aggregate.js
CHANGED
|
@@ -122,19 +122,26 @@ async function loadWeeklyExportBundles(inputDir) {
|
|
|
122
122
|
function bundlePersonKey(bundle) {
|
|
123
123
|
return toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
|
|
124
124
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
/**
|
|
126
|
+
* 某天 daySummary 的数据质量等级(越高越优先):
|
|
127
|
+
* 2 = 有 transcript 数据(userMessageCount / apiRequestCount > 0)
|
|
128
|
+
* 1 = 仅有 statusline 采样(sampleCount > 0,但 transcript 字段全为 0)
|
|
129
|
+
* 0 = 全空占位天
|
|
130
|
+
*
|
|
131
|
+
* transcript 级别优先于纯 sampleCount 级别,避免只有采样事件但无消息数的 bundle
|
|
132
|
+
* 抢走有真实 transcript 数据的 bundle 的 winner 位置。
|
|
133
|
+
*/
|
|
134
|
+
function dayDataTier(day) {
|
|
135
|
+
if (day.userMessageCount > 0 || day.apiRequestCount > 0)
|
|
136
|
+
return 2;
|
|
137
|
+
if (day.sampleCount > 0)
|
|
138
|
+
return 1;
|
|
139
|
+
return 0;
|
|
133
140
|
}
|
|
134
|
-
/** winner
|
|
135
|
-
function isBetterCandidate(
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
141
|
+
/** winner 比较:数据质量等级高优先,同级别内 generatedAt 较新优先,最后用 filePath 做稳定 tie-break。 */
|
|
142
|
+
function isBetterCandidate(nextTier, nextGeneratedAt, nextFilePath, currentTier, currentGeneratedAt, currentFilePath) {
|
|
143
|
+
if (nextTier !== currentTier) {
|
|
144
|
+
return nextTier > currentTier;
|
|
138
145
|
}
|
|
139
146
|
if (nextGeneratedAt !== currentGeneratedAt) {
|
|
140
147
|
return nextGeneratedAt > currentGeneratedAt;
|
|
@@ -150,28 +157,13 @@ function selectDailyWinners(bundles) {
|
|
|
150
157
|
for (const day of bundle.dailySummaries) {
|
|
151
158
|
const key = `${personKey}|${day.date}`;
|
|
152
159
|
const current = winners.get(key);
|
|
153
|
-
if (!current || isBetterCandidate(
|
|
160
|
+
if (!current || isBetterCandidate(dayDataTier(day), generatedAt, filePath, dayDataTier(current.day), current.generatedAt, current.filePath)) {
|
|
154
161
|
winners.set(key, { personKey, date: day.date, day, bundle, generatedAt, filePath });
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
return winners;
|
|
159
166
|
}
|
|
160
|
-
/** 对每个 (personKey, week) 选出 generatedAt 最新、且尽量有数据的那份 bundle。 */
|
|
161
|
-
function selectWeeklyWinners(bundles) {
|
|
162
|
-
const winners = new Map();
|
|
163
|
-
for (const { filePath, bundle } of bundles) {
|
|
164
|
-
const personKey = bundlePersonKey(bundle);
|
|
165
|
-
const week = weekKey(new Date(bundle.range.start));
|
|
166
|
-
const key = `${personKey}|${week}`;
|
|
167
|
-
const generatedAt = bundle.generatedAt ?? "";
|
|
168
|
-
const current = winners.get(key);
|
|
169
|
-
if (!current || isBetterCandidate(weekHasData(bundle), generatedAt, filePath, weekHasData(current.bundle), current.generatedAt, current.filePath)) {
|
|
170
|
-
winners.set(key, { personKey, week, bundle, generatedAt, filePath });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return winners;
|
|
174
|
-
}
|
|
175
167
|
/** 把 bundle 的 rawEvents 计算成 StatuslineEvent 并按本地自然日分组,结果做缓存复用。 */
|
|
176
168
|
const bundleEventsCache = new WeakMap();
|
|
177
169
|
function bundleEventsByDate(bundle) {
|
|
@@ -252,28 +244,78 @@ function buildAggregatedDailyRows(bundles) {
|
|
|
252
244
|
}
|
|
253
245
|
return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
|
|
254
246
|
}
|
|
255
|
-
/**
|
|
247
|
+
/** 周级 usage 回退:rawEvents 缺失时从各天 daySummary 自带值取 peak(max)/ latest(date 最新非空)。 */
|
|
248
|
+
function fallbackWeeklyUsage(days) {
|
|
249
|
+
const byDateDesc = [...days].sort((left, right) => right.date.localeCompare(left.date));
|
|
250
|
+
const fivePeaks = days.map((day) => day.fiveHourPeakUsagePct).filter((value) => value !== null);
|
|
251
|
+
const sevenPeaks = days.map((day) => day.sevenDayPeakUsagePct).filter((value) => value !== null);
|
|
252
|
+
return {
|
|
253
|
+
fiveHourPeakUsagePct: fivePeaks.length > 0 ? (0, time_1.roundNumber)(Math.max(...fivePeaks), 1) : null,
|
|
254
|
+
fiveHourLatestUsagePct: byDateDesc.find((day) => day.fiveHourLatestUsagePct !== null)?.fiveHourLatestUsagePct ?? null,
|
|
255
|
+
sevenDayPeakUsagePct: sevenPeaks.length > 0 ? (0, time_1.roundNumber)(Math.max(...sevenPeaks), 1) : null,
|
|
256
|
+
sevenDayLatestUsagePct: byDateDesc.find((day) => day.sevenDayLatestUsagePct !== null)?.sevenDayLatestUsagePct ?? null,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 展开 weekly.csv:一个人同一周有多份 bundle(多台电脑各导出)时,不取单独一份的整周汇总,
|
|
261
|
+
* 而是复用按天去重后的 daily winner(每天选有数据的那份),按 (person, 周) 把每天的 token / 计数累加上卷,
|
|
262
|
+
* usage 从该周所有 winner 天的事件重算(peak 取 max、latest 取时间戳最新),缺失时回退到 daySummary 自带值。
|
|
263
|
+
*/
|
|
256
264
|
function buildAggregatedWeeklyRows(bundles) {
|
|
257
|
-
const
|
|
265
|
+
const dailyWinners = selectDailyWinners(bundles);
|
|
266
|
+
const groups = new Map();
|
|
267
|
+
for (const winner of dailyWinners.values()) {
|
|
268
|
+
const week = weekKey(new Date(winner.bundle.range.start));
|
|
269
|
+
const key = `${winner.personKey}|${week}`;
|
|
270
|
+
let acc = groups.get(key);
|
|
271
|
+
if (!acc) {
|
|
272
|
+
acc = {
|
|
273
|
+
personKey: winner.personKey,
|
|
274
|
+
week,
|
|
275
|
+
userMessageCount: 0,
|
|
276
|
+
apiRequestCount: 0,
|
|
277
|
+
inputTokens: 0,
|
|
278
|
+
outputTokens: 0,
|
|
279
|
+
cacheReadInputTokens: 0,
|
|
280
|
+
sampleCount: 0,
|
|
281
|
+
uniqueSessions: 0,
|
|
282
|
+
uniqueWorkspaces: 0,
|
|
283
|
+
days: [],
|
|
284
|
+
events: [],
|
|
285
|
+
};
|
|
286
|
+
groups.set(key, acc);
|
|
287
|
+
}
|
|
288
|
+
const day = winner.day;
|
|
289
|
+
acc.userMessageCount += day.userMessageCount;
|
|
290
|
+
acc.apiRequestCount += day.apiRequestCount;
|
|
291
|
+
acc.inputTokens += day.inputTokens;
|
|
292
|
+
acc.outputTokens += day.outputTokens;
|
|
293
|
+
acc.cacheReadInputTokens += day.cacheReadInputTokens;
|
|
294
|
+
acc.sampleCount += day.sampleCount;
|
|
295
|
+
acc.uniqueSessions += day.uniqueSessions;
|
|
296
|
+
acc.uniqueWorkspaces += day.uniqueWorkspaces;
|
|
297
|
+
acc.days.push(day);
|
|
298
|
+
acc.events.push(...(bundleEventsByDate(winner.bundle).get(winner.date) ?? []));
|
|
299
|
+
}
|
|
258
300
|
const rows = [];
|
|
259
|
-
for (const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
301
|
+
for (const acc of groups.values()) {
|
|
302
|
+
const usage = recomputeUsage(acc.events);
|
|
303
|
+
const fallback = fallbackWeeklyUsage(acc.days);
|
|
262
304
|
rows.push({
|
|
263
|
-
personKey:
|
|
264
|
-
week:
|
|
265
|
-
userMessageCount:
|
|
266
|
-
apiRequestCount:
|
|
267
|
-
inputTokens:
|
|
268
|
-
outputTokens:
|
|
269
|
-
cacheReadInputTokens:
|
|
270
|
-
sampleCount:
|
|
271
|
-
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ??
|
|
272
|
-
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ??
|
|
273
|
-
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ??
|
|
274
|
-
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ??
|
|
275
|
-
uniqueSessions:
|
|
276
|
-
uniqueWorkspaces:
|
|
305
|
+
personKey: acc.personKey,
|
|
306
|
+
week: acc.week,
|
|
307
|
+
userMessageCount: acc.userMessageCount,
|
|
308
|
+
apiRequestCount: acc.apiRequestCount,
|
|
309
|
+
inputTokens: acc.inputTokens,
|
|
310
|
+
outputTokens: acc.outputTokens,
|
|
311
|
+
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
312
|
+
sampleCount: acc.sampleCount,
|
|
313
|
+
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? fallback.fiveHourPeakUsagePct,
|
|
314
|
+
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fallback.fiveHourLatestUsagePct,
|
|
315
|
+
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? fallback.sevenDayPeakUsagePct,
|
|
316
|
+
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? fallback.sevenDayLatestUsagePct,
|
|
317
|
+
uniqueSessions: acc.uniqueSessions,
|
|
318
|
+
uniqueWorkspaces: acc.uniqueWorkspaces,
|
|
277
319
|
});
|
|
278
320
|
}
|
|
279
321
|
return rows.sort((left, right) => `${left.personKey}|${left.week}`.localeCompare(`${right.personKey}|${right.week}`));
|