@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,119 @@
1
+ /**
2
+ * VOKO Agent 能力注册共享逻辑
3
+ *
4
+ * 供桌面端主进程(src/main.js)的 IPC handler、MCP 工具、下架流程等共同调用。
5
+ * 使用 DID + Ed25519 签名调用 /api/did-auth/register-capabilities。
6
+ */
7
+
8
+ const { signAsync } = require('@noble/ed25519');
9
+ const { VOKO_API_URL } = require('./api-signature');
10
+
11
+ /**
12
+ * 将 PEM 格式 Ed25519 私钥解析为 raw 32 字节
13
+ * @param {string} pem
14
+ * @returns {Uint8Array}
15
+ */
16
+ function extractEd25519PrivateKey(pem) {
17
+ const cleaned = String(pem || '')
18
+ .replace(/-----BEGIN [\w\s]+ KEY-----/g, '')
19
+ .replace(/-----END [\w\s]+ KEY-----/g, '')
20
+ .replace(/\s/g, '');
21
+ const bytes = Buffer.from(cleaned, 'base64');
22
+
23
+ if (bytes.length === 32) return new Uint8Array(bytes);
24
+
25
+ if (bytes.length > 32) {
26
+ const slice = bytes.slice(-32);
27
+ if (slice.length === 32) return new Uint8Array(slice);
28
+ }
29
+
30
+ if (cleaned.length === 64 && /^[0-9a-f]+$/i.test(cleaned)) {
31
+ return new Uint8Array(Buffer.from(cleaned, 'hex'));
32
+ }
33
+
34
+ console.warn('[extractEd25519PrivateKey] unexpected key length:', bytes.length, 'trying first 32 bytes');
35
+ return new Uint8Array(bytes.slice(0, 32));
36
+ }
37
+
38
+ /**
39
+ * 注册能力到服务器
40
+ * @param {object} params
41
+ * @param {object} params.db - better-sqlite3 数据库实例
42
+ * @param {string} params.agentId - Agent 标识 ID
43
+ * @param {object} [params.options={}] - 选项
44
+ * @param {boolean} [params.options.discoverable] - 是否可被发现;默认根据 access_mode 推断
45
+ * @returns {Promise<{success: boolean, message?: string, error?: string, detail?: object}>}
46
+ */
47
+ async function registerCapabilitiesForAgent({ db, agentId, options = {} }) {
48
+ const { discoverable } = options;
49
+ try {
50
+ const stmt = db.prepare(`SELECT did, private_key, ability, capability, publish_status, access_mode FROM agents WHERE agent_id = ?`);
51
+ const row = stmt.get(agentId);
52
+ if (!row) return { success: false, error: 'Agent not found' };
53
+ if (!row.did) return { success: false, error: 'Agent has no DID, please register first' };
54
+ if (!row.private_key) return { success: false, error: 'Agent has no private key, please register first' };
55
+
56
+ // 普通模式能力(来自 agents.ability 列,UI 编辑)
57
+ let normalCapabilities = [];
58
+ if (row.ability) {
59
+ try {
60
+ normalCapabilities = JSON.parse(row.ability);
61
+ if (!Array.isArray(normalCapabilities)) normalCapabilities = [];
62
+ } catch (e) {
63
+ normalCapabilities = [];
64
+ }
65
+ }
66
+
67
+ // 开发者模式能力(来自 agents.capability 列,MCP declare_capabilities)
68
+ let capabilities = [];
69
+ if (row.capability) {
70
+ try {
71
+ const parsed = JSON.parse(row.capability);
72
+ if (parsed && parsed.skills) {
73
+ capabilities = parsed.skills;
74
+ } else if (Array.isArray(parsed)) {
75
+ capabilities = parsed;
76
+ }
77
+ } catch (e) {
78
+ capabilities = [];
79
+ }
80
+ }
81
+
82
+ const isDiscoverable = discoverable !== undefined ? discoverable : (row.access_mode !== 'private');
83
+ const bodyPayload = JSON.stringify({ capabilities, normalCapabilities });
84
+ const nonce = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
85
+ const timestamp = Math.floor(Date.now() / 1000);
86
+ const toSign = row.did + '\n' + nonce + '\n' + timestamp + '\n' + bodyPayload;
87
+ const rawKey = extractEd25519PrivateKey(row.private_key);
88
+ const sigBytes = await signAsync(new TextEncoder().encode(toSign), rawKey);
89
+ const signature = Buffer.from(sigBytes).toString('base64');
90
+
91
+ const requestBody = {
92
+ did: row.did, nonce, timestamp, signature,
93
+ capabilities, normalCapabilities
94
+ };
95
+
96
+ console.log(`[registerCapabilities] Agent ${agentId}: sending discoverable=${isDiscoverable}...`);
97
+ const response = await fetch(`${VOKO_API_URL}/api/did-auth/register-capabilities`, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify(requestBody)
101
+ });
102
+
103
+ const result = await response.json();
104
+ console.log(`[registerCapabilities] Agent ${agentId} response:`, result);
105
+ if (result.success) {
106
+ db.prepare(`UPDATE agents SET cap_error = NULL, updated_at = ? WHERE agent_id = ?`).run(Date.now(), agentId);
107
+ return { success: true, message: '能力已注册到服务器' };
108
+ }
109
+ const errMsg = result.message || '注册失败';
110
+ db.prepare(`UPDATE agents SET cap_error = ?, updated_at = ? WHERE agent_id = ?`).run(errMsg, Date.now(), agentId);
111
+ return { success: false, error: errMsg, detail: result };
112
+ } catch (e) {
113
+ console.error(`[registerCapabilities] Agent ${agentId} error:`, e);
114
+ db.prepare(`UPDATE agents SET cap_error = ?, updated_at = ? WHERE agent_id = ?`).run(e.message, Date.now(), agentId);
115
+ return { success: false, error: e.message };
116
+ }
117
+ }
118
+
119
+ module.exports = { registerCapabilitiesForAgent };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * VOKO Agent 能力搜索共享逻辑
3
+ *
4
+ * 供桌面端主进程(src/main.js)和 MCP Lite 模式(src/mcp/standalone.js)共同调用:
5
+ * - DID 认证搜索:/api/did-auth/search-agents(优先)
6
+ * - HMAC 认证搜索:/api/external/v1/agents/search(兜底)
7
+ */
8
+
9
+ const { signAsync } = require('@noble/ed25519');
10
+ const { VOKO_API_URL, VOKO_API_KEY, generateApiSignature } = require('./api-signature');
11
+
12
+ /**
13
+ * 将 PEM 格式 Ed25519 私钥解析为 raw 32 字节
14
+ * @param {string} pem
15
+ * @returns {Uint8Array}
16
+ */
17
+ function extractEd25519PrivateKey(pem) {
18
+ const cleaned = String(pem || '')
19
+ .replace(/-----BEGIN [\w\s]+ KEY-----/g, '')
20
+ .replace(/-----END [\w\s]+ KEY-----/g, '')
21
+ .replace(/\s/g, '');
22
+ const bytes = Buffer.from(cleaned, 'base64');
23
+
24
+ if (bytes.length === 32) return new Uint8Array(bytes);
25
+
26
+ if (bytes.length > 32) {
27
+ const slice = bytes.slice(-32);
28
+ if (slice.length === 32) return new Uint8Array(slice);
29
+ }
30
+
31
+ if (cleaned.length === 64 && /^[0-9a-f]+$/i.test(cleaned)) {
32
+ return new Uint8Array(Buffer.from(cleaned, 'hex'));
33
+ }
34
+
35
+ console.warn('[extractEd25519PrivateKey] unexpected key length:', bytes.length, 'trying first 32 bytes');
36
+ return new Uint8Array(bytes.slice(0, 32));
37
+ }
38
+
39
+ /**
40
+ * 生成 DID 认证搜索请求签名
41
+ *
42
+ * 注意:/api/did-auth/search-agents 要求 bodyPayload 仅含 keyword/page/limit
43
+ * 三个 key,且顺序固定为 keyword、page、limit,不是字母序。
44
+ *
45
+ * @param {string} did
46
+ * @param {string} privateKey
47
+ * @param {object} businessFields
48
+ * @returns {Promise<{did, nonce, timestamp, signature}>}
49
+ */
50
+ async function signDidSearchRequest(did, privateKey, businessFields) {
51
+ const nonce = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
52
+ const timestamp = Math.floor(Date.now() / 1000);
53
+ const orderedPayload = {
54
+ keyword: businessFields.keyword || '',
55
+ page: businessFields.page || 1,
56
+ limit: businessFields.limit || 50,
57
+ };
58
+ const bodyPayload = JSON.stringify(orderedPayload);
59
+ const toSign = did + '\n' + nonce + '\n' + timestamp + '\n' + bodyPayload;
60
+ const rawKey = extractEd25519PrivateKey(privateKey);
61
+ const sigBytes = await signAsync(new TextEncoder().encode(toSign), rawKey);
62
+ const signature = Buffer.from(sigBytes).toString('base64');
63
+ return { did, nonce, timestamp, signature };
64
+ }
65
+
66
+ /**
67
+ * 使用 DID + Ed25519 签名搜索 agent 能力
68
+ * @param {object} options
69
+ * @param {object} options.db - better-sqlite3 数据库实例
70
+ * @param {string} options.agentId - 发起搜索的 agent ID
71
+ * @param {string} [options.keyword='']
72
+ * @param {number} [options.page=1]
73
+ * @param {number} [options.limit=20]
74
+ * @returns {Promise<{success:true, data, page, count}>}
75
+ */
76
+ async function searchCapabilitiesByDid({ db, agentId, keyword = '', page = 1, limit = 50 }) {
77
+ if (!db) throw new Error('searchCapabilitiesByDid requires db');
78
+ if (!agentId) throw new Error('缺少 agentId');
79
+
80
+ const row = db.prepare('SELECT did, private_key FROM agents WHERE agent_id = ?').get(agentId);
81
+ if (!row) throw new Error('Agent not found');
82
+ if (!row.did) throw new Error('Agent has no DID');
83
+ if (!row.private_key) throw new Error('Agent has no private key');
84
+
85
+ const safePage = parseInt(page) || 1;
86
+ const safeLimit = Math.min(parseInt(limit) || 20, 100);
87
+ const businessFields = { keyword: keyword || '', page: safePage, limit: safeLimit };
88
+
89
+ const { did, nonce, timestamp, signature } = await signDidSearchRequest(row.did, row.private_key, businessFields);
90
+
91
+ const res = await fetch(`${VOKO_API_URL}/api/did-auth/search-agents`, {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ did, nonce, timestamp, signature, ...businessFields }),
95
+ });
96
+
97
+ const json = await res.json();
98
+ if (!json.success) throw new Error(json.message || '搜索失败');
99
+ return { success: true, data: json.data, page: json.page, count: json.count };
100
+ }
101
+
102
+ /**
103
+ * 使用 HMAC 签名搜索 agent 能力(外部 API Key 兜底)
104
+ * @param {object} options
105
+ * @param {string} [options.keyword='']
106
+ * @param {number} [options.page=1]
107
+ * @param {number} [options.limit=20]
108
+ * @returns {Promise<{success:true, data, page, count}>}
109
+ */
110
+ async function searchCapabilitiesByHmac({ keyword = '', page = 1, limit = 50 }) {
111
+ const apiPath = '/api/external/v1/agents/search';
112
+ const safePage = parseInt(page) || 1;
113
+ const safeLimit = Math.min(parseInt(limit) || 20, 100);
114
+ const body = { keyword: keyword || '', page: safePage, limit: safeLimit };
115
+ const { timestamp, signature } = generateApiSignature(apiPath, body);
116
+
117
+ const res = await fetch(`${VOKO_API_URL}${apiPath}`, {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ 'X-API-Key': VOKO_API_KEY,
122
+ 'X-Timestamp': timestamp,
123
+ 'X-Signature': signature,
124
+ },
125
+ body: JSON.stringify(body),
126
+ });
127
+
128
+ const json = await res.json();
129
+ if (!json.success) throw new Error(json.message || '搜索失败');
130
+ return { success: true, data: json.data, page: json.page, count: json.count };
131
+ }
132
+
133
+ module.exports = {
134
+ searchCapabilitiesByDid,
135
+ searchCapabilitiesByHmac,
136
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * VOKO 发送 IM 消息共享逻辑
3
+ *
4
+ * 供桌面端主进程(src/main.js)的 IPC handler、HTTP API、MCP 工具共同调用。
5
+ * 流程:写 DB → 自动加白名单 → worker.send → 通知 UI,不等待回执。
6
+ */
7
+
8
+ const ac = require('./access-control-api');
9
+ const bus = require('./lite-bus');
10
+
11
+ /**
12
+ * 创建 sendMessageToWorker 函数
13
+ * @param {object} deps
14
+ * @param {object} deps.db - better-sqlite3 数据库实例
15
+ * @param {Map} deps.agentWorkers - agentId → { worker, config } 的 Map
16
+ * @param {BrowserWindow|null} deps.mainWindow - Electron 主窗口实例
17
+ * @returns {Function} sendMessageToWorker(agentId, channelId, content, fromUid, messageType)
18
+ */
19
+ function createSendMessageToWorker({ db, agentWorkers, mainWindow }) {
20
+ return function sendMessageToWorker(agentId, channelId, content, fromUid, messageType) {
21
+ const entry = agentWorkers.get(agentId);
22
+ if (!entry) return { success: false, error: 'Agent 未连接' };
23
+
24
+ const now = Date.now();
25
+ const msgId = `msg-${agentId}-${channelId}-${now}`;
26
+ const timestamp = Math.floor(now / 1000);
27
+ const uid = fromUid || 'voko';
28
+
29
+ // 归一化换行:客户端可能将 \n 作为字面字符发送
30
+ content = content.replace(/\\n/g, '\n');
31
+
32
+ // 1. 写入 messages 表
33
+ // content_type: 1=文字, 2=图片, 3=文件
34
+ let contentType = 1;
35
+ if (messageType === 'image') contentType = 2;
36
+ else if (messageType === 'file') contentType = 3;
37
+ try {
38
+ db.prepare(`
39
+ INSERT INTO messages (id, from_uid, to_uid, content, channel_id, channel_type, agent_id, timestamp, is_me, status, message_seq, client_msg_no, no_persist, red_dot, sync_once, content_type)
40
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
41
+ `).run(msgId, uid, channelId, content, channelId, 1, agentId, timestamp, 1, 'sent', null, null, 0, 0, 0, contentType);
42
+ } catch (e) {
43
+ if (!e.message.includes('UNIQUE constraint')) {
44
+ console.error('[sendMessage] 写入消息失败:', e.message);
45
+ }
46
+ }
47
+
48
+ // 1.5 自动将接收方加入白名单(静默,不通知)
49
+ // 主动发消息给某人 = 信任该访客,避免对方回复时被私密模式拦截
50
+ try {
51
+ if (!ac.isWhitelisted(db, agentId, channelId)) {
52
+ ac.addEntry(db, { agentId, listType: 'whitelist', visitorId: channelId });
53
+ }
54
+ } catch (_) { /* 静默失败不影响消息发送 */ }
55
+
56
+ // 2. 更新 conversations 表
57
+ try {
58
+ const agentRow = db.prepare(`SELECT imUid FROM agents WHERE agent_id = ?`).get(agentId);
59
+ const convUserUid = agentRow?.imUid || uid;
60
+ const exist = db.prepare(`SELECT user_uid FROM conversations WHERE user_uid = ? AND channel_id = ?`).get(convUserUid, channelId);
61
+ if (exist) {
62
+ db.prepare(`UPDATE conversations SET last_message = ?, last_timestamp = ? WHERE user_uid = ? AND channel_id = ?`)
63
+ .run(content, timestamp, convUserUid, channelId);
64
+ } else {
65
+ db.prepare(`INSERT INTO conversations (user_uid, channel_id, channel_type, name, last_message, last_timestamp, unread_count, agent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
66
+ .run(convUserUid, channelId, 1, channelId, content, timestamp, 0, agentId);
67
+ }
68
+ } catch (e) {
69
+ console.error('[sendMessage] 更新会话失败:', e.message);
70
+ }
71
+
72
+ // 3. 通过 worker 发送(fire and forget)
73
+ entry.worker.send({ type: 'send', channelId, content, messageType: messageType || 'text', localMsgId: msgId });
74
+
75
+ // 4. 通知渲染进程(通过事件总线)
76
+ bus.emit('agent-wukongim:message', {
77
+ agentId, fromUid: uid, toUid: channelId, channelId, content,
78
+ messageId: msgId, timestamp, isMe: true, contentType
79
+ });
80
+
81
+ return { success: true, messageId: msgId };
82
+ };
83
+ }
84
+
85
+ module.exports = { createSendMessageToWorker };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Agent 上下架 / 公开私有状态设置
3
+ *
4
+ * 通过 DID 认证 Ed25519 签名调用服务端接口。
5
+ * 供主进程 IPC 和 MCP 工具共享。
6
+ */
7
+
8
+ const { VOKO_API_URL } = require('./api-signature');
9
+ const { signDidRequest } = require('./did-auth');
10
+
11
+ /**
12
+ * 设置 Agent 上下架和公开私有状态
13
+ * @param {Object} opts
14
+ * @param {Object} opts.db - better-sqlite3 Database 实例
15
+ * @param {string} opts.agentId
16
+ * @param {number} opts.status - 0=下架, 1=上架
17
+ * @param {number} opts.visibility - 0=私有, 1=公开
18
+ * @returns {Promise<{success: boolean, error?: string}>}
19
+ */
20
+ async function setAgentStatus(opts) {
21
+ const { db, agentId, status, visibility } = opts || {};
22
+
23
+ if (!db) return { success: false, error: 'db is required' };
24
+ if (!agentId) return { success: false, error: 'agentId is required' };
25
+ if (status === undefined || visibility === undefined) {
26
+ return { success: false, error: '缺少 status 或 visibility' };
27
+ }
28
+
29
+ try {
30
+ const row = db.prepare(`SELECT did, private_key FROM agents WHERE agent_id = ?`).get(agentId);
31
+ if (!row?.did || !row?.private_key) {
32
+ console.warn(`[setAgentStatus] Agent ${agentId} 无 DID,跳过状态同步`);
33
+ return { success: false, error: 'Agent has no DID' };
34
+ }
35
+
36
+ const businessFields = { status, visibility };
37
+ const signed = await signDidRequest(row.did, row.private_key, businessFields);
38
+
39
+ const requestBody = { ...signed, ...businessFields };
40
+ console.log(`[setAgentStatus] Agent ${agentId}: status=${status}, visibility=${visibility}`);
41
+
42
+ const response = await fetch(`${VOKO_API_URL}/api/did-auth/set-agent-status`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(requestBody)
46
+ });
47
+ const result = await response.json();
48
+ if (result.success) {
49
+ // 同步本地 agents 表状态
50
+ const publishStatus = status === 1 ? 'published' : 'unpublished';
51
+ const accessMode = visibility === 1 ? 'public' : 'private';
52
+ db.prepare(`UPDATE agents SET publish_status=?, access_mode=?, updated_at=? WHERE agent_id=?`)
53
+ .run(publishStatus, accessMode, Date.now(), agentId);
54
+ console.log(`[setAgentStatus] Agent ${agentId} 成功 -> publish_status=${publishStatus}, access_mode=${accessMode}`);
55
+ return { success: true, publishStatus, accessMode };
56
+ }
57
+ console.warn(`[setAgentStatus] Agent ${agentId} 失败:`, result.message);
58
+ return { success: false, error: result.message || '设置状态失败' };
59
+ } catch (e) {
60
+ console.error(`[setAgentStatus] Agent ${agentId} error:`, e);
61
+ return { success: false, error: e.message };
62
+ }
63
+ }
64
+
65
+ module.exports = { setAgentStatus };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Agent 基础信息更新
3
+ *
4
+ * 通过 DID 认证 Ed25519 签名调用服务端更新接口,并同步本地 SQLite。
5
+ * 供主进程 IPC 和 MCP 工具共享。
6
+ */
7
+
8
+ const { signAsync } = require('@noble/ed25519');
9
+ const { VOKO_API_URL } = require('./api-signature');
10
+ const { extractEd25519PrivateKey } = require('./did-auth');
11
+
12
+ /**
13
+ * 更新 Agent 基础信息
14
+ * @param {Object} opts
15
+ * @param {Object} opts.db - better-sqlite3 Database 实例
16
+ * @param {string} opts.agentId
17
+ * @param {string} [opts.name]
18
+ * @param {string} [opts.description]
19
+ * @param {string} [opts.short_description]
20
+ * @param {string} [opts.category]
21
+ * @param {string} [opts.address]
22
+ * @param {string} [opts.contact_phone]
23
+ * @param {string} [opts.icon_url]
24
+ * @param {string} [opts.cover_url]
25
+ * @param {string|string[]} [opts.tags]
26
+ * @returns {Promise<{success: boolean, message?: string, error?: string, data?: any}>}
27
+ */
28
+ async function updateAgentProfile(opts) {
29
+ const { db, agentId, name, description, short_description, category } = opts || {};
30
+ const { address, contact_phone, icon_url, cover_url, tags } = opts || {};
31
+
32
+ if (!db) return { success: false, error: 'db is required' };
33
+ if (!agentId) return { success: false, error: 'agentId is required' };
34
+
35
+ try {
36
+ const stmt = db.prepare(`SELECT did, private_key FROM agents WHERE agent_id = ?`);
37
+ const row = stmt.get(agentId);
38
+ if (!row) return { success: false, error: 'Agent not found' };
39
+ if (!row.did) return { success: false, error: 'Agent has no DID' };
40
+ if (!row.private_key) return { success: false, error: 'Agent has no private key' };
41
+
42
+ // 构建 bodyPayload(仅可更新字段,key 按字母序排序)
43
+ const payload = {};
44
+ if (name !== undefined) payload.name = name;
45
+ if (description !== undefined) payload.description = description;
46
+ if (address !== undefined) payload.address = address;
47
+ if (contact_phone !== undefined) payload.contact_phone = contact_phone;
48
+ if (category !== undefined) payload.category = category;
49
+ if (short_description !== undefined) payload.short_description = short_description;
50
+ if (icon_url !== undefined) payload.icon_url = icon_url;
51
+ if (cover_url !== undefined) payload.cover_url = cover_url;
52
+ if (tags !== undefined) payload.tags = typeof tags === 'string' ? JSON.parse(tags) : tags;
53
+ const bodyPayload = JSON.stringify(payload, Object.keys(payload).sort());
54
+
55
+ // 签名
56
+ const nonce = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
57
+ const timestamp = Math.floor(Date.now() / 1000);
58
+ const toSign = row.did + '\n' + nonce + '\n' + timestamp + '\n' + bodyPayload;
59
+ const rawKey = extractEd25519PrivateKey(row.private_key);
60
+ const sigBytes = await signAsync(new TextEncoder().encode(toSign), rawKey);
61
+ const signature = Buffer.from(sigBytes).toString('base64');
62
+
63
+ const requestBody = { did: row.did, nonce, timestamp, signature, ...payload };
64
+
65
+ console.log(`[updateProfile] Agent ${agentId}: sending...`, JSON.stringify({ did: row.did, fields: Object.keys(payload) }));
66
+
67
+ const response = await fetch(`${VOKO_API_URL}/api/did-auth/update-agent-profile`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify(requestBody)
71
+ });
72
+ const result = await response.json();
73
+ console.log(`[updateProfile] Agent ${agentId} response:`, result);
74
+
75
+ if (result.success) {
76
+ // 本地同步更新
77
+ const sets = ['updated_at = ?'];
78
+ const vals = [Date.now()];
79
+ if (name !== undefined) { sets.push('agent_name = ?'); vals.push(name); }
80
+ if (description !== undefined) { sets.push('description = ?'); vals.push(description); }
81
+ if (address !== undefined) { sets.push('address = ?'); vals.push(address); }
82
+ if (contact_phone !== undefined) { sets.push('contact_phone = ?'); vals.push(contact_phone); }
83
+ if (category !== undefined) { sets.push('category = ?'); vals.push(category); }
84
+ // 从服务端响应中提取 category_label
85
+ const serverCategoryLabel = result.data?.categoryLabel || result.data?.category_label;
86
+ if (serverCategoryLabel) { sets.push('category_label = ?'); vals.push(serverCategoryLabel); }
87
+ if (short_description !== undefined) { sets.push('short_description = ?'); vals.push(short_description); }
88
+ if (icon_url !== undefined) { sets.push('icon_url = ?'); vals.push(icon_url); }
89
+ if (cover_url !== undefined) { sets.push('cover_url = ?'); vals.push(cover_url); }
90
+ if (tags !== undefined) { sets.push('tags = ?'); vals.push(typeof tags === 'string' ? tags : JSON.stringify(tags)); }
91
+ vals.push(agentId);
92
+ db.prepare(`UPDATE agents SET ${sets.join(', ')} WHERE agent_id = ?`).run(...vals);
93
+ return { success: true, message: '基础信息已更新', data: result.data };
94
+ }
95
+ return { success: false, error: result.message || '更新失败', detail: result };
96
+ } catch (e) {
97
+ console.error('[updateProfile] error:', e);
98
+ return { success: false, error: e.message };
99
+ }
100
+ }
101
+
102
+ module.exports = { updateAgentProfile };