ccus-cli 0.2.10 → 0.2.11
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 -3
- package/dist/cli.js +54 -16
- package/dist/lib/aggregate-dashboard.js +67 -8
- package/dist/lib/aggregate.js +113 -23
- package/dist/lib/api-equivalent-cost.js +189 -0
- package/dist/lib/api-pricing-catalog.json +270 -0
- package/dist/lib/api-pricing-table.js +163 -0
- package/dist/lib/claude.js +52 -9
- package/dist/lib/codex-sessions.js +70 -21
- package/dist/lib/dashboard-pages.js +15 -0
- package/dist/lib/dashboard.js +35 -1
- package/dist/lib/export.js +12 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ ccus aggregate --input-dir ./team-exports --out-dir ./team-report
|
|
|
147
147
|
ccus aggregate serve --input-dir ./team-exports
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`serve`
|
|
150
|
+
`dashboard build/open` 会在看板同目录生成独立 `pricing.html`。两种 `serve` 都通过 `/pricing.html` 提供当前价格页;看板的合计成本卡下可直接进入。`serve` 默认监听 `127.0.0.1` 上的随机端口,并在每次请求时实时读取最新日志生成页面。`dashboard serve` 默认查看 `this-week`(整周)使用量曲线(`build` / `open` 仍默认 `today`),可用 `--range` 覆盖。
|
|
151
151
|
|
|
152
152
|
其中:
|
|
153
153
|
|
|
@@ -167,7 +167,9 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
167
167
|
- 默认输出一个 `json` 数据包,里面同时包含 `rawEvents`、`weeklySummary`、`dailySummaries`
|
|
168
168
|
- 导出文件内容为**紧凑 JSON**(无缩进),并默认 **gzip 压缩**后写成 `.json.gz`;gzip 与紧凑化都只是存储/展示层变化,解压后的字段集合与 `schemaVersion` 不变
|
|
169
169
|
- 默认写 `.json.gz`;若用 `--out` 指定一个非 `.gz` 结尾的路径,则按明文 JSON 写出(不压缩)
|
|
170
|
-
- 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `
|
|
170
|
+
- 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `10`。顶层 `pricing` 记录本地价格目录版本、USD 币种和 `event-time-standard-api` 基准;`weeklySummary.apiEquivalentCost` 与 `dailySummaries[].apiEquivalentCost` 分别包含 `claude`、`codex`、`total` 的金额及定价覆盖度。v10 延续 v9 的 Codex token 口径:`inputTokens = max(0, input_tokens - cached_input_tokens)`,缓存输入由 `cacheReadInputTokens` 单列
|
|
171
|
+
- 等效 API 成本按每次请求的模型、发生时间和 token 分类套用内置标准同步 API 价格。Claude 区分输入、输出、缓存读取及 5 分钟/1 小时缓存写入;Codex 区分净输入、缓存输入和输出。它不是 Claude/Codex 订阅账单,也不含税费、折扣、批处理、工具附加费或区域溢价
|
|
172
|
+
- 未知模型不会丢弃:有部分请求可定价时金额是已知小计,页面显示“至少为”;全部请求均无法定价时金额为不可用。个人 dashboard 在顶部第五张卡显示 Claude 与 Codex 的合计成本,不显示来源分项。全部模型价格集中维护在 `src/lib/api-pricing-catalog.json`,价格目录随 ccus 发布,统计过程不联网;个人与多人 dashboard 的合计成本卡下链接独立 `pricing.html` 并在新页面打开,价格页只保留一套标题,Codex 排在 Claude 前并按模型版本从新到旧排列。当前 Claude 目录包含 Opus 4.7、Opus 4.8、Opus 5、Sonnet 5 和 Fable 5;Sonnet 5 按事件时间区分活动价与标准价
|
|
171
173
|
- 默认文件名会带 git email 的帐号名前缀和起止日期,例如:`alice_export_2026-05-26_to_2026-06-01.json.gz`
|
|
172
174
|
- `userMessageCount` 来自 `~/.claude/projects/**/*.jsonl` 的非 meta `type:user` 事件
|
|
173
175
|
- `apiRequestCount` 与 token 指标来自 `~/.claude/projects/**/*.jsonl` 中带 `message.usage` 的 `type:assistant` 事件
|
|
@@ -184,13 +186,14 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
184
186
|
## 多人汇总
|
|
185
187
|
|
|
186
188
|
- 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
|
|
187
|
-
- `aggregate` 接受 `schemaVersion: 6/7/8/9
|
|
189
|
+
- `aggregate` 接受 `schemaVersion: 6/7/8/9/10`。v6–v9 继续聚合原统计;存在 API 请求时成本为空并全部计入未定价,没有请求时成本为 0。旧版与 v10 或不同价格目录混用时,`pricingCatalogVersion` 按规则标记为 `mixed`
|
|
188
190
|
- 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d)从选中事件按真实时间戳重算,Claude+Codex 合并:peak 取两源 max、latest 两源相加
|
|
189
191
|
- `ccus aggregate --input-dir DIR --out-dir DIR`
|
|
190
192
|
- 输出三个文件:
|
|
191
193
|
- `detail.csv`:来自 winner bundle 的 `rawEvents`(同人同天只展开最新那份的事件),`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
|
|
192
194
|
- `daily.csv`:同人同天取最新导出 bundle 的 `dailySummaries`,usage 从该 bundle 当天事件重算
|
|
193
195
|
- `weekly.csv`:把同人同周的各天 winner(已按天去重)上卷累加得到,usage 从该周全部 winner 天的事件重算
|
|
196
|
+
- `daily.csv` / `weekly.csv` 在列尾包含 `estimatedApiEquivalentCostUsd`、`pricingCatalogVersion`,金额最多 6 位小数,无法定价时留空;成本沿 token 的按天代表选择与周上卷路径去重。`pricedApiRequestCount`、`unpricedApiRequestCount` 保留在 bundle 与 dashboard,不写入 CSV;`detail.csv` 保持原列集合,不包含成本字段
|
|
194
197
|
- `daily.csv` / `weekly.csv` 还各带一列 `sevenDayCumulativeUsagePct`:7 天额度的**累计真实使用量**,把锯齿波(涨到峰值→窗口重置归零→再涨)还原成实际消耗。计算分两层:先**去毛刺**(把持续短于 2 分钟的 stale 瞬时读数尖峰抹平,只保留真实持续的水平),再做**分段峰谷和**(按 reset 跌破段峰值一半切成上升段,每段累加「段内峰值−谷值」)。以百分比累加表达、可大于 100(用掉多于一个 7d 额度)。对 ±1 采样抖动和 stale 读数尖峰都鲁棒,不会被噪声虚增
|
|
195
198
|
- 该列**绕开按天 winner**,对同一个人**所有机器、所有周**的 bundle `rawEvents`(含 Claude 与 Codex 两源 7d 读数,codex 事件不再过滤)先按时间合并去重成一条账号级曲线再算,绝不分机相加(同账号 7d 额度共享)
|
|
196
199
|
- 区间内第一个有效样本无前值、不贡献增量,所以 **`daily.csv` 逐行相加只是对全局总量的近似**(跨天边界增量不计入单天);`weekly.csv` 在整周连续算、把跨天增量也计入,更连续,恒有 `weekly ≥ Σ 同周 daily`,想要更准的总量请看 weekly 这列
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,8 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
45
45
|
const dashboard_1 = require("./lib/dashboard");
|
|
46
46
|
const dashboard_2 = require("./lib/dashboard");
|
|
47
47
|
const aggregate_dashboard_1 = require("./lib/aggregate-dashboard");
|
|
48
|
+
const api_pricing_table_1 = require("./lib/api-pricing-table");
|
|
49
|
+
const dashboard_pages_1 = require("./lib/dashboard-pages");
|
|
48
50
|
const claude_1 = require("./lib/claude");
|
|
49
51
|
const codex_sessions_1 = require("./lib/codex-sessions");
|
|
50
52
|
const export_1 = require("./lib/export");
|
|
@@ -64,6 +66,7 @@ const sync_1 = require("./lib/sync");
|
|
|
64
66
|
const storage_1 = require("./lib/storage");
|
|
65
67
|
const time_1 = require("./lib/time");
|
|
66
68
|
const update_check_1 = require("./lib/update-check");
|
|
69
|
+
const api_equivalent_cost_1 = require("./lib/api-equivalent-cost");
|
|
67
70
|
const version_1 = require("./lib/version");
|
|
68
71
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
69
72
|
function printHelp() {
|
|
@@ -245,10 +248,10 @@ async function handleStatuslineEmit(options) {
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
/**
|
|
248
|
-
*
|
|
251
|
+
* 加载 dashboard 渲染数据:statusline 事件、Claude/Codex 每日消息数与标准 API 等效成本。
|
|
249
252
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
253
|
+
* 消息数和成本与导出契约同源,当前结果仅供 dashboard 展示。
|
|
254
|
+
* `defaultRange` 让 build/open 与 serve 各自决定缺省窗口。
|
|
252
255
|
*/
|
|
253
256
|
async function loadDashboardData(options, defaultRange) {
|
|
254
257
|
const dataDir = getDataDir(options);
|
|
@@ -268,8 +271,14 @@ async function loadDashboardData(options, defaultRange) {
|
|
|
268
271
|
userMessageCount: claudeDailyUsage.get(date)?.userMessageCount ?? 0,
|
|
269
272
|
codexUserMessageCount: codexDailyUsage.get(date)?.userMessageCount ?? 0,
|
|
270
273
|
}));
|
|
274
|
+
const claudeCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...claudeDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
275
|
+
const codexCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...codexDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
271
276
|
(0, debug_1.debugLog)("dashboard", "events loaded", { range, label: window.label, sampleCount: events.length, days: dailyUserMessages.length });
|
|
272
|
-
const html = (0, dashboard_1.buildDashboardHtml)(events, window.label, window.start, window.end, dailyUserMessages
|
|
277
|
+
const html = (0, dashboard_1.buildDashboardHtml)(events, window.label, window.start, window.end, dailyUserMessages, {
|
|
278
|
+
claude: claudeCost,
|
|
279
|
+
codex: codexCost,
|
|
280
|
+
total: (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([claudeCost, codexCost]),
|
|
281
|
+
}, api_equivalent_cost_1.API_PRICING_METADATA.catalogVersion);
|
|
273
282
|
return { html, window };
|
|
274
283
|
}
|
|
275
284
|
/** 构建 dashboard 的公共逻辑,build/open 复用同一套生成流程。 */
|
|
@@ -277,8 +286,16 @@ async function buildDashboard(options) {
|
|
|
277
286
|
const dataDir = getDataDir(options);
|
|
278
287
|
const { html, window } = await loadDashboardData(options, "today");
|
|
279
288
|
const outputPath = node_path_1.default.resolve(getStringOption(options, "out") ?? node_path_1.default.join((0, paths_1.getDashboardDir)(dataDir), `${window.label}.html`));
|
|
280
|
-
|
|
281
|
-
|
|
289
|
+
if (node_path_1.default.basename(outputPath).toLowerCase() === api_pricing_table_1.API_PRICING_PAGE_FILE.toLowerCase()) {
|
|
290
|
+
throw new Error(`--out cannot be named ${api_pricing_table_1.API_PRICING_PAGE_FILE}.`);
|
|
291
|
+
}
|
|
292
|
+
const pricingPath = node_path_1.default.join(node_path_1.default.dirname(outputPath), api_pricing_table_1.API_PRICING_PAGE_FILE);
|
|
293
|
+
const pricingHtml = (0, api_pricing_table_1.buildApiPricingPage)();
|
|
294
|
+
await Promise.all([
|
|
295
|
+
(0, export_1.writeTextFile)(outputPath, html),
|
|
296
|
+
(0, export_1.writeTextFile)(pricingPath, pricingHtml),
|
|
297
|
+
]);
|
|
298
|
+
(0, debug_1.debugLog)("dashboard", "html written", { outputPath, pricingPath, bytes: html.length });
|
|
282
299
|
return outputPath;
|
|
283
300
|
}
|
|
284
301
|
/** 直接返回 dashboard HTML 字符串,供本地 HTTP 服务复用。serve 默认看整周使用量曲线。 */
|
|
@@ -309,9 +326,15 @@ async function handleDashboardServe(options) {
|
|
|
309
326
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
310
327
|
throw new Error(`Invalid port: ${portOption ?? ""}`);
|
|
311
328
|
}
|
|
312
|
-
const server = node_http_1.default.createServer(async (
|
|
329
|
+
const server = node_http_1.default.createServer(async (request, response) => {
|
|
330
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
313
331
|
try {
|
|
314
|
-
const html = await renderDashboardHtml(options);
|
|
332
|
+
const html = await (0, dashboard_pages_1.renderDashboardPage)(url.pathname, () => renderDashboardHtml(options));
|
|
333
|
+
if (html === null) {
|
|
334
|
+
response.writeHead(404);
|
|
335
|
+
response.end();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
315
338
|
response.writeHead(200, {
|
|
316
339
|
"Content-Type": "text/html; charset=utf-8",
|
|
317
340
|
"Cache-Control": "no-store",
|
|
@@ -378,6 +401,8 @@ async function runExport(options) {
|
|
|
378
401
|
const claudeDailyUsage = await (0, claude_1.summarizeClaudeProjectUsageByDay)(window.start, window.end);
|
|
379
402
|
const codexUsage = await (0, codex_sessions_1.summarizeCodexSessionUsage)(window.start, window.end);
|
|
380
403
|
const codexDailyUsage = await (0, codex_sessions_1.summarizeCodexSessionUsageByDay)(window.start, window.end);
|
|
404
|
+
const claudeWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...claudeDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
405
|
+
const codexWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...codexDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
381
406
|
(0, debug_1.debugLog)("export", "data collected", {
|
|
382
407
|
statuslineSamples: records.length,
|
|
383
408
|
claudeProjectFiles: claudeUsage.matchedFileCount,
|
|
@@ -395,7 +420,7 @@ async function runExport(options) {
|
|
|
395
420
|
exportUserName = gitIdentity.userName;
|
|
396
421
|
}
|
|
397
422
|
const weeklySummary = {
|
|
398
|
-
schemaVersion:
|
|
423
|
+
schemaVersion: 10,
|
|
399
424
|
generatedAt: new Date().toISOString(),
|
|
400
425
|
range: {
|
|
401
426
|
label: window.label,
|
|
@@ -426,6 +451,11 @@ async function runExport(options) {
|
|
|
426
451
|
sevenDayPeakUsagePct: codexStatuslineSummary.sevenDayPeakUsagePct,
|
|
427
452
|
sevenDayLatestUsagePct: codexStatuslineSummary.sevenDayLatestUsagePct,
|
|
428
453
|
},
|
|
454
|
+
apiEquivalentCost: {
|
|
455
|
+
claude: claudeWeeklyCost,
|
|
456
|
+
codex: codexWeeklyCost,
|
|
457
|
+
total: (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([claudeWeeklyCost, codexWeeklyCost]),
|
|
458
|
+
},
|
|
429
459
|
statusline: {
|
|
430
460
|
sampleCount: statuslineSummary.sampleCount,
|
|
431
461
|
uniqueSessions: statuslineSummary.uniqueSessions,
|
|
@@ -451,6 +481,8 @@ async function runExport(options) {
|
|
|
451
481
|
const claudeDay = claudeDailyUsage.get(date);
|
|
452
482
|
const codexDay = codexDailyUsage.get(date);
|
|
453
483
|
const codexRow = codexStatuslineDailyMap.get(date);
|
|
484
|
+
const claudeCost = claudeDay?.apiEquivalentCost ?? (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
|
|
485
|
+
const codexCost = codexDay?.apiEquivalentCost ?? (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
|
|
454
486
|
return {
|
|
455
487
|
date,
|
|
456
488
|
userMessageCount: claudeDay?.userMessageCount ?? 0,
|
|
@@ -469,6 +501,11 @@ async function runExport(options) {
|
|
|
469
501
|
sevenDayPeakUsagePct: codexRow?.sevenDayPeakUsagePct ?? null,
|
|
470
502
|
sevenDayLatestUsagePct: codexRow?.sevenDayLatestUsagePct ?? null,
|
|
471
503
|
},
|
|
504
|
+
apiEquivalentCost: {
|
|
505
|
+
claude: claudeCost,
|
|
506
|
+
codex: codexCost,
|
|
507
|
+
total: (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([claudeCost, codexCost]),
|
|
508
|
+
},
|
|
472
509
|
sampleCount: row?.sampleCount ?? 0,
|
|
473
510
|
fiveHourLatestUsagePct: row?.fiveHourLatestUsagePct ?? null,
|
|
474
511
|
fiveHourPeakUsagePct: row?.fiveHourPeakUsagePct ?? null,
|
|
@@ -479,7 +516,7 @@ async function runExport(options) {
|
|
|
479
516
|
};
|
|
480
517
|
});
|
|
481
518
|
const bundle = {
|
|
482
|
-
schemaVersion:
|
|
519
|
+
schemaVersion: 10,
|
|
483
520
|
generatedAt: new Date().toISOString(),
|
|
484
521
|
range: {
|
|
485
522
|
label: window.label,
|
|
@@ -490,6 +527,7 @@ async function runExport(options) {
|
|
|
490
527
|
gitUserName: exportUserName,
|
|
491
528
|
gitUserEmail: exportUserEmail,
|
|
492
529
|
},
|
|
530
|
+
pricing: api_equivalent_cost_1.API_PRICING_METADATA,
|
|
493
531
|
rawEvents: records,
|
|
494
532
|
weeklySummary,
|
|
495
533
|
dailySummaries,
|
|
@@ -574,13 +612,13 @@ async function handleAggregateServe(options) {
|
|
|
574
612
|
}
|
|
575
613
|
const server = node_http_1.default.createServer(async (request, response) => {
|
|
576
614
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
577
|
-
if (url.pathname !== "/") {
|
|
578
|
-
response.writeHead(404);
|
|
579
|
-
response.end();
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
615
|
try {
|
|
583
|
-
const html = await renderAggregateDashboardHtml(resolvedInputDir);
|
|
616
|
+
const html = await (0, dashboard_pages_1.renderDashboardPage)(url.pathname, () => renderAggregateDashboardHtml(resolvedInputDir));
|
|
617
|
+
if (html === null) {
|
|
618
|
+
response.writeHead(404);
|
|
619
|
+
response.end();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
584
622
|
response.writeHead(200, {
|
|
585
623
|
"Content-Type": "text/html; charset=utf-8",
|
|
586
624
|
"Cache-Control": "no-store",
|
|
@@ -6,6 +6,8 @@ exports.summarizeOverall = summarizeOverall;
|
|
|
6
6
|
exports.buildAggregateDashboardHtml = buildAggregateDashboardHtml;
|
|
7
7
|
const chart_assets_1 = require("./chart-assets");
|
|
8
8
|
const time_1 = require("./time");
|
|
9
|
+
const api_equivalent_cost_1 = require("./api-equivalent-cost");
|
|
10
|
+
const api_pricing_table_1 = require("./api-pricing-table");
|
|
9
11
|
/** 折线 / 图例统一的调色板,保证同一个人在不同图表里颜色一致。 */
|
|
10
12
|
const CHART_PALETTE = ["#5eead4", "#f59e0b", "#a855f7", "#22c55e", "#f87171", "#60a5fa", "#fbbf24", "#34d399"];
|
|
11
13
|
/**
|
|
@@ -28,6 +30,26 @@ function maxOrNull(values) {
|
|
|
28
30
|
const numbers = values.filter((value) => value !== null);
|
|
29
31
|
return numbers.length > 0 ? Math.max(...numbers) : null;
|
|
30
32
|
}
|
|
33
|
+
function mergePricingCatalogVersions(contributions) {
|
|
34
|
+
const values = new Set(contributions
|
|
35
|
+
.map((contribution) => contribution.catalogVersion)
|
|
36
|
+
.filter((value) => value !== null));
|
|
37
|
+
if (values.size === 0)
|
|
38
|
+
return null;
|
|
39
|
+
if (values.has("mixed") || values.size > 1 || contributions.some((contribution) => contribution.legacyRequestCount > 0)) {
|
|
40
|
+
return "mixed";
|
|
41
|
+
}
|
|
42
|
+
return [...values][0];
|
|
43
|
+
}
|
|
44
|
+
function formatCostUsd(value) {
|
|
45
|
+
return `$${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
|
|
46
|
+
}
|
|
47
|
+
function costDisplay(estimatedUsd, priced, unpriced) {
|
|
48
|
+
if (priced === 0 && unpriced > 0)
|
|
49
|
+
return `不可用(${formatNumber(unpriced)} 个未定价)`;
|
|
50
|
+
const amount = formatCostUsd(estimatedUsd ?? 0);
|
|
51
|
+
return unpriced > 0 ? `≥ ${amount}(${formatNumber(unpriced)} 个未定价)` : amount;
|
|
52
|
+
}
|
|
31
53
|
/**
|
|
32
54
|
* 把多条不规则时间戳的 series 并集对齐到一个统一 x 轴。
|
|
33
55
|
*
|
|
@@ -92,6 +114,11 @@ function summarizePeople(dailyRows, weeklyRows = []) {
|
|
|
92
114
|
.reverse()
|
|
93
115
|
.find((row) => row.sevenDayLatestUsagePct !== null);
|
|
94
116
|
const activeDays = items.filter((row) => row.sampleCount > 0 || row.userMessageCount > 0 || row.apiRequestCount > 0).length;
|
|
117
|
+
const cost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)(items.map((row) => ({
|
|
118
|
+
estimatedUsd: row.estimatedApiEquivalentCostUsd,
|
|
119
|
+
pricedApiRequestCount: row.pricedApiRequestCount,
|
|
120
|
+
unpricedApiRequestCount: row.unpricedApiRequestCount,
|
|
121
|
+
})));
|
|
95
122
|
summaries.push({
|
|
96
123
|
personKey,
|
|
97
124
|
userMessageCount: items.reduce((sum, row) => sum + row.userMessageCount, 0),
|
|
@@ -110,6 +137,13 @@ function summarizePeople(dailyRows, weeklyRows = []) {
|
|
|
110
137
|
activeDays,
|
|
111
138
|
firstDate: sortedByDate[0]?.date ?? null,
|
|
112
139
|
lastDate: sortedByDate.at(-1)?.date ?? null,
|
|
140
|
+
estimatedApiEquivalentCostUsd: cost.estimatedUsd,
|
|
141
|
+
pricedApiRequestCount: cost.pricedApiRequestCount,
|
|
142
|
+
unpricedApiRequestCount: cost.unpricedApiRequestCount,
|
|
143
|
+
pricingCatalogVersion: mergePricingCatalogVersions(items.map((row) => ({
|
|
144
|
+
catalogVersion: row.pricingCatalogVersion,
|
|
145
|
+
legacyRequestCount: row.pricingCatalogVersion === null ? row.apiRequestCount : 0,
|
|
146
|
+
}))),
|
|
113
147
|
});
|
|
114
148
|
}
|
|
115
149
|
return summaries.sort((left, right) => {
|
|
@@ -122,6 +156,11 @@ function summarizePeople(dailyRows, weeklyRows = []) {
|
|
|
122
156
|
/** 把整体摘要算出来,方便顶部卡片直接展示。 */
|
|
123
157
|
function summarizeOverall(dailyRows, people) {
|
|
124
158
|
const dates = dailyRows.map((row) => row.date).sort((left, right) => left.localeCompare(right));
|
|
159
|
+
const cost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)(people.map((person) => ({
|
|
160
|
+
estimatedUsd: person.estimatedApiEquivalentCostUsd,
|
|
161
|
+
pricedApiRequestCount: person.pricedApiRequestCount,
|
|
162
|
+
unpricedApiRequestCount: person.unpricedApiRequestCount,
|
|
163
|
+
})));
|
|
125
164
|
return {
|
|
126
165
|
personCount: people.length,
|
|
127
166
|
totalUserMessageCount: people.reduce((sum, person) => sum + person.userMessageCount, 0),
|
|
@@ -132,6 +171,13 @@ function summarizeOverall(dailyRows, people) {
|
|
|
132
171
|
totalSampleCount: people.reduce((sum, person) => sum + person.sampleCount, 0),
|
|
133
172
|
startDate: dates[0] ?? null,
|
|
134
173
|
endDate: dates.at(-1) ?? null,
|
|
174
|
+
estimatedApiEquivalentCostUsd: cost.estimatedUsd,
|
|
175
|
+
pricedApiRequestCount: cost.pricedApiRequestCount,
|
|
176
|
+
unpricedApiRequestCount: cost.unpricedApiRequestCount,
|
|
177
|
+
pricingCatalogVersion: mergePricingCatalogVersions(people.map((person) => ({
|
|
178
|
+
catalogVersion: person.pricingCatalogVersion,
|
|
179
|
+
legacyRequestCount: person.pricingCatalogVersion === null ? person.apiRequestCount : 0,
|
|
180
|
+
}))),
|
|
135
181
|
};
|
|
136
182
|
}
|
|
137
183
|
/** 列出所有日期键,用于在按日柱状图上对齐 X 轴。 */
|
|
@@ -368,7 +414,7 @@ function renderFiveHourUsageChart(people, detailRows) {
|
|
|
368
414
|
</section>
|
|
369
415
|
`;
|
|
370
416
|
}
|
|
371
|
-
/**
|
|
417
|
+
/** 按人渲染汇总表,给出消息、token、成本和 usage 核心指标。 */
|
|
372
418
|
function renderPeopleLeaderboard(people) {
|
|
373
419
|
if (people.length === 0) {
|
|
374
420
|
return `
|
|
@@ -392,13 +438,14 @@ function renderPeopleLeaderboard(people) {
|
|
|
392
438
|
<td>${formatTokensM(person.inputTokens)}</td>
|
|
393
439
|
<td>${formatTokensM(person.outputTokens)}</td>
|
|
394
440
|
<td>${formatTokensM(person.cacheReadInputTokens)}</td>
|
|
441
|
+
<td>${escapeHtml(costDisplay(person.estimatedApiEquivalentCostUsd, person.pricedApiRequestCount, person.unpricedApiRequestCount))}</td>
|
|
442
|
+
<td>${escapeHtml(person.pricingCatalogVersion ?? "--")}</td>
|
|
395
443
|
<td>${escapeHtml(statValue(person.fiveHourPeakUsagePct))}</td>
|
|
396
444
|
<td>${escapeHtml(statValue(person.fiveHourLatestUsagePct))}</td>
|
|
397
445
|
<td>${escapeHtml(statValue(person.sevenDayPeakUsagePct))}</td>
|
|
398
446
|
<td>${escapeHtml(statValue(person.sevenDayLatestUsagePct))}</td>
|
|
399
447
|
<td>${escapeHtml(statValue(person.sevenDayCumulativeUsagePct))}</td>
|
|
400
448
|
<td>${person.activeDays}</td>
|
|
401
|
-
<td class="muted-col">${formatNumber(person.apiRequestCount)}</td>
|
|
402
449
|
</tr>`)
|
|
403
450
|
.join("");
|
|
404
451
|
return `
|
|
@@ -420,13 +467,14 @@ function renderPeopleLeaderboard(people) {
|
|
|
420
467
|
<th>Input tokens</th>
|
|
421
468
|
<th>Output tokens</th>
|
|
422
469
|
<th>Cache read tokens</th>
|
|
470
|
+
<th>等效 API 成本</th>
|
|
471
|
+
<th>价格目录</th>
|
|
423
472
|
<th>5h Peak</th>
|
|
424
473
|
<th>5h Latest</th>
|
|
425
474
|
<th>7d Peak</th>
|
|
426
475
|
<th>7d Latest</th>
|
|
427
476
|
<th>7d 累计</th>
|
|
428
477
|
<th>活跃天数</th>
|
|
429
|
-
<th class="muted-col">API 请求</th>
|
|
430
478
|
</tr>
|
|
431
479
|
</thead>
|
|
432
480
|
<tbody>${rows}</tbody>
|
|
@@ -500,6 +548,8 @@ function renderWeeklyTable(weeklyRows) {
|
|
|
500
548
|
<td>${formatTokensM(row.inputTokens)}</td>
|
|
501
549
|
<td>${formatTokensM(row.outputTokens)}</td>
|
|
502
550
|
<td>${formatTokensM(row.cacheReadInputTokens)}</td>
|
|
551
|
+
<td>${escapeHtml(costDisplay(row.estimatedApiEquivalentCostUsd, row.pricedApiRequestCount, row.unpricedApiRequestCount))}</td>
|
|
552
|
+
<td>${escapeHtml(row.pricingCatalogVersion ?? "--")}</td>
|
|
503
553
|
<td>${escapeHtml(statValue(row.fiveHourPeakUsagePct))}</td>
|
|
504
554
|
<td>${escapeHtml(statValue(row.fiveHourLatestUsagePct))}</td>
|
|
505
555
|
<td>${escapeHtml(statValue(row.sevenDayPeakUsagePct))}</td>
|
|
@@ -527,6 +577,8 @@ function renderWeeklyTable(weeklyRows) {
|
|
|
527
577
|
<th>Input tokens</th>
|
|
528
578
|
<th>Output tokens</th>
|
|
529
579
|
<th>Cache read tokens</th>
|
|
580
|
+
<th>等效 API 成本</th>
|
|
581
|
+
<th>价格目录</th>
|
|
530
582
|
<th>5h Peak</th>
|
|
531
583
|
<th>5h Latest</th>
|
|
532
584
|
<th>7d Peak</th>
|
|
@@ -555,6 +607,9 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
555
607
|
const peakUsage = maxOrNull(people.map((person) => person.fiveHourPeakUsagePct));
|
|
556
608
|
const peakSevenDay = maxOrNull(people.map((person) => person.sevenDayPeakUsagePct));
|
|
557
609
|
const rangeLabel = overall.startDate && overall.endDate ? `${overall.startDate} → ${overall.endDate}` : "--";
|
|
610
|
+
const mixedPricingNotice = overall.pricingCatalogVersion === "mixed"
|
|
611
|
+
? `<p class="muted">成本结果混合了旧版 schema 或不同价格目录,已知金额仅作不完整估算。</p>`
|
|
612
|
+
: "";
|
|
558
613
|
return `<!doctype html>
|
|
559
614
|
<html lang="zh-CN">
|
|
560
615
|
<head>
|
|
@@ -644,6 +699,7 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
644
699
|
.stat-card h2 { font-size: 14px; color: var(--muted); font-weight: 500; }
|
|
645
700
|
.stat-value { margin-top: 16px; font-size: 36px; line-height: 0.95; }
|
|
646
701
|
.stat-note { margin-top: 12px; color: var(--muted); font-size: 13px; }
|
|
702
|
+
.pricing-link { display: inline-block; margin-top: 12px; color: var(--accent); font-size: 13px; }
|
|
647
703
|
.panel-header {
|
|
648
704
|
display: flex;
|
|
649
705
|
align-items: end;
|
|
@@ -705,7 +761,9 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
705
761
|
<span class="hero-chip">人数:${overall.personCount}</span>
|
|
706
762
|
<span class="hero-chip">时间范围:${escapeHtml(rangeLabel)}</span>
|
|
707
763
|
<span class="hero-chip">生成时间:${escapeHtml(generatedAt.toISOString())}</span>
|
|
764
|
+
<span class="hero-chip">价格目录:${escapeHtml(overall.pricingCatalogVersion ?? "不可用")}</span>
|
|
708
765
|
</div>
|
|
766
|
+
${mixedPricingNotice}
|
|
709
767
|
</section>
|
|
710
768
|
<section class="stats">
|
|
711
769
|
<article class="panel stat-card">
|
|
@@ -723,11 +781,6 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
723
781
|
<p class="stat-value">${formatTokensM(overall.totalCacheReadInputTokens)}</p>
|
|
724
782
|
<p class="stat-note">来自 assistant usage 的 cache_read_input_tokens</p>
|
|
725
783
|
</article>
|
|
726
|
-
<article class="panel stat-card">
|
|
727
|
-
<h2>Total API requests</h2>
|
|
728
|
-
<p class="stat-value">${formatNumber(overall.totalApiRequestCount)}</p>
|
|
729
|
-
<p class="stat-note">所有人 API 请求数合计(次要参考)</p>
|
|
730
|
-
</article>
|
|
731
784
|
<article class="panel stat-card">
|
|
732
785
|
<h2>Peak 5h usage</h2>
|
|
733
786
|
<p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakUsage, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakUsage, 1)))}</p>
|
|
@@ -738,6 +791,12 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
738
791
|
<p class="stat-value">${escapeHtml((0, time_1.roundNumber)(peakSevenDay, 1) === null ? "--" : statValue((0, time_1.roundNumber)(peakSevenDay, 1)))}</p>
|
|
739
792
|
<p class="stat-note">团队内观测到的 7 天额度峰值</p>
|
|
740
793
|
</article>
|
|
794
|
+
<article class="panel stat-card">
|
|
795
|
+
<h2>合计等效 API 成本</h2>
|
|
796
|
+
<p class="stat-value">${escapeHtml(costDisplay(overall.estimatedApiEquivalentCostUsd, overall.pricedApiRequestCount, overall.unpricedApiRequestCount))}</p>
|
|
797
|
+
<p class="stat-note">标准同步 API 等效成本,不是订阅或实际账单</p>
|
|
798
|
+
<a class="pricing-link" href="${api_pricing_table_1.API_PRICING_PAGE_FILE}" target="_blank" rel="noopener noreferrer">查看当前价格表</a>
|
|
799
|
+
</article>
|
|
741
800
|
</section>
|
|
742
801
|
${renderPeopleLeaderboard(people)}
|
|
743
802
|
${renderSevenDayCumulativeChart(people)}
|