@bundy-lmw/hive-plugin-feishu 1.0.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.
@@ -0,0 +1,730 @@
1
+ /**
2
+ * @bundy-lmw/hive-plugin-feishu - FeishuChannel
3
+ *
4
+ * 飞书通道实现,基于 @larksuiteoapi/node-sdk。
5
+ * 支持 WebSocket 长连接模式(推荐)和 Webhook 模式(备用)。
6
+ */
7
+ import * as lark from '@larksuiteoapi/node-sdk';
8
+ import crypto from 'crypto';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ /**
12
+ * 飞书通道实现
13
+ */
14
+ export class FeishuChannel {
15
+ id;
16
+ name;
17
+ type = 'feishu';
18
+ appId;
19
+ capabilities = {
20
+ sendText: true,
21
+ sendImage: true,
22
+ sendFile: true,
23
+ sendCard: true,
24
+ sendMarkdown: true,
25
+ replyMessage: true,
26
+ editMessage: false,
27
+ deleteMessage: false,
28
+ };
29
+ /** HTTP API 客户端,用于发送消息 */
30
+ client;
31
+ /** WebSocket 客户端,用于接收事件 */
32
+ wsClient;
33
+ /** 事件分发器 */
34
+ dispatcher;
35
+ messageBus;
36
+ logger;
37
+ config;
38
+ /** 文件接收存储目录 */
39
+ receivedDir;
40
+ constructor(config, messageBus, logger, workspaceDir) {
41
+ this.appId = config.appId;
42
+ this.id = `feishu:${config.appId}`;
43
+ this.name = `Feishu Channel (${config.appId.slice(0, 8)})`;
44
+ this.config = config;
45
+ this.messageBus = messageBus;
46
+ this.logger = logger;
47
+ // HTTP API 客户端(发消息用)
48
+ this.client = new lark.Client({
49
+ appId: config.appId,
50
+ appSecret: config.appSecret,
51
+ appType: lark.AppType.SelfBuild,
52
+ domain: config.domain || lark.Domain.Feishu,
53
+ });
54
+ // WebSocket 客户端(接收事件用)
55
+ this.wsClient = new lark.WSClient({
56
+ appId: config.appId,
57
+ appSecret: config.appSecret,
58
+ domain: config.domain || lark.Domain.Feishu,
59
+ autoReconnect: true,
60
+ loggerLevel: lark.LoggerLevel.info,
61
+ });
62
+ // 事件分发器,注册消息接收事件
63
+ this.dispatcher = new lark.EventDispatcher({
64
+ encryptKey: config.encryptKey,
65
+ verificationToken: config.verificationToken,
66
+ }).register({
67
+ 'im.message.receive_v1': async (data) => {
68
+ await this.handleWSMessageEvent(data);
69
+ },
70
+ });
71
+ this.logger.info(`[FeishuChannel] Created channel for app ${this.appId}`);
72
+ // 文件接收目录
73
+ this.receivedDir = workspaceDir
74
+ ? path.join(workspaceDir, 'files', 'feishu', 'received')
75
+ : path.join(process.cwd(), 'files', 'feishu', 'received');
76
+ }
77
+ /**
78
+ * 获取飞书 HTTP API Client
79
+ */
80
+ getClient() {
81
+ return this.client;
82
+ }
83
+ /**
84
+ * 启动通道 — 建立 WebSocket 长连接
85
+ */
86
+ async start() {
87
+ this.logger.info(`[FeishuChannel] Starting WebSocket connection for app ${this.appId}...`);
88
+ await this.wsClient.start({
89
+ eventDispatcher: this.dispatcher,
90
+ });
91
+ this.logger.info(`[FeishuChannel] WebSocket connection established for app ${this.appId}`);
92
+ }
93
+ /**
94
+ * 停止通道 — 关闭 WebSocket 连接
95
+ */
96
+ async stop() {
97
+ this.logger.info(`[FeishuChannel] Stopping WebSocket connection for app ${this.appId}...`);
98
+ this.wsClient.close({ force: false });
99
+ this.logger.info(`[FeishuChannel] WebSocket connection closed for app ${this.appId}`);
100
+ }
101
+ /**
102
+ * 发送消息
103
+ */
104
+ async send(options) {
105
+ try {
106
+ const filePath = options.filePath ?? options.metadata?.filePath;
107
+ const msgType = this.resolveSendType(options.type, filePath);
108
+ // 文件/图片:先上传再发送
109
+ if (msgType === 'file' && filePath) {
110
+ return this.sendFileMessage(options.to, filePath);
111
+ }
112
+ if (msgType === 'image' && filePath) {
113
+ return this.sendImageMessage(options.to, filePath);
114
+ }
115
+ const response = await this.client.im.v1.message.create({
116
+ params: {
117
+ receive_id_type: 'chat_id',
118
+ },
119
+ data: {
120
+ receive_id: options.to,
121
+ msg_type: this.mapMessageType(msgType),
122
+ content: this.buildContent(options),
123
+ },
124
+ });
125
+ if (response.code !== 0) {
126
+ return {
127
+ success: false,
128
+ error: `Feishu API error: ${response.msg}`,
129
+ };
130
+ }
131
+ return {
132
+ success: true,
133
+ messageId: response.data?.message_id,
134
+ };
135
+ }
136
+ catch (error) {
137
+ this.logger.error(`[FeishuChannel] Send message failed:`, error);
138
+ return {
139
+ success: false,
140
+ error: error instanceof Error ? error.message : 'Unknown error',
141
+ };
142
+ }
143
+ }
144
+ /**
145
+ * 回复消息
146
+ */
147
+ async reply(messageId, options) {
148
+ try {
149
+ const filePath = options.filePath ?? options.metadata?.filePath;
150
+ const msgType = this.resolveSendType(options.type, filePath);
151
+ // 文件/图片:先上传再回复
152
+ if (msgType === 'file' && filePath) {
153
+ return this.replyFileMessage(messageId, filePath);
154
+ }
155
+ if (msgType === 'image' && filePath) {
156
+ return this.replyImageMessage(messageId, filePath);
157
+ }
158
+ const response = await this.client.im.v1.message.reply({
159
+ path: {
160
+ message_id: messageId,
161
+ },
162
+ data: {
163
+ content: this.buildContent(options),
164
+ msg_type: this.mapMessageType(msgType),
165
+ },
166
+ });
167
+ if (response.code !== 0) {
168
+ return {
169
+ success: false,
170
+ error: `Feishu API error: ${response.msg}`,
171
+ };
172
+ }
173
+ return {
174
+ success: true,
175
+ messageId: response.data?.message_id,
176
+ };
177
+ }
178
+ catch (error) {
179
+ this.logger.error(`[FeishuChannel] Reply message failed:`, error);
180
+ return {
181
+ success: false,
182
+ error: error instanceof Error ? error.message : 'Unknown error',
183
+ };
184
+ }
185
+ }
186
+ /**
187
+ * 处理 Webhook 请求(Webhook 模式备用)
188
+ */
189
+ async handleWebhook(body, signature, timestamp, nonce) {
190
+ // 验证签名
191
+ if (!this.verifySignature(signature, timestamp, nonce, body)) {
192
+ this.logger.warn(`[FeishuChannel] Invalid signature`);
193
+ throw new Error('Invalid signature');
194
+ }
195
+ // 处理 Challenge
196
+ const challengeReq = body;
197
+ if (challengeReq.type === 'url_verification') {
198
+ this.logger.info(`[FeishuChannel] Challenge received`);
199
+ return { challenge: challengeReq.challenge };
200
+ }
201
+ // 处理消息事件
202
+ const event = body;
203
+ if (event.header?.event_type?.startsWith('im.message.')) {
204
+ await this.handleWebhookMessageEvent(event);
205
+ return { code: 0, msg: 'success' };
206
+ }
207
+ this.logger.debug(`[FeishuChannel] Ignoring event: ${event.header?.event_type}`);
208
+ return { code: 0, msg: 'ignored' };
209
+ }
210
+ // ============================
211
+ // WebSocket 事件处理
212
+ // ============================
213
+ /**
214
+ * 处理 WebSocket 接收到的消息事件
215
+ */
216
+ async handleWSMessageEvent(data) {
217
+ const message = await this.convertWSMessage(data);
218
+ if (message) {
219
+ this.logger.info(`[FeishuChannel] [WS] Received message from ${message.from.id}: ${message.content.slice(0, 50)}`);
220
+ this.messageBus.publish(`channel:${this.id}:message:received`, message);
221
+ }
222
+ }
223
+ /**
224
+ * 转换 WebSocket 事件数据为通用消息格式
225
+ */
226
+ async convertWSMessage(data) {
227
+ const sender = data.sender;
228
+ const msg = data.message;
229
+ if (!sender?.sender_id || !msg?.message_id) {
230
+ this.logger.warn(`[FeishuChannel] [WS] Invalid message data, missing sender or message_id`);
231
+ return null;
232
+ }
233
+ let content = '';
234
+ const msgType = msg.message_type || 'text';
235
+ try {
236
+ if (msgType === 'text' && msg.content) {
237
+ const textContent = JSON.parse(msg.content);
238
+ content = textContent.text;
239
+ }
240
+ else if (msgType === 'interactive' && msg.content) {
241
+ content = this.extractCardText(msg.content);
242
+ }
243
+ else if (msgType === 'post' && msg.content) {
244
+ content = this.extractPostText(msg.content);
245
+ }
246
+ else if (msgType === 'image' && msg.content) {
247
+ content = await this.handleReceiveImage(msg.content);
248
+ }
249
+ else if ((msgType === 'file' || msgType === 'audio' || msgType === 'media') && msg.content) {
250
+ content = await this.handleReceiveFile(msg.content);
251
+ }
252
+ else {
253
+ content = msg.content || `[${msgType}]`;
254
+ }
255
+ }
256
+ catch {
257
+ content = msg.content || '';
258
+ }
259
+ return {
260
+ id: msg.message_id,
261
+ content,
262
+ type: this.mapToMessageType(msgType),
263
+ from: {
264
+ id: sender.sender_id.open_id || sender.sender_id.user_id || '',
265
+ type: 'user',
266
+ },
267
+ to: {
268
+ id: msg.chat_id || '',
269
+ type: 'group',
270
+ },
271
+ timestamp: parseInt(msg.create_time || '0', 10) * 1000,
272
+ raw: data,
273
+ metadata: {
274
+ channelId: this.id,
275
+ },
276
+ };
277
+ }
278
+ // ============================
279
+ // Webhook 事件处理(备用)
280
+ // ============================
281
+ /**
282
+ * 处理 Webhook 消息事件
283
+ */
284
+ async handleWebhookMessageEvent(event) {
285
+ const message = await this.convertWebhookMessage(event);
286
+ if (message) {
287
+ this.logger.info(`[FeishuChannel] [Webhook] Received message from ${message.from.id}: ${message.content.slice(0, 50)}`);
288
+ this.messageBus.publish(`channel:${this.id}:message:received`, message);
289
+ }
290
+ }
291
+ /**
292
+ * 转换 Webhook 消息为通用格式
293
+ */
294
+ async convertWebhookMessage(event) {
295
+ const { sender, message: msg } = event.event;
296
+ let content = '';
297
+ const msgType = msg.message_type || 'text';
298
+ try {
299
+ if (msgType === 'text') {
300
+ const textContent = JSON.parse(msg.content);
301
+ content = textContent.text;
302
+ }
303
+ else if (msgType === 'image' && msg.content) {
304
+ content = await this.handleReceiveImage(msg.content);
305
+ }
306
+ else if ((msgType === 'file' || msgType === 'audio' || msgType === 'media') && msg.content) {
307
+ content = await this.handleReceiveFile(msg.content);
308
+ }
309
+ else {
310
+ content = `[${msgType}]`;
311
+ }
312
+ }
313
+ catch {
314
+ content = msg.content;
315
+ }
316
+ return {
317
+ id: msg.message_id,
318
+ content,
319
+ type: this.mapToMessageType(msgType),
320
+ from: {
321
+ id: sender.sender_id.open_id || sender.sender_id.user_id,
322
+ type: 'user',
323
+ },
324
+ to: {
325
+ id: msg.chat_id,
326
+ type: 'group',
327
+ },
328
+ timestamp: parseInt(msg.create_time, 10) * 1000,
329
+ raw: event,
330
+ };
331
+ }
332
+ // ============================
333
+ // 消息内容提取与构建
334
+ // ============================
335
+ /**
336
+ * 从飞书 interactive card JSON 中提取纯文本
337
+ *
338
+ * 卡片结构: { header: { title: { content } }, elements: [{ tag: "div", text: { content } }] }
339
+ */
340
+ extractCardText(contentJson) {
341
+ try {
342
+ const card = JSON.parse(contentJson);
343
+ const parts = [];
344
+ // 提取 header title
345
+ const titleContent = card?.header?.title?.content;
346
+ if (titleContent)
347
+ parts.push(titleContent);
348
+ // 递归提取 elements 中的文本
349
+ const elements = card?.elements;
350
+ if (Array.isArray(elements)) {
351
+ this.extractElementsText(elements, parts);
352
+ }
353
+ return parts.length > 0 ? parts.join('\n') : '';
354
+ }
355
+ catch {
356
+ return contentJson;
357
+ }
358
+ }
359
+ /**
360
+ * 递归提取 elements 数组中的文本内容
361
+ */
362
+ extractElementsText(elements, parts) {
363
+ for (const el of elements) {
364
+ if (!el || typeof el !== 'object')
365
+ continue;
366
+ const element = el;
367
+ // div / note 等容器中的 text
368
+ if (element.text && typeof element.text === 'object') {
369
+ const text = element.text;
370
+ if (typeof text.content === 'string' && text.content) {
371
+ parts.push(text.content);
372
+ }
373
+ }
374
+ // column_set 中的 columns
375
+ if (element.columns && Array.isArray(element.columns)) {
376
+ for (const col of element.columns) {
377
+ if (col && typeof col === 'object') {
378
+ const column = col;
379
+ if (Array.isArray(column.elements)) {
380
+ this.extractElementsText(column.elements, parts);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ /**
388
+ * 从飞书 post JSON 中提取纯文本
389
+ *
390
+ * Post 结构: { zh_cn: { title, content: [{ tag: "text", text: "..." }] } }
391
+ */
392
+ extractPostText(contentJson) {
393
+ try {
394
+ const post = JSON.parse(contentJson);
395
+ const parts = [];
396
+ // 尝试提取中文内容,fallback 到英文
397
+ const lang = post.zh_cn || post.en_us || post.ja_jp;
398
+ if (!lang)
399
+ return contentJson;
400
+ if (lang.title)
401
+ parts.push(lang.title);
402
+ const content = lang.content;
403
+ if (Array.isArray(content)) {
404
+ for (const item of content) {
405
+ if (item.tag === 'text' && item.text) {
406
+ parts.push(item.text);
407
+ }
408
+ }
409
+ }
410
+ return parts.length > 0 ? parts.join('\n') : '';
411
+ }
412
+ catch {
413
+ return contentJson;
414
+ }
415
+ }
416
+ /**
417
+ * 将 Markdown 文本构建为飞书 interactive card JSON
418
+ *
419
+ * 使用 lark_md tag 让卡片内支持 Markdown 渲染
420
+ */
421
+ buildCardContent(markdownText) {
422
+ const card = {
423
+ config: { wide_screen_mode: true },
424
+ elements: [
425
+ {
426
+ tag: 'div',
427
+ text: { tag: 'lark_md', content: markdownText },
428
+ },
429
+ ],
430
+ };
431
+ return JSON.stringify(card);
432
+ }
433
+ // ============================
434
+ // 工具方法
435
+ // ============================
436
+ /**
437
+ * 图片扩展名集合
438
+ */
439
+ static IMAGE_EXTENSIONS = new Set([
440
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg',
441
+ ]);
442
+ /**
443
+ * 根据文件扩展名推断发送类型
444
+ */
445
+ resolveSendType(type, filePath) {
446
+ if (type === 'file' || type === 'image')
447
+ return type;
448
+ if (filePath) {
449
+ const ext = path.extname(filePath).toLowerCase();
450
+ if (FeishuChannel.IMAGE_EXTENSIONS.has(ext))
451
+ return 'image';
452
+ }
453
+ return type || 'text';
454
+ }
455
+ /**
456
+ * 上传文件到飞书
457
+ *
458
+ * @returns file_key
459
+ */
460
+ async uploadFile(filePath) {
461
+ const absolutePath = path.resolve(filePath);
462
+ if (!fs.existsSync(absolutePath)) {
463
+ throw new Error(`File not found: ${absolutePath}`);
464
+ }
465
+ const ext = path.extname(absolutePath).slice(1).toLowerCase() || 'bin';
466
+ const fileName = path.basename(absolutePath);
467
+ const response = await this.client.im.file.create({
468
+ data: {
469
+ file_type: ext,
470
+ file_name: fileName,
471
+ file: fs.readFileSync(absolutePath),
472
+ },
473
+ });
474
+ if (!response || !response.file_key) {
475
+ throw new Error('Upload file failed: no file_key returned');
476
+ }
477
+ return response.file_key;
478
+ }
479
+ /**
480
+ * 上传图片到飞书
481
+ *
482
+ * @returns image_key
483
+ */
484
+ async uploadImage(filePath) {
485
+ const absolutePath = path.resolve(filePath);
486
+ if (!fs.existsSync(absolutePath)) {
487
+ throw new Error(`Image not found: ${absolutePath}`);
488
+ }
489
+ const response = await this.client.im.image.create({
490
+ data: {
491
+ image_type: 'message',
492
+ image: fs.readFileSync(absolutePath),
493
+ },
494
+ });
495
+ if (!response || !response.image_key) {
496
+ throw new Error('Upload image failed: no image_key returned');
497
+ }
498
+ return response.image_key;
499
+ }
500
+ /**
501
+ * 下载文件并保存到本地
502
+ *
503
+ * @returns 本地文件路径,失败时返回 null
504
+ */
505
+ async downloadFile(fileKey, fileName) {
506
+ try {
507
+ fs.mkdirSync(this.receivedDir, { recursive: true });
508
+ const date = new Date().toISOString().slice(0, 10);
509
+ const ext = path.extname(fileName) || '.bin';
510
+ const localPath = path.join(this.receivedDir, `${date}_${fileKey}${ext}`);
511
+ const resp = await this.client.im.file.get({
512
+ path: { file_key: fileKey },
513
+ });
514
+ await resp.writeFile(localPath);
515
+ this.logger.info(`[FeishuChannel] File downloaded: ${localPath}`);
516
+ return localPath;
517
+ }
518
+ catch (error) {
519
+ this.logger.error(`[FeishuChannel] Download file failed (key=${fileKey}):`, error);
520
+ return null;
521
+ }
522
+ }
523
+ /**
524
+ * 下载图片并保存到本地
525
+ *
526
+ * @returns 本地文件路径,失败时返回 null
527
+ */
528
+ async downloadImage(imageKey) {
529
+ try {
530
+ fs.mkdirSync(this.receivedDir, { recursive: true });
531
+ const date = new Date().toISOString().slice(0, 10);
532
+ const localPath = path.join(this.receivedDir, `${date}_${imageKey}.png`);
533
+ const resp = await this.client.im.image.get({
534
+ path: { image_key: imageKey },
535
+ });
536
+ await resp.writeFile(localPath);
537
+ this.logger.info(`[FeishuChannel] Image downloaded: ${localPath}`);
538
+ return localPath;
539
+ }
540
+ catch (error) {
541
+ this.logger.error(`[FeishuChannel] Download image failed (key=${imageKey}):`, error);
542
+ return null;
543
+ }
544
+ }
545
+ /**
546
+ * 发送文件消息(上传 + 发送)
547
+ */
548
+ async sendFileMessage(chatId, filePath) {
549
+ try {
550
+ const fileKey = await this.uploadFile(filePath);
551
+ const response = await this.client.im.v1.message.create({
552
+ params: { receive_id_type: 'chat_id' },
553
+ data: {
554
+ receive_id: chatId,
555
+ msg_type: 'file',
556
+ content: JSON.stringify({ file_key: fileKey }),
557
+ },
558
+ });
559
+ if (response.code !== 0) {
560
+ return { success: false, error: `Feishu API error: ${response.msg}` };
561
+ }
562
+ return { success: true, messageId: response.data?.message_id };
563
+ }
564
+ catch (error) {
565
+ this.logger.error(`[FeishuChannel] Send file message failed:`, error);
566
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
567
+ }
568
+ }
569
+ /**
570
+ * 发送图片消息(上传 + 发送)
571
+ */
572
+ async sendImageMessage(chatId, filePath) {
573
+ try {
574
+ const imageKey = await this.uploadImage(filePath);
575
+ const response = await this.client.im.v1.message.create({
576
+ params: { receive_id_type: 'chat_id' },
577
+ data: {
578
+ receive_id: chatId,
579
+ msg_type: 'image',
580
+ content: JSON.stringify({ image_key: imageKey }),
581
+ },
582
+ });
583
+ if (response.code !== 0) {
584
+ return { success: false, error: `Feishu API error: ${response.msg}` };
585
+ }
586
+ return { success: true, messageId: response.data?.message_id };
587
+ }
588
+ catch (error) {
589
+ this.logger.error(`[FeishuChannel] Send image message failed:`, error);
590
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
591
+ }
592
+ }
593
+ /**
594
+ * 回复文件消息(上传 + 回复)
595
+ */
596
+ async replyFileMessage(messageId, filePath) {
597
+ try {
598
+ const fileKey = await this.uploadFile(filePath);
599
+ const response = await this.client.im.v1.message.reply({
600
+ path: { message_id: messageId },
601
+ data: {
602
+ msg_type: 'file',
603
+ content: JSON.stringify({ file_key: fileKey }),
604
+ },
605
+ });
606
+ if (response.code !== 0) {
607
+ return { success: false, error: `Feishu API error: ${response.msg}` };
608
+ }
609
+ return { success: true, messageId: response.data?.message_id };
610
+ }
611
+ catch (error) {
612
+ this.logger.error(`[FeishuChannel] Reply file message failed:`, error);
613
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
614
+ }
615
+ }
616
+ /**
617
+ * 回复图片消息(上传 + 回复)
618
+ */
619
+ async replyImageMessage(messageId, filePath) {
620
+ try {
621
+ const imageKey = await this.uploadImage(filePath);
622
+ const response = await this.client.im.v1.message.reply({
623
+ path: { message_id: messageId },
624
+ data: {
625
+ msg_type: 'image',
626
+ content: JSON.stringify({ image_key: imageKey }),
627
+ },
628
+ });
629
+ if (response.code !== 0) {
630
+ return { success: false, error: `Feishu API error: ${response.msg}` };
631
+ }
632
+ return { success: true, messageId: response.data?.message_id };
633
+ }
634
+ catch (error) {
635
+ this.logger.error(`[FeishuChannel] Reply image message failed:`, error);
636
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
637
+ }
638
+ }
639
+ /**
640
+ * 验证飞书签名
641
+ */
642
+ verifySignature(signature, timestamp, nonce, body) {
643
+ if (!this.config.encryptKey) {
644
+ this.logger.warn(`[FeishuChannel] Skipping signature verification (no encryptKey)`);
645
+ return true;
646
+ }
647
+ const token = this.config.verificationToken || '';
648
+ const bodyStr = JSON.stringify(body);
649
+ const signBase = timestamp + nonce + token + bodyStr;
650
+ const hash = crypto.createHash('sha256').update(signBase).digest('hex');
651
+ return hash === signature;
652
+ }
653
+ /**
654
+ * 处理接收到的图片消息,下载并返回本地路径
655
+ */
656
+ async handleReceiveImage(contentJson) {
657
+ try {
658
+ const parsed = JSON.parse(contentJson);
659
+ const imageKey = parsed.image_key;
660
+ if (!imageKey)
661
+ return contentJson;
662
+ const localPath = await this.downloadImage(imageKey);
663
+ return localPath ?? `[image: ${imageKey}]`;
664
+ }
665
+ catch {
666
+ return contentJson;
667
+ }
668
+ }
669
+ /**
670
+ * 处理接收到的文件消息,下载并返回本地路径
671
+ */
672
+ async handleReceiveFile(contentJson) {
673
+ try {
674
+ const parsed = JSON.parse(contentJson);
675
+ const fileKey = parsed.file_key;
676
+ const fileName = parsed.file_name ?? 'unknown';
677
+ if (!fileKey)
678
+ return contentJson;
679
+ const localPath = await this.downloadFile(fileKey, fileName);
680
+ return localPath ?? `[file: ${fileName}]`;
681
+ }
682
+ catch {
683
+ return contentJson;
684
+ }
685
+ }
686
+ /**
687
+ * 构建消息内容
688
+ */
689
+ buildContent(options) {
690
+ switch (options.type) {
691
+ case 'card':
692
+ case 'markdown':
693
+ // interactive card + lark_md 才能渲染 Markdown
694
+ return this.buildCardContent(options.content);
695
+ case 'text':
696
+ default:
697
+ return JSON.stringify({ text: options.content });
698
+ }
699
+ }
700
+ /**
701
+ * 映射消息类型(发送时)
702
+ */
703
+ mapMessageType(type) {
704
+ const typeMap = {
705
+ text: 'text',
706
+ card: 'interactive',
707
+ markdown: 'interactive',
708
+ image: 'image',
709
+ file: 'file',
710
+ };
711
+ return typeMap[type] || 'interactive';
712
+ }
713
+ /**
714
+ * 映射到通用消息类型(接收时)
715
+ */
716
+ mapToMessageType(feishuType) {
717
+ const typeMap = {
718
+ text: 'text',
719
+ post: 'markdown',
720
+ image: 'image',
721
+ file: 'file',
722
+ interactive: 'card',
723
+ audio: 'file',
724
+ media: 'file',
725
+ sticker: 'image',
726
+ };
727
+ return typeMap[feishuType] || 'text';
728
+ }
729
+ }
730
+ //# sourceMappingURL=channel.js.map