@voko/lite 0.3.1

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.
Files changed (62) hide show
  1. package/package.json +32 -0
  2. package/scripts/build-native.js +72 -0
  3. package/src/bankHeadOffices.js +20543 -0
  4. package/src/channels/email.js +35 -0
  5. package/src/channels/feishu.js +31 -0
  6. package/src/channels/qq-email.js +30 -0
  7. package/src/channels/registry.js +279 -0
  8. package/src/channels/telegram.js +28 -0
  9. package/src/channels/voko-email.js +7 -0
  10. package/src/channels/wechat.js +35 -0
  11. package/src/cli.js +120 -0
  12. package/src/context.js +164 -0
  13. package/src/core/access-control-api.js +150 -0
  14. package/src/core/access-control.js +56 -0
  15. package/src/core/agent-registration.js +319 -0
  16. package/src/core/api-signature.js +33 -0
  17. package/src/core/audit.js +133 -0
  18. package/src/core/database.js +1409 -0
  19. package/src/core/did-auth.js +54 -0
  20. package/src/core/hermes-paths.js +57 -0
  21. package/src/core/invitation.js +49 -0
  22. package/src/core/lite-bus.js +16 -0
  23. package/src/core/llm-client.js +1032 -0
  24. package/src/core/messenger.js +456 -0
  25. package/src/core/notifier.js +99 -0
  26. package/src/core/offline-sync.js +150 -0
  27. package/src/core/payment.js +285 -0
  28. package/src/core/publish-agent.js +166 -0
  29. package/src/core/register-capabilities.js +119 -0
  30. package/src/core/search-capabilities.js +136 -0
  31. package/src/core/send-message.js +85 -0
  32. package/src/core/set-agent-status.js +65 -0
  33. package/src/core/update-agent-profile.js +102 -0
  34. package/src/core/worker-manager.js +332 -0
  35. package/src/endpoints.json +21 -0
  36. package/src/index.js +712 -0
  37. package/src/mcp/CLAUDE_TEST.md +82 -0
  38. package/src/mcp/FULL_TEST.md +139 -0
  39. package/src/mcp/TEST.md +124 -0
  40. package/src/mcp/TEST_STEPS.md +75 -0
  41. package/src/mcp/server.js +612 -0
  42. package/src/mcp/tools.js +1367 -0
  43. package/src/mcp/transport/http.js +95 -0
  44. package/src/mcp/transport/stdio.js +20 -0
  45. package/src/preload.js +27 -0
  46. package/src/server/agent-email-api.js +120 -0
  47. package/src/server/agent-manager.js +580 -0
  48. package/src/server/email-handler.js +329 -0
  49. package/src/server/feishu-handler.js +249 -0
  50. package/src/server/hermes-api-client.js +166 -0
  51. package/src/server/hermes-discovery.js +80 -0
  52. package/src/server/hermes-handler.js +287 -0
  53. package/src/server/openclaw-handler-cli.js +131 -0
  54. package/src/server/openclaw-websocket-handler.js +1290 -0
  55. package/src/server/oss.js +186 -0
  56. package/src/server/owner-intervention-notifier.js +320 -0
  57. package/src/server/release-page.html +204 -0
  58. package/src/server/telegram-handler.js +208 -0
  59. package/src/server/voko-email-handler.js +68 -0
  60. package/src/server/wechat-handler.js +439 -0
  61. package/src/workers/agent-worker.js +378 -0
  62. package/src/workers/message-content.js +51 -0
@@ -0,0 +1,1367 @@
1
+ /**
2
+ * VOKO MCP — 24 个工具处理器
3
+ *
4
+ * 零 Electron 依赖,所有外部依赖通过 context (cx) 注入。
5
+ * 全部通过 createToolHandlers(cx) 工厂函数创建。
6
+ */
7
+
8
+ const ENDPOINTS = require('../endpoints.json');
9
+ const { buildInvitationPrompt, buildEmailSubject, buildEmailContent } = require('../core/invitation');
10
+
11
+ /**
12
+ * 归一化文件消息 content。
13
+ * 支持:JSON 字符串 / 普通对象 / 纯 URL 字符串。
14
+ * 纯 URL 会自动提取文件名并补全 {url, name, size, type}。
15
+ */
16
+ function normalizeFileContent(content) {
17
+ // 1. 如果是对象,先序列化
18
+ if (content && typeof content === 'object') {
19
+ return JSON.stringify(content);
20
+ }
21
+ if (typeof content !== 'string') {
22
+ return JSON.stringify({ url: String(content || ''), name: '', size: 0, type: '' });
23
+ }
24
+
25
+ const trimmed = content.trim();
26
+
27
+ // 2. 尝试解析 JSON
28
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
29
+ try {
30
+ const obj = JSON.parse(trimmed);
31
+ const url = obj.url || obj.fileUrl || '';
32
+ const name = obj.name || obj.fileName || extractFileNameFromUrl(url) || '';
33
+ const size = typeof obj.size === 'number' ? obj.size : (typeof obj.fileSize === 'number' ? obj.fileSize : 0);
34
+ const type = obj.type || obj.mimeType || '';
35
+ return JSON.stringify({ url, name, size, type });
36
+ } catch (_) {
37
+ // 解析失败则按普通字符串/URL 处理
38
+ }
39
+ }
40
+
41
+ // 3. 纯 URL 或普通文本:包装成文件 JSON
42
+ const isUrl = /^https?:\/\//i.test(trimmed);
43
+ const url = isUrl ? trimmed : '';
44
+ const name = isUrl ? (extractFileNameFromUrl(trimmed) || '') : trimmed;
45
+ return JSON.stringify({ url, name, size: 0, type: '' });
46
+ }
47
+
48
+ function extractFileNameFromUrl(url) {
49
+ try {
50
+ const pathname = new URL(url).pathname;
51
+ const parts = pathname.split('/');
52
+ const last = parts[parts.length - 1];
53
+ return decodeURIComponent(last) || '';
54
+ } catch (_) {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 通过 HEAD 请求探测远程文件的 size 和 MIME type。
61
+ * 失败时返回 { size: 0, type: '' },不影响发送。
62
+ */
63
+ async function probeFileMetadata(url) {
64
+ try {
65
+ const controller = new AbortController();
66
+ const timeout = setTimeout(() => controller.abort(), 8000);
67
+ const res = await fetch(url, { method: 'HEAD', signal: controller.signal, redirect: 'follow' });
68
+ clearTimeout(timeout);
69
+ if (!res.ok) return { size: 0, type: '' };
70
+ const length = res.headers.get('content-length');
71
+ const contentType = res.headers.get('content-type') || '';
72
+ const size = length ? parseInt(length, 10) : 0;
73
+ return { size: Number.isFinite(size) ? size : 0, type: contentType.split(';')[0].trim() };
74
+ } catch (_) {
75
+ return { size: 0, type: '' };
76
+ }
77
+ }
78
+
79
+ function createToolHandlers(cx) {
80
+ // cx: { db, query, exec, sendMessage, sendSystemMessage, startAgentWorker, stopAgentWorker,
81
+ // getAgentStatus, registerCapabilities, searchCapabilities, updateAgentProfile, setAgentStatus,
82
+ // publishAgent, unpublishAgent,
83
+ // generateOSSSignature, agentRegistration,
84
+ // getPaymentAuth, savePaymentOrder, getAgentImUid,
85
+ // getOpenclawHandler, processPaymentOrder,
86
+ // enqueueOwnerIntervention }
87
+
88
+ /** 格式化消息行 */
89
+ function fmtMsg(r) {
90
+ return {
91
+ id: r.id,
92
+ fromUid: r.from_uid,
93
+ toUid: r.to_uid,
94
+ content: r.content,
95
+ timestamp: r.timestamp,
96
+ messageSeq: r.message_seq,
97
+ isMe: r.is_me === 1,
98
+ contentType: r.content_type || 1,
99
+ agentId: r.agent_id || null,
100
+ };
101
+ }
102
+
103
+ return {
104
+
105
+ // ─── 1 & 2. 注册 ───
106
+
107
+ async register_agent(p) {
108
+ // Step 1:调后端发送邮箱验证码
109
+ const r = await cx.agentRegistration.sendCode({ email: p.email });
110
+ if (!r.success) {
111
+ return { success: false, error: r.error || r.data?.message || '发送验证码失败' };
112
+ }
113
+ return { success: true, message: '验证码已发送到邮箱' };
114
+ },
115
+
116
+ async verify_agent_email(p) {
117
+ // 预览模式:不传 agentId 也不传 agentName,只验码展示 Agent 列表
118
+ if (!p.agentId && !p.agentName) {
119
+ const r = await cx.agentRegistration.verifyCodePreview({ email: p.email, code: p.code });
120
+ if (!r.success) {
121
+ return { success: false, error: r.error || '验证码错误或已过期' };
122
+ }
123
+ const agents = r.agents || [];
124
+ const needChoice = r.userExists && agents.length > 0;
125
+ const result = { success: true, needChoice, userExists: r.userExists };
126
+ if (needChoice) {
127
+ result.agents = agents;
128
+ result.message = '该邮箱有如下Agent,请选择某个Agent或提供新的AgentName注册新Agent';
129
+ } else {
130
+ result.message = '该邮箱尚未注册VOKO,请为该Agent命名';
131
+ }
132
+ return result;
133
+ }
134
+
135
+ // 完整注册:调后端验证验证码
136
+ const v = await cx.agentRegistration.verifyCode({
137
+ email: p.email, code: p.code,
138
+ agentName: p.agentName,
139
+ agentId: p.agentId,
140
+ });
141
+ if (!v.success || !v.data) {
142
+ return { success: false, error: v.error || v.data?.message || '验证码无效或已过期' };
143
+ }
144
+
145
+ const data = v.data;
146
+ console.log('[verify_agent_email] 服务端返回:', JSON.stringify(data, null, 2));
147
+ const agentId = data.agents?.[0]?.agentId;
148
+ if (!agentId) {
149
+ return { success: false, error: '服务端未返回 agentId' };
150
+ }
151
+
152
+ const serverUrl = data.imServerUrl || ENDPOINTS.im.wsUrl;
153
+ const backendType = 'others';
154
+
155
+ const firstAgent = data.agents?.[0] || {};
156
+
157
+ // Step 2:写入 agents 表(注册,unpublished,不启动 worker)
158
+ const regRes = cx.agentRegistration.registerAgentInDb({
159
+ agentId,
160
+ uid: data.imUid,
161
+ token: data.imToken,
162
+ serverUrl,
163
+ ownerEmail: p.email,
164
+ backendType,
165
+ agentName: data.agentName,
166
+ did: data.did,
167
+ publicKey: data.publicKey,
168
+ privateKey: data.privateKey,
169
+ loginToken: data.loginToken,
170
+ paymentFeeRate: firstAgent.payment_fee_rate,
171
+ agentUsageFeeRate: firstAgent.agent_usage_fee_rate,
172
+ });
173
+ if (!regRes.success) {
174
+ return { success: false, error: regRes.error || '写入 agents 表失败' };
175
+ }
176
+
177
+ // Step 3:更新绑定字段
178
+ const upRes = cx.agentRegistration.updateAgentBinding({
179
+ agentId,
180
+ updates: {
181
+ did: data.did,
182
+ imUid: data.imUid,
183
+ imToken: data.imToken,
184
+ public_key: data.publicKey,
185
+ private_key: data.privateKey,
186
+ login_token: data.loginToken,
187
+ im_server_url: serverUrl,
188
+ },
189
+ });
190
+ if (upRes && !upRes.success) {
191
+ return { success: false, error: upRes.error || '更新绑定失败' };
192
+ }
193
+
194
+ // Step 4:启动 IM Worker
195
+ if (cx.startAgentWorker) {
196
+ cx.startAgentWorker(agentId, { uid: data.imUid, token: data.imToken, serverUrl, backendType: 'others' });
197
+ }
198
+
199
+ return { success: true, message: '注册成功', agentId, agentName: data.agentName };
200
+ },
201
+
202
+ // ─── 3. 编辑 Agent 基础信息 ───
203
+
204
+ async update_agent_profile(p) {
205
+ if (!cx.updateAgentProfile) {
206
+ return { success: false, error: '当前环境不支持更新 Agent 资料' };
207
+ }
208
+ // backendType 仅本地更新,不同步服务端
209
+ if (p.backendType) {
210
+ cx.exec(`UPDATE agents SET backend_type=?, updated_at=? WHERE agent_id=?`, [p.backendType, Date.now(), p.agentId]);
211
+ }
212
+
213
+ // 检查是否有需要同步服务端的字段
214
+ const serverFields = [p.name, p.description, p.short_description, p.category, p.tags, p.iconUrl, p.address, p.contact_phone];
215
+ if (serverFields.some(v => v !== undefined)) {
216
+ const result = await cx.updateAgentProfile({
217
+ db: cx.db,
218
+ agentId: p.agentId,
219
+ name: p.name,
220
+ description: p.description,
221
+ short_description: p.short_description,
222
+ category: p.category,
223
+ tags: p.tags,
224
+ icon_url: p.iconUrl,
225
+ address: p.address,
226
+ contact_phone: p.contact_phone,
227
+ });
228
+ return result;
229
+ }
230
+
231
+ return { success: true, message: '本地更新成功' };
232
+ },
233
+
234
+ // ─── 4. 设置 Agent 上下架 / 公开私有状态 ───
235
+
236
+ async set_agent_status(p) {
237
+ if (p.status === 1) {
238
+ if (!cx.publishAgent) {
239
+ return { success: false, error: '当前环境不支持发布 Agent' };
240
+ }
241
+ return cx.publishAgent({ agentId: p.agentId });
242
+ }
243
+ if (p.status === 0) {
244
+ if (!cx.unpublishAgent) {
245
+ return { success: false, error: '当前环境不支持下架 Agent' };
246
+ }
247
+ return cx.unpublishAgent({ agentId: p.agentId });
248
+ }
249
+ return { success: false, error: 'status 必须为 0(下架)或 1(上架)' };
250
+ },
251
+
252
+ // ─── 5. 状态 ───
253
+
254
+ async get_status(p) {
255
+ const agentStatus = cx.getAgentStatus ? cx.getAgentStatus(p.agentId) : null;
256
+ const warnings = global.__latestWarnings || [];
257
+ const uptime = process.uptime();
258
+ const version = cx.version || 'unknown';
259
+ // others 类型的 agent 无后端连接检测,去掉 backendConnected
260
+ if (agentStatus && agentStatus.backend_type !== 'openclaw' && agentStatus.backend_type !== 'hermes') {
261
+ delete agentStatus.backendConnected;
262
+ }
263
+ return { agent: agentStatus, warnings, uptime, version };
264
+ },
265
+
266
+ // ─── 5b. 查询 Agent 资料 ───
267
+
268
+ async get_agent_profile(p) {
269
+ const row = cx.query(`SELECT * FROM agents WHERE agent_id=?`, [p.agentId]);
270
+ if (!row || row.length === 0) {
271
+ return { success: false, error: 'Agent 不存在' };
272
+ }
273
+ const a = row[0];
274
+ let tags = null;
275
+ try { if (a.tags) tags = JSON.parse(a.tags); } catch { tags = a.tags; }
276
+ let ability = null;
277
+ try { if (a.ability) ability = JSON.parse(a.ability); } catch { ability = a.ability; }
278
+
279
+ // 计费模式
280
+ let pricing = { pricingModel: 'free', price: null, durationMinutes: null, enabled: true };
281
+ try {
282
+ const pricingRow = cx.query(`SELECT pricing_model, price, duration_minutes, enabled FROM agent_pricing WHERE agent_id=?`, [p.agentId])[0];
283
+ if (pricingRow) pricing = { pricingModel: pricingRow.pricing_model, price: pricingRow.price, durationMinutes: pricingRow.duration_minutes, enabled: pricingRow.enabled === 1 };
284
+ } catch (_) {}
285
+
286
+ // 支付认证(是否已配置)
287
+ let paymentAuthId = null;
288
+ let paymentConfigured = false;
289
+ try {
290
+ const auth = cx.query(`SELECT id FROM payment_auth WHERE id=?`, [a.payment_auth_id])[0];
291
+ paymentAuthId = a.payment_auth_id || null;
292
+ paymentConfigured = !!auth;
293
+ } catch (_) {}
294
+
295
+ return {
296
+ success: true,
297
+ data: {
298
+ agentId: a.agent_id,
299
+ agentName: a.agent_name,
300
+ description: a.description,
301
+ shortDescription: a.short_description,
302
+ category: a.category,
303
+ categoryLabel: a.category_label,
304
+ tags,
305
+ iconUrl: a.icon_url,
306
+ address: a.address,
307
+ contactPhone: a.contact_phone,
308
+ backendType: a.backend_type,
309
+ publishStatus: a.publish_status,
310
+ accessMode: a.access_mode,
311
+ did: a.did,
312
+ imUid: a.imUid,
313
+ ownerEmail: a.owner_email,
314
+ createdAt: a.created_at,
315
+ updatedAt: a.updated_at,
316
+ ability,
317
+ paymentFeeRate: a.payment_fee_rate,
318
+ agentUsageFeeRate: a.agent_usage_fee_rate,
319
+ paymentConfigured,
320
+ pricing,
321
+ },
322
+ fieldDescriptions: {
323
+ agentId: 'Agent 唯一标识',
324
+ agentName: 'Agent 显示名称(可修改)',
325
+ description: 'Agent详细描述',
326
+ shortDescription: 'Agent一句话简短介绍',
327
+ category: '分类标识(如 travel/finance/health_fitness/education 等)',
328
+ categoryLabel: '分类中文名称',
329
+ tags: '标签列表',
330
+ iconUrl: '头像图片链接',
331
+ backendType: 'Agent 类型(openclaw, hermes, others=其他)',
332
+ publishStatus: '发布状态(published=已上架, unpublished=未上架)',
333
+ paymentFeeRate: '支付手续费率(如 0.006 = 0.6%)',
334
+ agentUsageFeeRate: '按时计费模式下,平台抽取的佣金比例',
335
+ paymentConfigured: '是否已配置支付认证',
336
+ pricing: '订阅方式:pricingModel=free免费/timed按时订阅,price=价格,durationMinutes=时长(分钟),enabled=是否启用',
337
+ accessMode: '访问模式(public=公开, private=私密/白名单)',
338
+ address: '地址',
339
+ contactPhone: '联系电话',
340
+ did: 'DID标识',
341
+ imUid: 'IM 系统用户 ID',
342
+ ownerEmail: '主人邮箱',
343
+ createdAt: '注册时间(毫秒时间戳)',
344
+ updatedAt: '最后更新时间(毫秒时间戳)',
345
+ ability: '普通模式能力列表(JSON 数组)',
346
+ },
347
+ };
348
+ },
349
+
350
+ // ─── 6. 能力发现 ───
351
+
352
+ async search_capabilities(p) {
353
+ return cx.searchCapabilities ? await cx.searchCapabilities(p) : { success: false, error: '未实现' };
354
+ },
355
+
356
+ async declare_capabilities(p) {
357
+ const abilities = p.ability;
358
+
359
+ if (!Array.isArray(abilities)) {
360
+ return { success: false, error: 'ability 必须为数组格式,如 [{"name":"...","fields":[...]}]' };
361
+ }
362
+
363
+ // 读取旧值,用于服务端同步失败时回滚
364
+ const oldRow = cx.query(`SELECT ability FROM agents WHERE agent_id=?`, [p.agentId]);
365
+ const oldValue = oldRow?.[0]?.ability || null;
366
+
367
+ const jsonStr = JSON.stringify(abilities);
368
+ cx.exec(`UPDATE agents SET ability=?, updated_at=? WHERE agent_id=?`, [jsonStr, Date.now(), p.agentId]);
369
+
370
+ if (cx.registerCapabilities) {
371
+ const result = await cx.registerCapabilities(p.agentId);
372
+ if (!result.success) {
373
+ // 服务端同步失败,回滚本地 DB
374
+ cx.exec(`UPDATE agents SET ability=?, updated_at=? WHERE agent_id=?`, [oldValue, Date.now(), p.agentId]);
375
+ }
376
+ return result;
377
+ }
378
+ return { success: true };
379
+ },
380
+
381
+ // ─── 8. 消息 ───
382
+
383
+ async send_message(p) {
384
+ const fromUid = cx.wukongim?.getCurrentUid?.(p.agentId) || 'voko';
385
+ // 将数字 contentType 映射为字符串 messageType:1=text 2=image 3=file(内部统一按桌面端 4=file 处理)
386
+ let messageType = 'text';
387
+ if (p.contentType === 2 || p.contentType === '2' || p.contentType === 'image') messageType = 'image';
388
+ else if (p.contentType === 3 || p.contentType === '3' || p.contentType === 'file') messageType = 'file';
389
+ else if (typeof p.contentType === 'string') messageType = p.contentType;
390
+
391
+ // 文件消息 content 归一化:支持 JSON 字符串 / 对象 / 纯 URL
392
+ let content = p.content;
393
+ if (messageType === 'file') {
394
+ content = normalizeFileContent(content);
395
+ // 如果 size 为 0,尝试 HEAD 探测远程文件大小和 MIME type
396
+ try {
397
+ const payload = JSON.parse(content);
398
+ if (!payload.size && payload.url) {
399
+ const meta = await probeFileMetadata(payload.url);
400
+ if (meta.size) {
401
+ payload.size = meta.size;
402
+ if (meta.type) payload.type = meta.type;
403
+ content = JSON.stringify(payload);
404
+ }
405
+ }
406
+ } catch (_) {
407
+ // 非 JSON 时不处理,直接发送
408
+ }
409
+ }
410
+
411
+ const result = cx.sendMessage(p.agentId, p.toUid, content, fromUid, messageType);
412
+ // 检测收消息通道是否畅通
413
+ if (cx.checkReceiveChannel) {
414
+ const ch = cx.checkReceiveChannel(p.agentId);
415
+ if (ch && !ch.ok) {
416
+ result.receiveChannel = { ok: false, channel: ch.channel, suggest: 'voko_fetch_new_messages' };
417
+ }
418
+ }
419
+ return result;
420
+ },
421
+
422
+ // ─── 9. 聊天历史 ───
423
+
424
+ async get_chat_history(p) {
425
+ const limit = Math.min(p.limit || 20, 200);
426
+ const offset = p.offset || 0;
427
+ let sql = `SELECT * FROM messages WHERE channel_id=? AND agent_id=?`;
428
+ const params = [p.channelId, p.agentId];
429
+ if (p.keyword) { sql += ` AND content LIKE ?`; params.push(`%${p.keyword}%`); }
430
+ sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
431
+ params.push(limit + 1, offset);
432
+ const rows = cx.query(sql, params);
433
+ const hasMore = rows.length > limit;
434
+ if (hasMore) rows.pop();
435
+ return { messages: rows.map(fmtMsg), hasMore, count: rows.length, offset };
436
+ },
437
+
438
+ // ─── 10. 访客信息 ───
439
+
440
+ async get_visitor_profile(p) {
441
+ const { visitorId, agentId } = p;
442
+ const msgLimit = p.limit || 10;
443
+ const msgOffset = p.offset || 0;
444
+ const cache = cx.query(`SELECT uid, nickname, avatar_url FROM user_cache WHERE uid=? LIMIT 1`, [visitorId]);
445
+ const profile = cache && cache[0] ? cache[0] : { uid: visitorId };
446
+
447
+ // 消息统计
448
+ const msgStats = cx.query(`SELECT COUNT(*) as total, MIN(timestamp) as firstAt, MAX(timestamp) as lastAt FROM messages WHERE from_uid=?`, [visitorId]);
449
+ const totalMessages = msgStats[0]?.total || 0;
450
+ const firstMessageAt = msgStats[0]?.firstAt || null;
451
+ const lastMessageAt = msgStats[0]?.lastAt || null;
452
+
453
+ // 黑白名单状态(需要 agentId)
454
+ let isWhitelisted = false, isBlacklisted = false;
455
+ if (agentId) {
456
+ isWhitelisted = !!cx.query(`SELECT 1 FROM agent_access_lists WHERE agent_id=? AND list_type='whitelist' AND visitor_id=?`, [agentId, visitorId]).length;
457
+ isBlacklisted = !!cx.query(`SELECT 1 FROM agent_access_lists WHERE agent_id=? AND list_type='blacklist' AND visitor_id=?`, [agentId, visitorId]).length;
458
+ }
459
+
460
+ // 最近对话(可配置条数、可翻页)
461
+ const recentSql = agentId
462
+ ? `SELECT content, timestamp, is_me FROM messages WHERE channel_id=? AND agent_id=? AND content_type!=11 ORDER BY timestamp DESC LIMIT ? OFFSET ?`
463
+ : `SELECT content, timestamp, is_me FROM messages WHERE channel_id=? AND content_type!=11 ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
464
+ const recentMulti = msgLimit + 1; // 多取 1 条用来算 hasMore
465
+ const recentParams = agentId ? [visitorId, agentId, recentMulti, msgOffset] : [visitorId, recentMulti, msgOffset];
466
+ const recentRows = cx.query(recentSql, recentParams);
467
+ const hasMore = recentRows.length > msgLimit;
468
+ if (hasMore) recentRows.pop();
469
+ const recentMessages = recentRows.reverse().map(r => ({
470
+ content: r.content,
471
+ timestamp: r.timestamp,
472
+ isMe: r.is_me === 1,
473
+ }));
474
+
475
+ // 入站审核统计(只算访客触发的拦截,按 agent 隔离)
476
+ const auditSql = agentId
477
+ ? `SELECT is_me, content, timestamp FROM messages WHERE from_uid=? AND agent_id=? AND content_type=11 ORDER BY timestamp DESC LIMIT 50`
478
+ : `SELECT is_me, content, timestamp FROM messages WHERE from_uid=? AND content_type=11 ORDER BY timestamp DESC LIMIT 50`;
479
+ const auditParams = agentId ? [visitorId, agentId] : [visitorId];
480
+ const auditRows = cx.query(auditSql, auditParams);
481
+ let audit = { totalHits: 0, hardDenyCount: 0, softDenyCount: 0, lastHitAt: null, lastKeyword: null };
482
+ for (const r of auditRows) {
483
+ if (r.is_me !== 0) continue; // 只计入站
484
+ audit.totalHits++;
485
+ try {
486
+ const parsed = JSON.parse(r.content);
487
+ if (parsed.action === 'hard_deny') audit.hardDenyCount++;
488
+ else if (parsed.action === 'soft_deny') audit.softDenyCount++;
489
+ if (!audit.lastHitAt || r.timestamp > audit.lastHitAt) {
490
+ audit.lastHitAt = r.timestamp;
491
+ audit.lastKeyword = parsed.keyword || null;
492
+ }
493
+ } catch (_) {}
494
+ }
495
+
496
+ // 支付记录
497
+ const paidRows = cx.query(`SELECT 1 FROM payment_orders WHERE visitor_id=? AND status='paid' LIMIT 1`, [visitorId]);
498
+ const hasPaid = paidRows.length > 0;
499
+
500
+ return {
501
+ visitorId,
502
+ nickname: profile.nickname || null,
503
+ avatarUrl: profile.avatar_url || null,
504
+ totalMessages,
505
+ firstMessageAt,
506
+ lastMessageAt,
507
+ isWhitelisted,
508
+ isBlacklisted,
509
+ recentMessages,
510
+ hasMore,
511
+ audit,
512
+ hasPaid,
513
+ };
514
+ },
515
+
516
+ // ─── 11. 会话列表 ───
517
+
518
+ async list_conversations(p) {
519
+ const limit = Math.min(p.limit || 20, 100);
520
+ const filter = p.filter || 'unreplied';
521
+ const rows = cx.query(`SELECT * FROM conversations WHERE agent_id=? ORDER BY last_timestamp DESC LIMIT ?`, [p.agentId, limit]);
522
+ return {
523
+ conversations: rows.map(r => {
524
+ // 查最后一条消息是谁发的(排除审核拦截消息)
525
+ const lastMsg = cx.query(`SELECT is_me FROM messages WHERE channel_id=? AND agent_id=? AND content_type!=11 ORDER BY timestamp DESC LIMIT 1`, [r.channel_id, p.agentId]);
526
+ const needsReply = lastMsg?.[0]?.is_me !== 1;
527
+ // 计算未回复的访客消息数:最后一条 agent 回复之后,还有多少条访客消息
528
+ let unreadCount = 0;
529
+ if (needsReply) {
530
+ const lastAgentReply = cx.query(`SELECT timestamp FROM messages WHERE channel_id=? AND agent_id=? AND is_me=1 AND content_type!=11 ORDER BY timestamp DESC LIMIT 1`, [r.channel_id, p.agentId]);
531
+ const since = lastAgentReply?.[0]?.timestamp || 0;
532
+ unreadCount = cx.query(`SELECT COUNT(*) as cnt FROM messages WHERE channel_id=? AND agent_id=? AND is_me=0 AND timestamp > ?`, [r.channel_id, p.agentId, since])[0]?.cnt || 0;
533
+ }
534
+ return {
535
+ channelId: r.channel_id,
536
+ name: r.name,
537
+ lastMessage: r.last_message,
538
+ lastTimestamp: r.last_timestamp,
539
+ unreadCount,
540
+ needsReply,
541
+ };
542
+ })
543
+ .filter(c => filter === 'all' || c.needsReply),
544
+ };
545
+ },
546
+
547
+ // ─── 12. 文件上传 ───
548
+
549
+ async get_upload_url(p) {
550
+ if (!cx.uploadFileToOSS) {
551
+ return { success: false, error: 'OSS 未配置' };
552
+ }
553
+ try {
554
+ const fs = require('fs');
555
+ if (!p.filePath) return { success: false, error: '缺少 filePath' };
556
+ if (!fs.existsSync(p.filePath)) return { success: false, error: '文件不存在: ' + p.filePath };
557
+ const stat = fs.statSync(p.filePath);
558
+ const fileName = p.fileName || require('path').basename(p.filePath);
559
+ const ext = require('path').extname(fileName).toLowerCase();
560
+ const mimeMap = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.mp4': 'video/mp4', '.pdf': 'application/pdf', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.zip': 'application/zip', '.mp3': 'audio/mpeg', '.txt': 'text/plain', '.json': 'application/json' };
561
+ const mimeType = p.contentType || mimeMap[ext] || 'application/octet-stream';
562
+ const safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
563
+ // 图片上传到 chat/images/,其他文件上传到 chat/files/,与桌面端保持一致
564
+ const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
565
+ const dir = imageExts.includes(ext) ? 'chat/images' : 'chat/files';
566
+ const objectName = `${dir}/${safeName}`;
567
+ const url = await cx.uploadFileToOSS(p.filePath, objectName, mimeType);
568
+ return {
569
+ success: true,
570
+ url,
571
+ fileName,
572
+ fileSize: stat.size,
573
+ mimeType,
574
+ };
575
+ } catch (e) {
576
+ return { success: false, error: '上传失败: ' + e.message };
577
+ }
578
+ },
579
+
580
+ // ─── 13. whoami ───
581
+
582
+ async whoami(p) {
583
+ let sql = `SELECT agent_id AS agent_id, agent_name, description, short_description, category, backend_type, publish_status, access_mode, owner_email, created_at FROM agents`;
584
+ const params = [];
585
+ if (p.ownerEmail) {
586
+ sql += ` WHERE owner_email=?`;
587
+ params.push(p.ownerEmail);
588
+ }
589
+ sql += ` ORDER BY created_at ASC`;
590
+ const rows = cx.query(sql, params);
591
+ return {
592
+ agents: rows.map(r => ({
593
+ agentId: r.agent_id,
594
+ agentName: r.agent_name,
595
+ description: r.description,
596
+ shortDescription: r.short_description,
597
+ category: r.category,
598
+ backendType: r.backend_type,
599
+ publishStatus: r.publish_status,
600
+ accessMode: r.access_mode,
601
+ ownerEmail: r.owner_email,
602
+ createdAt: r.created_at,
603
+ })),
604
+ };
605
+ },
606
+
607
+ // ─── 14-16. 人工介入 ───
608
+
609
+ async ask_human_for_help(p) {
610
+ const now = Date.now();
611
+ const id = `mcp_${now}_${Math.random().toString(36).substr(2, 6)}`;
612
+ const channelType = cx.getEnabledChannel?.()?.name || null;
613
+ // 根据 backend 类型决定 session_key 前缀
614
+ const backendRow = cx.query ? cx.query(`SELECT backend_type FROM agents WHERE agent_id=?`, [p.agentId]) : [];
615
+ const backendType = backendRow?.[0]?.backend_type || 'openclaw';
616
+ const prefix = backendType === 'hermes' ? 'hermes' : 'agent';
617
+ cx.exec(`
618
+ INSERT INTO owner_interventions (id, agent_id, visitor_id, session_key, problem, agent_suggestion, ask_time, status, channel_type, created_at, updated_at)
619
+ VALUES (?,?,?,?,?,?,?,'pending',?,?,?)
620
+ `, [id, p.agentId, p.visitorId, `${prefix}:${p.agentId}:${p.visitorId}`, p.problem, p.suggestion || null, now, channelType, now, now]);
621
+ // 事件驱动:立即通知主人,不等轮询
622
+ if (cx.enqueueOwnerIntervention) {
623
+ cx.enqueueOwnerIntervention({
624
+ id, visitorId: p.visitorId, agentId: p.agentId,
625
+ sessionKey: `agent:${p.agentId}:${p.visitorId}`,
626
+ problem: p.problem, agentSuggestion: p.suggestion || '',
627
+ askTime: now, skipReply: 0,
628
+ });
629
+ }
630
+ return { success: true, interventionId: id };
631
+ },
632
+
633
+ async check_human_replies(p) {
634
+ // 按 id 查单条
635
+ if (p.id) {
636
+ const r = cx.query(`SELECT * FROM owner_interventions WHERE id=?`, [p.id])[0];
637
+ return {
638
+ interventions: r ? [{
639
+ id: r.id,
640
+ visitorId: r.visitor_id,
641
+ problem: r.problem,
642
+ suggestion: r.agent_suggestion,
643
+ askTime: r.ask_time,
644
+ ownerReply: r.owner_reply,
645
+ replyTime: r.reply_time,
646
+ status: r.status,
647
+ }] : [],
648
+ hasMore: false,
649
+ };
650
+ }
651
+
652
+ const limit = Math.min(p.limit || 20, 50);
653
+ const offset = p.offset || 0;
654
+ const multi = limit + 1;
655
+
656
+ // 自动游标:不传 since 时自动记录上次查询的最大 askTime
657
+ const cursorKey = `check_human_replies:${p.agentId}`;
658
+ let since = p.since;
659
+ if (!since && this._checkReplyCursor) {
660
+ since = this._checkReplyCursor.get(cursorKey);
661
+ }
662
+
663
+ // 构造 SQL
664
+ const conditions = [`agent_id=?`, `status!='resolved'`];
665
+ const params = [p.agentId];
666
+ if (p.visitorId) {
667
+ conditions.push(`visitor_id=?`);
668
+ params.push(p.visitorId);
669
+ }
670
+ if (since) {
671
+ conditions.push(`ask_time>?`);
672
+ params.push(since);
673
+ }
674
+ params.push(multi, offset);
675
+
676
+ const rows = cx.query(
677
+ `SELECT * FROM owner_interventions WHERE ${conditions.join(' AND ')} ORDER BY ask_time DESC LIMIT ? OFFSET ?`,
678
+ params
679
+ );
680
+
681
+ // 更新游标
682
+ if (!this._checkReplyCursor) this._checkReplyCursor = new Map();
683
+ if (!p.since && rows.length > 0) {
684
+ const maxTime = Math.max(...rows.map(r => r.ask_time));
685
+ this._checkReplyCursor.set(cursorKey, maxTime);
686
+ }
687
+ const hasMore = rows.length > limit;
688
+ if (hasMore) rows.pop();
689
+ return {
690
+ interventions: rows.map(r => ({
691
+ id: r.id,
692
+ visitorId: r.visitor_id,
693
+ problem: r.problem,
694
+ suggestion: r.agent_suggestion,
695
+ askTime: r.ask_time,
696
+ ownerReply: r.owner_reply,
697
+ replyTime: r.reply_time,
698
+ status: r.status,
699
+ })),
700
+ hasMore,
701
+ };
702
+ },
703
+
704
+ async close_human_request(p) {
705
+ cx.exec(`UPDATE owner_interventions SET status='resolved', resolved_at=?, updated_at=? WHERE id=?`, [Date.now(), Date.now(), p.id]);
706
+ return { success: true };
707
+ },
708
+
709
+ // ─── 16. 收款 ───
710
+
711
+ async request_payment(p) {
712
+ const hasAuth = cx.getPaymentAuth ? cx.getPaymentAuth(p.agentId) : null;
713
+ if (!hasAuth) return { success: false, error: '该 Agent 未配置支付认证,请通知主人配置' };
714
+ // 前置检查:Agent 必须已注册 DID 和私钥,否则后续无法完成 DID 签名
715
+ const agentKey = cx.query ? cx.query(`SELECT did, private_key FROM agents WHERE agent_id=? AND did IS NOT NULL AND private_key IS NOT NULL`, [p.agentId]) : [];
716
+ if (!agentKey || agentKey.length === 0) {
717
+ return { success: false, error: '该 Agent 未注册 DID 或未配置私钥,无法创建支付订单,请通知主人配置' };
718
+ }
719
+ const now = Date.now();
720
+ const orderId = `po_${now}_${Math.random().toString(36).substr(2, 8)}`;
721
+ const fromUid = cx.getAgentImUid ? cx.getAgentImUid(p.agentId) : '';
722
+ cx.savePaymentOrder({ id: orderId, agent_id: p.agentId, visitor_id: p.visitorId, from_uid: fromUid, amount: p.amount, description: p.description || '', type: 'service', status: 'pending', created_at: now, updated_at: now });
723
+ // 事件驱动:立即处理 pending 订单(DID 签名 → 调支付 API → 生成二维码 → 通知访客)
724
+ if (cx.processPaymentOrder) {
725
+ cx.processPaymentOrder({ id: orderId, agent_id: p.agentId, visitor_id: p.visitorId, from_uid: fromUid, amount: p.amount, description: p.description || '' }).catch(err => console.error('[MCP] 处理支付订单失败:', err.message));
726
+ }
727
+ return { success: true, orderId };
728
+ },
729
+
730
+ // ─── 18. 查询支付 ───
731
+
732
+ async check_payments(p) {
733
+ // 按 orderId 查单条
734
+ if (p.orderId) {
735
+ const r = cx.query(`SELECT id, agent_id, visitor_id, amount, description, order_no, status, created_at, updated_at FROM payment_orders WHERE id=?`, [p.orderId])[0];
736
+ return {
737
+ orders: r ? [{
738
+ orderId: r.id,
739
+ visitorId: r.visitor_id,
740
+ amount: r.amount,
741
+ description: r.description,
742
+ orderNo: r.order_no,
743
+ status: r.status,
744
+ createdAt: r.created_at,
745
+ updatedAt: r.updated_at,
746
+ }] : [],
747
+ hasMore: false,
748
+ };
749
+ }
750
+
751
+ const limit = Math.min(p.limit || 20, 50);
752
+ const offset = p.offset || 0;
753
+ const multi = limit + 1;
754
+
755
+ // 自动游标
756
+ const cursorKey = `check_payments:${p.agentId}`;
757
+ let since = p.since;
758
+ if (!since && this._checkPaymentCursor) {
759
+ since = this._checkPaymentCursor.get(cursorKey);
760
+ }
761
+
762
+ const conditions = [`agent_id=?`];
763
+ const params = [p.agentId];
764
+ if (p.visitorId) { conditions.push(`visitor_id=?`); params.push(p.visitorId); }
765
+ if (p.status) { conditions.push(`status=?`); params.push(p.status); }
766
+ if (since) { conditions.push(`created_at>?`); params.push(since); }
767
+ params.push(multi, offset);
768
+
769
+ const rows = cx.query(
770
+ `SELECT id, agent_id, visitor_id, amount, description, order_no, status, created_at, updated_at FROM payment_orders WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
771
+ params
772
+ );
773
+
774
+ // 更新游标
775
+ if (!this._checkPaymentCursor) this._checkPaymentCursor = new Map();
776
+ if (!p.since && rows.length > 0) {
777
+ const maxTime = Math.max(...rows.map(r => r.created_at));
778
+ this._checkPaymentCursor.set(cursorKey, maxTime);
779
+ }
780
+
781
+ const hasMore = rows.length > limit;
782
+ if (hasMore) rows.pop();
783
+ return {
784
+ orders: rows.map(r => ({
785
+ orderId: r.id,
786
+ visitorId: r.visitor_id,
787
+ amount: r.amount,
788
+ description: r.description,
789
+ orderNo: r.order_no,
790
+ status: r.status,
791
+ createdAt: r.created_at,
792
+ updatedAt: r.updated_at,
793
+ })),
794
+ hasMore,
795
+ };
796
+ },
797
+
798
+ // ─── 18b. 支付认证(入账银行卡)───
799
+
800
+ async add_payment_auth(p) {
801
+ const { name, idCard, bankCard, phone, bankCode, bankName } = p;
802
+ const now = Date.now();
803
+
804
+ if (!bankCode) return { success: false, error: 'bankCode 不能为空,请先通过 voko_search_banks 选择银行' };
805
+ if (!bankCard) return { success: false, error: 'bankCard 不能为空' };
806
+ const cleanBankCard = String(bankCard).replace(/\s/g, '');
807
+ if (!/^\d{13,19}$/.test(cleanBankCard)) return { success: false, error: '银行卡号格式不正确,应为13-19位数字' };
808
+ if (!phone || !/^1\d{10}$/.test(String(phone).trim())) return { success: false, error: '手机号格式不正确' };
809
+ if (!name) return { success: false, error: '姓名不能为空' };
810
+ if (!idCard) return { success: false, error: '身份证号不能为空' };
811
+ if (!/^\d{17}[\dXx]$/.test(String(idCard).trim())) return { success: false, error: '身份证号格式不正确,应为18位' };
812
+
813
+ const newId = 'pid_' + now + '_' + Math.random().toString(36).substr(2, 6);
814
+ cx.exec(`INSERT INTO payment_auth (id, name, id_card, bank_card, phone, receiver_type, bank_code, bank_name, company_name, unified_social_credit_code, legal_name, legal_licence_no, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
815
+ [newId, name, idCard || '', cleanBankCard || '', phone || '', 1, bankCode || '', bankName || '', '', '', '', '', '未认证', now, now]);
816
+ return { success: true, id: newId };
817
+ },
818
+
819
+ // ─── 18c. 查看入账银行卡列表 ───
820
+
821
+ async list_payment_auth(p) {
822
+ const keyword = (p.keyword || '').trim();
823
+ let sql = `SELECT * FROM payment_auth ORDER BY updated_at DESC`;
824
+ let params = [];
825
+ if (keyword) {
826
+ const kw = `%${keyword}%`;
827
+ sql = `SELECT * FROM payment_auth WHERE name LIKE ? OR bank_card LIKE ? OR phone LIKE ? ORDER BY updated_at DESC`;
828
+ params = [kw, kw, kw];
829
+ }
830
+ const rows = cx.query(sql, params);
831
+ const receiverStatusLabel = {
832
+ 'none': '未申请', PROCESSING: '申请中', AGREEMENT_SIGNING: '待签署',
833
+ COMPLETED: '已完成', APPLY_REJECTED: '已拒绝'
834
+ };
835
+ const masked = rows.map(r => ({
836
+ id: r.id,
837
+ name: r.name,
838
+ nameMask: r.name ? r.name[0] + '*'.repeat(r.name.length - 1) : '',
839
+ idCardMask: r.id_card ? r.id_card.substring(0, 4) + '**********' : '',
840
+ bankCardMask: r.bank_card ? r.bank_card.substring(0, 4) + '****' + r.bank_card.slice(-4) : '',
841
+ phoneMask: r.phone ? r.phone.substring(0, 3) + '****' + r.phone.slice(-4) : '',
842
+ receiverType: r.receiver_type,
843
+ receiverTypeLabel: r.receiver_type === 2 ? '对公' : '对私',
844
+ receiverApplyStatus: r.receiver_apply_status,
845
+ receiverApplyStatusLabel: receiverStatusLabel[r.receiver_apply_status] || r.status || '未知',
846
+ bankCode: r.bank_code,
847
+ bankName: r.bank_name,
848
+ companyName: r.company_name,
849
+ status: r.status,
850
+ createdAt: r.created_at,
851
+ updatedAt: r.updated_at,
852
+ }));
853
+ return { success: true, data: masked };
854
+ },
855
+
856
+ // ─── 18d. 删除入账银行卡 ───
857
+
858
+ async delete_payment_auth(p) {
859
+ if (!p.id) return { success: false, error: '缺少 id' };
860
+ // 若该银行卡已被 Agent 绑定,先解除绑定
861
+ cx.exec(`UPDATE agents SET payment_auth_id = NULL, updated_at = ? WHERE payment_auth_id = ?`, [Date.now(), p.id]);
862
+ cx.exec(`DELETE FROM payment_auth WHERE id = ?`, [p.id]);
863
+ return { success: true };
864
+ },
865
+
866
+ // ─── 18e. 申请认证 ───
867
+
868
+ async apply_payment_auth(p) {
869
+ const { paymentAuthId, email: explicitEmail } = p;
870
+ if (!paymentAuthId) return { success: false, error: '缺少 paymentAuthId' };
871
+
872
+ const auth = cx.query(`SELECT * FROM payment_auth WHERE id = ?`, [paymentAuthId])[0];
873
+ if (!auth) return { success: false, error: '未找到支付认证信息' };
874
+
875
+ let email = explicitEmail ? String(explicitEmail).trim().toLowerCase() : '';
876
+ if (!email) {
877
+ const boundAgent = cx.query(`SELECT owner_email FROM agents WHERE payment_auth_id = ? AND owner_email IS NOT NULL LIMIT 1`, [paymentAuthId])[0];
878
+ email = boundAgent?.owner_email || '';
879
+ }
880
+ if (!email) {
881
+ const anyAgent = cx.query(`SELECT owner_email FROM agents WHERE owner_email IS NOT NULL LIMIT 1`)[0];
882
+ email = anyAgent?.owner_email || '';
883
+ }
884
+ if (!email) return { success: false, error: '无法获取用户邮箱,请先通过邮箱验证码登录/注册,或显式传入 email' };
885
+
886
+ if (!cx.getUserAccessToken || !cx.VOKO_API_URL) {
887
+ return { success: false, error: 'MCP 上下文未提供 getUserAccessToken 或 VOKO_API_URL' };
888
+ }
889
+ const userToken = cx.getUserAccessToken(email);
890
+ if (!userToken) return { success: false, error: '缺少 User Access Token,请先通过邮箱验证码登录/注册以获取' };
891
+
892
+ const type = auth.receiver_type || 1;
893
+ const body = {
894
+ email,
895
+ type,
896
+ receiverName: type === 2 ? (auth.company_name || auth.name) : auth.name,
897
+ licenceNo: type === 2 ? (auth.unified_social_credit_code || auth.id_card) : auth.id_card,
898
+ bankCardNo: auth.bank_card,
899
+ bankCode: auth.bank_code || '',
900
+ mobile: auth.phone,
901
+ };
902
+ if (type === 2) {
903
+ if (auth.legal_name) body.legalName = auth.legal_name;
904
+ if (auth.legal_licence_no) body.legalLicenceNo = auth.legal_licence_no;
905
+ }
906
+
907
+ const resp = await fetch(`${cx.VOKO_API_URL}/api/external/v1/payment/receiver/apply`, {
908
+ method: 'POST',
909
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userToken}` },
910
+ body: JSON.stringify(body)
911
+ });
912
+ const result = await resp.json();
913
+ const data = result.data || {};
914
+ const failMsg = result.msg || result.message || '';
915
+
916
+ if (result.code !== 200) return { success: false, error: failMsg || '申请认证失败', code: result.code, data };
917
+
918
+ const now = Date.now();
919
+ let applyStatus = data.receiverApplyStatus || 'PROCESSING';
920
+ if (data.alreadyRegistered) applyStatus = 'COMPLETED';
921
+ let statusUpdate = auth.status || '未认证';
922
+ const upstreamMsg = data.upstreamMsg || data.hint || failMsg || '';
923
+ if (applyStatus === 'APPLY_REJECTED' && upstreamMsg) statusUpdate = '拒绝: ' + upstreamMsg.substring(0, 100);
924
+
925
+ cx.exec(`UPDATE payment_auth SET request_no = ?, receiver_no = ?, receiver_apply_status = ?, receiver_sign_status = ?, receiver_sign_url = ?, merchant_sign_url = ?, payment_user_uid = ?, status = ?, updated_at = ? WHERE id = ?`,
926
+ [data.requestNo || '', data.receiverNo || '', applyStatus, data.receiverSignStatus || '', data.receiverSignUrl || '', data.merchantSignUrl || '', data.paymentUserUid || '', statusUpdate, now, paymentAuthId]);
927
+ return { success: true, data };
928
+ },
929
+
930
+ // ─── 18f. 搜索银行总行 ───
931
+
932
+ async search_banks(p) {
933
+ const keyword = (p.keyword || '').trim();
934
+ let rows;
935
+ if (!keyword) {
936
+ rows = cx.query(`SELECT * FROM bank_head_offices ORDER BY id LIMIT 50`);
937
+ } else {
938
+ const kw = `%${keyword}%`;
939
+ rows = cx.query(`SELECT * FROM bank_head_offices WHERE code LIKE ? OR name LIKE ? OR short_name LIKE ? ORDER BY id LIMIT 50`, [kw, kw, kw]);
940
+ }
941
+ return { success: true, data: rows.map(r => ({ code: r.code, name: r.name, shortName: r.short_name })) };
942
+ },
943
+
944
+ // ─── 18f. Agent 绑定入账银行卡 ───
945
+
946
+ async bind_agent_payment_auth(p) {
947
+ const { agentId, paymentAuthId } = p;
948
+ if (!agentId || !paymentAuthId) return { success: false, error: '缺少 agentId 或 paymentAuthId' };
949
+
950
+ const auth = cx.query(`SELECT payment_user_uid, request_no, id_card, unified_social_credit_code, receiver_type FROM payment_auth WHERE id = ?`, [paymentAuthId])[0];
951
+ if (!auth) return { success: false, error: '未找到支付认证信息' };
952
+ if (!auth.payment_user_uid && !auth.request_no) {
953
+ return { success: false, error: '该银行卡尚未申请认证,请先调用 voko_apply_payment_auth 完成认证(receiverApplyStatus=COMPLETED 后再绑定)' };
954
+ }
955
+
956
+ const agent = cx.query(`SELECT owner_email, did, private_key FROM agents WHERE agent_id = ?`, [agentId])[0];
957
+ if (!agent) return { success: false, error: '未找到 Agent' };
958
+ if (!agent.did) return { success: false, error: 'Agent 未注册 DID' };
959
+ if (!agent.private_key) return { success: false, error: 'Agent 未配置私钥' };
960
+
961
+ // 1. 本地绑定
962
+ cx.exec(`UPDATE agents SET payment_auth_id = ? WHERE agent_id = ?`, [paymentAuthId, agentId]);
963
+ const bound = cx.query(`SELECT payment_fee_rate, agent_usage_fee_rate FROM agents WHERE payment_auth_id = ? AND agent_id != ? LIMIT 1`, [paymentAuthId, agentId])[0];
964
+ if (bound) {
965
+ cx.exec(`UPDATE agents SET payment_fee_rate = ?, agent_usage_fee_rate = ? WHERE agent_id = ?`, [bound.payment_fee_rate, bound.agent_usage_fee_rate, agentId]);
966
+ }
967
+
968
+ // 2. 服务端 link-agent 同步
969
+ if (!cx.signDidRequest) {
970
+ return { success: true, warning: '本地绑定成功,但 MCP 上下文未提供 signDidRequest,未同步服务端' };
971
+ }
972
+
973
+ try {
974
+ const bizFields = { email: agent.owner_email || '', agentDid: agent.did };
975
+ if (auth.payment_user_uid) {
976
+ bizFields.paymentUserUid = auth.payment_user_uid;
977
+ } else if (auth.request_no) {
978
+ bizFields.requestNo = auth.request_no;
979
+ } else {
980
+ bizFields.licenceNo = auth.receiver_type === 2 ? (auth.unified_social_credit_code || auth.id_card) : auth.id_card;
981
+ if (auth.receiver_type) bizFields.type = auth.receiver_type;
982
+ }
983
+
984
+ const authFields = await cx.signDidRequest(agent.did, agent.private_key, bizFields);
985
+ const body = { ...authFields, ...bizFields };
986
+
987
+ const resp = await fetch(ENDPOINTS.payment.baseUrl + '/payment/receiver/link-agent', {
988
+ method: 'POST',
989
+ headers: { 'Content-Type': 'application/json' },
990
+ body: JSON.stringify(body)
991
+ });
992
+ const result = await resp.json();
993
+ if (result.code === 200) {
994
+ const data = result.data || {};
995
+ const feeRate = data.paymentFeeRate;
996
+ const usageRate = data.agentUsageFeeRate;
997
+ if (feeRate != null || usageRate != null) {
998
+ const s = []; const v = [];
999
+ if (feeRate != null) { s.push('payment_fee_rate = ?'); v.push(feeRate); }
1000
+ if (usageRate != null) { s.push('agent_usage_fee_rate = ?'); v.push(usageRate); }
1001
+ v.push(agentId);
1002
+ cx.exec(`UPDATE agents SET ${s.join(', ')} WHERE agent_id = ?`, v);
1003
+ }
1004
+ return { success: true, data };
1005
+ }
1006
+ return { success: false, error: result.msg || '绑定失败', code: result.code };
1007
+ } catch (e) {
1008
+ console.error('[MCP] bind_agent_payment_auth link-agent 失败:', e.message);
1009
+ return { success: false, error: e.message };
1010
+ }
1011
+ },
1012
+
1013
+ // ─── 19. 计费模式 ───
1014
+
1015
+ async agent_pricing(p) {
1016
+ // 设置了 pricingModel 则为写操作
1017
+ if (p.pricingModel) {
1018
+ const now = Date.now();
1019
+ const isFree = p.pricingModel === 'free';
1020
+ const finalTrial = isFree ? null : (p.trialMinutes ?? 3);
1021
+ const existing = cx.query(`SELECT id FROM agent_pricing WHERE agent_id=?`, [p.agentId]);
1022
+ if (existing && existing.length > 0) {
1023
+ cx.exec(`UPDATE agent_pricing SET pricing_model=?, price=?, duration_minutes=?, trial_minutes=?, enabled=1, updated_at=? WHERE agent_id=?`,
1024
+ [p.pricingModel, isFree ? null : (p.price || null), isFree ? null : (p.durationMinutes || null), finalTrial, now, p.agentId]);
1025
+ } else {
1026
+ const id = 'ap_' + now + '_' + Math.random().toString(36).substr(2, 6);
1027
+ cx.exec(`INSERT INTO agent_pricing (id, agent_id, pricing_model, price, duration_minutes, trial_minutes, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,1,?,?)`,
1028
+ [id, p.agentId, p.pricingModel, isFree ? null : (p.price || null), isFree ? null : (p.durationMinutes || null), finalTrial, now, now]);
1029
+ }
1030
+ return { success: true };
1031
+ }
1032
+
1033
+ // 不传 pricingModel 则为查询
1034
+ const row = cx.query(`SELECT * FROM agent_pricing WHERE agent_id=?`, [p.agentId])[0] || null;
1035
+ return {
1036
+ agentId: p.agentId,
1037
+ pricingModel: row?.pricing_model || 'free',
1038
+ price: row?.price || null,
1039
+ durationMinutes: row?.duration_minutes || null,
1040
+ trialMinutes: row?.trial_minutes ?? 3,
1041
+ enabled: row?.enabled !== 0,
1042
+ };
1043
+ },
1044
+
1045
+ // ─── 20. 轮询新消息 ───
1046
+
1047
+ _fetchCursors: new Map(),
1048
+
1049
+ _channelCursorKey(agentId, channelId) {
1050
+ return `${agentId}:${channelId}`;
1051
+ },
1052
+
1053
+ _getChannelCursor(agentId, channelId) {
1054
+ return this._fetchCursors.get(this._channelCursorKey(agentId, channelId)) ?? 0;
1055
+ },
1056
+
1057
+ _setChannelCursor(agentId, channelId, seq) {
1058
+ const key = this._channelCursorKey(agentId, channelId);
1059
+ const current = this._fetchCursors.get(key) || 0;
1060
+ if (seq > current) this._fetchCursors.set(key, seq);
1061
+ },
1062
+
1063
+ async fetch_new_messages(p) {
1064
+ const blockTimeout = p.blockTimeout || 0;
1065
+ const limit = Math.min(p.limit || 50, 200);
1066
+ const onlyReplies = p.onlyReplies !== false;
1067
+
1068
+ // 单访客模式:key 用 agentId:visitorId,行为和以前一致
1069
+ if (p.visitorId) {
1070
+ const key = this._channelCursorKey(p.agentId, p.visitorId);
1071
+ const seq = p.messageSeq ?? this._fetchCursors.get(key) ?? 0;
1072
+
1073
+ if (blockTimeout > 0) {
1074
+ const prev = this._fetchBlocks?.get(key);
1075
+ if (prev) prev.abort();
1076
+ if (!this._fetchBlocks) this._fetchBlocks = new Map();
1077
+ const ctrl = { aborted: false };
1078
+ this._fetchBlocks.set(key, ctrl);
1079
+ try {
1080
+ const rows = await this._pollSingleChannel(p.agentId, p.visitorId, seq, limit, onlyReplies, blockTimeout, ctrl);
1081
+ if (rows.length > 0) {
1082
+ const maxSeq = Math.max(...rows.map(r => r.message_seq || 0));
1083
+ this._setChannelCursor(p.agentId, p.visitorId, maxSeq);
1084
+ }
1085
+ const hasMore = rows.length > limit;
1086
+ if (hasMore) rows.pop();
1087
+ return { messages: rows.map(fmtMsg), hasMore, count: rows.length };
1088
+ } finally {
1089
+ if (this._fetchBlocks?.get(key) === ctrl) this._fetchBlocks.delete(key);
1090
+ }
1091
+ }
1092
+
1093
+ const rows = this._queryMessages(p.agentId, p.visitorId, seq, onlyReplies, limit);
1094
+ const hasMore = rows.length > limit;
1095
+ if (hasMore) rows.pop();
1096
+ if (rows.length > 0) {
1097
+ const maxSeq = Math.max(...rows.map(r => r.message_seq || 0));
1098
+ this._setChannelCursor(p.agentId, p.visitorId, maxSeq);
1099
+ }
1100
+ return { messages: rows.map(fmtMsg), hasMore, count: rows.length };
1101
+ }
1102
+
1103
+ // ─── 全量模式(不指定 visitorId):按 channel 分别维护游标 ───
1104
+ // 因为 WuKongIM 的 message_seq 是按 channel 独立的,用单一全局游标
1105
+ // 会导致低 seq channel 的新消息被漏掉。
1106
+ const channels = cx.query(
1107
+ `SELECT channel_id FROM messages WHERE agent_id=? GROUP BY channel_id`,
1108
+ [p.agentId]
1109
+ );
1110
+
1111
+ const allRows = [];
1112
+ for (const { channel_id: channelId } of channels) {
1113
+ const autoCursor = this._getChannelCursor(p.agentId, channelId);
1114
+ let seq = autoCursor;
1115
+ // 该 channel 首次拉取且用户传了全局 messageSeq 时:
1116
+ // 仅当该 channel 的最大 seq 大于全局阈值,才把 messageSeq 作为起始点;
1117
+ // 否则从 0 开始,避免低 seq channel 的新消息被全局高 seq 漏掉。
1118
+ if (seq === 0 && p.messageSeq != null) {
1119
+ const maxRow = cx.query(
1120
+ `SELECT MAX(message_seq) as max_seq FROM messages WHERE agent_id=? AND channel_id=?`,
1121
+ [p.agentId, channelId]
1122
+ );
1123
+ const channelMaxSeq = maxRow[0]?.max_seq || 0;
1124
+ if (channelMaxSeq >= p.messageSeq) {
1125
+ seq = p.messageSeq;
1126
+ }
1127
+ }
1128
+ const rows = this._queryMessages(p.agentId, channelId, seq, onlyReplies, limit);
1129
+ allRows.push(...rows);
1130
+ }
1131
+
1132
+ // 合并后按 message_seq 升序,保证分页稳定
1133
+ allRows.sort((a, b) => (a.message_seq || 0) - (b.message_seq || 0));
1134
+
1135
+ const hasMore = allRows.length > limit;
1136
+ if (hasMore) allRows.length = limit;
1137
+
1138
+ // 更新各 channel 自动游标
1139
+ for (const row of allRows) {
1140
+ this._setChannelCursor(p.agentId, row.channel_id, row.message_seq || 0);
1141
+ }
1142
+
1143
+ return { messages: allRows.map(fmtMsg), hasMore, count: allRows.length };
1144
+ },
1145
+
1146
+ _queryMessages(agentId, channelId, seq, onlyReplies, limit) {
1147
+ let sql = `SELECT * FROM messages WHERE agent_id=? AND channel_id=? AND message_seq > ?`;
1148
+ const params = [agentId, channelId, seq];
1149
+ if (onlyReplies) sql += ` AND is_me!=1`;
1150
+ sql += ` ORDER BY message_seq ASC LIMIT ?`;
1151
+ params.push(limit + 1);
1152
+ return cx.query(sql, params);
1153
+ },
1154
+
1155
+ // 阻塞轮询单 channel:被取消时返回空数组
1156
+ async _pollSingleChannel(agentId, channelId, seq, limit, onlyReplies, timeoutSec, ctrl) {
1157
+ const start = Date.now();
1158
+ while (!ctrl.aborted) {
1159
+ const rows = this._queryMessages(agentId, channelId, seq, onlyReplies, limit);
1160
+ if (rows.length > 0) return rows;
1161
+ if (Date.now() - start >= timeoutSec * 1000) return [];
1162
+ await new Promise(r => setTimeout(r, 1000));
1163
+ }
1164
+ return [];
1165
+ },
1166
+
1167
+ // ─── 18. 白名单管理 ───
1168
+
1169
+ async manage_whitelist(p) {
1170
+ const ac = require('../core/access-control-api');
1171
+ if (p.action === 'remove') {
1172
+ return ac.removeEntryByVisitor(cx.db || db, p.agentId, p.visitorId, 'whitelist');
1173
+ }
1174
+ return ac.addEntry(cx.db || db, { agentId: p.agentId, listType: 'whitelist', visitorId: p.visitorId, reason: p.reason }, {
1175
+ onWhitelistAdded: (aid, vid) => {
1176
+ if (cx.sendSystemMessage) {
1177
+ cx.sendSystemMessage(aid, vid, '【系统消息】好友申请已通过,可以继续交流。', Math.floor(Date.now() / 1000));
1178
+ }
1179
+ }
1180
+ });
1181
+ },
1182
+
1183
+ // ─── 19. 黑名单管理 ───
1184
+
1185
+ async manage_blacklist(p) {
1186
+ const ac = require('../core/access-control-api');
1187
+ if (p.action === 'remove') {
1188
+ return ac.removeEntryByVisitor(cx.db || db, p.agentId, p.visitorId, 'blacklist');
1189
+ }
1190
+ return ac.addEntry(cx.db || db, { agentId: p.agentId, listType: 'blacklist', visitorId: p.visitorId, reason: p.reason });
1191
+ },
1192
+
1193
+ // ─── 20. 查看黑白名单 ───
1194
+
1195
+ async list_access_lists(p) {
1196
+ const ac = require('../core/access-control-api');
1197
+ return ac.getList(cx.db || db, { agentId: p.agentId, listType: p.listType });
1198
+ },
1199
+
1200
+ // ─── 21. 白名单模式 ───
1201
+
1202
+ async set_private_mode(p) {
1203
+ if (cx.toggleWhitelistMode) {
1204
+ return cx.toggleWhitelistMode({ agentId: p.agentId, enabled: p.enabled });
1205
+ }
1206
+ const newMode = p.enabled ? 'private' : 'public';
1207
+ cx.exec(`UPDATE agents SET access_mode=?, updated_at=? WHERE agent_id=?`, [newMode, Date.now(), p.agentId]);
1208
+ if (cx.registerCapabilities) {
1209
+ await cx.registerCapabilities(p.agentId, { discoverable: !p.enabled });
1210
+ }
1211
+ return { success: true, accessMode: newMode };
1212
+ },
1213
+
1214
+ // ─── 31. 邀请好友 ───
1215
+
1216
+ // ═══════════════════════════════════════════
1217
+ // 审核规则管理
1218
+ // ═══════════════════════════════════════════
1219
+
1220
+ async list_audit_rules(p) {
1221
+ const sql = p.direction
1222
+ ? 'SELECT * FROM audit_rules WHERE direction = ? ORDER BY is_default DESC, created_at ASC'
1223
+ : 'SELECT * FROM audit_rules ORDER BY is_default DESC, created_at ASC';
1224
+ const rows = p.direction ? cx.query(sql, [p.direction]) : cx.query(sql);
1225
+ return { success: true, data: rows };
1226
+ },
1227
+
1228
+ async manage_audit_rules(p) {
1229
+ if (p.action === 'add') {
1230
+ if (!p.direction || !p.keyword || !p.actionType) {
1231
+ return { success: false, error: 'add 需要 direction, keyword, actionType' };
1232
+ }
1233
+ const id = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
1234
+ cx.exec(
1235
+ `INSERT INTO audit_rules (id, direction, keyword, action, prompt, is_default, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?)`,
1236
+ [id, p.direction, p.keyword, p.actionType, p.prompt || null, Date.now(), Date.now()]
1237
+ );
1238
+ return { success: true, id, message: `审核规则已添加 (${id})` };
1239
+ }
1240
+
1241
+ if (p.action === 'update') {
1242
+ if (!p.ruleId) return { success: false, error: 'update 需要 ruleId' };
1243
+ const sets = []; const vals = [];
1244
+ if (p.keyword !== undefined) { sets.push('keyword = ?'); vals.push(p.keyword); }
1245
+ if (p.actionType !== undefined) { sets.push('action = ?'); vals.push(p.actionType); }
1246
+ if (p.prompt !== undefined) { sets.push('prompt = ?'); vals.push(p.prompt); }
1247
+ if (sets.length === 0) return { success: false, error: '无可更新的字段' };
1248
+ sets.push('updated_at = ?'); vals.push(Date.now());
1249
+ vals.push(p.ruleId);
1250
+ cx.exec(`UPDATE audit_rules SET ${sets.join(', ')} WHERE id = ?`, vals);
1251
+ return { success: true, message: `审核规则已更新 (${p.ruleId})` };
1252
+ }
1253
+
1254
+ if (p.action === 'delete') {
1255
+ if (!p.ruleId) return { success: false, error: 'delete 需要 ruleId' };
1256
+ cx.exec('DELETE FROM audit_rules WHERE id = ?', [p.ruleId]);
1257
+ return { success: true, message: `审核规则已删除 (${p.ruleId})` };
1258
+ }
1259
+
1260
+ return { success: false, error: `未知 action: ${p.action}` };
1261
+ },
1262
+
1263
+ async invite_friend(p) {
1264
+ const { agentId, friendEmail: rawEmails, friendName } = p;
1265
+ const row = cx.query(`SELECT agent_name, owner_email, did FROM agents WHERE agent_id=?`, [agentId]);
1266
+ if (!row?.[0]) return { success: false, error: 'Agent 不存在' };
1267
+ const myName = row[0].agent_name || agentId;
1268
+ const ownerDid = row[0].did;
1269
+ console.log('[Invite] agentId=' + agentId + ' myName=' + myName + ' rawEmails=' + rawEmails);
1270
+
1271
+ // 解析多个邮箱:逗号/分号/换行分隔,去重,过滤掉主人自己
1272
+ const ownerEmail = (row[0].owner_email || '').trim().toLowerCase();
1273
+ const emailList = [...new Set(
1274
+ (rawEmails || '').split(/[,;\n\r,]+/).map(e => e.trim().toLowerCase()).filter(Boolean)
1275
+ )].filter(e => e !== ownerEmail);
1276
+ if (emailList.length === 0) return { success: false, error: '没有有效的受邀邮箱' };
1277
+ console.log('[Invite] 有效受邀邮箱数=' + emailList.length + ' list=' + emailList.join(','));
1278
+
1279
+ const guideUrl = 'http://files.vokovoko.com/chat/files/VOKO_MCP_GUIDE.md';
1280
+ const currentVer = (() => { try { return require('../../package.json').version; } catch (_) { return '0.0.0'; } })();
1281
+
1282
+ // 从 latest.yml 获取最新版本号
1283
+ let latestVer = currentVer;
1284
+ try {
1285
+ const resp = await fetch('http://files.vokovoko.com/updates/latest.yml');
1286
+ if (resp.ok) {
1287
+ const text = await resp.text();
1288
+ const m = text.match(/^version:\s*(\S+)/m);
1289
+ if (m) latestVer = m[1];
1290
+ }
1291
+ } catch (_) {}
1292
+ const downloadUrl = `http://files.vokovoko.com/updates/VOKO-Desktop-Setup-${latestVer}.exe`;
1293
+
1294
+ // 每个邮箱生成独立的邀请码,入库(已有未使用的不重复生成)
1295
+ const invites = emailList.map(email => {
1296
+ const existing = cx.query(`SELECT code FROM friend_invitations WHERE agent_id=? AND friend_email=?`, [agentId, email]);
1297
+ if (existing?.[0]?.code) {
1298
+ console.log('[Invite] 复用邀请码 agent=' + agentId + ' email=' + email + ' code=' + existing[0].code);
1299
+ return { email, code: existing[0].code };
1300
+ }
1301
+ const code = Math.random().toString(36).substring(2, 8).toUpperCase();
1302
+ cx.exec(`INSERT OR REPLACE INTO friend_invitations (code, agent_id, friend_email, whitelisted, created_at) VALUES (?, ?, ?, 0, ?)`,
1303
+ [code, agentId, email, Date.now()]);
1304
+ console.log('[Invite] 生成邀请码 agent=' + agentId + ' email=' + email + ' code=' + code);
1305
+ return { email, code };
1306
+ });
1307
+
1308
+ // 每个邀请者生成独立的提示词
1309
+ const invitationPrompts = invites.map(inv => ({
1310
+ email: inv.email,
1311
+ code: inv.code,
1312
+ prompt: buildInvitationPrompt({
1313
+ myName, ownerEmail: row[0].owner_email, version: currentVer, guideUrl, downloadUrl,
1314
+ friendEmail: inv.email, inviteCode: inv.code
1315
+ })
1316
+ }));
1317
+
1318
+ // 通过 VOKO 系统邮件发送邀请(每封邮件独立生成提示词,含各自邮箱和邀请码)
1319
+ const emailResults = [];
1320
+ console.log('[Invite] 准备发送邮件 agentEmailApi=' + !!cx.agentEmailApi + ' ownerDid=' + (ownerDid || '无'));
1321
+ if (!cx.agentEmailApi) console.log('[Invite] cx.agentEmailApi 不存在');
1322
+ if (!ownerDid) console.log('[Invite] Agent 无 DID');
1323
+ if (cx.agentEmailApi && ownerDid) {
1324
+ for (const inv of invites) {
1325
+ try {
1326
+ const prompt = buildInvitationPrompt({
1327
+ myName, ownerEmail: row[0].owner_email, version: currentVer, guideUrl, downloadUrl,
1328
+ friendEmail: inv.email, inviteCode: inv.code
1329
+ });
1330
+ const sendRes = await cx.agentEmailApi.send(ownerDid, prompt, {
1331
+ to: inv.email,
1332
+ subject: buildEmailSubject(myName),
1333
+ external_id: `invite-${inv.code}`,
1334
+ reply_enabled: false,
1335
+ });
1336
+ const sent = !!sendRes?.message_id;
1337
+ emailResults.push({ email: inv.email, code: inv.code, sent, message_id: sendRes?.message_id || null });
1338
+ console.log('[Invite] 邮件发送 ' + (sent ? '成功' : '失败') + ' email=' + inv.email + ' code=' + inv.code + ' msgId=' + (sendRes?.message_id || '-'));
1339
+ } catch (e) {
1340
+ emailResults.push({ email: inv.email, code: inv.code, sent: false, note: e.message });
1341
+ console.log('[Invite] 邮件发送异常 email=' + inv.email + ' code=' + inv.code + ' err=' + e.message);
1342
+ }
1343
+ }
1344
+ } else {
1345
+ // 无法发送邮件时,为每个邀请记录失败原因
1346
+ for (const inv of invites) {
1347
+ emailResults.push({ email: inv.email, code: inv.code, sent: false, note: !cx.agentEmailApi ? '邮件服务未就绪' : 'Agent 未注册 DID' });
1348
+ }
1349
+ }
1350
+
1351
+ return {
1352
+ success: true,
1353
+ agentId,
1354
+ myName,
1355
+ guideUrl,
1356
+ downloadUrl,
1357
+ currentVersion: currentVer,
1358
+ invites,
1359
+ invitationPrompts,
1360
+ emailSent: emailResults.length > 0 ? emailResults : null,
1361
+ instructions: `已将 ${invites.length} 位好友的邀请码入库,并通过邮件发送邀请提示词。好友发来的消息包含对应邀请码时自动通过并加入白名单。`,
1362
+ };
1363
+ },
1364
+ };
1365
+ }
1366
+
1367
+ module.exports = { createToolHandlers };