engine7 7.1.41 → 7.1.42
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/cli.mjs +1010 -121
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -14,6 +14,951 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/channels/wechat.ts
|
|
18
|
+
var wechat_exports = {};
|
|
19
|
+
__export(wechat_exports, {
|
|
20
|
+
WechatAdapter: () => WechatAdapter,
|
|
21
|
+
wechatQrLogin: () => wechatQrLogin
|
|
22
|
+
});
|
|
23
|
+
import * as crypto from "crypto";
|
|
24
|
+
import * as fs from "fs";
|
|
25
|
+
import * as path from "path";
|
|
26
|
+
import * as os from "os";
|
|
27
|
+
import { promises as dns } from "dns";
|
|
28
|
+
function sleep2(ms) {
|
|
29
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
}
|
|
31
|
+
function pkcs7Pad(data, blockSize = 16) {
|
|
32
|
+
const pad = blockSize - data.length % blockSize;
|
|
33
|
+
return Buffer.concat([data, Buffer.alloc(pad, pad)]);
|
|
34
|
+
}
|
|
35
|
+
function pkcs7Unpad(data) {
|
|
36
|
+
if (!data.length) return data;
|
|
37
|
+
const pad = data[data.length - 1];
|
|
38
|
+
if (pad < 1 || pad > 16 || pad > data.length) return data;
|
|
39
|
+
return data.subarray(0, data.length - pad);
|
|
40
|
+
}
|
|
41
|
+
function aesEncrypt(plaintext, key) {
|
|
42
|
+
const c = crypto.createCipheriv("aes-128-ecb", key, null);
|
|
43
|
+
c.setAutoPadding(false);
|
|
44
|
+
return Buffer.concat([c.update(pkcs7Pad(plaintext)), c.final()]);
|
|
45
|
+
}
|
|
46
|
+
function aesDecrypt(ciphertext, key) {
|
|
47
|
+
const d = crypto.createDecipheriv("aes-128-ecb", key, null);
|
|
48
|
+
d.setAutoPadding(false);
|
|
49
|
+
return pkcs7Unpad(Buffer.concat([d.update(ciphertext), d.final()]));
|
|
50
|
+
}
|
|
51
|
+
function parseAesKey(b64) {
|
|
52
|
+
const decoded = Buffer.from(b64, "base64");
|
|
53
|
+
if (decoded.length === 16) return decoded;
|
|
54
|
+
if (decoded.length === 32) {
|
|
55
|
+
const text = decoded.toString("ascii");
|
|
56
|
+
if (/^[0-9a-fA-F]+$/.test(text)) return Buffer.from(text, "hex");
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`unexpected aes_key format (${decoded.length} bytes)`);
|
|
59
|
+
}
|
|
60
|
+
function randomUin() {
|
|
61
|
+
return Buffer.from(String(crypto.randomBytes(4).readUInt32BE(0)), "utf-8").toString("base64");
|
|
62
|
+
}
|
|
63
|
+
async function isNetworkUp(hostname) {
|
|
64
|
+
try {
|
|
65
|
+
await dns.resolve(hostname, "A");
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function buildHeaders(token) {
|
|
72
|
+
return {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"AuthorizationType": "ilink_bot_token",
|
|
75
|
+
"X-WECHAT-UIN": randomUin(),
|
|
76
|
+
"iLink-App-Id": ILINK_APP_ID,
|
|
77
|
+
"iLink-App-ClientVersion": String(ILINK_APP_CLIENT_VERSION),
|
|
78
|
+
"Authorization": `Bearer ${token}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function apiPost(baseUrl, endpoint, payload, token, timeoutMs) {
|
|
82
|
+
const body = JSON.stringify({ ...payload, base_info: { channel_version: CHANNEL_VERSION } });
|
|
83
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint}`;
|
|
84
|
+
const ctrl = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
86
|
+
try {
|
|
87
|
+
const resp = await fetch(url, { method: "POST", headers: buildHeaders(token), body, signal: ctrl.signal });
|
|
88
|
+
const text = await resp.text();
|
|
89
|
+
if (!resp.ok) throw new Error(`iLink POST ${endpoint} HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
90
|
+
return JSON.parse(text);
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function assertCdnUrl(url) {
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = new URL(url);
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(`Bad media URL: ${url}`);
|
|
101
|
+
}
|
|
102
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error(`Bad scheme: ${parsed.protocol}`);
|
|
103
|
+
if (!CDN_ALLOWLIST.has(parsed.hostname)) throw new Error(`SSRF: host ${parsed.hostname} not in allowlist`);
|
|
104
|
+
}
|
|
105
|
+
function cdnDownloadUrl(base, param) {
|
|
106
|
+
return `${base.replace(/\/$/, "")}/download?encrypted_query_param=${encodeURIComponent(param)}`;
|
|
107
|
+
}
|
|
108
|
+
async function downloadBytes(url, timeoutMs) {
|
|
109
|
+
const ctrl = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
const resp = await fetch(url, { signal: ctrl.signal });
|
|
113
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
114
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function downloadAndDecryptMedia(cdnBase, encParam, aesKeyB64, fullUrl, timeoutSec) {
|
|
120
|
+
let raw;
|
|
121
|
+
if (encParam) {
|
|
122
|
+
raw = await downloadBytes(cdnDownloadUrl(cdnBase, encParam), timeoutSec * 1e3);
|
|
123
|
+
} else if (fullUrl) {
|
|
124
|
+
assertCdnUrl(fullUrl);
|
|
125
|
+
raw = await downloadBytes(fullUrl, timeoutSec * 1e3);
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error("media: no encrypt_query_param or full_url");
|
|
128
|
+
}
|
|
129
|
+
if (aesKeyB64) raw = aesDecrypt(raw, parseAesKey(aesKeyB64));
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
132
|
+
function cdnUploadUrl(base, uploadParam, filekey) {
|
|
133
|
+
return `${base.replace(/\/$/, "")}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
134
|
+
}
|
|
135
|
+
async function uploadCiphertext(uploadUrl, ciphertext) {
|
|
136
|
+
const ctrl = new AbortController();
|
|
137
|
+
const timer = setTimeout(() => ctrl.abort(), 12e4);
|
|
138
|
+
try {
|
|
139
|
+
console.log(`[wechat:cdn] upload start: url=${uploadUrl.slice(0, 120)}... ciphertextLen=${ciphertext.length}`);
|
|
140
|
+
const resp = await fetch(uploadUrl, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
143
|
+
body: new Uint8Array(ciphertext),
|
|
144
|
+
signal: ctrl.signal
|
|
145
|
+
});
|
|
146
|
+
console.log(`[wechat:cdn] upload resp: status=${resp.status} headers=${JSON.stringify(Object.fromEntries(resp.headers.entries()))}`);
|
|
147
|
+
if (resp.status === 200) {
|
|
148
|
+
const param = resp.headers.get("x-encrypted-param");
|
|
149
|
+
const bodyText = await resp.text();
|
|
150
|
+
if (param) {
|
|
151
|
+
return param;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`CDN upload missing x-encrypted-param: ${bodyText.slice(0, 200)}`);
|
|
154
|
+
}
|
|
155
|
+
const text = await resp.text();
|
|
156
|
+
throw new Error(`CDN upload HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function safeId(id, keep = 8) {
|
|
162
|
+
return id.length <= keep ? id : id.slice(0, keep) + "...";
|
|
163
|
+
}
|
|
164
|
+
function isSessionExpired(ret, errcode, errmsg) {
|
|
165
|
+
if (ret === SESSION_EXPIRED_ERRCODE || errcode === SESSION_EXPIRED_ERRCODE) return true;
|
|
166
|
+
if ((ret === RATE_LIMIT_ERRCODE || errcode === RATE_LIMIT_ERRCODE) && (errmsg || "").toLowerCase() === "unknown error") return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function extractText(itemList) {
|
|
170
|
+
for (const item of itemList) {
|
|
171
|
+
if (item.type === ITEM_TEXT) {
|
|
172
|
+
const text = String(item.text_item?.text || "");
|
|
173
|
+
const ref = item.ref_msg || {};
|
|
174
|
+
const refItem = ref.message_item || {};
|
|
175
|
+
if ([ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE].includes(refItem.type)) {
|
|
176
|
+
const title = ref.title || "";
|
|
177
|
+
return `${title ? `[\u5F15\u7528\u5A92\u4F53: ${title}]
|
|
178
|
+
` : "[\u5F15\u7528\u5A92\u4F53]\n"}${text}`.trim();
|
|
179
|
+
}
|
|
180
|
+
if (refItem && Object.keys(refItem).length > 0) {
|
|
181
|
+
const parts = [];
|
|
182
|
+
if (ref.title) parts.push(String(ref.title));
|
|
183
|
+
const rt = extractText([refItem]);
|
|
184
|
+
if (rt) parts.push(rt);
|
|
185
|
+
if (parts.length) return `[\u5F15\u7528: ${parts.join(" | ")}]
|
|
186
|
+
${text}`.trim();
|
|
187
|
+
}
|
|
188
|
+
return text;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const item of itemList) {
|
|
192
|
+
if (item.type === ITEM_VOICE) {
|
|
193
|
+
const vt = String(item.voice_item?.text || "");
|
|
194
|
+
if (vt) return vt;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
function guessChatType(msg, accountId) {
|
|
200
|
+
const roomId = String(msg.room_id || msg.chat_room_id || "").trim();
|
|
201
|
+
const toId = String(msg.to_user_id || "").trim();
|
|
202
|
+
const isGroup = !!roomId || !!toId && !!accountId && toId !== accountId && msg.msg_type === 1;
|
|
203
|
+
return isGroup ? { type: "group", chatId: roomId || toId || String(msg.from_user_id || "") } : { type: "dm", chatId: String(msg.from_user_id || "") };
|
|
204
|
+
}
|
|
205
|
+
function mimeFromFilename(filename) {
|
|
206
|
+
const ext = path.extname(filename).toLowerCase();
|
|
207
|
+
const map = {
|
|
208
|
+
".jpg": "image/jpeg",
|
|
209
|
+
".jpeg": "image/jpeg",
|
|
210
|
+
".png": "image/png",
|
|
211
|
+
".gif": "image/gif",
|
|
212
|
+
".webp": "image/webp",
|
|
213
|
+
".bmp": "image/bmp",
|
|
214
|
+
".mp4": "video/mp4",
|
|
215
|
+
".mov": "video/quicktime",
|
|
216
|
+
".avi": "video/x-msvideo",
|
|
217
|
+
".mp3": "audio/mpeg",
|
|
218
|
+
".wav": "audio/wav",
|
|
219
|
+
".m4a": "audio/mp4",
|
|
220
|
+
".silk": "audio/silk",
|
|
221
|
+
".pdf": "application/pdf",
|
|
222
|
+
".doc": "application/msword",
|
|
223
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
224
|
+
".zip": "application/zip",
|
|
225
|
+
".txt": "text/plain"
|
|
226
|
+
};
|
|
227
|
+
return map[ext] || "application/octet-stream";
|
|
228
|
+
}
|
|
229
|
+
function formatForWechat(content) {
|
|
230
|
+
let lines = content.split("\n");
|
|
231
|
+
lines = lines.map((l) => {
|
|
232
|
+
const m = l.match(/^#{1,4}\s+(.*)/);
|
|
233
|
+
return m ? `\u3010${m[1].trim()}\u3011` : l;
|
|
234
|
+
});
|
|
235
|
+
const result = [];
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
if (line.trim().startsWith("|") && line.trim().endsWith("|")) {
|
|
238
|
+
if (/^\|[\s:-]+\|$/.test(line.trim())) continue;
|
|
239
|
+
const cells = line.trim().slice(1, -1).split("|").map((c) => c.trim());
|
|
240
|
+
if (cells.every((c) => /^[-:]+$/.test(c))) continue;
|
|
241
|
+
result.push(`\u2022 ${cells.join(" | ")}`);
|
|
242
|
+
} else {
|
|
243
|
+
result.push(line);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const out = [];
|
|
247
|
+
let prevBlank = false;
|
|
248
|
+
for (const l of result) {
|
|
249
|
+
if (!l.trim() && prevBlank) continue;
|
|
250
|
+
out.push(l);
|
|
251
|
+
prevBlank = !l.trim();
|
|
252
|
+
}
|
|
253
|
+
return out.join("\n").trim();
|
|
254
|
+
}
|
|
255
|
+
function splitText(text, maxLen) {
|
|
256
|
+
if (text.length <= maxLen) return [text];
|
|
257
|
+
const chunks = [];
|
|
258
|
+
let remaining = text;
|
|
259
|
+
while (remaining.length > maxLen) {
|
|
260
|
+
let idx = remaining.lastIndexOf("\n", maxLen);
|
|
261
|
+
if (idx <= 0) idx = remaining.lastIndexOf(" ", maxLen);
|
|
262
|
+
if (idx <= 0) idx = maxLen;
|
|
263
|
+
chunks.push(remaining.slice(0, idx).trim());
|
|
264
|
+
remaining = remaining.slice(idx).trim();
|
|
265
|
+
}
|
|
266
|
+
if (remaining) chunks.push(remaining);
|
|
267
|
+
return chunks.filter((c) => c);
|
|
268
|
+
}
|
|
269
|
+
function loadSyncBuf(stateDir, accountId) {
|
|
270
|
+
try {
|
|
271
|
+
const fp = path.join(stateDir, "weixin", `${accountId}.sync.json`);
|
|
272
|
+
if (fs.existsSync(fp)) return String(JSON.parse(fs.readFileSync(fp, "utf-8")).sync_buf || "");
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
function saveSyncBuf(stateDir, accountId, buf) {
|
|
278
|
+
try {
|
|
279
|
+
const fp = path.join(stateDir, "weixin", `${accountId}.sync.json`);
|
|
280
|
+
const dir = path.dirname(fp);
|
|
281
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
282
|
+
fs.writeFileSync(fp, JSON.stringify({ sync_buf: buf }));
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function wechatQrLogin(options) {
|
|
287
|
+
const botType = options?.botType || "3";
|
|
288
|
+
const timeoutSeconds = options?.timeoutSeconds || 480;
|
|
289
|
+
const stateDir = options?.stateDir || path.join(os.homedir?.() || "/tmp", ".engine7");
|
|
290
|
+
console.log("[wechat] Fetching QR code from iLink...");
|
|
291
|
+
let qrResp;
|
|
292
|
+
try {
|
|
293
|
+
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error(`[wechat] Failed to fetch QR code: ${err.message}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const qrcodeValue = String(qrResp?.qrcode || "");
|
|
299
|
+
const qrcodeUrl = String(qrResp?.qrcode_img_content || "");
|
|
300
|
+
if (!qrcodeValue) {
|
|
301
|
+
console.error("[wechat] QR response missing qrcode field");
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const qrScanData = qrcodeUrl || qrcodeValue;
|
|
305
|
+
console.log("\n========== \u5FAE\u4FE1\u626B\u7801\u767B\u5F55 ==========");
|
|
306
|
+
if (qrcodeUrl) {
|
|
307
|
+
console.log(`\u626B\u7801\u94FE\u63A5: ${qrcodeUrl}`);
|
|
308
|
+
}
|
|
309
|
+
console.log("\u8BF7\u7528\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801\uFF08\u6216\u6253\u5F00\u4E0A\u9762\u7684\u94FE\u63A5\uFF09:");
|
|
310
|
+
console.log("===================================\n");
|
|
311
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
312
|
+
let currentBaseUrl = ILINK_BASE_URL;
|
|
313
|
+
let refreshCount = 0;
|
|
314
|
+
while (Date.now() < deadline) {
|
|
315
|
+
let statusResp;
|
|
316
|
+
try {
|
|
317
|
+
statusResp = await apiGet(currentBaseUrl, `${EP_GET_QR_STATUS}?qrcode=${qrcodeValue}`, "", QR_TIMEOUT_MS);
|
|
318
|
+
} catch {
|
|
319
|
+
await sleep2(1e3);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const status = String(statusResp?.status || "wait");
|
|
323
|
+
if (status === "wait") {
|
|
324
|
+
process.stdout.write(".");
|
|
325
|
+
} else if (status === "scaned") {
|
|
326
|
+
console.log("\n\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u91CC\u786E\u8BA4...");
|
|
327
|
+
} else if (status === "scaned_but_redirect") {
|
|
328
|
+
const redirectHost = String(statusResp?.redirect_host || "");
|
|
329
|
+
if (redirectHost) currentBaseUrl = `https://${redirectHost}`;
|
|
330
|
+
} else if (status === "expired") {
|
|
331
|
+
refreshCount++;
|
|
332
|
+
if (refreshCount > 3) {
|
|
333
|
+
console.log("\n\u4E8C\u7EF4\u7801\u591A\u6B21\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u767B\u5F55\u3002");
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
console.log(`
|
|
337
|
+
\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u5237\u65B0\u4E2D... (${refreshCount}/3)`);
|
|
338
|
+
try {
|
|
339
|
+
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
340
|
+
const newQrValue = String(qrResp?.qrcode || "");
|
|
341
|
+
const newQrUrl = String(qrResp?.qrcode_img_content || "");
|
|
342
|
+
if (newQrUrl) console.log(`\u65B0\u626B\u7801\u94FE\u63A5: ${newQrUrl}`);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.error(`[wechat] QR refresh failed: ${err.message}`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
} else if (status === "confirmed") {
|
|
348
|
+
const accountId = String(statusResp?.ilink_bot_id || "");
|
|
349
|
+
const token = String(statusResp?.bot_token || "");
|
|
350
|
+
const baseUrl = String(statusResp?.baseurl || ILINK_BASE_URL);
|
|
351
|
+
const userId = String(statusResp?.ilink_user_id || "");
|
|
352
|
+
if (!accountId || !token) {
|
|
353
|
+
console.error("[wechat] QR confirmed but credential payload incomplete");
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
|
|
357
|
+
const credFile = path.join(stateDir, `weixin-${accountId}.json`);
|
|
358
|
+
fs.writeFileSync(credFile, JSON.stringify({ accountId, token, baseUrl, userId }, null, 2), "utf8");
|
|
359
|
+
console.log(`
|
|
360
|
+
\u2705 \u5FAE\u4FE1\u767B\u5F55\u6210\u529F!`);
|
|
361
|
+
console.log(` accountId: ${accountId}`);
|
|
362
|
+
console.log(` \u51ED\u8BC1\u5DF2\u4FDD\u5B58: ${credFile}`);
|
|
363
|
+
console.log(`
|
|
364
|
+
\u8BF7\u5C06\u4EE5\u4E0B\u914D\u7F6E\u6DFB\u52A0\u5230 xiaoke.json:`);
|
|
365
|
+
console.log(JSON.stringify({
|
|
366
|
+
wechat: {
|
|
367
|
+
token,
|
|
368
|
+
accountId,
|
|
369
|
+
baseUrl: baseUrl !== ILINK_BASE_URL ? baseUrl : void 0
|
|
370
|
+
}
|
|
371
|
+
}, null, 2));
|
|
372
|
+
return { accountId, token, baseUrl, userId };
|
|
373
|
+
}
|
|
374
|
+
await sleep2(1e3);
|
|
375
|
+
}
|
|
376
|
+
console.log("\n[wechat] QR login timed out");
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
async function apiGet(baseUrl, endpoint, _token, timeoutMs) {
|
|
380
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.replace(/^\//, "")}`;
|
|
381
|
+
const controller = new AbortController();
|
|
382
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
383
|
+
try {
|
|
384
|
+
const resp = await fetch(url, {
|
|
385
|
+
method: "GET",
|
|
386
|
+
signal: controller.signal,
|
|
387
|
+
headers: { "Accept": "application/json" }
|
|
388
|
+
});
|
|
389
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
390
|
+
return await resp.json();
|
|
391
|
+
} finally {
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
var ILINK_BASE_URL, WEIXIN_CDN_BASE_URL, ILINK_APP_ID, CHANNEL_VERSION, ILINK_APP_CLIENT_VERSION, EP_GET_UPDATES, EP_SEND_MESSAGE, EP_SEND_TYPING, EP_GET_CONFIG, EP_GET_UPLOAD_URL, LONG_POLL_TIMEOUT_MS, API_TIMEOUT_MS, MAX_MESSAGE_LENGTH, MAX_CONSECUTIVE_FAILURES, RETRY_DELAY_MS, BACKOFF_DELAY_MS, DISCONNECTED_THRESHOLD, DISCONNECTED_POLL_INTERVAL, SESSION_EXPIRED_ERRCODE, RATE_LIMIT_ERRCODE, SEND_CHUNK_DELAY_MS, SEND_CHUNK_RETRIES, SEND_CHUNK_RETRY_DELAY_MS, ITEM_TEXT, ITEM_IMAGE, ITEM_VOICE, ITEM_FILE, ITEM_VIDEO, MSG_TYPE_BOT, MSG_STATE_FINISH, MEDIA_IMAGE, MEDIA_VIDEO, MEDIA_FILE, MEDIA_VOICE, CDN_ALLOWLIST, ContextTokenStore, MessageDeduplicator, WechatAdapter, EP_GET_BOT_QR, EP_GET_QR_STATUS, QR_TIMEOUT_MS;
|
|
396
|
+
var init_wechat = __esm({
|
|
397
|
+
"src/channels/wechat.ts"() {
|
|
398
|
+
"use strict";
|
|
399
|
+
ILINK_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
400
|
+
WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
401
|
+
ILINK_APP_ID = "bot";
|
|
402
|
+
CHANNEL_VERSION = "2.2.0";
|
|
403
|
+
ILINK_APP_CLIENT_VERSION = 2 << 16 | 2 << 8 | 0;
|
|
404
|
+
EP_GET_UPDATES = "ilink/bot/getupdates";
|
|
405
|
+
EP_SEND_MESSAGE = "ilink/bot/sendmessage";
|
|
406
|
+
EP_SEND_TYPING = "ilink/bot/sendtyping";
|
|
407
|
+
EP_GET_CONFIG = "ilink/bot/getconfig";
|
|
408
|
+
EP_GET_UPLOAD_URL = "ilink/bot/getuploadurl";
|
|
409
|
+
LONG_POLL_TIMEOUT_MS = 35e3;
|
|
410
|
+
API_TIMEOUT_MS = 15e3;
|
|
411
|
+
MAX_MESSAGE_LENGTH = 2e3;
|
|
412
|
+
MAX_CONSECUTIVE_FAILURES = 3;
|
|
413
|
+
RETRY_DELAY_MS = 2e3;
|
|
414
|
+
BACKOFF_DELAY_MS = 3e4;
|
|
415
|
+
DISCONNECTED_THRESHOLD = 5;
|
|
416
|
+
DISCONNECTED_POLL_INTERVAL = 5e3;
|
|
417
|
+
SESSION_EXPIRED_ERRCODE = -14;
|
|
418
|
+
RATE_LIMIT_ERRCODE = -2;
|
|
419
|
+
SEND_CHUNK_DELAY_MS = 1500;
|
|
420
|
+
SEND_CHUNK_RETRIES = 4;
|
|
421
|
+
SEND_CHUNK_RETRY_DELAY_MS = 1e3;
|
|
422
|
+
ITEM_TEXT = 1;
|
|
423
|
+
ITEM_IMAGE = 2;
|
|
424
|
+
ITEM_VOICE = 3;
|
|
425
|
+
ITEM_FILE = 4;
|
|
426
|
+
ITEM_VIDEO = 5;
|
|
427
|
+
MSG_TYPE_BOT = 2;
|
|
428
|
+
MSG_STATE_FINISH = 2;
|
|
429
|
+
MEDIA_IMAGE = 1;
|
|
430
|
+
MEDIA_VIDEO = 2;
|
|
431
|
+
MEDIA_FILE = 3;
|
|
432
|
+
MEDIA_VOICE = 4;
|
|
433
|
+
CDN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
434
|
+
"novac2c.cdn.weixin.qq.com",
|
|
435
|
+
"ilinkai.weixin.qq.com",
|
|
436
|
+
"wx.qlogo.cn",
|
|
437
|
+
"thirdwx.qlogo.cn",
|
|
438
|
+
"res.wx.qq.com",
|
|
439
|
+
"mmbiz.qpic.cn",
|
|
440
|
+
"mmbiz.qlogo.cn"
|
|
441
|
+
]);
|
|
442
|
+
ContextTokenStore = class {
|
|
443
|
+
cache = /* @__PURE__ */ new Map();
|
|
444
|
+
filePath;
|
|
445
|
+
constructor(stateDir, accountId) {
|
|
446
|
+
this.filePath = path.join(stateDir, "weixin", `${accountId}.context-tokens.json`);
|
|
447
|
+
}
|
|
448
|
+
k(a, p) {
|
|
449
|
+
return `${a}:${p}`;
|
|
450
|
+
}
|
|
451
|
+
restore() {
|
|
452
|
+
try {
|
|
453
|
+
if (fs.existsSync(this.filePath)) {
|
|
454
|
+
for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(this.filePath, "utf-8"))))
|
|
455
|
+
this.cache.set(k, String(v));
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
save() {
|
|
461
|
+
try {
|
|
462
|
+
const dir = path.dirname(this.filePath);
|
|
463
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
464
|
+
const obj = {};
|
|
465
|
+
this.cache.forEach((v, k) => obj[k] = v);
|
|
466
|
+
fs.writeFileSync(this.filePath, JSON.stringify(obj));
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
get(a, p) {
|
|
471
|
+
return p !== void 0 ? this.cache.get(this.k(a, p)) : this.cache.get(a);
|
|
472
|
+
}
|
|
473
|
+
set(a, pOrT, t) {
|
|
474
|
+
if (t !== void 0) this.cache.set(this.k(a, pOrT), t);
|
|
475
|
+
else this.cache.set(a, pOrT);
|
|
476
|
+
this.save();
|
|
477
|
+
}
|
|
478
|
+
delete(a, p) {
|
|
479
|
+
const key = p !== void 0 ? this.k(a, p) : a;
|
|
480
|
+
this.cache.delete(key);
|
|
481
|
+
this.save();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
MessageDeduplicator = class {
|
|
485
|
+
ids = /* @__PURE__ */ new Map();
|
|
486
|
+
ttlMs;
|
|
487
|
+
constructor(ttlMs = 3e5) {
|
|
488
|
+
this.ttlMs = ttlMs;
|
|
489
|
+
}
|
|
490
|
+
isDuplicate(id) {
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
for (const [k, ts] of this.ids) {
|
|
493
|
+
if (now - ts > this.ttlMs) this.ids.delete(k);
|
|
494
|
+
}
|
|
495
|
+
if (this.ids.has(id)) return true;
|
|
496
|
+
this.ids.set(id, now);
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
WechatAdapter = class {
|
|
501
|
+
name = "wechat";
|
|
502
|
+
suppressToolDisplay = true;
|
|
503
|
+
// 微信不支持 tool display(rate limit 太严)
|
|
504
|
+
config;
|
|
505
|
+
messageHandler = null;
|
|
506
|
+
tokenStore;
|
|
507
|
+
dedup = new MessageDeduplicator();
|
|
508
|
+
running = false;
|
|
509
|
+
connected = false;
|
|
510
|
+
typingTasks = /* @__PURE__ */ new Map();
|
|
511
|
+
lastSendAt = 0;
|
|
512
|
+
// 全局发送节流
|
|
513
|
+
typingTicketCache = /* @__PURE__ */ new Map();
|
|
514
|
+
// typing ticket 缓存(10min TTL)
|
|
515
|
+
baseUrl;
|
|
516
|
+
cdnBaseUrl;
|
|
517
|
+
stateDir;
|
|
518
|
+
constructor(config) {
|
|
519
|
+
this.config = config;
|
|
520
|
+
this.baseUrl = config.baseUrl?.replace(/\/$/, "") || ILINK_BASE_URL;
|
|
521
|
+
this.cdnBaseUrl = config.cdnBaseUrl?.replace(/\/$/, "") || WEIXIN_CDN_BASE_URL;
|
|
522
|
+
this.stateDir = config.stateDir || path.join(os.homedir?.() || "/tmp", ".engine7");
|
|
523
|
+
if (!fs.existsSync(this.stateDir)) fs.mkdirSync(this.stateDir, { recursive: true });
|
|
524
|
+
}
|
|
525
|
+
// --- ChannelAdapter interface ---
|
|
526
|
+
onMessage(handler) {
|
|
527
|
+
this.messageHandler = handler;
|
|
528
|
+
}
|
|
529
|
+
async connect() {
|
|
530
|
+
if (!this.config.token) throw new Error("WechatAdapter: token is required");
|
|
531
|
+
if (!this.config.accountId) throw new Error("WechatAdapter: accountId is required");
|
|
532
|
+
this.tokenStore = new ContextTokenStore(this.stateDir, this.config.accountId);
|
|
533
|
+
this.tokenStore.restore();
|
|
534
|
+
this.running = true;
|
|
535
|
+
this.connected = true;
|
|
536
|
+
this.pollLoop().catch((err) => {
|
|
537
|
+
console.error(`[wechat] poll loop crashed: ${err.message}`);
|
|
538
|
+
this.connected = false;
|
|
539
|
+
});
|
|
540
|
+
console.log(`[wechat] Connected account=${safeId(this.config.accountId)} base=${this.baseUrl}`);
|
|
541
|
+
if (this.config.groupPolicy && this.config.groupPolicy !== "disabled") {
|
|
542
|
+
console.warn(`[wechat] groupPolicy=${this.config.groupPolicy} \u2014 iLink bot accounts typically cannot join ordinary WeChat groups`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async disconnect() {
|
|
546
|
+
this.running = false;
|
|
547
|
+
this.connected = false;
|
|
548
|
+
console.log(`[wechat] Disconnected`);
|
|
549
|
+
}
|
|
550
|
+
// --- Streaming preview(微信没有编辑API,只在finish时发一次) ---
|
|
551
|
+
async sendPreview(channelId, _content, _agentName) {
|
|
552
|
+
this.previewSent = false;
|
|
553
|
+
return { channelId, messageId: `preview-${Date.now()}` };
|
|
554
|
+
}
|
|
555
|
+
previewSent = false;
|
|
556
|
+
// 防止 freeze() 重复触发发送
|
|
557
|
+
async editPreview(_handle, _content, _agentName, _isFinal) {
|
|
558
|
+
}
|
|
559
|
+
async deletePreview(_handle) {
|
|
560
|
+
}
|
|
561
|
+
async send(target, message, options) {
|
|
562
|
+
const formatted = formatForWechat(message);
|
|
563
|
+
if (!formatted.trim()) return;
|
|
564
|
+
const elapsed = Date.now() - this.lastSendAt;
|
|
565
|
+
if (elapsed < 3e3) await sleep2(3e3 - elapsed);
|
|
566
|
+
const chunks = splitText(formatted, MAX_MESSAGE_LENGTH);
|
|
567
|
+
const contextToken = this.tokenStore.get(this.config.accountId, target);
|
|
568
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
569
|
+
await this.sendTextChunk(target, chunks[i], contextToken);
|
|
570
|
+
this.lastSendAt = Date.now();
|
|
571
|
+
if (i < chunks.length - 1) await sleep2(SEND_CHUNK_DELAY_MS);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async sendFile(target, message, attachment) {
|
|
575
|
+
const filePath = attachment.path;
|
|
576
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
577
|
+
const plaintext = fs.readFileSync(filePath);
|
|
578
|
+
const mediaType = this.guessMediaType(filePath, attachment.mimeType);
|
|
579
|
+
const filekey = crypto.randomBytes(16).toString("hex");
|
|
580
|
+
const aesKey = crypto.randomBytes(16);
|
|
581
|
+
const rawsize = plaintext.length;
|
|
582
|
+
const rawfilemd5 = crypto.createHash("md5").update(plaintext).digest("hex");
|
|
583
|
+
const ciphertext = aesEncrypt(plaintext, aesKey);
|
|
584
|
+
const aeskeyHex = aesKey.toString("hex");
|
|
585
|
+
const uploadResp = await apiPost(this.baseUrl, EP_GET_UPLOAD_URL, {
|
|
586
|
+
to_user_id: target,
|
|
587
|
+
media_type: mediaType,
|
|
588
|
+
filekey,
|
|
589
|
+
rawsize,
|
|
590
|
+
rawfilemd5,
|
|
591
|
+
filesize: ciphertext.length,
|
|
592
|
+
aeskey: aeskeyHex,
|
|
593
|
+
no_need_thumb: true
|
|
594
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
595
|
+
const uploadFullUrl = String(uploadResp.upload_full_url || "");
|
|
596
|
+
const uploadParam = String(uploadResp.upload_param || "");
|
|
597
|
+
console.log(`[wechat] sendFile getUploadUrl: mediaType=${mediaType} size=${rawsize} uploadFullUrl=${uploadFullUrl ? "YES" : "NO"} uploadParam=${uploadParam ? uploadParam.slice(0, 40) + "..." : "NO"} resp=${JSON.stringify(uploadResp).slice(0, 200)}`);
|
|
598
|
+
if (!uploadFullUrl && !uploadParam) {
|
|
599
|
+
throw new Error(`getUploadUrl returned neither upload_param nor upload_full_url: ${JSON.stringify(uploadResp).slice(0, 200)}`);
|
|
600
|
+
}
|
|
601
|
+
const uploadUrl = uploadFullUrl || cdnUploadUrl(this.cdnBaseUrl, uploadParam, filekey);
|
|
602
|
+
const encryptedQueryParam = await uploadCiphertext(uploadUrl, ciphertext);
|
|
603
|
+
const aesKeyForApi = Buffer.from(aeskeyHex, "ascii").toString("base64");
|
|
604
|
+
const mediaItem = this.buildMediaItem(mediaType, {
|
|
605
|
+
encryptedQueryParam,
|
|
606
|
+
aesKeyForApi,
|
|
607
|
+
ciphertextSize: ciphertext.length,
|
|
608
|
+
plaintextSize: rawsize,
|
|
609
|
+
filename: path.basename(filePath),
|
|
610
|
+
rawfilemd5,
|
|
611
|
+
voiceDurationSec: attachment.voiceDurationSec
|
|
612
|
+
});
|
|
613
|
+
if (message && message.trim()) {
|
|
614
|
+
const contextToken2 = this.tokenStore.get(target);
|
|
615
|
+
const clientId2 = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
616
|
+
await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
617
|
+
msg: {
|
|
618
|
+
from_user_id: "",
|
|
619
|
+
to_user_id: target,
|
|
620
|
+
client_id: clientId2,
|
|
621
|
+
message_type: MSG_TYPE_BOT,
|
|
622
|
+
message_state: MSG_STATE_FINISH,
|
|
623
|
+
item_list: [{ type: ITEM_TEXT, text_item: { text: this.formatMessage(message) } }],
|
|
624
|
+
...contextToken2 ? { context_token: contextToken2 } : {}
|
|
625
|
+
}
|
|
626
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
627
|
+
}
|
|
628
|
+
const clientId = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
629
|
+
const contextToken = this.tokenStore.get(target);
|
|
630
|
+
await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
631
|
+
msg: {
|
|
632
|
+
from_user_id: "",
|
|
633
|
+
to_user_id: target,
|
|
634
|
+
client_id: clientId,
|
|
635
|
+
message_type: MSG_TYPE_BOT,
|
|
636
|
+
message_state: MSG_STATE_FINISH,
|
|
637
|
+
item_list: [mediaItem],
|
|
638
|
+
...contextToken ? { context_token: contextToken } : {}
|
|
639
|
+
}
|
|
640
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
641
|
+
}
|
|
642
|
+
// --- Typing indicator ---
|
|
643
|
+
getTypingTicket(userId) {
|
|
644
|
+
const entry = this.typingTicketCache.get(userId);
|
|
645
|
+
if (!entry) return void 0;
|
|
646
|
+
if (Date.now() >= entry.expires) {
|
|
647
|
+
this.typingTicketCache.delete(userId);
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
return entry.ticket;
|
|
651
|
+
}
|
|
652
|
+
async fetchTypingTicket(target) {
|
|
653
|
+
if (!this.connected) return void 0;
|
|
654
|
+
try {
|
|
655
|
+
const ctxToken = this.tokenStore.get(this.config.accountId, target);
|
|
656
|
+
const payload = { ilink_user_id: target };
|
|
657
|
+
if (ctxToken) payload.context_token = ctxToken;
|
|
658
|
+
const resp = await apiPost(this.baseUrl, EP_GET_CONFIG, payload, this.config.token, 1e4);
|
|
659
|
+
const ticket = String(resp.typing_ticket || "");
|
|
660
|
+
if (ticket) {
|
|
661
|
+
this.typingTicketCache.set(target, { ticket, expires: Date.now() + 6e5 });
|
|
662
|
+
}
|
|
663
|
+
return ticket || void 0;
|
|
664
|
+
} catch {
|
|
665
|
+
return void 0;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async sendTypingIndicator(target, status) {
|
|
669
|
+
if (!this.connected) return;
|
|
670
|
+
try {
|
|
671
|
+
let ticket = this.getTypingTicket(target);
|
|
672
|
+
if (!ticket) ticket = await this.fetchTypingTicket(target);
|
|
673
|
+
if (!ticket) return;
|
|
674
|
+
await apiPost(this.baseUrl, EP_SEND_TYPING, {
|
|
675
|
+
ilink_user_id: target,
|
|
676
|
+
typing_ticket: ticket,
|
|
677
|
+
status
|
|
678
|
+
}, this.config.token, 1e4);
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
startTyping(channelId) {
|
|
683
|
+
if (this.typingTasks.has(channelId)) return;
|
|
684
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
685
|
+
});
|
|
686
|
+
const timer = setInterval(() => {
|
|
687
|
+
const state = this.typingTasks.get(channelId);
|
|
688
|
+
if (!state || state.paused) return;
|
|
689
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
690
|
+
this.stopTyping(channelId);
|
|
691
|
+
});
|
|
692
|
+
}, 8e3);
|
|
693
|
+
this.typingTasks.set(channelId, { timer, paused: false });
|
|
694
|
+
}
|
|
695
|
+
async stopTyping(channelId) {
|
|
696
|
+
const state = this.typingTasks.get(channelId);
|
|
697
|
+
if (!state) return;
|
|
698
|
+
clearInterval(state.timer);
|
|
699
|
+
this.typingTasks.delete(channelId);
|
|
700
|
+
await this.sendTypingIndicator(channelId, 2);
|
|
701
|
+
}
|
|
702
|
+
pauseTyping(channelId) {
|
|
703
|
+
const state = this.typingTasks.get(channelId);
|
|
704
|
+
if (state) state.paused = true;
|
|
705
|
+
}
|
|
706
|
+
resumeTyping(channelId) {
|
|
707
|
+
const state = this.typingTasks.get(channelId);
|
|
708
|
+
if (state) {
|
|
709
|
+
state.paused = false;
|
|
710
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// --- Internal: poll loop ---
|
|
715
|
+
async pollLoop() {
|
|
716
|
+
let syncBuf = loadSyncBuf(this.stateDir, this.config.accountId);
|
|
717
|
+
let timeoutMs = LONG_POLL_TIMEOUT_MS;
|
|
718
|
+
let consecutiveFailures = 0;
|
|
719
|
+
let disconnected = false;
|
|
720
|
+
while (this.running) {
|
|
721
|
+
try {
|
|
722
|
+
const response = await apiPost(this.baseUrl, EP_GET_UPDATES, {
|
|
723
|
+
get_updates_buf: syncBuf
|
|
724
|
+
}, this.config.token, timeoutMs);
|
|
725
|
+
const suggestedTimeout = response.longpolling_timeout_ms;
|
|
726
|
+
if (typeof suggestedTimeout === "number" && suggestedTimeout > 0) timeoutMs = suggestedTimeout;
|
|
727
|
+
const ret = response.ret ?? 0;
|
|
728
|
+
const errcode = response.errcode ?? 0;
|
|
729
|
+
if (ret !== 0 || errcode !== 0) {
|
|
730
|
+
if (isSessionExpired(ret, errcode, response.errmsg)) {
|
|
731
|
+
console.error(`[wechat] Session expired; pausing 10 min`);
|
|
732
|
+
await sleep2(6e5);
|
|
733
|
+
consecutiveFailures = 0;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
consecutiveFailures++;
|
|
737
|
+
if (consecutiveFailures >= DISCONNECTED_THRESHOLD && !disconnected) {
|
|
738
|
+
disconnected = true;
|
|
739
|
+
console.warn(`[wechat] \u26A0\uFE0F network disconnected \u2014 poll failing continuously`);
|
|
740
|
+
}
|
|
741
|
+
console.warn(`[wechat] getUpdates failed ret=${ret} errcode=${errcode} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
|
|
742
|
+
await sleep2(consecutiveFailures >= MAX_CONSECUTIVE_FAILURES ? BACKOFF_DELAY_MS : RETRY_DELAY_MS);
|
|
743
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) consecutiveFailures = 0;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (disconnected) {
|
|
747
|
+
disconnected = false;
|
|
748
|
+
console.log(`[wechat] \u2705 network recovered \u2014 poll resumed successfully`);
|
|
749
|
+
}
|
|
750
|
+
consecutiveFailures = 0;
|
|
751
|
+
const newSyncBuf = String(response.get_updates_buf || "");
|
|
752
|
+
if (newSyncBuf) {
|
|
753
|
+
syncBuf = newSyncBuf;
|
|
754
|
+
saveSyncBuf(this.stateDir, this.config.accountId, syncBuf);
|
|
755
|
+
}
|
|
756
|
+
for (const message of response.msgs || []) {
|
|
757
|
+
this.processMessage(message).catch((err) => {
|
|
758
|
+
console.error(`[wechat] processMessage error from=${safeId(String(message.from_user_id || ""))}: ${err.message}`);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
if (err.name === "AbortError") continue;
|
|
763
|
+
consecutiveFailures++;
|
|
764
|
+
if (consecutiveFailures >= DISCONNECTED_THRESHOLD && !disconnected) {
|
|
765
|
+
disconnected = true;
|
|
766
|
+
console.warn(`[wechat] \u26A0\uFE0F network disconnected \u2014 poll failing continuously`);
|
|
767
|
+
}
|
|
768
|
+
console.error(`[wechat] poll error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
|
|
769
|
+
if (disconnected) {
|
|
770
|
+
const host = new URL(this.baseUrl).hostname;
|
|
771
|
+
while (this.running && !await isNetworkUp(host)) {
|
|
772
|
+
await sleep2(DISCONNECTED_POLL_INTERVAL);
|
|
773
|
+
}
|
|
774
|
+
console.log(`[wechat] DNS probe passed for ${host}, retrying poll...`);
|
|
775
|
+
} else {
|
|
776
|
+
await sleep2(consecutiveFailures >= MAX_CONSECUTIVE_FAILURES ? BACKOFF_DELAY_MS : RETRY_DELAY_MS);
|
|
777
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) consecutiveFailures = 0;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async processMessage(message) {
|
|
783
|
+
const senderId = String(message.from_user_id || "").trim();
|
|
784
|
+
if (!senderId || senderId === this.config.accountId) return;
|
|
785
|
+
const messageId = String(message.message_id || "").trim();
|
|
786
|
+
if (messageId && this.dedup.isDuplicate(messageId)) return;
|
|
787
|
+
const itemList = message.item_list || [];
|
|
788
|
+
const text = extractText(itemList);
|
|
789
|
+
if (text) {
|
|
790
|
+
const contentKey = `content:${senderId}:${crypto.createHash("md5").update(text).digest("hex")}`;
|
|
791
|
+
if (this.dedup.isDuplicate(contentKey)) return;
|
|
792
|
+
}
|
|
793
|
+
const { type: chatType, chatId } = guessChatType(message, this.config.accountId);
|
|
794
|
+
if (chatType === "group") {
|
|
795
|
+
if (this.config.groupPolicy === "disabled") return;
|
|
796
|
+
if (this.config.groupPolicy === "allowlist" && !(this.config.groupAllowFrom || []).includes(chatId)) return;
|
|
797
|
+
} else {
|
|
798
|
+
if (!this.isDmAllowed(senderId)) return;
|
|
799
|
+
}
|
|
800
|
+
const contextToken = String(message.context_token || "").trim();
|
|
801
|
+
if (contextToken) this.tokenStore.set(senderId, contextToken);
|
|
802
|
+
const attachments = [];
|
|
803
|
+
for (const item of itemList) {
|
|
804
|
+
const media = await this.downloadMediaItem(item).catch((err) => {
|
|
805
|
+
console.warn(`[wechat] media download failed: ${err.message}`);
|
|
806
|
+
return null;
|
|
807
|
+
});
|
|
808
|
+
if (media) attachments.push(media);
|
|
809
|
+
}
|
|
810
|
+
if (!text && attachments.length === 0) return;
|
|
811
|
+
console.log(`[wechat] inbound from=${safeId(senderId)} type=${chatType} media=${attachments.length}`);
|
|
812
|
+
this.messageHandler?.({
|
|
813
|
+
content: text,
|
|
814
|
+
from: senderId,
|
|
815
|
+
fromName: senderId,
|
|
816
|
+
channel_id: chatId,
|
|
817
|
+
channel: "wechat",
|
|
818
|
+
channelType: chatType,
|
|
819
|
+
isBot: false,
|
|
820
|
+
messageId: messageId || void 0,
|
|
821
|
+
recvAt: Date.now(),
|
|
822
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
isDmAllowed(senderId) {
|
|
826
|
+
if (this.config.dmPolicy === "disabled") return false;
|
|
827
|
+
if (this.config.dmPolicy === "allowlist") return (this.config.allowFrom || []).includes(senderId);
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
async downloadMediaItem(item) {
|
|
831
|
+
const itemType = item.type;
|
|
832
|
+
let mediaRef = {};
|
|
833
|
+
let filename = "media.bin";
|
|
834
|
+
let contentType = "application/octet-stream";
|
|
835
|
+
let timeoutSec = 60;
|
|
836
|
+
if (itemType === ITEM_IMAGE) {
|
|
837
|
+
mediaRef = (item.image_item || {}).media || {};
|
|
838
|
+
filename = "image.jpg";
|
|
839
|
+
contentType = "image/jpeg";
|
|
840
|
+
timeoutSec = 30;
|
|
841
|
+
} else if (itemType === ITEM_VIDEO) {
|
|
842
|
+
mediaRef = (item.video_item || {}).media || {};
|
|
843
|
+
filename = "video.mp4";
|
|
844
|
+
contentType = "video/mp4";
|
|
845
|
+
timeoutSec = 120;
|
|
846
|
+
} else if (itemType === ITEM_FILE) {
|
|
847
|
+
const fileItem = item.file_item || {};
|
|
848
|
+
mediaRef = fileItem.media || {};
|
|
849
|
+
filename = String(fileItem.file_name || "document.bin");
|
|
850
|
+
contentType = mimeFromFilename(filename);
|
|
851
|
+
timeoutSec = 60;
|
|
852
|
+
} else if (itemType === ITEM_VOICE) {
|
|
853
|
+
mediaRef = (item.voice_item || {}).media || {};
|
|
854
|
+
if ((item.voice_item || {}).text) return null;
|
|
855
|
+
filename = "voice.silk";
|
|
856
|
+
contentType = "audio/silk";
|
|
857
|
+
timeoutSec = 60;
|
|
858
|
+
} else {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const data = await downloadAndDecryptMedia(
|
|
862
|
+
this.cdnBaseUrl,
|
|
863
|
+
mediaRef.encrypt_query_param,
|
|
864
|
+
mediaRef.aes_key,
|
|
865
|
+
mediaRef.full_url,
|
|
866
|
+
timeoutSec
|
|
867
|
+
);
|
|
868
|
+
const tmpDir = path.join(os.tmpdir(), "weixin-media");
|
|
869
|
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
870
|
+
const tmpPath = path.join(tmpDir, `${crypto.randomUUID().replace(/-/g, "")}-${filename}`);
|
|
871
|
+
fs.writeFileSync(tmpPath, data);
|
|
872
|
+
return { url: `file://${tmpPath}`, filename, contentType, size: data.length };
|
|
873
|
+
}
|
|
874
|
+
// --- Internal: send helpers ---
|
|
875
|
+
async sendTextChunk(target, text, contextToken) {
|
|
876
|
+
let lastError = null;
|
|
877
|
+
let retriedWithoutToken = false;
|
|
878
|
+
let currentToken = contextToken;
|
|
879
|
+
for (let attempt = 0; attempt <= SEND_CHUNK_RETRIES; attempt++) {
|
|
880
|
+
try {
|
|
881
|
+
const clientId = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
882
|
+
const resp = await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
883
|
+
msg: {
|
|
884
|
+
from_user_id: "",
|
|
885
|
+
to_user_id: target,
|
|
886
|
+
client_id: clientId,
|
|
887
|
+
message_type: MSG_TYPE_BOT,
|
|
888
|
+
message_state: MSG_STATE_FINISH,
|
|
889
|
+
item_list: [{ type: ITEM_TEXT, text_item: { text } }],
|
|
890
|
+
...currentToken ? { context_token: currentToken } : {}
|
|
891
|
+
}
|
|
892
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
893
|
+
const ret = resp.ret ?? 0;
|
|
894
|
+
const errcode = resp.errcode ?? 0;
|
|
895
|
+
if (ret !== 0 || errcode !== 0) {
|
|
896
|
+
if (isSessionExpired(ret, errcode, resp.errmsg) && !retriedWithoutToken && currentToken) {
|
|
897
|
+
retriedWithoutToken = true;
|
|
898
|
+
currentToken = void 0;
|
|
899
|
+
this.tokenStore.delete(target);
|
|
900
|
+
console.warn(`[wechat] session expired for ${safeId(target)}; retrying without context_token`);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (ret === RATE_LIMIT_ERRCODE || errcode === RATE_LIMIT_ERRCODE) {
|
|
904
|
+
lastError = new Error(`iLink rate limited: ret=${ret} errcode=${errcode}`);
|
|
905
|
+
if (attempt >= SEND_CHUNK_RETRIES) break;
|
|
906
|
+
await sleep2(5e3);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
throw new Error(`iLink sendmessage error: ret=${ret} errcode=${errcode} errmsg=${resp.errmsg || ""}`);
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
} catch (err) {
|
|
913
|
+
lastError = err;
|
|
914
|
+
if (attempt >= SEND_CHUNK_RETRIES) break;
|
|
915
|
+
await sleep2(SEND_CHUNK_RETRY_DELAY_MS * (attempt + 1));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
throw lastError;
|
|
919
|
+
}
|
|
920
|
+
guessMediaType(filePath, mimeType) {
|
|
921
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
922
|
+
const mime = mimeType || mimeFromFilename(filePath);
|
|
923
|
+
if (mime.startsWith("image/")) return MEDIA_IMAGE;
|
|
924
|
+
if (mime.startsWith("video/")) return MEDIA_VIDEO;
|
|
925
|
+
if (ext === ".silk") return MEDIA_VOICE;
|
|
926
|
+
return MEDIA_FILE;
|
|
927
|
+
}
|
|
928
|
+
buildMediaItem(mediaType, params) {
|
|
929
|
+
const media = {
|
|
930
|
+
encrypt_query_param: params.encryptedQueryParam,
|
|
931
|
+
aes_key: params.aesKeyForApi,
|
|
932
|
+
encrypt_type: 1
|
|
933
|
+
};
|
|
934
|
+
if (mediaType === MEDIA_IMAGE) {
|
|
935
|
+
return { type: ITEM_IMAGE, image_item: { media, mid_size: params.ciphertextSize } };
|
|
936
|
+
}
|
|
937
|
+
if (mediaType === MEDIA_VIDEO) {
|
|
938
|
+
return { type: ITEM_VIDEO, video_item: { media, video_size: params.ciphertextSize, video_md5: params.rawfilemd5 } };
|
|
939
|
+
}
|
|
940
|
+
if (mediaType === MEDIA_VOICE) {
|
|
941
|
+
const voiceItem = { media, encode_type: 6, sample_rate: 24e3, bits_per_sample: 16 };
|
|
942
|
+
if (typeof params.voiceDurationSec === "number" && params.voiceDurationSec > 0) {
|
|
943
|
+
voiceItem.playtime = Math.round(params.voiceDurationSec * 1e3);
|
|
944
|
+
}
|
|
945
|
+
return { type: ITEM_VOICE, voice_item: voiceItem };
|
|
946
|
+
}
|
|
947
|
+
return { type: ITEM_FILE, file_item: { media, file_name: params.filename, len: String(params.plaintextSize) } };
|
|
948
|
+
}
|
|
949
|
+
formatMessage(content) {
|
|
950
|
+
return formatForWechat(content);
|
|
951
|
+
}
|
|
952
|
+
splitText(text) {
|
|
953
|
+
return splitText(text, MAX_MESSAGE_LENGTH);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
EP_GET_BOT_QR = "ilink/bot/get_bot_qrcode";
|
|
957
|
+
EP_GET_QR_STATUS = "ilink/bot/get_qrcode_status";
|
|
958
|
+
QR_TIMEOUT_MS = 15e3;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
17
962
|
// src/cli-travel.ts
|
|
18
963
|
var cli_travel_exports = {};
|
|
19
964
|
__export(cli_travel_exports, {
|
|
@@ -743,127 +1688,8 @@ function sleep(ms) {
|
|
|
743
1688
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
744
1689
|
}
|
|
745
1690
|
|
|
746
|
-
// src/
|
|
747
|
-
|
|
748
|
-
import * as path from "path";
|
|
749
|
-
import * as os from "os";
|
|
750
|
-
function sleep2(ms) {
|
|
751
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
752
|
-
}
|
|
753
|
-
var ILINK_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
754
|
-
var ILINK_APP_CLIENT_VERSION = 2 << 16 | 2 << 8 | 0;
|
|
755
|
-
var EP_GET_BOT_QR = "ilink/bot/get_bot_qrcode";
|
|
756
|
-
var EP_GET_QR_STATUS = "ilink/bot/get_qrcode_status";
|
|
757
|
-
var QR_TIMEOUT_MS = 15e3;
|
|
758
|
-
async function wechatQrLogin(options) {
|
|
759
|
-
const botType = options?.botType || "3";
|
|
760
|
-
const timeoutSeconds = options?.timeoutSeconds || 480;
|
|
761
|
-
const stateDir = options?.stateDir || path.join(os.homedir?.() || "/tmp", ".engine7");
|
|
762
|
-
console.log("[wechat] Fetching QR code from iLink...");
|
|
763
|
-
let qrResp;
|
|
764
|
-
try {
|
|
765
|
-
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
766
|
-
} catch (err) {
|
|
767
|
-
console.error(`[wechat] Failed to fetch QR code: ${err.message}`);
|
|
768
|
-
return null;
|
|
769
|
-
}
|
|
770
|
-
const qrcodeValue = String(qrResp?.qrcode || "");
|
|
771
|
-
const qrcodeUrl = String(qrResp?.qrcode_img_content || "");
|
|
772
|
-
if (!qrcodeValue) {
|
|
773
|
-
console.error("[wechat] QR response missing qrcode field");
|
|
774
|
-
return null;
|
|
775
|
-
}
|
|
776
|
-
const qrScanData = qrcodeUrl || qrcodeValue;
|
|
777
|
-
console.log("\n========== \u5FAE\u4FE1\u626B\u7801\u767B\u5F55 ==========");
|
|
778
|
-
if (qrcodeUrl) {
|
|
779
|
-
console.log(`\u626B\u7801\u94FE\u63A5: ${qrcodeUrl}`);
|
|
780
|
-
}
|
|
781
|
-
console.log("\u8BF7\u7528\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801\uFF08\u6216\u6253\u5F00\u4E0A\u9762\u7684\u94FE\u63A5\uFF09:");
|
|
782
|
-
console.log("===================================\n");
|
|
783
|
-
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
784
|
-
let currentBaseUrl = ILINK_BASE_URL;
|
|
785
|
-
let refreshCount = 0;
|
|
786
|
-
while (Date.now() < deadline) {
|
|
787
|
-
let statusResp;
|
|
788
|
-
try {
|
|
789
|
-
statusResp = await apiGet(currentBaseUrl, `${EP_GET_QR_STATUS}?qrcode=${qrcodeValue}`, "", QR_TIMEOUT_MS);
|
|
790
|
-
} catch {
|
|
791
|
-
await sleep2(1e3);
|
|
792
|
-
continue;
|
|
793
|
-
}
|
|
794
|
-
const status = String(statusResp?.status || "wait");
|
|
795
|
-
if (status === "wait") {
|
|
796
|
-
process.stdout.write(".");
|
|
797
|
-
} else if (status === "scaned") {
|
|
798
|
-
console.log("\n\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u91CC\u786E\u8BA4...");
|
|
799
|
-
} else if (status === "scaned_but_redirect") {
|
|
800
|
-
const redirectHost = String(statusResp?.redirect_host || "");
|
|
801
|
-
if (redirectHost) currentBaseUrl = `https://${redirectHost}`;
|
|
802
|
-
} else if (status === "expired") {
|
|
803
|
-
refreshCount++;
|
|
804
|
-
if (refreshCount > 3) {
|
|
805
|
-
console.log("\n\u4E8C\u7EF4\u7801\u591A\u6B21\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u767B\u5F55\u3002");
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
console.log(`
|
|
809
|
-
\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u5237\u65B0\u4E2D... (${refreshCount}/3)`);
|
|
810
|
-
try {
|
|
811
|
-
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
812
|
-
const newQrValue = String(qrResp?.qrcode || "");
|
|
813
|
-
const newQrUrl = String(qrResp?.qrcode_img_content || "");
|
|
814
|
-
if (newQrUrl) console.log(`\u65B0\u626B\u7801\u94FE\u63A5: ${newQrUrl}`);
|
|
815
|
-
} catch (err) {
|
|
816
|
-
console.error(`[wechat] QR refresh failed: ${err.message}`);
|
|
817
|
-
return null;
|
|
818
|
-
}
|
|
819
|
-
} else if (status === "confirmed") {
|
|
820
|
-
const accountId = String(statusResp?.ilink_bot_id || "");
|
|
821
|
-
const token = String(statusResp?.bot_token || "");
|
|
822
|
-
const baseUrl = String(statusResp?.baseurl || ILINK_BASE_URL);
|
|
823
|
-
const userId = String(statusResp?.ilink_user_id || "");
|
|
824
|
-
if (!accountId || !token) {
|
|
825
|
-
console.error("[wechat] QR confirmed but credential payload incomplete");
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
|
|
829
|
-
const credFile = path.join(stateDir, `weixin-${accountId}.json`);
|
|
830
|
-
fs.writeFileSync(credFile, JSON.stringify({ accountId, token, baseUrl, userId }, null, 2), "utf8");
|
|
831
|
-
console.log(`
|
|
832
|
-
\u2705 \u5FAE\u4FE1\u767B\u5F55\u6210\u529F!`);
|
|
833
|
-
console.log(` accountId: ${accountId}`);
|
|
834
|
-
console.log(` \u51ED\u8BC1\u5DF2\u4FDD\u5B58: ${credFile}`);
|
|
835
|
-
console.log(`
|
|
836
|
-
\u8BF7\u5C06\u4EE5\u4E0B\u914D\u7F6E\u6DFB\u52A0\u5230 xiaoke.json:`);
|
|
837
|
-
console.log(JSON.stringify({
|
|
838
|
-
wechat: {
|
|
839
|
-
token,
|
|
840
|
-
accountId,
|
|
841
|
-
baseUrl: baseUrl !== ILINK_BASE_URL ? baseUrl : void 0
|
|
842
|
-
}
|
|
843
|
-
}, null, 2));
|
|
844
|
-
return { accountId, token, baseUrl, userId };
|
|
845
|
-
}
|
|
846
|
-
await sleep2(1e3);
|
|
847
|
-
}
|
|
848
|
-
console.log("\n[wechat] QR login timed out");
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
async function apiGet(baseUrl, endpoint, _token, timeoutMs) {
|
|
852
|
-
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.replace(/^\//, "")}`;
|
|
853
|
-
const controller = new AbortController();
|
|
854
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
855
|
-
try {
|
|
856
|
-
const resp = await fetch(url, {
|
|
857
|
-
method: "GET",
|
|
858
|
-
signal: controller.signal,
|
|
859
|
-
headers: { "Accept": "application/json" }
|
|
860
|
-
});
|
|
861
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
862
|
-
return await resp.json();
|
|
863
|
-
} finally {
|
|
864
|
-
clearTimeout(timer);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
1691
|
+
// src/cli-init.ts
|
|
1692
|
+
init_wechat();
|
|
867
1693
|
|
|
868
1694
|
// src/qr-render.ts
|
|
869
1695
|
import { createRequire } from "node:module";
|
|
@@ -920,6 +1746,7 @@ Engine 7 \u2014 Self-hosted AI agent engine
|
|
|
920
1746
|
|
|
921
1747
|
\u7528\u6CD5:
|
|
922
1748
|
engine7 init --state-dir <path> [\u9009\u9879] \u521D\u59CB\u5316 agent \u5DE5\u4F5C\u76EE\u5F55
|
|
1749
|
+
engine7 addchannel wechat [\u9009\u9879] \u7ED9\u5DF2\u88C5\u7684 agent \u52A0\u901A\u9053\uFF08\u626B\u7801\uFF0C\u4E0D\u91CD\u8DD1 init\uFF09
|
|
923
1750
|
engine7 start [--config <path>] \u542F\u52A8 Engine
|
|
924
1751
|
engine7 restart [--config <path>] \u91CD\u542F Engine\uFF08\u6740\u65E7\u8FDB\u7A0B+\u542F\u52A8\uFF09
|
|
925
1752
|
engine7 service install|uninstall|status \u5F00\u673A\u81EA\u542F\u52A8\u7BA1\u7406
|
|
@@ -1445,6 +2272,68 @@ async function main() {
|
|
|
1445
2272
|
printHelp();
|
|
1446
2273
|
process.exit(0);
|
|
1447
2274
|
}
|
|
2275
|
+
if (subcommand === "addchannel") {
|
|
2276
|
+
const channel = args[1] || "";
|
|
2277
|
+
let stateDir = "";
|
|
2278
|
+
let configName = "";
|
|
2279
|
+
for (let i = 1; i < args.length; i++) {
|
|
2280
|
+
if (args[i] === "--state-dir" && args[i + 1]) stateDir = args[++i];
|
|
2281
|
+
else if (args[i] === "--config" && args[i + 1]) configName = args[++i];
|
|
2282
|
+
}
|
|
2283
|
+
if (!stateDir) stateDir = path3.resolve(process.cwd());
|
|
2284
|
+
if (channel === "wechat") {
|
|
2285
|
+
console.log("\u{1F4F1} \u5FAE\u4FE1\u626B\u7801\u63A5\u5165\uFF08iLink \u4E2A\u4EBA\u5FAE\u4FE1\uFF0C1v1 \u79C1\u804A\uFF09");
|
|
2286
|
+
console.log(" \u63D0\u793A\uFF1A\u4E00\u4E2A\u5FAE\u4FE1\u53F7\u53EA\u80FD\u7ED1\u4E00\u4E2A bot\uFF1Bbot \u4E0D\u8FDB\u7FA4\uFF08\u817E\u8BAF\u9650\u5236\uFF09\n");
|
|
2287
|
+
const { wechatQrLogin: wechatQrLogin2 } = await Promise.resolve().then(() => (init_wechat(), wechat_exports));
|
|
2288
|
+
const cred = await wechatQrLogin2({ timeoutSeconds: 300, stateDir });
|
|
2289
|
+
if (!cred) {
|
|
2290
|
+
console.error("\u274C \u626B\u7801\u8D85\u65F6\u6216\u5931\u8D25\uFF0C\u672A\u4FEE\u6539\u4EFB\u4F55\u914D\u7F6E");
|
|
2291
|
+
process.exit(1);
|
|
2292
|
+
}
|
|
2293
|
+
const configsDir = path3.join(stateDir, "configs");
|
|
2294
|
+
let configFile = "";
|
|
2295
|
+
if (configName) {
|
|
2296
|
+
configFile = path3.join(configsDir, configName);
|
|
2297
|
+
} else if (fs3.existsSync(configsDir)) {
|
|
2298
|
+
const candidates = fs3.readdirSync(configsDir).filter((f) => f.endsWith(".json"));
|
|
2299
|
+
for (const f of candidates) {
|
|
2300
|
+
const full = path3.join(configsDir, f);
|
|
2301
|
+
try {
|
|
2302
|
+
const j2 = JSON.parse(fs3.readFileSync(full, "utf8"));
|
|
2303
|
+
if (j2.channels) {
|
|
2304
|
+
configFile = full;
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
} catch {
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (!configFile && candidates.length) configFile = path3.join(configsDir, candidates[0]);
|
|
2311
|
+
}
|
|
2312
|
+
if (!configFile || !fs3.existsSync(configFile)) {
|
|
2313
|
+
console.log("\n\u26A0\uFE0F \u6CA1\u627E\u5230 config \u6587\u4EF6\uFF0C\u8BF7\u624B\u52A8\u628A\u4EE5\u4E0B\u6BB5\u52A0\u8FDB\u4F60\u7684 config \u7684 channels \u91CC\uFF1A");
|
|
2314
|
+
console.log(JSON.stringify({ wechat: { enabled: true, token: cred.token, accountId: cred.accountId, dmPolicy: "pairing", group: { policy: "disabled" } } }, null, 2));
|
|
2315
|
+
process.exit(0);
|
|
2316
|
+
}
|
|
2317
|
+
const j = JSON.parse(fs3.readFileSync(configFile, "utf8"));
|
|
2318
|
+
j.channels = j.channels || {};
|
|
2319
|
+
j.channels.wechat = {
|
|
2320
|
+
enabled: true,
|
|
2321
|
+
token: cred.token,
|
|
2322
|
+
accountId: cred.accountId,
|
|
2323
|
+
dmPolicy: "pairing",
|
|
2324
|
+
group: { policy: "disabled" }
|
|
2325
|
+
};
|
|
2326
|
+
fs3.writeFileSync(configFile, JSON.stringify(j, null, 2), "utf8");
|
|
2327
|
+
console.log(`
|
|
2328
|
+
\u2705 \u5FAE\u4FE1\u901A\u9053\u5DF2\u5199\u5165: ${configFile}`);
|
|
2329
|
+
console.log(` accountId: ${cred.accountId}`);
|
|
2330
|
+
console.log("\n\u91CD\u542F engine \u751F\u6548: engine7 restart");
|
|
2331
|
+
process.exit(0);
|
|
2332
|
+
}
|
|
2333
|
+
console.error(`\u672A\u77E5\u901A\u9053: ${channel || "(\u7A7A)"}\u3002\u76EE\u524D\u652F\u6301: wechat`);
|
|
2334
|
+
console.error("\u7528\u6CD5: engine7 addchannel wechat [--state-dir <path>] [--config <file>]");
|
|
2335
|
+
process.exit(1);
|
|
2336
|
+
}
|
|
1448
2337
|
if (subcommand === "export") {
|
|
1449
2338
|
const { doExport: doExport2 } = await Promise.resolve().then(() => (init_cli_travel(), cli_travel_exports));
|
|
1450
2339
|
let exportStateDir = "";
|