@bolloon/bolloon-agent 0.3.24 → 0.3.25
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/README.md +83 -474
- package/dist/agents/pi-sdk-tools.js +337 -15
- package/dist/agents/plan-store.js +167 -0
- package/dist/agents/skill-writer.js +193 -0
- package/dist/agents/workflow-pivot-loop.js +86 -25
- package/dist/constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js +1 -1
- package/dist/constraint-runtime/src/tools/PolymarketSDK/createOrder.js +1 -1
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getOrders.js +1 -1
- package/dist/network/known-peers.js +52 -8
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/client.js +520 -17
- package/dist/web/index.html +6 -2
- package/dist/web/server.js +481 -29
- package/dist/web/style.css +14 -10
- package/dist/web/ui/message-renderer.js +42 -3
- package/dist/web/ui/step-timeline.js +5 -0
- package/package.json +6 -2
- package/bin/ipfs +0 -0
|
@@ -271,16 +271,18 @@ export function registerBuiltinTools(ctx) {
|
|
|
271
271
|
// add_friend_by_id — 通过 Hyperswarm publicKey 添加 P2P 好友
|
|
272
272
|
ctx.tools.set('add_friend_by_id', {
|
|
273
273
|
name: 'add_friend_by_id',
|
|
274
|
-
description: '通过 Hyperswarm P2P publicKey (64 字符 hex) 添加好友. 对方在线时会收到好友申请弹窗, 接受后自动分享 channel.',
|
|
274
|
+
description: '通过 Hyperswarm P2P publicKey (64 字符 hex) 添加好友. 对方在线时会收到好友申请弹窗, 接受后自动分享 channel. 强烈建议填 note 备注 (自我介绍/来源), 对方才能分辨你是谁.',
|
|
275
275
|
parameters: {
|
|
276
276
|
publicKey: '64 字符 hex publicKey (必填)',
|
|
277
277
|
name: '可选, 给好友的备注名 (如: 同事-张磊)',
|
|
278
|
-
message: '可选, 附加的好友申请消息'
|
|
278
|
+
message: '可选, 附加的好友申请消息',
|
|
279
|
+
note: '可选, 备注 (自我介绍/来源), 对方接受时会看到. 如 "我是[姓名]的 Bolloon agent[name], 来自[来源], 想加你为好友共享 channel 协作,技能: [技能列表]"'
|
|
279
280
|
},
|
|
280
281
|
execute: async (args) => {
|
|
281
282
|
const publicKey = String(args.publicKey || '').trim();
|
|
282
283
|
const name = String(args.name || '').trim();
|
|
283
284
|
const message = String(args.message || '想加你为 P2P 好友, 共享 channel 协作').trim();
|
|
285
|
+
const note = String(args.note || '').trim();
|
|
284
286
|
if (!publicKey || publicKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(publicKey)) {
|
|
285
287
|
return { success: false, error: 'publicKey 必须是 64 字符 hex 格式' };
|
|
286
288
|
}
|
|
@@ -289,7 +291,7 @@ export function registerBuiltinTools(ctx) {
|
|
|
289
291
|
const res = await fetch(`http://127.0.0.1:${port}/api/friend-request`, {
|
|
290
292
|
method: 'POST',
|
|
291
293
|
headers: { 'Content-Type': 'application/json' },
|
|
292
|
-
body: JSON.stringify({ targetPublicKey: publicKey, name: name || undefined, message })
|
|
294
|
+
body: JSON.stringify({ targetPublicKey: publicKey, name: name || undefined, message, note: note || undefined })
|
|
293
295
|
});
|
|
294
296
|
const data = await res.json();
|
|
295
297
|
if (!res.ok) {
|
|
@@ -303,6 +305,95 @@ export function registerBuiltinTools(ctx) {
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
});
|
|
308
|
+
// list_pending_friend_requests — 查看待处理的好友申请 (智能体侧处理入口)
|
|
309
|
+
ctx.tools.set('list_pending_friend_requests', {
|
|
310
|
+
name: 'list_pending_friend_requests',
|
|
311
|
+
description: '查看当前待处理的好友申请列表 (对方想加你为好友但还没接受). 每个申请带 fromName / note 备注 (自我介绍/来源), 便于判断是否接受. 用 accept_friend_request 接受, 或 ignore_friend_request 忽略.',
|
|
312
|
+
parameters: {},
|
|
313
|
+
execute: async () => {
|
|
314
|
+
try {
|
|
315
|
+
const port = process.env.PORT || '54188';
|
|
316
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/friend-requests`);
|
|
317
|
+
const data = await res.json();
|
|
318
|
+
if (!res.ok)
|
|
319
|
+
return { success: false, error: data.error || '查询失败' };
|
|
320
|
+
if (data.count === 0) {
|
|
321
|
+
return { success: true, output: '当前没有待处理的好友申请。' };
|
|
322
|
+
}
|
|
323
|
+
const lines = data.requests.map((r, i) => `${i + 1}. ${r.fromName} (publicKey=${r.fromPublicKey.substring(0, 16)}...)\n 备注: ${r.note || r.message || '(无)'}\n requestId: ${r.requestId}`);
|
|
324
|
+
return { success: true, output: `待处理好友申请 ${data.count} 个:\n${lines.join('\n')}\n\n用 accept_friend_request 接受, 或 ignore_friend_request 忽略.` };
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
return { success: false, error: `查询好友申请失败: ${String(e.message || e)}` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// accept_friend_request — 接受一个待处理的好友申请 (智能体侧一键通过)
|
|
332
|
+
ctx.tools.set('accept_friend_request', {
|
|
333
|
+
name: 'accept_friend_request',
|
|
334
|
+
description: '接受一个待处理的好友申请. 对方会出现在你的 P2P 好友列表, 对方分享的 channel 自动可见. 参数 requestId 来自 list_pending_friend_requests 的返回.',
|
|
335
|
+
parameters: {
|
|
336
|
+
requestId: '待处理申请的 requestId (必填, 来自 list_pending_friend_requests)',
|
|
337
|
+
name: '可选, 给这个新好友的备注名 (默认用对方自称的名字)'
|
|
338
|
+
},
|
|
339
|
+
execute: async (args) => {
|
|
340
|
+
const requestId = String(args.requestId || '').trim();
|
|
341
|
+
if (!requestId) {
|
|
342
|
+
return { success: false, error: 'requestId 必填 (先调 list_pending_friend_requests 获取)' };
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const port = process.env.PORT || '54188';
|
|
346
|
+
// 先查 pending 拿到 fromPublicKey / fromName
|
|
347
|
+
const q = await fetch(`http://127.0.0.1:${port}/api/friend-requests`);
|
|
348
|
+
const qd = await q.json();
|
|
349
|
+
const req = (qd.requests || []).find((r) => r.requestId === requestId);
|
|
350
|
+
if (!req) {
|
|
351
|
+
return { success: false, error: `未找到 requestId=${requestId} 的申请 (可能已被处理或过期)` };
|
|
352
|
+
}
|
|
353
|
+
const acceptName = String(args.name || '').trim() || req.fromName;
|
|
354
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/friend-accept`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: { 'Content-Type': 'application/json' },
|
|
357
|
+
body: JSON.stringify({ fromPublicKey: req.fromPublicKey, name: acceptName, requestId })
|
|
358
|
+
});
|
|
359
|
+
const data = await res.json();
|
|
360
|
+
if (!res.ok)
|
|
361
|
+
return { success: false, error: data.error || '接受失败' };
|
|
362
|
+
return { success: true, output: `✅ 已接受 ${req.fromName} 的好友申请 (备注: ${req.note || req.message || '(无)'}), 已加为好友: ${data.persistedAs || acceptName}` };
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
return { success: false, error: `接受好友申请失败: ${String(e.message || e)}` };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
// ignore_friend_request — 忽略一个待处理的好友申请
|
|
370
|
+
ctx.tools.set('ignore_friend_request', {
|
|
371
|
+
name: 'ignore_friend_request',
|
|
372
|
+
description: '忽略 (拒绝) 一个待处理的好友申请, 不加入好友. 参数 requestId 来自 list_pending_friend_requests 的返回.',
|
|
373
|
+
parameters: {
|
|
374
|
+
requestId: '待处理申请的 requestId (必填, 来自 list_pending_friend_requests)'
|
|
375
|
+
},
|
|
376
|
+
execute: async (args) => {
|
|
377
|
+
const requestId = String(args.requestId || '').trim();
|
|
378
|
+
if (!requestId)
|
|
379
|
+
return { success: false, error: 'requestId 必填' };
|
|
380
|
+
try {
|
|
381
|
+
const port = process.env.PORT || '54188';
|
|
382
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/friend-requests/ignore`, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ requestId })
|
|
386
|
+
});
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
if (!res.ok)
|
|
389
|
+
return { success: false, error: data.error || '忽略失败' };
|
|
390
|
+
return { success: true, output: `已忽略好友申请 ${requestId.substring(0, 8)}...` };
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
return { success: false, error: `忽略好友申请失败: ${String(e.message || e)}` };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
306
397
|
// delegate_to_engine — 把编码任务委派给本机已安装的其他 AI 编码智能体 CLI
|
|
307
398
|
// (codex / claude-code / opencode / openclaw / hermes). 它们必须已安装且可达 PATH.
|
|
308
399
|
// 实验 API 引擎 (experiment:xxx) 是供应商不是 CLI, 不支持委派, 工具会提示改用 import.
|
|
@@ -642,15 +733,12 @@ export function registerBuiltinTools(ctx) {
|
|
|
642
733
|
// 通用文件读取 (M4)
|
|
643
734
|
ctx.tools.set('read_file', {
|
|
644
735
|
name: 'read_file',
|
|
645
|
-
description: '读取任意文件内容 (相对 cwd).
|
|
736
|
+
description: '读取任意文件内容 (相对 cwd). 只读操作, 无白名单限制.',
|
|
646
737
|
parameters: { path: '相对路径 (必填)', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
|
|
647
738
|
execute: async (args) => {
|
|
648
739
|
const relPath = String(args.path || '').trim();
|
|
649
740
|
if (!relPath)
|
|
650
741
|
return { success: false, error: 'path 必填' };
|
|
651
|
-
const pathResult = checkWritePath(relPath);
|
|
652
|
-
if (!pathResult.allowed)
|
|
653
|
-
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
654
742
|
try {
|
|
655
743
|
const absPath = path.resolve(ctx.cwd, relPath);
|
|
656
744
|
const content = fsSync.readFileSync(absPath, 'utf-8');
|
|
@@ -740,16 +828,13 @@ export function registerBuiltinTools(ctx) {
|
|
|
740
828
|
});
|
|
741
829
|
ctx.tools.set('grep_files', {
|
|
742
830
|
name: 'grep_files',
|
|
743
|
-
description: '在文件中搜索匹配 pattern 的行. 类似 grep -rn.
|
|
831
|
+
description: '在文件中搜索匹配 pattern 的行. 类似 grep -rn. 只读操作, 无白名单限制.',
|
|
744
832
|
parameters: { pattern: '搜索 pattern (必填, 字符串, 不是正则)', path: '搜索目录 (可选, 默认 .)', filePattern: '文件名 glob (可选)' },
|
|
745
833
|
execute: async (args) => {
|
|
746
834
|
const pattern = String(args.pattern || '').trim();
|
|
747
835
|
if (!pattern)
|
|
748
836
|
return { success: false, error: 'pattern 必填' };
|
|
749
837
|
const searchPath = String(args.path || '.').trim();
|
|
750
|
-
const pathResult = checkWritePath(searchPath);
|
|
751
|
-
if (!pathResult.allowed)
|
|
752
|
-
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
753
838
|
try {
|
|
754
839
|
const { execFile } = await import('child_process');
|
|
755
840
|
const { promisify } = await import('util');
|
|
@@ -768,16 +853,13 @@ export function registerBuiltinTools(ctx) {
|
|
|
768
853
|
});
|
|
769
854
|
ctx.tools.set('glob_files', {
|
|
770
855
|
name: 'glob_files',
|
|
771
|
-
description: '用 glob pattern 找文件. 例如 "**/*.test.ts".',
|
|
856
|
+
description: '用 glob pattern 找文件. 例如 "**/*.test.ts". 只读操作, 无白名单限制.',
|
|
772
857
|
parameters: { pattern: 'glob pattern (必填, e.g. "src/**/*.ts")' },
|
|
773
858
|
execute: async (args) => {
|
|
774
859
|
const pattern = String(args.pattern || '').trim();
|
|
775
860
|
if (!pattern)
|
|
776
861
|
return { success: false, error: 'pattern 必填' };
|
|
777
862
|
try {
|
|
778
|
-
const pathResult = checkWritePath(pattern.replace(/\*\*.*$/, '').replace(/\/\*.*$/, '') || '.');
|
|
779
|
-
if (!pathResult.allowed && pattern !== '**/*' && pattern !== '*')
|
|
780
|
-
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
781
863
|
const { execFile } = await import('child_process');
|
|
782
864
|
const { promisify } = await import('util');
|
|
783
865
|
const pExecFile = promisify(execFile);
|
|
@@ -1111,6 +1193,246 @@ export function registerBuiltinTools(ctx) {
|
|
|
1111
1193
|
}
|
|
1112
1194
|
}
|
|
1113
1195
|
});
|
|
1196
|
+
// ============================================================
|
|
1197
|
+
// skill 写工具 (2026-08-02) — 让 agent 从成功经验沉淀技能
|
|
1198
|
+
// create_skill / update_skill / list_skill_candidates / promote_skill
|
|
1199
|
+
// 实现: skill-writer.ts (写 ~/.bolloon/skills/<name>/SKILL.md)
|
|
1200
|
+
// ============================================================
|
|
1201
|
+
ctx.tools.set('create_skill', {
|
|
1202
|
+
name: 'create_skill',
|
|
1203
|
+
description: '创建/覆盖一个 skill (SKILL.md). 当你学会一个可复用的做事方法 (成功流程/命令组合/踩坑教训) 时调用, 沉淀成技能供以后复用. 写 ~/.bolloon/skills/<name>/SKILL.md. 只读技能请用 update_skill 追加.',
|
|
1204
|
+
parameters: {
|
|
1205
|
+
name: 'skill 名 (必填, 小写字母数字连字符, e.g. "p2p-debug")',
|
|
1206
|
+
description: '一句话描述这个 skill 什么时候用 (必填)',
|
|
1207
|
+
body: 'Markdown 正文 (必填): 步骤 / 命令 / 注意事项',
|
|
1208
|
+
scope: '可选: user (默认, ~/.bolloon/skills) 或 project (.bolloon/skills)',
|
|
1209
|
+
triggers: '可选: 触发条件数组 (JSON 字符串数组, e.g. ["p2p", "连接失败"])',
|
|
1210
|
+
},
|
|
1211
|
+
execute: async (args) => {
|
|
1212
|
+
try {
|
|
1213
|
+
const { createSkill } = await import('./skill-writer.js');
|
|
1214
|
+
const name = String(args.name || '').trim();
|
|
1215
|
+
if (!name)
|
|
1216
|
+
return { success: false, error: 'name 必填' };
|
|
1217
|
+
const body = String(args.body || '').trim();
|
|
1218
|
+
if (!body)
|
|
1219
|
+
return { success: false, error: 'body 必填' };
|
|
1220
|
+
let triggers;
|
|
1221
|
+
try {
|
|
1222
|
+
const t = JSON.parse(String(args.triggers || '[]'));
|
|
1223
|
+
if (Array.isArray(t))
|
|
1224
|
+
triggers = t.map(String);
|
|
1225
|
+
}
|
|
1226
|
+
catch { /* triggers 解析失败忽略 */ }
|
|
1227
|
+
const r = await createSkill(name, String(args.description || ''), body, {
|
|
1228
|
+
scope: args.scope === 'project' ? 'project' : 'user',
|
|
1229
|
+
triggers,
|
|
1230
|
+
});
|
|
1231
|
+
return r.ok
|
|
1232
|
+
? { success: true, output: `✅ skill '${name}' 已写入 ${r.path}` }
|
|
1233
|
+
: { success: false, error: r.error };
|
|
1234
|
+
}
|
|
1235
|
+
catch (e) {
|
|
1236
|
+
return { success: false, error: `create_skill 失败: ${String(e).slice(0, 200)}` };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
ctx.tools.set('update_skill', {
|
|
1241
|
+
name: 'update_skill',
|
|
1242
|
+
description: '更新已有 skill: 追加新经验 (append_body) 或整体替换 (body), 或改描述/触发条件. skill 不存在时用 create_skill.',
|
|
1243
|
+
parameters: {
|
|
1244
|
+
name: 'skill 名 (必填)',
|
|
1245
|
+
append_body: '追加到正文尾部的增量经验 (可选)',
|
|
1246
|
+
body: '整体替换正文 (可选, 与 append_body 二选一)',
|
|
1247
|
+
description: '新描述 (可选)',
|
|
1248
|
+
triggers: '新触发条件数组 JSON (可选)',
|
|
1249
|
+
},
|
|
1250
|
+
execute: async (args) => {
|
|
1251
|
+
try {
|
|
1252
|
+
const { updateSkill } = await import('./skill-writer.js');
|
|
1253
|
+
const name = String(args.name || '').trim();
|
|
1254
|
+
if (!name)
|
|
1255
|
+
return { success: false, error: 'name 必填' };
|
|
1256
|
+
let triggers;
|
|
1257
|
+
try {
|
|
1258
|
+
const t = JSON.parse(String(args.triggers || '[]'));
|
|
1259
|
+
if (Array.isArray(t))
|
|
1260
|
+
triggers = t.map(String);
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* 忽略 */ }
|
|
1263
|
+
const r = await updateSkill(name, {
|
|
1264
|
+
description: args.description ? String(args.description) : undefined,
|
|
1265
|
+
appendBody: args.append_body ? String(args.append_body) : undefined,
|
|
1266
|
+
body: args.body ? String(args.body) : undefined,
|
|
1267
|
+
triggers,
|
|
1268
|
+
});
|
|
1269
|
+
return r.ok
|
|
1270
|
+
? { success: true, output: `✅ skill '${name}' 已更新 ${r.path}` }
|
|
1271
|
+
: { success: false, error: r.error };
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
return { success: false, error: `update_skill 失败: ${String(e).slice(0, 200)}` };
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
ctx.tools.set('list_skill_candidates', {
|
|
1279
|
+
name: 'list_skill_candidates',
|
|
1280
|
+
description: '查看待沉淀的 skill 候选 (后台任务从成功的工具调用模式生成的候选). 返回候选列表, 可用 promote_skill 转正.',
|
|
1281
|
+
parameters: {},
|
|
1282
|
+
execute: async () => {
|
|
1283
|
+
try {
|
|
1284
|
+
const { listSkillCandidates } = await import('./skill-writer.js');
|
|
1285
|
+
const cands = await listSkillCandidates();
|
|
1286
|
+
if (cands.length === 0)
|
|
1287
|
+
return { success: true, output: '暂无待沉淀的 skill 候选.' };
|
|
1288
|
+
const lines = cands.map(c => `- ${c.name}: ${c.description} [来源 ${c.source}]`).join('\n');
|
|
1289
|
+
return { success: true, output: `📋 ${cands.length} 个 skill 候选:\n${lines}` };
|
|
1290
|
+
}
|
|
1291
|
+
catch (e) {
|
|
1292
|
+
return { success: false, error: `list_skill_candidates 失败: ${String(e).slice(0, 200)}` };
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
ctx.tools.set('promote_skill', {
|
|
1297
|
+
name: 'promote_skill',
|
|
1298
|
+
description: '把 skill 候选转正为正式 skill. 转正后候选文件自动清理.',
|
|
1299
|
+
parameters: { name: '候选名 (必填, 见 list_skill_candidates)' },
|
|
1300
|
+
execute: async (args) => {
|
|
1301
|
+
try {
|
|
1302
|
+
const { promoteCandidate } = await import('./skill-writer.js');
|
|
1303
|
+
const name = String(args.name || '').trim();
|
|
1304
|
+
if (!name)
|
|
1305
|
+
return { success: false, error: 'name 必填' };
|
|
1306
|
+
const r = await promoteCandidate(name);
|
|
1307
|
+
return r.ok
|
|
1308
|
+
? { success: true, output: `✅ 候选 '${name}' 已转正 → ${r.path}` }
|
|
1309
|
+
: { success: false, error: r.error };
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
return { success: false, error: `promote_skill 失败: ${String(e).slice(0, 200)}` };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
// ============================================================
|
|
1317
|
+
// plan / todo / review 工具 (2026-08-02) — 显式执行闭环
|
|
1318
|
+
// create_plan / update_plan / review_plan / list_plans
|
|
1319
|
+
// 实现: plan-store.ts (~/.bolloon/plans/<planId>.json)
|
|
1320
|
+
// ============================================================
|
|
1321
|
+
ctx.tools.set('create_plan', {
|
|
1322
|
+
name: 'create_plan',
|
|
1323
|
+
description: '执行复杂任务前先显式列计划: 拆成 3-8 个可执行步骤. 之后每完成一步调 update_plan 勾选, 全部完成调 review_plan 总结. 落盘 ~/.bolloon/plans/.',
|
|
1324
|
+
parameters: {
|
|
1325
|
+
goal: '一句话目标 (必填)',
|
|
1326
|
+
steps: '步骤数组 JSON (必填, e.g. ["读需求", "写代码", "测试"])',
|
|
1327
|
+
},
|
|
1328
|
+
execute: async (args) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const { createPlan, planToContext } = await import('./plan-store.js');
|
|
1331
|
+
const goal = String(args.goal || '').trim();
|
|
1332
|
+
let steps = [];
|
|
1333
|
+
try {
|
|
1334
|
+
const s = JSON.parse(String(args.steps || '[]'));
|
|
1335
|
+
if (Array.isArray(s))
|
|
1336
|
+
steps = s.map(String);
|
|
1337
|
+
}
|
|
1338
|
+
catch { /* steps 解析失败 */ }
|
|
1339
|
+
const r = await createPlan({ goal, steps, createdBy: 'agent', originChannel: ctx.channelId || '' });
|
|
1340
|
+
if (!r.ok || !r.plan)
|
|
1341
|
+
return { success: false, error: r.error };
|
|
1342
|
+
return { success: true, output: `✅ 计划已创建 ${r.plan.planId}\n\n${planToContext(r.plan)}` };
|
|
1343
|
+
}
|
|
1344
|
+
catch (e) {
|
|
1345
|
+
return { success: false, error: `create_plan 失败: ${String(e).slice(0, 200)}` };
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
ctx.tools.set('update_plan', {
|
|
1350
|
+
name: 'update_plan',
|
|
1351
|
+
description: '更新计划: 勾选某步完成/阻塞 (step_id + status), 或追加新步骤 (append_steps), 或整体结束 (finish=true). 执行中每完成一步必调.',
|
|
1352
|
+
parameters: {
|
|
1353
|
+
plan_id: '计划 ID (必填, create_plan 返回)',
|
|
1354
|
+
step_id: '步骤 ID (可选, e.g. step_1)',
|
|
1355
|
+
status: '步骤新状态: done / blocked (可选)',
|
|
1356
|
+
note: '完成/阻塞备注 (可选)',
|
|
1357
|
+
append_steps: '追加步骤数组 JSON (可选)',
|
|
1358
|
+
finish: 'true 表示整个计划结束 (可选)',
|
|
1359
|
+
},
|
|
1360
|
+
execute: async (args) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const { updatePlan, planToContext } = await import('./plan-store.js');
|
|
1363
|
+
const planId = String(args.plan_id || '').trim();
|
|
1364
|
+
if (!planId)
|
|
1365
|
+
return { success: false, error: 'plan_id 必填' };
|
|
1366
|
+
let appendSteps;
|
|
1367
|
+
try {
|
|
1368
|
+
const s = JSON.parse(String(args.append_steps || '[]'));
|
|
1369
|
+
if (Array.isArray(s))
|
|
1370
|
+
appendSteps = s.map(String);
|
|
1371
|
+
}
|
|
1372
|
+
catch { /* 忽略 */ }
|
|
1373
|
+
const r = await updatePlan(planId, {
|
|
1374
|
+
stepId: args.step_id ? String(args.step_id) : undefined,
|
|
1375
|
+
status: (args.status === 'done' || args.status === 'blocked') ? args.status : undefined,
|
|
1376
|
+
note: args.note ? String(args.note) : undefined,
|
|
1377
|
+
appendSteps,
|
|
1378
|
+
finish: String(args.finish) === 'true',
|
|
1379
|
+
});
|
|
1380
|
+
if (!r.ok || !r.plan)
|
|
1381
|
+
return { success: false, error: r.error };
|
|
1382
|
+
return { success: true, output: `✅ 计划已更新\n\n${planToContext(r.plan)}` };
|
|
1383
|
+
}
|
|
1384
|
+
catch (e) {
|
|
1385
|
+
return { success: false, error: `update_plan 失败: ${String(e).slice(0, 200)}` };
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
ctx.tools.set('review_plan', {
|
|
1390
|
+
name: 'review_plan',
|
|
1391
|
+
description: '计划全部执行完后审查: 总结完成度 + 产出结论. 完成后 plan 标记 done.',
|
|
1392
|
+
parameters: {
|
|
1393
|
+
plan_id: '计划 ID (必填)',
|
|
1394
|
+
summary: '审查总结 (必填, 说明完成了什么/卡在哪/下一步)',
|
|
1395
|
+
},
|
|
1396
|
+
execute: async (args) => {
|
|
1397
|
+
try {
|
|
1398
|
+
const { reviewPlan } = await import('./plan-store.js');
|
|
1399
|
+
const planId = String(args.plan_id || '').trim();
|
|
1400
|
+
if (!planId)
|
|
1401
|
+
return { success: false, error: 'plan_id 必填' };
|
|
1402
|
+
const summary = String(args.summary || '').trim();
|
|
1403
|
+
if (!summary)
|
|
1404
|
+
return { success: false, error: 'summary 必填' };
|
|
1405
|
+
const r = await reviewPlan(planId, summary);
|
|
1406
|
+
if (!r.ok || !r.plan)
|
|
1407
|
+
return { success: false, error: r.error };
|
|
1408
|
+
return {
|
|
1409
|
+
success: true,
|
|
1410
|
+
output: `✅ 计划审查完成: ${r.plan.review.completedSteps}/${r.plan.review.totalSteps} 步\n📝 ${summary}`,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
catch (e) {
|
|
1414
|
+
return { success: false, error: `review_plan 失败: ${String(e).slice(0, 200)}` };
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
ctx.tools.set('list_plans', {
|
|
1419
|
+
name: 'list_plans',
|
|
1420
|
+
description: '列出所有进行中的计划 (active). 用于恢复上下文/继续未完成的计划.',
|
|
1421
|
+
parameters: {},
|
|
1422
|
+
execute: async () => {
|
|
1423
|
+
try {
|
|
1424
|
+
const { listActivePlans, planToContext } = await import('./plan-store.js');
|
|
1425
|
+
const plans = await listActivePlans();
|
|
1426
|
+
if (plans.length === 0)
|
|
1427
|
+
return { success: true, output: '暂无进行中的计划.' };
|
|
1428
|
+
const text = plans.map(p => planToContext(p)).join('\n\n');
|
|
1429
|
+
return { success: true, output: `📋 ${plans.length} 个进行中的计划:\n\n${text}` };
|
|
1430
|
+
}
|
|
1431
|
+
catch (e) {
|
|
1432
|
+
return { success: false, error: `list_plans 失败: ${String(e).slice(0, 200)}` };
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1114
1436
|
}
|
|
1115
1437
|
/**
|
|
1116
1438
|
* 注册 Wallet + Polymarket + Safe 工具 (基于 constraint-runtime/src/tools/).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-store.ts — 轻量 plan / todo / review 原语 (2026-08-02)
|
|
3
|
+
*
|
|
4
|
+
* 补齐 agent 执行闭环里的显式环节:
|
|
5
|
+
* - plan: 执行前显式列出步骤 (LLM 写计划)
|
|
6
|
+
* - todo: 步骤级状态 (pending / done / blocked), 执行中勾选
|
|
7
|
+
* - review: 执行后审查 (对照计划看完成度, 产出结论)
|
|
8
|
+
*
|
|
9
|
+
* 设计原则 (减法):
|
|
10
|
+
* - 不建数据库. 落盘 = ~/.bolloon/plans/<planId>.json (单文件, append 不适用, 直接重写)
|
|
11
|
+
* - 不引入新调度. 工具入口在 pi-sdk-tools.ts 注册, 复用现有 LLM 调度.
|
|
12
|
+
* - 任何 IO 失败静默返回 error 对象, 不阻塞主对话.
|
|
13
|
+
* - plan 上下文注入: create_plan 后会把计划摘要写回 messageHistory (由工具返回, LLM 自己看到),
|
|
14
|
+
* 同时 planId 存 session, 后续 update_plan / review_plan 按 planId 操作.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs/promises';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as crypto from 'crypto';
|
|
20
|
+
// ============================================================
|
|
21
|
+
// 落盘路径
|
|
22
|
+
// ============================================================
|
|
23
|
+
export function getPlansDir(home = os.homedir()) {
|
|
24
|
+
return path.join(home, '.bolloon', 'plans');
|
|
25
|
+
}
|
|
26
|
+
export function getPlanPath(planId, home = os.homedir()) {
|
|
27
|
+
return path.join(getPlansDir(home), `${planId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
28
|
+
}
|
|
29
|
+
export function sanitizePlanId(id) {
|
|
30
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
31
|
+
}
|
|
32
|
+
export async function createPlan(input, home) {
|
|
33
|
+
const goal = String(input.goal || '').trim();
|
|
34
|
+
const steps = Array.isArray(input.steps) ? input.steps.map(s => String(s).trim()).filter(Boolean) : [];
|
|
35
|
+
if (!goal)
|
|
36
|
+
return { ok: false, error: 'goal 必填' };
|
|
37
|
+
if (steps.length === 0)
|
|
38
|
+
return { ok: false, error: 'steps 至少 1 步' };
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
const planId = `plan_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
|
|
41
|
+
const plan = {
|
|
42
|
+
planId,
|
|
43
|
+
goal,
|
|
44
|
+
createdBy: input.createdBy || 'agent',
|
|
45
|
+
createdAt: now,
|
|
46
|
+
originChannel: input.originChannel || '',
|
|
47
|
+
steps: steps.map((s, i) => ({
|
|
48
|
+
id: `step_${i + 1}`,
|
|
49
|
+
description: s,
|
|
50
|
+
status: 'pending',
|
|
51
|
+
})),
|
|
52
|
+
status: 'active',
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const dir = getPlansDir(home);
|
|
57
|
+
await fs.mkdir(dir, { recursive: true });
|
|
58
|
+
await fs.writeFile(getPlanPath(planId, home), JSON.stringify(plan, null, 2), 'utf-8');
|
|
59
|
+
return { ok: true, plan };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return { ok: false, error: `写入失败: ${e?.message || String(e)}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function loadPlan(planId, home) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await fs.readFile(getPlanPath(planId, home), 'utf-8');
|
|
68
|
+
return JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function savePlan(plan, home) {
|
|
75
|
+
plan.updatedAt = new Date().toISOString();
|
|
76
|
+
await fs.writeFile(getPlanPath(plan.planId, home), JSON.stringify(plan, null, 2), 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
export async function updatePlan(planId, input, home) {
|
|
79
|
+
const plan = await loadPlan(planId, home);
|
|
80
|
+
if (!plan)
|
|
81
|
+
return { ok: false, error: `plan '${planId}' 不存在` };
|
|
82
|
+
if (input.stepId && input.status) {
|
|
83
|
+
const step = plan.steps.find(s => s.id === input.stepId);
|
|
84
|
+
if (!step)
|
|
85
|
+
return { ok: false, error: `步骤 '${input.stepId}' 不存在` };
|
|
86
|
+
step.status = input.status;
|
|
87
|
+
if (input.note)
|
|
88
|
+
step.note = input.note;
|
|
89
|
+
step.updatedAt = new Date().toISOString();
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(input.appendSteps) && input.appendSteps.length > 0) {
|
|
92
|
+
for (const s of input.appendSteps) {
|
|
93
|
+
const desc = String(s).trim();
|
|
94
|
+
if (!desc)
|
|
95
|
+
continue;
|
|
96
|
+
plan.steps.push({ id: `step_${plan.steps.length + 1}`, description: desc, status: 'pending' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (input.finish) {
|
|
100
|
+
plan.status = 'done';
|
|
101
|
+
// 未完成的步骤标 blocked (收尾语义)
|
|
102
|
+
for (const s of plan.steps) {
|
|
103
|
+
if (s.status === 'pending')
|
|
104
|
+
s.status = 'blocked';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await savePlan(plan, home);
|
|
109
|
+
return { ok: true, plan };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return { ok: false, error: `保存失败: ${e?.message || String(e)}` };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function reviewPlan(planId, summary, home) {
|
|
116
|
+
const plan = await loadPlan(planId, home);
|
|
117
|
+
if (!plan)
|
|
118
|
+
return { ok: false, error: `plan '${planId}' 不存在` };
|
|
119
|
+
const completedSteps = plan.steps.filter(s => s.status === 'done').length;
|
|
120
|
+
plan.review = {
|
|
121
|
+
completedSteps,
|
|
122
|
+
totalSteps: plan.steps.length,
|
|
123
|
+
summary: String(summary || '').trim() || `完成 ${completedSteps}/${plan.steps.length} 步`,
|
|
124
|
+
reviewedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
plan.status = 'done';
|
|
127
|
+
try {
|
|
128
|
+
await savePlan(plan, home);
|
|
129
|
+
return { ok: true, plan };
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
return { ok: false, error: `保存失败: ${e?.message || String(e)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function listActivePlans(home) {
|
|
136
|
+
const dir = getPlansDir(home);
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = await fs.readdir(dir);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const f of entries.filter(f => f.endsWith('.json')).sort().slice(-50)) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(path.join(dir, f), 'utf-8');
|
|
148
|
+
const p = JSON.parse(raw);
|
|
149
|
+
if (p.status === 'active')
|
|
150
|
+
out.push(p);
|
|
151
|
+
}
|
|
152
|
+
catch { /* 坏文件跳过 */ }
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/** 把 plan 渲染成注入 context 的文本 (LLM 看到计划 + 进度) */
|
|
157
|
+
export function planToContext(plan) {
|
|
158
|
+
const lines = [`📋 当前计划: ${plan.goal} (${plan.planId})`];
|
|
159
|
+
for (const s of plan.steps) {
|
|
160
|
+
const mark = s.status === 'done' ? '✅' : s.status === 'blocked' ? '⛔' : '⬜';
|
|
161
|
+
lines.push(` ${mark} ${s.id}: ${s.description}${s.note ? ` — ${s.note}` : ''}`);
|
|
162
|
+
}
|
|
163
|
+
if (plan.review) {
|
|
164
|
+
lines.push(` 📝 审查: ${plan.review.summary} (${plan.review.completedSteps}/${plan.review.totalSteps} 步)`);
|
|
165
|
+
}
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|