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 +103 -1
- package/cli/weixin.mjs +39 -23
- package/examples/image-test.mjs +1 -1
- package/package.json +1 -1
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
|
|
197
|
-
if (imageUrl.startsWith("
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
let tmpPath;
|
|
197
|
+
if (imageUrl.startsWith("/")) {
|
|
198
|
+
// 本地文件路径,直接使用
|
|
199
|
+
tmpPath = imageUrl;
|
|
200
200
|
} else {
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
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
|
|
209
|
-
const {
|
|
210
|
-
const cdn = await
|
|
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: {},
|
package/examples/image-test.mjs
CHANGED
|
@@ -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/
|
|
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");
|