chatccc 0.2.63 → 0.2.65

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.
Files changed (35) hide show
  1. package/config.sample.json +2 -3
  2. package/im-skills/feishu-skill/download-video.mjs +10 -17
  3. package/im-skills/wechat-file-skill/receive-send-file.md +39 -42
  4. package/im-skills/wechat-file-skill/send-file.mjs +84 -101
  5. package/im-skills/wechat-file-skill/skill.md +11 -11
  6. package/im-skills/wechat-image-skill/receive-send-image.md +5 -5
  7. package/im-skills/wechat-image-skill/send-image.mjs +2 -6
  8. package/im-skills/wechat-image-skill/skill.md +4 -4
  9. package/im-skills/wechat-video-skill/receive-send-video.md +39 -41
  10. package/im-skills/wechat-video-skill/send-video.mjs +80 -84
  11. package/im-skills/wechat-video-skill/skill.md +11 -11
  12. package/package.json +59 -59
  13. package/src/__tests__/agent-platform-routing.test.ts +26 -0
  14. package/src/__tests__/claude-adapter.test.ts +48 -48
  15. package/src/__tests__/config-reload.test.ts +14 -14
  16. package/src/__tests__/config-sample.test.ts +22 -22
  17. package/src/__tests__/im-skills.test.ts +49 -50
  18. package/src/__tests__/orchestrator.test.ts +1 -1
  19. package/src/__tests__/privacy.test.ts +142 -142
  20. package/src/__tests__/simplify.test.ts +282 -282
  21. package/src/__tests__/web-ui.test.ts +23 -23
  22. package/src/__tests__/wechat-platform.test.ts +0 -65
  23. package/src/adapters/claude-adapter.ts +40 -40
  24. package/src/agent-file-rpc.ts +19 -4
  25. package/src/agent-image-rpc.ts +19 -4
  26. package/src/agent-platform-routing.ts +28 -0
  27. package/src/config.ts +2 -24
  28. package/src/im-skills.ts +9 -0
  29. package/src/index.ts +11 -6
  30. package/src/orchestrator.ts +3 -4
  31. package/src/privacy.ts +67 -67
  32. package/src/session.ts +15 -3
  33. package/src/simplify.ts +119 -119
  34. package/src/web-ui.ts +3 -26
  35. package/src/wechat-platform.ts +68 -151
@@ -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 { basename, dirname, extname, join } from "node:path";
13
+ import { 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 { FileItem, ImageItem, VideoItem } from "@openilink/openilink-sdk-node";
22
+ import type { CDNMedia, 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,31 +502,8 @@ 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");
507
505
 
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 {
506
+ function extFromMimeOrName(mime?: string | null, fileName?: string | null): string {
530
507
  if (mime) {
531
508
  const map: Record<string, string> = {
532
509
  "image/png": ".png",
@@ -535,17 +512,6 @@ function extFromMimeOrName(
535
512
  "image/gif": ".gif",
536
513
  "image/bmp": ".bmp",
537
514
  "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",
549
515
  };
550
516
  const key = mime.split(";")[0].trim().toLowerCase();
551
517
  if (map[key]) return map[key];
@@ -554,187 +520,138 @@ function extFromMimeOrName(
554
520
  const ext = extname(fileName).toLowerCase();
555
521
  if (ext) return ext;
556
522
  }
557
- return fallback;
523
+ return ".png";
558
524
  }
559
525
 
560
- async function downloadWechatImage(
561
- imageItem: ImageItem,
562
- msgId?: number,
563
- options: WechatMediaDownloadOptions = {},
564
- ): Promise<string> {
565
- const wire = options.wire ?? ilinkWire;
526
+ async function downloadWechatImage(imageItem: ImageItem, msgId?: number): Promise<string> {
527
+ const wire = ilinkWire;
566
528
  if (!wire) throw new Error("iLink wire not available");
567
529
  if (!imageItem.media) throw new Error("image item has no media");
568
530
 
569
531
  const data = await wire.downloadMedia(imageItem.media);
570
532
  const mime = (imageItem as Record<string, unknown>).mime_type as string | undefined;
571
- const fileName = (imageItem as Record<string, unknown>).file_name as string | undefined;
572
- const ext = extFromMimeOrName(mime, fileName, ".png");
533
+ const ext = extFromMimeOrName(mime);
573
534
  const key = imageItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
574
- const downloadDir = options.imageDir ?? WECHAT_IMAGE_DOWNLOAD_DIR;
575
- mkdirSync(downloadDir, { recursive: true });
576
- const localPath = join(downloadDir, `wx_${key}${ext}`);
535
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
536
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}${ext}`);
577
537
  writeFileSync(localPath, data);
578
538
  platformLog(`图片已下载: ${localPath}`);
579
539
  return localPath;
580
540
  }
581
541
 
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;
542
+ async function downloadWechatFile(fileItem: FileItem, msgId?: number): Promise<string> {
543
+ const wire = ilinkWire;
592
544
  if (!wire) throw new Error("iLink wire not available");
593
545
  if (!fileItem.media) throw new Error("file item has no media");
594
546
 
595
547
  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);
548
+ const fileName = fileItem.file_name || "file";
549
+ const ext = extname(fileName).toLowerCase() || extFromMimeOrName(undefined);
550
+ const key = fileItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
551
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
552
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}_${fileName}`);
608
553
  writeFileSync(localPath, data);
609
- (options.log ?? platformLog)(`文件已下载: ${localPath}`);
554
+ platformLog(`文件已下载: ${localPath}`);
610
555
  return localPath;
611
556
  }
612
557
 
613
- async function downloadWechatVideo(
614
- videoItem: VideoItem,
615
- msgId?: number,
616
- options: WechatMediaDownloadOptions = {},
617
- ): Promise<string> {
618
- const wire = options.wire ?? ilinkWire;
558
+ async function downloadWechatVideo(videoItem: VideoItem, msgId?: number): Promise<string> {
559
+ const wire = ilinkWire;
619
560
  if (!wire) throw new Error("iLink wire not available");
620
561
  if (!videoItem.media) throw new Error("video item has no media");
621
562
 
622
563
  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}`);
564
+ const ext = extFromMimeOrName(undefined);
565
+ const key = videoItem.media.aes_key?.slice(0, 16) ?? (msgId?.toString() ?? Date.now().toString());
566
+ await mkdirSync(WECHAT_IMAGE_DOWNLOAD_DIR, { recursive: true });
567
+ const localPath = join(WECHAT_IMAGE_DOWNLOAD_DIR, `wx_${key}${ext}`);
633
568
  writeFileSync(localPath, data);
634
- (options.log ?? platformLog)(`视频已下载: ${localPath}`);
569
+ platformLog(`视频已下载: ${localPath}`);
635
570
  return localPath;
636
571
  }
637
572
 
638
- async function downloadWechatMediaAttachments(
573
+ async function handleWechatMessage(
639
574
  message: WeixinMessage,
640
- options: WechatMediaDownloadOptions = {},
641
- ): Promise<WechatDownloadedMediaAttachments> {
575
+ handler: MessageHandler,
576
+ ): Promise<void> {
577
+ const chatId = String(message.from_user_id ?? "");
578
+ if (!chatId) {
579
+ platformLog("跳过无 from_user_id 的消息");
580
+ return;
581
+ }
582
+
583
+ // 保存 lastChatId 供下次启动时发送通知
584
+ const current = readSnapshot();
585
+ if (current.lastChatId !== chatId) {
586
+ updateSnapshot({ lastChatId: chatId });
587
+ }
588
+
589
+ // 保存 context_token 到 snapshot(供重启后启动通知使用)和内存
590
+ if (message.context_token) {
591
+ contextTokenMap.set(chatId, message.context_token);
592
+ updateSnapshot({ contextToken: message.context_token, lastChatId: chatId });
593
+ }
594
+
595
+ const text = extractText(message).trim();
596
+ const msgTimestamp = message.create_time_ms ?? Date.now();
597
+
598
+ // 检测并下载图片/文件/视频
642
599
  const imagePaths: string[] = [];
643
600
  const filePaths: string[] = [];
644
601
  const videoPaths: string[] = [];
645
- const messageLines: string[] = [];
646
602
  const items = message.item_list;
647
603
  if (items) {
648
604
  for (const item of items) {
649
605
  if (item.image_item?.media) {
650
606
  try {
651
- const localPath = await downloadWechatImage(item.image_item, message.message_id, options);
607
+ const localPath = await downloadWechatImage(item.image_item, message.message_id);
652
608
  imagePaths.push(localPath);
653
- messageLines.push(`[图片] ${localPath}`);
654
609
  } catch (err) {
655
- (options.log ?? platformLog)(`图片下载失败: ${(err as Error).message}`);
610
+ platformLog(`图片下载失败: ${(err as Error).message}`);
656
611
  }
657
612
  }
658
-
659
613
  if (item.file_item?.media) {
660
614
  try {
661
- const localPath = await downloadWechatFile(item.file_item, message.message_id, options);
615
+ const localPath = await downloadWechatFile(item.file_item, message.message_id);
662
616
  filePaths.push(localPath);
663
- messageLines.push(`[文件] ${localPath}`);
664
617
  } catch (err) {
665
- (options.log ?? platformLog)(`文件下载失败: ${(err as Error).message}`);
618
+ platformLog(`文件下载失败: ${(err as Error).message}`);
666
619
  }
667
620
  }
668
-
669
621
  if (item.video_item?.media) {
670
622
  try {
671
- const localPath = await downloadWechatVideo(item.video_item, message.message_id, options);
623
+ const localPath = await downloadWechatVideo(item.video_item, message.message_id);
672
624
  videoPaths.push(localPath);
673
- messageLines.push(`[视频] ${localPath}`);
674
625
  } catch (err) {
675
- (options.log ?? platformLog)(`视频下载失败: ${(err as Error).message}`);
626
+ platformLog(`视频下载失败: ${(err as Error).message}`);
676
627
  }
677
628
  }
678
629
  }
679
630
  }
680
631
 
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
-
696
- async function handleWechatMessage(
697
- message: WeixinMessage,
698
- handler: MessageHandler,
699
- ): Promise<void> {
700
- const chatId = String(message.from_user_id ?? "");
701
- if (!chatId) {
702
- platformLog("跳过无 from_user_id 的消息");
703
- return;
704
- }
705
-
706
- // 保存 lastChatId 供下次启动时发送通知
707
- const current = readSnapshot();
708
- if (current.lastChatId !== chatId) {
709
- updateSnapshot({ lastChatId: chatId });
632
+ // 构建消息文本:文本内容 + 图片/文件/视频路径
633
+ let fullText = text;
634
+ if (imagePaths.length > 0) {
635
+ const imageLines = imagePaths.map((p) => `[图片] ${p}`).join("\n");
636
+ fullText = fullText ? `${fullText}\n${imageLines}` : imageLines;
710
637
  }
711
-
712
- // 保存 context_token snapshot(供重启后启动通知使用)和内存
713
- if (message.context_token) {
714
- contextTokenMap.set(chatId, message.context_token);
715
- updateSnapshot({ contextToken: message.context_token, lastChatId: chatId });
638
+ if (filePaths.length > 0) {
639
+ const fileLines = filePaths.map((p) => `[文件] ${p}`).join("\n");
640
+ fullText = fullText ? `${fullText}\n${fileLines}` : fileLines;
716
641
  }
717
-
718
- const text = extractText(message).trim();
719
- const msgTimestamp = message.create_time_ms ?? Date.now();
720
-
721
- const mediaAttachments = await downloadWechatMediaAttachments(message);
722
-
723
- // 构建消息文本:文本内容 + 媒体路径
724
- let fullText = text;
725
- if (mediaAttachments.messageLines.length > 0) {
726
- const mediaLines = mediaAttachments.messageLines.join("\n");
727
- fullText = fullText ? `${fullText}\n${mediaLines}` : mediaLines;
642
+ if (videoPaths.length > 0) {
643
+ const videoLines = videoPaths.map((p) => `[视频] ${p}`).join("\n");
644
+ fullText = fullText ? `${fullText}\n${videoLines}` : videoLines;
728
645
  }
729
646
 
730
- // 纯媒体且无可下载内容时跳过(避免空消息触发会话)
647
+ // 纯图片且无文字时跳过(避免空消息触发会话)
731
648
  if (!fullText.trim()) {
732
649
  platformLog(`跳过纯媒体消息(无文本): chatId=${chatId}`);
733
650
  return;
734
651
  }
735
652
 
736
653
  platformLog(
737
- `收到消息: chatId=${chatId} text="${text.slice(0, 80)}" images=${mediaAttachments.imagePaths.length} files=${mediaAttachments.filePaths.length} videos=${mediaAttachments.videoPaths.length}`,
654
+ `收到消息: chatId=${chatId} text="${text.slice(0, 80)}" images=${imagePaths.length} files=${filePaths.length} videos=${videoPaths.length}`,
738
655
  );
739
656
  appendChatLog(chatId, chatId, fullText);
740
657