@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,398 @@
|
|
|
1
|
+
// ---------------- hermes 型解析器(Hermes Agent:%LOCALAPPDATA%\hermes\state.db 或 ~/.hermes/state.db) ----------------
|
|
2
|
+
// 数据源是**一个明文 SQLite 库**(WAL 模式,schema v30 实测):
|
|
3
|
+
// sessions(id, source, model, title, parent_session_id, started_at/ended_at, end_reason,
|
|
4
|
+
// message_count, tool_call_count, api_call_count, input/output/cache_read/cache_write/reasoning_tokens,
|
|
5
|
+
// cwd, model_config(JSON), billing_provider, archived, hidden, …)
|
|
6
|
+
// messages(id, session_id, role, content, tool_calls(JSON 串), tool_call_id, tool_name,
|
|
7
|
+
// timestamp(REAL epoch), finish_reason, active, compacted, _compressed_summary, …)
|
|
8
|
+
// system_prompts / session_model_usage / messages_fts 等辅助表不进正文解析
|
|
9
|
+
// (session_model_usage **按 task 分行**,含 title_generation 这种辅助调用;sessions 行级聚合只等于
|
|
10
|
+
// task='' 的主体,所以逐会话 token 直接读 sessions 行即可,碰 usage 表反而会把标题调用混进来)。
|
|
11
|
+
//
|
|
12
|
+
// 轮口径(与 doubao/OpenAI 同族同构,2026-09-21 本机真库核验):
|
|
13
|
+
// user 行开轮 → assistant 行累加(content='' 且 tool_calls 非空 = 中间调用;finish_reason='stop' = 收轮)
|
|
14
|
+
// → role='tool' 行按 tool_call_id 回填上一条 call 的出入参 → 下一条 user 收轮。
|
|
15
|
+
// ⚠️ 只取 active=1 且 _compressed_summary=0 的行:compaction 会旧行置 active=0,
|
|
16
|
+
// 摘要行伪装成 user 注入,都混进轮里就是幽灵轮次(doubao 字节偏移前案同类)。
|
|
17
|
+
// 空 user 不开轮(判据只在 scan 一处写,详情读同一份内存数据,天然对齐)。
|
|
18
|
+
//
|
|
19
|
+
// **逐轮 token / 逐次调用在 DB 里没有**(messages.token_count 恒 NULL,sessions 只有会话级累计)——
|
|
20
|
+
// 唯一来源是同目录 logs/agent.log 的逐次行:
|
|
21
|
+
// "… INFO [会话id] agent.conversation_loop: API call #1: model=X provider=Y in=23662 out=11
|
|
22
|
+
// total=23673 latency=3.4s cache=256/23662 (1%) id=gen-… upstream=Reka"
|
|
23
|
+
// in **含** cache(=DB 的 input+cache_read),所以 tin=in-cache、tcache=cache,上下文占用=in(逐轮取最后一次)。
|
|
24
|
+
// #N 序号跨轮累加、title_generation 不打这行,所以按「时间戳落在轮窗口内」归属,不看序号。
|
|
25
|
+
// upstream= 是网关后面的真实模型(sessions.model 只是预设名,如 nous/welcome)。
|
|
26
|
+
// 日志缺失/滚掉时退化为「调用数=assistant 行数、token 只有会话聚合可摊到最后一轮」——不编造逐轮值。
|
|
27
|
+
// 失败轮(真库实测):assistant 行内容是合成的 "Your request was not processed…"、finish_reason=NULL,
|
|
28
|
+
// DB 里没有任何失败标记;错误原文只出现在 agent.log 的 "API call failed" 行 → 按轮窗口带出 errMsg。
|
|
29
|
+
//
|
|
30
|
+
// 只读纪律(与 opencode 同款):DatabaseSync {readOnly:true};签名 = state.db + -wal + agent.log 的
|
|
31
|
+
// mtime/size,变了整库重读,没变跳过。不进 OFF_KINDS / PARSER_REV。
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import {
|
|
35
|
+
entries, srcs, removedIds, sorted, addEntry, files,
|
|
36
|
+
toolNameCounts, modelName, toText, trunc, sqliteMod,
|
|
37
|
+
} from './shared.mjs';
|
|
38
|
+
|
|
39
|
+
export const hermesScanErr = new Map();
|
|
40
|
+
|
|
41
|
+
function safeJson(raw) {
|
|
42
|
+
if (typeof raw !== 'string' || !raw) return null;
|
|
43
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
function num(v) {
|
|
46
|
+
const n = Number(v);
|
|
47
|
+
return Number.isFinite(n) ? n : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------- agent.log 逐次调用行 ----------------
|
|
51
|
+
// 行头 "YYYY-MM-DD HH:MM:SS,mmm"(本机时区)+ [会话id] 标签;日志会滚,读不到的轮退回 DB 口径。
|
|
52
|
+
const LOG_TS_RE = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3})\b/;
|
|
53
|
+
const LOG_SID_RE = /\[(\d{8}_\d{6}_[0-9a-fA-F]{4,})\]\s+agent\.conversation_loop:\s+(.*)$/;
|
|
54
|
+
function logEpochMs(head) {
|
|
55
|
+
const m = LOG_TS_RE.exec(head);
|
|
56
|
+
if (!m) return 0;
|
|
57
|
+
// 返回 **毫秒**:全站条目时间口径是 epoch ms(页面按毫秒区间筛,喂秒会被 from/to 直接滤空)
|
|
58
|
+
const t = new Date(`${m[1]}T${m[2]}.${m[3]}`).getTime();
|
|
59
|
+
return Number.isFinite(t) ? t : 0;
|
|
60
|
+
}
|
|
61
|
+
// 返回 sid -> 按时间排序的 {t, in, out, cache, lat, model, provider, upstream, err?}
|
|
62
|
+
export function parseHermesAgentLog(fp) {
|
|
63
|
+
const bySid = new Map();
|
|
64
|
+
let text = '';
|
|
65
|
+
try { text = fs.readFileSync(fp, 'utf8'); } catch { return bySid; }
|
|
66
|
+
// 必须按 \r?\n 切:真库 agent.log 是 CRLF,而 JS 的 `.` 不认 \r、`$` 也不认行尾的 \r,
|
|
67
|
+
// 留着 \r 时 LOG_SID_RE 的 `(.*)$` 整档恒不命中(fixture 用 LF 建所以测不出来——真库实测踩的)
|
|
68
|
+
for (const line of text.split(/\r?\n/)) {
|
|
69
|
+
const sm = LOG_SID_RE.exec(line);
|
|
70
|
+
if (!sm) continue;
|
|
71
|
+
const [, sid, msg] = sm;
|
|
72
|
+
const t = logEpochMs(line);
|
|
73
|
+
if (!t) continue;
|
|
74
|
+
let item = null;
|
|
75
|
+
if (msg.startsWith('API call #')) {
|
|
76
|
+
const g = (re) => { const m2 = re.exec(msg); return m2 ? m2[1] : null; };
|
|
77
|
+
const cm = /cache=(\d+)\/(\d+)/.exec(msg);
|
|
78
|
+
item = {
|
|
79
|
+
t, kind: 'call',
|
|
80
|
+
model: g(/model=(\S+)/) || '', provider: g(/provider=(\S+)/) || '',
|
|
81
|
+
in: num(g(/in=(\d+)/)), out: num(g(/out=(\d+)/)),
|
|
82
|
+
cache: cm ? num(cm[1]) : 0,
|
|
83
|
+
lat: num(g(/latency=([\d.]+)s/)),
|
|
84
|
+
upstream: g(/upstream=(\S+)/) || '',
|
|
85
|
+
};
|
|
86
|
+
} else if (msg.startsWith('API call failed')) {
|
|
87
|
+
// 只留收尾那条("...after N retries. 原文 | provider=..."),attempt 那种中间重试行不重复记
|
|
88
|
+
if (!msg.includes('retries')) continue;
|
|
89
|
+
const m3 = /^API call failed after \d+ retries\.\s*([^|]*?)\s*(?:\|.*)?$/.exec(msg);
|
|
90
|
+
item = { t, kind: 'fail', err: (m3 && m3[1]) || 'API call failed' };
|
|
91
|
+
} else continue;
|
|
92
|
+
if (!bySid.has(sid)) bySid.set(sid, []);
|
|
93
|
+
bySid.get(sid).push(item);
|
|
94
|
+
}
|
|
95
|
+
for (const arr of bySid.values()) arr.sort((a, b) => a.t - b.t);
|
|
96
|
+
return bySid;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 把 [lo,hi] 窗口内的日志调用分给这一轮:calls 与 assistant 行按时间序一对一配对,
|
|
100
|
+
// 落不进任何轮窗口的(会话级杂项)忽略;配不到日志行的调用只留结构、不编 token。
|
|
101
|
+
function sliceLogCalls(all, lo, hi) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const x of all) if (x.t >= lo && x.t <= hi) out.push(x);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function makeTurn(s, ts) {
|
|
108
|
+
return {
|
|
109
|
+
sid: s.id, time: ts || 0, lastTs: ts || 0, endTs: 0,
|
|
110
|
+
model: s.model || '',
|
|
111
|
+
project: s.cwd || '(未知项目)', session: s.id,
|
|
112
|
+
name: (s.parent_session_id ? '⤷ ' : '') + String(s.title || '').trim(),
|
|
113
|
+
// I16 子 agent 拓扑:`sessions.parent_session_id` 是源头给的显式外键(本机 fixture 实测
|
|
114
|
+
// 子会话 B 的 parent = 主会话 A)。以前只用它给标题加「⤷ 」前缀,父子关系没往条目上落。
|
|
115
|
+
parentId: s.parent_session_id || null,
|
|
116
|
+
user: '', texts: [], tools: [], byTool: new Map(),
|
|
117
|
+
callList: [], models: [], events: [], others: [],
|
|
118
|
+
tin: 0, tout: 0, tcache: 0, ctxUsed: 0,
|
|
119
|
+
err: false, errMsg: '', finished: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addCallFromRow(t, row, logCall, sessionModel) {
|
|
124
|
+
const ts = num(row.timestamp);
|
|
125
|
+
const calls = safeJson(row.tool_calls);
|
|
126
|
+
const names = Array.isArray(calls) ? calls.map(c => modelName(c?.function?.name) || '?') : [];
|
|
127
|
+
const call = {
|
|
128
|
+
model: logCall?.model ? modelName(logCall.model) : modelName(sessionModel || '') || '(未知模型)',
|
|
129
|
+
provider: logCall?.provider || '',
|
|
130
|
+
tin: logCall ? Math.max(0, logCall.in - logCall.cache) : 0,
|
|
131
|
+
tout: logCall ? logCall.out : 0,
|
|
132
|
+
tcache: logCall ? logCall.cache : 0,
|
|
133
|
+
dur: logCall ? Math.round(logCall.lat * 1000) : 0,
|
|
134
|
+
ttft: 0, // 源头只有整次 latency,没有首 token 分段 → 不画 ttft(同 dsh 失败调用占位约定)
|
|
135
|
+
finishReason: row.finish_reason && row.finish_reason !== 'stop' ? String(row.finish_reason) : undefined,
|
|
136
|
+
};
|
|
137
|
+
if (logCall?.upstream) call.upstream = modelName(logCall.upstream);
|
|
138
|
+
const txt = toText(row.content).trim();
|
|
139
|
+
if (txt) call.text = txt;
|
|
140
|
+
if (names.length) call.tools = names;
|
|
141
|
+
t.callList.push(call);
|
|
142
|
+
const mdl = call.model && call.model !== '(未知模型)' ? call.model : modelName(sessionModel || '');
|
|
143
|
+
if (mdl && !t.models.includes(mdl)) t.models.push(mdl);
|
|
144
|
+
if (logCall) {
|
|
145
|
+
t.tin += call.tin; t.tout += call.tout; t.tcache += call.tcache;
|
|
146
|
+
if (logCall.in > 0) t.ctxUsed = logCall.in; // 逐轮上下文 = 轮内最后一次调用的 prompt 全量(含缓存)
|
|
147
|
+
}
|
|
148
|
+
t.events.push({
|
|
149
|
+
k: 'llm', t: ts, dur: call.dur, model: call.model, upstream: logCall?.upstream || '',
|
|
150
|
+
tin: call.tin, tout: call.tout, tcache: call.tcache,
|
|
151
|
+
err: false, blocks: names.length ? ['tool_calls'] : (txt ? ['text'] : []),
|
|
152
|
+
finish: row.finish_reason || '',
|
|
153
|
+
});
|
|
154
|
+
return call;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function turnFromMessages(s, rows, logBySid) {
|
|
158
|
+
const turns = [];
|
|
159
|
+
let cur = null;
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
// DB 的 timestamp 是 REAL **秒** → 统一乘 1000 进毫秒口径(条目/事件/耗时全站都是毫秒)
|
|
162
|
+
const ts = Math.round(num(row.timestamp) * 1000);
|
|
163
|
+
if (row.role === 'user') {
|
|
164
|
+
const text = toText(row.content).trim();
|
|
165
|
+
if (!text) continue; // 空 user 不开轮(与详情共用这一份判据)
|
|
166
|
+
cur = makeTurn(s, ts);
|
|
167
|
+
cur.key = String(row.id); // 稳定键:轮里首条用户消息的行号
|
|
168
|
+
cur.user = text.slice(0, 3000);
|
|
169
|
+
cur.events.push({ k: 'user', t: ts });
|
|
170
|
+
turns.push(cur);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!cur) continue; // 首条 user 之前的杂项(系统注入等)不进轮
|
|
174
|
+
if (row.role === 'assistant') {
|
|
175
|
+
cur.lastTs = Math.max(cur.lastTs, ts);
|
|
176
|
+
const txt = toText(row.content).trim();
|
|
177
|
+
if (txt) cur.texts.push(txt);
|
|
178
|
+
const calls = safeJson(row.tool_calls);
|
|
179
|
+
if (Array.isArray(calls)) {
|
|
180
|
+
for (const c of calls) {
|
|
181
|
+
const fn = c && typeof c === 'object' ? (c.function || {}) : {};
|
|
182
|
+
const tool = {
|
|
183
|
+
name: String(fn.name || '?'),
|
|
184
|
+
tid: String(c.id || c.call_id || ''),
|
|
185
|
+
ct: ts,
|
|
186
|
+
input: typeof fn.arguments === 'string' ? fn.arguments : toText(fn.arguments ?? c?.args ?? ''),
|
|
187
|
+
output: '', error: null, dur: 0,
|
|
188
|
+
};
|
|
189
|
+
if (tool.tid) cur.byTool.set(tool.tid, tool);
|
|
190
|
+
cur.tools.push(tool);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
cur._pendingRows = cur._pendingRows || [];
|
|
194
|
+
cur._pendingRows.push({ row, ts });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (row.role === 'tool') {
|
|
198
|
+
const tcid = String(row.tool_call_id || '');
|
|
199
|
+
const tool = tcid ? cur.byTool.get(tcid) : null;
|
|
200
|
+
const out = toText(row.content);
|
|
201
|
+
const j = safeJson(row.content);
|
|
202
|
+
const failed = !!(j && (j.error || j.is_error));
|
|
203
|
+
if (tool) {
|
|
204
|
+
tool.output = out;
|
|
205
|
+
tool.dur = Math.max(0, ts - (tool.ct || ts));
|
|
206
|
+
if (failed) tool.error = 'error';
|
|
207
|
+
} else {
|
|
208
|
+
cur.tools.push({
|
|
209
|
+
name: String(row.tool_name || '?'), tid: String(row.tool_call_id || ''),
|
|
210
|
+
ct: ts, input: '', output: out, error: failed ? 'error' : null, dur: 0,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
cur.lastTs = Math.max(cur.lastTs, ts);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// 逐次调用与日志行配对:先按 assistant 行序排,把窗口内的 log call 依次对上;
|
|
218
|
+
// 对不上的日志行(如没落行的失败重试)只贡献 errMsg / 聚合,不冒充一次「已结算调用」。
|
|
219
|
+
const logAll = logBySid.get(s.id) || [];
|
|
220
|
+
for (let i = 0; i < turns.length; i++) {
|
|
221
|
+
const t = turns[i];
|
|
222
|
+
// 窗口互不重叠(上界让开下一轮的下界 start−1s),一次日志调用只计进一轮 ——
|
|
223
|
+
// 1s 是抵日志秒级取整与 DB 毫秒时间戳的错位;宁可「回复后 2s 内又发消息」少计一次,
|
|
224
|
+
// 也不能双计(token 虚增更难查)。最后一轮没有下家,尾巴放宽 10s
|
|
225
|
+
const start = t.time;
|
|
226
|
+
const end = i + 1 < turns.length ? turns[i + 1].time - 2000 : t.lastTs + 10000;
|
|
227
|
+
const inWin = sliceLogCalls(logAll, start - 1000, end);
|
|
228
|
+
const winCalls = inWin.filter(x => x.kind === 'call');
|
|
229
|
+
const rows2 = t._pendingRows || [];
|
|
230
|
+
for (let r = 0; r < rows2.length; r++) addCallFromRow(t, rows2[r].row, winCalls[r] || null, t.model);
|
|
231
|
+
const fail = inWin.find(x => x.kind === 'fail');
|
|
232
|
+
const last = rows2[rows2.length - 1];
|
|
233
|
+
const stopSeen = rows2.some(x => x.row.finish_reason === 'stop');
|
|
234
|
+
t.finished = stopSeen || i + 1 < turns.length || num(s.ended_at) > 0;
|
|
235
|
+
if (!stopSeen && last && t.finished) {
|
|
236
|
+
// 收轮的 assistant 行没有 finish_reason = 失败轮(真库实测:合成分儿 + NULL),错误原文只来自日志
|
|
237
|
+
t.err = true;
|
|
238
|
+
if (fail) t.errMsg = fail.err;
|
|
239
|
+
}
|
|
240
|
+
delete t._pendingRows;
|
|
241
|
+
t.events.push({ k: 'end', t: t.endTs || t.lastTs || t.time, reason: t.err ? 'error' : 'completed' });
|
|
242
|
+
}
|
|
243
|
+
// call/result 事件(i 与 tools[] 同下标,页面直接复用 tools[i] 的出入参)
|
|
244
|
+
for (const t of turns) {
|
|
245
|
+
for (const [i, tool] of t.tools.entries()) {
|
|
246
|
+
t.events.push({ k: 'call', t: tool.ct || t.time, name: tool.name, tid: tool.tid || undefined, i });
|
|
247
|
+
t.events.push({ k: 'result', t: (tool.ct || t.time) + (tool.dur || 0), tid: tool.tid || undefined, i, dur: tool.dur || 0, error: !!tool.error });
|
|
248
|
+
if (tool.error) { t.err = true; if (!t.errMsg) t.errMsg = tool.error; }
|
|
249
|
+
}
|
|
250
|
+
t.events.sort((a, b) => (a.t || 0) - (b.t || 0));
|
|
251
|
+
t.preview = (t.user || t.texts.join('\n\n---\n\n') || t.errMsg || '').replace(/\s+/g, ' ').slice(0, 300);
|
|
252
|
+
}
|
|
253
|
+
return turns;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function dbSignature(dbPath) {
|
|
257
|
+
let dbSt, walSt = null, logSt = null;
|
|
258
|
+
try { dbSt = fs.statSync(dbPath); } catch { return null; }
|
|
259
|
+
try { walSt = fs.statSync(dbPath + '-wal'); } catch {}
|
|
260
|
+
try { logSt = fs.statSync(hermesLogPath(dbPath)); } catch {}
|
|
261
|
+
return {
|
|
262
|
+
m: dbSt.mtimeMs, s: dbSt.size,
|
|
263
|
+
sig: [
|
|
264
|
+
dbSt.mtimeMs + '|' + dbSt.size,
|
|
265
|
+
walSt ? walSt.mtimeMs + '|' + walSt.size : '-',
|
|
266
|
+
logSt ? logSt.mtimeMs + '|' + logSt.size : '-',
|
|
267
|
+
].join('#'),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function hermesLogPath(dbPath) {
|
|
271
|
+
return path.join(path.dirname(dbPath), 'logs', 'agent.log');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function hermesIdBase(dbPath) { return 'hs#' + dbPath.replace(/[\\/:]/g, '~') + '#'; }
|
|
275
|
+
|
|
276
|
+
function emit(agent, dbPath, data, prevData) {
|
|
277
|
+
const base = hermesIdBase(dbPath);
|
|
278
|
+
const ids = new Set();
|
|
279
|
+
data.turns.forEach((t, i) => {
|
|
280
|
+
const id = base + t.sid.replace(/#/g, '~') + '#' + t.key;
|
|
281
|
+
ids.add(id);
|
|
282
|
+
addEntry(id, {
|
|
283
|
+
agent,
|
|
284
|
+
project: t.project,
|
|
285
|
+
session: t.session,
|
|
286
|
+
time: t.time,
|
|
287
|
+
dur: Math.max(0, (t.endTs || t.lastTs || t.time) - t.time),
|
|
288
|
+
status: t.err ? 'error' : 'ok',
|
|
289
|
+
tin: t.tin, tout: t.tout, tcache: t.tcache,
|
|
290
|
+
total: t.tin + t.tout + t.tcache,
|
|
291
|
+
ctx: 0,
|
|
292
|
+
ctxUsed: t.ctxUsed || null,
|
|
293
|
+
rounds: t.callList.length,
|
|
294
|
+
calls: t.callList.length,
|
|
295
|
+
tools: t.tools.length,
|
|
296
|
+
models: t.models,
|
|
297
|
+
preview: t.preview,
|
|
298
|
+
name: t.name || null,
|
|
299
|
+
// I16:子会话谱系(parent_session_id → 父会话 id)。源头显式外键,直接透传。
|
|
300
|
+
parent: t.parentId || undefined, depth: t.parentId ? 1 : undefined,
|
|
301
|
+
finished: !!t.finished,
|
|
302
|
+
aborted: false,
|
|
303
|
+
toolNames: toolNameCounts(t.tools),
|
|
304
|
+
}, { file: dbPath, turn: i, kind: 'hermes' });
|
|
305
|
+
});
|
|
306
|
+
for (const old of prevData?.ids || []) {
|
|
307
|
+
if (ids.has(old)) continue;
|
|
308
|
+
entries.delete(old); srcs.delete(old); removedIds.push(old);
|
|
309
|
+
}
|
|
310
|
+
data.ids = [...ids];
|
|
311
|
+
sorted.cache = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function scanHermes(agent, dbPath) {
|
|
315
|
+
const stat = dbSignature(dbPath);
|
|
316
|
+
if (!stat) { hermesScanErr.set(agent, '数据库不存在:' + dbPath); return; }
|
|
317
|
+
const prev = files.get(dbPath);
|
|
318
|
+
if (prev && prev.data?.sig === stat.sig) {
|
|
319
|
+
const last = prev.data.turns?.[prev.data.turns.length - 1];
|
|
320
|
+
if (last && !entries.has(hermesIdBase(dbPath) + last.sid.replace(/#/g, '~') + '#' + last.key))
|
|
321
|
+
emit(agent, dbPath, prev.data, null);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const mod = sqliteMod();
|
|
325
|
+
if (!mod) { hermesScanErr.set(agent, '当前 Node 没有 node:sqlite(需要 22.5+)'); return; }
|
|
326
|
+
let db = null;
|
|
327
|
+
try {
|
|
328
|
+
db = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
329
|
+
const sessions = db.prepare(
|
|
330
|
+
'SELECT id, model, title, parent_session_id, started_at, ended_at, message_count, cwd, model_config FROM sessions ORDER BY started_at, id'
|
|
331
|
+
).all();
|
|
332
|
+
// active=1:compaction 掉的旧行被置 0;_compressed_summary=1:伪装成 user 的摘要注入行,都不算正文
|
|
333
|
+
const msgs = db.prepare(
|
|
334
|
+
"SELECT id, session_id, role, content, tool_calls, tool_call_id, tool_name, timestamp, finish_reason FROM messages WHERE active = 1 AND COALESCE(_compressed_summary,0) = 0 AND role IN ('user','assistant','tool') ORDER BY session_id, id"
|
|
335
|
+
).all();
|
|
336
|
+
const logBySid = parseHermesAgentLog(hermesLogPath(dbPath));
|
|
337
|
+
const bySession = new Map();
|
|
338
|
+
for (const m of msgs) {
|
|
339
|
+
if (!bySession.has(m.session_id)) bySession.set(m.session_id, []);
|
|
340
|
+
bySession.get(m.session_id).push(m);
|
|
341
|
+
}
|
|
342
|
+
const turns = [];
|
|
343
|
+
for (const s of sessions) {
|
|
344
|
+
const rows = bySession.get(s.id);
|
|
345
|
+
if (!rows || !rows.length) continue;
|
|
346
|
+
for (const t of turnFromMessages(s, rows, logBySid)) turns.push(t);
|
|
347
|
+
// 日志滚掉时的兜底:会话级 _usage_anchor(model_config 里,代表最后一次调用)只摊给最后一轮,
|
|
348
|
+
// 其余轮保持 0 —— 宁可缺,不给每轮摊一个假数字
|
|
349
|
+
const tail = turns.filter(t => t.sid === s.id).pop();
|
|
350
|
+
if (tail && !tail.ctxUsed) {
|
|
351
|
+
const anchor = safeJson(s.model_config)?._usage_anchor;
|
|
352
|
+
if (anchor && num(anchor.prompt_tokens)) tail.ctxUsed = num(anchor.prompt_tokens);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const data = { sig: stat.sig, turns, rev: (prev?.data?.rev || 0) + 1 };
|
|
356
|
+
files.set(dbPath, { agent, kind: 'hermes', m: stat.m, s: stat.s, off: 0, data });
|
|
357
|
+
hermesScanErr.delete(agent);
|
|
358
|
+
emit(agent, dbPath, data, prev?.data);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error('[hermes]', e.message);
|
|
361
|
+
hermesScanErr.set(agent, e.message);
|
|
362
|
+
} finally {
|
|
363
|
+
try { db?.close(); } catch {}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function hermesEntryContent(src, full) {
|
|
368
|
+
const st = files.get(src.file);
|
|
369
|
+
const t = st?.data?.turns?.[src.turn];
|
|
370
|
+
if (!t) return null;
|
|
371
|
+
const tools = t.tools.map(x => {
|
|
372
|
+
const fi = {}, fo = {};
|
|
373
|
+
return {
|
|
374
|
+
name: x.name, tid: x.tid, error: x.error, dur: x.dur || 0,
|
|
375
|
+
input: trunc(x.input, full, fi), inputTrunc: !!fi.t,
|
|
376
|
+
output: trunc(x.output, full, fo), outputTrunc: !!fo.t,
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
const calls = t.callList.map(c => {
|
|
380
|
+
if (!c.text) return { ...c };
|
|
381
|
+
const ft = {};
|
|
382
|
+
const text = trunc(c.text, full, ft);
|
|
383
|
+
return ft.t ? { ...c, text, textTrunc: true } : { ...c };
|
|
384
|
+
});
|
|
385
|
+
const out = {
|
|
386
|
+
user: t.user,
|
|
387
|
+
assistant: t.texts.join('\n\n---\n\n'),
|
|
388
|
+
tools, calls,
|
|
389
|
+
events: t.events || [],
|
|
390
|
+
others: (t.others || []).map(o => ({ type: o.type, t: o.t, json: full ? o.json : o.json.slice(0, 800) })),
|
|
391
|
+
v: 'h' + (st.data.rev || 0),
|
|
392
|
+
};
|
|
393
|
+
if (t.err) out.callsNote = '这一轮没有成功拿到模型回复' + (t.errMsg ? ':' + t.errMsg : '(agent.log 里可见报错原文)');
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 判据在 discovery.mjs(hermesDbVerify:sqlite 页头 + sessions/messages/session_model_usage 三张独有表),
|
|
398
|
+
// 与 opencode/zcode 同一处摆放 —— 嗅探每 3s 走一轮,读盘量在那里控制。
|
package/parsers/kilo.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ---------------- KiloCode(Kilo CLI)SQLite 解析器 ----------------
|
|
2
|
+
// 数据源:~/.local/share/kilo/kilo.db(明文 SQLite,WAL 模式;本机实测 Windows 也在这个 XDG 路径,
|
|
3
|
+
// 既不是 %APPDATA%\kilo 也不是 ~/.kilo)—— 同目录另有 kilo.db-wal / kilo.db-shm、log/、repos/、storage/。
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ Kilo 是 OpenCode 的 **fork**(本机 log/ 目录里直接叫 opencode.log),schema 与口径实测完全同构:
|
|
6
|
+
// session / message(data JSON) / part(data JSON) + event / event_sequence 是同一套;
|
|
7
|
+
// kilo_board / kilo_board_message(多 agent 协作板)与 session_context_epoch 是 kilo 自家扩展表;
|
|
8
|
+
// Kilo 保留了 opencode 的扩展表(credential / project_directory / session_input / session_message),
|
|
9
|
+
// 这点与 mimocode 相反(mimocode 把它们换成了 history_fts_idx / actor_registry 等)。
|
|
10
|
+
// 从目录结构看也是 opencode 那套:storage/session_diff/ses_*.json 是文件式 diff 存储。
|
|
11
|
+
//
|
|
12
|
+
// 所以核心解析**不重复实现**,直接用 opencode.mjs 的家族共享实现(opencodeFamilyApi)。
|
|
13
|
+
// 复用前已核对口径真的相同(本机实测样本 ses_f2ec0aeb0ffeupLomLTPbkTn7t,5 步 assistant):
|
|
14
|
+
// 每条 message.data.tokens 是**该次调用**的用量(input 逐条递增是上下文在长,不是累计),
|
|
15
|
+
// 逐条相加 == session 表的聚合值:
|
|
16
|
+
// input 16979+17163+17715+21306+26213 = 99376 = session.tokens_input ✓
|
|
17
|
+
// output 304 / reasoning 1142 同样对得上 ✓
|
|
18
|
+
// 轮语义也一致(一条 user message 开一轮,轮内多条 assistant message = 多步工具循环 → rounds)。
|
|
19
|
+
//
|
|
20
|
+
// 唯一必须自己处理的是**认领**(见 discovery.mjs):kilo.db 满足 opencodeDbVerify 的全部五个条件,
|
|
21
|
+
// 光按表认会被 opencode 探针接走(本机实测),所以 kilo 有自己的独有表判据 KILO_ONLY_TABLES,
|
|
22
|
+
// 且 sniffBase 里排在 opencodeDbFile 之前;opencodeDbVerify 也反向排除了 kilo 的独有表。
|
|
23
|
+
//
|
|
24
|
+
// 不在 OFF_KINDS 里(SQLite 是实时写入的活源,按字节偏移续读不可靠;同 opencode/zcode/dsh):
|
|
25
|
+
// 每次启动整份重解析,库没变时按 {mtime,size} 签名跳过。PARSER_REV['kilo'] 仍然登记 —— 它在这里
|
|
26
|
+
// 不管 index 落盘(本 kind 不落),管的是 ~/.agent-acta/search/ 全文索引分片的作废。
|
|
27
|
+
import { opencodeFamilyApi } from './opencode.mjs';
|
|
28
|
+
|
|
29
|
+
export const kiloScanErr = new Map();
|
|
30
|
+
|
|
31
|
+
// idPrefix 必须与 opencode 的 'oc#' 错开:两家若被指到同一个库(手工误填),
|
|
32
|
+
// 前缀相同会让后扫的那家把前一家的条目按 id 顶掉。
|
|
33
|
+
const family = opencodeFamilyApi({
|
|
34
|
+
kind: 'kilo',
|
|
35
|
+
idPrefix: 'kl#',
|
|
36
|
+
tag: '[kilo]',
|
|
37
|
+
errMap: kiloScanErr,
|
|
38
|
+
verPrefix: 'kl',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function scanKilo(agent, dbPath) { return family.scan(agent, dbPath); }
|
|
42
|
+
export function kiloEntryContent(src, full) { return family.entryContent(src, full); }
|