@lmcl/ailo-mcp-feishu 0.0.7 → 0.0.9
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/dist/feishu-handler.js +55 -97
- package/dist/index.js +2 -0
- package/package.json +2 -2
- package/src/feishu-handler.ts +56 -116
- package/src/index.ts +2 -0
package/dist/feishu-handler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
4
5
|
import { getWorkDir } from "@lmcl/ailo-mcp-sdk";
|
|
@@ -308,30 +309,30 @@ export class FeishuHandler {
|
|
|
308
309
|
this.onMessageRead = handler;
|
|
309
310
|
}
|
|
310
311
|
buildBridgeMessage(opts) {
|
|
311
|
-
const { chatId, text, chatType, senderId = "", senderName = "", chatName,
|
|
312
|
+
const { chatId, text, chatType, senderId = "", senderName = "", chatName, attachments } = opts;
|
|
312
313
|
const isPrivate = chatType === "私聊";
|
|
314
|
+
// 新 ContextTag 格式(kind/streamKey/routing):
|
|
315
|
+
// - TagChannel 由网关根据 displayName 自动注入,飞书不重复添加
|
|
316
|
+
// - chat_id 参与 stream_key 推导(streamKey=true),仅路由(routing=true)
|
|
317
|
+
// - 私聊:stream_key = feishu:{ou_xxx}(用户 ID)
|
|
318
|
+
// - 群聊:stream_key = feishu:{oc_xxx}(群 ID)
|
|
319
|
+
// - "@我" 是内容语义不是时空场,不放 contextTags
|
|
320
|
+
// - "时间" 由框架在接收时自动渲染,不由通道传入
|
|
313
321
|
const tags = [
|
|
314
|
-
{
|
|
315
|
-
{
|
|
322
|
+
{ kind: "conv_type", value: chatType, streamKey: false },
|
|
323
|
+
{ kind: "chat_id", value: chatId, streamKey: true, routing: true },
|
|
316
324
|
];
|
|
317
|
-
if (!isPrivate
|
|
318
|
-
|
|
325
|
+
if (!isPrivate) {
|
|
326
|
+
// 群聊:群名作为 stream_key 语义补充,group 标签不参与 stream_key(chat_id 已足够)
|
|
327
|
+
const groupName = chatName || `群${chatId.slice(-8)}`;
|
|
328
|
+
tags.push({ kind: "group", value: groupName, streamKey: false });
|
|
319
329
|
}
|
|
320
|
-
|
|
321
|
-
tags.push({
|
|
330
|
+
if (senderName) {
|
|
331
|
+
tags.push({ kind: "participant", value: senderName, streamKey: false });
|
|
322
332
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
tags.push({
|
|
326
|
-
}
|
|
327
|
-
if (timestamp != null && timestamp > 0) {
|
|
328
|
-
const d = new Date(timestamp);
|
|
329
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
330
|
-
tags.push({
|
|
331
|
-
desc: "时间",
|
|
332
|
-
value: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
|
333
|
-
core: false,
|
|
334
|
-
});
|
|
333
|
+
if (senderId) {
|
|
334
|
+
// sender_id 仅路由备用(recall agent 按参与者查询时使用),不展示在历史邮戳
|
|
335
|
+
tags.push({ kind: "sender_id", value: senderId, streamKey: false, routing: true });
|
|
335
336
|
}
|
|
336
337
|
return { text, contextTags: tags, attachments };
|
|
337
338
|
}
|
|
@@ -490,77 +491,43 @@ export class FeishuHandler {
|
|
|
490
491
|
return "p2p";
|
|
491
492
|
}
|
|
492
493
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
494
|
+
* 保存附件到本地,返回绝对路径。
|
|
495
|
+
* 收到飞书消息时,先调用此函数保存图片/视频/文件,再传给 LLM。
|
|
496
|
+
* 失败返回 null,不传 ref。
|
|
496
497
|
*/
|
|
497
|
-
async
|
|
498
|
-
const
|
|
499
|
-
if (!trimmed)
|
|
500
|
-
return null;
|
|
501
|
-
const workDir = getWorkDir();
|
|
502
|
-
if (!workDir)
|
|
503
|
-
return null;
|
|
498
|
+
async saveResourceToLocal(messageId, fileKey, resourceType, aidoType, fileName) {
|
|
499
|
+
const workDir = getWorkDir() ?? path.join(os.tmpdir(), "ailo-mcp-feishu-blobs");
|
|
504
500
|
const now = new Date();
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
const
|
|
501
|
+
const cacheDir = path.join(workDir, "blobs", String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0"));
|
|
502
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
503
|
+
const sanitized = fileName.replace(/[/\\?*:|"<>]/g, "_").slice(0, 200);
|
|
504
|
+
const outPath = path.join(cacheDir, `${Date.now()}_${sanitized}`);
|
|
505
|
+
let buffer = null;
|
|
508
506
|
try {
|
|
509
|
-
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
const sanitized = trimmed.replace(/[/\\?*:|"<>]/g, "_").slice(0, 200);
|
|
515
|
-
const filename = `${Date.now()}_${sanitized}`;
|
|
516
|
-
const outPath = path.join(cacheDir, filename);
|
|
517
|
-
const tryMessageResource = async () => {
|
|
518
507
|
const res = await this.client.im.v1.messageResource.get({
|
|
519
508
|
params: { type: resourceType },
|
|
520
509
|
path: { message_id: messageId, file_key: fileKey },
|
|
521
510
|
});
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
511
|
+
if (res?.getReadableStream)
|
|
512
|
+
buffer = await streamToBuffer(res.getReadableStream());
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// messageResource 可能失败,图片可尝试 image.get
|
|
516
|
+
}
|
|
517
|
+
if (!buffer && resourceType === "image" && aidoType === "image") {
|
|
518
|
+
try {
|
|
529
519
|
const res = await this.client.im.v1.image.get({ path: { image_key: fileKey } });
|
|
530
|
-
if (res?.getReadableStream)
|
|
520
|
+
if (res?.getReadableStream)
|
|
531
521
|
buffer = await streamToBuffer(res.getReadableStream());
|
|
532
|
-
}
|
|
533
522
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return outPath;
|
|
523
|
+
catch {
|
|
524
|
+
// ignore
|
|
525
|
+
}
|
|
538
526
|
}
|
|
539
|
-
|
|
540
|
-
console.warn(`[feishu] downloadToCache failed (${resourceType}):`, err);
|
|
527
|
+
if (!buffer)
|
|
541
528
|
return null;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* 获取消息资源并转为 attachment。有 file_name 时用原名;无则用默认名(如图片用 image_xxx.png)。
|
|
546
|
-
* 下载到本地后传绝对路径给 LLM,否则 LLM 无法访问。
|
|
547
|
-
*/
|
|
548
|
-
async fetchMessageResourceAttachment(messageId, fileKey, resourceType, aidoType, fileName) {
|
|
549
|
-
const effectiveName = fileName?.trim() || this.defaultFileNameForResource(aidoType, fileKey);
|
|
550
|
-
const pathOrNull = await this.downloadToCache(messageId, fileKey, resourceType, aidoType, effectiveName);
|
|
551
|
-
if (pathOrNull) {
|
|
552
|
-
return {
|
|
553
|
-
type: aidoType,
|
|
554
|
-
path: path.resolve(pathOrNull),
|
|
555
|
-
name: path.basename(pathOrNull),
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
|
|
559
|
-
}
|
|
560
|
-
/** 无 file_name 时的默认文件名(如图片、无名的文件等) */
|
|
561
|
-
defaultFileNameForResource(aidoType, fileKey) {
|
|
562
|
-
const ext = { image: "png", audio: "mp3", video: "mp4", file: "bin" }[aidoType] ?? "bin";
|
|
563
|
-
return `${aidoType}_${fileKey.slice(-12)}.${ext}`;
|
|
529
|
+
await fs.promises.writeFile(outPath, buffer);
|
|
530
|
+
return path.resolve(outPath);
|
|
564
531
|
}
|
|
565
532
|
/**
|
|
566
533
|
* 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
|
|
@@ -658,18 +625,11 @@ export class FeishuHandler {
|
|
|
658
625
|
else if (messageType === "post") {
|
|
659
626
|
text = extractTextFromPostContent(rawContent);
|
|
660
627
|
const postImageKeys = [...new Set(extractImageKeysFromPostContent(rawContent))];
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
attachments.push(attach);
|
|
667
|
-
console.error(`[feishu] post: fetched image ok (${attach.path ? "path" : "ref"})`);
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
console.warn(`[feishu] post: failed to fetch image ${imageKey}`);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
628
|
+
for (const imageKey of postImageKeys) {
|
|
629
|
+
const fileName = `image_${imageKey.slice(-12)}.png`;
|
|
630
|
+
const absPath = await this.saveResourceToLocal(messageId, imageKey, "image", "image", fileName);
|
|
631
|
+
if (absPath)
|
|
632
|
+
attachments.push({ type: "image", path: absPath, name: path.basename(absPath) });
|
|
673
633
|
}
|
|
674
634
|
}
|
|
675
635
|
else {
|
|
@@ -679,20 +639,18 @@ export class FeishuHandler {
|
|
|
679
639
|
const content = JSON.parse(rawContent || "{}");
|
|
680
640
|
const fileKey = content[mediaConfig.contentKey];
|
|
681
641
|
if (fileKey) {
|
|
682
|
-
const fileName = content["file_name"] ?? content["fileName"]
|
|
683
|
-
const
|
|
684
|
-
if (
|
|
685
|
-
attachments.push(
|
|
642
|
+
const fileName = content["file_name"] ?? content["fileName"] ?? `${mediaConfig.aidoType}_${fileKey.slice(-12)}.${mediaConfig.aidoType === "image" ? "png" : mediaConfig.aidoType === "video" ? "mp4" : mediaConfig.aidoType === "audio" ? "mp3" : "bin"}`;
|
|
643
|
+
const absPath = await this.saveResourceToLocal(messageId, fileKey, mediaConfig.resourceType, mediaConfig.aidoType, fileName);
|
|
644
|
+
if (absPath)
|
|
645
|
+
attachments.push({ type: mediaConfig.aidoType, path: absPath, name: path.basename(absPath) });
|
|
686
646
|
else
|
|
687
647
|
text = `[无法获取${mediaConfig.aidoType}资源]`;
|
|
688
648
|
}
|
|
689
649
|
else {
|
|
690
|
-
console.warn(`[feishu] ${messageType} message missing ${mediaConfig.contentKey} in content`);
|
|
691
650
|
text = "[无法解析的媒体消息]";
|
|
692
651
|
}
|
|
693
652
|
}
|
|
694
|
-
catch
|
|
695
|
-
console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
|
|
653
|
+
catch {
|
|
696
654
|
text = "[无法解析的媒体消息]";
|
|
697
655
|
}
|
|
698
656
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmcl/ailo-mcp-feishu",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Ailo 飞书/Lark 通道 MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@larksuiteoapi/node-sdk": "^1.56.1",
|
|
17
|
-
"@lmcl/ailo-mcp-sdk": "^0.0.
|
|
17
|
+
"@lmcl/ailo-mcp-sdk": "^0.0.4",
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
19
19
|
"dotenv": "^16.4.5",
|
|
20
20
|
"form-data": "^4.0.0",
|
package/src/feishu-handler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
4
5
|
import { getWorkDir, type BridgeHandler, type BridgeMessage, type ContextTag } from "@lmcl/ailo-mcp-sdk";
|
|
@@ -380,33 +381,36 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
380
381
|
timestamp?: number;
|
|
381
382
|
attachments?: Array<{ type: string; path?: string; url?: string; base64?: string; mime?: string; name?: string }>;
|
|
382
383
|
}): BridgeMessage {
|
|
383
|
-
const { chatId, text, chatType, senderId = "", senderName = "", chatName,
|
|
384
|
+
const { chatId, text, chatType, senderId = "", senderName = "", chatName, attachments } = opts;
|
|
384
385
|
const isPrivate = chatType === "私聊";
|
|
386
|
+
|
|
387
|
+
// 新 ContextTag 格式(kind/streamKey/routing):
|
|
388
|
+
// - TagChannel 由网关根据 displayName 自动注入,飞书不重复添加
|
|
389
|
+
// - chat_id 参与 stream_key 推导(streamKey=true),仅路由(routing=true)
|
|
390
|
+
// - 私聊:stream_key = feishu:{ou_xxx}(用户 ID)
|
|
391
|
+
// - 群聊:stream_key = feishu:{oc_xxx}(群 ID)
|
|
392
|
+
// - "@我" 是内容语义不是时空场,不放 contextTags
|
|
393
|
+
// - "时间" 由框架在接收时自动渲染,不由通道传入
|
|
385
394
|
const tags: ContextTag[] = [
|
|
386
|
-
{
|
|
387
|
-
{
|
|
395
|
+
{ kind: "conv_type", value: chatType, streamKey: false },
|
|
396
|
+
{ kind: "chat_id", value: chatId, streamKey: true, routing: true },
|
|
388
397
|
];
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
|
|
399
|
+
if (!isPrivate) {
|
|
400
|
+
// 群聊:群名作为 stream_key 语义补充,group 标签不参与 stream_key(chat_id 已足够)
|
|
401
|
+
const groupName = chatName || `群${chatId.slice(-8)}`;
|
|
402
|
+
tags.push({ kind: "group", value: groupName, streamKey: false });
|
|
393
403
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
{
|
|
397
|
-
);
|
|
398
|
-
if (mentionsSelf) {
|
|
399
|
-
tags.push({ desc: "@我", value: "是", core: isPrivate });
|
|
404
|
+
|
|
405
|
+
if (senderName) {
|
|
406
|
+
tags.push({ kind: "participant", value: senderName, streamKey: false });
|
|
400
407
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
tags.push({
|
|
405
|
-
desc: "时间",
|
|
406
|
-
value: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
|
407
|
-
core: false,
|
|
408
|
-
});
|
|
408
|
+
|
|
409
|
+
if (senderId) {
|
|
410
|
+
// sender_id 仅路由备用(recall agent 按参与者查询时使用),不展示在历史邮戳
|
|
411
|
+
tags.push({ kind: "sender_id", value: senderId, streamKey: false, routing: true });
|
|
409
412
|
}
|
|
413
|
+
|
|
410
414
|
return { text, contextTags: tags, attachments };
|
|
411
415
|
}
|
|
412
416
|
|
|
@@ -579,93 +583,45 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
579
583
|
}
|
|
580
584
|
|
|
581
585
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
586
|
+
* 保存附件到本地,返回绝对路径。
|
|
587
|
+
* 收到飞书消息时,先调用此函数保存图片/视频/文件,再传给 LLM。
|
|
588
|
+
* 失败返回 null,不传 ref。
|
|
585
589
|
*/
|
|
586
|
-
private async
|
|
590
|
+
private async saveResourceToLocal(
|
|
587
591
|
messageId: string,
|
|
588
592
|
fileKey: string,
|
|
589
593
|
resourceType: string,
|
|
590
594
|
aidoType: "image" | "audio" | "video" | "file",
|
|
591
595
|
fileName: string
|
|
592
596
|
): Promise<string | null> {
|
|
593
|
-
const
|
|
594
|
-
if (!trimmed) return null;
|
|
595
|
-
const workDir = getWorkDir();
|
|
596
|
-
if (!workDir) return null;
|
|
597
|
+
const workDir = getWorkDir() ?? path.join(os.tmpdir(), "ailo-mcp-feishu-blobs");
|
|
597
598
|
const now = new Date();
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
603
|
-
} catch {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
const sanitized = trimmed.replace(/[/\\?*:|"<>]/g, "_").slice(0, 200);
|
|
607
|
-
const filename = `${Date.now()}_${sanitized}`;
|
|
608
|
-
const outPath = path.join(cacheDir, filename);
|
|
599
|
+
const cacheDir = path.join(workDir, "blobs", String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0"));
|
|
600
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
601
|
+
const sanitized = fileName.replace(/[/\\?*:|"<>]/g, "_").slice(0, 200);
|
|
602
|
+
const outPath = path.join(cacheDir, `${Date.now()}_${sanitized}`);
|
|
609
603
|
|
|
610
|
-
|
|
604
|
+
let buffer: Buffer | null = null;
|
|
605
|
+
try {
|
|
611
606
|
const res = await this.client.im.v1.messageResource.get({
|
|
612
607
|
params: { type: resourceType },
|
|
613
608
|
path: { message_id: messageId, file_key: fileKey },
|
|
614
609
|
});
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (!buffer && resourceType === "image" && aidoType === "image") {
|
|
610
|
+
if (res?.getReadableStream) buffer = await streamToBuffer(res.getReadableStream());
|
|
611
|
+
} catch {
|
|
612
|
+
// messageResource 可能失败,图片可尝试 image.get
|
|
613
|
+
}
|
|
614
|
+
if (!buffer && resourceType === "image" && aidoType === "image") {
|
|
615
|
+
try {
|
|
622
616
|
const res = await this.client.im.v1.image.get({ path: { image_key: fileKey } });
|
|
623
|
-
if (res?.getReadableStream)
|
|
624
|
-
|
|
625
|
-
|
|
617
|
+
if (res?.getReadableStream) buffer = await streamToBuffer(res.getReadableStream());
|
|
618
|
+
} catch {
|
|
619
|
+
// ignore
|
|
626
620
|
}
|
|
627
|
-
if (!buffer) return null;
|
|
628
|
-
await fs.promises.writeFile(outPath, buffer);
|
|
629
|
-
return outPath;
|
|
630
|
-
} catch (err) {
|
|
631
|
-
console.warn(`[feishu] downloadToCache failed (${resourceType}):`, err);
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* 获取消息资源并转为 attachment。有 file_name 时用原名;无则用默认名(如图片用 image_xxx.png)。
|
|
638
|
-
* 下载到本地后传绝对路径给 LLM,否则 LLM 无法访问。
|
|
639
|
-
*/
|
|
640
|
-
private async fetchMessageResourceAttachment(
|
|
641
|
-
messageId: string,
|
|
642
|
-
fileKey: string,
|
|
643
|
-
resourceType: string,
|
|
644
|
-
aidoType: "image" | "audio" | "video" | "file",
|
|
645
|
-
fileName?: string
|
|
646
|
-
): Promise<FeishuAttachment | null> {
|
|
647
|
-
const effectiveName = fileName?.trim() || this.defaultFileNameForResource(aidoType, fileKey);
|
|
648
|
-
const pathOrNull = await this.downloadToCache(
|
|
649
|
-
messageId,
|
|
650
|
-
fileKey,
|
|
651
|
-
resourceType,
|
|
652
|
-
aidoType,
|
|
653
|
-
effectiveName
|
|
654
|
-
);
|
|
655
|
-
if (pathOrNull) {
|
|
656
|
-
return {
|
|
657
|
-
type: aidoType,
|
|
658
|
-
path: path.resolve(pathOrNull),
|
|
659
|
-
name: path.basename(pathOrNull),
|
|
660
|
-
};
|
|
661
621
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
/** 无 file_name 时的默认文件名(如图片、无名的文件等) */
|
|
666
|
-
private defaultFileNameForResource(aidoType: string, fileKey: string): string {
|
|
667
|
-
const ext = { image: "png", audio: "mp3", video: "mp4", file: "bin" }[aidoType] ?? "bin";
|
|
668
|
-
return `${aidoType}_${fileKey.slice(-12)}.${ext}`;
|
|
622
|
+
if (!buffer) return null;
|
|
623
|
+
await fs.promises.writeFile(outPath, buffer);
|
|
624
|
+
return path.resolve(outPath);
|
|
669
625
|
}
|
|
670
626
|
|
|
671
627
|
/**
|
|
@@ -781,22 +737,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
781
737
|
} else if (messageType === "post") {
|
|
782
738
|
text = extractTextFromPostContent(rawContent);
|
|
783
739
|
const postImageKeys = [...new Set(extractImageKeysFromPostContent(rawContent))];
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
messageId,
|
|
789
|
-
imageKey,
|
|
790
|
-
"image",
|
|
791
|
-
"image"
|
|
792
|
-
);
|
|
793
|
-
if (attach) {
|
|
794
|
-
attachments.push(attach);
|
|
795
|
-
console.error(`[feishu] post: fetched image ok (${attach.path ? "path" : "ref"})`);
|
|
796
|
-
} else {
|
|
797
|
-
console.warn(`[feishu] post: failed to fetch image ${imageKey}`);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
740
|
+
for (const imageKey of postImageKeys) {
|
|
741
|
+
const fileName = `image_${imageKey.slice(-12)}.png`;
|
|
742
|
+
const absPath = await this.saveResourceToLocal(messageId, imageKey, "image", "image", fileName);
|
|
743
|
+
if (absPath) attachments.push({ type: "image", path: absPath, name: path.basename(absPath) });
|
|
800
744
|
}
|
|
801
745
|
} else {
|
|
802
746
|
const mediaConfig = MEDIA_MESSAGE_CONFIG[messageType];
|
|
@@ -805,24 +749,20 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
805
749
|
const content = JSON.parse(rawContent || "{}") as Record<string, string>;
|
|
806
750
|
const fileKey = content[mediaConfig.contentKey];
|
|
807
751
|
if (fileKey) {
|
|
808
|
-
const fileName = content["file_name"] ?? content["fileName"]
|
|
809
|
-
const
|
|
752
|
+
const fileName = content["file_name"] ?? content["fileName"] ?? `${mediaConfig.aidoType}_${fileKey.slice(-12)}.${mediaConfig.aidoType === "image" ? "png" : mediaConfig.aidoType === "video" ? "mp4" : mediaConfig.aidoType === "audio" ? "mp3" : "bin"}`;
|
|
753
|
+
const absPath = await this.saveResourceToLocal(
|
|
810
754
|
messageId,
|
|
811
755
|
fileKey,
|
|
812
756
|
mediaConfig.resourceType,
|
|
813
757
|
mediaConfig.aidoType,
|
|
814
758
|
fileName
|
|
815
759
|
);
|
|
816
|
-
if (
|
|
760
|
+
if (absPath) attachments.push({ type: mediaConfig.aidoType, path: absPath, name: path.basename(absPath) });
|
|
817
761
|
else text = `[无法获取${mediaConfig.aidoType}资源]`;
|
|
818
762
|
} else {
|
|
819
|
-
console.warn(
|
|
820
|
-
`[feishu] ${messageType} message missing ${mediaConfig.contentKey} in content`
|
|
821
|
-
);
|
|
822
763
|
text = "[无法解析的媒体消息]";
|
|
823
764
|
}
|
|
824
|
-
} catch
|
|
825
|
-
console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
|
|
765
|
+
} catch {
|
|
826
766
|
text = "[无法解析的媒体消息]";
|
|
827
767
|
}
|
|
828
768
|
}
|