@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/context.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Lite MCP context 工厂
3
+ *
4
+ * 填实所有 stub,与 Desktop 共用同一套 core 模块。
5
+ * 本文件在 Step 3 切换为 @voko/core 包引用。
6
+ */
7
+
8
+ const { searchCapabilitiesByDid, searchCapabilitiesByHmac } = require('./core/search-capabilities');
9
+ const { updateAgentProfile } = require('./core/update-agent-profile');
10
+ const { setAgentStatus } = require('./core/set-agent-status');
11
+ const { publishAgent, unpublishAgent } = require('./core/publish-agent');
12
+ const { toggleWhitelistMode: coreToggleWhitelistMode } = require('./core/access-control');
13
+ const { registerCapabilitiesForAgent } = require('./core/register-capabilities');
14
+ const { generateOSSSignature } = require('./server/oss');
15
+ const { createSendMessageToWorker } = require('./core/send-message');
16
+
17
+ const pkg = require('../package.json');
18
+
19
+ function createContext({ db, databaseAPI, agentRegistration, agentManager }) {
20
+ // 创建 sendMessage 函数(写 DB + worker 发送,无 UI 通知)
21
+ const sendMessage = createSendMessageToWorker({
22
+ db,
23
+ agentWorkers: agentManager?.workers || new Map(),
24
+ mainWindow: null,
25
+ });
26
+
27
+ return {
28
+ db,
29
+ query: (sql, params) => {
30
+ try {
31
+ const stmt = db.prepare(sql);
32
+ return params ? stmt.all(...params) : stmt.all();
33
+ } catch (e) {
34
+ console.error('[Lite:query]', e.message);
35
+ return [];
36
+ }
37
+ },
38
+ exec: (sql, params) => {
39
+ try {
40
+ const stmt = db.prepare(sql);
41
+ return params ? stmt.run(...params) : stmt.run();
42
+ } catch (e) {
43
+ console.error('[Lite:exec]', e.message);
44
+ return { changes: 0 };
45
+ }
46
+ },
47
+ databaseAPI,
48
+
49
+ // ── 消息 ──
50
+ sendMessage: (agentId, toUid, content, fromUid, messageType) => {
51
+ return sendMessage(agentId, toUid, content, fromUid, messageType);
52
+ },
53
+
54
+ // ── Worker 管理 ──
55
+ startAgentWorker: (agentId, config, appPaths) => {
56
+ if (!agentManager) {
57
+ console.error('[Lite] agentManager 未初始化');
58
+ return;
59
+ }
60
+ agentManager.start(agentId, config, appPaths);
61
+ },
62
+
63
+ stopAgentWorker: async (agentId) => {
64
+ if (!agentManager) return;
65
+ await agentManager.stop(agentId);
66
+ },
67
+
68
+ getAgentStatus: (agentId) => {
69
+ if (!agentManager) return { imConnected: false, imStatus: 'unknown', backendConnected: false };
70
+ const status = agentManager.getStatus(agentId);
71
+ return {
72
+ imConnected: status.connected,
73
+ imStatus: status.status || 'unknown',
74
+ backendConnected: false,
75
+ uid: status.uid,
76
+ };
77
+ },
78
+
79
+ // ── 能力注册 ──
80
+ registerCapabilities: async (agentId, options) => {
81
+ try {
82
+ return await registerCapabilitiesForAgent({ db, agentId, options });
83
+ } catch (e) {
84
+ console.error('[Lite] registerCapabilities 失败:', e.message);
85
+ return { success: false, error: e.message };
86
+ }
87
+ },
88
+
89
+ // ── Agent 资料 ──
90
+ updateAgentProfile: (params) => updateAgentProfile({ db, ...params }),
91
+ setAgentStatus: (params) => setAgentStatus({ db, ...params }),
92
+
93
+ // ── 访问控制 ──
94
+ toggleWhitelistMode: async ({ agentId, enabled }) => coreToggleWhitelistMode({
95
+ db, agentId, enabled,
96
+ registerCapabilities: (aid, opts) => registerCapabilitiesForAgent({ db, agentId: aid, options: opts }),
97
+ setAgentStatus: (params) => setAgentStatus({ db, ...params }),
98
+ }),
99
+
100
+ // ── 发布/下架 ──
101
+ publishAgent: (params) => publishAgent({
102
+ db, ...params,
103
+ registerCapabilities: (aid, opts) => registerCapabilitiesForAgent({ db, agentId: aid, options: opts }),
104
+ updateAgentProfile: (p) => updateAgentProfile({ db, ...p }),
105
+ setAgentStatus: (p) => setAgentStatus({ db, ...p }),
106
+ startAgentWorker: (aid, cfg) => agentManager?.start(aid, cfg),
107
+ stopAgentWorker: (aid) => agentManager?.stop(aid),
108
+ }),
109
+
110
+ unpublishAgent: (params) => unpublishAgent({
111
+ db, ...params,
112
+ registerCapabilities: (aid, opts) => registerCapabilitiesForAgent({ db, agentId: aid, options: opts }),
113
+ setAgentStatus: (p) => setAgentStatus({ db, ...p }),
114
+ stopAgentWorker: (aid) => agentManager?.stop(aid),
115
+ }),
116
+
117
+ // ── 能力搜索 ──
118
+ searchCapabilities: async (params) => {
119
+ try {
120
+ const { agent_id, keyword, page, limit } = params || {};
121
+ const searchOpts = { db, agentId: agent_id, keyword, page, limit };
122
+ try { return await searchCapabilitiesByDid(searchOpts); } catch (_) {}
123
+ return await searchCapabilitiesByHmac({ keyword, page, limit });
124
+ } catch (e) {
125
+ return { success: false, error: e.message };
126
+ }
127
+ },
128
+
129
+ // ── OSS ──
130
+ generateOSSSignature: (filename, dir, contentType, maxSize) => {
131
+ try {
132
+ return generateOSSSignature(filename, contentType, maxSize);
133
+ } catch (e) {
134
+ console.error('[Lite] OSS 签名失败:', e.message);
135
+ return { uploadUrl: null, fileUrl: null, error: e.message };
136
+ }
137
+ },
138
+
139
+ // ── 注册 ──
140
+ agentRegistration,
141
+
142
+ // ── 支付 ──
143
+ getPaymentAuth: (agentId) => {
144
+ try { return databaseAPI.getPaymentAuth(agentId); } catch (_) { return null; }
145
+ },
146
+ getAgentImUid: (agentId) => {
147
+ try { return databaseAPI.getAgentImUid(agentId); } catch (_) { return ''; }
148
+ },
149
+ savePaymentOrder: (order) => {
150
+ try { return databaseAPI.savePaymentOrder(order); } catch (_) {}
151
+ },
152
+
153
+ // ── WuKongIM ──
154
+ wukongim: {
155
+ getCurrentUid: (agentId) => {
156
+ try { return databaseAPI.getAgentImUid(agentId); } catch (_) { return 'voko'; }
157
+ },
158
+ },
159
+
160
+ version: pkg.version,
161
+ };
162
+ }
163
+
164
+ module.exports = { createContext };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * 访问控制(黑白名单)API 模块
3
+ *
4
+ * 纯函数,无 Electron / IPC 依赖。
5
+ * 所有函数第一个参数为 db(better-sqlite3 实例),依赖显式传入。
6
+ */
7
+
8
+ /**
9
+ * 查询黑白名单列表
10
+ */
11
+ function getList(db, { agentId, listType }) {
12
+ try {
13
+ const rows = db.prepare(
14
+ `SELECT id, visitor_id, reason, created_at FROM agent_access_lists WHERE agent_id = ? AND list_type = ? ORDER BY created_at DESC`
15
+ ).all(agentId, listType);
16
+ return { success: true, data: rows };
17
+ } catch (e) {
18
+ return { success: false, error: e.message };
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 添加黑白名单条目
24
+ * 如果 listType='whitelist' 且传了 onWhitelistAdded,添加成功后自动回调通知
25
+ *
26
+ * @param {Function} [onWhitelistAdded] - (agentId, visitorId) => void
27
+ */
28
+ function addEntry(db, { agentId, listType, visitorId, reason }, { onWhitelistAdded } = {}) {
29
+ try {
30
+ const id = `acl-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
31
+ const now = Date.now();
32
+ db.prepare(
33
+ `INSERT INTO agent_access_lists (id, agent_id, list_type, visitor_id, reason, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
34
+ ).run(id, agentId, listType, visitorId, reason || null, now, now);
35
+ // 白名单添加成功后自动通知访客
36
+ if (listType === 'whitelist' && onWhitelistAdded) {
37
+ try { onWhitelistAdded(agentId, visitorId); } catch (_) {}
38
+ }
39
+ return { success: true, id };
40
+ } catch (e) {
41
+ if (e.message && e.message.includes('UNIQUE')) {
42
+ return { success: false, error: '该访客已在此名单中' };
43
+ }
44
+ return { success: false, error: e.message };
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 删除黑白名单条目(按条件)
50
+ */
51
+ function removeEntryByVisitor(db, agentId, visitorId, listType) {
52
+ try {
53
+ db.prepare(`DELETE FROM agent_access_lists WHERE agent_id=? AND visitor_id=? AND list_type=?`).run(agentId, visitorId, listType);
54
+ return { success: true };
55
+ } catch (e) {
56
+ return { success: false, error: e.message };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 删除黑白名单条目
62
+ */
63
+ function removeEntry(db, id) {
64
+ try {
65
+ db.prepare(`DELETE FROM agent_access_lists WHERE id = ?`).run(id);
66
+ return { success: true };
67
+ } catch (e) {
68
+ return { success: false, error: e.message };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 检查访客是否在黑名单中
74
+ */
75
+ function isBlacklisted(db, agentId, visitorId) {
76
+ return !!db.prepare(
77
+ `SELECT 1 FROM agent_access_lists WHERE agent_id = ? AND list_type = 'blacklist' AND visitor_id = ?`
78
+ ).get(agentId, visitorId);
79
+ }
80
+
81
+ /**
82
+ * 检查访客是否在白名单中
83
+ */
84
+ function isWhitelisted(db, agentId, visitorId) {
85
+ return !!db.prepare(
86
+ `SELECT 1 FROM agent_access_lists WHERE agent_id = ? AND list_type = 'whitelist' AND visitor_id = ?`
87
+ ).get(agentId, visitorId);
88
+ }
89
+
90
+ /**
91
+ * 检查主人回复是否是同意好友申请,是则自动加入白名单并通知访客
92
+ * 干预记录 id 以 private_req_ 开头表示好友申请类型
93
+ *
94
+ * @param {Object} db - better-sqlite3 实例
95
+ * @param {Function} sendSystemMessage - (agentId, visitorId, content, timestamp) => void
96
+ * @param {Object} intervention - 干预记录
97
+ * @param {string} ownerReply - 主人回复内容
98
+ * @returns {boolean} 是否已处理
99
+ */
100
+ function autoApproveIfFriendRequest(db, sendSystemMessage, intervention, ownerReply) {
101
+ if (!intervention || !intervention.id || !intervention.id.startsWith('private_req_')) return false;
102
+ if (!ownerReply || typeof ownerReply !== 'string') return false;
103
+ const trimmed = ownerReply.trim();
104
+ const isApproved = /同意|通过|好的|ok/i.test(trimmed);
105
+ if (!isApproved) return false;
106
+
107
+ const { agentId, visitorId } = intervention;
108
+ if (!agentId || !visitorId) return false;
109
+
110
+ // 检查是否已在白名单中
111
+ if (isWhitelisted(db, agentId, visitorId)) {
112
+ console.log(`[好友申请] 访客 ${visitorId} 已在白名单中,跳过`);
113
+ return true;
114
+ }
115
+
116
+ const systemMsg = '【系统消息】好友申请已通过,可以继续交流。';
117
+ const result = addEntry(db, { agentId, listType: 'whitelist', visitorId, reason: '好友申请已通过' }, {
118
+ onWhitelistAdded: (aid, vid) => {
119
+ sendSystemMessage(aid, vid, systemMsg, Math.floor(Date.now() / 1000));
120
+ }
121
+ });
122
+ if (result.success) {
123
+ console.log(`[好友申请] 访客 ${visitorId} 已自动加入 Agent ${agentId} 的白名单`);
124
+ } else {
125
+ console.error('[好友申请] 自动加入白名单失败:', result.error);
126
+ }
127
+ return true;
128
+ }
129
+
130
+ /**
131
+ * 通知服务端 Agent 状态变更(HTTP POST)
132
+ */
133
+ function postAgentStatus(apiBase, agentId, data) {
134
+ fetch(`${apiBase}/api/agent-status`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ agentId, ...data })
138
+ }).catch(err => console.warn('[AgentStatus] 通知服务端失败:', err.message));
139
+ }
140
+
141
+ module.exports = {
142
+ getList,
143
+ addEntry,
144
+ removeEntry,
145
+ removeEntryByVisitor,
146
+ isBlacklisted,
147
+ isWhitelisted,
148
+ autoApproveIfFriendRequest,
149
+ postAgentStatus,
150
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 访问控制(黑白名单 / 白名单模式)核心逻辑
3
+ *
4
+ * 供主进程 IPC 和 MCP 工具共享。
5
+ * 注意:UI 通知等 Electron 相关副作用由调用方处理。
6
+ */
7
+
8
+ /**
9
+ * 切换白名单模式(public ↔ private,不影响 publish_status)
10
+ * @param {Object} opts
11
+ * @param {Object} opts.db - better-sqlite3 Database 实例
12
+ * @param {string} opts.agentId
13
+ * @param {boolean} opts.enabled - true=private, false=public
14
+ * @param {Function} opts.registerCapabilities - (agentId, options?) => Promise
15
+ * @param {Function} opts.setAgentStatus - (params) => Promise
16
+ * @returns {Promise<{success: boolean, accessMode?: string, error?: string}>}
17
+ */
18
+ async function toggleWhitelistMode(opts) {
19
+ const { db, agentId, enabled, registerCapabilities, setAgentStatus } = opts || {};
20
+
21
+ if (!db) return { success: false, error: 'db is required' };
22
+ if (!agentId) return { success: false, error: 'agentId is required' };
23
+
24
+ try {
25
+ const newMode = enabled ? 'private' : 'public';
26
+ const now = Date.now();
27
+ db.prepare(`UPDATE agents SET access_mode = ?, updated_at = ? WHERE agent_id = ?`).run(newMode, now, agentId);
28
+
29
+ // 重新注册能力(更新 discoverable 标志)
30
+ if (registerCapabilities) {
31
+ try { await registerCapabilities(agentId, { discoverable: !enabled }); }
32
+ catch (e) { console.warn(`[toggleWhitelistMode] Agent ${agentId} 更新能力发现状态失败:`, e.message); }
33
+ }
34
+
35
+ // 同步公开/私有状态到服务端(保持当前上架状态)
36
+ if (setAgentStatus) {
37
+ const pubRow = db.prepare(`SELECT publish_status FROM agents WHERE agent_id = ?`).get(agentId);
38
+ const pubStatus = pubRow?.publish_status === 'published' ? 1 : 0;
39
+ try {
40
+ const syncResult = await setAgentStatus({ agentId, status: pubStatus, visibility: enabled ? 0 : 1 });
41
+ if (!syncResult?.success) {
42
+ console.warn(`[toggleWhitelistMode] Agent ${agentId} 同步服务端失败:`, syncResult?.error || '未知错误');
43
+ }
44
+ } catch (e) {
45
+ console.warn(`[toggleWhitelistMode] Agent ${agentId} 同步服务端异常:`, e.message);
46
+ }
47
+ }
48
+
49
+ return { success: true, accessMode: newMode };
50
+ } catch (e) {
51
+ console.error('[toggleWhitelistMode] error:', e);
52
+ return { success: false, error: e.message };
53
+ }
54
+ }
55
+
56
+ module.exports = { toggleWhitelistMode };
@@ -0,0 +1,319 @@
1
+ /**
2
+ * VOKO Agent 注册/验证共享逻辑
3
+ *
4
+ * 供桌面端 IPC handler 和 MCP 工具共同调用:
5
+ * - 发送邮箱验证码
6
+ * - 验证验证码并获取 DID/IM 账号/密钥
7
+ * - 用本地 userAccessToken 静默创建 Agent
8
+ * - 写入/更新本地 SQLite agents 表
9
+ *
10
+ * 注意:本模块只做“注册(unpublished)”,不启动 worker。
11
+ */
12
+
13
+ const { VOKO_API_URL, VOKO_API_KEY, generateApiSignature } = require('./api-signature');
14
+ const ENDPOINTS = require('../endpoints.json');
15
+
16
+ const USER_ACCESS_TOKEN_CONFIG_TYPE = 'user_access_token';
17
+ const DEFAULT_IM_SERVER_URL = ENDPOINTS.im.wsUrl;
18
+
19
+ function normalizeUserEmail(email) {
20
+ return String(email || '').trim().toLowerCase();
21
+ }
22
+
23
+ function createAgentRegistration({ db }) {
24
+ if (!db) throw new Error('AgentRegistration requires db');
25
+
26
+ // ─── userAccessToken 读写 ───
27
+ function loadUserAccessTokenConfig() {
28
+ try {
29
+ const row = db.prepare('SELECT data FROM config WHERE type = ?').get(USER_ACCESS_TOKEN_CONFIG_TYPE);
30
+ if (!row?.data) return {};
31
+ const parsed = JSON.parse(row.data);
32
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
33
+ } catch (e) {
34
+ console.warn('[AgentRegistration] loadUserAccessTokenConfig error:', e.message);
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function saveUserAccessTokenConfig(map) {
40
+ db.prepare('INSERT OR REPLACE INTO config (type, data, updated_at) VALUES (?, ?, ?)')
41
+ .run(USER_ACCESS_TOKEN_CONFIG_TYPE, JSON.stringify(map), Date.now());
42
+ }
43
+
44
+ function saveUserAccessToken(email, token) {
45
+ const normalized = normalizeUserEmail(email);
46
+ if (!normalized || !token) return;
47
+ saveUserAccessTokenConfig({ [normalized]: { user_access_token: token, updated_at: Date.now() } });
48
+ }
49
+
50
+ function getUserAccessToken() {
51
+ try {
52
+ const map = loadUserAccessTokenConfig();
53
+ const entries = Object.entries(map);
54
+ if (entries.length === 0) return { success: true, data: null };
55
+ entries.sort((a, b) => (b[1]?.updated_at || 0) - (a[1]?.updated_at || 0));
56
+ const [email, val] = entries[0];
57
+ if (val?.user_access_token) {
58
+ return { success: true, data: { email, token: val.user_access_token } };
59
+ }
60
+ return { success: true, data: null };
61
+ } catch (e) {
62
+ console.error('[AgentRegistration] getUserAccessToken error:', e.message);
63
+ return { success: false, error: e.message };
64
+ }
65
+ }
66
+
67
+ function saveUserAccessTokenFromVerify(email, verifyJson) {
68
+ const normalizedEmail = normalizeUserEmail(verifyJson.email || email);
69
+ if (!normalizedEmail || !verifyJson.userAccessToken) return null;
70
+ saveUserAccessToken(normalizedEmail, verifyJson.userAccessToken);
71
+ console.log('[AgentRegistration] saved userAccessToken for', normalizedEmail);
72
+ return verifyJson.userAccessToken;
73
+ }
74
+
75
+ // ─── 后端 API:发送验证码 ───
76
+ async function sendCode({ email, agentName }) {
77
+ try {
78
+ const path = '/api/external/v1/send-code';
79
+ const body = { email };
80
+ if (agentName) body.agentName = agentName;
81
+ const { timestamp, signature } = generateApiSignature(path, body);
82
+
83
+ console.log('[AgentRegistration] sendCode:', `${VOKO_API_URL}${path}`, 'email:', email);
84
+
85
+ const res = await fetch(`${VOKO_API_URL}${path}`, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'X-API-Key': VOKO_API_KEY,
90
+ 'X-Timestamp': timestamp,
91
+ 'X-Signature': signature,
92
+ },
93
+ body: JSON.stringify(body),
94
+ });
95
+
96
+ const text = await res.text();
97
+ console.log('[AgentRegistration] sendCode response:', res.status, text);
98
+ let json;
99
+ try { json = JSON.parse(text); } catch { json = { raw: text }; }
100
+ return { success: res.ok, status: res.status, data: json };
101
+ } catch (e) {
102
+ console.error('[AgentRegistration] sendCode error:', e);
103
+ return { success: false, error: e.message };
104
+ }
105
+ }
106
+
107
+ // ─── 后端 API:验证码预览(只验码不消费,返回 Agent 列表) ───
108
+ async function verifyCodePreview({ email, code }) {
109
+ try {
110
+ const path = '/api/external/v1/verify-code-preview';
111
+ const body = { email, code };
112
+ const { timestamp, signature } = generateApiSignature(path, body);
113
+
114
+ const res = await fetch(`${VOKO_API_URL}${path}`, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ 'X-API-Key': VOKO_API_KEY,
119
+ 'X-Timestamp': timestamp,
120
+ 'X-Signature': signature,
121
+ },
122
+ body: JSON.stringify(body),
123
+ });
124
+
125
+ const json = await res.json();
126
+ if (!res.ok || !json.success) {
127
+ return { success: false, error: json.message || '验证失败' };
128
+ }
129
+ return { success: true, agents: json.agents || [], userExists: !!json.userExists };
130
+ } catch (e) {
131
+ console.error('[AgentRegistration] verifyCodePreview error:', e);
132
+ return { success: false, error: e.message };
133
+ }
134
+ }
135
+
136
+ // ─── 后端 API:验证验证码 ───
137
+ async function verifyCode({ email, code, agentName, agentCategory, agentId }) {
138
+ try {
139
+ const path = '/api/external/v1/verify-code';
140
+ const body = { email, code };
141
+ if (agentName) body.agentName = agentName;
142
+ if (agentId) body.agentId = agentId;
143
+ if (agentCategory) body.agentCategory = agentCategory;
144
+ const { timestamp, signature } = generateApiSignature(path, body);
145
+
146
+ console.log('[AgentRegistration] verifyCode:', `${VOKO_API_URL}${path}`, 'email:', email);
147
+
148
+ const res = await fetch(`${VOKO_API_URL}${path}`, {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ 'X-API-Key': VOKO_API_KEY,
153
+ 'X-Timestamp': timestamp,
154
+ 'X-Signature': signature,
155
+ },
156
+ body: JSON.stringify(body),
157
+ });
158
+
159
+ const text = await res.text();
160
+ console.log('[AgentRegistration] verifyCode response:', res.status, text);
161
+ let json;
162
+ try { json = JSON.parse(text); } catch { json = { raw: text }; }
163
+
164
+ if (res.ok && json.did) {
165
+ const savedToken = saveUserAccessTokenFromVerify(email, json);
166
+ return { success: true, data: json, userAccessTokenSaved: !!savedToken };
167
+ }
168
+ return { success: false, status: res.status, data: json };
169
+ } catch (e) {
170
+ console.error('[AgentRegistration] verifyCode error:', e);
171
+ return { success: false, error: e.message };
172
+ }
173
+ }
174
+
175
+ // ─── 后端 API:用本地 token 静默创建 Agent ───
176
+ async function createAgentByToken({ agentId }) {
177
+ try {
178
+ if (!agentId) return { success: false, error: '缺少 agentId' };
179
+
180
+ const tokenRes = getUserAccessToken();
181
+ if (!tokenRes.success) return { success: false, error: tokenRes.error };
182
+ if (!tokenRes.data?.token) return { success: false, error: '未找到 token', noToken: true };
183
+ const token = tokenRes.data.token;
184
+
185
+ const path = '/api/external/v1/agent/create';
186
+ const body = { name: agentId };
187
+
188
+ console.log('[AgentRegistration] createAgentByToken:', agentId);
189
+
190
+ const res = await fetch(`${VOKO_API_URL}${path}`, {
191
+ method: 'POST',
192
+ headers: {
193
+ 'Content-Type': 'application/json',
194
+ 'Authorization': `Bearer ${token}`,
195
+ },
196
+ body: JSON.stringify(body),
197
+ });
198
+
199
+ const json = await res.json();
200
+ if (!res.ok || !json.success) {
201
+ const noToken = res.status === 401;
202
+ return { success: false, error: json.message || '创建失败', noToken, status: res.status };
203
+ }
204
+ return { success: true, data: json.data };
205
+ } catch (e) {
206
+ console.error('[AgentRegistration] createAgentByToken error:', e.message);
207
+ return { success: false, error: e.message };
208
+ }
209
+ }
210
+
211
+ // ─── 本地 SQLite:写入 agents 表(注册,unpublished) ───
212
+ function registerAgentInDb({
213
+ agentId,
214
+ uid,
215
+ token,
216
+ serverUrl,
217
+ ownerEmail,
218
+ backendType,
219
+ agentName,
220
+ category,
221
+ categoryLabel,
222
+ did,
223
+ publicKey,
224
+ privateKey,
225
+ loginToken,
226
+ paymentFeeRate,
227
+ agentUsageFeeRate,
228
+ }) {
229
+ try {
230
+ const now = Date.now();
231
+ const backend = backendType || 'others';
232
+ const imServerUrl = serverUrl || DEFAULT_IM_SERVER_URL;
233
+ const payRate = paymentFeeRate != null ? paymentFeeRate : 0.006;
234
+ const usageRate = agentUsageFeeRate != null ? agentUsageFeeRate : 0.1;
235
+
236
+ db.prepare(`
237
+ INSERT INTO agents (id, agent_id, imUid, imToken, im_server_url, owner_email,
238
+ agent_name, category, category_label, did, public_key, private_key, login_token,
239
+ payment_fee_rate, agent_usage_fee_rate,
240
+ publish_status, access_mode, backend_type, created_at, updated_at)
241
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', 'private', ?, ?, ?)
242
+ ON CONFLICT(agent_id) DO UPDATE SET
243
+ imUid = excluded.imUid, imToken = excluded.imToken,
244
+ im_server_url = excluded.im_server_url, owner_email = excluded.owner_email,
245
+ agent_name = excluded.agent_name, category = excluded.category,
246
+ category_label = excluded.category_label, did = excluded.did,
247
+ public_key = excluded.public_key, private_key = excluded.private_key,
248
+ login_token = excluded.login_token,
249
+ payment_fee_rate = excluded.payment_fee_rate,
250
+ agent_usage_fee_rate = excluded.agent_usage_fee_rate,
251
+ backend_type = excluded.backend_type, updated_at = excluded.updated_at
252
+ `).run(
253
+ `agent-${agentId}`, agentId, uid, token, imServerUrl, ownerEmail || null,
254
+ agentName || null, category || null, categoryLabel || null,
255
+ did || null, publicKey || null, privateKey || null, loginToken || null,
256
+ payRate, usageRate,
257
+ backend, now, now
258
+ );
259
+
260
+ console.log('[AgentRegistration] registerAgentInDb success:', agentId);
261
+ return { success: true };
262
+ } catch (e) {
263
+ console.error('[AgentRegistration] registerAgentInDb failed:', agentId, e.message);
264
+ return { success: false, error: e.message };
265
+ }
266
+ }
267
+
268
+ // ─── 本地 SQLite:更新 agents 绑定字段 ───
269
+ function updateAgentBinding({ agentId, updates }) {
270
+ try {
271
+ const sets = [];
272
+ const values = [];
273
+ if (updates.owner_email !== undefined) { sets.push('owner_email = ?'); values.push(updates.owner_email); }
274
+ if (updates.chatroom_url !== undefined) { sets.push('chatroom_url = ?'); values.push(updates.chatroom_url); }
275
+ if (updates.payment_url !== undefined) { sets.push('payment_url = ?'); values.push(updates.payment_url); }
276
+ if (updates.did !== undefined) { sets.push('did = ?'); values.push(updates.did); }
277
+ if (updates.public_key !== undefined) { sets.push('public_key = ?'); values.push(updates.public_key); }
278
+ if (updates.private_key !== undefined) { sets.push('private_key = ?'); values.push(updates.private_key); }
279
+ if (updates.login_token !== undefined) { sets.push('login_token = ?'); values.push(updates.login_token); }
280
+ if (updates.imUid !== undefined) { sets.push('imUid = ?'); values.push(updates.imUid); }
281
+ if (updates.imToken !== undefined) { sets.push('imToken = ?'); values.push(updates.imToken); }
282
+ if (updates.im_server_url !== undefined) { sets.push('im_server_url = ?'); values.push(updates.im_server_url); }
283
+ if (updates.imUid !== undefined) { sets.push('chatroom_url = ?'); values.push(ENDPOINTS.im.baseUrl + '/#/chat?peer=' + updates.imUid); }
284
+ if (updates.ability !== undefined) { sets.push('ability = ?'); values.push(updates.ability); }
285
+ if (updates.publish_status !== undefined) { sets.push('publish_status = ?'); values.push(updates.publish_status); }
286
+ if (updates.access_mode !== undefined) { sets.push('access_mode = ?'); values.push(updates.access_mode); }
287
+ if (updates.short_link_url !== undefined) { sets.push('short_link_url = ?'); values.push(updates.short_link_url); }
288
+ if (updates.qr_code_url !== undefined) { sets.push('qr_code_url = ?'); values.push(updates.qr_code_url); }
289
+ if (updates.icon_url !== undefined) { sets.push('icon_url = ?'); values.push(updates.icon_url); }
290
+
291
+ if (sets.length === 0) return { success: true };
292
+ sets.push('updated_at = ?');
293
+ values.push(Date.now());
294
+ values.push(agentId);
295
+
296
+ const sql = `UPDATE agents SET ${sets.join(', ')} WHERE agent_id = ?`;
297
+ db.prepare(sql).run(...values);
298
+ console.log('[AgentRegistration] updateAgentBinding success:', agentId, Object.keys(updates || {}));
299
+ return { success: true };
300
+ } catch (e) {
301
+ console.error('[AgentRegistration] updateAgentBinding error:', e);
302
+ return { success: false, error: e.message };
303
+ }
304
+ }
305
+
306
+ return {
307
+ sendCode,
308
+ verifyCode,
309
+ verifyCodePreview,
310
+ createAgentByToken,
311
+ getUserAccessToken,
312
+ saveUserAccessToken,
313
+ saveUserAccessTokenFromVerify,
314
+ registerAgentInDb,
315
+ updateAgentBinding,
316
+ };
317
+ }
318
+
319
+ module.exports = { createAgentRegistration };