wechat-to-anything 0.6.1 → 0.6.2

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > ⭐ 如果这个项目对你有帮助,请给个 Star!本项目仅用于技术学习和交流,开源不易。
6
6
 
7
- 微信双向支持 Agent 多种模态消息发送和接收,支持文本、图片、语音、文件。
7
+ 微信双向支持 Agent 多种模态消息发送和接收,支持文本、图片、语音、视频、文件。
8
8
 
9
9
  <p align="center">
10
10
  <img src="docs/wechat-image-send.png" width="250" alt="发送图片给 Agent 识别" />
@@ -24,10 +24,10 @@
24
24
 
25
25
  ### 多种模态支持
26
26
 
27
- | 方向 | 图片 | 语音 | 文件 |
28
- |---|---|---|---|
29
- | **微信 → Agent** | ✅ 自动识别 | ✅ 语音转文字 | ✅ 提取文本 |
30
- | **Agent → 微信** | ✅ 自动发图 | ✅ 语音消息 | 文本回复 |
27
+ | 方向 | 图片 | 语音 | 视频 | 文件 |
28
+ |---|---|---|---|---|
29
+ | **微信 → Agent** | ✅ 自动识别 | ✅ 语音转文字 | ✅ 自动接收 | ✅ 提取文本 |
30
+ | **Agent → 微信** | ✅ 自动发图 | ✅ 语音消息 | ✅ 视频消息 | 文本回复 |
31
31
 
32
32
  ## 前置条件
33
33
 
@@ -135,13 +135,21 @@ npx wechat-to-anything \
135
135
  ```python
136
136
  @app.post("/v1/chat/completions")
137
137
  def chat(request):
138
- message = request.json["messages"][-1]["content"]
139
138
  audio_path = your_tts(message) # → /tmp/reply.mp3
140
139
  reply = f"[audio:{audio_path}]\n这是文字版内容"
141
140
  return {"choices": [{"message": {"role": "assistant", "content": reply}}]}
142
141
  ```
143
142
 
144
- > 示例:[examples/image-test.mjs](examples/image-test.mjs) · [examples/voice-test.mjs](examples/voice-test.mjs)
143
+ **视频(Agent → 微信)**:回复中包含 `[video:path 或 URL]` 自动发视频消息(含缩略图)。需要 `ffmpeg`。
144
+
145
+ ```python
146
+ @app.post("/v1/chat/completions")
147
+ def chat(request):
148
+ reply = "[video:/tmp/demo.mp4]\n这是视频描述"
149
+ return {"choices": [{"message": {"role": "assistant", "content": reply}}]}
150
+ ```
151
+
152
+ > 示例:[examples/image-test.mjs](examples/image-test.mjs) · [examples/voice-test.mjs](examples/voice-test.mjs) · [examples/video-test-local.mjs](examples/video-test-local.mjs)
145
153
 
146
154
  ## 凭证
147
155
 
@@ -1,146 +1,55 @@
1
1
  /**
2
- * 视频发送 v18 — 完全匹配微信原始 video_item 格式
2
+ * 视频发送测试脚本
3
3
  *
4
- * 从微信收到的真实 video_item:
5
- * - aes_key: 44 chars (hex→UTF-8→base64)
6
- * - 没有 encrypt_type
7
- * - video_md5
8
- * - thumb 和 video 共享 aes_key
9
- * - 有 play_length, thumb_media 完整字段
4
+ * 用法: node examples/video-test-local.mjs [视频文件路径]
5
+ *
6
+ * 该脚本直接通过 CDN 上传视频文件,发送到自己的微信(用于测试)。
7
+ * 需要先扫码登录获取 credentials。
10
8
  */
11
- import { readFileSync, unlinkSync } from "fs";
12
- import { readFile } from "fs/promises";
13
- import { homedir, tmpdir } from "os";
14
- import { join } from "path";
15
- import { execSync } from "child_process";
16
- import { createHash, randomBytes } from "crypto";
17
- import crypto from "crypto";
9
+ import { readFileSync } from "fs";
10
+ import { homedir } from "os";
18
11
 
19
12
  const creds = JSON.parse(readFileSync(homedir() + "/.wechat-to-anything/credentials.json", "utf-8"));
20
- const videoPath = process.argv[2] || "/Users/zxw/Downloads/wechat-voice-demo.mp4";
13
+ const videoPath = process.argv[2];
14
+ if (!videoPath) {
15
+ console.error("用法: node examples/video-test-local.mjs <视频文件路径>");
16
+ process.exit(1);
17
+ }
21
18
 
22
- const { encryptAesEcb, aesEcbPaddedSize, CDN_BASE_URL } = await import("../cli/cdn.mjs");
19
+ const { uploadVideoWithThumb } = await import("../cli/cdn.mjs");
23
20
  const { getUpdates, buildHeaders, BASE_URL } = await import("../cli/weixin.mjs");
21
+ import crypto from "crypto";
24
22
 
25
- // 获取 fresh context_token
23
+ // 获取 context_token
26
24
  const msgs = await getUpdates(creds.token);
27
25
  const contextToken = msgs?.context_token || "";
28
26
 
29
- // 视频
30
- const plaintext = await readFile(videoPath);
31
- const rawsize = plaintext.length;
32
- const rawfilemd5 = createHash("md5").update(plaintext).digest("hex");
33
- const filesize = aesEcbPaddedSize(rawsize);
34
- const filekey = randomBytes(16).toString("hex");
35
- const aeskey = randomBytes(16);
36
-
37
- let playLength = 10;
38
- try {
39
- const dur = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`, { encoding: "utf-8" }).trim();
40
- playLength = Math.round(parseFloat(dur));
41
- } catch {}
42
-
43
- // 缩略图
44
- const thumbPath = join(tmpdir(), `wx-thumb-${Date.now()}.jpg`);
45
- execSync(`ffmpeg -y -i "${videoPath}" -vframes 1 -vf "scale=224:-1" -q:v 5 "${thumbPath}" 2>/dev/null`);
46
- const thumb = await readFile(thumbPath);
47
- let thumbWidth = 224, thumbHeight = 486;
48
- try {
49
- const s = execSync(`sips -g pixelWidth -g pixelHeight "${thumbPath}"`).toString();
50
- const w = s.match(/pixelWidth:\s*(\d+)/); const h = s.match(/pixelHeight:\s*(\d+)/);
51
- if (w) thumbWidth = parseInt(w[1]); if (h) thumbHeight = parseInt(h[1]);
52
- } catch {}
53
-
54
- console.log("视频:", rawsize, "→", filesize, "bytes | md5:", rawfilemd5);
55
- console.log("时长:", playLength, "s | 缩略图:", thumb.length, "bytes", thumbWidth, "x", thumbHeight);
56
-
57
- // aes_key 编码: hex string → UTF-8 bytes → base64 = 44 chars(和微信原始格式一致)
58
- const aesKeyHex = aeskey.toString("hex");
59
- const aesKey44 = Buffer.from(aesKeyHex).toString("base64");
60
- console.log("aes_key:", aesKey44, "(", aesKey44.length, "chars)");
61
-
62
- // getUploadUrl (bundled thumb, no_need_thumb=false)
63
- const uploadBody = JSON.stringify({
64
- filekey,
65
- media_type: 2,
66
- to_user_id: creds.userId,
67
- rawsize,
68
- rawfilemd5,
69
- filesize,
70
- thumb_rawsize: thumb.length,
71
- thumb_rawfilemd5: createHash("md5").update(thumb).digest("hex"),
72
- thumb_filesize: aesEcbPaddedSize(thumb.length),
73
- no_need_thumb: false,
74
- aeskey: aesKeyHex,
75
- base_info: {},
76
- });
77
-
78
- const uploadRes = await fetch(`${BASE_URL}/ilink/bot/getuploadurl`, {
79
- method: "POST",
80
- headers: buildHeaders(creds.token, uploadBody),
81
- body: uploadBody,
82
- });
83
- const uploadJson = await uploadRes.json();
84
- console.log("\ngetUploadUrl:", uploadJson.upload_param ? "✅" : "❌", "thumb:", uploadJson.thumb_upload_param ? "✅" : "❌");
27
+ console.log("上传视频...");
28
+ const cdn = await uploadVideoWithThumb(videoPath, creds.userId, creds.token);
29
+ console.log("✅ 上传完成 | 时长:", cdn.playLength, "s | 大小:", cdn.fileSizeCiphertext, "bytes");
85
30
 
86
- // 上传视频
87
- const videoCipher = encryptAesEcb(plaintext, aeskey);
88
- const videoUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.upload_param)}&filekey=${encodeURIComponent(filekey)}`;
89
- const videoRes = await fetch(videoUrl, {
90
- method: "POST",
91
- headers: { "Content-Type": "application/octet-stream" },
92
- body: new Uint8Array(videoCipher),
93
- });
94
- const videoDP = videoRes.headers.get("x-encrypted-query-param") || videoRes.headers.get("x-encrypted-param");
95
- console.log("视频 CDN:", videoRes.status, "(400=probe error, 可忽略) dp:", videoDP ? "✅" : "❌");
31
+ // aes_key: hex→UTF8→base64 = 44 chars(微信格式)
32
+ const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
96
33
 
97
- // 上传缩略图
98
- let thumbDP = null;
99
- if (uploadJson.thumb_upload_param) {
100
- const thumbCipher = encryptAesEcb(thumb, aeskey);
101
- const thumbUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.thumb_upload_param)}&filekey=${encodeURIComponent(filekey)}`;
102
- const thumbRes = await fetch(thumbUrl, {
103
- method: "POST",
104
- headers: { "Content-Type": "application/octet-stream" },
105
- body: new Uint8Array(thumbCipher),
106
- });
107
- thumbDP = thumbRes.headers.get("x-encrypted-query-param") || thumbRes.headers.get("x-encrypted-param");
108
- console.log("缩略图 CDN:", thumbRes.status, "dp:", thumbDP ? "✅" : "❌");
109
- }
110
- try { unlinkSync(thumbPath); } catch {}
111
-
112
- if (!videoDP) { console.error("❌ 无 videoDP"); process.exit(1); }
113
-
114
- // ═══ 构造消息 — 完全匹配微信原始格式 ═══
115
34
  const videoItem = {
116
- type: 5,
117
- video_item: {
118
- media: {
119
- encrypt_query_param: videoDP,
120
- aes_key: aesKey44,
121
- // 没有 encrypt_type — 微信原始格式没有
122
- },
123
- video_size: filesize,
124
- play_length: playLength,
125
- video_md5: rawfilemd5, // ← 之前一直少这个!
126
- ...(thumbDP ? {
127
- thumb_media: {
128
- encrypt_query_param: thumbDP,
129
- aes_key: aesKey44, // 共享 key
130
- // 没有 encrypt_type
131
- },
132
- thumb_size: aesEcbPaddedSize(thumb.length),
133
- thumb_height: thumbHeight,
134
- thumb_width: thumbWidth,
135
- } : {}),
35
+ media: {
36
+ encrypt_query_param: cdn.downloadParam,
37
+ aes_key: aesKeyB64,
136
38
  },
39
+ video_size: cdn.fileSizeCiphertext,
40
+ play_length: cdn.playLength,
41
+ video_md5: cdn.videoMd5,
137
42
  };
138
43
 
139
- console.log("\nvideo_item:", JSON.stringify(videoItem, null, 2));
140
-
141
- // 重新获取 fresh context_token
142
- const freshMsgs = await getUpdates(creds.token);
143
- const freshCT = freshMsgs?.context_token || contextToken;
44
+ if (cdn.thumbDownloadParam) {
45
+ videoItem.thumb_media = {
46
+ encrypt_query_param: cdn.thumbDownloadParam,
47
+ aes_key: aesKeyB64,
48
+ };
49
+ videoItem.thumb_size = cdn.thumbSizeCiphertext;
50
+ videoItem.thumb_width = cdn.thumbWidth;
51
+ videoItem.thumb_height = cdn.thumbHeight;
52
+ }
144
53
 
145
54
  const body = JSON.stringify({
146
55
  msg: {
@@ -149,8 +58,8 @@ const body = JSON.stringify({
149
58
  client_id: crypto.randomUUID(),
150
59
  message_type: 2,
151
60
  message_state: 2,
152
- item_list: [videoItem],
153
- context_token: freshCT,
61
+ item_list: [{ type: 5, video_item: videoItem }],
62
+ context_token: contextToken,
154
63
  },
155
64
  base_info: {},
156
65
  });
@@ -161,6 +70,8 @@ const resp = await fetch(`${BASE_URL}/ilink/bot/sendmessage`, {
161
70
  body,
162
71
  });
163
72
  const text = await resp.text();
164
- console.log("\nsendmessage:", resp.status, text || "(empty)");
165
- if (text === "{}" || text === "") console.log("✅ 请检查微信!");
166
- else console.log("ret:", JSON.parse(text).ret);
73
+ if (text === "{}" || text === "") {
74
+ console.log("✅ 发送成功!请检查微信");
75
+ } else {
76
+ console.log("❌ 发送失败:", text);
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-to-anything",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {