engine7 7.1.41 → 7.1.43
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 +1572 -565
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -14,185 +14,1280 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// src/
|
|
18
|
-
var
|
|
19
|
-
__export(
|
|
20
|
-
|
|
21
|
-
doImport: () => doImport,
|
|
22
|
-
loadTravelConfig: () => loadTravelConfig,
|
|
23
|
-
saveTravelConfig: () => saveTravelConfig
|
|
17
|
+
// src/feishu-quick-register.ts
|
|
18
|
+
var feishu_quick_register_exports = {};
|
|
19
|
+
__export(feishu_quick_register_exports, {
|
|
20
|
+
registerFeishuApp: () => registerFeishuApp
|
|
24
21
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
async function postRegistration(domain, body) {
|
|
23
|
+
const url = `${ACCOUNTS_URL[domain]}${REGISTRATION_PATH}`;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
27
|
+
body: new URLSearchParams(body).toString(),
|
|
28
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u63A5\u53E3\u8FD4\u56DE ${res.status}: ${await res.text()}`);
|
|
35
32
|
}
|
|
33
|
+
return res.json();
|
|
36
34
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
35
|
+
async function initRegistration(domain) {
|
|
36
|
+
const res = await postRegistration(domain, { action: "init" });
|
|
37
|
+
if (!res.supported_auth_methods?.includes("client_secret")) {
|
|
38
|
+
throw new Error("\u5F53\u524D\u98DE\u4E66\u73AF\u5883\u4E0D\u652F\u6301 client_secret \u8BA4\u8BC1\uFF0C\u65E0\u6CD5\u81EA\u52A8\u6CE8\u518C");
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
async function beginRegistration(domain) {
|
|
42
|
+
const res = await postRegistration(domain, {
|
|
43
|
+
action: "begin",
|
|
44
|
+
archetype: "PersonalAgent",
|
|
45
|
+
auth_method: "client_secret",
|
|
46
|
+
request_user_info: "open_id"
|
|
47
|
+
});
|
|
48
|
+
if (!res.device_code || !res.verification_uri_complete) {
|
|
49
|
+
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u5931\u8D25: \u672A\u8FD4\u56DE device_code
|
|
50
|
+
${JSON.stringify(res)}`);
|
|
49
51
|
}
|
|
50
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
deviceCode: res.device_code,
|
|
54
|
+
qrUrl: res.verification_uri_complete,
|
|
55
|
+
userCode: res.user_code,
|
|
56
|
+
interval: res.interval ?? DEFAULT_POLL_INTERVAL,
|
|
57
|
+
expireIn: res.expire_in ?? 300
|
|
58
|
+
};
|
|
51
59
|
}
|
|
52
|
-
function
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
60
|
+
async function pollRegistration(domain, deviceCode, interval, deadline, onLog, signal) {
|
|
61
|
+
let currentInterval = interval;
|
|
62
|
+
let currentDomain = domain;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
if (signal?.aborted) return null;
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await postRegistration(currentDomain, {
|
|
68
|
+
action: "poll",
|
|
69
|
+
device_code: deviceCode
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
await sleep(currentInterval * 1e3);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (res.user_info?.tenant_brand === "lark" && currentDomain === "feishu") {
|
|
76
|
+
currentDomain = "lark";
|
|
77
|
+
onLog?.("\u68C0\u6D4B\u5230 Lark \u8D26\u53F7\uFF0C\u5207\u6362\u57DF\u540D...");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (res.client_id && res.client_secret) {
|
|
81
|
+
return {
|
|
82
|
+
appId: res.client_id,
|
|
83
|
+
appSecret: res.client_secret,
|
|
84
|
+
openId: res.user_info?.open_id,
|
|
85
|
+
domain: currentDomain
|
|
86
|
+
};
|
|
58
87
|
}
|
|
88
|
+
if (res.error) {
|
|
89
|
+
if (res.error === "authorization_pending") {
|
|
90
|
+
} else if (res.error === "slow_down") {
|
|
91
|
+
currentInterval += 5;
|
|
92
|
+
} else if (res.error === "access_denied") {
|
|
93
|
+
throw new Error("\u7528\u6237\u62D2\u7EDD\u4E86\u6388\u6743");
|
|
94
|
+
} else if (res.error === "expired_token") {
|
|
95
|
+
throw new Error("\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u9519\u8BEF: ${res.error} \u2014 ${res.error_description ?? ""}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await sleep(currentInterval * 1e3);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
async function registerFeishuApp(options = {}) {
|
|
105
|
+
const domain = options.domain ?? "feishu";
|
|
106
|
+
const timeoutSec = options.timeoutSec ?? 300;
|
|
107
|
+
const log = options.onLog ?? (() => {
|
|
108
|
+
});
|
|
109
|
+
log("\u68C0\u67E5\u98DE\u4E66\u73AF\u5883...");
|
|
110
|
+
await initRegistration(domain);
|
|
111
|
+
log("\u751F\u6210\u4E8C\u7EF4\u7801...");
|
|
112
|
+
const { deviceCode, qrUrl, interval, expireIn } = await beginRegistration(domain);
|
|
113
|
+
options.onQrCode?.(qrUrl);
|
|
114
|
+
log(`\u8BF7\u7528\u98DE\u4E66 App \u626B\u63CF\u4E8C\u7EF4\u7801\uFF08${expireIn}\u79D2\u540E\u8FC7\u671F\uFF09...`);
|
|
115
|
+
const deadline = Date.now() + Math.min(expireIn, timeoutSec) * 1e3;
|
|
116
|
+
const result = await pollRegistration(domain, deviceCode, interval, deadline, log, options.signal);
|
|
117
|
+
if (result) {
|
|
118
|
+
log(`\u2705 \u6CE8\u518C\u6210\u529F\uFF01App ID: ${result.appId}`);
|
|
119
|
+
} else {
|
|
120
|
+
log("\u23F0 \u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
|
|
59
121
|
}
|
|
60
122
|
return result;
|
|
61
123
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
return TEXT_EXTENSIONS.has(ext);
|
|
124
|
+
function sleep(ms) {
|
|
125
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
65
126
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
var ACCOUNTS_URL, REGISTRATION_PATH, REQUEST_TIMEOUT_MS, DEFAULT_POLL_INTERVAL;
|
|
128
|
+
var init_feishu_quick_register = __esm({
|
|
129
|
+
"src/feishu-quick-register.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
ACCOUNTS_URL = {
|
|
132
|
+
feishu: "https://accounts.feishu.cn",
|
|
133
|
+
lark: "https://accounts.larksuite.com"
|
|
134
|
+
};
|
|
135
|
+
REGISTRATION_PATH = "/oauth/v1/app/registration";
|
|
136
|
+
REQUEST_TIMEOUT_MS = 1e4;
|
|
137
|
+
DEFAULT_POLL_INTERVAL = 5;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/channels/wechat.ts
|
|
142
|
+
var wechat_exports = {};
|
|
143
|
+
__export(wechat_exports, {
|
|
144
|
+
WechatAdapter: () => WechatAdapter,
|
|
145
|
+
wechatQrLogin: () => wechatQrLogin
|
|
146
|
+
});
|
|
147
|
+
import * as crypto from "crypto";
|
|
148
|
+
import * as fs from "fs";
|
|
149
|
+
import * as path from "path";
|
|
150
|
+
import * as os from "os";
|
|
151
|
+
import { promises as dns } from "dns";
|
|
152
|
+
function sleep2(ms) {
|
|
153
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
154
|
+
}
|
|
155
|
+
function pkcs7Pad(data, blockSize = 16) {
|
|
156
|
+
const pad = blockSize - data.length % blockSize;
|
|
157
|
+
return Buffer.concat([data, Buffer.alloc(pad, pad)]);
|
|
158
|
+
}
|
|
159
|
+
function pkcs7Unpad(data) {
|
|
160
|
+
if (!data.length) return data;
|
|
161
|
+
const pad = data[data.length - 1];
|
|
162
|
+
if (pad < 1 || pad > 16 || pad > data.length) return data;
|
|
163
|
+
return data.subarray(0, data.length - pad);
|
|
164
|
+
}
|
|
165
|
+
function aesEncrypt(plaintext, key) {
|
|
166
|
+
const c = crypto.createCipheriv("aes-128-ecb", key, null);
|
|
167
|
+
c.setAutoPadding(false);
|
|
168
|
+
return Buffer.concat([c.update(pkcs7Pad(plaintext)), c.final()]);
|
|
169
|
+
}
|
|
170
|
+
function aesDecrypt(ciphertext, key) {
|
|
171
|
+
const d = crypto.createDecipheriv("aes-128-ecb", key, null);
|
|
172
|
+
d.setAutoPadding(false);
|
|
173
|
+
return pkcs7Unpad(Buffer.concat([d.update(ciphertext), d.final()]));
|
|
174
|
+
}
|
|
175
|
+
function parseAesKey(b64) {
|
|
176
|
+
const decoded = Buffer.from(b64, "base64");
|
|
177
|
+
if (decoded.length === 16) return decoded;
|
|
178
|
+
if (decoded.length === 32) {
|
|
179
|
+
const text = decoded.toString("ascii");
|
|
180
|
+
if (/^[0-9a-fA-F]+$/.test(text)) return Buffer.from(text, "hex");
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`unexpected aes_key format (${decoded.length} bytes)`);
|
|
183
|
+
}
|
|
184
|
+
function randomUin() {
|
|
185
|
+
return Buffer.from(String(crypto.randomBytes(4).readUInt32BE(0)), "utf-8").toString("base64");
|
|
186
|
+
}
|
|
187
|
+
async function isNetworkUp(hostname) {
|
|
188
|
+
try {
|
|
189
|
+
await dns.resolve(hostname, "A");
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function buildHeaders(token) {
|
|
196
|
+
return {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"AuthorizationType": "ilink_bot_token",
|
|
199
|
+
"X-WECHAT-UIN": randomUin(),
|
|
200
|
+
"iLink-App-Id": ILINK_APP_ID,
|
|
201
|
+
"iLink-App-ClientVersion": String(ILINK_APP_CLIENT_VERSION),
|
|
202
|
+
"Authorization": `Bearer ${token}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function apiPost(baseUrl, endpoint, payload, token, timeoutMs) {
|
|
206
|
+
const body = JSON.stringify({ ...payload, base_info: { channel_version: CHANNEL_VERSION } });
|
|
207
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint}`;
|
|
208
|
+
const ctrl = new AbortController();
|
|
209
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
210
|
+
try {
|
|
211
|
+
const resp = await fetch(url, { method: "POST", headers: buildHeaders(token), body, signal: ctrl.signal });
|
|
212
|
+
const text = await resp.text();
|
|
213
|
+
if (!resp.ok) throw new Error(`iLink POST ${endpoint} HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
214
|
+
return JSON.parse(text);
|
|
215
|
+
} finally {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertCdnUrl(url) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = new URL(url);
|
|
223
|
+
} catch {
|
|
224
|
+
throw new Error(`Bad media URL: ${url}`);
|
|
225
|
+
}
|
|
226
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error(`Bad scheme: ${parsed.protocol}`);
|
|
227
|
+
if (!CDN_ALLOWLIST.has(parsed.hostname)) throw new Error(`SSRF: host ${parsed.hostname} not in allowlist`);
|
|
228
|
+
}
|
|
229
|
+
function cdnDownloadUrl(base, param) {
|
|
230
|
+
return `${base.replace(/\/$/, "")}/download?encrypted_query_param=${encodeURIComponent(param)}`;
|
|
231
|
+
}
|
|
232
|
+
async function downloadBytes(url, timeoutMs) {
|
|
233
|
+
const ctrl = new AbortController();
|
|
234
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
235
|
+
try {
|
|
236
|
+
const resp = await fetch(url, { signal: ctrl.signal });
|
|
237
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
238
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
239
|
+
} finally {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function downloadAndDecryptMedia(cdnBase, encParam, aesKeyB64, fullUrl, timeoutSec) {
|
|
244
|
+
let raw;
|
|
245
|
+
if (encParam) {
|
|
246
|
+
raw = await downloadBytes(cdnDownloadUrl(cdnBase, encParam), timeoutSec * 1e3);
|
|
247
|
+
} else if (fullUrl) {
|
|
248
|
+
assertCdnUrl(fullUrl);
|
|
249
|
+
raw = await downloadBytes(fullUrl, timeoutSec * 1e3);
|
|
250
|
+
} else {
|
|
251
|
+
throw new Error("media: no encrypt_query_param or full_url");
|
|
252
|
+
}
|
|
253
|
+
if (aesKeyB64) raw = aesDecrypt(raw, parseAesKey(aesKeyB64));
|
|
254
|
+
return raw;
|
|
255
|
+
}
|
|
256
|
+
function cdnUploadUrl(base, uploadParam, filekey) {
|
|
257
|
+
return `${base.replace(/\/$/, "")}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
258
|
+
}
|
|
259
|
+
async function uploadCiphertext(uploadUrl, ciphertext) {
|
|
260
|
+
const ctrl = new AbortController();
|
|
261
|
+
const timer = setTimeout(() => ctrl.abort(), 12e4);
|
|
262
|
+
try {
|
|
263
|
+
console.log(`[wechat:cdn] upload start: url=${uploadUrl.slice(0, 120)}... ciphertextLen=${ciphertext.length}`);
|
|
264
|
+
const resp = await fetch(uploadUrl, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
267
|
+
body: new Uint8Array(ciphertext),
|
|
268
|
+
signal: ctrl.signal
|
|
269
|
+
});
|
|
270
|
+
console.log(`[wechat:cdn] upload resp: status=${resp.status} headers=${JSON.stringify(Object.fromEntries(resp.headers.entries()))}`);
|
|
271
|
+
if (resp.status === 200) {
|
|
272
|
+
const param = resp.headers.get("x-encrypted-param");
|
|
273
|
+
const bodyText = await resp.text();
|
|
274
|
+
if (param) {
|
|
275
|
+
return param;
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`CDN upload missing x-encrypted-param: ${bodyText.slice(0, 200)}`);
|
|
72
278
|
}
|
|
279
|
+
const text = await resp.text();
|
|
280
|
+
throw new Error(`CDN upload HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
281
|
+
} finally {
|
|
282
|
+
clearTimeout(timer);
|
|
73
283
|
}
|
|
284
|
+
}
|
|
285
|
+
function safeId(id, keep = 8) {
|
|
286
|
+
return id.length <= keep ? id : id.slice(0, keep) + "...";
|
|
287
|
+
}
|
|
288
|
+
function isSessionExpired(ret, errcode, errmsg) {
|
|
289
|
+
if (ret === SESSION_EXPIRED_ERRCODE || errcode === SESSION_EXPIRED_ERRCODE) return true;
|
|
290
|
+
if ((ret === RATE_LIMIT_ERRCODE || errcode === RATE_LIMIT_ERRCODE) && (errmsg || "").toLowerCase() === "unknown error") return true;
|
|
74
291
|
return false;
|
|
75
292
|
}
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (lstat.isSymbolicLink()) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
293
|
+
function extractText(itemList) {
|
|
294
|
+
for (const item of itemList) {
|
|
295
|
+
if (item.type === ITEM_TEXT) {
|
|
296
|
+
const text = String(item.text_item?.text || "");
|
|
297
|
+
const ref = item.ref_msg || {};
|
|
298
|
+
const refItem = ref.message_item || {};
|
|
299
|
+
if ([ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE].includes(refItem.type)) {
|
|
300
|
+
const title = ref.title || "";
|
|
301
|
+
return `${title ? `[\u5F15\u7528\u5A92\u4F53: ${title}]
|
|
302
|
+
` : "[\u5F15\u7528\u5A92\u4F53]\n"}${text}`.trim();
|
|
90
303
|
}
|
|
304
|
+
if (refItem && Object.keys(refItem).length > 0) {
|
|
305
|
+
const parts = [];
|
|
306
|
+
if (ref.title) parts.push(String(ref.title));
|
|
307
|
+
const rt = extractText([refItem]);
|
|
308
|
+
if (rt) parts.push(rt);
|
|
309
|
+
if (parts.length) return `[\u5F15\u7528: ${parts.join(" | ")}]
|
|
310
|
+
${text}`.trim();
|
|
311
|
+
}
|
|
312
|
+
return text;
|
|
91
313
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
const
|
|
96
|
-
|
|
314
|
+
}
|
|
315
|
+
for (const item of itemList) {
|
|
316
|
+
if (item.type === ITEM_VOICE) {
|
|
317
|
+
const vt = String(item.voice_item?.text || "");
|
|
318
|
+
if (vt) return vt;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return "";
|
|
322
|
+
}
|
|
323
|
+
function guessChatType(msg, accountId) {
|
|
324
|
+
const roomId = String(msg.room_id || msg.chat_room_id || "").trim();
|
|
325
|
+
const toId = String(msg.to_user_id || "").trim();
|
|
326
|
+
const isGroup = !!roomId || !!toId && !!accountId && toId !== accountId && msg.msg_type === 1;
|
|
327
|
+
return isGroup ? { type: "group", chatId: roomId || toId || String(msg.from_user_id || "") } : { type: "dm", chatId: String(msg.from_user_id || "") };
|
|
328
|
+
}
|
|
329
|
+
function mimeFromFilename(filename) {
|
|
330
|
+
const ext = path.extname(filename).toLowerCase();
|
|
331
|
+
const map = {
|
|
332
|
+
".jpg": "image/jpeg",
|
|
333
|
+
".jpeg": "image/jpeg",
|
|
334
|
+
".png": "image/png",
|
|
335
|
+
".gif": "image/gif",
|
|
336
|
+
".webp": "image/webp",
|
|
337
|
+
".bmp": "image/bmp",
|
|
338
|
+
".mp4": "video/mp4",
|
|
339
|
+
".mov": "video/quicktime",
|
|
340
|
+
".avi": "video/x-msvideo",
|
|
341
|
+
".mp3": "audio/mpeg",
|
|
342
|
+
".wav": "audio/wav",
|
|
343
|
+
".m4a": "audio/mp4",
|
|
344
|
+
".silk": "audio/silk",
|
|
345
|
+
".pdf": "application/pdf",
|
|
346
|
+
".doc": "application/msword",
|
|
347
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
348
|
+
".zip": "application/zip",
|
|
349
|
+
".txt": "text/plain"
|
|
350
|
+
};
|
|
351
|
+
return map[ext] || "application/octet-stream";
|
|
352
|
+
}
|
|
353
|
+
function formatForWechat(content) {
|
|
354
|
+
let lines = content.split("\n");
|
|
355
|
+
lines = lines.map((l) => {
|
|
356
|
+
const m = l.match(/^#{1,4}\s+(.*)/);
|
|
357
|
+
return m ? `\u3010${m[1].trim()}\u3011` : l;
|
|
358
|
+
});
|
|
359
|
+
const result = [];
|
|
360
|
+
for (const line of lines) {
|
|
361
|
+
if (line.trim().startsWith("|") && line.trim().endsWith("|")) {
|
|
362
|
+
if (/^\|[\s:-]+\|$/.test(line.trim())) continue;
|
|
363
|
+
const cells = line.trim().slice(1, -1).split("|").map((c) => c.trim());
|
|
364
|
+
if (cells.every((c) => /^[-:]+$/.test(c))) continue;
|
|
365
|
+
result.push(`\u2022 ${cells.join(" | ")}`);
|
|
97
366
|
} else {
|
|
98
|
-
|
|
367
|
+
result.push(line);
|
|
99
368
|
}
|
|
100
369
|
}
|
|
101
|
-
|
|
370
|
+
const out = [];
|
|
371
|
+
let prevBlank = false;
|
|
372
|
+
for (const l of result) {
|
|
373
|
+
if (!l.trim() && prevBlank) continue;
|
|
374
|
+
out.push(l);
|
|
375
|
+
prevBlank = !l.trim();
|
|
376
|
+
}
|
|
377
|
+
return out.join("\n").trim();
|
|
102
378
|
}
|
|
103
|
-
function
|
|
104
|
-
|
|
379
|
+
function splitText(text, maxLen) {
|
|
380
|
+
if (text.length <= maxLen) return [text];
|
|
381
|
+
const chunks = [];
|
|
382
|
+
let remaining = text;
|
|
383
|
+
while (remaining.length > maxLen) {
|
|
384
|
+
let idx = remaining.lastIndexOf("\n", maxLen);
|
|
385
|
+
if (idx <= 0) idx = remaining.lastIndexOf(" ", maxLen);
|
|
386
|
+
if (idx <= 0) idx = maxLen;
|
|
387
|
+
chunks.push(remaining.slice(0, idx).trim());
|
|
388
|
+
remaining = remaining.slice(idx).trim();
|
|
389
|
+
}
|
|
390
|
+
if (remaining) chunks.push(remaining);
|
|
391
|
+
return chunks.filter((c) => c);
|
|
392
|
+
}
|
|
393
|
+
function loadSyncBuf(stateDir, accountId) {
|
|
105
394
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
if (shouldExclude(entry.name)) continue;
|
|
109
|
-
const fullPath = path2.join(dir, entry.name);
|
|
110
|
-
try {
|
|
111
|
-
if (fs2.lstatSync(fullPath).isSymbolicLink()) continue;
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
if (entry.isDirectory()) {
|
|
115
|
-
files.push(...collectAllFiles(fullPath));
|
|
116
|
-
} else {
|
|
117
|
-
files.push(fullPath);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
395
|
+
const fp = path.join(stateDir, "weixin", `${accountId}.sync.json`);
|
|
396
|
+
if (fs.existsSync(fp)) return String(JSON.parse(fs.readFileSync(fp, "utf-8")).sync_buf || "");
|
|
120
397
|
} catch {
|
|
121
398
|
}
|
|
122
|
-
return
|
|
399
|
+
return "";
|
|
123
400
|
}
|
|
124
|
-
function
|
|
401
|
+
function saveSyncBuf(stateDir, accountId, buf) {
|
|
125
402
|
try {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
const mainPlatformId = platformMap["scope:main"] || platformMap["main"];
|
|
131
|
-
if (!mainPlatformId) return null;
|
|
132
|
-
const indexMap = JSON.parse(fs2.readFileSync(indexMapPath, "utf-8"));
|
|
133
|
-
const entry = indexMap[mainPlatformId];
|
|
134
|
-
if (!entry || !entry.file) return null;
|
|
135
|
-
const basename4 = path2.basename(entry.file).replace(/\.jsonl$/, "");
|
|
136
|
-
return basename4;
|
|
403
|
+
const fp = path.join(stateDir, "weixin", `${accountId}.sync.json`);
|
|
404
|
+
const dir = path.dirname(fp);
|
|
405
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
406
|
+
fs.writeFileSync(fp, JSON.stringify({ sync_buf: buf }));
|
|
137
407
|
} catch {
|
|
138
|
-
return null;
|
|
139
408
|
}
|
|
140
409
|
}
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
410
|
+
async function wechatQrLogin(options) {
|
|
411
|
+
const botType = options?.botType || "3";
|
|
412
|
+
const timeoutSeconds = options?.timeoutSeconds || 480;
|
|
413
|
+
const stateDir = options?.stateDir || path.join(os.homedir?.() || "/tmp", ".engine7");
|
|
414
|
+
console.log("[wechat] Fetching QR code from iLink...");
|
|
415
|
+
let qrResp;
|
|
416
|
+
try {
|
|
417
|
+
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(`[wechat] Failed to fetch QR code: ${err.message}`);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const qrcodeValue = String(qrResp?.qrcode || "");
|
|
423
|
+
const qrcodeUrl = String(qrResp?.qrcode_img_content || "");
|
|
424
|
+
if (!qrcodeValue) {
|
|
425
|
+
console.error("[wechat] QR response missing qrcode field");
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const qrScanData = qrcodeUrl || qrcodeValue;
|
|
429
|
+
console.log("\n========== \u5FAE\u4FE1\u626B\u7801\u767B\u5F55 ==========");
|
|
430
|
+
if (qrcodeUrl) {
|
|
431
|
+
console.log(`\u626B\u7801\u94FE\u63A5: ${qrcodeUrl}`);
|
|
432
|
+
}
|
|
433
|
+
console.log("\u8BF7\u7528\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801\uFF08\u6216\u6253\u5F00\u4E0A\u9762\u7684\u94FE\u63A5\uFF09:");
|
|
434
|
+
console.log("===================================\n");
|
|
435
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
436
|
+
let currentBaseUrl = ILINK_BASE_URL;
|
|
437
|
+
let refreshCount = 0;
|
|
438
|
+
while (Date.now() < deadline) {
|
|
439
|
+
let statusResp;
|
|
146
440
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
441
|
+
statusResp = await apiGet(currentBaseUrl, `${EP_GET_QR_STATUS}?qrcode=${qrcodeValue}`, "", QR_TIMEOUT_MS);
|
|
442
|
+
} catch {
|
|
443
|
+
await sleep2(1e3);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const status = String(statusResp?.status || "wait");
|
|
447
|
+
if (status === "wait") {
|
|
448
|
+
process.stdout.write(".");
|
|
449
|
+
} else if (status === "scaned") {
|
|
450
|
+
console.log("\n\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u91CC\u786E\u8BA4...");
|
|
451
|
+
} else if (status === "scaned_but_redirect") {
|
|
452
|
+
const redirectHost = String(statusResp?.redirect_host || "");
|
|
453
|
+
if (redirectHost) currentBaseUrl = `https://${redirectHost}`;
|
|
454
|
+
} else if (status === "expired") {
|
|
455
|
+
refreshCount++;
|
|
456
|
+
if (refreshCount > 3) {
|
|
457
|
+
console.log("\n\u4E8C\u7EF4\u7801\u591A\u6B21\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u767B\u5F55\u3002");
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
console.log(`
|
|
461
|
+
\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u5237\u65B0\u4E2D... (${refreshCount}/3)`);
|
|
462
|
+
try {
|
|
463
|
+
qrResp = await apiGet(ILINK_BASE_URL, `${EP_GET_BOT_QR}?bot_type=${botType}`, "", QR_TIMEOUT_MS);
|
|
464
|
+
const newQrValue = String(qrResp?.qrcode || "");
|
|
465
|
+
const newQrUrl = String(qrResp?.qrcode_img_content || "");
|
|
466
|
+
if (newQrUrl) console.log(`\u65B0\u626B\u7801\u94FE\u63A5: ${newQrUrl}`);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.error(`[wechat] QR refresh failed: ${err.message}`);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
} else if (status === "confirmed") {
|
|
472
|
+
const accountId = String(statusResp?.ilink_bot_id || "");
|
|
473
|
+
const token = String(statusResp?.bot_token || "");
|
|
474
|
+
const baseUrl = String(statusResp?.baseurl || ILINK_BASE_URL);
|
|
475
|
+
const userId = String(statusResp?.ilink_user_id || "");
|
|
476
|
+
if (!accountId || !token) {
|
|
477
|
+
console.error("[wechat] QR confirmed but credential payload incomplete");
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
|
|
481
|
+
const credFile = path.join(stateDir, `weixin-${accountId}.json`);
|
|
482
|
+
fs.writeFileSync(credFile, JSON.stringify({ accountId, token, baseUrl, userId }, null, 2), "utf8");
|
|
483
|
+
console.log(`
|
|
484
|
+
\u2705 \u5FAE\u4FE1\u767B\u5F55\u6210\u529F!`);
|
|
485
|
+
console.log(` accountId: ${accountId}`);
|
|
486
|
+
console.log(` \u51ED\u8BC1\u5DF2\u4FDD\u5B58: ${credFile}`);
|
|
487
|
+
console.log(`
|
|
488
|
+
\u8BF7\u5C06\u4EE5\u4E0B\u914D\u7F6E\u6DFB\u52A0\u5230 xiaoke.json:`);
|
|
489
|
+
console.log(JSON.stringify({
|
|
490
|
+
wechat: {
|
|
491
|
+
token,
|
|
492
|
+
accountId,
|
|
493
|
+
baseUrl: baseUrl !== ILINK_BASE_URL ? baseUrl : void 0
|
|
153
494
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
495
|
+
}, null, 2));
|
|
496
|
+
return { accountId, token, baseUrl, userId };
|
|
497
|
+
}
|
|
498
|
+
await sleep2(1e3);
|
|
499
|
+
}
|
|
500
|
+
console.log("\n[wechat] QR login timed out");
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
async function apiGet(baseUrl, endpoint, _token, timeoutMs) {
|
|
504
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.replace(/^\//, "")}`;
|
|
505
|
+
const controller = new AbortController();
|
|
506
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
507
|
+
try {
|
|
508
|
+
const resp = await fetch(url, {
|
|
509
|
+
method: "GET",
|
|
510
|
+
signal: controller.signal,
|
|
511
|
+
headers: { "Accept": "application/json" }
|
|
512
|
+
});
|
|
513
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
514
|
+
return await resp.json();
|
|
515
|
+
} finally {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
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;
|
|
520
|
+
var init_wechat = __esm({
|
|
521
|
+
"src/channels/wechat.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
ILINK_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
524
|
+
WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
525
|
+
ILINK_APP_ID = "bot";
|
|
526
|
+
CHANNEL_VERSION = "2.2.0";
|
|
527
|
+
ILINK_APP_CLIENT_VERSION = 2 << 16 | 2 << 8 | 0;
|
|
528
|
+
EP_GET_UPDATES = "ilink/bot/getupdates";
|
|
529
|
+
EP_SEND_MESSAGE = "ilink/bot/sendmessage";
|
|
530
|
+
EP_SEND_TYPING = "ilink/bot/sendtyping";
|
|
531
|
+
EP_GET_CONFIG = "ilink/bot/getconfig";
|
|
532
|
+
EP_GET_UPLOAD_URL = "ilink/bot/getuploadurl";
|
|
533
|
+
LONG_POLL_TIMEOUT_MS = 35e3;
|
|
534
|
+
API_TIMEOUT_MS = 15e3;
|
|
535
|
+
MAX_MESSAGE_LENGTH = 2e3;
|
|
536
|
+
MAX_CONSECUTIVE_FAILURES = 3;
|
|
537
|
+
RETRY_DELAY_MS = 2e3;
|
|
538
|
+
BACKOFF_DELAY_MS = 3e4;
|
|
539
|
+
DISCONNECTED_THRESHOLD = 5;
|
|
540
|
+
DISCONNECTED_POLL_INTERVAL = 5e3;
|
|
541
|
+
SESSION_EXPIRED_ERRCODE = -14;
|
|
542
|
+
RATE_LIMIT_ERRCODE = -2;
|
|
543
|
+
SEND_CHUNK_DELAY_MS = 1500;
|
|
544
|
+
SEND_CHUNK_RETRIES = 4;
|
|
545
|
+
SEND_CHUNK_RETRY_DELAY_MS = 1e3;
|
|
546
|
+
ITEM_TEXT = 1;
|
|
547
|
+
ITEM_IMAGE = 2;
|
|
548
|
+
ITEM_VOICE = 3;
|
|
549
|
+
ITEM_FILE = 4;
|
|
550
|
+
ITEM_VIDEO = 5;
|
|
551
|
+
MSG_TYPE_BOT = 2;
|
|
552
|
+
MSG_STATE_FINISH = 2;
|
|
553
|
+
MEDIA_IMAGE = 1;
|
|
554
|
+
MEDIA_VIDEO = 2;
|
|
555
|
+
MEDIA_FILE = 3;
|
|
556
|
+
MEDIA_VOICE = 4;
|
|
557
|
+
CDN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
558
|
+
"novac2c.cdn.weixin.qq.com",
|
|
559
|
+
"ilinkai.weixin.qq.com",
|
|
560
|
+
"wx.qlogo.cn",
|
|
561
|
+
"thirdwx.qlogo.cn",
|
|
562
|
+
"res.wx.qq.com",
|
|
563
|
+
"mmbiz.qpic.cn",
|
|
564
|
+
"mmbiz.qlogo.cn"
|
|
565
|
+
]);
|
|
566
|
+
ContextTokenStore = class {
|
|
567
|
+
cache = /* @__PURE__ */ new Map();
|
|
568
|
+
filePath;
|
|
569
|
+
constructor(stateDir, accountId) {
|
|
570
|
+
this.filePath = path.join(stateDir, "weixin", `${accountId}.context-tokens.json`);
|
|
571
|
+
}
|
|
572
|
+
k(a, p) {
|
|
573
|
+
return `${a}:${p}`;
|
|
574
|
+
}
|
|
575
|
+
restore() {
|
|
576
|
+
try {
|
|
577
|
+
if (fs.existsSync(this.filePath)) {
|
|
578
|
+
for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(this.filePath, "utf-8"))))
|
|
579
|
+
this.cache.set(k, String(v));
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
157
582
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
583
|
+
}
|
|
584
|
+
save() {
|
|
585
|
+
try {
|
|
586
|
+
const dir = path.dirname(this.filePath);
|
|
587
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
588
|
+
const obj = {};
|
|
589
|
+
this.cache.forEach((v, k) => obj[k] = v);
|
|
590
|
+
fs.writeFileSync(this.filePath, JSON.stringify(obj));
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
get(a, p) {
|
|
595
|
+
return p !== void 0 ? this.cache.get(this.k(a, p)) : this.cache.get(a);
|
|
596
|
+
}
|
|
597
|
+
set(a, pOrT, t) {
|
|
598
|
+
if (t !== void 0) this.cache.set(this.k(a, pOrT), t);
|
|
599
|
+
else this.cache.set(a, pOrT);
|
|
600
|
+
this.save();
|
|
601
|
+
}
|
|
602
|
+
delete(a, p) {
|
|
603
|
+
const key = p !== void 0 ? this.k(a, p) : a;
|
|
604
|
+
this.cache.delete(key);
|
|
605
|
+
this.save();
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
MessageDeduplicator = class {
|
|
609
|
+
ids = /* @__PURE__ */ new Map();
|
|
610
|
+
ttlMs;
|
|
611
|
+
constructor(ttlMs = 3e5) {
|
|
612
|
+
this.ttlMs = ttlMs;
|
|
613
|
+
}
|
|
614
|
+
isDuplicate(id) {
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
for (const [k, ts] of this.ids) {
|
|
617
|
+
if (now - ts > this.ttlMs) this.ids.delete(k);
|
|
618
|
+
}
|
|
619
|
+
if (this.ids.has(id)) return true;
|
|
620
|
+
this.ids.set(id, now);
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
WechatAdapter = class {
|
|
625
|
+
name = "wechat";
|
|
626
|
+
suppressToolDisplay = true;
|
|
627
|
+
// 微信不支持 tool display(rate limit 太严)
|
|
628
|
+
config;
|
|
629
|
+
messageHandler = null;
|
|
630
|
+
tokenStore;
|
|
631
|
+
dedup = new MessageDeduplicator();
|
|
632
|
+
running = false;
|
|
633
|
+
connected = false;
|
|
634
|
+
typingTasks = /* @__PURE__ */ new Map();
|
|
635
|
+
lastSendAt = 0;
|
|
636
|
+
// 全局发送节流
|
|
637
|
+
typingTicketCache = /* @__PURE__ */ new Map();
|
|
638
|
+
// typing ticket 缓存(10min TTL)
|
|
639
|
+
baseUrl;
|
|
640
|
+
cdnBaseUrl;
|
|
641
|
+
stateDir;
|
|
642
|
+
constructor(config) {
|
|
643
|
+
this.config = config;
|
|
644
|
+
this.baseUrl = config.baseUrl?.replace(/\/$/, "") || ILINK_BASE_URL;
|
|
645
|
+
this.cdnBaseUrl = config.cdnBaseUrl?.replace(/\/$/, "") || WEIXIN_CDN_BASE_URL;
|
|
646
|
+
this.stateDir = config.stateDir || path.join(os.homedir?.() || "/tmp", ".engine7");
|
|
647
|
+
if (!fs.existsSync(this.stateDir)) fs.mkdirSync(this.stateDir, { recursive: true });
|
|
648
|
+
}
|
|
649
|
+
// --- ChannelAdapter interface ---
|
|
650
|
+
onMessage(handler) {
|
|
651
|
+
this.messageHandler = handler;
|
|
652
|
+
}
|
|
653
|
+
async connect() {
|
|
654
|
+
if (!this.config.token) throw new Error("WechatAdapter: token is required");
|
|
655
|
+
if (!this.config.accountId) throw new Error("WechatAdapter: accountId is required");
|
|
656
|
+
this.tokenStore = new ContextTokenStore(this.stateDir, this.config.accountId);
|
|
657
|
+
this.tokenStore.restore();
|
|
658
|
+
this.running = true;
|
|
659
|
+
this.connected = true;
|
|
660
|
+
this.pollLoop().catch((err) => {
|
|
661
|
+
console.error(`[wechat] poll loop crashed: ${err.message}`);
|
|
662
|
+
this.connected = false;
|
|
663
|
+
});
|
|
664
|
+
console.log(`[wechat] Connected account=${safeId(this.config.accountId)} base=${this.baseUrl}`);
|
|
665
|
+
if (this.config.groupPolicy && this.config.groupPolicy !== "disabled") {
|
|
666
|
+
console.warn(`[wechat] groupPolicy=${this.config.groupPolicy} \u2014 iLink bot accounts typically cannot join ordinary WeChat groups`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async disconnect() {
|
|
670
|
+
this.running = false;
|
|
671
|
+
this.connected = false;
|
|
672
|
+
console.log(`[wechat] Disconnected`);
|
|
673
|
+
}
|
|
674
|
+
// --- Streaming preview(微信没有编辑API,只在finish时发一次) ---
|
|
675
|
+
async sendPreview(channelId, _content, _agentName) {
|
|
676
|
+
this.previewSent = false;
|
|
677
|
+
return { channelId, messageId: `preview-${Date.now()}` };
|
|
678
|
+
}
|
|
679
|
+
previewSent = false;
|
|
680
|
+
// 防止 freeze() 重复触发发送
|
|
681
|
+
async editPreview(_handle, _content, _agentName, _isFinal) {
|
|
682
|
+
}
|
|
683
|
+
async deletePreview(_handle) {
|
|
684
|
+
}
|
|
685
|
+
async send(target, message, options) {
|
|
686
|
+
const formatted = formatForWechat(message);
|
|
687
|
+
if (!formatted.trim()) return;
|
|
688
|
+
const elapsed = Date.now() - this.lastSendAt;
|
|
689
|
+
if (elapsed < 3e3) await sleep2(3e3 - elapsed);
|
|
690
|
+
const chunks = splitText(formatted, MAX_MESSAGE_LENGTH);
|
|
691
|
+
const contextToken = this.tokenStore.get(this.config.accountId, target);
|
|
692
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
693
|
+
await this.sendTextChunk(target, chunks[i], contextToken);
|
|
694
|
+
this.lastSendAt = Date.now();
|
|
695
|
+
if (i < chunks.length - 1) await sleep2(SEND_CHUNK_DELAY_MS);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async sendFile(target, message, attachment) {
|
|
699
|
+
const filePath = attachment.path;
|
|
700
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
701
|
+
const plaintext = fs.readFileSync(filePath);
|
|
702
|
+
const mediaType = this.guessMediaType(filePath, attachment.mimeType);
|
|
703
|
+
const filekey = crypto.randomBytes(16).toString("hex");
|
|
704
|
+
const aesKey = crypto.randomBytes(16);
|
|
705
|
+
const rawsize = plaintext.length;
|
|
706
|
+
const rawfilemd5 = crypto.createHash("md5").update(plaintext).digest("hex");
|
|
707
|
+
const ciphertext = aesEncrypt(plaintext, aesKey);
|
|
708
|
+
const aeskeyHex = aesKey.toString("hex");
|
|
709
|
+
const uploadResp = await apiPost(this.baseUrl, EP_GET_UPLOAD_URL, {
|
|
710
|
+
to_user_id: target,
|
|
711
|
+
media_type: mediaType,
|
|
712
|
+
filekey,
|
|
713
|
+
rawsize,
|
|
714
|
+
rawfilemd5,
|
|
715
|
+
filesize: ciphertext.length,
|
|
716
|
+
aeskey: aeskeyHex,
|
|
717
|
+
no_need_thumb: true
|
|
718
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
719
|
+
const uploadFullUrl = String(uploadResp.upload_full_url || "");
|
|
720
|
+
const uploadParam = String(uploadResp.upload_param || "");
|
|
721
|
+
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)}`);
|
|
722
|
+
if (!uploadFullUrl && !uploadParam) {
|
|
723
|
+
throw new Error(`getUploadUrl returned neither upload_param nor upload_full_url: ${JSON.stringify(uploadResp).slice(0, 200)}`);
|
|
724
|
+
}
|
|
725
|
+
const uploadUrl = uploadFullUrl || cdnUploadUrl(this.cdnBaseUrl, uploadParam, filekey);
|
|
726
|
+
const encryptedQueryParam = await uploadCiphertext(uploadUrl, ciphertext);
|
|
727
|
+
const aesKeyForApi = Buffer.from(aeskeyHex, "ascii").toString("base64");
|
|
728
|
+
const mediaItem = this.buildMediaItem(mediaType, {
|
|
729
|
+
encryptedQueryParam,
|
|
730
|
+
aesKeyForApi,
|
|
731
|
+
ciphertextSize: ciphertext.length,
|
|
732
|
+
plaintextSize: rawsize,
|
|
733
|
+
filename: path.basename(filePath),
|
|
734
|
+
rawfilemd5,
|
|
735
|
+
voiceDurationSec: attachment.voiceDurationSec
|
|
736
|
+
});
|
|
737
|
+
if (message && message.trim()) {
|
|
738
|
+
const contextToken2 = this.tokenStore.get(target);
|
|
739
|
+
const clientId2 = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
740
|
+
await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
741
|
+
msg: {
|
|
742
|
+
from_user_id: "",
|
|
743
|
+
to_user_id: target,
|
|
744
|
+
client_id: clientId2,
|
|
745
|
+
message_type: MSG_TYPE_BOT,
|
|
746
|
+
message_state: MSG_STATE_FINISH,
|
|
747
|
+
item_list: [{ type: ITEM_TEXT, text_item: { text: this.formatMessage(message) } }],
|
|
748
|
+
...contextToken2 ? { context_token: contextToken2 } : {}
|
|
749
|
+
}
|
|
750
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
751
|
+
}
|
|
752
|
+
const clientId = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
753
|
+
const contextToken = this.tokenStore.get(target);
|
|
754
|
+
await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
755
|
+
msg: {
|
|
756
|
+
from_user_id: "",
|
|
757
|
+
to_user_id: target,
|
|
758
|
+
client_id: clientId,
|
|
759
|
+
message_type: MSG_TYPE_BOT,
|
|
760
|
+
message_state: MSG_STATE_FINISH,
|
|
761
|
+
item_list: [mediaItem],
|
|
762
|
+
...contextToken ? { context_token: contextToken } : {}
|
|
763
|
+
}
|
|
764
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
765
|
+
}
|
|
766
|
+
// --- Typing indicator ---
|
|
767
|
+
getTypingTicket(userId) {
|
|
768
|
+
const entry = this.typingTicketCache.get(userId);
|
|
769
|
+
if (!entry) return void 0;
|
|
770
|
+
if (Date.now() >= entry.expires) {
|
|
771
|
+
this.typingTicketCache.delete(userId);
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
return entry.ticket;
|
|
775
|
+
}
|
|
776
|
+
async fetchTypingTicket(target) {
|
|
777
|
+
if (!this.connected) return void 0;
|
|
778
|
+
try {
|
|
779
|
+
const ctxToken = this.tokenStore.get(this.config.accountId, target);
|
|
780
|
+
const payload = { ilink_user_id: target };
|
|
781
|
+
if (ctxToken) payload.context_token = ctxToken;
|
|
782
|
+
const resp = await apiPost(this.baseUrl, EP_GET_CONFIG, payload, this.config.token, 1e4);
|
|
783
|
+
const ticket = String(resp.typing_ticket || "");
|
|
784
|
+
if (ticket) {
|
|
785
|
+
this.typingTicketCache.set(target, { ticket, expires: Date.now() + 6e5 });
|
|
786
|
+
}
|
|
787
|
+
return ticket || void 0;
|
|
788
|
+
} catch {
|
|
789
|
+
return void 0;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async sendTypingIndicator(target, status) {
|
|
793
|
+
if (!this.connected) return;
|
|
794
|
+
try {
|
|
795
|
+
let ticket = this.getTypingTicket(target);
|
|
796
|
+
if (!ticket) ticket = await this.fetchTypingTicket(target);
|
|
797
|
+
if (!ticket) return;
|
|
798
|
+
await apiPost(this.baseUrl, EP_SEND_TYPING, {
|
|
799
|
+
ilink_user_id: target,
|
|
800
|
+
typing_ticket: ticket,
|
|
801
|
+
status
|
|
802
|
+
}, this.config.token, 1e4);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
startTyping(channelId) {
|
|
807
|
+
if (this.typingTasks.has(channelId)) return;
|
|
808
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
809
|
+
});
|
|
810
|
+
const timer = setInterval(() => {
|
|
811
|
+
const state = this.typingTasks.get(channelId);
|
|
812
|
+
if (!state || state.paused) return;
|
|
813
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
814
|
+
this.stopTyping(channelId);
|
|
815
|
+
});
|
|
816
|
+
}, 8e3);
|
|
817
|
+
this.typingTasks.set(channelId, { timer, paused: false });
|
|
818
|
+
}
|
|
819
|
+
async stopTyping(channelId) {
|
|
820
|
+
const state = this.typingTasks.get(channelId);
|
|
821
|
+
if (!state) return;
|
|
822
|
+
clearInterval(state.timer);
|
|
823
|
+
this.typingTasks.delete(channelId);
|
|
824
|
+
await this.sendTypingIndicator(channelId, 2);
|
|
825
|
+
}
|
|
826
|
+
pauseTyping(channelId) {
|
|
827
|
+
const state = this.typingTasks.get(channelId);
|
|
828
|
+
if (state) state.paused = true;
|
|
829
|
+
}
|
|
830
|
+
resumeTyping(channelId) {
|
|
831
|
+
const state = this.typingTasks.get(channelId);
|
|
832
|
+
if (state) {
|
|
833
|
+
state.paused = false;
|
|
834
|
+
this.sendTypingIndicator(channelId, 1).catch(() => {
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// --- Internal: poll loop ---
|
|
839
|
+
async pollLoop() {
|
|
840
|
+
let syncBuf = loadSyncBuf(this.stateDir, this.config.accountId);
|
|
841
|
+
let timeoutMs = LONG_POLL_TIMEOUT_MS;
|
|
842
|
+
let consecutiveFailures = 0;
|
|
843
|
+
let disconnected = false;
|
|
844
|
+
while (this.running) {
|
|
845
|
+
try {
|
|
846
|
+
const response = await apiPost(this.baseUrl, EP_GET_UPDATES, {
|
|
847
|
+
get_updates_buf: syncBuf
|
|
848
|
+
}, this.config.token, timeoutMs);
|
|
849
|
+
const suggestedTimeout = response.longpolling_timeout_ms;
|
|
850
|
+
if (typeof suggestedTimeout === "number" && suggestedTimeout > 0) timeoutMs = suggestedTimeout;
|
|
851
|
+
const ret = response.ret ?? 0;
|
|
852
|
+
const errcode = response.errcode ?? 0;
|
|
853
|
+
if (ret !== 0 || errcode !== 0) {
|
|
854
|
+
if (isSessionExpired(ret, errcode, response.errmsg)) {
|
|
855
|
+
console.error(`[wechat] Session expired; pausing 10 min`);
|
|
856
|
+
await sleep2(6e5);
|
|
857
|
+
consecutiveFailures = 0;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
consecutiveFailures++;
|
|
861
|
+
if (consecutiveFailures >= DISCONNECTED_THRESHOLD && !disconnected) {
|
|
862
|
+
disconnected = true;
|
|
863
|
+
console.warn(`[wechat] \u26A0\uFE0F network disconnected \u2014 poll failing continuously`);
|
|
864
|
+
}
|
|
865
|
+
console.warn(`[wechat] getUpdates failed ret=${ret} errcode=${errcode} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
|
|
866
|
+
await sleep2(consecutiveFailures >= MAX_CONSECUTIVE_FAILURES ? BACKOFF_DELAY_MS : RETRY_DELAY_MS);
|
|
867
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) consecutiveFailures = 0;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (disconnected) {
|
|
871
|
+
disconnected = false;
|
|
872
|
+
console.log(`[wechat] \u2705 network recovered \u2014 poll resumed successfully`);
|
|
873
|
+
}
|
|
874
|
+
consecutiveFailures = 0;
|
|
875
|
+
const newSyncBuf = String(response.get_updates_buf || "");
|
|
876
|
+
if (newSyncBuf) {
|
|
877
|
+
syncBuf = newSyncBuf;
|
|
878
|
+
saveSyncBuf(this.stateDir, this.config.accountId, syncBuf);
|
|
879
|
+
}
|
|
880
|
+
for (const message of response.msgs || []) {
|
|
881
|
+
this.processMessage(message).catch((err) => {
|
|
882
|
+
console.error(`[wechat] processMessage error from=${safeId(String(message.from_user_id || ""))}: ${err.message}`);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
} catch (err) {
|
|
886
|
+
if (err.name === "AbortError") continue;
|
|
887
|
+
consecutiveFailures++;
|
|
888
|
+
if (consecutiveFailures >= DISCONNECTED_THRESHOLD && !disconnected) {
|
|
889
|
+
disconnected = true;
|
|
890
|
+
console.warn(`[wechat] \u26A0\uFE0F network disconnected \u2014 poll failing continuously`);
|
|
891
|
+
}
|
|
892
|
+
console.error(`[wechat] poll error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
|
|
893
|
+
if (disconnected) {
|
|
894
|
+
const host = new URL(this.baseUrl).hostname;
|
|
895
|
+
while (this.running && !await isNetworkUp(host)) {
|
|
896
|
+
await sleep2(DISCONNECTED_POLL_INTERVAL);
|
|
897
|
+
}
|
|
898
|
+
console.log(`[wechat] DNS probe passed for ${host}, retrying poll...`);
|
|
899
|
+
} else {
|
|
900
|
+
await sleep2(consecutiveFailures >= MAX_CONSECUTIVE_FAILURES ? BACKOFF_DELAY_MS : RETRY_DELAY_MS);
|
|
901
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) consecutiveFailures = 0;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async processMessage(message) {
|
|
907
|
+
const senderId = String(message.from_user_id || "").trim();
|
|
908
|
+
if (!senderId || senderId === this.config.accountId) return;
|
|
909
|
+
const messageId = String(message.message_id || "").trim();
|
|
910
|
+
if (messageId && this.dedup.isDuplicate(messageId)) return;
|
|
911
|
+
const itemList = message.item_list || [];
|
|
912
|
+
const text = extractText(itemList);
|
|
913
|
+
if (text) {
|
|
914
|
+
const contentKey = `content:${senderId}:${crypto.createHash("md5").update(text).digest("hex")}`;
|
|
915
|
+
if (this.dedup.isDuplicate(contentKey)) return;
|
|
916
|
+
}
|
|
917
|
+
const { type: chatType, chatId } = guessChatType(message, this.config.accountId);
|
|
918
|
+
if (chatType === "group") {
|
|
919
|
+
if (this.config.groupPolicy === "disabled") return;
|
|
920
|
+
if (this.config.groupPolicy === "allowlist" && !(this.config.groupAllowFrom || []).includes(chatId)) return;
|
|
161
921
|
} else {
|
|
162
|
-
|
|
163
|
-
if (m) sessionKey = m[1];
|
|
922
|
+
if (!this.isDmAllowed(senderId)) return;
|
|
164
923
|
}
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
924
|
+
const contextToken = String(message.context_token || "").trim();
|
|
925
|
+
if (contextToken) this.tokenStore.set(senderId, contextToken);
|
|
926
|
+
const attachments = [];
|
|
927
|
+
for (const item of itemList) {
|
|
928
|
+
const media = await this.downloadMediaItem(item).catch((err) => {
|
|
929
|
+
console.warn(`[wechat] media download failed: ${err.message}`);
|
|
930
|
+
return null;
|
|
931
|
+
});
|
|
932
|
+
if (media) attachments.push(media);
|
|
168
933
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
934
|
+
if (!text && attachments.length === 0) return;
|
|
935
|
+
console.log(`[wechat] inbound from=${safeId(senderId)} type=${chatType} media=${attachments.length}`);
|
|
936
|
+
this.messageHandler?.({
|
|
937
|
+
content: text,
|
|
938
|
+
from: senderId,
|
|
939
|
+
fromName: senderId,
|
|
940
|
+
channel_id: chatId,
|
|
941
|
+
channel: "wechat",
|
|
942
|
+
channelType: chatType,
|
|
943
|
+
isBot: false,
|
|
944
|
+
messageId: messageId || void 0,
|
|
945
|
+
recvAt: Date.now(),
|
|
946
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
isDmAllowed(senderId) {
|
|
950
|
+
if (this.config.dmPolicy === "disabled") return false;
|
|
951
|
+
if (this.config.dmPolicy === "allowlist") return (this.config.allowFrom || []).includes(senderId);
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
async downloadMediaItem(item) {
|
|
955
|
+
const itemType = item.type;
|
|
956
|
+
let mediaRef = {};
|
|
957
|
+
let filename = "media.bin";
|
|
958
|
+
let contentType = "application/octet-stream";
|
|
959
|
+
let timeoutSec = 60;
|
|
960
|
+
if (itemType === ITEM_IMAGE) {
|
|
961
|
+
mediaRef = (item.image_item || {}).media || {};
|
|
962
|
+
filename = "image.jpg";
|
|
963
|
+
contentType = "image/jpeg";
|
|
964
|
+
timeoutSec = 30;
|
|
965
|
+
} else if (itemType === ITEM_VIDEO) {
|
|
966
|
+
mediaRef = (item.video_item || {}).media || {};
|
|
967
|
+
filename = "video.mp4";
|
|
968
|
+
contentType = "video/mp4";
|
|
969
|
+
timeoutSec = 120;
|
|
970
|
+
} else if (itemType === ITEM_FILE) {
|
|
971
|
+
const fileItem = item.file_item || {};
|
|
972
|
+
mediaRef = fileItem.media || {};
|
|
973
|
+
filename = String(fileItem.file_name || "document.bin");
|
|
974
|
+
contentType = mimeFromFilename(filename);
|
|
975
|
+
timeoutSec = 60;
|
|
976
|
+
} else if (itemType === ITEM_VOICE) {
|
|
977
|
+
mediaRef = (item.voice_item || {}).media || {};
|
|
978
|
+
if ((item.voice_item || {}).text) return null;
|
|
979
|
+
filename = "voice.silk";
|
|
980
|
+
contentType = "audio/silk";
|
|
981
|
+
timeoutSec = 60;
|
|
172
982
|
} else {
|
|
173
|
-
|
|
174
|
-
group.archived.push({ file: fullPath, mtime: stat.mtimeMs });
|
|
983
|
+
return null;
|
|
175
984
|
}
|
|
985
|
+
const data = await downloadAndDecryptMedia(
|
|
986
|
+
this.cdnBaseUrl,
|
|
987
|
+
mediaRef.encrypt_query_param,
|
|
988
|
+
mediaRef.aes_key,
|
|
989
|
+
mediaRef.full_url,
|
|
990
|
+
timeoutSec
|
|
991
|
+
);
|
|
992
|
+
const tmpDir = path.join(os.tmpdir(), "weixin-media");
|
|
993
|
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
994
|
+
const tmpPath = path.join(tmpDir, `${crypto.randomUUID().replace(/-/g, "")}-${filename}`);
|
|
995
|
+
fs.writeFileSync(tmpPath, data);
|
|
996
|
+
return { url: `file://${tmpPath}`, filename, contentType, size: data.length };
|
|
176
997
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
998
|
+
// --- Internal: send helpers ---
|
|
999
|
+
async sendTextChunk(target, text, contextToken) {
|
|
1000
|
+
let lastError = null;
|
|
1001
|
+
let retriedWithoutToken = false;
|
|
1002
|
+
let currentToken = contextToken;
|
|
1003
|
+
for (let attempt = 0; attempt <= SEND_CHUNK_RETRIES; attempt++) {
|
|
1004
|
+
try {
|
|
1005
|
+
const clientId = `engine-weixin-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
1006
|
+
const resp = await apiPost(this.baseUrl, EP_SEND_MESSAGE, {
|
|
1007
|
+
msg: {
|
|
1008
|
+
from_user_id: "",
|
|
1009
|
+
to_user_id: target,
|
|
1010
|
+
client_id: clientId,
|
|
1011
|
+
message_type: MSG_TYPE_BOT,
|
|
1012
|
+
message_state: MSG_STATE_FINISH,
|
|
1013
|
+
item_list: [{ type: ITEM_TEXT, text_item: { text } }],
|
|
1014
|
+
...currentToken ? { context_token: currentToken } : {}
|
|
1015
|
+
}
|
|
1016
|
+
}, this.config.token, API_TIMEOUT_MS);
|
|
1017
|
+
const ret = resp.ret ?? 0;
|
|
1018
|
+
const errcode = resp.errcode ?? 0;
|
|
1019
|
+
if (ret !== 0 || errcode !== 0) {
|
|
1020
|
+
if (isSessionExpired(ret, errcode, resp.errmsg) && !retriedWithoutToken && currentToken) {
|
|
1021
|
+
retriedWithoutToken = true;
|
|
1022
|
+
currentToken = void 0;
|
|
1023
|
+
this.tokenStore.delete(target);
|
|
1024
|
+
console.warn(`[wechat] session expired for ${safeId(target)}; retrying without context_token`);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (ret === RATE_LIMIT_ERRCODE || errcode === RATE_LIMIT_ERRCODE) {
|
|
1028
|
+
lastError = new Error(`iLink rate limited: ret=${ret} errcode=${errcode}`);
|
|
1029
|
+
if (attempt >= SEND_CHUNK_RETRIES) break;
|
|
1030
|
+
await sleep2(5e3);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error(`iLink sendmessage error: ret=${ret} errcode=${errcode} errmsg=${resp.errmsg || ""}`);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
lastError = err;
|
|
1038
|
+
if (attempt >= SEND_CHUNK_RETRIES) break;
|
|
1039
|
+
await sleep2(SEND_CHUNK_RETRY_DELAY_MS * (attempt + 1));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
throw lastError;
|
|
1043
|
+
}
|
|
1044
|
+
guessMediaType(filePath, mimeType) {
|
|
1045
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1046
|
+
const mime = mimeType || mimeFromFilename(filePath);
|
|
1047
|
+
if (mime.startsWith("image/")) return MEDIA_IMAGE;
|
|
1048
|
+
if (mime.startsWith("video/")) return MEDIA_VIDEO;
|
|
1049
|
+
if (ext === ".silk") return MEDIA_VOICE;
|
|
1050
|
+
return MEDIA_FILE;
|
|
1051
|
+
}
|
|
1052
|
+
buildMediaItem(mediaType, params) {
|
|
1053
|
+
const media = {
|
|
1054
|
+
encrypt_query_param: params.encryptedQueryParam,
|
|
1055
|
+
aes_key: params.aesKeyForApi,
|
|
1056
|
+
encrypt_type: 1
|
|
1057
|
+
};
|
|
1058
|
+
if (mediaType === MEDIA_IMAGE) {
|
|
1059
|
+
return { type: ITEM_IMAGE, image_item: { media, mid_size: params.ciphertextSize } };
|
|
1060
|
+
}
|
|
1061
|
+
if (mediaType === MEDIA_VIDEO) {
|
|
1062
|
+
return { type: ITEM_VIDEO, video_item: { media, video_size: params.ciphertextSize, video_md5: params.rawfilemd5 } };
|
|
1063
|
+
}
|
|
1064
|
+
if (mediaType === MEDIA_VOICE) {
|
|
1065
|
+
const voiceItem = { media, encode_type: 6, sample_rate: 24e3, bits_per_sample: 16 };
|
|
1066
|
+
if (typeof params.voiceDurationSec === "number" && params.voiceDurationSec > 0) {
|
|
1067
|
+
voiceItem.playtime = Math.round(params.voiceDurationSec * 1e3);
|
|
1068
|
+
}
|
|
1069
|
+
return { type: ITEM_VOICE, voice_item: voiceItem };
|
|
1070
|
+
}
|
|
1071
|
+
return { type: ITEM_FILE, file_item: { media, file_name: params.filename, len: String(params.plaintextSize) } };
|
|
1072
|
+
}
|
|
1073
|
+
formatMessage(content) {
|
|
1074
|
+
return formatForWechat(content);
|
|
1075
|
+
}
|
|
1076
|
+
splitText(text) {
|
|
1077
|
+
return splitText(text, MAX_MESSAGE_LENGTH);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
EP_GET_BOT_QR = "ilink/bot/get_bot_qrcode";
|
|
1081
|
+
EP_GET_QR_STATUS = "ilink/bot/get_qrcode_status";
|
|
1082
|
+
QR_TIMEOUT_MS = 15e3;
|
|
187
1083
|
}
|
|
188
|
-
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// src/qr-render.ts
|
|
1087
|
+
var qr_render_exports = {};
|
|
1088
|
+
__export(qr_render_exports, {
|
|
1089
|
+
renderQrTerminal: () => renderQrTerminal
|
|
1090
|
+
});
|
|
1091
|
+
import { createRequire } from "node:module";
|
|
1092
|
+
async function renderQrTerminal(url, options) {
|
|
1093
|
+
return new Promise((resolve2, reject) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const qt = require2("qrcode-terminal");
|
|
1096
|
+
qt.generate(url, { small: options?.small ?? true }, (output) => {
|
|
1097
|
+
resolve2(output);
|
|
1098
|
+
});
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
reject(err);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
189
1103
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1104
|
+
var require2;
|
|
1105
|
+
var init_qr_render = __esm({
|
|
1106
|
+
"src/qr-render.ts"() {
|
|
1107
|
+
"use strict";
|
|
1108
|
+
require2 = createRequire(import.meta.url);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// src/cli-travel.ts
|
|
1113
|
+
var cli_travel_exports = {};
|
|
1114
|
+
__export(cli_travel_exports, {
|
|
1115
|
+
doExport: () => doExport,
|
|
1116
|
+
doImport: () => doImport,
|
|
1117
|
+
loadTravelConfig: () => loadTravelConfig,
|
|
1118
|
+
saveTravelConfig: () => saveTravelConfig
|
|
1119
|
+
});
|
|
1120
|
+
import * as path2 from "node:path";
|
|
1121
|
+
import * as fs2 from "node:fs";
|
|
1122
|
+
import * as os2 from "node:os";
|
|
1123
|
+
import { execSync } from "node:child_process";
|
|
1124
|
+
function loadTravelConfig() {
|
|
1125
|
+
try {
|
|
1126
|
+
if (!fs2.existsSync(CONFIG_PATH)) return null;
|
|
1127
|
+
return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf-8"));
|
|
1128
|
+
} catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function saveTravelConfig(cfg) {
|
|
1133
|
+
fs2.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
1134
|
+
}
|
|
1135
|
+
function sanitizeContent(content, dirs) {
|
|
1136
|
+
let result = content;
|
|
1137
|
+
for (const { placeholder, getOriginal } of PATH_PLACEHOLDERS) {
|
|
1138
|
+
const original = getOriginal(dirs);
|
|
1139
|
+
if (original) {
|
|
1140
|
+
result = result.split(original).join(placeholder);
|
|
1141
|
+
result = result.split(original.replace(/\//g, "\\")).join(placeholder);
|
|
1142
|
+
result = result.split(original.replace(/\//g, "\\\\")).join(placeholder);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
function restoreContent(content, dirs) {
|
|
1148
|
+
let result = content;
|
|
1149
|
+
for (const { placeholder, getOriginal } of PATH_PLACEHOLDERS) {
|
|
1150
|
+
const target = getOriginal(dirs);
|
|
1151
|
+
if (target) {
|
|
1152
|
+
result = result.split(placeholder).join(target);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return result;
|
|
1156
|
+
}
|
|
1157
|
+
function isTextFile(filePath) {
|
|
1158
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
1159
|
+
return TEXT_EXTENSIONS.has(ext);
|
|
1160
|
+
}
|
|
1161
|
+
function shouldExclude(name) {
|
|
1162
|
+
for (const pattern of WORKSPACE_EXCLUDE) {
|
|
1163
|
+
if (pattern.startsWith("*")) {
|
|
1164
|
+
if (name.includes(pattern.slice(1).replace("*", ""))) return true;
|
|
1165
|
+
} else if (name === pattern) {
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
function collectFiles(rootDir, includeSet, optionalSet) {
|
|
1172
|
+
const files = [];
|
|
1173
|
+
const entries = fs2.readdirSync(rootDir, { withFileTypes: true });
|
|
1174
|
+
for (const entry of entries) {
|
|
1175
|
+
if (shouldExclude(entry.name)) continue;
|
|
1176
|
+
const fullPath = path2.join(rootDir, entry.name);
|
|
1177
|
+
const realPath = fs2.realpathSync(fullPath);
|
|
1178
|
+
if (realPath !== fullPath) {
|
|
1179
|
+
try {
|
|
1180
|
+
const lstat = fs2.lstatSync(fullPath);
|
|
1181
|
+
if (lstat.isSymbolicLink()) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
} catch {
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const isInclude = includeSet.has(entry.name) || optionalSet.has(entry.name);
|
|
1188
|
+
if (!isInclude) continue;
|
|
1189
|
+
if (entry.isDirectory()) {
|
|
1190
|
+
const subFiles = collectAllFiles(fullPath);
|
|
1191
|
+
files.push(...subFiles);
|
|
1192
|
+
} else {
|
|
1193
|
+
files.push(fullPath);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return files;
|
|
1197
|
+
}
|
|
1198
|
+
function collectAllFiles(dir) {
|
|
1199
|
+
const files = [];
|
|
1200
|
+
try {
|
|
1201
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1202
|
+
for (const entry of entries) {
|
|
1203
|
+
if (shouldExclude(entry.name)) continue;
|
|
1204
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1205
|
+
try {
|
|
1206
|
+
if (fs2.lstatSync(fullPath).isSymbolicLink()) continue;
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
if (entry.isDirectory()) {
|
|
1210
|
+
files.push(...collectAllFiles(fullPath));
|
|
1211
|
+
} else {
|
|
1212
|
+
files.push(fullPath);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
return files;
|
|
1218
|
+
}
|
|
1219
|
+
function readMainEngineUuid(agentsDir) {
|
|
1220
|
+
try {
|
|
1221
|
+
const platformMapPath = path2.join(agentsDir, "main", "sessions", "platform-map.json");
|
|
1222
|
+
const indexMapPath = path2.join(agentsDir, "main", "sessions", "session-index.json");
|
|
1223
|
+
if (!fs2.existsSync(platformMapPath) || !fs2.existsSync(indexMapPath)) return null;
|
|
1224
|
+
const platformMap = JSON.parse(fs2.readFileSync(platformMapPath, "utf-8"));
|
|
1225
|
+
const mainPlatformId = platformMap["scope:main"] || platformMap["main"];
|
|
1226
|
+
if (!mainPlatformId) return null;
|
|
1227
|
+
const indexMap = JSON.parse(fs2.readFileSync(indexMapPath, "utf-8"));
|
|
1228
|
+
const entry = indexMap[mainPlatformId];
|
|
1229
|
+
if (!entry || !entry.file) return null;
|
|
1230
|
+
const basename4 = path2.basename(entry.file).replace(/\.jsonl$/, "");
|
|
1231
|
+
return basename4;
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function collectRecentSessions(agentsDir, _days = 7) {
|
|
1237
|
+
const files = [];
|
|
1238
|
+
if (!fs2.existsSync(agentsDir)) return files;
|
|
1239
|
+
const sessionGroups = /* @__PURE__ */ new Map();
|
|
1240
|
+
function scanSessionsDir(dir) {
|
|
1241
|
+
try {
|
|
1242
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1243
|
+
for (const entry of entries) {
|
|
1244
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1245
|
+
if (entry.isDirectory()) {
|
|
1246
|
+
scanSessionsDir(fullPath);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (entry.name === "platform-map.json" || entry.name === "session-index.json") {
|
|
1250
|
+
files.push(fullPath);
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
let sessionKey = null;
|
|
1254
|
+
if (entry.name.endsWith(".jsonl")) {
|
|
1255
|
+
sessionKey = entry.name.slice(0, -6);
|
|
1256
|
+
} else {
|
|
1257
|
+
const m = entry.name.match(/^(.+)\.jsonl\.(archived|compaction)\./);
|
|
1258
|
+
if (m) sessionKey = m[1];
|
|
1259
|
+
}
|
|
1260
|
+
if (!sessionKey) continue;
|
|
1261
|
+
if (!sessionGroups.has(sessionKey)) {
|
|
1262
|
+
sessionGroups.set(sessionKey, { archived: [] });
|
|
1263
|
+
}
|
|
1264
|
+
const group = sessionGroups.get(sessionKey);
|
|
1265
|
+
if (entry.name.endsWith(".jsonl")) {
|
|
1266
|
+
group.jsonl = fullPath;
|
|
1267
|
+
} else {
|
|
1268
|
+
const stat = fs2.statSync(fullPath);
|
|
1269
|
+
group.archived.push({ file: fullPath, mtime: stat.mtimeMs });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
scanSessionsDir(agentsDir);
|
|
1276
|
+
const mainEngineUuid = readMainEngineUuid(agentsDir);
|
|
1277
|
+
for (const [sessionKey, group] of sessionGroups) {
|
|
1278
|
+
if (!mainEngineUuid || !sessionKey.startsWith(mainEngineUuid)) continue;
|
|
1279
|
+
if (group.jsonl) files.push(group.jsonl);
|
|
1280
|
+
group.archived.sort((a, b) => b.mtime - a.mtime);
|
|
1281
|
+
if (group.archived.length > 0) files.push(group.archived[0].file);
|
|
1282
|
+
}
|
|
1283
|
+
return files;
|
|
1284
|
+
}
|
|
1285
|
+
async function doExport(opts) {
|
|
1286
|
+
const { agentName, stateDir, note, dryRun: dryRun2 } = opts;
|
|
1287
|
+
const workspace = (opts.workspace || path2.join(stateDir, "workspace")).replace(/[/\\]+$/, "");
|
|
1288
|
+
const engineHome = path2.join(os2.homedir(), ".engine7").replace(/[/\\]+$/, "");
|
|
1289
|
+
console.log(`\u{1F4E6} engine7 export`);
|
|
1290
|
+
console.log(` agent: ${agentName}`);
|
|
196
1291
|
console.log(` state: ${stateDir}`);
|
|
197
1292
|
console.log(` workspace: ${workspace}`);
|
|
198
1293
|
if (!fs2.existsSync(workspace)) {
|
|
@@ -446,442 +1541,191 @@ async function uploadToGitHub(cfg, archiveFile, agentName, version, manifest) {
|
|
|
446
1541
|
const tag = `${agentName}-${version}`;
|
|
447
1542
|
console.log(`
|
|
448
1543
|
\u{1F4E4} \u4E0A\u4F20\u5230 GitHub: ${repo} release ${tag}`);
|
|
449
|
-
try {
|
|
450
|
-
const releaseBody = Object.entries(manifest).map(([k, v]) => `- **${k}**: ${typeof v === "object" ? JSON.stringify(v) : v}`).join("\n");
|
|
451
|
-
const createRes = await fetch(`https://api.github.com/repos/${repo}/releases`, {
|
|
452
|
-
method: "POST",
|
|
453
|
-
headers: {
|
|
454
|
-
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
455
|
-
"Accept": "application/vnd.github+json",
|
|
456
|
-
"Content-Type": "application/json"
|
|
457
|
-
},
|
|
458
|
-
body: JSON.stringify({
|
|
459
|
-
tag_name: tag,
|
|
460
|
-
name: `${agentName} ${version}`,
|
|
461
|
-
body: releaseBody,
|
|
462
|
-
prerelease: false,
|
|
463
|
-
make_latest: "true"
|
|
464
|
-
})
|
|
465
|
-
});
|
|
466
|
-
if (!createRes.ok) {
|
|
467
|
-
const err = await createRes.text();
|
|
468
|
-
throw new Error(`\u521B\u5EFA release \u5931\u8D25: ${createRes.status} ${err}`);
|
|
469
|
-
}
|
|
470
|
-
const release = await createRes.json();
|
|
471
|
-
const uploadUrl = release.upload_url.replace("{?name,label}", "");
|
|
472
|
-
const fileBuffer = fs2.readFileSync(archiveFile);
|
|
473
|
-
const fileName = path2.basename(archiveFile);
|
|
474
|
-
const uploadRes = await fetch(`${uploadUrl}?name=${fileName}`, {
|
|
475
|
-
method: "POST",
|
|
476
|
-
headers: {
|
|
477
|
-
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
478
|
-
"Accept": "application/vnd.github+json",
|
|
479
|
-
"Content-Type": "application/gzip",
|
|
480
|
-
"Content-Length": String(fileBuffer.length)
|
|
481
|
-
},
|
|
482
|
-
body: fileBuffer
|
|
483
|
-
});
|
|
484
|
-
if (!uploadRes.ok) {
|
|
485
|
-
const err = await uploadRes.text();
|
|
486
|
-
throw new Error(`\u4E0A\u4F20 asset \u5931\u8D25: ${uploadRes.status} ${err}`);
|
|
487
|
-
}
|
|
488
|
-
console.log(`\u2705 \u4E0A\u4F20\u6210\u529F!`);
|
|
489
|
-
console.log(` release: ${release.html_url}`);
|
|
490
|
-
} catch (e) {
|
|
491
|
-
console.error(`\u274C GitHub \u4E0A\u4F20\u5931\u8D25: ${e.message}`);
|
|
492
|
-
console.error(` archive \u4ECD\u5728: ${archiveFile}`);
|
|
493
|
-
throw e;
|
|
494
|
-
}
|
|
495
|
-
try {
|
|
496
|
-
fs2.rmSync(path2.dirname(archiveFile), { recursive: true, force: true });
|
|
497
|
-
} catch {
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async function downloadFromGitHub(cfg, agentName, version, destDir) {
|
|
501
|
-
const repo = `${cfg.githubOwner}/${cfg.repoName}`;
|
|
502
|
-
const tag = version || await getLatestReleaseTag(cfg, agentName);
|
|
503
|
-
console.log(`\u2B07\uFE0F \u4E0B\u8F7D: ${repo} release ${tag}`);
|
|
504
|
-
const res = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, {
|
|
505
|
-
headers: {
|
|
506
|
-
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
507
|
-
"Accept": "application/vnd.github+json"
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
if (!res.ok) {
|
|
511
|
-
throw new Error(`\u83B7\u53D6 release \u5931\u8D25: ${res.status}`);
|
|
512
|
-
}
|
|
513
|
-
const release = await res.json();
|
|
514
|
-
const asset = release.assets?.find((a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".zip"));
|
|
515
|
-
if (!asset) {
|
|
516
|
-
throw new Error(`release ${tag} \u6CA1\u6709 tar.gz/zip asset`);
|
|
517
|
-
}
|
|
518
|
-
const downloadRes = await fetch(asset.url, {
|
|
519
|
-
headers: {
|
|
520
|
-
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
521
|
-
"Accept": "application/octet-stream"
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
if (!downloadRes.ok) {
|
|
525
|
-
throw new Error(`\u4E0B\u8F7D asset \u5931\u8D25: ${downloadRes.status}`);
|
|
526
|
-
}
|
|
527
|
-
const buffer = Buffer.from(await downloadRes.arrayBuffer());
|
|
528
|
-
const archiveFile = path2.join(destDir, asset.name);
|
|
529
|
-
fs2.writeFileSync(archiveFile, buffer);
|
|
530
|
-
return archiveFile;
|
|
531
|
-
}
|
|
532
|
-
async function getLatestReleaseTag(cfg, agentName) {
|
|
533
|
-
const repo = `${cfg.githubOwner}/${cfg.repoName}`;
|
|
534
|
-
const res = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, {
|
|
535
|
-
headers: {
|
|
536
|
-
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
537
|
-
"Accept": "application/vnd.github+json"
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
if (!res.ok) {
|
|
541
|
-
throw new Error(`\u83B7\u53D6 release \u5217\u8868\u5931\u8D25: ${res.status}`);
|
|
542
|
-
}
|
|
543
|
-
const releases = await res.json();
|
|
544
|
-
const match = releases.find((r) => r.tag_name?.startsWith(`${agentName}-`));
|
|
545
|
-
if (!match) {
|
|
546
|
-
throw new Error(`\u6CA1\u6709\u627E\u5230 ${agentName} \u7684 release`);
|
|
547
|
-
}
|
|
548
|
-
return match.tag_name;
|
|
549
|
-
}
|
|
550
|
-
var CONFIG_PATH, PATH_PLACEHOLDERS, WORKSPACE_INCLUDE, WORKSPACE_OPTIONAL, WORKSPACE_EXCLUDE, TEXT_EXTENSIONS;
|
|
551
|
-
var init_cli_travel = __esm({
|
|
552
|
-
"src/cli-travel.ts"() {
|
|
553
|
-
"use strict";
|
|
554
|
-
CONFIG_PATH = path2.join(os2.homedir(), ".engine7-travel.json");
|
|
555
|
-
PATH_PLACEHOLDERS = [
|
|
556
|
-
{ placeholder: "{{WORKSPACE}}", getOriginal: (d) => d.workspace },
|
|
557
|
-
{ placeholder: "{{ENGINE_HOME}}", getOriginal: (d) => d.engineHome },
|
|
558
|
-
{ placeholder: "{{STATE_DIR}}", getOriginal: (d) => d.stateDir }
|
|
559
|
-
];
|
|
560
|
-
WORKSPACE_INCLUDE = /* @__PURE__ */ new Set([
|
|
561
|
-
// 核心文件
|
|
562
|
-
"AGENTS.md",
|
|
563
|
-
"SOUL.md",
|
|
564
|
-
"MEMORY.md",
|
|
565
|
-
"USER.md",
|
|
566
|
-
"HEARTBEAT.md",
|
|
567
|
-
"INDEX.md",
|
|
568
|
-
"SESSION-STATE.md",
|
|
569
|
-
// 核心目录
|
|
570
|
-
"prompts",
|
|
571
|
-
"topics",
|
|
572
|
-
"memory",
|
|
573
|
-
"inner-voice",
|
|
574
|
-
"docs",
|
|
575
|
-
"skills",
|
|
576
|
-
"voice-chat",
|
|
577
|
-
"scripts",
|
|
578
|
-
"selfie",
|
|
579
|
-
"moodboard",
|
|
580
|
-
// 状态文件 + 目录
|
|
581
|
-
".calendar",
|
|
582
|
-
"nudge-state.json"
|
|
583
|
-
]);
|
|
584
|
-
WORKSPACE_OPTIONAL = /* @__PURE__ */ new Set([
|
|
585
|
-
"images",
|
|
586
|
-
"tools"
|
|
587
|
-
]);
|
|
588
|
-
WORKSPACE_EXCLUDE = /* @__PURE__ */ new Set([
|
|
589
|
-
"livestream",
|
|
590
|
-
"content-library",
|
|
591
|
-
"tmp",
|
|
592
|
-
".git",
|
|
593
|
-
"node_modules",
|
|
594
|
-
"memory_runs",
|
|
595
|
-
"workspace",
|
|
596
|
-
"prompt-archive",
|
|
597
|
-
"aim-archive",
|
|
598
|
-
"test-agent",
|
|
599
|
-
"nul",
|
|
600
|
-
"*.bak*",
|
|
601
|
-
"*.bak-*",
|
|
602
|
-
"~$*"
|
|
603
|
-
]);
|
|
604
|
-
TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
605
|
-
".md",
|
|
606
|
-
".json",
|
|
607
|
-
".txt",
|
|
608
|
-
".js",
|
|
609
|
-
".ts",
|
|
610
|
-
".mjs",
|
|
611
|
-
".py",
|
|
612
|
-
".yaml",
|
|
613
|
-
".yml",
|
|
614
|
-
".cmd",
|
|
615
|
-
".bat",
|
|
616
|
-
".sh",
|
|
617
|
-
".csv",
|
|
618
|
-
".html",
|
|
619
|
-
".toml",
|
|
620
|
-
".list"
|
|
621
|
-
// .everos 的 everos.toml/config.toml/ome.toml/everos-env.list
|
|
622
|
-
]);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// src/cli-init.ts
|
|
627
|
-
import * as path3 from "node:path";
|
|
628
|
-
import * as fs3 from "node:fs";
|
|
629
|
-
import * as readline from "node:readline";
|
|
630
|
-
import { fileURLToPath } from "node:url";
|
|
631
|
-
|
|
632
|
-
// src/feishu-quick-register.ts
|
|
633
|
-
var ACCOUNTS_URL = {
|
|
634
|
-
feishu: "https://accounts.feishu.cn",
|
|
635
|
-
lark: "https://accounts.larksuite.com"
|
|
636
|
-
};
|
|
637
|
-
var REGISTRATION_PATH = "/oauth/v1/app/registration";
|
|
638
|
-
var REQUEST_TIMEOUT_MS = 1e4;
|
|
639
|
-
var DEFAULT_POLL_INTERVAL = 5;
|
|
640
|
-
async function postRegistration(domain, body) {
|
|
641
|
-
const url = `${ACCOUNTS_URL[domain]}${REGISTRATION_PATH}`;
|
|
642
|
-
const res = await fetch(url, {
|
|
643
|
-
method: "POST",
|
|
644
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
645
|
-
body: new URLSearchParams(body).toString(),
|
|
646
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
647
|
-
});
|
|
648
|
-
if (!res.ok) {
|
|
649
|
-
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u63A5\u53E3\u8FD4\u56DE ${res.status}: ${await res.text()}`);
|
|
650
|
-
}
|
|
651
|
-
return res.json();
|
|
652
|
-
}
|
|
653
|
-
async function initRegistration(domain) {
|
|
654
|
-
const res = await postRegistration(domain, { action: "init" });
|
|
655
|
-
if (!res.supported_auth_methods?.includes("client_secret")) {
|
|
656
|
-
throw new Error("\u5F53\u524D\u98DE\u4E66\u73AF\u5883\u4E0D\u652F\u6301 client_secret \u8BA4\u8BC1\uFF0C\u65E0\u6CD5\u81EA\u52A8\u6CE8\u518C");
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
async function beginRegistration(domain) {
|
|
660
|
-
const res = await postRegistration(domain, {
|
|
661
|
-
action: "begin",
|
|
662
|
-
archetype: "PersonalAgent",
|
|
663
|
-
auth_method: "client_secret",
|
|
664
|
-
request_user_info: "open_id"
|
|
665
|
-
});
|
|
666
|
-
if (!res.device_code || !res.verification_uri_complete) {
|
|
667
|
-
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u5931\u8D25: \u672A\u8FD4\u56DE device_code
|
|
668
|
-
${JSON.stringify(res)}`);
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
deviceCode: res.device_code,
|
|
672
|
-
qrUrl: res.verification_uri_complete,
|
|
673
|
-
userCode: res.user_code,
|
|
674
|
-
interval: res.interval ?? DEFAULT_POLL_INTERVAL,
|
|
675
|
-
expireIn: res.expire_in ?? 300
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
async function pollRegistration(domain, deviceCode, interval, deadline, onLog, signal) {
|
|
679
|
-
let currentInterval = interval;
|
|
680
|
-
let currentDomain = domain;
|
|
681
|
-
while (Date.now() < deadline) {
|
|
682
|
-
if (signal?.aborted) return null;
|
|
683
|
-
let res;
|
|
684
|
-
try {
|
|
685
|
-
res = await postRegistration(currentDomain, {
|
|
686
|
-
action: "poll",
|
|
687
|
-
device_code: deviceCode
|
|
688
|
-
});
|
|
689
|
-
} catch {
|
|
690
|
-
await sleep(currentInterval * 1e3);
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (res.user_info?.tenant_brand === "lark" && currentDomain === "feishu") {
|
|
694
|
-
currentDomain = "lark";
|
|
695
|
-
onLog?.("\u68C0\u6D4B\u5230 Lark \u8D26\u53F7\uFF0C\u5207\u6362\u57DF\u540D...");
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (res.client_id && res.client_secret) {
|
|
699
|
-
return {
|
|
700
|
-
appId: res.client_id,
|
|
701
|
-
appSecret: res.client_secret,
|
|
702
|
-
openId: res.user_info?.open_id,
|
|
703
|
-
domain: currentDomain
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
if (res.error) {
|
|
707
|
-
if (res.error === "authorization_pending") {
|
|
708
|
-
} else if (res.error === "slow_down") {
|
|
709
|
-
currentInterval += 5;
|
|
710
|
-
} else if (res.error === "access_denied") {
|
|
711
|
-
throw new Error("\u7528\u6237\u62D2\u7EDD\u4E86\u6388\u6743");
|
|
712
|
-
} else if (res.error === "expired_token") {
|
|
713
|
-
throw new Error("\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
|
|
714
|
-
} else {
|
|
715
|
-
throw new Error(`\u98DE\u4E66\u6CE8\u518C\u9519\u8BEF: ${res.error} \u2014 ${res.error_description ?? ""}`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
await sleep(currentInterval * 1e3);
|
|
719
|
-
}
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
async function registerFeishuApp(options = {}) {
|
|
723
|
-
const domain = options.domain ?? "feishu";
|
|
724
|
-
const timeoutSec = options.timeoutSec ?? 300;
|
|
725
|
-
const log = options.onLog ?? (() => {
|
|
726
|
-
});
|
|
727
|
-
log("\u68C0\u67E5\u98DE\u4E66\u73AF\u5883...");
|
|
728
|
-
await initRegistration(domain);
|
|
729
|
-
log("\u751F\u6210\u4E8C\u7EF4\u7801...");
|
|
730
|
-
const { deviceCode, qrUrl, interval, expireIn } = await beginRegistration(domain);
|
|
731
|
-
options.onQrCode?.(qrUrl);
|
|
732
|
-
log(`\u8BF7\u7528\u98DE\u4E66 App \u626B\u63CF\u4E8C\u7EF4\u7801\uFF08${expireIn}\u79D2\u540E\u8FC7\u671F\uFF09...`);
|
|
733
|
-
const deadline = Date.now() + Math.min(expireIn, timeoutSec) * 1e3;
|
|
734
|
-
const result = await pollRegistration(domain, deviceCode, interval, deadline, log, options.signal);
|
|
735
|
-
if (result) {
|
|
736
|
-
log(`\u2705 \u6CE8\u518C\u6210\u529F\uFF01App ID: ${result.appId}`);
|
|
737
|
-
} else {
|
|
738
|
-
log("\u23F0 \u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F");
|
|
739
|
-
}
|
|
740
|
-
return result;
|
|
741
|
-
}
|
|
742
|
-
function sleep(ms) {
|
|
743
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// src/channels/wechat.ts
|
|
747
|
-
import * as fs from "fs";
|
|
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 };
|
|
1544
|
+
try {
|
|
1545
|
+
const releaseBody = Object.entries(manifest).map(([k, v]) => `- **${k}**: ${typeof v === "object" ? JSON.stringify(v) : v}`).join("\n");
|
|
1546
|
+
const createRes = await fetch(`https://api.github.com/repos/${repo}/releases`, {
|
|
1547
|
+
method: "POST",
|
|
1548
|
+
headers: {
|
|
1549
|
+
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
1550
|
+
"Accept": "application/vnd.github+json",
|
|
1551
|
+
"Content-Type": "application/json"
|
|
1552
|
+
},
|
|
1553
|
+
body: JSON.stringify({
|
|
1554
|
+
tag_name: tag,
|
|
1555
|
+
name: `${agentName} ${version}`,
|
|
1556
|
+
body: releaseBody,
|
|
1557
|
+
prerelease: false,
|
|
1558
|
+
make_latest: "true"
|
|
1559
|
+
})
|
|
1560
|
+
});
|
|
1561
|
+
if (!createRes.ok) {
|
|
1562
|
+
const err = await createRes.text();
|
|
1563
|
+
throw new Error(`\u521B\u5EFA release \u5931\u8D25: ${createRes.status} ${err}`);
|
|
845
1564
|
}
|
|
846
|
-
await
|
|
1565
|
+
const release = await createRes.json();
|
|
1566
|
+
const uploadUrl = release.upload_url.replace("{?name,label}", "");
|
|
1567
|
+
const fileBuffer = fs2.readFileSync(archiveFile);
|
|
1568
|
+
const fileName = path2.basename(archiveFile);
|
|
1569
|
+
const uploadRes = await fetch(`${uploadUrl}?name=${fileName}`, {
|
|
1570
|
+
method: "POST",
|
|
1571
|
+
headers: {
|
|
1572
|
+
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
1573
|
+
"Accept": "application/vnd.github+json",
|
|
1574
|
+
"Content-Type": "application/gzip",
|
|
1575
|
+
"Content-Length": String(fileBuffer.length)
|
|
1576
|
+
},
|
|
1577
|
+
body: fileBuffer
|
|
1578
|
+
});
|
|
1579
|
+
if (!uploadRes.ok) {
|
|
1580
|
+
const err = await uploadRes.text();
|
|
1581
|
+
throw new Error(`\u4E0A\u4F20 asset \u5931\u8D25: ${uploadRes.status} ${err}`);
|
|
1582
|
+
}
|
|
1583
|
+
console.log(`\u2705 \u4E0A\u4F20\u6210\u529F!`);
|
|
1584
|
+
console.log(` release: ${release.html_url}`);
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
console.error(`\u274C GitHub \u4E0A\u4F20\u5931\u8D25: ${e.message}`);
|
|
1587
|
+
console.error(` archive \u4ECD\u5728: ${archiveFile}`);
|
|
1588
|
+
throw e;
|
|
847
1589
|
}
|
|
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
1590
|
try {
|
|
856
|
-
|
|
857
|
-
|
|
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);
|
|
1591
|
+
fs2.rmSync(path2.dirname(archiveFile), { recursive: true, force: true });
|
|
1592
|
+
} catch {
|
|
865
1593
|
}
|
|
866
1594
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1595
|
+
async function downloadFromGitHub(cfg, agentName, version, destDir) {
|
|
1596
|
+
const repo = `${cfg.githubOwner}/${cfg.repoName}`;
|
|
1597
|
+
const tag = version || await getLatestReleaseTag(cfg, agentName);
|
|
1598
|
+
console.log(`\u2B07\uFE0F \u4E0B\u8F7D: ${repo} release ${tag}`);
|
|
1599
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, {
|
|
1600
|
+
headers: {
|
|
1601
|
+
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
1602
|
+
"Accept": "application/vnd.github+json"
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
if (!res.ok) {
|
|
1606
|
+
throw new Error(`\u83B7\u53D6 release \u5931\u8D25: ${res.status}`);
|
|
1607
|
+
}
|
|
1608
|
+
const release = await res.json();
|
|
1609
|
+
const asset = release.assets?.find((a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".zip"));
|
|
1610
|
+
if (!asset) {
|
|
1611
|
+
throw new Error(`release ${tag} \u6CA1\u6709 tar.gz/zip asset`);
|
|
1612
|
+
}
|
|
1613
|
+
const downloadRes = await fetch(asset.url, {
|
|
1614
|
+
headers: {
|
|
1615
|
+
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
1616
|
+
"Accept": "application/octet-stream"
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
if (!downloadRes.ok) {
|
|
1620
|
+
throw new Error(`\u4E0B\u8F7D asset \u5931\u8D25: ${downloadRes.status}`);
|
|
1621
|
+
}
|
|
1622
|
+
const buffer = Buffer.from(await downloadRes.arrayBuffer());
|
|
1623
|
+
const archiveFile = path2.join(destDir, asset.name);
|
|
1624
|
+
fs2.writeFileSync(archiveFile, buffer);
|
|
1625
|
+
return archiveFile;
|
|
1626
|
+
}
|
|
1627
|
+
async function getLatestReleaseTag(cfg, agentName) {
|
|
1628
|
+
const repo = `${cfg.githubOwner}/${cfg.repoName}`;
|
|
1629
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, {
|
|
1630
|
+
headers: {
|
|
1631
|
+
"Authorization": `Bearer ${cfg.githubToken}`,
|
|
1632
|
+
"Accept": "application/vnd.github+json"
|
|
880
1633
|
}
|
|
881
1634
|
});
|
|
1635
|
+
if (!res.ok) {
|
|
1636
|
+
throw new Error(`\u83B7\u53D6 release \u5217\u8868\u5931\u8D25: ${res.status}`);
|
|
1637
|
+
}
|
|
1638
|
+
const releases = await res.json();
|
|
1639
|
+
const match = releases.find((r) => r.tag_name?.startsWith(`${agentName}-`));
|
|
1640
|
+
if (!match) {
|
|
1641
|
+
throw new Error(`\u6CA1\u6709\u627E\u5230 ${agentName} \u7684 release`);
|
|
1642
|
+
}
|
|
1643
|
+
return match.tag_name;
|
|
882
1644
|
}
|
|
1645
|
+
var CONFIG_PATH, PATH_PLACEHOLDERS, WORKSPACE_INCLUDE, WORKSPACE_OPTIONAL, WORKSPACE_EXCLUDE, TEXT_EXTENSIONS;
|
|
1646
|
+
var init_cli_travel = __esm({
|
|
1647
|
+
"src/cli-travel.ts"() {
|
|
1648
|
+
"use strict";
|
|
1649
|
+
CONFIG_PATH = path2.join(os2.homedir(), ".engine7-travel.json");
|
|
1650
|
+
PATH_PLACEHOLDERS = [
|
|
1651
|
+
{ placeholder: "{{WORKSPACE}}", getOriginal: (d) => d.workspace },
|
|
1652
|
+
{ placeholder: "{{ENGINE_HOME}}", getOriginal: (d) => d.engineHome },
|
|
1653
|
+
{ placeholder: "{{STATE_DIR}}", getOriginal: (d) => d.stateDir }
|
|
1654
|
+
];
|
|
1655
|
+
WORKSPACE_INCLUDE = /* @__PURE__ */ new Set([
|
|
1656
|
+
// 核心文件
|
|
1657
|
+
"AGENTS.md",
|
|
1658
|
+
"SOUL.md",
|
|
1659
|
+
"MEMORY.md",
|
|
1660
|
+
"USER.md",
|
|
1661
|
+
"HEARTBEAT.md",
|
|
1662
|
+
"INDEX.md",
|
|
1663
|
+
"SESSION-STATE.md",
|
|
1664
|
+
// 核心目录
|
|
1665
|
+
"prompts",
|
|
1666
|
+
"topics",
|
|
1667
|
+
"memory",
|
|
1668
|
+
"inner-voice",
|
|
1669
|
+
"docs",
|
|
1670
|
+
"skills",
|
|
1671
|
+
"voice-chat",
|
|
1672
|
+
"scripts",
|
|
1673
|
+
"selfie",
|
|
1674
|
+
"moodboard",
|
|
1675
|
+
// 状态文件 + 目录
|
|
1676
|
+
".calendar",
|
|
1677
|
+
"nudge-state.json"
|
|
1678
|
+
]);
|
|
1679
|
+
WORKSPACE_OPTIONAL = /* @__PURE__ */ new Set([
|
|
1680
|
+
"images",
|
|
1681
|
+
"tools"
|
|
1682
|
+
]);
|
|
1683
|
+
WORKSPACE_EXCLUDE = /* @__PURE__ */ new Set([
|
|
1684
|
+
"livestream",
|
|
1685
|
+
"content-library",
|
|
1686
|
+
"tmp",
|
|
1687
|
+
".git",
|
|
1688
|
+
"node_modules",
|
|
1689
|
+
"memory_runs",
|
|
1690
|
+
"workspace",
|
|
1691
|
+
"prompt-archive",
|
|
1692
|
+
"aim-archive",
|
|
1693
|
+
"test-agent",
|
|
1694
|
+
"nul",
|
|
1695
|
+
"*.bak*",
|
|
1696
|
+
"*.bak-*",
|
|
1697
|
+
"~$*"
|
|
1698
|
+
]);
|
|
1699
|
+
TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1700
|
+
".md",
|
|
1701
|
+
".json",
|
|
1702
|
+
".txt",
|
|
1703
|
+
".js",
|
|
1704
|
+
".ts",
|
|
1705
|
+
".mjs",
|
|
1706
|
+
".py",
|
|
1707
|
+
".yaml",
|
|
1708
|
+
".yml",
|
|
1709
|
+
".cmd",
|
|
1710
|
+
".bat",
|
|
1711
|
+
".sh",
|
|
1712
|
+
".csv",
|
|
1713
|
+
".html",
|
|
1714
|
+
".toml",
|
|
1715
|
+
".list"
|
|
1716
|
+
// .everos 的 everos.toml/config.toml/ome.toml/everos-env.list
|
|
1717
|
+
]);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
883
1720
|
|
|
884
1721
|
// src/cli-init.ts
|
|
1722
|
+
init_feishu_quick_register();
|
|
1723
|
+
init_wechat();
|
|
1724
|
+
init_qr_render();
|
|
1725
|
+
import * as path3 from "node:path";
|
|
1726
|
+
import * as fs3 from "node:fs";
|
|
1727
|
+
import * as readline from "node:readline";
|
|
1728
|
+
import { fileURLToPath } from "node:url";
|
|
885
1729
|
var __filename = fileURLToPath(import.meta.url);
|
|
886
1730
|
var __dirname = path3.dirname(__filename);
|
|
887
1731
|
var SCHEMA_VERSION = 1;
|
|
@@ -920,6 +1764,7 @@ Engine 7 \u2014 Self-hosted AI agent engine
|
|
|
920
1764
|
|
|
921
1765
|
\u7528\u6CD5:
|
|
922
1766
|
engine7 init --state-dir <path> [\u9009\u9879] \u521D\u59CB\u5316 agent \u5DE5\u4F5C\u76EE\u5F55
|
|
1767
|
+
engine7 reconfig <\u76EE\u6807> [\u9009\u9879] \u91CD\u65B0\u914D\u7F6E\u5DF2\u88C5\u7684 agent\uFF08wechat/feishu/discord/llm/channels\uFF09
|
|
923
1768
|
engine7 start [--config <path>] \u542F\u52A8 Engine
|
|
924
1769
|
engine7 restart [--config <path>] \u91CD\u542F Engine\uFF08\u6740\u65E7\u8FDB\u7A0B+\u542F\u52A8\uFF09
|
|
925
1770
|
engine7 service install|uninstall|status \u5F00\u673A\u81EA\u542F\u52A8\u7BA1\u7406
|
|
@@ -1445,6 +2290,168 @@ async function main() {
|
|
|
1445
2290
|
printHelp();
|
|
1446
2291
|
process.exit(0);
|
|
1447
2292
|
}
|
|
2293
|
+
if (subcommand === "reconfig" || subcommand === "addchannel") {
|
|
2294
|
+
const isLegacyAddchannel = subcommand === "addchannel";
|
|
2295
|
+
let target = args[1] || "";
|
|
2296
|
+
if (isLegacyAddchannel) target = target || "wechat";
|
|
2297
|
+
const channel = target;
|
|
2298
|
+
let stateDir = "";
|
|
2299
|
+
let configName = "";
|
|
2300
|
+
for (let i = 1; i < args.length; i++) {
|
|
2301
|
+
if (args[i] === "--state-dir" && args[i + 1]) stateDir = args[++i];
|
|
2302
|
+
else if (args[i] === "--config" && args[i + 1]) configName = args[++i];
|
|
2303
|
+
}
|
|
2304
|
+
if (!stateDir) stateDir = path3.resolve(process.cwd());
|
|
2305
|
+
if (!target && !isLegacyAddchannel) {
|
|
2306
|
+
console.log("\u{1F527} engine7 reconfig \u2014 \u91CD\u65B0\u914D\u7F6E\u5DF2\u88C5\u7684 agent");
|
|
2307
|
+
console.log(" \u7528\u6CD5: engine7 reconfig <\u76EE\u6807>");
|
|
2308
|
+
console.log("");
|
|
2309
|
+
console.log(" \u901A\u9053\u7C7B:");
|
|
2310
|
+
console.log(" wechat \u5FAE\u4FE1\u626B\u7801\u63A5\u5165\uFF08iLink\uFF0C1v1 \u79C1\u804A\uFF09");
|
|
2311
|
+
console.log(" feishu \u98DE\u4E66\u626B\u7801\u63A5\u5165\uFF08OAuth\uFF0C30\u79D2\uFF09");
|
|
2312
|
+
console.log(" discord Discord bot \u914D\u7F6E");
|
|
2313
|
+
console.log(" \u6A21\u578B\u7C7B:");
|
|
2314
|
+
console.log(" llm \u66F4\u6362 LLM provider / API key / \u7AEF\u70B9");
|
|
2315
|
+
console.log(" \u5176\u4ED6:");
|
|
2316
|
+
console.log(" channels \u67E5\u770B\u5F53\u524D\u901A\u9053\u72B6\u6001");
|
|
2317
|
+
console.log("");
|
|
2318
|
+
console.log(" \u9009\u9879: --state-dir <path> --config <file>");
|
|
2319
|
+
process.exit(0);
|
|
2320
|
+
}
|
|
2321
|
+
if (channel === "channels") {
|
|
2322
|
+
const configsDir = path3.join(stateDir, "configs");
|
|
2323
|
+
if (fs3.existsSync(configsDir)) {
|
|
2324
|
+
const files = fs3.readdirSync(configsDir).filter((f) => f.endsWith(".json"));
|
|
2325
|
+
for (const f of files) {
|
|
2326
|
+
try {
|
|
2327
|
+
const j = JSON.parse(fs3.readFileSync(path3.join(configsDir, f), "utf8"));
|
|
2328
|
+
if (!j.channels) continue;
|
|
2329
|
+
console.log(`
|
|
2330
|
+
\u{1F4C4} ${f}:`);
|
|
2331
|
+
for (const [name, conf] of Object.entries(j.channels)) {
|
|
2332
|
+
const c = conf;
|
|
2333
|
+
console.log(` ${c.enabled ? "\u2705" : "\u26D4"} ${name}${c.appId ? " (" + c.appId + ")" : ""}${c.accountId ? " (" + c.accountId + ")" : ""}`);
|
|
2334
|
+
}
|
|
2335
|
+
} catch {
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
} else {
|
|
2339
|
+
console.log(`\u26A0\uFE0F \u672A\u627E\u5230 configs \u76EE\u5F55: ${configsDir}`);
|
|
2340
|
+
}
|
|
2341
|
+
process.exit(0);
|
|
2342
|
+
}
|
|
2343
|
+
if (channel === "llm") {
|
|
2344
|
+
console.log("\u{1F527} LLM \u91CD\u914D\uFF08\u5F00\u53D1\u4E2D\uFF0Cv7.2 \u8BA1\u5212\uFF09");
|
|
2345
|
+
console.log(" \u76EE\u524D\u8BF7\u624B\u52A8\u6539 config \u7684 models.providers \u6BB5");
|
|
2346
|
+
process.exit(0);
|
|
2347
|
+
}
|
|
2348
|
+
if (channel === "discord") {
|
|
2349
|
+
console.log("\u{1F527} Discord \u91CD\u914D\uFF08\u5F00\u53D1\u4E2D\uFF09");
|
|
2350
|
+
console.log(" \u76EE\u524D\u8BF7\u624B\u52A8\u6539 config \u7684 channels.discord \u6BB5\uFF08token/userId\uFF09");
|
|
2351
|
+
process.exit(0);
|
|
2352
|
+
}
|
|
2353
|
+
if (channel === "feishu") {
|
|
2354
|
+
console.log("\u{1F527} \u98DE\u4E66\u626B\u7801\u63A5\u5165...");
|
|
2355
|
+
const { registerFeishuApp: registerFeishuApp2 } = await Promise.resolve().then(() => (init_feishu_quick_register(), feishu_quick_register_exports));
|
|
2356
|
+
const { renderQrTerminal: renderQrTerminal2 } = await Promise.resolve().then(() => (init_qr_render(), qr_render_exports));
|
|
2357
|
+
const result = await registerFeishuApp2({
|
|
2358
|
+
onLog: (msg) => console.log(` ${msg}`),
|
|
2359
|
+
onQrCode: async (url) => {
|
|
2360
|
+
try {
|
|
2361
|
+
console.log(await renderQrTerminal2(url, { small: true }));
|
|
2362
|
+
} catch {
|
|
2363
|
+
console.log(`
|
|
2364
|
+
\u6D4F\u89C8\u5668\u6253\u5F00\u626B\u7801: ${url}
|
|
2365
|
+
`);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
if (!result) {
|
|
2370
|
+
console.error("\u274C \u626B\u7801\u8D85\u65F6\u6216\u5931\u8D25");
|
|
2371
|
+
process.exit(1);
|
|
2372
|
+
}
|
|
2373
|
+
const configsDir2 = path3.join(stateDir, "configs");
|
|
2374
|
+
let configFile2 = configName ? path3.join(configsDir2, configName) : "";
|
|
2375
|
+
if (!configFile2 || !fs3.existsSync(configFile2)) {
|
|
2376
|
+
if (fs3.existsSync(configsDir2)) {
|
|
2377
|
+
for (const f of fs3.readdirSync(configsDir2).filter((f2) => f2.endsWith(".json"))) {
|
|
2378
|
+
const full = path3.join(configsDir2, f);
|
|
2379
|
+
try {
|
|
2380
|
+
if (JSON.parse(fs3.readFileSync(full, "utf8")).channels) {
|
|
2381
|
+
configFile2 = full;
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
} catch {
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (configFile2 && fs3.existsSync(configFile2)) {
|
|
2390
|
+
const j = JSON.parse(fs3.readFileSync(configFile2, "utf8"));
|
|
2391
|
+
j.channels = j.channels || {};
|
|
2392
|
+
j.channels.feishu = { enabled: true, appId: result.appId, appSecret: result.appSecret, connectionMode: "websocket", dmPolicy: "pairing", groupPolicy: "open" };
|
|
2393
|
+
fs3.writeFileSync(configFile2, JSON.stringify(j, null, 2), "utf8");
|
|
2394
|
+
console.log("\u2705 \u98DE\u4E66\u901A\u9053\u5DF2\u5199\u5165: " + configFile2);
|
|
2395
|
+
console.log("\u91CD\u542F\u751F\u6548: engine7 restart");
|
|
2396
|
+
} else {
|
|
2397
|
+
console.log("\u26A0\uFE0F \u6CA1\u627E\u5230 config\uFF0C\u624B\u52A8\u52A0\uFF1A");
|
|
2398
|
+
console.log(JSON.stringify({ feishu: { enabled: true, appId: result.appId, appSecret: result.appSecret, connectionMode: "websocket", dmPolicy: "pairing", groupPolicy: "open" } }, null, 2));
|
|
2399
|
+
}
|
|
2400
|
+
process.exit(0);
|
|
2401
|
+
}
|
|
2402
|
+
if (channel === "wechat") {
|
|
2403
|
+
console.log("\u{1F4F1} \u5FAE\u4FE1\u626B\u7801\u63A5\u5165\uFF08iLink \u4E2A\u4EBA\u5FAE\u4FE1\uFF0C1v1 \u79C1\u804A\uFF09");
|
|
2404
|
+
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");
|
|
2405
|
+
const { wechatQrLogin: wechatQrLogin2 } = await Promise.resolve().then(() => (init_wechat(), wechat_exports));
|
|
2406
|
+
const cred = await wechatQrLogin2({ timeoutSeconds: 300, stateDir });
|
|
2407
|
+
if (!cred) {
|
|
2408
|
+
console.error("\u274C \u626B\u7801\u8D85\u65F6\u6216\u5931\u8D25\uFF0C\u672A\u4FEE\u6539\u4EFB\u4F55\u914D\u7F6E");
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
const configsDir = path3.join(stateDir, "configs");
|
|
2412
|
+
let configFile = "";
|
|
2413
|
+
if (configName) {
|
|
2414
|
+
configFile = path3.join(configsDir, configName);
|
|
2415
|
+
} else if (fs3.existsSync(configsDir)) {
|
|
2416
|
+
const candidates = fs3.readdirSync(configsDir).filter((f) => f.endsWith(".json"));
|
|
2417
|
+
for (const f of candidates) {
|
|
2418
|
+
const full = path3.join(configsDir, f);
|
|
2419
|
+
try {
|
|
2420
|
+
const j2 = JSON.parse(fs3.readFileSync(full, "utf8"));
|
|
2421
|
+
if (j2.channels) {
|
|
2422
|
+
configFile = full;
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (!configFile && candidates.length) configFile = path3.join(configsDir, candidates[0]);
|
|
2429
|
+
}
|
|
2430
|
+
if (!configFile || !fs3.existsSync(configFile)) {
|
|
2431
|
+
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");
|
|
2432
|
+
console.log(JSON.stringify({ wechat: { enabled: true, token: cred.token, accountId: cred.accountId, dmPolicy: "pairing", group: { policy: "disabled" } } }, null, 2));
|
|
2433
|
+
process.exit(0);
|
|
2434
|
+
}
|
|
2435
|
+
const j = JSON.parse(fs3.readFileSync(configFile, "utf8"));
|
|
2436
|
+
j.channels = j.channels || {};
|
|
2437
|
+
j.channels.wechat = {
|
|
2438
|
+
enabled: true,
|
|
2439
|
+
token: cred.token,
|
|
2440
|
+
accountId: cred.accountId,
|
|
2441
|
+
dmPolicy: "pairing",
|
|
2442
|
+
group: { policy: "disabled" }
|
|
2443
|
+
};
|
|
2444
|
+
fs3.writeFileSync(configFile, JSON.stringify(j, null, 2), "utf8");
|
|
2445
|
+
console.log(`
|
|
2446
|
+
\u2705 \u5FAE\u4FE1\u901A\u9053\u5DF2\u5199\u5165: ${configFile}`);
|
|
2447
|
+
console.log(` accountId: ${cred.accountId}`);
|
|
2448
|
+
console.log("\n\u91CD\u542F engine \u751F\u6548: engine7 restart");
|
|
2449
|
+
process.exit(0);
|
|
2450
|
+
}
|
|
2451
|
+
console.error(`\u672A\u77E5\u76EE\u6807: ${channel || "(\u7A7A)"}\u3002\u652F\u6301: wechat / feishu / discord / llm / channels`);
|
|
2452
|
+
console.error("\u7528\u6CD5: engine7 reconfig <\u76EE\u6807> [--state-dir <path>] [--config <file>]");
|
|
2453
|
+
process.exit(1);
|
|
2454
|
+
}
|
|
1448
2455
|
if (subcommand === "export") {
|
|
1449
2456
|
const { doExport: doExport2 } = await Promise.resolve().then(() => (init_cli_travel(), cli_travel_exports));
|
|
1450
2457
|
let exportStateDir = "";
|