evolclaw 2.0.0
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/README.md +191 -0
- package/bin/evolclaw +10 -0
- package/data/evolclaw.sample.json +39 -0
- package/dist/channels/aun.js +28 -0
- package/dist/channels/feishu.js +452 -0
- package/dist/cli.js +759 -0
- package/dist/config.js +81 -0
- package/dist/core/agent-runner.js +326 -0
- package/dist/core/command-handler.js +823 -0
- package/dist/core/message-cache.js +56 -0
- package/dist/core/message-processor.js +516 -0
- package/dist/core/message-queue.js +110 -0
- package/dist/core/message-stream.js +59 -0
- package/dist/core/session-manager.js +803 -0
- package/dist/index.js +239 -0
- package/dist/paths.js +45 -0
- package/dist/types.js +1 -0
- package/dist/utils/error-utils.js +54 -0
- package/dist/utils/init.js +352 -0
- package/dist/utils/logger.js +47 -0
- package/dist/utils/markdown-to-feishu.js +38 -0
- package/dist/utils/permission.js +36 -0
- package/dist/utils/session-file-health.js +67 -0
- package/dist/utils/stream-flusher.js +151 -0
- package/dist/utils/stream-idle-monitor.js +103 -0
- package/package.json +38 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import imageType from 'image-type';
|
|
5
|
+
import { ensureDir } from '../config.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { markdownToFeishuPost, hasMarkdownSyntax } from '../utils/markdown-to-feishu.js';
|
|
8
|
+
export class FeishuChannel {
|
|
9
|
+
config;
|
|
10
|
+
client = null;
|
|
11
|
+
wsClient = null;
|
|
12
|
+
messageHandler;
|
|
13
|
+
projectPathProvider;
|
|
14
|
+
db;
|
|
15
|
+
cleanupInterval;
|
|
16
|
+
chatTypeCache = new Map();
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.db = config.db;
|
|
20
|
+
this.initChatTypeTable();
|
|
21
|
+
}
|
|
22
|
+
initChatTypeTable() {
|
|
23
|
+
this.db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS chat_types (
|
|
25
|
+
chat_id TEXT PRIMARY KEY,
|
|
26
|
+
chat_mode TEXT NOT NULL,
|
|
27
|
+
updated_at INTEGER NOT NULL
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
async connect() {
|
|
32
|
+
// 检查配置有效性
|
|
33
|
+
if (!this.config.appId || !this.config.appSecret) {
|
|
34
|
+
throw new Error('Feishu credentials missing (appId or appSecret is empty)');
|
|
35
|
+
}
|
|
36
|
+
if (this.config.appId.startsWith('YOUR_') || this.config.appSecret.startsWith('YOUR_')) {
|
|
37
|
+
throw new Error('Feishu credentials not configured (placeholder values detected)');
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
this.client = new lark.Client({
|
|
41
|
+
appId: this.config.appId,
|
|
42
|
+
appSecret: this.config.appSecret,
|
|
43
|
+
});
|
|
44
|
+
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
45
|
+
'im.message.receive_v1': async (data) => {
|
|
46
|
+
const msg = data.message;
|
|
47
|
+
logger.debug('[Feishu] Received message, message_id:', msg.message_id, 'type:', msg.message_type);
|
|
48
|
+
logger.debug('[Feishu] Full data object:', JSON.stringify(data, null, 2));
|
|
49
|
+
if (!msg.message_id || this.isDuplicate(msg.message_id)) {
|
|
50
|
+
logger.debug('[Feishu] Duplicate message ignored:', msg.message_id);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.markSeen(msg.message_id);
|
|
54
|
+
this.addAckReaction(msg.message_id);
|
|
55
|
+
if (!this.messageHandler)
|
|
56
|
+
return;
|
|
57
|
+
// 提取发送者信息
|
|
58
|
+
const userId = data.sender?.sender_id?.open_id;
|
|
59
|
+
let userName;
|
|
60
|
+
try {
|
|
61
|
+
userName = await this.getUserName(userId);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
userName = undefined;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
// 处理引用消息
|
|
68
|
+
let quotedText = '';
|
|
69
|
+
let quotedImages = [];
|
|
70
|
+
if (msg.parent_id && this.client) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await this.client.im.message.get({
|
|
73
|
+
path: { message_id: msg.parent_id }
|
|
74
|
+
});
|
|
75
|
+
if (!res.data?.items?.[0]?.body) {
|
|
76
|
+
throw new Error('Invalid response');
|
|
77
|
+
}
|
|
78
|
+
const quotedMsgType = res.data.items[0].msg_type;
|
|
79
|
+
const quotedContent = res.data.items[0].body.content;
|
|
80
|
+
if (quotedMsgType === 'text') {
|
|
81
|
+
const parsed = JSON.parse(quotedContent);
|
|
82
|
+
quotedText = `> ${parsed.text}\n\n`;
|
|
83
|
+
}
|
|
84
|
+
else if (quotedMsgType === 'post') {
|
|
85
|
+
const parsed = JSON.parse(quotedContent);
|
|
86
|
+
logger.info('[Feishu] Post message structure:', JSON.stringify(parsed, null, 2));
|
|
87
|
+
let text = '';
|
|
88
|
+
const content = parsed.zh_cn?.content || parsed.en_us?.content || parsed.content;
|
|
89
|
+
if (content) {
|
|
90
|
+
for (const line of content) {
|
|
91
|
+
for (const elem of line) {
|
|
92
|
+
if (elem.text)
|
|
93
|
+
text += elem.text;
|
|
94
|
+
}
|
|
95
|
+
text += '\n';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
quotedText = `> ${text.trim()}\n\n`;
|
|
99
|
+
}
|
|
100
|
+
else if (quotedMsgType === 'image') {
|
|
101
|
+
const parsed = JSON.parse(quotedContent);
|
|
102
|
+
const imageKey = parsed.image_key;
|
|
103
|
+
const projectPath = this.projectPathProvider
|
|
104
|
+
? await this.projectPathProvider(msg.chat_id)
|
|
105
|
+
: process.cwd();
|
|
106
|
+
const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.parent_id, projectPath);
|
|
107
|
+
if (imageData) {
|
|
108
|
+
quotedImages.push(imageData);
|
|
109
|
+
quotedText = `> [引用的图片]\n\n`;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
quotedText = `> [图片消息]\n\n`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (quotedMsgType === 'file') {
|
|
116
|
+
quotedText = `> [文件消息]\n\n`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
quotedText = `> [${quotedMsgType}消息]\n\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.warn({ err }, '[Feishu] Failed to fetch quoted message');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 处理文本消息
|
|
127
|
+
if (msg.message_type === 'text') {
|
|
128
|
+
const parsed = JSON.parse(msg.content);
|
|
129
|
+
// 优先使用 text_without_at_bot(去除机器人 @),否则使用 text
|
|
130
|
+
let content = parsed.text_without_at_bot || parsed.text;
|
|
131
|
+
// 去除消息中所有的 @ 提及(支持命令在前或在后)
|
|
132
|
+
content = content.replace(/@[^\s]+\s*/g, '').trim();
|
|
133
|
+
const finalContent = quotedText + content;
|
|
134
|
+
await this.messageHandler(msg.chat_id, finalContent, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
135
|
+
}
|
|
136
|
+
// 处理图片消息
|
|
137
|
+
else if (msg.message_type === 'image') {
|
|
138
|
+
const imageContent = JSON.parse(msg.content);
|
|
139
|
+
const imageKey = imageContent.image_key;
|
|
140
|
+
logger.debug('[Feishu] Received image message, image_key:', imageKey, 'message_id:', msg.message_id);
|
|
141
|
+
const projectPath = this.projectPathProvider
|
|
142
|
+
? await this.projectPathProvider(msg.chat_id)
|
|
143
|
+
: process.cwd();
|
|
144
|
+
const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.message_id, projectPath);
|
|
145
|
+
if (imageData) {
|
|
146
|
+
const allImages = [...quotedImages, imageData];
|
|
147
|
+
const prompt = quotedText + '用户发送了一张图片,请分析这张图片的内容。';
|
|
148
|
+
await this.messageHandler(msg.chat_id, prompt, allImages, userId, userName, msg.message_id);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const prompt = quotedText + '[图片下载失败] 应用可能缺少 im:resource 权限';
|
|
152
|
+
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 处理文件消息
|
|
156
|
+
else if (msg.message_type === 'file') {
|
|
157
|
+
const fileContent = JSON.parse(msg.content);
|
|
158
|
+
const fileKey = fileContent.file_key;
|
|
159
|
+
const fileName = fileContent.file_name || 'unknown';
|
|
160
|
+
logger.debug('[Feishu] Received file message, file_key:', fileKey, 'file_name:', fileName);
|
|
161
|
+
const projectPath = this.projectPathProvider
|
|
162
|
+
? await this.projectPathProvider(msg.chat_id)
|
|
163
|
+
: process.cwd();
|
|
164
|
+
const filePath = await this.downloadFile(fileKey, fileName, msg.message_id, projectPath);
|
|
165
|
+
if (filePath) {
|
|
166
|
+
const prompt = quotedText + `用户发送了文件:${fileName}\n文件已保存到:${filePath}\n请使用 Read 工具读取并分析文件内容。`;
|
|
167
|
+
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const prompt = quotedText + '[文件下载失败] 应用可能缺少 im:resource 权限';
|
|
171
|
+
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 处理其他类型消息
|
|
175
|
+
else {
|
|
176
|
+
logger.debug('[Feishu] Unsupported message type:', msg.message_type);
|
|
177
|
+
const prompt = quotedText + `[不支持的消息类型: ${msg.message_type}]`;
|
|
178
|
+
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logger.error('[Feishu] Failed to process message:', error);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
'im.message.message_read_v1': async () => { }
|
|
186
|
+
});
|
|
187
|
+
this.wsClient = new lark.WSClient({
|
|
188
|
+
appId: this.config.appId,
|
|
189
|
+
appSecret: this.config.appSecret,
|
|
190
|
+
});
|
|
191
|
+
await this.wsClient.start({ eventDispatcher });
|
|
192
|
+
this.startCleanupTask();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof Error) {
|
|
196
|
+
throw new Error(`Feishu connection failed: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
onMessage(handler) {
|
|
202
|
+
this.messageHandler = handler;
|
|
203
|
+
}
|
|
204
|
+
onProjectPathRequest(provider) {
|
|
205
|
+
this.projectPathProvider = provider;
|
|
206
|
+
}
|
|
207
|
+
async getChatMode(chatId) {
|
|
208
|
+
logger.info(`[Feishu] getChatMode called for chatId: ${chatId}`);
|
|
209
|
+
// 检查缓存
|
|
210
|
+
if (this.chatTypeCache.has(chatId)) {
|
|
211
|
+
logger.info(`[Feishu] getChatMode from cache: ${this.chatTypeCache.get(chatId)}`);
|
|
212
|
+
return this.chatTypeCache.get(chatId);
|
|
213
|
+
}
|
|
214
|
+
// 检查数据库
|
|
215
|
+
const row = this.db.prepare('SELECT chat_mode FROM chat_types WHERE chat_id = ?').get(chatId);
|
|
216
|
+
if (row) {
|
|
217
|
+
logger.info(`[Feishu] getChatMode from db: ${row.chat_mode}`);
|
|
218
|
+
this.chatTypeCache.set(chatId, row.chat_mode);
|
|
219
|
+
return row.chat_mode;
|
|
220
|
+
}
|
|
221
|
+
// 调用 API 获取
|
|
222
|
+
if (!this.client)
|
|
223
|
+
return 'p2p';
|
|
224
|
+
try {
|
|
225
|
+
logger.info(`[Feishu] Calling API to get chat mode for ${chatId}`);
|
|
226
|
+
const res = await this.client.im.chat.get({ path: { chat_id: chatId } });
|
|
227
|
+
const chatMode = res.data?.chat_mode || 'p2p';
|
|
228
|
+
logger.info(`[Feishu] API returned chat_mode: ${chatMode}`);
|
|
229
|
+
// 保存到数据库和缓存
|
|
230
|
+
this.db.prepare('INSERT OR REPLACE INTO chat_types (chat_id, chat_mode, updated_at) VALUES (?, ?, ?)').run(chatId, chatMode, Date.now());
|
|
231
|
+
this.chatTypeCache.set(chatId, chatMode);
|
|
232
|
+
return chatMode;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
logger.warn('[Feishu] Failed to get chat mode, defaulting to p2p:', error);
|
|
236
|
+
return 'p2p';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async getUserName(_userId) {
|
|
240
|
+
// TODO: 需要开通 contact:contact.base:readonly 权限后启用
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
async sendMessage(chatId, content, options) {
|
|
244
|
+
if (!this.client)
|
|
245
|
+
return;
|
|
246
|
+
if (!content || content.trim() === '') {
|
|
247
|
+
logger.warn('[Feishu] Attempted to send empty message, skipping');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
|
|
251
|
+
try {
|
|
252
|
+
const useMarkdown = !options?.forceText && hasMarkdownSyntax(content);
|
|
253
|
+
const msgType = useMarkdown ? 'post' : 'text';
|
|
254
|
+
const msgContent = useMarkdown
|
|
255
|
+
? JSON.stringify(markdownToFeishuPost(content, options?.title))
|
|
256
|
+
: JSON.stringify({ text: content });
|
|
257
|
+
if (options?.replyToMessageId) {
|
|
258
|
+
await this.client.im.message.reply({
|
|
259
|
+
path: { message_id: options.replyToMessageId },
|
|
260
|
+
data: { msg_type: msgType, content: msgContent }
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
await this.client.im.message.create({
|
|
265
|
+
params: { receive_id_type: 'chat_id' },
|
|
266
|
+
data: { receive_id: chatId, msg_type: msgType, content: msgContent }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
logger.error('[Feishu] Failed to send message:', error);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async sendFile(chatId, filePath) {
|
|
277
|
+
if (!this.client)
|
|
278
|
+
return;
|
|
279
|
+
try {
|
|
280
|
+
logger.info('[Feishu] Uploading file:', filePath);
|
|
281
|
+
const fileStream = fs.createReadStream(filePath);
|
|
282
|
+
const fileName = path.basename(filePath);
|
|
283
|
+
const uploadResponse = await this.client.im.file.create({
|
|
284
|
+
data: {
|
|
285
|
+
file_type: 'stream',
|
|
286
|
+
file_name: fileName,
|
|
287
|
+
file: fileStream
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (!uploadResponse || !uploadResponse.file_key) {
|
|
291
|
+
logger.error('[Feishu] File upload failed: no file_key returned');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const fileKey = uploadResponse.file_key;
|
|
295
|
+
logger.info('[Feishu] File uploaded, file_key:', fileKey);
|
|
296
|
+
await this.client.im.message.create({
|
|
297
|
+
params: { receive_id_type: 'chat_id' },
|
|
298
|
+
data: {
|
|
299
|
+
receive_id: chatId,
|
|
300
|
+
msg_type: 'file',
|
|
301
|
+
content: JSON.stringify({ file_key: fileKey })
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
logger.info('[Feishu] File message sent successfully');
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger.error('[Feishu] Failed to send file:', error);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async disconnect() {
|
|
312
|
+
if (this.cleanupInterval) {
|
|
313
|
+
clearInterval(this.cleanupInterval);
|
|
314
|
+
this.cleanupInterval = undefined;
|
|
315
|
+
}
|
|
316
|
+
if (this.wsClient) {
|
|
317
|
+
await this.wsClient.close();
|
|
318
|
+
this.wsClient = null;
|
|
319
|
+
}
|
|
320
|
+
this.client = null;
|
|
321
|
+
}
|
|
322
|
+
async downloadAndSaveImage(imageKey, chatId, messageId, projectPath) {
|
|
323
|
+
if (!this.client)
|
|
324
|
+
return null;
|
|
325
|
+
try {
|
|
326
|
+
logger.debug('[Feishu] Downloading image, image_key:', imageKey);
|
|
327
|
+
// 使用 message-resource API 下载用户发送的图片
|
|
328
|
+
const response = await this.client.im.messageResource.get({
|
|
329
|
+
path: {
|
|
330
|
+
message_id: messageId,
|
|
331
|
+
file_key: imageKey
|
|
332
|
+
},
|
|
333
|
+
params: {
|
|
334
|
+
type: 'image'
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// 读取图片数据流并转换为 base64
|
|
338
|
+
if (response && typeof response.getReadableStream === 'function') {
|
|
339
|
+
const stream = response.getReadableStream();
|
|
340
|
+
const chunks = [];
|
|
341
|
+
for await (const chunk of stream) {
|
|
342
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
343
|
+
}
|
|
344
|
+
const buffer = Buffer.concat(chunks);
|
|
345
|
+
if (buffer.length === 0) {
|
|
346
|
+
logger.warn('[Feishu] Empty response from image download');
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
// 使用 image-type 检测真实的图片格式
|
|
350
|
+
const type = await imageType(buffer);
|
|
351
|
+
if (!type) {
|
|
352
|
+
logger.warn('[Feishu] Unable to detect image type');
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
// 白名单验证:只允许常见的图片格式
|
|
356
|
+
const allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
357
|
+
if (!allowedMimes.includes(type.mime)) {
|
|
358
|
+
logger.warn('[Feishu] Unsupported image type:', type.mime);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
// 大小限制:10MB
|
|
362
|
+
if (buffer.length > 10 * 1024 * 1024) {
|
|
363
|
+
logger.warn('[Feishu] Image too large:', buffer.length, 'bytes');
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const base64Data = buffer.toString('base64');
|
|
367
|
+
logger.debug('[Feishu] Image downloaded successfully, type:', type.mime, 'size:', base64Data.length);
|
|
368
|
+
return {
|
|
369
|
+
data: base64Data,
|
|
370
|
+
mimeType: type.mime // 使用真实检测的 MIME 类型
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
logger.error('[Feishu] Image download failed: no valid method');
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logger.error('[Feishu] Failed to download image:', error);
|
|
378
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
379
|
+
const axiosError = error;
|
|
380
|
+
logger.error('[Feishu] Response status:', axiosError.response?.status);
|
|
381
|
+
logger.error('[Feishu] Response data:', JSON.stringify(axiosError.response?.data));
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async downloadFile(fileKey, fileName, messageId, projectPath) {
|
|
387
|
+
if (!this.client)
|
|
388
|
+
return null;
|
|
389
|
+
try {
|
|
390
|
+
logger.debug('[Feishu] Downloading file, file_key:', fileKey, 'file_name:', fileName);
|
|
391
|
+
const response = await this.client.im.messageResource.get({
|
|
392
|
+
path: {
|
|
393
|
+
message_id: messageId,
|
|
394
|
+
file_key: fileKey
|
|
395
|
+
},
|
|
396
|
+
params: {
|
|
397
|
+
type: 'file'
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
if (response && typeof response.getReadableStream === 'function') {
|
|
401
|
+
const stream = response.getReadableStream();
|
|
402
|
+
const chunks = [];
|
|
403
|
+
for await (const chunk of stream) {
|
|
404
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
405
|
+
}
|
|
406
|
+
const buffer = Buffer.concat(chunks);
|
|
407
|
+
if (buffer.length === 0) {
|
|
408
|
+
logger.warn('[Feishu] Empty response from file download');
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const uploadsDir = path.join(projectPath, '.claude', 'uploads');
|
|
412
|
+
ensureDir(uploadsDir);
|
|
413
|
+
const filePath = path.join(uploadsDir, fileName);
|
|
414
|
+
fs.writeFileSync(filePath, buffer);
|
|
415
|
+
logger.info('[Feishu] File downloaded successfully:', filePath, 'size:', buffer.length);
|
|
416
|
+
return filePath;
|
|
417
|
+
}
|
|
418
|
+
logger.error('[Feishu] File download failed: no valid method');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
logger.error('[Feishu] Failed to download file:', error);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
isDuplicate(msgId) {
|
|
427
|
+
const result = this.db.prepare('SELECT 1 FROM processed_messages WHERE message_id = ? LIMIT 1').get(msgId);
|
|
428
|
+
return !!result;
|
|
429
|
+
}
|
|
430
|
+
markSeen(msgId) {
|
|
431
|
+
this.db.prepare('INSERT OR IGNORE INTO processed_messages (message_id, channel, channel_id, processed_at) VALUES (?, ?, ?, ?)').run(msgId, 'feishu', '', Date.now());
|
|
432
|
+
}
|
|
433
|
+
addAckReaction(messageId) {
|
|
434
|
+
if (!this.client)
|
|
435
|
+
return;
|
|
436
|
+
this.client.im.messageReaction.create({
|
|
437
|
+
path: { message_id: messageId },
|
|
438
|
+
data: {
|
|
439
|
+
reaction_type: { emoji_type: 'CheckMark' }
|
|
440
|
+
}
|
|
441
|
+
}).catch(() => { });
|
|
442
|
+
}
|
|
443
|
+
startCleanupTask() {
|
|
444
|
+
this.cleanupInterval = setInterval(() => {
|
|
445
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
446
|
+
const result = this.db.prepare('DELETE FROM processed_messages WHERE processed_at < ?').run(cutoff);
|
|
447
|
+
if (result.changes > 0) {
|
|
448
|
+
logger.info(`[Feishu] Cleaned ${result.changes} old processed messages`);
|
|
449
|
+
}
|
|
450
|
+
}, 60 * 60 * 1000);
|
|
451
|
+
}
|
|
452
|
+
}
|