wechat-to-anything 0.5.4 → 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/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,29 +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
195
  // 获取图片数据
196
- let buf;
197
- if (imageUrl.startsWith("data:")) {
198
- const b64 = imageUrl.split(",")[1];
199
- buf = Buffer.from(b64, "base64");
196
+ let tmpPath;
197
+ if (imageUrl.startsWith("/")) {
198
+ // 本地文件路径,直接使用
199
+ tmpPath = imageUrl;
200
200
  } else {
201
- const resp = await fetch(imageUrl);
202
- if (!resp.ok) throw new Error(`图片下载失败: ${resp.status}`);
203
- buf = Buffer.from(await resp.arrayBuffer());
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);
204
212
  }
205
- const tmpPath = "/tmp/wxta_image_send.jpg";
206
- await wf(tmpPath, buf);
207
213
 
208
- // CDN 上传 (mediaType=1 = IMAGE)
209
- const { uploadToCdn } = await import("./cdn.mjs");
210
- 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);
211
217
  const aesKeyB64 = Buffer.from(cdn.aeskey).toString("base64");
212
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
+
213
234
  // 发送
214
235
  await apiPost(
215
236
  "ilink/bot/sendmessage",
@@ -220,15 +241,7 @@ export async function sendImageByUrl(token, to, contextToken, imageUrl) {
220
241
  client_id: crypto.randomUUID(),
221
242
  message_type: 2,
222
243
  message_state: 2,
223
- item_list: [{
224
- type: 2, // IMAGE
225
- image_item: {
226
- media: {
227
- encrypt_query_param: cdn.downloadParam,
228
- aes_key: aesKeyB64,
229
- },
230
- },
231
- }],
244
+ item_list: [{ type: 2, image_item: imageItem }],
232
245
  context_token: contextToken,
233
246
  },
234
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.4",
3
+ "version": "0.5.5",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {