@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,348 @@
1
+ // ---------------- tracecode / tracework 解析器 ----------------
2
+ // 数据源:TraeCode / TraeWork 桌面版的明文 renderer.log
3
+ // 路径形态:<APPDATA>\<Trae CN 或 Trae Work>\logs\<启动时间戳>\window1\renderer.log
4
+ //
5
+ // 定位(R23 起):**回退源**。TraeCode/TraeWork 的完整会话数据在
6
+ // ModularData/ai-agent/database.db(SQLCipher 加密库)——该库现在由 parsers/traedb.mjs 读取
7
+ // (密钥配置项 traeKey,见 REFERENCE.md「Trae 的 SQLCipher 库」);主文件 scanTraeAgent 在库读不出来时
8
+ // (没配密钥 / 密钥不对 / 库正在被写)自动回退到本文件。所以这里的原则是**保留骨架能力**:
9
+ // 事件、工具名、状态都有,token 与 AI 正文没有(见下面「口径限制」),不必也不该去猜。
10
+ // 这台机器上库一直读得出来时,这份解析器就不会被调用(两边同时上线=每轮两张重复卡片,
11
+ // 由主文件的源切换逻辑二选一)。
12
+ //
13
+ // snapshot/v2 是工作区文件的 git 快照(before/after 标签),sandbox json 是权限配置,
14
+ // 都不是会话内容。明文出口里含完整 ai-chat/v2 协议事件的就是 renderer.log。
15
+ //
16
+ // 事件流(每行一条 [ai-chat/v2] [Handler] 文字 + JSON):
17
+ // [MetadataHandler] received metadata {sessionId, chat_start_time, model_info,
18
+ // user_message_context.parsed_query, references, agent_name, agent_id, trace_id, ...}
19
+ // —— 一轮用户提问一次,含模型与用户原话
20
+ // [PlanItemHandler] New plan item created {sessionId, planItemId, toolCallName}
21
+ // —— 一次工具调用,toolCallName 可能为空串(思考/计划项,不算工具)
22
+ // [DoneHandler] Stream done event received {sessionId, status, agentMessageId}
23
+ // —— 一次回答结束,status: completed | canceled
24
+ // [SessionTitleHandler] Session title updated {sessionId} —— 会话标题
25
+ //
26
+ // 聚合粒度:**按轮聚合**(与 claude / codex / kimi 同款),不是按 session 聚合。
27
+ // 每个 MetadataHandler 事件 = 开一轮(一条 entry);
28
+ // 该轮内(到下一个同 sid 的 MetadataHandler 之前)的 PlanItemHandler 归这一轮;
29
+ // 该轮的 DoneHandler 事件 = 收轮(status 决定 finished/aborted)。
30
+ // entry id = `tc#<filepath>#<sessionId>#<seq>`,seq = 该 session 内第几个 metadata(从 0 开始)。
31
+ //
32
+ // 口径限制(与 LOGFORMATS.md 同步):
33
+ // · tin/tout/tcache 恒为 0:renderer.log 不含 token 用量(fee_usage 字段恒为 null,计费在后端)
34
+ // · AI 输出正文不在 renderer.log(流式 SSE 走内存,不落地)—— 详情页只能给用户提示与工具列表
35
+ // · calls = 1(每轮一次 LLM 调用,metadata 事件本身就是这次 LLM 调用的开始;
36
+ // renderer.log 没有 LLM 调用级别的独立 token_count 事件,无法精确计数多次调用)
37
+ import fs from 'node:fs';
38
+ import path from 'node:path';
39
+ import { files, addEntry, markKind, listDirCached, isDir, readCompleteLines } from './shared.mjs';
40
+
41
+ // 行首时间戳:2026-09-17T15:47:57.011+08:00
42
+ // 模块 tag 不写死单个名字:老版本是 [ai-chat/v2],新版 Trae(如 TRAE SOLO CN)改成了 [trae-chat-core],
43
+ // 写死就一整行匹配不上、整份 renderer.log 白扫。放宽成字符类后两类都认,HANDLERS 的 prefix 仍负责挑出真正的事件行。
44
+ const TRAE_TS = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2})\s+\[info\]\s+\[[\w\-\/\.\s]+\]\s+\[(\w+)\]\s+(.*)$/;
45
+
46
+ // 各类 Handler 的事件关键字(出现在 message 文字开头)
47
+ // 注意:TRAE_TS 正则已经吃掉 `[Handler]`,所以 m[3] 直接从 message 开始,
48
+ // 这里的 prefix **不带** `[Handler] ` 前缀。
49
+ const HANDLERS = {
50
+ META: 'received metadata ',
51
+ PLAN: 'New plan item created ',
52
+ DONE: 'Stream done event received ',
53
+ TITLE: 'Session title updated ',
54
+ };
55
+
56
+ // 行尾 JSON 对象的解析:从字符串里找第一个能 JSON.parse 的子串。
57
+ function extractJson(text) {
58
+ const start = text.indexOf('{');
59
+ if (start < 0) return null;
60
+ const end = text.lastIndexOf('}');
61
+ if (end > start) {
62
+ try { return JSON.parse(text.slice(start, end + 1)); } catch {}
63
+ }
64
+ // 兜底:从 start 起,每遇到一个 } 都试一次(应对「JSON 后还有文字」的边角)
65
+ let depth = 0, inStr = false, esc = false, candidate = null;
66
+ for (let i = start; i < text.length; i++) {
67
+ const ch = text[i];
68
+ if (inStr) {
69
+ if (esc) { esc = false; }
70
+ else if (ch === '\\') { esc = true; }
71
+ else if (ch === '"') { inStr = false; }
72
+ } else {
73
+ if (ch === '"') inStr = true;
74
+ else if (ch === '{') depth++;
75
+ else if (ch === '}') {
76
+ depth--;
77
+ if (depth === 0) { candidate = text.slice(start, i + 1); break; }
78
+ }
79
+ }
80
+ }
81
+ if (candidate) { try { return JSON.parse(candidate); } catch {} }
82
+ return null;
83
+ }
84
+
85
+ // 解析单行:返回 { handler, json, ts } 或 null
86
+ function parseLine(line) {
87
+ // Windows CRLF:split('\n') 后每行末尾留一个 \r,会让 `(.*)$` 匹配失败($ 在 \r 前)
88
+ if (line.endsWith('\r')) line = line.slice(0, -1);
89
+ const m = line.match(TRAE_TS);
90
+ if (!m) return null;
91
+ const ts = Date.parse(m[1]);
92
+ const rest = m[3];
93
+ for (const [key, prefix] of Object.entries(HANDLERS)) {
94
+ if (rest.startsWith(prefix)) {
95
+ const json = extractJson(rest.slice(prefix.length));
96
+ return { handler: key, json, ts, rest };
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // 把 parsed_query 数组(用户原话)规整成单行预览
103
+ function previewOf(parsedQuery) {
104
+ if (!Array.isArray(parsedQuery)) return '';
105
+ for (const q of parsedQuery) {
106
+ if (typeof q === 'string' && q.trim()) return q.replace(/\s+/g, ' ').slice(0, 300);
107
+ if (q && typeof q === 'object' && typeof q.data === 'object' && typeof q.data.content === 'string') {
108
+ return q.data.content.replace(/\s+/g, ' ').slice(0, 300);
109
+ }
110
+ if (q && typeof q === 'object' && typeof q.content === 'string') {
111
+ return q.content.replace(/\s+/g, ' ').slice(0, 300);
112
+ }
113
+ }
114
+ return '';
115
+ }
116
+
117
+ // 把 parsed_query 数组里所有字符串块拼成完整用户输入(详情页用)
118
+ function userTextOf(parsedQuery) {
119
+ if (!Array.isArray(parsedQuery)) return '';
120
+ const strs = parsedQuery.filter(q => typeof q === 'string');
121
+ if (strs.length) return strs.join('\n');
122
+ // 兜底:query 字段是 JSON 字符串形式的 content blocks
123
+ return '';
124
+ }
125
+
126
+ // 引用文件提取:references[].uri 取第一个作为 project 锚点
127
+ function projectOf(references) {
128
+ if (!Array.isArray(references)) return '(Trae)';
129
+ for (const r of references) {
130
+ if (r && typeof r === 'object' && typeof r.uri === 'string' && r.uri.trim()) {
131
+ try { return path.dirname(r.uri) || r.uri; } catch { return r.uri; }
132
+ }
133
+ }
134
+ return '(Trae)';
135
+ }
136
+
137
+ // entry id 基:跨多个 renderer.log 文件不会冲突(带文件路径)
138
+ function traeIdBase(fp) { return 'tc#' + fp.replace(/[\\/:]/g, '~') + '#'; }
139
+
140
+ // 单文件增量扫描:按轮聚合
141
+ function scanTraeRendererFile(agent, kind, fp, onlyFile) {
142
+ let fst; try { fst = fs.statSync(fp); } catch { return false; }
143
+ const key = fp, m = fst.mtimeMs, s = fst.size;
144
+ const prev = files.get(key);
145
+ if (prev && prev.m === m && prev.s === s) {
146
+ // 签名没变:重建一遍条目(trace 同款语义:files 不进 OFF_KINDS 时 entries 可能被 evict)
147
+ if (prev.data?.turns?.length) emitTraeTurns(agent, kind, fp, prev.data, 0);
148
+ return true;
149
+ }
150
+ const off = prev?.off || 0;
151
+ const data = prev?.data || { turns: [], bySidSeq: Object.create(null), emitted: 0 };
152
+ // turns: [turnEntry];bySidSeq: sessionId -> 下一个 seq(用于给新 metadata 分配序号)
153
+ if (s <= off) { files.set(key, { agent, kind, m, s, off, data }); markKind(kind); return true; }
154
+ try {
155
+ const rd = readCompleteLines(fp, off, s);
156
+ if (!rd) { files.set(key, { agent, kind, m, s, off, data }); markKind(kind); return true; }
157
+ const newOff = rd.newOff;
158
+ for (const line of rd.lines) {
159
+ const ev = parseLine(line);
160
+ if (!ev || !ev.json) continue;
161
+ const sid = ev.json.sessionId || ev.json.session_id;
162
+ if (!sid) continue;
163
+ if (ev.handler === 'META') {
164
+ // 开一轮
165
+ const seq = data.bySidSeq[sid] || 0;
166
+ data.bySidSeq[sid] = seq + 1;
167
+ const uc = ev.json.user_message_context;
168
+ const cst = typeof ev.json.chat_start_time === 'number' ? ev.json.chat_start_time : ev.ts;
169
+ const mi = uc?.model_info;
170
+ const modelName = mi ? (mi.display_model_name || mi.model_name || mi.config_name || '') : '';
171
+ const turn = {
172
+ sid, seq,
173
+ firstTime: cst || ev.ts || 0,
174
+ lastTs: ev.ts || cst || 0,
175
+ models: modelName ? [modelName] : [],
176
+ preview: uc ? previewOf(uc.parsed_query) : '',
177
+ user: uc ? userTextOf(uc.parsed_query) : '',
178
+ references: Array.isArray(ev.json.references) ? ev.json.references : [],
179
+ agentName: ev.json.agent_name || '',
180
+ tools: 0, toolNames: {},
181
+ finished: false, aborted: false, err: false, status: '',
182
+ doneTs: 0,
183
+ };
184
+ data.turns.push(turn);
185
+ } else if (ev.handler === 'PLAN') {
186
+ // 归到该 session 内最新一个未 finished 的 turn(即最近一个 metadata 之后的轮)
187
+ const tn = ev.json.toolCallName;
188
+ if (!tn) continue; // 思考/计划项,不算工具
189
+ const turn = lastOpenTurn(data, sid);
190
+ if (!turn) continue; // 没有 metadata 就有 plan,异常,丢
191
+ turn.tools++;
192
+ turn.toolNames[tn] = (turn.toolNames[tn] || 0) + 1;
193
+ if (ev.ts && ev.ts > turn.lastTs) turn.lastTs = ev.ts;
194
+ } else if (ev.handler === 'DONE') {
195
+ // 收轮:标该 session 内最新一个未 finished 的 turn
196
+ const turn = lastOpenTurn(data, sid);
197
+ if (!turn) continue;
198
+ turn.finished = true;
199
+ turn.doneTs = ev.ts || 0;
200
+ const st = ev.json.status;
201
+ if (st === 'canceled') turn.aborted = true;
202
+ else if (st && st !== 'completed') turn.err = true;
203
+ if (st) turn.status = st;
204
+ if (ev.ts && ev.ts > turn.lastTs) turn.lastTs = ev.ts;
205
+ }
206
+ // TITLE 事件体只有 sessionId,没带 title 正文,忽略
207
+ }
208
+ files.set(key, { agent, kind, m, s, off: newOff, data });
209
+ markKind(kind);
210
+ // 增量 emit:已 emit 过的 turn 也可能因新事件变化,重发最后一个(与 codearts 同款)
211
+ emitTraeTurns(agent, kind, fp, data, Math.max(0, data.emitted - 1));
212
+ data.emitted = data.turns.length;
213
+ } catch {}
214
+ return true;
215
+ }
216
+
217
+ // 找该 session 内最新一个未 finished 的 turn(用于把 plan/done 事件归到正确的轮)
218
+ function lastOpenTurn(data, sid) {
219
+ for (let i = data.turns.length - 1; i >= 0; i--) {
220
+ const t = data.turns[i];
221
+ if (t.sid === sid && !t.finished) return t;
222
+ }
223
+ return null;
224
+ }
225
+
226
+ function emitTraeTurns(agent, kind, fp, data, fromIdx) {
227
+ const idBase = traeIdBase(fp);
228
+ for (let i = fromIdx; i < data.turns.length; i++) {
229
+ const t = data.turns[i];
230
+ const project = projectOf(t.references);
231
+ const dur = Math.max(0, (t.doneTs || t.lastTs) - t.firstTime);
232
+ // entry id = `tc#<filepath>#<sessionId>#<seq>`,src.turn = `<sessionId>#<seq>`
233
+ const turnKey = t.sid + '#' + t.seq;
234
+ addEntry(idBase + turnKey, {
235
+ agent, project, session: t.sid,
236
+ time: t.firstTime, dur,
237
+ status: t.err ? 'error' : (t.aborted ? 'canceled' : 'ok'),
238
+ tin: 0, tout: 0, tcache: 0, total: 0, // renderer.log 不含 token 用量
239
+ ctx: 0, rounds: null, tools: t.tools, calls: 1, // 每轮一次 LLM 调用(metadata 即开始)
240
+ models: t.models, preview: t.preview, name: t.agentName || '', nollm: false,
241
+ finished: t.finished, aborted: t.aborted,
242
+ toolNames: { ...t.toolNames },
243
+ }, { file: fp, turn: turnKey, kind });
244
+ }
245
+ }
246
+
247
+ // logs 根目录扫描:枚举 <timestamp>/window1/renderer.log
248
+ function scanTraeLogs(agent, kind, root, onlyFile) {
249
+ const tses = listDirCached(root);
250
+ if (!tses) return;
251
+ for (const ts of tses) {
252
+ const tsDir = path.join(root, ts);
253
+ if (!isDir(tsDir)) continue;
254
+ const w1 = path.join(tsDir, 'window1');
255
+ if (!isDir(w1)) continue;
256
+ const names = listDirCached(w1);
257
+ if (!names) continue;
258
+ for (const n of names) {
259
+ if (n !== 'renderer.log') continue;
260
+ const fp = path.join(w1, n);
261
+ if (onlyFile && fp !== onlyFile) continue;
262
+ scanTraeRendererFile(agent, kind, fp, onlyFile);
263
+ }
264
+ }
265
+ }
266
+
267
+ // 两个独立 kind 的入口:路径由 server 通过 conf.sessions 注入
268
+ export function scanTraecode(agent, root, onlyFile) { scanTraeLogs(agent, 'tracecode', root, onlyFile); }
269
+ export function scanTraework(agent, root, onlyFile) { scanTraeLogs(agent, 'tracework', root, onlyFile); }
270
+
271
+ // 详情页内容:从 renderer.log 重新提取指定 turn(sid#seq)的全部事件
272
+ export function traeEntryContent(src, full) {
273
+ if (!src || !src.file) return null;
274
+ const fstate = files.get(src.file);
275
+ const version = fstate ? fstate.m + ':' + (fstate.off || 0) : null;
276
+ let lines;
277
+ try { lines = fs.readFileSync(src.file, 'utf8').split('\n'); } catch { return null; }
278
+ // src.turn = `<sessionId>#<seq>`
279
+ const parts = String(src.turn).split('#');
280
+ const sid = parts[0];
281
+ const seq = parseInt(parts[1], 10);
282
+ const out = {
283
+ user: '', assistant: '', tools: [], calls: [],
284
+ v: version,
285
+ note: 'TraeCode/TraeWork 的 renderer.log 只记录事件元数据(用户提问、工具调用名、结束状态),'
286
+ + '不含 AI 输出正文与 token 用量(流式 SSE 走内存不落地,token 计费在后端)'
287
+ + '——只能给出这一份事件流水。',
288
+ };
289
+ let metaCount = 0; // 该 session 内已经数到第几个 metadata
290
+ let inTargetTurn = false; // 是否已进入目标 turn(目标 metadata 已出现、下一个同 sid metadata 还没来)
291
+ for (const line of lines) {
292
+ const ev = parseLine(line);
293
+ if (!ev || !ev.json) continue;
294
+ const curSid = ev.json.sessionId || ev.json.session_id;
295
+ if (curSid !== sid) continue;
296
+ if (ev.handler === 'META') {
297
+ // 进入新的一轮:如果目标 turn 还在进行中,遇到下一个 metadata 表示该轮结束
298
+ if (inTargetTurn) break;
299
+ if (metaCount === seq) {
300
+ // 进入目标 turn
301
+ inTargetTurn = true;
302
+ const uc = ev.json.user_message_context;
303
+ if (uc) {
304
+ out.user = userTextOf(uc.parsed_query);
305
+ if (!out.user) {
306
+ const q = uc.query;
307
+ if (typeof q === 'string') {
308
+ try {
309
+ const qb = JSON.parse(q);
310
+ if (Array.isArray(qb)) {
311
+ const texts = qb
312
+ .filter(b => b && b.type === 'text' && b.data && typeof b.data.content === 'string')
313
+ .map(b => b.data.content);
314
+ if (texts.length) out.user = texts.join('\n');
315
+ }
316
+ } catch {}
317
+ }
318
+ }
319
+ const mi = uc.model_info;
320
+ if (mi) {
321
+ const mn = mi.display_model_name || mi.model_name || mi.config_name;
322
+ if (mn) out.calls.push({
323
+ model: mn, tin: 0, tout: 0, tcache: 0, dur: 0,
324
+ note: 'renderer.log 不含逐次 token 用量',
325
+ });
326
+ }
327
+ }
328
+ }
329
+ metaCount++;
330
+ } else if (ev.handler === 'PLAN' && inTargetTurn) {
331
+ const tn = ev.json.toolCallName;
332
+ if (!tn) continue;
333
+ const pid = ev.json.planItemId;
334
+ out.tools.push({
335
+ name: tn, tid: pid || null, ts: ev.ts,
336
+ input: '(调用入参不落地)', inputTrunc: false,
337
+ output: '(工具返回不落地)', outputTrunc: false, error: null,
338
+ });
339
+ } else if (ev.handler === 'DONE' && inTargetTurn) {
340
+ const st = ev.json.status;
341
+ if (st === 'canceled') out.aborted = true;
342
+ else if (st && st !== 'completed') out.err = true;
343
+ if (out.tools.length) out.tools[out.tools.length - 1].status = st;
344
+ break; // 该轮结束,跳出循环
345
+ }
346
+ }
347
+ return out;
348
+ }