@ecat/weixin-bot-cli 0.1.0
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/LICENSE +27 -0
- package/README.md +77 -0
- package/dist/api/api.d.ts +47 -0
- package/dist/api/api.js +233 -0
- package/dist/api/api.js.map +1 -0
- package/dist/api/config-cache.d.ts +18 -0
- package/dist/api/config-cache.js +64 -0
- package/dist/api/config-cache.js.map +1 -0
- package/dist/api/session-guard.d.ts +15 -0
- package/dist/api/session-guard.js +49 -0
- package/dist/api/session-guard.js.map +1 -0
- package/dist/api/types.d.ts +201 -0
- package/dist/api/types.js +35 -0
- package/dist/api/types.js.map +1 -0
- package/dist/auth/accounts.d.ts +30 -0
- package/dist/auth/accounts.js +158 -0
- package/dist/auth/accounts.js.map +1 -0
- package/dist/auth/login-qr.d.ts +31 -0
- package/dist/auth/login-qr.js +235 -0
- package/dist/auth/login-qr.js.map +1 -0
- package/dist/cdn/aes-ecb.d.ts +6 -0
- package/dist/cdn/aes-ecb.js +19 -0
- package/dist/cdn/aes-ecb.js.map +1 -0
- package/dist/cdn/cdn-upload.d.ts +17 -0
- package/dist/cdn/cdn-upload.js +73 -0
- package/dist/cdn/cdn-upload.js.map +1 -0
- package/dist/cdn/cdn-url.d.ts +13 -0
- package/dist/cdn/cdn-url.js +14 -0
- package/dist/cdn/cdn-url.js.map +1 -0
- package/dist/cdn/pic-decrypt.d.ts +9 -0
- package/dist/cdn/pic-decrypt.js +89 -0
- package/dist/cdn/pic-decrypt.js.map +1 -0
- package/dist/cdn/upload.d.ts +42 -0
- package/dist/cdn/upload.js +106 -0
- package/dist/cdn/upload.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/config-schema.d.ts +16 -0
- package/dist/config/config-schema.js +17 -0
- package/dist/config/config-schema.js.map +1 -0
- package/dist/media/media-download.d.ts +18 -0
- package/dist/media/media-download.js +95 -0
- package/dist/media/media-download.js.map +1 -0
- package/dist/media/mime.d.ts +6 -0
- package/dist/media/mime.js +73 -0
- package/dist/media/mime.js.map +1 -0
- package/dist/media/silk-transcode.d.ts +8 -0
- package/dist/media/silk-transcode.js +64 -0
- package/dist/media/silk-transcode.js.map +1 -0
- package/dist/messaging/debug-mode.d.ts +9 -0
- package/dist/messaging/debug-mode.js +63 -0
- package/dist/messaging/debug-mode.js.map +1 -0
- package/dist/messaging/inbound.d.ts +69 -0
- package/dist/messaging/inbound.js +201 -0
- package/dist/messaging/inbound.js.map +1 -0
- package/dist/messaging/send-media.d.ts +21 -0
- package/dist/messaging/send-media.js +54 -0
- package/dist/messaging/send-media.js.map +1 -0
- package/dist/messaging/send.d.ts +70 -0
- package/dist/messaging/send.js +203 -0
- package/dist/messaging/send.js.map +1 -0
- package/dist/monitor/monitor.d.ts +12 -0
- package/dist/monitor/monitor.js +145 -0
- package/dist/monitor/monitor.js.map +1 -0
- package/dist/storage/state-dir.d.ts +2 -0
- package/dist/storage/state-dir.js +8 -0
- package/dist/storage/state-dir.js.map +1 -0
- package/dist/storage/sync-buf.d.ts +20 -0
- package/dist/storage/sync-buf.js +64 -0
- package/dist/storage/sync-buf.js.map +1 -0
- package/dist/util/logger.d.ts +14 -0
- package/dist/util/logger.js +119 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/random.d.ts +10 -0
- package/dist/util/random.js +16 -0
- package/dist/util/random.js.map +1 -0
- package/dist/util/redact.d.ts +20 -0
- package/dist/util/redact.js +54 -0
- package/dist/util/redact.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { apiGetFetch } from "../api/api.js";
|
|
3
|
+
import { logger } from "../util/logger.js";
|
|
4
|
+
import { redactToken } from "../util/redact.js";
|
|
5
|
+
const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
|
|
6
|
+
/** Client-side timeout for the get_bot_qrcode request. */
|
|
7
|
+
const GET_QRCODE_TIMEOUT_MS = 5_000;
|
|
8
|
+
/** Client-side timeout for the long-poll get_qrcode_status request. */
|
|
9
|
+
const QR_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
10
|
+
/** Default `bot_type` for ilink get_bot_qrcode / get_qrcode_status (this channel build). */
|
|
11
|
+
export const DEFAULT_ILINK_BOT_TYPE = "3";
|
|
12
|
+
/** Fixed API base URL for all QR code requests. */
|
|
13
|
+
const FIXED_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
14
|
+
const activeLogins = new Map();
|
|
15
|
+
function isLoginFresh(login) {
|
|
16
|
+
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
|
|
17
|
+
}
|
|
18
|
+
/** Remove all expired entries from the activeLogins map to prevent memory leaks. */
|
|
19
|
+
function purgeExpiredLogins() {
|
|
20
|
+
for (const [id, login] of activeLogins) {
|
|
21
|
+
if (!isLoginFresh(login)) {
|
|
22
|
+
activeLogins.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function fetchQRCode(apiBaseUrl, botType) {
|
|
27
|
+
logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
|
|
28
|
+
const rawText = await apiGetFetch({
|
|
29
|
+
baseUrl: apiBaseUrl,
|
|
30
|
+
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
|
31
|
+
timeoutMs: GET_QRCODE_TIMEOUT_MS,
|
|
32
|
+
label: "fetchQRCode",
|
|
33
|
+
});
|
|
34
|
+
return JSON.parse(rawText);
|
|
35
|
+
}
|
|
36
|
+
async function pollQRStatus(apiBaseUrl, qrcode) {
|
|
37
|
+
logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
|
|
38
|
+
try {
|
|
39
|
+
const rawText = await apiGetFetch({
|
|
40
|
+
baseUrl: apiBaseUrl,
|
|
41
|
+
endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
42
|
+
timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
|
|
43
|
+
label: "pollQRStatus",
|
|
44
|
+
});
|
|
45
|
+
logger.debug(`pollQRStatus: body=${rawText.substring(0, 200)}`);
|
|
46
|
+
return JSON.parse(rawText);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
50
|
+
logger.debug(`pollQRStatus: client-side timeout after ${QR_LONG_POLL_TIMEOUT_MS}ms, returning wait`);
|
|
51
|
+
return { status: "wait" };
|
|
52
|
+
}
|
|
53
|
+
// 网关超时(如 Cloudflare 524)或其他网络错误,视为等待状态继续轮询
|
|
54
|
+
logger.warn(`pollQRStatus: network/gateway error, will retry: ${String(err)}`);
|
|
55
|
+
return { status: "wait" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function startWeixinLoginWithQr(opts) {
|
|
59
|
+
const sessionKey = opts.accountId || randomUUID();
|
|
60
|
+
purgeExpiredLogins();
|
|
61
|
+
const existing = activeLogins.get(sessionKey);
|
|
62
|
+
if (!opts.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
|
|
63
|
+
return {
|
|
64
|
+
qrcodeUrl: existing.qrcodeUrl,
|
|
65
|
+
message: "二维码已就绪,请使用微信扫描。",
|
|
66
|
+
sessionKey,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
|
|
71
|
+
logger.info(`Starting Weixin login with bot_type=${botType}`);
|
|
72
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
73
|
+
logger.info(`QR code received, qrcode=${redactToken(qrResponse.qrcode)} imgContentLen=${qrResponse.qrcode_img_content?.length ?? 0}`);
|
|
74
|
+
logger.info(`二维码链接: ${qrResponse.qrcode_img_content}`);
|
|
75
|
+
const login = {
|
|
76
|
+
sessionKey,
|
|
77
|
+
id: randomUUID(),
|
|
78
|
+
qrcode: qrResponse.qrcode,
|
|
79
|
+
qrcodeUrl: qrResponse.qrcode_img_content,
|
|
80
|
+
startedAt: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
activeLogins.set(sessionKey, login);
|
|
83
|
+
return {
|
|
84
|
+
qrcodeUrl: qrResponse.qrcode_img_content,
|
|
85
|
+
message: "使用微信扫描以下二维码,以完成连接。",
|
|
86
|
+
sessionKey,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
logger.error(`Failed to start Weixin login: ${String(err)}`);
|
|
91
|
+
return {
|
|
92
|
+
message: `Failed to start login: ${String(err)}`,
|
|
93
|
+
sessionKey,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const MAX_QR_REFRESH_COUNT = 3;
|
|
98
|
+
export async function waitForWeixinLogin(opts) {
|
|
99
|
+
let activeLogin = activeLogins.get(opts.sessionKey);
|
|
100
|
+
if (!activeLogin) {
|
|
101
|
+
logger.warn(`waitForWeixinLogin: no active login sessionKey=${opts.sessionKey}`);
|
|
102
|
+
return {
|
|
103
|
+
connected: false,
|
|
104
|
+
message: "当前没有进行中的登录,请先发起登录。",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!isLoginFresh(activeLogin)) {
|
|
108
|
+
logger.warn(`waitForWeixinLogin: login QR expired sessionKey=${opts.sessionKey}`);
|
|
109
|
+
activeLogins.delete(opts.sessionKey);
|
|
110
|
+
return {
|
|
111
|
+
connected: false,
|
|
112
|
+
message: "二维码已过期,请重新生成。",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const timeoutMs = Math.max(opts.timeoutMs ?? 480_000, 1000);
|
|
116
|
+
const deadline = Date.now() + timeoutMs;
|
|
117
|
+
let scannedPrinted = false;
|
|
118
|
+
let qrRefreshCount = 1;
|
|
119
|
+
// Initialize the effective polling base URL; may be updated on IDC redirect.
|
|
120
|
+
activeLogin.currentApiBaseUrl = FIXED_BASE_URL;
|
|
121
|
+
logger.info("Starting to poll QR code status...");
|
|
122
|
+
while (Date.now() < deadline) {
|
|
123
|
+
try {
|
|
124
|
+
const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
|
|
125
|
+
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
|
|
126
|
+
logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
|
|
127
|
+
activeLogin.status = statusResponse.status;
|
|
128
|
+
switch (statusResponse.status) {
|
|
129
|
+
case "wait":
|
|
130
|
+
if (opts.verbose) {
|
|
131
|
+
process.stdout.write(".");
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case "scaned":
|
|
135
|
+
if (!scannedPrinted) {
|
|
136
|
+
process.stdout.write("\n👀 已扫码,在微信继续操作...\n");
|
|
137
|
+
scannedPrinted = true;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case "expired": {
|
|
141
|
+
qrRefreshCount++;
|
|
142
|
+
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
143
|
+
logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
|
|
144
|
+
activeLogins.delete(opts.sessionKey);
|
|
145
|
+
return {
|
|
146
|
+
connected: false,
|
|
147
|
+
message: "登录超时:二维码多次过期,请重新开始登录流程。",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write(`\n⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})\n`);
|
|
151
|
+
logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
|
|
152
|
+
try {
|
|
153
|
+
const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
|
|
154
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
155
|
+
activeLogin.qrcode = qrResponse.qrcode;
|
|
156
|
+
activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
|
|
157
|
+
activeLogin.startedAt = Date.now();
|
|
158
|
+
scannedPrinted = false;
|
|
159
|
+
logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
|
|
160
|
+
process.stdout.write(`🔄 新二维码已生成,请重新扫描\n\n`);
|
|
161
|
+
try {
|
|
162
|
+
const qrterm = await import("qrcode-terminal");
|
|
163
|
+
qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
|
|
164
|
+
process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:\n`);
|
|
165
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}\n`);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:\n`);
|
|
169
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (refreshErr) {
|
|
173
|
+
logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
|
|
174
|
+
activeLogins.delete(opts.sessionKey);
|
|
175
|
+
return {
|
|
176
|
+
connected: false,
|
|
177
|
+
message: `刷新二维码失败: ${String(refreshErr)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "scaned_but_redirect": {
|
|
183
|
+
const redirectHost = statusResponse.redirect_host;
|
|
184
|
+
if (redirectHost) {
|
|
185
|
+
const newBaseUrl = `https://${redirectHost}`;
|
|
186
|
+
activeLogin.currentApiBaseUrl = newBaseUrl;
|
|
187
|
+
process.stdout.write(`\n🔀 IDC 跳转,切换 host 为 ${redirectHost}\n`);
|
|
188
|
+
logger.info(`waitForWeixinLogin: IDC redirect, switching polling host to ${redirectHost}`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.warn(`waitForWeixinLogin: received scaned_but_redirect but redirect_host is missing, continuing with current host`);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "confirmed": {
|
|
196
|
+
if (!statusResponse.ilink_bot_id) {
|
|
197
|
+
activeLogins.delete(opts.sessionKey);
|
|
198
|
+
logger.error("Login confirmed but ilink_bot_id missing from response");
|
|
199
|
+
return {
|
|
200
|
+
connected: false,
|
|
201
|
+
message: "登录失败:服务器未返回 ilink_bot_id。",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
activeLogin.botToken = statusResponse.bot_token;
|
|
205
|
+
activeLogins.delete(opts.sessionKey);
|
|
206
|
+
logger.info(`✅ Login confirmed! ilink_bot_id=${statusResponse.ilink_bot_id} ilink_user_id=${redactToken(statusResponse.ilink_user_id)}`);
|
|
207
|
+
return {
|
|
208
|
+
connected: true,
|
|
209
|
+
botToken: statusResponse.bot_token,
|
|
210
|
+
accountId: statusResponse.ilink_bot_id,
|
|
211
|
+
baseUrl: statusResponse.baseurl,
|
|
212
|
+
userId: statusResponse.ilink_user_id,
|
|
213
|
+
message: "✅ 与微信连接成功!",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
logger.error(`Error polling QR status: ${String(err)}`);
|
|
220
|
+
activeLogins.delete(opts.sessionKey);
|
|
221
|
+
return {
|
|
222
|
+
connected: false,
|
|
223
|
+
message: `Login failed: ${String(err)}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
227
|
+
}
|
|
228
|
+
logger.warn(`waitForWeixinLogin: timed out waiting for QR scan sessionKey=${opts.sessionKey} timeoutMs=${timeoutMs}`);
|
|
229
|
+
activeLogins.delete(opts.sessionKey);
|
|
230
|
+
return {
|
|
231
|
+
connected: false,
|
|
232
|
+
message: "登录超时,请重试。",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=login-qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-qr.js","sourceRoot":"","sources":["../../src/auth/login-qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
2
|
+
export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
|
|
3
|
+
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
4
|
+
export declare function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer;
|
|
5
|
+
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
6
|
+
export declare function aesEcbPaddedSize(plaintextSize: number): number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AES-128-ECB crypto utilities for CDN upload and download.
|
|
3
|
+
*/
|
|
4
|
+
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
5
|
+
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
6
|
+
export function encryptAesEcb(plaintext, key) {
|
|
7
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
8
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9
|
+
}
|
|
10
|
+
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
11
|
+
export function decryptAesEcb(ciphertext, key) {
|
|
12
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
13
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
14
|
+
}
|
|
15
|
+
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
16
|
+
export function aesEcbPaddedSize(plaintextSize) {
|
|
17
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=aes-ecb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes-ecb.js","sourceRoot":"","sources":["../../src/cdn/aes-ecb.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/D,kEAAkE;AAClE,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,GAAW;IAC1D,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACxD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,GAAW;IAC3D,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,aAAqB;IACpD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload one buffer to the Weixin CDN with AES-128-ECB encryption.
|
|
3
|
+
* Returns the download encrypted_query_param from the CDN response.
|
|
4
|
+
* Retries up to UPLOAD_MAX_RETRIES times on server errors; client errors (4xx) abort immediately.
|
|
5
|
+
*/
|
|
6
|
+
export declare function uploadBufferToCdn(params: {
|
|
7
|
+
buf: Buffer;
|
|
8
|
+
/** From getUploadUrl.upload_full_url; POST target when set (takes precedence over uploadParam). */
|
|
9
|
+
uploadFullUrl?: string;
|
|
10
|
+
uploadParam?: string;
|
|
11
|
+
filekey: string;
|
|
12
|
+
cdnBaseUrl: string;
|
|
13
|
+
label: string;
|
|
14
|
+
aeskey: Buffer;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
downloadParam: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { encryptAesEcb } from "./aes-ecb.js";
|
|
2
|
+
import { buildCdnUploadUrl } from "./cdn-url.js";
|
|
3
|
+
import { logger } from "../util/logger.js";
|
|
4
|
+
import { redactUrl } from "../util/redact.js";
|
|
5
|
+
/** Maximum retry attempts for CDN upload. */
|
|
6
|
+
const UPLOAD_MAX_RETRIES = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Upload one buffer to the Weixin CDN with AES-128-ECB encryption.
|
|
9
|
+
* Returns the download encrypted_query_param from the CDN response.
|
|
10
|
+
* Retries up to UPLOAD_MAX_RETRIES times on server errors; client errors (4xx) abort immediately.
|
|
11
|
+
*/
|
|
12
|
+
export async function uploadBufferToCdn(params) {
|
|
13
|
+
const { buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey } = params;
|
|
14
|
+
const ciphertext = encryptAesEcb(buf, aeskey);
|
|
15
|
+
const trimmedFull = uploadFullUrl?.trim();
|
|
16
|
+
let cdnUrl;
|
|
17
|
+
if (trimmedFull) {
|
|
18
|
+
cdnUrl = trimmedFull;
|
|
19
|
+
}
|
|
20
|
+
else if (uploadParam) {
|
|
21
|
+
cdnUrl = buildCdnUploadUrl({ cdnBaseUrl, uploadParam, filekey });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error(`${label}: CDN upload URL missing (need upload_full_url or upload_param)`);
|
|
25
|
+
}
|
|
26
|
+
logger.debug(`${label}: CDN POST url=${redactUrl(cdnUrl)} ciphertextSize=${ciphertext.length}`);
|
|
27
|
+
let downloadParam;
|
|
28
|
+
let lastError;
|
|
29
|
+
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(cdnUrl, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
34
|
+
body: new Uint8Array(ciphertext),
|
|
35
|
+
});
|
|
36
|
+
if (res.status >= 400 && res.status < 500) {
|
|
37
|
+
const errMsg = res.headers.get("x-error-message") ?? (await res.text());
|
|
38
|
+
logger.error(`${label}: CDN client error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
39
|
+
throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);
|
|
40
|
+
}
|
|
41
|
+
if (res.status !== 200) {
|
|
42
|
+
const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
|
|
43
|
+
logger.error(`${label}: CDN server error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
44
|
+
throw new Error(`CDN upload server error: ${errMsg}`);
|
|
45
|
+
}
|
|
46
|
+
downloadParam = res.headers.get("x-encrypted-param") ?? undefined;
|
|
47
|
+
if (!downloadParam) {
|
|
48
|
+
logger.error(`${label}: CDN response missing x-encrypted-param header attempt=${attempt}`);
|
|
49
|
+
throw new Error("CDN upload response missing x-encrypted-param header");
|
|
50
|
+
}
|
|
51
|
+
logger.debug(`${label}: CDN upload success attempt=${attempt}`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
lastError = err;
|
|
56
|
+
if (err instanceof Error && err.message.includes("client error"))
|
|
57
|
+
throw err;
|
|
58
|
+
if (attempt < UPLOAD_MAX_RETRIES) {
|
|
59
|
+
logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!downloadParam) {
|
|
67
|
+
throw lastError instanceof Error
|
|
68
|
+
? lastError
|
|
69
|
+
: new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
|
|
70
|
+
}
|
|
71
|
+
return { downloadParam };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=cdn-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn-upload.js","sourceRoot":"","sources":["../../src/cdn/cdn-upload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified CDN URL construction for Weixin CDN upload/download.
|
|
3
|
+
*/
|
|
4
|
+
/** 设为 true 时,当服务端未返回 full_url 字段,回退到客户端拼接 URL;false 则直接报错。 */
|
|
5
|
+
export declare const ENABLE_CDN_URL_FALLBACK = true;
|
|
6
|
+
/** Build a CDN download URL from encrypt_query_param. */
|
|
7
|
+
export declare function buildCdnDownloadUrl(encryptedQueryParam: string, cdnBaseUrl: string): string;
|
|
8
|
+
/** Build a CDN upload URL from upload_param and filekey. */
|
|
9
|
+
export declare function buildCdnUploadUrl(params: {
|
|
10
|
+
cdnBaseUrl: string;
|
|
11
|
+
uploadParam: string;
|
|
12
|
+
filekey: string;
|
|
13
|
+
}): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified CDN URL construction for Weixin CDN upload/download.
|
|
3
|
+
*/
|
|
4
|
+
/** 设为 true 时,当服务端未返回 full_url 字段,回退到客户端拼接 URL;false 则直接报错。 */
|
|
5
|
+
export const ENABLE_CDN_URL_FALLBACK = true;
|
|
6
|
+
/** Build a CDN download URL from encrypt_query_param. */
|
|
7
|
+
export function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
|
|
8
|
+
return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
|
|
9
|
+
}
|
|
10
|
+
/** Build a CDN upload URL from upload_param and filekey. */
|
|
11
|
+
export function buildCdnUploadUrl(params) {
|
|
12
|
+
return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=cdn-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn-url.js","sourceRoot":"","sources":["../../src/cdn/cdn-url.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,yDAAyD;AACzD,MAAM,UAAU,mBAAmB,CAAC,mBAA2B,EAAE,UAAkB;IACjF,OAAO,GAAG,UAAU,mCAAmC,kBAAkB,CAAC,mBAAmB,CAAC,EAAE,CAAC;AACnG,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,iBAAiB,CAAC,MAIjC;IACC,OAAO,GAAG,MAAM,CAAC,UAAU,iCAAiC,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,kBAAkB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;AACrJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download and AES-128-ECB decrypt a CDN media file. Returns plaintext Buffer.
|
|
3
|
+
* aesKeyBase64: CDNMedia.aes_key JSON field (see parseAesKey for supported formats).
|
|
4
|
+
*/
|
|
5
|
+
export declare function downloadAndDecryptBuffer(encryptedQueryParam: string, aesKeyBase64: string, cdnBaseUrl: string, label: string, fullUrl?: string): Promise<Buffer>;
|
|
6
|
+
/**
|
|
7
|
+
* Download plain (unencrypted) bytes from the CDN. Returns the raw Buffer.
|
|
8
|
+
*/
|
|
9
|
+
export declare function downloadPlainCdnBuffer(encryptedQueryParam: string, cdnBaseUrl: string, label: string, fullUrl?: string): Promise<Buffer>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { decryptAesEcb } from "./aes-ecb.js";
|
|
2
|
+
import { buildCdnDownloadUrl, ENABLE_CDN_URL_FALLBACK } from "./cdn-url.js";
|
|
3
|
+
import { logger } from "../util/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Download raw bytes from the CDN (no decryption).
|
|
6
|
+
*/
|
|
7
|
+
async function fetchCdnBytes(url, label) {
|
|
8
|
+
let res;
|
|
9
|
+
try {
|
|
10
|
+
res = await fetch(url);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const cause = err.cause ?? err.code ?? "(no cause)";
|
|
14
|
+
logger.error(`${label}: fetch network error url=${url} err=${String(err)} cause=${String(cause)}`);
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
logger.debug(`${label}: response status=${res.status} ok=${res.ok}`);
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const body = await res.text().catch(() => "(unreadable)");
|
|
20
|
+
const msg = `${label}: CDN download ${res.status} ${res.statusText} body=${body}`;
|
|
21
|
+
logger.error(msg);
|
|
22
|
+
throw new Error(msg);
|
|
23
|
+
}
|
|
24
|
+
return Buffer.from(await res.arrayBuffer());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse CDNMedia.aes_key into a raw 16-byte AES key.
|
|
28
|
+
*
|
|
29
|
+
* Two encodings are seen in the wild:
|
|
30
|
+
* - base64(raw 16 bytes) → images (aes_key from media field)
|
|
31
|
+
* - base64(hex string of 16 bytes) → file / voice / video
|
|
32
|
+
*
|
|
33
|
+
* In the second case, base64-decoding yields 32 ASCII hex chars which must
|
|
34
|
+
* then be parsed as hex to recover the actual 16-byte key.
|
|
35
|
+
*/
|
|
36
|
+
function parseAesKey(aesKeyBase64, label) {
|
|
37
|
+
const decoded = Buffer.from(aesKeyBase64, "base64");
|
|
38
|
+
if (decoded.length === 16) {
|
|
39
|
+
return decoded;
|
|
40
|
+
}
|
|
41
|
+
if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))) {
|
|
42
|
+
// hex-encoded key: base64 → hex string → raw bytes
|
|
43
|
+
return Buffer.from(decoded.toString("ascii"), "hex");
|
|
44
|
+
}
|
|
45
|
+
const msg = `${label}: aes_key must decode to 16 raw bytes or 32-char hex string, got ${decoded.length} bytes (base64="${aesKeyBase64}")`;
|
|
46
|
+
logger.error(msg);
|
|
47
|
+
throw new Error(msg);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Download and AES-128-ECB decrypt a CDN media file. Returns plaintext Buffer.
|
|
51
|
+
* aesKeyBase64: CDNMedia.aes_key JSON field (see parseAesKey for supported formats).
|
|
52
|
+
*/
|
|
53
|
+
export async function downloadAndDecryptBuffer(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl) {
|
|
54
|
+
const key = parseAesKey(aesKeyBase64, label);
|
|
55
|
+
let url;
|
|
56
|
+
if (fullUrl) {
|
|
57
|
+
url = fullUrl;
|
|
58
|
+
}
|
|
59
|
+
else if (ENABLE_CDN_URL_FALLBACK) {
|
|
60
|
+
url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error(`${label}: fullUrl is required (CDN URL fallback is disabled)`);
|
|
64
|
+
}
|
|
65
|
+
logger.debug(`${label}: fetching url=${url}`);
|
|
66
|
+
const encrypted = await fetchCdnBytes(url, label);
|
|
67
|
+
logger.debug(`${label}: downloaded ${encrypted.byteLength} bytes, decrypting`);
|
|
68
|
+
const decrypted = decryptAesEcb(encrypted, key);
|
|
69
|
+
logger.debug(`${label}: decrypted ${decrypted.length} bytes`);
|
|
70
|
+
return decrypted;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Download plain (unencrypted) bytes from the CDN. Returns the raw Buffer.
|
|
74
|
+
*/
|
|
75
|
+
export async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, label, fullUrl) {
|
|
76
|
+
let url;
|
|
77
|
+
if (fullUrl) {
|
|
78
|
+
url = fullUrl;
|
|
79
|
+
}
|
|
80
|
+
else if (ENABLE_CDN_URL_FALLBACK) {
|
|
81
|
+
url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error(`${label}: fullUrl is required (CDN URL fallback is disabled)`);
|
|
85
|
+
}
|
|
86
|
+
logger.debug(`${label}: fetching url=${url}`);
|
|
87
|
+
return fetchCdnBytes(url, label);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=pic-decrypt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pic-decrypt.js","sourceRoot":"","sources":["../../src/cdn/pic-decrypt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WeixinApiOptions } from "../api/api.js";
|
|
2
|
+
export type UploadedFileInfo = {
|
|
3
|
+
filekey: string;
|
|
4
|
+
/** 由 upload_param 上传后 CDN 返回的下载加密参数; fill into ImageItem.media.encrypt_query_param */
|
|
5
|
+
downloadEncryptedQueryParam: string;
|
|
6
|
+
/** AES-128-ECB key, hex-encoded; convert to base64 for CDNMedia.aes_key */
|
|
7
|
+
aeskey: string;
|
|
8
|
+
/** Plaintext file size in bytes */
|
|
9
|
+
fileSize: number;
|
|
10
|
+
/** Ciphertext file size in bytes (AES-128-ECB with PKCS7 padding); use for ImageItem.hd_size / mid_size */
|
|
11
|
+
fileSizeCiphertext: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
15
|
+
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
16
|
+
*/
|
|
17
|
+
export declare function downloadRemoteImageToTemp(url: string, destDir: string): Promise<string>;
|
|
18
|
+
/** Upload a local image file to the Weixin CDN with AES-128-ECB encryption. */
|
|
19
|
+
export declare function uploadFileToWeixin(params: {
|
|
20
|
+
filePath: string;
|
|
21
|
+
toUserId: string;
|
|
22
|
+
opts: WeixinApiOptions;
|
|
23
|
+
cdnBaseUrl: string;
|
|
24
|
+
}): Promise<UploadedFileInfo>;
|
|
25
|
+
/** Upload a local video file to the Weixin CDN. */
|
|
26
|
+
export declare function uploadVideoToWeixin(params: {
|
|
27
|
+
filePath: string;
|
|
28
|
+
toUserId: string;
|
|
29
|
+
opts: WeixinApiOptions;
|
|
30
|
+
cdnBaseUrl: string;
|
|
31
|
+
}): Promise<UploadedFileInfo>;
|
|
32
|
+
/**
|
|
33
|
+
* Upload a local file attachment (non-image, non-video) to the Weixin CDN.
|
|
34
|
+
* Uses media_type=FILE; no thumbnail required.
|
|
35
|
+
*/
|
|
36
|
+
export declare function uploadFileAttachmentToWeixin(params: {
|
|
37
|
+
filePath: string;
|
|
38
|
+
fileName: string;
|
|
39
|
+
toUserId: string;
|
|
40
|
+
opts: WeixinApiOptions;
|
|
41
|
+
cdnBaseUrl: string;
|
|
42
|
+
}): Promise<UploadedFileInfo>;
|