@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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email 渠道处理器
|
|
3
|
+
* 使用 IMAP (IDLE) 接收邮件,SMTP 发送邮件
|
|
4
|
+
*
|
|
5
|
+
* 用途:
|
|
6
|
+
* - 当 agent 需要主人介入时,通过 Email 通知主人
|
|
7
|
+
* - 主人回复后,将消息转发给对应 agent 的 session
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { ImapFlow } = require('imapflow');
|
|
11
|
+
const nodemailer = require('nodemailer');
|
|
12
|
+
const PostalMime = require('postal-mime');
|
|
13
|
+
|
|
14
|
+
class EmailHandler {
|
|
15
|
+
constructor(config, options = {}) {
|
|
16
|
+
// Email 配置存储
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.ownerEmail = config.ownerEmail;
|
|
19
|
+
this.agentEmail = config.agentEmail;
|
|
20
|
+
this.authCode = config.authCode;
|
|
21
|
+
|
|
22
|
+
// 回调函数
|
|
23
|
+
this.onOwnerReply = options.onOwnerReply || null;
|
|
24
|
+
this.getInterventionByParentMsgId = options.getInterventionByParentMsgId || null;
|
|
25
|
+
this.getLatestPendingIntervention = options.getLatestPendingIntervention || null;
|
|
26
|
+
this.getPendingByAgentAndVisitor = options.getPendingByAgentAndVisitor || null;
|
|
27
|
+
this.isEnabled = options.isEnabled || (() => true);
|
|
28
|
+
|
|
29
|
+
// 状态
|
|
30
|
+
this.imapClient = null;
|
|
31
|
+
this.enabled = false;
|
|
32
|
+
this.pollTimer = null;
|
|
33
|
+
this._generation = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async start() {
|
|
37
|
+
if (this.enabled) {
|
|
38
|
+
console.log('[Email] 已经启动');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.config.smtp || !this.config.imap || !this.config.ownerEmail || !this.config.agentEmail) {
|
|
43
|
+
console.log('[Email] 配置不完整,跳过启动');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.enabled = true;
|
|
48
|
+
console.log('[Email] 启动 IMAP 监听...');
|
|
49
|
+
await this.startImapListener();
|
|
50
|
+
|
|
51
|
+
// 备用轮询:每 30 秒检查一次未读邮件(防止 IDLE 漏接)
|
|
52
|
+
this.pollTimer = setInterval(() => {
|
|
53
|
+
if (this.enabled && this.imapClient) {
|
|
54
|
+
this.checkUnreadEmails().catch(err => {
|
|
55
|
+
console.error('[Email] 轮询错误:', err.message);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}, 30000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async startImapListener() {
|
|
62
|
+
try {
|
|
63
|
+
const imapConfig = this.config.imap;
|
|
64
|
+
|
|
65
|
+
this.imapClient = new ImapFlow({
|
|
66
|
+
host: imapConfig.host,
|
|
67
|
+
port: imapConfig.port,
|
|
68
|
+
secure: imapConfig.secure,
|
|
69
|
+
auth: {
|
|
70
|
+
user: imapConfig.auth.user,
|
|
71
|
+
pass: imapConfig.auth.pass
|
|
72
|
+
},
|
|
73
|
+
logger: false
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await this.imapClient.connect();
|
|
77
|
+
console.log('[Email] IMAP 连接成功');
|
|
78
|
+
|
|
79
|
+
// 使用 select 方法选择收件箱
|
|
80
|
+
try {
|
|
81
|
+
await this.imapClient.mailbox.select('INBOX');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// 使用默认邮箱
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 监听新邮件事件(IDLE 模式)
|
|
87
|
+
this.imapClient.on('mail', (obj) => {
|
|
88
|
+
if (this.getLatestPendingIntervention) {
|
|
89
|
+
const pending = this.getLatestPendingIntervention();
|
|
90
|
+
if (!pending) return;
|
|
91
|
+
}
|
|
92
|
+
const messages = (obj && obj.messages) || [];
|
|
93
|
+
if (messages.length > 0) {
|
|
94
|
+
const latest = messages[messages.length - 1];
|
|
95
|
+
const seqNo = typeof latest === 'number' ? latest : (latest?.uid || latest?.seqNo);
|
|
96
|
+
if (seqNo) {
|
|
97
|
+
this.processOneEmail(seqNo).catch(err => {
|
|
98
|
+
console.error('[Email] 处理新邮件错误:', err.message);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 监听连接错误和关闭,自动重连
|
|
105
|
+
this.imapClient.on('error', (err) => {
|
|
106
|
+
console.error('[Email] IMAP 连接错误:', err.message);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const gen = this._generation;
|
|
110
|
+
this.imapClient.on('close', () => {
|
|
111
|
+
console.log('[Email] IMAP 连接已关闭');
|
|
112
|
+
// 用代数标记避免旧 close 事件覆盖新连接
|
|
113
|
+
if (gen !== this._generation) return;
|
|
114
|
+
this.imapClient = null;
|
|
115
|
+
if (this.enabled) {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
if (gen !== this._generation) return;
|
|
118
|
+
if (this.enabled) {
|
|
119
|
+
this.startImapListener().catch(err => {
|
|
120
|
+
console.error('[Email] 重连失败:', err.message);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, 5000);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 初始检查未读邮件
|
|
128
|
+
await this.checkUnreadEmails();
|
|
129
|
+
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error('[Email] IMAP 连接失败:', err.message);
|
|
132
|
+
this.enabled = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async checkUnreadEmails() {
|
|
137
|
+
if (!this.imapClient) return;
|
|
138
|
+
|
|
139
|
+
// 如果没有 pending 记录,跳过检查
|
|
140
|
+
if (this.getLatestPendingIntervention) {
|
|
141
|
+
const pending = this.getLatestPendingIntervention();
|
|
142
|
+
if (!pending) return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const lock = await this.imapClient.getMailboxLock('INBOX');
|
|
147
|
+
try {
|
|
148
|
+
// search 返回 Array<sequenceNumber>,升序排列(旧→新)
|
|
149
|
+
const seqNumbers = await this.imapClient.search({ seen: false });
|
|
150
|
+
// 只取最近 100 封(最新),倒序从最新开始查
|
|
151
|
+
const recentSeqNos = seqNumbers.slice(-100).reverse();
|
|
152
|
+
|
|
153
|
+
// 从最新到最旧,逐封检查 VOKO 邮件,找到第一个就处理并停止
|
|
154
|
+
for (const seqNo of recentSeqNos) {
|
|
155
|
+
const found = await this.processOneEmail(seqNo);
|
|
156
|
+
if (found) break;
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
if (lock && typeof lock.release === 'function') {
|
|
160
|
+
lock.release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('[Email] 检查未读邮件错误:', err.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async processOneEmail(seqNo) {
|
|
169
|
+
if (!this.enabled || !this.imapClient) return false;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const raw = await this.imapClient.fetchOne(seqNo, { source: true });
|
|
173
|
+
if (!raw || !raw.source) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parser = new PostalMime();
|
|
178
|
+
const parsed = await parser.parse(raw.source);
|
|
179
|
+
|
|
180
|
+
// 只处理 VOKO 干预邮件
|
|
181
|
+
if (!parsed.subject || !parsed.subject.includes('[VOKO]')) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fullContent = parsed.text || parsed.html || '';
|
|
186
|
+
|
|
187
|
+
// 只取回复部分(分隔符之前),去掉原邮件引用
|
|
188
|
+
const separator = '---- 回复的原邮件 ----';
|
|
189
|
+
let content = fullContent;
|
|
190
|
+
const sepIdx = fullContent.indexOf(separator);
|
|
191
|
+
if (sepIdx > 0) {
|
|
192
|
+
content = fullContent.substring(0, sepIdx).trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 从标题提取 clientId(格式: [VOKO] xxx [msg_xxx])
|
|
196
|
+
const match = parsed.subject.match(/\[VOKO\].*\[(msg_\S+)\]/);
|
|
197
|
+
if (!match) {
|
|
198
|
+
console.log('[Email] 标题无追踪 ID,跳过:', parsed.subject);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const msgId = match[1].trim();
|
|
202
|
+
|
|
203
|
+
console.log('[Email] 收到干预回复, msgId:', msgId, 'content:', content.substring(0, 100));
|
|
204
|
+
|
|
205
|
+
if (this.getInterventionByParentMsgId) {
|
|
206
|
+
const intervention = this.getInterventionByParentMsgId(msgId);
|
|
207
|
+
if (intervention) {
|
|
208
|
+
console.log('[Email] 匹配到干预记录:', intervention.id);
|
|
209
|
+
if (this.onOwnerReply) {
|
|
210
|
+
this.onOwnerReply(intervention, content, parsed.messageId);
|
|
211
|
+
}
|
|
212
|
+
// 处理成功后标记为已读
|
|
213
|
+
try {
|
|
214
|
+
await this.imapClient.messageFlagsAdd(seqNo, ['\\Seen']);
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error('[Email] 标记已读失败:', e.message);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error('[Email] 处理邮件错误:', err.message);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async processNewEmails(events) {
|
|
229
|
+
if (!this.enabled || !events || !Array.isArray(events)) return;
|
|
230
|
+
for (const seqNo of events) {
|
|
231
|
+
await this.processOneEmail(seqNo);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async handleReply(messageId, content, replyMessageId) {
|
|
236
|
+
// 1. 精确匹配 messageId → parent_message_id
|
|
237
|
+
if (this.getInterventionByParentMsgId) {
|
|
238
|
+
const intervention = this.getInterventionByParentMsgId(messageId);
|
|
239
|
+
if (intervention) {
|
|
240
|
+
console.log('[Email] 精确匹配到干预记录:', intervention.id);
|
|
241
|
+
if (this.onOwnerReply) {
|
|
242
|
+
this.onOwnerReply(intervention, content, replyMessageId);
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. Fallback 已禁用,只保留精确匹配
|
|
249
|
+
return false;
|
|
250
|
+
// }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 发送邮件给主人(带记录,用于后续匹配)
|
|
255
|
+
*/
|
|
256
|
+
async sendMessageToOwnerWithTracking(content, visitorId, sessionKey, agentId) {
|
|
257
|
+
const clientId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
258
|
+
const subject = agentId
|
|
259
|
+
? `[VOKO] ${agentId}的访客${visitorId}的问题需答复 [${clientId}]`
|
|
260
|
+
: `[VOKO] 访客${visitorId}的问题需答复 [${clientId}]`;
|
|
261
|
+
|
|
262
|
+
// 构造邮件正文(content 已包含全部格式化信息)
|
|
263
|
+
let body = content;
|
|
264
|
+
|
|
265
|
+
if (visitorId.startsWith('system_test:')) {
|
|
266
|
+
body += `\n(这是一条来自 voko 的测试消息)`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const smtpConfig = this.config.smtp;
|
|
271
|
+
|
|
272
|
+
const transporter = nodemailer.createTransport({
|
|
273
|
+
host: smtpConfig.host,
|
|
274
|
+
port: smtpConfig.port,
|
|
275
|
+
secure: smtpConfig.secure,
|
|
276
|
+
auth: {
|
|
277
|
+
user: smtpConfig.auth.user,
|
|
278
|
+
pass: smtpConfig.auth.pass
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
transporter.sendMail({
|
|
283
|
+
from: this.agentEmail,
|
|
284
|
+
to: this.ownerEmail,
|
|
285
|
+
subject,
|
|
286
|
+
text: body
|
|
287
|
+
}, (err, info) => {
|
|
288
|
+
if (err) {
|
|
289
|
+
console.error('[Email] 发送邮件失败:', err.message);
|
|
290
|
+
reject(err);
|
|
291
|
+
} else {
|
|
292
|
+
console.log('[Email] 邮件发送成功, messageId:', clientId);
|
|
293
|
+
resolve({ messageId: clientId, sentMessageId: clientId });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
stop() {
|
|
300
|
+
this.enabled = false;
|
|
301
|
+
this._generation++; // 代数递增,使旧 close 回调失效
|
|
302
|
+
if (this.imapClient) {
|
|
303
|
+
const client = this.imapClient;
|
|
304
|
+
this.imapClient = null;
|
|
305
|
+
if (client.close && typeof client.close.then === 'function') {
|
|
306
|
+
client.close().catch(err => {
|
|
307
|
+
if (err.message !== 'Connection closed') {
|
|
308
|
+
console.error('[Email] 关闭 IMAP 连接错误:', err.message);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (this.pollTimer) {
|
|
314
|
+
clearInterval(this.pollTimer);
|
|
315
|
+
this.pollTimer = null;
|
|
316
|
+
}
|
|
317
|
+
console.log('[Email] 已停止');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
updateConfig(config) {
|
|
321
|
+
if (config.ownerEmail) this.ownerEmail = config.ownerEmail;
|
|
322
|
+
if (config.agentEmail) this.agentEmail = config.agentEmail;
|
|
323
|
+
if (config.authCode) this.authCode = config.authCode;
|
|
324
|
+
if (config.smtp) this.config.smtp = config.smtp;
|
|
325
|
+
if (config.imap) this.config.imap = config.imap;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = EmailHandler;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOKO Desktop - 飞书消息处理器
|
|
3
|
+
* 通过轮询接收飞书消息事件
|
|
4
|
+
*
|
|
5
|
+
* 用途:
|
|
6
|
+
* - 当 agent 需要主人介入时,通过飞书通知主人
|
|
7
|
+
* - 主人回复后,将消息转发给对应 agent 的 session
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Client } = require('@larksuiteoapi/node-sdk');
|
|
11
|
+
|
|
12
|
+
class FeishuHandler {
|
|
13
|
+
constructor(config, options = {}) {
|
|
14
|
+
this.appId = config.appId;
|
|
15
|
+
this.appSecret = config.appSecret;
|
|
16
|
+
this.ownerOpenId = config.ownerOpenId;
|
|
17
|
+
|
|
18
|
+
this.onOwnerReply = options.onOwnerReply;
|
|
19
|
+
this.getInterventionByParentMsgId = options.getInterventionByParentMsgId || null;
|
|
20
|
+
this.getLatestPendingIntervention = options.getLatestPendingIntervention || null;
|
|
21
|
+
|
|
22
|
+
this.pollInterval = config.pollInterval || 5000;
|
|
23
|
+
this._replyPollTimer = null;
|
|
24
|
+
this._ownerChatId = null;
|
|
25
|
+
|
|
26
|
+
this.larkClient = null;
|
|
27
|
+
this.enabled = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async start() {
|
|
31
|
+
if (this.enabled) {
|
|
32
|
+
console.log('[Feishu] 已经启动');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 禁用 axios 自动检测系统代理(飞书 API 直连可达)
|
|
38
|
+
process.env.NO_PROXY = process.env.NO_PROXY ? process.env.NO_PROXY + ',open.feishu.cn' : 'open.feishu.cn';
|
|
39
|
+
|
|
40
|
+
this.larkClient = new Client({
|
|
41
|
+
appId: this.appId,
|
|
42
|
+
appSecret: this.appSecret,
|
|
43
|
+
domain: 'https://open.feishu.cn'
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.enabled = true;
|
|
47
|
+
console.log('[Feishu] 启动回复轮询...');
|
|
48
|
+
this.startReplyPolling();
|
|
49
|
+
console.log('[Feishu] ✅ 飞书处理器已启动');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('[Feishu] 启动失败:', err.message);
|
|
52
|
+
this.enabled = false;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stop() {
|
|
58
|
+
this.larkClient = null;
|
|
59
|
+
this.enabled = false;
|
|
60
|
+
this.stopReplyPolling();
|
|
61
|
+
console.log('[Feishu] 已停止');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============ 回复轮询 ============
|
|
65
|
+
|
|
66
|
+
startReplyPolling() {
|
|
67
|
+
this.stopReplyPolling();
|
|
68
|
+
this._replyPollTimer = setInterval(async () => {
|
|
69
|
+
await this.checkForReplies();
|
|
70
|
+
}, this.pollInterval);
|
|
71
|
+
console.log('[Feishu] 回复轮询已启动,间隔', this.pollInterval, 'ms');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
stopReplyPolling() {
|
|
75
|
+
if (this._replyPollTimer) {
|
|
76
|
+
clearInterval(this._replyPollTimer);
|
|
77
|
+
this._replyPollTimer = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async checkForReplies() {
|
|
82
|
+
if (!this.enabled || !this.getInterventionByParentMsgId || !this.onOwnerReply) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const https = require('https');
|
|
86
|
+
const token = await this._getTenantAccessToken();
|
|
87
|
+
if (!token) return;
|
|
88
|
+
|
|
89
|
+
let chatId = this._ownerChatId;
|
|
90
|
+
if (!chatId) {
|
|
91
|
+
const pending = this.getLatestPendingIntervention ? this.getLatestPendingIntervention() : null;
|
|
92
|
+
if (pending && pending.parentMessageId) {
|
|
93
|
+
const msgInfo = await new Promise((resolve) => {
|
|
94
|
+
const req = https.get(`https://open.feishu.cn/open-apis/im/v1/messages/${pending.parentMessageId}`, {
|
|
95
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
96
|
+
}, (res) => {
|
|
97
|
+
let d = '';
|
|
98
|
+
res.on('data', c => d += c);
|
|
99
|
+
res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve(null); } });
|
|
100
|
+
});
|
|
101
|
+
req.on('error', () => resolve(null));
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
if (msgInfo?.data?.items?.[0]?.chat_id) {
|
|
105
|
+
chatId = msgInfo.data.items[0].chat_id;
|
|
106
|
+
this._ownerChatId = chatId;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!chatId) return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pending = this.getLatestPendingIntervention ? this.getLatestPendingIntervention() : null;
|
|
113
|
+
if (!pending) return;
|
|
114
|
+
|
|
115
|
+
const result = await new Promise((resolve, reject) => {
|
|
116
|
+
const req = https.get(`https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=${chatId}&page_size=20&sort_type=ByCreateTimeDesc`, {
|
|
117
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
118
|
+
}, (res) => {
|
|
119
|
+
let data = '';
|
|
120
|
+
res.on('data', chunk => data += chunk);
|
|
121
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch(e) { resolve(null); } });
|
|
122
|
+
});
|
|
123
|
+
req.on('error', reject);
|
|
124
|
+
req.end();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!result?.data?.items) return;
|
|
128
|
+
|
|
129
|
+
for (const msg of result.data.items) {
|
|
130
|
+
if (!msg.parent_id) continue;
|
|
131
|
+
if (msg.sender?.id !== this.ownerOpenId) continue;
|
|
132
|
+
|
|
133
|
+
const intervention = this.getInterventionByParentMsgId(msg.parent_id);
|
|
134
|
+
if (!intervention) continue;
|
|
135
|
+
|
|
136
|
+
const body = JSON.parse(msg.body?.content || '{}');
|
|
137
|
+
const content = body.text || '';
|
|
138
|
+
console.log('[Feishu] 轮询匹配到主人回复:', { parentId: msg.parent_id, content: content?.substring(0, 50) });
|
|
139
|
+
this.onOwnerReply(intervention, content, msg.message_id);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('[Feishu] 回复轮询错误:', err.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async _getTenantAccessToken() {
|
|
148
|
+
try {
|
|
149
|
+
const https = require('https');
|
|
150
|
+
const data = JSON.stringify({ app_id: this.appId, app_secret: this.appSecret });
|
|
151
|
+
return await new Promise((resolve) => {
|
|
152
|
+
const req = https.request('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
|
|
155
|
+
}, (res) => {
|
|
156
|
+
let body = '';
|
|
157
|
+
res.on('data', chunk => body += chunk);
|
|
158
|
+
res.on('end', () => { try { resolve(JSON.parse(body).tenant_access_token); } catch(e) { resolve(null); } });
|
|
159
|
+
});
|
|
160
|
+
req.on('error', () => resolve(null));
|
|
161
|
+
req.write(data);
|
|
162
|
+
req.end();
|
|
163
|
+
});
|
|
164
|
+
} catch { return null; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============ 发送消息 ============
|
|
168
|
+
|
|
169
|
+
async sendMessageToOwnerWithTracking(content, visitorId, sessionKey, testId) {
|
|
170
|
+
if (!this.larkClient) {
|
|
171
|
+
throw new Error('Feishu client not initialized');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await this.larkClient.im.v1.message.create({
|
|
176
|
+
params: { receive_id_type: 'open_id' },
|
|
177
|
+
data: {
|
|
178
|
+
receive_id: this.ownerOpenId,
|
|
179
|
+
content: JSON.stringify({ text: content }),
|
|
180
|
+
msg_type: 'text'
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const sentMessageId = response?.data?.message_id || response?.message_id || `sent-${Date.now()}`;
|
|
185
|
+
const chatId = response?.data?.chat_id;
|
|
186
|
+
if (chatId) this._ownerChatId = chatId;
|
|
187
|
+
|
|
188
|
+
console.log('[Feishu] 发送消息给主人成功:', { sentMessageId, visitorId, sessionKey });
|
|
189
|
+
|
|
190
|
+
return { messageId: sentMessageId, sentMessageId };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[Feishu] 发送消息失败:', err.message);
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async sendMessageToOwner(content) {
|
|
198
|
+
if (!this.larkClient) {
|
|
199
|
+
throw new Error('Feishu client not initialized');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const response = await this.larkClient.im.v1.message.create({
|
|
204
|
+
params: { receive_id_type: 'open_id' },
|
|
205
|
+
data: {
|
|
206
|
+
receive_id: this.ownerOpenId,
|
|
207
|
+
content: JSON.stringify({ text: content }),
|
|
208
|
+
msg_type: 'text'
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log('[Feishu] 发送消息给主人成功');
|
|
213
|
+
return response;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error('[Feishu] 发送消息失败:', err.message);
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async replyToOwnerWithParentId(sentMessageId, content) {
|
|
221
|
+
if (!this.larkClient) {
|
|
222
|
+
throw new Error('Feishu client not initialized');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const response = await this.larkClient.im.v1.message.create({
|
|
227
|
+
params: { receive_id_type: 'open_id' },
|
|
228
|
+
data: {
|
|
229
|
+
receive_id: this.ownerOpenId,
|
|
230
|
+
content: JSON.stringify({ text: content }),
|
|
231
|
+
msg_type: 'text',
|
|
232
|
+
reply_to_message_id: sentMessageId
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const messageId = response?.data?.message_id || response?.message_id || `reply-${Date.now()}`;
|
|
237
|
+
console.log('[Feishu] 回复主人成功:', messageId, 'parent:', sentMessageId);
|
|
238
|
+
|
|
239
|
+
return { messageId, sentMessageId };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('[Feishu] 回复主人失败:', err.message);
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getOwnerOpenId() { /* removed - unused */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = FeishuHandler;
|