chatccc 0.2.61 → 0.2.63

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.
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
- import { dirname, extname, join } from "node:path";
13
+ import { basename, dirname, extname, join } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
 
16
16
  import {
@@ -19,7 +19,7 @@ import {
19
19
  type GetUpdatesResponse,
20
20
  type WeixinMessage,
21
21
  } from "@openilink/openilink-sdk-node";
22
- import type { CDNMedia, ImageItem } from "@openilink/openilink-sdk-node";
22
+ import type { FileItem, ImageItem, VideoItem } from "@openilink/openilink-sdk-node";
23
23
 
24
24
  import type { PlatformAdapter } from "./platform-adapter.ts";
25
25
  import { setupFileLogging } from "./shared.ts";
@@ -502,8 +502,31 @@ export async function startWechatPlatform(
502
502
  }
503
503
 
504
504
  const WECHAT_IMAGE_DOWNLOAD_DIR = join(homedir(), ".chatccc", "images", "downloads");
505
+ const WECHAT_FILE_DOWNLOAD_DIR = join(homedir(), ".chatccc", "files", "downloads");
506
+ const WECHAT_VIDEO_DOWNLOAD_DIR = join(homedir(), ".chatccc", "videos", "downloads");
505
507
 
506
- function extFromMimeOrName(mime?: string | null, fileName?: string | null): string {
508
+ type WechatMediaDownloader = Pick<OpenIlinkWire, "downloadMedia">;
509
+
510
+ interface WechatMediaDownloadOptions {
511
+ wire?: WechatMediaDownloader | null;
512
+ imageDir?: string;
513
+ fileDir?: string;
514
+ videoDir?: string;
515
+ log?: (msg: string) => void;
516
+ }
517
+
518
+ interface WechatDownloadedMediaAttachments {
519
+ imagePaths: string[];
520
+ filePaths: string[];
521
+ videoPaths: string[];
522
+ messageLines: string[];
523
+ }
524
+
525
+ function extFromMimeOrName(
526
+ mime?: string | null,
527
+ fileName?: string | null,
528
+ fallback = ".bin",
529
+ ): string {
507
530
  if (mime) {
508
531
  const map: Record<string, string> = {
509
532
  "image/png": ".png",
@@ -512,6 +535,17 @@ function extFromMimeOrName(mime?: string | null, fileName?: string | null): stri
512
535
  "image/gif": ".gif",
513
536
  "image/bmp": ".bmp",
514
537
  "image/svg+xml": ".svg",
538
+ "video/mp4": ".mp4",
539
+ "video/quicktime": ".mov",
540
+ "video/x-msvideo": ".avi",
541
+ "video/x-matroska": ".mkv",
542
+ "video/webm": ".webm",
543
+ "video/x-flv": ".flv",
544
+ "text/plain": ".txt",
545
+ "text/csv": ".csv",
546
+ "application/pdf": ".pdf",
547
+ "application/zip": ".zip",
548
+ "application/gzip": ".gz",
515
549
  };
516
550
  const key = mime.split(";")[0].trim().toLowerCase();
517
551
  if (map[key]) return map[key];
@@ -520,25 +554,145 @@ function extFromMimeOrName(mime?: string | null, fileName?: string | null): stri
520
554
  const ext = extname(fileName).toLowerCase();
521
555
  if (ext) return ext;
522
556
  }
523
- return ".png";
557
+ return fallback;
524
558
  }
525
559
 
526
- async function downloadWechatImage(imageItem: ImageItem, msgId?: number): Promise<string> {
527
- const wire = ilinkWire;
560
+ async function downloadWechatImage(
561
+ imageItem: ImageItem,
562
+ msgId?: number,
563
+ options: WechatMediaDownloadOptions = {},
564
+ ): Promise<string> {
565
+ const wire = options.wire ?? ilinkWire;
528
566
  if (!wire) throw new Error("iLink wire not available");
529
567
  if (!imageItem.media) throw new Error("image item has no media");
530
568
 
531
569
  const data = await wire.downloadMedia(imageItem.media);
532
570
  const mime = (imageItem as Record<string, unknown>).mime_type as string | undefined;
533
- const ext = extFromMimeOrName(mime);
571
+ const fileName = (imageItem as Record<string, unknown>).file_name as string | undefined;
572
+ const ext = extFromMimeOrName(mime, fileName, ".png");
534
573
  const key = imageItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
535
- await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
536
- const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}${ext}`);
574
+ const downloadDir = options.imageDir ?? WECHAT_IMAGE_DOWNLOAD_DIR;
575
+ mkdirSync(downloadDir, { recursive: true });
576
+ const localPath = join(downloadDir, `wx_${key}${ext}`);
537
577
  writeFileSync(localPath, data);
538
578
  platformLog(`图片已下载: ${localPath}`);
539
579
  return localPath;
540
580
  }
541
581
 
582
+ function safeLocalFileName(fileName: string): string {
583
+ return basename(fileName).replace(/[<>:"/\\|?*\x00-\x1F]/g, "_");
584
+ }
585
+
586
+ async function downloadWechatFile(
587
+ fileItem: FileItem,
588
+ msgId?: number,
589
+ options: WechatMediaDownloadOptions = {},
590
+ ): Promise<string> {
591
+ const wire = options.wire ?? ilinkWire;
592
+ if (!wire) throw new Error("iLink wire not available");
593
+ if (!fileItem.media) throw new Error("file item has no media");
594
+
595
+ const data = await wire.downloadMedia(fileItem.media);
596
+ const mime = (fileItem as Record<string, unknown>).mime_type as string | undefined;
597
+ const ext = extFromMimeOrName(mime, fileItem.file_name, ".bin");
598
+ const key =
599
+ fileItem.md5?.slice(0, 16) ??
600
+ fileItem.media.aes_key?.slice(0, 16) ??
601
+ (msgId?.toString() ?? Date.now().toString());
602
+ const localName = fileItem.file_name
603
+ ? `wx_${key}_${safeLocalFileName(fileItem.file_name)}`
604
+ : `wx_${key}${ext}`;
605
+ const downloadDir = options.fileDir ?? WECHAT_FILE_DOWNLOAD_DIR;
606
+ mkdirSync(downloadDir, { recursive: true });
607
+ const localPath = join(downloadDir, localName);
608
+ writeFileSync(localPath, data);
609
+ (options.log ?? platformLog)(`文件已下载: ${localPath}`);
610
+ return localPath;
611
+ }
612
+
613
+ async function downloadWechatVideo(
614
+ videoItem: VideoItem,
615
+ msgId?: number,
616
+ options: WechatMediaDownloadOptions = {},
617
+ ): Promise<string> {
618
+ const wire = options.wire ?? ilinkWire;
619
+ if (!wire) throw new Error("iLink wire not available");
620
+ if (!videoItem.media) throw new Error("video item has no media");
621
+
622
+ const data = await wire.downloadMedia(videoItem.media);
623
+ const mime = (videoItem as Record<string, unknown>).mime_type as string | undefined;
624
+ const fileName = (videoItem as Record<string, unknown>).file_name as string | undefined;
625
+ const ext = extFromMimeOrName(mime, fileName, ".mp4");
626
+ const key =
627
+ videoItem.video_md5?.slice(0, 16) ??
628
+ videoItem.media.aes_key?.slice(0, 16) ??
629
+ (msgId?.toString() ?? Date.now().toString());
630
+ const downloadDir = options.videoDir ?? WECHAT_VIDEO_DOWNLOAD_DIR;
631
+ mkdirSync(downloadDir, { recursive: true });
632
+ const localPath = join(downloadDir, `wx_${key}${ext}`);
633
+ writeFileSync(localPath, data);
634
+ (options.log ?? platformLog)(`视频已下载: ${localPath}`);
635
+ return localPath;
636
+ }
637
+
638
+ async function downloadWechatMediaAttachments(
639
+ message: WeixinMessage,
640
+ options: WechatMediaDownloadOptions = {},
641
+ ): Promise<WechatDownloadedMediaAttachments> {
642
+ const imagePaths: string[] = [];
643
+ const filePaths: string[] = [];
644
+ const videoPaths: string[] = [];
645
+ const messageLines: string[] = [];
646
+ const items = message.item_list;
647
+ if (items) {
648
+ for (const item of items) {
649
+ if (item.image_item?.media) {
650
+ try {
651
+ const localPath = await downloadWechatImage(item.image_item, message.message_id, options);
652
+ imagePaths.push(localPath);
653
+ messageLines.push(`[图片] ${localPath}`);
654
+ } catch (err) {
655
+ (options.log ?? platformLog)(`图片下载失败: ${(err as Error).message}`);
656
+ }
657
+ }
658
+
659
+ if (item.file_item?.media) {
660
+ try {
661
+ const localPath = await downloadWechatFile(item.file_item, message.message_id, options);
662
+ filePaths.push(localPath);
663
+ messageLines.push(`[文件] ${localPath}`);
664
+ } catch (err) {
665
+ (options.log ?? platformLog)(`文件下载失败: ${(err as Error).message}`);
666
+ }
667
+ }
668
+
669
+ if (item.video_item?.media) {
670
+ try {
671
+ const localPath = await downloadWechatVideo(item.video_item, message.message_id, options);
672
+ videoPaths.push(localPath);
673
+ messageLines.push(`[视频] ${localPath}`);
674
+ } catch (err) {
675
+ (options.log ?? platformLog)(`视频下载失败: ${(err as Error).message}`);
676
+ }
677
+ }
678
+ }
679
+ }
680
+
681
+ return {
682
+ imagePaths,
683
+ filePaths,
684
+ videoPaths,
685
+ messageLines,
686
+ };
687
+ }
688
+
689
+ export async function _downloadWechatMediaAttachmentsForTest(
690
+ message: WeixinMessage,
691
+ options: WechatMediaDownloadOptions,
692
+ ): Promise<WechatDownloadedMediaAttachments> {
693
+ return downloadWechatMediaAttachments(message, options);
694
+ }
695
+
542
696
  async function handleWechatMessage(
543
697
  message: WeixinMessage,
544
698
  handler: MessageHandler,
@@ -564,37 +718,23 @@ async function handleWechatMessage(
564
718
  const text = extractText(message).trim();
565
719
  const msgTimestamp = message.create_time_ms ?? Date.now();
566
720
 
567
- // 检测并下载图片
568
- const imagePaths: string[] = [];
569
- const items = message.item_list;
570
- if (items) {
571
- for (const item of items) {
572
- if (item.image_item?.media) {
573
- try {
574
- const localPath = await downloadWechatImage(item.image_item, message.message_id);
575
- imagePaths.push(localPath);
576
- } catch (err) {
577
- platformLog(`图片下载失败: ${(err as Error).message}`);
578
- }
579
- }
580
- }
581
- }
721
+ const mediaAttachments = await downloadWechatMediaAttachments(message);
582
722
 
583
- // 构建消息文本:文本内容 + 图片路径
723
+ // 构建消息文本:文本内容 + 媒体路径
584
724
  let fullText = text;
585
- if (imagePaths.length > 0) {
586
- const imageLines = imagePaths.map((p) => `[图片] ${p}`).join("\n");
587
- fullText = fullText ? `${fullText}\n${imageLines}` : imageLines;
725
+ if (mediaAttachments.messageLines.length > 0) {
726
+ const mediaLines = mediaAttachments.messageLines.join("\n");
727
+ fullText = fullText ? `${fullText}\n${mediaLines}` : mediaLines;
588
728
  }
589
729
 
590
- // 纯图片且无文字时跳过(避免空消息触发会话)
730
+ // 纯媒体且无可下载内容时跳过(避免空消息触发会话)
591
731
  if (!fullText.trim()) {
592
732
  platformLog(`跳过纯媒体消息(无文本): chatId=${chatId}`);
593
733
  return;
594
734
  }
595
735
 
596
736
  platformLog(
597
- `收到消息: chatId=${chatId} text="${text.slice(0, 80)}" images=${imagePaths.length}`,
737
+ `收到消息: chatId=${chatId} text="${text.slice(0, 80)}" images=${mediaAttachments.imagePaths.length} files=${mediaAttachments.filePaths.length} videos=${mediaAttachments.videoPaths.length}`,
598
738
  );
599
739
  appendChatLog(chatId, chatId, fullText);
600
740