@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,1230 @@
|
|
|
1
|
+
// ---------------- 目录嗅探与摆法(discovery:sniffBase 及其全部判据) ----------------
|
|
2
|
+
// 从 agent-acta-server.mjs 拆出(纯搬家,逻辑零改动)。这是「日志在哪、认成什么格式」的唯一来源:
|
|
3
|
+
// * candidateBases / probeBases —— 各平台候选根目录(自动发现与诊断共用)
|
|
4
|
+
// * hasXxx 系列判据 —— 每种 kind 的目录/文件特征(读盘量刻意压到最小:这是每 3s 一轮的路径)
|
|
5
|
+
// * sniffBase —— 判据编排(分支顺序就是优先级,见各分支注释,顺序动不得)
|
|
6
|
+
// 解析器(scanXxx)不在这里;kimi 的扫描(scanKimi)import 本文件的 kimiWires。
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { HOME, isDir, isFile, listDirCached, sqliteMod } from './shared.mjs';
|
|
10
|
+
import { hasMavisSession } from './minimax.mjs';
|
|
11
|
+
import { OPENCLAW_STATE_DIRNAMES, openclawTranscripts } from './openclaw.mjs';
|
|
12
|
+
import { clineTranscripts } from './cline.mjs';
|
|
13
|
+
import { buddyExtRoot } from './buddyext.mjs';
|
|
14
|
+
// hasCursorTranscripts 里枚举转录走的是 cursor 解析器那一个函数(两种落法共用一套判定,见该函数注释)。
|
|
15
|
+
// ⚠️ 这条 import 曾经漏掉过:函数体在外层 try{}catch{} 里,ReferenceError 被静默吞掉 →
|
|
16
|
+
// hasCursorTranscripts **恒 false** → 自动发现整条 cursor 分支永不命中(症状 = 侧栏 cursor 恒 0 条,
|
|
17
|
+
// 且因为本机那条是历史手动项而完全看不出来)。cursor-hook-test.mjs 的隔离 profile 现在守着它。
|
|
18
|
+
import { cursorTranscriptFiles } from './cursor.mjs';
|
|
19
|
+
|
|
20
|
+
// 各平台候选根目录:win / mac / linux / harmonyos(ohos 走 POSIX 分支)
|
|
21
|
+
// 名字别名:codeart 的真实目录是 codearts-agent(华为 CodeArts IDE)
|
|
22
|
+
// kimi:两个**已知**落盘格式的变体——kimi-code CLI(~/.kimi-code,走别名)与
|
|
23
|
+
// Kimi CLI(~/.kimi,就是名字本身),两者的 wire.jsonl 摆法见 hasKimiSessions
|
|
24
|
+
// trae:TraeCode 桌面版的 Electron 应用目录名是 'Trae CN'(带空格),不是 'trae',
|
|
25
|
+
// 故别名指向真实的安装目录名,basesFor 才能在 %APPDATA% 下找到它。
|
|
26
|
+
// traework:TraeWork 桌面版前身叫 'Trae SOLO'(2026年6月改名),两个目录名都列上,
|
|
27
|
+
// 装了哪个版本都能被发现。
|
|
28
|
+
// qoder:Qoder CN 版落盘目录是 ~/.qoder-cn,不加别名自动发现永远认不出(只能手动添加)。
|
|
29
|
+
// minimax:Mavis 本地运行时的数据根目录固定是 ~/.minimax(点目录),与「显式填入的 kind 名」
|
|
30
|
+
// `minimax` 一致;HOME 下的小写别名 `mavis` 也算一份,避免同事按真实目录名手填时落空。
|
|
31
|
+
const NAME_ALIASES = {
|
|
32
|
+
codeart: ['codearts-agent'], codearts: ['codearts-agent'], kimi: ['kimi-code'],
|
|
33
|
+
trae: ['Trae CN', 'TraeCN', 'Trae Code', 'TraeCode'],
|
|
34
|
+
traework: ['Trae Work', 'TraeWork', 'Trae SOLO', 'TraeSOLO', 'TRAE SOLO CN', 'Trae SOLO CN'],
|
|
35
|
+
qoder: ['qoder-cn'],
|
|
36
|
+
minimax: ['minimax', 'Mavis'],
|
|
37
|
+
mimocode: ['mimocode'],
|
|
38
|
+
kilo: ['kilo', 'kilocode'],
|
|
39
|
+
};
|
|
40
|
+
// 「只给诊断看、**不参与自动发现**」的候选路径。
|
|
41
|
+
//
|
|
42
|
+
// 为什么单列一份:需求书 R15 还要求覆盖 Kimi 桌面版(Win/mac/Linux/鸿蒙 PC),但这几个变体的
|
|
43
|
+
// 落盘格式本机无从查证(没装、也没有样本)。把它们塞进 candidateBases 的代价是**目录里随便一个
|
|
44
|
+
// `sessions/` 子目录都会被 sniffBase 的兜底分支认成 atomcode**,于是自动发现把 kimi 注册成
|
|
45
|
+
// atomcode、侧栏显示 0 条 —— 正好是这一版刚修掉的那个故障的另一个入口。
|
|
46
|
+
// 所以:诊断里照常列出来(有人装了桌面版就能一眼看到路径与顶层结构,截图发回来补解析器),
|
|
47
|
+
// 自动发现一律不碰。等真拿到样本、写了解析器,再把它挪进 NAME_ALIASES。
|
|
48
|
+
//
|
|
49
|
+
// 名单是按**社区/论坛里报过的落盘位置**列的(官网没写):kimi-desktop 与 KimiDesktop 是同一个
|
|
50
|
+
// Electron 目录的两种书写(大小写不一致的报道都有),kimi-work / kimi-webbridge 是主目录下那两个
|
|
51
|
+
// 点目录,kimi-desktop-updater 是自动更新器 —— 更新器命中的概率低,但列上不花钱,
|
|
52
|
+
// 万一会话日志落在它那儿,诊断里会直接标出来。
|
|
53
|
+
// 注:`kimi-desktop` / `KimiDesktop` 这两个**不在**这里 —— 它们已经从「猜的路径」升级成
|
|
54
|
+
// 「有明确锚点的真路径」(读它的 daimon-storage.json → shareDir → 内嵌 kimi-code home,见
|
|
55
|
+
// kimiDesktopSessionsRoots)。剩下的三个仍旧只是「论坛里提过的落盘位置」,没有格式样本。
|
|
56
|
+
const PROBE_ONLY_BASES = {
|
|
57
|
+
kimi: ['kimi-work', 'kimi-webbridge', 'kimi-desktop-updater'],
|
|
58
|
+
// doubao 应用根目录(`%LOCALAPPDATA%\DoubaoWork`):会话日志真正在它底下的
|
|
59
|
+
// `User Data\Default\.doubaowork\...`,但不同安装/平台的具体摆法只在本机验证过 Windows 一条,
|
|
60
|
+
// 归入「只给诊断看」——机器上装了但路径不对时,诊断里能一眼看到候选路径与顶层结构。
|
|
61
|
+
doubao: ['DoubaoWork', 'Doubao'],
|
|
62
|
+
};
|
|
63
|
+
// 把「若干名字 → 各平台约定目录」展开(candidateBases 与 probeBases 共用)
|
|
64
|
+
//
|
|
65
|
+
// 这里就是「四平台都能用」里**平台相关的那一半**,它按 `process.platform` 分支、不认具体 agent:
|
|
66
|
+
// win32 → ~/.<n>、%APPDATA%\<n>、%LOCALAPPDATA%\<n>(Electron 应用一般在这里)
|
|
67
|
+
// darwin → ~/.<n>、~/Library/Application Support/<n>、~/.config/<n>
|
|
68
|
+
// linux / ohos / 其他 POSIX → ~/.<n>、$XDG_CONFIG_HOME(默认 ~/.config)/<n>、~/.local/share/<n>
|
|
69
|
+
// 鸿蒙 PC 的 Node 不是 win32 也不是 darwin,会落到最后一个分支 —— 与 Linux 同一套 XDG 约定,
|
|
70
|
+
// 这正是它该走的那套(真机要是报出别的 platform 值,在这里加一个分支即可,解析器不用动)。
|
|
71
|
+
// 另一半(解析器本身)与平台无关:只认文件内容,不碰路径分隔符、不调平台专属 API。
|
|
72
|
+
//
|
|
73
|
+
// 平台上下文**可注入**(第二参数,缺省 = 本机真实值):这台开发机是 Windows,darwin / linux 两条
|
|
74
|
+
// 分支不可能在本机端到端验证,注入之后至少能把「哪个平台铺出哪些候选目录」钉在测试里,
|
|
75
|
+
// 而不是靠读代码猜(见 test/platform-test.mjs)。生产路径一律不传,行为与从前逐字一致。
|
|
76
|
+
function platCtx(c) {
|
|
77
|
+
return {
|
|
78
|
+
platform: (c && c.platform) || process.platform,
|
|
79
|
+
home: (c && c.home) || HOME,
|
|
80
|
+
env: (c && c.env) || process.env,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function basesFor(names, c) {
|
|
84
|
+
const { platform, home, env } = platCtx(c);
|
|
85
|
+
const bases = [];
|
|
86
|
+
for (const n of names) {
|
|
87
|
+
bases.push(path.join(home, '.' + n)); // 全平台通用约定
|
|
88
|
+
if (platform === 'win32') {
|
|
89
|
+
if (env.APPDATA) bases.push(path.join(env.APPDATA, n)); // 桌面版 Electron 走这里
|
|
90
|
+
if (env.LOCALAPPDATA) bases.push(path.join(env.LOCALAPPDATA, n));
|
|
91
|
+
} else if (platform === 'darwin') {
|
|
92
|
+
bases.push(path.join(home, 'Library', 'Application Support', n));
|
|
93
|
+
bases.push(path.join(home, '.config', n));
|
|
94
|
+
} else { // linux / ohos(HarmonyOS) / 其他 POSIX
|
|
95
|
+
bases.push(path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), n));
|
|
96
|
+
bases.push(path.join(home, '.local', 'share', n));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return bases;
|
|
100
|
+
}
|
|
101
|
+
function probeBases(name, c) {
|
|
102
|
+
const n = PROBE_ONLY_BASES[name];
|
|
103
|
+
return n ? basesFor(n, c) : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------- Kimi 桌面版的日志根(它内嵌了一份 kimi-code) ----------------
|
|
107
|
+
// 桌面版的会话**不在** `%APPDATA%\kimi-desktop` 里 —— 那个目录只有 Electron 的缓存与自身配置。
|
|
108
|
+
// 真正的会话在它自己挑的一个「数据根」下,结构是:
|
|
109
|
+
// <shareDir>/…/runtime/kimi-code/home/sessions/<slug>/<会话 id>/agents/main/wire.jsonl
|
|
110
|
+
// 也就是一份**标准 kimi-code home**(同样带 session_index.jsonl、workspaces.json),
|
|
111
|
+
// 所以解析器完全复用 scanKimi,一个新格式都不用写。
|
|
112
|
+
//
|
|
113
|
+
// shareDir 记在桌面版自己的 `daimon-storage.json` 里(`{"shareDir":"E:\\KimiData"}`),
|
|
114
|
+
// 用户可以在设置里把它挪到别的盘 —— 本机就挪到了 E 盘,所以**不能写死路径**,必须读这个文件。
|
|
115
|
+
//
|
|
116
|
+
// 为什么不写死「daimon-share/daimon」那两层:那两层的名字是实现细节,可能随版本变;
|
|
117
|
+
// 而 `runtime/kimi-code/home` 是它内嵌的 kimi-code 的固定布局。所以按「往下探两层、每层都试一下
|
|
118
|
+
// <此处>/runtime/kimi-code/home」来找 —— 只探两层是刻意的:更深会走进 `app/daimon/node_modules`,
|
|
119
|
+
// 那是几万次 readdir,而 discoverAgents 每次启动都要跑。
|
|
120
|
+
const KIMI_DESKTOP_APPS = ['kimi-desktop', 'KimiDesktop'];
|
|
121
|
+
function kimiDesktopShareDirs() {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const dir of basesFor(KIMI_DESKTOP_APPS)) {
|
|
124
|
+
try {
|
|
125
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, 'daimon-storage.json'), 'utf8'));
|
|
126
|
+
if (j && typeof j.shareDir === 'string' && j.shareDir.trim()) out.push(j.shareDir.trim());
|
|
127
|
+
} catch {} // 没装 / 老版本没这个文件 / 文件坏了 —— 都当没有
|
|
128
|
+
}
|
|
129
|
+
return [...new Set(out)];
|
|
130
|
+
}
|
|
131
|
+
function findKimiHomes(dir, depth, out) {
|
|
132
|
+
const sessions = path.join(dir, 'runtime', 'kimi-code', 'home', 'sessions');
|
|
133
|
+
if (isDir(sessions)) { out.push(sessions); return; }
|
|
134
|
+
if (depth >= 2) return;
|
|
135
|
+
let names; try { names = fs.readdirSync(dir); } catch { return; }
|
|
136
|
+
for (const n of names.slice(0, 60)) {
|
|
137
|
+
const p = path.join(dir, n);
|
|
138
|
+
if (isDir(p)) findKimiHomes(p, depth + 1, out);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function kimiDesktopSessionsRoots() {
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const sd of kimiDesktopShareDirs()) findKimiHomes(sd, 0, out);
|
|
144
|
+
return [...new Set(out)];
|
|
145
|
+
}
|
|
146
|
+
function candidateBases(name, c) {
|
|
147
|
+
const { platform, home, env } = platCtx(c);
|
|
148
|
+
const out = basesFor([name, ...(NAME_ALIASES[name] || [])], c);
|
|
149
|
+
// opencode 的数据根跟在 XDG 约定下(`~/.local/share/opencode`)——本机实测 Windows 上也在这里,
|
|
150
|
+
// 既不是 `~/.opencode`,也不是 `%APPDATA%\opencode`。basesFor 只在 POSIX 分支里铺 .local/share,
|
|
151
|
+
// 所以这里给它单补一条(其余平台与 basesFor 的结果重复,Set 去重后无副作用)。
|
|
152
|
+
if (name === 'opencode') out.push(path.join(home, '.local', 'share', 'opencode'));
|
|
153
|
+
// doubao(豆包 Work)的会话根在应用数据目录深处,不走 `~/.doubaowork` 约定:
|
|
154
|
+
// Windows 实测 = `%LOCALAPPDATA%\DoubaoWork\User Data\Default\.doubaowork\agent_mode\workspace`
|
|
155
|
+
// (basesFor 的 `%LOCALAPPDATA%\doubaowork` 小写写法不存在,auto-discovery 靠这条真路径命中;
|
|
156
|
+
// 上层目录的其它摆法交给 doubaoSessionsRoot 逐层下探,见下。)
|
|
157
|
+
// 只在 win32 下铺:这是 Windows 应用的安装位置,别的平台补这条只会多出两段死路径。
|
|
158
|
+
if (name === 'doubao' && platform === 'win32' && env.LOCALAPPDATA) {
|
|
159
|
+
out.push(path.join(env.LOCALAPPDATA, 'DoubaoWork', 'User Data', 'Default', '.doubaowork', 'agent_mode', 'workspace'));
|
|
160
|
+
}
|
|
161
|
+
// hermes 的 home 整体可被 HERMES_HOME 改写(源码 hermes_constants.py 的唯一覆盖入口),
|
|
162
|
+
// 指到哪儿都该被发现;basesFor 铺的 ~/.hermes 与 %LOCALAPPDATA%\hermes 已覆盖默认摆法。
|
|
163
|
+
if (name === 'hermes' && env.HERMES_HOME) out.push(env.HERMES_HOME);
|
|
164
|
+
// devin(Devin CLI)的数据目录按产品文档就是 XDG 式摆法:`~/.local/share/devin`(mac 与 Linux 同一处,
|
|
165
|
+
// 库在它下面的 cli/sessions.db)。basesFor 只在 POSIX 分支铺 .local/share,darwin 分支铺的是
|
|
166
|
+
// Library/Application Support 与 .config —— 这里补一条把 mac 也覆盖上。Windows 不用补:
|
|
167
|
+
// 真身 %APPDATA%\Devin\cli 已被 basesFor 的 %APPDATA%\devin 命中(大小写不敏感)。
|
|
168
|
+
if (name === 'devin') out.push(path.join(home, '.local', 'share', 'devin'));
|
|
169
|
+
// minimax(Mavis 本地运行时)的会话**不在** ~/.minimax 顶层 —— 真根是 <home>/.minimax/v2/sessions,
|
|
170
|
+
// 下面按 y/m/d/<session_id> 四层摆;顶层与 v2/ 都只是配置/缓存,没有 messages.jsonl。
|
|
171
|
+
// basesFor 铺的 ~/.minimax + %APPDATA%/%LOCALAPPDATA% 下的同名目录都不够深,自动发现靠
|
|
172
|
+
// 这条 v2/sessions 真路径命中(嗅探首行 JSON 形状,再由 hasMavisSession 下探)。
|
|
173
|
+
if (name === 'minimax') out.push(path.join(home, '.minimax', 'v2', 'sessions'));
|
|
174
|
+
// openclaw(OpenClaw):stateDir 默认 ~/.openclaw(源码 dist/config-utils-*.js 的 resolveStateDir:
|
|
175
|
+
// NEW_STATE_DIRNAME=".openclaw"、LEGACY_STATE_DIRNAMES=[".clawdbot"]),且 OPENCLAW_STATE_DIR 可整体改写。
|
|
176
|
+
// 会话在 <stateDir>/agents/<id>/sessions/*.jsonl —— 所以铺的是 stateDir 本身,不是它的某个子目录。
|
|
177
|
+
// basesFor 铺的 ~/.openclaw 已覆盖默认摆法;这里再补「环境变量指哪儿认哪儿」与老版本的 .clawdbot。
|
|
178
|
+
if (name === 'openclaw') {
|
|
179
|
+
if (env.OPENCLAW_STATE_DIR) {
|
|
180
|
+
const sd = String(env.OPENCLAW_STATE_DIR).trim();
|
|
181
|
+
if (sd) out.push(sd.startsWith('~') ? path.join(home, sd.replace(/^~[\\/]?/, '')) : sd);
|
|
182
|
+
}
|
|
183
|
+
if (env.OPENCLAW_HOME) {
|
|
184
|
+
const oh = String(env.OPENCLAW_HOME).trim();
|
|
185
|
+
if (oh) out.push(path.join(oh, OPENCLAW_STATE_DIRNAMES[0]));
|
|
186
|
+
}
|
|
187
|
+
for (const n of OPENCLAW_STATE_DIRNAMES) out.push(path.join(home, n));
|
|
188
|
+
}
|
|
189
|
+
// cline(Cline CLI 3.x):会话在 <data>/sessions/<会话 id>/<会话 id>.messages.json,
|
|
190
|
+
// data 根默认 `~/.cline/data`(basesFor 铺的 `~/.cline` 由 clineTranscripts 自己下探到 data/sessions)。
|
|
191
|
+
// 二进制里认这三个环境变量(一处改写、其余派生):CLINE_DATA_DIR 是 data 根,
|
|
192
|
+
// CLINE_SESSION_DATA_DIR 直接就是 sessions 根,CLINE_DB_DATA_DIR 是旁边的库目录(不是会话根,不铺)。
|
|
193
|
+
// 指到哪儿就得认到哪儿:用户把会话数据挪出家目录时,只按 `~/.cline` 铺路径会一无所获。
|
|
194
|
+
if (name === 'cline') {
|
|
195
|
+
const abs = v => {
|
|
196
|
+
const s = String(v || '').trim();
|
|
197
|
+
if (!s) return '';
|
|
198
|
+
return s.startsWith('~') ? path.join(home, s.replace(/^~[\\/]?/, '')) : s;
|
|
199
|
+
};
|
|
200
|
+
const dd = abs(env.CLINE_DATA_DIR);
|
|
201
|
+
if (dd) { out.push(dd, path.join(dd, 'sessions')); }
|
|
202
|
+
const sd = abs(env.CLINE_SESSION_DATA_DIR);
|
|
203
|
+
if (sd) out.push(sd);
|
|
204
|
+
}
|
|
205
|
+
// mimocode(记忆/检查点类 agent):真源是 <home>/.local/share/mimocode/mimocode.db。
|
|
206
|
+
// Windows 实测就是 C:\Users\<u>\.local\share\mimocode\mimocode.db(mac/Linux 同源,XDG 式摆法,
|
|
207
|
+
// 没有 Electron 的 %APPDATA% 分支)。memory/ 是库导出的展示层,在 db 下一层。
|
|
208
|
+
// 真路径 hit 靠下面两条:应用根(直接命中 mimocode.db)优先,memory/ 作为兜底。
|
|
209
|
+
if (name === 'mimocode') {
|
|
210
|
+
out.push(path.join(home, '.local', 'share', 'mimocode'));
|
|
211
|
+
out.push(path.join(home, '.local', 'share', 'mimocode', 'memory'));
|
|
212
|
+
}
|
|
213
|
+
// kilo(KiloCode / Kilo CLI):真源是 <home>/.local/share/kilo/kilo.db,XDG 摆法,本机 Windows 实测
|
|
214
|
+
// 也在这里(既不是 %APPDATA%\kilo 也不是 ~/.kilo)。basesFor 只在 POSIX 分支铺 .local/share,
|
|
215
|
+
// 所以这里单补一条(其余平台重复、Set 去重后无副作用,与 opencode 同款做法)。
|
|
216
|
+
if (name === 'kilo') out.push(path.join(home, '.local', 'share', 'kilo'));
|
|
217
|
+
// comate(百度 Comate):会话真源是 <home>/.comate-engine/store/(本机 Windows 实测),
|
|
218
|
+
// 不是 ~/.comate(那是 IDE 壳的 skills/hooks/extensions)也不是 %APPDATA%\Comate(Electron 缓存
|
|
219
|
+
// + state.vscdb 索引指针)。basesFor 铺的三个候选都在 store 的**外面**,自动发现靠这条真路径命中;
|
|
220
|
+
// 嗅探判据是内容(store 里有 chat_session_* 文件),不会误接。
|
|
221
|
+
if (name === 'comate') out.push(path.join(home, '.comate-engine', 'store'));
|
|
222
|
+
return [...new Set(out)];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function hasCodeartsLogs(logsDir) {
|
|
226
|
+
try { return fs.readdirSync(logsDir).some(n => /^CodeArts Agent-.*\.log$/i.test(n)); } catch { return false; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// TraeCode / TraeWork 桌面版:<base>/logs/<启动时间戳>/window1/renderer.log
|
|
230
|
+
// 判据:logs 目录下存在任意形如 YYYYMMDDTHHMMSS 的 timestamp 目录,且该目录下有 window1/renderer.log,
|
|
231
|
+
// 并在 renderer.log 头 256KB 内扫到 `[ai-chat/v2]` 锚点(确认是 Trae 系的会话日志,不是 VS Code 等邻居)。
|
|
232
|
+
//
|
|
233
|
+
// 为什么读 256KB 而不是头 4KB:Trae 的 renderer.log 头部是 Electron 初始化日志
|
|
234
|
+
// ([ThirdPartyToken]、[HubNet]、[external-modules] 等),第一条 `[ai-chat/v2]` 事件要等到
|
|
235
|
+
// 第 1800 行左右(约 200KB)才出现。头 4KB 一条都扫不到,判据恒为 false → 自动发现静默失败。
|
|
236
|
+
// 256KB 内若还没有,基本可认定这份 renderer.log 不是 Trae 的会话日志(只是同名文件)。
|
|
237
|
+
//
|
|
238
|
+
// 区分 tracecode 与 tracework **不靠这里**——sniffBase 根据 base 目录名(Trae CN vs Trae Work/SOLO)
|
|
239
|
+
// 返回不同 kind;判据本身只回答「是不是 Trae 系的日志根」。
|
|
240
|
+
//
|
|
241
|
+
// 读盘量:最多枚举 5 个 timestamp 目录,每个最多读 256KB。
|
|
242
|
+
export function hasTraeRendererLogs(logsDir) {
|
|
243
|
+
if (!isDir(logsDir)) return false;
|
|
244
|
+
let tsCount = 0;
|
|
245
|
+
let names; try { names = fs.readdirSync(logsDir); } catch { return false; }
|
|
246
|
+
for (const ts of names) {
|
|
247
|
+
if (tsCount++ > 5) break;
|
|
248
|
+
// timestamp 目录名形如 20260917T154413(YYYYMMDDTHHMMSS)—— 这是 Trae 启动时创建的目录
|
|
249
|
+
if (!/^\d{8}T\d{6}$/.test(ts)) continue;
|
|
250
|
+
const fp = path.join(logsDir, ts, 'window1', 'renderer.log');
|
|
251
|
+
try {
|
|
252
|
+
const st = fs.statSync(fp);
|
|
253
|
+
if (!st.isFile() || st.size === 0) continue;
|
|
254
|
+
const fd = fs.openSync(fp, 'r');
|
|
255
|
+
try {
|
|
256
|
+
const buf = Buffer.alloc(Math.min(262144, st.size)); // 256KB
|
|
257
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
258
|
+
const head = buf.toString('utf8', 0, n);
|
|
259
|
+
// 锚点认两种日志 tag:老版 [ai-chat/v2],新版(TRAE SOLO CN)[trae-chat-core],否则新版机器识别不了自己的日志根。
|
|
260
|
+
if (/\[ai-chat\/v2\]|\[trae-chat-core\]/.test(head)) return true;
|
|
261
|
+
} finally { fs.closeSync(fd); }
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function hasTraceFiles(tracesDir) {
|
|
268
|
+
try {
|
|
269
|
+
for (const d of fs.readdirSync(tracesDir)) {
|
|
270
|
+
const dd = path.join(tracesDir, d);
|
|
271
|
+
if (!isDir(dd)) continue;
|
|
272
|
+
try { if (fs.readdirSync(dd).some(n => n.startsWith('trace_') && n.endsWith('.json'))) return true; } catch {}
|
|
273
|
+
}
|
|
274
|
+
} catch {}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// codex CLI: <base>/sessions/YYYY/MM/DD/rollout-<时间>-<uuid>.jsonl(按日期三层嵌套)
|
|
279
|
+
export function hasCodexRollouts(dir, depth = 0) {
|
|
280
|
+
let names; try { names = fs.readdirSync(dir); } catch { return false; }
|
|
281
|
+
if (names.some(n => /^rollout-.*\.jsonl$/.test(n))) return true;
|
|
282
|
+
if (depth >= 3) return false;
|
|
283
|
+
for (const n of names) {
|
|
284
|
+
const p = path.join(dir, n);
|
|
285
|
+
if (isDir(p) && hasCodexRollouts(p, depth + 1)) return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function hasClaudeJsonl(projectsDir) {
|
|
291
|
+
try {
|
|
292
|
+
for (const d of fs.readdirSync(projectsDir)) {
|
|
293
|
+
const dd = path.join(projectsDir, d);
|
|
294
|
+
if (!isDir(dd)) continue;
|
|
295
|
+
try { if (fs.readdirSync(dd).some(n => n.endsWith('.jsonl'))) return true; } catch {}
|
|
296
|
+
}
|
|
297
|
+
} catch {}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// buddy 型(codebuddy / workbuddy)与 claude 型同为 projects/<slug>/*.jsonl,靠记录判别:
|
|
302
|
+
// claude 顶层是 {"type":"user"|"assistant"}("message" 是嵌套对象),buddy 顶层是 {"type":"message","role":...}
|
|
303
|
+
// 这是每个扫描周期都会跑的嗅探路径,读盘量必须小:最多看 3 个目录 × 2 个文件 × 头 16KB
|
|
304
|
+
export function hasBuddyJsonl(projectsDir) {
|
|
305
|
+
try {
|
|
306
|
+
let dirs = 0;
|
|
307
|
+
for (const d of fs.readdirSync(projectsDir)) {
|
|
308
|
+
const dd = path.join(projectsDir, d);
|
|
309
|
+
if (!isDir(dd)) continue;
|
|
310
|
+
if (++dirs > 3) break;
|
|
311
|
+
let names; try { names = fs.readdirSync(dd).filter(n => n.endsWith('.jsonl')).slice(0, 2); } catch { continue; }
|
|
312
|
+
for (const n of names) {
|
|
313
|
+
try {
|
|
314
|
+
const head = fs.readFileSync(path.join(dd, n), 'utf8').slice(0, 16384);
|
|
315
|
+
for (const line of head.split('\n')) {
|
|
316
|
+
if (!line.includes('"type":"message"')) continue;
|
|
317
|
+
try { const j = JSON.parse(line); if (j.type === 'message' && (j.role === 'user' || j.role === 'assistant')) return true; } catch {}
|
|
318
|
+
}
|
|
319
|
+
} catch {}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// gemini 型:<base>/tmp/<项目slug>/ 下每个会话一份 session-*.json,外加同目录的 logs.json 提示词流水。
|
|
327
|
+
// 目录名是项目 slug(不是 hash),判据取「有会话文件」或「有 logs.json」都算——新版 CLI 只写后者
|
|
328
|
+
export function hasGeminiSessions(tmpDir) {
|
|
329
|
+
let slugs = 0;
|
|
330
|
+
for (const d of listDirCached(tmpDir) || []) {
|
|
331
|
+
const dd = path.join(tmpDir, d);
|
|
332
|
+
if (!isDir(dd)) continue;
|
|
333
|
+
if (++slugs > 3) break;
|
|
334
|
+
const names = listDirCached(path.join(dd, 'chats'));
|
|
335
|
+
if (names && names.some(n => n.startsWith('session-') && n.endsWith('.jsonl'))) return true;
|
|
336
|
+
try { if (fs.statSync(path.join(dd, 'logs.json')).isFile()) return true; } catch {}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// cursor 型:<base>/projects/<项目slug>/agent-transcripts/<会话uuid>/<同名>.jsonl,每行一个 {role, message}。
|
|
342
|
+
// 判据是**顶行没有 type**——这一条就把两个同构的邻居区分开了:claude 顶层 type=user/assistant、
|
|
343
|
+
// buddy 顶层 type=message,cursor 顶层只有 role/message。读取量与 hasBuddyJsonl 同级(≤3 目录 × 头 16KB)
|
|
344
|
+
//
|
|
345
|
+
// 探测深度是**故意放宽**的:原来只看前 3 个 slug、每个 slug 只看第一个文件、每条文件只看**第一条**
|
|
346
|
+
// 能解析的记录。三条都可能误判「这里不是 cursor」:
|
|
347
|
+
// · .cursor/projects 下并存两种项目目录 —— 真跑过 Agent 的(有 agent-transcripts/,只有它算数)
|
|
348
|
+
// 和 Cursor 自己临时工作区的(只有 mcps/)。按字母序排在前面的常常是后者;
|
|
349
|
+
// · 第一个会话可能刚开、转录是空的或只有半行;
|
|
350
|
+
// · 首行是半行 JSON 时原来直接 break 整个文件,后面明明有正经记录也看不到。
|
|
351
|
+
// 代价仍然有上限:最多 5 个项目 × 2 个会话 × 1 个文件 × 头 16KB。
|
|
352
|
+
export function hasCursorTranscripts(projectsDir) {
|
|
353
|
+
try {
|
|
354
|
+
let slugs = 0;
|
|
355
|
+
for (const d of fs.readdirSync(projectsDir)) {
|
|
356
|
+
const at = path.join(projectsDir, d, 'agent-transcripts');
|
|
357
|
+
if (!isDir(at)) continue;
|
|
358
|
+
if (++slugs > 5) break;
|
|
359
|
+
// 每个项目最多试 2 份转录(枚举交给 cursorTranscriptFiles,与扫描共用同一套落法判定)
|
|
360
|
+
for (const { fp } of cursorTranscriptFiles(at).slice(0, 2)) {
|
|
361
|
+
try {
|
|
362
|
+
const head = fs.readFileSync(fp, 'utf8').slice(0, 16384);
|
|
363
|
+
for (const line of head.split('\n').slice(0, 8)) { // 头 8 行里有一条像样的记录就够(原来只试 1 行)
|
|
364
|
+
const t = line.trim();
|
|
365
|
+
if (!t || !t.includes('"role"')) continue;
|
|
366
|
+
let j; try { j = JSON.parse(t); } catch { continue; } // 半行/截断行:跳过继续,别放弃整个文件
|
|
367
|
+
if (!j || j.type) continue; // 有顶层 type ⇒ 不是 cursor(claude/buddy 都有)
|
|
368
|
+
if (j.role !== 'user' && j.role !== 'assistant') continue;
|
|
369
|
+
// message 既可能是 {content:[…]},也可能是纯字符串 —— cursorText() 两种都认,
|
|
370
|
+
// 判据这边必须同样两种都认,否则会出现「解析器读得出来、探测却说不认识」的自相矛盾
|
|
371
|
+
if (typeof j.message === 'string' || (j.message && Array.isArray(j.message.content))) return true;
|
|
372
|
+
}
|
|
373
|
+
} catch {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch {}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Kimi 的会话落盘(两个变体,wire.jsonl 事件流格式相同,只是摆法差一层):
|
|
381
|
+
// kimi-code CLI : ~/.kimi-code/sessions/<workdir slug>/session_<uuid>/agents/main/wire.jsonl
|
|
382
|
+
// Kimi CLI : ~/.kimi/sessions/<md5(workdir)>/<uuid>/wire.jsonl (直接躺在会话目录里)
|
|
383
|
+
// 两个都可能在会话里挂子 agent 目录(agents/<agent-id>/、subagents/<agent-id>/),同样各有一份
|
|
384
|
+
// wire.jsonl —— 那些也算数(否则子 agent 那一轮的 token 就丢了),所以判据按「会话目录里任意深度
|
|
385
|
+
// ≤2 处存在 wire.jsonl」写,而不是写死 `agents/main`。
|
|
386
|
+
//
|
|
387
|
+
// 为什么不能只看路径、还要验一下文件头:`<x>/<y>/wire.jsonl` 这个形状并非 kimi 专属,
|
|
388
|
+
// atomcode 的 sessions/<hash>/ 里同样可能出现同名文件(扫描器是按目录走的,误认成 kimi 会让
|
|
389
|
+
// 整个 atomcode 目录一条都扫不出来)。所以两手都用:
|
|
390
|
+
// · 结构 —— 会话目录下 ≤2 层内找到 wire.jsonl;
|
|
391
|
+
// · 内容 —— 只读**头 512 字节**,要求第一行是 {"type":"metadata","protocol_version":…}。
|
|
392
|
+
// 只读头 512 字节是因为这一行**必然在最前且很短**(本机实测 94 字节),
|
|
393
|
+
// 与下面那段踩坑正好相反:以前的判据去读头 8KB 找 turn.prompt,而第 2 行是 20KB 的 systemPrompt,
|
|
394
|
+
// 8KB 里一行有效事件都没有,判据恒为 false → sniffBase 落到 atomcode 兜底 → 自动发现把 kimi
|
|
395
|
+
// 认成 atomcode、扫出 0 条。
|
|
396
|
+
//
|
|
397
|
+
// 那个错的隐蔽之处在于**升级过的机器上完全看不出来**:本机配置里那条 kimi 是历史手动项
|
|
398
|
+
// kimi-code 迁移过来的(迁移时直接把 kind 写成 kimi),它一直有 59 条数据,看着一切正常;
|
|
399
|
+
// 只有干净环境(新同事、或用户删掉 kimi 条目后让自动发现重新接管)才会暴露成「装得好好的 kimi,
|
|
400
|
+
// 侧栏里 0 条」。所以断言别信本机,要信隔离 profile(见根目录 r15-kimi-test.mjs)。
|
|
401
|
+
//
|
|
402
|
+
// 上限 200 个 session:这是每个扫描周期都会走的嗅探路径,不能让一个畸形目录把它拖住。
|
|
403
|
+
const WIRE_RELS = [['agents', 'main', 'wire.jsonl'], ['wire.jsonl'], ['subagents', 'main', 'wire.jsonl']];
|
|
404
|
+
export function isKimiWire(fp) {
|
|
405
|
+
if (!fs.existsSync(fp)) return false;
|
|
406
|
+
try {
|
|
407
|
+
let fd; try { fd = fs.openSync(fp, 'r'); } catch { return false; }
|
|
408
|
+
try {
|
|
409
|
+
const buf = Buffer.alloc(512);
|
|
410
|
+
const n = fs.readSync(fd, buf, 0, 512, 0);
|
|
411
|
+
if (n <= 0) return false;
|
|
412
|
+
const first = buf.toString('utf8', 0, n);
|
|
413
|
+
// 头一行就是 metadata:不去 find('turn.prompt'),那一行可能在 20KB 之外(见上)
|
|
414
|
+
return /^\{[^\n]*"type"\s*:\s*"metadata"/.test(first) && first.includes('"protocol_version"');
|
|
415
|
+
} finally { fs.closeSync(fd); }
|
|
416
|
+
} catch { return false; }
|
|
417
|
+
}
|
|
418
|
+
// 会话目录 -> 那一份 wire.jsonl 的绝对路径(找不到返回 null)。子目录只探一层:agents/<id>/、subagents/<id>/
|
|
419
|
+
export function kimiWireIn(sessDir) {
|
|
420
|
+
for (const rel of WIRE_RELS) {
|
|
421
|
+
const fp = path.join(sessDir, ...rel);
|
|
422
|
+
if (isKimiWire(fp)) return fp;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
for (const sub of listDirCached(sessDir) || []) {
|
|
426
|
+
if (sub !== 'agents' && sub !== 'subagents') continue;
|
|
427
|
+
const sd = path.join(sessDir, sub);
|
|
428
|
+
if (!isDir(sd)) continue;
|
|
429
|
+
for (const id of (listDirCached(sd) || []).slice(0, 8)) {
|
|
430
|
+
const fp = path.join(sd, id, 'wire.jsonl');
|
|
431
|
+
if (isKimiWire(fp)) return fp;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
// 一个 sessions 根目录下的全部 (会话目录, wire.jsonl):扫描与嗅探共用,避免两处各写一遍摆法
|
|
438
|
+
export function kimiWires(base) {
|
|
439
|
+
const out = [];
|
|
440
|
+
for (const slug of listDirCached(base) || []) {
|
|
441
|
+
const slugDir = path.join(base, slug);
|
|
442
|
+
if (!isDir(slugDir)) continue;
|
|
443
|
+
for (const sess of listDirCached(slugDir) || []) {
|
|
444
|
+
const sessDir = path.join(slugDir, sess);
|
|
445
|
+
if (!isDir(sessDir)) continue;
|
|
446
|
+
const fp = kimiWireIn(sessDir);
|
|
447
|
+
if (fp) out.push({ sessDir, fp });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
export function hasKimiSessions(base) {
|
|
453
|
+
if (!isDir(base)) return false;
|
|
454
|
+
let checked = 0;
|
|
455
|
+
try {
|
|
456
|
+
for (const slug of (listDirCached(base) || []).slice(0, 60)) {
|
|
457
|
+
const slugDir = path.join(base, slug);
|
|
458
|
+
if (!isDir(slugDir)) continue;
|
|
459
|
+
for (const sess of (listDirCached(slugDir) || []).slice(0, 40)) {
|
|
460
|
+
if (++checked > 200) return false;
|
|
461
|
+
if (kimiWireIn(path.join(slugDir, sess))) return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch {}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------- dsh(DeepSeek Harness) ----------------
|
|
469
|
+
// 落盘:~/.dsh/sessions/<项目 slug(形如 --F-centos-next-admin--)>/session-<uuid>/session.v3.jsonl.zstd
|
|
470
|
+
// (同级的 settings.yaml / profiles/ / storages/ 都不是会话正文)
|
|
471
|
+
//
|
|
472
|
+
// 摆法与 atomcode 只差在第二层:atomcode 是 sessions/<hash>/<文件>.jsonl,dsh 是
|
|
473
|
+
// sessions/<slug>/<会话目录>/<文件>.zstd。所以光靠「sessions 下是目录」这个兜底判据,
|
|
474
|
+
// dsh 会被认成 atomcode,而 atomcode 的解析器一个文件都匹配不到 → 侧栏 0 条、且完全静默
|
|
475
|
+
// (这正是 kimi 当初的处境,见 sniffBase 里那段)。判据因此必须排在兜底前面。
|
|
476
|
+
//
|
|
477
|
+
// 会话文件名**带格式版本**(dsh 自己升版就改名):实测 v3,2026-09-28 起落 v4。
|
|
478
|
+
// · 同目录里可能同时躺着多份,取**版本号最高**的那份;
|
|
479
|
+
// · 早期格式 session.jsonl.zstd(无版本号)是迁移前残留(实测只有会话头 + 几条配置事件、
|
|
480
|
+
// 没有轮次),永远排最后兜底。
|
|
481
|
+
// ⚠️ 曾经把版本号**写死**成 v3(`['session.v3.jsonl.zstd', …]`):dsh 一升到 v4,新会话就整批
|
|
482
|
+
// 静默消失 —— 症状是「聊了一下午,面板今天 0 条」,而解析器本身完全读得动 v4(事件类型与 v3 同构,
|
|
483
|
+
// 多出来的 request/header、system/message 等新类型本就被忽略)。所以这里**不许再写死版本号**。
|
|
484
|
+
const DSH_SESSION_RE = /^session(?:\.v(\d+))?\.jsonl\.zstd$/;
|
|
485
|
+
const DSH_PLAIN_RANK = -1; // 无版本号:比任何 vN 都旧
|
|
486
|
+
|
|
487
|
+
export function dshSessionFile(sessDir) {
|
|
488
|
+
let ents;
|
|
489
|
+
try { ents = fs.readdirSync(sessDir, { withFileTypes: true }); } catch { return null; }
|
|
490
|
+
let best = '', bestRank = DSH_PLAIN_RANK - 1;
|
|
491
|
+
for (const e of ents) {
|
|
492
|
+
if (!e.isFile()) continue;
|
|
493
|
+
const m = DSH_SESSION_RE.exec(e.name);
|
|
494
|
+
if (!m) continue;
|
|
495
|
+
const rank = m[1] ? Number(m[1]) : DSH_PLAIN_RANK;
|
|
496
|
+
if (rank > bestRank) { bestRank = rank; best = e.name; }
|
|
497
|
+
}
|
|
498
|
+
return best ? path.join(sessDir, best) : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 判据**只看有没有 .zstd 会话文件、不解压**:sniffBase 每轮扫描都要跑,解压太贵。
|
|
502
|
+
// 代价是老 Node(< 22.15,没有 zlib.zstdDecompressSync)上仍会把它认成 dsh、却扫不出条目 ——
|
|
503
|
+
// 那种机器由诊断直说原因(见 diagnose 里的 dsh 分支),而不是静默变成 0 条。
|
|
504
|
+
export function hasDshSessions(base) {
|
|
505
|
+
if (!isDir(base)) return false;
|
|
506
|
+
let checked = 0;
|
|
507
|
+
try {
|
|
508
|
+
for (const slug of (listDirCached(base) || []).slice(0, 60)) {
|
|
509
|
+
const slugDir = path.join(base, slug);
|
|
510
|
+
if (!isDir(slugDir)) continue;
|
|
511
|
+
for (const sess of (listDirCached(slugDir) || []).slice(0, 40)) {
|
|
512
|
+
if (++checked > 200) return false;
|
|
513
|
+
if (dshSessionFile(path.join(slugDir, sess))) return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ---------------- mimocode(记忆/检查点类 agent) ----------------
|
|
521
|
+
// mimocode 的**真源是 SQLite 库** mimocode.db(Drizzle ORM 之上);memory/ 下那些 checkpoint.md/notes.md/
|
|
522
|
+
// MEMORY.md 只是库导出的展示层(本机实测 task 表为空)。库的位置:<mimocode 应用根>/mimocode.db,
|
|
523
|
+
// 而用户给的 memory 根在 <应用根>/memory/ 下 —— 所以嗅探/扫描都要**往上爬一层**找到 mimocode.db。
|
|
524
|
+
// 应用根通常是 <home>/.local/share/mimocode(Windows 实测 C:\Users\<u>\.local\share\mimocode)。
|
|
525
|
+
//
|
|
526
|
+
// 判据优先级:① 先看 base 这一层及往上数层有没有 mimocode.db(真源,命中即认);② 没有库时退回到
|
|
527
|
+
// memory 展示层判据(sessions/projects/global 目录 + 目标文件存在),作为「库读不动」时的只读兜底根。
|
|
528
|
+
// mimocode 库的**内容**判据(与 opencode/zcode 一样按表认,不按文件名)。
|
|
529
|
+
//
|
|
530
|
+
// ⚠️ 为什么必须按内容认:mimocode.db 与 opencode.db 是**同源 schema**(都源自同一套
|
|
531
|
+
// session/message/part + event/event_sequence),opencodeDbVerify 的判据(那五张表)在
|
|
532
|
+
// mimocode.db 上**全为真**(本机实测)—— 光按表名认,mimocode 的库会被 opencode 探针接走,
|
|
533
|
+
// 结果是 mimocode 一条都出不来(与 dsh/kimi 当初被 atomcode 兜底接走同源)。
|
|
534
|
+
// 区分点在各自**独有**的表上:
|
|
535
|
+
// mimocode 独有:history_fts(_idx) / actor_registry / inbox / task / workflow_run / memory_fts
|
|
536
|
+
// opencode 独有:session_message / session_input / credential / project_directory
|
|
537
|
+
// 两边互相排斥,顺序就不再重要(本机实测各自独有表稳定存在)。
|
|
538
|
+
export const MIMOCODE_ONLY_TABLES = ['history_fts_idx', 'actor_registry', 'memory_fts_idx', 'workflow_run', 'task_event'];
|
|
539
|
+
export function mimocodeDbVerify(fp) {
|
|
540
|
+
if (!isFile(fp)) return false;
|
|
541
|
+
const mod = sqliteMod();
|
|
542
|
+
if (!mod) return false;
|
|
543
|
+
let db = null;
|
|
544
|
+
try {
|
|
545
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
546
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
547
|
+
if (!names.has('session') || !names.has('message') || !names.has('part')) return false;
|
|
548
|
+
// 至少命中一张 mimocode 独有表才算数(一张就够,别把「多了一张表」当必要条件)
|
|
549
|
+
return MIMOCODE_ONLY_TABLES.some(t => names.has(t));
|
|
550
|
+
} catch { return false; }
|
|
551
|
+
finally { try { db && db.close(); } catch {} }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function hasMimocodeDb(root) {
|
|
555
|
+
if (!isDir(root)) return false;
|
|
556
|
+
const probe = p => {
|
|
557
|
+
if (!isFile(p)) return false;
|
|
558
|
+
// node:sqlite 不可用(Node < 22.5)时退回纯文件名判据:此时反正读不了库、要回退 markdown,
|
|
559
|
+
// 且旧环境里不会同时装着 opencode 的库,认错的代价仅是「回退层能扫到东西」。
|
|
560
|
+
if (!sqliteMod()) return true;
|
|
561
|
+
return mimocodeDbVerify(p);
|
|
562
|
+
};
|
|
563
|
+
if (probe(path.join(root, 'mimocode.db'))) return true;
|
|
564
|
+
// 往上爬几层(memory -> mimocode 应用根)
|
|
565
|
+
let cur = root;
|
|
566
|
+
for (let i = 0; i < 4; i++) {
|
|
567
|
+
const up = path.dirname(cur);
|
|
568
|
+
if (up === cur) break;
|
|
569
|
+
if (probe(path.join(up, 'mimocode.db'))) return true;
|
|
570
|
+
cur = up;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
export function hasMimocodeSessions(root) {
|
|
575
|
+
const s = path.join(root, 'sessions');
|
|
576
|
+
if (!isDir(s)) return false;
|
|
577
|
+
let n = 0;
|
|
578
|
+
for (const sid of listDirCached(s) || []) {
|
|
579
|
+
if (++n > 8) break;
|
|
580
|
+
const d = path.join(s, sid);
|
|
581
|
+
if (!isDir(d)) continue;
|
|
582
|
+
if (isFile(path.join(d, 'checkpoint.md')) || isFile(path.join(d, 'notes.md'))) return true;
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
export function hasMimocodeProjects(root) {
|
|
587
|
+
// projects 与 global 两者任一存在且非空就算命中(global 是 projects/global 的平级目录)。
|
|
588
|
+
// 先探 projects:下面 <pid>/MEMORY.md 才算数;
|
|
589
|
+
const p = path.join(root, 'projects');
|
|
590
|
+
if (isDir(p)) {
|
|
591
|
+
for (const pid of listDirCached(p) || []) {
|
|
592
|
+
if (isFile(path.join(p, pid, 'MEMORY.md'))) return true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return isFile(path.join(root, 'global', 'MEMORY.md'));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 从用户填的任意一层路径解析出「mimocode 应用根」(含 mimocode.db 的那层),找不到返回 null。
|
|
599
|
+
// 处理两层意思:① base 本身就是应用根(含 mimocode.db 或 memory/);② base 是 memory 或更上层,
|
|
600
|
+
// 沿 known 目录名(memory、mimocode)逐层下探 / 上爬,命中 mimocode.db 即认。
|
|
601
|
+
export function mimocodeRoot(base) {
|
|
602
|
+
if (!base) return null;
|
|
603
|
+
// 直接是文件(用户误填 mimocode.db 本身):那就是根
|
|
604
|
+
try {
|
|
605
|
+
if (fs.statSync(base).isFile()) {
|
|
606
|
+
if (path.basename(base).toLowerCase() === 'mimocode.db') return path.dirname(base);
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
} catch {}
|
|
610
|
+
if (!isDir(base)) return null;
|
|
611
|
+
if (hasMimocodeDb(base)) return base;
|
|
612
|
+
// ② 没库时回退到 memory 展示层判据(本函数上方注释写的「②」,此前只写了判据函数、
|
|
613
|
+
// **从未接进来** —— 后果是库被删/被锁的机器上,memory/ 会被 atomcode 兜底接走
|
|
614
|
+
// (isDir(sessions) 就认),解析器一个文件都匹配不到 → 0 条且静默。
|
|
615
|
+
// 这正是 mimocode-test 里「隔离 HOME、只拷 memory」那条用例长期红的根因。)
|
|
616
|
+
if (hasMimocodeSessions(base) || hasMimocodeProjects(base)) return base;
|
|
617
|
+
// 往下探:memory / mimocode 子目录
|
|
618
|
+
for (const d of ['memory', 'mimocode']) {
|
|
619
|
+
const p = path.join(base, d);
|
|
620
|
+
if (isDir(p) && (hasMimocodeDb(p) || hasMimocodeSessions(p) || hasMimocodeProjects(p)))
|
|
621
|
+
return mimocodeRoot(p);
|
|
622
|
+
}
|
|
623
|
+
// 往上爬:找 mimocode.db
|
|
624
|
+
let cur = base;
|
|
625
|
+
for (let i = 0; i < 5; i++) {
|
|
626
|
+
const up = path.dirname(cur);
|
|
627
|
+
if (up === cur) break;
|
|
628
|
+
if (hasMimocodeDb(up)) return up;
|
|
629
|
+
cur = up;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
// 兼容旧名:返回 memory 展示层根(库读不动时的兜底入口)
|
|
634
|
+
export function mimocodeMemoryRoot(base) { return mimocodeRoot(base); }
|
|
635
|
+
|
|
636
|
+
// ---------------- GitHub Copilot CLI(~/.copilot/session-state/<id>/events.jsonl) ----------------
|
|
637
|
+
// Copilot 的 ~/.copilot/ide/*.lock 只是 IDE/MCP 通信锁;真正的会话正文落在 session-state 下。
|
|
638
|
+
// 判据与扫描器共用 copilotSessionFiles,避免“探测认得、扫描读不到”的漂移。
|
|
639
|
+
function copilotStateDir(base) {
|
|
640
|
+
if (!base) return null;
|
|
641
|
+
if (path.basename(base).toLowerCase() === 'session-state') return isDir(base) ? base : null;
|
|
642
|
+
const p = path.join(base, 'session-state');
|
|
643
|
+
return isDir(p) ? p : null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function isCopilotEvents(fp) {
|
|
647
|
+
if (!isFile(fp)) return false;
|
|
648
|
+
try {
|
|
649
|
+
const head = fs.readFileSync(fp, 'utf8').slice(0, 32768);
|
|
650
|
+
for (const line of head.split(/\r?\n/)) {
|
|
651
|
+
if (!line.trim()) continue;
|
|
652
|
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
|
653
|
+
if (j?.type === 'session.start' || j?.type === 'user.message') return true;
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function copilotSessionFiles(base) {
|
|
660
|
+
const state = copilotStateDir(base);
|
|
661
|
+
if (!state) return [];
|
|
662
|
+
const out = [];
|
|
663
|
+
for (const id of listDirCached(state) || []) {
|
|
664
|
+
const dir = path.join(state, id);
|
|
665
|
+
if (!isDir(dir)) continue;
|
|
666
|
+
const fp = path.join(dir, 'events.jsonl');
|
|
667
|
+
if (isCopilotEvents(fp)) out.push({ sessionId: id, dir, fp });
|
|
668
|
+
}
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function hasCopilotSessions(base) {
|
|
673
|
+
return copilotSessionFiles(base).length > 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ---------------- kilo(KiloCode / Kilo CLI:opencode 的 fork) ----------------
|
|
677
|
+
// 真源:<home>/.local/share/kilo/kilo.db(明文 SQLite + WAL,**不需要解密**;页头就是 `SQLite format 3\0`)。
|
|
678
|
+
// XDG 摆法,本机 Windows 实测也在这里(不是 %APPDATA%\kilo,也不是 ~/.kilo);同目录还有
|
|
679
|
+
// kilo.db-wal / kilo.db-shm、log/、repos/、storage/(storage/session_diff/ 是 opencode 那套文件式 diff)。
|
|
680
|
+
//
|
|
681
|
+
// ⚠️ 判据按**独有表**认,且各处必须排在 opencode 之前:kilo.db 与 opencode.db 是**同源 schema**,
|
|
682
|
+
// opencodeDbVerify 的五个条件(session/message/part/event/event_sequence)在 kilo.db 上**全为真**
|
|
683
|
+
// (本机实测)—— 光按那些表认,kilo 的库会被 opencode 探针接走,结果是 kilo 一条都出不来
|
|
684
|
+
// (与 mimocode 当初被接走、dsh/kimi 被 atomcode 兜底接走同源)。
|
|
685
|
+
// 区分点用 kilo **自己**的扩展表:
|
|
686
|
+
// kilo 独有:kilo_board / kilo_board_message(多 agent 协作板)
|
|
687
|
+
// ⚠️ 别拿 credential / project_directory / session_input / session_message 当判据 —— 那四张是
|
|
688
|
+
// **opencode 本体**也有的扩展表,而 kilo 作为 fork 把它们**保留**了下来(本机实测全在),
|
|
689
|
+
// 拿它们当判据等于两家都认(mimocode 恰好相反:它把这些换成了 history_fts_idx / actor_registry 等)。
|
|
690
|
+
// ⚠️ session_context_epoch 看着像 kilo 新增,**其实 opencode 本体也有**(本机实测两个库都在):
|
|
691
|
+
// 把它放进判据的后果是 opencode.db 反过来被 kiloDbVerify 认成 kilo,而 sniffBase 里 kilo 又排在
|
|
692
|
+
// opencode 前面 —— 真 opencode 会被静默抢走(实测踩到,故只留 kilo_board 两张)。
|
|
693
|
+
export const KILO_ONLY_TABLES = ['kilo_board', 'kilo_board_message'];
|
|
694
|
+
export function kiloDbVerify(fp) {
|
|
695
|
+
if (!isFile(fp)) return false;
|
|
696
|
+
// 先验页头再交给 node:sqlite:加密库(SQLCipher)与任意二进制在这里就被挡掉,
|
|
697
|
+
// 不会让 sniffBase 每轮去尝试打开一个注定失败的文件。
|
|
698
|
+
try {
|
|
699
|
+
const fd = fs.openSync(fp, 'r');
|
|
700
|
+
try {
|
|
701
|
+
const head = Buffer.alloc(16);
|
|
702
|
+
const n = fs.readSync(fd, head, 0, 16, 0);
|
|
703
|
+
if (n !== 16 || head.toString('latin1') !== 'SQLite format 3\0') return false;
|
|
704
|
+
} finally { fs.closeSync(fd); }
|
|
705
|
+
} catch { return false; }
|
|
706
|
+
const mod = sqliteMod();
|
|
707
|
+
if (!mod) return false;
|
|
708
|
+
let db = null;
|
|
709
|
+
try {
|
|
710
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
711
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
712
|
+
// 先把同源家族的那三张基础表作为门槛,再要求至少命中一张 kilo 独有表
|
|
713
|
+
if (!(names.has('session') && names.has('message') && names.has('part'))) return false;
|
|
714
|
+
return KILO_ONLY_TABLES.some(t => names.has(t));
|
|
715
|
+
} catch { return false; }
|
|
716
|
+
finally { try { db && db.close(); } catch {} }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 手工添加可能填:kilo.db 文件本身 / 数据根 ~/.local/share/kilo / ~/.local/share /
|
|
720
|
+
// ~/.local / 用户主目录 —— 都认。返回命中的**数据库文件路径**(sessions 字段存文件路径,与 opencode 同款)。
|
|
721
|
+
const KILO_DB_RELS = [
|
|
722
|
+
'kilo.db',
|
|
723
|
+
path.join('kilo', 'kilo.db'),
|
|
724
|
+
path.join('share', 'kilo', 'kilo.db'),
|
|
725
|
+
path.join('.local', 'share', 'kilo', 'kilo.db'),
|
|
726
|
+
path.join('local', 'share', 'kilo', 'kilo.db'),
|
|
727
|
+
];
|
|
728
|
+
export function kiloDbFile(base) {
|
|
729
|
+
if (!base) return null;
|
|
730
|
+
try { if (fs.statSync(base).isFile()) return kiloDbVerify(base) ? base : null; } catch { return null; }
|
|
731
|
+
for (const rel of KILO_DB_RELS) {
|
|
732
|
+
const fp = path.join(base, rel);
|
|
733
|
+
try { if (fs.statSync(fp).isFile() && kiloDbVerify(fp)) return fp; } catch {}
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ---------------- opencode(~/.local/share/opencode/opencode.db,明文 SQLite) ----------------
|
|
739
|
+
// OpenCode 的会话也**不在文件树里**,而是集中写在一个明文 SQLite 库(WAL 模式,旁边有 opencode.db-wal):
|
|
740
|
+
// session / message(data JSON) / part(data JSON) 是正文与用量,event + event_sequence 是同一份状态的
|
|
741
|
+
// 事件投影(解析器不把它当第二条数据源,见 parsers/opencode.mjs 的文件头)。
|
|
742
|
+
// **不需要解密**:这是标准 SQLite(页头就是 `SQLite format 3\0`),与 traedb 那种 SQLCipher 库不是一回事。
|
|
743
|
+
//
|
|
744
|
+
// 判据必须是**内容**,而且要比 zcode 更严:两个库都有 session + message + part 三张表,
|
|
745
|
+
// 光按那三张认会把 OpenCode 的库判成 zcode(或反之)。区分点在各自独有的表上 ——
|
|
746
|
+
// OpenCode 独有:event / event_sequence(事件投影)
|
|
747
|
+
// ZCode 独有:turn_usage / model_usage / tool_usage
|
|
748
|
+
// 所以 opencode 的判据加上 event + event_sequence,并且在 sniffBase 里排在 zcode 前面。
|
|
749
|
+
export function opencodeDbVerify(fp) {
|
|
750
|
+
if (!isFile(fp)) return false;
|
|
751
|
+
// 先验页头再交给 node:sqlite:加密库(SQLCipher)与任意二进制在这里就被挡掉,
|
|
752
|
+
// 不会让 sniffBase 每轮去尝试打开一个注定失败的文件。
|
|
753
|
+
try {
|
|
754
|
+
const fd = fs.openSync(fp, 'r');
|
|
755
|
+
try {
|
|
756
|
+
const head = Buffer.alloc(16);
|
|
757
|
+
const n = fs.readSync(fd, head, 0, 16, 0);
|
|
758
|
+
if (n !== 16 || head.toString('latin1') !== 'SQLite format 3\0') return false;
|
|
759
|
+
} finally { fs.closeSync(fd); }
|
|
760
|
+
} catch { return false; }
|
|
761
|
+
const mod = sqliteMod();
|
|
762
|
+
if (!mod) return false;
|
|
763
|
+
let db = null;
|
|
764
|
+
try {
|
|
765
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
766
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
767
|
+
if (!(names.has('session') && names.has('message') && names.has('part')
|
|
768
|
+
&& names.has('event') && names.has('event_sequence'))) return false;
|
|
769
|
+
// ⚠️ 必须把 mimocode 的库挡回去:两家是**同源 schema**,上面五张表在 mimocode.db 上全为真
|
|
770
|
+
//(本机实测),不挡就会被这里接走 → mimocode 恒 0 条。判据见 MIMOCODE_ONLY_TABLES。
|
|
771
|
+
if (MIMOCODE_ONLY_TABLES.some(t => names.has(t))) return false;
|
|
772
|
+
// ⚠️ kilo 同理(KiloCode 是 opencode 的 fork,上面五张表在 kilo.db 上同样全为真,本机实测):
|
|
773
|
+
// 不挡的话,用户把 kilo.db 手工填给 opencode(或把 ~/.local/share 整个填进来)就会被这里接走,
|
|
774
|
+
// 且 server 的自愈逻辑还会把 kilo 的 kind 改写成 opencode。判据见 KILO_ONLY_TABLES。
|
|
775
|
+
if (KILO_ONLY_TABLES.some(t => names.has(t))) return false;
|
|
776
|
+
return true;
|
|
777
|
+
} catch { return false; }
|
|
778
|
+
finally { try { db && db.close(); } catch {} }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 手工添加可能填:opencode.db 文件本身 / 数据根 ~/.local/share/opencode / ~/.local/share /
|
|
782
|
+
// ~/.local / 用户主目录 —— 都认。返回命中的**数据库文件路径**(sessions 字段存文件路径,与 zcode 同款)。
|
|
783
|
+
const OPENCODE_DB_RELS = [
|
|
784
|
+
'opencode.db',
|
|
785
|
+
path.join('opencode', 'opencode.db'),
|
|
786
|
+
path.join('share', 'opencode', 'opencode.db'),
|
|
787
|
+
path.join('.local', 'share', 'opencode', 'opencode.db'),
|
|
788
|
+
path.join('local', 'share', 'opencode', 'opencode.db'),
|
|
789
|
+
];
|
|
790
|
+
export function opencodeDbFile(base) {
|
|
791
|
+
if (!base) return null;
|
|
792
|
+
try { if (fs.statSync(base).isFile()) return opencodeDbVerify(base) ? base : null; } catch { return null; }
|
|
793
|
+
for (const rel of OPENCODE_DB_RELS) {
|
|
794
|
+
const fp = path.join(base, rel);
|
|
795
|
+
try { if (fs.statSync(fp).isFile() && opencodeDbVerify(fp)) return fp; } catch {}
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---------------- hermes(Hermes Agent:state.db,明文 SQLite + WAL) ----------------
|
|
801
|
+
// 落盘:<hermes home>/state.db。home 按平台(源码 hermes_constants.py 核实):
|
|
802
|
+
// Windows 原生 = %LOCALAPPDATA%\hermes;Linux/mac/WSL2 = ~/.hermes;HERMES_HOME 环境变量可整体改写。
|
|
803
|
+
// 判据要比 zcode/opencode 更留意重名:**state.db 这种文件名不稀奇**,认的是三张独有表
|
|
804
|
+
// (sessions/messages/session_model_usage,复数命名 + 用量表,与 zcode 的 session/message/part、
|
|
805
|
+
// opencode 的 session/message/part+event 都错开)。旁边常年挂着 -wal(未 checkpoint 的新数据在 WAL 里,
|
|
806
|
+
// node:sqlite 只读连接会带上,与 opencode 同款处理)。
|
|
807
|
+
// 同目录 logs/agent.log 是逐次用量的辅源,判据不看它(缺失只降级口径,不影响认领)。
|
|
808
|
+
export function hermesDbVerify(fp) {
|
|
809
|
+
if (!isFile(fp)) return false;
|
|
810
|
+
try {
|
|
811
|
+
const fd = fs.openSync(fp, 'r');
|
|
812
|
+
try {
|
|
813
|
+
const head = Buffer.alloc(16);
|
|
814
|
+
const n = fs.readSync(fd, head, 0, 16, 0);
|
|
815
|
+
if (n !== 16 || head.toString('latin1') !== 'SQLite format 3\0') return false;
|
|
816
|
+
} finally { fs.closeSync(fd); }
|
|
817
|
+
} catch { return false; }
|
|
818
|
+
const mod = sqliteMod();
|
|
819
|
+
if (!mod) return false;
|
|
820
|
+
let db = null;
|
|
821
|
+
try {
|
|
822
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
823
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
824
|
+
return names.has('sessions') && names.has('messages') && names.has('session_model_usage');
|
|
825
|
+
} catch { return false; }
|
|
826
|
+
finally { try { db && db.close(); } catch {} }
|
|
827
|
+
}
|
|
828
|
+
// 手工添加可能填:state.db 文件本身 / hermes home(~/.hermes、%LOCALAPPDATA%\hermes)/ 其父目录 —— 都认。
|
|
829
|
+
// 返回命中的**数据库文件路径**(sessions 字段存文件路径,与 zcode/opencode 同款)。
|
|
830
|
+
const HERMES_DB_RELS = [
|
|
831
|
+
'state.db',
|
|
832
|
+
path.join('hermes', 'state.db'),
|
|
833
|
+
path.join('.hermes', 'state.db'),
|
|
834
|
+
];
|
|
835
|
+
export function hermesDbFile(base) {
|
|
836
|
+
if (!base) return null;
|
|
837
|
+
try { if (fs.statSync(base).isFile()) return hermesDbVerify(base) ? base : null; } catch { return null; }
|
|
838
|
+
for (const rel of HERMES_DB_RELS) {
|
|
839
|
+
const fp = path.join(base, rel);
|
|
840
|
+
try { if (fs.statSync(fp).isFile() && hermesDbVerify(fp)) return fp; } catch {}
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ---------------- devin(Devin CLI:sessions.db,明文 SQLite + WAL) ----------------
|
|
846
|
+
// 落盘:<cli 数据目录>/sessions.db。数据目录(产品文档 devin 的 configuration/extensibility 章节核实):
|
|
847
|
+
// Windows = %APPDATA%\devin\cli;mac/Linux = ~/.local/share/devin/cli。
|
|
848
|
+
// 判据里**不含 `sessions` 这张表** —— 它跟 hermes / zcode / opencode 都重名;认的是三张**独有表同时在场**:
|
|
849
|
+
// sessions + message_nodes(消息森林)+ tool_call_state(工具调用状态)。
|
|
850
|
+
// 另注:老版本(cognition 时代)的数据在 ~/.local/share/cognition/cli/,升级时**留下了兼容符号链接**指向
|
|
851
|
+
// devin/ —— 所以**故意不去探那条老路径**:跟进去只会把同一个库按两个路径注册成两个 agent。
|
|
852
|
+
export function devinDbVerify(fp) {
|
|
853
|
+
if (!isFile(fp)) return false;
|
|
854
|
+
try {
|
|
855
|
+
const fd = fs.openSync(fp, 'r');
|
|
856
|
+
try {
|
|
857
|
+
const head = Buffer.alloc(16);
|
|
858
|
+
const n = fs.readSync(fd, head, 0, 16, 0);
|
|
859
|
+
if (n !== 16 || head.toString('latin1') !== 'SQLite format 3\0') return false;
|
|
860
|
+
} finally { fs.closeSync(fd); }
|
|
861
|
+
} catch { return false; }
|
|
862
|
+
const mod = sqliteMod();
|
|
863
|
+
if (!mod) return false;
|
|
864
|
+
let db = null;
|
|
865
|
+
try {
|
|
866
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
867
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
868
|
+
return names.has('sessions') && names.has('message_nodes') && names.has('tool_call_state');
|
|
869
|
+
} catch { return false; }
|
|
870
|
+
finally { try { db && db.close(); } catch {} }
|
|
871
|
+
}
|
|
872
|
+
// 手工添加可能填:sessions.db 文件本身 / cli 数据目录(%APPDATA%\Devin\cli、~/.local/share/devin/cli)/
|
|
873
|
+
// 产品根(%APPDATA%\Devin、~/.local/share/devin)—— 都认。返回命中的**数据库文件路径**(与 hermes 同款,
|
|
874
|
+
// sessions 字段存文件路径)。'Devin'/'devin' 两种大小写都列上:Windows 路径不敏感,POSIX 上两种写法
|
|
875
|
+
// 都有人建。
|
|
876
|
+
const DEVIN_DB_RELS = [
|
|
877
|
+
'sessions.db',
|
|
878
|
+
path.join('cli', 'sessions.db'),
|
|
879
|
+
path.join('Devin', 'cli', 'sessions.db'),
|
|
880
|
+
path.join('devin', 'cli', 'sessions.db'),
|
|
881
|
+
];
|
|
882
|
+
export function devinDbFile(base) {
|
|
883
|
+
if (!base) return null;
|
|
884
|
+
try { if (fs.statSync(base).isFile()) return devinDbVerify(base) ? base : null; } catch { return null; }
|
|
885
|
+
for (const rel of DEVIN_DB_RELS) {
|
|
886
|
+
const fp = path.join(base, rel);
|
|
887
|
+
try { if (fs.statSync(fp).isFile() && devinDbVerify(fp)) return fp; } catch {}
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ---------------- zcode(~/.zcode/cli/db/db.sqlite,WAL 模式的 SQLite 库) ----------------
|
|
893
|
+
// ZCode 的会话**不在工作区里**(workspace 下没有 claude 那样的 jsonl 转录),而是集中写在用户目录的
|
|
894
|
+
// 一个 SQLite 库里:session / message / part / session_input / tool_usage / model_usage / turn_usage。
|
|
895
|
+
// 这正是「聊了几轮、手动添加却检测不到日志」的根因 —— 扫文件树的嗅探永远看不到数据库里的数据。
|
|
896
|
+
//
|
|
897
|
+
// 判据是**内容**(sqlite_master 里有没有 session+message+part 三张核心表),不是文件名:
|
|
898
|
+
// 叫 db.sqlite 的文件遍地都是,内容不符一律不认,免得把别的工具的库接进来。
|
|
899
|
+
// 只读打开、查完即关;库被锁/损坏/不是 sqlite 一律返回 null(下一轮扫描再试,不报错不崩溃)。
|
|
900
|
+
// 嗅探路径每轮扫描都会走,这里只开一次库、查一张 sqlite_master,开销与读一个小文件同级。
|
|
901
|
+
// node:sqlite 加载器在 shared.mjs(cursor / zcode / discovery 共用同一份实例)。
|
|
902
|
+
export function zcodeDbVerify(fp) {
|
|
903
|
+
const mod = sqliteMod();
|
|
904
|
+
if (!mod) return false;
|
|
905
|
+
let db = null;
|
|
906
|
+
try {
|
|
907
|
+
db = new mod.DatabaseSync(fp, { readOnly: true });
|
|
908
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
|
|
909
|
+
return names.has('session') && names.has('message') && names.has('part');
|
|
910
|
+
} catch { return false; }
|
|
911
|
+
finally { try { db && db.close(); } catch {} }
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// 手工添加可能填:db.sqlite 文件本身 / db 目录 / cli 目录 / .zcode 基目录 —— 四种都认。
|
|
915
|
+
// 返回命中的**数据库文件路径**(sessions 字段存文件路径,不是目录),认不出返回 null。
|
|
916
|
+
export function zcodeDbFile(base) {
|
|
917
|
+
if (!base) return null;
|
|
918
|
+
try {
|
|
919
|
+
if (fs.statSync(base).isFile()) return zcodeDbVerify(base) ? base : null;
|
|
920
|
+
} catch { return null; }
|
|
921
|
+
for (const rel of ['db.sqlite', path.join('db', 'db.sqlite'), path.join('cli', 'db', 'db.sqlite')]) {
|
|
922
|
+
const fp = path.join(base, rel);
|
|
923
|
+
try { if (fs.statSync(fp).isFile() && zcodeDbVerify(fp)) return fp; } catch {}
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ---------------- traedb(Trae CN / Trae Work 的 SQLCipher 会话库,见 parsers/traedb.mjs) ----------------
|
|
929
|
+
// 路径:<产品根>/ModularData/ai-agent/database.db。**判据只能是「路径 + 文件特征」**:
|
|
930
|
+
// 库是 SQLCipher 加密的(头 16 字节是随机 salt,不是 'SQLite format 3\0'),拿 sqlite 去开必然失败 ——
|
|
931
|
+
// zcode 那种「查 sqlite_master 认表」的内容判据在这里用不了;而解密需要密钥,嗅探每轮都跑、不能配密钥。
|
|
932
|
+
// 所以:路径组合命中 + 不是明文 sqlite + 有起码的体量,就认。认错的风险被路径本身兜住
|
|
933
|
+
// (ModularData/ai-agent/database.db 这个组合是 Trae 专属的,别的产品不这么摆)。
|
|
934
|
+
// 文件特征只挡一种误接:用户手工指定时指到一份**明文** sqlite(database.db 这个名字太普通)——
|
|
935
|
+
// 那种库不是这个解析器该读的源(下游解密一定读不出),宁可在这里不认。
|
|
936
|
+
function traeDbVerify(fp) {
|
|
937
|
+
try {
|
|
938
|
+
const st = fs.statSync(fp);
|
|
939
|
+
if (!st.isFile() || st.size < 1024) return false;
|
|
940
|
+
const fd = fs.openSync(fp, 'r');
|
|
941
|
+
try {
|
|
942
|
+
const head = Buffer.alloc(16);
|
|
943
|
+
const n = fs.readSync(fd, head, 0, 16, 0);
|
|
944
|
+
return n === 16 && head.toString('latin1') !== 'SQLite format 3\0';
|
|
945
|
+
} finally { fs.closeSync(fd); }
|
|
946
|
+
} catch { return false; }
|
|
947
|
+
}
|
|
948
|
+
// 手工添加可能填:database.db 文件本身 / ai-agent 目录 / ModularData 目录 / 产品根目录 —— 都认。
|
|
949
|
+
// 返回命中的**数据库文件路径**(sessions 字段存文件路径,与 zcode 同款)。
|
|
950
|
+
export function traeDbFile(base) {
|
|
951
|
+
if (!base) return null;
|
|
952
|
+
try {
|
|
953
|
+
const st = fs.statSync(base);
|
|
954
|
+
if (st.isFile()) return /^database\.db$/i.test(path.basename(base)) && traeDbVerify(base) ? base : null;
|
|
955
|
+
} catch { return null; }
|
|
956
|
+
for (const rel of ['database.db', path.join('ai-agent', 'database.db'), path.join('ModularData', 'ai-agent', 'database.db')]) {
|
|
957
|
+
const fp = path.join(base, rel);
|
|
958
|
+
try { if (fs.statSync(fp).isFile() && traeDbVerify(fp)) return fp; } catch {}
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
// db 读不出来(没配密钥 / 密钥不对 / Trae 正在写库)时回退 renderer.log 需要的两样东西:
|
|
963
|
+
// traeLogKindOf —— 这个产品该用哪个日志解析器(Trae Work / SOLO → tracework,其余 → tracecode)
|
|
964
|
+
// traeLogsFromDb —— logs 根目录在哪(沿 database.db 往上找 <产品根>/logs)
|
|
965
|
+
// 判据与 sniffBase 的日志分支、candidateBases 的别名表保持同一套(目录名带 work/solo 就是 TraeWork)。
|
|
966
|
+
export function traeLogKindOf(dbPath) {
|
|
967
|
+
let d = path.dirname(dbPath);
|
|
968
|
+
for (let i = 0; i < 4; i++) {
|
|
969
|
+
const bn = path.basename(d).toLowerCase();
|
|
970
|
+
if (bn.startsWith('trae')) return (bn.includes('work') || bn.includes('solo')) ? 'tracework' : 'tracecode';
|
|
971
|
+
const up = path.dirname(d);
|
|
972
|
+
if (up === d) break;
|
|
973
|
+
d = up;
|
|
974
|
+
}
|
|
975
|
+
return 'tracecode'; // 认不出产品根(数据被挪到自定义路径)时按主产品 Trae CN 兜底
|
|
976
|
+
}
|
|
977
|
+
export function traeLogsFromDb(dbPath) {
|
|
978
|
+
let d = path.dirname(dbPath);
|
|
979
|
+
for (let i = 0; i < 4; i++) {
|
|
980
|
+
const l = path.join(d, 'logs');
|
|
981
|
+
if (isDir(l) && hasTraeRendererLogs(l)) return l;
|
|
982
|
+
const up = path.dirname(d);
|
|
983
|
+
if (up === d) break;
|
|
984
|
+
d = up;
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
// ---------------- doubao(豆包 Work 智能体,trajectory.jsonl) ----------------
|
|
991
|
+
// 落盘:<workspace>/.sessions/<会话 id>/agents/<agent id>/system/trajectory.jsonl
|
|
992
|
+
// OpenAI 风格逐行 jsonl({"role":"user"|"assistant"|"tool",…}),user 开轮、assistant 累加、
|
|
993
|
+
// tool 行按 tool_call_id 回填。同目录 assignment.md 是每轮 user 消息的 ISO 时间戳源。
|
|
994
|
+
//
|
|
995
|
+
// 判据 = 目录结构 + 内容,读盘量压到最小(嗅探每 3s 走一轮):
|
|
996
|
+
// 最多看 8 个会话 × 8 个 agent × 每个 trajectory 头 4KB,要求其中至少一条记录的
|
|
997
|
+
// role 是 user/assistant/tool 之一。`agents/<id>/system/trajectory.jsonl` 这个摆法
|
|
998
|
+
// 加上 role 字段双保险,不会把别的工具的 trajectory.jsonl 误认进来。
|
|
999
|
+
export function hasDoubaoSessions(base) {
|
|
1000
|
+
if (!isDir(base)) return false;
|
|
1001
|
+
let s = 0;
|
|
1002
|
+
try {
|
|
1003
|
+
for (const sid of listDirCached(base) || []) {
|
|
1004
|
+
if (++s > 8) break;
|
|
1005
|
+
const agents = path.join(base, sid, 'agents');
|
|
1006
|
+
if (!isDir(agents)) continue;
|
|
1007
|
+
for (const aid of (listDirCached(agents) || []).slice(0, 8)) {
|
|
1008
|
+
const fp = path.join(agents, aid, 'system', 'trajectory.jsonl');
|
|
1009
|
+
if (!isFile(fp)) continue;
|
|
1010
|
+
try {
|
|
1011
|
+
const st = fs.statSync(fp);
|
|
1012
|
+
if (st.size < 8) continue; // 空文件 / 只有半行:不算
|
|
1013
|
+
const fd = fs.openSync(fp, 'r');
|
|
1014
|
+
try {
|
|
1015
|
+
const buf = Buffer.alloc(Math.min(4096, st.size));
|
|
1016
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
1017
|
+
const head = buf.toString('utf8', 0, n);
|
|
1018
|
+
for (const line of head.split('\n')) {
|
|
1019
|
+
if (!line.includes('"role"')) continue;
|
|
1020
|
+
try {
|
|
1021
|
+
const j = JSON.parse(line);
|
|
1022
|
+
if (j.role === 'user' || j.role === 'assistant' || j.role === 'tool') return true;
|
|
1023
|
+
} catch {}
|
|
1024
|
+
}
|
|
1025
|
+
} finally { fs.closeSync(fd); }
|
|
1026
|
+
} catch {}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} catch {}
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 从用户填的任意一层路径解析出 `.sessions` 根(找不到返回 null)。
|
|
1034
|
+
// 处理两层意思:① base 本身就是 .sessions;② base 是它上面任意一层(DoubaoWork 应用根 /
|
|
1035
|
+
// User Data / Default / .doubaowork / agent_mode / workspace),沿已知目录名逐层下探。
|
|
1036
|
+
// 下探是「名字命中」+「内容验证」双闸:名字猜错不会误认(最后一道是 hasDoubaoSessions 的
|
|
1037
|
+
// 内容判据),所以可以放心把非 doubao 的目录也放进来探。
|
|
1038
|
+
const DOUBAO_DESCENT = ['User Data', 'Default', '.doubaowork', 'agent_mode', 'workspace'];
|
|
1039
|
+
export function doubaoSessionsRoot(base) {
|
|
1040
|
+
if (!isDir(base)) return null;
|
|
1041
|
+
if (path.basename(base) === '.sessions' && hasDoubaoSessions(base)) return base;
|
|
1042
|
+
let cur = base;
|
|
1043
|
+
for (let i = 0; i < 6; i++) {
|
|
1044
|
+
const s = path.join(cur, '.sessions');
|
|
1045
|
+
if (isDir(s) && hasDoubaoSessions(s)) return s;
|
|
1046
|
+
let next = null;
|
|
1047
|
+
for (const d of DOUBAO_DESCENT) {
|
|
1048
|
+
const p = path.join(cur, d);
|
|
1049
|
+
if (isDir(p)) { next = p; break; }
|
|
1050
|
+
}
|
|
1051
|
+
if (!next) return null;
|
|
1052
|
+
cur = next;
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// buddy 型: 同上目录结构,但记录格式不同(codebuddy / workbuddy)
|
|
1058
|
+
// codearts 型: <base>/User/logs/CodeArts Agent-*.log(sessions 字段存 logs 目录)
|
|
1059
|
+
// gemini 型: <base>/tmp/<项目slug>/{chats/session-*.json, logs.json}(sessions 字段存 tmp 目录)
|
|
1060
|
+
// cursor 型: <base>/projects/<项目slug>/agent-transcripts/<会话uuid>/<同名>.jsonl(sessions 字段存 projects 目录)
|
|
1061
|
+
// zcode 型: <base>/cli/db/db.sqlite(sessions 字段存**数据库文件本身**,不是目录——
|
|
1062
|
+
// 手工添加时填 db.sqlite 文件 / db 目录 / cli 目录 / .zcode 基目录都认)
|
|
1063
|
+
// kilo 型: <base>/kilo.db(同 opencode 型,sessions 存数据库文件本身;手工添加时填 kilo.db 文件 /
|
|
1064
|
+
// 数据根 ~/.local/share/kilo / ~/.local/share / ~/.local / 用户主目录都认)
|
|
1065
|
+
// opencode 型: <base>/opencode.db(同上,sessions 存数据库文件本身;手工添加时填 opencode.db 文件 /
|
|
1066
|
+
// 数据根 ~/.local/share/opencode / ~/.local/share / ~/.local / 用户主目录都认)
|
|
1067
|
+
// traedb 型: <base>/ModularData/ai-agent/database.db(同上,sessions 存数据库文件本身;
|
|
1068
|
+
// 手工添加时填 database.db 文件 / ai-agent 目录 / ModularData 目录 / 产品根目录都认)
|
|
1069
|
+
// ---------------- Comate(百度)会话嗅探 ----------------
|
|
1070
|
+
// 判据是**内容**:store 目录里至少有一个 chat_session_* 普通文件(会话正文是整份 JSON,
|
|
1071
|
+
// 首字符 '{'、含 "sessionUuid")。blobs/ 里的 .d 是目录、comate_chat_sessions.jsonl 是索引,
|
|
1072
|
+
// 都不算数 —— 名字猜错不会误接,与 openclaw 的首行判据同一条约定。
|
|
1073
|
+
// 探测(sniffBase)与扫描(scanComate)都认「平铺 chat_session_* 文件」这一种摆法。
|
|
1074
|
+
export function comateSessionFiles(storeDir) {
|
|
1075
|
+
const out = [];
|
|
1076
|
+
for (const n of listDirCached(storeDir) || []) {
|
|
1077
|
+
if (!n.startsWith('chat_session_')) continue;
|
|
1078
|
+
const fp = path.join(storeDir, n);
|
|
1079
|
+
let st; try { st = fs.statSync(fp); } catch { continue; }
|
|
1080
|
+
if (st.isFile()) out.push({ fp, st });
|
|
1081
|
+
}
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
export function hasComateSessions(storeDir) {
|
|
1085
|
+
if (!isDir(storeDir)) return false;
|
|
1086
|
+
const list = comateSessionFiles(storeDir);
|
|
1087
|
+
if (!list.length) return false;
|
|
1088
|
+
try {
|
|
1089
|
+
const head = fs.readFileSync(list[0].fp, 'utf8').slice(0, 512);
|
|
1090
|
+
return head.trimStart().startsWith('{') && head.includes('"sessionUuid"');
|
|
1091
|
+
} catch { return false; }
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export function sniffBase(base) {
|
|
1095
|
+
// kilo 必须排在 opencode **之前**:kilo.db 与 opencode.db 是同源 schema,kilo.db 在 opencodeDbVerify
|
|
1096
|
+
// 下五张表全为真(本机实测),排在后面就永远轮不到它 → kilo 恒 0 条。两家靠独有表互斥
|
|
1097
|
+
// (opencodeDbVerify 也反向排除了 KILO_ONLY_TABLES),判据见各自 verify 的注释。
|
|
1098
|
+
const klf = kiloDbFile(base);
|
|
1099
|
+
if (klf) return { kind: 'kilo', sessions: klf, traces: null };
|
|
1100
|
+
// opencode 是**文件型**源头(sessions 存 opencode.db 路径),且必须排在 zcode 前面:
|
|
1101
|
+
// 两个库都有 session + message + part,zcode 的判据(只看这三张表)会把 opencode 的库接走。
|
|
1102
|
+
const okf = opencodeDbFile(base);
|
|
1103
|
+
if (okf) return { kind: 'opencode', sessions: okf, traces: null };
|
|
1104
|
+
// zcode 是**文件型**源头,必须在 isDir 兜底之前判断:手工添加直接填 db.sqlite 的路径时,
|
|
1105
|
+
// base 是个文件,进不了下面任何一个 isDir 分支
|
|
1106
|
+
const zf = zcodeDbFile(base);
|
|
1107
|
+
if (zf) return { kind: 'zcode', sessions: zf, traces: null };
|
|
1108
|
+
// traedb 同理是文件型(sessions 存 database.db 的路径),也必须在 isDir 兜底之前
|
|
1109
|
+
const tf = traeDbFile(base);
|
|
1110
|
+
if (tf) return { kind: 'traedb', sessions: tf, traces: null };
|
|
1111
|
+
// hermes 同理是文件型(sessions 存 state.db 的路径):~/.hermes / %LOCALAPPDATA%\hermes /
|
|
1112
|
+
// HERMES_HOME 指哪儿都命中;判据是三张独有表,与上面两家错开(见 hermesDbVerify 注释)
|
|
1113
|
+
const hf = hermesDbFile(base);
|
|
1114
|
+
if (hf) return { kind: 'hermes', sessions: hf, traces: null };
|
|
1115
|
+
// devin 同理是文件型(sessions 存 sessions.db 的路径):%APPDATA%\Devin\cli /
|
|
1116
|
+
// ~/.local/share/devin/cli 指哪儿都命中;判据是三张独有表(见 devinDbVerify 注释)
|
|
1117
|
+
const df = devinDbFile(base);
|
|
1118
|
+
if (df) return { kind: 'devindb', sessions: df, traces: null };
|
|
1119
|
+
// comate(百度 Comate):store 目录判据是内容(chat_session_* 文件首部含 "sessionUuid"),
|
|
1120
|
+
// 必须抢在 atomcode 兜底(isDir(sessions) 就认)之前——store 下没有 sessions/ 子目录,
|
|
1121
|
+
// 但用户手工填 ~/.comate-engine 或 /store 时不能静默变成 0 条(与 dsh/kimi 当初的坑同源)。
|
|
1122
|
+
if (hasComateSessions(base)) return { kind: 'comate', sessions: base, traces: null };
|
|
1123
|
+
// minimax(Mavis 本地运行时):路径可能直接是会话目录(含 messages.jsonl + manifest.json),
|
|
1124
|
+
// 也可能是 v2/sessions 根(向下四层到 session_<id>)。必须在 isDir(sessions) 兜底之前:
|
|
1125
|
+
// 否则 ~/.minimax/v2/sessions 会被嗅探认成 atomcode、且解析器在 messages.jsonl 上一个都匹配不到 → 0 条。
|
|
1126
|
+
// minimax(Mavis 本地运行时):base 可能是会话目录、v2/sessions 根、或 ~/.minimax 应用根。
|
|
1127
|
+
// hasMavisSession 直接返回解析出的会话根(~/.minimax → ~/.minimax/v2/sessions),sniffBase 用它当 sessions。
|
|
1128
|
+
{ const mv = hasMavisSession(base); if (mv) return { kind: 'minimax', sessions: mv, traces: null }; }
|
|
1129
|
+
// openclaw(OpenClaw):真源是 <stateDir>/agents/<id>/sessions/<uuid>.jsonl(stateDir 默认 ~/.openclaw)。
|
|
1130
|
+
// 必须抢在 atomcode 兜底(isDir(sessions) 就认)之前认 —— 用户手工填 <stateDir>/agents/<id>/sessions 时,
|
|
1131
|
+
// 那正是 atomcode 兜底要接的形状,而 openclaw 的条目在里面一条都匹配不到 → 0 条且静默(dsh/kimi 同源)。
|
|
1132
|
+
// 判据是**内容**(首行必须是会话头 {"type":"session","version":…,"id":…}),名字猜错不会误接。
|
|
1133
|
+
{ const oc = openclawTranscripts(base); if (oc) return { kind: 'openclaw', sessions: oc.root, traces: null }; }
|
|
1134
|
+
// cline(Cline CLI):真源是 <data>/sessions/<会话 id>/<会话 id>.messages.json(**整份 JSON、每次落盘整体重写**)。
|
|
1135
|
+
// 必须抢在 atomcode 兜底之前:手工填 `<...>/data/sessions` 时,那正是兜底两分支(isDir(sessions) /
|
|
1136
|
+
// basename==='sessions')要接的形状,而 atomcode 找的是 <hash>/*.jsonl|*.meta → 0 条且静默
|
|
1137
|
+
// (与 dsh / kimi / openclaw 当初同源)。判据是**内容**(文件名以 .messages.json 结尾 + 头 2KB 里同时
|
|
1138
|
+
// 出现 "sessionId" 与 "messages": [),所以 `~/.cline` / `data` / `sessions` / 单个会话目录 /
|
|
1139
|
+
// 直接填那份 json 五种写法都认,名字猜错也不会误接邻居的数据。
|
|
1140
|
+
{ const cl = clineTranscripts(base); if (cl) return { kind: 'cline', sessions: cl.root, traces: null }; }
|
|
1141
|
+
// buddyext(CodeBuddy **扩展版**,与上面 codebuddy=CLI 是两个产品):真源是
|
|
1142
|
+
// %LOCALAPPDATA%\CodeBuddyExtension\Data/<acct>/<Host>/(<acct>/)?history/<wsKey>/<会话 id>/index.json。
|
|
1143
|
+
// 同样必须抢在 atomcode 兜底之前:手工填到 `<Host>` 那一层时底下并没有 sessions/,但填到
|
|
1144
|
+
// `<Data>` 或 `<Data>/<acct>` 时隔壁就有别的 agent 形状的目录,兜底会认成 atomcode → 0 条且静默
|
|
1145
|
+
// (与 dsh / kimi / openclaw / cline 当初同源)。判据是**结构**(acct/Host/history 三层齐不齐),
|
|
1146
|
+
// 且只在 `<base>/Data` 存在、或 base 顺着祖先走到名为 data 的层时才付 readdir 代价 —— 见 buddyExtRoot。
|
|
1147
|
+
{ const bx = buddyExtRoot(base); if (bx) return { kind: 'buddyext', sessions: bx, traces: null }; }
|
|
1148
|
+
if (!isDir(base)) return null;
|
|
1149
|
+
// TraeCode / TraeWork 桌面版:<base>/logs/<timestamp>/window1/renderer.log
|
|
1150
|
+
// 必须排在所有 sessions/projects 判据之前 —— Trae CN 应用目录下没有 sessions/、projects/、
|
|
1151
|
+
// traces/ 等子目录,但有 ModularData/、logs/、User/ 等,落到 atomcode 兜底会变成 0 条且静默。
|
|
1152
|
+
// 区分 kind 靠 base 目录名(Trae CN → tracecode;Trae Work / SOLO → tracework):
|
|
1153
|
+
// 装在同一台机器上的两个产品会被自动发现分别注册(不同 name、不同 kind、不同 sessions 路径)。
|
|
1154
|
+
// 这条分支只伺候**库不在**的机器(老版本 Trae / 库被删)或密钥读不出来的回退:库在时上面那条
|
|
1155
|
+
// traedb 已经接走了 —— 回退由主文件按 traeLogKindOf/traeLogsFromDb 指路,不靠这里。⚠️
|
|
1156
|
+
const traeLogs = path.join(base, 'logs');
|
|
1157
|
+
if (isDir(traeLogs) && hasTraeRendererLogs(traeLogs)) {
|
|
1158
|
+
const bn = path.basename(base).toLowerCase();
|
|
1159
|
+
if (bn.includes('work') || bn.includes('solo')) {
|
|
1160
|
+
return { kind: 'tracework', sessions: traeLogs, traces: null };
|
|
1161
|
+
}
|
|
1162
|
+
return { kind: 'tracecode', sessions: traeLogs, traces: null };
|
|
1163
|
+
}
|
|
1164
|
+
// Copilot:session-state/*/events.jsonl 必须抢在通用目录兜底之前,否则 ~/.copilot 会被误判为未知目录。
|
|
1165
|
+
if (hasCopilotSessions(base)) return { kind: 'copilot', sessions: base, traces: null };
|
|
1166
|
+
// doubao:workspace/.sessions/<会话 id>/agents/<agent id>/system/trajectory.jsonl。
|
|
1167
|
+
// 必须排在 atomcode 兜底(isDir(sessions) 就认)之前——用户手工填的可能就是 .sessions /
|
|
1168
|
+
// workspace / 应用根任意一层,全由 doubaoSessionsRoot 下探解析;内容判据兜底,不会误接。
|
|
1169
|
+
const dbSess = doubaoSessionsRoot(base);
|
|
1170
|
+
if (dbSess) return { kind: 'doubao', sessions: dbSess, traces: null };
|
|
1171
|
+
const sessions = path.join(base, 'sessions');
|
|
1172
|
+
const traces = path.join(base, 'traces');
|
|
1173
|
+
const projects = path.join(base, 'projects');
|
|
1174
|
+
const caLogs = path.join(base, 'User', 'logs');
|
|
1175
|
+
if (isDir(caLogs) && hasCodeartsLogs(caLogs)) return { kind: 'codearts', sessions: caLogs, traces: null };
|
|
1176
|
+
const geminiTmp = path.join(base, 'tmp');
|
|
1177
|
+
if (isDir(geminiTmp) && hasGeminiSessions(geminiTmp)) return { kind: 'gemini', sessions: geminiTmp, traces: null };
|
|
1178
|
+
// cursor 必须排在 hasClaudeJsonl 前面:那个判据只看「slug 目录里有没有 .jsonl」,不认内容
|
|
1179
|
+
if (isDir(projects) && hasCursorTranscripts(projects)) return { kind: 'cursor', sessions: projects, traces: null };
|
|
1180
|
+
// buddy 先于 trace:这类目录(codebuddy/workbuddy)同时有 traces,但 traces 只是空壳,projects 才是正文
|
|
1181
|
+
if (isDir(projects) && hasBuddyJsonl(projects)) return { kind: 'buddyjsonl', sessions: projects, traces: null };
|
|
1182
|
+
if (isDir(traces) && hasTraceFiles(traces)) return { kind: 'trace', sessions: isDir(sessions) ? sessions : null, traces };
|
|
1183
|
+
if (isDir(projects) && hasClaudeJsonl(projects)) return { kind: 'claude', sessions: projects, traces: null };
|
|
1184
|
+
if (isDir(sessions) && hasCodexRollouts(sessions)) return { kind: 'codex', sessions, traces: null };
|
|
1185
|
+
// kimi:判据是 wire.jsonl 的事件结构,必须排在 atomcode 兜底(isDir(sessions) 就认)前面
|
|
1186
|
+
if (isDir(sessions) && hasKimiSessions(sessions)) return { kind: 'kimi', sessions, traces: null };
|
|
1187
|
+
// dsh:同理必须排在兜底前面 —— 它的 sessions/<slug>/<会话目录>/ 这个摆法会被 atomcode 兜底接走,
|
|
1188
|
+
// 而 atomcode 找的是 <hash>/<文件>.jsonl|.meta,对 dsh 一个都匹配不到 → 0 条且静默
|
|
1189
|
+
if (isDir(sessions) && hasDshSessions(sessions)) return { kind: 'dsh', sessions, traces: null };
|
|
1190
|
+
// mimocode(记忆/检查点类 agent):真源是 mimocode.db,位于 mimocode 应用根(通常为
|
|
1191
|
+
// <home>/.local/share/mimocode,库在 memory/ 上一层)。memory/ 下的 markdown 只是展示层。
|
|
1192
|
+
// 必须抢在 atomcode 兜底(isDir(sessions) 就认)前认,否则 memory/sessions/<id>/ 会被认成 atomcode
|
|
1193
|
+
// (atomcode 解析器一个文件都匹配不到 → 0 条且静默,与 dsh / kimi 当初的处境同源)。
|
|
1194
|
+
// sessions 字段存解析出的「应用根」(含 mimocode.db 或 memory/),扫描器内部再定位库 / 兜底 markdown。
|
|
1195
|
+
{ const mm = mimocodeRoot(base); if (mm) return { kind: 'mimocode', sessions: mm, traces: null }; }
|
|
1196
|
+
if (isDir(sessions)) return { kind: 'atomcode', sessions, traces: null };
|
|
1197
|
+
// 直接给的就是 projects 目录(或形如 projects 的目录:<slug>/*.jsonl)
|
|
1198
|
+
if (hasBuddyJsonl(base)) return { kind: 'buddyjsonl', sessions: base, traces: null };
|
|
1199
|
+
if (hasCursorTranscripts(base)) return { kind: 'cursor', sessions: base, traces: null }; // 同样要抢在 hasClaudeJsonl 前
|
|
1200
|
+
if (hasClaudeJsonl(base)) return { kind: 'claude', sessions: base, traces: null };
|
|
1201
|
+
if (path.basename(base).toLowerCase() === 'projects') return { kind: 'claude', sessions: base, traces: null };
|
|
1202
|
+
// 用户手工添加时很可能**直接填 sessions 目录**(本机那条历史配置就是这么填的)。
|
|
1203
|
+
// 这一支同样要抢在 atomcode 前面,否则手工加 dsh 会静默变成 0 条 —— 与 kimi 踩过的坑同源。
|
|
1204
|
+
if (path.basename(base).toLowerCase() === 'sessions' && hasDshSessions(base)) return { kind: 'dsh', sessions: base, traces: null };
|
|
1205
|
+
if (path.basename(base).toLowerCase() === 'sessions') return { kind: 'atomcode', sessions: base, traces: null };
|
|
1206
|
+
if (path.basename(base).toLowerCase() === 'logs' && hasCodeartsLogs(base)) return { kind: 'codearts', sessions: base, traces: null };
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
export { candidateBases, probeBases, kimiDesktopSessionsRoots, basesFor };
|
|
1211
|
+
|
|
1212
|
+
// genie 扩展(VSCode / CodeBuddyIDE / JetBrains 与 CodeBuddy CN 应用共用)的数据目录名。
|
|
1213
|
+
// 它**不是**一个独立 agent:会话并到 `codebuddy` 那一个 agent 名下(两份落盘是同一产品的不同形态,
|
|
1214
|
+
// 用户心智里那就是一个 CodeBuddy;并源的那段理由见主文件 discoverAgents 里的注释)。
|
|
1215
|
+
// 所以它不进 NAME_ALIASES(否则自动发现会再注册出一行 codebuddy-ext),只给下面这个函数用 ——
|
|
1216
|
+
// 主文件每轮拿它算出「该挂到 codebuddy.sessionsExtra 上的根」。
|
|
1217
|
+
const BUDDY_EXT_DIRNAMES = ['CodeBuddyExtension'];
|
|
1218
|
+
export function buddyExtDataRoots() {
|
|
1219
|
+
const out = [];
|
|
1220
|
+
for (const base of basesFor(BUDDY_EXT_DIRNAMES)) {
|
|
1221
|
+
const r = buddyExtRoot(base);
|
|
1222
|
+
if (r && !out.includes(r)) out.push(r);
|
|
1223
|
+
}
|
|
1224
|
+
return out;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// re-export 让 sniffBase 能直接用 hasMavisSession(定义在 parsers/minimax.mjs):
|
|
1228
|
+
// 不在头部 import 是为避开「discovery 静态 import minimax.mjs → 服务启动必须先解析 minimax.mjs」
|
|
1229
|
+
// 的耦合;minimax.mjs 现在只 import shared.mjs,本身已是轻量模块,import 进来即可。
|
|
1230
|
+
export { hasMavisSession } from './minimax.mjs';
|