@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
@@ -0,0 +1,490 @@
1
+ // ---------------- openclaw 解析器(OpenClaw:<stateDir>/agents/<agentId>/sessions/<sessionId>.jsonl) ----------------
2
+ // stateDir(默认 ~/.openclaw,Windows 与 mac/Linux 同)下的摆法:
3
+ // <stateDir>/agents/<agentId>/agent/models.json ← 模型目录:每家 provider 的 models[].id → contextWindow(ctx 分母)
4
+ // <stateDir>/agents/<agentId>/sessions/sessions.json ← 会话索引:sessionKey("agent:main:main")→ sessionId / sessionFile
5
+ // <stateDir>/agents/<agentId>/sessions/<sessionId>.jsonl ← **主源**(本文件只读它)
6
+ // <stateDir>/agents/<agentId>/sessions/<sessionId>.trajectory.jsonl ← 运行轨迹(另一份视图,本版不读,见文末)
7
+ // <stateDir>/state/openclaw.sqlite ← 认证/设备/租约状态,**不是**会话源(实测:表里没有会话)
8
+ //
9
+ // 主源是**追加式会话树**(源码 packages/agent-core/src/harness/types.d.ts 的 SessionTreeEntry 核实),
10
+ // 逐行 JSON,首行是会话头、其后是条目:
11
+ // {"type":"session","version":3,"id":"<uuid>","timestamp":"<ISO>","cwd":"…"} ← 头(只有它没有 parentId)
12
+ // {"type":"message","id","parentId","timestamp","message":{…}} ← 对话消息(唯一带正文的类型)
13
+ // {"type":"session_info","name":"…"} ← 会话名(产品自己写的标题)
14
+ // {"type":"model_change","provider","modelId"} / {"type":"thinking_level_change","thinkingLevel"}
15
+ // {"type":"custom","customType","data"} ← 注入上下文但**不**回放给模型
16
+ // {"type":"custom_message","customType","content","display"} ← 注入且回放(实测 customType="openclaw:bootstrap-context:full")
17
+ // {"type":"compaction","summary","firstKeptEntryId","tokensBefore"} / {"type":"branch_summary","fromId","summary"}
18
+ // {"type":"label","targetId","label"} / {"type":"leaf","targetId"} ← 纯导航标记,不含对话内容
19
+ //
20
+ // message 是 llm-core 的 Message(源码 packages/llm-core/src/types.d.ts 核实),三种角色:
21
+ // user {role:"user", content: string | [{type:"text",text}] , timestamp}
22
+ // assistant {role:"assistant", content: [{type:"text"|"thinking"|"toolCall"}],
23
+ // api, provider, model, usage, stopReason, errorMessage?/errorCode?, timestamp}
24
+ // toolResult {role:"toolResult", toolCallId, toolName, content:[{type:"text"}], isError, timestamp}
25
+ // toolCall 块 = {type:"toolCall", id, name, arguments:{…}}(arguments 是**对象**,不是 JSON 字符串)
26
+ // usage = {input, output, cacheRead, cacheWrite, totalTokens, cost:{…}}
27
+ // stopReason = "stop" | "length" | "toolUse" | "error" | "aborted"
28
+ //
29
+ // ⚠️ 口径关键:**input 不含 cache**。不是推断,是源码里写死的 ——
30
+ // dist/openai-completions-*.js: `const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens)`
31
+ // 且 `totalTokens = input + output + cacheRead + cacheWrite`;dist/agent-runner.runtime-*.js 另有一处
32
+ // `const promptTotal = (cacheRead ?? 0) + (cacheWrite ?? 0) + (input ?? 0)`。
33
+ // ⇒ tin = usage.input 直接取(与 minimax 同口径),tcache = cacheRead + cacheWrite(两个互斥桶,**不能**取 max)。
34
+ // ⇒ 上下文占用 = 三者之和(那正是 promptTotal);**不是** input 单独一项。
35
+ //
36
+ // 轮口径(与 minimax / hermes 同款):message.role==="user" 且正文非空 → 开新轮,下一条 user 收轮;
37
+ // 轮内多轮 assistant(工具循环)的 usage 求和;time = 首条 user 消息的 message.timestamp(毫秒)。
38
+ // 失败轮:assistant 的 stopReason==="error"(带 errorMessage/errorCode,详情原样带出来),
39
+ // 或 toolResult 的 isError=true;stopReason==="aborted"(用户中断)按 claude 口径与 status 独立,只标 aborted。
40
+ // 空用户消息不开轮(同 doubao / minimax):开了就会让「扫到 N 轮」与「详情第 N 轮」错位。
41
+ import fs from 'node:fs';
42
+ import path from 'node:path';
43
+ import { files, entries, addEntry, markKind, listDirCached, isDir, isFile, toText, trunc, readCompleteLines } from './shared.mjs';
44
+
45
+ // ---------------- 根目录解析(供 discovery 的 sniffBase 与手工添加共用) ----------------
46
+ // 真实摆法(源码 dist/config-utils-*.js 的 resolveStateDir):stateDir 名字是 `.openclaw`,
47
+ // 老版本叫 `.clawdbot`(LEGACY_STATE_DIRNAMES),且 OPENCLAW_STATE_DIR 可整体改写。
48
+ // 手工添加时用户可能填任意一层:stateDir / agents / agents/<id> / agents/<id>/sessions —— 都认。
49
+ export const OPENCLAW_STATE_DIRNAMES = ['.openclaw', '.clawdbot'];
50
+
51
+ // 一个文件是不是 openclaw 转录:首行必须是会话头(带 type:"session" + version + id)。
52
+ // 只读头 2KB —— 会话头是首行且很短(实测 ~150 字节)。
53
+ export function isOpenclawTranscript(fp) {
54
+ let fd; try { fd = fs.openSync(fp, 'r'); } catch { return false; }
55
+ try {
56
+ const buf = Buffer.alloc(2048);
57
+ const n = fs.readSync(fd, buf, 0, 2048, 0);
58
+ if (n <= 0) return false;
59
+ const head = buf.toString('utf8', 0, n);
60
+ const end = head.indexOf('\n');
61
+ const first = end < 0 ? head : head.slice(0, end);
62
+ if (!first) return false;
63
+ try {
64
+ const j = JSON.parse(first);
65
+ return j && j.type === 'session' && j.version !== undefined && typeof j.id === 'string';
66
+ } catch { return false; }
67
+ } finally { try { fs.closeSync(fd); } catch {} }
68
+ }
69
+
70
+ // transcriptsOf(root):root 是**任意一层**,返回 {root: <stateDir>, files: [{fp, agentId, sessionId}]}。
71
+ // 内容判据兜底(认头行形状),所以名字猜错不会误接别的 agent 的数据。
72
+ export function openclawTranscripts(base) {
73
+ if (!isDir(base)) return null;
74
+ const cands = [];
75
+ const bn = path.basename(base).toLowerCase();
76
+ if (bn === 'agents') cands.push(path.dirname(base));
77
+ else if (bn === 'sessions') cands.push(path.dirname(path.dirname(path.dirname(base)))); // <root>/agents/<id>/sessions
78
+ else {
79
+ cands.push(base);
80
+ cands.push(path.dirname(path.dirname(base))); // 填的是 agents/<id> → 上两级
81
+ cands.push(path.dirname(base)); // 填的是 <root>/agents → 上一级
82
+ }
83
+ for (const root of [...new Set(cands)]) {
84
+ if (!root) continue;
85
+ const out = scanTranscriptFiles(root);
86
+ if (out.length) return { root, files: out };
87
+ }
88
+ return null;
89
+ }
90
+
91
+ // root = stateDir(含 agents/)。枚举 agents/<id>/sessions/*.jsonl(**排除** .trajectory.jsonl)。
92
+ // 上限只为「别在嗅探上失控」:agent 目录最多 16 个、每个会话目录最多认 200 个转录。
93
+ function scanTranscriptFiles(root) {
94
+ const out = [];
95
+ const agentsDir = path.join(root, 'agents');
96
+ if (!isDir(agentsDir)) return out;
97
+ for (const aid of (listDirCached(agentsDir) || []).slice(0, 16)) {
98
+ const sessDir = path.join(agentsDir, aid, 'sessions');
99
+ if (!isDir(sessDir)) continue;
100
+ for (const fn of (listDirCached(sessDir) || []).slice(0, 200)) {
101
+ if (!fn.endsWith('.jsonl') || fn.includes('.trajectory.')) continue;
102
+ const fp = path.join(sessDir, fn);
103
+ if (!isFile(fp) || !isOpenclawTranscript(fp)) continue;
104
+ out.push({ fp, agentId: aid, sessionId: fn.replace(/\.jsonl$/, '') });
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+
110
+ // 供 agent-acta-server 的 confAvailable 用:目录型来源,只要 root 下能枚举出转录就算可用
111
+ export function hasOpenclawSessions(root) {
112
+ return scanTranscriptFiles(root).length > 0;
113
+ }
114
+
115
+ export function openclawIdBase(fp) { return 'oc#' + fp.replace(/[\\/:]/g, '~') + '#'; }
116
+
117
+ // ---------------- 两个带 mtime 缓存的小目录表(每次扫描都调,不能每轮重读) ----------------
118
+ // sessions.json(会话索引):sessionId → sessionKey,拿它当 entry.session 比裸 UUID 可读得多。
119
+ const _keyCache = new Map();
120
+ function sessionKeyMap(sessDir) {
121
+ const fp = path.join(sessDir, 'sessions.json');
122
+ let st; try { st = fs.statSync(fp); } catch { return null; }
123
+ const c = _keyCache.get(fp);
124
+ if (c && c.m === st.mtimeMs) return c.map;
125
+ const map = new Map();
126
+ try {
127
+ const j = JSON.parse(fs.readFileSync(fp, 'utf8'));
128
+ for (const [k, v] of Object.entries(j || {})) {
129
+ const sid = v && v.sessionId;
130
+ if (sid) map.set(String(sid), k);
131
+ }
132
+ } catch {}
133
+ _keyCache.set(fp, { m: st.mtimeMs, map });
134
+ return map;
135
+ }
136
+
137
+ // agent/models.json:模型 id → contextWindow(ctx 分母)。文件里带 apiKey —— **只取 id 与 contextWindow**,
138
+ // 绝不把整份内容带进任何日志或响应。
139
+ const _winCache = new Map();
140
+ function windowMapFor(root, agentId) {
141
+ const fp = path.join(root, 'agents', agentId, 'agent', 'models.json');
142
+ let st; try { st = fs.statSync(fp); } catch { return null; }
143
+ const c = _winCache.get(fp);
144
+ if (c && c.m === st.mtimeMs) return c.map;
145
+ const map = new Map();
146
+ try {
147
+ const j = JSON.parse(fs.readFileSync(fp, 'utf8'));
148
+ for (const p of Object.values((j && j.providers) || {})) {
149
+ const arr = p && Array.isArray(p.models) ? p.models : [];
150
+ for (const m of arr) {
151
+ const id = m && m.id, w = Number(m && m.contextWindow);
152
+ if (id && Number.isFinite(w) && w > 0) map.set(String(id), w);
153
+ }
154
+ }
155
+ } catch {}
156
+ _winCache.set(fp, { m: st.mtimeMs, map });
157
+ return map;
158
+ }
159
+
160
+ // I8 复现包原始片段提取复用:与扫描侧 role==='user' 的开轮判定**同一套**(正文非空即开轮)。
161
+ export function openclawUserText(content) {
162
+ const t = contentText(content, /*dropThinking*/true);
163
+ return t.trim() ? t : '';
164
+ }
165
+
166
+ // ---------------- 文本与用量小工具 ----------------
167
+ function num(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; }
168
+
169
+ // content 数组里抽文本。dropThinking=true 用于**用户**正文:思考块不该混进用户输入列。
170
+ // 字符串型 content(user 可以就是纯字符串)原样交给 toText。
171
+ function contentText(content, dropThinking) {
172
+ if (Array.isArray(content)) {
173
+ const buf = [];
174
+ for (const p of content) {
175
+ if (!p || typeof p !== 'object') continue;
176
+ if (p.type === 'text' && typeof p.text === 'string') buf.push(p.text);
177
+ else if (p.type === 'thinking' && !dropThinking && typeof p.thinking === 'string') buf.push(p.thinking);
178
+ }
179
+ return buf.join('\n\n');
180
+ }
181
+ return toText(content);
182
+ }
183
+
184
+ // ---------------- emit:把一轮落成 entry ----------------
185
+ // 轮字段:time/dur/status/tin/tout/tcache/total/ctx/ctxUsed/rounds/calls/tools/models/preview/finished/aborted/toolNames
186
+ // · ctx = 该轮模型的 contextWindow(agents/<id>/agent/models.json;取不到 → 0,页面退化成只显示占用)
187
+ // · ctxUsed = 轮内最后一次 assistant 的 prompt 总量 = input + cacheRead + cacheWrite(见文件头 ⚠️)
188
+ // · finished = i < turns.length-1;最后一轮由页面按 lastTs 兜底(同 doubao / minimax)
189
+ function emitTurns(agent, fp, data, fromIdx) {
190
+ const idBase = openclawIdBase(fp);
191
+ for (let i = fromIdx; i < data.turns.length; i++) {
192
+ const t = data.turns[i];
193
+ const next = data.turns[i + 1];
194
+ addEntry(idBase + t.idx, {
195
+ agent,
196
+ project: data.project,
197
+ session: data.session,
198
+ name: data.name || undefined,
199
+ time: t.time,
200
+ dur: next ? Math.max(0, (next.time || t.time) - t.time) : 0,
201
+ status: t.err ? 'error' : 'ok',
202
+ tin: t.tin, tout: t.tout, tcache: t.tcache,
203
+ total: t.tin + t.tout + t.tcache,
204
+ ctx: t.win || 0,
205
+ ctxUsed: t.ctxUsed || 0,
206
+ rounds: t.calls,
207
+ calls: t.calls,
208
+ tools: t.tools,
209
+ models: t.models,
210
+ preview: t.preview,
211
+ finished: i < data.turns.length - 1,
212
+ aborted: !!t.aborted,
213
+ toolNames: t.toolNames,
214
+ }, { file: fp, turn: t.idx, kind: 'openclaw' });
215
+ }
216
+ }
217
+
218
+ // 会话头(首行)元数据补读:拿到 sessionId / cwd / 起始时间。index 恢复、文件截断重来、首读落在 off>0
219
+ // 这三种情况都拿不到首行,所以单独读一次 2KB —— 首行实测只有 ~150 字节。
220
+ function readHeader(fp, data, firstLine) {
221
+ try {
222
+ let line = firstLine;
223
+ if (line === undefined) {
224
+ const fd = fs.openSync(fp, 'r');
225
+ try {
226
+ const buf = Buffer.alloc(2048);
227
+ const n = fs.readSync(fd, buf, 0, 2048, 0);
228
+ const head = buf.toString('utf8', 0, n);
229
+ const end = head.indexOf('\n');
230
+ line = end < 0 ? head : head.slice(0, end);
231
+ } finally { fs.closeSync(fd); }
232
+ }
233
+ const h = JSON.parse(line || '');
234
+ if (!h || h.type !== 'session') return false;
235
+ if (typeof h.id === 'string' && h.id) data.sessionId = h.id;
236
+ if (typeof h.cwd === 'string') data.cwd = h.cwd;
237
+ data.startedAt = Date.parse(h.timestamp) || 0;
238
+ return true;
239
+ } catch { return false; }
240
+ }
241
+
242
+ // ---------------- 扫描主入口(OFF_KINDS:逐行 off 增量续读,语义与 claude / doubao 一致) ----------------
243
+ // root = stateDir(由 discovery 的 sniffBase 解析出的那个)。每个会话一个文件,逐行追加。
244
+ export function scanOpenclaw(agent, root, onlyFile) {
245
+ if (!isDir(root)) return;
246
+ const agentsDir = path.join(root, 'agents');
247
+ if (!isDir(agentsDir)) return;
248
+ for (const aid of listDirCached(agentsDir) || []) {
249
+ const sessDir = path.join(agentsDir, aid, 'sessions');
250
+ if (!isDir(sessDir)) continue;
251
+ const keys = sessionKeyMap(sessDir);
252
+ const wins = windowMapFor(root, aid);
253
+ for (const fn of listDirCached(sessDir) || []) {
254
+ // ⚠️ 必须排掉 .trajectory.jsonl —— 它同样以 .jsonl 结尾,但格式完全不同(运行轨迹,不是会话树)
255
+ if (!fn.endsWith('.jsonl') || fn.includes('.trajectory.')) continue;
256
+ const fp = path.join(sessDir, fn);
257
+ if (onlyFile && fp !== onlyFile) continue;
258
+ if (!isFile(fp)) continue;
259
+ let fst; try { fst = fs.statSync(fp); } catch { continue; }
260
+ const key = fp, m = fst.mtimeMs, s = fst.size;
261
+ const prev = files.get(key);
262
+ if (prev && prev.m === m && prev.s === s) {
263
+ // 重启后 entries 为空但 files 状态已从 index 恢复:直接重建条目,不重扫文件
264
+ if (prev.data?.turns?.length && !entries.has(openclawIdBase(fp) + (prev.data.turns.length - 1)))
265
+ emitTurns(agent, fp, prev.data, 0);
266
+ continue;
267
+ }
268
+ const off = prev?.off || 0;
269
+ const data = prev?.data || {
270
+ turns: [], emitted: 0, sessionId: '', session: '',
271
+ name: '', cwd: '', project: '', startedAt: 0, curModel: '', markerTypes: [],
272
+ };
273
+ // 文件被截断(off 已推到文件尾之后):旧写法在这里会永久卡住(s <= off 短路 → 再也不读)。
274
+ // 转录正常只追加,但压缩/重建会重写整份 —— 宁可整份重来,也不能装作没事。
275
+ if (s < off) { off = 0; data.turns = []; data.emitted = 0; }
276
+ if (s <= off) {
277
+ if (!data.cwd) readHeader(fp, data);
278
+ if (!data.sessionId) data.sessionId = fn.replace(/\.jsonl$/, '');
279
+ if (!data.session) data.session = (keys && keys.get(data.sessionId)) || (aid + ':' + data.sessionId);
280
+ if (!data.project) data.project = data.cwd || '(openclaw)';
281
+ files.set(key, { agent, kind: 'openclaw', m, s, off, data });
282
+ markKind('openclaw');
283
+ continue;
284
+ }
285
+ try {
286
+ const rd = readCompleteLines(fp, off, s);
287
+ if (!rd) { files.set(key, { agent, kind: 'openclaw', m, s, off, data }); markKind('openclaw'); continue; }
288
+ const newOff = rd.newOff;
289
+ // 会话头(首行)只在 off===0 那轮落进 rd.lines;其余情况(index 恢复、截断重来)单独补读首行
290
+ if (off === 0 && rd.lines.length) readHeader(fp, data, rd.lines[0]);
291
+ else if (!data.cwd) readHeader(fp, data);
292
+ if (!data.sessionId) data.sessionId = fn.replace(/\.jsonl$/, '');
293
+ if (!data.session) data.session = (keys && keys.get(data.sessionId)) || (aid + ':' + data.sessionId);
294
+ if (!data.project) data.project = data.cwd || '(openclaw)';
295
+ for (const line of rd.lines) {
296
+ if (!line.trim()) continue;
297
+ let j; try { j = JSON.parse(line); } catch { continue; }
298
+ // ---- 会话头:只在 off=0 那轮见到,元数据已在上方补过 ----
299
+ if (j.type === 'session') continue;
300
+ // ---- 会话名(产品自己写的标题):只影响 entry.name,最后一笔为准 ----
301
+ if (j.type === 'session_info') {
302
+ if (typeof j.name === 'string' && j.name.trim()) data.name = j.name.trim();
303
+ continue;
304
+ }
305
+ if (j.type === 'model_change') {
306
+ if (typeof j.modelId === 'string' && j.modelId) data.curModel = j.modelId;
307
+ continue;
308
+ }
309
+ // ---- 注入/压缩类标记:不进正文、不开轮,但**不静默丢弃** ——
310
+ // 更新 lastTs(否则末尾的注入会截断该轮 dur)、计数并记类型,详情侧留痕。
311
+ if (j.type === 'custom' || j.type === 'custom_message' || j.type === 'compaction' || j.type === 'branch_summary') {
312
+ const cur = data.turns[data.turns.length - 1];
313
+ const ct = typeof j.customType === 'string' && j.customType ? j.customType : j.type;
314
+ if (!data.markerTypes.includes(ct)) data.markerTypes.push(ct);
315
+ if (cur) {
316
+ const ts = num(j.data && j.data.timestamp) || Date.parse(j.timestamp) || 0;
317
+ if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
318
+ cur.markers = (cur.markers || 0) + 1;
319
+ if (!cur.markerTypes) cur.markerTypes = [];
320
+ if (!cur.markerTypes.includes(ct)) cur.markerTypes.push(ct);
321
+ }
322
+ continue;
323
+ }
324
+ // ---- 纯导航/状态标记(label / leaf / thinking_level_change):不含对话内容,跳过 ----
325
+ if (j.type !== 'message') continue;
326
+ const msg = j.message || {};
327
+ const role = msg.role;
328
+ if (role === 'user') {
329
+ const text = openclawUserText(msg.content);
330
+ if (!text) continue; // 空用户消息不开轮(否则扫描与详情的轮序号会错位)
331
+ data.turns.push({
332
+ idx: data.turns.length,
333
+ time: num(msg.timestamp) || Date.parse(j.timestamp) || 0,
334
+ lastTs: num(msg.timestamp) || Date.parse(j.timestamp) || 0,
335
+ tin: 0, tout: 0, tcache: 0, ctxUsed: 0, win: 0,
336
+ calls: 0, tools: 0, models: [], err: false, aborted: false, errMsg: '',
337
+ markers: 0, markerTypes: [], toolNames: {},
338
+ preview: text.replace(/\s+/g, ' ').slice(0, 300),
339
+ });
340
+ } else if (role === 'assistant' && data.turns.length) {
341
+ const cur = data.turns[data.turns.length - 1];
342
+ cur.calls++;
343
+ const ts = num(msg.timestamp) || Date.parse(j.timestamp) || 0;
344
+ if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
345
+ const u = msg.usage;
346
+ if (u) {
347
+ // ⚠️ input 不含 cache(源码写死),tin 直接取;tcache = 两个 cache 桶之和
348
+ cur.tin += num(u.input);
349
+ cur.tout += num(u.output);
350
+ cur.tcache += num(u.cacheRead) + num(u.cacheWrite);
351
+ cur.ctxUsed = num(u.input) + num(u.cacheRead) + num(u.cacheWrite); // prompt 总量
352
+ }
353
+ if (msg.model) {
354
+ if (!cur.models.includes(msg.model)) cur.models.push(msg.model);
355
+ const w = wins && wins.get(msg.model);
356
+ if (w) cur.win = w;
357
+ }
358
+ const sr = msg.stopReason;
359
+ if (sr === 'error') {
360
+ cur.err = true;
361
+ if (!cur.errMsg) cur.errMsg = msg.errorMessage || (msg.errorCode ? 'errorCode=' + msg.errorCode : '');
362
+ } else if (sr === 'aborted') {
363
+ cur.aborted = true; // 用户中断:与 status 独立(同 claude 口径)
364
+ }
365
+ const content = Array.isArray(msg.content) ? msg.content : [];
366
+ for (const p of content) {
367
+ if (!p || typeof p !== 'object' || p.type !== 'toolCall') continue;
368
+ const nm = typeof p.name === 'string' && p.name ? p.name : '?';
369
+ cur.tools++;
370
+ cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
371
+ }
372
+ } else if (role === 'toolResult' && data.turns.length) {
373
+ // 扫描侧只吃两个信号:轮时间与失败标志(工具计数已在 assistant 行完成,避免重复计数)
374
+ const cur = data.turns[data.turns.length - 1];
375
+ const ts = num(msg.timestamp) || Date.parse(j.timestamp) || 0;
376
+ if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
377
+ if (msg.isError) cur.err = true;
378
+ }
379
+ }
380
+ files.set(key, { agent, kind: 'openclaw', m, s, off: newOff, data });
381
+ markKind('openclaw');
382
+ emitTurns(agent, fp, data, Math.max(0, data.emitted - 1));
383
+ data.emitted = data.turns.length;
384
+ } catch {}
385
+ }
386
+ }
387
+ }
388
+
389
+ // ---------------- 详情:重读转录按 src.turn 过滤 ----------------
390
+ // 开轮判定与扫描侧同一份(role=user 且正文非空),天然对齐,不会出现「扫到这一轮、详情找不到」。
391
+ export function openclawEntryContent(src, full) {
392
+ const st = files.get(src.file);
393
+ const version = st ? st.m + ':' + (st.off || 0) : null;
394
+ let lines;
395
+ try { lines = fs.readFileSync(src.file, 'utf8').split('\n'); } catch { return null; }
396
+ let turnIdx = -1;
397
+ const out = {
398
+ user: '', assistant: '', tools: [], calls: [], v: version,
399
+ callsNote: 'openclaw 转录(sessions/<id>.jsonl)是追加式会话树:token/模型在 assistant 消息行内 ' +
400
+ '(usage.input **不含** cache —— totalTokens = input+output+cacheRead+cacheWrite,故 tin = input 直接取、' +
401
+ 'tcache = cacheRead+cacheWrite),轮耗时 = 下一轮 time − 本轮 time;' +
402
+ 'context 窗口容量取同 agent 的 agent/models.json 里该模型的 contextWindow(取不到则只显示占用)。' +
403
+ '转录不落逐次调用耗时,故 calls[].dur 恒为 0。',
404
+ };
405
+ const texts = [];
406
+ const callArgs = {};
407
+ let errMsg = '';
408
+ let stopped = '';
409
+ let markerCount = 0;
410
+ const markerTypes = [];
411
+ for (const line of lines) {
412
+ if (!line.trim()) continue;
413
+ let j; try { j = JSON.parse(line); } catch { continue; }
414
+ if (j.type === 'session') continue;
415
+ if (j.type === 'custom' || j.type === 'custom_message' || j.type === 'compaction' || j.type === 'branch_summary') {
416
+ if (turnIdx === src.turn) {
417
+ markerCount++;
418
+ const ct = typeof j.customType === 'string' && j.customType ? j.customType : j.type;
419
+ if (!markerTypes.includes(ct)) markerTypes.push(ct);
420
+ }
421
+ continue;
422
+ }
423
+ if (j.type !== 'message') continue; // session_info / model_change / label / leaf / thinking_level_change 不进正文
424
+ const msg = j.message || {};
425
+ if (msg.role === 'user') {
426
+ const text = openclawUserText(msg.content);
427
+ if (!text) continue; // 与扫描侧同判据(同一个函数),否则轮序号错开
428
+ turnIdx++;
429
+ if (turnIdx === src.turn) out.user = text;
430
+ continue;
431
+ }
432
+ if (turnIdx !== src.turn) continue;
433
+ if (msg.role === 'assistant') {
434
+ // 正文 = text + thinking 一起拼(页面 md 渲染,与 minimax 同款)
435
+ const text = contentText(msg.content, /*dropThinking*/false);
436
+ if (text) texts.push(text);
437
+ const u = msg.usage;
438
+ const call = {
439
+ model: msg.model || '',
440
+ tin: u ? num(u.input) : 0,
441
+ tout: u ? num(u.output) : 0,
442
+ tcache: u ? num(u.cacheRead) + num(u.cacheWrite) : 0,
443
+ dur: 0,
444
+ };
445
+ if (text) { const ft = {}; call.text = trunc(text, full, ft); call.textTrunc = !!ft.t; }
446
+ const tcs = Array.isArray(msg.content) ? msg.content.filter(p => p && p.type === 'toolCall') : [];
447
+ if (tcs.length) call.tools = tcs.map(t => (typeof t.name === 'string' && t.name) || '?');
448
+ for (const tc of tcs) {
449
+ const nm = (typeof tc.name === 'string' && tc.name) || '?';
450
+ const fi = {};
451
+ const args = typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments ?? {});
452
+ out.tools.push({
453
+ name: nm, tid: tc.id || null, error: null,
454
+ input: trunc(args, full, fi), inputTrunc: !!fi.t, output: '', outputTrunc: false,
455
+ });
456
+ if (tc.id) callArgs[tc.id] = true;
457
+ }
458
+ if (msg.stopReason === 'error' && !errMsg) errMsg = msg.errorMessage || (msg.errorCode ? 'errorCode=' + msg.errorCode : '');
459
+ if (msg.stopReason) stopped = msg.stopReason;
460
+ out.calls.push(call);
461
+ } else if (msg.role === 'toolResult') {
462
+ // toolResult 自带 toolCallId / toolName,按 id 回填上一条 assistant 的 toolCall(同 doubao)
463
+ const tcid = msg.toolCallId || '';
464
+ const t = out.tools.find(x => x.tid === tcid);
465
+ if (!t) continue;
466
+ const fo = {};
467
+ t.output = trunc(toText(msg.content), full, fo);
468
+ t.outputTrunc = !!fo.t;
469
+ if (msg.isError) t.error = 'error';
470
+ }
471
+ if (turnIdx > src.turn) break;
472
+ }
473
+ out.assistant = texts.join('\n\n---\n\n');
474
+ if (errMsg) out.callsNote = '这一轮没有成功拿到模型回复:' + errMsg;
475
+ else if (stopped === 'aborted') out.callsNote += ' 本轮被中断(stopReason=aborted)。';
476
+ else if (stopped === 'length') out.callsNote += ' 本轮因达到输出上限被截断(stopReason=length)。';
477
+ if (markerCount) {
478
+ out.callsNote += ' 本轮含 ' + markerCount + ' 条注入/压缩标记(' + markerTypes.join(', ') +
479
+ ':context.compiled 快照、bootstrap 上下文、compaction 等,由运行时自动插入,不进正文)。';
480
+ }
481
+ return out;
482
+ }
483
+
484
+ // sniffBase 的分支与 candidateBases 见 discovery.mjs;这里只负责解析。
485
+ //
486
+ // 本版**不读** <sessionId>.trajectory.jsonl:它是同一批事件的另一份视图(session.started / trace.metadata /
487
+ // context.compiled / prompt.submitted / model.completed / trace.artifacts / session.ended / model.fallback_step),
488
+ // 其中的用量是 `promptCache.lastCallUsage`(**整轮最后一次调用**的聚合),而主源的 usage 在每条 assistant
489
+ // 消息上、逐次都在 —— 主源粒度更细,且 messagesSnapshot 会把整轮消息再复制一遍(按它计数会翻倍)。
490
+ // 轨迹里有、主源没有的只有 harness 元信息(版本 / 工具数 / invocation / finalStatus),解析器不消费,故不读。