@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.
@@ -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
- * 下载消息资源到本地缓存,返回 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 时下载到 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
- if (postImageKeys.length > 0) {
658
- console.error(`[feishu] post: fetching ${postImageKeys.length} image(s) from message ${messageId}`);
659
- for (const imageKey of postImageKeys) {
660
- const attach = await this.fetchMessageResourceAttachment(messageId, imageKey, "image", "image");
661
- if (attach) {
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 attach = await this.fetchMessageResourceAttachment(messageId, fileKey, mediaConfig.resourceType, mediaConfig.aidoType, fileName);
680
- if (attach)
681
- 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) });
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 (err) {
691
- console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
653
+ catch {
692
654
  text = "[无法解析的媒体消息]";
693
655
  }
694
656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmcl/ailo-mcp-feishu",
3
- "version": "0.0.6",
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,88 +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
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
- if (postImageKeys.length > 0) {
780
- console.error(`[feishu] post: fetching ${postImageKeys.length} image(s) from message ${messageId}`);
781
- for (const imageKey of postImageKeys) {
782
- const attach = await this.fetchMessageResourceAttachment(
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 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(
805
751
  messageId,
806
752
  fileKey,
807
753
  mediaConfig.resourceType,
808
754
  mediaConfig.aidoType,
809
755
  fileName
810
756
  );
811
- if (attach) attachments.push(attach);
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 (err) {
820
- console.warn(`[feishu] failed to parse ${messageType} message content:`, err);
762
+ } catch {
821
763
  text = "[无法解析的媒体消息]";
822
764
  }
823
765
  }