@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
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// ---------------- cline 解析器(Cline CLI 3.x:~/.cline/data/sessions/<会话 id>/<会话 id>.messages.json) ----------------
|
|
2
|
+
// 真机口径(2026-09-24 用本机 CLI 3.0.64 的活会话逐字段核过,不是照社区文档写的)。Cline 家目录 `~/.cline/`:
|
|
3
|
+
// <data>/sessions/<sid>/<sid>.messages.json ← **主源**(本文件只读它 + 下面的清单)
|
|
4
|
+
// <data>/sessions/<sid>/<sid>.json ← 清单:cwd / model / provider / status / metadata.title
|
|
5
|
+
// <data>/sessions/<sid>/<sid>.compaction.json ← 压缩快照(**不读**,理由见下)
|
|
6
|
+
// <data>/db/sessions.db ← 会话索引(字段与清单重复,**不读**)
|
|
7
|
+
// <data>/settings/providers.json ← ⚠️ 明文 apiKey,**一个字节都不许读**(同 secrets.json / globalState.json)
|
|
8
|
+
// 本机 Windows 的 data 根就是 `~/.cline/data`;`CLINE_SESSION_DATA_DIR` 可整体改写会话根(discovery 认这个变量)。
|
|
9
|
+
//
|
|
10
|
+
// **整份 JSON,不是 JSONL** —— `{version, updated_at, agent, sessionId, origin, messages[], system_prompt}`,
|
|
11
|
+
// 会话进行中每次落盘都是**整份重写**(实测 33 → 37 → 39 条,`updated_at` 跟着变)。所以没有字节偏移可续读,
|
|
12
|
+
// 与 hermes / opencode / dsh 同一路数:按 `{mtime,size}` 签名整份重解析,签名没变就跳过。
|
|
13
|
+
// ⇒ **不进 OFF_KINDS**(本 kind 的 files 状态根本不落 index,重启即整份重扫);
|
|
14
|
+
// `PARSER_REV.cline` 仍然登记,它在这里管的是 `~/.agent-acta/search/` 那份全文分片的作废。
|
|
15
|
+
// 签名带清单:清单里的 `status`/`metadata.title` 变了(换标题、会话结束)也要重解析,否则 name 会长期是旧的。
|
|
16
|
+
//
|
|
17
|
+
// 消息形状(messages[] 元素):
|
|
18
|
+
// {id, role, content[], ts(毫秒), // user 与 assistant 都有
|
|
19
|
+
// modelInfo:{id, provider}, metrics:{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens}} // 只有 assistant 有
|
|
20
|
+
// content 块只有三种:{type:"text",text} / {type:"tool_use",id,name,input(对象)} / {type:"tool_result",tool_use_id,name,content[]}
|
|
21
|
+
//
|
|
22
|
+
// ⚠️ 两个会把解析器写废的形状陷阱:
|
|
23
|
+
// 1. **tool_result 也是 `role:"user"`** —— 切轮只看 role 的话,本机这一份 39 条的会话会被切成 23 轮
|
|
24
|
+
// (真实只有 8 轮)。开轮判据必须是「role==="user" **且** content 里有非空 text 块」,
|
|
25
|
+
// 扫描侧与详情侧共用 `clineUserInput()` 这一个函数,否则「扫到第 N 轮」与「详情第 N 轮」会错位。
|
|
26
|
+
// 2. 用户正文被产品自己的标签包着:`<user_input mode="act">…</user_input>`(切模式时另有
|
|
27
|
+
// `<mode_notice>…</mode_notice>`)—— 不剥壳,卡片预览就是一串标签,搜索也搜不到正文。
|
|
28
|
+
//
|
|
29
|
+
// ⚠️ token 口径与 openclaw / minimax **相反**:这里的 `inputTokens` 就是**整次请求的 prompt 总量,
|
|
30
|
+
// cacheRead/cacheWrite 是它的子集**,不是「不含 cache」的净输入。三条独立证据(本机实测):
|
|
31
|
+
// · 二进制里的用量归一化(cline.exe 内 `inputTokens: e8(X,"inputTokens","total") || t8(X,…,"prompt_tokens")`、
|
|
32
|
+
// `cacheReadTokens: … || e8(X,"prompt_tokens_details","cached_tokens")`)—— OpenAI 语义下
|
|
33
|
+
// cached_tokens 本来就是 prompt_tokens 的子集;
|
|
34
|
+
// · 同一份二进制里的成本函数按 `inputTokens - cacheReadTokens - cacheWriteTokens` 才当未命中缓存的输入计价;
|
|
35
|
+
// · 实测逐次增量:第 2 次调用若按「input 不含 cache」解释,prompt 会从 8378 变成下一次的 7868(**负增长**),
|
|
36
|
+
// 而按「input 就是 prompt 总量」解释,相邻差值 633/2093/… 恰好对上中间那些消息的字符数。
|
|
37
|
+
// ⇒ tin = max(0, inputTokens − cacheReadTokens − cacheWriteTokens)(净输入),tcache = 两个 cache 桶之和,
|
|
38
|
+
// total = tin + tout + tcache = inputTokens + outputTokens(与产品自己的口径一致);
|
|
39
|
+
// ctxUsed = 轮内**最后一次**调用的 inputTokens。逐条相加 == 清单/库里的会话级 usage(实测 440706/8187/365056/0 全等)。
|
|
40
|
+
// ⚠️ 另因为此:**inputTokens 同会话内不单调**(/compact 之后第一次请求从 43662 掉到 19526)。它既不是累计值、
|
|
41
|
+
// 也不能拿相邻差值推轮边界 —— 轮边界只用上面那条 role+text 判据。
|
|
42
|
+
//
|
|
43
|
+
// 轮口径:user(text 非空) 开轮 → 轮内多条 assistant(工具循环)聚合成一条 entry → 下一条 user 收轮。
|
|
44
|
+
// time = 该 user 消息的 ts;dur = 轮内最后一条消息的 ts − time(tool_result 也带 ts,参与 lastTs)。
|
|
45
|
+
//
|
|
46
|
+
// 失败口径(这是本 kind 唯一的失败信号,源头给的就这一处):
|
|
47
|
+
// tool_result 的 content[] 是**逐项**结果 `{query, result, success, error?}`,批量调用里可以只有一条失败
|
|
48
|
+
// (本机实测 fetch 404/403 与 `Command exited with code 1`)。success===false 或有 error → 该工具标红 +
|
|
49
|
+
// 整轮 status=error(与 minimax / openclaw 的 isError 同口径),错误原文进详情 callsNote。
|
|
50
|
+
// ⚠️ **模型侧失败不落盘**:assistant 行没有 stopReason / errorMessage 这种字段,请求挂掉就是少一行,
|
|
51
|
+
// 所以没有「这一轮失败了」的通用判据 —— 认不出来的失败宁可不标,也不拿「缺 assistant」猜成失败。
|
|
52
|
+
// 同理:会话级才有 status(idle/running…)与 exit_code,**逐轮**没有状态、没有耗时字段、没有成本
|
|
53
|
+
// (本机第三方 provider 的 totalCost 恒 0)⇒ aborted 恒 false、calls[].dur 恒 0(逐工具耗时能算,见详情)。
|
|
54
|
+
// ctx(窗口容量分母)**一律 0**:模型目录(contextWindow / maxInputTokens)只嵌在 144MB 的 cline.exe 里,
|
|
55
|
+
// 盘上没有任何 JSON 版本(data/ 全目录 grep contextWindow 零命中)—— 不编分母,页面退化成只显示占用。
|
|
56
|
+
//
|
|
57
|
+
// **不读 `<sid>.compaction.json`**:/compact 是**纯投影、不是记账事件** —— 实测它写出快照(1 条合成的
|
|
58
|
+
// `Context summary:` user 消息 + 尾部原样搬运)之后,messages.json **一个字节都没改**、库里的 usage 也不变,
|
|
59
|
+
// 全量流水照旧单调追加。读它会把同一批消息数两遍(那份快照里连 metrics 都是复制的)。
|
|
60
|
+
// **不读 `data/db/sessions.db`**:它是清单的超集,字段全重;只有 lineage(parent_session_id / is_subagent /
|
|
61
|
+
// agent_id)是独有的 —— 将来要标子会话再从它取,现在不值得为多一个活 SQLite 源付每轮扫描的代价。
|
|
62
|
+
// **不读 `data/db/hub-events-*.db`**:那里的 iteration/tool 事件确实带逐次耗时,但它是 hub(teams)守护进程
|
|
63
|
+
// 的事件流水,会话不经过 hub 就没有行 —— 拿它当主源会让同一份会话有两种详略不同的形状。
|
|
64
|
+
import fs from 'node:fs';
|
|
65
|
+
import path from 'node:path';
|
|
66
|
+
import {
|
|
67
|
+
files, entries, srcs, removedIds, sorted, addEntry,
|
|
68
|
+
modelName, toText, trunc, listDirCached, isDir, isFile,
|
|
69
|
+
} from './shared.mjs';
|
|
70
|
+
|
|
71
|
+
export const clineScanErr = new Map();
|
|
72
|
+
// 会话根可被环境变量整体改写(CLI 自己认这个),discovery 的 candidateBases 要用
|
|
73
|
+
export const CLINE_SESSION_DATA_ENV = 'CLINE_SESSION_DATA_DIR';
|
|
74
|
+
|
|
75
|
+
// id 前缀 `cl#`:与 claude 的、codex 的 `x#`、openclaw 的 `oc#`、kilo 的 `kl#` 都不撞
|
|
76
|
+
export function clineIdBase(msgPath) { return 'cl#' + String(msgPath).replace(/[\\/:]/g, '~') + '#'; }
|
|
77
|
+
function turnSuffix(key) { return String(key == null ? '' : key).replace(/#/g, '~'); }
|
|
78
|
+
|
|
79
|
+
function num(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; }
|
|
80
|
+
function safeJson(raw) {
|
|
81
|
+
if (raw && typeof raw === 'object') return raw; // 清单里 metadata 本来就是对象(库里的 metadata_json 才是字符串)
|
|
82
|
+
if (typeof raw !== 'string' || !raw) return null;
|
|
83
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------- 用户正文:只取 text 块 + 剥产品自己的标签 ----------------
|
|
87
|
+
// 必须**先按块类型挑**再交给 toText:tool_result 块没有 text 字段却有 content 数组,
|
|
88
|
+
// 整块 toText 会把工具返回原文当成「用户说的话」—— 于是每一条工具结果都开一个幽灵轮。
|
|
89
|
+
export function clineUserText(s) {
|
|
90
|
+
let t = toText(s);
|
|
91
|
+
t = t.replace(/<mode_notice>[\s\S]*?<\/mode_notice>/gi, '');
|
|
92
|
+
t = t.replace(/<\/?user_input\b[^>]*>/gi, '');
|
|
93
|
+
return t.trim();
|
|
94
|
+
}
|
|
95
|
+
export function clineUserInput(content) {
|
|
96
|
+
if (typeof content === 'string') return clineUserText(content);
|
|
97
|
+
if (!Array.isArray(content)) return '';
|
|
98
|
+
const buf = [];
|
|
99
|
+
for (const b of content) if (b && b.type === 'text' && typeof b.text === 'string') buf.push(b.text);
|
|
100
|
+
return clineUserText(buf.join('\n\n'));
|
|
101
|
+
}
|
|
102
|
+
// assistant 正文:Cline 目前只有 text 块(没有 thinking 块),仍按 type 挑,
|
|
103
|
+
// 免的哪天多了 reasoning/tool_result 块被 toText 兜底塞进正文。
|
|
104
|
+
function clineAssistantText(content) {
|
|
105
|
+
if (typeof content === 'string') return toText(content);
|
|
106
|
+
if (!Array.isArray(content)) return '';
|
|
107
|
+
const buf = [];
|
|
108
|
+
for (const b of content) {
|
|
109
|
+
if (!b || typeof b !== 'object') continue;
|
|
110
|
+
if (b.type === 'text' && typeof b.text === 'string') buf.push(b.text);
|
|
111
|
+
else if ((b.type === 'thinking' || b.type === 'reasoning') && typeof b.text === 'string') buf.push(b.text);
|
|
112
|
+
}
|
|
113
|
+
return buf.join('\n\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 一次工具调用的入参:input 是**对象**({path, content} 这种)。不能丢给 toText ——
|
|
117
|
+
// toText 见到 `content` 字段就只回那段正文,路径会被吞掉(写文件工具正好长这样)。
|
|
118
|
+
function toolArgsText(input) {
|
|
119
|
+
if (input == null) return '';
|
|
120
|
+
if (typeof input === 'string') return input;
|
|
121
|
+
try { return JSON.stringify(input, null, 2); } catch { return toText(input); }
|
|
122
|
+
}
|
|
123
|
+
// 工具出参:content[] 是**逐项**结果 `{query, result, success, error?}`(也可能是纯字符串,老版本/别的工具)。
|
|
124
|
+
// 返回 {text, error}:error 取第一条失败项的原文(批量调用可以只错一项)。
|
|
125
|
+
function toolResultText(content) {
|
|
126
|
+
const arr = Array.isArray(content) ? content : (content == null ? [] : [content]);
|
|
127
|
+
const buf = [];
|
|
128
|
+
let error = '';
|
|
129
|
+
for (const e of arr) {
|
|
130
|
+
if (e == null) continue;
|
|
131
|
+
if (typeof e === 'string') { buf.push(e); continue; }
|
|
132
|
+
if (e.success === false || e.error) error = error || String(e.error || e.result || '工具执行失败');
|
|
133
|
+
const r = typeof e.result === 'string' ? e.result : (e.text != null ? toText(e.text) : toText(e));
|
|
134
|
+
if (typeof e.query === 'string' && e.query) buf.push('▸ ' + e.query + '\n' + r);
|
|
135
|
+
else buf.push(r);
|
|
136
|
+
}
|
|
137
|
+
return { text: buf.join('\n\n'), error };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------- 轮:整份 messages[] → turns[] ----------------
|
|
141
|
+
// 只存**卡片需要**的聚合量(正文/出入参不进内存:本 kind 一个会话就是一整份 JSON,
|
|
142
|
+
// 十几个长会话就能到 MB 级),详情侧按 src.turn 重读文件(与 openclaw 同一套分工)。
|
|
143
|
+
function buildTurns(messages) {
|
|
144
|
+
const turns = [];
|
|
145
|
+
let cur = null;
|
|
146
|
+
for (const m of messages) {
|
|
147
|
+
if (!m || typeof m !== 'object') continue;
|
|
148
|
+
const role = m.role;
|
|
149
|
+
const ts = num(m.ts);
|
|
150
|
+
if (role === 'user') {
|
|
151
|
+
const text = clineUserInput(m.content);
|
|
152
|
+
if (!text) {
|
|
153
|
+
// 只有 tool_result 的 user 行:不开轮,但它**是轮内发生过的最新一件事** —— 不带进 lastTs 的话
|
|
154
|
+
// 「以工具收尾」的那一轮耗时会短一截(实测能差几秒)
|
|
155
|
+
if (cur && ts) cur.lastTs = Math.max(cur.lastTs, ts);
|
|
156
|
+
if (cur) readToolFailures(m.content, cur);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
cur = {
|
|
160
|
+
key: typeof m.id === 'string' && m.id ? m.id : 'i' + turns.length,
|
|
161
|
+
time: ts, lastTs: ts,
|
|
162
|
+
tin: 0, tout: 0, tcache: 0, ctxUsed: 0,
|
|
163
|
+
calls: 0, tools: 0, models: [], toolNames: {},
|
|
164
|
+
err: false, errMsg: '',
|
|
165
|
+
preview: text.replace(/\s+/g, ' ').slice(0, 300),
|
|
166
|
+
};
|
|
167
|
+
turns.push(cur);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!cur) continue; // 首条用户消息之前的东西(防御:真实数据里没有)
|
|
171
|
+
if (role === 'assistant') {
|
|
172
|
+
if (ts) cur.lastTs = Math.max(cur.lastTs, ts);
|
|
173
|
+
cur.calls++;
|
|
174
|
+
const u = m.metrics;
|
|
175
|
+
if (u) {
|
|
176
|
+
const cr = num(u.cacheReadTokens), cw = num(u.cacheWriteTokens), prompt = num(u.inputTokens);
|
|
177
|
+
// ⚠️ inputTokens **含** cache(见文件头),净输入要减掉两个 cache 桶
|
|
178
|
+
cur.tin += Math.max(0, prompt - cr - cw);
|
|
179
|
+
cur.tout += num(u.outputTokens);
|
|
180
|
+
cur.tcache += cr + cw;
|
|
181
|
+
if (prompt > 0) cur.ctxUsed = prompt; // 上下文占用 = 末次调用的 prompt 全量
|
|
182
|
+
}
|
|
183
|
+
const mdl = modelName(m.modelInfo && m.modelInfo.id);
|
|
184
|
+
if (mdl && !cur.models.includes(mdl)) cur.models.push(mdl);
|
|
185
|
+
for (const b of (Array.isArray(m.content) ? m.content : [])) {
|
|
186
|
+
if (!b || b.type !== 'tool_use') continue;
|
|
187
|
+
const nm = typeof b.name === 'string' && b.name ? b.name : '?';
|
|
188
|
+
cur.tools++;
|
|
189
|
+
cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ts) cur.lastTs = Math.max(cur.lastTs, ts); // 别的角色(system 等)只贡献时间,不进轮统计
|
|
194
|
+
}
|
|
195
|
+
return turns;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 工具失败:逐项 success/error(这是本 kind 唯一可得的失败信号)
|
|
199
|
+
function readToolFailures(content, cur) {
|
|
200
|
+
for (const b of (Array.isArray(content) ? content : [])) {
|
|
201
|
+
if (!b || b.type !== 'tool_result') continue;
|
|
202
|
+
const { error } = toolResultText(b.content);
|
|
203
|
+
if (error && !cur.err) { cur.err = true; cur.errMsg = error; }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------- emit:一轮一条 entry ----------------
|
|
208
|
+
function emit(agent, msgPath, data, prevData) {
|
|
209
|
+
const base = clineIdBase(msgPath);
|
|
210
|
+
const ids = new Set();
|
|
211
|
+
data.turns.forEach((t, i) => {
|
|
212
|
+
const id = base + turnSuffix(t.key);
|
|
213
|
+
ids.add(id);
|
|
214
|
+
addEntry(id, {
|
|
215
|
+
agent,
|
|
216
|
+
project: data.project,
|
|
217
|
+
session: data.sid,
|
|
218
|
+
name: data.name || undefined,
|
|
219
|
+
time: t.time,
|
|
220
|
+
dur: Math.max(0, (t.lastTs || t.time) - t.time),
|
|
221
|
+
status: t.err ? 'error' : 'ok',
|
|
222
|
+
tin: t.tin, tout: t.tout, tcache: t.tcache,
|
|
223
|
+
total: t.tin + t.tout + t.tcache,
|
|
224
|
+
ctx: 0, // 窗口容量只在 CLI 二进制里,盘上没有 —— 不编分母
|
|
225
|
+
ctxUsed: t.ctxUsed || 0,
|
|
226
|
+
rounds: t.calls,
|
|
227
|
+
calls: t.calls,
|
|
228
|
+
tools: t.tools,
|
|
229
|
+
models: t.models,
|
|
230
|
+
preview: t.preview,
|
|
231
|
+
finished: i < data.turns.length - 1, // 末轮交给页面按最近时间兜底(同 openclaw / hermes)
|
|
232
|
+
aborted: false, // 逐轮无中断信号(会话级 status 不是逐轮的)
|
|
233
|
+
toolNames: t.toolNames,
|
|
234
|
+
}, { file: msgPath, turn: i, kind: 'cline' });
|
|
235
|
+
});
|
|
236
|
+
// 整份重写会一次性丢掉轮(比如手工编辑过会话文件):按 id 集合回收,否则老条目悬在页面上
|
|
237
|
+
for (const old of (prevData && prevData.ids) || []) {
|
|
238
|
+
if (ids.has(old)) continue;
|
|
239
|
+
entries.delete(old); srcs.delete(old); removedIds.push(old);
|
|
240
|
+
}
|
|
241
|
+
data.ids = [...ids];
|
|
242
|
+
sorted.cache = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------- 主源枚举(discovery 的 sniffBase 与手工添加共用) ----------------
|
|
246
|
+
// 一个 sessions 根下的 (会话 id, messages.json, 清单):只认「目录里有 *.messages.json」的形状。
|
|
247
|
+
function sessionDirs(root, limit) {
|
|
248
|
+
const out = [];
|
|
249
|
+
const names = listDirCached(root);
|
|
250
|
+
if (!names) return out;
|
|
251
|
+
for (const sid of names.slice(0, limit || 400)) {
|
|
252
|
+
const dir = path.join(root, sid);
|
|
253
|
+
if (!isDir(dir)) continue;
|
|
254
|
+
const inner = listDirCached(dir);
|
|
255
|
+
if (!inner) continue;
|
|
256
|
+
const want = sid + '.messages.json';
|
|
257
|
+
let msgPath = inner.includes(want) ? path.join(dir, want) : null;
|
|
258
|
+
if (!msgPath) {
|
|
259
|
+
const alt = inner.find(n => n.endsWith('.messages.json'));
|
|
260
|
+
if (alt) msgPath = path.join(dir, alt);
|
|
261
|
+
}
|
|
262
|
+
if (!msgPath || !isFile(msgPath)) continue;
|
|
263
|
+
let manPath = path.join(dir, sid + '.json');
|
|
264
|
+
if (!isFile(manPath)) {
|
|
265
|
+
const alt = inner.find(n => n.endsWith('.json') && !n.endsWith('.messages.json') && !n.endsWith('.compaction.json'));
|
|
266
|
+
if (alt) manPath = path.join(dir, alt);
|
|
267
|
+
}
|
|
268
|
+
out.push({ sid, dir, msgPath, manPath });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 内容判据(嗅探每 3s 一轮,读盘量必须卡死):整份 JSON 是**pretty-printed**,头几个键就在最前面,
|
|
274
|
+
// 所以只读头 2KB,要求同时出现 `"sessionId"` 与 `"messages": [`。文件名再兜一道 `.messages.json`。
|
|
275
|
+
export function isClineMessages(fp) {
|
|
276
|
+
if (!/\.messages\.json$/i.test(String(fp))) return false;
|
|
277
|
+
let fd; try { fd = fs.openSync(fp, 'r'); } catch { return false; }
|
|
278
|
+
try {
|
|
279
|
+
const buf = Buffer.alloc(2048);
|
|
280
|
+
const n = fs.readSync(fd, buf, 0, 2048, 0);
|
|
281
|
+
if (n <= 0) return false;
|
|
282
|
+
const head = buf.toString('utf8', 0, n);
|
|
283
|
+
return /^\s*\{/.test(head) && /"sessionId"\s*:/.test(head) && /"messages"\s*:\s*\[/.test(head);
|
|
284
|
+
} catch { return false; } finally { try { fs.closeSync(fd); } catch {} }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 手工添加时用户可能填任意一层:`~/.cline` / `<home>/.cline/data` / `<...>/data/sessions` / 某个会话目录 /
|
|
288
|
+
// 甚至直接填那份 `*.messages.json`。返回 {root: <sessions 目录>, files:[{sid,msgPath,manPath}]};认不出返回 null。
|
|
289
|
+
export function clineTranscripts(base) {
|
|
290
|
+
if (isFile(base) && isClineMessages(base)) {
|
|
291
|
+
const dir = path.dirname(base);
|
|
292
|
+
return { root: dir, files: [{ sid: path.basename(dir), msgPath: base, manPath: path.join(dir, path.basename(dir) + '.json') }] };
|
|
293
|
+
}
|
|
294
|
+
if (!isDir(base)) return null;
|
|
295
|
+
const bn = path.basename(base).toLowerCase();
|
|
296
|
+
const cands = [];
|
|
297
|
+
if (bn === 'sessions') cands.push(base);
|
|
298
|
+
else if (bn === 'data') cands.push(path.join(base, 'sessions'));
|
|
299
|
+
else if (bn === '.cline' || bn === 'cline') cands.push(path.join(base, 'data', 'sessions'), path.join(base, 'sessions'));
|
|
300
|
+
else {
|
|
301
|
+
cands.push(base); // 填的就是 sessions 根
|
|
302
|
+
cands.push(path.join(base, 'sessions')); // 填的是 data 那一层
|
|
303
|
+
cands.push(path.dirname(base)); // 填的是某个会话目录 → 父目录才是 sessions 根
|
|
304
|
+
}
|
|
305
|
+
for (const root of [...new Set(cands)]) {
|
|
306
|
+
if (!isDir(root)) continue;
|
|
307
|
+
const hit = sessionDirs(root, 12).filter(s => isClineMessages(s.msgPath));
|
|
308
|
+
if (hit.length) return { root, files: hit };
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------- 清单(会话级元数据)----------------
|
|
314
|
+
// 只取 cwd / workspace_root / metadata.title / status —— **不**把 systemPrompt、usage 之外的内容带出去。
|
|
315
|
+
function readManifest(fp) {
|
|
316
|
+
const out = { project: '', name: '', status: '' };
|
|
317
|
+
let j; try { j = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return out; }
|
|
318
|
+
if (!j || typeof j !== 'object') return out;
|
|
319
|
+
if (typeof j.cwd === 'string' && j.cwd) out.project = j.cwd;
|
|
320
|
+
else if (typeof j.workspace_root === 'string' && j.workspace_root) out.project = j.workspace_root;
|
|
321
|
+
if (typeof j.status === 'string') out.status = j.status;
|
|
322
|
+
const md = safeJson(j.metadata);
|
|
323
|
+
if (md && typeof md.title === 'string' && md.title.trim()) out.name = md.title.trim();
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------- 扫描主入口 ----------------
|
|
328
|
+
// root = sessions 目录(由 discovery 的 sniffBase 解析出来,不是 ~/.cline 那一层)
|
|
329
|
+
export function scanCline(agent, root, onlyFile) {
|
|
330
|
+
if (!isDir(root)) return;
|
|
331
|
+
for (const s of sessionDirs(root)) {
|
|
332
|
+
if (onlyFile && s.msgPath !== onlyFile) continue;
|
|
333
|
+
let mst; try { mst = fs.statSync(s.msgPath); } catch { continue; }
|
|
334
|
+
let ast = null; try { ast = fs.statSync(s.manPath); } catch {}
|
|
335
|
+
const sig = mst.mtimeMs + '|' + mst.size + '#' + (ast ? ast.mtimeMs + '|' + ast.size : '-');
|
|
336
|
+
const key = s.msgPath;
|
|
337
|
+
const prev = files.get(key);
|
|
338
|
+
if (prev && prev.data && prev.data.sig === sig) {
|
|
339
|
+
// 本 kind 不落 index:重启后 files 也可能从「本轮更早那次扫描」带来,条目却已被清掉 → 按内存里那份重建
|
|
340
|
+
const last = prev.data.turns && prev.data.turns[prev.data.turns.length - 1];
|
|
341
|
+
if (last && !entries.has(clineIdBase(key) + turnSuffix(last.key))) emit(agent, key, prev.data, null);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
let payload;
|
|
345
|
+
try { payload = JSON.parse(fs.readFileSync(s.msgPath, 'utf8')); } catch (e) {
|
|
346
|
+
clineScanErr.set(agent, s.msgPath + ' 读不动(整份 JSON 解析失败):' + e.message);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!payload || !Array.isArray(payload.messages)) {
|
|
350
|
+
clineScanErr.set(agent, s.msgPath + ' 里没有 messages[] —— 不是认得的 Cline 会话文件');
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
clineScanErr.delete(agent);
|
|
354
|
+
const man = readManifest(s.manPath);
|
|
355
|
+
const turns = buildTurns(payload.messages);
|
|
356
|
+
const data = {
|
|
357
|
+
sig, sid: s.sid, turns,
|
|
358
|
+
project: man.project || '(cline)',
|
|
359
|
+
name: man.name,
|
|
360
|
+
status: man.status,
|
|
361
|
+
rev: (prev && prev.data ? (prev.data.rev || 0) : 0) + 1,
|
|
362
|
+
};
|
|
363
|
+
files.set(key, { agent, kind: 'cline', m: mst.mtimeMs, s: mst.size, off: 0, data });
|
|
364
|
+
emit(agent, key, data, prev && prev.data);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------- 详情:重读整份 JSON,按 src.turn 过滤 ----------------
|
|
369
|
+
// 开轮判据与扫描侧是同一个函数(clineUserInput),所以「扫到的第 N 轮」必定就是「详情的第 N 轮」。
|
|
370
|
+
export function clineEntryContent(src, full) {
|
|
371
|
+
const st = files.get(src.file);
|
|
372
|
+
const version = st ? st.m + ':' + st.s : null;
|
|
373
|
+
let payload;
|
|
374
|
+
try { payload = JSON.parse(fs.readFileSync(src.file, 'utf8')); } catch { return null; }
|
|
375
|
+
const msgs = payload && Array.isArray(payload.messages) ? payload.messages : [];
|
|
376
|
+
const out = {
|
|
377
|
+
user: '', assistant: '', tools: [], calls: [], v: version,
|
|
378
|
+
callsNote: 'Cline CLI 的 sessions/<id>.messages.json 是**整份 JSON、每次落盘整体重写**(不是 JSONL):' +
|
|
379
|
+
'逐次用量挂在每条 assistant 消息的 metrics 上。metrics.inputTokens **含**缓存(cacheRead/Write 是它的子集,' +
|
|
380
|
+
'与 openclaw/minimax 相反),故 tin = input − cacheRead − cacheWrite、tcache = 两个 cache 桶之和、' +
|
|
381
|
+
'ctxUsed = 本轮末次调用的 input。⚠️ /compact 之后 inputTokens 会**回落**(上下文被换成了摘要),' +
|
|
382
|
+
'它不是累计值。逐次调用耗时不落盘,故 calls[].dur 恒 0(逐工具耗时按 tool_use→tool_result 的 ts 差算,在 tools[].dur)。',
|
|
383
|
+
};
|
|
384
|
+
const texts = [];
|
|
385
|
+
const notes = new Set();
|
|
386
|
+
let turnIdx = -1;
|
|
387
|
+
let toolCount = 0;
|
|
388
|
+
let prevAssistant = null; // 逐次调用与它发出的 tool_use 配对(详情里只挂名字)
|
|
389
|
+
for (const m of msgs) {
|
|
390
|
+
if (!m || typeof m !== 'object') continue;
|
|
391
|
+
const ts = num(m.ts);
|
|
392
|
+
if (m.role === 'user') {
|
|
393
|
+
const text = clineUserInput(m.content);
|
|
394
|
+
if (!text) {
|
|
395
|
+
if (turnIdx === src.turn) {
|
|
396
|
+
for (const b of (Array.isArray(m.content) ? m.content : [])) {
|
|
397
|
+
if (!b || b.type !== 'tool_result') continue;
|
|
398
|
+
const t = out.tools.find(x => x.tid && x.tid === b.tool_use_id) ||
|
|
399
|
+
out.tools.find(x => !x.filled && x.name === b.name);
|
|
400
|
+
if (!t) { notes.add('有工具返回找不到对应的调用(可能是上一轮被压缩掉的调用)'); continue; }
|
|
401
|
+
t.filled = true;
|
|
402
|
+
const { text: ot, error } = toolResultText(b.content);
|
|
403
|
+
const fo = {};
|
|
404
|
+
t.output = trunc(ot, full, fo); t.outputTrunc = !!fo.t;
|
|
405
|
+
if (error) t.error = error;
|
|
406
|
+
if (ts && t.ct) t.dur = Math.max(0, ts - t.ct);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
turnIdx++;
|
|
412
|
+
if (turnIdx === src.turn) out.user = text;
|
|
413
|
+
if (turnIdx > src.turn) break;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (m.role !== 'assistant' || turnIdx !== src.turn) continue;
|
|
417
|
+
const u = m.metrics;
|
|
418
|
+
const cr = u ? num(u.cacheReadTokens) : 0;
|
|
419
|
+
const cw = u ? num(u.cacheWriteTokens) : 0;
|
|
420
|
+
const prompt = u ? num(u.inputTokens) : 0;
|
|
421
|
+
const text = clineAssistantText(m.content);
|
|
422
|
+
if (text) texts.push(text);
|
|
423
|
+
const call = {
|
|
424
|
+
model: modelName(m.modelInfo && m.modelInfo.id) || '',
|
|
425
|
+
provider: (m.modelInfo && typeof m.modelInfo.provider === 'string') ? m.modelInfo.provider : '',
|
|
426
|
+
tin: u ? Math.max(0, prompt - cr - cw) : 0,
|
|
427
|
+
tout: u ? num(u.outputTokens) : 0,
|
|
428
|
+
tcache: cr + cw,
|
|
429
|
+
dur: 0,
|
|
430
|
+
};
|
|
431
|
+
if (text) { const ft = {}; call.text = trunc(text, full, ft); call.textTrunc = !!ft.t; }
|
|
432
|
+
const names = [];
|
|
433
|
+
for (const b of (Array.isArray(m.content) ? m.content : [])) {
|
|
434
|
+
if (!b || b.type !== 'tool_use') continue;
|
|
435
|
+
const nm = typeof b.name === 'string' && b.name ? b.name : '?';
|
|
436
|
+
names.push(nm);
|
|
437
|
+
const fi = {};
|
|
438
|
+
out.tools.push({
|
|
439
|
+
name: nm, tid: b.id || null, ct: ts, dur: 0, error: null, filled: false,
|
|
440
|
+
input: trunc(toolArgsText(b.input), full, fi), inputTrunc: !!fi.t,
|
|
441
|
+
output: '', outputTrunc: false,
|
|
442
|
+
});
|
|
443
|
+
toolCount++;
|
|
444
|
+
}
|
|
445
|
+
if (names.length) call.tools = names;
|
|
446
|
+
out.calls.push(call);
|
|
447
|
+
prevAssistant = call;
|
|
448
|
+
}
|
|
449
|
+
out.assistant = texts.join('\n\n---\n\n');
|
|
450
|
+
// ct / filled 只是这一轮内配对用的临时字段,不进 API 出参(与 hermes / openclaw 的 tools[] 形状对齐)
|
|
451
|
+
for (const t of out.tools) { delete t.filled; delete t.ct; }
|
|
452
|
+
// 失败原话:批量工具里可以只错一项,逐条列出来才看得清是哪一次
|
|
453
|
+
const errs = out.tools.filter(t => t.error);
|
|
454
|
+
if (errs.length) {
|
|
455
|
+
out.callsNote = '本轮有 ' + errs.length + ' 个工具执行失败(' +
|
|
456
|
+
errs.slice(0, 3).map(t => t.name + ':' + t.error).join(';') +
|
|
457
|
+
(errs.length > 3 ? ' 等' : '') + ')。Cline 只在 tool_result 里记成败,模型侧失败不落盘。' +
|
|
458
|
+
(notes.size ? ' ' + [...notes].join(';') : '');
|
|
459
|
+
} else if (prevAssistant && !out.assistant && !toolCount) {
|
|
460
|
+
out.callsNote += ' 本轮没有正文也没有工具调用(源头就没有这一轮的记录)。';
|
|
461
|
+
} else if (notes.size) {
|
|
462
|
+
out.callsNote += ' ' + [...notes].join(';');
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 供 discovery 的判据/根解析见上方 clineTranscripts / hasClineSessions;服务端接线(scanBranch、
|
|
468
|
+
// entry-content 分发、KNOWN_AGENTS、confAvailable)在 agent-acta-server.mjs。
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ---------------- codearts 解析器(%APPDATA%/codearts-agent/User/logs/CodeArts Agent-*.log) ----------------
|
|
2
|
+
// 从 agent-acta-server.mjs 拆出(纯搬家,逻辑零改动)。导出 scanCodearts。
|
|
3
|
+
// 文本日志,opencode 风格事件:
|
|
4
|
+
// 【AgentCore question】partsInfo {sessionID, model.modelID, parts[text]} → 用户开新轮
|
|
5
|
+
// 【AgentCore updated tokens】 tokens:[...],"input":N,"output":N,... → 每次 LLM 调用的 token(跟在 step-finish 后)
|
|
6
|
+
// type: tool info {...} → 工具调用事件
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { files, entries, addEntry, toolNameCounts, markKind, listDirCached, readCompleteLines } from './shared.mjs';
|
|
10
|
+
|
|
11
|
+
const CA_TS = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d{3}/;
|
|
12
|
+
const CA_TOKENS = /"input":(\d+),"output":(\d+),"reasoning":(\d+),"cache":\{"write":(\d+),"read":(\d+)\}/;
|
|
13
|
+
|
|
14
|
+
export function codeartsIdBase(fp) { return 'ca#' + fp.replace(/[\\/:]/g, '~') + '#'; }
|
|
15
|
+
function emitCodeartsTurns(agent, fp, data, fromIdx) {
|
|
16
|
+
const idBase = codeartsIdBase(fp);
|
|
17
|
+
for (let i = fromIdx; i < data.turns.length; i++) {
|
|
18
|
+
const t = data.turns[i];
|
|
19
|
+
const total = t.tin + t.tout + t.tcache;
|
|
20
|
+
addEntry(idBase + t.idx, {
|
|
21
|
+
agent, project: 'codearts-agent', session: t.session || path.basename(fp, '.log'),
|
|
22
|
+
time: t.time, dur: Math.max(0, t.lastTs - t.time), status: t.err ? 'error' : 'ok',
|
|
23
|
+
tin: t.tin, tout: t.tout, tcache: t.tcache, total,
|
|
24
|
+
ctx: 0, rounds: t.calls.length, tools: t.tools, calls: t.calls.length,
|
|
25
|
+
models: t.models, preview: t.preview,
|
|
26
|
+
finished: !!t.finished || i < data.turns.length - 1, aborted: !!t.aborted,
|
|
27
|
+
toolNames: toolNameCounts(t.toolList || []),
|
|
28
|
+
}, { file: fp, turn: t.idx, kind: 'codearts' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function scanCodearts(agent, root, onlyFile) { // onlyFile 见 scanClaude 的说明
|
|
33
|
+
const names = listDirCached(root);
|
|
34
|
+
if (!names) return;
|
|
35
|
+
for (const n of names) {
|
|
36
|
+
if (!/^CodeArts Agent-.*\.log$/i.test(n)) continue;
|
|
37
|
+
const fp = path.join(root, n);
|
|
38
|
+
if (onlyFile && fp !== onlyFile) continue;
|
|
39
|
+
let fst; try { fst = fs.statSync(fp); } catch { continue; }
|
|
40
|
+
const key = fp, m = fst.mtimeMs, s = fst.size;
|
|
41
|
+
const prev = files.get(key);
|
|
42
|
+
if (prev && prev.m === m && prev.s === s) {
|
|
43
|
+
if (prev.data?.turns?.length && !entries.has(codeartsIdBase(fp) + (prev.data.turns.length - 1))) emitCodeartsTurns(agent, fp, prev.data, 0);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const off = prev?.off || 0;
|
|
47
|
+
const data = prev?.data || { turns: [], emitted: 0 };
|
|
48
|
+
if (s <= off) { files.set(key, { agent, kind: 'codearts', m, s, off, data }); markKind('codearts'); continue; }
|
|
49
|
+
try {
|
|
50
|
+
const rd = readCompleteLines(fp, off, s);
|
|
51
|
+
if (!rd) { files.set(key, { agent, kind: 'codearts', m, s, off, data }); markKind('codearts'); continue; }
|
|
52
|
+
const newOff = rd.newOff;
|
|
53
|
+
for (const line of rd.lines) {
|
|
54
|
+
const tsm = line.match(CA_TS);
|
|
55
|
+
const ts = tsm ? Date.parse(tsm[1].replace(' ', 'T')) : 0; // 日志是本地时间
|
|
56
|
+
const cur = data.turns[data.turns.length - 1];
|
|
57
|
+
if (line.includes('【AgentCore question】')) {
|
|
58
|
+
const pi = line.indexOf('partsInfo ');
|
|
59
|
+
let q = null;
|
|
60
|
+
if (pi >= 0) { try { q = JSON.parse(line.slice(pi + 10)); } catch {} }
|
|
61
|
+
const text0 = (q?.parts || []).filter(p => p && p.type === 'text').map(p => String(p.text || '')).join('\n');
|
|
62
|
+
if (!text0) continue;
|
|
63
|
+
data.turns.push({
|
|
64
|
+
idx: data.turns.length, time: ts, lastTs: ts,
|
|
65
|
+
session: q?.sessionID || '', models: q?.model?.modelID ? [q.model.modelID] : [],
|
|
66
|
+
tin: 0, tout: 0, tcache: 0, tools: 0, err: false,
|
|
67
|
+
user: text0.slice(0, 3000), preview: text0.replace(/\s+/g, ' ').slice(0, 300),
|
|
68
|
+
calls: [], toolList: [], finished: false, aborted: false,
|
|
69
|
+
});
|
|
70
|
+
} else if (line.includes('【AgentCore updated tokens】') && cur) {
|
|
71
|
+
const tm = line.match(CA_TOKENS);
|
|
72
|
+
if (tm) {
|
|
73
|
+
const tin = +tm[1], tout = +tm[2], tcache = +tm[4] + +tm[5];
|
|
74
|
+
cur.tin += tin; cur.tout += tout; cur.tcache += tcache;
|
|
75
|
+
cur.calls.push({ model: cur.models[0] || '', tin, tout, tcache, dur: ts && cur.lastTs ? Math.max(0, ts - cur.lastTs) : 0 });
|
|
76
|
+
}
|
|
77
|
+
if (ts) cur.lastTs = ts;
|
|
78
|
+
} else if (line.includes('【AgentCore turn complete】') && cur) {
|
|
79
|
+
cur.finished = true;
|
|
80
|
+
} else if (line.includes('type: tool info') && cur) {
|
|
81
|
+
cur.tools++;
|
|
82
|
+
const tm = line.match(/(?:name|toolName)\s*[:=]\s*["']([^"']+)/i);
|
|
83
|
+
cur.toolList.push({ name: tm ? tm[1] : '?' });
|
|
84
|
+
} else if (line.includes('"session.error"') && cur) {
|
|
85
|
+
cur.err = true;
|
|
86
|
+
cur.finished = true;
|
|
87
|
+
}
|
|
88
|
+
if (cur && ts && ts > cur.lastTs) cur.lastTs = ts;
|
|
89
|
+
}
|
|
90
|
+
files.set(key, { agent, kind: 'codearts', m, s, off: newOff, data });
|
|
91
|
+
markKind('codearts');
|
|
92
|
+
emitCodeartsTurns(agent, fp, data, Math.max(0, data.emitted - 1));
|
|
93
|
+
data.emitted = data.turns.length;
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
}
|