@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,494 @@
1
+ // ---------------- generic-jsonl:受限声明式解析器(R18) ----------------
2
+ // 让「claude 同族」的逐行 jsonl agent 通过 config 声明接入,不再为每一个手写 scanXxx。
3
+ // 「受限」的边界(写进需求验收,别越界):
4
+ // * 只支持**逐行 JSON 的线性状态机**:开轮 / 累加 / 收轮三类事件;
5
+ // * 字段路径是白名单子集:单层 message.xxx、content[](迭代)、content[type=tool_use](过滤);
6
+ // * 表达式只允许字段引用与 + 组合(如 in + cacheRead + cacheCreate),**禁止 eval / new Function**;
7
+ // * 不支持跨行配对(buddy 的 callId)、多帧压缩(dsh)、事件流重放(kimi)、SQLite(cursor/zcode)
8
+ // —— 这些仍走各自的手写解析器,generic-jsonl **不替代**任何现有 kind。
9
+ // 版本纪律:kind 级的 PARSER_REV 表管不了它 —— 同一 kind 下每个 agent 的 rules 各不相同,
10
+ // 口径版本(rulesRev + rulesHash)随 config 落盘,主文件 loadIndex 恢复时逐文件比对,
11
+ // 不一致即丢弃该文件状态整份重扫(不允许「半新半旧」)。
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import crypto from 'node:crypto';
15
+ import { files, entries, addEntry, modelName, modelList, windowOf, markKind, listDirCached, isDir, toText, readCompleteLines } from './shared.mjs';
16
+
17
+ // ---------------- 路径与表达式的白名单 ----------------
18
+ // 路径段:name | name[](迭代数组) | name[k=v](按字段过滤数组元素)。
19
+ // 组合按从左到右逐段求值;`[]` 之后还可以接过滤器(content[][type=x]),但常规写法是 content[type=tool_use]。
20
+ const SEG_RE = /^([A-Za-z_][\w-]*)(\[\])?(?:\[([A-Za-z_][\w-]*)=([^\]]*)\])?$/;
21
+ // 表达式项:只能是 usage 规则里配置的那几个字段名(in/out/cacheRead/cacheCreate)之一,用 + 连接。
22
+ const EXPR_FIELDS = ['in', 'out', 'cacheRead', 'cacheCreate'];
23
+
24
+ // 校验一条路径 spec(字符串或字符串数组),返回 null(合法)或错误信息。
25
+ function pathErr(spec) {
26
+ const list = Array.isArray(spec) ? spec : [spec];
27
+ if (!list.length) return '路径为空';
28
+ for (const s of list) {
29
+ if (typeof s !== 'string' || !s.trim()) return '路径必须是非空字符串';
30
+ for (const seg of s.trim().split('.')) {
31
+ if (!SEG_RE.test(seg)) return '路径段「' + seg + '」不合法(只允许 字段名 / 字段名[] / 字段名[字段=值])';
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ // 校验表达式:按 + 拆项,每一项必须是白名单字段名。返回 null(合法)或错误信息。
37
+ function exprErr(expr) {
38
+ if (typeof expr !== 'string' || !expr.trim()) return '表达式为空';
39
+ for (const term of expr.split('+')) {
40
+ const t = term.trim();
41
+ if (!EXPR_FIELDS.includes(t)) return '表达式项「' + t + '」不合法(只允许 ' + EXPR_FIELDS.join('/') + ' 与 + 组合)';
42
+ }
43
+ return null;
44
+ }
45
+ // 表达式求值:vars 是 {in, out, cacheRead, cacheCreate} 的数值表,缺项按 0。split 而非 eval。
46
+ function evalExpr(expr, vars) {
47
+ let sum = 0;
48
+ for (const term of String(expr).split('+')) {
49
+ const v = vars[term.trim()];
50
+ sum += typeof v === 'number' && isFinite(v) ? v : 0;
51
+ }
52
+ return sum;
53
+ }
54
+ // 路径求值:返回全部命中值(数组)。求值对象中途碰到标量/缺失即断(不报错,返回空)。
55
+ function evalPath(obj, spec) {
56
+ const list = Array.isArray(spec) ? spec : [spec];
57
+ let out = [];
58
+ for (const s of list) {
59
+ let cur = [obj];
60
+ let dead = false;
61
+ for (const seg of s.trim().split('.')) {
62
+ const m = SEG_RE.exec(seg);
63
+ if (!m) { dead = true; break; }
64
+ const next = [];
65
+ for (const o of cur) {
66
+ if (o == null || typeof o !== 'object') continue;
67
+ let v = o[m[1]];
68
+ if (v == null) continue;
69
+ if (m[2]) { // name[]:迭代数组
70
+ if (!Array.isArray(v)) continue;
71
+ next.push(...v);
72
+ } else next.push(v);
73
+ }
74
+ if (m[3]) { // name[k=v]:按字段过滤。值是数组时**对元素过滤**(content[type=tool_use] 的本意),
75
+ // 不是标量时才当单对象看 —— 数组本身不是候选值(tool_use 块在数组里,不在数组上)
76
+ const kept = [];
77
+ for (const o of next) {
78
+ const items = Array.isArray(o) ? o : [o];
79
+ for (const el of items) {
80
+ if (el && typeof el === 'object' && String(el[m[3]]) === m[4]) kept.push(el);
81
+ }
82
+ }
83
+ cur = kept;
84
+ } else cur = next;
85
+ if (!cur.length) { dead = true; break; }
86
+ }
87
+ if (!dead) out.push(...cur);
88
+ }
89
+ return out;
90
+ }
91
+ // 路径取**第一个**命中值;'textPath' 支持数组(多个候选路径,先命中的赢 —— 见 turnOpen)。
92
+ function firstOf(obj, spec) {
93
+ const v = evalPath(obj, spec);
94
+ return v.length ? v[0] : undefined;
95
+ }
96
+ // turnOpen 的正文提取:textPath 可以是单个路径,也可以是**按序兜底**的路径数组(块数组形态在先、
97
+ // 字符串 content 兜底)。两条口径与 claudeUserText 对齐:
98
+ // * 只收**字符串**命中值 —— 兜底路径 'message.content' 在 content 是块数组时会命中整个数组,
99
+ // 那不是正文;数组必须走 `content[]` 迭代。只收字符串顺带保证「只有 tool_result 的 user 行」
100
+ // 取不到正文 → 不开轮(与 claude 的 tool_result-only 跳过同口径);
101
+ // * 多个候选按序试,第一个取到非空正文的赢;全空返回 ''(不开轮)。
102
+ function textFromPath(j, spec) {
103
+ const list = Array.isArray(spec) ? spec : [spec];
104
+ for (const s of list) {
105
+ const t = evalPath(j, s).filter(v => typeof v === 'string').join('\n');
106
+ if (t) return t;
107
+ }
108
+ return '';
109
+ }
110
+
111
+ // ---------------- rules 校验(loadConfig 阶段调用;失败回落 enabled:false) ----------------
112
+ // 返回错误数组(空数组 = 合法)。每条错误带 rule 名 —— 诊断页要能说出**具体哪条 rule 坏了**,
113
+ // 不允许静默扫出 0 条。
114
+ export function validateGenericRules(rules) {
115
+ const errs = [];
116
+ const bad = (rule, msg) => errs.push({ rule, msg });
117
+ if (!rules || typeof rules !== 'object' || Array.isArray(rules)) {
118
+ bad('rules', 'rules 必须是对象(至少要有 turnOpen)');
119
+ return errs;
120
+ }
121
+ const to = rules.turnOpen;
122
+ if (!to || typeof to !== 'object') bad('turnOpen', 'turnOpen 缺失:没有开轮规则就扫不出任何轮');
123
+ else {
124
+ if (typeof to.lineType !== 'string' || !to.lineType) bad('turnOpen', 'lineType 必须是非空字符串(行的 type 字段值)');
125
+ if (to.textPath == null) bad('turnOpen', 'textPath 缺失');
126
+ else { const e = pathErr(to.textPath); if (e) bad('turnOpen', 'textPath:' + e); }
127
+ if (to.skipIf != null && (!Array.isArray(to.skipIf) || to.skipIf.some(x => typeof x !== 'string')))
128
+ bad('turnOpen', 'skipIf 必须是字符串数组(前缀黑名单)');
129
+ }
130
+ if (rules.usage != null) {
131
+ const u = rules.usage;
132
+ if (typeof u !== 'object') bad('usage', 'usage 必须是对象');
133
+ else {
134
+ if (typeof u.on !== 'string' || !u.on) bad('usage', 'on 必须是非空字符串');
135
+ if (u.path != null) { const e = pathErr(u.path); if (e) bad('usage', 'path:' + e); }
136
+ for (const k of EXPR_FIELDS) {
137
+ if (u[k] != null && (typeof u[k] !== 'string' || !/^[A-Za-z_][\w-]*$/.test(u[k])))
138
+ bad('usage', k + ' 必须是 usage 对象上的字段名');
139
+ }
140
+ if (u.ctxUsed != null) { const e = exprErr(u.ctxUsed); if (e) bad('usage', 'ctxUsed:' + e); }
141
+ if (u.dedupeBy != null) { const e = pathErr(u.dedupeBy); if (e) bad('usage', 'dedupeBy:' + e); }
142
+ }
143
+ }
144
+ if (rules.model != null) {
145
+ const m = rules.model;
146
+ if (typeof m !== 'object' || m.path == null) bad('model', 'model 必须是对象且带 path');
147
+ else { const e = pathErr(m.path); if (e) bad('model', 'path:' + e); }
148
+ }
149
+ if (rules.tools != null) {
150
+ const t = rules.tools;
151
+ if (typeof t !== 'object') bad('tools', 'tools 必须是对象');
152
+ else {
153
+ if (typeof t.on !== 'string' || !t.on) bad('tools', 'on 必须是非空字符串');
154
+ if (t.count == null) bad('tools', 'count 缺失');
155
+ else { const e = pathErr(t.count); if (e) bad('tools', 'count:' + e); }
156
+ }
157
+ }
158
+ if (rules.turnClose != null) {
159
+ const tc = rules.turnClose;
160
+ if (typeof tc !== 'object') bad('turnClose', 'turnClose 必须是对象');
161
+ else {
162
+ if (typeof tc.on !== 'string' || !tc.on) bad('turnClose', 'on 必须是非空字符串(行的 type 字段值)');
163
+ if (tc.setFlag == null) bad('turnClose', 'setFlag 缺失');
164
+ else { const e = pathErr(tc.setFlag); if (e) bad('turnClose', 'setFlag:' + e); }
165
+ }
166
+ }
167
+ if (rules.ts != null) {
168
+ const t = rules.ts;
169
+ if (typeof t !== 'object' || t.path == null) bad('ts', 'ts 必须是对象且带 path');
170
+ else { const e = pathErr(t.path); if (e) bad('ts', 'path:' + e); }
171
+ }
172
+ if (rules.cwd != null) {
173
+ const c = rules.cwd;
174
+ if (typeof c !== 'object' || c.path == null) bad('cwd', 'cwd 必须是对象且带 path');
175
+ else { const e = pathErr(c.path); if (e) bad('cwd', 'path:' + e); }
176
+ }
177
+ // 扩展(R18 实现补充,需求 schema 之外的可选项):详情页的「AI 输出」正文路径。
178
+ // 没有它卡片照常出(轮次/token/模型都在),只是点开详情 assistant 一栏为空。
179
+ if (rules.assistantText != null) {
180
+ const a = rules.assistantText;
181
+ if (typeof a !== 'object') bad('assistantText', 'assistantText 必须是对象');
182
+ else {
183
+ if (typeof a.on !== 'string' || !a.on) bad('assistantText', 'on 必须是非空字符串');
184
+ if (a.path == null) bad('assistantText', 'path 缺失');
185
+ else { const e = pathErr(a.path); if (e) bad('assistantText', 'path:' + e); }
186
+ }
187
+ }
188
+ return errs;
189
+ }
190
+
191
+ // rules 指纹:键排序后 JSON 串的 sha1。config 落盘用它判断「rules 变了没」。
192
+ export function genericRulesHash(rules) {
193
+ const norm = (function sortKeys(v) {
194
+ if (Array.isArray(v)) return v.map(sortKeys);
195
+ if (v && typeof v === 'object') {
196
+ const o = {};
197
+ for (const k of Object.keys(v).sort()) o[k] = sortKeys(v[k]);
198
+ return o;
199
+ }
200
+ return v;
201
+ })(rules || {});
202
+ return crypto.createHash('sha1').update(JSON.stringify(norm)).digest('hex').slice(0, 12);
203
+ }
204
+
205
+ // ---------------- 扫描 ----------------
206
+ // fileGlob → 正则(只支持 * 与 ?,够用且受限)。匹配不了返回 null(回落 '*.jsonl')。
207
+ function globToRe(g) {
208
+ if (typeof g !== 'string' || !g) return null;
209
+ try {
210
+ const esc = g.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/\\\\]*').replace(/\?/g, '.');
211
+ return new RegExp('^' + esc + '$', 'i');
212
+ } catch { return null; }
213
+ }
214
+ // 编译结果缓存:agent 名 -> {hash, conf}。rulesHash 变了自动重编(改配置不用重启扫描循环也认新规则)。
215
+ const compiledCache = new Map();
216
+ function compiledOf(agent, conf) {
217
+ const hash = genericRulesHash(conf && conf.rules);
218
+ const c = compiledCache.get(agent);
219
+ if (c && c.hash === hash) return c;
220
+ const nc = { hash, conf };
221
+ compiledCache.set(agent, nc);
222
+ return nc;
223
+ }
224
+ function num(v) { const n = Number(v); return typeof v === 'number' && isFinite(v) ? v : (isFinite(n) ? n : 0); }
225
+ function tsOf(v) {
226
+ if (v == null || v === '') return 0;
227
+ if (typeof v === 'number') return isFinite(v) ? v : 0;
228
+ const t = Date.parse(String(v));
229
+ return isFinite(t) ? t : 0;
230
+ }
231
+
232
+ // 从一行 JSON 推进状态机(compiled = {conf})。与 scanClaude 的分支一一对应,规则全部来自 config。
233
+ function processLine(j, compiled, data) {
234
+ const r = compiled.conf.rules || {};
235
+ const ts = r.ts ? tsOf(firstOf(j, r.ts.path)) : 0;
236
+ if (r.cwd) {
237
+ const cv = firstOf(j, r.cwd.path);
238
+ if (cv != null && String(cv) && !data.cwd) data.cwd = String(cv);
239
+ }
240
+ const to = r.turnOpen || {};
241
+ const type = j.type;
242
+ if (type === to.lineType) {
243
+ const text = textFromPath(j, to.textPath);
244
+ if (!text) return; // 没有正文:工具结果这类行不开轮(同 claude 的 tool_result-only)
245
+ const t0 = String(text).trimStart();
246
+ if ((to.skipIf || []).some(p => t0.startsWith(p))) return; // 命令消息不开轮(同 claudeIsCommandText 口径)
247
+ data.turns.push({
248
+ idx: data.turns.length, time: ts, lastTs: ts,
249
+ tin: 0, tout: 0, tcache: 0, tools: 0, calls: 0, ctxUsed: 0,
250
+ models: [], mids: [], err: false,
251
+ preview: text.replace(/\s+/g, ' ').slice(0, 300),
252
+ toolNames: {},
253
+ });
254
+ return;
255
+ }
256
+ const u = r.usage;
257
+ const modelOn = (r.model && r.model.on) || (u && u.on);
258
+ const assistantLike = (u && type === u.on) || (!u && modelOn && type === modelOn);
259
+ if (assistantLike && data.turns.length) {
260
+ const cur = data.turns[data.turns.length - 1];
261
+ // 去重口径与 claude 的 mids 相同:dedupeBy 命中的行只计一次 usage/calls/model;
262
+ // 命中重复时 tools / 时间戳照常累计(tool_use 是独立调用,不能被 mid 去重吞掉)。
263
+ let seenMid = false;
264
+ if (u) {
265
+ const midPath = u.dedupeBy;
266
+ const midRaw = midPath ? firstOf(j, midPath) : undefined;
267
+ const mid = midRaw == null || midRaw === '' ? null : String(midRaw);
268
+ let uv = {};
269
+ if (u.path) uv = firstOf(j, u.path) || {};
270
+ if (!(uv && typeof uv === 'object')) uv = {};
271
+ if (mid) {
272
+ if ((seenMid = cur.mids.includes(mid))) { /* duplicate – skip token fields */ }
273
+ else cur.mids.push(mid);
274
+ }
275
+ if (!seenMid) {
276
+ const g = k => (u[k] != null ? num(uv[u[k]]) : 0);
277
+ cur.tin += g('in'); cur.tout += g('out'); cur.tcache += g('cacheRead') + g('cacheCreate');
278
+ if (u.ctxUsed) {
279
+ const vars = {};
280
+ for (const k of EXPR_FIELDS) vars[k] = g(k);
281
+ const ctxNow = evalExpr(u.ctxUsed, vars);
282
+ if (ctxNow > 0) cur.ctxUsed = ctxNow;
283
+ }
284
+ cur.calls++;
285
+ }
286
+ }
287
+ // 模型名与 usage 同守卫(去重命中时不重复收);一律过 modelName 强制字符串化
288
+ if (r.model && !seenMid) {
289
+ const mn = modelName(firstOf(j, r.model.path));
290
+ if (mn && !cur.models.includes(mn)) cur.models.push(mn);
291
+ }
292
+ if (ts) cur.lastTs = ts;
293
+ }
294
+ if (r.tools && type === r.tools.on && data.turns.length) {
295
+ const cur = data.turns[data.turns.length - 1];
296
+ for (const b of evalPath(j, r.tools.count)) {
297
+ cur.tools++;
298
+ const nm = b && typeof b === 'object' && typeof b.name === 'string' && b.name ? b.name : '?';
299
+ cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
300
+ }
301
+ }
302
+ if (r.turnClose && type === r.turnClose.on && data.turns.length) {
303
+ const cur = data.turns[data.turns.length - 1];
304
+ const fv = firstOf(j, r.turnClose.setFlag);
305
+ if (fv) cur.err = true;
306
+ }
307
+ }
308
+
309
+ function emitGenericTurns(agent, fp, projFallback, data, fromIdx) {
310
+ const idBase = 'g#' + fp.replace(/[\\/:]/g, '~') + '#';
311
+ for (let i = fromIdx; i < data.turns.length; i++) {
312
+ const t = data.turns[i];
313
+ const total = t.tin + t.tout + t.tcache;
314
+ addEntry(idBase + t.idx, {
315
+ agent, project: data.cwd || projFallback, session: path.basename(fp).replace(/\.[^.]+$/, ''),
316
+ time: t.time, dur: Math.max(0, t.lastTs - t.time), status: t.err ? 'error' : 'ok',
317
+ tin: t.tin, tout: t.tout, tcache: t.tcache, total,
318
+ ctx: windowOf(t.models), ctxUsed: t.ctxUsed || null,
319
+ rounds: t.calls, tools: t.tools, calls: t.calls,
320
+ models: modelList(t.models), preview: t.preview,
321
+ // 同 claude:转录没有「轮结束」标志,后面还有轮 = 确定结束;最后一轮由页面按最后活动时间兜底
322
+ finished: i < data.turns.length - 1,
323
+ toolNames: t.toolNames || {},
324
+ }, { file: fp, turn: t.idx, kind: 'generic-jsonl' });
325
+ }
326
+ }
327
+
328
+ // onlyFile:分片首扫用(与 scanClaude 同参语义);不传则整段同步扫完。
329
+ export function scanGeneric(agent, conf, root, onlyFile) {
330
+ const compiled = compiledOf(agent, conf);
331
+ const sniff = conf.sniff || {};
332
+ const dirLayout = sniff.dirLayout === 'flat' ? 'flat' : 'claude'; // 认不出的一律按 claude 摆法
333
+ const re = globToRe(sniff.fileGlob) || /\.jsonl$/i;
334
+ let groups = []; // [{dir, slug, names}]:claude 摆法 slug 目录若干;flat 摆法就一层
335
+ if (dirLayout === 'flat') {
336
+ const names = listDirCached(root);
337
+ if (names) groups.push({ dir: root, slug: path.basename(root), names: names.filter(n => re.test(n)) });
338
+ } else {
339
+ const slugs = listDirCached(root);
340
+ if (slugs) {
341
+ for (const slug of slugs) {
342
+ const dir = path.join(root, slug);
343
+ if (!isDir(dir)) continue;
344
+ const names = listDirCached(dir);
345
+ if (names) groups.push({ dir, slug, names: names.filter(n => re.test(n)) });
346
+ }
347
+ }
348
+ }
349
+ const onlyDir = onlyFile ? path.dirname(onlyFile) : null;
350
+ for (const g of groups) {
351
+ if (onlyDir && g.dir !== onlyDir) continue;
352
+ for (const n of g.names) {
353
+ const fp = path.join(g.dir, n);
354
+ if (onlyFile && fp !== onlyFile) continue;
355
+ let fst; try { fst = fs.statSync(fp); } catch { continue; }
356
+ const key = fp, m = fst.mtimeMs, s = fst.size;
357
+ const prev = files.get(key);
358
+ if (prev && prev.m === m && prev.s === s) {
359
+ // 重启后 entries 为空但 files 状态被恢复:从状态直接重建条目,不重扫文件(同 scanClaude)
360
+ if (prev.data?.turns?.length && !entriesHasTurn(prev, fp)) emitGenericTurns(agent, fp, g.slug, prev.data, 0);
361
+ continue;
362
+ }
363
+ // 口径守卫:恢复出来的旧状态若不是当前 rules 版本(理论上 loadIndex 已挡),这里再兜一道
364
+ const usable = prev && prev.rr === conf.rulesRev && prev.rh === conf.rulesHash;
365
+ const off = usable ? prev.off : 0;
366
+ const data = usable ? prev.data : { cwd: '', turns: [], emitted: 0 };
367
+ if (s <= off) {
368
+ files.set(key, { agent, kind: 'generic-jsonl', m, s, off, rr: conf.rulesRev, rh: conf.rulesHash, data });
369
+ markKind('generic-jsonl'); continue;
370
+ }
371
+ try {
372
+ const rd = readCompleteLines(fp, off, s);
373
+ if (!rd) { // 尚无完整行(半行):只记状态不解析
374
+ files.set(key, { agent, kind: 'generic-jsonl', m, s, off, rr: conf.rulesRev, rh: conf.rulesHash, data });
375
+ markKind('generic-jsonl'); continue;
376
+ }
377
+ const newOff = rd.newOff;
378
+ for (const line of rd.lines) {
379
+ if (!line.trim()) continue;
380
+ let j; try { j = JSON.parse(line); } catch { continue; }
381
+ processLine(j, compiled, data);
382
+ }
383
+ files.set(key, { agent, kind: 'generic-jsonl', m, s, off: newOff, rr: conf.rulesRev, rh: conf.rulesHash, data });
384
+ markKind('generic-jsonl');
385
+ emitGenericTurns(agent, fp, g.slug, data, Math.max(0, data.emitted - 1));
386
+ data.emitted = data.turns.length;
387
+ } catch {}
388
+ }
389
+ }
390
+ }
391
+ function entriesHasTurn(prev, fp) {
392
+ const idBase = 'g#' + fp.replace(/[\\/:]/g, '~') + '#';
393
+ return entries.has(idBase + (prev.data.turns.length - 1));
394
+ }
395
+
396
+ // ---------------- 详情(entryContent 的 generic-jsonl 分支) ----------------
397
+ // 用同一套 rules 重读转录文件重建某一轮的正文。能力声明(与受限边界一致):
398
+ // user / assistant 正文、calls(按 dedupeBy 归并)、tools 入参都给;工具**输出**回填需要
399
+ // 跨行配对规则(tool_result → tool_use_id),声明式 schema 里没有 —— 留空,不硬编。
400
+ export function genericEntryContent(fp, turn, full, conf) {
401
+ const compiled = { conf };
402
+ const r = conf.rules || {};
403
+ const to = r.turnOpen || {};
404
+ const out = { user: '', assistant: '', tools: [], calls: [], v: null };
405
+ const callByMid = new Map(); // mid -> calls[] 项(同 claude:正文/工具块常不在带 usage 的首条上)
406
+ const textByMid = new Map();
407
+ const texts = [];
408
+ let turnIdx = -1, prevTs = 0, anon = 0;
409
+ let lines;
410
+ try { lines = fs.readFileSync(fp, 'utf8').split('\n'); } catch { return out; }
411
+ for (const line of lines) {
412
+ if (!line.trim()) continue;
413
+ let j; try { j = JSON.parse(line); } catch { continue; }
414
+ const type = j.type;
415
+ if (type === to.lineType) {
416
+ const text = textFromPath(j, to.textPath);
417
+ if (!text) continue;
418
+ const t0 = String(text).trimStart();
419
+ if ((to.skipIf || []).some(p => t0.startsWith(p))) continue;
420
+ turnIdx++;
421
+ if (turnIdx > turn) break;
422
+ if (turnIdx === turn) {
423
+ out.user = text;
424
+ prevTs = r.ts ? tsOf(firstOf(j, r.ts.path)) : 0;
425
+ }
426
+ continue;
427
+ }
428
+ if (turnIdx !== turn) continue;
429
+ const u = r.usage;
430
+ const modelOn = (r.model && r.model.on) || (u && u.on);
431
+ const assistantLike = (u && type === u.on) || (!u && modelOn && type === modelOn);
432
+ if (assistantLike) {
433
+ const ts = r.ts ? tsOf(firstOf(j, r.ts.path)) : 0;
434
+ const midRaw = u && u.dedupeBy ? firstOf(j, u.dedupeBy) : undefined;
435
+ const mid = midRaw == null || midRaw === '' ? '#' + (anon++) : String(midRaw);
436
+ if (!callByMid.has(mid)) {
437
+ let uv = u && u.path ? (firstOf(j, u.path) || {}) : {};
438
+ if (!(uv && typeof uv === 'object')) uv = {};
439
+ const g = k => (u && u[k] != null ? num(uv[u[k]]) : 0);
440
+ const call = {
441
+ model: r.model ? (modelName(firstOf(j, r.model.path)) || '') : '',
442
+ tin: g('in'), tout: g('out'), tcache: g('cacheRead') + g('cacheCreate'),
443
+ dur: ts && prevTs ? Math.max(0, ts - prevTs) : 0,
444
+ };
445
+ callByMid.set(mid, call);
446
+ out.calls.push(call);
447
+ if (ts) prevTs = ts;
448
+ }
449
+ if (r.assistantText && type === r.assistantText.on) {
450
+ for (const seg of evalPath(j, r.assistantText.path)) {
451
+ const s0 = typeof seg === 'string' ? seg : toText(seg);
452
+ if (!s0) continue;
453
+ texts.push(s0);
454
+ textByMid.set(mid, textByMid.has(mid) ? textByMid.get(mid) + '\n' + s0 : s0);
455
+ }
456
+ }
457
+ }
458
+ if (r.tools && type === r.tools.on) {
459
+ for (const b of evalPath(j, r.tools.count)) {
460
+ out.tools.push({
461
+ name: b && typeof b.name === 'string' && b.name ? b.name : '?',
462
+ tid: b && b.id ? b.id : null,
463
+ input: truncOut(typeof b === 'object' && b ? JSON.stringify(b.input ?? '') : '', full),
464
+ inputTrunc: false, output: '', error: null,
465
+ });
466
+ }
467
+ }
468
+ }
469
+ for (const [mid, call] of callByMid) {
470
+ const seg = textByMid.get(mid);
471
+ if (seg) call.text = truncOut(seg, full);
472
+ }
473
+ out.assistant = texts.join('\n');
474
+ return out;
475
+ }
476
+ // 详情正文截断(800 字)——与 trunc 同口径但没有 shared 的 flag 对象接线,就地实现
477
+ function truncOut(s, full) {
478
+ s = toText(s);
479
+ if (full || s.length <= 800) return s;
480
+ return s.slice(0, 800);
481
+ }
482
+
483
+ // I8 复现包原始片段提取复用:与 processLine 的 turnOpen 分支完全同口径(lineType 命中 &&
484
+ // 正文非空 && 不以 skipIf 任一前缀开头)。rules 即 agent 的 rules 对象(conf.rules)。
485
+ export function genericTurnStart(j, rules) {
486
+ const r = rules || {};
487
+ const to = r.turnOpen || {};
488
+ if ((j.type || '') !== to.lineType) return false;
489
+ const text = textFromPath(j, to.textPath);
490
+ if (!text) return false;
491
+ const t0 = String(text).trimStart();
492
+ if ((to.skipIf || []).some(p => t0.startsWith(p))) return false;
493
+ return true;
494
+ }