@bolloon/bolloon-agent 0.4.9 → 0.4.11

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.
@@ -34,6 +34,84 @@ export const SIDE_EFFECT_TOOLS = new Set([
34
34
  'write_file', 'edit_file', 'shell_exec', 'git_commit', 'git_push', 'git_branch',
35
35
  'create_task', 'update_task', 'terminal', 'process',
36
36
  ]);
37
+ /** 2026-08-12 (TaskA): 探测可用的命令 (跨平台: Windows 无 python3, 有 python). 返回 [cmd, ...] 或 null. */
38
+ async function detectRunner(candidates) {
39
+ const { spawn } = await import('child_process');
40
+ for (const c of candidates) {
41
+ try {
42
+ const ok = await new Promise((resolve) => {
43
+ const p = spawn(c, ['--version'], { stdio: 'ignore', windowsHide: true });
44
+ p.on('close', (code) => resolve(code === 0));
45
+ p.on('error', () => resolve(false));
46
+ });
47
+ if (ok)
48
+ return [c];
49
+ }
50
+ catch { /* 下一个 */ }
51
+ }
52
+ return null;
53
+ }
54
+ export async function runCodeSnippet(opts) {
55
+ const code = String(opts.code ?? '').trim();
56
+ if (!code)
57
+ return { success: false, error: 'code 必填' };
58
+ const lang = String(opts.language || '').trim().toLowerCase();
59
+ // 语言 → (扩展名, 解释器命令)
60
+ const ext = lang === 'python' || lang === 'py' ? 'py'
61
+ : lang === 'javascript' || lang === 'js' || lang === 'node' ? 'js'
62
+ : lang === 'typescript' || lang === 'ts' ? 'ts'
63
+ : lang === 'shell' || lang === 'bash' || lang === 'sh' ? 'sh'
64
+ : lang === 'html' ? 'html'
65
+ : 'txt';
66
+ const runner = lang === 'python' || lang === 'py' ? await detectRunner(['python3', 'python'])
67
+ : lang === 'javascript' || lang === 'js' || lang === 'node' ? await detectRunner(['node'])
68
+ : lang === 'typescript' || lang === 'ts' ? ['npx', 'tsx']
69
+ : lang === 'shell' || lang === 'bash' || lang === 'sh' ? ['bash']
70
+ : null;
71
+ if (!runner) {
72
+ if (['python', 'py', 'js', 'javascript', 'node', 'ts', 'typescript', 'shell', 'bash', 'sh'].includes(lang)) {
73
+ return { success: false, error: `解释器未找到 (${lang}), 请装对应运行时或改用 command` };
74
+ }
75
+ return { success: false, error: `不支持的语言 '${lang}', 支持: python/js/ts/shell/html` };
76
+ }
77
+ const { tmpdir } = await import('os');
78
+ const tmpDir = opts.tmpDir ?? tmpdir();
79
+ const file = `${tmpDir}/bolloon-code-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
80
+ const fsMod = await import('fs/promises');
81
+ await fsMod.writeFile(file, code, 'utf-8');
82
+ try {
83
+ const { spawn } = await import('child_process');
84
+ const timeoutMs = opts.timeoutMs ?? 30000;
85
+ const result = await new Promise((resolve) => {
86
+ const proc = spawn(runner[0], [...runner.slice(1), file], {
87
+ cwd: opts.cwd ?? process.cwd(),
88
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
89
+ windowsHide: true,
90
+ });
91
+ let stdout = '';
92
+ let stderr = '';
93
+ const timer = setTimeout(() => {
94
+ try {
95
+ proc.kill('SIGKILL');
96
+ }
97
+ catch { /* 忽略 */ }
98
+ resolve({ success: false, error: `代码执行超时 (>${timeoutMs}ms)`, output: (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim().slice(0, 8000) });
99
+ }, timeoutMs);
100
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
101
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
102
+ proc.on('error', (e) => { clearTimeout(timer); resolve({ success: false, error: `启动失败: ${e.message}` }); });
103
+ proc.on('close', (code) => {
104
+ clearTimeout(timer);
105
+ const output = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim().slice(0, 8000) || '(无输出)';
106
+ resolve({ success: code === 0, output, exitCode: code, language: lang, script: file });
107
+ });
108
+ });
109
+ return result;
110
+ }
111
+ finally {
112
+ await fsMod.rm(file, { force: true }).catch(() => { });
113
+ }
114
+ }
37
115
  export async function runTerminalCommand(raw, opts = {}) {
38
116
  const { checkTerminalCommand } = await import('./shell-guard.js');
39
117
  const timeoutMs = opts.timeoutMs ?? 30000;
@@ -716,14 +794,20 @@ export function registerBuiltinTools(ctx) {
716
794
  // 与 shell_exec 的区别: 直接接受完整 shell 命令字符串, 更适合模型自主写命令.
717
795
  ctx.tools.set('terminal', {
718
796
  name: 'terminal',
719
- description: '执行完整 shell 命令 (支持管道/重定向/写文件/跑脚本). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据), 其余灵活放行. 适合: 写 HTML 文件、跑 python/node 脚本、查系统状态、装依赖. 多条命令用 commands 数组并行执行. 长命令 (服务器/构建/后台任务) 设 background=true 后台执行不阻塞对话.',
720
- parameters: { command: '完整 shell 命令 (必填, 如: echo "<html>" > /tmp/site/index.html && ls /tmp/site)', commands: '可选: 多条命令数组 (并行执行), 每条独立字符串', timeoutMs: '超时毫秒, 默认 30000', background: '可选: true 后台执行, 立即返回 session_id (用 process 工具 poll/wait/kill)' },
797
+ description: '执行完整 shell 命令 (支持管道/重定向/写文件/跑脚本). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据), 其余灵活放行. 适合: 写 HTML 文件、跑 python/node 脚本、查系统状态、装依赖. 多条命令用 commands 数组并行执行. 长命令 (服务器/构建/后台任务) 设 background=true 后台执行不阻塞对话. 也可直接传 code+language 自动写脚本执行 (便捷代码运行: python/js/ts/shell/html).',
798
+ parameters: { command: '完整 shell 命令 (可选, 如: echo "<html>" > /tmp/site/index.html && ls /tmp/site)', commands: '可选: 多条命令数组 (并行执行), 每条独立字符串', code: '可选: 一段代码, 传 code+language 时自动写脚本执行 (便捷代码运行)', language: '可选: code 的语言 (python/js/ts/shell/html), 默认自动', timeoutMs: '超时毫秒, 默认 30000', background: '可选: true 后台执行, 立即返回 session_id (用 process 工具 poll/wait/kill)' },
721
799
  execute: async (args) => {
800
+ const timeoutMs = Number(args.timeoutMs) || 30000;
801
+ // 便捷代码运行: 传 code → 自动写脚本执行
802
+ const code = String(args.code ?? '').trim();
803
+ if (code) {
804
+ const lang = String(args.language || '').trim().toLowerCase();
805
+ return await runCodeSnippet({ code, language: lang, timeoutMs, cwd: ctx.cwd });
806
+ }
722
807
  const raw = String(args.command || '').trim();
723
808
  const commands = Array.isArray(args.commands) ? args.commands.map((c) => String(c || '').trim()).filter(Boolean) : [];
724
809
  if (!raw && commands.length === 0)
725
- return { success: false, error: 'commandcommands 必填' };
726
- const timeoutMs = Number(args.timeoutMs) || 30000;
810
+ return { success: false, error: 'command/code/commands 至少一个必填' };
727
811
  return await runTerminalCommand(raw, { timeoutMs, cwd: ctx.cwd, commands: commands.length > 0 ? commands : undefined, background: String(args.background).toLowerCase() === 'true' });
728
812
  }
729
813
  });
@@ -2131,6 +2215,53 @@ export function registerBuiltinTools(ctx) {
2131
2215
  }
2132
2216
  }
2133
2217
  });
2218
+ // 2026-08-12: MCP 驱动前端 UI 工具 — agent 理解用户意图后调用, 通过 SSE 广播驱动前端组件.
2219
+ // 复用 ui-tools (dispatchUiAction → broadcast {type:'ui'}), 前端订阅 /events 执行.
2220
+ const registerUiAgentTools = async () => {
2221
+ const ui = await import('../pi-ecosystem-mcp/ui-tools.js');
2222
+ // 注册到 MCP 系统 (供 mcp_list_tools 可见) + 注册为 agent 工具
2223
+ ui.registerUiControlTools();
2224
+ const uiTools = [
2225
+ { name: 'ui_switch_tab', description: '驱动前端切换底部 tab (微信/通讯录/发现/我). 用户想"去通讯录/去设置/去我的"时调用.', params: { tab: 'wechat|contacts|discover|me (必填)' }, map: (a) => ({ action: 'switchTab', data: { tab: String(a.tab || '') } }) },
2226
+ { name: 'ui_open_chat', description: '驱动前端打开某个智能体聊天页. 用户想"打开和 X 的聊天"时调用.', params: { channelId: '目标 channel id (必填)' }, map: (a) => ({ action: 'openChat', data: { channelId: String(a.channelId || '') } }) },
2227
+ { name: 'ui_open_settings', description: '驱动前端打开设置页.', params: {}, map: () => ({ action: 'openSettings', data: {} }) },
2228
+ { name: 'ui_open_wallet', description: '驱动前端打开钱包.', params: {}, map: () => ({ action: 'openWallet', data: {} }) },
2229
+ { name: 'ui_open_add_friend', description: '驱动前端打开添加好友.', params: {}, map: () => ({ action: 'openAddFriend', data: {} }) },
2230
+ { name: 'ui_show_toast', description: '驱动前端顶部显示提示 (toast).', params: { message: '提示内容 (必填)' }, map: (a) => ({ action: 'showToast', data: { message: String(a.message || '') } }) },
2231
+ { name: 'ui_go_back', description: '驱动前端返回上一页.', params: {}, map: () => ({ action: 'goBack', data: {} }) },
2232
+ ];
2233
+ for (const t of uiTools) {
2234
+ ctx.tools.set(t.name, {
2235
+ name: t.name,
2236
+ description: t.description,
2237
+ parameters: t.params,
2238
+ execute: async (args) => {
2239
+ const r = ui.dispatchUiAction(t.map(args));
2240
+ return r.success ? { success: true, output: r.output } : { success: false, error: r.output };
2241
+ },
2242
+ });
2243
+ }
2244
+ };
2245
+ registerUiAgentTools().catch(() => { });
2246
+ // 2026-08-12: A2UI (Agent to UI) 工具 — agent 生成 A2UI 消息 (createSurface/updateComponents),
2247
+ // 经 SSE 广播, 前端用 @a2ui/react renderer 渲染. 见 src/pi-ecosystem-a2ui/.
2248
+ (async () => {
2249
+ try {
2250
+ const a2ui = await import('../pi-ecosystem-a2ui/index.js');
2251
+ for (const t of a2ui.A2UI_TOOL_DEFS) {
2252
+ ctx.tools.set(t.name, {
2253
+ name: t.name,
2254
+ description: t.description,
2255
+ parameters: t.params,
2256
+ execute: async (args) => {
2257
+ const r = a2ui.dispatchA2uiMessage(t.build(args));
2258
+ return r.success ? { success: true, output: r.output } : { success: false, error: r.output };
2259
+ },
2260
+ });
2261
+ }
2262
+ }
2263
+ catch { /* A2UI 工具注册失败静默 */ }
2264
+ })();
2134
2265
  // ============================================================
2135
2266
  // publish_did (2026-08-03) — 把当前 agent 的 DID 发布到 IPFS + IPNS
2136
2267
  // 全自动: 自动安装/启动本地 Kubo → 上传 DID 文档 → 发布 IPNS name
@@ -0,0 +1,115 @@
1
+ /**
2
+ * python-exec.ts — Python 代码执行引擎 (2026-08-12, TaskP)
3
+ *
4
+ * 借鉴 hermes code_execution_tool: 让 agent 输入 Python 代码运行操作, 而非逐条 shell 命令.
5
+ * 设计:
6
+ * - 把 LLM 给的 Python 代码写到临时文件, 用 python3 子进程执行 (隔离, 不污染主进程)
7
+ * - 带超时 + kill (子进程跑太久强制终止)
8
+ * - stdout/stderr 合并捕获 + 截断 (head+tail, 显式 truncation 标记 — hermes 模式)
9
+ * - 依赖检测: 找不到 python3 返回明确错误
10
+ *
11
+ * 安全: 不 eval (子进程隔离); 代码里若含高危 shell (os.system("rm -rf /") 等) 走 checkTerminalCommand 预检 (最佳努力).
12
+ */
13
+ import { spawn } from 'child_process';
14
+ import * as fs from 'fs/promises';
15
+ import * as os from 'os';
16
+ import * as path from 'path';
17
+ const MAX_OUTPUT = 16000; // 捕获输出上限 (head+tail)
18
+ /** 探测可用的 python 解释器 (python3 / python) */
19
+ async function detectPython() {
20
+ const candidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
21
+ for (const c of candidates) {
22
+ try {
23
+ await new Promise((resolve, reject) => {
24
+ const p = spawn(c, ['--version'], { stdio: 'ignore', windowsHide: true });
25
+ p.on('close', (code) => (code === 0 ? resolve() : reject(new Error('exit ' + code))));
26
+ p.on('error', reject);
27
+ });
28
+ return c;
29
+ }
30
+ catch { /* 下一个 */ }
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * 截断输出: 保留 head + tail, 中间省略, 返回截断标记 (hermes 模式).
36
+ */
37
+ export function truncateOutput(text) {
38
+ if (text.length <= MAX_OUTPUT)
39
+ return { text, truncated: false };
40
+ const head = text.slice(0, 4000);
41
+ const tail = text.slice(-4000);
42
+ return { text: `${head}\n…[输出截断 ${text.length - 8000} 字节]…\n${tail}`, truncated: true };
43
+ }
44
+ /** 最佳努力: 代码里明显的高危 shell 调用预检 (防 os.system("rm -rf /") 等) */
45
+ function checkDangerousCode(code) {
46
+ const dangerous = [
47
+ { re: /os\.system\s*\(\s*["']\s*rm\s+(-[a-z]*f[a-z]*\s+)?-[a-z]*r[a-z]*\s+\//, reason: '禁止递归删除根目录' },
48
+ { re: /shutil\.rmtree\s*\(\s*["']\s*\//, reason: '禁止删除根目录' },
49
+ { re: /os\.system\s*\(\s*["']\s*(sudo|mkfs|dd\s+if=.*of=\/dev)/, reason: '禁止高危系统命令' },
50
+ ];
51
+ for (const { re, reason } of dangerous) {
52
+ if (re.test(code))
53
+ return { allowed: false, reason };
54
+ }
55
+ return { allowed: true };
56
+ }
57
+ /**
58
+ * 执行一段 Python 代码 (隔离子进程 + 超时 + 输出截断).
59
+ */
60
+ export async function executePython(opts) {
61
+ const code = String(opts.code ?? '').trim();
62
+ if (!code)
63
+ return { success: false, error: 'code 必填', output: '' };
64
+ // 高危代码预检 (最佳努力)
65
+ const d = checkDangerousCode(code);
66
+ if (!d.allowed)
67
+ return { success: false, error: `[python-guard] ${d.reason}`, deniedByGuard: true, output: '' };
68
+ // 探测 python
69
+ const python = await detectPython();
70
+ if (!python) {
71
+ return { success: false, error: '未找到 python3/python 解释器, 无法执行 Python 代码', output: '' };
72
+ }
73
+ // 写临时文件
74
+ const tmpFile = path.join(os.tmpdir(), `bolloon-py-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.py`);
75
+ await fs.writeFile(tmpFile, code, 'utf-8');
76
+ const timeoutMs = opts.timeoutMs ?? 30000;
77
+ try {
78
+ const result = await new Promise((resolve) => {
79
+ const proc = spawn(python, [tmpFile], {
80
+ cwd: opts.cwd ?? process.cwd(),
81
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', PYTHONIOENCODING: 'utf-8' },
82
+ windowsHide: true,
83
+ });
84
+ let stdout = '';
85
+ let stderr = '';
86
+ const timer = setTimeout(() => {
87
+ try {
88
+ proc.kill('SIGKILL');
89
+ }
90
+ catch { /* 忽略 */ }
91
+ resolve({ success: false, output: '', error: `Python 执行超时 (>${timeoutMs}ms), 已终止`, exitCode: -1, python });
92
+ }, timeoutMs);
93
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
94
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
95
+ proc.on('error', (e) => { clearTimeout(timer); resolve({ success: false, output: '', error: `启动失败: ${e.message}`, python }); });
96
+ proc.on('close', (code) => {
97
+ clearTimeout(timer);
98
+ const raw = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
99
+ const { text, truncated } = truncateOutput(raw || '(无输出)');
100
+ resolve({
101
+ success: code === 0,
102
+ output: text,
103
+ stdoutTruncated: truncated,
104
+ exitCode: code,
105
+ error: code === 0 ? undefined : `exit code ${code}`,
106
+ python,
107
+ });
108
+ });
109
+ });
110
+ return result;
111
+ }
112
+ finally {
113
+ await fs.rm(tmpFile, { force: true }).catch(() => { });
114
+ }
115
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * a2ui.ts — A2UI (Agent to UI) 协议集成 (2026-08-12)
3
+ *
4
+ * bolloon agent 生成 A2UI 消息 (createSurface / updateComponents / updateDataModel / deleteSurface),
5
+ * 经 SSE 广播给前端 (web / 手机端 Capacitor), 前端用 @a2ui/react renderer 渲染.
6
+ *
7
+ * 参考: https://a2ui.org/specification/v1.0-a2ui/ + D:\AI\A2UI (本地 spec 源码)
8
+ *
9
+ * 机制:
10
+ * - agent 工具 (a2ui_create_surface 等) 生成 A2UI JSON 消息
11
+ * - dispatchA2uiMessage → broadcast({ type: 'a2ui', message }) 给前端
12
+ * - 前端 MessageProcessor 接收渲染 (A2uiSurface)
13
+ */
14
+ /** broadcast 注入点 (server 调用 setA2uiBroadcast 注入, 关联 SSE /events) */
15
+ let a2uiBroadcast = null;
16
+ export function setA2uiBroadcast(fn) {
17
+ a2uiBroadcast = fn;
18
+ }
19
+ /** 广播一条 A2UI 消息给所有前端 */
20
+ export function broadcastA2uiMessage(message) {
21
+ if (!a2uiBroadcast)
22
+ return false;
23
+ a2uiBroadcast({ type: 'a2ui', message });
24
+ return true;
25
+ }
26
+ /** 校验 + 广播一条 A2UI 消息 (agent 工具 execute 调用) */
27
+ export function dispatchA2uiMessage(message) {
28
+ const type = message?.type;
29
+ const surfaceId = String(message?.surfaceId || '').trim();
30
+ if (!type || !['createSurface', 'updateComponents', 'updateDataModel', 'deleteSurface'].includes(type)) {
31
+ return { success: false, output: 'type 必须是 createSurface/updateComponents/updateDataModel/deleteSurface' };
32
+ }
33
+ if (!surfaceId)
34
+ return { success: false, output: 'surfaceId 必填' };
35
+ const ok = broadcastA2uiMessage({ type, surfaceId, ...message });
36
+ return { success: ok, output: ok ? `已广播 A2UI ${type} (surface=${surfaceId})` : 'A2UI 广播未连接' };
37
+ }
38
+ /**
39
+ * agent 工具注册表 (a2ui-* 工具定义). 由 pi-sdk-tools 注册为 agent 工具.
40
+ */
41
+ export const A2UI_TOOL_DEFS = [
42
+ {
43
+ name: 'a2ui_create_surface',
44
+ description: '创建 A2UI surface (前端渲染区). surfaceId: 渲染区标识. 用户想要一个动态 UI 面板/表单/卡片时先创建 surface.',
45
+ params: { surfaceId: '渲染区 id (必填)', title: 'surface 标题 (可选)' },
46
+ build: (a) => ({ type: 'createSurface', surfaceId: String(a.surfaceId || ''), title: String(a.title || '') }),
47
+ },
48
+ {
49
+ name: 'a2ui_update_components',
50
+ description: '向 A2UI surface 添加/更新组件 (componentTree JSON). 前端用 @a2ui/react 渲染. components: 组件树 JSON 数组 (Text/Column/Button 等 basicCatalog 组件).',
51
+ params: { surfaceId: '渲染区 id (必填)', components: '组件树 JSON (必填, e.g. [{"type":"text","data":{"text":"你好"}}])' },
52
+ build: (a) => {
53
+ let components = a.components;
54
+ if (typeof components === 'string') {
55
+ try {
56
+ components = JSON.parse(components);
57
+ }
58
+ catch {
59
+ components = [];
60
+ }
61
+ }
62
+ return { type: 'updateComponents', surfaceId: String(a.surfaceId || ''), components };
63
+ },
64
+ },
65
+ {
66
+ name: 'a2ui_update_data',
67
+ description: '更新 A2UI surface 的数据模型. path: 数据路径 (如 /user/name), value: 数据值.',
68
+ params: { surfaceId: '渲染区 id (必填)', path: '数据路径 (必填)', value: '数据值' },
69
+ build: (a) => ({ type: 'updateDataModel', surfaceId: String(a.surfaceId || ''), path: String(a.path || ''), value: a.value }),
70
+ },
71
+ {
72
+ name: 'a2ui_delete_surface',
73
+ description: '删除 A2UI surface (移除前端渲染区). surfaceId: 渲染区 id.',
74
+ params: { surfaceId: '渲染区 id (必填)' },
75
+ build: (a) => ({ type: 'deleteSurface', surfaceId: String(a.surfaceId || '') }),
76
+ },
77
+ ];
78
+ /** 工具名 → build 函数 */
79
+ export function a2uiToolDef(name) {
80
+ return A2UI_TOOL_DEFS.find((t) => t.name === name);
81
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ui-tools.ts — MCP 驱动的前端 UI 控制工具 (2026-08-12)
3
+ *
4
+ * 目标: bolloon 作为 MCP server 暴露 UI 控制工具, agent 理解用户意图后
5
+ * 通过 MCP 调用这些工具, 驱动前端 (web / 手机端 Capacitor) 的 UI 组件.
6
+ *
7
+ * 机制:
8
+ * - 注册一组 UI 控制工具到 MCP 系统 (registerTool), 供 agent 用 mcp_tool 调用
9
+ * - 工具 execute 时通过注入的 broadcast 回调, 广播 { type: 'ui', action, data } 给前端 (SSE /events)
10
+ * - 前端订阅 /events, 收到 ui 指令执行对应组件 (switchTab/openChat/openSettings 等)
11
+ *
12
+ * 设计: UI 工具不走外部 MCP server (executeTool 硬依赖 server), 而是本地 handler + broadcast.
13
+ */
14
+ import { registerTool, listTools } from './index.js';
15
+ /** broadcast 注入点 (由 server 调用 setUiBroadcast 注入, 广播给前端 SSE) */
16
+ let uiBroadcast = null;
17
+ /** 注入广播函数 (server 启动时调用, 关联到 SSE /events) */
18
+ export function setUiBroadcast(fn) {
19
+ uiBroadcast = fn;
20
+ }
21
+ /** 广播一条 UI 控制指令给所有前端 */
22
+ export function broadcastUiAction(action, data) {
23
+ if (!uiBroadcast)
24
+ return false;
25
+ uiBroadcast({ type: 'ui', action, data: data ?? {} });
26
+ return true;
27
+ }
28
+ /** 把一次 UI 工具调用 (agent 意图理解后发起) 广播给前端 */
29
+ export function dispatchUiAction(call) {
30
+ const { action, data } = call || {};
31
+ if (!action)
32
+ return { success: false, output: 'action 必填 (switchTab/openChat/openSettings 等)' };
33
+ const ok = broadcastUiAction(action, data);
34
+ return { success: ok, output: ok ? `已驱动前端: ${action}` : 'UI 广播未连接' };
35
+ }
36
+ /** 注册 UI 控制工具到 MCP 系统 (供 agent mcp_tool 调用) */
37
+ export function registerUiControlTools() {
38
+ const toolDefs = [
39
+ {
40
+ name: 'ui_switch_tab',
41
+ description: '驱动前端切换底部 tab. 用户想"去通讯录/去我的/去设置"时调用. tab: wechat|contacts|discover|me',
42
+ inputSchema: { type: 'object', properties: { tab: { type: 'string' } }, required: ['tab'] },
43
+ },
44
+ {
45
+ name: 'ui_open_chat',
46
+ description: '驱动前端打开某个智能体聊天页. 用户想"打开和 X 的聊天"时调用. channelId: 目标 channel',
47
+ inputSchema: { type: 'object', properties: { channelId: { type: 'string' } }, required: ['channelId'] },
48
+ },
49
+ {
50
+ name: 'ui_open_settings',
51
+ description: '驱动前端打开设置页. 用户想"打开设置/配置"时调用.',
52
+ inputSchema: { type: 'object', properties: {} },
53
+ },
54
+ {
55
+ name: 'ui_open_wallet',
56
+ description: '驱动前端打开钱包. 用户想"看钱包/支付"时调用.',
57
+ inputSchema: { type: 'object', properties: {} },
58
+ },
59
+ {
60
+ name: 'ui_open_add_friend',
61
+ description: '驱动前端打开添加好友. 用户想"添加好友"时调用.',
62
+ inputSchema: { type: 'object', properties: {} },
63
+ },
64
+ {
65
+ name: 'ui_send_message',
66
+ description: '驱动前端在当前聊天发送消息. channelId: 目标 channel, text: 要发的消息文本.',
67
+ inputSchema: { type: 'object', properties: { channelId: { type: 'string' }, text: { type: 'string' } }, required: ['channelId', 'text'] },
68
+ },
69
+ {
70
+ name: 'ui_show_toast',
71
+ description: '在前端顶部显示一条提示 (toast). message: 提示内容.',
72
+ inputSchema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] },
73
+ },
74
+ {
75
+ name: 'ui_go_back',
76
+ description: '驱动前端返回上一页. 用户想"返回/回去"时调用.',
77
+ inputSchema: { type: 'object', properties: {} },
78
+ },
79
+ ];
80
+ let count = 0;
81
+ for (const t of toolDefs) {
82
+ // 已注册则跳过 (幂等)
83
+ if (listTools().some((x) => x.name === t.name))
84
+ continue;
85
+ registerTool({
86
+ name: t.name,
87
+ description: t.description,
88
+ inputSchema: t.inputSchema,
89
+ serverName: 'ui', // 本地 UI 工具 (不走外部 server; 由 agent 调用方 dispatch)
90
+ });
91
+ count++;
92
+ }
93
+ return count;
94
+ }
95
+ /** agent 工具 (pi-sdk-tools 注册) 的 execute 适配: name → UiAction */
96
+ export function uiToolNameToAction(name) {
97
+ switch (name) {
98
+ case 'ui_switch_tab': return 'switchTab';
99
+ case 'ui_open_chat': return 'openChat';
100
+ case 'ui_open_settings': return 'openSettings';
101
+ case 'ui_open_wallet': return 'openWallet';
102
+ case 'ui_open_add_friend': return 'openAddFriend';
103
+ case 'ui_send_message': return 'sendMessage';
104
+ case 'ui_show_toast': return 'showToast';
105
+ case 'ui_go_back': return 'goBack';
106
+ default: return null;
107
+ }
108
+ }