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 +1 -1
- package/cli/bridge.mjs +2 -2
- package/cli/cdn.mjs +90 -1
- package/cli/weixin.mjs +39 -20
- package/examples/image-test.mjs +1 -1
- package/package.json +1 -1
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
|
-
>
|
|
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
|
-
//
|
|
296
|
-
const imageMatch = reply.match(/!\[.*?\]\((https
|
|
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
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
203
|
-
const {
|
|
204
|
-
const cdn = await
|
|
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: {},
|
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");
|