@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.
- package/package.json +32 -0
- package/scripts/build-native.js +72 -0
- package/src/bankHeadOffices.js +20543 -0
- package/src/channels/email.js +35 -0
- package/src/channels/feishu.js +31 -0
- package/src/channels/qq-email.js +30 -0
- package/src/channels/registry.js +279 -0
- package/src/channels/telegram.js +28 -0
- package/src/channels/voko-email.js +7 -0
- package/src/channels/wechat.js +35 -0
- package/src/cli.js +120 -0
- package/src/context.js +164 -0
- package/src/core/access-control-api.js +150 -0
- package/src/core/access-control.js +56 -0
- package/src/core/agent-registration.js +319 -0
- package/src/core/api-signature.js +33 -0
- package/src/core/audit.js +133 -0
- package/src/core/database.js +1409 -0
- package/src/core/did-auth.js +54 -0
- package/src/core/hermes-paths.js +57 -0
- package/src/core/invitation.js +49 -0
- package/src/core/lite-bus.js +16 -0
- package/src/core/llm-client.js +1032 -0
- package/src/core/messenger.js +456 -0
- package/src/core/notifier.js +99 -0
- package/src/core/offline-sync.js +150 -0
- package/src/core/payment.js +285 -0
- package/src/core/publish-agent.js +166 -0
- package/src/core/register-capabilities.js +119 -0
- package/src/core/search-capabilities.js +136 -0
- package/src/core/send-message.js +85 -0
- package/src/core/set-agent-status.js +65 -0
- package/src/core/update-agent-profile.js +102 -0
- package/src/core/worker-manager.js +332 -0
- package/src/endpoints.json +21 -0
- package/src/index.js +712 -0
- package/src/mcp/CLAUDE_TEST.md +82 -0
- package/src/mcp/FULL_TEST.md +139 -0
- package/src/mcp/TEST.md +124 -0
- package/src/mcp/TEST_STEPS.md +75 -0
- package/src/mcp/server.js +612 -0
- package/src/mcp/tools.js +1367 -0
- package/src/mcp/transport/http.js +95 -0
- package/src/mcp/transport/stdio.js +20 -0
- package/src/preload.js +27 -0
- package/src/server/agent-email-api.js +120 -0
- package/src/server/agent-manager.js +580 -0
- package/src/server/email-handler.js +329 -0
- package/src/server/feishu-handler.js +249 -0
- package/src/server/hermes-api-client.js +166 -0
- package/src/server/hermes-discovery.js +80 -0
- package/src/server/hermes-handler.js +287 -0
- package/src/server/openclaw-handler-cli.js +131 -0
- package/src/server/openclaw-websocket-handler.js +1290 -0
- package/src/server/oss.js +186 -0
- package/src/server/owner-intervention-notifier.js +320 -0
- package/src/server/release-page.html +204 -0
- package/src/server/telegram-handler.js +208 -0
- package/src/server/voko-email-handler.js +68 -0
- package/src/server/wechat-handler.js +439 -0
- package/src/workers/agent-worker.js +378 -0
- package/src/workers/message-content.js +51 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* offline-sync.js — 离线消息同步
|
|
3
|
+
*
|
|
4
|
+
* 从 WuKongIM 服务端拉取遗漏的离线消息,逐条入库后按组分批转发给 agent。
|
|
5
|
+
* 纯 Node.js,无 Electron 依赖。
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { enqueueDbWrite, waitForDbQueue } = require('./database');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 拉取离线消息并转发
|
|
14
|
+
*
|
|
15
|
+
* @param {object} db - better-sqlite3 实例
|
|
16
|
+
* @param {object} messageHandler - MessageHandler 实例(需有 handleAgentMessage / forwardToAgent)
|
|
17
|
+
* @param {string} [agentIdFilter] - 可选,仅同步指定 agent
|
|
18
|
+
* @returns {Promise<number>} 同步的消息总数
|
|
19
|
+
*/
|
|
20
|
+
async function syncOfflineMessages(db, messageHandler, agentIdFilter) {
|
|
21
|
+
console.log(`[离线同步] 开始拉取离线消息...` + (agentIdFilter ? ` (仅 agent=${agentIdFilter})` : ''));
|
|
22
|
+
try {
|
|
23
|
+
const agents = db.prepare(`SELECT agent_id, imUid, imToken, im_server_url FROM agents WHERE publish_status = 'published'`).all();
|
|
24
|
+
const pendingMessages = [];
|
|
25
|
+
|
|
26
|
+
for (const agent of agents) {
|
|
27
|
+
if (agentIdFilter && agent.agent_id !== agentIdFilter) {
|
|
28
|
+
console.log(`[离线同步] 跳过 agent=${agent.agent_id}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// WukongIM WS → HTTP API 端口映射(默认 WS:5200 → API:5001)
|
|
32
|
+
const httpBase = agent.im_server_url
|
|
33
|
+
.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:')
|
|
34
|
+
.replace(/:5200(?=\/|$)/, ':5001');
|
|
35
|
+
const convs = db.prepare(`SELECT DISTINCT channel_id FROM conversations WHERE agent_id = ?`).all(agent.agent_id);
|
|
36
|
+
|
|
37
|
+
for (const conv of convs) {
|
|
38
|
+
const maxRow = db.prepare(`SELECT MAX(message_seq) as m FROM messages WHERE channel_id = ? AND agent_id = ?`).get(conv.channel_id, agent.agent_id);
|
|
39
|
+
const startSeq = (maxRow?.m || 0) + 1;
|
|
40
|
+
if (startSeq <= 1) continue;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const resp = await fetch(`${httpBase}/channel/messagesync`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
login_uid: agent.imUid,
|
|
48
|
+
channel_id: conv.channel_id,
|
|
49
|
+
channel_type: 1,
|
|
50
|
+
start_message_seq: startSeq,
|
|
51
|
+
end_message_seq: 0,
|
|
52
|
+
limit: 100,
|
|
53
|
+
pull_mode: 1
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
if (!resp.ok) {
|
|
57
|
+
console.warn(`[离线同步] agent=${agent.agent_id} channel=${conv.channel_id} HTTP ${resp.status}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const data = await resp.json();
|
|
61
|
+
const msgs = data.messages || [];
|
|
62
|
+
console.log(`[离线同步] agent=${agent.agent_id} channel=${conv.channel_id} 拉取到 ${msgs.length} 条`);
|
|
63
|
+
|
|
64
|
+
for (const msg of msgs) {
|
|
65
|
+
const msgId = msg.message_id || msg.messageID;
|
|
66
|
+
if (!msgId) continue;
|
|
67
|
+
let content = msg.content || '';
|
|
68
|
+
let contentType = msg.content_type || 1;
|
|
69
|
+
if (msg.payload && !content) {
|
|
70
|
+
try {
|
|
71
|
+
const decoded = JSON.parse(Buffer.from(msg.payload, 'base64').toString());
|
|
72
|
+
content = decoded.content || '';
|
|
73
|
+
contentType = decoded.type || 1;
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
const toUid = msg.from_uid === agent.imUid ? conv.channel_id : agent.imUid;
|
|
77
|
+
pendingMessages.push({
|
|
78
|
+
agentId: agent.agent_id,
|
|
79
|
+
data: {
|
|
80
|
+
fromUid: msg.from_uid || '',
|
|
81
|
+
toUid,
|
|
82
|
+
channelId: conv.channel_id,
|
|
83
|
+
channelType: 1,
|
|
84
|
+
content,
|
|
85
|
+
contentType,
|
|
86
|
+
messageId: msgId,
|
|
87
|
+
timestamp: msg.timestamp || 0,
|
|
88
|
+
messageSeq: msg.message_seq,
|
|
89
|
+
clientMsgNo: msg.client_msg_no,
|
|
90
|
+
noPersist: msg.header?.no_persist ? 1 : 0,
|
|
91
|
+
redDot: msg.header?.red_dot ? 1 : 0,
|
|
92
|
+
syncOnce: msg.header?.sync_once ? 1 : 0
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(`[离线同步] agent=${agent.agent_id} channel=${conv.channel_id} 请求失败:`, e.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`[离线同步] 共收集 ${pendingMessages.length} 条离线消息,开始分组转发...`);
|
|
103
|
+
|
|
104
|
+
// 第一步:逐条入库(skipForward=true,不转发到 agent 后端)
|
|
105
|
+
await new Promise(resolve => {
|
|
106
|
+
enqueueDbWrite(() => {
|
|
107
|
+
for (const p of pendingMessages) {
|
|
108
|
+
messageHandler.handleAgentMessage(p.agentId, p.data, true);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
waitForDbQueue().then(() => setImmediate(resolve));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 第二步:按 agent + 会话分组,合并转发
|
|
115
|
+
const sessions = new Map();
|
|
116
|
+
for (const p of pendingMessages) {
|
|
117
|
+
const key = `${p.agentId}:${p.data.fromUid}`;
|
|
118
|
+
if (!sessions.has(key)) {
|
|
119
|
+
sessions.set(key, {
|
|
120
|
+
agentId: p.agentId, fromUid: p.data.fromUid, toUid: p.data.toUid,
|
|
121
|
+
channelId: p.data.channelId, list: []
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
sessions.get(key).list.push(p.data);
|
|
125
|
+
}
|
|
126
|
+
console.log(`[离线同步] 分组 ${sessions.size} 组,合并转发`);
|
|
127
|
+
for (const [key, session] of sessions) {
|
|
128
|
+
if (session.list.length === 1) {
|
|
129
|
+
enqueueDbWrite(() => messageHandler.handleAgentMessage(session.agentId, session.list[0]));
|
|
130
|
+
} else {
|
|
131
|
+
const lines = [`访客共发送了 ${session.list.length} 条离线消息,请合并回复:\n`];
|
|
132
|
+
for (let i = 0; i < session.list.length; i++) {
|
|
133
|
+
const msg = session.list[i];
|
|
134
|
+
const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleString('zh-CN') : '';
|
|
135
|
+
lines.push(`--- 消息 ${i + 1} (${time}) ---`);
|
|
136
|
+
lines.push(msg.content);
|
|
137
|
+
lines.push('');
|
|
138
|
+
}
|
|
139
|
+
messageHandler.forwardToAgent(session.agentId, session.fromUid, lines.join('\n'), session.channelId, null, null, null, null);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.log(`[离线同步] 完成`);
|
|
143
|
+
return pendingMessages.length;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('[离线同步] 失败:', e.message);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { syncOfflineMessages };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payment.js — 支付处理逻辑
|
|
3
|
+
*
|
|
4
|
+
* 包括支付订单处理、二维码生成、消息发送等核心逻辑。
|
|
5
|
+
* 纯 Node.js,无 Electron 依赖。UI 通知通过 callback 注入。
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const QRCode = require('qrcode');
|
|
11
|
+
const { uploadBase64ToOSS } = require('../server/oss');
|
|
12
|
+
const { signDidRequest } = require('./did-auth');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 处理支付订单
|
|
16
|
+
*
|
|
17
|
+
* @param {object} order - 订单对象 { id, agent_id, visitor_id, from_uid, amount, description }
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {object} deps.db - better-sqlite3 实例
|
|
20
|
+
* @param {object} deps.databaseAPI - 数据库 API
|
|
21
|
+
* @param {object} deps.agentWorkers - Map<agentId, {worker}> IM worker 进程映射
|
|
22
|
+
* @param {object} deps.endpoints - { payment: { baseUrl } }
|
|
23
|
+
* @param {Function} deps.notifyUI - (type, data) => {} 可选 UI 通知回调
|
|
24
|
+
* @param {Function} deps.payLog - (data) => {} 可选支付日志回调
|
|
25
|
+
*/
|
|
26
|
+
async function processPendingPaymentOrder(order, deps) {
|
|
27
|
+
const { db, databaseAPI, agentWorkers, endpoints, notifyUI, payLog } = deps;
|
|
28
|
+
const _log = payLog || (() => {});
|
|
29
|
+
const _notify = notifyUI || (() => {});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// 原子领取:只有当前仍是 pending 才能成功
|
|
33
|
+
const claimed = db.prepare(`UPDATE payment_orders SET status = 'processing' WHERE id = ? AND status = 'pending'`).run(order.id);
|
|
34
|
+
if (claimed.changes === 0) {
|
|
35
|
+
console.log('[Payment] 跳过,订单已被处理, id:', order.id);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 获取 Agent DID 和私钥
|
|
40
|
+
const agentDid = databaseAPI.getAgentDid(order.agent_id);
|
|
41
|
+
const agentKeyRow = db.prepare(`SELECT private_key FROM agents WHERE agent_id = ? AND private_key IS NOT NULL`).get(order.agent_id);
|
|
42
|
+
if (!agentDid || !agentKeyRow) {
|
|
43
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'failed', result: 'Agent 未注册 DID 或未配置私钥' });
|
|
44
|
+
console.error('[Payment] Agent ' + order.agent_id + ' 未注册 DID 或未配置私钥');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const bizFields = {
|
|
49
|
+
agentDid,
|
|
50
|
+
amount: order.amount,
|
|
51
|
+
description: order.description || '支付收款',
|
|
52
|
+
imUid: order.visitor_id || undefined
|
|
53
|
+
};
|
|
54
|
+
const authFields = await signDidRequest(agentDid, agentKeyRow.private_key, bizFields);
|
|
55
|
+
const body = { ...authFields, ...bizFields };
|
|
56
|
+
|
|
57
|
+
const initResp = await fetch(endpoints.payment.baseUrl + '/payment/create-order', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify(body)
|
|
61
|
+
});
|
|
62
|
+
const initResult = await initResp.json();
|
|
63
|
+
|
|
64
|
+
if (!initResult.success) {
|
|
65
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'failed', result: initResult.message || '创建支付失败' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payUrl = initResult.data?.payUrl;
|
|
70
|
+
const orderNo = initResult.data?.orderNo;
|
|
71
|
+
const queryToken = initResult.data?.queryToken || '';
|
|
72
|
+
if (!payUrl) {
|
|
73
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'failed', result: '未获取到支付链接' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (queryToken) {
|
|
78
|
+
db.prepare(`UPDATE payment_orders SET query_token = ? WHERE id = ?`).run(queryToken, order.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 二维码
|
|
82
|
+
let qrImageUrl = '';
|
|
83
|
+
try {
|
|
84
|
+
const qrDataUrl = await QRCode.toDataURL(payUrl, { width: 256, margin: 2 });
|
|
85
|
+
qrImageUrl = await uploadBase64ToOSS(qrDataUrl, `chat/pay/qr_${Date.now()}.png`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
_log({ event: 'qr_fail', orderId: order.id, error: e.message });
|
|
88
|
+
console.error('[Payment] 二维码生成/上传失败:', e.message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 发送给访客
|
|
92
|
+
const entry = agentWorkers?.get(order.agent_id);
|
|
93
|
+
let fromUid = order.from_uid || '';
|
|
94
|
+
if (!fromUid) {
|
|
95
|
+
try {
|
|
96
|
+
const row = db.prepare(`SELECT imUid FROM agents WHERE agent_id = ?`).get(order.agent_id);
|
|
97
|
+
fromUid = row?.imUid || 'voko';
|
|
98
|
+
} catch (_) { fromUid = 'voko'; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const textMsg = `请支付 ¥${order.amount.toFixed(2)},支付链接:${payUrl}`;
|
|
102
|
+
const textMsgId = `pay_msg_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
103
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
104
|
+
|
|
105
|
+
db.prepare(`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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
106
|
+
.run(textMsgId, fromUid, order.visitor_id, textMsg, order.visitor_id, 1, order.agent_id, timestamp, 1, 'sent', null, null, 0, 0, 0, 1);
|
|
107
|
+
|
|
108
|
+
db.prepare(`INSERT OR IGNORE INTO conversations (user_uid, channel_id, channel_type, name, last_message, last_timestamp, unread_count, agent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
109
|
+
.run(fromUid, order.visitor_id, 1, order.visitor_id, textMsg, timestamp, 0, order.agent_id);
|
|
110
|
+
db.prepare(`UPDATE conversations SET last_message = ?, last_timestamp = ? WHERE user_uid = ? AND channel_id = ?`)
|
|
111
|
+
.run(textMsg, timestamp, fromUid, order.visitor_id);
|
|
112
|
+
|
|
113
|
+
if (entry) {
|
|
114
|
+
entry.worker.send({ type: 'send', channelId: order.visitor_id, content: textMsg, messageType: 'text', localMsgId: textMsgId });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (qrImageUrl) {
|
|
118
|
+
const imgMsgId = `pay_msg_img_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
119
|
+
db.prepare(`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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
120
|
+
.run(imgMsgId, fromUid, order.visitor_id, qrImageUrl, order.visitor_id, 1, order.agent_id, timestamp + 1, 1, 'sent', null, null, 0, 0, 0, 2);
|
|
121
|
+
|
|
122
|
+
if (entry) {
|
|
123
|
+
entry.worker.send({ type: 'send', channelId: order.visitor_id, content: qrImageUrl, messageType: 'image', localMsgId: imgMsgId });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// UI 通知(通过回调)
|
|
128
|
+
_notify('agent-wukongim:message', {
|
|
129
|
+
agentId: order.agent_id, fromUid, toUid: order.visitor_id,
|
|
130
|
+
channelId: order.visitor_id, content: textMsg, timestamp
|
|
131
|
+
});
|
|
132
|
+
if (qrImageUrl) {
|
|
133
|
+
_notify('agent-wukongim:message', {
|
|
134
|
+
agentId: order.agent_id, fromUid, toUid: order.visitor_id,
|
|
135
|
+
channelId: order.visitor_id, content: qrImageUrl, contentType: 2, timestamp: timestamp + 1
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'created', order_no: orderNo, pay_url: payUrl });
|
|
140
|
+
_log({ event: 'order_created', orderId: order.id, orderNo });
|
|
141
|
+
console.log('[Payment] 订单已创建并通知访客:', order.id, orderNo);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
_log({ event: 'pending_fail', orderId: order.id, error: e.message });
|
|
144
|
+
console.error('[Payment] 处理 pending 订单失败:', order.id, e.message);
|
|
145
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'failed', result: e.message });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 启动支付轮询
|
|
151
|
+
*
|
|
152
|
+
* 每 5 秒检查 created 订单:超 30 分钟 → expired,成功支付 → paid 并通知各方。
|
|
153
|
+
*
|
|
154
|
+
* @param {object} deps
|
|
155
|
+
* @param {object} deps.db
|
|
156
|
+
* @param {object} deps.databaseAPI
|
|
157
|
+
* @param {object} deps.agentWorkers - Map<agentId, {worker}>
|
|
158
|
+
* @param {object} deps.endpoints - { payment: { baseUrl } }
|
|
159
|
+
* @param {object} deps.hermesHandler
|
|
160
|
+
* @param {object} deps.openclawHandler
|
|
161
|
+
* @param {Function} deps.sendSystemMessage - (agentId, visitorId, content) => {}
|
|
162
|
+
* @param {Function} deps.payLog - (data) => {}
|
|
163
|
+
* @param {object} deps.ownerInterventionNotifier - 可选
|
|
164
|
+
*/
|
|
165
|
+
function startPaymentPolling(deps) {
|
|
166
|
+
const { db, databaseAPI, agentWorkers, endpoints, hermesHandler, openclawHandler, sendSystemMessage, payLog, ownerInterventionNotifier } = deps;
|
|
167
|
+
const _log = payLog || (() => {});
|
|
168
|
+
const _sendMsg = sendSystemMessage || (() => {});
|
|
169
|
+
|
|
170
|
+
let pollInterval = 5000;
|
|
171
|
+
let pollTimer = null;
|
|
172
|
+
|
|
173
|
+
const scheduleNext = () => {
|
|
174
|
+
pollTimer = setTimeout(doPoll, pollInterval);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const doPoll = async () => {
|
|
178
|
+
try {
|
|
179
|
+
const createdOrders = databaseAPI.getPaymentOrdersByStatus('created');
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
for (const order of createdOrders) {
|
|
182
|
+
try {
|
|
183
|
+
// 超 30 分钟未支付 → expired
|
|
184
|
+
if (now - order.updated_at > 30 * 60 * 1000) {
|
|
185
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'expired' });
|
|
186
|
+
try {
|
|
187
|
+
const expWorker = agentWorkers?.get(order.agent_id);
|
|
188
|
+
if (expWorker) {
|
|
189
|
+
const isTimed = order.type === 'timed';
|
|
190
|
+
const expireMsg = isTimed
|
|
191
|
+
? '【系统消息】支付超时,订单已过期。如需继续使用请回复"购买"。'
|
|
192
|
+
: '【系统消息】支付超时!订单号:' + (order.order_no || '-') + ',项目:' + (order.description || '无') + ',金额:¥' + order.amount.toFixed(2) + '。支付链接已过期,如需重新购买请联系 Agent。';
|
|
193
|
+
const uid = db.prepare(`SELECT imUid FROM agents WHERE agent_id = ?`).get(order.agent_id)?.imUid;
|
|
194
|
+
if (uid) _sendMsg(order.agent_id, order.visitor_id, expireMsg);
|
|
195
|
+
}
|
|
196
|
+
} catch (e) { console.error('[Payment] 通知访客超时失败:', e.message); }
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const qt = db.prepare(`SELECT query_token FROM payment_orders WHERE id = ?`).get(order.id);
|
|
201
|
+
const queryToken = qt?.query_token || '';
|
|
202
|
+
const queryResp = await fetch(endpoints.payment.baseUrl + '/payment/order/' + order.order_no + '?token=' + queryToken, { method: 'GET' });
|
|
203
|
+
const queryResult = await queryResp.json();
|
|
204
|
+
|
|
205
|
+
if (queryResult.success && queryResult.data?.status === 1) {
|
|
206
|
+
const paidData = queryResult.data;
|
|
207
|
+
paidData.transaction_no = paidData.transactionNo || paidData.tradeNo || paidData.thirdTradeNo || '';
|
|
208
|
+
databaseAPI.updatePaymentOrder(order.id, { status: 'paid', result: JSON.stringify(paidData) });
|
|
209
|
+
|
|
210
|
+
// 激活会话
|
|
211
|
+
try {
|
|
212
|
+
if (order.type === 'timed') {
|
|
213
|
+
const pa = db.prepare('SELECT duration_minutes FROM agent_pricing WHERE agent_id = ? AND enabled = 1').get(order.agent_id);
|
|
214
|
+
if (pa && pa.duration_minutes) {
|
|
215
|
+
const conv = db.prepare('SELECT session_status, session_expire_at FROM conversations WHERE user_uid = ? AND channel_id = ?').get(order.from_uid, order.visitor_id);
|
|
216
|
+
const n2 = Date.now();
|
|
217
|
+
const dur = pa.duration_minutes * 60 * 1000;
|
|
218
|
+
const expireAt = (conv && conv.session_status === 'active' && conv.session_expire_at > n2) ? conv.session_expire_at + dur : n2 + dur;
|
|
219
|
+
if (conv) {
|
|
220
|
+
db.prepare('UPDATE conversations SET session_status=?, session_expire_at=? WHERE user_uid=? AND channel_id=?').run('active', expireAt, order.from_uid, order.visitor_id);
|
|
221
|
+
} else {
|
|
222
|
+
db.prepare(`INSERT INTO conversations (user_uid, channel_id, channel_type, name, last_message, last_timestamp, session_status, session_expire_at, agent_id) VALUES (?, ?, 1, ?, '', ?, 'active', ?, ?)`).run(order.from_uid, order.visitor_id, order.visitor_id, n2, expireAt, order.agent_id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (e) { console.error('[计费] 激活会话失败:', e.message); }
|
|
227
|
+
|
|
228
|
+
// 通知访客
|
|
229
|
+
try {
|
|
230
|
+
const payWorker = agentWorkers?.get(order.agent_id);
|
|
231
|
+
if (payWorker) {
|
|
232
|
+
const isTimed = order.type === 'timed';
|
|
233
|
+
const payMsg = isTimed
|
|
234
|
+
? '【系统消息】支付成功!已开通使用时长,请继续与 Agent 对话。'
|
|
235
|
+
: '【系统消息】支付成功!金额:¥' + order.amount.toFixed(2) + ',项目:' + (order.description || '无') + ',订单号:' + (order.order_no || '-') + '。已通知 Agent 准备后续服务。';
|
|
236
|
+
_sendMsg(order.agent_id, order.visitor_id, payMsg);
|
|
237
|
+
}
|
|
238
|
+
} catch (e) { console.error('[Payment] 通知访客支付结果失败:', e.message); }
|
|
239
|
+
|
|
240
|
+
// 通知 agent
|
|
241
|
+
try {
|
|
242
|
+
const payAgent = db.prepare(`SELECT backend_type FROM agents WHERE agent_id = ?`).get(order.agent_id);
|
|
243
|
+
const payBackend = payAgent?.backend_type || 'openclaw';
|
|
244
|
+
const payMsg2 = `[Payment Notification]\n访客: ${order.visitor_id}\n金额: ¥${order.amount.toFixed(2)}\n描述: ${order.description || '无'}\n订单号: ${order.order_no || '-'}\n交易流水号: ${paidData.transactionNo || ''}`;
|
|
245
|
+
if (payBackend === 'hermes' && hermesHandler?.connected) {
|
|
246
|
+
hermesHandler.steer(`hermes:${order.agent_id}:${order.visitor_id}`, payMsg2);
|
|
247
|
+
} else if (openclawHandler?.connected) {
|
|
248
|
+
openclawHandler.sendToSession(`agent:${order.agent_id}:${order.visitor_id}`, payMsg2);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) { console.error('[Payment] 通知 agent 失败:', e.message); }
|
|
251
|
+
|
|
252
|
+
// 通知主人
|
|
253
|
+
try {
|
|
254
|
+
const now2 = Date.now();
|
|
255
|
+
const oiId = 'pay_' + now2 + '_' + Math.random().toString(36).substr(2, 6);
|
|
256
|
+
const ownerMsg = '💰 支付成功通知\nAgent: ' + order.agent_id + '\n访客: ' + order.visitor_id + '\n金额: ¥' + order.amount.toFixed(2);
|
|
257
|
+
const payPrefix = (db.prepare(`SELECT backend_type FROM agents WHERE agent_id = ?`).get(order.agent_id)?.backend_type) === 'hermes' ? 'hermes' : 'agent';
|
|
258
|
+
databaseAPI.saveOwnerIntervention({
|
|
259
|
+
id: oiId, visitorId: order.visitor_id, sessionKey: payPrefix + ':' + order.agent_id + ':' + order.visitor_id,
|
|
260
|
+
problem: ownerMsg, agentSuggestion: '支付成功通知,无需回复', askTime: now2,
|
|
261
|
+
status: 'pending', channelType: 'voko', createdAt: now2, updatedAt: now2, agentId: order.agent_id,
|
|
262
|
+
});
|
|
263
|
+
if (ownerInterventionNotifier) ownerInterventionNotifier.enqueue({ id: oiId, visitorId: order.visitor_id, agentId: order.agent_id, sessionKey: payPrefix + ':' + order.agent_id + ':' + order.visitor_id, problem: ownerMsg, agentSuggestion: '支付成功通知,无需回复', askTime: now2, skipReply: 1 });
|
|
264
|
+
} catch (e) { console.error('[Payment] 通知主人失败:', e.message); }
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
_log({ event: 'query_fail', orderId: order.id, error: e.message });
|
|
268
|
+
console.error('[Payment] 查询订单状态失败:', order.id, e.message);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
_log({ event: 'poll_error', error: err.message });
|
|
273
|
+
console.error('[Payment] 支付轮询错误:', err.message);
|
|
274
|
+
}
|
|
275
|
+
scheduleNext();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
scheduleNext();
|
|
279
|
+
console.log('[Payment] 启动支付轮询,初始间隔 5s');
|
|
280
|
+
|
|
281
|
+
// 返回停止函数
|
|
282
|
+
return () => { if (pollTimer) clearTimeout(pollTimer); };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { processPendingPaymentOrder, startPaymentPolling };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 发布/下架核心逻辑
|
|
3
|
+
*
|
|
4
|
+
* 把原来 main.js 中 agent-wukongim:connect / disconnect 的业务逻辑抽取出来,
|
|
5
|
+
* 供主进程 IPC 和 MCP 工具共享。
|
|
6
|
+
*
|
|
7
|
+
* 注意:本模块零 Electron 依赖,所有副作用(启动/停止 worker、能力注册、通知 UI 等)
|
|
8
|
+
* 都通过 opts 注入。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 发布 Agent:启动 Worker、连接 IM、注册能力、同步资料、同步服务端状态
|
|
13
|
+
* @param {Object} opts
|
|
14
|
+
* @param {Object} opts.db - better-sqlite3 Database 实例
|
|
15
|
+
* @param {string} opts.agentId
|
|
16
|
+
* @param {Function} opts.startAgentWorker - (agentId, config) => void
|
|
17
|
+
* @param {Function} opts.stopAgentWorker - (agentId) => Promise|void
|
|
18
|
+
* @param {Function} opts.registerCapabilities - (agentId, options?) => Promise
|
|
19
|
+
* @param {Function} opts.updateAgentProfile - (params) => Promise
|
|
20
|
+
* @param {Function} opts.setAgentStatus - (params) => Promise
|
|
21
|
+
* @param {Object} [opts.endpoints] - 端点配置,用于 chatroom_url
|
|
22
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
23
|
+
*/
|
|
24
|
+
async function publishAgent(opts) {
|
|
25
|
+
const { db, agentId, startAgentWorker, stopAgentWorker, registerCapabilities, updateAgentProfile, setAgentStatus, endpoints } = opts || {};
|
|
26
|
+
|
|
27
|
+
if (!db) return { success: false, error: 'db is required' };
|
|
28
|
+
if (!agentId) return { success: false, error: 'agentId is required' };
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const row = db.prepare(`SELECT * FROM agents WHERE agent_id = ?`).get(agentId);
|
|
32
|
+
if (!row) return { success: false, error: 'Agent not found' };
|
|
33
|
+
if (!row.imUid || !row.imToken || !row.im_server_url) {
|
|
34
|
+
return { success: false, error: 'Agent 缺少 IM 绑定信息' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { imUid: uid, imToken: token, im_server_url: serverUrl } = row;
|
|
38
|
+
|
|
39
|
+
// 如果该 uid 已绑定其他 agent,先解除其绑定
|
|
40
|
+
const existingStmt = db.prepare(`SELECT agent_id FROM agents WHERE imUid = ? AND agent_id != ?`);
|
|
41
|
+
const existing = existingStmt.get(uid, agentId);
|
|
42
|
+
if (existing) {
|
|
43
|
+
db.prepare(`UPDATE agents SET publish_status = 'unpublished' WHERE agent_id = ?`).run(existing.agent_id);
|
|
44
|
+
if (stopAgentWorker) {
|
|
45
|
+
try { await stopAgentWorker(existing.agent_id); } catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
console.log(`[${existing.agent_id}] WuKongIM 账号已被其他 agent 占用,已自动下架`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 启动 Worker 进程
|
|
51
|
+
if (startAgentWorker) {
|
|
52
|
+
startAgentWorker(agentId, { uid, token, serverUrl });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const imBaseUrl = endpoints?.im?.baseUrl || '';
|
|
57
|
+
const chatroomUrl = uid && imBaseUrl ? imBaseUrl + '/#/chat?peer=' + uid : '';
|
|
58
|
+
const backend = row.backend_type || 'openclaw';
|
|
59
|
+
// 上架不影响 access_mode,由 voko_set_private_mode 专门控制
|
|
60
|
+
const accessMode = row.access_mode || 'public';
|
|
61
|
+
const visibility = accessMode === 'private' ? 0 : 1;
|
|
62
|
+
|
|
63
|
+
db.prepare(`
|
|
64
|
+
INSERT INTO agents (id, agent_id, imUid, imToken, im_server_url, owner_email, chatroom_url, agent_name, category, category_label, publish_status, access_mode, backend_type, created_at, updated_at)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', ?, ?, ?, ?)
|
|
66
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
67
|
+
imUid = excluded.imUid,
|
|
68
|
+
imToken = excluded.imToken,
|
|
69
|
+
im_server_url = excluded.im_server_url,
|
|
70
|
+
owner_email = excluded.owner_email,
|
|
71
|
+
chatroom_url = excluded.chatroom_url,
|
|
72
|
+
agent_name = excluded.agent_name,
|
|
73
|
+
category = excluded.category,
|
|
74
|
+
category_label = excluded.category_label,
|
|
75
|
+
publish_status = 'published',
|
|
76
|
+
backend_type = excluded.backend_type,
|
|
77
|
+
updated_at = excluded.updated_at
|
|
78
|
+
`).run(`agent-${agentId}`, agentId, uid, token, serverUrl, row.owner_email || null, chatroomUrl, row.agent_name || null, row.category || null, row.category_label || null, accessMode, backend, now, now);
|
|
79
|
+
|
|
80
|
+
// 注册能力到服务端
|
|
81
|
+
if (registerCapabilities) {
|
|
82
|
+
try { await registerCapabilities(agentId); } catch (e) {
|
|
83
|
+
console.warn(`[publishAgent] Agent ${agentId} 注册能力失败:`, e.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 同步本地已有资料字段到服务端
|
|
88
|
+
if (updateAgentProfile) {
|
|
89
|
+
try {
|
|
90
|
+
const fresh = db.prepare(`SELECT * FROM agents WHERE agent_id = ?`).get(agentId);
|
|
91
|
+
const fields = {};
|
|
92
|
+
if (fresh?.description) fields.description = fresh.description;
|
|
93
|
+
if (fresh?.short_description) fields.short_description = fresh.short_description;
|
|
94
|
+
if (fresh?.address) fields.address = fresh.address;
|
|
95
|
+
if (fresh?.contact_phone) fields.contact_phone = fresh.contact_phone;
|
|
96
|
+
if (fresh?.tags) fields.tags = fresh.tags;
|
|
97
|
+
if (fresh?.category) fields.category = fresh.category;
|
|
98
|
+
if (fresh?.icon_url) fields.icon_url = fresh.icon_url;
|
|
99
|
+
if (Object.keys(fields).length) {
|
|
100
|
+
await updateAgentProfile({ agentId, ...fields });
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.warn(`[publishAgent] Agent ${agentId} 同步资料失败:`, e.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 同步上下架状态到服务端(保持当前 access_mode 对应的 visibility)
|
|
108
|
+
if (setAgentStatus) {
|
|
109
|
+
setAgentStatus({ agentId, status: 1, visibility }).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { success: true, publishStatus: 'published', accessMode };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`[publishAgent] Agent ${agentId} error:`, e);
|
|
115
|
+
return { success: false, error: e.message };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 下架 Agent:能力设为不可发现、停止 Worker、更新本地状态、同步服务端状态
|
|
121
|
+
* @param {Object} opts
|
|
122
|
+
* @param {Object} opts.db - better-sqlite3 Database 实例
|
|
123
|
+
* @param {string} opts.agentId
|
|
124
|
+
* @param {Function} opts.stopAgentWorker - (agentId) => Promise|void
|
|
125
|
+
* @param {Function} opts.registerCapabilities - (agentId, options?) => Promise
|
|
126
|
+
* @param {Function} opts.setAgentStatus - (params) => Promise
|
|
127
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
128
|
+
*/
|
|
129
|
+
async function unpublishAgent(opts) {
|
|
130
|
+
const { db, agentId, stopAgentWorker, registerCapabilities, setAgentStatus } = opts || {};
|
|
131
|
+
|
|
132
|
+
if (!db) return { success: false, error: 'db is required' };
|
|
133
|
+
if (!agentId) return { success: false, error: 'agentId is required' };
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// 读取当前可见性,下架时保持其不变
|
|
137
|
+
const currentRow = db.prepare(`SELECT access_mode FROM agents WHERE agent_id = ?`).get(agentId);
|
|
138
|
+
const currentAccessMode = currentRow?.access_mode || 'public';
|
|
139
|
+
const visibility = currentAccessMode === 'private' ? 0 : 1;
|
|
140
|
+
|
|
141
|
+
// 通知服务端该 agent 不再可发现
|
|
142
|
+
if (registerCapabilities) {
|
|
143
|
+
try { await registerCapabilities(agentId, { discoverable: false }); } catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 同步上下架状态到服务端(保持原有 visibility)
|
|
147
|
+
if (setAgentStatus) {
|
|
148
|
+
setAgentStatus({ agentId, status: 0, visibility }).catch(() => {});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 停止 Worker
|
|
152
|
+
if (stopAgentWorker) {
|
|
153
|
+
try { await stopAgentWorker(agentId); } catch (_) {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 更新本地 DB(只改 publish_status,保留 access_mode)
|
|
157
|
+
db.prepare(`UPDATE agents SET publish_status = 'unpublished', updated_at = ? WHERE agent_id = ?`).run(Date.now(), agentId);
|
|
158
|
+
|
|
159
|
+
return { success: true, publishStatus: 'unpublished' };
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error(`[unpublishAgent] Agent ${agentId} error:`, e);
|
|
162
|
+
return { success: false, error: e.message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { publishAgent, unpublishAgent };
|