@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,503 @@
1
+ // ---------------- minimax 解析器(Mavis 本地运行时:~/.minimax/v2/sessions/.../messages.jsonl) ----------------
2
+ // 落盘:
3
+ // ~/.minimax/v2/sessions/YYYY/MM/DD/<HH-MM-SS-mmm-session_<base64-id>>/
4
+ // ├─ messages.jsonl ← 主源(逐行追加,OFF 增量语义与 claude/doubao 同)
5
+ // ├─ manifest.json ← sessionId / createdAtMs / layout(=v2-final-dated-session)
6
+ // ├─ history-catalog.json(activeRevision / artifacts[],目前只起版本校验作用)
7
+ // ├─ user-message-locators.jsonl(每条 user 消息的字节定位,目前不进解析)
8
+ // └─ llm-call.json ← **聚合**的系统提示/工具 schema 一次性快照(不是逐次调用日志),
9
+ // 但带 maxTokens —— context 窗口容量(ctx 分母)就从这里取
10
+ //
11
+ // 每行 messages.jsonl(一行 = 一条消息,Anthropic 风格):
12
+ // {"message_id":"msg-...","turn_id":"<uuid>","message":{...}}
13
+ // 其中 message 才是消息体,**所有字段都挂在 message 里**(这是真库实测、与想象不同之处):
14
+ // message.role = "user" | "assistant" | "toolResult" | "custom"
15
+ // message.content = [...](user/assistant 是 [{type:"text"|"thinking"|"toolCall", ...}])
16
+ // message.timestamp = 毫秒(user/assistant/toolResult 都有;custom 也有)
17
+ // message.api / message.provider / message.model / message.responseId(仅 assistant 带)
18
+ // message.usage = {input,output,cacheRead,cacheWrite,totalTokens,cost}(**仅 assistant 带**;
19
+ // ⚠️ input **不含** cache:真库 95/95 行恒有 totalTokens = input+output+cacheRead+cacheWrite,
20
+ // 且 93/95 行 input < cacheRead(如 input=652 / cacheRead=107410),hermes 那套
21
+ // 「input 含 cache、tin = input − cacheRead」的假设在 minimax 上**不成立**)
22
+ // message.stopReason = "toolUse"|"endTurn"|"stop"|...(**仅 assistant 带**)
23
+ // message.toolCallId / message.toolName / message.isError / message.details(**仅 toolResult 带**;
24
+ // isError 真库实测存在(bash 失败行),会进轮状态,见下)
25
+ // message.customType / message.display(**仅 custom 带**):系统注入消息
26
+ // (实测 customType="todo_cadence_reminder"、display=false、content 是字符串),
27
+ // 不进正文、不开轮,但**不能静默丢弃**(会更新 lastTs、在详情 callsNote 留痕)
28
+ // ⚠️ 顶层 message_id / turn_id / message 之外**没有** usage/model/stopReason —— 解析器一律从
29
+ // message 取,不能从顶层 j 取(早期版本误读顶层 → 全部 token/model 落空,见 scanMavis/mavisEntryContent)。
30
+ //
31
+ // user 行的 content 是 [{type:"text",text:"..."}],首行常带 <system-reminder> 块:用户正文
32
+ // 用 stripReminder 截到提醒块之后;提醒块没出现 → 整段当用户正文。
33
+ // assistant 行的 content 同时可含 {type:"thinking",thinking:...} / {type:"toolCall",id,name,arguments}:
34
+ // thinking 累到轮内 text(页面 md 渲染与正文同列),toolCall 累到工具表,toolCall.arguments 为对象。
35
+ // toolResult 行(message.role==="toolResult")的 message.content 是 [{type:"text",text:"..."}],
36
+ // 按 message.toolCallId 回填上一条 assistant 的 toolCall 的 output(与 OpenAI 系同语义)。
37
+ //
38
+ // 轮口径:message.role==="user" 开新轮(同一 turn_id 内的 user + 多轮 assistant + toolResult 都归本轮),
39
+ // 下一条 user 收轮。每轮的 time / dur / status / 用量按「轮窗口内的 assistant 行 usage 求和」摊派(与 hermes 同款)。
40
+ // ⚠️ 同 turn_id 内多次调用的 usage 一律按 assistant 行**聚合**:input **不含** cache
41
+ // (真库恒等式 totalTokens = input + output + cacheRead + cacheWrite,95/95 行成立),
42
+ // 所以 tin = input 直接取、tcache = cacheRead;上下文占用 = 轮内最后一次 assistant 的 input。
43
+ // 失败轮:① assistant 的 stopReason 非 toolUse/endTurn/stop;② toolResult 的 isError=true
44
+ // (真库实测存在,bash 失败行)—— 两种都记 err,详情侧 callsNote 提示。
45
+ import fs from 'node:fs';
46
+ import path from 'node:path';
47
+ import {
48
+ files, entries, addEntry, markKind, listDirCached, isDir, isFile,
49
+ toText, trunc, readCompleteLines,
50
+ } from './shared.mjs';
51
+
52
+ // ---------------- 入口候选根(一个日期文件夹对应一个会话) ----------------
53
+ // minimax 的 v2 会话目录是有日期前缀的:v2/sessions/YYYY/MM/DD/<HH-MM-SS-mmm-session_<id>>/
54
+ // 同一 v2/sessions 根下可能横跨多天(v2/sessions/2026/09/20 与 v2/sessions/2026/09/21 同在)。
55
+ // 扫描入口以「会话根」为单位,循环向 YYYY/MM/DD 下探一层。
56
+ //
57
+ // 这个下探只发生在 scanMavis 内部:sniffBase 已经把 base 修到 v2/sessions(由 candidateBases /
58
+ // discovery.mjs 里的 candidateBases 分支兜底,见 discovery.mjs 的 minimax 注释),下面再细探。
59
+ //
60
+ // 子目录名形如 `07-49-21-725-session_<base64-id>`:识别只看 basename 是否以 `session_` 开头,
61
+ // 时间戳前缀是装饰、不靠它排序(同一分钟内多开两个会话就分不出先后;所以轮顺序按文件内消息序号)。
62
+ function mavisSessionDirs(root) {
63
+ const out = [];
64
+ if (!isDir(root)) return out;
65
+ // 第一层:YYYY(最多 6 年份)
66
+ for (const y of (listDirCached(root) || []).slice(0, 6)) {
67
+ const yDir = path.join(root, y);
68
+ if (!isDir(yDir) || !/^\d{4}$/.test(y)) continue;
69
+ // 第二层:MM
70
+ for (const m of (listDirCached(yDir) || []).slice(0, 12)) {
71
+ const mDir = path.join(yDir, m);
72
+ if (!isDir(mDir) || !/^\d{2}$/.test(m)) continue;
73
+ // 第三层:DD
74
+ for (const d of (listDirCached(mDir) || []).slice(0, 31)) {
75
+ const dDir = path.join(mDir, d);
76
+ if (!isDir(dDir) || !/^\d{2}$/.test(d)) continue;
77
+ // 第四层:HH-MM-SS-mmm-session_<id>
78
+ for (const s of (listDirCached(dDir) || []).slice(0, 80)) {
79
+ if (!/^[\d-]+-session_/.test(s)) continue;
80
+ const sd = path.join(dDir, s);
81
+ if (isDir(sd)) out.push(sd);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ // 用户文本清理——剥掉行首自动注入的 system-reminder(常见:首条 user 内容里嵌了一整段 xml)。
90
+ // 不剥的话预览就会是「跳了一大段提醒块 + 几十字真用户输入」,搜索/筛选体验很差。
91
+ // 严格策略:必须以 <system-reminder> 开头、且以 </system-reminder> 收尾;其余情况原样保留。
92
+ function stripReminder(text) {
93
+ if (typeof text !== 'string') return text;
94
+ const t = text.trimStart();
95
+ if (!t.startsWith('<system-reminder>')) return text;
96
+ const close = t.indexOf('</system-reminder>');
97
+ if (close < 0) return text; // 没找到收尾标签 → 不剥(残缺 reminder 比错剥更安全)
98
+ return t.slice(close + '</system-reminder>'.length).replace(/^\s+/, '');
99
+ }
100
+
101
+ // 从 content 数组里抽 text(与 doubao 的 toText 同款:数组 → 逐项 .text;字符串 → 原样)。
102
+ // thinking 文本不进 userText(只进 assistant 文本),避免详情头部把模型思考混进用户列。
103
+ function contentText(content, dropThinking) {
104
+ if (Array.isArray(content)) {
105
+ const buf = [];
106
+ for (const p of content) {
107
+ if (!p || typeof p !== 'object') continue;
108
+ if (p.type === 'text' && typeof p.text === 'string') buf.push(p.text);
109
+ else if (p.type === 'thinking' && !dropThinking && typeof p.thinking === 'string') buf.push(p.thinking);
110
+ }
111
+ return buf.join('\n\n');
112
+ }
113
+ return toText(content);
114
+ }
115
+
116
+ // 用户消息正文(I8 复现包原始片段提取复用):与扫描侧 role==='user' 的开轮判定同一套口径——
117
+ // 空文本不开轮、剥完 system-reminder 是空也不开轮。返回非空文本即「这一行是开轮行」。
118
+ export function mavisUserText(content) {
119
+ const rawText = contentText(content, /*dropThinking*/ true);
120
+ if (!rawText) return '';
121
+ const text = stripReminder(rawText);
122
+ return text ? text : '';
123
+ }
124
+
125
+ // 数字规整(与 hermes 同款)
126
+ function num(v) {
127
+ const n = Number(v);
128
+ return Number.isFinite(n) ? n : 0;
129
+ }
130
+ function usageTin(u) {
131
+ if (!u) return 0;
132
+ // minimax 的 usage.input **不含** cache(真库实测:totalTokens = input+output+cacheRead+cacheWrite
133
+ // 95/95 行成立,且 input 常远小于 cacheRead,如 input=652 / cacheRead=107410)。
134
+ // hermes 那套「input 含 cache → tin = input - cacheRead」是**另一个库的口径**,照抄会把
135
+ // 真实输入砍掉 80%+(79 次调用的轮:真实 105,196 → 旧口径 18,513)。tin 直接取 input。
136
+ return num(u.input);
137
+ }
138
+
139
+ // 探测一个 messages.jsonl 是否真属于 minimax 格式。
140
+ // sniffBase 用它防「空目录 / 别名目录被认成 minimax 而一条都扫不出」。
141
+ // 只读头 2KB(首行 JSON 通常 < 256B,CJK 累 3 字节,单行常落在 4KB 之内)。
142
+ //
143
+ // ⚠️ 真库实测:每行都是 {"message_id":...,"turn_id":...,"message":{role,content,...}},
144
+ // 而 usage/model/stopReason 这些**只在 assistant 行**的 message 里出现、user 行没有。
145
+ // 会话首行永远是 user 消息 → 首行绝不可能带着 usage/model。所以判据不能要求「首行含 usage/model」,
146
+ // 否则每个真实会话都会被认成 false、自动发现整体失效。正确判据:含 message_id + message.role
147
+ // 取 Anthropic 风格三态之一(user/assistant/toolResult),与 usage/model 无关。
148
+ export function isMavisMessages(fp) {
149
+ let fd; try { fd = fs.openSync(fp, 'r'); } catch { return false; }
150
+ try {
151
+ const buf = Buffer.alloc(2048);
152
+ const n = fs.readSync(fd, buf, 0, 2048, 0);
153
+ if (n <= 0) return false;
154
+ const head = buf.toString('utf8', 0, n);
155
+ const firstBrace = head.indexOf('{');
156
+ const firstLineEnd = head.indexOf('\n');
157
+ if (firstBrace < 0 || firstLineEnd < 0 || firstLineEnd <= firstBrace) return false;
158
+ const firstLine = head.slice(firstBrace, firstLineEnd);
159
+ return /"message_id"\s*:/.test(firstLine)
160
+ && /"message"\s*:\s*\{\s*"role"\s*:\s*"(user|assistant|toolResult)"/.test(firstLine);
161
+ } finally { try { fs.closeSync(fd); } catch {} }
162
+ }
163
+
164
+ // 嗅探一个目录是否含 minimax 会话,并返回**解析出的 v2/sessions 根**(由 discovery.mjs 的 sniffBase 调用)。
165
+ // 返回:能认出 minimax 就返回「会话根目录」字符串(后续 scanMavis 直接拿它下探),否则返回 null。
166
+ // 三种命中形态:
167
+ // 1) 自己就是会话目录(含 messages.jsonl + manifest.json + isMavisMessages) → 返回自己
168
+ // 2) 自己是 v2/sessions 根,下探四层命中 session_<id> → 返回自己
169
+ // 3) 自己是 ~/.minimax 应用根(顶层有 v2/ 子目录)→ 返回 <base>/v2/sessions(真库实测:会话数据在这)
170
+ // 形态 3 是真实摆法:candidateBases 铺出的 ~/.minimax 顶层只有 v2/ 与配置/缓存,没有 messages.jsonl,
171
+ // 必须一路指到 v2/sessions 才能让 scanMavis 扫到;否则 sniffBase 会把 ~/.minimax 兜底认成 atomcode。
172
+ export function hasMavisSession(base) {
173
+ if (!isDir(base)) return null;
174
+ // 形态 A:自己就是会话目录
175
+ const fpSelf = path.join(base, 'messages.jsonl');
176
+ if (isFile(fpSelf) && isFile(path.join(base, 'manifest.json')) && isMavisMessages(fpSelf)) return base;
177
+ // 形态 C:应用根下的 v2/sessions(优先——真库数据都在这)
178
+ const v2 = path.join(base, 'v2', 'sessions');
179
+ if (isDir(v2) && mavisSessionsRoot(v2)) return v2;
180
+ // 形态 B:自己就是 v2/sessions 根,下面任意深度≤4 命中一个会话目录
181
+ if (mavisSessionsRoot(base)) return base;
182
+ return null;
183
+ }
184
+
185
+ // 判断一个「候选 v2/sessions 根」下面有没有真实会话目录(四层 y/m/d/session_<id>)。
186
+ function mavisSessionsRoot(root) {
187
+ if (!isDir(root)) return false;
188
+ for (const y of (listDirCached(root) || []).slice(0, 6)) {
189
+ const yDir = path.join(root, y);
190
+ if (!isDir(yDir) || !/^\d{4}$/.test(y)) continue;
191
+ for (const m of (listDirCached(yDir) || []).slice(0, 12)) {
192
+ const mDir = path.join(yDir, m);
193
+ if (!isDir(mDir) || !/^\d{2}$/.test(m)) continue;
194
+ for (const d of (listDirCached(mDir) || []).slice(0, 31)) {
195
+ const dDir = path.join(mDir, d);
196
+ if (!isDir(dDir) || !/^\d{2}$/.test(d)) continue;
197
+ for (const s of (listDirCached(dDir) || []).slice(0, 80)) {
198
+ if (!/^[\d-]+-session_/.test(s)) continue;
199
+ const sd = path.join(dDir, s);
200
+ if (isDir(sd)) return true;
201
+ }
202
+ }
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+
208
+ // id 前缀:mvs-session_<base64-id> 里的 base64 可能含 `/` 与 `+`,所以用「把路径哈希化」做 key
209
+ // (doubao 的 idBase 是同样的「路径 → 唯一字符串」法)。会话目录路径已是「唯一标识」,无需 hash。
210
+ export function mavisIdBase(sessionDir) {
211
+ return 'mv#' + sessionDir.replace(/[\\/:]/g, '~') + '#';
212
+ }
213
+
214
+ // manifest 解析(取 createdAtMs 当会话起始时间;sessionId 写到 entry 的 session 字段,调试向日追溯)
215
+ function readManifest(sessionDir) {
216
+ try { return JSON.parse(fs.readFileSync(path.join(sessionDir, 'manifest.json'), 'utf8')); }
217
+ catch { return null; }
218
+ }
219
+
220
+ // context 窗口容量(ctx 分母):同目录 llm-call.json 的 maxTokens(真库实测 128000)。
221
+ // 它是「聚合快照」不是逐次调用日志(拿不到逐次耗时),但 maxTokens 是现成的容量口径 ——
222
+ // 页面据此画「占用 / 窗口」进度条;取不到才留 0(页面退化为只显示占用)。
223
+ function readCtxWindow(sessionDir) {
224
+ try {
225
+ const j = JSON.parse(fs.readFileSync(path.join(sessionDir, 'llm-call.json'), 'utf8'));
226
+ const n = Number(j && j.maxTokens);
227
+ return Number.isFinite(n) && n > 0 ? n : 0;
228
+ } catch { return 0; }
229
+ }
230
+
231
+ // ---------------- emit:把一轮的数据落成 entry ----------------
232
+ // 轮字段(与 hermes 同款语义,但写得更紧——minimax 的 usage 现成、不必等旁路日志凑):
233
+ // time 首条 user 消息的 timestamp(毫秒)
234
+ // dur endTs - time;endTs = 轮内最后一条 assistant/toolResult 的 timestamp
235
+ // status err?'error':'ok',按 stopReason != 'toolUse' 且 stopReason != null 当 ok
236
+ // tin/tout/tcache/total 轮内 assistant usage 之和
237
+ // ctx = 同目录 llm-call.json 的 maxTokens(窗口容量;取不到 → 0,页面不画进度条)
238
+ // ctxUsed 轮内最后一次 assistant 的 usage.input(不含 cache;页面拿来画占用进度条)
239
+ // rounds = calls(一轮 = 一条 user;calls = assistant 行数)
240
+ // tools 轮内 toolCall 计数
241
+ // models 轮内去重后的 model 名
242
+ // preview 首条 user 文本截前 300 字符
243
+ // finished i < turns.length - 1;最后一轮由页面按 lastTs 兜底(同 doubao)
244
+ // aborted 仅按 stopReason 字面量认;aborted 字面量没见到,恒 false(不编造)
245
+ function emitTurns(agent, sessionDir, data, fromIdx) {
246
+ const base = mavisIdBase(sessionDir);
247
+ const sid = data.manifest?.sessionId || path.basename(sessionDir);
248
+ for (let i = fromIdx; i < data.turns.length; i++) {
249
+ const t = data.turns[i];
250
+ const next = data.turns[i + 1];
251
+ addEntry(base + t.idx, {
252
+ agent,
253
+ project: data.project,
254
+ session: sid,
255
+ time: t.time,
256
+ dur: next ? Math.max(0, (next.time || t.time) - t.time) : 0,
257
+ status: t.err ? 'error' : 'ok',
258
+ tin: t.tin, tout: t.tout, tcache: t.tcache,
259
+ total: t.tin + t.tout + t.tcache,
260
+ ctx: data.win || 0, // 窗口容量来自同目录 llm-call.json 的 maxTokens(取不到 → 0)
261
+ ctxUsed: t.ctxUsed || 0,
262
+ rounds: t.calls,
263
+ calls: t.calls,
264
+ tools: t.tools,
265
+ models: t.models,
266
+ preview: t.preview,
267
+ finished: i < data.turns.length - 1,
268
+ aborted: false,
269
+ toolNames: t.toolNames,
270
+ }, { file: data.fp, turn: t.idx, kind: 'minimax' });
271
+ }
272
+ }
273
+
274
+ // ---------------- 扫描主入口 ----------------
275
+ // root 通常是 v2/sessions 根;scanMavis 内部按 y/m/d/session-id 四层下探每个会话目录。
276
+ // 约定:root 下每多一层,都按 y(4) / m(2) / d(2) / session-id 走;不是这套的就跳过(不报错)。
277
+ //
278
+ // 单文件增量续读:每条 OFF_KINDS.jsonl 的标准做法(claude/doubao/kimi/codex 走 readCompleteLines)。
279
+ // messages.jsonl 的 usage 字段在每行里现成,所以不需要额外旁路(hermes 的 agent.log / doubao 的
280
+ // assignment.md 都不必打开)。
281
+ export function scanMavis(agent, root, onlyFile) {
282
+ if (!isDir(root)) return;
283
+ for (const sessionDir of mavisSessionDirs(root)) {
284
+ const fp = path.join(sessionDir, 'messages.jsonl');
285
+ if (!isFile(fp)) continue;
286
+ if (onlyFile && fp !== onlyFile) continue;
287
+ let fst; try { fst = fs.statSync(fp); } catch { continue; }
288
+ const key = fp;
289
+ const m = fst.mtimeMs, s = fst.size;
290
+ const prev = files.get(key);
291
+ if (prev && prev.m === m && prev.s === s) {
292
+ // 没变 → 不重发条目(首扫时 prev.data 由 emitTurns 推过;这里只补一次已发条目)
293
+ if (prev.data?.turns?.length && !entries.has(mavisIdBase(sessionDir) + (prev.data.turns.length - 1)))
294
+ emitTurns(agent, sessionDir, prev.data, 0);
295
+ continue;
296
+ }
297
+ const off = prev?.off || 0;
298
+ const data = prev?.data || {
299
+ turns: [], emitted: 0, session: path.basename(sessionDir),
300
+ project: '', manifest: null, fp,
301
+ };
302
+ if (!data.manifest) data.manifest = readManifest(sessionDir);
303
+ if (data.win === undefined) data.win = readCtxWindow(sessionDir); // ctx 分母:llm-call.json maxTokens
304
+ if (!data.project) {
305
+ // 缺省项目名 = 会话目录的祖上 4 层(v2/sessions/YYYY/MM/DD);同根所有会话共享同一项目,
306
+ // 避免项目筛选下拉里冒出一长串「会话号 / 日期」项
307
+ const ancestors = sessionDir.split(path.sep);
308
+ data.project = ancestors.slice(0, Math.max(0, ancestors.length - 4)).join(path.sep) || '(minimax)';
309
+ }
310
+ if (s <= off) { files.set(key, { agent, kind: 'minimax', m, s, off, data }); markKind('minimax'); continue; }
311
+ try {
312
+ const rd = readCompleteLines(fp, off, s);
313
+ if (!rd) { files.set(key, { agent, kind: 'minimax', m, s, off, data }); markKind('minimax'); continue; }
314
+ const newOff = rd.newOff;
315
+ for (const line of rd.lines) {
316
+ if (!line.trim()) continue;
317
+ let j; try { j = JSON.parse(line); } catch { continue; }
318
+ // ⚠️ 真库实测:每行都是 {message_id, turn_id, message:{...}},usage/model/stopReason
319
+ // 这些只在 assistant 行的 message 里;user / toolResult 行没有这些字段。
320
+ // 所以 role 与所有用量/模型都从 `msg`(=j.message)取,不能从顶层 `j` 取。
321
+ const msg = j.message || {};
322
+ const role = msg.role || (msg.toolCallId ? 'toolResult' : null);
323
+ if (!role) continue;
324
+ if (role === 'user') {
325
+ const rawText = contentText(msg.content, /*dropThinking*/true);
326
+ if (!rawText) continue; // 空用户消息不开轮(与 doubao / hermes 同款)
327
+ const text = stripReminder(rawText);
328
+ if (!text) continue; // 剥完 reminder 是空 → 同样不开轮
329
+ data.turns.push({
330
+ idx: data.turns.length,
331
+ time: num(msg.timestamp) || 0,
332
+ lastTs: num(msg.timestamp) || 0,
333
+ tin: 0, tout: 0, tcache: 0, ctxUsed: 0,
334
+ calls: 0, tools: 0, models: [], err: false,
335
+ custom: 0, customTypes: [],
336
+ preview: text.replace(/\s+/g, ' ').slice(0, 300),
337
+ toolNames: {},
338
+ });
339
+ } else if (role === 'assistant' && data.turns.length) {
340
+ const cur = data.turns[data.turns.length - 1];
341
+ cur.calls++;
342
+ const ts = num(msg.timestamp) || 0;
343
+ if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
344
+ // usage 求和(多个 LLM 调用属于同一个 user → 同一轮)
345
+ // minimax 的 usage 在 message.usage(不是顶层 j.usage);input 不含 cache → tin 直接取
346
+ const u = msg.usage;
347
+ if (u) {
348
+ const tin = usageTin(u);
349
+ const tout = num(u.output);
350
+ const tcache = Math.max(num(u.cacheRead), num(u.cacheWrite));
351
+ cur.tin += tin; cur.tout += tout; cur.tcache += tcache;
352
+ cur.ctxUsed = num(u.input); // 逐轮上下文 = 轮内最后一次 assistant 的 input(不含 cache)
353
+ }
354
+ if (msg.model && !cur.models.includes(msg.model)) cur.models.push(msg.model);
355
+ // 失败判定:stopReason 既不是 toolUse(中间调用)也不是正常的 endTurn/stop 时记 err
356
+ // (isError 的兜底在 toolResult 分支;两者都进同一门 err)
357
+ const sr = msg.stopReason;
358
+ if (sr && sr !== 'toolUse' && sr !== 'endTurn' && sr !== 'stop') cur.err = true;
359
+ // 工具调用累积(minimax 的 toolCall.arguments 是对象,不是 JSON 字符串)
360
+ const content = Array.isArray(msg.content) ? msg.content : [];
361
+ for (const p of content) {
362
+ if (!p || typeof p !== 'object') continue;
363
+ if (p.type === 'toolCall') {
364
+ const nm = typeof p.name === 'string' && p.name ? p.name : '?';
365
+ cur.tools++;
366
+ cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
367
+ }
368
+ }
369
+ } else if (role === 'toolResult' && data.turns.length) {
370
+ // toolResult:暂不参与轮统计(详情侧按 toolCallId 回填,扫描侧只标记有结果回来)
371
+ // 这样不会重复计数 tools(toolCall 计数已在 assistant 行完成)。
372
+ // toolResult 行的时间戳在 message.timestamp(不是顶层 j.timestamp)
373
+ const ts = num(msg.timestamp) || 0;
374
+ if (ts) {
375
+ const cur = data.turns[data.turns.length - 1];
376
+ cur.lastTs = Math.max(cur.lastTs, ts);
377
+ }
378
+ // isError=true(真库实测:bash 失败行)→ 该轮记 err。
379
+ // 详情侧本来就会标红这个工具,但轮状态不记的话「列表一片 ok、点开才有红」,
380
+ // 一个含失败工具的轮在列表里看着是健康的。
381
+ if (msg.isError) data.turns[data.turns.length - 1].err = true;
382
+ } else if (role === 'custom' && data.turns.length) {
383
+ // custom(系统注入,实测 customType=todo_cadence_reminder / display=false / content 是字符串):
384
+ // 不开轮、不进正文,但**不静默丢弃** —— ① 更新 lastTs(否则提醒恰好落在轮尾会截断该轮 dur);
385
+ // ② 记数与类型进轮状态,详情侧 callsNote 留痕("本轮含 N 条系统注入")。
386
+ const ts = num(msg.timestamp) || 0;
387
+ const cur = data.turns[data.turns.length - 1];
388
+ if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
389
+ cur.custom = (cur.custom || 0) + 1;
390
+ const ct = typeof msg.customType === 'string' && msg.customType ? msg.customType : 'custom';
391
+ if (!cur.customTypes) cur.customTypes = [];
392
+ if (!cur.customTypes.includes(ct)) cur.customTypes.push(ct);
393
+ }
394
+ }
395
+ files.set(key, { agent, kind: 'minimax', m, s, off: newOff, data });
396
+ markKind('minimax');
397
+ emitTurns(agent, sessionDir, data, Math.max(0, data.emitted - 1));
398
+ data.emitted = data.turns.length;
399
+ } catch {}
400
+ }
401
+ }
402
+
403
+ // ---------------- 详情:重读 messages.jsonl 按 src.turn 过滤 ----------------
404
+ // 开轮判定与扫描侧同一份(role=user 且文本非空)——天然对齐,不会出现「扫到这一轮、详情找不到」。
405
+ export function mavisEntryContent(src, full) {
406
+ const st = files.get(src.file);
407
+ const version = st ? st.m + ':' + (st.off || 0) : null;
408
+ const lines = fs.readFileSync(src.file, 'utf8').split('\n');
409
+ let turnIdx = -1;
410
+ const out = {
411
+ user: '', assistant: '', tools: [], calls: [],
412
+ v: version,
413
+ callsNote: 'minimax 转录(messages.jsonl)每行带 usage / model / stopReason:token 按行聚合' +
414
+ '(tin = input 直接取——minimax 的 input 不含 cache,totalTokens = input+output+cacheRead+cacheWrite;tcache = cacheRead),' +
415
+ '模型名 = assistant 行 model,轮耗时 = 下一轮 time − 本轮 time;' +
416
+ 'context 窗口容量取同目录 llm-call.json 的 maxTokens(取不到则只显示占用);' +
417
+ '逐次调用耗时源头不落盘(assistant 行无时长字段,llm-call.json 是聚合快照不是逐次日志),故 calls[].dur 恒为 0。',
418
+ };
419
+ const texts = [];
420
+ let lastStopReason = '';
421
+ let lastModel = '';
422
+ // 本轮 custom(系统注入)消息的计数与类型 —— 不开轮、不进正文,只在 callsNote 留痕
423
+ let customCount = 0;
424
+ const customTypes = [];
425
+ // 收集 assistant 行的 toolCall 入参(arguments 是对象),按 toolCallId 索引,
426
+ // 回填到对应的 toolResult 详情(toolResult 行本身不含入参,只有出参)。
427
+ const callArgs = {};
428
+ for (const line of lines) {
429
+ if (!line.trim()) continue;
430
+ let j; try { j = JSON.parse(line); } catch { continue; }
431
+ // ⚠️ 真库实测:toolResult 的 toolCallId / toolName / content / isError 都在 message 里
432
+ // (顶层只有 message_id / turn_id / message)。role 与所有工具字段都从 msg 取。
433
+ const msg = j.message || {};
434
+ const role = msg.role || (msg.toolCallId ? 'toolResult' : null);
435
+ if (!role) continue;
436
+ if (role === 'user') {
437
+ const rawText = contentText(msg.content, true);
438
+ const text = rawText ? stripReminder(rawText) : '';
439
+ if (!text) continue;
440
+ turnIdx++;
441
+ if (turnIdx === src.turn) out.user = text;
442
+ continue;
443
+ }
444
+ if (turnIdx !== src.turn) continue;
445
+ if (role === 'assistant') {
446
+ // 文本:thinking + 散落 text 都拼起来(页面 md 渲染),与扫描侧对应
447
+ const text = contentText(msg.content, /*dropThinking*/false);
448
+ if (text) texts.push(text);
449
+ // usage / model / stopReason 都在 message 里(不是顶层 j)
450
+ const u = msg.usage;
451
+ if (msg.stopReason) lastStopReason = msg.stopReason;
452
+ if (msg.model) lastModel = msg.model;
453
+ const call = {
454
+ model: msg.model || lastModel || '',
455
+ tin: u ? usageTin(u) : 0,
456
+ tout: u ? num(u.output) : 0,
457
+ tcache: u ? Math.max(num(u.cacheRead), num(u.cacheWrite)) : 0,
458
+ dur: 0,
459
+ };
460
+ if (text) { const ft = {}; call.text = trunc(text, full, ft); call.textTrunc = !!ft.t; }
461
+ const tcs = Array.isArray(msg.content) ? msg.content.filter(p => p && p.type === 'toolCall') : [];
462
+ if (tcs.length) call.tools = tcs.map(t => t.name || '?');
463
+ for (const tc of tcs) if (tc.id) callArgs[tc.id] = tc.arguments; // 入参按 id 暂存
464
+ out.calls.push(call);
465
+ } else if (role === 'toolResult') {
466
+ // toolCallId / toolName / content / isError 都在 message 里
467
+ const tcid = msg.toolCallId || '';
468
+ // 找上一条 assistant 行里同 id 的 toolCall;找不到就先暂存,等下一条 assistant 再补
469
+ // (scan 端 toolCall 计数以 assistant 行为准;详情侧要保证输出在正确 tool 上)
470
+ const t = out.tools.find(x => x.tid === tcid) || (() => {
471
+ // 还没建:建一个最小占位
472
+ const ph = { name: msg.toolName || '?', tid: tcid, error: null, input: '', inputTrunc: false, output: '', outputTrunc: false };
473
+ out.tools.push(ph);
474
+ return ph;
475
+ })();
476
+ if (t.input === '' && callArgs[tcid] !== undefined) {
477
+ const fi = {}; t.input = trunc(JSON.stringify(callArgs[tcid]), full, fi); t.inputTrunc = !!fi.t;
478
+ }
479
+ const cArr = Array.isArray(msg.content) ? msg.content : [];
480
+ const txt = cArr.filter(p => p && p.type === 'text').map(p => p.text).join('\n');
481
+ const fo = {}; t.output = trunc(txt || toText(msg.content), full, fo); t.outputTrunc = !!fo.t;
482
+ if (msg.isError) t.error = 'error';
483
+ } else if (role === 'custom') {
484
+ // custom(系统注入,如 todo_cadence_reminder):不进正文、不开轮,只留痕(与扫描侧同一口径)
485
+ customCount++;
486
+ const ct = typeof msg.customType === 'string' && msg.customType ? msg.customType : 'custom';
487
+ if (!customTypes.includes(ct)) customTypes.push(ct);
488
+ }
489
+ if (turnIdx > src.turn) break;
490
+ }
491
+ // assistant 文本里若没有 toolCall 文本,前面已经拼好;若 toolResult 在 user 之前到达(畸形)也不会错位
492
+ out.assistant = texts.join('\n\n---\n\n');
493
+ if (lastStopReason && lastStopReason !== 'toolUse' && lastStopReason !== 'endTurn' && lastStopReason !== 'stop') {
494
+ out.callsNote += ' 本轮 stopReason=' + lastStopReason + '(非正常收尾)。';
495
+ }
496
+ if (customCount) {
497
+ out.callsNote += ' 本轮含 ' + customCount + ' 条系统注入消息(custom:' + customTypes.join(', ') +
498
+ ',display=false 由运行时自动插入,不进正文)。';
499
+ }
500
+ return out;
501
+ }
502
+
503
+ // sniffBase 与 candidateBases 见 discovery.mjs;这里只负责解析。