@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,531 @@
|
|
|
1
|
+
// ---------------- mimocode 型解析器(记忆/检查点类 agent,真源是 mimocode.db SQLite) ----------------
|
|
2
|
+
// 从其它 agent 的解析器套用同一套接口(addEntry / files / entries / srcs / markKind),纯新增、不改动主文件其它路径。
|
|
3
|
+
// 导出 scanMimocode / mimocodeEntryContent。
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ 重要修正:mimocode 真正的会话数据在 **SQLite 库**(`mimocode.db`,Drizzle ORM 之上),
|
|
6
|
+
// 不是 markdown。用户给的那几个 checkpoint.md / notes.md / progress.md 是由库导出的展示层、
|
|
7
|
+
// 且本机实测 task 表为空(任务目前只存在于 markdown)。所以**以 mimocode.db 为权威源**,
|
|
8
|
+
// markdown 落盘的那几份仅在「SQLite 库读不动」时作为只读兜底回退(见下面 scanMimocode 的分支)。
|
|
9
|
+
//
|
|
10
|
+
// SQLite 真源的数据模型(本机实测 mimocode.db):
|
|
11
|
+
// session : 一次会话;id / title / directory / project_id / time_created / time_updated
|
|
12
|
+
// message : 一条消息(**不是**一轮);data = {role, agent, time:{created,completed},
|
|
13
|
+
// model:{providerID,modelID}(user) 或顶层 modelID/providerID(assistant),
|
|
14
|
+
// tokens:{input,output,reasoning,cache:{read,write}}, cost, finish, error, ...}
|
|
15
|
+
// part : 一条 message 的内容碎片;data.type ∈ {text, reasoning, tool, step-start, step-finish};
|
|
16
|
+
// tool 碎片带 callID / tool / state:{status,input,output} / time:{start,end}
|
|
17
|
+
// project : id / worktree(如 F:\centos\next-admin)/ name
|
|
18
|
+
// task / history_fts 等:task 常为空;history_fts 是库自己的搜索索引(不另取)
|
|
19
|
+
//
|
|
20
|
+
// 映射(**一条 user message 开一轮 = AgentActa 一条 entry**):
|
|
21
|
+
// ⚠️ 这是本文件的核心口径,别退回「一条 message 一条 entry」:mimocode 的 agent 循环是
|
|
22
|
+
// 「想一步 → 调工具 → 看结果 → 再想一步」,一次提问会落 **十几到几十条 assistant message**
|
|
23
|
+
// (本机实测:一句「hello,我能去哪找你的应用图标的svg」落了 19 条 assistant、20 次工具调用)。
|
|
24
|
+
// 若每条 message 各灌一条 entry,页面一张卡就变 20 条,与 claude/opencode/zcode 的「轮」语义也对不上。
|
|
25
|
+
// · session = session.id;project = project.worktree 或会话自己的 directory
|
|
26
|
+
// · rounds = 轮内 LLM 调用数(= 轮内 assistant message 数);calls 同值;tools = 轮内 tool 碎片数
|
|
27
|
+
// · time = 轮内最早 message.time.created(**毫秒**,全项目统一 13 位)
|
|
28
|
+
// · dur = 轮内 (最晚 completed) − time(**毫秒**,与 claude/opencode 一致,页面 fmtDur 按毫秒读)
|
|
29
|
+
// · tin/tout = 逐次 tokens.input / (output + reasoning) 之和;tcache = cache.read + cache.write
|
|
30
|
+
// · models = 轮内出现过的 modelID(assistant 看顶层,user 看 model.modelID)
|
|
31
|
+
// · status = 轮内任何 tool 碎片 state.status==='error' 或 message.error → 'error'
|
|
32
|
+
// · nollm = 轮内没有任何 assistant message(用户刚发问、还没回复)
|
|
33
|
+
// · user/assistant(详情)= 该轮的用户输入 / 逐次 reasoning+text;tool 碎片作为工具调用明细
|
|
34
|
+
//
|
|
35
|
+
// 不在 OFF_KINDS 里:SQLite 库是实时写入的活源(带 -wal),按字节偏移续读既没必要也不可靠;
|
|
36
|
+
// 同 dsh/gemini/cursor 一路 —— 每次启动整份重解析 + 库的 {mtime,size} 签名跳过没变的库。
|
|
37
|
+
// 因为不进 OFF_KINDS,entries 本身不落 index 分片(只落 files 状态没意义),重启后靠重扫重建,
|
|
38
|
+
// 与 dsh 的处理一致。
|
|
39
|
+
// PARSER_REV['mimocode'] 仍然登记(见 shared.mjs):它在这里**不**管 index 落盘(本 kind 不落),
|
|
40
|
+
// 管的是**搜索分片**的作废 —— search.mjs 用 revOf(kind) 比对分片里的 rev,对不上就整片丢弃重扫。
|
|
41
|
+
// 本轮把「message 下标」改成了「轮下标」,turn 的语义变了,老分片的键会对到错的正文上,
|
|
42
|
+
// 所以必须给编号让它失效(不给的话要等库自己变一次才自愈)。
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { files, entries, srcs, removedIds, sorted, addEntry, toText, markKind, listDirCached, isDir, isFile, sqliteMod, toolNameCounts } from './shared.mjs';
|
|
46
|
+
// 诊断出口(core/log.mjs):下面三行「回退 markdown」的提示以前是靠 core 那行全局 console 补丁
|
|
47
|
+
// 才没漏进 MCP 的协议通道的 —— 补丁拆了,这里得自己走同一个出口(零 import 的叶子模块,不成环)。
|
|
48
|
+
import { log } from '../core/log.mjs';
|
|
49
|
+
|
|
50
|
+
// 库 + WAL 的签名:变了才整库重读,没变直接跳过。
|
|
51
|
+
// ⚠️ 这不是「优化」而是必需:本机实测 mimocode.db 已达 187MB(part.data 就有 72MB、29302 行),
|
|
52
|
+
// 而默认扫描节奏是 3s。没有签名跳过就是每 3 秒把整库 2181 条 message + 29302 条 part 全读出来
|
|
53
|
+
// 重新 JSON.parse 一遍(实测单轮 1.2s、常驻 RSS 219MB)—— 页面同步卡住、浏览器都跟着顿。
|
|
54
|
+
// 与 opencode / hermes 同款:不进 OFF_KINDS、不登记 PARSER_REV,靠签名跳过没变的库。
|
|
55
|
+
function dbSignature(dbPath) {
|
|
56
|
+
let dbSt, walSt = null;
|
|
57
|
+
try { dbSt = fs.statSync(dbPath); } catch { return null; }
|
|
58
|
+
try { walSt = fs.statSync(dbPath + '-wal'); } catch {}
|
|
59
|
+
return {
|
|
60
|
+
m: dbSt.mtimeMs,
|
|
61
|
+
s: dbSt.size,
|
|
62
|
+
sig: dbSt.mtimeMs + '|' + dbSt.size + '|' + (walSt ? walSt.mtimeMs + '|' + walSt.size : '-'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------- 找 mimocode.db(root 可能是 memory 根、mimocode 应用根、或其上层)----------
|
|
67
|
+
function findDb(root) {
|
|
68
|
+
if (!root) return null;
|
|
69
|
+
const candidates = [
|
|
70
|
+
path.join(root, 'mimocode.db'),
|
|
71
|
+
path.join(root, 'memory', 'mimocode.db'),
|
|
72
|
+
path.join(root, 'mimocode', 'mimocode.db'),
|
|
73
|
+
];
|
|
74
|
+
for (const c of candidates) if (isFile(c)) return c;
|
|
75
|
+
// root 上层逐级探:<root>/../mimocode.db 等(用户填 memory 时)
|
|
76
|
+
let cur = root;
|
|
77
|
+
for (let i = 0; i < 4; i++) {
|
|
78
|
+
const up = path.dirname(cur);
|
|
79
|
+
if (up === cur) break;
|
|
80
|
+
const c = path.join(up, 'mimocode.db');
|
|
81
|
+
if (isFile(c)) return c;
|
|
82
|
+
cur = up;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 单个 tool 碎片存进内存时的截断上限。详情页本身只展示 output 前 4000 字符(见 mimocodeEntryContent),
|
|
88
|
+
// 而本机实测存在 518KB 的 Read 返回、459KB 的截图返回 —— 原样留在内存里是纯浪费(也是 RSS 200MB+ 的主因)。
|
|
89
|
+
const TOOL_TEXT_CAP = 8000;
|
|
90
|
+
const cap = s => { const t = String(s == null ? '' : s); return t.length > TOOL_TEXT_CAP ? t.slice(0, TOOL_TEXT_CAP) : t; };
|
|
91
|
+
|
|
92
|
+
// 会话内「一轮」的序号:user 消息开一轮,后续 assistant(含多步工具循环)都归这一轮。
|
|
93
|
+
// 原实现用「message 在库里的序号」当 rounds,一个 user 后面跟 20 步 assistant 时会数到 20+,
|
|
94
|
+
// 与页面「第几轮」的语义对不上(opencode/zcode 都是按 user 开轮)。
|
|
95
|
+
function roleOf(md) { return String(md && md.role || 'unknown'); }
|
|
96
|
+
function modelOfMessage(md) {
|
|
97
|
+
// ⚠️ 真实数据里两个角色把模型放在**不同位置**(本机 2181 条实测):
|
|
98
|
+
// assistant:顶层 modelID / providerID(873/873 有;`.model` 对象**一个都没有**)
|
|
99
|
+
// user :`model: { providerID, modelID }`(表示这条消息用的模型)
|
|
100
|
+
// 原实现只读 md.model.modelID → assistant 侧恒为空,页面模型名全部退化成「未知模型」
|
|
101
|
+
//(实测 873 条 assistant 的 models[] 全是空的,正是这个原因)。
|
|
102
|
+
const id = md && (md.modelID || (md.model && md.model.modelID));
|
|
103
|
+
const prov = md && (md.providerID || (md.model && md.model.providerID));
|
|
104
|
+
return String(id || prov || '').trim();
|
|
105
|
+
}
|
|
106
|
+
function statusOf(md, errTools) {
|
|
107
|
+
const finish = String(md && md.finish || '').toLowerCase();
|
|
108
|
+
const aborted = ['abort', 'cancel', 'interrupt'].some(x => finish.includes(x));
|
|
109
|
+
const failed = !!md?.error || /error|fail/.test(finish) || errTools > 0;
|
|
110
|
+
return { failed, aborted };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scanMimocodeDb(agent, dbPath) {
|
|
114
|
+
const stat = dbSignature(dbPath);
|
|
115
|
+
if (!stat) return false;
|
|
116
|
+
// 库没变:不重读,但要确认上次的条目还在(首次扫描后才载入内存/被 LRU 淘汰的情形),
|
|
117
|
+
// 与 opencode 的 prev 分支同款处理。
|
|
118
|
+
const prev = files.get(dbPath);
|
|
119
|
+
if (prev && prev.data && prev.data.sig === stat.sig && prev.data.list) {
|
|
120
|
+
const firstId = prev.data.ids && prev.data.ids[0];
|
|
121
|
+
if (!firstId || !entries.has(firstId)) emitMimocodeDb(agent, dbPath, prev.data, null);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const S = sqliteMod();
|
|
125
|
+
if (!S) { log('[mimocode] node:sqlite 不可用,回退 markdown'); return false; }
|
|
126
|
+
let db;
|
|
127
|
+
try { db = new S.DatabaseSync(dbPath, { readOnly: true }); }
|
|
128
|
+
catch (e) { console.error('[mimocode] 打开库失败:' + e.message + ',回退 markdown'); return false; }
|
|
129
|
+
try {
|
|
130
|
+
// 库结构防御:缺核心表就当作非 mimocode 库,回退 markdown
|
|
131
|
+
const have = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
132
|
+
if (!have.has('session') || !have.has('message') || !have.has('part')) {
|
|
133
|
+
log('[mimocode] 库缺少 session/message/part,回退 markdown'); return false;
|
|
134
|
+
}
|
|
135
|
+
// project: id -> worktree(给条目 project 用)
|
|
136
|
+
const projMap = new Map();
|
|
137
|
+
try { for (const r of db.prepare('SELECT id, worktree FROM project').all()) projMap.set(r.id, r.worktree || r.id); } catch {}
|
|
138
|
+
// project 名:worktree 是 '/' 或缺失时退回会话自己的 directory —— 否则 global 项目那批
|
|
139
|
+
//(本机 60+ 个会话)会全挤在 'mimocode:global' 一个分组里,看不出是哪个工作目录。
|
|
140
|
+
const projOf = (pid, dir) => {
|
|
141
|
+
const w = projMap.get(pid);
|
|
142
|
+
if (w && w !== '/' && w !== '\\') return w;
|
|
143
|
+
if (dir) return dir;
|
|
144
|
+
return 'mimocode:' + (pid || 'global');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const sessions = db.prepare('SELECT id, project_id, title, directory, time_created, time_updated FROM session ORDER BY time_created').all();
|
|
148
|
+
const list = []; // 每项 = 一轮(src.turn 就是下标)
|
|
149
|
+
const ids = [];
|
|
150
|
+
for (const s of sessions) {
|
|
151
|
+
const session = s.id;
|
|
152
|
+
const project = projOf(s.project_id, s.directory);
|
|
153
|
+
const msgs = db.prepare('SELECT id, session_id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created, id').all(session);
|
|
154
|
+
const parts = db.prepare('SELECT message_id, time_created, data FROM part WHERE session_id = ? ORDER BY time_created, id').all(session);
|
|
155
|
+
// 按 message_id 归组 parts(解析 data.type)
|
|
156
|
+
const partsByMsg = new Map();
|
|
157
|
+
for (const p of parts) {
|
|
158
|
+
let d; try { d = JSON.parse(p.data); } catch { continue; }
|
|
159
|
+
// 只留详情页真正用得到的三类碎片(text/reasoning/tool);step-start / step-finish
|
|
160
|
+
// 这类纯标记不留在内存里(step-finish 的 tokens 与 message.tokens 实测完全一致,见 README 口径)
|
|
161
|
+
const ty = d && d.type;
|
|
162
|
+
if (ty !== 'text' && ty !== 'reasoning' && ty !== 'tool') continue;
|
|
163
|
+
if (!partsByMsg.has(p.message_id)) partsByMsg.set(p.message_id, []);
|
|
164
|
+
partsByMsg.get(p.message_id).push({ t: p.time_created, ...d });
|
|
165
|
+
}
|
|
166
|
+
// ---- 轮聚合:user message 开一轮,其后所有 assistant message 都并入这一轮 ----
|
|
167
|
+
// 首条 message 若不是 user(会话被截断/只留了回复)也算一轮,否则那些消息会整批丢掉。
|
|
168
|
+
let cur = null;
|
|
169
|
+
const flush = () => {
|
|
170
|
+
if (!cur) return;
|
|
171
|
+
const t = finalizeTurn(cur, s);
|
|
172
|
+
// 全空的轮不灌条目(同 claude/doubao 的约定):只有 user 行、没有任何正文/工具/错误
|
|
173
|
+
// 的那种「打开了没说话」的记录铺在页面上只是噪音。
|
|
174
|
+
if (t) { list.push(t); ids.push('mc#' + t.key); }
|
|
175
|
+
cur = null;
|
|
176
|
+
};
|
|
177
|
+
for (const m of msgs) {
|
|
178
|
+
let md; try { md = JSON.parse(m.data); } catch { md = {}; }
|
|
179
|
+
const role = roleOf(md);
|
|
180
|
+
if (role === 'user' || !cur) {
|
|
181
|
+
flush();
|
|
182
|
+
cur = {
|
|
183
|
+
key: m.id, session, project,
|
|
184
|
+
sTitle: s.title || '',
|
|
185
|
+
time: 0, lastTs: 0, endTs: 0,
|
|
186
|
+
userTexts: [], texts: [], reasonings: [], tools: [], models: [], callList: [],
|
|
187
|
+
calls: 0, tin: 0, tout: 0, tcache: 0, total: 0,
|
|
188
|
+
failed: false, aborted: false, finished: false, errTools: 0, errMsg: '',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const ps = partsByMsg.get(m.id) || [];
|
|
192
|
+
const created = Number(md.time?.created) || Number(m.time_created) || 0;
|
|
193
|
+
const completed = Number(md.time?.completed) || 0;
|
|
194
|
+
if (created) cur.time = cur.time ? Math.min(cur.time, created) : created;
|
|
195
|
+
cur.lastTs = Math.max(cur.lastTs, completed || created || 0);
|
|
196
|
+
if (completed) cur.endTs = Math.max(cur.endTs, completed);
|
|
197
|
+
|
|
198
|
+
// 正文(text / reasoning)按角色归位:user 的进 userTexts,assistant 的进 texts/reasonings
|
|
199
|
+
const texts = ps.filter(p => p.type === 'text').map(p => p.text || '');
|
|
200
|
+
const reasonings = ps.filter(p => p.type === 'reasoning').map(p => p.text || '');
|
|
201
|
+
if (role === 'user') cur.userTexts.push(...texts);
|
|
202
|
+
else { cur.texts.push(...texts); cur.reasonings.push(...reasonings); }
|
|
203
|
+
|
|
204
|
+
const tools = ps.filter(p => p.type === 'tool').map(p => ({
|
|
205
|
+
callID: p.callID, name: p.tool || '?',
|
|
206
|
+
status: p.state?.status || '',
|
|
207
|
+
input: p.state?.input != null ? cap(JSON.stringify(p.state.input)) : '',
|
|
208
|
+
output: p.state?.output != null ? cap(p.state.output) : '',
|
|
209
|
+
start: p.time?.start || 0, end: p.time?.end || 0,
|
|
210
|
+
// 这次工具调用发生在第几次 LLM 调用之后(callList 的下标),详情页按调用分组用
|
|
211
|
+
call: cur.calls,
|
|
212
|
+
}));
|
|
213
|
+
cur.tools.push(...tools);
|
|
214
|
+
const errTools = tools.filter(t => t.status === 'error').length;
|
|
215
|
+
cur.errTools += errTools;
|
|
216
|
+
|
|
217
|
+
// token 口径(与 opencode / zcode 对齐,也和库自己的 tokens.total 一致):
|
|
218
|
+
// tin = input
|
|
219
|
+
// tout = output + reasoning ← 推理 token 属输出侧;原实现漏了 reasoning,
|
|
220
|
+
// 本机 18 条有推理的消息少算 25~462 token
|
|
221
|
+
// tcache = cache.read + cache.write
|
|
222
|
+
// 实测库里的 tokens.total 恰等于 input+output+reasoning+cache(49/49 条吻合),可佐证口径。
|
|
223
|
+
// ⚠️ 只累加 assistant 的用量:user message 的 tokens 实测为空,但万一有值也不该算成一次调用。
|
|
224
|
+
if (role === 'assistant') {
|
|
225
|
+
const tk = md.tokens || {};
|
|
226
|
+
const cache = tk.cache || {};
|
|
227
|
+
const tin = Number(tk.input) || 0;
|
|
228
|
+
const tout = (Number(tk.output) || 0) + (Number(tk.reasoning) || 0);
|
|
229
|
+
const tcache = (Number(cache.read) || 0) + (Number(cache.write) || 0);
|
|
230
|
+
const total = tk.total != null ? (Number(tk.total) || 0) : (tin + tout + tcache);
|
|
231
|
+
cur.tin += tin; cur.tout += tout; cur.tcache += tcache; cur.total += total;
|
|
232
|
+
cur.calls++;
|
|
233
|
+
// 逐次调用的明细(详情页「LLM 调用明细」块 + 会话视图):
|
|
234
|
+
// 模型/用量/正文各归各的,一次调用只装自己产出的那段文字。
|
|
235
|
+
cur.callList.push({
|
|
236
|
+
model: modelOfMessage(md) || '(未知模型)',
|
|
237
|
+
tin, tout, tcache,
|
|
238
|
+
dur: (created && completed && completed >= created) ? (completed - created) : 0,
|
|
239
|
+
finishReason: md.finish || '',
|
|
240
|
+
error: md.error ? String(md.error.message || md.error.name || md.error || '') : '',
|
|
241
|
+
text: [...reasonings.map(x => '💭 ' + x), ...texts].join('\n\n').trim(),
|
|
242
|
+
});
|
|
243
|
+
const mdl = modelOfMessage(md);
|
|
244
|
+
if (mdl && !cur.models.includes(mdl)) cur.models.push(mdl);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const st = statusOf(md, errTools);
|
|
248
|
+
if (st.failed) { cur.failed = true; if (!cur.errMsg) cur.errMsg = String(md.error?.message || md.error?.name || md.error || md.finish || ''); }
|
|
249
|
+
if (st.aborted) cur.aborted = true;
|
|
250
|
+
if (md.finish || completed) cur.finished = true;
|
|
251
|
+
}
|
|
252
|
+
flush();
|
|
253
|
+
}
|
|
254
|
+
// ⚠️ 这里的 data 就是详情页要用的那份(mimocodeEntryContent 读 files.get(src.file)),
|
|
255
|
+
// 所以 mimoKind 必须落 'db':漏了它详情会一路落到最后的「项目记忆」兜底分支,
|
|
256
|
+
// 症状是正文全空、tools/calls 都没有(本机踩过)。
|
|
257
|
+
const data = { mimoKind: 'db', sig: stat.sig, list, ids, rev: (prev?.data?.rev || 0) + 1 };
|
|
258
|
+
files.set(dbPath, { agent, kind: 'mimocode', m: stat.m, s: stat.s, off: 0, data });
|
|
259
|
+
emitMimocodeDb(agent, dbPath, data, prev?.data);
|
|
260
|
+
return true;
|
|
261
|
+
} finally { try { db.close(); } catch {} }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 一轮定稿:算出页面要用的预览/状态。
|
|
265
|
+
// user 正文要**剥掉 <system-reminder> 注入块**:mimocode 会把运行上下文(MCP 配置、工作目录、
|
|
266
|
+
// 「上次回复没有可用答案」等)当作 text 碎片塞进 user message,本机实测一条 2014 字的注入块里
|
|
267
|
+
// 真用户输入只有 21 字。不剥的话卡片预览与「用户输入」全是那段注入物,搜索/筛选也都废了。
|
|
268
|
+
// 与 minimax 的 stripReminder 同口径(严格:必须是块状标签,剥完为空则退回原文)。
|
|
269
|
+
function stripInject(text) {
|
|
270
|
+
const t = String(text || '');
|
|
271
|
+
const cleaned = t.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
|
|
272
|
+
.replace(/<command-name>[\s\S]*?<\/command-name>/gi, ' ')
|
|
273
|
+
.replace(/<local-command[\s\S]*?<\/local-command[^>]*>/gi, ' ')
|
|
274
|
+
.replace(/\s+/g, ' ').trim();
|
|
275
|
+
return cleaned;
|
|
276
|
+
}
|
|
277
|
+
function turnPreview(t) {
|
|
278
|
+
const u = stripInject(t.userTexts.join(' '));
|
|
279
|
+
if (u) return ('👤 ' + u).slice(0, 300);
|
|
280
|
+
const a = t.texts.join(' ').replace(/\s+/g, ' ').trim();
|
|
281
|
+
if (a) return ('🤖 ' + a).slice(0, 300);
|
|
282
|
+
if (t.errMsg) return ('⚠ ' + t.errMsg).slice(0, 300);
|
|
283
|
+
if (t.tools.length) return ('🔧 ' + t.tools.length + ' 次工具调用').slice(0, 300);
|
|
284
|
+
return '(空轮)';
|
|
285
|
+
}
|
|
286
|
+
function finalizeTurn(t, s) {
|
|
287
|
+
// 全空的轮:既没有用户正文、也没有任何回复/工具/错误 → 返回 null(调用方丢掉不灌条目)。
|
|
288
|
+
// 判据只看「有没有内容」,不看 userTexts 是否为空:只有一句「hi」的轮也是有效记录。
|
|
289
|
+
const hasUser = t.userTexts.some(x => String(x || '').trim());
|
|
290
|
+
if (!hasUser && !t.texts.length && !t.reasonings.length && !t.tools.length && !t.errMsg) return null;
|
|
291
|
+
if (t.tools.length) t.toolNames = toolNameCounts(t.tools);
|
|
292
|
+
t.time = t.time || Number(s.time_created) || 0; // 兜底:全部 message 都没时间戳时才用会话时间
|
|
293
|
+
t.preview = turnPreview(t);
|
|
294
|
+
t.name = t.sTitle || '';
|
|
295
|
+
return t;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 把一次扫描的结果灌成条目。与 opencode 的 emit 同款:末尾把「库没了 / 这轮不再出现的 id」清掉,
|
|
299
|
+
// 否则删掉的会话会一直挂在页面上(原实现只 add 不删)。
|
|
300
|
+
// 一条 entry = 一轮(见 scanMimocodeDb 的口径注释)。
|
|
301
|
+
function emitMimocodeDb(agent, dbPath, data, prevData) {
|
|
302
|
+
const list = data.list || [];
|
|
303
|
+
for (let i = 0; i < list.length; i++) {
|
|
304
|
+
const r = list[i];
|
|
305
|
+
const id = 'mc#' + r.key;
|
|
306
|
+
addEntry(id, {
|
|
307
|
+
agent, project: r.project, session: r.session,
|
|
308
|
+
time: r.time,
|
|
309
|
+
// dur 与 claude/opencode 同口径:**毫秒**(页面 fmtDur 按毫秒渲染,旧实现写的是秒)
|
|
310
|
+
dur: Math.max(0, (r.endTs || r.lastTs || r.time) - r.time),
|
|
311
|
+
status: r.failed ? 'error' : (r.aborted ? 'canceled' : 'ok'),
|
|
312
|
+
tin: r.tin, tout: r.tout, tcache: r.tcache, total: r.total,
|
|
313
|
+
rounds: r.calls, // 轮内 LLM 调用数(= 轮内 assistant message 数)
|
|
314
|
+
tools: r.tools.length, calls: r.calls,
|
|
315
|
+
models: r.models,
|
|
316
|
+
preview: r.preview,
|
|
317
|
+
finished: r.finished, aborted: r.aborted, toolNames: r.toolNames || {},
|
|
318
|
+
ctx: 0, ctxUsed: null,
|
|
319
|
+
name: r.sTitle ? (r.sTitle + ' · 第 ' + (i + 1) + ' 轮') : ('第 ' + (i + 1) + ' 轮'),
|
|
320
|
+
nollm: r.calls === 0, // 只有 user 消息、还没有任何回复
|
|
321
|
+
mimoKind: 'db',
|
|
322
|
+
}, { file: dbPath, turn: i, kind: 'mimocode' });
|
|
323
|
+
}
|
|
324
|
+
// 清理上一轮存在、这一轮不再出现的条目(会话/消息被删)
|
|
325
|
+
const keep = new Set(data.ids || []);
|
|
326
|
+
for (const old of prevData?.ids || []) {
|
|
327
|
+
if (keep.has(old)) continue;
|
|
328
|
+
entries.delete(old); srcs.delete(old); removedIds.push(old);
|
|
329
|
+
}
|
|
330
|
+
sorted.cache = null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------- markdown 兜底(库读不动时)----------
|
|
334
|
+
function scanMimocodeMarkdown(agent, root) {
|
|
335
|
+
let mem = root;
|
|
336
|
+
if (!isDir(path.join(mem, 'sessions')) && !isDir(path.join(mem, 'projects')) && !isDir(path.join(mem, 'global'))) {
|
|
337
|
+
const d = path.join(mem, 'memory');
|
|
338
|
+
if (isDir(d)) mem = d;
|
|
339
|
+
}
|
|
340
|
+
const sDir = path.join(mem, 'sessions');
|
|
341
|
+
if (isDir(sDir)) {
|
|
342
|
+
for (const sid of listDirCached(sDir) || []) {
|
|
343
|
+
const sd = path.join(sDir, sid);
|
|
344
|
+
if (!isDir(sd)) continue;
|
|
345
|
+
try { scanMimocodeSessionMd(agent, mem, sid, sd); } catch (e) { console.error('[mimocode] md session', sid, e.message); }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const pDir = path.join(mem, 'projects');
|
|
349
|
+
if (isDir(pDir)) for (const pid of listDirCached(pDir) || []) {
|
|
350
|
+
const mdP = path.join(pDir, pid, 'MEMORY.md');
|
|
351
|
+
if (isFile(mdP)) try { scanMimocodeProjectMd(agent, mem, pid, mdP, false); } catch (e) { console.error('[mimocode] md project', pid, e.message); }
|
|
352
|
+
}
|
|
353
|
+
const gMd = path.join(mem, 'global', 'MEMORY.md');
|
|
354
|
+
if (isFile(gMd)) try { scanMimocodeProjectMd(agent, mem, 'global', gMd, true); } catch (e) { console.error('[mimocode] md global', e.message); }
|
|
355
|
+
markKind('mimocode');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function scanMimocodeSessionMd(agent, root, sessId, sessDir) {
|
|
359
|
+
const session = sessId, project = 'mimocode:' + sessId;
|
|
360
|
+
const idBase = 'mc#' + path.join(root, sessId).replace(/[\\/:]/g, '~') + '#';
|
|
361
|
+
const stat = p => { try { return fs.statSync(p); } catch { return null; } };
|
|
362
|
+
const cp = path.join(sessDir, 'checkpoint.md'); const cpSt = stat(cp);
|
|
363
|
+
if (cpSt && cpSt.isFile()) {
|
|
364
|
+
let text = ''; try { text = fs.readFileSync(cp, 'utf8'); } catch {}
|
|
365
|
+
const data = parseCheckpoint(text); data.sessId = sessId; data.mtime = cpSt.mtimeMs;
|
|
366
|
+
files.set(cp, { agent, kind: 'mimocode', m: cpSt.mtimeMs, s: cpSt.size, off: 0, data });
|
|
367
|
+
emitCheckpointMd(agent, cp, root, sessId, session, project, data);
|
|
368
|
+
}
|
|
369
|
+
const nt = path.join(sessDir, 'notes.md'); const ntSt = stat(nt);
|
|
370
|
+
if (ntSt && ntSt.isFile()) {
|
|
371
|
+
let text = ''; try { text = fs.readFileSync(nt, 'utf8'); } catch {}
|
|
372
|
+
const data = parseNotes(text, ntSt);
|
|
373
|
+
files.set(nt, { agent, kind: 'mimocode', m: ntSt.mtimeMs, s: ntSt.size, off: 0, data });
|
|
374
|
+
emitNotesMd(agent, nt, root, sessId, session, project, data);
|
|
375
|
+
}
|
|
376
|
+
const tasksDir = path.join(sessDir, 'tasks');
|
|
377
|
+
if (isDir(tasksDir)) for (const tid of listDirCached(tasksDir) || []) {
|
|
378
|
+
const pr = path.join(tasksDir, tid, 'progress.md'); const prSt = stat(pr);
|
|
379
|
+
if (!prSt || !prSt.isFile()) continue;
|
|
380
|
+
let text = ''; try { text = fs.readFileSync(pr, 'utf8'); } catch {}
|
|
381
|
+
const data = { text, mtime: prSt.mtimeMs, tid, mimoKind: 'task' };
|
|
382
|
+
files.set(pr, { agent, kind: 'mimocode', m: prSt.mtimeMs, s: prSt.size, off: 0, data });
|
|
383
|
+
emitTaskMd(agent, pr, root, sessId, session, project, tid, data);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function emitCheckpointMd(agent, fp, root, sessId, session, project, data) {
|
|
387
|
+
const id = 'mc#' + path.join(root, sessId).replace(/[\\/:]/g, '~') + '#checkpoint';
|
|
388
|
+
const active = (data.sections && data.sections['§1']) ? data.sections['§1'].trim() : '';
|
|
389
|
+
addEntry(id, { agent, project, session, time: data.mtime || 0, dur: 0, status: 'ok', tin: 0, tout: 0, tcache: 0, total: 0, rounds: 0, tools: 0, calls: 0, models: [], preview: ('📌 检查点 · ' + (active || (data.title || '无活动意图'))).slice(0, 300), finished: false, aborted: false, toolNames: {}, ctx: 0, ctxUsed: null, name: data.title || ('检查点 · ' + sessId), mimoKind: 'checkpoint' }, { file: fp, turn: 0, kind: 'mimocode' });
|
|
390
|
+
}
|
|
391
|
+
function emitNotesMd(agent, fp, root, sessId, session, project, data) {
|
|
392
|
+
const idBase = 'mc#' + path.join(root, sessId).replace(/[\\/:]/g, '~') + '#note';
|
|
393
|
+
data.turns.forEach((t, i) => {
|
|
394
|
+
const id = idBase + i;
|
|
395
|
+
const bp = (t.body || '').replace(/\s+/g, ' ').slice(0, 300);
|
|
396
|
+
addEntry(id, { agent, project, session, time: t.time || data.mtime || 0, dur: 0, status: 'ok', tin: 0, tout: 0, tcache: 0, total: 0, rounds: 0, tools: 0, calls: 0, models: [], preview: ('📝 turn ' + t.n + (t.ts ? ' · ' + t.ts : '') + (bp ? ' · ' + bp : '')).slice(0, 300), finished: false, aborted: false, toolNames: {}, ctx: 0, ctxUsed: null, name: '笔记 turn ' + t.n, mimoKind: 'note' }, { file: fp, turn: i, kind: 'mimocode' });
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function emitTaskMd(agent, fp, root, sessId, session, project, tid, data) {
|
|
400
|
+
const id = 'mc#' + path.join(root, sessId).replace(/[\\/:]/g, '~') + '#task#' + tid;
|
|
401
|
+
const head = (data.text.split('\n').find(l => l.trim() && !l.trim().startsWith('#')) || '').trim();
|
|
402
|
+
const bp = data.text.replace(/\s+/g, ' ').slice(0, 300);
|
|
403
|
+
addEntry(id, { agent, project, session, time: data.mtime || 0, dur: 0, status: 'ok', tin: 0, tout: 0, tcache: 0, total: 0, rounds: 0, tools: 0, calls: 0, models: [], preview: ('🗂 任务 ' + tid + (head ? ' · ' + head : '') + (bp ? ' · ' + bp : '')).slice(0, 300), finished: false, aborted: false, toolNames: {}, ctx: 0, ctxUsed: null, name: '任务 ' + tid, mimoKind: 'task' }, { file: fp, turn: 0, kind: 'mimocode' });
|
|
404
|
+
}
|
|
405
|
+
function scanMimocodeProjectMd(agent, root, pid, mdPath, isGlobal) {
|
|
406
|
+
const session = isGlobal ? 'global' : ('proj-' + pid);
|
|
407
|
+
const project = isGlobal ? 'mimocode:global' : ('mimocode:proj-' + pid);
|
|
408
|
+
let st; try { st = fs.statSync(mdPath); } catch { return; }
|
|
409
|
+
let text = ''; try { text = fs.readFileSync(mdPath, 'utf8'); } catch { return; }
|
|
410
|
+
const data = { text, mtime: st.mtimeMs, pid, mimoKind: isGlobal ? 'global' : 'project' };
|
|
411
|
+
files.set(mdPath, { agent, kind: 'mimocode', m: st.mtimeMs, s: st.size, off: 0, data });
|
|
412
|
+
const id = 'mc#' + path.join(path.dirname(mdPath), path.basename(mdPath, '.md')).replace(/[\\/:]/g, '~') + '#mem';
|
|
413
|
+
const head = (text.split('\n').find(l => l.trim() && !l.trim().startsWith('#')) || '').trim();
|
|
414
|
+
addEntry(id, { agent, project, session, time: data.mtime || 0, dur: 0, status: 'ok', tin: 0, tout: 0, tcache: 0, total: 0, rounds: 0, tools: 0, calls: 0, models: [], preview: ('📚 ' + (isGlobal ? '全局记忆' : ('项目记忆 · ' + pid)) + (head ? ' · ' + head : '')).slice(0, 300), finished: false, aborted: false, toolNames: {}, ctx: 0, ctxUsed: null, name: isGlobal ? '全局记忆' : ('项目记忆 · ' + pid), mimoKind: isGlobal ? 'global' : 'project' }, { file: mdPath, turn: 0, kind: 'mimocode' });
|
|
415
|
+
}
|
|
416
|
+
function parseCheckpoint(text) {
|
|
417
|
+
const lines = text.split('\n'); let title = ''; const sections = {}; let cur = null;
|
|
418
|
+
for (const ln of lines) {
|
|
419
|
+
const t = ln.trim();
|
|
420
|
+
if (!title && /^Topic:\s*(.+)$/i.test(t)) title = t.replace(/^Topic:\s*/i, '').trim();
|
|
421
|
+
const sm = /^##\s+(§\d+[^\n]*)/.exec(t);
|
|
422
|
+
if (sm) { cur = sm[1].trim(); sections[cur] = ''; continue; }
|
|
423
|
+
if (cur != null) sections[cur] += ln + '\n';
|
|
424
|
+
}
|
|
425
|
+
return { title, sections };
|
|
426
|
+
}
|
|
427
|
+
function parseNotes(text, st) {
|
|
428
|
+
const turns = []; const re = /^##\s*\[\s*turn\s+(\d+)\s*·\s*([^\]]*)\]/i; let cur = null;
|
|
429
|
+
const TS_RE = /(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/;
|
|
430
|
+
for (const ln of text.split('\n')) {
|
|
431
|
+
const m = re.exec(ln.trim());
|
|
432
|
+
if (m) { if (cur) turns.push(cur); const ts = m[2].trim(); const t = TS_RE.exec(ts); cur = { n: Number(m[1]), ts, time: t ? Date.parse(t[1].replace(' ', 'T')) : 0, body: '' }; }
|
|
433
|
+
else if (cur) cur.body += ln + '\n';
|
|
434
|
+
}
|
|
435
|
+
if (cur) turns.push(cur);
|
|
436
|
+
return { turns, mtime: st ? st.mtimeMs : 0 };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------- 主入口 ----------
|
|
440
|
+
export function scanMimocode(agent, root) {
|
|
441
|
+
const db = findDb(root);
|
|
442
|
+
if (db && scanMimocodeDb(agent, db)) return; // 真源优先
|
|
443
|
+
log('[mimocode] 走 markdown 兜底(库不存在或读不动)');
|
|
444
|
+
scanMimocodeMarkdown(agent, root);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---------- 详情 ----------
|
|
448
|
+
// 库分支的「一轮」:正文与工具都在扫描时就切好放在 list[turn] 里(不必重开库)。
|
|
449
|
+
export function mimocodeEntryContent(src, full) {
|
|
450
|
+
const st = files.get(src.file);
|
|
451
|
+
if (!st) return null;
|
|
452
|
+
const data = st.data || {};
|
|
453
|
+
const fi = {}, fo = {};
|
|
454
|
+
// 库分支(mimoKind='db'):src.turn 是 data.list 的下标(一项 = 一轮)
|
|
455
|
+
if (data.mimoKind === 'db') {
|
|
456
|
+
const r = (data.list || [])[src.turn];
|
|
457
|
+
if (!r) return null;
|
|
458
|
+
const tools = (r.tools || []).map(t => {
|
|
459
|
+
const tin = {}, tout = {};
|
|
460
|
+
return {
|
|
461
|
+
name: t.name, tid: t.callID || '', error: t.status === 'error',
|
|
462
|
+
dur: (t.end && t.start) ? Math.max(0, t.end - t.start) : 0, // 毫秒(与条目 dur 同口径)
|
|
463
|
+
input: trunc(t.input, full, tin), inputTrunc: !!tin.t,
|
|
464
|
+
output: trunc(t.output, full, tout), outputTrunc: !!tout.t,
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
// user:剥掉 <system-reminder> 注入块(否则「用户输入」显示的是运行上下文,见 stripInject)
|
|
468
|
+
const userRaw = (r.userTexts || []).join('\n');
|
|
469
|
+
const userStripped = stripInject(userRaw);
|
|
470
|
+
const user = userStripped || userRaw.trim();
|
|
471
|
+
const assistant = ([...r.reasonings.map(x => '💭 ' + x), ...r.texts].join('\n\n')).trim();
|
|
472
|
+
// LLM 调用明细:逐次(轮内每次 assistant message 一条),与 opencode 的 calls 同形
|
|
473
|
+
const calls = (r.callList || []).map(c => {
|
|
474
|
+
const ft = {};
|
|
475
|
+
const text = trunc(c.text, full, ft);
|
|
476
|
+
const o = {
|
|
477
|
+
model: c.model, tin: c.tin, tout: c.tout, tcache: c.tcache, dur: c.dur,
|
|
478
|
+
};
|
|
479
|
+
if (text) { o.text = text; if (ft.t) o.textTrunc = true; }
|
|
480
|
+
if (c.error) o.error = c.error;
|
|
481
|
+
if (c.finishReason && c.finishReason !== 'stop') o.finishReason = c.finishReason;
|
|
482
|
+
return o;
|
|
483
|
+
});
|
|
484
|
+
// 每次调用发起了哪些工具(页面调用行上用 toolText 展示)——按 tools[].call 分组
|
|
485
|
+
for (let ci = 0; ci < calls.length; ci++) {
|
|
486
|
+
const names = (r.tools || []).filter(t => t.call === ci).map(t => t.name);
|
|
487
|
+
if (names.length) calls[ci].tools = names;
|
|
488
|
+
}
|
|
489
|
+
const note = 'mimocode 助手回复(来自 mimocode.db message 表;token 取自 message.data.tokens,reasoning 计入输出)。';
|
|
490
|
+
const out = {
|
|
491
|
+
user: trunc(user, full, fi), assistant: trunc(assistant, full, fo), tools, calls,
|
|
492
|
+
note: r.calls === 0 ? '这一轮只有你的输入,还没有任何模型回复(mimocode.db)。' : note,
|
|
493
|
+
v: 'mc' + src.turn,
|
|
494
|
+
};
|
|
495
|
+
if (r.errTools) out.callsNote = '这一轮有 ' + r.errTools + ' 次工具调用失败。';
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
// 旧形状兜底(mimoKind='message'):内存里若还留着升级前扫描的结果,别让它炸
|
|
499
|
+
if (data.mimoKind === 'message') {
|
|
500
|
+
const ps = data.ps || [];
|
|
501
|
+
const texts = ps.filter(p => p.type === 'text').map(p => p.text || '');
|
|
502
|
+
const reasonings = ps.filter(p => p.type === 'reasoning').map(p => p.text || '');
|
|
503
|
+
const tools = ps.filter(p => p.type === 'tool').map(p => ({
|
|
504
|
+
name: p.tool || '?', tid: p.callID || '', error: p.state?.status === 'error',
|
|
505
|
+
input: p.state?.input != null ? JSON.stringify(p.state.input) : '',
|
|
506
|
+
output: p.state?.output != null ? String(p.state.output) : '',
|
|
507
|
+
dur: (p.time?.end && p.time?.start) ? Math.max(0, Math.round((p.time.end - p.time.start) / 1000)) : 0,
|
|
508
|
+
}));
|
|
509
|
+
const user = (data.role === 'user') ? texts.join('\n') : '';
|
|
510
|
+
const assistant = (data.role === 'assistant' || data.role === 'unknown')
|
|
511
|
+
? ([...reasonings.map(r => '💭 ' + r), ...texts].join('\n\n')).trim() : '';
|
|
512
|
+
const calls = tools.map(t => ({ text: ('#' + t.name + (t.error ? ' [失败]' : '') + (t.input ? '\n入参: ' + t.input : '') + (t.output ? '\n返回: ' + t.output.slice(0, 4000) : '')), tool: t.name, error: t.error, dur: t.dur }));
|
|
513
|
+
const note = data.role === 'user' ? 'mimocode 用户消息(来自 mimocode.db message 表)。' : 'mimocode 助手回复(来自 mimocode.db message 表;token 取自 message.data.tokens)。';
|
|
514
|
+
return { user: trunc(user, full, fi), assistant: trunc(assistant, full, fo), tools, calls, note, v: 'mc' + src.turn };
|
|
515
|
+
}
|
|
516
|
+
// markdown 兜底分支(data 形状同上)
|
|
517
|
+
if (data.sections) {
|
|
518
|
+
const sec = data.sections || {};
|
|
519
|
+
const body = Object.keys(sec).map(k => '## ' + k + '\n\n' + sec[k].trim()).join('\n\n');
|
|
520
|
+
return { user: '', assistant: body, tools: [], calls: [], note: 'mimocode 检查点快照(§1~§11)。本源为记忆文件,不含 token 与工具调用数据。', v: 'mc' + src.turn };
|
|
521
|
+
}
|
|
522
|
+
if (Array.isArray(data.turns)) { const t = data.turns[src.turn]; return { user: '', assistant: t ? ('turn ' + t.n + (t.ts ? ' · ' + t.ts : '') + '\n\n' + (t.body || '').trim()) : '', tools: [], calls: [], note: 'mimocode 会话笔记的一条记录。', v: 'mc' + src.turn }; }
|
|
523
|
+
const txt = (data.text || '').trim();
|
|
524
|
+
return { user: '', assistant: txt, tools: [], calls: [], note: data.mimoKind === 'global' ? 'mimocode 全局记忆(global/MEMORY.md)。' : data.mimoKind === 'task' ? 'mimocode 任务进度(tasks/<id>/progress.md)。' : 'mimocode 项目记忆(projects/<pid>/MEMORY.md)。', v: 'mc' + src.turn };
|
|
525
|
+
}
|
|
526
|
+
function trunc(s, full, flag) {
|
|
527
|
+
s = toText(s);
|
|
528
|
+
if (full || s.length <= 8000) return s;
|
|
529
|
+
flag.t = true;
|
|
530
|
+
return s.slice(0, 8000);
|
|
531
|
+
}
|