/** * Agent Phone Runtime — MCP Tool Wrappers * * These tools call the Android device via HTTP (ADB port-forwarded). * Set PHONE_MCP_URL env var or use default http://localhost:8766 */ const http = require('http'); const PHONE_URL = process.env.PHONE_MCP_URL || 'http://localhost:8766'; const EVENT_URL = process.env.PHONE_EVENT_URL || 'http://localhost:8765'; // ─── HTTP helper ────────────────────────────────────────────────────────────── async function callPhone(path, body = null, baseUrl = PHONE_URL) { return new Promise((resolve, reject) => { const payload = body ? JSON.stringify(body) : null; const url = new URL(path, baseUrl); const options = { hostname: url.hostname, port: url.port || 80, path: url.pathname + url.search, method: payload ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}), }, timeout: 10_000, }; const req = http.request(options, (res) => { let data = ''; res.on('data', (c) => { data += c; }); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(data) }); } catch { resolve({ status: res.statusCode, body: data }); } }); }); req.on('error', (e) => reject(new Error(`手机连接失败: ${e.message}。请确认手机通过 USB 连接并运行了 start.sh`))); req.on('timeout', () => { req.destroy(); reject(new Error('手机响应超时 (10s),请检查 Agent Phone Runtime 是否运行')); }); if (payload) req.write(payload); req.end(); }); } // ─── Tool definitions ───────────────────────────────────────────────────────── function createPhoneTools() { return [ // ── send_message ────────────────────────────────────────────────────── { name: 'send_message', description: '通过 IM App(微信、Telegram 等)向指定联系人发送消息。如不指定 app,自动路由到最常用的 IM。', parameters: { type: 'object', properties: { to: { type: 'string', description: '收件人姓名或联系人 ID' }, content: { type: 'string', description: '消息内容' }, app: { type: 'string', description: '可选:wechat / telegram / auto(默认 auto)' }, }, required: ['to', 'content'], }, async execute({ to, content, app = 'auto' }) { const res = await callPhone('/mcp/tools/send_message', { to, content, app }); if (res.status === 200 || res.status === 201) { return { success: true, message: `消息已发送给 ${to}`, detail: res.body }; } throw new Error(res.body?.error || `发送失败 (HTTP ${res.status})`); }, }, // ── search_memory ───────────────────────────────────────────────────── { name: 'search_memory', description: '搜索手机 Memory Graph。可查询联系人、对话记录、日程、任务、文档等所有结构化记忆。', parameters: { type: 'object', properties: { query: { type: 'string', description: '自然语言查询,如"和张三上周的对话"、"所有未完成任务"' }, type: { type: 'string', description: '可选过滤类型:conversation / message / person / task / event / document' }, limit: { type: 'number', description: '返回条数上限,默认 20' }, }, required: ['query'], }, async execute({ query, type, limit = 20 }) { const res = await callPhone('/mcp/tools/search_memory', { query, type, limit }); if (res.status === 200) return res.body; throw new Error(res.body?.error || `搜索失败 (HTTP ${res.status})`); }, }, // ── create_event ────────────────────────────────────────────────────── { name: 'create_event', description: '在手机系统日历中创建日程/会议。', parameters: { type: 'object', properties: { title: { type: 'string', description: '日程标题' }, time: { type: 'string', description: '开始时间,ISO 8601 或自然语言如"明天下午3点"' }, duration_minutes: { type: 'number', description: '时长(分钟),默认 60' }, attendees: { type: 'array', items: { type: 'string' }, description: '参与者姓名列表(可选)' }, description: { type: 'string', description: '备注(可选)' }, }, required: ['title', 'time'], }, async execute({ title, time, duration_minutes = 60, attendees, description }) { const res = await callPhone('/mcp/tools/create_event', { title, time, duration_minutes, attendees, description }); if (res.status === 200 || res.status === 201) { return { success: true, message: `日程"${title}"已创建`, detail: res.body }; } throw new Error(res.body?.error || `创建日程失败 (HTTP ${res.status})`); }, }, // ── create_task ─────────────────────────────────────────────────────── { name: 'create_task', description: '在手机 Todo App 创建待办任务。', parameters: { type: 'object', properties: { title: { type: 'string', description: '任务标题' }, deadline: { type: 'string', description: '截止时间(可选),ISO 8601 或自然语言' }, assignee: { type: 'string', description: '负责人(可选)' }, priority: { type: 'string', description: '优先级:high / medium / low(默认 medium)' }, }, required: ['title'], }, async execute({ title, deadline, assignee, priority = 'medium' }) { const res = await callPhone('/mcp/tools/create_task', { title, deadline, assignee, priority }); if (res.status === 200 || res.status === 201) { return { success: true, message: `任务"${title}"已创建`, detail: res.body }; } throw new Error(res.body?.error || `创建任务失败 (HTTP ${res.status})`); }, }, // ── read_document ───────────────────────────────────────────────────── { name: 'read_document', description: '读取手机备忘录、笔记等文档。支持语义搜索。', parameters: { type: 'object', properties: { query: { type: 'string', description: '搜索关键词或描述(可选)' }, source: { type: 'string', description: '来源 App,如 notes / wechat / telegram(可选,不填则搜所有)' }, limit: { type: 'number', description: '返回条数,默认 10' }, }, required: [], }, async execute({ query, source, limit = 10 }) { const res = await callPhone('/mcp/tools/read_document', { query, source, limit }); if (res.status === 200) return res.body; throw new Error(res.body?.error || `读取文档失败 (HTTP ${res.status})`); }, }, // ── get_device_status ───────────────────────────────────────────────── { name: 'get_device_status', description: '获取手机 Agent Phone Runtime 的当前运行状态,包括各服务是否启动、Root 状态、已监控的 App 列表等。', parameters: { type: 'object', properties: {}, required: [] }, async execute() { try { const res = await callPhone('/status'); return res.body; } catch { // Try event server as fallback try { const res = await callPhone('/health', null, EVENT_URL); return { event_server: 'online', detail: res.body }; } catch (e2) { return { status: 'offline', message: '手机未连接或 Agent Phone Runtime 未运行', hint: '请运行 bash start.sh 然后在 Pixel 上的 Termux 中启动服务', }; } } }, }, // ── get_recent_events ───────────────────────────────────────────────── { name: 'get_recent_events', description: '获取手机最近发生的事件,包括通知、UI 操作、App 切换、剪贴板变化等。', parameters: { type: 'object', properties: { limit: { type: 'number', description: '条数,默认 20' }, type: { type: 'string', description: '过滤事件类型:notification / ui_event / clipboard / intent(可选)' }, since_minutes: { type: 'number', description: '只返回最近 N 分钟内的事件(可选)' }, }, required: [], }, async execute({ limit = 20, type, since_minutes }) { const params = new URLSearchParams({ limit }); if (type) params.set('type', type); if (since_minutes) params.set('since_minutes', since_minutes); try { const res = await callPhone(`/events?${params}`, null, EVENT_URL); return res.body; } catch (e) { return { error: e.message, events: [], hint: '事件服务未运行,请在 Termux 中启动 event_server.py' }; } }, }, ]; } module.exports = { createPhoneTools };