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.
- package/config.sample.json +3 -2
- package/im-skills/feishu-skill/download-video.mjs +17 -10
- package/im-skills/wechat-file-skill/receive-send-file.md +42 -0
- package/im-skills/wechat-file-skill/send-file.mjs +101 -0
- package/im-skills/wechat-file-skill/skill.md +11 -0
- package/im-skills/{wechat-skill → wechat-image-skill}/receive-send-image.md +39 -39
- package/im-skills/{wechat-skill → wechat-image-skill}/send-image.mjs +84 -80
- package/im-skills/{wechat-skill → wechat-image-skill}/skill.md +11 -11
- package/im-skills/wechat-video-skill/receive-send-video.md +41 -0
- package/im-skills/wechat-video-skill/send-video.mjs +84 -0
- package/im-skills/wechat-video-skill/skill.md +11 -0
- package/package.json +59 -59
- package/src/__tests__/im-skills.test.ts +57 -2
- package/src/__tests__/orchestrator.test.ts +1 -1
- package/src/__tests__/wechat-platform.test.ts +65 -0
- package/src/config.ts +24 -2
- package/src/index.ts +5 -2
- package/src/orchestrator.ts +4 -3
- package/src/session.ts +7 -3
- package/src/web-ui.ts +26 -3
- package/src/wechat-platform.ts +170 -30
package/src/wechat-platform.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
557
|
+
return fallback;
|
|
524
558
|
}
|
|
525
559
|
|
|
526
|
-
async function downloadWechatImage(
|
|
527
|
-
|
|
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
|
|
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
|
-
|
|
536
|
-
|
|
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 (
|
|
586
|
-
const
|
|
587
|
-
fullText = fullText ? `${fullText}\n${
|
|
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
|
|