@yxzpro/agent-acta 2.14.4

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/LOGFORMATS.md +960 -0
  3. package/README.md +238 -0
  4. package/REFERENCE.md +1428 -0
  5. package/agent-acta-page.html +1998 -0
  6. package/agent-acta-server.mjs +1066 -0
  7. package/agent-acta-widget.html +724 -0
  8. package/archive.mjs +335 -0
  9. package/core/log.mjs +20 -0
  10. package/core/service.mjs +4285 -0
  11. package/hooks/cursor-usage-hook.mjs +89 -0
  12. package/mcp-server.mjs +372 -0
  13. package/package.json +68 -0
  14. package/page/dialog-add-agent.js +65 -0
  15. package/page/dialog-alert.js +159 -0
  16. package/page/dialog-archive.js +342 -0
  17. package/page/dialog-compare.js +238 -0
  18. package/page/dialog-ctxw.js +101 -0
  19. package/page/dialog-diag.js +72 -0
  20. package/page/dialog-heatmap.js +215 -0
  21. package/page/dialog-selftest.js +202 -0
  22. package/page/dialog-sess-timeline.js +108 -0
  23. package/page/dialog-stats.js +694 -0
  24. package/page/dialog-usage.js +158 -0
  25. package/page/log-card.js +277 -0
  26. package/page/log-view.js +273 -0
  27. package/page/sess-view.js +382 -0
  28. package/page/shared.js +408 -0
  29. package/page/sidebar.js +129 -0
  30. package/page/style.css +973 -0
  31. package/page/topbar.js +61 -0
  32. package/parsers/atomcode.mjs +321 -0
  33. package/parsers/buddy.mjs +272 -0
  34. package/parsers/buddyext.mjs +582 -0
  35. package/parsers/claude.mjs +496 -0
  36. package/parsers/cline.mjs +468 -0
  37. package/parsers/codearts.mjs +96 -0
  38. package/parsers/codex.mjs +188 -0
  39. package/parsers/comate.mjs +195 -0
  40. package/parsers/copilot.mjs +308 -0
  41. package/parsers/cursor.mjs +493 -0
  42. package/parsers/devindb.mjs +358 -0
  43. package/parsers/discovery.mjs +1230 -0
  44. package/parsers/doubao.mjs +406 -0
  45. package/parsers/dsh.mjs +377 -0
  46. package/parsers/gemini.mjs +267 -0
  47. package/parsers/generic.mjs +494 -0
  48. package/parsers/hermes.mjs +398 -0
  49. package/parsers/kilo.mjs +42 -0
  50. package/parsers/kimi.mjs +249 -0
  51. package/parsers/mimocode.mjs +531 -0
  52. package/parsers/minimax.mjs +503 -0
  53. package/parsers/openclaw.mjs +490 -0
  54. package/parsers/opencode.mjs +465 -0
  55. package/parsers/shared.mjs +418 -0
  56. package/parsers/trace.mjs +66 -0
  57. package/parsers/tracecode.mjs +348 -0
  58. package/parsers/traedb.mjs +528 -0
  59. package/parsers/zcode.mjs +356 -0
  60. package/plugin/client.js +51 -0
  61. package/plugin/cordis.patch.yml +5 -0
  62. package/plugin/plugin.mjs +220 -0
  63. package/search.mjs +554 -0
  64. package/tools/grab-trae-key.ps1 +265 -0
  65. package/vendor/element-plus-icons.js +3 -0
  66. package/vendor/element-plus.css +1 -0
  67. package/vendor/element-plus.dark.css +1 -0
  68. package/vendor/element-plus.full.min.js +65 -0
  69. package/vendor/element-plus.zh-cn.js +2 -0
  70. package/vendor/svg/antgroup-color.svg +1 -0
  71. package/vendor/svg/bytedance-color.svg +1 -0
  72. package/vendor/svg/chatglm-color.svg +1 -0
  73. package/vendor/svg/claude-color.svg +1 -0
  74. package/vendor/svg/claudecode-color.svg +1 -0
  75. package/vendor/svg/cline.svg +1 -0
  76. package/vendor/svg/codebuddy-color.svg +1 -0
  77. package/vendor/svg/codebuddy.svg +1 -0
  78. package/vendor/svg/codex-color.svg +1 -0
  79. package/vendor/svg/copilot.svg +1 -0
  80. package/vendor/svg/cursor.svg +1 -0
  81. package/vendor/svg/deepseek-color.svg +1 -0
  82. package/vendor/svg/deepseek-harness-whale.svg +8 -0
  83. package/vendor/svg/doubao-color.svg +1 -0
  84. package/vendor/svg/gemini-color.svg +1 -0
  85. package/vendor/svg/glmv-color.svg +1 -0
  86. package/vendor/svg/grok.svg +1 -0
  87. package/vendor/svg/hermesagent.svg +1 -0
  88. package/vendor/svg/huawei-color.svg +1 -0
  89. package/vendor/svg/hunyuan-color.svg +1 -0
  90. package/vendor/svg/kilocode-avatar.svg +1 -0
  91. package/vendor/svg/kilocode.svg +1 -0
  92. package/vendor/svg/kimi-color.svg +1 -0
  93. package/vendor/svg/kimi.svg +1 -0
  94. package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
  95. package/vendor/svg/logo-AtomGit-dark.svg +4 -0
  96. package/vendor/svg/logo-AtomGit-light.svg +4 -0
  97. package/vendor/svg/meta-color.svg +1 -0
  98. package/vendor/svg/mimo.svg +66 -0
  99. package/vendor/svg/minimax-code-logo.svg +8 -0
  100. package/vendor/svg/minimax-color.svg +1 -0
  101. package/vendor/svg/moonshot.svg +1 -0
  102. package/vendor/svg/nex-color.svg +1 -0
  103. package/vendor/svg/nex.svg +1 -0
  104. package/vendor/svg/nvidia-color.svg +1 -0
  105. package/vendor/svg/openai.svg +1 -0
  106. package/vendor/svg/openclaw-color.svg +1 -0
  107. package/vendor/svg/openclaw.svg +1 -0
  108. package/vendor/svg/opencode.svg +1 -0
  109. package/vendor/svg/openrouter-color.svg +1 -0
  110. package/vendor/svg/qoder-color.svg +1 -0
  111. package/vendor/svg/qwen-color.svg +1 -0
  112. package/vendor/svg/sensenova-color.svg +1 -0
  113. package/vendor/svg/stepfun.svg +1 -0
  114. package/vendor/svg/trae-color.svg +1 -0
  115. package/vendor/svg/traework-v2.svg +1 -0
  116. package/vendor/svg/wenxin-color.svg +1 -0
  117. package/vendor/svg/windsurf.svg +1 -0
  118. package/vendor/svg/workbuddy-color.svg +29 -0
  119. package/vendor/svg/xiaomimimo.svg +1 -0
  120. package/vendor/svg/yuanbao-color.svg +1 -0
  121. package/vendor/svg/yuanbao.svg +1 -0
  122. package/vendor/svg/zai.svg +1 -0
  123. package/vendor/svg/zhipu-color.svg +1 -0
  124. package/vendor/vue.global.prod.js +9 -0
  125. package/widget/main.js +202 -0
  126. package/widget/package.json +7 -0
  127. package/widget/preload.js +11 -0
package/REFERENCE.md ADDED
@@ -0,0 +1,1428 @@
1
+ # AgentActa —— 本地 AI agent 请求日志面板
2
+
3
+ 聚合多款 AI agent 的本地日志,在浏览器里查看每一「轮次/请求」的耗时、token、缓存、上下文占用与工具调用明细。
4
+ 本地常驻服务 + SSE 实时推送,无外部依赖、无上报。
5
+
6
+ ## 安装
7
+
8
+ ### 发给别人 / 长期部署:发压缩包(推荐)
9
+
10
+ ```bash
11
+ # 打包方(源码目录里;版本号取自 package.json 的 version,发版前先 bump)
12
+ mkdir -p ../dist # ⚠️ 必须先建:npm pack --pack-destination 不会自动建目录,
13
+ # 缺了它报的是 ENOENT: open ...tgz,看不出是目录不存在
14
+ npm pack --pack-destination ../dist # → dist/agent-acta-<版本>.tgz(约 0.68MB)
15
+ node test/pack-smoke.mjs # 冒烟:解包→隔离环境跑起来→逐个接口与 vendor 资源探一遍
16
+ # 想连口径一起验(冒烟只看「包里有没有」):解包后把 R6_SRC 指过去
17
+ # tar -xzf ../dist/agent-acta-<版本>.tgz -C /tmp/al && \
18
+ # R6_SRC=/tmp/al/package/agent-acta-server.mjs node test/r6-verify.mjs
19
+
20
+ # 接收方(只要有 node,不需要 git、不需要网络、不需要源码目录)
21
+ npm i -g agent-acta-<版本>.tgz # 装完多一个命令 agentacta
22
+ agentacta --install-hooks # 把「会话启动自动拉起」写进你的 agent 配置
23
+ agentacta --open # 起服务并打开页面
24
+ ```
25
+
26
+ > **`npm pack` 之后跑一下 `pack-smoke.mjs`**(在仓库根):`node --check` 查的是源码目录,
27
+ > 跟包里那两份不是一回事 —— 少一个 `vendor/` 文件、页面内联脚本被压坏,只有装出来才会暴露。
28
+ > 它用隔离 `USERPROFILE` + junction 指向真实 `~/.claude/projects`,扫真数据但只读,跑完自动清理。
29
+
30
+ 包内含服务、页面、`vendor/` 静态资源与 `package.json`,无 `.git`、无 `node_modules`。
31
+ `npm i -g <tgz>` 装出来是**真拷贝**——重新打包后要重装一次;`private: true` 只挡 `npm publish`,不挡 `npm pack`。
32
+
33
+ `--install-hooks` 干的事:读改写 `~/.atomcode/hooks.json`(atomcode)与 `~/.claude/settings.json`(claude code),
34
+ **保留原有内容,只加自己那一条**;重复跑无副作用(幂等);改动前把原文件备份成 `<文件名>.bak`;
35
+ 目标文件 JSON 解析失败就整体放弃、一个字都不改;没检测到的 agent(目录不存在)自动跳过。
36
+ 加 `--dry` 只预览不写,`--agent atomcode` 可只装一个;反悔就 `agentacta --uninstall-hooks`。
37
+
38
+ ### 自己用 / 要改源码
39
+
40
+ ```bash
41
+ npm i -g <本目录> # 从本地目录装 = 软链(等价 npm link),不留副本、改源码即时生效
42
+ ```
43
+
44
+ - 不用 npm 也行:把本目录拷到任意位置,跑一次 `node <目录>/agent-acta-server.mjs --ensure`,
45
+ 浏览器打开 <http://127.0.0.1:14570>(加 `--open` 顺手开页面)。
46
+
47
+ ### 命令一览
48
+
49
+ | 命令 | 作用 |
50
+ |---|---|
51
+ | `agentacta` | 前台常驻(服务本体) |
52
+ | `agentacta --ensure` | 确保在跑的是**本版**服务:同版就幂等退出,是旧版则自动重启(hook 里调的就是它) |
53
+ | `agentacta --open` | 起服务并打开浏览器 |
54
+ | `agentacta --client` | 起桌面悬浮卡片(无边框置顶小窗:今日/累计 token + 最新卡片流,点小球收成水球)。首次执行会把 Electron 运行时(~100MB)装进 `~/.agent-acta/widget-runtime/`(默认走 npmmirror,`ELECTRON_MIRROR` 可覆盖),装一次之后只是启动;重复执行只是聚焦已有窗口。关闭:悬停卡片顶栏/水球右上角露出的 `✕` |
55
+ | `agentacta --stop` | 停掉在跑的服务(先走 `/api/shutdown` 优雅关闭并落盘索引,拿不到响应才按 pid 强杀;没在跑则直接退出) |
56
+ | `agentacta --install-hooks [--dry] [--agent atomcode,claude]` | 把 hook 写进 atomcode / claude 的配置 |
57
+ | `agentacta --uninstall-hooks` | 移除上面写入的 hook |
58
+ | `agentacta --where` | 打印自身安装路径 + 两段可直接抄的 hook 命令 |
59
+ | `agentacta --doctor` | 自检一次,输出一段可粘贴的病历(配置 / 目录 / 索引 / 端口版本 / 页面文件 / 归档),退出码 0 无失败、1 有失败。**只读**,不改数据也不改配置。口径见「自检」一节 |
60
+ | `agentacta --archive` | 归档一次:把「已结束、且早于今天 0 点」的条目连**详情快照**冻进 `~/.agent-acta/archive/`。服务在跑时会每天自动跑一次,这条是手动补跑/排查用 |
61
+ | `agentacta --archive --list` | 只打印归档清单(几个 agent / 几个日期文件 / 多少条 / 多大),不做归档。**只许配 `--archive` 用**,单独敲会报错并退 2 |
62
+ | `agentacta --archive --dry` | 只报要给哪些日期文件写多少条,不落盘 |
63
+ | `agentacta --search-reindex [--dry]` | 手动重建全文搜索索引(`~/.agent-acta/search/`)。日常不用跑 —— 服务启动 60 秒后自己建首轮、之后每 10 分钟一轮增量。**服务在跑时拒绝执行**(索引只有它一个写者),要立刻整体重建先 `--stop`;`--dry` 只统计不落盘(代价照付),口径见「全文搜索」一节 |
64
+ | `agentacta-mcp` | **第二个命令名**(不是 `agentacta` 的开关):以 MCP server 身份跑,stdio 传输、七个只读工具,给 Claude / Cursor / Qoder 当工具用。独立进程直读日志与索引,**不连 14570、不写任何文件**,口径见「作为 MCP server 接入(R31)」一节 |
65
+ | `agentacta --version`(`-v`) | 打印版本号 + 服务脚本指纹(就是 `--where` 里那行「版本/指纹」,用来判断端口上跑的是不是这一版) |
66
+ | `agentacta --help`(`-h`) | 打印用法(内容取的就是服务脚本开头的注释,不存在两份会各改各的用法) |
67
+
68
+ > **参数写错了会报错,不会默默起服务**:认不出来的参数(含 typo,如 `--versoin`)走 stderr 报错并以退出码 `2` 结束。
69
+ > 以前它们被静默忽略,所有开关落空就落到默认分支=前台常驻起服务 —— 只想查个版本,结果占住了端口和终端。
70
+ >
71
+ > **改完服务端代码 / 升级到新版,重启一步到位**:直接 `agentacta --ensure`(或 `--open`)。
72
+ > 它会比对端口上那个服务自报的代码指纹,不一致就优雅停掉再拉起来 —— 不用再手工 `--stop` 再等端口释放。
73
+ >
74
+ > 为什么必须有这一步:升级包换掉的是**磁盘上的文件**,换不掉**已经在跑的进程**;
75
+ > 而服务是从磁盘现读 `agent-acta-page.html` 的,于是会变成**页面是新的、接口是旧的** ——
76
+ > 新接口一律 404(页面报 `统计失败:Unexpected token 'o', "not found" is not valid JSON`),
77
+ > 新接入的 agent 也扫不到、手动加也加不上。三个症状一个根因。
78
+ >
79
+ > 直接跑 `agentacta`(前台常驻)撞上端口被占时,若版本不同它会**明说**是哪一版在跑,不再静默退出。
80
+ > 页面侧也有一条常驻红条说同一件事,并给出该敲的命令。
81
+
82
+ ### 想让它自动常驻
83
+
84
+ **不需要任何常驻脚本**:`--ensure` 幂等,挂在**你常用 agent 自己的 hook** 上,用哪个 agent 就由它把服务带起来。
85
+
86
+ ```bash
87
+ agentacta --install-hooks # 一条命令写完(先 --dry 可预览)
88
+ ```
89
+
90
+ 想手工写就照下面抄。实测冷启动 0.4s、热启动 0.1s(幂等),不会拖慢会话。
91
+
92
+ **atomcode** —— `~/.atomcode/hooks.json`:
93
+
94
+ ```json
95
+ {
96
+ "hooks": {
97
+ "agent-acta": {
98
+ "event": "session_start",
99
+ "command": "node <本目录>/agent-acta-server.mjs --ensure",
100
+ "timeout_ms": 15000
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ⚠️ atomcode 的 hook 命令**不经过 shell**,引号会被当成路径的一部分、含空格的路径无解——所以这里**不要加引号**,
107
+ 本目录路径里也别有空格。改完可用 `atomcode hooks list` 确认已加载,`atomcode hooks test agent-acta` 实跑一次。
108
+
109
+ **claude code** —— `~/.claude/settings.json`(走 shell,引号是安全的):
110
+
111
+ ```json
112
+ {
113
+ "hooks": {
114
+ "SessionStart": [
115
+ { "hooks": [ { "type": "command", "command": "node \"<本目录>/agent-acta-server.mjs\" --ensure", "timeout": 15 } ] }
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ 其它支持 hook 的 agent 同理加一行;一个都没有的话,`agentacta --open` 手动起也一样。
122
+
123
+ > **别做成 atomcode 插件**:实测 atomcode **不会加载已安装插件里的 hook**(只装插件时 `atomcode hooks list` 是 0 个,
124
+ > 跑完会话服务也没被拉起),`marketplace add/install/trust` 这一套在"自动拉起"上没有作用。
125
+
126
+ 服务是纯旁路只读——只按已知路径读各 agent 写的本地日志,不注入、不改配置、不上报。
127
+
128
+ > 需要机器上有 `node`(`node -v` 能跑即可)。服务与页面都在本目录内,配置与索引写在用户目录,换目录不会冲掉你的配置。
129
+
130
+ ## 使用
131
+
132
+ - 打开 <http://127.0.0.1:14570>(`agentacta --open` 会替你起服务并打开)
133
+ - 右上角状态徽标 = SSE 实时连接状态;**点它可以调刷新频率**(1s / 2s / 3s 默认 / 5s / 10s / 不实时更新)
134
+ - 徽标旁「空闲自停」下拉:页面全关且没有新日志超过 30 分钟 / 2 小时 / 6 小时后,服务自己退出(默认关)
135
+ - 可用 **Agent / 项目 / 时间范围 / 状态 / 关键词**筛选;点任意卡片展开用户输入、AI 输出、执行链路、LLM 调用明细与工具调用
136
+ - 三个长列表(工具调用 / LLM 调用明细 / 执行链路)**默认只露前几条**,够长时下面给「展开全部 N 个(还有 M 个)」/「收起」。
137
+ 实测单轮最多 81 个工具调用,全铺开有 19,318px(约 19 屏)——所以默认就是收着的。
138
+ - **「LLM 调用明细」每一行说明这次调用干了什么**:序号前的蓝点 = 这次调用有正文(点开是「本次输出」);
139
+ 模型名后的灰标记 = 这次调用发起的工具(`Bash×2` = 同一次响应里并行调了两个 Bash,完整列表在悬停提示里)。
140
+ 两个标记都没有的行 = 那次调用只回了工具调用、一个字的正文都没说 —— 正常,产物在下面的「工具调用」块里
141
+ (实测一轮 114 次调用里 107 次是这样)。
142
+ 逐次正文目前只有 **claude / codebuddy / workbuddy / gemini** 解析得出来,其余 agent 不显示「本次输出」;
143
+ 单条超 800 字先截断,点「查看完整内容」按 `full=1` 重取。
144
+ - 详情末尾有吸底的「收起」按钮:滚到再深也点得到,点了折叠卡片并把卡片滚回视野
145
+ (实测滚到 9,659px 深处时,卡片头已在视野上方 9,081px,靠点卡片头是收不掉的)。
146
+ - **点侧栏 agent、选项目、选时间范围 = 把窗口收窄到它**(服务端只取该范围内的最近 N 条),
147
+ 所以停更很久的 agent 点进去也能立刻看到它自己的日志,不会因为「全局最近 2000 条里没有它」而空着。
148
+ - 「项目」下拉按**归一化后的项目**分组:同一个项目被各家 agent 写成 `F:\centos\next-admin` / `f:\centos\next-admin` / `f-centos-next-admin`
149
+ (cursor 只有 slug)时并成一项,标签写「(N 种写法)」、悬停可看全部写法。**卡片上显示的仍是原始写法**,不会被改写。
150
+ - 侧边栏每个 agent 旁的开关可临时禁用(只停止扫描并隐藏条目,**不删配置**)
151
+ - 开关右边常显的 `✕` = 移除该 agent 配置(**同样不动本地日志文件**,鼠标移上去变红)。
152
+ 只有「有配置」的 agent 才有这个键;手动添加的入口是 Agent 标题右侧的 `+`
153
+ - 鼠标悬停「Token」标签旁的问号可看输入/输出/缓存/合计明细
154
+ - **时间范围**:全部时间 / 今天 / 近 7 天 / 近 30 天(按**本地自然日**算,近 7 天 = 今天 + 前 6 天)
155
+ - **按会话浏览**(侧栏第 2 项,与「请求日志」并列的**独立视图**):以 session 为单位看完整的多轮演进,而不是散落的单轮卡片。
156
+ 左侧是会话列表(标题 / agent / 项目 / 轮数 / token / 耗时 / 相对时间,可搜索、可换排序),右侧是该会话的汇总与逐轮轨迹;
157
+ **跟随左侧 agent / 项目 / 时间范围筛选**(同一个筛选集,不会出现「侧栏选了项目、会话列表却没跟着收窄」)。
158
+ - 会话级汇总:轮数 / 累计 token(输入·输出·缓存)/ 总耗时 / 起止时间 / 涉及模型。数据来自 `GET /api/sessions` + `/api/session`,
159
+ **服务端算**(页面手里只有最多 5000 条的窗口,客户端分组会漏会话、汇总会偏)。
160
+ - **会话名单取「产品自己起的标题」**(2026-09-23,I14):`name` 来自各 agent 自带的标题 ——
161
+ claude 系取转录里的 `ai-title`(字段 `aiTitle`,没有则退回 `last-prompt`),dsh / gemini / buddy /
162
+ zcode / traedb / opencode / kilo / hermes / devin / cursor 各有各的源头。都没有时 `name` 为 `null`,
163
+ 列表退回显示会话 key(裸 UUID/sessionId)——**不拿文件名冒充标题**。
164
+ claude 系是这次**新接上**的(此前 101 个会话 0% 有名字、全是裸 UUID);因 `ai-title` 平均落在
165
+ 文件 48.8% 处,老会话必须重扫才补得上(`PARSER_REV.claude` 已 7→8)。
166
+ - **子 agent 拓扑(2026-09-28,I16)**:会话列表里,**子 agent 会话**缩进一级并带 `└` 前缀;
167
+ 没有子会话的行 token 那一格照旧,**有**子会话的父会话那一格换成「合计 N 轮 · x tok」(含自身,
168
+ 鼠标悬浮说明含几个子会话)。右栏顶部多一块「父会话 / 子 agent」:能点着**跳到父**、也能点着**跳到某个子**。
169
+ 判据全是源头的**显式谱系外键**(dsh 会话头的 `parentSession` / `delegationDepth`;
170
+ claude 的 `<slug>/<父会话>/subagents/agent-*.jsonl` 目录层级 + `<同名>.meta.json` 的
171
+ `description` / `spawnDepth`;zcode / opencode `/ kilo` 的 `session.parent_id`;hermes 的
172
+ `parent_session_id`),**不按时间区间判父子** —— 那条路已被 2026-09-28 的前置闸证伪(见 `LOGFORMATS.md`
173
+ 的「子 agent 谱系」一节)。认不出父的会话**如实当根**(不硬造父节点),源里没这一层的 agent
174
+ 不显示任何层级(**不印 0、不画假树**)。
175
+ - **`updated_at` 不再冒充轮时间(I16/D2)**:atomcode 里 `.jsonl` 0 字节、或 `turn_id` 在 jsonl 里
176
+ 不存在(打断/回退留下的轮)的轮**拿不到时间戳**,本机实测占 **44.6%**。这类轮现在 `time = 0` +
177
+ `timeUnknown: true`:卡片与会话视图如实标「**时间未知**」,**不参与按天统计**(否则 1970-01-01
178
+ 会冒出一个巨桶把横轴拉成 56 年),统计弹层会写明「另有 N 轮时间未知,未计入」。同时 `time` 改成取
179
+ **轮起点**(`started_at`)而不是轮终点(`ts`)—— 后者是 atomcode 独有的取法,会让卡片时间整体偏一个轮长。
180
+ 动了落盘口径 ⇒ `PARSER_REV.jsonl` 已 1→2。
181
+ - 点开某一轮:**dsh** 会画出**真实时间轴** —— 每一步的等待(TTFT)、流式解码段、每次工具的真实耗时,
182
+ 横轴按真实时间比例(**不给任何段画等宽**);顶部的会话总览条按同一套比例画段,点击可定位到对应轮次并高亮。
183
+ 概览条**不支持滚轮缩放/拖动**(这是既有决策,见 `agent-log-next-steps.md`)。
184
+ - **逐事件时间线目前有 dsh 和 zcode**(dsh 的解析器把逐事件留成一条流:`step` 边界、`stream[]` 流式起止、工具起止;zcode 则由 `model_usage`/`tool_usage` 表的真实起止时间直接生成)。
185
+ ⚠️ 别写成「只有 dsh 的日志带时间戳」—— **claude / codebuddy / gemini 的日志里都有逐行时间戳,而且解析器已经拿它算出了
186
+ 每次 LLM 调用的真实耗时**(实测 15/15、5/5、1/1 有非零 `dur`),只是**算完没留成流**。缺的是「留成流」,不是「时间」。
187
+ codex / kimi 才是真的拿不到(逐次 `dur` 恒为 0)。实测见 `agent-log-next-steps.md` §30.9。
188
+ 别的 agent 打开会看到一条说明,轮次退回「LLM 调用明细 + 工具调用」两块渲染、时间列显示 `—`,
189
+ **不假装有时间轴**。界面据 `/api/sessions` 的 `hasTimeline` 判断,页面**不硬编码 agent 名单**。
190
+ - 失败/被中断的调用(没有 `stream[]`)画成**细占位条 + `?`**,不编造时长。
191
+ - **逐轮用量图里那根线是两态的**(2026-09-23):会话里只要有任意一轮拿得到窗口容量(`e.ctx`),线就是**上下文占用率**
192
+ (该轮结束时的占用 ÷ 窗口,0–100 绝对刻度、**顶格=满窗**);一根分母都没有(kimi / gemini / workbuddy / zcode 这类)
193
+ 才退回**耗时折线**(按本会话峰值归一)。占用率线**按连续有分母的轮分段**,缺分母的那轮断开、不连线,
194
+ 不足两个点的一段不画。柱子始终按本会话最大那轮归一,所以图下那行小字会同时说明「柱与线不是同一把尺」;
195
+ 分母来自「上下文窗口」查表的(`/api/session` 的 `ctxTbl`,即 claude / kimi)还会写明「表里填错曲线就跟着错」。
196
+ - **压缩红点**落在该轮占用率的高度上,拿不到分母时改画**基线空心点**(只说「这轮压过」,高度不带含义)。
197
+ 画法对所有 agent 生效;**claude 与 qoder 两家都会落**这些字段(认的是 `system/compact_boundary` 那行)。
198
+ ⚠️ **2026-09-23 更正**:这里原先写的理由是「claude 转录没有它」—— **该理由不成立**。本机实测
199
+ `~/.claude/projects` 下有 39 条 `compact_boundary`,且 `parsers/claude.mjs` **本来就无条件**
200
+ 解析并累计了这些字段;丢掉它们的是 `emitClaudeTurns` 的 `isQoder` 守卫。
201
+ **I12(2026-09-28)已把这个守卫松开**(落盘口径先 bump `PARSER_REV['claude']` 10→11、后因 compDrop
202
+ 口径修正再 bump 到 12,老索引失效重扫,老会话的压缩画像补得回来),并顺带落了三个新位:
203
+ `compDrop`(**本轮**丢弃 token)/ `compMs`(本轮压缩等待,`durationMs`)/
204
+ `compAuto` + `compManual`(`trigger` 逐条计数)。逐轮、逐会话、两轮对比三处都有展示。
205
+ ⚠️ **`compDrop` 不是源里那个累计值**:`cumulativeDroppedTokens` 是**会话累计**(每压一次往上叠),
206
+ 落盘取相邻事件累计的增量再按轮求和 ⇒ 才是「本轮丢了多少」;直接把累计值逐条相加会把每轮显示成
207
+ 「到该轮为止的运行总量」(会话小结再求和约虚高 (k+1)/2 倍)。
208
+ ⚠️ **缺席 ≠ 0**:`compDrop` **只有 claude 源有**这一位,qoder 的 `compactMetadata` 里没有 ⇒
209
+ 缺席时落 `undefined`(JSON 自动掉键),页面据「有没有这一位」决定说不说,**绝不补一个 0**
210
+ (否则 qoder 会被读成「压过、却一个 token 都没丢」)。`trigger` 认不出的取值两边都不计。
211
+ - 卡片页脚的 session 链接仍然打开原来的会话时间线弹层(那是「一轮一行」的旧视图,两者并存)。
212
+ - **历史归档**(侧栏第 4 项,R28):浏览 `~/.agent-acta/archive/` 里被冻结的历史条目。
213
+ 左侧是 agent → 日期文件的树(带条数与最近归档日),每个 agent 前的箭头单独管**折叠/展开**
214
+ (点整行才是「筛到这个 agent」,顺手展开它 —— 18 个 agent 全展开是 214 行,不给折叠就只能一路滚);
215
+ **每次打开都回到全展开**(收起只属于本次浏览 —— 弹层内容不随关闭销毁,不显式清就会跨次记忆,
216
+ 变成「一打开全都收起」);
217
+ 右侧是该范围的条目列表(可关键词搜索、可翻页),
218
+ 点开某条看**归档当时冻下来的**用户输入 / AI 输出 / 工具 / 调用明细 —— 产品把源日志删了也照样打得开。
219
+ **刻意不跟随**左侧的 agent / 项目 / 时间筛选(它读的是归档目录,不是扫描出来的条目),
220
+ 是**只读旁路**、不并入主列表。左下的**归档开关**是这个弹层唯一的「写」入口:每个已启用的 agent
221
+ 一个开关,写 `config.archive.skip`(见下一节)。加载失败**不清场**:已拉到的树与列表留在原地,错误只占一条横幅带「重试」,
222
+ 且网络层失败(服务重启/瞬断)会自动重试一次 —— 整层换成红条会让人以为归档数据没了。
223
+ 口径与边界见「历史归档(R28)」一节。
224
+ - **磁盘占用**(侧栏第 6 项,R30):按 agent 列「日志 / 归档 / 合计」谁最大(合计带占比条、列头可点排序),
225
+ 下面另列 `index/` 分片与 `~/.agent-acta` 顶层构成。**只统计、不删除**,也不给「一键清理」——
226
+ 删日志不可逆(想留历史该走归档)。四件事在页面上都露出来:库根连 `-wal`/`-shm` 一起算、索引片单列不与
227
+ agent 行相加、共用同一目录的两个 agent 打「共用目录」且总数只算一次、遍历超预算时打「统计到一半」。
228
+ 口径与预算见「磁盘占用(R30)」一节。
229
+ - **用量统计**(工具栏那个折线图标):按天聚合 token 与耗时,跟随当前的 agent / 项目 / 时间范围
230
+ - 顶部四格 = 输入(不含缓存)/ 输出 / 缓存命中 / 合计,**合计 = 三者之和**(自洽,不会出现「合计 < 输入」)
231
+ - 图上的柱子一天一根(宽度上限 48px,所以「今天」就只有一根柱子,不会拉成整条色块),悬停看当天明细;「输入 + 输出 / 含缓存」可切换 —— 缓存通常占 95% 以上,
232
+ 默认这档能直接看出「实打实」的部分;**两档的纵轴各自归一,别跨档比柱高**
233
+ - 下面的表格就是同一份数据(可滚动),也是无障碍上的「表格视图」
234
+ - ⚠ **耗时不是工作时长**:它是各轮「首条记录 → 末条记录」时间差之和,挂机/隔夜的轮会把一整天算爆
235
+ (本机实测有单轮 63 小时)。某天最长单轮超过当天总耗时一半(且当天合计 ≥ 1 小时)时会打 ⚠,那种天别当工作量看
236
+ - **按模型**(同一对话框下半部分):每个模型一行,条形长度 = 它占合计 token 的**绝对**比例(不是按第一名归一,
237
+ 所以「某个模型占七成」直接看得出来),分段仍是输入/输出/缓存;右侧给出合计、占比、条数,悬停看各字段原始值与用到它的 agent
238
+ - 同一个模型被不同 agent 写成两种大小写(`Deepseek-V4-Flash` / `deepseek-v4-flash`)按**小写**并成一行,
239
+ 显示名取条目最多的那个写法;**大小写之外一个字都不动** —— 去空格/连字符能并掉 `hy4 preview`/`hy4-preview`,
240
+ 但同一条规则也会并掉 `gpt-5.4`/`gpt-54`,宁可少并也不并错
241
+ - 没有模型记录的条目(只剩 `logs.json` 的老 gemini 会话、读不到 Cursor IDE 会话库的 cursor 转录等)单列一行,**不混进任何模型**:
242
+ 它们 token 恒为 0,只占条数。这样「各模型 + 无模型 = 合计」永远对得上账
243
+ - 极少数轮次会挂多个模型(本机 20/4718)—— 这些轮的 token 全部归给**第一个**模型,条数在下方说明里写出来(不静默挑一个)
244
+ - 统计**不受右上角「条/页」影响**:算的是当前筛选下的全部条目。所以它显示的条数常大于列表标题那行 —— 那是设计如此
245
+ (不然换个档位数字就变,没法信)。列表标题在范围内条目超过窗口时也会明说「该范围内共 N 条,只显示最近 M 条」
246
+ - **导出**(工具栏那个下载图标 → 下拉选 JSON / CSV / Excel;展开某一轮后详情底部另有「导出该轮完整详情」,R11):
247
+ 导的是**列表此刻这份筛选集**(已加载窗口里筛出来的全部,不止屏幕上渲染的那一屏),三种格式都带「按什么条件导出」的
248
+ 说明行,能自证这批数是按什么筛出来的。文件由浏览器直接下载,**服务端一个字节都不写**。口径与上限见「导出(R11)」一节。
249
+
250
+ ## 支持的日志格式
251
+
252
+ 支持 agent 与各格式的字段口径、解析边界、踩坑记录(dsh 的 zstd 多帧、cursor 从 `state.vscdb` 取模型名、
253
+ atomcode 要开 datalog 才有逐次明细、kimi 三份日志根、trae 的 SQLCipher 库等)完整说明见 **LOGFORMATS.md**。
254
+
255
+ - **内置**(开箱即读,不需要配置):`atomcode`(`~/.atomcode/sessions`)、`codebuddy`(**两个根**:CLI 的 `~/.codebuddy/projects` + genie 扩展的 `%LOCALAPPDATA%\CodeBuddyExtension\Data`,后者挂在 `sessionsExtra` 上、每轮重算)、`workbuddy`(`~/.workbuddy/projects`)
256
+ - **自动发现**:服务启动与每轮扫描时对 `claude / codex / cursor / trae / qoder / opencode / gemini / copilot / windsurf / codearts / kimi / dsh / zcode / doubao / hermes / devin / minimax / mimocode / kilo / openclaw / cline`
257
+ 在 `~/.<name>`、`%APPDATA%\<name>`、Application Support/<name>、`~/.config/<name>` 等位置自动嗅探,命中即注册(`source:auto`),目录消失自动摘除。mimocode 的会话不在 `~/.<name>` 那套约定里,而在 `~/.local/share/mimocode/memory`(`candidateBases('mimocode')` 单独补这条真路径命中),见下面「mimocode」一节。
258
+ 注意 **zcode 是例外形态**:它的会话不在目录里,而在 SQLite 库 `~/.zcode/cli/db/db.sqlite`(只读)——所以「ZCode 聊了几轮、这里却扫不到」在 1.8.4 起不再发生
259
+ **opencode 也是例外形态**:会话全在 `~/.local/share/opencode/opencode.db`(**明文** SQLite + WAL,**不需要密钥**),
260
+ 按内容认库(`session`/`message`/`part` + `event`/`event_sequence` 五张表齐才算),手工添加填 `opencode.db` 文件或数据根都认——
261
+ 注意它与 ZCode 的库都有 `session`/`message`/`part`,服务靠「事件投影两张表」把两者分开,不会互相抢库
262
+ **kilo 也是例外形态,而且是 opencode 的 fork**:KiloCode(Kilo CLI)的会话同样集中在一个**明文** SQLite 库里
263
+ (`~/.local/share/kilo/kilo.db` + `-wal`,不需要密钥),schema 与 opencode 实测**同构**(`session`/`message`/`part` +
264
+ `event`/`event_sequence` 全在),所以**解析层直接复用 opencode 的核心**(`parsers/kilo.mjs` 只是薄壳,口径已用真实样本核对:
265
+ 每条 `message.data.tokens` 是**该次调用**的用量,逐条相加 == `session` 表的聚合值)。认领层必须与 opencode **双向互斥**:
266
+ kilo.db 满足 opencode 的全部五张表判据,两家各靠**独有表**分开(kilo:`kilo_board`/`kilo_board_message`),
267
+ 且 kilo 在 `sniffBase()` 里排在 opencode **之前** —— 口径见 LOGFORMATS.md 的 kilo 一节
268
+ **trae 同样是例外形态**:优先读它的 SQLCipher 库 `%APPDATA%\Trae CN\ModularData\ai-agent\database.db`(完整会话记录,**要配 `traeKey`**),
269
+ 读不了时自动回退 `renderer.log` 骨架——密钥从哪来见下面「Trae 的 SQLCipher 库」一节
270
+ **Copilot 也有独立落盘格式**:`~/.copilot/ide/*.lock` 只是 VS Code/MCP 通信锁;真正的会话正文在 `~/.copilot/session-state/<sessionId>/events.jsonl`,同根 `session-store.db` 的 `assistant_usage_events` 补 token、耗时与模型用量。服务会自动识别 `copilot`;用量库暂时不可读时仍保留正文卡片。
271
+ **doubao 也是例外形态**:Windows 上会话不在 `~/.doubaowork`(那条约定不存在),而在 `%LOCALAPPDATA%\DoubaoWork\User Data\Default\.doubaowork\agent_mode\workspace\.sessions\<会话 id>\agents\<agent id>\system\trajectory.jsonl`;
272
+ 自动发现走这条真路径,手工填 `DoubaoWork 应用根 / User Data / Default / .doubaowork / agent_mode / workspace` 任意一层都认(沿已知目录名下探 + 首行 role 字段内容判据双保险);转录里没有时间戳与 token/模型,时间取自同目录 `assignment.md`,见 LOGFORMATS.md 的 doubao 一节。
273
+ **hermes 也是例外形态**:会话全在 SQLite 库 `state.db`(Win=`%LOCALAPPDATA%\hermes`、mac/Linux=`~/.hermes`,`HERMES_HOME` 指到哪认到哪,只读打开),
274
+ 按页头 + `sessions`/`messages`/`session_model_usage` 三张表验明正身;但它**没有逐轮 token**(DB 只有会话聚合),
275
+ 真实用量从同目录 `logs/agent.log` 的 `API call #N` 行按轮窗口配对 —— 日志滚掉时该轮退回 0 token,口径见 LOGFORMATS.md 的 hermes 一节。
276
+ **devin 也是例外形态**:会话全在 SQLite 库 `sessions.db`(Win=`%APPDATA%\Devin\cli`、mac/Linux=`~/.local/share/devin/cli`,只读打开,**明文、不需要密钥**),
277
+ 按页头 + `sessions`/`message_nodes`/`tool_call_state` 三张**独有表**验明正身(`sessions` 这个名字与 hermes/zcode/opencode 重名,单靠它不算数);
278
+ 消息是 `node_id`/`parent_node_id` 连成的**森林**,只沿 `main_chain_id` 回溯主线(本机实测 261 个节点里 164 个在主线,其余是重新生成留下的旁支);
279
+ 逐次 token/耗时/工具出入参**全在库里**(`assistant.metadata.metrics`),口径见 LOGFORMATS.md 的 devin 一节。
280
+ **minimax 也是例外形态**:会话在 `~/.minimax/v2/sessions/YYYY/MM/DD/<…-session_<id>>/messages.jsonl`(`v2/sessions` 要下探四层才到会话目录),
281
+ usage/model/stopReason 全在消息行的 `message` 里;token 口径是 `tin = input` 直接取(input 不含 cache),ctx 分母取同目录 `llm-call.json` 的 `maxTokens`,见 LOGFORMATS.md 的 minimax 一节。
282
+ **mimocode 也是例外形态**:它落盘的是 **SQLite 库**(`~/.local/share/mimocode/mimocode.db`,Drizzle ORM 之上),`memory/` 下那些 `checkpoint.md`/`notes.md`/`MEMORY.md` 只是库导出的展示层(本机实测 `task` 表为空、任务只活在 markdown)。
283
+ 解析器**以 mimocode.db 为权威源**:**一条 user `message` 开一轮**,其后所有 assistant `message` 并入该轮 → 映射成 AgentActa 一条 entry(⚠️ 别退回「一条 message 一条 entry」:mimocode 的 agent 循环是「想一步→调工具→看结果→再想一步」,一次提问会落十几到几十条 assistant message —— 本机实测一句「hello,我能去哪找你的应用图标的svg」落了 19 条 assistant / 20 次工具调用,逐条灌 entry 就是一张卡变 20 张,与 claude/opencode 的轮语义也对不上)。`rounds=calls=`轮内 LLM 调用数、`tools=`轮内 tool 碎片数;token 逐次累加(`tin=Σtokens.input` / `tout=Σ(tokens.output+reasoning)`,推理 token 属输出侧 / `tcache=Σcache.read+write`);模型进 `models[]`(⚠️ **assistant 的模型在顶层 `modelID`/`providerID`**,`user` 才是 `model.{providerID,modelID}` —— 两者位置不同,只读后者会让助手侧模型全空)、`part` 里的 tool 碎片进 `tools[]/calls[]`(`state.status==='error'` 的 → 轮 `status='error'`)、`time` 用轮内最早 `message.data.time.created` **原样毫秒**(全项目统一 13 位毫秒,**不要除以 1000**,否则条目全落到 1970)、`dur` 取「轮内最晚 completed − 最早 created」的**毫秒**(与 claude/opencode 同口径,页面 `fmtDur` 按毫秒读)。详情里的「用户输入」会**剥掉 `<system-reminder>` 注入块**(库会把运行上下文当 text 碎片塞进 user message,本机实测一个 2014 字的注入块里真用户输入只有 21 字,不剥则预览与搜索全废)。库读不动时回退 markdown 层(token/工具恒 0,详情 `note` 说明)。整份重解析 + 库 `{mtime,size}`(含 `-wal`)签名跳过,不进 `OFF_KINDS`;但**登记 `PARSER_REV.mimocode`** —— 它不管 index 落盘(本 kind 不落),管的是 `~/.agent-acta/search/` 分片的作废(轮口径变了必须 bump,否则老分片按旧 `turn` 下标返回错轮正文且不会自愈),见 LOGFORMATS.md 的 mimocode 一节。
284
+ ⚠️ **嗅探判据必须与 opencode 互斥**:mimocode.db 与 opencode.db 是**同源 schema**(都源自 session/message/part + event/event_sequence),opencode 原本的判据(那五张表)在 mimocode.db 上**全为真** —— 不挡就会把 mimocode 的库接走,mimocode 恒 0 条且静默。两家各靠**独有表**互斥(mimocode:`history_fts_idx`/`actor_registry` 等;opencode:`session_message`/`session_input` 等),见 `discovery.mjs` 的 `MIMOCODE_ONLY_TABLES`。
285
+ 另:`mimocode` 的 `sessions` 就是**应用根本身**(不是「根下的某层」),重新嗅探时不能对它取 `dirname` —— 曾因此爬到 `~/.local/share`、被 opencode 的相对候选 `share/opencode/opencode.db` 抢走并把 kind 改写,且因 `dirTaken` 对称判据**永不自愈**(详见 `agent-acta-server.mjs` 的 `ROOT_SESSIONS_AGENTS` 与 `ownConfFor`)。
286
+ **openclaw 也是例外形态**:会话不在 `~/.<name>` 顶层,而在 `<stateDir>/agents/<agentId>/sessions/<sessionId>.jsonl`(`stateDir` 默认 `~/.openclaw`、老版本 `.clawdbot`,`OPENCLAW_STATE_DIR`/`OPENCLAW_HOME` 可整体改写)。
287
+ 判据 = **首行**是会话头(`type:"session"` + `version` + `id`,只读头 2KB),并硬排同样以 `.jsonl` 结尾的 `<id>.trajectory.jsonl`(运行轨迹,不是会话树);`sessions` 填 `stateDir` 本身(转录在它下面的 `agents/<id>/sessions/`),手工填 `stateDir / agents / agents/<id> / agents/<id>/sessions` 任意一层都认。⚠️ `agent/models.json` 里带 `apiKey`,解析器只取 `id` 与 `contextWindow`,绝不外带。口径见 LOGFORMATS.md 的 openclaw 一节。
288
+ **cline 也是例外形态**(Cline CLI 3.x):会话在 `<data>/sessions/<会话 id>/<会话 id>.messages.json`,data 根默认 `~/.cline/data`(`CLINE_DATA_DIR` / `CLINE_SESSION_DATA_DIR` 可整体改写)。它是**整份 JSON 文档、每次落盘整体重写**(不是 JSONL)⇒ 没有 off 可续读,按 `{mtime,size}`(含清单)签名整份重解析,**不进 `OFF_KINDS`**,`PARSER_REV.cline` 只管搜索分片作废。两条最容易被照抄写错的口径:① ⚠️ **`tool_result` 也是 `role:"user"`**,只看 role 切轮会把本机 8 轮的会话切成 23 轮 —— 开轮要「role=user **且** content 有非空 text 块」,正文还得剥掉 `<user_input mode="act">` / `<mode_notice>` 壳;② ⚠️ **`metrics.inputTokens` 含缓存**(cacheRead/Write 是它的子集,OpenAI 语义,与 openclaw/minimax 相反、与 hermes 同侧)⇒ `tin = input − cacheRead − cacheWrite`、`ctxUsed` 取轮内末次 `input`;`/compact` 之后它会**回落**(43662→19526),不是累计值。`ctx` 恒 0(模型目录只嵌在 144MB 的 `cline.exe` 里,盘上没有)。逐轮无状态/耗时/成本,唯一失败信号是 `tool_result.content[]` **逐项**的 `success===false`/`error`(批量调用可只错一项)。`<id>.compaction.json` 与 `sessions.db` 都不读(压缩是纯投影、不改全量日志);⚠️ `data/settings/providers.json` 是**明文 apiKey**,一个字节都不许读。口径见 LOGFORMATS.md 的 cline 一节。
289
+ - **手动添加**:侧栏 Agent 标题右侧 `+`,填名字(留空路径自动探测)或直接指定 `sessions` / `projects` / `traces` 目录
290
+ - 目录在但格式没认出来 → 用 `GET /api/diagnose`(侧栏「环境诊断」)逐条看原因
291
+
292
+ ## Trae 的 SQLCipher 库
293
+
294
+ Trae CN 把 AI 会话的完整记录写在 `%APPDATA%\Trae CN\ModularData\ai-agent\database.db`,这是**真 SQLCipher 加密库**
295
+ (SQLCipher 4.5.7,AES-256;口径见 `LOGFORMATS.md` 的 traedb 一节)。AgentActa 读它需要配置里给一把钥匙(`traeKey`),
296
+ 这一节讲清:为什么这把钥匙必须手动搞到、怎么搞。
297
+
298
+ **为什么不能自动**:这把钥匙是**整机安装级**的(本机实测:`ai-agent` 与 `ai-chat` 两个库、跨 2025-08 → 2026-09
299
+ 是同一把),只存在于 **Trae 运行时的进程内存**里——磁盘上任何文件都不落(数据目录 2953 个文件 × 6 种编码全扫,
300
+ 0 命中),也不是 machineid / 机器码派生出来的(769 组派生组合试过,0 命中)。它是 32 字节**随机** key(不是口令),
301
+ **没有口令材料可爆破,也不能从库文件反推**——拿到它的唯一办法是从 Trae 进程内存里取。
302
+
303
+ **怎么拿(每台机器做一次)**:包里带了 `tools/grab-trae-key.ps1`,在 Trae 开着的情况下运行:
304
+
305
+ ```bash
306
+ powershell -ExecutionPolicy Bypass -File tools/grab-trae-key.ps1
307
+ ```
308
+
309
+ 1. 脚本先以普通权限把 Trae 的所有进程扫一遍(约 20 秒);
310
+ 2. Trae 的关键宿主进程带反调试组件、对普通权限进程拒绝内存读取(实测 `OpenProcess(VM_READ)` 报 err=5),
311
+ 扫不到会自动**以管理员身份重来**(弹一次 UAC);
312
+ 3. 扫描期间建议在 Trae 里**随便发一条 AI 消息**(或重启 Trae),触发它打开数据库——只要它开过一次库,
313
+ `PRAGMA key = "x'…'"` 的整条字符串会在进程堆里驻留数小时(实测),内存扫描直接可捞;
314
+ 4. 抓到后脚本会把 key 打印出来并**复制到剪贴板**。
315
+
316
+ **填进配置**:`~/.agent-acta/config.json` 的 trae 条目补一个字段(示例路径按需改名):
317
+
318
+ ```json
319
+ "trae": {
320
+ "kind": "traedb",
321
+ "sessions": "C:\\Users\\<你>\\AppData\\Roaming\\Trae CN\\ModularData\\ai-agent\\database.db",
322
+ "traeKey": "<64 位十六进制>",
323
+ "source": "auto"
324
+ }
325
+ ```
326
+
327
+ 不想提权、或暂时不配也不影响用:AgentActa 自动回退读 `renderer.log`(轮次 / 用户输入 / 工具 / 状态 / 轮级耗时都在,
328
+ **但没有 token、没有 AI 正文、没有逐次调用明细**),诊断页会写「SQLCipher 库认出来了,但读不出来」并指到本节。填对后下一轮扫描自动换成完整数据。
329
+
330
+ > key 是**每台机器各自一把**:分发给同事时,每人都要在自己机器上跑一次上面的脚本——
331
+ > 别人机器上的 key 解不开你的库,反之亦然。同机抓到一次即可长期使用;将来若 Trae 升级换代
332
+ > 导致诊断页变回「密钥不对」,重跑一次脚本即可。
333
+
334
+ ## 服务端 HTTP API
335
+
336
+ ```
337
+ GET / 页面
338
+ GET /widget 桌面小卡片页:今日/累计 token 汇总 + 最新卡片流(SSE 实时刷新)。
339
+ 桌面挂件用法:msedge --app=http://127.0.0.1:14570/widget
340
+ 再用 PowerToys Always On Top(Win+Ctrl+T)置顶即成桌面卡片。
341
+ 注意:卡片开着 = 有 SSE 连接,空闲自停不会触发(这正是挂件想要的行为)
342
+ GET /vendor/<file> 静态资源(本地化,无 CDN);品牌图标在 /vendor/svg/ 下,支持一层以上子目录
343
+ GET /api/ping 存活探测
344
+ GET /api/agents 列出 agent 配置
345
+ GET /api/diagnose 环境诊断:每个候选 agent 每条候选路径的探测结果与未识别原因
346
+ GET /api/usage?force=1 磁盘占用(R30,见下面「磁盘占用」一节)。一次遍历三处:agent 的日志根、
347
+ index/ 分片、archive/ 清单。返回
348
+ {at, cached, dataDir, port, partial, budget{maxFiles,maxMs,visited},
349
+ agents[{agent,kind,enabled,roots[],logBytes,logFiles,partial,sharedBy,
350
+ archiveBytes,archiveDays,archiveCount,totalBytes,entries}],
351
+ indexShards[{kind,bytes,rev,persisted}], archive{root,agents,days,count,bytes},
352
+ dataItems[{name,dir,bytes,files,partial,link}],
353
+ totals{logBytes,logBytesRows,archiveBytes,indexBytes,dataBytes,agents,logFiles}}
354
+ 结果缓存 60s(?force=1 绕开);archiveCount=-1 表示 manifest 没记这条日期文件
355
+ (手工拷进来的),条数未知、页面显示 ?;sharedBy 非空 = 该 agent 与另一个 agent
356
+ 指向同一份目录,行内各报各的、totals.logBytes 只算一次
357
+ GET /api/selftest 上一次回归自检的结果(R32,见下面「解析器自检」一节)。**只读 ~/.agent-acta/selftest.json**,
358
+ 服务端不跑任何测试。没跑过/读不动都返 200 + {ok:false, error, hint:"node test/selftest.mjs"};
359
+ 有结果则原样透传 runner 写的字段(v, at, durationMs, self{version,build,parserRev},
360
+ env{node,platform}, groups[{id,label,def,note}], rows[{file,title,group,status,ms,code,
361
+ tail,reds[],note}], skipped[{file,title,group,why}], summary{total,pass,fail,timeout,error})
362
+ 并添三个服务端算的标注:file(结果文件绝对路径)、ageMs(at 读不出时退到文件修改时间)、
363
+ sameBuild / sameParserRev(结果出自哪版代码 vs 当前服务是哪版;不一致页面顶栏黄条)
364
+ POST /api/agents {name, path?} 添加
365
+ ⚠️ 路径解析出的根若与**任一已有 agent 的根重叠**(相等 / 在其内 / 包住它),
366
+ 返回 400 与 `{ok:false, ownedBy:"<那个 agent>", kind, sessions, error}` ——
367
+ 同一批文件被两条 agent 扫不会多出一份数据,只会因条目 id 不含 agent 名而互相抢归属。
368
+ 详见 LOGFORMATS.md「手动添加」一节
369
+ DELETE /api/agents?name= 移除该 agent(任何 agent 都可移除,只删配置不动本地日志;v2 配置不会把它再加回来)
370
+ POST /api/agents/toggle {name, enabled} 启用/禁用
371
+ GET /api/cursor-hook cursor 逐轮 token 采集 hook 的状态
372
+ {ok, injected, script, file, log, command, error?}
373
+ · injected = ~/.cursor/hooks.json 的 stop 里是否已有我们那条命令;
374
+ · script = 随包的 hooks/cursor-usage-hook.mjs 在不在(不在则注入必然失败);
375
+ · file = hooks.json 的绝对路径,log = 侧车日志 ~/.agent-acta/cursor-usage.jsonl 路径
376
+ POST /api/cursor-hook {action:'install'|'uninstall'} 注入 / 卸载(侧栏 cursor 行那个图标按钮打的)
377
+ · **合并,不覆盖**:只往 hooks.stop 里加/删我们那一条,用户自己的钩子与
378
+ 其它事件(beforeSubmitPrompt 等)原样保留;
379
+ · 幂等:已注入再 install 回 {ok:true, already:true},不会重复追加;
380
+ · hooks.json **不是合法 JSON 时拒绝改动**(回 ok:false + error,文件一个字节不动)——
381
+ 宁可让人手工修,也不「重置成默认」把用户自己的钩子静默删掉;
382
+ · 卸载只摘我们那条,**不删已采集的 cursor-usage.jsonl**(历史数据留着)
383
+ 口径(为什么只有这条路能拿到 cursor 的 token)见 LOGFORMATS.md 的 cursor 一节
384
+ GET /api/snapshot?limit 快照 {seq,total,shown,rangeTotal,scanMs,agents{名字:条数},projects{名字:条数},entries[]}
385
+ ?scan=1 先扫一轮再返回;?agent=名字 / ?project=归一化key / ?range=today|7d|30d
386
+ 把「最近 limit 条」收窄到该 agent / 该项目的所有写法 / 该时间范围的内部(可叠加)
387
+ (agents/projects 计数仍是全量,不随窗口缩水;project 的 key 见服务端 SHARED_JS
388
+ 的 projNorm —— R12 起它是唯一实现,GET / 时注入页面,页面无本地副本;
389
+ rangeTotal = 过滤后(切窗口前)的条数,可能大于 entries.length)
390
+ 注:range 传的是**符号**不是时间戳,边界由服务端每次请求现算 ——
391
+ 标签页挂过午夜后「今天」跟着走,页面显示的范围与服务端过滤的范围永远一致
392
+ GET /api/daily 按自然日聚合 {label, from, to, daysCapped, days[{day,count,tin,tout,tcache,
393
+ total,dur,maxDur,calls,tools,agents{}}], totals{}}
394
+ 参数同 snapshot 的 ?agent= / ?project= / ?range=(同样可叠加),
395
+ 但**不吃 limit**:算的是整个筛选集 —— 聚合口径不能跟着「条/页」档位变。
396
+ 没有日志的自然日补 0(柱状图 x 轴必须连续);
397
+ total 一律现算 = tin+tout+tcache(保证四个数自洽);
398
+ totals.maxDur 是「这些天里最长的那一轮」,不是求和
399
+ GET /api/models 按模型聚合 {models[{key,label,names[],count,tin,tout,tcache,total,dur,
400
+ calls,tools,credits,agents{}}], totals{}, noModel, noModelTotal, multi}
401
+ 参数与 /api/daily 完全一致(同样可叠加、同样不吃 limit),排序按合计 token 降序。
402
+ key = 模型名小写(归一只有小写这一步),label = 组内最多的写法,names = 全部写法;
403
+ Σ(models.count) + noModel == totals.count,Σ(models.total) == totals.total,
404
+ 且 totals 与 /api/daily 的 totals 逐字段相等(同一屏里两个「共 N 条」不能对不上)
405
+ GET /api/entry?id=&full=1 详情(full=1 返回未截断内容)
406
+ **dsh 的返回里多一个 events[]**:本轮按真实时间顺序的事件流
407
+ [{k:'user'|'sys'|'step'|'llm'|'call'|'result'|'end', t, …}]。
408
+ · llm 段把一次调用拆成 ttft(排队 + prefill 等待)与 dur(流式解码),
409
+ 两者都取自 dsh 原生的 stream[] 时间戳;
410
+ · call / result 的 i 是**指向同响应里 tools[] 的下标**,不是正文副本
411
+ (否则内存翻倍,且 full=1 时会出现「事件里截断、tools 里完整」两份真相);
412
+ · result.dur = 该次工具的真实耗时;sys 只记 len 不记正文(系统提示不入内存)。
413
+ 失败调用(assistant/attempt)常常没有 stream[],ttft/dur 都是 0 —— 页面画占位。
414
+ **非 dsh 没有 events 字段**,页面必须 v-if 兜底。
415
+ 口径与验收见 AGENTLOG-HISTORY.md §30(原 agent-log-r6-trajectory-task.md 已并入归档)
416
+ GET /api/session?key=&agent=&project=
417
+ 单个会话的聚合 + 正序轮次。**实测返回字段**(别按直觉猜,这里踩过一次):
418
+ {ok, session, name?, count, tin, tout, tcache, dur, calls, tools,
419
+ agents{}, projects{}, from, to, models[], entries[]}
420
+ · session = 回显的会话键;agents / projects 是**计数表**({名字: 条数});
421
+ · **`name`(2026-09-23,I14 加)= 会话名**(同 /api/sessions 的那一档,
422
+ 任一轮有标题即可,都没有则**整个键不出现**);页面标题走
423
+ `sessPick.name || sessCur.name || sessCur.session`,所以不带 sessPick
424
+ 直接按 key 打开也不会退回裸 UUID;
425
+ · **calls / tools 在这里是数字**(会话内合计),跟 /api/entry 里的同名
426
+ 字段(那是**数组**)不是一个东西 —— 同一个名字两种类型,写代码时注意;
427
+ · **没有 `total`**(只有 tin/tout/tcache 三项,要合计得自己加),
428
+ 也**没有 `hasTimeline`**(那是 /api/sessions 才有);
429
+ **但 I16 起有顶层 `agent` / `project` / `projectKey`** ——
430
+ 页面从「父会话 / 子 agent」那两条链接跳转时要靠它们(session 值跨 agent、
431
+ 跨项目都不唯一,只按 key 打开会跳错人);
432
+ · **I16 子 agent 拓扑(2026-09-28)**:`parent`(父会话 id)/ `depth` / `sub` /
433
+ `subMode` + `children[]`(这个会话下面挂的子 agent 会话清单,每行
434
+ `{key,name,sub,subMode,turns,total,dur,status,from,to,childCount,subKids,subTurns,subTotal,subDur}`)
435
+ + `subTurns` / `subTotal` / `subDur`(**含自身**的子树合计)。
436
+ 判据与 /api/sessions 的树**同一份**(显式谱系外键 + 同 agent + 同归一化项目);
437
+ 父会话不在当前筛选集里时 `children` **如实为空数组**,不编;
438
+ · `to` = **最后一轮的起点**(= max(轮.time)),不是会话结束时间 ——
439
+ 所以 `to - from` ≠ `dur`(dur 是各轮耗时之和);
440
+ · entries 按时间**正序**,元素 {id,time,dur,status,preview,agent,project,
441
+ tin,tout,tcache,total,models,calls,tools},**没有轮号字段**,轮号用下标。
442
+ ⚠️ **cursor 的老版会话例外**:整段会话共用文件 mtime、所有轮 `time` 并列,
443
+ 而解析器是**倒序插入**的(见 `emitCursorTurns` 的注释),稳定排序下
444
+ `entries[0]` 是**最后一轮** —— 这时下标**不是**轮号,要轮号得从 `id`
445
+ 末尾的 `#<ord>` 取(回归 `test/cursor-hook-test.mjs` 就是这么取的)。
446
+ 另有一批**按需透传**的键(没有就是 undefined、序列化时直接掉,别当必然存在):
447
+ `ctx`(窗口容量)/ `ctxUsed`(该轮结束时的占用)/ `ctxTbl`(这个 `ctx` 来自
448
+ 「上下文窗口」查表而非日志,逐轮现算自 `srcs.kind`,不落盘)/ `compacts` +
449
+ `compPre` + `compPost`(压缩次数 / 压缩前 / 压缩后 token,**claude 与 qoder 两家**都落,
450
+ 见上方「压缩红点」段)/ `compDrop` + `compMs`(**本轮**丢弃 token / 本轮压缩等待时长 ——
451
+ compDrop 由源里**会话累计** `cumulativeDroppedTokens` 取相邻增量得到,
452
+ **只有 claude 源有 `compDrop`**,qoder 缺席即 undefined)/ `compAuto` + `compManual`
453
+ (`trigger` 逐条计数的自动 / 手动压缩次数)/ `credits` + `ctxRatio`(qoder 专属,
454
+ 页面据此决定逐轮用量图画积分还是画 token)。
455
+ agent / project 是**可选**收窄参数:都不传 = 只按 session 匹配(老行为,
456
+ 页面的会话时间线弹层走的就是它);传了才能精确命中重名的 session。
457
+ ⚠️ session 值跨 agent、跨 project 都**不保证唯一**(claude/buddy 用文件名、
458
+ codex/dsh/kimi/gemini/cursor 用 sessionId、atomcode 用 .meta 里的标题),
459
+ 所以传 project 时要传**归一化后的 key**(projNorm / projKey,与页面项目下拉同口径)。
460
+ GET /api/sessions?agent=&project=&range=&from=&to=&limit=
461
+ **会话列表 + 会话级汇总**(页面「按会话浏览」用的就是它)
462
+ {ok,range,count,capped,sessions[{key,agent,project,projectKey,name,turns,
463
+ tin,tout,tcache,total,dur,calls,tools,from,to,status,models[],hasTimeline,
464
+ parent?,parentKey?,children[]?,depth?,sub?,subMode?,timeUnknown?,
465
+ subTurns,subTotal,subDur,subKids,subSelf}]}
466
+ · name = 会话名(各 agent 自带的标题,如 claude 系的 ai-title;都没有则 null,
467
+ 页面退回显示 key)—— 见页面上方「按会话浏览」那条的说明;
468
+ · 参数与 /api/daily 完全一致(可叠加),同样**不吃「条/页」窗口** ——
469
+ 会话级汇总必须覆盖整个筛选集,否则会和逐轮累加对不上账;
470
+ · 聚合键 = **agent + projKey(project) + session**。三者缺一都不行:
471
+ 只用 session 会把不同 agent、乃至同一 agent 下不同项目的同名会话并成一个
472
+ (atomcode 的 session 取的是标题,本机实测会重名),
473
+ 不过 projKey 会把 F:\x / f:\x / cursor 的 slug 误拆成三行;
474
+ · 排序按 to 降序(最近活跃在前);limit 默认 200、上限 2000,超了给 capped:1;
475
+ · hasTimeline = 该会话的轮次有没有逐事件时间线,**目前 dsh / zcode / traedb 为 true**。
476
+ 页面据它决定画时间轴还是走「无逐事件时间」的兜底渲染 ——
477
+ 这样页面**不需要硬编码 agent 名单**,将来别的 agent 补上 events 也自动生效。
478
+ · **I16 子 agent 拓扑(2026-09-28)**:`parent`(父会话 id)/ `depth` / `sub`
479
+ (子 agent 的派活描述)来自解析器落的**显式谱系外键**(dsh 会话头 parentSession、
480
+ claude 的 `subagents/` 目录层级 + `.meta.json`、zcode/opencode 的
481
+ `session.parent_id`、hermes 的 `parent_session_id`)。聚合侧按
482
+ `agent + projKey + parent→key` 上卷:
483
+ `parentKey` = 父会话在**本次返回的列表里**的 key(认不出父时**缺席**,
484
+ 这时该会话如实当根用,不硬造一个不存在的父节点);
485
+ `children[]` = 直接子会话(每行 `{key,name,sub,subMode,turns,total,dur,status,from,to,subKids}`);
486
+ `subTurns / subTotal / subDur / subTin / subTout / subTcache / subCalls / subTools`
487
+ = **含自身**的子树合计(回答「这一坨一共花了多少」);
488
+ `subKids` = 后代子会话数(不含自己);`subSelf` = true 表示没有子会话
489
+ (页面据此不显示「合计」那一列,免得每行都挂一个和本体相同的数)。
490
+ ⚠️ **不按时间区间判父子** —— 那条路已被 2026-09-28 的前置闸证伪
491
+ (atomcode 那 11047 对区间嵌套是「time 取轮终点 + 时间未知回落 updated_at」
492
+ 两处缺陷叠出来的噪声,修正后嵌套 = 0)。源里没有谱系字段的 agent 一律不带这些键。
493
+ · **`timeUnknown`** = 这一轮**拿不到时间戳**(atomcode 的 `.jsonl` 0 字节,或
494
+ `turn_id` 在 jsonl 里不存在)。这类轮 `time = 0` 且带 `timeUnknown: true`,
495
+ **不回落会话 `updated_at`**(那会让同会话几十轮塌成同一时刻);它们
496
+ **不参与按天统计**(否则 1970-01-01 会冒出一个巨桶把横轴拉成 56 年),
497
+ 页面在卡片与会话视图里如实标「时间未知」。
498
+ GET /api/events SSE:hello / update / remove / agents / settings / resync / scanning + 每 25s 心跳
499
+ hello 里带 scanning:{done,total,agent}(连上来时首扫还没跑完才有,否则为 null)
500
+ scanning {phase:'start'|'progress'|'done',done,total,agent}:首扫进度,
501
+ 只推给已连接的 SSE 客户端、不进 dirty(漏了不影响正确性,hello 会补当前状态)
502
+ GET /api/settings 当前设置 {scanMs, choices, idleExitMs, idleChoices}
503
+ POST /api/settings {scanMs} 调整扫描节奏(0/1000/2000/3000/5000/10000,0=不实时更新;落盘并广播)
504
+ {idleExitMs} 调整空闲自停(0/1800000/7200000/21600000)
505
+ GET /api/alert 异常巡检规则与命中 {ok, rules, summary, hits[]}
506
+ rules = {tokens:{on,val}, durMs:{on,val}, fail:{on,val,window}}(顶层三规则,
507
+ 保存于 config.alert,随 scanMs 同套机制持久化,重启服务仍在;全关 = 绝不打扰)
508
+ summary = {count, agents[{agent,count,rules}]} —— count 是侧栏徽章数 / 「只看异常」条目数
509
+ hits = 命中的完整条目 [{id, agent, rules[], time, dur, total, status, preview, project}],
510
+ 按 time 降序(弹层点击行 -> 前端用 id 调 locateEntry 定位回主列表)
511
+ POST /api/alert {rules:{tokens,durMs,fail}} 整组替换规则(白名单校验,坏值回落默认),
512
+ 落盘 config.alert 并立刻重算命中 + 广播 alert 事件 → 徽章 / 只看异常即时刷新。
513
+ 返回 {ok, rules, summary}(不含 hit 明细,前端保存后另拉 GET 取列表)
514
+ 规则语义:tokens = 单轮 total(输入+输出+缓存)超阈值;durMs = 单轮耗时
515
+ (轮内首条→末条时间差,含挂机/隔夜)超阈值;fail = 某 agent **最近 N 轮**
516
+ (fail.window,最多 100)中失败占比(status != ok,1..100%)超阈值,命中该 agent 里构成失败的那几轮。
517
+ GET 在 alertDirty 时先就地重算;扫描 / 规则改动 / 归档都会触发 recomputeAlerts。
518
+ GET /api/ctxwindows 上下文窗口表 {windows:[{name,value,builtin,user,off}], unknown:[{model,n}]}
519
+ 这张表只服务于 claude 卡片的进度条**分母**:别的 agent 自带窗口字段,
520
+ claude 的转录里没有(请求体不落盘),只能按模型名查表。
521
+ unknown = 日志里出现过、表里查不到的模型,页面据此列「待填」。
522
+ POST /api/ctxwindows {windows:[{name,value}]} **整表替换**用户条目;value<=0 = 停用该条
523
+ (内置默认也能这样关掉);与内置默认同值的条目不落盘。
524
+ 保存后就地重算已有条目的 ctx 并广播,无需重扫/重启。
525
+ POST /api/shutdown 优雅关闭(落盘索引后退出;--stop 用的就是它,同源校验)
526
+ GET /api/version {version, build, pid};build = 服务脚本内容的 SHA1 前 12 位。
527
+ --ensure 用它判断「端口上跑的是不是我这版」,页面用它判断「我这张页面配不配得上服务」。
528
+ 刻意**不走 whenReady**:首扫阻塞事件循环时,ensure 恰恰要尽快拿到答案
529
+ GET /api/archive/index 归档清单 {root, days[{agent,day,count,bytes}], agents[{agent,days,count,bytes,lastDay,unknown?}],
530
+ totals{agents,days,count,bytes}, skip[], configAgents[]}
531
+ skip = `config.archive.skip` 的原样回显;configAgents = **已启用**的 agent 名
532
+ (开关列表要用它管到「还没有任何归档」的 agent —— 那些在清单里是查不到的)
533
+ 清单每次读盘都与目录 readdir 对账:手工删掉的日期文件会消失,
534
+ 目录里多出来的未知文件以 count=-1(页面显示「?」)出现,不会被静默吞掉
535
+ POST /api/archive/skip 改 `config.archive.skip`:body {agent, skip:true|false} → {ok, skip[]}。
536
+ 即改即落盘、立刻影响下一轮归档(不用重启,与 --archive 共用同一份配置)。
537
+ agent 必须是**已配置**的,否则 400(拼错的名字不能进 config)。
538
+ 只停未来的归档,绝不删已冻好的文件。
539
+ GET /api/archive/entries 归档条目列表 {list[], total, scanned, offset, limit}
540
+ ?agent= / ?day= 至少给一个(页面点树给);?q= 关键词对
541
+ preview/user/assistant/session/models/工具名 做**包含**匹配(与主列表同口径,不分词不正则);
542
+ ?limit 默认 200(夹在 1..2000)/ ?offset 翻页。
543
+ **list 里的条目不含 detail**,只有 hasDetail 标记 —— 详情要单条去取,
544
+ 否则「全部 agent」一页 200 条会把几百 KB 正文一起塞进响应。
545
+ 注:total 是**扫完所有匹配的日期文件**才得出的,所以不传 ?day= 时耗时随归档总量线性增长
546
+ (本机 195 个文件 / 5022 条实测 ~0.6s)
547
+ GET /api/archive/entry 单条冻结的详情快照 {entry};agent/day/id 任一查不到 → 404 {ok:false,error:"not found"}
548
+ 返回的是**归档当时冻下来的那份**,与源日志在不在无关 —— 这正是归档的意义
549
+ ```
550
+
551
+ ## 历史归档(R28)
552
+
553
+ **解决的问题**:有些 agent 自己会删历史(CodeArts 只留约 30 天 `User/logs`,本机历史月已出现空档)。
554
+ 日志没了,页面上的统计和详情就永久缺一块 —— 这是**不可逆**的,只能靠提前冻一份来对抗。
555
+
556
+ **冻什么(条目级,不是压原文)**:每个「已结束、且发生在今天 0 点之前」的条目落一条 JSONL 记录:
557
+
558
+ - 统计字段逐个显式列出(agent/project/session/time/day/dur/status/tin/tout/tcache/total/ctx/rounds/tools/calls/
559
+ models/preview/finished/aborted/toolNames/rev/kind)—— 不用 `{...e}` 整对象摊开,
560
+ 免得将来 entry 里多出内部字段被悄悄冻进去、以后想删都删不掉;
561
+ - **详情快照**:归档那一刻走一遍实时详情那条路(`entryContent`),把用户输入 / AI 输出 /
562
+ 工具 / 逐次调用 / 事件一起冻下来 —— 原文没了也照样打得开。
563
+ 截断口径与非 full 的页面一致(正文 800 字),且每类**最多 40 项**,被砍掉的只记个数(`detailMore`),
564
+ 页面明说「另有 N 项未入库」而不是假装完整;
565
+ - `rev` = 归档时该 kind 的 `PARSER_REV`。**归档不随口径升级重放**(源文件可能已被产品删了),
566
+ 这个字段只用来在页面上标「这份是什么口径冻的」,不是重算的依据。
567
+
568
+ **为什么不做「按天压日志」**:压原文要靠各 agent 的解析器事后重读,而详情本来就是懒加载的 ——
569
+ 真按天压,冻下来的只是一坨无法自证的原始文本,还得信任「以后解析器认得出它」。
570
+ 条目级冻结换来两件事:① 详情与统计一起冻,源文件删了也能看;② **天然幂等**(见下)。
571
+
572
+ **幂等靠「合并不重建」**:每次归档都**读回当天的日期文件、按 id 合并增量**,绝不拿内存里的 `entries` 重建整天文件。
573
+ 这条是硬约束 —— 内存里的条目是 LRU 上限 20000 的,第 20001 条会把最老的挤掉;
574
+ 真按内存重建,昨天已归档的历史会在某天被静默抹掉一大截,而且**看起来一切正常**。
575
+ 副作用是「已经冻过且当时取到了详情」的条目直接跳过(`已是最新`),所以复跑很快:
576
+ 本机 5022 条首跑 1m22s,复跑 3.7s / 写入 0 个文件。
577
+
578
+ **什么时候跑**:
579
+
580
+ - **服务内每日自动**:服务跑着的时候每 3 小时检查一次,跨过 0 点后跑一轮(`AGENT_LOG_ARCHIVE_DELAY_MS`
581
+ 可覆盖首次延迟,测试用);单轮最多 4000 条(`maxPerRun`),剩下的下一轮继续。
582
+ 关掉:`config.archive.enabled = false` —— **只影响自动轮,`agentacta --archive` 手动跑不受影响**。
583
+ - **`agentacta --archive [--list] [--dry]`**:手动补跑/排查,不启 HTTP、不占端口。
584
+ ⚠️ 手动跑完会直接退出,**不落盘索引**(索引本来就有 5s 延迟写盘的定时器,进程先退了),
585
+ 所以它跟「重启后再看」是两件事——这符合预期(归档读的是内存里已扫出来的条目,不动索引)。
586
+
587
+ **按 agent 关掉归档**(`config.archive.skip = ["cursor", "trae"]`):有的 agent 日志本来就不值得留
588
+ (量小 / 没正文 / 临时跑跑),逐个关比整机关自动轮实用。语义钉死两条:**只停未来的归档**,
589
+ 已经冻好的日期文件**一个字都不动** —— 它是省磁盘的开关,不是删数据的入口(要清已有归档用 `maxDays`/`maxMB`,
590
+ 或者自己去归档目录删)。
591
+ **页面上就能开关**:`config.archive.skip` 是唯一一个「手改 config 不如点一下」的配置 —— 服务端加了
592
+ `POST /api/archive/skip`(`{agent, skip}`,写入即 `saveConfig()` 落盘,**立刻生效、不用重启** ——
593
+ 它和 `agentacta --archive` / 服务内自动轮读的是同一份 `archiveCfg`),页面「历史归档」左下的
594
+ **归档开关**列表就是它的编辑口(名单来自已启用的 agent,**包括还没有任何归档的那些** ——
595
+ 树里根本没有它们,而这是唯一能给它们开关的地方)。
596
+ 被跳过的条目会在 `agentacta --archive` 的输出里报一行条数,`--list` 也会跟着打一行「已排除(config.archive.skip…)」,
597
+ 页面的开关列表表头则显示「已关 N 个」—— 不然「清单里怎么少了个 agent」会变成一次排障。
598
+ `POST` 只收**已配置**的 agent 名(拼错的名字会被拒 400,否则会永久躺在 config 里查不出出处)。
599
+ **配置只在服务启动时读一次**:手改 `config.json` 后,在跑的那个服务要重启才认(`agentacta --stop` 再 `--ensure`);
600
+ 只想马上看效果就直接敲一次 `agentacta --archive`(它每次都重新读配置)。
601
+
602
+ **保留策略:默认永久**。归档的意义就是对抗产品删日志,自动删归档等于又制造一次空档。
603
+ 要限量的机器给 `config.json` 的 `archive.maxDays`(按日期保留最近 N 天)/ `archive.maxMB`(整目录上限,
604
+ 超了从最老的日期文件开始删)。**删除只针对整天文件**,不做单条裁剪 —— 归档的价值在「那天发生过什么」,
605
+ 留半天的残卷没意义。
606
+
607
+ **存哪**:`~/.agent-acta/archive/<agent>/<YYYY-MM-DD>.jsonl` + `manifest.json`
608
+ (`{v, updatedAt, agents:{<agent>:{<day>:{count,bytes}}}}`)。agent 名做 `[^A-Za-z0-9._-] → _` 归一后当目录名。
609
+
610
+ **边界(刻意不做的事,别当缺陷)**:
611
+
612
+ - **不并入主列表**:归档是**只读旁路**,只在「历史归档」弹层里看。主列表的扫描/查询链路一律不碰归档目录 ——
613
+ 一旦接进去,「归档」就变成了第二份数据源,两边口径打架时没人说得清谁对。
614
+ - **不收进行中的轮**:冻结时要 `finished !== false`。半截数据(token 还没累完)没意义,它下一轮扫就会补齐。
615
+ - **events 只在非空时存**:只有 dsh / zcode / traedb 这类解析器会给 `events[]`,其余 agent 不存这个键。
616
+ - **容量量级**:本机 5022 条 → 195 个日期文件 ≈ 47.8MB(约 9.5KB/条)。
617
+ 按这个密度,一年全量归档约 **190MB 量级**;重活是 atomcode / claude / codebuddy 三家(各 10MB+),
618
+ codearts 这类文本日志最轻(约 1.2KB/条)。
619
+
620
+ **回归测试**:`node test/archive-test.mjs` —— 隔离 profile 起真服务,四条不变式各钉一条:
621
+ ① 详情确实冻住(删源文件后仍读得到)、② 复跑逐字节幂等、③ 合并不重建(删源+删索引后历史仍在)、
622
+ ④ 保留策略只删整天文件;另带 `config.archive.skip` 五条(报跳过条数 + 已冻文件不动 + 归档目录不被创建 +
623
+ 去掉开关能恢复 + 端点回显 skip)、三个端点、服务内自动归档(`AGENT_LOG_ARCHIVE_DELAY_MS`)、
624
+ `--archive --list` 输出与「`--list` 单敲必须退 2」。
625
+
626
+ ## 全文搜索(搜索框回车 / `GET /api/search`,R33)
627
+
628
+ **解决的问题**:页面搜索框过去是**纯客户端**过滤,只作用于已经拉回浏览器的窗口条目(占位符自己都写着
629
+ 「仅搜索当前 N 条」),而且只匹配「用户输入的前 300 字摘要 / 会话 / 模型 / 项目」——
630
+ AI 回复正文、工具入参/返回压根不在条目上。于是「上周那次报错」「哪个工具超时了」一律搜不到。
631
+
632
+ **怎么用**:在搜索框里敲词 = 照旧只筛当前窗口(免费、即时);**按回车** = 走后端全库检索,
633
+ 结果直接接手卡片列表,顶部多一条横幅。横幅上那几行都是**必须看**的:
634
+
635
+ ```
636
+ 全文搜索「npm run build」 · 命中 2 条 · 已索引 3382/3564 条 · 未套用时间范围 [限定当前时间范围] [清除]
637
+ ```
638
+
639
+ **四条口径(都是刻意定的,别当 bug)**:
640
+
641
+ - **默认不套时间范围**。这是全文搜索存在的理由 —— 「上周那次报错」本来就在页面默认的最近两天之外。
642
+ 想按当前日期区间收窄,勾横幅上的「限定当前时间范围」(会立刻重搜一次)。
643
+ - **一轮最多取回 200 条命中**(`/api/search` 的 `limit`,最大 2000)。服务端在 `capped` 里说「其实还有更多」,
644
+ 横幅据此多一段「命中不止这些,本次只取回 200 条」并带悬浮说明(换更具体的词 / 勾限定时间 / 先按 agent·项目收窄)。
645
+ 没这一段的话「命中 200 条」会被读成全库就 200 条 —— 与快照那行「仅保留最近 5000 条」同一处理方式。
646
+ - **每条轮次正文上限 8000 字**,且是**逐段先截再兜总上限**(用户输入 4000 / AI 回复 4000 /
647
+ 每个工具入参·返回各 800 / 每次调用正文 800)。不逐段截的话,一段长回复就能把额度吃干净、
648
+ 把排在它后面的工具行全挤出索引 —— 而「哪一轮跑过那条命令」正是最常搜的。
649
+ 环境变量 `AGENT_LOG_SEARCH_MAX_CHARS` 可改总上限。
650
+ - **正文之外还搜条目的元信息**(输入摘要 / 会话 / 项目 / 模型 / 工具名)。这层是内存里的实时值、
651
+ 不进索引。工具名必须走这条路:像 codearts 这种 `entryContent` 明确回 `tools: []` 的 kind,
652
+ 工具名只存在于条目的 `toolNames` 上,不匹配它就永远搜不到「哪一轮跑过 Bash」。
653
+ 命中元信息时片段上会带 `where`,页面因此能说清「命中:会话名」而不是摆一段看着不相干的正文。
654
+
655
+ **索引落在哪、长什么样**:`~/.agent-acta/search/<kind>.json`,一个**源标识**一条记录:
656
+
657
+ ```jsonc
658
+ { "v": 3, "rev": 7, "files": [ { "a": "claude", "f": "<源标识>", "sig": "m:…:s:…:o:…",
659
+ "t": { "<轮次>": "该轮正文" },
660
+ "tf": { "<轮次>": { "Bash": { "e": 2, "t": 1, "s": 0 } } } } ] }
661
+ ```
662
+
663
+ - 条目 id **不冻进索引**(id 的构造规则属于各解析器),查询时用 `(源标识, 轮次)` 反查 `srcs` 拿 id。
664
+ - `tf`(v2 起 I6;**v3 起 I13 扩三分类**)= 逐轮的工具失败分类计数,**三态**:`{"Bash":{"e":2,"t":1,"s":0}}`
665
+ 有失败、`0` 核对过没失败、**键缺失** 没索引到。三个分量是 `e`=error / `t`=timeout / `s`=soft
666
+ (只读结构化标志、不扫正文;超时算失败、软失败单列,见「工具失败画像」与 `LOGFORMATS.md`)。
667
+ 它与正文是同一次 `entryContent` 抽出来的(`indexFailsOf`),**不额外读一遍盘**;
668
+ 内存视图按 `(源标识, 轮次)` 键、不按条目 id —— id 要经 `srcs` 反查,而「载入那一刻首扫跑完没有」
669
+ 是不该影响正确性的外部状态(刚重启时会有一大批条目盘上有数据却查成"未知")。
670
+ `v` 变了就是**整片丢弃重扫**(与 `rev` 同一条纪律),所以 v2 的老分片会自愈重建,不需要手工删。
671
+ - `sig` = 扫描链 `files` 里那份**同一套**判活签名(整份重解析的 kind 用 `data.sig`,增量读的用 `mtime/size/off`);
672
+ 签名没变就整条跳过。扫描链里没有该源状态时退回 `fs.statSync` 兜底(见下)。
673
+ - `rev` = `PARSER_REV[解析器 kind]`,对不上就**整片丢弃重扫**(与 `index/` 分片同一条纪律)。
674
+
675
+ **什么时候建**:服务启动 **60 秒后**跑首轮(首轮不限量,一次建完),之后**每 10 分钟**一轮增量
676
+ (只重建签名变过的源,稳态下就是当天动过的那几个会话)。载入(读回盘上的分片)是懒的、且只做一次。
677
+ `LIB` / MCP 进程**只读不写**(`readOnly` 闸),`agentacta --search-reindex [--dry]` 是手动重建入口
678
+ (**服务在跑时拒绝执行**:索引分片一个 kind 一整片,两边同时写就是丢更新;先 `--stop`)。
679
+
680
+ **几条会以为是 bug 的地方**:
681
+
682
+ - **「已索引 A/B 条」里的 A 可以小于 B**。A 是「反查得到、这一轮查得动」的条数,B 是扫描链当前认识的条数。
683
+ 差额主要是两类:**没有可索引正文**的条目(源文件读不出来,或者这一轮本来就没有正文 ——
684
+ 例如 atomcode 有一批 **0 字节 `.jsonl`**,对话其实在 `.snapshot` 里),以及**已滑出内存窗口**的条目。
685
+ 两种都如实报在横幅上,不假装搜过。
686
+ - **命中的条目可能「在当前内存窗口外」**(`stale`)。它们在索引里、盘上也还在,只是这一轮拿不到统计字段
687
+ 就没法排序,所以这一轮不返回;源回来时立刻又能搜到。
688
+ - **搜不到归档条目**。归档(R28)是刻意隔离的只读数据源,不进这个索引;它的关键词搜索在「历史归档」弹层里。
689
+ - **正则模式**:把词当正则(`re=1`)。非法正则回 `400 + ok:false` 加原话,不会 500、
690
+ 更不会落到 404 兜底被说成「接口不存在」。回溯过深的模式有 2 秒预算兜底。
691
+
692
+ **两条「差点做错」的判活(改代码前先读)**:
693
+
694
+ - **不能自己 `statSync` 源标识**:cursor / gemini 的源标识是**伪路径**(`'cursor:' + dir`),
695
+ 一个目录算一个源、整份重解析,`files` 里记的是 `{m:0, s:0}`、真签名在 `data.sig`。
696
+ 按「stat 不到 = 源没了」处理会把这**两家整家静默漏掉**(本机 714 条,占 13%)。
697
+ 同理 `PARSER_REV` 要按**解析器 kind** 查(atomcode 的 `src.kind='atomcode'` 而 `files.kind='jsonl'`)。
698
+ - **扫描链没有某源的状态 ≠ 不能索引**:atomcode 会话目录里有一批 0 字节 `.jsonl`,
699
+ `scanAtomcodeDir` 对它是 `if (s <= off) continue`(0 <= 0),在 `files.set` 之前就短路了;
700
+ 可条目照旧由 `.meta` 产出来、`src.file` 指的正是这个空 jsonl。不加「退回 `fs.statSync`」这条兜底,
701
+ 本机 **47 个源 / 885 条(占 25%)会被整批判成「源没了」**,连按会话名/模型名都搜不到。加上之后覆盖率 2679 → 3382。
702
+
703
+ **接口**:
704
+
705
+ - `GET /api/search?q=&re=1&agent=&agents=&project=&projects=&status=&from=&to=&limit=&offset=`
706
+ —— `from`/`to` **只在页面勾了「限定当前时间范围」时才传**;不传 = 不限时间。
707
+ 返回 `{ ok, total, scanned, stale, capped, entries[], index, rangeLabel, agents, projects }`,
708
+ 条目与 `/api/snapshot` 同形状、另加 `_snip = { pre, hit, post, where? }`(三段纯文本,页面自己转义再高亮)。
709
+ 关键词为空 → 400;非法正则 → 400 + 原话。
710
+ - `GET /api/search/status` → `{ ok, entries, files, total, building, unreadable, noState, bytes, at, maxChars, pruned }`
711
+ —— 横幅的「已索引 A/B」与「正在建」用的就是前四个;`pruned` 是 R34 加的,见下。
712
+ - **记录按绝对路径认人**:一条记录只有在扫描链把它的源文件扫成条目、给出 `(源标识, 轮次) → id` 的映射之后
713
+ 才**可能**被搜到,所以源不在本机的记录(跨平台/跨机器搬过来的、或日志已被产品删掉的)载入时就被剪掉
714
+ (`pruned` 报数,写进日志),留着只会让「已索引 A」虚高。被 LRU 淘汰但源还在盘上的记录**不受影响**,
715
+ 那正是「合并而非重建」要保住的那批。
716
+
717
+ **回归测试**:`node test/search-test.mjs`(30 条断言)。分两段:第一段拿**假 deps** 直接单测 `search.mjs`
718
+ (增量跳过 / 只重建变了的源 / 大小写不敏感 / 片段保持原文大小写 / 非法正则不抛 / LRU 淘汰后正文留盘 /
719
+ `rev` 失效 / `readOnly` 一个字节不写 / 元信息兜底层);第二段起真服务走 `/api/search`,验接线、
720
+ 筛选口径与「不套时间范围」这条(fixture 里特意放了 6 天前的那一轮)。
721
+
722
+ ## 导出(列表筛选集 JSON / CSV / Excel + 单条完整详情,R11)
723
+
724
+ **入口两处**:列表工具栏的下载图标 → 「导出当前筛选结果」下拉(JSON / CSV / Excel);展开某一轮后,
725
+ 详情底部吸底条上的「导出该轮完整详情」。
726
+
727
+ **先认清导的是哪一份**(最容易对不上账的一条):就是列表此刻那份筛选集 `filtered`,
728
+ 也就是「服务端按 agent / 项目 / 时间 / 状态取回来的那批」再过一遍客户端关键词、`只看异常`、侧栏那枚
729
+ 「显示非 LLM 轨迹」的显示偏好。**它跟列表一致,所以也跟着一致地受限**:
730
+ - 快照窗口由右上角「条/页」档位决定(默认 200,档位最大 500;服务端那边最高认到 10000,
731
+ 前端另有 5000 条内存兜底 `MAX_ALL`,真触到了列表那行会写「仅保留最近 5000 条」)——
732
+ 要更多就收窄时间范围或把档位调大。**导出的是已加载窗口里筛出来的全部,不止渲染出来的那一屏**;
733
+ - 搜索态(R33 那种「搜索框回车接管列表」)下 `filtered` 源自 `/api/search` 的 `entries`:那里默认只回 **200 条**
734
+ (`limit` 最大 2000),且条数按「命中集 ∩ 当前筛选集」算。横幅写的「命中 N 条」就是这 N 条,
735
+ 于是**导出与横幅永远一致**;命中真被 200 截了,横幅会多一段「本次只取回 200 条」(见「全文搜索」那节的四条口径)。
736
+
737
+ **三种格式都带「按什么条件导出」**(R11 的验收点,`exportFilterText`):时间 / agent / 项目 / 状态 / 关键字 /
738
+ 只看异常(勾了才写)/ 显示档位。字段名照实写、不自欺 —— 档位也在里面,因为「这批为什么只有 200 条」正是靠它回答。
739
+ - **JSON**:`{meta:{exportedAt,count,filters[]},entries[]}`。`meta.exportedAt` 是 ISO(UTC,带 `Z`),给机器读的。
740
+ - **CSV**:**UTF-8 BOM + CRLF**(中文在表格软件里不乱码)。头两行是 `筛选条件: …` 与 `# 共 N 条`,第三行是表头,其后是数据。
741
+ 值里含逗号 / 双引号 / 换行时按 CSV 正规做法用双引号包住、内部双引号翻倍。
742
+ - **Excel**:扩展名 `.xls`,内容是 **SpreadsheetML 2003 XML**(同样带 BOM)—— Excel / WPS 原生打开。
743
+ 说明行在前、一个空行隔开、再表头 + 数据;纯数字的值写成 `ss:Type="Number"`,其余 `String`。
744
+ **为什么不做真 `.xlsx`**:那要么引 SheetJS 之类的库、要么走 CDN,而这是个离线本地旁路 ——
745
+ 多一个依赖就多一条「断网 / 装不上」的烂路;XML 那一套表格软件认,手写够了。
746
+
747
+ **15 列**(CSV/Excel 表头与 JSON 的 `entries` 同序):`time`(本地时间串,与卡面一致)、`agent`、`project`、
748
+ `session`、`status`、`preview`(用户输入摘要,源头截 300 字)、`models`、`dur`(毫秒)、`tin` / `tout` /
749
+ `tcache` / `total`、`calls`、`tools`、`nollm`(是 / 空)。**没有正文与工具入参** —— 那是单条详情的活。
750
+
751
+ **单条完整详情**走 `GET /api/entry?id=&full=1`:用户输入 / AI 输出 / 每次工具调用的入参与返回都是**未截断**的
752
+ 完整内容(`full=1` 就是为此存在的),连同条目元信息(agent / 项目 / 会话 / 时间 / 状态 / 耗时 / 模型 / token 合计)
753
+ 写成 `{meta,entry}`。条目已经不在列表里(被 LRU 淘汰、或列表已筛走)会提示「无法导出:找不到该条目」,
754
+ 不会闷声导一个空文件;拉取失败给的是带原因的报错。
755
+
756
+ **文件名一律本地时间**:`agent-acta-YYYYMMDDHHmmss.<ext>`,单条那份是 `agent-acta-<时间>-entry-<id 前 40 字符>.json`。
757
+ 用本地而不是 `toISOString()`,因为文件名要和筛选区间(本地自然日)与卡面时间对得上 ——
758
+ 东八区拿 UTC 命名会出现「9 点导出的文件叫 01 点」。
759
+
760
+ **实现落点**:序列化与下载是 `page/shared.js` 里四个零依赖纯函数(`csvCell` / `buildCSV` / `buildExcelXLSText` /
761
+ `downloadBlob`),「筛了什么、导哪些行」留在根 setup(`exportFilterText` / `exportRows` / `exportData` /
762
+ `exportEntryDetail`)—— 前者不认响应式状态,才放得进 R12 那份跨端共享逻辑的单一面孔(见 `test/shared-snippet-test.mjs`)。
763
+ 下载走 `a.download` + `URL.createObjectURL`,**不经过服务端**:整个导出不新增任何写盘、也不新增网络出口。
764
+
765
+ ## 两轮对比(卡片顶行「对比」选两条,I9)
766
+
767
+ 回答的是「**同一个 prompt 这次为什么慢**」:主列表每张卡片顶行有一个「对比」按钮,点两条自动弹开并排对比弹层。
768
+ 只许两条 —— 选第三条会给一句「一次只对比两条轮」;再点已选中的那条是取消选择。
769
+
770
+ **比的是什么**:上半区是条目**本体**的指标(快照就带,不必等详情):耗时(两条并排的相对柱,以较长者为满刻度)+
771
+ 模型 / LLM 调用 / 工具 / 积分 / Token 输入·输出·缓存命中·合计 / 上下文占用 / 压缩。
772
+ 下半区是正文(用户输入、AI 输出、工具调用逐条),走 `/api/entry` **懒加载**,点开对比才取,两条各自独立。
773
+
774
+ **每条指标行都标「谁大」**:标签后跟 `差 N`,数值大的一侧加深加粗。前提是**两侧同单位**:
775
+ qoder 与别家混比时,「积分」行的别家一侧、Token 行的 qoder 一侧都给 `—` 并且**不作差**
776
+ (单位不同的差没有意义),`—` 上挂了 title 说明是"不适用"还是"没数据"。
777
+
778
+ **qoder 一律不给 token 数字**:它日志里 `input/output/total_tokens` 恒为 0,真实花费只有积分(`credits`),
779
+ 上下文也只有最后一次调用的占比(`context_usage_ratio`)而不是占用/容量。所以:
780
+
781
+ - 只要有一条是 qoder,就出现「积分」行;两条都不是 qoder 才完全没有这一行;
782
+ - 两条都是 qoder 时 Token 四行整段不出现(没有可比的东西);
783
+ - 「上下文占用」行 qoder 侧显示占比(如 `19.7%`),别家显示 `占用 / 容量(百分比)` —— 与卡片同一口径。
784
+
785
+ **关掉再开**:弹层关掉不清掉手里那两条,所以重选同一对不会先闪空态;但弹层里**没有**「换条目」入口,
786
+ 要换就得回列表重选两条。「选完第一条后那条掉出当前列表」(SSE 增量、改筛选、回车切进全库搜索命中集都算)
787
+ 时不会弹一个空层:会提示「先选的那条已不在当前列表里」,并把你刚点的那条留在选中态。
788
+
789
+ **实现落点**:组件 `page/dialog-compare.js`(222 行,模板 + `cmpRows` 那张表全在里),
790
+ 根只持 `compareSet / compareVisible / compareReq` 三个 ref 与 `onCompare`(`agent-acta-page.html`)。
791
+ 状态"谁用谁持有":选中组、两条本体、详情缓存都在根(切视图/关弹层都不丢),组件只认 props 与 inject 的 `getJSON`。
792
+ 卡片模板为按钮高亮新读了一份 `compareSet`,所以 `cardMemo` 同步加了 `cmpBit` 一项(漏了就点不亮)。
793
+ **纯前端**:不新增任何接口、不写盘、不碰解析口径(无 `PARSER_REV` 影响)。
794
+
795
+ ## 单轮复现包(卡片详情「导出复现包」/ `GET /api/repro`,I8)
796
+
797
+ 一键把**某一轮**打包成 zip,用于贴群 / 提 bug 时带上完整上下文:原始日志片段 + 完整解析结果 + 版本指纹 +
798
+ 导出那一刻的筛选条件。入口在卡片展开后的底栏「导出复现包」。
799
+
800
+ **接口**:`GET /api/repro?id=<条目 id>&filters=<JSON 串>` → `{ ok, meta, parsed, raw }`。
801
+
802
+ - `meta`:`version / build / pageBuild / parserRev` 四枚指纹 + `generatedAt` + 条目本体 `entry` +
803
+ `src`(原始文件路径、轮序号、kind)+ `filters`。
804
+ - `parsed`:就是 `/api/entry?full=1` 那份完整解析结果(工具入参/返回、逐次调用都不截断)。
805
+ - `raw`:`reproRaw()` 切出的**该轮原始片段**,三种结果之一 ——
806
+ ① 能切:`extractable:true` + `lines[]`,`note` 里写清「本文件第 N–M 行(1-based,与磁盘一致,空行已跳过)」;
807
+ ② 源不是单文件 JSONL(数据库 / 多帧压缩 / 内存转录 / 多文件):只给 `file` 路径 + 一句说明;
808
+ ③ 源文件未载入内存(还没扫到或已淘汰)、或按开轮判定定位不到那一轮(源被轮转/截断):同样退回说明文字。
809
+ ②③ 不是 bug,是设计边界 —— 能切的这九家:`claude / codex / buddyjsonl / doubao / minimax / kimi / copilot / generic-jsonl / openclaw`。
810
+ - 坏 id 或条目已淘汰 → **404** `{ ok:false, error:'找不到该条目…' }`。
811
+
812
+ **「原始片段」的归轮口径**:从这一轮的**用户输入行**起,到**下一轮开始前**止,含该轮内的模型响应、工具调用与
813
+ `tool_result` 等全部交互 —— 用的就是各解析器**同一套**开轮判定(`copilotTurnStart` / `genericTurnStart` /
814
+ `mavisUserText` 三个新导出的 helper),不是另写一遍"看着像开头"的启发式。读原始行走 `readCompleteLines`,
815
+ 与扫描侧同为"只吃完整行",半截尾行不会被算进来。
816
+
817
+ **打包在浏览器里做**:`page/shared.js` 的 `makeZip()` 手写 **store 模式 ZIP**(不压缩、零依赖,不引 JSZip ——
818
+ 离线本地服务引不了 CDN,而内容本就是文本,压不压无所谓),CRC-32 自己算。zip 里四个条目:
819
+ `repro-meta.json` / `repro-raw.jsonl` / `repro-parsed.json` / `README.txt`。文件名
820
+ `agent-acta-<本地时间>-repro-<id 前 40 字符>.zip`(时间用本地、与筛选区间和卡面对得上,同 R11 那条理由)。
821
+ 下载走 `URL.createObjectURL` + `a.download`,**不经过服务端**:整个导出既不在服务端写盘,也不新增网络出口。
822
+
823
+ **筛选条件是页面递进来的**(服务端没有"筛选态"这个东西),原样回显进 `meta.filters` 并抄一份进 zip 里的
824
+ `README.txt`,所以事后能自证「当时是按什么条件看的这一轮」。原始片段切不出来时,`repro-raw.jsonl` 里会写
825
+ `# 原始片段不可用:<原因>` 并附上原始路径,成功提示里也如实带一句"原始片段不可用"。
826
+
827
+ **那个原始片段文件为什么叫 `.jsonl`**:内容就是磁盘上那几行原文(逐行 JSON),所以扩展名跟着走。
828
+ 2.10.3 及之前包内 `README.txt` 的清单误写成 `repro-raw.log`(条目名本身一直是对的 `.jsonl`),
829
+ **2.10.4 统一过来**,同时删掉了当初那个两个分支都返回 `.jsonl` 的 `rawExt` 三元。
830
+
831
+ **回归**:`test/repro-test.mjs`(隔离 HOME,四条验收:形状 / claude 按开轮精准切片 / `filters` 回显 / 坏 id 退 404),
832
+ 已登记进 `test/selftest.mjs` 的 `cli` 组。**没做**:zip 解压回验、多轮合并打包、不可提取 kind 的尽力重读。
833
+
834
+ ## 工具失败画像(统计弹层 →「时延分析」→ 第 4 个子 tab,I6;I13 扩三分类)
835
+
836
+ **回答的问题**:「哪个工具最常把我绊倒」。同一段筛选里给出每个 **agent × 工具名** 的
837
+ `失败次数 / 调用次数 / 失败率`,外加一张按 `agent × 来源 kind` 摊开的**覆盖度表**(谁有信号、谁为什么是空的)。
838
+
839
+ 入口在「用量统计 → 时延分析」下面,与最慢轮 / 工具调用 / 缓存命中率共用同一次 `/api/analyze` 取数
840
+ (切子 tab 不再发请求)。
841
+
842
+ ```
843
+ GET /api/analyze?range=30d&top=10 → { …slowest, byTools, byCacheRate,
844
+ toolFails: { minTotal: 10, building: 0, truncated: 0, totalKinds: 61,
845
+ totals: { err, timeout, soft }, // 三分类总数,**全量行**口径(不受 topN 截断影响)
846
+ rows: [ { agent, name, fail, err, timeout, soft, total, rate, lowDenom, hasTs } ], // 按 fail 倒序,只含 fail>0
847
+ agents: [ { agent, kind, tier, tsSig, turns, known, unknown, failTurns, softTurns, turnRate } ] } }
848
+ ```
849
+
850
+ - `fail = err + timeout`(**超时算失败** —— 它就是失败);`soft`(软失败)**不进 fail、不进失败率**,
851
+ 只进 `totals.soft` 与轮级 `softTurns`(`No matches found` = rg 没搜到,是最常见的正常结果)。
852
+ - `fail` 来自搜索分片的逐轮计数,`total` 来自条目的 `toolNames`,**且只累加"分子那一批同集合的轮"** ——
853
+ 没核对过的轮连分母一起不算,否则失败率会被"根本没看过的轮"稀释成假的低值。
854
+ - `tier` 三档(`signal` / `noDetail` / `noFlag`)与「未索引」是四件事,页面分开交代;
855
+ 后三种**一律不画 0**(那里的空是"没有这个信息",不是"从不出错")。
856
+ - `tsSig=1` = 这个来源的详情带得出**超时 / 软失败**信号(I13,现只有 `claude` 那一档,qoder-cn 共用)。
857
+ 没这一层的来源 `timeout` / `soft` 是 **`null` 而不是 0**,页面整段不印 —— 那个 0 会被读成
858
+ 「这个 agent 一次没超时」,而真相是「源里根本没有这一层」。
859
+ - `lowDenom=1` = 分母不足 `minTotal`(默认 10 次)。这么定的原因很实在:本机实测
860
+ `StopCommand 1/1 = 100%`、`ExitPlanMode 1/2 = 50%`,而真正天天绊人的 `Bash` 只有 1.0%~4%。
861
+ 排行主序按**绝对失败次数**,率只作并列维度与第二列;条形按本次最高的那个率归一(不是按 100% 归一 ——
862
+ 真实失败率都在个位数,按满刻度画会得到一排看不见的空条)。
863
+ - `building=1` = 后台还在建索引,`unknown` 那批还会往下降;此时画像不完整,页面顶上有这一句。
864
+
865
+ **为什么挂在搜索索引上**:工具失败标志只存在于**详情侧**(`tools[].error` / `.timeout` / `.soft`,见
866
+ `LOGFORMATS.md`「工具失败信号」),而逐条读详情的代价极高(claude 一条 41ms —— 每次 `readFileSync` 整份转录)。
867
+ `search.mjs` 建索引那一遍本来就在逐条 `entryContent`,I6 就顺手在同一遍里抽出 `tf` 计数落进分片:
868
+ **零额外读盘、不 bump `PARSER_REV`**(分片自己的 `SHARD_V` 1→2;I13 又 2→3,老搜索分片自愈重建)。
869
+
870
+ **与「异常巡检」(R9)的 `fail` 规则不重叠**:那条按 agent 统计**轮级** `status != ok` 的占比,
871
+ 而工具失败在多数 agent 里根本不冒泡成轮 err(claude 全体非 ok 2.9%,抽样带工具的轮 20% 含失败工具)。
872
+
873
+ **I13 补上的那两层(超时 / 软失败)**:判据仍然是「只读解析器落好的结构化标志、一个字正文都不扫」,
874
+ 变的只是"有几位可读"。信号在 claude 转录的**记录级** `toolUseResult` 上(不在 `tool_result` 块里),
875
+ 由 `parsers/claude.mjs` 的详情回填搬到 `tools[]`:`timedOutAfterMs` → `timeout`、
876
+ `returnCodeInterpretation` / `staleRecovered` → `soft`。要害是**超时那批 `tool_result.is_error` 是显式 `false`**
877
+ (本机 141 份转录 / 30 次超时,一律如此,不是缺键)⇒ 旧口径下它们不是"未知",是被**明确记成"这一跑没问题"**;
878
+ 软失败同理(`No matches found` 39 / `Files differ` 2 / `staleRecovered` 16),只是它本来就该算正常。
879
+ 这一笔**只扩详情侧字段、落盘 turns 一个字节没动 ⇒ 不 bump `PARSER_REV`**。
880
+
881
+ **没做**:点行下钻回"该工具失败的那些轮"(要新的筛选维度);MCP 面新增工具 —— 目前只暴露
882
+ `slowest_turns`(不读 `toolFails`,**不受影响**),所以 agent 现在还**问不出**「哪个工具最常超时」;
883
+ 补 codex / generic 的失败信号(属解析口径,要单独立项 + bump)。
884
+
885
+ **回归**:`test/tool-fail-test.mjs`(隔离 HOME,两阶段 25 条:没建索引时必须报
886
+ `unknown` 而不是"零失败" / 分片 `tf` 三分类落盘 / 排行与分母同批 / `lowDenom` 两档 / 按天筛选同时收窄分子分母 /
887
+ `fail = err + timeout` 且软失败不占排行 / 无信号来源出 `null` 不出 0(用 gemini 一家验)/
888
+ `PARSER_REV` 未动),`test/platform-test.mjs` 第 5 节钉住三态在载入与剪枝后仍可区分。已登记进
889
+ `test/selftest.mjs` 的 `cli` 组。
890
+
891
+ ## 数据与配置
892
+
893
+ > **数据目录 2.0.0 起在 `~/.agent-acta/`。**沿革:最早在 `~/.atomcode/agent-log/`(那时它还只是个
894
+ > 「看 atomcode 逐次 token」的小工具,名字顺着宿主的窝叫)→ 1.9.0 搬到 `~/.agent-log/`(跟
895
+ > `~/.claude` / `~/.codex` 同一层规则)→ 2.0.0 随改名落到 `~/.agent-acta/`。
896
+ > **升级不用手工操作**:下次启动服务时自动搬一次(配置、索引、pid 一起过去),**两条老位置都认**,
897
+ > 跨代升级(从没装过 1.9.0 的机器)也一步到位;旧位置各留一张 `<老目录名>-MOVED.txt` 说明。
898
+ > 若新位置里已经有真配置,则一切照旧、不再搬;搬迁只在「新位置还没有真配置」时发生。
899
+
900
+ - 配置:`~/.agent-acta/config.json`(v3,含扫描节奏 `scanMs`)
901
+ - 老版本(v1/v2)会自动升级到 v3;v2 里 codebuddy/workbuddy 若仍是出厂默认的 trace 预设,
902
+ 且本机存在 `~/.codebuddy/projects`,会被就地改成 `projects` 解析;**自己改过路径的一律不动**。
903
+ - `sessionsExtra`(可选,数组):**额外的同格式日志根**。一个 agent 默认只有一个 `sessions`,
904
+ 但 kimi 有两份(CLI 的 `~/.kimi-code` 与桌面版内嵌的那份 home,可能分在两个盘),
905
+ 只认一个就等于静默丢一半数据。这一项由服务端每轮自动发现时重算并写回,
906
+ 桌面版卸载/挪盘后会自动摘掉,**不用手改**。
907
+ - `ctxWindows`(可选):上下文窗口表,模型名或前缀 -> 容量。一般不用手改——
908
+ 页面侧栏「上下文窗口」就是它的编辑器(含「日志里出现过但没填」的模型清单)。
909
+ **这张表只服务 claude 与 kimi**:这两个的日志里**没有**窗口容量字段(claude 的死在网关侧,
910
+ kimi 的 `wire.jsonl` 只记用量),只能按模型名查表;查不到就只显示占用、不画进度条。
911
+ 其余 agent 的窗口来自各自源头(atomcode 的 `.meta`、cursor 的 `composerData`),不由这张表决定。
912
+ - `traeKey`(可选):Trae CN 的 SQLCipher 库密钥(64 位十六进制)。**不配也能用**——trae 自动回退
913
+ `renderer.log` 骨架;配置后才读库(有完整用户输入与逐调用明细)。获取办法见「Trae 的 SQLCipher 库」一节。
914
+ - `archive`(可选,R28):`{enabled, maxDays, maxMB, skip}`。默认 `{enabled:true, maxDays:0, maxMB:0, skip:[]}`
915
+ = 自动归档开、**全 agent 参与**、**永久保留**。
916
+ `enabled:false` 只关自动轮(`agentacta --archive` 手动跑不受影响);`maxDays`/`maxMB` > 0 才限量,
917
+ 超了从最老的**整天文件**开始删;`skip`(agent 名数组)**按 agent 停掉归档**,
918
+ **只停未来、绝不删已冻的文件**(写 `["cursor","trae"]` 这样)。与出厂默认同值的写法不会落盘。
919
+ 口径详见「历史归档(R28)」一节。
920
+ - `alert`(可选,R9):异常巡检规则:`{tokens:{on,val}, durMs:{on,val}, fail:{on,val,window}}`。默认全关。
921
+ 挂在同 scanMs 的持久化通道上:页面弹层「异常巡检」就是它的编辑器(POST /api/alert),
922
+ **保存即落盘、重启服务仍在**;三条规则全是顶层规则,只有 `fail` 在求值时**按 agent** 统计。
923
+ `tokens`:单轮 total(输入+输出+缓存)超 `val`(token);`durMs`:单轮耗时
924
+ (轮内首条→末条时间差,含挂机/隔夜)超 `val`(毫秒);`fail`:某 agent 最近 `window`(1..100)轮里
925
+ 失败(status != ok)占比超 `val`(%),命中该 agent 构成失败的那几轮。全关 = 绝不打扰。
926
+ 命中集合是**纯内存派生**(不落盘不占索引),任一规则/阈值改动或新日志扫完会就地重算并
927
+ 广播 `alert` 事件,前端据此点亮侧栏徽章与「只看异常」,新产生的异常条目无需人工翻找。
928
+ 口径详见「异常巡检(R9)」一节。
929
+ - 索引:`~/.agent-acta/index/<kind>.json`(按格式分片,仅存带增量偏移的状态)
930
+ - 每份索引带 `rev`,与解析器版本不符时该项整体丢弃、下次冷读一遍——改了统计口径又不想让页面报旧数字时用它。
931
+ - 归档:`~/.agent-acta/archive/<agent>/<YYYY-MM-DD>.jsonl` + `manifest.json`(R28,条目级冻结;默认永久保留)
932
+ - 端口:`AGENT_LOG_PORT` 环境变量可覆盖,默认 `14570`
933
+ - pid 文件:`~/.agent-acta/server-<端口>.pid`(`--stop` 的兜底线索;判活仍以端口为准,
934
+ 被强杀过的进程留下的陈旧文件不会误导它)
935
+ - **空闲自停**:右上角「空闲自停」下拉可选 30 分钟 / 2 小时 / 6 小时(默认关)。
936
+ 条件是**页面全关(无 SSE 连接)且这段时间没有任何新日志**——正在跑会话就会一直续期。
937
+ 自停只是退出进程,配置与索引都在,下次开会话会被 hook 的 `--ensure` 重新拉起。
938
+ 临时调试可用 `AGENT_LOG_IDLE_MS=60000` 覆盖阈值(不落盘)
939
+
940
+ ## 异常巡检(R9)
941
+
942
+ **解决的问题**:异常(单轮 token 爆表、单轮卡了很久、某 agent 失败率飙升)要命的是**不知道它发生了**,
943
+ 等手动翻列表才看到,往往已经过了半天。R9 让它**主动说出来**:侧栏多了「异常巡检」一项,
944
+ 命中时该项带红色角标(条数),请求日志列表多一个「只看异常」勾选框,点开弹层逐条查看并一键定位回主列表。
945
+
946
+ 三套规则**全在顶层**(不是 per-agent),保存即落盘 `config.alert`,**与 scanMs 同一套设置机制**——
947
+
948
+ | 规则 | 求值对象 | 阈值 | 命中 |
949
+ |------|---------|------|------|
950
+ | 单轮 token 超阈值 | 每条日志 | total = 输入+输出+缓存 > N token | 该轮 |
951
+ | 单轮耗时超阈值 | 每条日志 | 轮内首条→末条时间差(含挂机/隔夜)> N ms | 该轮 |
952
+ | 失败率超阈值 | **按 agent** | 最近 N 轮(fail.window,1..100)中失败(status != ok)占比 > X%(1..100) | 该 agent 里构成失败的那几轮 |
953
+
954
+ - 命中集合 `alertHits` 是**纯内存派生**,不落盘、不进索引;默认全关 = 绝不打扰。
955
+ - 任何一轮扫描后(有新日志)或规则被改动时**就地重算**并广播一条 `alert` 事件
956
+ (带摘要 `summary` + 命中 id 列表),前端据它点亮徽章 / 更新「只看异常」集合,
957
+ 于是**新产生的异常条目无需人工翻找即可被提示**(R9 验收点)。
958
+ - 接口:`GET /api/alert` / `POST /api/alert`,详见「服务端 HTTP API」。
959
+ - 回归:`node test/alert-test.mjs`(不发浏览器,走接口断言命中数与字段)。
960
+
961
+ ## 磁盘占用(侧栏「磁盘占用」/ `GET /api/usage`,R30)
962
+
963
+ **解决的问题**:占用散在三处 —— 各 agent 自己的日志目录、`~/.agent-acta/index/` 的分片、`archive/` 的日期文件,
964
+ 没有任何一处能一次看全。「要清磁盘」时就只能 `du` 来 `du` 去,还得记得 trae 的库要连 `-wal` 一起算。
965
+
966
+ **页面**(侧栏「磁盘占用」):按 agent 一行,列 日志 / 归档 / 内存条目 / 合计(合计带占比条,列头可点排序);
967
+ 下面两块是索引分片与 `~/.agent-acta` 顶层构成。**只读** —— 这一页不删任何东西,删都要自己动手。
968
+
969
+ 四条口径(服务端刻意这么定的):
970
+
971
+ | 口径 | 为什么 |
972
+ | --- | --- |
973
+ | 索引片**单列**,不并进 agent 行 | 一片按 kind 存、可能同时供好几个 agent(traedb 用的就是 tracecode 那片),并进行里相加就是重复计数 |
974
+ | 库类根连 `-wal` / `-shm` 一起算 | 那两个常常比主文件还大,只算主文件会把占用报小一半 |
975
+ | 两个 agent 指同一份目录时:行内各报各的,`totals.logBytes` 只算一次(`logBytesRows` 才是各行相加) | 这一页要回答「要清多少地方」,重复计数的那个数不能当总数用;页面上那行带「共用目录」标记 |
976
+ | 遍历**带预算**(默认最多 6 万个目录项 / 4 秒),走不完标 `partial` | 整个服务只有一个线程,扫描与 SSE 都在这上面;宁可写「统计到一半」也不给一个看似完整的假数。截断按目录**轮转**摊到每个根,避免「只有第一行有数、其余全 0」把排名带偏 |
977
+
978
+ 临时调预算用环境变量(不落盘):`AGENT_LOG_USAGE_MAX_FILES` / `AGENT_LOG_USAGE_MAX_MS`。
979
+ 结果缓存 60 秒,页脚「刷新(用缓存)」走缓存、「重新统计」带 `?force=1` 绕开。
980
+
981
+ ## 自检(`agentacta --doctor`,R29)
982
+
983
+ 输出一段**可粘贴的病历**:排障第一步跑它,把整段贴出来,省掉「你那边是哪一版 / 有没有改过配置」这类来回。
984
+ 与页面上的「环境诊断」分工不同 —— 诊断页回答「**能不能发现路径**」(目录在不在、认成什么格式、没认出来是为什么),
985
+ `--doctor` 回答「**已经攒下的这些东西还可不可用**」。
986
+
987
+ 查这些,判据全部取自服务端真正在用的那套(不另编一套判据,否则体检结果与真实行为会分家):
988
+
989
+ | 检查 | 判据 | 说明 |
990
+ |---|---|---|
991
+ | 配置能不能解析 | `config.json` 的 JSON + 版本号 | 解析失败=`[fail]`:服务会**静默回落成默认预置**,手工加的 agent 与 `traeKey` 全丢 |
992
+ | 配置指向的目录还在不在 | `confAvailable()`(库类 kind 认文件,其余认目录) | `source:auto` 的会自动摘掉;手工项会一直留着,确认卸载了就删 |
993
+ | `kind` 还有没有解析器 | `scanBranch()` 的兜底分支 | 认不出会**按原子码式目录扫**(有目录也扫得出东西,只是字段少一截)——静默,所以必须点出来 |
994
+ | generic-jsonl 的 rules | `validateGenericRules()` | 坏规则会被回落成禁用,点名是哪条 rule、坏在哪 |
995
+ | dsh 的 zstd | `DSH_ZSTD_OK` | Node < 22.15 解不开 zstd,目录在也一条都出不来(升级 Node,不是路径问题) |
996
+ | 索引片 | `index/<kind>.json` 能否解析、`rev` 与 `PARSER_REV` 是否一致 | 坏片/旧口径片会在启动时整片丢弃重扫(源日志还在就只是慢一轮,数据不丢) |
997
+ | 端口上跑的是不是这一版 | `/api/version` 的 `build` 指纹 | 不一致=`[fail]`,典型症状「页面是新的、接口是旧的」 |
998
+ | 页面文件 | 两个占位符 + `/page/` 资源齐不齐 | 缺了就是白屏(这条自检原本只写进服务日志,用户看不到) |
999
+ | 归档现状 | `manifest.json` + 归档目录实况 | 只报多大、几天、多少条、跳过了谁 |
1000
+
1001
+ - **只读**:不搬家、不改配置、不动索引。跑体检本身不该改变被诊断的状态 —— 否则「跑一下就好了」这件事
1002
+ 永远说不清原因。所以它不调 `loadConfig()` / `loadIndex()`(那两个会做 v1→v2 迁移、把旧配置/旧索引改名、
1003
+ 把 `rulesHash` 落盘),配置与索引都是自己按只读方式读一遍。测试里对这一点有逐字节断言
1004
+ (`test/doctor-test.mjs`)。
1005
+ - **退出码**:`0` 没有失败项 / `1` 有失败项。`[warn]` 不拦脚本(它多半会自己好,或需要人判断)。
1006
+ - **在源码目录里跑,会必然报一条「端口上跑的是另一版」——这是对的**:那份指纹取的是**当前这个文件**,
1007
+ 而端口上常驻的通常是全局 npm 包那份(内容不同版号相同)。要验装出来的那版就敲 `agentacta --doctor`。
1008
+ - **不上色**:这段输出是给人粘贴的,ANSI 转义一贴就成乱码。级别用 `[ok]`/`[info]`/`[warn]`/`[fail]` 前缀表达。
1009
+ 级别口径:`[fail]` = 现在就已经是坏的(有东西在静默地少);`[warn]` = 会自己好、或需要人判断;
1010
+ `[info]` = 正常但值得知道(还没跑过服务、还没归档)。
1011
+
1012
+ ## 作为 MCP server 接入(`agentacta-mcp`,R31)
1013
+
1014
+ 面板只能「看」,而看的前提是人已经知道要看哪一列。复盘类的提问天然是问句("我上周最慢的一轮是哪一轮"
1015
+ "哪个项目 token 涨得最凶"),拿界面去答就得人肉翻筛子。`agentacta-mcp`(随包发的第二个 bin,`mcp-server.mjs`)
1016
+ 把这套查询面包成 MCP server,claude / cursor / qoder 之类的客户端可以直接问。
1017
+
1018
+ ### 三条硬边界
1019
+
1020
+ - **只读**。数据全部来自 `agent-acta-server.mjs` 导出的 `LIBRO`(**只读库模式**:同一个文件被 `import` 而不是
1021
+ 作为入口跑起来时,落索引 / 写配置 / 搬数据 / 起 HTTP / 挂定时器这五处全部短路,写盘只剩三个入口
1022
+ ——`saveConfig`、`saveIndex`、`loadConfig`/`loadIndex` 里的一次性改名,都各自带了门)。
1023
+ 所以常驻服务在不在跑都能用,两个进程也不会互相踩文件;`test/mcp-test.mjs` 里对"跑前跑后整棵树逐字节相同"
1024
+ 有断言,不是"看着像只读"。
1025
+ - **不新增网络出口**。传输只有 stdio:客户端 fork 本进程,我们只往 stdout 写协议消息。stdout 是协议通道,
1026
+ 所以一行诊断都不许往那儿打(服务端的 `console.log` 在只读模式下已整体改道 stderr)。
1027
+ - **不美化口径**。七个工具调的就是页面/HTTP 接口用的那批函数(`filterEntries` / `dailyAgg` / `modelAgg` /
1028
+ `analyzeAgg` / `sessionsAgg` / `entryContent`),bin 里不另算第二套数字;截断与上限都在返回里如实标注。
1029
+
1030
+ ### 接入
1031
+
1032
+ 命令路径填 `agentacta --where` 打出来的那份(全局包),或源码目录里的 `mcp-server.mjs`:
1033
+
1034
+ ```json
1035
+ { "mcpServers": { "agent-acta": { "command": "node", "args": ["C:\\Users\\<你>\\AppData\\Roaming\\npm\\node_modules\\agent-acta\\mcp-server.mjs"] } } }
1036
+ ```
1037
+
1038
+ - **Claude Code**:`claude mcp add agent-acta -- node "…\\mcp-server.mjs"`(或写进项目根的 `.mcp.json`,键名同上)
1039
+ - **Cursor**:Settings → MCP,或 `~/.cursor/mcp.json` 里加上面那一段
1040
+ - **Qoder**:MCP 设置里新增一个 server,command 填 `node`,args 填那个绝对路径
1041
+
1042
+ 各客户端的键名/入口略有差异,但都要的是"`command` + `args` 两个字符串";stdio 传输不需要端口,也不需要先起服务。
1043
+
1044
+ ### 七个工具
1045
+
1046
+ | 工具 | 参数 | 返回 |
1047
+ |---|---|---|
1048
+ | `overview` | — | 版本/指纹/数据目录、每个 agent 的条数与是否缺目录、项目 Top30,外加三条"读法提醒" |
1049
+ | `search_entries` | 筛选 + `text` / `status` / `slowest` / `limit`(默认 50,上限 500) | 轮次列表:`id at agent project session dur status models tin tout tcache total calls tools preview` |
1050
+ | `get_entry` | `id`(必填)、`full` | 这一轮的详情:用户输入、助手正文、工具调用入参出参、逐次 LLM 调用,附源文件路径 |
1051
+ | `list_sessions` | 筛选 + `limit`(默认 30) | 会话级汇总:几轮、首末时间、合计 token/耗时、模型、是否有时间轴 |
1052
+ | `usage_stats` | 筛选 + `by=day\|model`、`last`(默认 90 天)、`top`(默认 20 个模型) | 逐日序列(含空天)或按模型汇总 + `totals` |
1053
+ | `slowest_turns` | 筛选 + `topN`(默认 10) | 最慢 N 轮 + `matched/p50/p95/maxDur` |
1054
+ | `tool_fails` | 筛选 + `top`(默认 10 个工具行) | 工具失败画像:`rows`(按失败次数倒序,含 `fail/err/timeout/soft/total/rate/lowDenom/hasTs`)、`totals`、逐 agent 覆盖度 `agents`、`minTotal`、`notes` |
1055
+
1056
+ 公共筛选键(除 `overview`、`get_entry` 外都吃):`agent`、`project`、`range`(`today`/`7d`/`30d`)、
1057
+ `from`/`to`(`YYYY-MM-DD` 本地自然日,同给时优先于 `range`)、`refresh`。
1058
+
1059
+ `tool_fails` 的口径与页面「工具失败画像」同源(同一个 `analyzeAgg.toolFails`):**超时算失败**
1060
+ (`fail = err + timeout`)、**软失败单列一档不进失败率**(`No matches found` 这类被判定为正常的结果)、
1061
+ **源里没这一层的 agent 出 `null` 而不是 `0`**。所以 `timeout`/`soft` 为 `null` 时别读成"从没超时",
1062
+ `notes` 里会写清是哪几家、以及"多少轮还没索引到"。
1063
+
1064
+ ### 行为细节(会以为是 bug 的地方)
1065
+
1066
+ - **第一次握手慢**:进程起来要先全量扫一遍本机日志(本机实测 18 个 agent / 5405 条 ≈ 2.2s)。这期间 stdin
1067
+ 由内核缓冲,消息不会丢,所以是"问第一句要等一下",不是卡住。
1068
+ - **最多滞后 15 秒**:超过这个年龄才补扫一轮(`AGENT_LOG_MCP_TTL_MS` 可改)。刚产生的日志问不到就显式传
1069
+ `refresh: true`。
1070
+ - **响应有上限**(默认 22 万字符,`AGENT_LOG_MCP_MAX_CHARS` 可改):超了会削行数组并在 `truncated` 里说明削了谁。
1071
+ 宁可不给全,也不给一个被客户端掐掉的 JSON。`get_entry` 的 `full: true` 是唯一故意放大的口子。
1072
+ - **`text` 不是全文检索**:只在"用户输入摘要"(前 300 字)里做子串匹配,助手正文与工具输出不在索引里。
1073
+ - **token 为 0 不等于没消耗**:qoder 走 `credits`、cursor 的转录里没有 usage 字段(要注了侧栏那个
1074
+ `stop` 钩子才有逐轮 token,见 LOGFORMATS.md 的 cursor 一节)。这些键只在该条真有值时才出现。
1075
+ - **v1 不吃归档**:内存窗口有条数上限(LRU 淘汰最旧、每个 agent 至少保留最近 200 条),窗口外的历史只有页面
1076
+ 「历史归档」能查。工具面暂时没这条,`overview` 的提醒里写明了。
1077
+ - **`tool_fails` 依赖搜索分片,而 MCP 进程自己不建索引**:工具失败画像的分子来自 `~/.agent-acta/search/`
1078
+ 里的 `tf`,那份索引只有**常驻服务**会建(本进程只读)。所以刚装好、服务还没建过索引时问 `tool_fails`,
1079
+ 它会如实回 `rows: []` + `agents[].unknown = turns`,并在 `notes` 里说明"还没索引到" —— 那不是"零失败"。
1080
+ 常驻服务建好那份索引后,两边共用同一份数据目录,这里立刻就看得见了。
1081
+
1082
+ ### 不装客户端怎么验
1083
+
1084
+ ```
1085
+ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
1086
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
1087
+ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"overview","arguments":{}}}' | node mcp-server.mjs
1088
+ ```
1089
+
1090
+ stdout 一行一条 JSON-RPC 消息,stderr 是诊断。回归见 `test/mcp-test.mjs`(协议/工具面/容错/只读/新鲜度/stdout 纯净六段)。
1091
+
1092
+ ## 作为 DSH 插件接入(`plugin/`,I11)
1093
+
1094
+ 第三种载体:装进 DeepSeek Harness(DSH),在宿主左侧栏拿一个图标、中间栏占一整块面板。与 CLI 载体**并存、共用同一个数据目录**,读的还是那 22 家本机日志;`parsers/`、`archive.mjs`、`search.mjs` 与页面文件都是同一批,插件只是**装配层**(`plugin/plugin.mjs` 服务端壳 + `plugin/client.js` 客户端垫片)。
1095
+
1096
+ ### 装与生效
1097
+
1098
+ ```
1099
+ dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm 包名(发布后可用)
1100
+ dsh plugin --profile desktop add "git+https://atomgit.com/elegant001/agent-acta.git#v2.14.4"
1101
+ dsh plugin --profile desktop add "file:C:/绝对路径/yxzpro-agent-acta-2.14.4.tgz" # 或直接给包
1102
+ dsh plugin --profile desktop list # 看装没装上
1103
+ ```
1104
+
1105
+ `dsh plugin` 的参数逐字转发给 pnpm(认 registry / git / tarball / path 四态),**开发期不用发版**:`link:<绝对目录>` 也能装(只拒相对路径)。三条纪律:
1106
+
1107
+ - **改代码必须彻底退出 DSH 再启动**,只关窗口无效(宿主有模块缓存)。
1108
+ - URL 里的 `#` 在 Git Bash / PowerShell 要整条加引号;`github:` 缩写认的是 GitHub,本仓库在 AtomGit 得写全 `git+https`。
1109
+ - 图标不出现有两类原因,别再怀疑「路由没挂上」:① 宿主按 semver 判兼容性,不匹配**只进 `skippedBundles`、不报错**;② `dsh.client.inject` 必须列出声明目标 slot 的宿主包(`@deepseek-ai/dsh-client-ui-sidebar` / `-layout`,已在本包写好),少了它服务端路由、client 模块、两条 `slots.register` 全都「成功」而图标就是不出现。
1110
+
1111
+ 本项目以 **MIT** 许可发布(`LICENSE`)。npm 上的名字是 **`@yxzpro/agent-acta`** —— 裸名 `agent-acta` 发不出去:npm 的相似性规则判它与已存在的 `agentacta`(Miraj Chokshi 的同类项目,2026-02 首发,MIT)太像,`PUT` 时直接 403 并建议改用作用域名。⚠ **这只改 npm 标识与 DSH 的 bundle 名**:CLI 命令仍是 `agentacta`,仓库名、面板标题、`/api/agent-acta/*` 那些 URL 都没动。发到 npm 之后装法可以直接写包名 —— `dsh plugin --profile desktop add "@yxzpro/agent-acta"`,这也是社区目录 `awesome-dsh-plugin.com/plugins.json` 里主流形态(4392 条中 2264 条走 npm 包名、1903 条走 `github:` 简写、`git+https` 全形式 0 条先例,详见 `DSH-PLUGIN-PLAN.md` §8.9)。代码里没有任何网络出口,也不上传日志内容:它只读你自己机器上那些 agent 已经落盘的文件。
1112
+
1113
+ ### 面板给什么内容:装载那一刻定的二选一
1114
+
1115
+ 插件 `apply()` 时探一次 `127.0.0.1:14570`,**这个选择不会再变**(不重探):
1116
+
1117
+ | 探到 | 行为 |
1118
+ |---|---|
1119
+ | 有 CLI 服务在跑 | 只注册入口一条,面板显示说明页(含对方的 `version` / `build`),**不起第二套扫描器** —— 两个写者同抢 `~/.agent-acta` 的索引与归档是要出事的那种抢 |
1120
+ | 没有 | `start({ mode:'hosted', listen:false })`:扫描 / 落索引 / SSE 心跳照跑,但**不监听端口、不写 pid、不挂空闲自停**,业务路由逐条注册进宿主的 `webServer` |
1121
+
1122
+ 想从前者换到后者:`agentacta --stop` → 重启 DSH。反过来 `agentacta --ensure` 即可回浏览器用法。
1123
+
1124
+ ### 为什么面板不是「打开一个 URL」,以及静态资源为什么要中转
1125
+
1126
+ 宿主前端在 `dsh-app://app` 这个协议下,**只有 `/api/*` 被代理到宿主 `webServer`**,别的路径当宿主自己的静态资源处理。所以:
1127
+
1128
+ - iframe 的 src 只能写 `/api/…` —— 入口是 `/api/agent-acta/entry`,由它**同源递出面板 HTML**(借 core 的 `/` 路由,占位符注入仍是那一份实现)。早先试过「入口 302 到宿主 http 口」,在 iframe 里是跨源、被拦成空白,已废。
1129
+ - 页面里 `/page/…`、`/vendor/…` 那 23 处绝对引用在浏览器侧到不了(**curl 宿主 http 口是 200,会骗人**)⇒ 递送时把「紧跟引号」的这两类前缀翻译成 `/api/agent-acta/asset?p=page/x.js`,由这条中转路由换回原路径交给 core 的 `routePageFile` / `routeVendor`(防穿越、MIME、no-store 只有一份)。只认引号后面那一处,所以注释与报错文案不动,而 `page/shared.js` 里 `'/vendor/svg/' + name` 那种运行时拼接照样覆盖。
1130
+ - 静态文件**必须逐条 `exact` 注册**(宿主的 `prefix` 注册不报错、但请求根本不进 handler),清单是启动时**递归**列出来的 78 个(`page/` 18 + `vendor/` 60,含 `vendor/svg/` 54 个图标)—— 加新文件要重启 DSH 才认。
1131
+ - 重复的 `(kind, path)` 在宿主是 **throw + 先注册的赢**,所以入口与资产这两条在自己的循环里先 `continue` 再单独挂 handler。
1132
+ - **绝不注册 `exact /`**:宿主的认证兜底挂在 `/`(壳启动拿 `GET /?token=` 换 303 + set-cookie),exact 优先于兜底 ⇒ 插件抢了它,DSH 每次开机 `Desktop Host authentication failed`。`'/'` 已在 `HOSTED_DENY` 里,`test/plugin-hosted-test.mjs` 有一条专门守它。
1133
+
1134
+ ### 宿主里刻意不存在的四条路由
1135
+
1136
+ `/api/shutdown`(原义=退出进程)、`/api/client`(spawn Electron 悬浮卡片壳)、`/api/trae/capture-key`(会弹 UAC)、`/widget`(卡片页)。它们在 core 的路由表里照旧留着给 CLI 用,只是不往宿主注册 —— 否则一次误触会用掉用户的 DSH。Origin 闸在 hosted 下不再拿「端口等于 14570」当同源标识(宿主端口每次动态),改收「http(s) + 环回主机名(端口不限)」与 `dsh-app:`,且**只管写操作**,与 CLI 同口径。
1137
+
1138
+ ### 边界与回归
1139
+
1140
+ 不跟宿主明暗切换(面板是自家深色皮,且实测注入 `--dsw-alias-*` 也零变化);不唤起悬浮卡片;无网络出口。验收:`test/plugin-delivery-test.mjs`(静态:改写覆盖 / 产物可达 / 白名单挡穿越)+ `test/plugin-hosted-test.mjs`(隔离端口 14631 + `mkdtemp` 数据目录里驱动 `apply()`:108 条注册、`exact /` 不在表里、入口 200 非 302、资产与图标真取到、Origin 闸、不写 pid、卸载撤干净),两者都在 selftest 的 `cli` 组;老 CLI 那一面由 `test/cli-behavior-guard.mjs` 七类不变式守着。宿主侧逐条取证与推翻记录在 `DSH-PLUGIN-PLAN.md` §8。
1141
+
1142
+ ## 解析器自检(侧栏「解析器自检」/ `GET /api/selftest`,R32)
1143
+
1144
+ `test/` 下攒了三四十个回归脚本,改完一个解析器想知道有没有弄坏邻居,从前得挨个敲、挨个看终端。
1145
+ 这一条把它们收成**一份机器可读的结果**,页面只负责把那份结果讲实话。两件事分开:
1146
+
1147
+ | 谁 | 干什么 |
1148
+ |---|---|
1149
+ | `node test/selftest.mjs`(终端) | **唯一会跑测试的东西**。串行跑完选中的脚本,把结果写进 `~/.agent-acta/selftest.json` |
1150
+ | 服务端的 `GET /api/selftest` + 侧栏弹层 | **只读那份文件**,加上「多旧 / 出自哪版代码」三个标注(`ageMs` / `sameBuild` / `sameParserRev`)。服务端不 spawn 任何测试子进程 |
1151
+
1152
+ 为什么不做成页面上一个「跑一遍」按钮:一轮默认清单要串行起停二十来个服务、跑两分钟,这种副作用挂在弹层上
1153
+ 就没人敢刷新了;跑的节奏得由人在终端里定。也不在这一页里「拿真日志重解析一遍」—— 那是
1154
+ `agentacta --doctor` 的活(见上面「自检」一节),两边各答各的问题。
1155
+
1156
+ ### runner 的三条口径
1157
+
1158
+ - **串行,绝不并行**:脚本里的端口是硬编码的,但已做到**全 `test/` 唯一**(14589–14658 那一带 + 14677 + 14690–14703,
1159
+ 外加 `mimocode` 的派生号 `PORT+1`);另有 `%TEMP%\dsh-home` 这类固定名临时目录 —— 仍是串行跑的理由:
1160
+ 两条会话同时跑同一批脚本,哪怕端口不撞,也会互相**改写对方的隔离 HOME / 数据目录**。
1161
+ - **成败只认退出码**:这些脚本没有统一的 JSON 结果协议,末行文案还有八个都是「✓ 全部通过」,光看文字分不出是谁。
1162
+ 超时、被信号打死、脚本自己 `exit 1` 三者**判得分开**(超时不是「解析器坏了」,报错了要说清是哪一类)。
1163
+ - **默认清单必须不依赖仓库外的东西**:要 `../dist` 历史包的、要真实 `~/.dsh` 的、靶子是常驻 14570 的,
1164
+ 全部另立分组、**默认不跑**,并把「为什么不跑」随结果一起写出去(页面据此解释「为什么这里少了一类测试」)。
1165
+ 名字里带 `probe` / `bench` 的那几个连失败概念都没有(找不到目录也退 0),永久排除 —— 一个永远绿的检查比没有检查更糟。
1166
+
1167
+ ```bash
1168
+ node test/selftest.mjs # 默认组:static / parser / cli,约两分钟
1169
+ node test/selftest.mjs --only page # 前缀或标题里含这个词的都算命中
1170
+ node test/selftest.mjs --group parser # 只跑一组
1171
+ node test/selftest.mjs --all # 连默认不跑的组一起跑(要看本机脸色)
1172
+ node test/selftest.mjs --json # 结果 JSON 同时打到 stdout
1173
+ node test/selftest.mjs --timeout 60000 --out /tmp/r.json # 单脚本超时 / 换落点
1174
+ ```
1175
+
1176
+ 新加脚本必须登记进 runner 的 `SUITES`(分组 + 一句话标题;非默认组还必须写 `why`)——
1177
+ 没登记的脚本 runner 会在开头点名警告,`test/selftest-contract-test.mjs` 还会把它判成红。
1178
+
1179
+ ### 结果文件为什么在 `~/.agent-acta/` 而不是仓库的 `test/` 下
1180
+
1181
+ `npm pack` 的 `files` 不含 `test/`,而常驻服务跑的正是全局安装那一份(`agentacta --where` 可看路径)。
1182
+ 路径要是跟着代码目录走,服务读到的永远是「还没跑过自检」。跟着 `HOME` 走,源码树里的 runner 和全局包的服务
1183
+ 才读到同一个文件。代价是**它记的是「哪一版代码、什么时候跑的」**:改了代码后旧结果照样在,
1184
+ 所以页面必须拿 `sameBuild` 警告,不能让人把上一版的绿当成这一版的。
1185
+
1186
+ ### 页面上每一栏的意思
1187
+
1188
+ - 顶部黄条:结果出自另一版代码(`sameBuild:false`)。若 `sameParserRev` 仍为真,会补一句「口径倒是没变」。
1189
+ - 「这次没跑的 N 项」:按分组列出,每条带 runner 写下的 `why`。这一栏在,页面才不会把「默认清单全绿」说成「测试全过了」。
1190
+ - 不绿的行:把脚本里带 `✗` 的那几行原样摊开(最多 12 条),另有末行摘要与退出码。
1191
+ - 「断言红 / 超时 / runner 没收住」三种红分开标:处置方式完全不同 —— 前两个查代码,最后一个查环境。
1192
+
1193
+ 回归:`test/selftest-api-test.mjs`(接口的透传 / 版本章 / 年龄 / 没跑过与读不动分得开)、
1194
+ `test/selftest-contract-test.mjs`(runner 自己的四条口径,含一条「真红必须判成红」的替身演)。
1195
+
1196
+ ## 跨平台(mac / Linux / 鸿蒙 PC,R34)
1197
+
1198
+ **一句话**:解析与展示那一半本来就与平台无关,平台相关的只有「去哪些目录找日志」和少数几个平台专属动作。
1199
+ 开发机是 Windows,所以 mac / Linux 的路径分支是靠 `test/platform-test.mjs` **注入 platform/home/env**
1200
+ 钉住的(能在 Windows 上跑),不是靠读代码猜 —— 端到端的真机验证仍未做过,见本节末尾「验证边界」。
1201
+
1202
+ ### 与平台无关的那一半
1203
+
1204
+ - **全部解析器**:只认文件内容,不碰路径分隔符、不调平台专属 API(`parsers/discovery.mjs` 头部的承诺)。
1205
+ - 索引分片、归档文件、页面、HTTP API、SSE、MCP 工具面 —— 都是平台无关的读写与格式。
1206
+ - `--open` 按平台走 `start` / `open` / `xdg-open`;`--client` 的 Electron 运行时按平台装(`electron.exe` / `electron`,
1207
+ 首次执行时由 `npm install electron@44` 拉当前平台的预编译包);`--stop` 只在 win32 调 `taskkill`,POSIX 走信号。
1208
+ hook 安装(`--install-hooks`)写的是 `node "<自身路径>" --ensure`,路径统一成正斜杠,两个平台都吃得下。
1209
+
1210
+ ### 平台相关的那一半:候选根目录
1211
+
1212
+ `parsers/discovery.mjs` 的 `basesFor()` 是**唯一**按 `process.platform` 分支的地方:
1213
+
1214
+ | 平台 | 除各名字的 `~/.<name>` 外还铺 |
1215
+ |---|---|
1216
+ | `win32` | `%APPDATA%\<name>`、`%LOCALAPPDATA%\<name>` |
1217
+ | `darwin` | `~/Library/Application Support/<name>`、`~/.config/<name>` |
1218
+ | `linux` / 鸿蒙 PC / 其他 POSIX | `$XDG_CONFIG_HOME`(默认 `~/.config`)`/<name>`、`~/.local/share/<name>` |
1219
+
1220
+ 几条**只在特定平台**才铺的补丁路径:doubao 的 `%LOCALAPPDATA%\DoubaoWork\…\workspace` 仅 win32;
1221
+ opencode 的 `~/.local/share/opencode` 与 minimax 的 `~/.minimax/v2/sessions` 三平台都补(它们本来就长这样)。
1222
+
1223
+ ### 平台专属能力(不是 bug)
1224
+
1225
+ - **trae 的 SQLCipher 密钥抓取只有 Windows**(`tools/grab-trae-key.ps1` 是 PowerShell 脚本,服务端 `traeCaptureSupported()`
1226
+ 直接判 `win32`)。mac / Linux 上 trae 退到 `renderer.log` 那条路:轮次、耗时、工具照常,**token 与 AI 正文拿不到**。
1227
+ - **doubao** 的自动发现只在 Windows 补那条深路径,别的平台要手工添加。
1228
+
1229
+ ### 换机器 / 换平台:整个 `~/.agent-acta` 搬过去会怎样
1230
+
1231
+ 三份状态各有各的自愈方式,都是**载入时按本机磁盘校验**,不需要手工清:
1232
+
1233
+ | 状态 | 搬过去之后 |
1234
+ |---|---|
1235
+ | 扫描索引 `index/<kind>.json` | `loadIndex` 恢复每条 state 前 `statSync` 校验 `mtime/size`,对不上就丢弃让它重扫(老代码就有) |
1236
+ | 全文索引 `search/<kind>.json` | **R34 新增**:载入时按 `sourceAlive` 剪掉「源不在本机」的记录(记录是按绝对路径认人的,跨平台一整片对不上),剪掉的条数进日志与 `/api/search/status.pruned`;盘上那份借下一次落盘清干净 |
1237
+ | `config.json` 里的 agent 根 | `source: auto` 的会自己摘掉再重新发现;**手工加的一直留着**(0 条),`--doctor` 会点名,且 R34 起会额外提示「这是 Windows 的路径形态,而当前平台是 darwin,像是从别的平台搬过来的配置」 |
1238
+
1239
+ `widget-runtime/` 里那份 Electron 是**按平台编译**的:搬到 mac 上找不到 `dist/electron`(只有 `electron.exe`),
1240
+ `--client` 会自动重装一份当前平台的,不用手工删。`archive/` 是自描述的(`manifest.json` + 按天 JSONL,
1241
+ 详情快照就在里面),换平台照样能翻。pid 文件不作数(判活一律以端口为准)。
1242
+
1243
+ ### 文件权限(POSIX 才有的问题)
1244
+
1245
+ 这三处落的都是**明文对话**(归档是完整快照、全文索引是正文、扫描索引是元信息与路径)。R34 起在 POSIX 上
1246
+ 按私有建 —— 目录 `0700`、文件 `0600`(win32 上 `mode` 无副作用,那边的 `chmod` 只管只读位):
1247
+
1248
+ | 位置 | 权限 | 老文件 |
1249
+ |---|---|---|
1250
+ | `search/` | `0700` / `0600` | **写的时候顺带 `chmod` 收回来**(它是缓存,没有共享的用法) |
1251
+ | `index/` | `0700` / `0600` | 不动(`writeFileSync` 的 `mode` 只对新建文件生效) |
1252
+ | `archive/` | `0700` / `0600` | 不动 —— **刻意的**:归档是用户可见的数据,有人可能故意放在共享位置让同事读或在网络上备份,悄悄改老文件会砸掉那种用法 |
1253
+
1254
+ `writeFileSync` 的 `mode` 只对**新建**的文件生效,所以早先版本写下的 0644 分片/归档不会自动转过来;
1255
+ 想一次收干净:`chmod -R go-rwx ~/.agent-acta`。macOS 的家目录默认就是 0700,这三处在 mac 上本来也不暴露。
1256
+
1257
+ ### 验证边界(如实说)
1258
+
1259
+ - 已在**真机验证**的:Windows(本项目的开发与日常使用平台)。
1260
+ - 未被真机验证、但有测试钉住的:darwin / linux 的候选目录展开、别名、平台归属判定、换平台的索引剪枝
1261
+ —— `test/platform-test.mjs`(37 条断言,随默认回归清单跑,任何平台都能跑)。
1262
+ - **测试脚手架本身已不是 Windows 专有的**(R34 补的一遍):
1263
+ - 隔离用的目录链接统一走 `test/lib/iso.mjs` 的 `linkDir()` —— win32 上等价于 `mklink /J`(同样不需要
1264
+ 管理员权限),POSIX 上就是普通目录符号链接。此前 4 个脚本直接 spawn `cmd /c mklink`,mac/Linux 上必然失败;
1265
+ - `import.meta.url` 取路径统一成 `fileURLToPath()`(34 个脚本)。此前有两种写法,其中
1266
+ `…pathname.replace(/^\//, '')` **在 POSIX 上就是错的**(把开头的 `/` 当盘符前缀剥掉,得到相对路径,
1267
+ spawn 出去的服务根本起不来);另一种只是碰巧对,但走 URL 的 `pathname` 遇到带空格或中文用户名的
1268
+ 路径会被百分号编码。这一条在换机后是高发场景;
1269
+ - 「跨卷搬迁」那条用例原先写死 `D:\`,改成按 `st.dev` 找一块**真的与家目录不同设备**的卷
1270
+ (win32 试 D:/E:/F:,POSIX 试 /Volumes、/media、/mnt),找不到就打印原因跳过,而不是默默不跑;
1271
+ - `platform-test.mjs` 第 6 段是一条静态体检:**用到 `linkDir` / `fileURLToPath` 却没 import 的脚本直接点名**。
1272
+ 加它是因为这类错 `node --check` 拦不住(那是语法检查,这是运行到那一行才炸的 ReferenceError),
1273
+ 而 real / release / slow / none 四组的脚本默认清单里不跑 —— 今晚这两类各栽了一次,都是慢组/真机组才炸。
1274
+ - 仍未验过的:mac / Linux 上的端到端(首扫、各 agent 解析、页面、桌面卡片)。上面这几刀是**把已知的
1275
+ 平台专属机制拆掉**,不等于真机跑过 —— 第一次在 mac/Linux 上跑 `node test/selftest.mjs` 时,
1276
+ 要按「第一次上这台机器」的心态看待结果。
1277
+
1278
+ ## 常见问题
1279
+
1280
+ **先跑 `agentacta --doctor`**
1281
+ 它一次列出配置 / 目录 / 索引 / 端口版本 / 页面文件 / 归档的现状,带 `[fail]`/`[warn]` 的行就是该看的地方,
1282
+ 整段贴出来也省得来回问版本。下面这些症状大多能先在它那几行里看出方向。
1283
+
1284
+ **想知道哪个 agent 最占磁盘、清理该动哪里**
1285
+ 侧栏「磁盘占用」(口径见上面「磁盘占用」一节)。它把散在三处的占用合成一张表:各 agent 的日志根
1286
+ (库类连 `-wal`/`-shm`)、`index/` 分片、`archive/` 日期文件,外加 `~/.agent-acta` 的顶层构成
1287
+ (桌面卡片那份 Electron 运行时经常是最大的一项)。**这一页只统计不删除**,删哪一处自己判断;
1288
+ 如果它标了「统计到一半」,说明目录大到超了遍历预算,那行的数只当参考。
1289
+
1290
+ **页面打不开 / 一直「重连中…」**
1291
+ 先 `curl http://127.0.0.1:14570/api/ping`。无响应说明服务没起来:跑一次 `agentacta --ensure`
1292
+ (没走 npm 就 `node <本目录>/agent-acta-server.mjs --ensure`);
1293
+ 仍不行多半是 `node` 不在 PATH(`node -v` 确认)。
1294
+
1295
+ > **刷新时先看到一个会呼吸的绿方块 +「正在加载 AgentActa…」是正常的**:那是页面在等 `vendor/` 下的
1296
+ > vue / element-plus 加载完(本机一闪而过)。**超过 8 秒**它会定格成灰色方块、文字变红写着
1297
+ > 「页面没加载起来 —— …(多半是 /vendor 下的脚本没取到)」——真看到这句就按 F12 看控制台的 404,
1298
+ > 再刷一次通常就好。(这段占位只存在于 Vue 挂载之前,挂载完自动消失。)
1299
+
1300
+ > **首扫期间页面不再「重连中…」**:服务一监听端口就先放行就绪闸门(旧索引数据立刻可查),
1301
+ > 首扫拆成一小片一小片跑、每片之间让出事件循环,进度通过 SSE 的 `scanning` 事件推给页面。
1302
+ > 所以首屏立刻就有内容,右上角显示**「扫描中 N/M(当前 agent)」**,扫完自动变成「实时更新 · Ns」;
1303
+ > 列表一时为空时写的是「正在扫描…(N/M)」,不是「暂无匹配的请求记录」。
1304
+ > 本机实测(约 4700 条):从端口监听到页面连上 SSE ≈0.15~0.2s、看到首批数据 ≈0.2s,
1305
+ > 首扫整轮 ≈4.2s、热启动(有索引)≈0.9s。慢只影响「N/M 走到头」的时间,不影响能不能看、能不能点。
1306
+
1307
+ **codex 的「用户输入」那一栏一直是空的**
1308
+ 已经修了(1.8.2 起):Codex Desktop 的较新会话把用户输入写在 `event_msg` / `item_completed` 里,
1309
+ 而不是老的 `event_msg` / `user_message`,早先只认后者。**两版 `cli_version` 相同也可能是不同写法**,
1310
+ 所以是按事件形状认、不按版本号认,两种形态并存。看到这个症状先确认服务是新版;
1311
+ 仍是空的就把那份 rollout 发给开发(判断口径见 `LOGFORMATS.md` 的 codex 一节)。
1312
+ 注意这类会话里**轮次、token、耗时、AI 输出都是正常的**,只有用户输入这一格塌了,别误判成整条记录坏了。
1313
+
1314
+ **某个 agent 一直是 0 条**
1315
+ 打开侧栏「环境诊断」(或 `curl http://127.0.0.1:14570/api/diagnose`):它逐个列出每个候选 agent 的每条候选路径,
1316
+ 以及没认出来时该目录里长什么样。三种判定要分清——**已接入**是正常;**目录在但未识别**说明日志就在本机、
1317
+ 只是还没写解析器(照着它给出的路径补一个);**已由 xxx 接入**是同一份目录被另一个 agent 接了,不是故障。
1318
+
1319
+ **trae 看不到 token / AI 正文 / 逐次调用**
1320
+ 这是回退态:库没读到(没配 `traeKey`、密钥不对、或库正在被重写),服务退回了 `renderer.log`(只有事件骨架)。
1321
+ 诊断页会写明原因;按「Trae 的 SQLCipher 库」一节抓一次密钥(每台机器各自一把),配好后 trae 立刻读库、卡片变完整。
1322
+
1323
+ **侧栏写着有几十条,点进去却是空的 / 「都早于当前加载的窗口」**
1324
+ 以前会这样,现在不会了:**窗口跟着你选的东西走**——「2000 条/页」在选中某个 agent 或某个项目时,
1325
+ 指的是**它自己的最近 2000 条**,而不是「全局最近 2000 条里属于它的那些」。
1326
+ 所以停更的 agent(比如停在 8 月的 gemini)、只留旧日志的项目照样点进去就有内容。清空筛选(或点侧栏「全部」)即恢复全局窗口。
1327
+
1328
+ > 老日志「整组落在窗口外」的那种提示已经彻底删掉了——列表为空 = 真的没有,不再有第二种含义。
1329
+ > 选 agent 与选项目可以叠加(两个都选 = 同时满足)。
1330
+
1331
+ **项目下拉里出现了「(N 种写法)」**
1332
+ 不是脏数据:同一个项目的路径被不同 agent 写成了不同样子(大小写盘符、盘符与分隔符退化成 `-` 的 slug),
1333
+ 下拉按「去掉所有非字母数字后是否相同」把它们并成了一项,显示名取**条目最多**的那个写法,鼠标悬停可看全部写法。
1334
+ 点这一项 = 选中该项目的**所有写法**。卡片页脚的路径仍是各家写进去的原样,方便排查。
1335
+
1336
+ **重启后要不要全量重扫**
1337
+ 不会。带增量偏移的格式(atomcode/claude/codex/codearts/codebuddy/workbuddy)会把偏移持久化,重启只读新增部分。
1338
+ gemini 与 cursor 例外:它们按 `{mtime,size}` 签名判断要不要重解析(没变就跳过整个目录)。
1339
+ gemini 是整文件重写、没有偏移语义;cursor 是追加式但「后到的 assistant 行会挂到当前那一轮上」,
1340
+ 用增量偏移极易把最后几轮算重,而单份转录只有几十 KB,整份重读更可靠。
1341
+
1342
+ **atomcode 的卡片写着「N 次 LLM」,点开却没有调用明细**
1343
+ 正常,不是坏了:那份逐次账在 **datalog** 里,而它**默认关闭**(`~/.atomcode/config.toml` 的 `[datalog]`,
1344
+ 键是 `enabled` 和 `dir`;本机实测这个 `dir` 可以是别的盘)。没开、或者这一轮早于开启时间,
1345
+ 都只有轮级汇总——详情里会直接写一行说明是哪种。面板不去改你的配置:想开自己开,
1346
+ 开完**只对新轮生效**(旧的不会补写)。
1347
+
1348
+ **codebuddy 某一轮的 token 比它自己记的少**
1349
+ 个别轮次里 `turn-metrics.tokenDelta` 会大于该轮所有已落盘调用的用量之和(全量抽样约 62/431 轮)——
1350
+ 大概是有些调用没写进会话文件。面板报的是**可核对的逐次调用之和**,`turn-metrics` 只用来取轮级耗时。
1351
+
1352
+ **用了 CodeBuddy CN(或 VSCode / JetBrains 里的 CodeBuddy 插件),`codebuddy` 那一行却一条都没有**
1353
+ 先去看**环境诊断里 `codebuddy` 那行的「额外根」**,别去 `%APPDATA%\CodeBuddy CN` 找 —— 那个目录只有
1354
+ IDE 壳自己的 Electron 缓存(Cache / Backups / DIPS / state.vscdb),**会话正文一条都不在那儿**。
1355
+ 插件形态的正文在 `%LOCALAPPDATA%\CodeBuddyExtension\Data`,服务把它挂成 `codebuddy` 的第二个根
1356
+ (不是新开一行 `codebuddy-ext`:同一个 CodeBuddy 拆两行会把同一个项目的历史劈成两半)。
1357
+ 那行 note 会写成 `buddyjsonl · 885 条(含 genie 扩展 314 条)`,括号里的数字就是第二根贡献的;
1358
+ **没有这个括号**才说明这台机器上真没装扩展,或 `Data/<acct>/<Host>/history/` 那三层结构不齐。
1359
+ 另:`Data/Public/auth/` 是 CodeBuddy 自己的凭据文件,解析器按结构判据挡在门外,一个字节都不读。
1360
+
1361
+ **验 hook 时 `atomcode hooks test agent-acta | grep …` 卡住不返回**
1362
+ 不是 hook 卡了,是**管道卡了**:服务子进程是 `detached` 被拉起来的,它会把 hook 进程 stdout 管道的写端一直攥着,
1363
+ 于是任何等 EOF 的消费者(`| grep`、`| tail`、命令替换)都要等到服务退出为止——而服务默认是**永不退出**的(空闲自停需手动开)。
1364
+ 把 hook 输出**重定向到文件**再看即可,本机实测 0.8s 返回、`Duration 357ms / SUCCESS`:
1365
+
1366
+ ```bash
1367
+ atomcode hooks test agent-acta > /tmp/hook.txt 2>&1; grep -E "Duration|Status" /tmp/hook.txt
1368
+ ```
1369
+
1370
+ 真正要看的是「服务有没有被拉起来」,直接跑 hook 那条命令再探端口最实在:
1371
+
1372
+ ```bash
1373
+ agentacta --stop && node <本目录>/agent-acta-server.mjs --ensure && curl http://127.0.0.1:14570/api/ping
1374
+ ```
1375
+
1376
+ **端口被占**
1377
+ 服务自身检测到 `EADDRINUSE` 会静默退出(幂等),不会起两个实例。
1378
+
1379
+ **想把它停掉**
1380
+ `agentacta --stop`。服务本身没有常驻控制通道,这是唯一的停止入口:先 POST `/api/shutdown`
1381
+ (会落盘索引再退,本机实测 ~0.3s),老版本实例没有这个接口,才退化为按监听 pid 强杀(约 5s)。
1382
+ 不想手动停就开「空闲自停」,或干脆让它挂着——它只读文件,不占 CPU。
1383
+
1384
+ > ⚠️ 实测补齐:`/api/shutdown` **不保证进程立刻退干净**。曾出现 4 个残留进程继续占着端口,
1385
+ > 下一次启动时被它们挡下(日志写「端口 X 上的服务是另一版…本次未启动」)并**连到旧实例**——
1386
+ > 而旧实例跑的是旧代码,于是「新接口 404、新字段 undefined」全都冒出来,排查时很容易得出相反结论。
1387
+ > 判断端口上到底跑的是哪一版,一律以 `agentacta --where` / `GET /api/version` 的 `build` 指纹为准。
1388
+
1389
+ **改了源码,刷新页面却没变化**
1390
+ 先确认「端口上跑的到底是哪一份」——`agentacta --where` 打印的是 **`agentacta` 这条命令**会用哪一份,
1391
+ 而服务也可能是被 hook 直接拉起的**源码**那份(本机就是这样:`~/.claude/settings.json` 里的 hook 是
1392
+ `node "F:/centos/AgentActa/agent-acta/agent-acta-server.mjs" --ensure`,
1393
+ 服务脚本内容一变就把在跑的重启成源码版)。判据取进程命令行,或比对 `GET /api/version` 的 `build`
1394
+ 与两份脚本各自的 SHA1。
1395
+
1396
+ 服务只从**自己脚本所在的目录**读 `agent-acta-page.html`、`page/`(页面拆出的 `style.css` 与各 `*.js`,2026-09-21 R25 批 0 起)和 `vendor/`,所以「服务跑哪份,页面就是哪份」。
1397
+ 反过来:**改页面(含 `page/` 下的文件)只要刷新浏览器**(都是每个请求现读、`no-store`),**改服务端才需要重启**。
1398
+ ⚠️ 只改了 `page/` 下的文件时 `build` 指纹**不变**(BUILD 只哈希服务脚本),页面顶部不会立刻报版本不一致;
1399
+ 新开的标签页会拿到新文件,**开着的旧标签页等 SSE 重连时才会出现「页面文件更新过…刷新一下即可」的红条**(比对的是第二枚指纹 `pageBuild`)。
1400
+
1401
+ ⚠️ 两个容易踩的坑:
1402
+ - `agentacta` 这个 npm shim 指向**全局安装副本**(`%APPDATA%\npm\node_modules\agent-acta\`,真拷贝)。
1403
+ 端口上跑的若是源码那份,**单独执行 `agentacta --ensure` 会把服务换成旧副本**(版本号可能一样,
1404
+ `build` 指纹不同,于是「新接口 404、新字段 undefined」一起冒出来)。要重启就统一走源码:
1405
+ `node <源码目录>/agent-acta-server.mjs --ensure`;或先 `agentacta --stop`,再用同一条源码命令起来。
1406
+ - 想让全局装成软链、改完即时生效:`npm i -g <源码目录>`(等价 `npm link`,不留副本);
1407
+ 发版给别人则用 `npm pack --pack-destination ../dist` + `npm i -g ./../dist/<新包>.tgz`
1408
+ (落点固定在**仓库外**的同级 `../dist` —— `test/pack-smoke.mjs` 找的就是那里,`.gitignore` 也把
1409
+ 仓库内的 `dist/` 挡了;别在仓库里打包。装的时候那个路径**必须带 `./`**,否则 npm 把它当 git 地址去 ssh 拉)。
1410
+
1411
+ **「按会话浏览」里有的会话没有时间轴**
1412
+ 正常,不是解析漏了 —— 但**原因不是「这些 agent 的日志里没有时间戳」**(早先这么写过,是错的):
1413
+ claude / codebuddy / gemini 的日志里**就有**逐行时间戳,解析器也**已经**拿它算出了每次 LLM 调用的真实耗时,
1414
+ 只是**算完没留成一条事件流**。逐事件流水目前只有 dsh / zcode / trae 的解析器做了保留:dsh 的原始事件流里本来就有
1415
+ `step/start|end`、`assistant/message.stream[]` 的逐块时间戳、`tool/call` → `tool/result` 的起止;
1416
+ zcode 则是 `model_usage` / `tool_usage` 表里本来就记着每次调用的真实起止(含 TTFT);
1417
+ trae 走库里的开轮/收轮与 `timing`(工具真实起止),但 llm 事件源头没有流式分段,dur 恒 0。
1418
+ 其余 agent 的会话在详情顶部会有一条说明,轮次退回「LLM 调用明细 + 工具调用」两块渲染
1419
+ (**codex / kimi 连逐次调用耗时都拿不到**,时间列才显示 `—`)。判断依据是 `/api/sessions` 返回的
1420
+ `hasTimeline`(服务端算的,本质是一份**能力名单** `EVENT_AGENTS`),页面不硬编码 agent 名单 ——
1421
+ 将来别的 agent 补上 `events` 会自动生效(页面另有一道自纠正:真加载到 `events` 就不显示那条说明)。
1422
+
1423
+ **想用 chrome-devtools MCP 量页面 DOM / 点控件,但连不上浏览器**
1424
+ 先确认浏览器那边开着**远程调试服务**:Chrome 地址栏进 `chrome://inspect/#remote-debugging`,
1425
+ 把里面的 remote debugging server 打开(Chrome 144+ 才有这个开关;`chrome-devtools-mcp --autoConnect` 依赖它)。
1426
+ 只给 Chrome 加 `--remote-debugging-port` 启动参数**不是**这个开关,是两回事。
1427
+ 没开时 MCP 的 `new_page` / `evaluate_script` 会连不上,而页面本身是好的——
1428
+ `curl http://127.0.0.1:14570/` 能拿到 HTML 就说明服务侧没问题。