phone-tools.js 11.3 KB
/**
 * 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 };