@lmcl/ailo-mcp-feishu 0.0.6 → 0.0.8
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 +36 -74
- package/package.json +1 -1
- package/src/feishu-handler.ts +32 -90
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";
|
|
@@ -490,73 +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 时下载到 cache 传 path;无则传 ref(不保存)。
|
|
546
|
-
*/
|
|
547
|
-
async fetchMessageResourceAttachment(messageId, fileKey, resourceType, aidoType, fileName) {
|
|
548
|
-
if (!fileName?.trim()) {
|
|
549
|
-
return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
|
|
550
|
-
}
|
|
551
|
-
const pathOrNull = await this.downloadToCache(messageId, fileKey, resourceType, aidoType, fileName);
|
|
552
|
-
if (pathOrNull) {
|
|
553
|
-
return {
|
|
554
|
-
type: aidoType,
|
|
555
|
-
path: pathOrNull,
|
|
556
|
-
name: path.basename(pathOrNull),
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
|
|
529
|
+
await fs.promises.writeFile(outPath, buffer);
|
|
530
|
+
return path.resolve(outPath);
|
|
560
531
|
}
|
|
561
532
|
/**
|
|
562
533
|
* 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
|
|
@@ -654,18 +625,11 @@ export class FeishuHandler {
|
|
|
654
625
|
else if (messageType === "post") {
|
|
655
626
|
text = extractTextFromPostContent(rawContent);
|
|
656
627
|
const postImageKeys = [...new Set(extractImageKeysFromPostContent(rawContent))];
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
attachments.push(attach);
|
|
663
|
-
console.error(`[feishu] post: fetched image ok (${attach.path ? "path" : "ref"})`);
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
console.warn(`[feishu] post: failed to fetch image ${imageKey}`);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
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) });
|
|
669
633
|
}
|
|
670
634
|
}
|
|
671
635
|
else {
|
|
@@ -675,20 +639,18 @@ export class FeishuHandler {
|
|
|
675
639
|
const content = JSON.parse(rawContent || "{}");
|
|
676
640
|
const fileKey = content[mediaConfig.contentKey];
|
|
677
641
|
if (fileKey) {
|
|
678
|
-
const fileName = content["file_name"] ?? content["fileName"]
|
|
679
|
-
const
|
|
680
|
-
if (
|
|
681
|
-
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) });
|
|
682
646
|
else
|
|
683
647
|
text = `[无法获取${mediaConfig.aidoType}资源]`;
|
|
684
648
|
}
|
|
685
649
|
else {
|
|
686
|
-
console.warn(`[feishu] ${messageType} message missing ${mediaConfig.contentKey} in content`);
|
|
687
650
|
text = "[无法解析的媒体消息]";
|
|
688
651
|
}
|
|
689
652
|
}
|
|
690
|
-
catch
|
|
691
|
-
console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
|
|
653
|
+
catch {
|
|
692
654
|
text = "[无法解析的媒体消息]";
|
|
693
655
|
}
|
|
694
656
|
}
|
package/package.json
CHANGED
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";
|
|
@@ -579,88 +580,45 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
579
580
|
}
|
|
580
581
|
|
|
581
582
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
583
|
+
* 保存附件到本地,返回绝对路径。
|
|
584
|
+
* 收到飞书消息时,先调用此函数保存图片/视频/文件,再传给 LLM。
|
|
585
|
+
* 失败返回 null,不传 ref。
|
|
585
586
|
*/
|
|
586
|
-
private async
|
|
587
|
+
private async saveResourceToLocal(
|
|
587
588
|
messageId: string,
|
|
588
589
|
fileKey: string,
|
|
589
590
|
resourceType: string,
|
|
590
591
|
aidoType: "image" | "audio" | "video" | "file",
|
|
591
592
|
fileName: string
|
|
592
593
|
): Promise<string | null> {
|
|
593
|
-
const
|
|
594
|
-
if (!trimmed) return null;
|
|
595
|
-
const workDir = getWorkDir();
|
|
596
|
-
if (!workDir) return null;
|
|
594
|
+
const workDir = getWorkDir() ?? path.join(os.tmpdir(), "ailo-mcp-feishu-blobs");
|
|
597
595
|
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);
|
|
596
|
+
const cacheDir = path.join(workDir, "blobs", String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0"));
|
|
597
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
598
|
+
const sanitized = fileName.replace(/[/\\?*:|"<>]/g, "_").slice(0, 200);
|
|
599
|
+
const outPath = path.join(cacheDir, `${Date.now()}_${sanitized}`);
|
|
609
600
|
|
|
610
|
-
|
|
601
|
+
let buffer: Buffer | null = null;
|
|
602
|
+
try {
|
|
611
603
|
const res = await this.client.im.v1.messageResource.get({
|
|
612
604
|
params: { type: resourceType },
|
|
613
605
|
path: { message_id: messageId, file_key: fileKey },
|
|
614
606
|
});
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (!buffer && resourceType === "image" && aidoType === "image") {
|
|
607
|
+
if (res?.getReadableStream) buffer = await streamToBuffer(res.getReadableStream());
|
|
608
|
+
} catch {
|
|
609
|
+
// messageResource 可能失败,图片可尝试 image.get
|
|
610
|
+
}
|
|
611
|
+
if (!buffer && resourceType === "image" && aidoType === "image") {
|
|
612
|
+
try {
|
|
622
613
|
const res = await this.client.im.v1.image.get({ path: { image_key: fileKey } });
|
|
623
|
-
if (res?.getReadableStream)
|
|
624
|
-
|
|
625
|
-
|
|
614
|
+
if (res?.getReadableStream) buffer = await streamToBuffer(res.getReadableStream());
|
|
615
|
+
} catch {
|
|
616
|
+
// ignore
|
|
626
617
|
}
|
|
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
618
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
* 获取消息资源并转为 attachment。有 file_name 时下载到 cache 传 path;无则传 ref(不保存)。
|
|
638
|
-
*/
|
|
639
|
-
private async fetchMessageResourceAttachment(
|
|
640
|
-
messageId: string,
|
|
641
|
-
fileKey: string,
|
|
642
|
-
resourceType: string,
|
|
643
|
-
aidoType: "image" | "audio" | "video" | "file",
|
|
644
|
-
fileName?: string
|
|
645
|
-
): Promise<FeishuAttachment | null> {
|
|
646
|
-
if (!fileName?.trim()) {
|
|
647
|
-
return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
|
|
648
|
-
}
|
|
649
|
-
const pathOrNull = await this.downloadToCache(
|
|
650
|
-
messageId,
|
|
651
|
-
fileKey,
|
|
652
|
-
resourceType,
|
|
653
|
-
aidoType,
|
|
654
|
-
fileName
|
|
655
|
-
);
|
|
656
|
-
if (pathOrNull) {
|
|
657
|
-
return {
|
|
658
|
-
type: aidoType,
|
|
659
|
-
path: pathOrNull,
|
|
660
|
-
name: path.basename(pathOrNull),
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
|
|
619
|
+
if (!buffer) return null;
|
|
620
|
+
await fs.promises.writeFile(outPath, buffer);
|
|
621
|
+
return path.resolve(outPath);
|
|
664
622
|
}
|
|
665
623
|
|
|
666
624
|
/**
|
|
@@ -776,22 +734,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
776
734
|
} else if (messageType === "post") {
|
|
777
735
|
text = extractTextFromPostContent(rawContent);
|
|
778
736
|
const postImageKeys = [...new Set(extractImageKeysFromPostContent(rawContent))];
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
messageId,
|
|
784
|
-
imageKey,
|
|
785
|
-
"image",
|
|
786
|
-
"image"
|
|
787
|
-
);
|
|
788
|
-
if (attach) {
|
|
789
|
-
attachments.push(attach);
|
|
790
|
-
console.error(`[feishu] post: fetched image ok (${attach.path ? "path" : "ref"})`);
|
|
791
|
-
} else {
|
|
792
|
-
console.warn(`[feishu] post: failed to fetch image ${imageKey}`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
737
|
+
for (const imageKey of postImageKeys) {
|
|
738
|
+
const fileName = `image_${imageKey.slice(-12)}.png`;
|
|
739
|
+
const absPath = await this.saveResourceToLocal(messageId, imageKey, "image", "image", fileName);
|
|
740
|
+
if (absPath) attachments.push({ type: "image", path: absPath, name: path.basename(absPath) });
|
|
795
741
|
}
|
|
796
742
|
} else {
|
|
797
743
|
const mediaConfig = MEDIA_MESSAGE_CONFIG[messageType];
|
|
@@ -800,24 +746,20 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
800
746
|
const content = JSON.parse(rawContent || "{}") as Record<string, string>;
|
|
801
747
|
const fileKey = content[mediaConfig.contentKey];
|
|
802
748
|
if (fileKey) {
|
|
803
|
-
const fileName = content["file_name"] ?? content["fileName"]
|
|
804
|
-
const
|
|
749
|
+
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"}`;
|
|
750
|
+
const absPath = await this.saveResourceToLocal(
|
|
805
751
|
messageId,
|
|
806
752
|
fileKey,
|
|
807
753
|
mediaConfig.resourceType,
|
|
808
754
|
mediaConfig.aidoType,
|
|
809
755
|
fileName
|
|
810
756
|
);
|
|
811
|
-
if (
|
|
757
|
+
if (absPath) attachments.push({ type: mediaConfig.aidoType, path: absPath, name: path.basename(absPath) });
|
|
812
758
|
else text = `[无法获取${mediaConfig.aidoType}资源]`;
|
|
813
759
|
} else {
|
|
814
|
-
console.warn(
|
|
815
|
-
`[feishu] ${messageType} message missing ${mediaConfig.contentKey} in content`
|
|
816
|
-
);
|
|
817
760
|
text = "[无法解析的媒体消息]";
|
|
818
761
|
}
|
|
819
|
-
} catch
|
|
820
|
-
console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
|
|
762
|
+
} catch {
|
|
821
763
|
text = "[无法解析的媒体消息]";
|
|
822
764
|
}
|
|
823
765
|
}
|