wechat-to-anything 0.5.3 → 0.5.5

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
@@ -141,7 +141,7 @@ def chat(request):
141
141
  return {"choices": [{"message": {"role": "assistant", "content": reply}}]}
142
142
  ```
143
143
 
144
- > 完整语音发送示例见 [examples/voice-test.mjs](examples/voice-test.mjs)
144
+ > 示例:[examples/image-test.mjs](examples/image-test.mjs) · [examples/voice-test.mjs](examples/voice-test.mjs)
145
145
 
146
146
  ## 凭证
147
147
 
package/cli/bridge.mjs CHANGED
@@ -292,8 +292,8 @@ export async function start(agents, defaultAgent) {
292
292
 
293
293
  // 检查回复是否包含 [audio:path/url]
294
294
  const audioMatch = reply.match(/\[audio:(.*?)\]/);
295
- // 检查回复是否包含图片 URL(markdown 格式)
296
- const imageMatch = reply.match(/!\[.*?\]\((https?:\/\/[^\s)]+)\)/);
295
+ // 检查回复是否包含图片(markdown 格式,支持 URL 和 data URI)
296
+ const imageMatch = reply.match(/!\[.*?\]\(((?:https?:\/\/|data:image\/)[^\s)]+)\)/);
297
297
 
298
298
  if (audioMatch) {
299
299
  const audioSrc = audioMatch[1];
package/cli/cdn.mjs CHANGED
@@ -72,7 +72,96 @@ export async function downloadMediaToFile(encryptQueryParam, aesKeyBase64, ext =
72
72
  return { filePath, buffer: buf };
73
73
  }
74
74
 
75
- // ─── 上传 ───────────────────────────────────────────────────────────
75
+ // ─── 图片上传(含缩略图,HD 质量)─────────────────────────────────
76
+
77
+ /**
78
+ * 上传图片到 CDN,同时生成并上传缩略图(微信需要缩略图才显示高清)
79
+ * @returns {{ downloadParam, thumbDownloadParam, aeskey, fileSize, fileSizeCiphertext }}
80
+ */
81
+ export async function uploadImageWithThumb(filePath, toUserId, token) {
82
+ const { buildHeaders, BASE_URL } = await import("./weixin.mjs");
83
+ const { execSync } = await import("child_process");
84
+
85
+ const plaintext = await readFile(filePath);
86
+ const rawsize = plaintext.length;
87
+ const rawfilemd5 = createHash("md5").update(plaintext).digest("hex");
88
+ const filesize = aesEcbPaddedSize(rawsize);
89
+ const filekey = randomBytes(16).toString("hex");
90
+ const aeskey = randomBytes(16);
91
+
92
+ // 生成缩略图
93
+ const thumbPath = `/tmp/wxta_thumb_${Date.now()}.jpg`;
94
+ try {
95
+ execSync(`sips --resampleWidth 120 "${filePath}" --out "${thumbPath}" 2>/dev/null`);
96
+ } catch {
97
+ // sips 失败时用原图当缩略图
98
+ await writeFile(thumbPath, plaintext);
99
+ }
100
+ const thumb = await readFile(thumbPath);
101
+
102
+ // 1. getUploadUrl(带缩略图信息)
103
+ const uploadBody = JSON.stringify({
104
+ filekey,
105
+ media_type: 1, // IMAGE
106
+ to_user_id: toUserId,
107
+ rawsize,
108
+ rawfilemd5,
109
+ filesize,
110
+ thumb_rawsize: thumb.length,
111
+ thumb_rawfilemd5: createHash("md5").update(thumb).digest("hex"),
112
+ thumb_filesize: aesEcbPaddedSize(thumb.length),
113
+ no_need_thumb: false,
114
+ aeskey: aeskey.toString("hex"),
115
+ base_info: {},
116
+ });
117
+
118
+ const uploadRes = await fetch(`${BASE_URL}/ilink/bot/getuploadurl`, {
119
+ method: "POST",
120
+ headers: buildHeaders(token, uploadBody),
121
+ body: uploadBody,
122
+ });
123
+ if (!uploadRes.ok) throw new Error(`getUploadUrl failed: ${uploadRes.status}`);
124
+ const uploadJson = await uploadRes.json();
125
+ if (!uploadJson.upload_param) throw new Error(`getUploadUrl: no upload_param`);
126
+
127
+ // 2. 上传原图
128
+ const origCipher = encryptAesEcb(plaintext, aeskey);
129
+ const origUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.upload_param)}&filekey=${encodeURIComponent(filekey)}`;
130
+ const origRes = await fetch(origUrl, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/octet-stream" },
133
+ body: new Uint8Array(origCipher),
134
+ });
135
+ if (origRes.status !== 200) throw new Error(`CDN upload orig failed: ${origRes.status}`);
136
+ const downloadParam = origRes.headers.get("x-encrypted-query-param") || origRes.headers.get("x-encrypted-param");
137
+ if (!downloadParam) throw new Error("CDN upload: missing download param");
138
+
139
+ // 3. 上传缩略图
140
+ let thumbDownloadParam = null;
141
+ if (uploadJson.thumb_upload_param) {
142
+ const thumbCipher = encryptAesEcb(thumb, aeskey);
143
+ const thumbUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.thumb_upload_param)}&filekey=${encodeURIComponent(filekey)}`;
144
+ const thumbRes = await fetch(thumbUrl, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/octet-stream" },
147
+ body: new Uint8Array(thumbCipher),
148
+ });
149
+ if (thumbRes.status === 200) {
150
+ thumbDownloadParam = thumbRes.headers.get("x-encrypted-query-param") || thumbRes.headers.get("x-encrypted-param");
151
+ }
152
+ }
153
+
154
+ return {
155
+ downloadParam,
156
+ thumbDownloadParam,
157
+ aeskey: aeskey.toString("hex"),
158
+ fileSize: rawsize,
159
+ fileSizeCiphertext: filesize,
160
+ filekey,
161
+ };
162
+ }
163
+
164
+ // ─── 通用上传 ───────────────────────────────────────────────────────
76
165
 
77
166
  /**
78
167
  * 上传文件到微信 CDN
package/cli/weixin.mjs CHANGED
@@ -187,23 +187,50 @@ export async function sendMessage(token, to, text, contextToken) {
187
187
  }
188
188
 
189
189
  /**
190
- * 发送图片消息(下载 → CDN 上传发送)
190
+ * 发送图片消息(下载 → 生成缩略图 → CDN 上传原图+缩略图发送 HD)
191
191
  */
192
192
  export async function sendImageByUrl(token, to, contextToken, imageUrl) {
193
- const { writeFile: wf } = await import("node:fs/promises");
193
+ const { writeFile: wf, readFile: rf } = await import("node:fs/promises");
194
194
 
195
- // 下载图片
196
- const resp = await fetch(imageUrl);
197
- if (!resp.ok) throw new Error(`图片下载失败: ${resp.status}`);
198
- const buf = Buffer.from(await resp.arrayBuffer());
199
- const tmpPath = "/tmp/wxta_image_send.jpg";
200
- await wf(tmpPath, buf);
195
+ // 获取图片数据
196
+ let tmpPath;
197
+ if (imageUrl.startsWith("/")) {
198
+ // 本地文件路径,直接使用
199
+ tmpPath = imageUrl;
200
+ } else {
201
+ let buf;
202
+ if (imageUrl.startsWith("data:")) {
203
+ const b64 = imageUrl.split(",")[1];
204
+ buf = Buffer.from(b64, "base64");
205
+ } else {
206
+ const resp = await fetch(imageUrl);
207
+ if (!resp.ok) throw new Error(`图片下载失败: ${resp.status}`);
208
+ buf = Buffer.from(await resp.arrayBuffer());
209
+ }
210
+ tmpPath = "/tmp/wxta_image_send.jpg";
211
+ await wf(tmpPath, buf);
212
+ }
201
213
 
202
- // CDN 上传 (mediaType=1 = IMAGE)
203
- const { uploadToCdn } = await import("./cdn.mjs");
204
- const cdn = await uploadToCdn(tmpPath, to, token, 1);
214
+ // CDN 上传(含缩略图,确保高清显示)
215
+ const { uploadImageWithThumb } = await import("./cdn.mjs");
216
+ const cdn = await uploadImageWithThumb(tmpPath, to, token);
205
217
  const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
206
218
 
219
+ // 构造 image_item
220
+ const imageItem = {
221
+ media: {
222
+ encrypt_query_param: cdn.downloadParam,
223
+ aes_key: aesKeyB64,
224
+ },
225
+ mid_size: cdn.fileSizeCiphertext,
226
+ };
227
+ if (cdn.thumbDownloadParam) {
228
+ imageItem.thumb_media = {
229
+ encrypt_query_param: cdn.thumbDownloadParam,
230
+ aes_key: aesKeyB64,
231
+ };
232
+ }
233
+
207
234
  // 发送
208
235
  await apiPost(
209
236
  "ilink/bot/sendmessage",
@@ -214,15 +241,7 @@ export async function sendImageByUrl(token, to, contextToken, imageUrl) {
214
241
  client_id: crypto.randomUUID(),
215
242
  message_type: 2,
216
243
  message_state: 2,
217
- item_list: [{
218
- type: 2, // IMAGE
219
- image_item: {
220
- media: {
221
- encrypt_query_param: cdn.downloadParam,
222
- aes_key: aesKeyB64,
223
- },
224
- },
225
- }],
244
+ item_list: [{ type: 2, image_item: imageItem }],
226
245
  context_token: contextToken,
227
246
  },
228
247
  base_info: {},
@@ -7,7 +7,7 @@ const token = creds.token;
7
7
  const to = creds.userId;
8
8
 
9
9
  // 测试图片 URL(可替换为任意图片地址)
10
- const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png";
10
+ const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
11
11
 
12
12
  // 获取 contextToken
13
13
  const { getUpdates, buildHeaders, BASE_URL } = await import("../cli/weixin.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-to-anything",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {