@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.
@@ -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
- * 下载消息资源到本地缓存,返回 path。失败时返回 null(调用方传 ref)。
494
- * 路径按年/月组织:blobs/YYYY/MM/
495
- * 文件名:时间戳_原始file_name(必须提供 file_name,否则不保存,走 ref
494
+ * 保存附件到本地,返回绝对路径。
495
+ * 收到飞书消息时,先调用此函数保存图片/视频/文件,再传给 LLM。
496
+ * 失败返回 null,不传 ref
496
497
  */
497
- async downloadToCache(messageId, fileKey, resourceType, aidoType, fileName) {
498
- const trimmed = fileName?.trim();
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 year = String(now.getFullYear());
506
- const month = String(now.getMonth() + 1).padStart(2, "0");
507
- const cacheDir = path.join(workDir, "blobs", year, month);
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 (!res?.getReadableStream)
523
- return null;
524
- return streamToBuffer(res.getReadableStream());
525
- };
526
- try {
527
- let buffer = await tryMessageResource();
528
- if (!buffer && resourceType === "image" && aidoType === "image") {
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
- if (!buffer)
535
- return null;
536
- await fs.promises.writeFile(outPath, buffer);
537
- return outPath;
523
+ catch {
524
+ // ignore
525
+ }
538
526
  }
539
- catch (err) {
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
- if (postImageKeys.length > 0) {
662
- console.error(`[feishu] post: fetching ${postImageKeys.length} image(s) from message ${messageId}`);
663
- for (const imageKey of postImageKeys) {
664
- const attach = await this.fetchMessageResourceAttachment(messageId, imageKey, "image", "image");
665
- if (attach) {
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 attach = await this.fetchMessageResourceAttachment(messageId, fileKey, mediaConfig.resourceType, mediaConfig.aidoType, fileName);
684
- if (attach)
685
- attachments.push(attach);
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 (err) {
695
- console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
653
+ catch {
696
654
  text = "[无法解析的媒体消息]";
697
655
  }
698
656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmcl/ailo-mcp-feishu",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Ailo 飞书/Lark 通道 MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- * 下载消息资源到本地缓存,返回 path。失败时返回 null(调用方传 ref)。
583
- * 路径按年/月组织:blobs/YYYY/MM/
584
- * 文件名:时间戳_原始file_name(必须提供 file_name,否则不保存,走 ref
583
+ * 保存附件到本地,返回绝对路径。
584
+ * 收到飞书消息时,先调用此函数保存图片/视频/文件,再传给 LLM。
585
+ * 失败返回 null,不传 ref
585
586
  */
586
- private async downloadToCache(
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 trimmed = fileName?.trim();
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 year = String(now.getFullYear());
599
- const month = String(now.getMonth() + 1).padStart(2, "0");
600
- const cacheDir = path.join(workDir, "blobs", year, month);
601
- try {
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
- const tryMessageResource = async (): Promise<Buffer | null> => {
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 (!res?.getReadableStream) return null;
616
- return streamToBuffer(res.getReadableStream());
617
- };
618
-
619
- try {
620
- let buffer: Buffer | null = await tryMessageResource();
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
- buffer = await streamToBuffer(res.getReadableStream());
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
- return { type: aidoType, ref: `im:${messageId}:${fileKey}:${aidoType}`, channel: "feishu" };
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
- if (postImageKeys.length > 0) {
785
- console.error(`[feishu] post: fetching ${postImageKeys.length} image(s) from message ${messageId}`);
786
- for (const imageKey of postImageKeys) {
787
- const attach = await this.fetchMessageResourceAttachment(
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 attach = await this.fetchMessageResourceAttachment(
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 (attach) attachments.push(attach);
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 (err) {
825
- console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
762
+ } catch {
826
763
  text = "[无法解析的媒体消息]";
827
764
  }
828
765
  }