@bolloon/bolloon-agent 0.2.12 → 0.2.14
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 +495 -467
- package/bin/bolloon-cli.cjs +183 -183
- package/bin/bolloon-daemon.sh +207 -0
- package/bin/bolloon.cjs +0 -0
- package/bin/ipfs +0 -0
- package/dist/agents/agent-manifest-protocol.js +52 -0
- package/dist/agents/chat-segmenter.js +298 -0
- package/dist/agents/constraint-layer.js +19 -19
- package/dist/agents/judgment-protocol.js +479 -0
- package/dist/agents/parse-tool-call.js +304 -0
- package/dist/agents/peer-manifest-loader.js +210 -0
- package/dist/agents/pi-sdk-session-factory.js +110 -0
- package/dist/agents/pi-sdk-session-manager.js +297 -0
- package/dist/agents/pi-sdk-tools.js +1288 -0
- package/dist/agents/pi-sdk-types.js +13 -0
- package/dist/agents/pi-sdk.js +553 -1306
- package/dist/agents/react-loop.js +120 -0
- package/dist/agents/session-store.js +171 -0
- package/dist/agents/shell-guard.js +2 -2
- package/dist/agents/tool-registry.js +136 -0
- package/dist/agents/workflow-pivot-loop.js +200 -22
- package/dist/bollharness/src/scripts/context_router.js +7 -0
- package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
- package/dist/bollharness-integration/context-router-judgment.js +4 -4
- package/dist/bollharness-integration/context-router.js +292 -292
- package/dist/bollharness-integration/gate-state-machine.js +26 -13
- package/dist/bollharness-integration/integration.js +71 -0
- package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
- package/dist/bollharness-integration/skill-adapter.js +57 -127
- package/dist/bootstrap/chat-archiver.js +276 -0
- package/dist/bootstrap/context-collector.js +6 -3
- package/dist/bootstrap/event-log.js +160 -0
- package/dist/bootstrap/lifecycle-hooks.js +15 -1
- package/dist/bootstrap/memory-compressor.js +170 -0
- package/dist/bootstrap/persona-loader.js +94 -0
- package/dist/bootstrap/project-state.js +159 -0
- package/dist/bootstrap/remote-mirror.js +97 -0
- package/dist/bootstrap/session-window.js +121 -0
- package/dist/bootstrap/vector-index.js +248 -0
- package/dist/cli/loading-tui.js +40 -0
- package/dist/cli-entry.js +41 -37
- package/dist/context-compaction/auto-compact.js +7 -7
- package/dist/electron/config.js +21 -0
- package/dist/electron/config.js.map +1 -0
- package/dist/electron/dialogs.js +108 -0
- package/dist/electron/dialogs.js.map +1 -0
- package/dist/electron/first-run.js +170 -0
- package/dist/electron/first-run.js.map +1 -0
- package/dist/electron/ipc.js +20 -0
- package/dist/electron/ipc.js.map +1 -0
- package/dist/electron/logger.js +114 -0
- package/dist/electron/logger.js.map +1 -0
- package/dist/electron/main.js +63 -0
- package/dist/electron/main.js.map +1 -0
- package/dist/electron/menu.js +145 -0
- package/dist/electron/menu.js.map +1 -0
- package/dist/electron/paths.js +75 -0
- package/dist/electron/paths.js.map +1 -0
- package/dist/electron/server.js +119 -0
- package/dist/electron/server.js.map +1 -0
- package/dist/electron/tray.js +111 -0
- package/dist/electron/tray.js.map +1 -0
- package/dist/electron/window.js +111 -0
- package/dist/electron/window.js.map +1 -0
- package/dist/electron-preload.js +26 -6
- package/dist/electron-preload.js.map +1 -1
- package/dist/electron.js +6 -168
- package/dist/electron.js.map +1 -1
- package/dist/index.js +323 -266
- package/dist/llm/config-store.js +29 -8
- package/dist/llm/llm-judgment-client.js +102 -102
- package/dist/llm/pi-ai.js +139 -85
- package/dist/llm/system-prompt/layers/channel/local.md +14 -14
- package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
- package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
- package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
- package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
- package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
- package/dist/llm/system-prompt/layers/core/identity.md +37 -37
- package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
- package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
- package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
- package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
- package/dist/llm/system-prompt/layers/core/tone.md +31 -31
- package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
- package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
- package/dist/llm/system-prompt/layers/role/architect.md +20 -20
- package/dist/llm/system-prompt/layers/role/expert.md +19 -19
- package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
- package/dist/llm/system-prompt/layers/role/security.md +15 -15
- package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
- package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
- package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
- package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
- package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
- package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
- package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
- package/dist/llm/tool-manifest/ask_user_input.js +1 -1
- package/dist/llm/tool-manifest/bash.js +3 -3
- package/dist/llm/tool-manifest/create_file.js +4 -4
- package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
- package/dist/llm/tool-manifest/image_search.js +2 -2
- package/dist/llm/tool-manifest/mcp.js +2 -2
- package/dist/llm/tool-manifest/message_compose.js +3 -3
- package/dist/llm/tool-manifest/places.js +2 -2
- package/dist/llm/tool-manifest/present_files.js +1 -1
- package/dist/llm/tool-manifest/recipe.js +2 -2
- package/dist/llm/tool-manifest/str_replace.js +5 -5
- package/dist/llm/tool-manifest/view.js +3 -3
- package/dist/llm/tool-manifest/weather.js +4 -4
- package/dist/llm/tool-manifest/web.js +4 -4
- package/dist/network/local-inbox-bus.js +73 -0
- package/dist/network/p2p-outbox.js +161 -0
- package/dist/network/peer-fs.js +420 -0
- package/dist/network/peer-resource-bridge.js +216 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
- package/dist/pi-ecosystem-judgment/cleanup.js +126 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
- package/dist/pi-ecosystem-judgment/distillation.js +14 -14
- package/dist/pi-ecosystem-judgment/human-value-store.js +45 -11
- package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
- package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
- package/dist/security/context-router-tool.js +15 -15
- package/dist/security/tool-gate.js +11 -0
- package/dist/web/api-config.html +520 -520
- package/dist/web/client-loop-status.js +220 -0
- package/dist/web/client.js +22 -5
- package/dist/web/client.js.map +7 -0
- package/dist/web/components/wallet-viem.mjs +118 -118
- package/dist/web/index.html +375 -375
- package/dist/web/input-validator.js +103 -0
- package/dist/web/manifest.json +20 -20
- package/dist/web/routes-judgments.js +738 -0
- package/dist/web/routes-llm-config.js +279 -0
- package/dist/web/routes-tasks.js +222 -0
- package/dist/web/server-sse.js +121 -0
- package/dist/web/server-storage.js +154 -0
- package/dist/web/server-types.js +15 -0
- package/dist/web/server-v3-p2p.js +224 -0
- package/dist/web/server.js +1070 -1307
- package/dist/web/style.css +4742 -4742
- package/dist/web/util/safe-name.js +32 -0
- package/package.json +162 -160
- package/scripts/build-cli.js +215 -215
- package/scripts/build-web.ts +130 -130
- package/scripts/postinstall.js +152 -152
- package/bin/bolloon.js +0 -157
- package/dist/constraint-runtime/src/_archive_helper.js +0 -9
- package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
- package/dist/constraint-runtime/src/agent/index.js +0 -1
- package/dist/constraint-runtime/src/assistant/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
- package/dist/constraint-runtime/src/bridge/index.js +0 -12
- package/dist/constraint-runtime/src/buddy/index.js +0 -12
- package/dist/constraint-runtime/src/cli/index.js +0 -12
- package/dist/constraint-runtime/src/command_graph.js +0 -10
- package/dist/constraint-runtime/src/commands.js +0 -60
- package/dist/constraint-runtime/src/components/index.js +0 -12
- package/dist/constraint-runtime/src/constants/index.js +0 -12
- package/dist/constraint-runtime/src/constraint/budget.js +0 -22
- package/dist/constraint-runtime/src/constraint/index.js +0 -2
- package/dist/constraint-runtime/src/constraint/permission.js +0 -20
- package/dist/constraint-runtime/src/context.js +0 -30
- package/dist/constraint-runtime/src/coordinator/index.js +0 -12
- package/dist/constraint-runtime/src/cost_hook.js +0 -4
- package/dist/constraint-runtime/src/cost_tracker.js +0 -8
- package/dist/constraint-runtime/src/deferred_init.js +0 -10
- package/dist/constraint-runtime/src/direct_modes.js +0 -6
- package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
- package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/src/execution_registry.js +0 -44
- package/dist/constraint-runtime/src/history.js +0 -9
- package/dist/constraint-runtime/src/hooks/index.js +0 -12
- package/dist/constraint-runtime/src/index.js +0 -26
- package/dist/constraint-runtime/src/ink.js +0 -4
- package/dist/constraint-runtime/src/keybindings/index.js +0 -12
- package/dist/constraint-runtime/src/memdir/index.js +0 -12
- package/dist/constraint-runtime/src/migrations/index.js +0 -12
- package/dist/constraint-runtime/src/models.js +0 -1
- package/dist/constraint-runtime/src/moreright/index.js +0 -12
- package/dist/constraint-runtime/src/native_ts/index.js +0 -12
- package/dist/constraint-runtime/src/output_styles/index.js +0 -12
- package/dist/constraint-runtime/src/parity_audit.js +0 -12
- package/dist/constraint-runtime/src/plugins/index.js +0 -12
- package/dist/constraint-runtime/src/port_manifest.js +0 -11
- package/dist/constraint-runtime/src/prefetch.js +0 -9
- package/dist/constraint-runtime/src/query.js +0 -1
- package/dist/constraint-runtime/src/remote/index.js +0 -12
- package/dist/constraint-runtime/src/remote_runtime.js +0 -9
- package/dist/constraint-runtime/src/runtime/index.js +0 -1
- package/dist/constraint-runtime/src/runtime/session.js +0 -35
- package/dist/constraint-runtime/src/schemas/index.js +0 -12
- package/dist/constraint-runtime/src/screens/index.js +0 -12
- package/dist/constraint-runtime/src/server/index.js +0 -12
- package/dist/constraint-runtime/src/services/index.js +0 -12
- package/dist/constraint-runtime/src/session_store.js +0 -22
- package/dist/constraint-runtime/src/setup.js +0 -30
- package/dist/constraint-runtime/src/skills/index.js +0 -1
- package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
- package/dist/constraint-runtime/src/state/index.js +0 -12
- package/dist/constraint-runtime/src/system_init.js +0 -20
- package/dist/constraint-runtime/src/thinking/engine.js +0 -42
- package/dist/constraint-runtime/src/thinking/index.js +0 -1
- package/dist/constraint-runtime/src/tool_pool.js +0 -8
- package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
- package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
- package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools.js +0 -80
- package/dist/constraint-runtime/src/transcript.js +0 -19
- package/dist/constraint-runtime/src/types/index.js +0 -12
- package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/src/utils/index.js +0 -12
- package/dist/constraint-runtime/src/vim/index.js +0 -12
- package/dist/constraint-runtime/src/voice/index.js +0 -12
- package/dist/constraint-runtime/tests/agent.test.js +0 -16
- package/dist/constraint-runtime/tests/constraint.test.js +0 -41
- package/dist/constraint-runtime/tests/skill.test.js +0 -19
- package/dist/constraint-runtime/tests/thinking.test.js +0 -22
- package/dist/pi-ecosystem-colony/index.js +0 -365
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
- package/dist/social/ant-colony/PheromoneEngine.js +0 -227
- package/dist/social/ant-colony/index.js +0 -6
- package/dist/social/ant-colony/types.js +0 -24
- package/dist/test/ai-judgment-test.js +0 -80
- package/dist/test/bollharness-integration.test.js +0 -318
- package/dist/test/channel-agent-multi-dialogue.js +0 -205
- package/dist/test/channel-heartbeat-agent-test.js +0 -201
- package/dist/test/constraint-layer.test.js +0 -164
- package/dist/test/diap-identity-test.js +0 -172
- package/dist/test/diap-quick-test.js +0 -62
- package/dist/test/global-shared-context.test.js +0 -315
- package/dist/test/harness-judgment-injection.test.js +0 -246
- package/dist/test/harness-workflow-integrator-test.js +0 -228
- package/dist/test/human-value-store.test.js +0 -243
- package/dist/test/hybrid-integration-test.js +0 -118
- package/dist/test/hybrid-messenger-verify.js +0 -55
- package/dist/test/iroh-bistream-debug.js +0 -38
- package/dist/test/iroh-communication.test.js +0 -66
- package/dist/test/iroh-debug-test.js +0 -57
- package/dist/test/iroh-diap-test.js +0 -71
- package/dist/test/iroh-direct-connect.js +0 -55
- package/dist/test/iroh-e2e-fixed.js +0 -89
- package/dist/test/iroh-e2e-same-process.js +0 -63
- package/dist/test/iroh-e2e.js +0 -66
- package/dist/test/iroh-final-e2e.js +0 -72
- package/dist/test/iroh-relay-test.js +0 -37
- package/dist/test/iroh-simple-test.js +0 -41
- package/dist/test/iroh-transport-verify.js +0 -54
- package/dist/test/iroh-transport.test.js +0 -37
- package/dist/test/iroh-two-nodes.js +0 -70
- package/dist/test/iroh-verify.js +0 -44
- package/dist/test/judgment-decision.test.js +0 -219
- package/dist/test/llm-judgment-integration.test.js +0 -220
- package/dist/test/p2p-agent-complex-dialogue.js +0 -385
- package/dist/test/p2p-agent-dialogue.js +0 -341
- package/dist/test/p2p-agent-full-bidirectional.js +0 -510
- package/dist/test/p2p-agent-harness-flow.js +0 -437
- package/dist/test/p2p-agent-harness-single.js +0 -143
- package/dist/test/p2p-ai-dialogue-test.js +0 -318
- package/dist/test/p2p-cid-connect-test.js +0 -195
- package/dist/test/p2p-connect-receiver.js +0 -69
- package/dist/test/p2p-doc-transfer.js +0 -110
- package/dist/test/p2p-identity-page-test.js +0 -77
- package/dist/test/p2p-iroh-test.js +0 -171
- package/dist/test/p2p-minimal-test.js +0 -241
- package/dist/test/p2p-node-1.js +0 -148
- package/dist/test/p2p-node-2.js +0 -148
- package/dist/test/p2p-server.js +0 -281
- package/dist/test/p2p-two-nodes-test.js +0 -438
- package/dist/test/pi-sdk.test.js +0 -44
- package/dist/test/set-persona.js +0 -40
- package/dist/test/simple.test.js +0 -9
- package/dist/test/storage-integration.test.js +0 -150
- package/dist/test/subagent-manager.test.js +0 -276
- package/dist/test/test-gate-flow.test.js +0 -81
- package/dist/test/workflow-engine.test.js +0 -87
- package/dist/test/workflow-pivot-loop.test.js +0 -246
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as fsSync from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { documentReader } from '../documents/reader.js';
|
|
5
|
+
import { p2pNetwork } from '../network/p2p.js';
|
|
6
|
+
import { shellExec } from './shell-tool.js';
|
|
7
|
+
import { checkWritePath } from './shell-guard.js';
|
|
8
|
+
import { p2pDocumentTools } from './p2p-document-tools.js';
|
|
9
|
+
import { runSelfImproveLoop } from './pi-sdk-session-factory.js';
|
|
10
|
+
import { getMinimax } from '../constraints/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Tools 模块 — 从 pi-sdk.ts 抽出的 registerTools() / _registerWalletTools() / _setupInboxListener()
|
|
13
|
+
* / wrapToolsWithIdempotency() (2026-07-06).
|
|
14
|
+
*
|
|
15
|
+
* 三个工具来源:
|
|
16
|
+
* 1. 内置工具: read_document, summarize_document, improve_document, list_peers, send_message,
|
|
17
|
+
* broadcast_message, list_local_channels, send_to_channel, check_inbox, send_to_peer,
|
|
18
|
+
* p2p_broadcast, agent_call, get_identity, set_persona, get_operation_logs, list_files,
|
|
19
|
+
* read_directory, shell_exec, self_improve, list_skills, use_skill, write_file, edit_file,
|
|
20
|
+
* git_diff, git_commit, git_push, git_branch, read_file, delete_file, mkdir, move_file,
|
|
21
|
+
* grep_files, glob_files, git_log, git_show, git_stash, vitest_run, tsc_check, create_task,
|
|
22
|
+
* update_task, get_task, list_tasks, p2pDocumentTools
|
|
23
|
+
* 2. 钱包工具 (EVM / Polymarket / Safe) — 单独的 _registerWalletTools
|
|
24
|
+
* 3. P2P 消息 inbox 监听 — _setupInboxListener
|
|
25
|
+
*
|
|
26
|
+
* 调用方: PiAgentSession.registerTools() 会调下面的三个函数, 并执行
|
|
27
|
+
* wrapToolsWithIdempotency + 镜像到 ToolRegistry.
|
|
28
|
+
*
|
|
29
|
+
* 工具缓存策略: SIDE_EFFECT_TOOLS 子集 (write_file / edit_file / shell_exec / git_commit 等)
|
|
30
|
+
* 走幂等性 cache, 防止 LLM loop 重试时副作用执行两次.
|
|
31
|
+
*/
|
|
32
|
+
export const SIDE_EFFECT_TOOLS = new Set([
|
|
33
|
+
'write_file', 'edit_file', 'shell_exec', 'git_commit', 'git_push', 'git_branch',
|
|
34
|
+
'create_task', 'update_task',
|
|
35
|
+
]);
|
|
36
|
+
export function registerBuiltinTools(ctx) {
|
|
37
|
+
ctx.tools.set('read_document', {
|
|
38
|
+
name: 'read_document',
|
|
39
|
+
description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
|
|
40
|
+
parameters: { path: 'string' },
|
|
41
|
+
execute: async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
const content = await documentReader.read(args.path);
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
output: `📄 ${content.metadata.filename}\n大小: ${content.metadata.size} 字节\n\n${content.text.substring(0, 1000)}${content.text.length > 1000 ? '...' : ''}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return { success: false, error: String(e) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
ctx.tools.set('summarize_document', {
|
|
55
|
+
name: 'summarize_document',
|
|
56
|
+
description: '总结文档内容,分析并生成摘要',
|
|
57
|
+
parameters: { path: 'string', context: 'string' },
|
|
58
|
+
execute: async (args) => {
|
|
59
|
+
try {
|
|
60
|
+
if (!ctx.minimaxAvailable) {
|
|
61
|
+
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
62
|
+
}
|
|
63
|
+
// summarizeDocument 走 PiAgentSession.summarizeDocument, 这里通过 tools 反向调用不好处理
|
|
64
|
+
// 简化: 让 LLM 自己直接走 shell_exec / use_skill 路径
|
|
65
|
+
const llm = getMinimax();
|
|
66
|
+
const content = await documentReader.read(args.path);
|
|
67
|
+
const r = await llm.summarize(content.text, args.context);
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
output: `📝 摘要:\n${r.summary}\n\n质量评分: ${(r.qualityScore * 10).toFixed(1)}/10`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return { success: false, error: String(e) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
ctx.tools.set('improve_document', {
|
|
79
|
+
name: 'improve_document',
|
|
80
|
+
description: '根据要求改进文档内容',
|
|
81
|
+
parameters: { path: 'string', requirements: 'string' },
|
|
82
|
+
execute: async (args) => {
|
|
83
|
+
try {
|
|
84
|
+
if (!ctx.minimaxAvailable) {
|
|
85
|
+
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
86
|
+
}
|
|
87
|
+
const llm = getMinimax();
|
|
88
|
+
const content = await documentReader.read(args.path);
|
|
89
|
+
const improved = await llm.summarize(content.text + '\n\n改进要求: ' + args.requirements, undefined);
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
output: `✅ 改进完成\n质量评分: ${(improved.qualityScore * 10).toFixed(1)}/10\n${improved.summary ? '\n改进内容:\n' + improved.summary.substring(0, 500) + '...' : ''}`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return { success: false, error: String(e) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
ctx.tools.set('list_peers', {
|
|
101
|
+
name: 'list_peers',
|
|
102
|
+
description: '列出已连接的对等节点',
|
|
103
|
+
parameters: {},
|
|
104
|
+
execute: async () => {
|
|
105
|
+
const peers = p2pNetwork.getPeers();
|
|
106
|
+
if (peers.length === 0) {
|
|
107
|
+
return { success: true, output: '当前无连接的对等节点' };
|
|
108
|
+
}
|
|
109
|
+
return { success: true, output: `已连接节点 (${peers.length}):\n${peers.map(p => ` - ${p}`).join('\n')}` };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
ctx.tools.set('send_message', {
|
|
113
|
+
name: 'send_message',
|
|
114
|
+
description: '向指定对等节点发送消息',
|
|
115
|
+
parameters: { peer_id: '对等节点ID', message: '消息内容' },
|
|
116
|
+
execute: async (args) => {
|
|
117
|
+
try {
|
|
118
|
+
await p2pNetwork.sendMessage(args.peer_id, 'message', args.message);
|
|
119
|
+
return { success: true, output: `消息已发送到 ${args.peer_id}` };
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
return { success: false, error: String(e) };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
ctx.tools.set('broadcast_message', {
|
|
127
|
+
name: 'broadcast_message',
|
|
128
|
+
description: '向所有对等节点广播消息',
|
|
129
|
+
parameters: { message: '消息内容' },
|
|
130
|
+
execute: async (args) => {
|
|
131
|
+
try {
|
|
132
|
+
await p2pNetwork.broadcast('message', args.message);
|
|
133
|
+
return { success: true, output: '消息已广播到所有节点' };
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
return { success: false, error: String(e) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Agent Mesh 工具集
|
|
141
|
+
ctx.tools.set('list_local_channels', {
|
|
142
|
+
name: 'list_local_channels',
|
|
143
|
+
description: '列出当前 PiAgentSession 注册的所有 channel. 每个 channel 是 bolloon 内的智能体会话, 可含 messages[] / peerDid(对端 DID) / peerName. 供 send_to_channel 选 channel_id.',
|
|
144
|
+
parameters: {},
|
|
145
|
+
execute: async () => {
|
|
146
|
+
const channels = ctx.sessionManager ? ctx.sessionManager.getAllChannels?.() || [] : [];
|
|
147
|
+
if (channels.length === 0) {
|
|
148
|
+
return { success: true, output: '📭 当前 session 没有注册任何 channel (用 send_to_channel 留空 channel_id 会自动创建一个)' };
|
|
149
|
+
}
|
|
150
|
+
const lines = channels.map((ch, i) => {
|
|
151
|
+
const peer = ch.peerDid ? ` peer=${ch.peerDid.substring(0, 20)}` : (ch.peerName ? ` peer="${ch.peerName}"` : '');
|
|
152
|
+
const msgCount = ch.messages?.length || 0;
|
|
153
|
+
return ` ${i + 1}. ${ch.id} name="${ch.name}" msgs=${msgCount}${peer}`;
|
|
154
|
+
});
|
|
155
|
+
return { success: true, output: `📬 当前 session 有 ${channels.length} 个 channel:\n${lines.join('\n')}` };
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
ctx.tools.set('send_to_channel', {
|
|
159
|
+
name: 'send_to_channel',
|
|
160
|
+
description: '发送消息到指定 channel. channel_id 留空会自动创建一个. 如果 channel 已关联 peerDid, 会同时通过 P2P 转发到对端 agent.',
|
|
161
|
+
parameters: { channel_id: '目标 channel id (留空自动创建)', message: '消息内容 (必填)', peer_did: '可选 创建新 channel 时绑定的对端 DID (e.g. did:key:...)' },
|
|
162
|
+
execute: async (args) => {
|
|
163
|
+
const message = String(args.message || '').trim();
|
|
164
|
+
if (!message)
|
|
165
|
+
return { success: false, error: 'message 必填' };
|
|
166
|
+
let channelId = String(args.channel_id || '').trim();
|
|
167
|
+
const peerDid = args.peer_did ? String(args.peer_did) : undefined;
|
|
168
|
+
if (!channelId) {
|
|
169
|
+
const ch = await ctx.sessionManager.getOrCreatePeerChannel(peerDid || 'auto-created', 'auto-created');
|
|
170
|
+
channelId = ch.id;
|
|
171
|
+
}
|
|
172
|
+
const ch = ctx.sessionManager.getAllChannels().find((c) => c.id === channelId);
|
|
173
|
+
if (ch?.peerDid) {
|
|
174
|
+
try {
|
|
175
|
+
const peerId = ch.peerDid.replace(/^did:key:/, '').replace(/^did:pi:/, '');
|
|
176
|
+
await p2pNetwork.sendMessage(peerId, 'channel-message', JSON.stringify({ channelId, content: message, from: ctx.identity.did, timestamp: new Date().toISOString() }));
|
|
177
|
+
return { success: true, output: `📨 消息已存到 channel ${channelId} + P2P 转发到 ${ch.peerDid.substring(0, 30)}...` };
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return { success: true, output: `📨 消息已存到 channel ${channelId} (P2P 转发失败: ${String(e).slice(0, 100)})` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { success: true, output: `📨 消息已存到 channel ${channelId}` };
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
ctx.tools.set('check_inbox', {
|
|
187
|
+
name: 'check_inbox',
|
|
188
|
+
description: '检查 P2P + 本地 inbox 收到的所有消息. 跨 channel 视角, 按时间倒序.',
|
|
189
|
+
parameters: { max: '可选, 最多返回 N 条 (默认 50)' },
|
|
190
|
+
execute: async (args) => {
|
|
191
|
+
const max = Number(args.max) || 50;
|
|
192
|
+
if (ctx._inboxMessages.length === 0) {
|
|
193
|
+
return { success: true, output: '📭 当前 inbox 空' };
|
|
194
|
+
}
|
|
195
|
+
const slice = ctx._inboxMessages.slice(-max).reverse();
|
|
196
|
+
const lines = slice.map((m, i) => {
|
|
197
|
+
const fromTxt = m.fromDid ? `${m.from} (${m.fromDid.substring(0, 20)}...)` : m.from;
|
|
198
|
+
return ` ${i + 1}. [${m.type}] from=${fromTxt} time=${new Date(m.timestamp).toISOString()} src=${m.source}\n ${String(m.payload).substring(0, 200)}`;
|
|
199
|
+
});
|
|
200
|
+
return { success: true, output: `📬 inbox 有 ${ctx._inboxMessages.length} 条消息 (最近 ${slice.length}):\n${lines.join('\n')}` };
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
ctx.tools.set('send_to_peer', {
|
|
204
|
+
name: 'send_to_peer',
|
|
205
|
+
description: '发送结构化消息到指定 P2P 节点 (远端 bolloon 实例). 对方会通过远端 channel 收到.',
|
|
206
|
+
parameters: { peer_id: '目标 P2P 节点 publicKey (用 list_peers 查)', message: '消息内容 (任意字符串)', type: '可选, 消息类型标签 (默认 agent-message)' },
|
|
207
|
+
execute: async (args) => {
|
|
208
|
+
const peerId = String(args.peer_id || '').trim();
|
|
209
|
+
const msg = String(args.message || '').trim();
|
|
210
|
+
const type = String(args.type || 'agent-message').trim();
|
|
211
|
+
if (!peerId)
|
|
212
|
+
return { success: false, error: 'peer_id 必填' };
|
|
213
|
+
if (!msg)
|
|
214
|
+
return { success: false, error: 'message 必填' };
|
|
215
|
+
try {
|
|
216
|
+
await p2pNetwork.sendMessage(peerId, type, msg);
|
|
217
|
+
return { success: true, output: `✅ 消息已发送到 ${peerId.substring(0, 16)}...` };
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return { success: false, error: `发送失败: ${String(e)}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
ctx.tools.set('p2p_broadcast', {
|
|
225
|
+
name: 'p2p_broadcast',
|
|
226
|
+
description: '广播消息到所有连接的 P2P 节点',
|
|
227
|
+
parameters: { message: '消息内容', type: '可选, 消息类型标签 (默认 agent-broadcast)' },
|
|
228
|
+
execute: async (args) => {
|
|
229
|
+
const msg = String(args.message || '').trim();
|
|
230
|
+
const type = String(args.type || 'agent-broadcast').trim();
|
|
231
|
+
if (!msg)
|
|
232
|
+
return { success: false, error: 'message 必填' };
|
|
233
|
+
try {
|
|
234
|
+
await p2pNetwork.broadcast(type, msg);
|
|
235
|
+
return { success: true, output: `📡 已广播 type=${type}` };
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
return { success: false, error: `广播失败: ${String(e)}` };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
ctx.tools.set('agent_call', {
|
|
243
|
+
name: 'agent_call',
|
|
244
|
+
description: 'RPC: 让远端 P2P agent 跑一个任务, 等待结果返回. 远端 agent 会基于 task 描述自主完成, 完成后回复. (RPC 结果回收机制待实现)',
|
|
245
|
+
parameters: { peer_id: '目标 P2P 节点', task: '任务描述 (远端 agent 收到的 prompt)', timeoutMs: '可选 超时 (ms, 默认 30000)' },
|
|
246
|
+
execute: async (args) => {
|
|
247
|
+
const peerId = String(args.peer_id || '').trim();
|
|
248
|
+
const task = String(args.task || '').trim();
|
|
249
|
+
if (!peerId)
|
|
250
|
+
return { success: false, error: 'peer_id 必填' };
|
|
251
|
+
if (!task)
|
|
252
|
+
return { success: false, error: 'task 必填' };
|
|
253
|
+
const requestId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
254
|
+
await p2pNetwork.sendMessage(peerId, 'agent-call', JSON.stringify({ requestId, task, from: ctx.identity.did }));
|
|
255
|
+
return { success: true, output: `📞 RPC 任务已发送给 ${peerId.substring(0, 16)}...\n requestId=${requestId}` };
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
ctx.tools.set('get_identity', {
|
|
259
|
+
name: 'get_identity',
|
|
260
|
+
description: '获取当前智能体身份信息',
|
|
261
|
+
parameters: {},
|
|
262
|
+
execute: async () => {
|
|
263
|
+
const id = ctx.identity;
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
output: `DID: ${id.did}\n名称: ${id.name}\n`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
ctx.tools.set('set_persona', {
|
|
271
|
+
name: 'set_persona',
|
|
272
|
+
description: '更新智能体的 persona 信息',
|
|
273
|
+
parameters: { persona_json: 'Persona JSON 对象' },
|
|
274
|
+
execute: async (args) => {
|
|
275
|
+
try {
|
|
276
|
+
const personaData = typeof args.persona_json === 'string' ? JSON.parse(args.persona_json) : args.persona_json;
|
|
277
|
+
const now = new Date().toISOString();
|
|
278
|
+
const newPersona = {
|
|
279
|
+
name: personaData.name || ctx.identity.name,
|
|
280
|
+
description: personaData.description || '',
|
|
281
|
+
capabilities: personaData.capabilities || [],
|
|
282
|
+
personality: personaData.personality || '',
|
|
283
|
+
greeting: personaData.greeting || '',
|
|
284
|
+
interests: personaData.interests || [],
|
|
285
|
+
createdAt: ctx.persona?.createdAt || now,
|
|
286
|
+
updatedAt: now
|
|
287
|
+
};
|
|
288
|
+
await ctx.setPersona(newPersona);
|
|
289
|
+
return { success: true, output: `Persona 已更新:\n名称: ${newPersona.name}\n描述: ${newPersona.description}` };
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return { success: false, error: `更新 persona 失败: ${String(e)}` };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
ctx.tools.set('get_operation_logs', {
|
|
297
|
+
name: 'get_operation_logs',
|
|
298
|
+
description: '获取约束层的操作日志',
|
|
299
|
+
parameters: {},
|
|
300
|
+
execute: async () => {
|
|
301
|
+
const logs = ctx.constraintLayer.getLogs();
|
|
302
|
+
if (logs.length === 0) {
|
|
303
|
+
return { success: true, output: '暂无操作日志' };
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
output: `操作日志 (${logs.length} 条):\n${logs.slice(-10).map((l) => `[${new Date(l.timestamp).toISOString()}] ${l.action} - ${l.status}`).join('\n')}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// 文件系统工具
|
|
312
|
+
ctx.tools.set('list_files', {
|
|
313
|
+
name: 'list_files',
|
|
314
|
+
description: '列出目录中的文件',
|
|
315
|
+
parameters: { path: '目录路径(可选,默认为当前目录)' },
|
|
316
|
+
execute: async (args) => {
|
|
317
|
+
try {
|
|
318
|
+
const fs = await import('fs');
|
|
319
|
+
const targetPath = args.path || ctx.cwd;
|
|
320
|
+
const files = fs.readdirSync(targetPath);
|
|
321
|
+
return {
|
|
322
|
+
success: true,
|
|
323
|
+
output: `📁 目录 ${targetPath} 中的文件 (${files.length} 个):\n${files.slice(0, 20).map((f) => ` - ${f}`).join('\n')}${files.length > 20 ? '\n ...' : ''}`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
return { success: false, error: String(e) };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
ctx.tools.set('read_directory', {
|
|
332
|
+
name: 'read_directory',
|
|
333
|
+
description: '读取目录内容,返回文件列表和目录结构',
|
|
334
|
+
parameters: { path: '目录路径(可选,默认为当前目录)' },
|
|
335
|
+
execute: async (args) => {
|
|
336
|
+
try {
|
|
337
|
+
const fs = await import('fs');
|
|
338
|
+
const pathModule = await import('path');
|
|
339
|
+
const targetPath = args.path || ctx.cwd;
|
|
340
|
+
const items = fs.readdirSync(targetPath);
|
|
341
|
+
const result = [];
|
|
342
|
+
for (const item of items.slice(0, 30)) {
|
|
343
|
+
const fullPath = pathModule.join(targetPath, item);
|
|
344
|
+
try {
|
|
345
|
+
const stat = fs.statSync(fullPath);
|
|
346
|
+
const type = stat.isDirectory() ? '📁' : '📄';
|
|
347
|
+
result.push(`${type} ${item}${stat.isDirectory() ? '/' : ''}`);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
result.push(`📄 ${item}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
output: `📂 ${targetPath} (${items.length} 项):\n${result.join('\n')}${items.length > 30 ? '\n... 还有更多文件' : ''}`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
return { success: false, error: `无法读取目录: ${String(e)}` };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// P2P Document Tools
|
|
364
|
+
for (const tool of p2pDocumentTools) {
|
|
365
|
+
ctx.tools.set(tool.name, tool);
|
|
366
|
+
}
|
|
367
|
+
// shell_exec
|
|
368
|
+
ctx.tools.set('shell_exec', {
|
|
369
|
+
name: 'shell_exec',
|
|
370
|
+
description: '在 cwd 跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒.',
|
|
371
|
+
parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
|
|
372
|
+
execute: async (args) => {
|
|
373
|
+
const cmd = String(args.command || '').trim();
|
|
374
|
+
if (!cmd)
|
|
375
|
+
return { success: false, error: 'command 必填' };
|
|
376
|
+
let argList = [];
|
|
377
|
+
const rawArgs = args.args;
|
|
378
|
+
if (Array.isArray(rawArgs)) {
|
|
379
|
+
argList = rawArgs.map((s) => String(s).trim()).filter(Boolean);
|
|
380
|
+
}
|
|
381
|
+
else if (typeof rawArgs === 'string') {
|
|
382
|
+
const trimmed = rawArgs.trim();
|
|
383
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(trimmed);
|
|
386
|
+
if (Array.isArray(parsed)) {
|
|
387
|
+
argList = parsed.map((s) => String(s).trim()).filter(Boolean);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch { /* fall through to comma split */ }
|
|
391
|
+
}
|
|
392
|
+
if (argList.length === 0) {
|
|
393
|
+
argList = trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
397
|
+
const result = await shellExec(cmd, argList, { timeoutMs });
|
|
398
|
+
if (result.deniedByGuard) {
|
|
399
|
+
return { success: false, error: result.error };
|
|
400
|
+
}
|
|
401
|
+
if (!result.success) {
|
|
402
|
+
return { success: false, error: result.error, output: result.output };
|
|
403
|
+
}
|
|
404
|
+
return { success: true, output: result.output };
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
// self_improve
|
|
408
|
+
ctx.tools.set('self_improve', {
|
|
409
|
+
name: 'self_improve',
|
|
410
|
+
description: '触发自我改进循环. AI 会在沙箱分支上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审.',
|
|
411
|
+
parameters: { goal: '本轮改进目标 (1 句话)' },
|
|
412
|
+
execute: async (args) => {
|
|
413
|
+
const goal = String(args.goal || '').trim();
|
|
414
|
+
if (!goal)
|
|
415
|
+
return { success: false, error: 'goal 必填' };
|
|
416
|
+
return await runSelfImproveLoop(goal);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
// list_skills / use_skill — 走 skillRegistry
|
|
420
|
+
// (由 registerBuiltinTools 调用方单独注册, 因为依赖 skillRegistry 实例)
|
|
421
|
+
// write_file / edit_file / git_*
|
|
422
|
+
ctx.tools.set('write_file', {
|
|
423
|
+
name: 'write_file',
|
|
424
|
+
description: '写入一个文件. 路径必须在白名单. 大文件 (> 100KB) 会被拒. 命中护栏黑名单会拒.',
|
|
425
|
+
parameters: { path: '相对路径 (必填, 相对 cwd)', content: '文件内容 (必填)' },
|
|
426
|
+
execute: async (args) => {
|
|
427
|
+
const relPath = String(args.path || '').trim();
|
|
428
|
+
const content = String(args.content ?? '');
|
|
429
|
+
if (!relPath)
|
|
430
|
+
return { success: false, error: 'path 必填' };
|
|
431
|
+
if (content.length > 100_000)
|
|
432
|
+
return { success: false, error: `内容过大 (${content.length} > 100000 字节), 请分块写` };
|
|
433
|
+
const pathResult = checkWritePath(relPath);
|
|
434
|
+
if (!pathResult.allowed) {
|
|
435
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const absPath = path.resolve(ctx.cwd, relPath);
|
|
439
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
440
|
+
await fs.writeFile(absPath, content, 'utf-8');
|
|
441
|
+
return { success: true, output: `✅ wrote ${relPath} (${content.length} bytes)` };
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
return { success: false, error: `写文件失败: ${String(e)}` };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
ctx.tools.set('edit_file', {
|
|
449
|
+
name: 'edit_file',
|
|
450
|
+
description: '编辑一个文件: 在 path 处查找 old_text, 替换为 new_text. 找不到 old_text 会失败.',
|
|
451
|
+
parameters: { path: '相对路径 (必填)', old_text: '要替换的文本 (必填, 全文匹配)', new_text: '新文本 (必填)' },
|
|
452
|
+
execute: async (args) => {
|
|
453
|
+
const relPath = String(args.path || '').trim();
|
|
454
|
+
const oldText = String(args.old_text ?? '');
|
|
455
|
+
const newText = String(args.new_text ?? '');
|
|
456
|
+
if (!relPath)
|
|
457
|
+
return { success: false, error: 'path 必填' };
|
|
458
|
+
if (!oldText)
|
|
459
|
+
return { success: false, error: 'old_text 必填' };
|
|
460
|
+
const pathResult = checkWritePath(relPath);
|
|
461
|
+
if (!pathResult.allowed) {
|
|
462
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const absPath = path.resolve(ctx.cwd, relPath);
|
|
466
|
+
const original = await fs.readFile(absPath, 'utf-8');
|
|
467
|
+
if (!original.includes(oldText)) {
|
|
468
|
+
return { success: false, error: `old_text 在 ${relPath} 中未找到, 拒绝静默写入. 请先用 read_document 读最新内容.` };
|
|
469
|
+
}
|
|
470
|
+
const updated = original.replace(oldText, newText);
|
|
471
|
+
await fs.writeFile(absPath, updated, 'utf-8');
|
|
472
|
+
return { success: true, output: `✅ edited ${relPath} (${oldText.length} → ${newText.length} 字节)` };
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
return { success: false, error: `编辑文件失败: ${String(e)}` };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
ctx.tools.set('git_diff', {
|
|
480
|
+
name: 'git_diff',
|
|
481
|
+
description: '查看 git diff. 默认显示未提交改动 (staged + unstaged), 可指定 ref1..ref2 看两个 commit/分支之间的 diff. 输出会截到 8000 字符避免超长.',
|
|
482
|
+
parameters: { range: '可选. e.g. "HEAD~3..HEAD". 省略则看未提交改动.' },
|
|
483
|
+
execute: async (args) => {
|
|
484
|
+
const range = String(args.range || '').trim();
|
|
485
|
+
const argv = range ? ['diff', range] : ['diff'];
|
|
486
|
+
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
487
|
+
if (result.deniedByGuard)
|
|
488
|
+
return { success: false, error: result.error };
|
|
489
|
+
if (!result.success)
|
|
490
|
+
return { success: false, error: result.error, output: result.output };
|
|
491
|
+
const out = (result.output || '').slice(0, 8000);
|
|
492
|
+
return { success: true, output: out || '(空 diff — 没有未提交改动)' };
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
ctx.tools.set('git_commit', {
|
|
496
|
+
name: 'git_commit',
|
|
497
|
+
description: 'git add -A + git commit. 提交信息由 LLM 提供. 不会 push.',
|
|
498
|
+
parameters: { message: 'commit message (必填)' },
|
|
499
|
+
execute: async (args) => {
|
|
500
|
+
const message = String(args.message || '').trim();
|
|
501
|
+
if (!message)
|
|
502
|
+
return { success: false, error: 'message 必填' };
|
|
503
|
+
const addResult = await shellExec('git', ['add', '-A'], { timeoutMs: 10_000 });
|
|
504
|
+
if (addResult.deniedByGuard)
|
|
505
|
+
return { success: false, error: addResult.error };
|
|
506
|
+
if (!addResult.success)
|
|
507
|
+
return { success: false, error: `git add 失败: ${addResult.error}` };
|
|
508
|
+
try {
|
|
509
|
+
const { spawn: spawnFn } = await import('child_process');
|
|
510
|
+
const env = { ...process.env, BOLLOON_AUTO_EVOLVE: '1' };
|
|
511
|
+
const output = await new Promise((resolve, reject) => {
|
|
512
|
+
const proc = spawnFn('git', ['commit', '-m', message], {
|
|
513
|
+
cwd: ctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'],
|
|
514
|
+
});
|
|
515
|
+
let stdout = '';
|
|
516
|
+
let stderr = '';
|
|
517
|
+
proc.stdout.on('data', (d) => stdout += d.toString());
|
|
518
|
+
proc.stderr.on('data', (d) => stderr += d.toString());
|
|
519
|
+
proc.on('close', (code) => code === 0 ? resolve(stdout) : reject(new Error(stderr || `git commit exited ${code}`)));
|
|
520
|
+
proc.on('error', reject);
|
|
521
|
+
});
|
|
522
|
+
return { success: true, output: `✅ committed: ${message.split('\n')[0]}\n${output}` };
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
return { success: false, error: `git commit 失败: ${String(e.message || e).slice(0, 500)}` };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
ctx.tools.set('git_push', {
|
|
530
|
+
name: 'git_push',
|
|
531
|
+
description: 'git push 当前分支到 origin. 命中护栏 (push to master/main, --force) 仍会被拒.',
|
|
532
|
+
parameters: { remote: '可选, 默认 origin', branch: '可选, 默认当前分支' },
|
|
533
|
+
execute: async (args) => {
|
|
534
|
+
const remote = String(args.remote || 'origin').trim();
|
|
535
|
+
const branch = String(args.branch || '').trim();
|
|
536
|
+
const argv = branch ? ['push', remote, branch] : ['push', remote];
|
|
537
|
+
const result = await shellExec('git', argv, { timeoutMs: 60_000 });
|
|
538
|
+
if (result.deniedByGuard)
|
|
539
|
+
return { success: false, error: result.error };
|
|
540
|
+
if (!result.success)
|
|
541
|
+
return { success: false, error: result.error, output: result.output };
|
|
542
|
+
return { success: true, output: `✅ pushed to ${remote}${branch ? `/${branch}` : ''}\n${result.output || ''}` };
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
ctx.tools.set('git_branch', {
|
|
546
|
+
name: 'git_branch',
|
|
547
|
+
description: 'git checkout -b <name> 或 git checkout <name>.',
|
|
548
|
+
parameters: { name: '分支名 (必填)', create: '可选, "true" 表示创建新分支' },
|
|
549
|
+
execute: async (args) => {
|
|
550
|
+
const name = String(args.name || '').trim();
|
|
551
|
+
const create = String(args.create || 'false') === 'true';
|
|
552
|
+
if (!name)
|
|
553
|
+
return { success: false, error: 'name 必填' };
|
|
554
|
+
const argv = create ? ['checkout', '-b', name] : ['checkout', name];
|
|
555
|
+
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
556
|
+
if (result.deniedByGuard)
|
|
557
|
+
return { success: false, error: result.error };
|
|
558
|
+
if (!result.success)
|
|
559
|
+
return { success: false, error: result.error, output: result.output };
|
|
560
|
+
return { success: true, output: `✅ ${create ? 'created + checked out' : 'checked out'} ${name}\n${result.output || ''}` };
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// 通用文件读取 (M4)
|
|
564
|
+
ctx.tools.set('read_file', {
|
|
565
|
+
name: 'read_file',
|
|
566
|
+
description: '读取任意文件内容 (相对 cwd). 受 shell-guard 路径白名单保护.',
|
|
567
|
+
parameters: { path: '相对路径 (必填)', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
|
|
568
|
+
execute: async (args) => {
|
|
569
|
+
const relPath = String(args.path || '').trim();
|
|
570
|
+
if (!relPath)
|
|
571
|
+
return { success: false, error: 'path 必填' };
|
|
572
|
+
const pathResult = checkWritePath(relPath);
|
|
573
|
+
if (!pathResult.allowed)
|
|
574
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
575
|
+
try {
|
|
576
|
+
const absPath = path.resolve(ctx.cwd, relPath);
|
|
577
|
+
const content = fsSync.readFileSync(absPath, 'utf-8');
|
|
578
|
+
const start = Math.max(0, parseInt(String(args.startLine || '0')) || 0);
|
|
579
|
+
const max = parseInt(String(args.maxLines || '500')) || 500;
|
|
580
|
+
const lines = content.split('\n');
|
|
581
|
+
const slice = lines.slice(start, start + max);
|
|
582
|
+
return { success: true, output: `📄 ${relPath} (第 ${start + 1}-${start + slice.length} 行, 共 ${lines.length} 行):\n${slice.map((l, i) => `${String(start + i + 1).padStart(4)} | ${l}`).join('\n')}` };
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
return { success: false, error: `读取失败: ${e.message}` };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
ctx.tools.set('delete_file', {
|
|
590
|
+
name: 'delete_file',
|
|
591
|
+
description: '删除一个文件. 受 shell-guard 路径白名单保护. 不可恢复, 调用前请确认.',
|
|
592
|
+
parameters: { path: '相对路径 (必填)' },
|
|
593
|
+
execute: async (args) => {
|
|
594
|
+
const relPath = String(args.path || '').trim();
|
|
595
|
+
if (!relPath)
|
|
596
|
+
return { success: false, error: 'path 必填' };
|
|
597
|
+
const pathResult = checkWritePath(relPath);
|
|
598
|
+
if (!pathResult.allowed)
|
|
599
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
600
|
+
try {
|
|
601
|
+
const absPath = path.resolve(ctx.cwd, relPath);
|
|
602
|
+
if (!fsSync.existsSync(absPath))
|
|
603
|
+
return { success: false, error: `文件不存在: ${relPath}` };
|
|
604
|
+
fsSync.unlinkSync(absPath);
|
|
605
|
+
return { success: true, output: `✅ deleted ${relPath}` };
|
|
606
|
+
}
|
|
607
|
+
catch (e) {
|
|
608
|
+
return { success: false, error: `删除失败: ${e.message}` };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
ctx.tools.set('mkdir', {
|
|
613
|
+
name: 'mkdir',
|
|
614
|
+
description: '创建一个或多个目录. 自动 mkdir -p.',
|
|
615
|
+
parameters: { path: '目录路径 (必填, 相对 cwd)' },
|
|
616
|
+
execute: async (args) => {
|
|
617
|
+
const relPath = String(args.path || '').trim();
|
|
618
|
+
if (!relPath)
|
|
619
|
+
return { success: false, error: 'path 必填' };
|
|
620
|
+
const pathResult = checkWritePath(relPath);
|
|
621
|
+
if (!pathResult.allowed)
|
|
622
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
623
|
+
try {
|
|
624
|
+
const absPath = path.resolve(ctx.cwd, relPath);
|
|
625
|
+
fsSync.mkdirSync(absPath, { recursive: true });
|
|
626
|
+
return { success: true, output: `✅ mkdir ${relPath}` };
|
|
627
|
+
}
|
|
628
|
+
catch (e) {
|
|
629
|
+
return { success: false, error: `创建失败: ${e.message}` };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
ctx.tools.set('move_file', {
|
|
634
|
+
name: 'move_file',
|
|
635
|
+
description: '移动或重命名文件. 源和目标路径都在白名单内才允许.',
|
|
636
|
+
parameters: { from: '源路径 (必填)', to: '目标路径 (必填)' },
|
|
637
|
+
execute: async (args) => {
|
|
638
|
+
const from = String(args.from || '').trim();
|
|
639
|
+
const to = String(args.to || '').trim();
|
|
640
|
+
if (!from || !to)
|
|
641
|
+
return { success: false, error: 'from 和 to 都必填' };
|
|
642
|
+
const fromCheck = checkWritePath(from);
|
|
643
|
+
if (!fromCheck.allowed)
|
|
644
|
+
return { success: false, error: `from 路径被护栏拒: ${fromCheck.reason}` };
|
|
645
|
+
const toCheck = checkWritePath(to);
|
|
646
|
+
if (!toCheck.allowed)
|
|
647
|
+
return { success: false, error: `to 路径被护栏拒: ${toCheck.reason}` };
|
|
648
|
+
try {
|
|
649
|
+
const fromAbs = path.resolve(ctx.cwd, from);
|
|
650
|
+
const toAbs = path.resolve(ctx.cwd, to);
|
|
651
|
+
if (!fsSync.existsSync(fromAbs))
|
|
652
|
+
return { success: false, error: `源文件不存在: ${from}` };
|
|
653
|
+
fsSync.mkdirSync(path.dirname(toAbs), { recursive: true });
|
|
654
|
+
fsSync.renameSync(fromAbs, toAbs);
|
|
655
|
+
return { success: true, output: `✅ ${from} → ${to}` };
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
return { success: false, error: `移动失败: ${e.message}` };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
ctx.tools.set('grep_files', {
|
|
663
|
+
name: 'grep_files',
|
|
664
|
+
description: '在文件中搜索匹配 pattern 的行. 类似 grep -rn. 路径必须在白名单.',
|
|
665
|
+
parameters: { pattern: '搜索 pattern (必填, 字符串, 不是正则)', path: '搜索目录 (可选, 默认 .)', filePattern: '文件名 glob (可选)' },
|
|
666
|
+
execute: async (args) => {
|
|
667
|
+
const pattern = String(args.pattern || '').trim();
|
|
668
|
+
if (!pattern)
|
|
669
|
+
return { success: false, error: 'pattern 必填' };
|
|
670
|
+
const searchPath = String(args.path || '.').trim();
|
|
671
|
+
const pathResult = checkWritePath(searchPath);
|
|
672
|
+
if (!pathResult.allowed)
|
|
673
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
674
|
+
try {
|
|
675
|
+
const { execFile } = await import('child_process');
|
|
676
|
+
const { promisify } = await import('util');
|
|
677
|
+
const pExecFile = promisify(execFile);
|
|
678
|
+
const argv = ['-rn', '--include=' + (args.filePattern || '*'), pattern, searchPath];
|
|
679
|
+
const { stdout, stderr } = await pExecFile('grep', argv, { cwd: ctx.cwd, maxBuffer: 1024 * 1024 });
|
|
680
|
+
const lines = stdout.split('\n').filter(Boolean).slice(0, 50);
|
|
681
|
+
return { success: true, output: `🔍 grep "${pattern}" in ${searchPath} (${args.filePattern || '*'}, 最多 50 行):\n${lines.join('\n')}${lines.length === 50 ? '\n... (truncated)' : ''}` };
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
if (e.code === 1)
|
|
685
|
+
return { success: true, output: `🔍 grep "${pattern}" in ${searchPath}: 0 matches` };
|
|
686
|
+
return { success: false, error: `grep 失败: ${e.message}` };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
ctx.tools.set('glob_files', {
|
|
691
|
+
name: 'glob_files',
|
|
692
|
+
description: '用 glob pattern 找文件. 例如 "**/*.test.ts".',
|
|
693
|
+
parameters: { pattern: 'glob pattern (必填, e.g. "src/**/*.ts")' },
|
|
694
|
+
execute: async (args) => {
|
|
695
|
+
const pattern = String(args.pattern || '').trim();
|
|
696
|
+
if (!pattern)
|
|
697
|
+
return { success: false, error: 'pattern 必填' };
|
|
698
|
+
try {
|
|
699
|
+
const pathResult = checkWritePath(pattern.replace(/\*\*.*$/, '').replace(/\/\*.*$/, '') || '.');
|
|
700
|
+
if (!pathResult.allowed && pattern !== '**/*' && pattern !== '*')
|
|
701
|
+
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
702
|
+
const { execFile } = await import('child_process');
|
|
703
|
+
const { promisify } = await import('util');
|
|
704
|
+
const pExecFile = promisify(execFile);
|
|
705
|
+
const { stdout } = await pExecFile('find', [ctx.cwd, '-path', `*${pattern.replace(/\*\*\//, '*').replace(/\*\*$/, '*').replace(/\*$/, '*')}`, '-type', 'f'], { maxBuffer: 1024 * 1024 });
|
|
706
|
+
const files = stdout.split('\n').filter(Boolean).slice(0, 100);
|
|
707
|
+
const relFiles = files.map((f) => path.relative(ctx.cwd, f));
|
|
708
|
+
return { success: true, output: `🔍 glob "${pattern}" 找到 ${relFiles.length} 个文件${relFiles.length === 100 ? ' (truncated)' : ''}:\n${relFiles.join('\n')}` };
|
|
709
|
+
}
|
|
710
|
+
catch (e) {
|
|
711
|
+
return { success: false, error: `glob 失败: ${e.message}` };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
ctx.tools.set('git_log', {
|
|
716
|
+
name: 'git_log',
|
|
717
|
+
description: '查看 git log. 默认 --oneline -10. 支持过滤和范围.',
|
|
718
|
+
parameters: { range: '可选, e.g. "HEAD~5..HEAD" 或分支名', maxCount: '可选, 默认 10', oneline: '可选, "true" 用 --oneline (默认 true)' },
|
|
719
|
+
execute: async (args) => {
|
|
720
|
+
const range = String(args.range || '').trim();
|
|
721
|
+
const maxCount = parseInt(String(args.maxCount || '10')) || 10;
|
|
722
|
+
const oneline = String(args.oneline || 'true') === 'true';
|
|
723
|
+
const argv = ['log'];
|
|
724
|
+
if (oneline)
|
|
725
|
+
argv.push('--oneline');
|
|
726
|
+
if (maxCount > 0)
|
|
727
|
+
argv.push(`-n`, String(maxCount));
|
|
728
|
+
if (range)
|
|
729
|
+
argv.push(range);
|
|
730
|
+
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
731
|
+
if (result.deniedByGuard)
|
|
732
|
+
return { success: false, error: result.error };
|
|
733
|
+
if (!result.success)
|
|
734
|
+
return { success: false, error: result.error, output: result.output };
|
|
735
|
+
return { success: true, output: `📜 git log:\n${result.output || '(empty)'}` };
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
ctx.tools.set('git_show', {
|
|
739
|
+
name: 'git_show',
|
|
740
|
+
description: '查看 commit 内容. 默认 HEAD. 支持 --stat 看统计, --patch 看 diff.',
|
|
741
|
+
parameters: { ref: '可选, 默认 HEAD', stat: '可选, "true" 只看 stat (默认 false)' },
|
|
742
|
+
execute: async (args) => {
|
|
743
|
+
const ref = String(args.ref || 'HEAD').trim();
|
|
744
|
+
const argv = ['show', ref];
|
|
745
|
+
if (String(args.stat || 'false') === 'true')
|
|
746
|
+
argv.push('--stat');
|
|
747
|
+
const result = await shellExec('git', argv, { timeoutMs: 15_000 });
|
|
748
|
+
if (result.deniedByGuard)
|
|
749
|
+
return { success: false, error: result.error };
|
|
750
|
+
if (!result.success)
|
|
751
|
+
return { success: false, error: result.error, output: result.output };
|
|
752
|
+
return { success: true, output: `📜 git show ${ref}:\n${result.output || '(empty)'}` };
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
ctx.tools.set('git_stash', {
|
|
756
|
+
name: 'git_stash',
|
|
757
|
+
description: 'git stash 暂存当前未提交改动. action: save/pop/list/apply/drop. 支持 message.',
|
|
758
|
+
parameters: { action: '动作 (必填)', message: '可选, save 时的描述', index: '可选, apply/pop/drop 的 stash index' },
|
|
759
|
+
execute: async (args) => {
|
|
760
|
+
const action = String(args.action || '').trim();
|
|
761
|
+
if (!['save', 'pop', 'list', 'apply', 'drop'].includes(action)) {
|
|
762
|
+
return { success: false, error: `action 必须是 save/pop/list/apply/drop 之一, 收到: ${action}` };
|
|
763
|
+
}
|
|
764
|
+
const argv = ['stash', action];
|
|
765
|
+
if (action === 'save' && args.message)
|
|
766
|
+
argv.push('-m', String(args.message));
|
|
767
|
+
if ((action === 'apply' || action === 'drop' || action === 'pop') && args.index) {
|
|
768
|
+
argv.push('stash@{' + String(args.index) + '}');
|
|
769
|
+
}
|
|
770
|
+
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
771
|
+
if (result.deniedByGuard)
|
|
772
|
+
return { success: false, error: result.error };
|
|
773
|
+
if (!result.success)
|
|
774
|
+
return { success: false, error: result.error, output: result.output };
|
|
775
|
+
return { success: true, output: `✅ git stash ${action}\n${result.output || ''}` };
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// 测试 & 类型检查
|
|
779
|
+
ctx.tools.set('vitest_run', {
|
|
780
|
+
name: 'vitest_run',
|
|
781
|
+
description: '跑 vitest 测试. 自动 bail (失败就停). 默认 60s timeout.',
|
|
782
|
+
parameters: { pattern: '可选, 文件 glob', timeoutMs: '可选, 默认 60000' },
|
|
783
|
+
execute: async (args) => {
|
|
784
|
+
const argv = ['vitest', 'run', '--reporter=default', '--no-color', '--bail=1'];
|
|
785
|
+
if (args.pattern)
|
|
786
|
+
argv.push(String(args.pattern));
|
|
787
|
+
const timeoutMs = parseInt(String(args.timeoutMs || '60000')) || 60000;
|
|
788
|
+
const result = await shellExec('npx', argv, { timeoutMs });
|
|
789
|
+
if (result.deniedByGuard)
|
|
790
|
+
return { success: false, error: result.error };
|
|
791
|
+
if (!result.success)
|
|
792
|
+
return { success: false, error: result.error || 'vitest 失败', output: result.output };
|
|
793
|
+
return { success: true, output: `✅ vitest 通过:\n${(result.output || '').slice(0, 2000)}` };
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
ctx.tools.set('tsc_check', {
|
|
797
|
+
name: 'tsc_check',
|
|
798
|
+
description: '跑 tsc --noEmit 检查 TypeScript 编译. 默认 60s timeout.',
|
|
799
|
+
parameters: { project: '可选, tsconfig 路径 (默认 tsconfig.json)', timeoutMs: '可选, 默认 60000' },
|
|
800
|
+
execute: async (args) => {
|
|
801
|
+
const argv = ['tsc', '--noEmit'];
|
|
802
|
+
if (args.project)
|
|
803
|
+
argv.push('-p', String(args.project));
|
|
804
|
+
else
|
|
805
|
+
argv.push('-p', 'tsconfig.json');
|
|
806
|
+
const timeoutMs = parseInt(String(args.timeoutMs || '60000')) || 60000;
|
|
807
|
+
const result = await shellExec('npx', argv, { timeoutMs });
|
|
808
|
+
if (result.deniedByGuard)
|
|
809
|
+
return { success: false, error: result.error };
|
|
810
|
+
if (!result.success)
|
|
811
|
+
return { success: false, error: result.error || 'tsc 失败', output: result.output };
|
|
812
|
+
return { success: true, output: `✅ tsc 通过:\n${(result.output || 'no errors').slice(0, 1000)}` };
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
// Task 工具
|
|
816
|
+
ctx.tools.set('create_task', {
|
|
817
|
+
name: 'create_task',
|
|
818
|
+
description: '创建一个新多步任务, 初始 steps 列表由 LLM 给出.',
|
|
819
|
+
parameters: { goal: '任务目标 (必填)', steps: '步骤列表 (必填)', sessionKey: '可选', branch: '可选' },
|
|
820
|
+
execute: async (args) => {
|
|
821
|
+
const goal = String(args.goal || '').trim();
|
|
822
|
+
const stepsRaw = args.steps;
|
|
823
|
+
if (!goal)
|
|
824
|
+
return { success: false, error: 'goal 必填' };
|
|
825
|
+
let steps = [];
|
|
826
|
+
if (Array.isArray(stepsRaw)) {
|
|
827
|
+
steps = stepsRaw.map((s) => String(s).trim()).filter(Boolean);
|
|
828
|
+
}
|
|
829
|
+
else if (typeof stepsRaw === 'string') {
|
|
830
|
+
steps = stepsRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean);
|
|
831
|
+
}
|
|
832
|
+
if (steps.length === 0)
|
|
833
|
+
return { success: false, error: 'steps 必填且至少 1 步' };
|
|
834
|
+
const sessionKey = String(args.sessionKey || '').trim() || undefined;
|
|
835
|
+
const branch = String(args.branch || '').trim() || undefined;
|
|
836
|
+
try {
|
|
837
|
+
const { createTask } = await import('./task-state.js');
|
|
838
|
+
const task = await createTask({ goal, steps, sessionKey, branch });
|
|
839
|
+
return { success: true, output: `✅ task created: ${task.id}\nbranch: ${branch || '(未指定)'}\nsteps:\n${steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}` };
|
|
840
|
+
}
|
|
841
|
+
catch (e) {
|
|
842
|
+
return { success: false, error: `create_task 失败: ${String(e)}` };
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
ctx.tools.set('update_task', {
|
|
847
|
+
name: 'update_task',
|
|
848
|
+
description: '更新任务的某一步状态.',
|
|
849
|
+
parameters: { task_id: '任务 id', step_id: '步骤 id', status: '新状态', result_summary: '可选', error: '可选' },
|
|
850
|
+
execute: async (args) => {
|
|
851
|
+
const taskId = String(args.task_id || '').trim();
|
|
852
|
+
const stepId = String(args.step_id || '').trim();
|
|
853
|
+
const status = String(args.status || '').trim();
|
|
854
|
+
if (!taskId || !stepId)
|
|
855
|
+
return { success: false, error: 'task_id + step_id 必填' };
|
|
856
|
+
if (!['running', 'done', 'failed', 'skipped'].includes(status)) {
|
|
857
|
+
return { success: false, error: `status 必须是 running|done|failed|skipped` };
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const { updateStep } = await import('./task-state.js');
|
|
861
|
+
const patch = { status };
|
|
862
|
+
if (args.result_summary)
|
|
863
|
+
patch.resultSummary = String(args.result_summary);
|
|
864
|
+
if (args.error)
|
|
865
|
+
patch.error = String(args.error);
|
|
866
|
+
const updated = await updateStep(taskId, stepId, patch);
|
|
867
|
+
if (!updated)
|
|
868
|
+
return { success: false, error: `任务 ${taskId} 未找到` };
|
|
869
|
+
const nextRunning = updated.steps.find((s) => s.status === 'running');
|
|
870
|
+
return {
|
|
871
|
+
success: true,
|
|
872
|
+
output: `✅ step ${stepId} → ${status}\ntask 状态: ${updated.status}${nextRunning ? `\n下一步: ${nextRunning.id} — ${nextRunning.description}` : ''}`,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
catch (e) {
|
|
876
|
+
return { success: false, error: `update_task 失败: ${String(e)}` };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
ctx.tools.set('get_task', {
|
|
881
|
+
name: 'get_task',
|
|
882
|
+
description: '查任务的当前状态和步骤进度.',
|
|
883
|
+
parameters: { task_id: '任务 id (必填)' },
|
|
884
|
+
execute: async (args) => {
|
|
885
|
+
const taskId = String(args.task_id || '').trim();
|
|
886
|
+
if (!taskId)
|
|
887
|
+
return { success: false, error: 'task_id 必填' };
|
|
888
|
+
try {
|
|
889
|
+
const { getTask } = await import('./task-state.js');
|
|
890
|
+
const t = await getTask(taskId);
|
|
891
|
+
if (!t)
|
|
892
|
+
return { success: false, error: `任务 ${taskId} 未找到` };
|
|
893
|
+
const lines = [
|
|
894
|
+
`任务: ${t.id}`,
|
|
895
|
+
`目标: ${t.goal}`,
|
|
896
|
+
`状态: ${t.status}`,
|
|
897
|
+
`branch: ${t.branch || '(未指定)'}`,
|
|
898
|
+
`sessionKey: ${t.sessionKey || '(未指定)'}`,
|
|
899
|
+
`创建: ${t.createdAt}`,
|
|
900
|
+
`更新: ${t.updatedAt}`,
|
|
901
|
+
``,
|
|
902
|
+
`步骤:`,
|
|
903
|
+
...t.steps.map((s) => ` ${s.status === 'done' ? '✅' : s.status === 'running' ? '🔄' : s.status === 'failed' ? '❌' : s.status === 'skipped' ? '⏭️' : '⏳'} ${s.id} — ${s.description}${s.resultSummary ? `\n 结果: ${s.resultSummary}` : ''}${s.error ? `\n 错误: ${s.error}` : ''}`),
|
|
904
|
+
];
|
|
905
|
+
return { success: true, output: lines.join('\n') };
|
|
906
|
+
}
|
|
907
|
+
catch (e) {
|
|
908
|
+
return { success: false, error: `get_task 失败: ${String(e)}` };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
ctx.tools.set('list_tasks', {
|
|
913
|
+
name: 'list_tasks',
|
|
914
|
+
description: '列出最近 N 个任务 (默认 10).',
|
|
915
|
+
parameters: { limit: '可选, 默认 10' },
|
|
916
|
+
execute: async (args) => {
|
|
917
|
+
const limit = Number(args.limit) || 10;
|
|
918
|
+
try {
|
|
919
|
+
const { listTasks } = await import('./task-state.js');
|
|
920
|
+
const tasks = await listTasks(limit);
|
|
921
|
+
if (tasks.length === 0) {
|
|
922
|
+
return { success: true, output: '当前没有任务. 用 create_task 创建一个.' };
|
|
923
|
+
}
|
|
924
|
+
const lines = tasks.map((t) => {
|
|
925
|
+
const done = t.steps.filter((s) => s.status === 'done').length;
|
|
926
|
+
const total = t.steps.length;
|
|
927
|
+
return `${t.status === 'completed' ? '✅' : t.status === 'failed' ? '❌' : '🔄'} ${t.id} — ${t.goal} (${done}/${total} steps, branch: ${t.branch || '-'})`;
|
|
928
|
+
});
|
|
929
|
+
return { success: true, output: `最近 ${tasks.length} 个任务:\n${lines.join('\n')}` };
|
|
930
|
+
}
|
|
931
|
+
catch (e) {
|
|
932
|
+
return { success: false, error: `list_tasks 失败: ${String(e)}` };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* 注册 Wallet + Polymarket + Safe 工具 (基于 constraint-runtime/src/tools/).
|
|
939
|
+
*/
|
|
940
|
+
export function registerWalletTools(ctx) {
|
|
941
|
+
ctx.tools.set('wallet_create', {
|
|
942
|
+
name: 'wallet_create',
|
|
943
|
+
description: '创建新 EVM 钱包 (BIP-39 12 词助记词 + 私钥 + 地址).',
|
|
944
|
+
parameters: {},
|
|
945
|
+
execute: async () => {
|
|
946
|
+
try {
|
|
947
|
+
const { createWallet } = await import('../constraint-runtime/dist/tools/WalletTools/createWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/createWallet.js'));
|
|
948
|
+
const r = await createWallet();
|
|
949
|
+
return { success: true, output: `✅ 钱包创建成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n mnemonic: ${r.mnemonic}\n createdAt: ${r.createdAt}` };
|
|
950
|
+
}
|
|
951
|
+
catch (e) {
|
|
952
|
+
return { success: false, error: `创建失败: ${String(e.message || e)}` };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
ctx.tools.set('wallet_import', {
|
|
957
|
+
name: 'wallet_import',
|
|
958
|
+
description: '导入已有 EVM 钱包 (用助记词或私钥)',
|
|
959
|
+
parameters: { mnemonic: '可选, 12/15/18/21/24 词助记词', privateKey: '可选, 0x 开头的私钥' },
|
|
960
|
+
execute: async (args) => {
|
|
961
|
+
try {
|
|
962
|
+
const { importWallet } = await import('../constraint-runtime/dist/tools/WalletTools/importWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/importWallet.js'));
|
|
963
|
+
const r = await importWallet({ mnemonic: args.mnemonic, privateKey: args.privateKey });
|
|
964
|
+
return { success: true, output: `✅ 钱包导入成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n source: ${r.source}` };
|
|
965
|
+
}
|
|
966
|
+
catch (e) {
|
|
967
|
+
return { success: false, error: `导入失败: ${String(e.message || e)}` };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
ctx.tools.set('wallet_get_balance', {
|
|
972
|
+
name: 'wallet_get_balance',
|
|
973
|
+
description: '查 EVM 钱包 ETH 余额.',
|
|
974
|
+
parameters: { address: '0x 开头的 EVM 地址 (必填)', rpcUrl: '可选 RPC URL (默认 eth.llamarpc.com)' },
|
|
975
|
+
execute: async (args) => {
|
|
976
|
+
try {
|
|
977
|
+
const { getBalance } = await import('../constraint-runtime/dist/tools/WalletTools/getBalance.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/getBalance.js'));
|
|
978
|
+
const r = await getBalance({ address: String(args.address), rpcUrl: args.rpcUrl });
|
|
979
|
+
return { success: true, output: `💰 ${r.address}\n ${r.balanceEth} ${r.symbol} (${r.balance} wei)` };
|
|
980
|
+
}
|
|
981
|
+
catch (e) {
|
|
982
|
+
return { success: false, error: `查余额失败: ${String(e.message || e)}` };
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
ctx.tools.set('wallet_sign_message', {
|
|
987
|
+
name: 'wallet_sign_message',
|
|
988
|
+
description: '用私钥对消息做 EIP-191 personal_sign 签名',
|
|
989
|
+
parameters: { message: '要签名的消息 (必填)', privateKey: '0x 开头的私钥 (必填)' },
|
|
990
|
+
execute: async (args) => {
|
|
991
|
+
try {
|
|
992
|
+
const { signMessage } = await import('../constraint-runtime/dist/tools/WalletTools/signMessage.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/signMessage.js'));
|
|
993
|
+
const r = await signMessage({ message: String(args.message), privateKey: String(args.privateKey) });
|
|
994
|
+
return { success: true, output: `✅ 签名完成:\n address: ${r.address}\n message: ${r.message}\n signature: ${r.signature}` };
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
return { success: false, error: `签名失败: ${String(e.message || e)}` };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
ctx.tools.set('wallet_send_tx', {
|
|
1002
|
+
name: 'wallet_send_tx',
|
|
1003
|
+
description: '用 EVM 钱包发送交易 (转账 ETH 或调用合约).',
|
|
1004
|
+
parameters: { privateKey: '私钥 (必填)', to: '接收地址 (必填)', value: '发送 wei 数量 (必填)', data: '可选 0x 开头的 calldata', rpcUrl: '可选 RPC URL' },
|
|
1005
|
+
execute: async (args) => {
|
|
1006
|
+
try {
|
|
1007
|
+
const { sendTransaction } = await import('../constraint-runtime/dist/tools/WalletTools/sendTransaction.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/sendTransaction.js'));
|
|
1008
|
+
const r = await sendTransaction({
|
|
1009
|
+
privateKey: String(args.privateKey),
|
|
1010
|
+
to: String(args.to),
|
|
1011
|
+
value: String(args.value),
|
|
1012
|
+
data: args.data ? String(args.data) : undefined,
|
|
1013
|
+
rpcUrl: args.rpcUrl,
|
|
1014
|
+
});
|
|
1015
|
+
return { success: true, output: `✅ 交易已发送:\n hash: ${r.hash}\n from: ${r.from} → to: ${r.to}\n value: ${r.value} wei` };
|
|
1016
|
+
}
|
|
1017
|
+
catch (e) {
|
|
1018
|
+
return { success: false, error: `交易失败: ${String(e.message || e)}` };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
ctx.tools.set('wallet_transfer_token', {
|
|
1023
|
+
name: 'wallet_transfer_token',
|
|
1024
|
+
description: '用 EVM 钱包转 ERC20 token.',
|
|
1025
|
+
parameters: { privateKey: '私钥 (必填)', tokenAddress: 'ERC20 合约地址 (必填)', to: '接收地址 (必填)', amount: 'token 数量', decimals: '可选 token decimals', rpcUrl: '可选 RPC URL' },
|
|
1026
|
+
execute: async (args) => {
|
|
1027
|
+
try {
|
|
1028
|
+
const { transferToken } = await import('../constraint-runtime/dist/tools/WalletTools/transferToken.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/transferToken.js'));
|
|
1029
|
+
const r = await transferToken({
|
|
1030
|
+
privateKey: String(args.privateKey),
|
|
1031
|
+
tokenAddress: String(args.tokenAddress),
|
|
1032
|
+
to: String(args.to),
|
|
1033
|
+
amount: String(args.amount),
|
|
1034
|
+
decimals: args.decimals ? Number(args.decimals) : undefined,
|
|
1035
|
+
rpcUrl: args.rpcUrl,
|
|
1036
|
+
});
|
|
1037
|
+
return { success: true, output: `✅ Token 转账完成:\n hash: ${r.hash}\n ${r.amount} ${r.tokenAddress.substring(0, 10)}...` };
|
|
1038
|
+
}
|
|
1039
|
+
catch (e) {
|
|
1040
|
+
return { success: false, error: `转账失败: ${String(e.message || e)}` };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
ctx.tools.set('wallet_autopay', {
|
|
1045
|
+
name: 'wallet_autopay',
|
|
1046
|
+
description: '设置自动付款 (订阅/定期扣款, 用 SafeSDK 自动执行).',
|
|
1047
|
+
parameters: { from: '付款方地址 (必填)', to: '收款方地址 (必填)', amount: '金额 (必填)', interval: '周期 (e.g. daily/weekly/monthly)', token: '可选, 默认 ETH' },
|
|
1048
|
+
execute: async (args) => {
|
|
1049
|
+
try {
|
|
1050
|
+
const mod = await import('../constraint-runtime/dist/tools/WalletTools/autoPay.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/autoPay.js'));
|
|
1051
|
+
const fn = mod.autoPay || mod.setAutoPay || mod.default;
|
|
1052
|
+
if (!fn)
|
|
1053
|
+
return { success: false, error: 'autoPay 接口未找到' };
|
|
1054
|
+
const r = await fn({
|
|
1055
|
+
from: String(args.from), to: String(args.to), amount: String(args.amount),
|
|
1056
|
+
interval: String(args.interval || 'monthly'), token: args.token,
|
|
1057
|
+
});
|
|
1058
|
+
return { success: true, output: `✅ 自动付款已设置: ${JSON.stringify(r)}` };
|
|
1059
|
+
}
|
|
1060
|
+
catch (e) {
|
|
1061
|
+
return { success: false, error: `设置失败: ${String(e.message || e)}` };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
// Polymarket
|
|
1066
|
+
ctx.tools.set('polymarket_list_markets', {
|
|
1067
|
+
name: 'polymarket_list_markets',
|
|
1068
|
+
description: '列出 Polymarket 预测市场.',
|
|
1069
|
+
parameters: { limit: '可选 数量 (默认 50)', offset: '可选 偏移', closed: '可选 是否只显示已关闭' },
|
|
1070
|
+
execute: async (args) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const { listMarkets } = await import('../constraint-runtime/dist/tools/PolymarketSDK/listMarkets.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/listMarkets.js'));
|
|
1073
|
+
const markets = await listMarkets({
|
|
1074
|
+
limit: args.limit ? Number(args.limit) : 50,
|
|
1075
|
+
offset: args.offset ? Number(args.offset) : 0,
|
|
1076
|
+
closed: args.closed ? String(args.closed) === 'true' : false,
|
|
1077
|
+
});
|
|
1078
|
+
if (!markets || markets.length === 0) {
|
|
1079
|
+
return { success: true, output: '📊 Polymarket 当前没有活跃市场' };
|
|
1080
|
+
}
|
|
1081
|
+
const lines = markets.slice(0, 10).map((m, i) => ` ${i + 1}. [${m.id}] ${m.question}`);
|
|
1082
|
+
return { success: true, output: `📊 Polymarket 找到 ${markets.length} 个市场 (前 10):\n${lines.join('\n')}` };
|
|
1083
|
+
}
|
|
1084
|
+
catch (e) {
|
|
1085
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
ctx.tools.set('polymarket_get_market', {
|
|
1090
|
+
name: 'polymarket_get_market',
|
|
1091
|
+
description: '获取单个 Polymarket 市场的详情.',
|
|
1092
|
+
parameters: { marketId: '市场 ID (必填)' },
|
|
1093
|
+
execute: async (args) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const mod = await import('../constraint-runtime/dist/tools/PolymarketSDK/getMarket.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getMarket.js'));
|
|
1096
|
+
const fn = mod.getMarket || mod.default;
|
|
1097
|
+
const m = await fn(String(args.marketId));
|
|
1098
|
+
if (!m)
|
|
1099
|
+
return { success: false, error: '市场不存在' };
|
|
1100
|
+
return { success: true, output: `📊 市场详情:\n ${JSON.stringify(m, null, 2).substring(0, 1500)}` };
|
|
1101
|
+
}
|
|
1102
|
+
catch (e) {
|
|
1103
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
ctx.tools.set('polymarket_get_orders', {
|
|
1108
|
+
name: 'polymarket_get_orders',
|
|
1109
|
+
description: '查询 Polymarket 订单.',
|
|
1110
|
+
parameters: { marketId: '可选 按市场 ID 过滤' },
|
|
1111
|
+
execute: async (args) => {
|
|
1112
|
+
try {
|
|
1113
|
+
const { getOrders } = await import('../constraint-runtime/dist/tools/PolymarketSDK/getOrders.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getOrders.js'));
|
|
1114
|
+
const orders = await getOrders(args.marketId ? { marketId: String(args.marketId) } : {});
|
|
1115
|
+
return { success: true, output: `📋 订单列表: ${JSON.stringify(orders, null, 2).substring(0, 1500)}` };
|
|
1116
|
+
}
|
|
1117
|
+
catch (e) {
|
|
1118
|
+
return { success: false, error: `查询失败: ${String(e.message || e)}` };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
ctx.tools.set('polymarket_create_order', {
|
|
1123
|
+
name: 'polymarket_create_order',
|
|
1124
|
+
description: '在 Polymarket 下单 (BUY/SELL).',
|
|
1125
|
+
parameters: { marketId: '市场 ID (必填)', side: 'BUY 或 SELL (必填)', price: '价格 0-1 (必填)', size: '数量 USDC (必填)' },
|
|
1126
|
+
execute: async (args) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const { createOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/createOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/createOrder.js'));
|
|
1129
|
+
const r = await createOrder({
|
|
1130
|
+
marketId: String(args.marketId),
|
|
1131
|
+
side: String(args.side).toUpperCase() === 'SELL' ? 'SELL' : 'BUY',
|
|
1132
|
+
price: Number(args.price),
|
|
1133
|
+
size: Number(args.size),
|
|
1134
|
+
});
|
|
1135
|
+
if (r.success)
|
|
1136
|
+
return { success: true, output: `✅ 订单: ${r.message}` };
|
|
1137
|
+
return { success: false, error: r.message, output: r.message };
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
return { success: false, error: `下单失败: ${String(e.message || e)}` };
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
ctx.tools.set('polymarket_cancel_order', {
|
|
1145
|
+
name: 'polymarket_cancel_order',
|
|
1146
|
+
description: '取消 Polymarket 订单.',
|
|
1147
|
+
parameters: { orderId: '订单 ID (必填)' },
|
|
1148
|
+
execute: async (args) => {
|
|
1149
|
+
try {
|
|
1150
|
+
const { cancelOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/cancelOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js'));
|
|
1151
|
+
const r = await cancelOrder({ orderId: String(args.orderId) });
|
|
1152
|
+
if (r.success)
|
|
1153
|
+
return { success: true, output: `✅ 取消订单: ${r.message}` };
|
|
1154
|
+
return { success: false, error: r.message, output: r.message };
|
|
1155
|
+
}
|
|
1156
|
+
catch (e) {
|
|
1157
|
+
return { success: false, error: `取消失败: ${String(e.message || e)}` };
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
// Safe
|
|
1162
|
+
ctx.tools.set('safe_deploy', {
|
|
1163
|
+
name: 'safe_deploy',
|
|
1164
|
+
description: '部署 Safe 多签钱包.',
|
|
1165
|
+
parameters: { owners: 'JSON 数组 owner 地址 (必填)', threshold: '需要几个签名 (必填)' },
|
|
1166
|
+
execute: async (args) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const { deploySafe } = await import('../constraint-runtime/dist/tools/SafeSDK/deploySafe.js').catch(() => import('../constraint-runtime/src/tools/SafeSDK/deploySafe.js'));
|
|
1169
|
+
const owners = Array.isArray(args.owners) ? args.owners : JSON.parse(String(args.owners));
|
|
1170
|
+
const r = await deploySafe({ owners, threshold: Number(args.threshold) });
|
|
1171
|
+
return { success: true, output: `✅ Safe 部署: ${JSON.stringify(r)}` };
|
|
1172
|
+
}
|
|
1173
|
+
catch (e) {
|
|
1174
|
+
return { success: false, error: `部署失败: ${String(e.message || e)}` };
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* 监听 P2P 消息 (远端 + 本地 inbox bus), 把消息存到 _inboxMessages 供 check_inbox 读.
|
|
1181
|
+
* 失败静默, 不阻塞 PiAgentSession 构造.
|
|
1182
|
+
*/
|
|
1183
|
+
export function setupInboxListener(ctx) {
|
|
1184
|
+
try {
|
|
1185
|
+
p2pNetwork.onMessage('*', (msg, from, did) => {
|
|
1186
|
+
try {
|
|
1187
|
+
const text = new TextDecoder().decode(msg);
|
|
1188
|
+
let type = 'message';
|
|
1189
|
+
let payload = text;
|
|
1190
|
+
if (text.startsWith('DID:')) {
|
|
1191
|
+
const pipeIdx = text.indexOf('|');
|
|
1192
|
+
if (pipeIdx > 0) {
|
|
1193
|
+
const rest = text.substring(pipeIdx + 1);
|
|
1194
|
+
const colonIdx = rest.indexOf(':');
|
|
1195
|
+
if (colonIdx > 0) {
|
|
1196
|
+
type = rest.substring(0, colonIdx);
|
|
1197
|
+
payload = rest.substring(colonIdx + 1);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
type = rest;
|
|
1201
|
+
payload = '';
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
const colonIdx = text.indexOf(':');
|
|
1207
|
+
if (colonIdx > 0) {
|
|
1208
|
+
type = text.substring(0, colonIdx);
|
|
1209
|
+
payload = text.substring(colonIdx + 1);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
ctx._inboxMessages.push({
|
|
1213
|
+
id: `p2p-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1214
|
+
from: from.substring(0, 32),
|
|
1215
|
+
fromDid: did,
|
|
1216
|
+
type,
|
|
1217
|
+
payload,
|
|
1218
|
+
timestamp: Date.now(),
|
|
1219
|
+
source: 'p2p',
|
|
1220
|
+
});
|
|
1221
|
+
if (ctx._inboxMessages.length > 1000) {
|
|
1222
|
+
ctx._inboxMessages.splice(0, ctx._inboxMessages.length - 1000);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
catch (err) {
|
|
1226
|
+
console.warn('[PiAgent] inbox listener decode error:', err);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
catch (err) {
|
|
1231
|
+
console.warn('[PiAgent] setupInboxListener (p2pNetwork) failed (non-fatal):', err);
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
const { LocalInboxBus } = require('../network/local-inbox-bus.js');
|
|
1235
|
+
const myRole = process.env.BOLLOON_ROLE || 'default';
|
|
1236
|
+
LocalInboxBus.getInstance().subscribe(myRole, (msg) => {
|
|
1237
|
+
ctx._inboxMessages.push({
|
|
1238
|
+
id: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1239
|
+
from: msg.from || 'unknown',
|
|
1240
|
+
fromDid: msg.fromDid,
|
|
1241
|
+
type: msg.type || 'agent-local-message',
|
|
1242
|
+
payload: msg.payload || '',
|
|
1243
|
+
timestamp: msg.timestamp || Date.now(),
|
|
1244
|
+
source: 'local',
|
|
1245
|
+
});
|
|
1246
|
+
if (ctx._inboxMessages.length > 1000) {
|
|
1247
|
+
ctx._inboxMessages.splice(0, ctx._inboxMessages.length - 1000);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
// LocalInboxBus 可能还没创建, 不阻塞
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次.
|
|
1257
|
+
* 5 分钟 TTL, 容量 200, 只缓存 success 结果.
|
|
1258
|
+
*/
|
|
1259
|
+
export class IdempotencyCache {
|
|
1260
|
+
cache = new Map();
|
|
1261
|
+
TTL_MS = 5 * 60 * 1000;
|
|
1262
|
+
MAX = 200;
|
|
1263
|
+
wrap(tools) {
|
|
1264
|
+
for (const [name, tool] of tools.entries()) {
|
|
1265
|
+
if (!SIDE_EFFECT_TOOLS.has(name))
|
|
1266
|
+
continue;
|
|
1267
|
+
const original = tool.execute;
|
|
1268
|
+
tool.execute = async (args) => {
|
|
1269
|
+
const key = `${name}|${JSON.stringify(args)}`;
|
|
1270
|
+
const cached = this.cache.get(key);
|
|
1271
|
+
if (cached && Date.now() - cached.ts < this.TTL_MS) {
|
|
1272
|
+
return { ...cached.result, output: (cached.result.output || '') + '\n[↻ idempotency cache hit]' };
|
|
1273
|
+
}
|
|
1274
|
+
const result = await original(args);
|
|
1275
|
+
if (result && result.success) {
|
|
1276
|
+
if (this.cache.size >= this.MAX) {
|
|
1277
|
+
this.cache.clear();
|
|
1278
|
+
}
|
|
1279
|
+
this.cache.set(key, { result, ts: Date.now() });
|
|
1280
|
+
}
|
|
1281
|
+
return result;
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
clear() {
|
|
1286
|
+
this.cache.clear();
|
|
1287
|
+
}
|
|
1288
|
+
}
|