@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
package/src/index.js ADDED
@@ -0,0 +1,712 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * VOKO Lite — Node.js 轻量版入口
5
+ *
6
+ * 用法:
7
+ * voko 默认启动 MCP Server
8
+ * voko start 启动 MCP Server
9
+ * voko list 列出 agents
10
+ * voko status 查看运行状态
11
+ * voko register-capabilities <agentId> 注册能力
12
+ *
13
+ * @package @voko/lite
14
+ */
15
+
16
+ // ── 优先加载本地编译的 better-sqlite3(避免与 Electron 版冲突) ──
17
+ require('./preload');
18
+
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const express = require('express');
22
+
23
+ // ── core 模块 ──
24
+ const { initDatabase, createDatabaseAPI } = require('./core/database');
25
+ const { createAgentRegistration } = require('./core/agent-registration');
26
+ const { generateOSSSignature } = require('./server/oss');
27
+ const { AgentWorkerManager } = require('./core/worker-manager');
28
+
29
+ // ── Lite 模块 ──
30
+ const { createContext } = require('./context');
31
+ const cli = require('./cli');
32
+
33
+ // ── MCP 模块 ──
34
+ const { createMcpServer } = require('./mcp/server');
35
+ const { createToolHandlers } = require('./mcp/tools');
36
+ const { createHttpTransport } = require('./mcp/transport/http');
37
+
38
+ const pkg = require('../package.json');
39
+
40
+ // ═══════════════════════════════════════════════
41
+ // 工具函数
42
+ // ═══════════════════════════════════════════════
43
+
44
+ function parseArgs(argv) {
45
+ const args = {};
46
+ for (let i = 0; i < argv.length; i++) {
47
+ if (argv[i].startsWith('--')) {
48
+ const eq = argv[i].indexOf('=');
49
+ if (eq > 0) {
50
+ args[argv[i].slice(2, eq)] = argv[i].slice(eq + 1);
51
+ } else {
52
+ const key = argv[i].slice(2);
53
+ const next = argv[i + 1];
54
+ if (next && !next.startsWith('--')) {
55
+ args[key] = next;
56
+ i++;
57
+ } else {
58
+ args[key] = true;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return args;
64
+ }
65
+
66
+ function getDesktopDbPath() {
67
+ const platform = process.platform;
68
+ if (platform === 'win32' && process.env.APPDATA) {
69
+ return path.join(process.env.APPDATA, 'voko', 'wukongim.db');
70
+ }
71
+ if (platform === 'darwin') {
72
+ return path.join(os.homedir(), 'Library', 'Application Support', 'voko', 'wukongim.db');
73
+ }
74
+ const xdg = process.env.XDG_CONFIG_HOME;
75
+ const configDir = xdg || path.join(os.homedir(), '.config');
76
+ return path.join(configDir, 'voko', 'wukongim.db');
77
+ }
78
+
79
+ function resolveDbPath(args) {
80
+ if (args.db) return args.db;
81
+ const desktopDb = getDesktopDbPath();
82
+ if (require('fs').existsSync(desktopDb)) {
83
+ console.error('[VOKO Lite] 使用数据库:', desktopDb);
84
+ return desktopDb;
85
+ }
86
+ const fallbackDir = path.join(os.homedir(), '.voko');
87
+ try { require('fs').mkdirSync(fallbackDir, { recursive: true }); } catch (_) {}
88
+ const fallback = path.join(fallbackDir, 'data.db');
89
+ console.error('[VOKO Lite] 使用默认数据库:', fallback);
90
+ return fallback;
91
+ }
92
+
93
+ function initCore(args) {
94
+ const dbPath = resolveDbPath(args);
95
+ const db = initDatabase(dbPath);
96
+ const databaseAPI = createDatabaseAPI(db);
97
+ const agentRegistration = createAgentRegistration({ db });
98
+ const agentManager = new AgentWorkerManager(db);
99
+ AgentWorkerManager.killOrphanedWorkers();
100
+ return { db, databaseAPI, agentRegistration, agentManager };
101
+ }
102
+
103
+ // ═══════════════════════════════════════════════
104
+ // MCP Server 模式
105
+ // ═══════════════════════════════════════════════
106
+
107
+ async function startMcpServer(args, core) {
108
+ const { db, databaseAPI, agentRegistration, agentManager } = core;
109
+
110
+ // ── 自动恢复已发布的 agent(仅当前用户名下) ──
111
+ const userEmail = getCurrentUserEmail(db);
112
+ const published = userEmail
113
+ ? db.prepare("SELECT * FROM agents WHERE publish_status = 'published' AND owner_email = ?").all(userEmail)
114
+ : db.prepare("SELECT * FROM agents WHERE publish_status = 'published'").all();
115
+ for (const agent of published) {
116
+ const config = { uid: agent.imUid, token: agent.imToken, serverUrl: agent.im_server_url };
117
+ agentManager.start(agent.agent_id, config);
118
+ console.error(`[VOKO Lite] 已恢复 agent: ${agent.agent_name || agent.agent_id}`);
119
+ }
120
+
121
+ // ── 处理 worker 上报的消息 ──
122
+ agentManager.on('message', (msg) => {
123
+ try {
124
+ const d = msg.data || msg;
125
+ databaseAPI.saveMessage({
126
+ id: d.messageId || `wk-${msg.agentId}-${Date.now()}`,
127
+ channelId: d.channelId,
128
+ channelType: d.channelType || 1,
129
+ fromUid: d.fromUid,
130
+ toUid: d.toUid || msg.agentId,
131
+ agentId: msg.agentId,
132
+ content: d.content || '',
133
+ timestamp: d.timestamp || Math.floor(Date.now() / 1000),
134
+ isMe: false,
135
+ status: 'received',
136
+ messageSeq: d.messageSeq,
137
+ clientMsgNo: d.clientMsgNo,
138
+ contentType: d.contentType || 1,
139
+ });
140
+ } catch (e) {
141
+ console.error('[VOKO Lite] 消息写入失败:', e.message);
142
+ }
143
+ });
144
+
145
+ // ── 版本检查(异步,不阻塞) ──
146
+ cli.checkVersion();
147
+
148
+ // ── 启动心跳(仅上报 IM 状态) ──
149
+ startHeartbeat(db, agentManager, null, null);
150
+
151
+ const cx = createContext({ db, databaseAPI, agentRegistration, agentManager });
152
+ const handlers = createToolHandlers(cx);
153
+ const mcpServer = createMcpServer(handlers, { version: pkg.version });
154
+
155
+ if (args.transport === 'stdio') {
156
+ const { createStdioTransport } = require('./mcp/transport/stdio');
157
+ await createStdioTransport(mcpServer);
158
+ return;
159
+ }
160
+
161
+ const port = parseInt(args.port, 10) || 3100;
162
+ const app = express();
163
+ app.use(express.json());
164
+
165
+ const httpTransport = createHttpTransport(mcpServer, { version: pkg.version });
166
+ app.use('/mcp', httpTransport);
167
+ app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
168
+ app.post('/api/quit', (req, res) => {
169
+ res.json({ success: true, message: 'Lite 正在关闭' });
170
+ shutdownAll(agentManager, db, 'api-quit');
171
+ });
172
+
173
+ // 模拟访客消息(供测试用,写入 DB)
174
+ app.post('/api/simulate-message', (req, res) => {
175
+ try {
176
+ const { fromUid, toUid, content, agentId } = req.body;
177
+ if (!fromUid || !toUid || !content || !agentId) {
178
+ return res.status(400).json({ success: false, error: '缺少参数: fromUid, toUid, content, agentId' });
179
+ }
180
+ const now = Math.floor(Date.now() / 1000);
181
+ const msgId = `sim_${agentId}_${Date.now()}`;
182
+ databaseAPI.saveMessage({
183
+ id: msgId, channelId: fromUid, channelType: 1,
184
+ fromUid, toUid, agentId, content, timestamp: now,
185
+ isMe: false, status: 'received',
186
+ messageSeq: null, clientMsgNo: null,
187
+ contentType: 1,
188
+ });
189
+ res.json({ success: true, messageId: msgId, note: '已写入 DB(Lite 模式,不触发 agent 处理)' });
190
+ } catch (e) {
191
+ res.status(500).json({ success: false, error: e.message });
192
+ }
193
+ });
194
+
195
+ // 退出清理 —— 跨平台
196
+ process.on('SIGINT', () => shutdownAll(agentManager, db, 'SIGINT')); // Ctrl+C (全平台)
197
+ process.on('SIGTERM', () => shutdownAll(agentManager, db, 'SIGTERM')); // kill (Unix), taskkill (Win)
198
+ if (process.platform === 'win32') {
199
+ process.on('SIGBREAK', () => shutdownAll(agentManager, db, 'SIGBREAK')); // taskkill / 任务管理器 (Win)
200
+ } else {
201
+ process.on('SIGHUP', () => shutdownAll(agentManager, db, 'SIGHUP')); // 终端关闭 (Unix/macOS)
202
+ }
203
+
204
+ // 子进程退出时也清理(Desktop 作为父进程关闭的场景)
205
+ process.on('disconnect', () => shutdownAll(agentManager, db, 'disconnect'));
206
+
207
+ // 父进程死亡检测(终端关闭、客户端断连时主动退出)
208
+ const _parentCheck = setInterval(() => {
209
+ try { process.kill(process.ppid, 0); } catch (_) {
210
+ clearInterval(_parentCheck);
211
+ console.error('[VOKO Lite] 父进程已退出,自动终止');
212
+ shutdownAll(agentManager, db, 'parent-dead');
213
+ }
214
+ }, 3000);
215
+
216
+ // 兜底:进程退出前强杀所有 worker
217
+ process.on('exit', () => {
218
+ if (agentManager && agentManager._allWorkers.size > 0) {
219
+ for (const [id, entry] of agentManager._allWorkers) {
220
+ try { entry.worker?.kill('SIGKILL'); } catch (_) {}
221
+ }
222
+ }
223
+ try { if (db?.open) db.close(); } catch (_) {}
224
+ });
225
+
226
+ (function tryListen(port, maxAttempts = 10) {
227
+ app.listen(port)
228
+ .on('listening', () => console.error(`[VOKO Lite] 已启动 → http://localhost:${port}/mcp`))
229
+ .on('error', (err) => {
230
+ if (err.code === 'EADDRINUSE' && maxAttempts > 0) {
231
+ console.error(`[VOKO Lite] 端口 ${port} 已被占用,尝试 ${port + 1}...`);
232
+ tryListen(port + 1, maxAttempts - 1);
233
+ } else {
234
+ console.error(`[VOKO Lite] 端口 ${port} 启动失败:`, err.message);
235
+ process.exit(1);
236
+ }
237
+ });
238
+ })(port);
239
+ }
240
+
241
+ // ═══════════════════════════════════════════════
242
+ // createHandlers — 创建后端处理器(OpenClaw + Hermes)
243
+ // ═══════════════════════════════════════════════
244
+
245
+ /**
246
+ * 创建后端处理器实例,供 Desktop 调用。
247
+ * OpenClawWSHandler / HermesHandler 的 mainWindow 参数传 null,
248
+ * 因为两者实际不使用 mainWindow(已确认仅存储未引用)。
249
+ *
250
+ * @param {object} params
251
+ * @param {object} params.databaseAPI - 来自 createDatabaseAPI()
252
+ * @param {string} [params.openclawMode='ws'] - 'ws' | 'cli'
253
+ * @param {object} [params.hermesConfig] - { apiHost, apiPort, apiKey, profiles }
254
+ * @param {Function} [params.onAgentReply] - callback(data) 收到 agent 回复时触发
255
+ * @returns {{ openclawHandler: object|null, hermesHandler: object|null }}
256
+ */
257
+ function createHandlers({ databaseAPI, openclawMode = 'ws', hermesConfig = {}, onAgentReply } = {}) {
258
+ let openclawHandler = null;
259
+ let hermesHandler = null;
260
+
261
+ // ── OpenClaw 处理器 ──
262
+ try {
263
+ const OpenClawHandler = openclawMode === 'ws'
264
+ ? require('./server/openclaw-websocket-handler')
265
+ : require('./server/openclaw-handler-cli');
266
+ openclawHandler = new OpenClawHandler(databaseAPI, null); // mainWindow=null(未使用)
267
+
268
+ if (openclawMode === 'ws') {
269
+ const status = openclawHandler.getStatus();
270
+ if (!status.hasToken) {
271
+ console.warn('[Lite] OpenClaw WebSocket 模式需要配置 Gateway token');
272
+ }
273
+ }
274
+
275
+ if (onAgentReply) {
276
+ openclawHandler.on('agent.reply', onAgentReply);
277
+ }
278
+
279
+ // 异步启动 gateway
280
+ if (openclawMode === 'ws') {
281
+ openclawHandler._ensureGatewayRunning().then(running => {
282
+ if (!running) console.warn('[Lite] OpenClaw Gateway 启动失败');
283
+ openclawHandler.setEnabled(true);
284
+ });
285
+ } else {
286
+ openclawHandler.setEnabled(true);
287
+ }
288
+ console.error(`[Lite] OpenClaw 处理器已创建 (${openclawMode} 模式)`);
289
+ } catch (err) {
290
+ console.error('[Lite] OpenClaw 处理器创建失败:', err.message);
291
+ }
292
+
293
+ // ── Hermes 处理器 ──
294
+ try {
295
+ const HermesHandler = require('./server/hermes-handler');
296
+ hermesHandler = new HermesHandler(databaseAPI, null, { // mainWindow=null(未使用)
297
+ host: hermesConfig.apiHost || '127.0.0.1',
298
+ port: hermesConfig.apiPort || 8642,
299
+ apiKey: hermesConfig.apiKey || '',
300
+ profiles: hermesConfig.profiles || {},
301
+ });
302
+ if (onAgentReply) {
303
+ hermesHandler.on('agent.reply', onAgentReply);
304
+ }
305
+ hermesHandler.setEnabled(true);
306
+ const keyHint = hermesConfig.apiKey ? `${hermesConfig.apiKey.substring(0, 8)}...` : '(空)';
307
+ console.error(`[Lite] Hermes 处理器已创建 host=${hermesConfig.apiHost || '127.0.0.1'}:${hermesConfig.apiPort || 8642} apiKey=${keyHint}`);
308
+ } catch (err) {
309
+ console.error('[Lite] Hermes 处理器创建失败:', err.message);
310
+ }
311
+
312
+ return { openclawHandler, hermesHandler };
313
+ }
314
+
315
+ // ═══════════════════════════════════════════════
316
+ // createMessageHandler — 创建消息处理器
317
+ // ═══════════════════════════════════════════════
318
+
319
+ /**
320
+ * 创建 MessageHandler 实例。
321
+ * MessageHandler 本身在 packages/lite/src/core/messenger.js,纯 Node.js 无 Electron 依赖。
322
+ * Desktop 通过 params 注入回调(notifyUI、enqueueIntervention 等)。
323
+ *
324
+ * @param {object} db - better-sqlite3 实例
325
+ * @param {object} params - 与 MessageHandler 构造器 options 一致
326
+ * @returns {object} MessageHandler 实例
327
+ */
328
+ function createMessageHandler(db, params) {
329
+ const { MessageHandler } = require('./core/messenger');
330
+ return new MessageHandler(db, params);
331
+ }
332
+
333
+ // ═══════════════════════════════════════════════
334
+ // createLiteApp — 程序化入口,供 Desktop 和外部调用
335
+ // ═══════════════════════════════════════════════
336
+
337
+ /**
338
+ * 创建 Lite 应用实例(清孤儿 → 初始化 DB → 自动恢复 worker → 按需创建处理器/消息处理/心跳)
339
+ *
340
+ * @param {object} [options]
341
+ * @param {string} [options.dbPath] - 数据库路径,默认自动检测
342
+ * @param {boolean} [options.autoStartWorkers] - 是否自动恢复 worker,默认 true
343
+ * @param {object} [options.appPaths] - Electron 打包路径(isPackaged/resourcesPath/userDataPath),
344
+ * 仅 Desktop 传入,纯 Node.js 环境不需要
345
+ * @param {object} [options.handlers] - 后端处理器配置(传入则自动创建 OpenClaw + Hermes)
346
+ * @param {string} [options.handlers.openclawMode='ws']
347
+ * @param {object} [options.handlers.hermesConfig]
348
+ * @param {Function} [options.handlers.onAgentReply] - agent.reply 回调
349
+ * @param {object} [options.messageHandler] - MessageHandler 配置(传入则自动创建)
350
+ * @param {object} [options.messageHandler.callbacks] - notifyUI/enqueueIntervention 等回调
351
+ * @param {object} [options.messageHandler.ac] - access-control 模块
352
+ * @param {object} [options.heartbeat] - 心跳配置(传入则自动启动)
353
+ * @param {Function} [options.heartbeat.onWarnings] - 警告回调
354
+ * @returns {Promise<{
355
+ * db, databaseAPI, agentManager, agentRegistration,
356
+ * openclawHandler, hermesHandler, messageHandler,
357
+ * stopHeartbeat: Function,
358
+ * dispose: Function
359
+ * }>}
360
+ */
361
+ async function createLiteApp(options = {}) {
362
+ const dbPath = options.dbPath || resolveDbPath({});
363
+ const db = initDatabase(dbPath);
364
+ const databaseAPI = createDatabaseAPI(db);
365
+ const agentRegistration = createAgentRegistration({ db });
366
+ const agentManager = new AgentWorkerManager(db);
367
+ AgentWorkerManager.killOrphanedWorkers();
368
+
369
+ // ── 读取当前登录用户邮箱 ──
370
+ const currentUserEmail = getCurrentUserEmail(db);
371
+ if (currentUserEmail) {
372
+ console.error(`[Auth] 当前登录用户: ${currentUserEmail}`);
373
+ } else {
374
+ console.error('[Auth] 未检测到登录用户,跳过 worker 启动');
375
+ }
376
+
377
+ // ── 自动恢复 worker(仅启动当前用户名下 agent) ──
378
+ if (options.autoStartWorkers !== false) {
379
+ const sql = currentUserEmail
380
+ ? "SELECT * FROM agents WHERE publish_status = 'published' AND owner_email = ?"
381
+ : "SELECT * FROM agents WHERE publish_status = 'published'";
382
+ const published = currentUserEmail ? db.prepare(sql).all(currentUserEmail) : db.prepare(sql).all();
383
+ for (const agent of published) {
384
+ const config = { uid: agent.imUid, token: agent.imToken, serverUrl: agent.im_server_url };
385
+ agentManager.start(agent.agent_id, config, options.appPaths);
386
+ console.error(`[VOKO Lite] 已恢复 agent: ${agent.agent_name || agent.agent_id}`);
387
+ }
388
+
389
+ agentManager.on('message', (msg) => {
390
+ try {
391
+ const d = msg.data || msg;
392
+ databaseAPI.saveMessage({
393
+ id: d.messageId || `wk-${msg.agentId}-${Date.now()}`,
394
+ channelId: d.channelId,
395
+ channelType: d.channelType || 1,
396
+ fromUid: d.fromUid,
397
+ toUid: d.toUid || msg.agentId,
398
+ agentId: msg.agentId,
399
+ content: d.content || '',
400
+ timestamp: d.timestamp || Math.floor(Date.now() / 1000),
401
+ isMe: false,
402
+ status: 'received',
403
+ messageSeq: d.messageSeq,
404
+ clientMsgNo: d.clientMsgNo,
405
+ contentType: d.contentType || 1,
406
+ });
407
+ } catch (e) {
408
+ console.error('[VOKO Lite] 消息写入失败:', e.message);
409
+ }
410
+ });
411
+ }
412
+
413
+ // ── 创建 MessageHandler(可选) ──
414
+ let messageHandler = null;
415
+ if (options.messageHandler) {
416
+ const { MessageHandler } = require('./core/messenger');
417
+ messageHandler = new MessageHandler(db, {
418
+ databaseAPI,
419
+ agentWorkers: agentManager.workers,
420
+ ac: options.messageHandler.ac || null,
421
+ ...options.messageHandler.callbacks,
422
+ });
423
+ }
424
+
425
+ // ── 创建后端处理器(可选) ──
426
+ let openclawHandler = null;
427
+ let hermesHandler = null;
428
+ if (options.handlers) {
429
+ const h = options.handlers;
430
+ // 如果未提供 hermesConfig 或 apiKey 为空,从数据库读取 channel_config
431
+ let hermesConfig = h.hermesConfig || {};
432
+ if (!hermesConfig.apiKey) {
433
+ try {
434
+ const channelCfg = databaseAPI.getConfigFromDb();
435
+ const hc = channelCfg?.hermes_config || {};
436
+ hermesConfig = {
437
+ apiHost: hc.apiHost || '127.0.0.1',
438
+ apiPort: hc.apiPort || 8642,
439
+ apiKey: hc.apiKey || '',
440
+ profiles: hc.profiles || {},
441
+ };
442
+ } catch (_) {}
443
+ }
444
+ const result = createHandlers({
445
+ databaseAPI,
446
+ openclawMode: h.openclawMode || 'ws',
447
+ hermesConfig,
448
+ onAgentReply: h.onAgentReply
449
+ ? (data) => h.onAgentReply(data, messageHandler)
450
+ : undefined,
451
+ });
452
+ openclawHandler = result.openclawHandler;
453
+ hermesHandler = result.hermesHandler;
454
+ messageHandler?.setOpenclawHandler(openclawHandler);
455
+ messageHandler?.setHermesHandler(hermesHandler);
456
+ }
457
+
458
+ // ── 启动心跳(可选) ──
459
+ let stopHeartbeat = null;
460
+ if (options.heartbeat !== false) {
461
+ const hbOpts = options.heartbeat || {};
462
+ stopHeartbeat = startHeartbeat(db, agentManager, openclawHandler, hermesHandler, {
463
+ onWarnings: hbOpts.onWarnings || undefined,
464
+ });
465
+ }
466
+
467
+ // ── 版本检查(异步,不阻塞) ──
468
+ cli.checkVersion();
469
+
470
+ return {
471
+ db, databaseAPI, agentManager, agentRegistration,
472
+ openclawHandler, hermesHandler, messageHandler,
473
+ currentUserEmail,
474
+ stopHeartbeat: stopHeartbeat || (() => {}),
475
+ dispose: () => shutdownAll(agentManager, db, 'dispose'),
476
+ };
477
+ }
478
+
479
+ /**
480
+ * 获取当前登录用户邮箱
481
+ */
482
+ function getCurrentUserEmail(db) {
483
+ try {
484
+ const { loadUserAccessTokenConfig } = require('./core/database');
485
+ const map = loadUserAccessTokenConfig(db);
486
+ const emails = Object.keys(map);
487
+ if (emails.length > 0) return emails[0];
488
+ } catch (_) {}
489
+ return null;
490
+ }
491
+
492
+ /**
493
+ * 启动心跳检测(每 60 秒上报 agent 状态到服务端)
494
+ *
495
+ * 完整模式(传 handler 引用):健康检查 + 网关恢复 + 逐 agent IM ping/pong + 心跳上报
496
+ * 简单模式(handler=null):仅上报 IM 连接数
497
+ *
498
+ * @param {object} db - better-sqlite3 实例
499
+ * @param {object} agentManager - AgentWorkerManager 实例
500
+ * @param {object} [openclawHandler] - OpenClawWSHandler 实例(可选)
501
+ * @param {object} [hermesHandler] - HermesHandler 实例(可选)
502
+ * @param {object} [options]
503
+ * @param {Function} [options.onWarnings] - 警告回调 (warnings[])
504
+ * @returns {Function} stop - 调用此函数停止心跳
505
+ */
506
+ function startHeartbeat(db, agentManager, openclawHandler, hermesHandler, options = {}) {
507
+ const { onWarnings } = options;
508
+ const ENDPOINTS = require('./endpoints.json');
509
+ const BASE_URL = ENDPOINTS.im.baseUrl;
510
+
511
+ const timer = setInterval(async () => {
512
+ try {
513
+ const userEmail = getCurrentUserEmail(db);
514
+ // ── Hermes 健康检查 + 自动恢复 ──
515
+ if (hermesHandler?.client) {
516
+ try { await hermesHandler.healthCheck(); } catch (_) {}
517
+ if (!hermesHandler.connected) {
518
+ try {
519
+ const sql = userEmail
520
+ ? `SELECT agent_id FROM agents WHERE backend_type = 'hermes' AND publish_status = 'published' AND owner_email = ?`
521
+ : `SELECT agent_id FROM agents WHERE backend_type = 'hermes' AND publish_status = 'published'`;
522
+ const downAgents = userEmail ? db.prepare(sql).all(userEmail) : db.prepare(sql).all();
523
+ for (const { agent_id } of downAgents) {
524
+ try { await hermesHandler._ensureGatewayRunning(agent_id); }
525
+ catch (e) { console.error('[Lite] Hermes gateway 恢复失败:', agent_id, e.message); }
526
+ }
527
+ } catch (_) {}
528
+ }
529
+ }
530
+
531
+ // ── 遍历 agent 检测状态 ──
532
+ const warnings = [];
533
+ const agentSql = userEmail
534
+ ? "SELECT agent_id, agent_name, imUid, backend_type FROM agents WHERE publish_status = 'published' AND owner_email = ?"
535
+ : "SELECT agent_id, agent_name, imUid, backend_type FROM agents WHERE publish_status = 'published'";
536
+ const rows = userEmail ? db.prepare(agentSql).all(userEmail) : db.prepare(agentSql).all();
537
+ let imOnline = 0, backendOnline = 0, posted = 0;
538
+
539
+ for (const agent of rows) {
540
+ // IM 状态
541
+ let imOk = agentManager?.getStatus(agent.agent_id)?.connected || false;
542
+ if (!imOk && agentManager) {
543
+ try {
544
+ const workers = agentManager.workers;
545
+ const entry = workers?.get(agent.agent_id);
546
+ if (entry?.worker) {
547
+ imOk = await new Promise(resolve => {
548
+ const timer2 = setTimeout(() => resolve(false), 2000);
549
+ entry.worker.once('message', (msg) => {
550
+ if (msg?.type === 'pong') { clearTimeout(timer2); resolve(msg.connected === true); }
551
+ });
552
+ entry.worker.send({ type: 'ping' });
553
+ });
554
+ }
555
+ } catch (_) {}
556
+ }
557
+
558
+ // 后端状态
559
+ let backendOk = false;
560
+ if (agent.backend_type === 'hermes') {
561
+ const ca = hermesHandler?.connectedAgents;
562
+ backendOk = ca ? ca.has(agent.agent_id) : !!hermesHandler?.connected;
563
+ } else {
564
+ backendOk = openclawHandler?.getStatus?.()?.connected || false;
565
+ }
566
+
567
+ if (imOk) imOnline++;
568
+ if (backendOk) backendOnline++;
569
+
570
+ const agentName = agent.agent_name || agent.agent_id;
571
+ if (!imOk) warnings.push({ type: 'agent-im-offline', message: `⚠️ ${agentName} IM 连接断开`, action: 'agent-detail', agentId: agent.agent_id });
572
+ if (!backendOk) warnings.push({ type: 'agent-backend-offline', message: `⚠️ ${agentName} 后端连接断开`, action: 'agent-detail', agentId: agent.agent_id });
573
+
574
+ if (imOk && backendOk) {
575
+ try {
576
+ const r = await fetch(`${BASE_URL}/api/heartbeat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uid: agent.imUid }) });
577
+ if (r.ok) posted++;
578
+ } catch (_) {}
579
+ }
580
+ }
581
+
582
+ // ── OpenClaw gateway 恢复 ──
583
+ if (openclawHandler) {
584
+ const ocStatus = openclawHandler.getStatus?.();
585
+ if (!ocStatus?.connected && !ocStatus?.connecting && openclawHandler._ensureGatewayRunning) {
586
+ openclawHandler._ensureGatewayRunning();
587
+ }
588
+ }
589
+
590
+ // ── 收集网关级警告 ──
591
+ if (openclawHandler?.getStatus) {
592
+ const s = openclawHandler.getStatus();
593
+ if (!s?.hasToken) warnings.push({ type: 'ws-no-token', message: '⚠️ OpenClaw Gateway 未配置 Token', action: 'settings-ws' });
594
+ else if (!s?.connected && !s?.connecting) warnings.push({ type: 'ws-disconnected', message: '⚠️ OpenClaw Gateway 未连接', action: null });
595
+ else if (s?.connecting) warnings.push({ type: 'ws-connecting', message: '⏳ OpenClaw Gateway 连接中', action: null });
596
+ }
597
+ if (hermesHandler && !hermesHandler.connected) {
598
+ warnings.push({ type: 'hermes-disconnected', message: '⚠️ Hermes Gateway 异常', action: 'settings-hermes' });
599
+ }
600
+
601
+ // ── 上报 ──
602
+ const ts = new Date().toLocaleTimeString();
603
+ console.error(`[${ts}][心跳] IM=${imOnline}/${rows.length} 后端=${backendOnline}/${rows.length}`);
604
+ if (posted > 0 || rows.length > 0) {
605
+ console.error(`[${ts}][心跳] 上报=${posted}个agent`);
606
+ }
607
+
608
+ if (warnings.length > 0) {
609
+ console.error(`[${ts}][心跳] 发现 ${warnings.length} 个异常:`, warnings.map(w => w.type).join(', '));
610
+ }
611
+ if (onWarnings) onWarnings(warnings);
612
+ } catch (e) {
613
+ console.error('[Lite] 心跳异常:', e.message);
614
+ }
615
+ }, 60000);
616
+
617
+ console.error('[Lite] 心跳检测已启动(60 秒间隔)');
618
+ return () => { clearInterval(timer); };
619
+ }
620
+
621
+ // ═══════════════════════════════════════════════
622
+ // 退出清理
623
+ // ═══════════════════════════════════════════════
624
+
625
+ async function shutdownAll(agentManager, db, signal) {
626
+ console.error(`[VOKO Lite] 收到 ${signal},正在清理...`);
627
+ // 先发 disconnect 让 worker 主动断开 IM(防止 zombie 连接)
628
+ if (agentManager) {
629
+ for (const [id, entry] of agentManager._allWorkers) {
630
+ try { entry.worker?.send({ type: 'disconnect' }); } catch (_) {}
631
+ }
632
+ }
633
+ // 等一刹那让消息发出去
634
+ await new Promise(r => setTimeout(r, 200));
635
+ if (db && db.open) {
636
+ try { db.close(); } catch (_) {}
637
+ console.error('[VOKO Lite] 数据库已关闭');
638
+ }
639
+ // process.exit 会触发 exit 事件兜底强杀所有 worker
640
+ process.exit(0);
641
+ }
642
+
643
+ // ── CLI 命令入口 ──
644
+
645
+ function printUsage() {
646
+ console.log(`
647
+ VOKO Lite — 纯 Node.js 轻量版
648
+
649
+ 用法:
650
+ voko 启动 MCP Server(默认)
651
+ voko start 启动 MCP Server
652
+ voko start --port 3100 指定端口
653
+ voko start --transport stdio 标准输入/输出模式
654
+ voko list 列出所有 agents
655
+ voko status 查看运行状态
656
+ voko register-capabilities <agentId> 注册能力
657
+ voko update 升级到最新版本
658
+ voko --help 显示帮助
659
+ `);
660
+ }
661
+
662
+ async function main() {
663
+ const argv = process.argv.slice(2);
664
+ const subcommand = argv[0];
665
+
666
+ // 帮助
667
+ if (subcommand === '--help' || subcommand === '-h') {
668
+ printUsage();
669
+ return;
670
+ }
671
+
672
+ // 解析 --port 等参数(兼容 subcommand 后的参数)
673
+ const args = parseArgs(argv);
674
+
675
+ // 初始化 core(DB + AgentManager)
676
+ const core = initCore(args);
677
+
678
+ // 路由
679
+ if (!subcommand || subcommand === 'start') {
680
+ // MCP Server 模式
681
+ await startMcpServer(args, core);
682
+ } else if (subcommand === 'list') {
683
+ cli.listAgents(core);
684
+ core.db.close();
685
+ } else if (subcommand === 'status') {
686
+ cli.showStatus(core);
687
+ core.db.close();
688
+ } else if (subcommand === 'register-capabilities') {
689
+ const agentId = argv[1];
690
+ await cli.registerCapabilities(core, agentId);
691
+ core.db.close();
692
+ } else if (subcommand === 'update') {
693
+ cli.updateLite();
694
+ } else {
695
+ console.error(`未知命令: ${subcommand}`);
696
+ printUsage();
697
+ process.exit(1);
698
+ }
699
+ }
700
+
701
+ if (require.main === module) {
702
+ main().catch(err => {
703
+ console.error('[VOKO Lite] 启动失败:', err);
704
+ process.exit(1);
705
+ });
706
+ }
707
+
708
+ // ═══════════════════════════════════════════════
709
+ // 程序化导出 — 供 Desktop 和外部调用
710
+ // ═══════════════════════════════════════════════
711
+
712
+ module.exports = { initCore, createContext, createLiteApp, createHandlers, createMessageHandler, startHeartbeat, getCurrentUserEmail, syncOfflineMessages: require('./core/offline-sync').syncOfflineMessages, processPendingPaymentOrder: require('./core/payment').processPendingPaymentOrder, startPaymentPolling: require('./core/payment').startPaymentPolling, AgentWorkerManager, MessageFile: require('./workers/message-content').MessageFile };