@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,465 @@
|
|
|
1
|
+
// ---------------- OpenCode SQLite 解析器(同时是两个 fork 的共享实现) ----------------
|
|
2
|
+
// 数据源:~/.local/share/opencode/opencode.db(明文 SQLite,WAL 模式)
|
|
3
|
+
//
|
|
4
|
+
// OpenCode 与 zcode 一样是 SQLite 文件型源头,但 schema/口径不同:
|
|
5
|
+
// session(id, project_id, parent_id, directory, path, title, agent, model, time_created/updated)
|
|
6
|
+
// message(id, session_id, time_created/updated, data JSON)
|
|
7
|
+
// part(id, message_id, session_id, time_created/updated, data JSON)
|
|
8
|
+
// event/event_sequence 是同一份状态的事件投影,不作为第二条数据源重复解析。
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ 本文件是 **opencode 家族**的共享实现:KiloCode(Kilo CLI)是 opencode 的 fork,schema 与
|
|
11
|
+
// 口径实测完全同构(session/message/part 三表 + 同一套 message.data.tokens 语义),差的只是
|
|
12
|
+
// 库路径、日志文件名和几张自家扩展表。所以核心逻辑只写一份,由 `FAMILY` 描述符区分来源——
|
|
13
|
+
// kind / idPrefix(条目 id 前缀,两家必须错开,否则同一 db 路径会互相顶掉 entries)/
|
|
14
|
+
// tag(错误日志前缀)/ errMap(各家的「最近一次读取失败」映射)/ verPrefix(详情版本号前缀)。
|
|
15
|
+
// 入口:scanOpencode + opencodeEntryContent(opencode 本体);parsers/kilo.mjs 薄壳拿同一份核心。
|
|
16
|
+
// ⚠️ 复用的前提是**口径真的相同**:接新 fork 前先核对 token 求和关系(每条 message 的 tokens 是
|
|
17
|
+
// 该次调用的用量,逐条相加 == session.tokens_* 聚合),不同就另写解析器,别硬套。
|
|
18
|
+
//
|
|
19
|
+
// 只读纪律:DatabaseSync 一律 {readOnly:true},签名包含 db + -wal 的 mtime/size;
|
|
20
|
+
// 签名变化时整库重读,库没变时跳过。本家族不进入 OFF_KINDS(活源,按字节续读不可靠),
|
|
21
|
+
// 但 PARSER_REV 仍登记(只管 ~/.agent-acta/search/ 那份全文索引分片的作废)。
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import {
|
|
24
|
+
files, entries, srcs, removedIds, sorted, addEntry,
|
|
25
|
+
toolNameCounts, modelName, toText, trunc, sqliteMod,
|
|
26
|
+
} from './shared.mjs';
|
|
27
|
+
|
|
28
|
+
export const opencodeScanErr = new Map();
|
|
29
|
+
|
|
30
|
+
function safeJson(raw) {
|
|
31
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function num(v) {
|
|
35
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function textOf(v) {
|
|
39
|
+
return toText(v).trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function messageData(raw) {
|
|
43
|
+
const d = safeJson(raw);
|
|
44
|
+
return d && typeof d === 'object' ? d : {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function partTime(d, row, fallback) {
|
|
48
|
+
return num(d?.time?.start) || num(d?.time?.created) || num(row?.time_created) || fallback || 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function partEndTime(d, row, start) {
|
|
52
|
+
return num(d?.time?.end) || num(d?.time?.completed) || num(row?.time_updated) || start || 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tokenFields(tokens) {
|
|
56
|
+
const t = tokens && typeof tokens === 'object' ? tokens : {};
|
|
57
|
+
const cache = t.cache && typeof t.cache === 'object' ? t.cache : {};
|
|
58
|
+
return {
|
|
59
|
+
tin: num(t.input),
|
|
60
|
+
tout: num(t.output) + num(t.reasoning),
|
|
61
|
+
tcache: num(cache.read) + num(cache.write),
|
|
62
|
+
rawInput: num(t.input),
|
|
63
|
+
rawOutput: num(t.output),
|
|
64
|
+
reasoning: num(t.reasoning),
|
|
65
|
+
cacheRead: num(cache.read),
|
|
66
|
+
cacheWrite: num(cache.write),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function statusFromMessage(d) {
|
|
71
|
+
const finish = String(d?.finish || '').toLowerCase();
|
|
72
|
+
const err = d?.error;
|
|
73
|
+
const aborted = ['abort', 'aborted', 'cancel', 'canceled', 'cancelled'].some(x => finish.includes(x));
|
|
74
|
+
const failed = !!err || ['error', 'failed', 'failure'].some(x => finish.includes(x));
|
|
75
|
+
return { failed, aborted, finish };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function errorText(d) {
|
|
79
|
+
return textOf(d?.error?.data?.message || d?.error?.message || d?.error?.name || d?.error || '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function turnFor(turns, byKey, sid, key, ts, session) {
|
|
83
|
+
const k = sid + '\u0000' + key;
|
|
84
|
+
let t = byKey.get(k);
|
|
85
|
+
if (!t) {
|
|
86
|
+
const title = String(session?.title || '').trim();
|
|
87
|
+
t = {
|
|
88
|
+
sid,
|
|
89
|
+
key,
|
|
90
|
+
time: ts || 0,
|
|
91
|
+
lastTs: ts || 0,
|
|
92
|
+
endTs: 0,
|
|
93
|
+
project: session?.directory || session?.path || '(未知项目)',
|
|
94
|
+
name: (session?.parent_id ? '⤷ ' : '') + (title || ''),
|
|
95
|
+
parentId: session?.parent_id || null,
|
|
96
|
+
user: '',
|
|
97
|
+
texts: [],
|
|
98
|
+
tools: [],
|
|
99
|
+
byTool: new Map(),
|
|
100
|
+
callList: [],
|
|
101
|
+
models: [],
|
|
102
|
+
events: [],
|
|
103
|
+
others: [],
|
|
104
|
+
tin: 0,
|
|
105
|
+
tout: 0,
|
|
106
|
+
tcache: 0,
|
|
107
|
+
ctxUsed: 0,
|
|
108
|
+
err: false,
|
|
109
|
+
errMsg: '',
|
|
110
|
+
aborted: false,
|
|
111
|
+
finished: false,
|
|
112
|
+
};
|
|
113
|
+
byKey.set(k, t);
|
|
114
|
+
turns.push(t);
|
|
115
|
+
}
|
|
116
|
+
if (ts) {
|
|
117
|
+
t.time = t.time ? Math.min(t.time, ts) : ts;
|
|
118
|
+
t.lastTs = Math.max(t.lastTs, ts);
|
|
119
|
+
}
|
|
120
|
+
return t;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addOther(t, type, when, value) {
|
|
124
|
+
let json;
|
|
125
|
+
try { json = typeof value === 'string' ? value : JSON.stringify(value); }
|
|
126
|
+
catch { json = '[无法序列化的值]'; }
|
|
127
|
+
t.others.push({ type, t: when || t.time, json: String(json || '').slice(0, 2000) });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function addTool(t, d, row, fallbackTs) {
|
|
131
|
+
const state = d.state && typeof d.state === 'object' ? d.state : {};
|
|
132
|
+
// 工具的真实起止在 state.time(实测工具 part 顶层没有 time),消息行时间只是兜底
|
|
133
|
+
const st = state.time && typeof state.time === 'object' ? state.time : {};
|
|
134
|
+
const tid = d.callID || d.callId || d.toolCallID || null;
|
|
135
|
+
const start = num(st.start) || partTime(d, row, fallbackTs);
|
|
136
|
+
const end = num(st.end) || partEndTime(d, row, start);
|
|
137
|
+
const status = String(state.status || d.status || '').toLowerCase();
|
|
138
|
+
const error = status && !['completed', 'complete', 'success', 'succeeded', 'done'].includes(status)
|
|
139
|
+
? textOf(state.error || state.message || status)
|
|
140
|
+
: null;
|
|
141
|
+
let tool = tid ? t.byTool.get(String(tid)) : null;
|
|
142
|
+
if (!tool) {
|
|
143
|
+
tool = {
|
|
144
|
+
name: String(d.tool || d.name || '?'),
|
|
145
|
+
tid: tid ? String(tid) : null,
|
|
146
|
+
pid: row?.id || null, // 来源 part:没有 callID 的工具靠它避免被重复登记一次
|
|
147
|
+
ct: start,
|
|
148
|
+
input: state.input ?? d.input ?? '',
|
|
149
|
+
output: state.output ?? state.result ?? d.output ?? '',
|
|
150
|
+
error,
|
|
151
|
+
dur: start && end ? Math.max(0, end - start) : 0,
|
|
152
|
+
};
|
|
153
|
+
t.tools.push(tool);
|
|
154
|
+
if (tool.tid) t.byTool.set(tool.tid, tool);
|
|
155
|
+
} else {
|
|
156
|
+
if (state.input != null) tool.input = state.input;
|
|
157
|
+
if (state.output != null) tool.output = state.output;
|
|
158
|
+
if (error) tool.error = error;
|
|
159
|
+
if (start) tool.ct = Math.min(tool.ct || start, start);
|
|
160
|
+
if (start && end) tool.dur = Math.max(tool.dur || 0, end - start);
|
|
161
|
+
}
|
|
162
|
+
return tool;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function addAssistantCall(t, msg, d, parts) {
|
|
166
|
+
const created = num(d?.time?.created) || num(msg.time_created) || t.time;
|
|
167
|
+
const completed = num(d?.time?.completed) || num(msg.time_updated) || created;
|
|
168
|
+
const token = tokenFields(d?.tokens);
|
|
169
|
+
const status = statusFromMessage(d);
|
|
170
|
+
const mdl = modelName(d?.modelID || d?.model?.modelID || d?.model || '');
|
|
171
|
+
const textParts = [];
|
|
172
|
+
let firstOutput = 0;
|
|
173
|
+
for (const p of parts) {
|
|
174
|
+
const pd = p.data;
|
|
175
|
+
if (!pd) continue;
|
|
176
|
+
// 首 token 时刻取本调用**最早一段内容**(reasoning 或 text)的起点:
|
|
177
|
+
// 只认 text 的话,先思考后回答的那种调用 ttft 会被算成接近 dur,看着像「没有排队时间」
|
|
178
|
+
if (pd.type === 'text' || pd.type === 'reasoning') {
|
|
179
|
+
const pt = partTime(pd, p, created);
|
|
180
|
+
if (pt && pt >= created && (!firstOutput || pt < firstOutput)) firstOutput = pt;
|
|
181
|
+
}
|
|
182
|
+
if (pd.type === 'text') {
|
|
183
|
+
const s = textOf(pd.text);
|
|
184
|
+
if (s) textParts.push(s);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const firstToken = firstOutput && firstOutput >= created ? firstOutput : 0;
|
|
188
|
+
const call = {
|
|
189
|
+
model: mdl || '(未知模型)',
|
|
190
|
+
provider: modelName(d?.providerID || ''),
|
|
191
|
+
tin: token.tin,
|
|
192
|
+
tout: token.tout,
|
|
193
|
+
tcache: token.tcache,
|
|
194
|
+
dur: completed >= created ? completed - created : 0,
|
|
195
|
+
ttft: firstToken ? Math.max(0, firstToken - created) : 0,
|
|
196
|
+
finishReason: d?.finish && d.finish !== 'stop' ? String(d.finish) : undefined,
|
|
197
|
+
};
|
|
198
|
+
if (textParts.length) call.text = textParts.join('\n');
|
|
199
|
+
if (status.failed) call.error = errorText(d) || status.finish || 'error';
|
|
200
|
+
const names = [];
|
|
201
|
+
for (const p of parts) {
|
|
202
|
+
if (p.data?.type !== 'tool') continue;
|
|
203
|
+
// 工具已经在 parsePart 里登记过(可能没有 callID,所以不能靠 tid 找):按来源 part 认领,不重复登记
|
|
204
|
+
const tid = p.data.callID || p.data.callId || p.data.toolCallID || null;
|
|
205
|
+
const tool = t.tools.find(x => x.pid === p.row.id) || (tid ? t.byTool.get(String(tid)) : null);
|
|
206
|
+
if (tool?.name) names.push(tool.name);
|
|
207
|
+
}
|
|
208
|
+
if (names.length) call.tools = names;
|
|
209
|
+
t.callList.push(call);
|
|
210
|
+
if (mdl && !t.models.includes(mdl)) t.models.push(mdl);
|
|
211
|
+
t.tin += token.tin;
|
|
212
|
+
t.tout += token.tout;
|
|
213
|
+
t.tcache += token.tcache;
|
|
214
|
+
const ctx = token.tin + token.tcache;
|
|
215
|
+
if (ctx > 0) t.ctxUsed = ctx;
|
|
216
|
+
if (created) t.time = t.time ? Math.min(t.time, created) : created;
|
|
217
|
+
t.lastTs = Math.max(t.lastTs, completed, created);
|
|
218
|
+
t.endTs = Math.max(t.endTs, completed);
|
|
219
|
+
if (status.failed) {
|
|
220
|
+
t.err = true;
|
|
221
|
+
if (!t.errMsg) t.errMsg = errorText(d) || call.error || '';
|
|
222
|
+
}
|
|
223
|
+
if (status.aborted) t.aborted = true;
|
|
224
|
+
if (completed || d?.finish) t.finished = true;
|
|
225
|
+
t.events.push({
|
|
226
|
+
k: 'llm', t: created, ttft: call.ttft, dur: firstToken && completed >= firstToken ? completed - firstToken : call.dur,
|
|
227
|
+
model: mdl || '', tin: call.tin, tout: call.tout, tcache: call.tcache,
|
|
228
|
+
err: !!status.failed, blocks: call.text ? ['text'] : [], finish: d?.finish || '',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parsePart(t, msg, p, role) {
|
|
233
|
+
const d = safeJson(p.data);
|
|
234
|
+
const when = num(p.time_created) || num(msg.time_created) || t.time;
|
|
235
|
+
if (!d) { addOther(t, '(损坏的 JSON)', when, String(p.data || '')); return; }
|
|
236
|
+
const type = typeof d.type === 'string' ? d.type : '(无 type)';
|
|
237
|
+
if (type === 'text') {
|
|
238
|
+
const s = textOf(d.text);
|
|
239
|
+
if (!s) return;
|
|
240
|
+
if (role === 'user') { if (!t.user) t.user = s; }
|
|
241
|
+
else if (role === 'assistant') t.texts.push(s);
|
|
242
|
+
else addOther(t, role + ' 文本', when, s);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (type === 'tool') {
|
|
246
|
+
addTool(t, d, p, when);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (type === 'step-start') {
|
|
250
|
+
t.events.push({ k: 'step', t: when, ph: 'start' });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (type === 'step-finish') {
|
|
254
|
+
addOther(t, type, when, { reason: d.reason, tokens: d.tokens || null, cost: d.cost });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (type === 'reasoning') {
|
|
258
|
+
// 推理正文不混入 assistant 输出,但保留在详情 others 中,便于完整查看。
|
|
259
|
+
addOther(t, type, when, { text: textOf(d.text), len: textOf(d.text).length });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
addOther(t, type, when, d);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function finalizeTurn(t, index, count) {
|
|
266
|
+
const tools = t.tools;
|
|
267
|
+
for (const [i, tool] of tools.entries()) {
|
|
268
|
+
const ct = tool.ct || t.time;
|
|
269
|
+
t.events.push({ k: 'call', t: ct, name: tool.name, tid: tool.tid, i });
|
|
270
|
+
t.events.push({ k: 'result', t: ct + (tool.dur || 0), tid: tool.tid, i, dur: tool.dur || 0, error: !!tool.error });
|
|
271
|
+
// 工具失败也算这一轮出错(与 zcode 同口径);用户主动打断仍只标 aborted,不混进 error 红
|
|
272
|
+
if (tool.error) { t.err = true; if (!t.errMsg) t.errMsg = String(tool.error); }
|
|
273
|
+
}
|
|
274
|
+
t.events.push({ k: 'end', t: t.endTs || t.lastTs || t.time, reason: t.err ? 'error' : (t.aborted ? 'canceled' : 'completed') });
|
|
275
|
+
t.events.sort((a, b) => (a.t || 0) - (b.t || 0));
|
|
276
|
+
t.preview = (t.user || t.texts.join('\n\n---\n\n') || t.errMsg || '').replace(/\s+/g, ' ').slice(0, 300);
|
|
277
|
+
t.finished = !!t.finished || index < count - 1;
|
|
278
|
+
t.user = t.user.slice(0, 3000);
|
|
279
|
+
t.assistant = t.texts.join('\n\n---\n\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function dbSignature(dbPath) {
|
|
283
|
+
let dbSt, walSt = null;
|
|
284
|
+
try { dbSt = fs.statSync(dbPath); } catch { return null; }
|
|
285
|
+
try { walSt = fs.statSync(dbPath + '-wal'); } catch {}
|
|
286
|
+
return {
|
|
287
|
+
m: dbSt.mtimeMs,
|
|
288
|
+
s: dbSt.size,
|
|
289
|
+
sig: dbSt.mtimeMs + '|' + dbSt.size + '|' + (walSt ? walSt.mtimeMs + '|' + walSt.size : '-'),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 条目 id 前缀由各家自己给(fam.idPrefix):同一路径的同一个库若被两家同时登记,
|
|
294
|
+
// 前缀相同会让后扫的那家把前一家留在 entries 里的条目按 id 顶掉(见 file 头)。
|
|
295
|
+
function dbIdBase(prefix, dbPath) { return prefix + dbPath.replace(/[\\/:]/g, '~') + '#'; }
|
|
296
|
+
|
|
297
|
+
function emit(agent, dbPath, data, prevData, fam) {
|
|
298
|
+
const base = dbIdBase(fam.idPrefix, dbPath);
|
|
299
|
+
const ids = new Set();
|
|
300
|
+
data.turns.forEach((t, i) => {
|
|
301
|
+
const id = base + t.sid.replace(/#/g, '~') + '#' + t.key;
|
|
302
|
+
ids.add(id);
|
|
303
|
+
addEntry(id, {
|
|
304
|
+
agent,
|
|
305
|
+
project: t.project,
|
|
306
|
+
session: t.sid,
|
|
307
|
+
time: t.time,
|
|
308
|
+
dur: Math.max(0, (t.endTs || t.lastTs || t.time) - t.time),
|
|
309
|
+
status: t.err ? 'error' : (t.aborted ? 'canceled' : 'ok'),
|
|
310
|
+
tin: t.tin,
|
|
311
|
+
tout: t.tout,
|
|
312
|
+
tcache: t.tcache,
|
|
313
|
+
total: t.tin + t.tout + t.tcache,
|
|
314
|
+
ctx: 0,
|
|
315
|
+
ctxUsed: t.ctxUsed || null,
|
|
316
|
+
rounds: t.callList.length,
|
|
317
|
+
calls: t.callList.length,
|
|
318
|
+
tools: t.tools.length,
|
|
319
|
+
models: t.models,
|
|
320
|
+
preview: t.preview,
|
|
321
|
+
name: t.name || null,
|
|
322
|
+
// I16:子会话谱系(session.parent_id → 父会话 id)。源头显式外键(kilo 是它的 fork,同一份口径)。
|
|
323
|
+
parent: t.parentId || undefined, depth: t.parentId ? 1 : undefined,
|
|
324
|
+
finished: !!t.finished,
|
|
325
|
+
aborted: !!t.aborted,
|
|
326
|
+
toolNames: toolNameCounts(t.tools),
|
|
327
|
+
}, { file: dbPath, turn: i, kind: fam.kind });
|
|
328
|
+
});
|
|
329
|
+
for (const old of prevData?.ids || []) {
|
|
330
|
+
if (ids.has(old)) continue;
|
|
331
|
+
entries.delete(old); srcs.delete(old); removedIds.push(old);
|
|
332
|
+
}
|
|
333
|
+
data.ids = [...ids];
|
|
334
|
+
sorted.cache = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 家族共享的扫描主体:opencode 本体与 kilo 都走这里,差异全在 fam 描述符里。
|
|
338
|
+
function scanOpencodeLike(agent, dbPath, fam) {
|
|
339
|
+
const stat = dbSignature(dbPath);
|
|
340
|
+
if (!stat) { fam.errMap.set(agent, '数据库不存在:' + dbPath); return; }
|
|
341
|
+
const prev = files.get(dbPath);
|
|
342
|
+
if (prev && prev.data?.sig === stat.sig) {
|
|
343
|
+
const last = prev.data.turns?.[prev.data.turns.length - 1];
|
|
344
|
+
if (last && !entries.has(dbIdBase(fam.idPrefix, dbPath) + last.sid.replace(/#/g, '~') + '#' + last.key)) emit(agent, dbPath, prev.data, null, fam);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const mod = sqliteMod();
|
|
348
|
+
if (!mod) { fam.errMap.set(agent, '当前 Node 没有 node:sqlite(需要 22.5+)'); return; }
|
|
349
|
+
let db = null;
|
|
350
|
+
try {
|
|
351
|
+
db = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
352
|
+
const qAll = (sql) => { try { return db.prepare(sql).all(); } catch { return null; } };
|
|
353
|
+
const sessions = qAll('SELECT id, project_id, parent_id, directory, path, title, agent, model, time_created, time_updated FROM session ORDER BY time_created, id');
|
|
354
|
+
if (!sessions) throw new Error('读不到 session 表(schema 不兼容或库被锁定)');
|
|
355
|
+
const messages = qAll('SELECT id, session_id, time_created, time_updated, data FROM message ORDER BY session_id, time_created, id') || [];
|
|
356
|
+
const parts = qAll('SELECT id, message_id, session_id, time_created, time_updated, data FROM part ORDER BY session_id, time_created, id') || [];
|
|
357
|
+
const sessionById = new Map(sessions.map(s => [s.id, s]));
|
|
358
|
+
const partsByMsg = new Map();
|
|
359
|
+
for (const row of parts) {
|
|
360
|
+
const d = safeJson(row.data);
|
|
361
|
+
const item = { row, data: d };
|
|
362
|
+
if (!partsByMsg.has(row.message_id)) partsByMsg.set(row.message_id, []);
|
|
363
|
+
partsByMsg.get(row.message_id).push(item);
|
|
364
|
+
}
|
|
365
|
+
const messagesBySession = new Map();
|
|
366
|
+
for (const row of messages) {
|
|
367
|
+
if (!messagesBySession.has(row.session_id)) messagesBySession.set(row.session_id, []);
|
|
368
|
+
messagesBySession.get(row.session_id).push(row);
|
|
369
|
+
}
|
|
370
|
+
const turns = [];
|
|
371
|
+
const byKey = new Map();
|
|
372
|
+
for (const [sid, rows] of messagesBySession) {
|
|
373
|
+
const session = sessionById.get(sid) || { id: sid, directory: '', path: '', title: '' };
|
|
374
|
+
let current = null;
|
|
375
|
+
for (const row of rows) {
|
|
376
|
+
const d = messageData(row.data);
|
|
377
|
+
const role = String(d.role || '');
|
|
378
|
+
const ts = num(d.time?.created) || num(row.time_created);
|
|
379
|
+
if (role === 'user' || !current) {
|
|
380
|
+
const key = role === 'user' ? row.id : 'pre#' + row.id;
|
|
381
|
+
current = turnFor(turns, byKey, sid, key, ts, session);
|
|
382
|
+
if (role === 'user') current.events.push({ k: 'user', t: ts });
|
|
383
|
+
}
|
|
384
|
+
if (ts) current.lastTs = Math.max(current.lastTs, ts);
|
|
385
|
+
if (num(d.time?.completed)) current.endTs = Math.max(current.endTs, num(d.time.completed));
|
|
386
|
+
const p = partsByMsg.get(row.id) || [];
|
|
387
|
+
for (const item of p) parsePart(current, { ...row, role }, item.row, role);
|
|
388
|
+
if (role === 'assistant') addAssistantCall(current, row, d, p);
|
|
389
|
+
const msgStatus = statusFromMessage(d);
|
|
390
|
+
if (d.error && !current.errMsg) current.errMsg = errorText(d);
|
|
391
|
+
if (msgStatus.failed) current.err = true;
|
|
392
|
+
if (msgStatus.aborted) current.aborted = true;
|
|
393
|
+
if (msgStatus.finish || num(d.time?.completed)) current.finished = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (let i = 0; i < turns.length; i++) finalizeTurn(turns[i], i, turns.length);
|
|
397
|
+
const liveTurns = turns.filter(t => t.user || t.texts.length || t.tools.length || t.callList.length || t.errMsg || t.others.length);
|
|
398
|
+
const data = { sig: stat.sig, turns: liveTurns, rev: (prev?.data?.rev || 0) + 1 };
|
|
399
|
+
files.set(dbPath, { agent, kind: fam.kind, m: stat.m, s: stat.s, off: 0, data });
|
|
400
|
+
fam.errMap.delete(agent);
|
|
401
|
+
emit(agent, dbPath, data, prev?.data, fam);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.error(fam.tag, e.message);
|
|
404
|
+
fam.errMap.set(agent, e.message);
|
|
405
|
+
} finally {
|
|
406
|
+
try { db?.close(); } catch {}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function familyEntryContent(src, full, fam) {
|
|
411
|
+
const st = files.get(src.file);
|
|
412
|
+
const t = st?.data?.turns?.[src.turn];
|
|
413
|
+
if (!t) return null;
|
|
414
|
+
const tools = t.tools.map(x => {
|
|
415
|
+
const fi = {}, fo = {};
|
|
416
|
+
return {
|
|
417
|
+
name: x.name,
|
|
418
|
+
tid: x.tid,
|
|
419
|
+
error: x.error,
|
|
420
|
+
dur: x.dur || 0,
|
|
421
|
+
input: trunc(x.input, full, fi),
|
|
422
|
+
inputTrunc: !!fi.t,
|
|
423
|
+
output: trunc(x.output, full, fo),
|
|
424
|
+
outputTrunc: !!fo.t,
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
const calls = t.callList.map(c => {
|
|
428
|
+
if (!c.text) return { ...c };
|
|
429
|
+
const ft = {};
|
|
430
|
+
const text = trunc(c.text, full, ft);
|
|
431
|
+
return ft.t ? { ...c, text, textTrunc: true } : { ...c };
|
|
432
|
+
});
|
|
433
|
+
const out = {
|
|
434
|
+
user: t.user,
|
|
435
|
+
assistant: t.assistant,
|
|
436
|
+
tools,
|
|
437
|
+
calls,
|
|
438
|
+
events: t.events || [],
|
|
439
|
+
others: (t.others || []).map(o => ({ type: o.type, t: o.t, json: full ? o.json : o.json.slice(0, 800) })),
|
|
440
|
+
v: fam.verPrefix + (st.data.rev || 0),
|
|
441
|
+
};
|
|
442
|
+
if (t.err && t.errMsg) out.callsNote = '这一轮没有成功拿到模型回复:' + t.errMsg;
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- 家族描述符与对外入口 ----
|
|
447
|
+
// opencode 本体的对外接口(server 在用):行为与重构前逐字一致。
|
|
448
|
+
const OPENCODE_FAMILY = {
|
|
449
|
+
kind: 'opencode',
|
|
450
|
+
idPrefix: 'oc#',
|
|
451
|
+
tag: '[opencode]',
|
|
452
|
+
errMap: opencodeScanErr,
|
|
453
|
+
verPrefix: 'oc',
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
export function scanOpencode(agent, dbPath) { return scanOpencodeLike(agent, dbPath, OPENCODE_FAMILY); }
|
|
457
|
+
export function opencodeEntryContent(src, full) { return familyEntryContent(src, full, OPENCODE_FAMILY); }
|
|
458
|
+
|
|
459
|
+
// 供 fork(parsers/kilo.mjs)复用同一份核心:只暴露描述符化的两个入口,不暴露内部状态。
|
|
460
|
+
export function opencodeFamilyApi(fam) {
|
|
461
|
+
return {
|
|
462
|
+
scan: (agent, dbPath) => scanOpencodeLike(agent, dbPath, fam),
|
|
463
|
+
entryContent: (src, full) => familyEntryContent(src, full, fam),
|
|
464
|
+
};
|
|
465
|
+
}
|