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 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` 会启动一个本地 HTTP 服务,默认监听 `127.0.0.1` 上的随机端口,并在每次请求时实时读取最新日志生成页面。`serve` 默认查看 `this-week`(整周)使用量曲线(`build` / `open` 仍默认 `today`),可用 `--range` 覆盖。
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` 为 `9`:v7 起新增 `weeklySummary.codex` 与 `dailySummaries[].codex`(Codex CLI 的消息/请求/token 统计),v8 起该 codex 段再加 5h/7d 的 peak/latest 额度(从 `source="codex"` 事件重算、与 Claude 额度分开看),v9 起 codex `inputTokens` 改为净输入(`input_tokens - cached_input_tokens`,不含缓存命中)以对齐 Claude `input_tokens` 口径;aggregate daily/weekly CSV 的主字段(消息/请求/token/额度)已把 Codex **叠加进 Claude 合计**(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源读数),不再单列 `codex*` 列,detail.csv 加 `source` 列区分来源
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` 的 bundle(v9 codex inputTokens 为净输入,v8 codex 额度,v7/v6 容错回退 null/零值);更旧的导出请先用当前版本重新 `ccus export`
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
- * 加载某个时间窗口内的 dashboard 渲染数据:statusline 事件 + 每日用户消息数。
251
+ * 加载 dashboard 渲染数据:statusline 事件、Claude/Codex 每日消息数与标准 API 等效成本。
249
252
  *
250
- * 每日用户消息数与导出契约同源(Claude 本地 transcript 的真实用户请求数),仅供 dashboard 展示,
251
- * 不落盘、不进任何导出/聚合契约。`defaultRange` 让 build/open 与 serve 各自决定缺省窗口。
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
- await (0, export_1.writeTextFile)(outputPath, html);
281
- (0, debug_1.debugLog)("dashboard", "html written", { outputPath, bytes: html.length });
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 (_request, response) => {
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: 9,
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: 9,
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
- /** 按人渲染汇总卡片,给出请求 / 消息 / token / usage 几个核心指标。 */
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)}