ccus-cli 0.1.26 → 0.2.2
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 +51 -4
- package/dist/cli.js +211 -6
- package/dist/lib/aggregate-dashboard.js +1 -1
- package/dist/lib/aggregate.js +69 -37
- package/dist/lib/codex-fetcher.js +281 -0
- package/dist/lib/codex-install.js +228 -0
- package/dist/lib/codex-sessions.js +197 -0
- package/dist/lib/dashboard.js +26 -15
- package/dist/lib/export.js +2 -0
- package/dist/lib/paths.js +20 -0
- package/dist/lib/payload.js +10 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
162
162
|
- 默认输出一个 `json` 数据包,里面同时包含 `rawEvents`、`weeklySummary`、`dailySummaries`
|
|
163
163
|
- 导出文件内容为**紧凑 JSON**(无缩进),并默认 **gzip 压缩**后写成 `.json.gz`;gzip 与紧凑化都只是存储/展示层变化,解压后的字段集合与 `schemaVersion` 不变
|
|
164
164
|
- 默认写 `.json.gz`;若用 `--out` 指定一个非 `.gz` 结尾的路径,则按明文 JSON 写出(不压缩)
|
|
165
|
-
- 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `
|
|
165
|
+
- 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `8`:v7 起新增 `weeklySummary.codex` 与 `dailySummaries[].codex`(Codex CLI 的消息/请求/token 统计),v8 起该 codex 段再加 5h/7d 的 peak/latest 额度(从 `source="codex"` 事件重算、与 Claude 额度分开看);aggregate daily/weekly CSV 的主字段(消息/请求/token/额度)已把 Codex **叠加进 Claude 合计**(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源读数),不再单列 `codex*` 列,detail.csv 加 `source` 列区分来源
|
|
166
166
|
- 默认文件名会带 git email 的帐号名前缀和起止日期,例如:`alice_export_2026-05-26_to_2026-06-01.json.gz`
|
|
167
167
|
- `userMessageCount` 来自 `~/.claude/projects/**/*.jsonl` 的非 meta `type:user` 事件
|
|
168
168
|
- `apiRequestCount` 与 token 指标来自 `~/.claude/projects/**/*.jsonl` 中带 `message.usage` 的 `type:assistant` 事件
|
|
@@ -172,15 +172,15 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
172
172
|
## 多人汇总
|
|
173
173
|
|
|
174
174
|
- 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
|
|
175
|
-
- `aggregate`
|
|
176
|
-
- 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d
|
|
175
|
+
- `aggregate` 接受 `schemaVersion: 6/7/8` 的 bundle(v8 含 codex 额度,v7/v6 容错回退 null/零值);更旧的导出请先用当前版本重新 `ccus export`
|
|
176
|
+
- 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d)从选中事件按真实时间戳重算,Claude+Codex 合并:peak 取两源 max、latest 两源相加
|
|
177
177
|
- `ccus aggregate --input-dir DIR --out-dir DIR`
|
|
178
178
|
- 输出三个文件:
|
|
179
179
|
- `detail.csv`:来自 winner bundle 的 `rawEvents`(同人同天只展开最新那份的事件),`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
|
|
180
180
|
- `daily.csv`:同人同天取最新导出 bundle 的 `dailySummaries`,usage 从该 bundle 当天事件重算
|
|
181
181
|
- `weekly.csv`:把同人同周的各天 winner(已按天去重)上卷累加得到,usage 从该周全部 winner 天的事件重算
|
|
182
182
|
- `daily.csv` / `weekly.csv` 还各带一列 `sevenDayCumulativeUsagePct`:7 天额度的**累计真实使用量**,把锯齿波(涨到峰值→窗口重置归零→再涨)还原成实际消耗。计算分两层:先**去毛刺**(把持续短于 2 分钟的 stale 瞬时读数尖峰抹平,只保留真实持续的水平),再做**分段峰谷和**(按 reset 跌破段峰值一半切成上升段,每段累加「段内峰值−谷值」)。以百分比累加表达、可大于 100(用掉多于一个 7d 额度)。对 ±1 采样抖动和 stale 读数尖峰都鲁棒,不会被噪声虚增
|
|
183
|
-
- 该列**绕开按天 winner**,对同一个人**所有机器、所有周**的 bundle `rawEvents
|
|
183
|
+
- 该列**绕开按天 winner**,对同一个人**所有机器、所有周**的 bundle `rawEvents`(含 Claude 与 Codex 两源 7d 读数,codex 事件不再过滤)先按时间合并去重成一条账号级曲线再算,绝不分机相加(同账号 7d 额度共享)
|
|
184
184
|
- 区间内第一个有效样本无前值、不贡献增量,所以 **`daily.csv` 逐行相加只是对全局总量的近似**(跨天边界增量不计入单天);`weekly.csv` 在整周连续算、把跨天增量也计入,更连续,恒有 `weekly ≥ Σ 同周 daily`,想要更准的总量请看 weekly 这列
|
|
185
185
|
- 无有效样本写空(`null`),有样本但无净增长写 `0`;`detail.csv` 不含此列(单事件行不承载区间累计语义)
|
|
186
186
|
- CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
|
|
@@ -255,6 +255,53 @@ ccus api config # 不带参数:打印当前配置
|
|
|
255
255
|
|
|
256
256
|
> API 模式填充的 5h/7d 会和官方 `rate_limits` 走同一条聚合管线(含 7d 累计去毛刺算法,该算法针对 Claude 官方锯齿波设计);不同厂商的额度曲线语义可能不完全一致,混用聚合时注意。
|
|
257
257
|
|
|
258
|
+
## Codex 额度采集(Codex CLI)
|
|
259
|
+
|
|
260
|
+
ccus 也能采集 Codex CLI 的 5 小时 / 周额度,复用同一条 statusline 落盘管线。Codex 没有 statusline hook,改用它的 **hooks.json `Stop` 事件**(每个 turn 结束触发)调起 ccus 的隐藏命令,再 spawn `codex app-server` 走 JSON-RPC `account/rateLimits/read` 拉额度。
|
|
261
|
+
|
|
262
|
+
**一键安装(推荐)**:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
ccus install --codex # 把 Stop hook 挂进 ~/.codex/hooks.json
|
|
266
|
+
ccus install --codex --data-dir <目录> # 让 Codex 事件落到指定数据目录
|
|
267
|
+
ccus install --codex --uninstall # 移除 ccus 的 Stop hook
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
装完**重启 Codex** 后会弹出 `hook need review` 审查提示,二选一通过即可:
|
|
271
|
+
|
|
272
|
+
- 选 `Trust all and continue`,直接信任全部 hook 并继续;
|
|
273
|
+
- 或选 `1 Review hooks`,在列表里找到 ccus 的 `Stop` hook,按 `t` 允许。
|
|
274
|
+
|
|
275
|
+
也可启动 Codex 时加 `--dangerously-bypass-hook-trust` 跳过审查。
|
|
276
|
+
|
|
277
|
+
或手动在 `~/.codex/hooks.json` 的 `Stop` 事件加一条 hook(与一键等价):
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"hooks": {
|
|
282
|
+
"Stop": [
|
|
283
|
+
{ "hooks": [ { "type": "command", "command": "ccus __codex-hook", "timeout": 60 } ] }
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
之后每个 Codex turn 结束都会调起 `ccus __codex-hook`,把 hook payload(含 `cwd`、`session_id`,不含 usage)作为 stdin 的一个 JSON 对象传入。ccus 拉额度、落盘一条 `source="codex"` 的事件,**不写 stdout、不干预 Codex**(Stop 要求 stdout 空或 JSON,ccus 选空 + exit 0 = success;失败静默退出 0)。`ccus install --codex` 只往 `Stop` 追加一条 hook,保留其它事件 / 其它 hook / description / 格式;已是目标则提示已配置、不动文件。
|
|
290
|
+
|
|
291
|
+
> **为什么是 hooks.json 而不是 config.toml 的 notify?** Codex 原生也支持 `config.toml` 的 `notify`,但 orca 等会重写 `config.toml`(实测把 ccus 的 notify 顶成它自己的弹窗 notify,ccus 永远收不到回调)。hooks.json 不被频繁重写,是 hook-only 环境的持久触发入口;同一 `Stop` 事件下多条 hook 由 Codex 并发执行,ccus 与 orca 自带的 hook 互不干扰。
|
|
292
|
+
|
|
293
|
+
要点:
|
|
294
|
+
|
|
295
|
+
- **只支持 Codex CLI**:hooks 与 `codex app-server` 都是 CLI 能力。Codex 桌面版 app(已并入 ChatGPT 桌面 app)不一定触发 CLI 的 hooks、也不一定随附可 spawn 的 `codex` 二进制,不在本期范围。
|
|
296
|
+
- **代理继承**:ccus 由 Codex 直接 spawn,已继承 Codex 的代理 / `CODEX_HOME`;它再 spawn `codex app-server` 时透传 `process.env`,整条链同环境。必须让 codex 子进程发请求(它读 `HTTP_PROXY`/`HTTPS_PROXY`),不能用 ccus 自己的 Node `fetch`(裸 fetch 不读代理、代理环境下直连失败)。
|
|
297
|
+
- **定时同步**:Stop hook 还兜底触发 `ccus sync`(与 Claude statusline 对称)——配过 `ccus sync config --target` 后,Codex 每 turn 结束都会检查 3h 周期,到期自动 export + 复制 bundle(含 Codex token/消息)到目标目录。只用 Codex、不开 Claude Code 时也能自动同步。
|
|
298
|
+
- **缓存节流**:Stop 每 turn 触发,额度按 5 分钟 TTL 缓存(`codex-quota-cache.json`),命中秒回不 spawn;过期才拉一次(带 ~10s 超时),失败回退旧缓存。
|
|
299
|
+
- **字段映射**:app-server 返回 `primary`(5h)/ `secondary`(weekly)两窗口,取各自的 `usedPercent`(驼峰,clamp 0–100);ccus 填进 `rate_limits.five_hour` / `seven_day` 的 `used_percentage`,`computeStatuslineEvent` 读时自动算出 usage。
|
|
300
|
+
- **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数从 `<CODEX_HOME>/sessions` 的 rollout 统计,进 `ccus export` 的 `weeklySummary.codex` / `dailySummaries[].codex` 段;Stop 落盘的额度快照(`source="codex"` 事件)也进 export/aggregate——export bundle 里 Codex 额度单列到 codex 段、与 Claude 分开看,aggregate daily/weekly CSV 则把 Codex **叠加进 Claude 主字段**合计(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源),不再单列 `codex*` 列;detail.csv 的 `source` 列区分来源。
|
|
301
|
+
- **Windows**:`install --codex` 在 Windows 写 `ccus.cmd __codex-hook`(npm 全局装会生成 `ccus.cmd`);Windows 上 Stop hook 偶发收到非法 JSON(已知 bug #23784),ccus 容错按无 payload 处理、仍照常拉额度 + 落盘。
|
|
302
|
+
|
|
303
|
+
> 该路径依赖 Codex 内部 app-server 协议(`account/rateLimits/read` 的返回结构)与 hooks payload schema,随版本变;解析层宽松,字段缺失返回 null、失败静默。
|
|
304
|
+
|
|
258
305
|
## 调试
|
|
259
306
|
|
|
260
307
|
出问题时(比如 statusline 不出数据、导出/聚合结果不对),可以打开详细日志:
|
package/dist/cli.js
CHANGED
|
@@ -38,12 +38,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.resolveExportOptions = resolveExportOptions;
|
|
41
|
+
exports.handleCodexHook = handleCodexHook;
|
|
42
|
+
exports.main = main;
|
|
41
43
|
const node_http_1 = __importDefault(require("node:http"));
|
|
42
44
|
const node_path_1 = __importDefault(require("node:path"));
|
|
43
45
|
const dashboard_1 = require("./lib/dashboard");
|
|
44
46
|
const dashboard_2 = require("./lib/dashboard");
|
|
45
47
|
const aggregate_dashboard_1 = require("./lib/aggregate-dashboard");
|
|
46
48
|
const claude_1 = require("./lib/claude");
|
|
49
|
+
const codex_sessions_1 = require("./lib/codex-sessions");
|
|
47
50
|
const export_1 = require("./lib/export");
|
|
48
51
|
const aggregate_1 = require("./lib/aggregate");
|
|
49
52
|
const debug_1 = require("./lib/debug");
|
|
@@ -55,6 +58,8 @@ const payload_1 = require("./lib/payload");
|
|
|
55
58
|
const paths_1 = require("./lib/paths");
|
|
56
59
|
const scheduler_1 = require("./lib/scheduler");
|
|
57
60
|
const api_mode_1 = require("./lib/api-mode");
|
|
61
|
+
const codex_fetcher_1 = require("./lib/codex-fetcher");
|
|
62
|
+
const codex_install_1 = require("./lib/codex-install");
|
|
58
63
|
const sync_1 = require("./lib/sync");
|
|
59
64
|
const storage_1 = require("./lib/storage");
|
|
60
65
|
const time_1 = require("./lib/time");
|
|
@@ -62,7 +67,7 @@ const update_check_1 = require("./lib/update-check");
|
|
|
62
67
|
const version_1 = require("./lib/version");
|
|
63
68
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
64
69
|
function printHelp() {
|
|
65
|
-
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 ~/.claude/projects 本周活跃 session 打包成 zip,名如 projects_<dates>_<user>.zip)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
70
|
+
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 ~/.claude/projects 本周活跃 session 打包成 zip,名如 projects_<dates>_<user>.zip)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
66
71
|
}
|
|
67
72
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
68
73
|
function parseOptions(args) {
|
|
@@ -125,6 +130,9 @@ function buildDefaultStatuslineCommand(dataDir) {
|
|
|
125
130
|
* 默认写 `~/.claude/settings.json`,只覆盖 statusLine 字段,其它设置原样保留。
|
|
126
131
|
*/
|
|
127
132
|
async function handleInstall(options) {
|
|
133
|
+
if (getBooleanOption(options, "codex")) {
|
|
134
|
+
return handleCodexInstall(options);
|
|
135
|
+
}
|
|
128
136
|
const settingsPath = node_path_1.default.resolve(getStringOption(options, "settings") ?? (0, paths_1.getClaudeSettingsPath)());
|
|
129
137
|
const explicitCommand = getStringOption(options, "command");
|
|
130
138
|
const dataDirOption = getStringOption(options, "data-dir");
|
|
@@ -140,6 +148,37 @@ async function handleInstall(options) {
|
|
|
140
148
|
process.stdout.write(` replaced: ${result.previousCommand}\n`);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* `ccus install --codex`:把 Stop hook 挂进 Codex 的 hooks.json,让 Codex 每 turn 结束调起 ccus 采集额度。
|
|
153
|
+
*
|
|
154
|
+
* 默认挂 `ccus __codex-hook`(Windows 用 `ccus.cmd`)到 `hooks.Stop`;带 `--data-dir` 时追加,让事件落到指定目录。
|
|
155
|
+
* `--codex --uninstall` 移除 ccus 的 Stop hook;`--config PATH` 覆盖 hooks.json 路径。
|
|
156
|
+
* hooks.json 是 orca 等 hook-only 环境的持久触发入口(config.toml 的 notify 会被 orca 覆盖,hooks.json 不会频繁重写)。
|
|
157
|
+
*/
|
|
158
|
+
async function handleCodexInstall(options) {
|
|
159
|
+
const hooksPath = node_path_1.default.resolve(getStringOption(options, "config") ?? (0, paths_1.getCodexHooksPath)());
|
|
160
|
+
const dataDirOption = getStringOption(options, "data-dir");
|
|
161
|
+
const uninstall = getBooleanOption(options, "uninstall");
|
|
162
|
+
const baseArgs = dataDirOption
|
|
163
|
+
? `__codex-hook --data-dir ${node_path_1.default.resolve(dataDirOption).replaceAll("\\", "/")}`
|
|
164
|
+
: `__codex-hook`;
|
|
165
|
+
const command = process.platform === "win32" ? `ccus.cmd ${baseArgs}` : `ccus ${baseArgs}`;
|
|
166
|
+
if (uninstall) {
|
|
167
|
+
const result = await (0, codex_install_1.uninstallCodexHook)(hooksPath);
|
|
168
|
+
process.stdout.write(result.removed
|
|
169
|
+
? `已从 Codex hooks.json 移除 Stop hook:${result.hooksPath}\n`
|
|
170
|
+
: `Codex hooks.json 里未找到 ccus Stop hook:${result.hooksPath}\n`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const result = await (0, codex_install_1.installCodexHook)(hooksPath, command);
|
|
174
|
+
const header = result.created
|
|
175
|
+
? `已创建 Codex hooks.json 并挂载 Stop hook:${result.hooksPath}`
|
|
176
|
+
: result.unchanged
|
|
177
|
+
? `Codex Stop hook 已挂载:${result.hooksPath}`
|
|
178
|
+
: `已在 Codex hooks.json 挂载 Stop hook:${result.hooksPath}`;
|
|
179
|
+
process.stdout.write(`${header}\n Stop → ${JSON.stringify(command)}\n`);
|
|
180
|
+
process.stdout.write(` 首次需在 Codex 用 /hooks 信任该 hook;若 hooks.json 被 orca 等外部工具覆盖,重新执行本命令即可。\n`);
|
|
181
|
+
}
|
|
143
182
|
/**
|
|
144
183
|
* statusline 主路径:读 payload、归一化、落盘、输出单行状态文本。
|
|
145
184
|
*
|
|
@@ -218,11 +257,16 @@ async function loadDashboardData(options, defaultRange) {
|
|
|
218
257
|
// this-week 固定补齐到完整一周(周一到周日),即使本周还没过完,曲线 x 轴也按 7 天逐日展示;
|
|
219
258
|
// 其它范围(today / last-week / 5h)不受影响。
|
|
220
259
|
const window = (0, time_1.expandToFullWeekWindow)((0, time_1.resolveRange)(range, now));
|
|
221
|
-
|
|
260
|
+
// events 含 Claude 与 Codex:个人看板额度叠加 Codex(peak max、latest 相加、7d 累计合并),与 aggregate 同口径。
|
|
261
|
+
// codex 消息柱图走 dailyUserMessages,不受影响。
|
|
262
|
+
const events = (await (0, storage_1.readEventsForRange)(dataDir, range, now))
|
|
263
|
+
.map((record) => (0, payload_1.computeStatuslineEvent)(record));
|
|
222
264
|
const claudeDailyUsage = await (0, claude_1.summarizeClaudeProjectUsageByDay)(window.start, window.end);
|
|
265
|
+
const codexDailyUsage = await (0, codex_sessions_1.summarizeCodexSessionUsageByDay)(window.start, window.end);
|
|
223
266
|
const dailyUserMessages = (0, time_1.enumerateDateKeys)(window.start, window.end).map((date) => ({
|
|
224
267
|
date,
|
|
225
268
|
userMessageCount: claudeDailyUsage.get(date)?.userMessageCount ?? 0,
|
|
269
|
+
codexUserMessageCount: codexDailyUsage.get(date)?.userMessageCount ?? 0,
|
|
226
270
|
}));
|
|
227
271
|
(0, debug_1.debugLog)("dashboard", "events loaded", { range, label: window.label, sampleCount: events.length, days: dailyUserMessages.length });
|
|
228
272
|
const html = (0, dashboard_1.buildDashboardHtml)(events, window.label, window.start, window.end, dailyUserMessages);
|
|
@@ -323,10 +367,17 @@ async function runExport(options) {
|
|
|
323
367
|
(0, debug_1.debugLog)("export", "range resolved", { range, label: window.label, start: window.start.toISOString(), end: window.end.toISOString() });
|
|
324
368
|
const records = await (0, storage_1.readEventsForRange)(dataDir, range, now);
|
|
325
369
|
const events = records.map((record) => (0, payload_1.computeStatuslineEvent)(record));
|
|
326
|
-
|
|
327
|
-
const
|
|
370
|
+
// 按 source 分流:Claude usage 只算 claude 事件,Codex 额度单列(避免 codex 额度污染 claude usage)。
|
|
371
|
+
const claudeEvents = events.filter((event) => !(0, payload_1.isCodexSourceEvent)(event));
|
|
372
|
+
const codexEvents = events.filter(payload_1.isCodexSourceEvent);
|
|
373
|
+
const statuslineSummary = (0, dashboard_2.summarizeEvents)(claudeEvents);
|
|
374
|
+
const statuslineDailyRows = (0, export_1.buildSummaryRows)(claudeEvents);
|
|
375
|
+
const codexStatuslineSummary = (0, dashboard_2.summarizeEvents)(codexEvents);
|
|
376
|
+
const codexStatuslineDailyRows = (0, export_1.buildSummaryRows)(codexEvents);
|
|
328
377
|
const claudeUsage = await (0, claude_1.summarizeClaudeProjectUsage)(window.start, window.end);
|
|
329
378
|
const claudeDailyUsage = await (0, claude_1.summarizeClaudeProjectUsageByDay)(window.start, window.end);
|
|
379
|
+
const codexUsage = await (0, codex_sessions_1.summarizeCodexSessionUsage)(window.start, window.end);
|
|
380
|
+
const codexDailyUsage = await (0, codex_sessions_1.summarizeCodexSessionUsageByDay)(window.start, window.end);
|
|
330
381
|
(0, debug_1.debugLog)("export", "data collected", {
|
|
331
382
|
statuslineSamples: records.length,
|
|
332
383
|
claudeProjectFiles: claudeUsage.matchedFileCount,
|
|
@@ -344,7 +395,7 @@ async function runExport(options) {
|
|
|
344
395
|
exportUserName = gitIdentity.userName;
|
|
345
396
|
}
|
|
346
397
|
const weeklySummary = {
|
|
347
|
-
schemaVersion:
|
|
398
|
+
schemaVersion: 8,
|
|
348
399
|
generatedAt: new Date().toISOString(),
|
|
349
400
|
range: {
|
|
350
401
|
label: window.label,
|
|
@@ -364,6 +415,17 @@ async function runExport(options) {
|
|
|
364
415
|
outputTokens: claudeUsage.outputTokens,
|
|
365
416
|
cacheReadInputTokens: claudeUsage.cacheReadInputTokens,
|
|
366
417
|
},
|
|
418
|
+
codex: {
|
|
419
|
+
userMessageCount: codexUsage.userMessageCount,
|
|
420
|
+
apiRequestCount: codexUsage.apiRequestCount,
|
|
421
|
+
inputTokens: codexUsage.inputTokens,
|
|
422
|
+
outputTokens: codexUsage.outputTokens,
|
|
423
|
+
cacheReadInputTokens: codexUsage.cacheReadInputTokens,
|
|
424
|
+
fiveHourPeakUsagePct: codexStatuslineSummary.fiveHourPeakUsagePct,
|
|
425
|
+
fiveHourLatestUsagePct: codexStatuslineSummary.fiveHourLatestUsagePct,
|
|
426
|
+
sevenDayPeakUsagePct: codexStatuslineSummary.sevenDayPeakUsagePct,
|
|
427
|
+
sevenDayLatestUsagePct: codexStatuslineSummary.sevenDayLatestUsagePct,
|
|
428
|
+
},
|
|
367
429
|
statusline: {
|
|
368
430
|
sampleCount: statuslineSummary.sampleCount,
|
|
369
431
|
uniqueSessions: statuslineSummary.uniqueSessions,
|
|
@@ -383,9 +445,12 @@ async function runExport(options) {
|
|
|
383
445
|
},
|
|
384
446
|
};
|
|
385
447
|
const statuslineDailyMap = new Map(statuslineDailyRows.map((row) => [row.date, row]));
|
|
448
|
+
const codexStatuslineDailyMap = new Map(codexStatuslineDailyRows.map((row) => [row.date, row]));
|
|
386
449
|
const dailySummaries = (0, time_1.enumerateDateKeys)(window.start, window.end).map((date) => {
|
|
387
450
|
const row = statuslineDailyMap.get(date);
|
|
388
451
|
const claudeDay = claudeDailyUsage.get(date);
|
|
452
|
+
const codexDay = codexDailyUsage.get(date);
|
|
453
|
+
const codexRow = codexStatuslineDailyMap.get(date);
|
|
389
454
|
return {
|
|
390
455
|
date,
|
|
391
456
|
userMessageCount: claudeDay?.userMessageCount ?? 0,
|
|
@@ -393,6 +458,17 @@ async function runExport(options) {
|
|
|
393
458
|
inputTokens: claudeDay?.inputTokens ?? 0,
|
|
394
459
|
outputTokens: claudeDay?.outputTokens ?? 0,
|
|
395
460
|
cacheReadInputTokens: claudeDay?.cacheReadInputTokens ?? 0,
|
|
461
|
+
codex: {
|
|
462
|
+
userMessageCount: codexDay?.userMessageCount ?? 0,
|
|
463
|
+
apiRequestCount: codexDay?.apiRequestCount ?? 0,
|
|
464
|
+
inputTokens: codexDay?.inputTokens ?? 0,
|
|
465
|
+
outputTokens: codexDay?.outputTokens ?? 0,
|
|
466
|
+
cacheReadInputTokens: codexDay?.cacheReadInputTokens ?? 0,
|
|
467
|
+
fiveHourPeakUsagePct: codexRow?.fiveHourPeakUsagePct ?? null,
|
|
468
|
+
fiveHourLatestUsagePct: codexRow?.fiveHourLatestUsagePct ?? null,
|
|
469
|
+
sevenDayPeakUsagePct: codexRow?.sevenDayPeakUsagePct ?? null,
|
|
470
|
+
sevenDayLatestUsagePct: codexRow?.sevenDayLatestUsagePct ?? null,
|
|
471
|
+
},
|
|
396
472
|
sampleCount: row?.sampleCount ?? 0,
|
|
397
473
|
fiveHourLatestUsagePct: row?.fiveHourLatestUsagePct ?? null,
|
|
398
474
|
fiveHourPeakUsagePct: row?.fiveHourPeakUsagePct ?? null,
|
|
@@ -403,7 +479,7 @@ async function runExport(options) {
|
|
|
403
479
|
};
|
|
404
480
|
});
|
|
405
481
|
const bundle = {
|
|
406
|
-
schemaVersion:
|
|
482
|
+
schemaVersion: 8,
|
|
407
483
|
generatedAt: new Date().toISOString(),
|
|
408
484
|
range: {
|
|
409
485
|
label: window.label,
|
|
@@ -836,6 +912,127 @@ async function handleBackgroundSync(options) {
|
|
|
836
912
|
(0, debug_1.debugLog)("sync", "background sync failed", error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
837
913
|
}
|
|
838
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Codex 回调(notify / hook)共享的采集入口:拉额度(走缓存)→ 构造 source=codex 事件落盘 → 心跳 → 兜底 sync。
|
|
917
|
+
*
|
|
918
|
+
* 全程不写 stdout(Codex 继承的 stdio 写了会污染终端);失败 debugLog 到 stderr 后静默。
|
|
919
|
+
* 拉不到额度(未安装 / 未登录 / 无缓存)时不落空事件,静默返回。
|
|
920
|
+
*/
|
|
921
|
+
async function recordCodexEvent(dataDir, identity) {
|
|
922
|
+
(0, debug_1.debugLog)("codex-event", "start", { dataDir, cwd: identity.cwd, sessionId: identity.sessionId });
|
|
923
|
+
try {
|
|
924
|
+
const quota = await (0, codex_fetcher_1.resolveCodexQuota)(dataDir);
|
|
925
|
+
if (quota === null) {
|
|
926
|
+
(0, debug_1.debugLog)("codex-event", "no quota available, skip persist");
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const payload = { source: "codex" };
|
|
930
|
+
if (identity.sessionId) {
|
|
931
|
+
payload.session_id = identity.sessionId;
|
|
932
|
+
}
|
|
933
|
+
if (identity.cwd) {
|
|
934
|
+
payload.workspace = { current_dir: identity.cwd };
|
|
935
|
+
}
|
|
936
|
+
const record = (0, payload_1.createPersistedStatuslineEvent)(payload);
|
|
937
|
+
const gitIdentity = await (0, git_1.readGitIdentity)();
|
|
938
|
+
record.gitUserName = gitIdentity.userName;
|
|
939
|
+
record.gitUserEmail = gitIdentity.userEmail;
|
|
940
|
+
record.gitUserAccount = (0, time_1.extractGitEmailAccount)(gitIdentity.userEmail);
|
|
941
|
+
(0, api_mode_1.applyQuotaToPayload)(record.rawPayload, { fiveHour: quota.fiveHour, sevenDay: quota.sevenDay, level: null });
|
|
942
|
+
await (0, storage_1.appendEvent)(dataDir, record);
|
|
943
|
+
(0, debug_1.debugLog)("codex-event", "persisted", { fiveHour: quota.fiveHour, sevenDay: quota.sevenDay });
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
(0, debug_1.debugLog)("codex-event", "failed", error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
947
|
+
}
|
|
948
|
+
finally {
|
|
949
|
+
// Codex 继承的 stdio 写 stdout/stderr 会污染终端;改写心跳文件留痕,证明回调被触发(notify 与 hook 共用)。
|
|
950
|
+
try {
|
|
951
|
+
const fsp = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
952
|
+
await fsp.writeFile(node_path_1.default.join(dataDir, "codex-notify.heartbeat"), `${new Date().toISOString()}\n`, "utf8");
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
// 心跳写失败静默,不影响主流程。
|
|
956
|
+
}
|
|
957
|
+
// Codex 没有 statusline,回调兜底触发定时同步(与 Claude statusline 的 maybeSpawnBackgroundSync 对称):
|
|
958
|
+
// 每 turn 检查一次 sync 周期,到期才 spawn detached __sync,不阻塞、不写 stdout。
|
|
959
|
+
(0, sync_1.maybeSpawnBackgroundSync)(dataDir);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* 隐藏命令 `__codex-notify`:Codex `notify`(config.toml,agent-turn-complete)回调入口。
|
|
964
|
+
*
|
|
965
|
+
* Codex 每 turn 结束 spawn 本命令,把 notify JSON 作为最后一个 argv 追加(含 cwd / thread-id)。
|
|
966
|
+
* 从 argv 解析身份 → recordCodexEvent。orca 等会覆盖 config.toml 的环境改用 `__codex-hook`(hooks.json)。
|
|
967
|
+
*/
|
|
968
|
+
async function handleCodexNotify(args) {
|
|
969
|
+
const options = parseOptions(args);
|
|
970
|
+
const dataDir = getDataDir(options);
|
|
971
|
+
const notify = parseCodexNotify(extractCodexNotifyJson(args));
|
|
972
|
+
await recordCodexEvent(dataDir, { cwd: notify.cwd, sessionId: notify.threadId });
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* 隐藏命令 `__codex-hook`:Codex hooks.json 的 Stop 事件回调入口(orca 等 hook-only 环境用)。
|
|
976
|
+
*
|
|
977
|
+
* Codex 把 hook payload 作为 stdin 的一个 JSON 对象传入(含 cwd / session_id / hook_event_name)。
|
|
978
|
+
* 解析身份 → recordCodexEvent。不写 stdout(Stop 要求 stdout 空或 JSON;ccus 选空 + exit 0 = success,不干预 Codex)。
|
|
979
|
+
* stdin 非法 JSON(Windows Stop 偶发 #23784)容错:当无 payload 处理,仍照常拉额度 + 落盘。
|
|
980
|
+
*/
|
|
981
|
+
async function handleCodexHook(args, deps = {}) {
|
|
982
|
+
const options = parseOptions(args);
|
|
983
|
+
const dataDir = getDataDir(options);
|
|
984
|
+
const read = deps.readStdin ?? io_1.readStdin;
|
|
985
|
+
let cwd = null;
|
|
986
|
+
let sessionId = null;
|
|
987
|
+
try {
|
|
988
|
+
const raw = await read();
|
|
989
|
+
if (raw.trim()) {
|
|
990
|
+
const parsed = JSON.parse(raw);
|
|
991
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
992
|
+
const record = parsed;
|
|
993
|
+
if (typeof record["cwd"] === "string") {
|
|
994
|
+
cwd = record["cwd"];
|
|
995
|
+
}
|
|
996
|
+
if (typeof record["session_id"] === "string") {
|
|
997
|
+
sessionId = record["session_id"];
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
(0, debug_1.debugLog)("codex-hook", "stdin parse failed", error instanceof Error ? error.message : String(error));
|
|
1004
|
+
}
|
|
1005
|
+
await recordCodexEvent(dataDir, { cwd, sessionId });
|
|
1006
|
+
}
|
|
1007
|
+
/** Codex 把 notify JSON 作为最后一个 argv 追加;取末尾首个以 `{` 开头的参数。 */
|
|
1008
|
+
function extractCodexNotifyJson(args) {
|
|
1009
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
1010
|
+
if (args[index].trim().startsWith("{")) {
|
|
1011
|
+
return args[index];
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
/** 宽松解析 notify JSON:cwd + thread-id(兼容 threadId 驼峰);非法返回空。 */
|
|
1017
|
+
function parseCodexNotify(json) {
|
|
1018
|
+
if (!json) {
|
|
1019
|
+
return { cwd: null, threadId: null };
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const parsed = JSON.parse(json);
|
|
1023
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1024
|
+
return { cwd: null, threadId: null };
|
|
1025
|
+
}
|
|
1026
|
+
const record = parsed;
|
|
1027
|
+
const cwd = typeof record["cwd"] === "string" ? record["cwd"] : null;
|
|
1028
|
+
const rawThreadId = record["thread-id"] ?? record["threadId"];
|
|
1029
|
+
const threadId = typeof rawThreadId === "string" ? rawThreadId : null;
|
|
1030
|
+
return { cwd, threadId };
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return { cwd: null, threadId: null };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
839
1036
|
/** 解析时长字面量为毫秒:纯数字按 ms,支持 ms/s/m/h 后缀。非法返回 null。 */
|
|
840
1037
|
function parseDurationMs(raw) {
|
|
841
1038
|
if (raw === undefined) {
|
|
@@ -1136,6 +1333,14 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1136
1333
|
await handleBackgroundSync(parseOptions(args.slice(1)));
|
|
1137
1334
|
return;
|
|
1138
1335
|
}
|
|
1336
|
+
if (group === "__codex-notify") {
|
|
1337
|
+
await handleCodexNotify(args.slice(1));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (group === "__codex-hook") {
|
|
1341
|
+
await handleCodexHook(args.slice(1));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1139
1344
|
if (group === "install") {
|
|
1140
1345
|
await handleInstall(parseOptions(args.slice(1)));
|
|
1141
1346
|
return;
|
|
@@ -297,7 +297,7 @@ function renderFiveHourUsageChart(people, detailRows) {
|
|
|
297
297
|
const built = people
|
|
298
298
|
.map((person, index) => {
|
|
299
299
|
const personRows = detailRows
|
|
300
|
-
.filter((row) => row.personKey === person.personKey)
|
|
300
|
+
.filter((row) => row.personKey === person.personKey && row.source !== "codex")
|
|
301
301
|
.map((row) => ({ t: new Date(row.timestamp).getTime(), five: row.usagePct, seven: row.sevenDayUsagePct }))
|
|
302
302
|
.filter((point) => Number.isFinite(point.t))
|
|
303
303
|
.sort((left, right) => left.t - right.t);
|
package/dist/lib/aggregate.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addNullable = addNullable;
|
|
6
7
|
exports.loadWeeklyExportBundles = loadWeeklyExportBundles;
|
|
7
8
|
exports.computeCumulativeSevenDayCurve = computeCumulativeSevenDayCurve;
|
|
8
9
|
exports.computeCumulativeSevenDay = computeCumulativeSevenDay;
|
|
@@ -23,6 +24,12 @@ function maxOrNull(values) {
|
|
|
23
24
|
const numbers = values.filter((v) => v !== null);
|
|
24
25
|
return numbers.length > 0 ? Math.max(...numbers) : null;
|
|
25
26
|
}
|
|
27
|
+
/** 两个可空数值相加:null 视为 0;两者皆 null 返回 null(表示两源都无该指标)。用于 codex+claude latest 叠加。 */
|
|
28
|
+
function addNullable(a, b) {
|
|
29
|
+
if (a === null && b === null)
|
|
30
|
+
return null;
|
|
31
|
+
return (a ?? 0) + (b ?? 0);
|
|
32
|
+
}
|
|
26
33
|
function isRecord(value) {
|
|
27
34
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
35
|
}
|
|
@@ -43,7 +50,7 @@ function isWeeklyExportBundle(value) {
|
|
|
43
50
|
if (!isRecord(value)) {
|
|
44
51
|
return false;
|
|
45
52
|
}
|
|
46
|
-
if (value.schemaVersion !== 6) {
|
|
53
|
+
if (typeof value.schemaVersion !== "number" || ![6, 7, 8].includes(value.schemaVersion)) {
|
|
47
54
|
return false;
|
|
48
55
|
}
|
|
49
56
|
if (!Array.isArray(value.rawEvents) || !Array.isArray(value.dailySummaries) || !isRecord(value.weeklySummary) || !isRecord(value.identity) || !isRecord(value.range)) {
|
|
@@ -124,7 +131,7 @@ async function loadWeeklyExportBundles(inputDir) {
|
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
if (invalidFiles.length > 0) {
|
|
127
|
-
throw new Error(`Unsupported export bundle schema in files: ${invalidFiles.join(", ")}. Re-export with current ccus so aggregate receives schemaVersion 6 bundles.`);
|
|
134
|
+
throw new Error(`Unsupported export bundle schema in files: ${invalidFiles.join(", ")}. Re-export with current ccus so aggregate receives schemaVersion 6/7/8 bundles.`);
|
|
128
135
|
}
|
|
129
136
|
return bundles;
|
|
130
137
|
}
|
|
@@ -419,7 +426,8 @@ function buildPersonSevenDayCurve(bundles) {
|
|
|
419
426
|
const collected = new Map();
|
|
420
427
|
for (const { bundle } of bundles) {
|
|
421
428
|
const personKey = bundlePersonKey(bundle);
|
|
422
|
-
|
|
429
|
+
// 含 Claude + Codex 两源读数:codex 7d 读数不再过滤,并入同一条曲线算累计
|
|
430
|
+
for (const record of bundle.rawEvents.filter((r) => isPersistedStatuslineEvent(r))) {
|
|
423
431
|
const event = (0, payload_1.computeStatuslineEvent)(record);
|
|
424
432
|
if (event.sevenDayUsagePct === null) {
|
|
425
433
|
continue;
|
|
@@ -468,20 +476,28 @@ function buildAggregatedDetailRows(bundles) {
|
|
|
468
476
|
for (const rep of reps) {
|
|
469
477
|
const events = bundleEventsByDate(rep.bundle).get(rep.date) ?? [];
|
|
470
478
|
for (const event of events) {
|
|
479
|
+
const isCodex = (0, payload_1.isCodexSourceEvent)(event);
|
|
471
480
|
rows.push({
|
|
472
481
|
...event,
|
|
473
482
|
personKey: rep.personKey,
|
|
474
483
|
weekKey: weekKey(new Date(event.timestamp)),
|
|
475
484
|
dateKey: rep.date,
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
485
|
+
source: isCodex ? "codex" : "claude",
|
|
486
|
+
// codex 事件无单事件 token 语义(token 在 daySummary.codex 按天聚合),不附 claude 的日总量。
|
|
487
|
+
inputTokens: isCodex ? 0 : rep.day.inputTokens,
|
|
488
|
+
outputTokens: isCodex ? 0 : rep.day.outputTokens,
|
|
489
|
+
cacheReadInputTokens: isCodex ? 0 : rep.day.cacheReadInputTokens,
|
|
479
490
|
});
|
|
480
491
|
}
|
|
481
492
|
}
|
|
482
493
|
}
|
|
483
494
|
return rows.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
484
495
|
}
|
|
496
|
+
const ZERO_CODEX = { userMessageCount: 0, apiRequestCount: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, fiveHourPeakUsagePct: null, fiveHourLatestUsagePct: null, sevenDayPeakUsagePct: null, sevenDayLatestUsagePct: null };
|
|
497
|
+
/** 取 daySummary 的 codex 快照;缺字段(手编 bundle)回退零值,避免 reduce 抛错或 NaN。 */
|
|
498
|
+
function codexOf(day) {
|
|
499
|
+
return day.codex ?? ZERO_CODEX;
|
|
500
|
+
}
|
|
485
501
|
/**
|
|
486
502
|
* 展开 daily.csv:同人同天的不同机器数据直接叠加(计数字段相加),usage 从所有机器该天事件合并后重算。
|
|
487
503
|
* 同机器重复导出由 selectDailyRepresentatives 在分组阶段去重,不会翻倍。
|
|
@@ -493,21 +509,23 @@ function buildAggregatedDailyRows(bundles) {
|
|
|
493
509
|
for (const reps of repsMap.values()) {
|
|
494
510
|
const { personKey, date } = reps[0];
|
|
495
511
|
// 不同机器的独立数据直接叠加
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
const
|
|
512
|
+
// 累加量含 Codex:Claude + Codex 同字段相加
|
|
513
|
+
const userMessageCount = reps.reduce((sum, r) => sum + r.day.userMessageCount + codexOf(r.day).userMessageCount, 0);
|
|
514
|
+
const apiRequestCount = reps.reduce((sum, r) => sum + r.day.apiRequestCount + codexOf(r.day).apiRequestCount, 0);
|
|
515
|
+
const inputTokens = reps.reduce((sum, r) => sum + r.day.inputTokens + codexOf(r.day).inputTokens, 0);
|
|
516
|
+
const outputTokens = reps.reduce((sum, r) => sum + r.day.outputTokens + codexOf(r.day).outputTokens, 0);
|
|
517
|
+
const cacheReadInputTokens = reps.reduce((sum, r) => sum + r.day.cacheReadInputTokens + codexOf(r.day).cacheReadInputTokens, 0);
|
|
501
518
|
const sampleCount = reps.reduce((sum, r) => sum + r.day.sampleCount, 0);
|
|
502
519
|
const uniqueSessions = reps.reduce((sum, r) => sum + r.day.uniqueSessions, 0);
|
|
503
520
|
const uniqueWorkspaces = reps.reduce((sum, r) => sum + r.day.uniqueWorkspaces, 0);
|
|
504
|
-
// usage
|
|
521
|
+
// 按 source 分流:Claude usage 只算 claude 事件,Codex usage 单列重算(避免 codex 额度污染 claude usage)。
|
|
505
522
|
const allEvents = reps.flatMap((r) => bundleEventsByDate(r.bundle).get(date) ?? []);
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
523
|
+
const claudeEvents = allEvents.filter((event) => !(0, payload_1.isCodexSourceEvent)(event));
|
|
524
|
+
const codexEvents = allEvents.filter(payload_1.isCodexSourceEvent);
|
|
525
|
+
const usage = recomputeUsage(claudeEvents);
|
|
526
|
+
const codexUsage = recomputeUsage(codexEvents);
|
|
527
|
+
// 额度回退:rawEvents 缺时从 daySummary 取(Claude+Codex 合并,见 fallbackWeeklyUsage)
|
|
528
|
+
const fallback = fallbackWeeklyUsage(reps.map((r) => r.day));
|
|
511
529
|
// 累计指标走全样本合并曲线,不走单机的 recomputeUsage,避免漏掉另一台机器的样本。
|
|
512
530
|
const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByDate(curves.get(personKey) ?? [], date));
|
|
513
531
|
rows.push({
|
|
@@ -519,10 +537,10 @@ function buildAggregatedDailyRows(bundles) {
|
|
|
519
537
|
outputTokens,
|
|
520
538
|
cacheReadInputTokens,
|
|
521
539
|
sampleCount,
|
|
522
|
-
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ??
|
|
523
|
-
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ??
|
|
524
|
-
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ??
|
|
525
|
-
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ??
|
|
540
|
+
fiveHourPeakUsagePct: maxOrNull([usage.fiveHourPeakUsagePct, codexUsage.fiveHourPeakUsagePct]) ?? fallback.fiveHourPeakUsagePct,
|
|
541
|
+
fiveHourLatestUsagePct: addNullable(usage.fiveHourLatestUsagePct, codexUsage.fiveHourLatestUsagePct) ?? fallback.fiveHourLatestUsagePct,
|
|
542
|
+
sevenDayPeakUsagePct: maxOrNull([usage.sevenDayPeakUsagePct, codexUsage.sevenDayPeakUsagePct]) ?? fallback.sevenDayPeakUsagePct,
|
|
543
|
+
sevenDayLatestUsagePct: addNullable(usage.sevenDayLatestUsagePct, codexUsage.sevenDayLatestUsagePct) ?? fallback.sevenDayLatestUsagePct,
|
|
526
544
|
sevenDayCumulativeUsagePct,
|
|
527
545
|
uniqueSessions,
|
|
528
546
|
uniqueWorkspaces,
|
|
@@ -531,15 +549,23 @@ function buildAggregatedDailyRows(bundles) {
|
|
|
531
549
|
return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
|
|
532
550
|
}
|
|
533
551
|
/** 周级 usage 回退:rawEvents 缺失时从各天 daySummary 自带值取 peak(max)/ latest(date 最新非空)。 */
|
|
552
|
+
/**
|
|
553
|
+
* rawEvents 缺失时的额度回退:从各天 daySummary 取。
|
|
554
|
+
* Claude 与 Codex 合并:peak 取两源 max、latest 两源相加(与主字段叠加口径一致)。daily/weekly 共用。
|
|
555
|
+
*/
|
|
534
556
|
function fallbackWeeklyUsage(days) {
|
|
535
557
|
const byDateDesc = [...days].sort((left, right) => right.date.localeCompare(left.date));
|
|
536
|
-
const fivePeaks = days.
|
|
537
|
-
const sevenPeaks = days.
|
|
558
|
+
const fivePeaks = days.flatMap((day) => [day.fiveHourPeakUsagePct, codexOf(day).fiveHourPeakUsagePct]);
|
|
559
|
+
const sevenPeaks = days.flatMap((day) => [day.sevenDayPeakUsagePct, codexOf(day).sevenDayPeakUsagePct]);
|
|
560
|
+
const claudeFiveLatest = byDateDesc.find((day) => day.fiveHourLatestUsagePct !== null)?.fiveHourLatestUsagePct ?? null;
|
|
561
|
+
const codexFiveDay = byDateDesc.find((day) => codexOf(day).fiveHourLatestUsagePct !== null);
|
|
562
|
+
const claudeSevenLatest = byDateDesc.find((day) => day.sevenDayLatestUsagePct !== null)?.sevenDayLatestUsagePct ?? null;
|
|
563
|
+
const codexSevenDay = byDateDesc.find((day) => codexOf(day).sevenDayLatestUsagePct !== null);
|
|
538
564
|
return {
|
|
539
|
-
fiveHourPeakUsagePct:
|
|
540
|
-
fiveHourLatestUsagePct:
|
|
541
|
-
sevenDayPeakUsagePct:
|
|
542
|
-
sevenDayLatestUsagePct:
|
|
565
|
+
fiveHourPeakUsagePct: (0, time_1.roundNumber)(maxOrNull(fivePeaks), 1),
|
|
566
|
+
fiveHourLatestUsagePct: addNullable(claudeFiveLatest, codexFiveDay ? codexOf(codexFiveDay).fiveHourLatestUsagePct : null),
|
|
567
|
+
sevenDayPeakUsagePct: (0, time_1.roundNumber)(maxOrNull(sevenPeaks), 1),
|
|
568
|
+
sevenDayLatestUsagePct: addNullable(claudeSevenLatest, codexSevenDay ? codexOf(codexSevenDay).sevenDayLatestUsagePct : null),
|
|
543
569
|
};
|
|
544
570
|
}
|
|
545
571
|
/**
|
|
@@ -573,11 +599,13 @@ function buildAggregatedWeeklyRows(bundles) {
|
|
|
573
599
|
groups.set(key, acc);
|
|
574
600
|
}
|
|
575
601
|
const day = rep.day;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
acc.
|
|
579
|
-
acc.
|
|
580
|
-
acc.
|
|
602
|
+
const codexDay = codexOf(day);
|
|
603
|
+
// 累加量含 Codex:Claude + Codex 同字段相加
|
|
604
|
+
acc.userMessageCount += day.userMessageCount + codexDay.userMessageCount;
|
|
605
|
+
acc.apiRequestCount += day.apiRequestCount + codexDay.apiRequestCount;
|
|
606
|
+
acc.inputTokens += day.inputTokens + codexDay.inputTokens;
|
|
607
|
+
acc.outputTokens += day.outputTokens + codexDay.outputTokens;
|
|
608
|
+
acc.cacheReadInputTokens += day.cacheReadInputTokens + codexDay.cacheReadInputTokens;
|
|
581
609
|
acc.sampleCount += day.sampleCount;
|
|
582
610
|
acc.uniqueSessions += day.uniqueSessions;
|
|
583
611
|
acc.uniqueWorkspaces += day.uniqueWorkspaces;
|
|
@@ -587,7 +615,11 @@ function buildAggregatedWeeklyRows(bundles) {
|
|
|
587
615
|
}
|
|
588
616
|
const rows = [];
|
|
589
617
|
for (const acc of groups.values()) {
|
|
590
|
-
|
|
618
|
+
// 按 source 分流:Claude usage 只算 claude 事件,Codex usage 单列重算。
|
|
619
|
+
const claudeEvents = acc.events.filter((event) => !(0, payload_1.isCodexSourceEvent)(event));
|
|
620
|
+
const codexEvents = acc.events.filter(payload_1.isCodexSourceEvent);
|
|
621
|
+
const usage = recomputeUsage(claudeEvents);
|
|
622
|
+
const codexUsage = recomputeUsage(codexEvents);
|
|
591
623
|
const fallback = fallbackWeeklyUsage(acc.days);
|
|
592
624
|
// 整周累计:在整周合并曲线上一次性求正增量,跨天边界增量被计入,故 weekly ≥ Σ daily。
|
|
593
625
|
const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByWeek(curves.get(acc.personKey) ?? [], acc.week));
|
|
@@ -600,10 +632,10 @@ function buildAggregatedWeeklyRows(bundles) {
|
|
|
600
632
|
outputTokens: acc.outputTokens,
|
|
601
633
|
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
602
634
|
sampleCount: acc.sampleCount,
|
|
603
|
-
fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? fallback.fiveHourPeakUsagePct,
|
|
604
|
-
fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fallback.fiveHourLatestUsagePct,
|
|
605
|
-
sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? fallback.sevenDayPeakUsagePct,
|
|
606
|
-
sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? fallback.sevenDayLatestUsagePct,
|
|
635
|
+
fiveHourPeakUsagePct: maxOrNull([usage.fiveHourPeakUsagePct, codexUsage.fiveHourPeakUsagePct]) ?? fallback.fiveHourPeakUsagePct,
|
|
636
|
+
fiveHourLatestUsagePct: addNullable(usage.fiveHourLatestUsagePct, codexUsage.fiveHourLatestUsagePct) ?? fallback.fiveHourLatestUsagePct,
|
|
637
|
+
sevenDayPeakUsagePct: maxOrNull([usage.sevenDayPeakUsagePct, codexUsage.sevenDayPeakUsagePct]) ?? fallback.sevenDayPeakUsagePct,
|
|
638
|
+
sevenDayLatestUsagePct: addNullable(usage.sevenDayLatestUsagePct, codexUsage.sevenDayLatestUsagePct) ?? fallback.sevenDayLatestUsagePct,
|
|
607
639
|
sevenDayCumulativeUsagePct,
|
|
608
640
|
uniqueSessions: acc.uniqueSessions,
|
|
609
641
|
uniqueWorkspaces: acc.uniqueWorkspaces,
|