@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.
- package/LICENSE +21 -0
- package/LOGFORMATS.md +960 -0
- package/README.md +238 -0
- package/REFERENCE.md +1428 -0
- package/agent-acta-page.html +1998 -0
- package/agent-acta-server.mjs +1066 -0
- package/agent-acta-widget.html +724 -0
- package/archive.mjs +335 -0
- package/core/log.mjs +20 -0
- package/core/service.mjs +4285 -0
- package/hooks/cursor-usage-hook.mjs +89 -0
- package/mcp-server.mjs +372 -0
- package/package.json +68 -0
- package/page/dialog-add-agent.js +65 -0
- package/page/dialog-alert.js +159 -0
- package/page/dialog-archive.js +342 -0
- package/page/dialog-compare.js +238 -0
- package/page/dialog-ctxw.js +101 -0
- package/page/dialog-diag.js +72 -0
- package/page/dialog-heatmap.js +215 -0
- package/page/dialog-selftest.js +202 -0
- package/page/dialog-sess-timeline.js +108 -0
- package/page/dialog-stats.js +694 -0
- package/page/dialog-usage.js +158 -0
- package/page/log-card.js +277 -0
- package/page/log-view.js +273 -0
- package/page/sess-view.js +382 -0
- package/page/shared.js +408 -0
- package/page/sidebar.js +129 -0
- package/page/style.css +973 -0
- package/page/topbar.js +61 -0
- package/parsers/atomcode.mjs +321 -0
- package/parsers/buddy.mjs +272 -0
- package/parsers/buddyext.mjs +582 -0
- package/parsers/claude.mjs +496 -0
- package/parsers/cline.mjs +468 -0
- package/parsers/codearts.mjs +96 -0
- package/parsers/codex.mjs +188 -0
- package/parsers/comate.mjs +195 -0
- package/parsers/copilot.mjs +308 -0
- package/parsers/cursor.mjs +493 -0
- package/parsers/devindb.mjs +358 -0
- package/parsers/discovery.mjs +1230 -0
- package/parsers/doubao.mjs +406 -0
- package/parsers/dsh.mjs +377 -0
- package/parsers/gemini.mjs +267 -0
- package/parsers/generic.mjs +494 -0
- package/parsers/hermes.mjs +398 -0
- package/parsers/kilo.mjs +42 -0
- package/parsers/kimi.mjs +249 -0
- package/parsers/mimocode.mjs +531 -0
- package/parsers/minimax.mjs +503 -0
- package/parsers/openclaw.mjs +490 -0
- package/parsers/opencode.mjs +465 -0
- package/parsers/shared.mjs +418 -0
- package/parsers/trace.mjs +66 -0
- package/parsers/tracecode.mjs +348 -0
- package/parsers/traedb.mjs +528 -0
- package/parsers/zcode.mjs +356 -0
- package/plugin/client.js +51 -0
- package/plugin/cordis.patch.yml +5 -0
- package/plugin/plugin.mjs +220 -0
- package/search.mjs +554 -0
- package/tools/grab-trae-key.ps1 +265 -0
- package/vendor/element-plus-icons.js +3 -0
- package/vendor/element-plus.css +1 -0
- package/vendor/element-plus.dark.css +1 -0
- package/vendor/element-plus.full.min.js +65 -0
- package/vendor/element-plus.zh-cn.js +2 -0
- package/vendor/svg/antgroup-color.svg +1 -0
- package/vendor/svg/bytedance-color.svg +1 -0
- package/vendor/svg/chatglm-color.svg +1 -0
- package/vendor/svg/claude-color.svg +1 -0
- package/vendor/svg/claudecode-color.svg +1 -0
- package/vendor/svg/cline.svg +1 -0
- package/vendor/svg/codebuddy-color.svg +1 -0
- package/vendor/svg/codebuddy.svg +1 -0
- package/vendor/svg/codex-color.svg +1 -0
- package/vendor/svg/copilot.svg +1 -0
- package/vendor/svg/cursor.svg +1 -0
- package/vendor/svg/deepseek-color.svg +1 -0
- package/vendor/svg/deepseek-harness-whale.svg +8 -0
- package/vendor/svg/doubao-color.svg +1 -0
- package/vendor/svg/gemini-color.svg +1 -0
- package/vendor/svg/glmv-color.svg +1 -0
- package/vendor/svg/grok.svg +1 -0
- package/vendor/svg/hermesagent.svg +1 -0
- package/vendor/svg/huawei-color.svg +1 -0
- package/vendor/svg/hunyuan-color.svg +1 -0
- package/vendor/svg/kilocode-avatar.svg +1 -0
- package/vendor/svg/kilocode.svg +1 -0
- package/vendor/svg/kimi-color.svg +1 -0
- package/vendor/svg/kimi.svg +1 -0
- package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
- package/vendor/svg/logo-AtomGit-dark.svg +4 -0
- package/vendor/svg/logo-AtomGit-light.svg +4 -0
- package/vendor/svg/meta-color.svg +1 -0
- package/vendor/svg/mimo.svg +66 -0
- package/vendor/svg/minimax-code-logo.svg +8 -0
- package/vendor/svg/minimax-color.svg +1 -0
- package/vendor/svg/moonshot.svg +1 -0
- package/vendor/svg/nex-color.svg +1 -0
- package/vendor/svg/nex.svg +1 -0
- package/vendor/svg/nvidia-color.svg +1 -0
- package/vendor/svg/openai.svg +1 -0
- package/vendor/svg/openclaw-color.svg +1 -0
- package/vendor/svg/openclaw.svg +1 -0
- package/vendor/svg/opencode.svg +1 -0
- package/vendor/svg/openrouter-color.svg +1 -0
- package/vendor/svg/qoder-color.svg +1 -0
- package/vendor/svg/qwen-color.svg +1 -0
- package/vendor/svg/sensenova-color.svg +1 -0
- package/vendor/svg/stepfun.svg +1 -0
- package/vendor/svg/trae-color.svg +1 -0
- package/vendor/svg/traework-v2.svg +1 -0
- package/vendor/svg/wenxin-color.svg +1 -0
- package/vendor/svg/windsurf.svg +1 -0
- package/vendor/svg/workbuddy-color.svg +29 -0
- package/vendor/svg/xiaomimimo.svg +1 -0
- package/vendor/svg/yuanbao-color.svg +1 -0
- package/vendor/svg/yuanbao.svg +1 -0
- package/vendor/svg/zai.svg +1 -0
- package/vendor/svg/zhipu-color.svg +1 -0
- package/vendor/vue.global.prod.js +9 -0
- package/widget/main.js +202 -0
- package/widget/package.json +7 -0
- package/widget/preload.js +11 -0
package/parsers/kimi.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ---------------- kimi 解析器(~/.kimi-code 与 ~/.kimi 两个变体,wire.jsonl 事件流) ----------------
|
|
2
|
+
// 从 agent-acta-server.mjs 拆出(纯搬家,逻辑零改动)。导出 scanKimiFile / scanKimi /
|
|
3
|
+
// kimiEntryContent / stripKimiMeta(详情与扫描共用同一套口径函数)。
|
|
4
|
+
//
|
|
5
|
+
// 目录摆法(isKimiWire / kimiWireIn / kimiWires)在主文件的 discovery.mjs——嗅探与扫描共用,
|
|
6
|
+
// 这里只 import kimiWires。
|
|
7
|
+
// wire.jsonl 事件流(本机 kimi-code 0.10.1 实测):
|
|
8
|
+
// metadata(协议版本)→ turn.prompt(用户开轮,input[].text + time 毫秒)
|
|
9
|
+
// → context.append_loop_event(event.type = step.begin / tool.call / tool.result / text / think)
|
|
10
|
+
// → usage.record(usageScope=turn,model + usage{inputOther,output,inputCacheRead,inputCacheCreation})
|
|
11
|
+
// → turn.cancel(用户中断收轮)
|
|
12
|
+
// 一轮 = turn.prompt 到下一个 turn.prompt(或文件尾)。token 只认 usageScope=turn 的 usage.record(每轮最后一条是轮级汇总口径)。
|
|
13
|
+
//
|
|
14
|
+
// 已知的版本差异(本机 866 条 usage.record 全是 turn 域、model 就是真模型名,故按此实现;
|
|
15
|
+
// 若将来某版本把 model 写成别名(__xxx_model__)、或出现只带 usageScope=session 的压缩调用,
|
|
16
|
+
// 症状是「模型列显示占位名 / 少算一块 token」,对着 wire.jsonl 核一遍即可,排查入口在这);
|
|
17
|
+
// 增量:文件按 off 续读(OFF_KINDS 已含 kimi),turn 按 idx 重发(进行中的轮原地更新)。
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { files, entries, addEntry, modelName, windowOf, markKind, toText, trunc, readCompleteLines } from './shared.mjs';
|
|
21
|
+
import { kimiWires } from './discovery.mjs';
|
|
22
|
+
|
|
23
|
+
export function kimiIdBase(fp) { return 'k#' + fp.replace(/[\\/:]/g, '~') + '#'; }
|
|
24
|
+
|
|
25
|
+
// 会话级「chrome」事件:跟「这一轮干了什么」无关,只是会话/工具注册表的状态。
|
|
26
|
+
// 一律跳过,**不能**让它们参与 lastTs 累加 —— 桌面版(daimon 内核)每一轮开始前都会把整段
|
|
27
|
+
// 重放一遍(本机实测第 33–52 行:config.update ×3、tools.set_active_tools ×2、
|
|
28
|
+
// tools.register_user_tool ×13、permission.set_mode ×1),而它们的时间戳是**下一轮的**。
|
|
29
|
+
// 落在上一轮的尾巴上,就把「第一轮跑了多久」从 4.6 秒拉成了 30.6 分钟(03:58:12 → 04:28:45)。
|
|
30
|
+
// CLI 版没这么明显(16 个文件里只有 6 处 chrome 落在轮中),所以这个坑只有接了桌面版才暴露。
|
|
31
|
+
const KIMI_CHROME = new Set(['metadata', 'config.update', 'tools.set_active_tools', 'tools.register_user_tool', 'permission.set_mode']);
|
|
32
|
+
|
|
33
|
+
// 桌面版 Work 模式会在用户输入前面注入一段 `<meta awareness="low" timestamp="…" />`:
|
|
34
|
+
// turn.prompt 的 input = [{"type":"text","text":"<meta awareness=\"low\" … />\n真正的问题"}]
|
|
35
|
+
// 不剥掉的话,卡片预览和详情里的「用户输入」显示的就是这行注入物,而不是用户问的话。
|
|
36
|
+
// 只在**最开头**剥一次,且只认这一种形状 —— 用户自己打一个 `<meta>` 开头的消息不受影响(概率极低,
|
|
37
|
+
// 且真遇到了也只是少显示一个标签)。
|
|
38
|
+
const KIMI_META = /^\s*<meta\b[^>]*>\s*/i;
|
|
39
|
+
export const stripKimiMeta = s => String(s || '').replace(KIMI_META, '');
|
|
40
|
+
|
|
41
|
+
// 从一条事件里挖出工作目录:工具调用入参里的 "cwd"(shell 工具跑在哪儿就是项目在哪)。
|
|
42
|
+
//
|
|
43
|
+
// 这是**给没有 session_index.jsonl 的变体兜底的**:kimi-code 的项目名来自
|
|
44
|
+
// ~/.kimi-code/session_index.jsonl(slug → workDir,权威),而 Kimi CLI 那套
|
|
45
|
+
// ~/.kimi/sessions/<md5(workdir)>/<uuid>/ 的目录名是**哈希**,反推不出来,索引文件也没有。
|
|
46
|
+
// 兜底只在索引没给值时生效,不会覆盖权威值。
|
|
47
|
+
//
|
|
48
|
+
// 准不准:拿本机 16 个 kimi-code 会话对过账(那批有索引当标准答案)——抓到 cwd 的 9 个**全部一致**,
|
|
49
|
+
// 另外 7 个压根没有带 cwd 的工具调用(没跑过 shell),抓不到就留空、退回 slug,不会编一个出来。
|
|
50
|
+
// 深度限 6 层:tool.call 的入参嵌在 event → args 里,再深就不是我们要的东西了。
|
|
51
|
+
function findCwd(o, depth = 0) {
|
|
52
|
+
if (!o || typeof o !== 'object' || depth > 6) return '';
|
|
53
|
+
if (typeof o.cwd === 'string' && o.cwd.trim()) return o.cwd.trim();
|
|
54
|
+
for (const v of Object.values(o)) {
|
|
55
|
+
if (v && typeof v === 'object') { const r = findCwd(v, depth + 1); if (r) return r; }
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function emitKimiTurns(agent, fp, data, fromIdx) {
|
|
61
|
+
const idBase = kimiIdBase(fp);
|
|
62
|
+
for (let i = fromIdx; i < data.turns.length; i++) {
|
|
63
|
+
const t = data.turns[i];
|
|
64
|
+
addEntry(idBase + t.idx, {
|
|
65
|
+
// 项目名三档:索引给的(权威)> 从工具调用挖的(兜底,见 findCwd)> 目录名
|
|
66
|
+
agent, project: data.cwdIdx || data.cwdGuess || data.slug || '(未知项目)', session: data.sessionId || data.sessName || '',
|
|
67
|
+
time: t.time, dur: Math.max(0, t.lastTs - t.time), status: t.err ? 'error' : 'ok',
|
|
68
|
+
tin: t.tin, tout: t.tout, tcache: t.tcache, total: t.tin + t.tout + t.tcache,
|
|
69
|
+
rounds: t.calls, tools: t.tools, calls: t.calls,
|
|
70
|
+
models: t.models, preview: t.preview,
|
|
71
|
+
// 上下文占用 = 本轮**最后一次**调用的 prompt 全量(与 claude 同义),不是轮内累加;
|
|
72
|
+
// 窗口**容量** kimi 这边没有(wire.jsonl 里只有用量没有窗口),只能按模型名查表(见
|
|
73
|
+
// CTX_WINDOW_KINDS):表里查不到就是 0,页面只显示占用、不画进度条 —— 与页面那段
|
|
74
|
+
// 「宁可少一根条,也不要用一个假分母凑出一根像真的的条」一致。
|
|
75
|
+
// 注意别留 undefined:页面 ctxUsed 取不到时会退回 e.total(轮内累加,跑多轮能到千万级),
|
|
76
|
+
// 那是给「自带窗口的 agent」用的近似,套到 kimi 上会显示一个虚高的占用。
|
|
77
|
+
ctx: windowOf(t.models), ctxUsed: t.ctxUsed || null,
|
|
78
|
+
finished: !!t.finished, aborted: !!t.aborted, toolNames: t.toolNames || {},
|
|
79
|
+
}, { file: fp, turn: t.idx, kind: 'kimi' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 项目名有两个来源,**分开存**:cwdIdx 来自 session_index.jsonl(权威),cwdGuess 从工具调用里挖
|
|
84
|
+
// (兜底,见 findCwd)。分开放是因为它们到得晚——索引晚于文件读到、工具调用要读到那行才有——
|
|
85
|
+
// 合用一个字段的话先到的那档会把后到的权威值挡在外面(`if (!data.cwd)` 那句就是干这个的)。
|
|
86
|
+
// state.json 帮不上忙:它只有 createdAt/title/lastPrompt,没有 workDir。
|
|
87
|
+
// sessName / slug 由调用方传入(就是会话目录名与它上一层):以前从 fp 往上数三层推,
|
|
88
|
+
// 只对 kimi-code 的 <slug>/<session>/agents/main/wire.jsonl 成立;Kimi CLI 的
|
|
89
|
+
// <hash>/<uuid>/wire.jsonl 少一层,会推成 hash 那一层的名字。
|
|
90
|
+
export function scanKimiFile(agent, fp, cwd, sessName, slug) {
|
|
91
|
+
let fst; try { fst = fs.statSync(fp); } catch { return false; }
|
|
92
|
+
const key = fp, m = fst.mtimeMs, s = fst.size;
|
|
93
|
+
const prev = files.get(key);
|
|
94
|
+
if (prev && prev.m === m && prev.s === s) {
|
|
95
|
+
if (prev.data?.turns?.length && !entries.has(kimiIdBase(fp) + (prev.data.turns.length - 1))) emitKimiTurns(agent, fp, prev.data, 0);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
const off = prev?.off || 0;
|
|
99
|
+
const data = prev?.data || { sessionId: '', cwdIdx: cwd || '', cwdGuess: '', slug: slug || '', sessName: sessName || '', turns: [], emitted: 0 };
|
|
100
|
+
if (!data.cwdIdx && cwd) data.cwdIdx = cwd; // 索引值后到也要补上(它比兜底值优先)
|
|
101
|
+
if (!data.slug && slug) data.slug = slug;
|
|
102
|
+
if (!data.sessName && sessName) data.sessName = sessName;
|
|
103
|
+
if (s <= off) { files.set(key, { agent, kind: 'kimi', m, s, off, data }); markKind('kimi'); return true; }
|
|
104
|
+
try {
|
|
105
|
+
const rd = readCompleteLines(fp, off, s);
|
|
106
|
+
if (!rd) { files.set(key, { agent, kind: 'kimi', m, s, off, data }); markKind('kimi'); return true; }
|
|
107
|
+
const newOff = rd.newOff;
|
|
108
|
+
for (const line of rd.lines) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
|
111
|
+
const ts = typeof j.time === 'number' ? j.time : 0;
|
|
112
|
+
if (KIMI_CHROME.has(j.type)) continue; // 会话 chrome 与 metadata 同处理,见 KIMI_CHROME
|
|
113
|
+
// 项目名兜底:挖到了就存下(索引值优先,在 emit 那一步排序,这里不做取舍)
|
|
114
|
+
// 先过一遍 '"cwd"' 子串再深挖:绝大多数事件里没有这个键,省掉每行的对象树遍历
|
|
115
|
+
if (!data.cwdGuess && line.includes('"cwd"')) { const c = findCwd(j); if (c) data.cwdGuess = c; }
|
|
116
|
+
if (j.type === 'turn.prompt') {
|
|
117
|
+
const text0 = stripKimiMeta(toText(j.input)); // 去掉桌面版注入的 <meta …/>
|
|
118
|
+
data.turns.push({
|
|
119
|
+
idx: data.turns.length, time: ts, lastTs: ts,
|
|
120
|
+
tin: 0, tout: 0, tcache: 0, tools: 0, calls: 0, ctxUsed: 0,
|
|
121
|
+
models: [], mids: [], err: false, preview: (text0 || '').replace(/\s+/g, ' ').slice(0, 300), user: (text0 || '').slice(0, 3000),
|
|
122
|
+
toolNames: {}, finished: false, aborted: false,
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const cur = data.turns[data.turns.length - 1];
|
|
127
|
+
if (!cur) continue;
|
|
128
|
+
if (j.type === 'turn.cancel') { cur.aborted = true; cur.finished = true; if (ts) cur.lastTs = Math.max(cur.lastTs, ts); continue; }
|
|
129
|
+
if (j.type === 'usage.record' && j.usage && j.usageScope === 'turn') {
|
|
130
|
+
const u = j.usage;
|
|
131
|
+
const inNow = (u.inputOther || 0) + (u.input || 0);
|
|
132
|
+
cur.tin += inNow;
|
|
133
|
+
cur.tout += u.output || 0;
|
|
134
|
+
cur.tcache += (u.inputCacheRead || 0) + (u.inputCacheCreation || 0);
|
|
135
|
+
cur.calls++;
|
|
136
|
+
// 每次调用**覆盖**、轮内最后一次胜出 = 这一轮结束时的上下文占用(与 claude 同口径,不是累加)
|
|
137
|
+
const ctxNow = inNow + (u.inputCacheRead || 0) + (u.inputCacheCreation || 0);
|
|
138
|
+
if (ctxNow > 0) cur.ctxUsed = ctxNow;
|
|
139
|
+
const mdl = modelName(j.model);
|
|
140
|
+
if (mdl && !cur.models.includes(mdl)) cur.models.push(mdl);
|
|
141
|
+
if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (j.type === 'context.append_loop_event' && j.event) {
|
|
145
|
+
const ev = j.event;
|
|
146
|
+
if (ev.type === 'tool.call') {
|
|
147
|
+
cur.tools++;
|
|
148
|
+
const nm = typeof ev.name === 'string' && ev.name ? ev.name : (typeof ev.tool === 'string' && ev.tool ? ev.tool : '?');
|
|
149
|
+
cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
|
|
150
|
+
}
|
|
151
|
+
else if (ev.type === 'tool.result' && (ev.error || ev.result?.is_error)) cur.err = true;
|
|
152
|
+
if (typeof j.time === 'number') cur.lastTs = Math.max(cur.lastTs, j.time);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ts && ts > cur.lastTs) cur.lastTs = ts;
|
|
156
|
+
}
|
|
157
|
+
files.set(key, { agent, kind: 'kimi', m, s, off: newOff, data });
|
|
158
|
+
markKind('kimi');
|
|
159
|
+
emitKimiTurns(agent, fp, data, Math.max(0, data.emitted - 1));
|
|
160
|
+
data.emitted = data.turns.length;
|
|
161
|
+
} catch {}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function scanKimi(agent, root) {
|
|
166
|
+
// 顺带从 session_index.jsonl 补 cwd(slug → workDir 映射就在这里,一次读全量)
|
|
167
|
+
const cwdByDir = new Map();
|
|
168
|
+
try {
|
|
169
|
+
const idxFp = path.join(path.dirname(root), 'session_index.jsonl');
|
|
170
|
+
if (fs.existsSync(idxFp)) {
|
|
171
|
+
const lines = fs.readFileSync(idxFp, 'utf8').split('\n');
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (!line.trim()) continue;
|
|
174
|
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
|
175
|
+
if (j.sessionDir && j.workDir) cwdByDir.set(path.normalize(j.sessionDir), j.workDir);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
// 摆法交给 kimiWires():kimi-code 的 agents/main/wire.jsonl 与 Kimi CLI 的裸 wire.jsonl 都在这里被收进来
|
|
180
|
+
for (const { sessDir, fp: wire } of kimiWires(root)) {
|
|
181
|
+
// cwd / 会话名 / slug 都在进解析器前取好:emit 发生在 scanKimiFile 内部,事后补会漏掉首扫条目
|
|
182
|
+
scanKimiFile(agent, wire, cwdByDir.get(path.normalize(sessDir)) || '',
|
|
183
|
+
path.basename(sessDir), path.basename(path.dirname(sessDir)));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 详情:重读 wire.jsonl 按 src.turn 过滤。开轮(turn.prompt 计数)与扫描同一口径,
|
|
188
|
+
// 用户输入同过 stripKimiMeta —— 桌面版注入的 <meta …/> 在两边都不该出现。
|
|
189
|
+
export function kimiEntryContent(src, full) {
|
|
190
|
+
const fstate = files.get(src.file);
|
|
191
|
+
const version = fstate ? fstate.m + ':' + (fstate.off || 0) : null;
|
|
192
|
+
const lines = fs.readFileSync(src.file, 'utf8').split('\n');
|
|
193
|
+
let turnIdx = -1;
|
|
194
|
+
const out = { user: '', assistant: '', tools: [], calls: [], v: version };
|
|
195
|
+
const texts = new Map(); // uuid -> 正文片段(按出现顺序,见 content.part 那段注释)
|
|
196
|
+
const byCall = new Map(); // toolCallId -> tool(tool.result 精确回填)
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
if (!line.trim()) continue;
|
|
199
|
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
|
200
|
+
if (j.type === 'turn.prompt') {
|
|
201
|
+
turnIdx++;
|
|
202
|
+
if (turnIdx > src.turn) break;
|
|
203
|
+
// 与扫描同口径:桌面版注入的 <meta …/> 前缀在这里也要剥掉,否则详情里的「用户输入」是那行注入物
|
|
204
|
+
if (turnIdx === src.turn && !out.user) out.user = stripKimiMeta(toText(j.input));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (turnIdx !== src.turn) continue;
|
|
208
|
+
if (j.type === 'usage.record' && j.usage && j.usageScope === 'turn') {
|
|
209
|
+
const u = j.usage;
|
|
210
|
+
out.calls.push({ model: j.model || '', tin: (u.inputOther || 0) + (u.input || 0), tout: u.output || 0, tcache: (u.inputCacheRead || 0) + (u.inputCacheCreation || 0), dur: 0 });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (j.type !== 'context.append_loop_event' || !j.event) continue;
|
|
214
|
+
const ev = j.event;
|
|
215
|
+
if (ev.type === 'text') {
|
|
216
|
+
// 老写法(原样保留):正文直接挂在 event 上
|
|
217
|
+
const t0 = toText(ev.text ?? ev.content);
|
|
218
|
+
// 与下面 content.part 同一套键:texts 是 Map(uuid 去重)。这里原为 texts.push —— 该分支
|
|
219
|
+
// 一命中就是 TypeError(Map 没有 push),老形状日志的详情会整个读失败
|
|
220
|
+
if (t0) texts.set(ev.uuid || ('#' + texts.size), t0);
|
|
221
|
+
} else if (ev.type === 'content.part' && ev.part) {
|
|
222
|
+
// **实际形状**(CLI protocol 1.3 与桌面版 1.4 实测一致):正文与思考都包在
|
|
223
|
+
// `event.part` 里,`part.type` 是 'text' | 'think'。原来只认 `ev.type === 'text'`,
|
|
224
|
+
// 而 ev.type 永远是 'content.part' —— 那个分支从接入起就没命中过,所以 kimi 的详情
|
|
225
|
+
// 一直**没有助手正文**(卡片能出数字、能出用户输入,唯独正文那一段是空的)。
|
|
226
|
+
// 这里只取正文;think 是模型推理过程,与其它 agent 一样不落详情。
|
|
227
|
+
// 按 uuid 去重:实测两个版本都不重复写,但万一将来改成流式覆盖写,直接 push 会拼出重复正文。
|
|
228
|
+
if (ev.part.type === 'text') {
|
|
229
|
+
const t0 = toText(ev.part.text ?? ev.part.content);
|
|
230
|
+
if (t0) texts.set(ev.uuid || ('#' + texts.size), t0);
|
|
231
|
+
}
|
|
232
|
+
} else if (ev.type === 'tool.call') {
|
|
233
|
+
const fi = {};
|
|
234
|
+
const t = { name: ev.name || '?', tid: ev.toolCallId || null, input: trunc(ev.args ?? ev.arguments, full, fi), inputTrunc: !!fi.t, output: '', error: null };
|
|
235
|
+
out.tools.push(t);
|
|
236
|
+
if (t.tid) byCall.set(t.tid, t);
|
|
237
|
+
} else if (ev.type === 'tool.result') {
|
|
238
|
+
const t = (ev.toolCallId && byCall.get(ev.toolCallId)) || out.tools.find(x => !x.output);
|
|
239
|
+
if (t) {
|
|
240
|
+
const fo = {};
|
|
241
|
+
t.output = trunc(ev.result ?? ev.output, full, fo);
|
|
242
|
+
t.outputTrunc = !!fo.t;
|
|
243
|
+
t.error = !!(ev.error || ev.result?.is_error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
out.assistant = [...texts.values()].join('\n\n---\n\n');
|
|
248
|
+
return out;
|
|
249
|
+
}
|