@lmcl/ailo-mcp-feishu 0.0.7 → 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 -78
- package/package.json +1 -1
- package/src/feishu-handler.ts +32 -95
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,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/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,93 +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
|
-
}
|
|
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
618
|
}
|
|
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}`;
|
|
619
|
+
if (!buffer) return null;
|
|
620
|
+
await fs.promises.writeFile(outPath, buffer);
|
|
621
|
+
return path.resolve(outPath);
|
|
669
622
|
}
|
|
670
623
|
|
|
671
624
|
/**
|
|
@@ -781,22 +734,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
781
734
|
} else if (messageType === "post") {
|
|
782
735
|
text = extractTextFromPostContent(rawContent);
|
|
783
736
|
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
|
-
}
|
|
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) });
|
|
800
741
|
}
|
|
801
742
|
} else {
|
|
802
743
|
const mediaConfig = MEDIA_MESSAGE_CONFIG[messageType];
|
|
@@ -805,24 +746,20 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
805
746
|
const content = JSON.parse(rawContent || "{}") as Record<string, string>;
|
|
806
747
|
const fileKey = content[mediaConfig.contentKey];
|
|
807
748
|
if (fileKey) {
|
|
808
|
-
const fileName = content["file_name"] ?? content["fileName"]
|
|
809
|
-
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(
|
|
810
751
|
messageId,
|
|
811
752
|
fileKey,
|
|
812
753
|
mediaConfig.resourceType,
|
|
813
754
|
mediaConfig.aidoType,
|
|
814
755
|
fileName
|
|
815
756
|
);
|
|
816
|
-
if (
|
|
757
|
+
if (absPath) attachments.push({ type: mediaConfig.aidoType, path: absPath, name: path.basename(absPath) });
|
|
817
758
|
else text = `[无法获取${mediaConfig.aidoType}资源]`;
|
|
818
759
|
} else {
|
|
819
|
-
console.warn(
|
|
820
|
-
`[feishu] ${messageType} message missing ${mediaConfig.contentKey} in content`
|
|
821
|
-
);
|
|
822
760
|
text = "[无法解析的媒体消息]";
|
|
823
761
|
}
|
|
824
|
-
} catch
|
|
825
|
-
console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
|
|
762
|
+
} catch {
|
|
826
763
|
text = "[无法解析的媒体消息]";
|
|
827
764
|
}
|
|
828
765
|
}
|