phone-tools.js
11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/**
* 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 };