@qwen-code/channel-weixin 0.21.0-preview.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/dist/WeixinAdapter.d.ts +23 -0
- package/dist/WeixinAdapter.js +278 -0
- package/dist/WeixinAdapter.js.map +1 -0
- package/dist/accounts.d.ts +15 -0
- package/dist/accounts.js +91 -0
- package/dist/accounts.js.map +1 -0
- package/dist/api.d.ts +29 -0
- package/dist/api.js +265 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/login.d.ts +18 -0
- package/dist/login.js +76 -0
- package/dist/login.js.map +1 -0
- package/dist/media.d.ts +17 -0
- package/dist/media.js +58 -0
- package/dist/media.js.map +1 -0
- package/dist/monitor.d.ts +30 -0
- package/dist/monitor.js +140 -0
- package/dist/monitor.js.map +1 -0
- package/dist/send.d.ts +43 -0
- package/dist/send.js +227 -0
- package/dist/send.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/package.json +34 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API wrapper for WeChat iLink Bot API.
|
|
3
|
+
*/
|
|
4
|
+
// ── Error handling ────────────────────────────────────────────────
|
|
5
|
+
/** Structured error from WeChat iLink Bot API. */
|
|
6
|
+
export class WeixinApiError extends Error {
|
|
7
|
+
/** HTTP status code (0 if network/timeout error). */
|
|
8
|
+
status;
|
|
9
|
+
/** API-level return code (ret field in response body). */
|
|
10
|
+
ret;
|
|
11
|
+
/** API-level error code (errcode field in response body). */
|
|
12
|
+
errcode;
|
|
13
|
+
constructor(message, status, ret, errcode) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'WeixinApiError';
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.ret = ret;
|
|
18
|
+
this.errcode = errcode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Errors that are safe to retry (transient / network). */
|
|
22
|
+
function isRetryableError(err) {
|
|
23
|
+
if (err instanceof WeixinApiError) {
|
|
24
|
+
// Session expired — not retryable (needs re-login)
|
|
25
|
+
if (err.errcode === -14)
|
|
26
|
+
return false;
|
|
27
|
+
// API-level transient errors (system busy, rate limit)
|
|
28
|
+
if (err.errcode === -1 || err.errcode === 45011)
|
|
29
|
+
return true;
|
|
30
|
+
// ret field is used by getUploadUrl and other endpoints
|
|
31
|
+
if (err.ret !== undefined && err.ret !== 0)
|
|
32
|
+
return false;
|
|
33
|
+
// Client errors (4xx except 429) — not retryable
|
|
34
|
+
if (err.status >= 400 && err.status < 500)
|
|
35
|
+
return err.status === 429;
|
|
36
|
+
// Server errors (5xx) or network errors (status 0) — retryable
|
|
37
|
+
return err.status === 0 || err.status >= 500;
|
|
38
|
+
}
|
|
39
|
+
if (err instanceof TypeError || err.code) {
|
|
40
|
+
// Network errors (fetch TypeError, ECONNRESET, ETIMEDOUT, etc.)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** Exponential backoff retry wrapper. */
|
|
46
|
+
async function retryWithBackoff(fn, maxRetries = 3, baseDelayMs = 1000) {
|
|
47
|
+
let lastError;
|
|
48
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
49
|
+
try {
|
|
50
|
+
return await fn(attempt);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
lastError = err;
|
|
54
|
+
if (attempt > maxRetries || !isRetryableError(err)) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
const delay = baseDelayMs * Math.pow(2, attempt - 1);
|
|
58
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastError;
|
|
62
|
+
}
|
|
63
|
+
// iLink Bot API protocol version we are compatible with.
|
|
64
|
+
// Used both in the request body (base_info.channel_version) and in the
|
|
65
|
+
// iLink-App-ClientVersion header (encoded as 0x00MMNNPP).
|
|
66
|
+
const ILINK_PROTOCOL_VERSION = '2.1.3';
|
|
67
|
+
function buildClientVersion(version) {
|
|
68
|
+
const parts = version.split('.').map((p) => parseInt(p, 10));
|
|
69
|
+
const major = parts[0] ?? 0;
|
|
70
|
+
const minor = parts[1] ?? 0;
|
|
71
|
+
const patch = parts[2] ?? 0;
|
|
72
|
+
return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);
|
|
73
|
+
}
|
|
74
|
+
function baseInfo() {
|
|
75
|
+
return { channel_version: ILINK_PROTOCOL_VERSION };
|
|
76
|
+
}
|
|
77
|
+
function randomUin() {
|
|
78
|
+
const buf = new Uint8Array(4);
|
|
79
|
+
crypto.getRandomValues(buf);
|
|
80
|
+
return btoa(String.fromCharCode(...buf));
|
|
81
|
+
}
|
|
82
|
+
export function buildHeaders(token) {
|
|
83
|
+
const headers = {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'X-WECHAT-UIN': randomUin(),
|
|
86
|
+
'iLink-App-Id': 'bot',
|
|
87
|
+
'iLink-App-ClientVersion': String(buildClientVersion(ILINK_PROTOCOL_VERSION)),
|
|
88
|
+
};
|
|
89
|
+
if (token) {
|
|
90
|
+
headers['AuthorizationType'] = 'ilink_bot_token';
|
|
91
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
92
|
+
}
|
|
93
|
+
return headers;
|
|
94
|
+
}
|
|
95
|
+
async function post(baseUrl, path, body, token, timeoutMs = 40000, signal) {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
98
|
+
if (signal) {
|
|
99
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const resp = await fetch(`${baseUrl}${path}`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: buildHeaders(token),
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
if (!resp.ok) {
|
|
109
|
+
// Try to parse the API error body for ret/errcode/errmsg
|
|
110
|
+
let ret;
|
|
111
|
+
let errcode;
|
|
112
|
+
let errmsg;
|
|
113
|
+
try {
|
|
114
|
+
const errBody = (await resp.json());
|
|
115
|
+
ret = errBody.ret;
|
|
116
|
+
errcode = errBody.errcode;
|
|
117
|
+
errmsg = errBody.errmsg;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// ignore parse errors — use status-based message
|
|
121
|
+
}
|
|
122
|
+
const message = errmsg
|
|
123
|
+
? `WeChat API error (HTTP ${resp.status}, ret=${ret}, errcode=${errcode}): ${errmsg}`
|
|
124
|
+
: `WeChat API error (HTTP ${resp.status})`;
|
|
125
|
+
throw new WeixinApiError(message, resp.status, ret, errcode);
|
|
126
|
+
}
|
|
127
|
+
return (await resp.json());
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function getUpdates(baseUrl, token, getUpdatesBuf, timeoutMs = 40000, signal) {
|
|
134
|
+
const body = {
|
|
135
|
+
get_updates_buf: getUpdatesBuf,
|
|
136
|
+
base_info: baseInfo(),
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
return await post(baseUrl, '/ilink/bot/getupdates', body, token, timeoutMs, signal);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
143
|
+
return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function sendMessage(baseUrl, token, msg) {
|
|
149
|
+
const body = { msg, base_info: baseInfo() };
|
|
150
|
+
await retryWithBackoff(async (_attempt) => {
|
|
151
|
+
const resp = await post(baseUrl, '/ilink/bot/sendmessage', body, token);
|
|
152
|
+
if ((resp.ret !== undefined && resp.ret !== 0) ||
|
|
153
|
+
(resp.errcode !== undefined && resp.errcode !== 0)) {
|
|
154
|
+
throw new WeixinApiError(`sendMessage failed: ret=${resp.ret} errcode=${resp.errcode} ${resp.errmsg || ''}`, 200, resp.ret, resp.errcode);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export async function getConfig(baseUrl, token, userId, contextToken) {
|
|
159
|
+
const body = {
|
|
160
|
+
ilink_user_id: userId,
|
|
161
|
+
context_token: contextToken,
|
|
162
|
+
base_info: baseInfo(),
|
|
163
|
+
};
|
|
164
|
+
return post(baseUrl, '/ilink/bot/getconfig', body, token);
|
|
165
|
+
}
|
|
166
|
+
export async function sendTyping(baseUrl, token, req) {
|
|
167
|
+
const body = { ...req, base_info: baseInfo() };
|
|
168
|
+
return post(baseUrl, '/ilink/bot/sendtyping', body, token);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Request an upload URL and CDN credentials for media.
|
|
172
|
+
* @param aeskeyHex 16-byte AES key as 32-char hex string (e.g. "00112233445566778899aabbccddeeff")
|
|
173
|
+
* @returns Either the full CDN upload URL or the upload_param string
|
|
174
|
+
*/
|
|
175
|
+
export async function getUploadUrl(baseUrl, token, toUserId, filekey, rawsize, rawfilemd5, encryptedSize, aeskeyHex) {
|
|
176
|
+
const body = {
|
|
177
|
+
filekey,
|
|
178
|
+
media_type: 1,
|
|
179
|
+
to_user_id: toUserId,
|
|
180
|
+
rawsize,
|
|
181
|
+
rawfilemd5,
|
|
182
|
+
filesize: encryptedSize,
|
|
183
|
+
no_need_thumb: true,
|
|
184
|
+
aeskey: aeskeyHex,
|
|
185
|
+
base_info: baseInfo(),
|
|
186
|
+
};
|
|
187
|
+
return retryWithBackoff(async (_attempt) => {
|
|
188
|
+
const resp = await post(baseUrl, '/ilink/bot/getuploadurl', body, token);
|
|
189
|
+
// Check API-level error first
|
|
190
|
+
if ((resp.ret !== undefined && resp.ret !== 0) ||
|
|
191
|
+
(resp.errcode !== undefined && resp.errcode !== 0)) {
|
|
192
|
+
throw new WeixinApiError(`getuploadurl failed: ret=${resp.ret} errcode=${resp.errcode ?? '(none)'} errmsg=${resp.errmsg || '(none)'}`, 200, resp.ret, resp.errcode);
|
|
193
|
+
}
|
|
194
|
+
// upload_full_url: CDN upload URL with all params embedded
|
|
195
|
+
if (resp.upload_full_url) {
|
|
196
|
+
return resp.upload_full_url;
|
|
197
|
+
}
|
|
198
|
+
// upload_param: CDN upload params only (must construct URL with filekey)
|
|
199
|
+
if (resp.upload_param) {
|
|
200
|
+
return resp.upload_param;
|
|
201
|
+
}
|
|
202
|
+
throw new WeixinApiError(`getuploadurl returned no URL: ret=${resp.ret} errcode=${resp.errcode ?? '(none)'} errmsg=${resp.errmsg || '(none)'}`, 200, resp.ret, resp.errcode);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Upload encrypted media to CDN.
|
|
206
|
+
* If urlOrParam is a full URL, use it directly (host must match).
|
|
207
|
+
* If it's just a param, construct the URL. */
|
|
208
|
+
export async function uploadToCdn(urlOrParam, filekey, encryptedData) {
|
|
209
|
+
const CDN_HOST = 'novac2c.cdn.weixin.qq.com';
|
|
210
|
+
let url;
|
|
211
|
+
const lowerUrlOrParam = urlOrParam.toLowerCase();
|
|
212
|
+
if (lowerUrlOrParam.startsWith('https://')) {
|
|
213
|
+
const parsed = new URL(urlOrParam);
|
|
214
|
+
if (parsed.hostname !== CDN_HOST) {
|
|
215
|
+
throw new Error(`CDN upload URL has unexpected host: ${parsed.hostname}`);
|
|
216
|
+
}
|
|
217
|
+
url = urlOrParam;
|
|
218
|
+
}
|
|
219
|
+
else if (lowerUrlOrParam.startsWith('http://')) {
|
|
220
|
+
throw new Error('CDN upload URL must use HTTPS');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
url = `https://${CDN_HOST}/c2c/upload?encrypted_query_param=${encodeURIComponent(urlOrParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
224
|
+
}
|
|
225
|
+
return retryWithBackoff(async (_attempt) => {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timeout = setTimeout(() => controller.abort(), 40000);
|
|
228
|
+
try {
|
|
229
|
+
const resp = await fetch(url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
232
|
+
body: encryptedData,
|
|
233
|
+
signal: controller.signal,
|
|
234
|
+
});
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
// Try to extract error details from CDN response
|
|
237
|
+
let cdnErrMsg;
|
|
238
|
+
let cdnRet;
|
|
239
|
+
let cdnErrCode;
|
|
240
|
+
try {
|
|
241
|
+
const errBody = (await resp.json());
|
|
242
|
+
cdnErrMsg = errBody.errmsg;
|
|
243
|
+
cdnRet = errBody.ret;
|
|
244
|
+
cdnErrCode = errBody.errcode;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// ignore
|
|
248
|
+
}
|
|
249
|
+
throw new WeixinApiError(cdnErrMsg
|
|
250
|
+
? `CDN upload failed: HTTP ${resp.status} — ${cdnErrMsg}`
|
|
251
|
+
: `CDN upload failed: HTTP ${resp.status}`, resp.status, cdnRet, cdnErrCode);
|
|
252
|
+
}
|
|
253
|
+
// Extract x-encrypted-param from response header
|
|
254
|
+
const encryptParam = resp.headers.get('x-encrypted-param');
|
|
255
|
+
if (!encryptParam) {
|
|
256
|
+
throw new WeixinApiError('CDN upload succeeded but missing x-encrypted-param header', resp.status);
|
|
257
|
+
}
|
|
258
|
+
return encryptParam;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { WeixinChannel } from './WeixinAdapter.js';
|
|
2
|
+
import { WeixinChannel } from './WeixinAdapter.js';
|
|
3
|
+
export const plugin = {
|
|
4
|
+
channelType: 'weixin',
|
|
5
|
+
displayName: 'WeChat',
|
|
6
|
+
createChannel: (name, config, bridge, options) => new WeixinChannel(name, config, bridge, options),
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,MAAM,CAAC,MAAM,MAAM,GAAkB;IACnC,WAAW,EAAE,QAAQ;IACrB,WAAW,EAAE,QAAQ;IACrB,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAC/C,IAAI,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC;CACnD,CAAC"}
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR code login flow for WeChat iLink Bot.
|
|
3
|
+
*/
|
|
4
|
+
export interface LoginResult {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
token?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
userId?: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
/** Step 1: Get QR code from server and display in terminal */
|
|
12
|
+
export declare function startLogin(apiBaseUrl: string): Promise<string>;
|
|
13
|
+
/** Step 2: Poll for scan result */
|
|
14
|
+
export declare function waitForLogin(params: {
|
|
15
|
+
qrcodeId: string;
|
|
16
|
+
apiBaseUrl: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}): Promise<LoginResult>;
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR code login flow for WeChat iLink Bot.
|
|
3
|
+
*/
|
|
4
|
+
import { buildHeaders } from './api.js';
|
|
5
|
+
/** Step 1: Get QR code from server and display in terminal */
|
|
6
|
+
export async function startLogin(apiBaseUrl) {
|
|
7
|
+
const resp = await fetch(`${apiBaseUrl}/ilink/bot/get_bot_qrcode?bot_type=3`);
|
|
8
|
+
if (!resp.ok) {
|
|
9
|
+
throw new Error(`Failed to get QR code: HTTP ${resp.status}`);
|
|
10
|
+
}
|
|
11
|
+
const data = (await resp.json());
|
|
12
|
+
if (!data.qrcode) {
|
|
13
|
+
throw new Error('No qrcode in response');
|
|
14
|
+
}
|
|
15
|
+
if (data.qrcode_img_content) {
|
|
16
|
+
process.stderr.write(`QR code URL: ${data.qrcode_img_content}\nScan this URL with WeChat.\n`);
|
|
17
|
+
}
|
|
18
|
+
process.stderr.write('Scan the QR code with WeChat to connect.\n');
|
|
19
|
+
return data.qrcode;
|
|
20
|
+
}
|
|
21
|
+
/** Step 2: Poll for scan result */
|
|
22
|
+
export async function waitForLogin(params) {
|
|
23
|
+
const { apiBaseUrl, timeoutMs = 480000 } = params;
|
|
24
|
+
let currentQrcodeId = params.qrcodeId;
|
|
25
|
+
const deadline = Date.now() + timeoutMs;
|
|
26
|
+
let retryCount = 0;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
try {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
31
|
+
const resp = await fetch(`${apiBaseUrl}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(currentQrcodeId)}`, {
|
|
32
|
+
headers: buildHeaders(),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
if (!resp.ok)
|
|
37
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
38
|
+
const data = (await resp.json());
|
|
39
|
+
switch (data.status) {
|
|
40
|
+
case 'confirmed':
|
|
41
|
+
return {
|
|
42
|
+
connected: true,
|
|
43
|
+
token: data.bot_token,
|
|
44
|
+
baseUrl: data.baseurl,
|
|
45
|
+
userId: data.ilink_user_id,
|
|
46
|
+
message: 'Connected to WeChat successfully!',
|
|
47
|
+
};
|
|
48
|
+
case 'scaned':
|
|
49
|
+
process.stderr.write('QR code scanned, waiting for confirmation...\n');
|
|
50
|
+
break;
|
|
51
|
+
case 'expired':
|
|
52
|
+
retryCount++;
|
|
53
|
+
if (retryCount >= 3) {
|
|
54
|
+
return {
|
|
55
|
+
connected: false,
|
|
56
|
+
message: 'QR code expired after maximum retries.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write('QR code expired, refreshing...\n');
|
|
60
|
+
currentQrcodeId = await startLogin(apiBaseUrl);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
73
|
+
}
|
|
74
|
+
return { connected: false, message: 'Login timed out.' };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"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"}
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN download with AES-128-ECB decryption.
|
|
3
|
+
* Ported from cc-weixin/plugins/weixin/src/media.ts (download path only).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse aes_key from CDNMedia into a raw 16-byte Buffer.
|
|
7
|
+
* Two encodings exist:
|
|
8
|
+
* - base64(raw 16 bytes)
|
|
9
|
+
* - base64(hex string of 16 bytes)
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseAesKey(aesKeyBase64: string): Buffer;
|
|
12
|
+
/** Download encrypted media from CDN and decrypt it. */
|
|
13
|
+
export declare function downloadAndDecrypt(encryptQueryParam: string, aesKey: string): Promise<Buffer>;
|
|
14
|
+
/** AES-128-ECB encryption for CDN upload. */
|
|
15
|
+
export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
|
|
16
|
+
/** Compute MD5 hash of a buffer, returning hex string. */
|
|
17
|
+
export declare function computeMd5(data: Buffer): string;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN download with AES-128-ECB decryption.
|
|
3
|
+
* Ported from cc-weixin/plugins/weixin/src/media.ts (download path only).
|
|
4
|
+
*/
|
|
5
|
+
import { createCipheriv, createDecipheriv, createHash } from 'node:crypto';
|
|
6
|
+
const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
|
|
7
|
+
function buildCdnDownloadUrl(encryptedQueryParam) {
|
|
8
|
+
return `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
|
|
9
|
+
}
|
|
10
|
+
function decryptAesEcb(ciphertext, key) {
|
|
11
|
+
const decipher = createDecipheriv('aes-128-ecb', key, null);
|
|
12
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse aes_key from CDNMedia into a raw 16-byte Buffer.
|
|
16
|
+
* Two encodings exist:
|
|
17
|
+
* - base64(raw 16 bytes)
|
|
18
|
+
* - base64(hex string of 16 bytes)
|
|
19
|
+
*/
|
|
20
|
+
export function parseAesKey(aesKeyBase64) {
|
|
21
|
+
const decoded = Buffer.from(aesKeyBase64, 'base64');
|
|
22
|
+
if (decoded.length === 16) {
|
|
23
|
+
return decoded;
|
|
24
|
+
}
|
|
25
|
+
if (decoded.length === 32 &&
|
|
26
|
+
/^[0-9a-fA-F]{32}$/.test(decoded.toString('ascii'))) {
|
|
27
|
+
return Buffer.from(decoded.toString('ascii'), 'hex');
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Invalid aes_key: expected 16 raw bytes or 32 hex chars, got ${decoded.length} bytes`);
|
|
30
|
+
}
|
|
31
|
+
/** Download encrypted media from CDN and decrypt it. */
|
|
32
|
+
export async function downloadAndDecrypt(encryptQueryParam, aesKey) {
|
|
33
|
+
const url = buildCdnDownloadUrl(encryptQueryParam);
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), 40000);
|
|
36
|
+
try {
|
|
37
|
+
const resp = await fetch(url, { signal: controller.signal });
|
|
38
|
+
if (!resp.ok) {
|
|
39
|
+
throw new Error(`CDN download failed: HTTP ${resp.status}`);
|
|
40
|
+
}
|
|
41
|
+
const ciphertext = Buffer.from(await resp.arrayBuffer());
|
|
42
|
+
const keyBuf = parseAesKey(aesKey);
|
|
43
|
+
return decryptAesEcb(ciphertext, keyBuf);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** AES-128-ECB encryption for CDN upload. */
|
|
50
|
+
export function encryptAesEcb(plaintext, key) {
|
|
51
|
+
const cipher = createCipheriv('aes-128-ecb', key, null);
|
|
52
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
53
|
+
}
|
|
54
|
+
/** Compute MD5 hash of a buffer, returning hex string. */
|
|
55
|
+
export function computeMd5(data) {
|
|
56
|
+
return createHash('md5').update(data).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3E,MAAM,YAAY,GAAG,uCAAuC,CAAC;AAE7D,SAAS,mBAAmB,CAAC,mBAA2B;IACtD,OAAO,GAAG,YAAY,mCAAmC,kBAAkB,CAAC,mBAAmB,CAAC,EAAE,CAAC;AACrG,CAAC;AAED,SAAS,aAAa,CAAC,UAAkB,EAAE,GAAW;IACpD,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;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,YAAoB;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACpD,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IACE,OAAO,CAAC,MAAM,KAAK,EAAE;QACrB,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EACnD,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,IAAI,KAAK,CACb,+DAA+D,OAAO,CAAC,MAAM,QAAQ,CACtF,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,iBAAyB,EACzB,MAAc;IAEd,MAAM,GAAG,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IAEnD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC;IAE5D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACnC,OAAO,aAAa,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED,6CAA6C;AAC7C,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,0DAA0D;AAC1D,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-polling loop: getUpdates -> callback.
|
|
3
|
+
* Platform-agnostic: the onMessage callback handles delivery.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getContextToken(userId: string): string | undefined;
|
|
6
|
+
export interface CdnRef {
|
|
7
|
+
encryptQueryParam: string;
|
|
8
|
+
aesKey: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FileCdnRef extends CdnRef {
|
|
11
|
+
fileName: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedMessage {
|
|
14
|
+
fromUserId: string;
|
|
15
|
+
messageId: string;
|
|
16
|
+
text: string;
|
|
17
|
+
/** CDN reference for deferred image download. */
|
|
18
|
+
image?: CdnRef;
|
|
19
|
+
/** CDN reference for deferred file download. */
|
|
20
|
+
file?: FileCdnRef;
|
|
21
|
+
/** Text of the referenced (replied-to) message. */
|
|
22
|
+
refText?: string;
|
|
23
|
+
}
|
|
24
|
+
export type OnMessageCallback = (msg: ParsedMessage) => Promise<void>;
|
|
25
|
+
export declare function startPollLoop(params: {
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
token: string;
|
|
28
|
+
onMessage: OnMessageCallback;
|
|
29
|
+
abortSignal: AbortSignal;
|
|
30
|
+
}): Promise<void>;
|
package/dist/monitor.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-polling loop: getUpdates -> callback.
|
|
3
|
+
* Platform-agnostic: the onMessage callback handles delivery.
|
|
4
|
+
*/
|
|
5
|
+
import { getUpdates } from './api.js';
|
|
6
|
+
import { MessageType, MessageItemType } from './types.js';
|
|
7
|
+
import { getStateDir } from './accounts.js';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
/** In-memory context token cache: userId -> contextToken */
|
|
11
|
+
const contextTokens = new Map();
|
|
12
|
+
export function getContextToken(userId) {
|
|
13
|
+
return contextTokens.get(userId);
|
|
14
|
+
}
|
|
15
|
+
function cursorPath() {
|
|
16
|
+
return join(getStateDir(), 'cursor.txt');
|
|
17
|
+
}
|
|
18
|
+
function loadCursor() {
|
|
19
|
+
const p = cursorPath();
|
|
20
|
+
if (existsSync(p))
|
|
21
|
+
return readFileSync(p, 'utf-8').trim();
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
function saveCursor(cursor) {
|
|
25
|
+
writeFileSync(cursorPath(), cursor, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
export async function startPollLoop(params) {
|
|
28
|
+
const { baseUrl, token, onMessage, abortSignal } = params;
|
|
29
|
+
let cursor = loadCursor();
|
|
30
|
+
let consecutiveErrors = 0;
|
|
31
|
+
let pollTimeoutMs = 40000;
|
|
32
|
+
process.stderr.write('[weixin] Starting message poll loop...\n');
|
|
33
|
+
while (!abortSignal.aborted) {
|
|
34
|
+
try {
|
|
35
|
+
const resp = await getUpdates(baseUrl, token, cursor, pollTimeoutMs, abortSignal);
|
|
36
|
+
if (resp.errcode === -14) {
|
|
37
|
+
process.stderr.write('[weixin] Session expired (errcode -14). Pausing 30s...\n');
|
|
38
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (resp.ret !== 0 && resp.ret !== undefined) {
|
|
42
|
+
throw new Error(`getUpdates error: ret=${resp.ret} errcode=${resp.errcode} ${resp.errmsg}`);
|
|
43
|
+
}
|
|
44
|
+
consecutiveErrors = 0;
|
|
45
|
+
// Respect server-suggested poll timeout
|
|
46
|
+
if (resp.longpolling_timeout_ms && resp.longpolling_timeout_ms > 0) {
|
|
47
|
+
pollTimeoutMs = resp.longpolling_timeout_ms + 5000; // add buffer for network
|
|
48
|
+
}
|
|
49
|
+
if (resp.msgs && resp.msgs.length > 0) {
|
|
50
|
+
for (const msg of resp.msgs) {
|
|
51
|
+
await processMessage(msg, onMessage);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Persist cursor after messages are processed to avoid losing messages on crash
|
|
55
|
+
if (resp.get_updates_buf) {
|
|
56
|
+
cursor = resp.get_updates_buf;
|
|
57
|
+
saveCursor(cursor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (abortSignal.aborted)
|
|
62
|
+
break;
|
|
63
|
+
consecutiveErrors++;
|
|
64
|
+
process.stderr.write(`[weixin] Poll error (${consecutiveErrors}): ${err instanceof Error ? err.message : err}\n`);
|
|
65
|
+
if (consecutiveErrors >= 3) {
|
|
66
|
+
process.stderr.write('[weixin] Too many consecutive errors, backing off 30s...\n');
|
|
67
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
68
|
+
consecutiveErrors = 0;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.stderr.write('[weixin] Poll loop stopped.\n');
|
|
76
|
+
}
|
|
77
|
+
async function processMessage(msg, onMessage) {
|
|
78
|
+
if (msg.message_type !== MessageType.USER)
|
|
79
|
+
return;
|
|
80
|
+
const fromUserId = msg.from_user_id;
|
|
81
|
+
if (!fromUserId)
|
|
82
|
+
return;
|
|
83
|
+
// Cache context token (required for replies)
|
|
84
|
+
if (msg.context_token) {
|
|
85
|
+
contextTokens.set(fromUserId, msg.context_token);
|
|
86
|
+
}
|
|
87
|
+
// Extract text, image, file CDN references, and referenced message
|
|
88
|
+
let textContent = '';
|
|
89
|
+
let image;
|
|
90
|
+
let file;
|
|
91
|
+
let refText;
|
|
92
|
+
if (msg.item_list) {
|
|
93
|
+
for (const item of msg.item_list) {
|
|
94
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text) {
|
|
95
|
+
textContent += (textContent ? '\n' : '') + item.text_item.text;
|
|
96
|
+
}
|
|
97
|
+
// Extract referenced message text
|
|
98
|
+
if (item.ref_msg) {
|
|
99
|
+
const refItem = item.ref_msg.message_item;
|
|
100
|
+
if (refItem?.text_item?.text) {
|
|
101
|
+
refText = refItem.text_item.text;
|
|
102
|
+
}
|
|
103
|
+
else if (item.ref_msg.title) {
|
|
104
|
+
refText = item.ref_msg.title;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (item.type === MessageItemType.IMAGE && item.image_item) {
|
|
108
|
+
const media = item.image_item.media;
|
|
109
|
+
if (media?.encrypt_query_param && media.aes_key) {
|
|
110
|
+
image = {
|
|
111
|
+
encryptQueryParam: media.encrypt_query_param,
|
|
112
|
+
aesKey: media.aes_key,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (item.type === MessageItemType.FILE && item.file_item) {
|
|
117
|
+
const media = item.file_item.media;
|
|
118
|
+
if (media?.encrypt_query_param && media.aes_key) {
|
|
119
|
+
file = {
|
|
120
|
+
encryptQueryParam: media.encrypt_query_param,
|
|
121
|
+
aesKey: media.aes_key,
|
|
122
|
+
fileName: item.file_item.file_name || `file_${Date.now()}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Need either text, image, or file to proceed
|
|
129
|
+
if (!textContent && !image && !file)
|
|
130
|
+
return;
|
|
131
|
+
await onMessage({
|
|
132
|
+
fromUserId,
|
|
133
|
+
messageId: String(msg.message_id || ''),
|
|
134
|
+
text: textContent || (file ? `(file: ${file.fileName})` : '(image)'),
|
|
135
|
+
image,
|
|
136
|
+
file,
|
|
137
|
+
refText,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=monitor.js.map
|