@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
- externalAgentMigrations = await migrateAllExternalAgents();
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
- * → 目标映射 (OpenClaw workspace 布局):
13
- * - workspace/SOUL.md persona/<agent>/soul.md
14
- * - workspace/IDENTITY.md → persona/<agent>/identity.md
15
- * - workspace/USER.md → persona/<agent>/user.md
16
- * - workspace/AGENTS.md persona/<agent>/agent.md
17
- * - workspace/TOOLS.md → persona/<agent>/project.md
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: os.homedir(),
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
- /** 各源的根目录 (相对 home) */
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
- /** workspace 路径 (openclaw 用 workspace/, hermes 假设平铺在根) */
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 hash = sha1(content);
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, content);
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 = sourceRootPath(source, deps.home);
128
- const st = await deps.stat(root);
129
- if (st?.isDirectory)
130
- return root;
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 6 文件映射
189
- const personaMap = [
190
- ['SOUL.md', 'soul.md'],
191
- ['IDENTITY.md', 'identity.md'],
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: workspace/skills/*/ → ~/.bolloon/skills/<name>/
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
- for (const dirName of skillDirs) {
210
- const srcDir = path.join(skillsSrc, dirName);
211
- const st = await deps.stat(srcDir);
212
- if (!st?.isDirectory)
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
- if (dirName.startsWith('.'))
289
+ const entry = path.join(skillsSrc, entryName);
290
+ const st = await deps.stat(entry);
291
+ if (!st?.isDirectory)
215
292
  continue;
216
- const destDir = path.join(skillsRoot, dirName);
217
- const copied = await copyDirIfNeeded(deps, srcDir, destDir, manifest);
218
- if (copied.length > 0) {
219
- report.skillsCopied.push(dirName);
220
- report.entries.push({ from: srcDir, to: destDir, kind: 'skill' });
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/<agent>/sessions/
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 = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md', 'AGENTS.md', 'TOOLS.md', 'MEMORY.md']);
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.has(f))
353
+ if (excluded.includes(f))
249
354
  continue;
250
355
  const from = path.join(ws, f);
251
356
  const st = await deps.stat(from);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",