wechat-to-anything 0.5.4 → 0.5.6

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,109 @@ 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
+ // 获取缩略图尺寸
103
+ let thumbWidth = 120, thumbHeight = 120;
104
+ try {
105
+ const sipsOut = execSync(`sips -g pixelWidth -g pixelHeight "${thumbPath}" 2>/dev/null`).toString();
106
+ const wm = sipsOut.match(/pixelWidth:\s*(\d+)/);
107
+ const hm = sipsOut.match(/pixelHeight:\s*(\d+)/);
108
+ if (wm) thumbWidth = parseInt(wm[1]);
109
+ if (hm) thumbHeight = parseInt(hm[1]);
110
+ } catch {}
111
+
112
+ // 1. getUploadUrl(带缩略图信息)
113
+ const uploadBody = JSON.stringify({
114
+ filekey,
115
+ media_type: 1, // IMAGE
116
+ to_user_id: toUserId,
117
+ rawsize,
118
+ rawfilemd5,
119
+ filesize,
120
+ thumb_rawsize: thumb.length,
121
+ thumb_rawfilemd5: createHash("md5").update(thumb).digest("hex"),
122
+ thumb_filesize: aesEcbPaddedSize(thumb.length),
123
+ no_need_thumb: false,
124
+ aeskey: aeskey.toString("hex"),
125
+ base_info: {},
126
+ });
127
+
128
+ const uploadRes = await fetch(`${BASE_URL}/ilink/bot/getuploadurl`, {
129
+ method: "POST",
130
+ headers: buildHeaders(token, uploadBody),
131
+ body: uploadBody,
132
+ });
133
+ if (!uploadRes.ok) throw new Error(`getUploadUrl failed: ${uploadRes.status}`);
134
+ const uploadJson = await uploadRes.json();
135
+ if (!uploadJson.upload_param) throw new Error(`getUploadUrl: no upload_param`);
136
+
137
+ // 2. 上传原图
138
+ const origCipher = encryptAesEcb(plaintext, aeskey);
139
+ const origUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.upload_param)}&filekey=${encodeURIComponent(filekey)}`;
140
+ const origRes = await fetch(origUrl, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/octet-stream" },
143
+ body: new Uint8Array(origCipher),
144
+ });
145
+ if (origRes.status !== 200) throw new Error(`CDN upload orig failed: ${origRes.status}`);
146
+ const downloadParam = origRes.headers.get("x-encrypted-query-param") || origRes.headers.get("x-encrypted-param");
147
+ if (!downloadParam) throw new Error("CDN upload: missing download param");
148
+
149
+ // 3. 上传缩略图
150
+ let thumbDownloadParam = null;
151
+ if (uploadJson.thumb_upload_param) {
152
+ const thumbCipher = encryptAesEcb(thumb, aeskey);
153
+ const thumbUrl = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadJson.thumb_upload_param)}&filekey=${encodeURIComponent(filekey)}`;
154
+ const thumbRes = await fetch(thumbUrl, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/octet-stream" },
157
+ body: new Uint8Array(thumbCipher),
158
+ });
159
+ if (thumbRes.status === 200) {
160
+ thumbDownloadParam = thumbRes.headers.get("x-encrypted-query-param") || thumbRes.headers.get("x-encrypted-param");
161
+ }
162
+ }
163
+
164
+ return {
165
+ downloadParam,
166
+ thumbDownloadParam,
167
+ aeskey: aeskey.toString("hex"),
168
+ fileSize: rawsize,
169
+ fileSizeCiphertext: filesize,
170
+ thumbSizeCiphertext: aesEcbPaddedSize(thumb.length),
171
+ thumbWidth,
172
+ thumbHeight,
173
+ filekey,
174
+ };
175
+ }
176
+
177
+ // ─── 通用上传 ───────────────────────────────────────────────────────
76
178
 
77
179
  /**
78
180
  * 上传文件到微信 CDN
package/cli/weixin.mjs CHANGED
@@ -187,29 +187,53 @@ 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
+ imageItem.thumb_size = cdn.thumbSizeCiphertext;
233
+ imageItem.thumb_width = cdn.thumbWidth;
234
+ imageItem.thumb_height = cdn.thumbHeight;
235
+ }
236
+
213
237
  // 发送
214
238
  await apiPost(
215
239
  "ilink/bot/sendmessage",
@@ -220,15 +244,7 @@ export async function sendImageByUrl(token, to, contextToken, imageUrl) {
220
244
  client_id: crypto.randomUUID(),
221
245
  message_type: 2,
222
246
  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
- }],
247
+ item_list: [{ type: 2, image_item: imageItem }],
232
248
  context_token: contextToken,
233
249
  },
234
250
  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.6",
4
4
  "description": "一条命令,把微信变成任何 AI Agent 的入口",
5
5
  "type": "module",
6
6
  "bin": {