@bolloon/bolloon-agent 0.3.39 → 0.3.41
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.
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { runAdaptiveScan, logEvolution } from '../pi-ecosystem-judgment/adaptive-scan.js';
|
|
12
12
|
import { collectBolloonContext } from './context-collector.js';
|
|
13
|
-
import { migrateAllExternalAgents, formatMigrationNotices } from '../migration/external-agent-migrator.js';
|
|
13
|
+
import { migrateAllExternalAgents, defaultDeps, formatMigrationNotices } from '../migration/external-agent-migrator.js';
|
|
14
14
|
/**
|
|
15
15
|
* 入口: web server / CLI 启动时调一次
|
|
16
16
|
*/
|
|
@@ -20,7 +20,12 @@ export async function bootstrapBolloon(opts = {}) {
|
|
|
20
20
|
// 0. 外部智能体 (openclaw/hermes) 数据迁移 — 隐式处理, 静默跑, 结果通告用户
|
|
21
21
|
let externalAgentMigrations = [];
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const depsM = defaultDeps();
|
|
24
|
+
if (opts.home)
|
|
25
|
+
depsM.home = opts.home;
|
|
26
|
+
if (opts.localAppData)
|
|
27
|
+
depsM.localAppData = opts.localAppData;
|
|
28
|
+
externalAgentMigrations = await migrateAllExternalAgents(depsM);
|
|
24
29
|
for (const line of formatMigrationNotices(externalAgentMigrations)) {
|
|
25
30
|
console.log(`[bootstrap] ${line}`);
|
|
26
31
|
}
|
|
@@ -9,16 +9,12 @@
|
|
|
9
9
|
* 这样 Bolloon 能直接加载同一套性格 / 记忆 / 技能, 无缝兼容.
|
|
10
10
|
* - 隐式处理: 启动时静默跑, 失败不影响主流程; 完成后通告给用户 (见 report).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* - workspace/SOUL.md
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - workspace/MEMORY.md → persona/<agent>/wiki.md
|
|
19
|
-
* - workspace/skills/<name>/SKILL.md → ~/.bolloon/skills/<name>/ (整目录复制)
|
|
20
|
-
* - workspace/memory/*.md → ~/.bolloon/memory/<agent>/sessions/<n>.summary.md
|
|
21
|
-
* - workspace/*.md (其它) → ~/.bolloon/context-os/04-Projects/<agent>-docs/
|
|
12
|
+
* 异构布局 (2026-08-08 v0.3.40):
|
|
13
|
+
* - OpenClaw 平铺在 ~/.openclaw/workspace/ (SOUL/IDENTITY/USER/AGENTS/TOOLS/MEMORY.md +
|
|
14
|
+
* skills/<name>/ + memory/*.md)
|
|
15
|
+
* - Hermes 根在 %LOCALAPPDATA%\hermes (Windows, 兜底 ~/.hermes), persona 分布在
|
|
16
|
+
* SOUL.md(根) + memories/{USER,MEMORY}.md, skills 是 skills/<分类>/<技能>/SKILL.md
|
|
17
|
+
* 两级嵌套 (235 个). 迁移时展平并以 <分类>-<技能> 命名避免重名冲突.
|
|
22
18
|
*
|
|
23
19
|
* 幂等: 每个源落一份 manifest (~/.bolloon/migration/<source>.json),
|
|
24
20
|
* 记录已迁移的文件 hash; 未变化则跳过, 已存在则覆盖源文档 (文档允许演进),
|
|
@@ -49,8 +45,13 @@ function realExists(p) {
|
|
|
49
45
|
return fs.access(p).then(() => true).catch(() => false);
|
|
50
46
|
}
|
|
51
47
|
export function defaultDeps() {
|
|
48
|
+
const home = os.homedir();
|
|
52
49
|
return {
|
|
53
|
-
home
|
|
50
|
+
home,
|
|
51
|
+
platform: os.platform(),
|
|
52
|
+
localAppData: (typeof process !== 'undefined' && process.env && (process.env.LOCALAPPDATA || process.env.ProgramData))
|
|
53
|
+
? (process.env.LOCALAPPDATA || process.env.ProgramData)
|
|
54
|
+
: path.join(home, 'AppData', 'Local'),
|
|
54
55
|
readFile: realReadFile,
|
|
55
56
|
readdir: realReaddir,
|
|
56
57
|
stat: realStat,
|
|
@@ -63,13 +64,47 @@ export function defaultDeps() {
|
|
|
63
64
|
// ============================================================
|
|
64
65
|
// 纯函数: 目录布局 + hash
|
|
65
66
|
// ============================================================
|
|
66
|
-
/**
|
|
67
|
+
/** 各源默认根目录: openclaw 在 home/.openclaw; hermes 兜底 home/.hermes (真实见 candidates) */
|
|
67
68
|
export function sourceRootPath(source, home) {
|
|
68
69
|
return source === 'openclaw'
|
|
69
70
|
? path.join(home, '.openclaw')
|
|
70
71
|
: path.join(home, '.hermes');
|
|
71
72
|
}
|
|
72
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 各源全部候选根路径 (按优先级, 遍历时取第一个存在者).
|
|
75
|
+
*
|
|
76
|
+
* 覆盖三大平台的实际安装位置:
|
|
77
|
+
* OpenClaw: 主目录 ~/.openclaw (三平台一致), 兜底 ~/.config/openclaw
|
|
78
|
+
* Hermes :
|
|
79
|
+
* - win32 %LOCALAPPDATA%\hermes → ~/.hermes
|
|
80
|
+
* - darwin ~/Library/Application Support/hermes → ~/.hermes
|
|
81
|
+
* - linux ~/.local/share/hermes → ~/.config/hermes → ~/.hermes
|
|
82
|
+
*/
|
|
83
|
+
export function sourceRootCandidates(source, deps) {
|
|
84
|
+
const home = deps.home;
|
|
85
|
+
const platform = deps.platform || 'linux';
|
|
86
|
+
if (source === 'openclaw') {
|
|
87
|
+
return [
|
|
88
|
+
path.join(home, '.openclaw'),
|
|
89
|
+
path.join(home, '.config', 'openclaw'),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const candidates = [];
|
|
93
|
+
if (platform === 'win32') {
|
|
94
|
+
if (deps.localAppData)
|
|
95
|
+
candidates.push(path.join(deps.localAppData, 'hermes'));
|
|
96
|
+
}
|
|
97
|
+
else if (platform === 'darwin') {
|
|
98
|
+
candidates.push(path.join(home, 'Library', 'Application Support', 'hermes'));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
candidates.push(path.join(home, '.local', 'share', 'hermes'));
|
|
102
|
+
candidates.push(path.join(home, '.config', 'hermes'));
|
|
103
|
+
}
|
|
104
|
+
candidates.push(path.join(home, '.hermes'));
|
|
105
|
+
return candidates;
|
|
106
|
+
}
|
|
107
|
+
/** workspace 路径 (openclaw 用 workspace/, hermes 平铺在根) */
|
|
73
108
|
export function workspacePath(source, sourceRoot) {
|
|
74
109
|
return source === 'openclaw'
|
|
75
110
|
? path.join(sourceRoot, 'workspace')
|
|
@@ -79,6 +114,36 @@ export function workspacePath(source, sourceRoot) {
|
|
|
79
114
|
export function sha1(content) {
|
|
80
115
|
return crypto.createHash('sha1').update(content).digest('hex');
|
|
81
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* 内容级脱敏: 抹掉明敏凭据, 防止迁移产物把真实的 Bearer token / API key /
|
|
119
|
+
* MT5 会话标识 / 长随机串 带进 Bolloon 落盘. 迁移只搬运"知识", 不搬运"秘密".
|
|
120
|
+
*
|
|
121
|
+
* 处理模式:
|
|
122
|
+
* - Authorization / Bearer <token>
|
|
123
|
+
* - token: / api_key: / access_token: / secret: 等 key 声明后的随机串
|
|
124
|
+
* - 形如 sk-... / ghp_... / AKIA... 的 platform token
|
|
125
|
+
* - 一行冒号后跟 20+ 位 base64/hex 随机串 ("MT5 data: D0E8...", "token: Ab...")
|
|
126
|
+
*
|
|
127
|
+
* 保留中文句子与结构说明, 只替换被判定为凭据的 token 片段.
|
|
128
|
+
*/
|
|
129
|
+
export function redactSecrets(text) {
|
|
130
|
+
const REDACTED = '***REDACTED***';
|
|
131
|
+
let out = text;
|
|
132
|
+
// 0. Bearer <JWT/base64> / Authorization: Bearer ... — 最优先, 连同包头一起抹
|
|
133
|
+
out = out.replace(/\bBearer\s+[A-Za-z0-9_./\-=+]{12,}/gi, REDACTED);
|
|
134
|
+
out = out.replace(/\bAuthorization\s*[:=]\s*(?:Bearer\s+)?[A-Za-z0-9_./\-=+]{12,}/gi, REDACTED);
|
|
135
|
+
// 1. explicit key=value 声明 (token/apiKey/access_token/key/secret/password)
|
|
136
|
+
out = out.replace(/\b(token|api[_-]?key|access[_-]?token|secret|password)\b\s*[:=]\s*["']?[A-Za-z0-9_./+\-]{8,}["']?/gi, (_m, k) => `${k}: ${REDACTED}`);
|
|
137
|
+
// 2. platform 前缀 token (sk- / sk-proj- / sk-ant- / ghp_ / AKIA / xoxb-)
|
|
138
|
+
out = out.replace(/\b(sk-[A-Za-z0-9_]{8,}|sk-proj-[A-Za-z0-9_-]{8,}|sk-ant-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bp]-[A-Za-z0-9-]{8,})\b/g, REDACTED);
|
|
139
|
+
// 3. "标签: <20+位随机串>" 结构 — 冒号后跟长的 alnum token (MT5 data: D0E8...).
|
|
140
|
+
// 保守: 只匹配不含 `/`(避开 URL/路径) 且不含域名点 的纯 token 骨架.
|
|
141
|
+
out = out.replace(/([A-Za-z][A-Za-z0-9 _-]{0,20})\s*[::]\s*([A-Za-z0-9_+\-]{20,})\b(?![A-Za-z0-9])/g, (_m, name) => `${name}: ${REDACTED}`);
|
|
142
|
+
// 4. 宽松: 独立长串 24+ (base64) — 前后为空白/标点/行首行尾, 避开 URL 与中文段.
|
|
143
|
+
// 保守: 不含 `/` 与 `.`, 防误伤 URL/域名/路径.
|
|
144
|
+
out = out.replace(/(^|[\s(([::\])])[A-Za-z0-9_+=]{22,}(?=[\s))\].,,。;;::§]|$)/gm, (_m, prefix) => `${prefix}${REDACTED}`);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
82
147
|
/** bolloon 目标根 (默认 ~/.bolloon, 可注入 override) */
|
|
83
148
|
function bolloonRoot(home) {
|
|
84
149
|
return path.join(home, '.bolloon');
|
|
@@ -86,15 +151,16 @@ function bolloonRoot(home) {
|
|
|
86
151
|
// ============================================================
|
|
87
152
|
// 单文件迁移 helper
|
|
88
153
|
// ============================================================
|
|
89
|
-
async function copyIfNeeded(deps, from, to, manifest) {
|
|
154
|
+
async function copyIfNeeded(deps, from, to, manifest, redact = false) {
|
|
90
155
|
const content = await deps.readFile(from);
|
|
91
156
|
if (content === undefined)
|
|
92
157
|
return false;
|
|
93
|
-
const
|
|
158
|
+
const contentOut = redact ? redactSecrets(content) : content;
|
|
159
|
+
const hash = sha1(contentOut);
|
|
94
160
|
if (manifest.get(to) === hash)
|
|
95
161
|
return false; // 未变化, 跳过
|
|
96
162
|
await deps.mkdir(path.dirname(to));
|
|
97
|
-
await deps.writeFile(to,
|
|
163
|
+
await deps.writeFile(to, contentOut);
|
|
98
164
|
manifest.set(to, hash);
|
|
99
165
|
return true;
|
|
100
166
|
}
|
|
@@ -122,14 +188,28 @@ async function copyDirIfNeeded(deps, srcDir, destDir, manifest) {
|
|
|
122
188
|
// ============================================================
|
|
123
189
|
// 主迁移
|
|
124
190
|
// ============================================================
|
|
125
|
-
/**
|
|
191
|
+
/** 探测某源是否安装: 返回选中的根目录 or null. */
|
|
126
192
|
export async function detectSource(deps, source) {
|
|
127
|
-
const root
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
for (const root of sourceRootCandidates(source, deps)) {
|
|
194
|
+
const st = await deps.stat(root);
|
|
195
|
+
if (st?.isDirectory)
|
|
196
|
+
return root;
|
|
197
|
+
}
|
|
131
198
|
return null;
|
|
132
199
|
}
|
|
200
|
+
const OPENCLAW_PERSONA = [
|
|
201
|
+
{ src: 'SOUL.md', toName: 'soul.md' },
|
|
202
|
+
{ src: 'IDENTITY.md', toName: 'identity.md' },
|
|
203
|
+
{ src: 'USER.md', toName: 'user.md' },
|
|
204
|
+
{ src: 'AGENTS.md', toName: 'agent.md' },
|
|
205
|
+
{ src: 'TOOLS.md', toName: 'project.md' },
|
|
206
|
+
{ src: 'MEMORY.md', toName: 'wiki.md' },
|
|
207
|
+
];
|
|
208
|
+
const HERMES_PERSONA = [
|
|
209
|
+
{ src: 'SOUL.md', toName: 'soul.md' },
|
|
210
|
+
{ src: 'memories/USER.md', toName: 'user.md' },
|
|
211
|
+
{ src: 'memories/MEMORY.md', toName: 'wiki.md' },
|
|
212
|
+
];
|
|
133
213
|
/**
|
|
134
214
|
* 迁移单个源的全部数据到 Bolloon.
|
|
135
215
|
* 返回 report; 源不存在 → migrated=false 且不抛错 (静默).
|
|
@@ -154,6 +234,7 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
154
234
|
report.migrated = false;
|
|
155
235
|
return report; // 未安装, 静默
|
|
156
236
|
}
|
|
237
|
+
report.sourceRoot = root;
|
|
157
238
|
const ws = workspacePath(source, root);
|
|
158
239
|
const wsStat = await deps.stat(ws);
|
|
159
240
|
if (!wsStat?.isDirectory) {
|
|
@@ -185,43 +266,67 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
185
266
|
catch { /* 损坏忽略, 从头迁 */ }
|
|
186
267
|
}
|
|
187
268
|
await deps.mkdir(path.join(bRoot, 'migration'));
|
|
188
|
-
// 1. persona
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
['USER.md', 'user.md'],
|
|
193
|
-
['AGENTS.md', 'agent.md'],
|
|
194
|
-
['TOOLS.md', 'project.md'],
|
|
195
|
-
['MEMORY.md', 'wiki.md'],
|
|
196
|
-
];
|
|
197
|
-
for (const [fromName, toName] of personaMap) {
|
|
198
|
-
const from = path.join(ws, fromName);
|
|
269
|
+
// 1. persona (per-source spec) — 含敏感 token, 内容级脱敏后写入
|
|
270
|
+
const personaSpec = source === 'openclaw' ? OPENCLAW_PERSONA : HERMES_PERSONA;
|
|
271
|
+
for (const { src, toName } of personaSpec) {
|
|
272
|
+
const from = path.join(ws, src);
|
|
199
273
|
const to = path.join(personaDir, toName);
|
|
200
|
-
if (await copyIfNeeded(deps, from, to, manifest)) {
|
|
274
|
+
if (await copyIfNeeded(deps, from, to, manifest, true)) {
|
|
201
275
|
report.persona.push(toName);
|
|
202
276
|
report.entries.push({ from, to, kind: 'persona' });
|
|
203
277
|
}
|
|
204
278
|
}
|
|
205
|
-
// 2. skills
|
|
279
|
+
// 2. skills → ~/.bolloon/skills/<name>/
|
|
206
280
|
const skillsSrc = path.join(ws, 'skills');
|
|
207
281
|
const skillDirs = await deps.readdir(skillsSrc);
|
|
208
282
|
if (skillDirs) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
283
|
+
// OpenClaw: skills/<name>/SKILL.md 一层, 直接落盘 <name>.
|
|
284
|
+
// Hermes: skills/<分类>/<技能>/SKILL.md 两层, 逐 <技能> 递归找 SKILL.md,
|
|
285
|
+
// 落盘 <分类>-<技能> 展平, 避免跨分类重名.
|
|
286
|
+
for (const entryName of skillDirs) {
|
|
287
|
+
if (entryName.startsWith('.'))
|
|
213
288
|
continue;
|
|
214
|
-
|
|
289
|
+
const entry = path.join(skillsSrc, entryName);
|
|
290
|
+
const st = await deps.stat(entry);
|
|
291
|
+
if (!st?.isDirectory)
|
|
215
292
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
293
|
+
if (source === 'hermes') {
|
|
294
|
+
// 分类下的每个技能目录 → 目标 <分类>-<技能>
|
|
295
|
+
const cat = entryName;
|
|
296
|
+
const subDirs = await deps.readdir(entry);
|
|
297
|
+
if (!subDirs)
|
|
298
|
+
continue;
|
|
299
|
+
for (const skillName of subDirs) {
|
|
300
|
+
if (skillName.startsWith('.'))
|
|
301
|
+
continue;
|
|
302
|
+
const skillDir = path.join(entry, skillName);
|
|
303
|
+
const sst = await deps.stat(skillDir);
|
|
304
|
+
if (!sst?.isDirectory)
|
|
305
|
+
continue;
|
|
306
|
+
// 该分类下菊 不一定有 SKILL.md → 跳过
|
|
307
|
+
const hasSkill = await deps.exists(path.join(skillDir, 'SKILL.md'));
|
|
308
|
+
if (!hasSkill)
|
|
309
|
+
continue;
|
|
310
|
+
const targetName = `${cat}-${skillName}`;
|
|
311
|
+
const destDir = path.join(skillsRoot, targetName);
|
|
312
|
+
const copied = await copyDirIfNeeded(deps, skillDir, destDir, manifest);
|
|
313
|
+
if (copied.length > 0) {
|
|
314
|
+
report.skillsCopied.push(targetName);
|
|
315
|
+
report.entries.push({ from: skillDir, to: destDir, kind: 'skill' });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const destDir = path.join(skillsRoot, entryName);
|
|
321
|
+
const copied = await copyDirIfNeeded(deps, entry, destDir, manifest);
|
|
322
|
+
if (copied.length > 0) {
|
|
323
|
+
report.skillsCopied.push(entryName);
|
|
324
|
+
report.entries.push({ from: entry, to: destDir, kind: 'skill' });
|
|
325
|
+
}
|
|
221
326
|
}
|
|
222
327
|
}
|
|
223
328
|
}
|
|
224
|
-
// 3. memory: workspace/memory/*.md → memory
|
|
329
|
+
// 3. memory: openclaw workspace/memory/*.md → sessions/; hermes 无独立 memory 目录
|
|
225
330
|
const memSrc = path.join(ws, 'memory');
|
|
226
331
|
const memFiles = await deps.readdir(memSrc);
|
|
227
332
|
if (memFiles) {
|
|
@@ -231,7 +336,7 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
231
336
|
continue;
|
|
232
337
|
const from = path.join(memSrc, f);
|
|
233
338
|
const to = path.join(memoryRoot, `${idx + 1}-${f}`);
|
|
234
|
-
if (await copyIfNeeded(deps, from, to, manifest)) {
|
|
339
|
+
if (await copyIfNeeded(deps, from, to, manifest, true)) {
|
|
235
340
|
report.memoryCopied.push(f);
|
|
236
341
|
report.entries.push({ from, to, kind: 'memory' });
|
|
237
342
|
}
|
|
@@ -241,11 +346,11 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
241
346
|
// 4. docs: workspace 根其他 .md → context-os/04-Projects/<source>-docs/
|
|
242
347
|
const wsFiles = await deps.readdir(ws);
|
|
243
348
|
if (wsFiles) {
|
|
244
|
-
const excluded =
|
|
349
|
+
const excluded = personaSpec.map((p) => p.toName);
|
|
245
350
|
for (const f of wsFiles) {
|
|
246
351
|
if (!f.endsWith('.md'))
|
|
247
352
|
continue;
|
|
248
|
-
if (excluded.
|
|
353
|
+
if (excluded.includes(f))
|
|
249
354
|
continue;
|
|
250
355
|
const from = path.join(ws, f);
|
|
251
356
|
const st = await deps.stat(from);
|