evolclaw 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-specific init flows: Feishu, WeChat, AUN
|
|
3
|
+
*
|
|
4
|
+
* Each channel provides:
|
|
5
|
+
* - cmdInit<Channel>() — standalone `evolclaw init <channel>` entry
|
|
6
|
+
* - run<Channel>QrFlow() or setupAun*() — reusable primitives for the main init wizard
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import readline from 'readline';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import { resolvePaths } from '../paths.js';
|
|
13
|
+
import { normalizeChannelInstances } from '../config.js';
|
|
14
|
+
import { selectInstance } from './init.js';
|
|
15
|
+
function ask(rl, question) {
|
|
16
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
17
|
+
}
|
|
18
|
+
// ==================== Feishu ====================
|
|
19
|
+
const FEISHU_PROD_URL = 'https://accounts.feishu.cn';
|
|
20
|
+
const LARK_PROD_URL = 'https://accounts.larksuite.com';
|
|
21
|
+
const SKIP = Symbol('SKIP');
|
|
22
|
+
const QUIT = Symbol('QUIT');
|
|
23
|
+
class FeishuQrRegistrationClient {
|
|
24
|
+
baseUrl;
|
|
25
|
+
constructor(isLark = false) {
|
|
26
|
+
this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
|
|
27
|
+
}
|
|
28
|
+
setDomain(isLark) {
|
|
29
|
+
this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
|
|
30
|
+
}
|
|
31
|
+
async init() {
|
|
32
|
+
return this.postRegistration('init', {});
|
|
33
|
+
}
|
|
34
|
+
async begin() {
|
|
35
|
+
return this.postRegistration('begin', {
|
|
36
|
+
archetype: 'PersonalAgent',
|
|
37
|
+
auth_method: 'client_secret',
|
|
38
|
+
request_user_info: 'open_id',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async poll(deviceCode) {
|
|
42
|
+
return this.postRegistration('poll', { device_code: deviceCode });
|
|
43
|
+
}
|
|
44
|
+
async postRegistration(action, extraParams) {
|
|
45
|
+
const body = new URLSearchParams({ action, ...extraParams }).toString();
|
|
46
|
+
const res = await fetch(`${this.baseUrl}/oauth/v1/app/registration`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
49
|
+
body,
|
|
50
|
+
});
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
if (!text)
|
|
53
|
+
return {};
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function runQrRegistrationFlow() {
|
|
58
|
+
const client = new FeishuQrRegistrationClient();
|
|
59
|
+
const initResult = await client.init();
|
|
60
|
+
const authMethods = Array.isArray(initResult.supported_auth_methods) ? initResult.supported_auth_methods : [];
|
|
61
|
+
if (!authMethods.includes('client_secret')) {
|
|
62
|
+
throw new Error('当前环境不支持 client_secret 注册');
|
|
63
|
+
}
|
|
64
|
+
const beginResult = await client.begin();
|
|
65
|
+
if (!beginResult.verification_uri_complete || !beginResult.device_code) {
|
|
66
|
+
throw new Error('服务端未返回扫码链接或 device_code');
|
|
67
|
+
}
|
|
68
|
+
// 显示二维码
|
|
69
|
+
try {
|
|
70
|
+
const qrterm = await import('qrcode-terminal');
|
|
71
|
+
await new Promise(resolve => {
|
|
72
|
+
qrterm.default.generate(beginResult.verification_uri_complete, { small: true }, (qr) => {
|
|
73
|
+
console.log(qr);
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
console.log(`请在浏览器中打开此链接扫码: ${beginResult.verification_uri_complete}\n`);
|
|
80
|
+
}
|
|
81
|
+
console.log('请用飞书/Lark 扫描上方二维码...\n');
|
|
82
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret\n');
|
|
83
|
+
let userAction = null;
|
|
84
|
+
const setupKeyListener = () => {
|
|
85
|
+
if (!process.stdin.isTTY)
|
|
86
|
+
return () => { };
|
|
87
|
+
process.stdin.setRawMode(true);
|
|
88
|
+
process.stdin.resume();
|
|
89
|
+
process.stdin.setEncoding('utf8');
|
|
90
|
+
const handler = (key) => {
|
|
91
|
+
if (key === 'q' || key === '\u0003')
|
|
92
|
+
userAction = QUIT;
|
|
93
|
+
if (key === 's')
|
|
94
|
+
userAction = SKIP;
|
|
95
|
+
};
|
|
96
|
+
process.stdin.on('data', handler);
|
|
97
|
+
return () => {
|
|
98
|
+
process.stdin.removeListener('data', handler);
|
|
99
|
+
process.stdin.setRawMode(false);
|
|
100
|
+
process.stdin.pause();
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const cleanup = setupKeyListener();
|
|
104
|
+
const startedAt = Date.now();
|
|
105
|
+
let pollIntervalSeconds = Number(beginResult.interval ?? 5);
|
|
106
|
+
const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
|
|
107
|
+
let domainResolved = false;
|
|
108
|
+
let currentDomain = 'feishu';
|
|
109
|
+
try {
|
|
110
|
+
while (Date.now() - startedAt < expireInSeconds * 1000) {
|
|
111
|
+
if (userAction === QUIT)
|
|
112
|
+
return QUIT;
|
|
113
|
+
if (userAction === SKIP)
|
|
114
|
+
return SKIP;
|
|
115
|
+
const pollResult = await client.poll(beginResult.device_code);
|
|
116
|
+
if (pollResult.user_info?.tenant_brand === 'lark' && !domainResolved) {
|
|
117
|
+
client.setDomain(true);
|
|
118
|
+
currentDomain = 'lark';
|
|
119
|
+
domainResolved = true;
|
|
120
|
+
}
|
|
121
|
+
if (pollResult.client_id && pollResult.client_secret) {
|
|
122
|
+
return {
|
|
123
|
+
appId: pollResult.client_id,
|
|
124
|
+
appSecret: pollResult.client_secret,
|
|
125
|
+
domain: currentDomain,
|
|
126
|
+
openId: pollResult.user_info?.open_id ?? '',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (pollResult.error === 'authorization_pending') {
|
|
130
|
+
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (pollResult.error === 'slow_down') {
|
|
134
|
+
pollIntervalSeconds += 5;
|
|
135
|
+
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (pollResult.error === 'access_denied') {
|
|
139
|
+
throw new Error('用户拒绝了扫码授权');
|
|
140
|
+
}
|
|
141
|
+
if (pollResult.error === 'expired_token') {
|
|
142
|
+
throw new Error('扫码会话已过期');
|
|
143
|
+
}
|
|
144
|
+
if (pollResult.error) {
|
|
145
|
+
throw new Error(`扫码注册失败: ${pollResult.error}${pollResult.error_description ? ` - ${pollResult.error_description}` : ''}`);
|
|
146
|
+
}
|
|
147
|
+
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
148
|
+
}
|
|
149
|
+
throw new Error('等待扫码结果超时');
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
cleanup();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function manualInput(rl) {
|
|
156
|
+
console.log('\n手动输入模式:\n');
|
|
157
|
+
let appId = '';
|
|
158
|
+
while (!appId) {
|
|
159
|
+
appId = (await ask(rl, ' 飞书 App ID: ')).trim();
|
|
160
|
+
if (!appId)
|
|
161
|
+
console.log(' ⚠ 不能为空');
|
|
162
|
+
}
|
|
163
|
+
let appSecret = '';
|
|
164
|
+
while (!appSecret) {
|
|
165
|
+
appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
|
|
166
|
+
if (!appSecret)
|
|
167
|
+
console.log(' ⚠ 不能为空');
|
|
168
|
+
}
|
|
169
|
+
return { appId, appSecret, domain: 'unknown', openId: '' };
|
|
170
|
+
}
|
|
171
|
+
export async function runFeishuQrFlow() {
|
|
172
|
+
try {
|
|
173
|
+
const result = await runQrRegistrationFlow();
|
|
174
|
+
if (result === QUIT || result === SKIP)
|
|
175
|
+
return null;
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function cmdInitFeishu() {
|
|
184
|
+
const p = resolvePaths();
|
|
185
|
+
if (!fs.existsSync(p.config)) {
|
|
186
|
+
console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
190
|
+
// Normalize existing instances and filter out placeholders
|
|
191
|
+
const allInstances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
|
|
192
|
+
const validInstances = [];
|
|
193
|
+
for (let i = 0; i < allInstances.length; i++) {
|
|
194
|
+
const inst = allInstances[i];
|
|
195
|
+
if (!inst.appId || !inst.appSecret)
|
|
196
|
+
continue;
|
|
197
|
+
if (inst.appId.includes('your-') || inst.appId.includes('placeholder'))
|
|
198
|
+
continue;
|
|
199
|
+
if (inst.appSecret.includes('your-') || inst.appSecret.includes('placeholder'))
|
|
200
|
+
continue;
|
|
201
|
+
validInstances.push({ ...inst, originalIndex: i });
|
|
202
|
+
}
|
|
203
|
+
let choice = null;
|
|
204
|
+
if (validInstances.length > 0) {
|
|
205
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
206
|
+
try {
|
|
207
|
+
choice = await selectInstance(rl, 'feishu', validInstances);
|
|
208
|
+
if (choice === null)
|
|
209
|
+
return; // user cancelled
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
rl.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
console.log('正在获取飞书登录二维码...\n');
|
|
216
|
+
let result;
|
|
217
|
+
try {
|
|
218
|
+
const flowResult = await runQrRegistrationFlow();
|
|
219
|
+
if (flowResult === QUIT) {
|
|
220
|
+
console.log('已退出');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (flowResult === SKIP) {
|
|
224
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
225
|
+
try {
|
|
226
|
+
result = await manualInput(rl);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
rl.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
result = flowResult;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
// Write config to the correct slot
|
|
241
|
+
if (!config.channels)
|
|
242
|
+
config.channels = {};
|
|
243
|
+
if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.feishu)) {
|
|
244
|
+
// Overwrite existing instance in array — use originalIndex to find the right slot
|
|
245
|
+
const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
|
|
246
|
+
config.channels.feishu[idx].appId = result.appId;
|
|
247
|
+
config.channels.feishu[idx].appSecret = result.appSecret;
|
|
248
|
+
config.channels.feishu[idx].enabled = true;
|
|
249
|
+
if (result.openId)
|
|
250
|
+
config.channels.feishu[idx].owner = result.openId;
|
|
251
|
+
}
|
|
252
|
+
else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.feishu)) {
|
|
253
|
+
// Overwrite single-object
|
|
254
|
+
config.channels.feishu = config.channels.feishu || {};
|
|
255
|
+
config.channels.feishu.appId = result.appId;
|
|
256
|
+
config.channels.feishu.appSecret = result.appSecret;
|
|
257
|
+
config.channels.feishu.enabled = true;
|
|
258
|
+
if (result.openId)
|
|
259
|
+
config.channels.feishu.owner = result.openId;
|
|
260
|
+
else
|
|
261
|
+
delete config.channels.feishu.owner;
|
|
262
|
+
}
|
|
263
|
+
else if (choice && choice.action === 'add') {
|
|
264
|
+
// Add new instance — upgrade to array if needed
|
|
265
|
+
const newInst = {
|
|
266
|
+
name: choice.name,
|
|
267
|
+
appId: result.appId,
|
|
268
|
+
appSecret: result.appSecret,
|
|
269
|
+
enabled: true,
|
|
270
|
+
...(result.openId ? { owner: result.openId } : {}),
|
|
271
|
+
};
|
|
272
|
+
if (Array.isArray(config.channels.feishu)) {
|
|
273
|
+
config.channels.feishu.push(newInst);
|
|
274
|
+
}
|
|
275
|
+
else if (config.channels.feishu) {
|
|
276
|
+
// Upgrade single object to array
|
|
277
|
+
const oldInst = { ...config.channels.feishu, name: config.channels.feishu.name || 'feishu' };
|
|
278
|
+
config.channels.feishu = [oldInst, newInst];
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
config.channels.feishu = [newInst];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// First instance — single object format (backward compat)
|
|
286
|
+
config.channels.feishu = config.channels.feishu || {};
|
|
287
|
+
config.channels.feishu.appId = result.appId;
|
|
288
|
+
config.channels.feishu.appSecret = result.appSecret;
|
|
289
|
+
config.channels.feishu.enabled = true;
|
|
290
|
+
if (result.openId)
|
|
291
|
+
config.channels.feishu.owner = result.openId;
|
|
292
|
+
else
|
|
293
|
+
delete config.channels.feishu.owner;
|
|
294
|
+
}
|
|
295
|
+
if (!config.channels.defaultChannel)
|
|
296
|
+
config.channels.defaultChannel = 'feishu';
|
|
297
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
298
|
+
console.log(`\n✅ 飞书连接成功!`);
|
|
299
|
+
console.log(` App ID: ${result.appId}`);
|
|
300
|
+
if (result.openId) {
|
|
301
|
+
console.log(` Owner: ${result.openId}`);
|
|
302
|
+
}
|
|
303
|
+
if (result.domain !== 'unknown') {
|
|
304
|
+
console.log(` Domain: ${result.domain}`);
|
|
305
|
+
}
|
|
306
|
+
if (choice) {
|
|
307
|
+
console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
|
|
308
|
+
}
|
|
309
|
+
console.log(` 配置已写入: ${p.config}`);
|
|
310
|
+
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
311
|
+
}
|
|
312
|
+
// ==================== WeChat ====================
|
|
313
|
+
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
314
|
+
const BOT_TYPE = '3';
|
|
315
|
+
const QR_POLL_TIMEOUT_MS = 35_000;
|
|
316
|
+
const LOGIN_TIMEOUT_MS = 480_000;
|
|
317
|
+
async function fetchQRCode(baseUrl) {
|
|
318
|
+
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
319
|
+
const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
|
|
320
|
+
const res = await fetch(url);
|
|
321
|
+
if (!res.ok)
|
|
322
|
+
throw new Error(`QR fetch failed: ${res.status}`);
|
|
323
|
+
return (await res.json());
|
|
324
|
+
}
|
|
325
|
+
async function pollQRStatus(baseUrl, qrcode) {
|
|
326
|
+
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
327
|
+
const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
328
|
+
const controller = new AbortController();
|
|
329
|
+
const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(url, {
|
|
332
|
+
headers: { 'iLink-App-ClientVersion': '1' },
|
|
333
|
+
signal: controller.signal,
|
|
334
|
+
});
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
if (!res.ok)
|
|
337
|
+
throw new Error(`QR status failed: ${res.status}`);
|
|
338
|
+
return (await res.json());
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
343
|
+
return { status: 'wait' };
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export async function runWechatQrFlow() {
|
|
349
|
+
const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
|
|
350
|
+
try {
|
|
351
|
+
const qrterm = await import('qrcode-terminal');
|
|
352
|
+
await new Promise(resolve => {
|
|
353
|
+
qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
|
|
354
|
+
console.log(qr);
|
|
355
|
+
resolve();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
|
|
361
|
+
}
|
|
362
|
+
console.log('请用微信扫描上方二维码...\n');
|
|
363
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
364
|
+
let scannedPrinted = false;
|
|
365
|
+
while (Date.now() < deadline) {
|
|
366
|
+
const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
|
|
367
|
+
switch (status.status) {
|
|
368
|
+
case 'wait':
|
|
369
|
+
process.stdout.write('.');
|
|
370
|
+
break;
|
|
371
|
+
case 'scaned':
|
|
372
|
+
if (!scannedPrinted) {
|
|
373
|
+
console.log('\n👀 已扫码,请在微信中确认...');
|
|
374
|
+
scannedPrinted = true;
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
case 'expired':
|
|
378
|
+
console.error('\n二维码已过期');
|
|
379
|
+
return null;
|
|
380
|
+
case 'confirmed':
|
|
381
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
382
|
+
console.error('\n登录失败:服务器未返回完整信息');
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
baseUrl: status.baseurl || DEFAULT_BASE_URL,
|
|
387
|
+
token: status.bot_token,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
391
|
+
}
|
|
392
|
+
console.log('\n登录超时');
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
export async function cmdInitWechat() {
|
|
396
|
+
const p = resolvePaths();
|
|
397
|
+
if (!fs.existsSync(p.config)) {
|
|
398
|
+
console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
402
|
+
// Normalize existing instances and filter out placeholders
|
|
403
|
+
const allInstances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
|
|
404
|
+
const validInstances = [];
|
|
405
|
+
for (let i = 0; i < allInstances.length; i++) {
|
|
406
|
+
const inst = allInstances[i];
|
|
407
|
+
if (!inst.token)
|
|
408
|
+
continue;
|
|
409
|
+
if (inst.token.includes('your-') || inst.token.includes('placeholder'))
|
|
410
|
+
continue;
|
|
411
|
+
validInstances.push({ ...inst, originalIndex: i });
|
|
412
|
+
}
|
|
413
|
+
let choice = null;
|
|
414
|
+
if (validInstances.length > 0) {
|
|
415
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
416
|
+
try {
|
|
417
|
+
choice = await selectInstance(rl, 'wechat', validInstances);
|
|
418
|
+
if (choice === null)
|
|
419
|
+
return; // user cancelled
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
rl.close();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
console.log('正在获取微信登录二维码...\n');
|
|
426
|
+
const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
|
|
427
|
+
// 终端显示二维码
|
|
428
|
+
try {
|
|
429
|
+
const qrterm = await import('qrcode-terminal');
|
|
430
|
+
await new Promise(resolve => {
|
|
431
|
+
qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
|
|
432
|
+
console.log(qr);
|
|
433
|
+
resolve();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
|
|
439
|
+
}
|
|
440
|
+
console.log('请用微信扫描上方二维码...\n');
|
|
441
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
442
|
+
let scannedPrinted = false;
|
|
443
|
+
while (Date.now() < deadline) {
|
|
444
|
+
const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
|
|
445
|
+
switch (status.status) {
|
|
446
|
+
case 'wait':
|
|
447
|
+
process.stdout.write('.');
|
|
448
|
+
break;
|
|
449
|
+
case 'scaned':
|
|
450
|
+
if (!scannedPrinted) {
|
|
451
|
+
console.log('\n\ud83d\udc40 已扫码,请在微信中确认...');
|
|
452
|
+
scannedPrinted = true;
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
case 'expired':
|
|
456
|
+
console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
|
|
457
|
+
process.exit(1);
|
|
458
|
+
break;
|
|
459
|
+
case 'confirmed': {
|
|
460
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
461
|
+
console.error('\n登录失败:服务器未返回完整信息');
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const baseUrl = status.baseurl || DEFAULT_BASE_URL;
|
|
465
|
+
const token = status.bot_token;
|
|
466
|
+
// Write config to the correct slot
|
|
467
|
+
if (!config.channels)
|
|
468
|
+
config.channels = {};
|
|
469
|
+
if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.wechat)) {
|
|
470
|
+
// Overwrite existing instance in array — use originalIndex to find the right slot
|
|
471
|
+
const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
|
|
472
|
+
config.channels.wechat[idx].enabled = true;
|
|
473
|
+
config.channels.wechat[idx].baseUrl = baseUrl;
|
|
474
|
+
config.channels.wechat[idx].token = token;
|
|
475
|
+
}
|
|
476
|
+
else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.wechat)) {
|
|
477
|
+
// Overwrite single-object
|
|
478
|
+
config.channels.wechat = config.channels.wechat || {};
|
|
479
|
+
config.channels.wechat.enabled = true;
|
|
480
|
+
config.channels.wechat.baseUrl = baseUrl;
|
|
481
|
+
config.channels.wechat.token = token;
|
|
482
|
+
}
|
|
483
|
+
else if (choice && choice.action === 'add') {
|
|
484
|
+
// Add new instance — upgrade to array if needed
|
|
485
|
+
const newInst = {
|
|
486
|
+
name: choice.name,
|
|
487
|
+
enabled: true,
|
|
488
|
+
baseUrl,
|
|
489
|
+
token,
|
|
490
|
+
};
|
|
491
|
+
if (Array.isArray(config.channels.wechat)) {
|
|
492
|
+
config.channels.wechat.push(newInst);
|
|
493
|
+
}
|
|
494
|
+
else if (config.channels.wechat) {
|
|
495
|
+
// Upgrade single object to array
|
|
496
|
+
const oldInst = { ...config.channels.wechat, name: config.channels.wechat.name || 'wechat' };
|
|
497
|
+
config.channels.wechat = [oldInst, newInst];
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
config.channels.wechat = [newInst];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// First instance — single object format (backward compat)
|
|
505
|
+
config.channels.wechat = {
|
|
506
|
+
enabled: true,
|
|
507
|
+
baseUrl,
|
|
508
|
+
token,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (!config.channels.defaultChannel)
|
|
512
|
+
config.channels.defaultChannel = 'wechat';
|
|
513
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
514
|
+
console.log(`\n✅ 微信连接成功!`);
|
|
515
|
+
console.log(` Bot ID: ${status.ilink_bot_id}`);
|
|
516
|
+
console.log(` User ID: ${status.ilink_user_id}`);
|
|
517
|
+
if (choice) {
|
|
518
|
+
console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
|
|
519
|
+
}
|
|
520
|
+
console.log(` 配置已写入: ${p.config}`);
|
|
521
|
+
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
526
|
+
}
|
|
527
|
+
console.log('\n登录超时,请重新运行');
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
// ==================== AUN ====================
|
|
531
|
+
export async function checkAunEnvironment(rl) {
|
|
532
|
+
console.log('\n🔍 AUN 环境检查...\n');
|
|
533
|
+
// TS SDK (@eleans/aun-core-node) is bundled as npm dependency — no external deps needed
|
|
534
|
+
console.log(' ✓ @eleans/aun-core-node (TS SDK)');
|
|
535
|
+
console.log('');
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
function isValidAid(name) {
|
|
539
|
+
const labels = name.split('.');
|
|
540
|
+
return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
|
|
541
|
+
}
|
|
542
|
+
export async function setupAunAid(rl, _config) {
|
|
543
|
+
let aid = '';
|
|
544
|
+
let gatewayPort;
|
|
545
|
+
// Outer loop: allows retrying with a different AID
|
|
546
|
+
while (true) {
|
|
547
|
+
// Ask AID with format validation
|
|
548
|
+
aid = '';
|
|
549
|
+
while (!aid) {
|
|
550
|
+
aid = (await ask(rl, ' AUN Agent ID (例: mybot.agentid.pub): ')).trim();
|
|
551
|
+
if (!aid) {
|
|
552
|
+
console.log(' ⚠ 不能为空');
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (!isValidAid(aid)) {
|
|
556
|
+
console.log(' ⚠ 无效 AID 格式(需要合法域名,至少三级,如 alice.agentid.pub)');
|
|
557
|
+
aid = '';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const portStr = (await ask(rl, ' Gateway 端口 [留空使用默认 443]: ')).trim();
|
|
561
|
+
gatewayPort = portStr ? parseInt(portStr, 10) : undefined;
|
|
562
|
+
if (gatewayPort !== undefined && (isNaN(gatewayPort) || gatewayPort < 1 || gatewayPort > 65535)) {
|
|
563
|
+
console.log(' ⚠ 端口号无效,使用默认 443');
|
|
564
|
+
gatewayPort = undefined;
|
|
565
|
+
}
|
|
566
|
+
// Check if AID exists locally
|
|
567
|
+
const aunPath = path.join(os.homedir(), '.aun');
|
|
568
|
+
const aidDir = path.join(aunPath, 'AIDs', aid);
|
|
569
|
+
if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
|
|
570
|
+
console.log(` ✓ AID ${aid} 已存在`);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
const answer = (await ask(rl, ` ⚠ AID ${aid} 本地不存在,是否创建?[Y/n] `)).trim().toLowerCase();
|
|
574
|
+
if (answer === 'n' || answer === 'no') {
|
|
575
|
+
console.log(' 已跳过 AID 创建(启动时可能连接失败)');
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
// Create AID using TS SDK directly
|
|
579
|
+
console.log(' 正在创建 AID...');
|
|
580
|
+
let failed = false;
|
|
581
|
+
try {
|
|
582
|
+
const { AUNClient } = await import('@eleans/aun-core-node');
|
|
583
|
+
const client = new AUNClient({ aun_path: aunPath });
|
|
584
|
+
// Set gateway URL from AID domain + port
|
|
585
|
+
const domain = aid.split('.').slice(1).join('.');
|
|
586
|
+
const port = gatewayPort || 443;
|
|
587
|
+
client._gatewayUrl = `wss://gateway.${domain}:${port}/aun`;
|
|
588
|
+
const result = await client.auth.createAid({ aid });
|
|
589
|
+
console.log(` ✓ AID ${result.aid} 创建成功`);
|
|
590
|
+
try {
|
|
591
|
+
await client.close();
|
|
592
|
+
}
|
|
593
|
+
catch { /* ignore */ }
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
const msg = e.message || String(e);
|
|
597
|
+
console.log(` ✗ AID 创建失败: ${msg.slice(0, 200)}`);
|
|
598
|
+
failed = true;
|
|
599
|
+
}
|
|
600
|
+
if (!failed)
|
|
601
|
+
break;
|
|
602
|
+
// Creation failed — retry or give up
|
|
603
|
+
const retry = (await ask(rl, ' → 重新输入 (r) / 跳过 (s) / 取消 (c)?[r/s/c] ')).trim().toLowerCase();
|
|
604
|
+
if (retry === 'c')
|
|
605
|
+
return null;
|
|
606
|
+
if (retry === 's')
|
|
607
|
+
break;
|
|
608
|
+
// default: retry with new AID
|
|
609
|
+
}
|
|
610
|
+
return { aid, gatewayPort };
|
|
611
|
+
}
|
|
612
|
+
export async function cmdInitAun() {
|
|
613
|
+
const p = resolvePaths();
|
|
614
|
+
if (!fs.existsSync(p.config)) {
|
|
615
|
+
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
619
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
620
|
+
try {
|
|
621
|
+
if (config.channels?.aun?.aid) {
|
|
622
|
+
const answer = (await ask(rl, '已有 AUN 配置,是否重新配置?[y/N] ')).trim().toLowerCase();
|
|
623
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
624
|
+
console.log('已取消');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (!await checkAunEnvironment(rl)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const result = await setupAunAid(rl, config);
|
|
632
|
+
if (!result)
|
|
633
|
+
return;
|
|
634
|
+
if (!config.channels)
|
|
635
|
+
config.channels = {};
|
|
636
|
+
config.channels.aun = {
|
|
637
|
+
enabled: true,
|
|
638
|
+
aid: result.aid,
|
|
639
|
+
...(result.gatewayPort && { gatewayPort: result.gatewayPort }),
|
|
640
|
+
};
|
|
641
|
+
if (!config.channels.defaultChannel)
|
|
642
|
+
config.channels.defaultChannel = 'aun';
|
|
643
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
644
|
+
console.log('\n✓ AUN 配置已写入');
|
|
645
|
+
}
|
|
646
|
+
finally {
|
|
647
|
+
rl.close();
|
|
648
|
+
}
|
|
649
|
+
}
|