evolclaw 2.0.0 → 2.0.2

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.
@@ -0,0 +1,170 @@
1
+ import fs from 'fs';
2
+ import readline from 'readline';
3
+ import { resolvePaths } from '../paths.js';
4
+ const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
5
+ const BOT_TYPE = '3';
6
+ const QR_POLL_TIMEOUT_MS = 35_000;
7
+ const LOGIN_TIMEOUT_MS = 480_000;
8
+ function ask(rl, question) {
9
+ return new Promise(resolve => rl.question(question, resolve));
10
+ }
11
+ async function fetchQRCode(baseUrl) {
12
+ const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
13
+ const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
14
+ const res = await fetch(url);
15
+ if (!res.ok)
16
+ throw new Error(`QR fetch failed: ${res.status}`);
17
+ return (await res.json());
18
+ }
19
+ async function pollQRStatus(baseUrl, qrcode) {
20
+ const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
21
+ const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
24
+ try {
25
+ const res = await fetch(url, {
26
+ headers: { 'iLink-App-ClientVersion': '1' },
27
+ signal: controller.signal,
28
+ });
29
+ clearTimeout(timer);
30
+ if (!res.ok)
31
+ throw new Error(`QR status failed: ${res.status}`);
32
+ return (await res.json());
33
+ }
34
+ catch (err) {
35
+ clearTimeout(timer);
36
+ if (err instanceof Error && err.name === 'AbortError') {
37
+ return { status: 'wait' };
38
+ }
39
+ throw err;
40
+ }
41
+ }
42
+ export async function runWechatQrFlow() {
43
+ const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
44
+ try {
45
+ const qrterm = await import('qrcode-terminal');
46
+ await new Promise(resolve => {
47
+ qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
48
+ console.log(qr);
49
+ resolve();
50
+ });
51
+ });
52
+ }
53
+ catch {
54
+ console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
55
+ }
56
+ console.log('请用微信扫描上方二维码...\n');
57
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
58
+ let scannedPrinted = false;
59
+ while (Date.now() < deadline) {
60
+ const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
61
+ switch (status.status) {
62
+ case 'wait':
63
+ process.stdout.write('.');
64
+ break;
65
+ case 'scaned':
66
+ if (!scannedPrinted) {
67
+ console.log('\n👀 已扫码,请在微信中确认...');
68
+ scannedPrinted = true;
69
+ }
70
+ break;
71
+ case 'expired':
72
+ console.error('\n二维码已过期');
73
+ return null;
74
+ case 'confirmed':
75
+ if (!status.ilink_bot_id || !status.bot_token) {
76
+ console.error('\n登录失败:服务器未返回完整信息');
77
+ return null;
78
+ }
79
+ return {
80
+ baseUrl: status.baseurl || DEFAULT_BASE_URL,
81
+ token: status.bot_token,
82
+ };
83
+ }
84
+ await new Promise(r => setTimeout(r, 1000));
85
+ }
86
+ console.log('\n登录超时');
87
+ return null;
88
+ }
89
+ export async function cmdInitWechat() {
90
+ const p = resolvePaths();
91
+ if (!fs.existsSync(p.config)) {
92
+ console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
93
+ return;
94
+ }
95
+ const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
96
+ // 检查已有配置
97
+ if (config.channels?.wechat?.token) {
98
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
99
+ try {
100
+ const answer = (await ask(rl, '已有微信配置,是否重新登录?[y/N] ')).trim().toLowerCase();
101
+ if (answer !== 'y' && answer !== 'yes') {
102
+ console.log('已取消');
103
+ return;
104
+ }
105
+ }
106
+ finally {
107
+ rl.close();
108
+ }
109
+ }
110
+ console.log('正在获取微信登录二维码...\n');
111
+ const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
112
+ // 终端显示二维码
113
+ try {
114
+ const qrterm = await import('qrcode-terminal');
115
+ await new Promise(resolve => {
116
+ qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
117
+ console.log(qr);
118
+ resolve();
119
+ });
120
+ });
121
+ }
122
+ catch {
123
+ console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
124
+ }
125
+ console.log('请用微信扫描上方二维码...\n');
126
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
127
+ let scannedPrinted = false;
128
+ while (Date.now() < deadline) {
129
+ const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
130
+ switch (status.status) {
131
+ case 'wait':
132
+ process.stdout.write('.');
133
+ break;
134
+ case 'scaned':
135
+ if (!scannedPrinted) {
136
+ console.log('\n👀 已扫码,请在微信中确认...');
137
+ scannedPrinted = true;
138
+ }
139
+ break;
140
+ case 'expired':
141
+ console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
142
+ process.exit(1);
143
+ break;
144
+ case 'confirmed': {
145
+ if (!status.ilink_bot_id || !status.bot_token) {
146
+ console.error('\n登录失败:服务器未返回完整信息');
147
+ process.exit(1);
148
+ }
149
+ // 写入配置
150
+ if (!config.channels)
151
+ config.channels = {};
152
+ config.channels.wechat = {
153
+ enabled: true,
154
+ baseUrl: status.baseurl || DEFAULT_BASE_URL,
155
+ token: status.bot_token,
156
+ };
157
+ fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
158
+ console.log(`\n✅ 微信连接成功!`);
159
+ console.log(` Bot ID: ${status.ilink_bot_id}`);
160
+ console.log(` User ID: ${status.ilink_user_id}`);
161
+ console.log(` 配置已写入: ${p.config}`);
162
+ console.log(`\n现在可以启动服务: evolclaw restart`);
163
+ return;
164
+ }
165
+ }
166
+ await new Promise(r => setTimeout(r, 1000));
167
+ }
168
+ console.log('\n登录超时,请重新运行');
169
+ process.exit(1);
170
+ }
@@ -26,12 +26,19 @@ async function npmInstallGlobal(pkg) {
26
26
  }
27
27
  }
28
28
  async function sudoExec(cmd, args) {
29
+ // 让 n 安装到当前 node 所在的 prefix 目录
30
+ const env = { ...process.env };
31
+ if (cmd === 'n' && !env.N_PREFIX) {
32
+ const nodePrefix = process.config.variables?.node_prefix;
33
+ if (nodePrefix)
34
+ env.N_PREFIX = nodePrefix;
35
+ }
29
36
  try {
30
- await execFileAsync(cmd, args, { timeout: 120000 });
37
+ await execFileAsync(cmd, args, { timeout: 120000, env });
31
38
  }
32
39
  catch (e) {
33
40
  if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES') || e.code === 'EACCES') {
34
- await execFileAsync('sudo', [cmd, ...args], { timeout: 120000 });
41
+ await execFileAsync('sudo', [cmd, ...args], { timeout: 120000, env });
35
42
  }
36
43
  else {
37
44
  throw e;
@@ -49,6 +56,18 @@ async function checkEnvironment(rl) {
49
56
  else {
50
57
  console.log(` ✗ Node.js v${process.versions.node} — 需要 >= 22(node:sqlite 依赖)`);
51
58
  // 检测 nvm
59
+ // 检测 bash 是否存在(nvm 和 n 都依赖 bash)
60
+ let hasBash = false;
61
+ try {
62
+ execFileSync('which', ['bash'], { encoding: 'utf-8' });
63
+ hasBash = true;
64
+ }
65
+ catch { }
66
+ if (!hasBash) {
67
+ console.log(' ⚠ 当前环境没有 bash(Alpine 容器?),无法自动升级 Node.js');
68
+ console.log(' → 请手动升级: apk add nodejs-current 或重建容器使用 node:22-alpine');
69
+ return false;
70
+ }
52
71
  const hasNvm = !!process.env.NVM_DIR && fs.existsSync(process.env.NVM_DIR);
53
72
  if (hasNvm) {
54
73
  const answer = (await ask(rl, ' → 是否通过 nvm 升级到 Node.js 22?[Y/n] ')).trim().toLowerCase();
@@ -61,7 +80,8 @@ async function checkEnvironment(rl) {
61
80
  const nvmDir = process.env.NVM_DIR;
62
81
  const { stdout } = await execFileAsync('bash', ['-c', `source "${nvmDir}/nvm.sh" && nvm install 22 && nvm alias default 22`], { timeout: 120000 });
63
82
  console.log(stdout.trim().split('\n').map(l => ` ${l}`).join('\n'));
64
- console.log(' ✓ Node.js 升级完成,请重新运行 evolclaw init');
83
+ console.log(' ✓ Node.js 升级完成');
84
+ console.log(' → 请打开新终端后重新运行 evolclaw init');
65
85
  return false;
66
86
  }
67
87
  catch (e) {
@@ -86,7 +106,8 @@ async function checkEnvironment(rl) {
86
106
  console.log(' 正在升级 Node.js...');
87
107
  try {
88
108
  await sudoExec('n', ['22']);
89
- console.log(' ✓ Node.js 升级完成,请重新运行 evolclaw init');
109
+ console.log(' ✓ Node.js 升级完成');
110
+ console.log(' → 请打开新终端后重新运行 evolclaw init');
90
111
  return false;
91
112
  }
92
113
  catch (e) {
@@ -105,7 +126,8 @@ async function checkEnvironment(rl) {
105
126
  await npmInstallGlobal('n');
106
127
  console.log(' 正在升级 Node.js...');
107
128
  await sudoExec('n', ['22']);
108
- console.log(' ✓ Node.js 升级完成,请重新运行 evolclaw init');
129
+ console.log(' ✓ Node.js 升级完成');
130
+ console.log(' → 请打开新终端后重新运行 evolclaw init');
109
131
  return false;
110
132
  }
111
133
  catch (e) {
@@ -162,22 +184,10 @@ async function checkEnvironment(rl) {
162
184
  // @anthropic-ai/claude-agent-sdk >= 0.2.75
163
185
  let sdkAction = 'ok';
164
186
  try {
165
- let sdkPkgPath = null;
187
+ // require.resolve 找到 SDK 入口,推导 package.json 路径
166
188
  const esmRequire = createRequire(import.meta.url);
167
- try {
168
- sdkPkgPath = esmRequire.resolve('@anthropic-ai/claude-agent-sdk/package.json');
169
- }
170
- catch {
171
- try {
172
- const globalRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf-8' }).trim();
173
- const globalPath = path.join(globalRoot, '@anthropic-ai', 'claude-agent-sdk', 'package.json');
174
- if (fs.existsSync(globalPath))
175
- sdkPkgPath = globalPath;
176
- }
177
- catch { }
178
- }
179
- if (!sdkPkgPath)
180
- throw new Error('not found');
189
+ const sdkEntry = esmRequire.resolve('@anthropic-ai/claude-agent-sdk');
190
+ const sdkPkgPath = path.join(path.dirname(sdkEntry), 'package.json');
181
191
  const sdkPkg = JSON.parse(fs.readFileSync(sdkPkgPath, 'utf-8'));
182
192
  const sdkVer = sdkPkg.version;
183
193
  const parts = sdkVer.split('.').map(Number);
@@ -246,11 +256,50 @@ function setupEnvVar(home) {
246
256
  }
247
257
  console.log(' ⚠ 请重新打开终端或执行 source 使其生效');
248
258
  }
259
+ // ==================== Feishu Manual Input ====================
260
+ async function initFeishuManual(rl, config) {
261
+ let appId = '';
262
+ while (!appId) {
263
+ appId = (await ask(rl, ' 飞书 App ID: ')).trim();
264
+ if (!appId)
265
+ console.log(' ⚠ 不能为空');
266
+ }
267
+ let appSecret = '';
268
+ while (!appSecret) {
269
+ appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
270
+ if (!appSecret)
271
+ console.log(' ⚠ 不能为空');
272
+ }
273
+ console.log(' 正在验证飞书凭证...');
274
+ try {
275
+ const lark = await import('@larksuiteoapi/node-sdk');
276
+ const client = new lark.Client({ appId, appSecret });
277
+ const res = await client.auth.tenantAccessToken.internal({
278
+ data: { app_id: appId, app_secret: appSecret },
279
+ });
280
+ if (res.code === 0) {
281
+ console.log(' ✓ 飞书凭证验证通过');
282
+ }
283
+ else {
284
+ console.log(` ✗ 飞书凭证验证失败: ${res.msg}`);
285
+ const answer = (await ask(rl, ' → 是否继续?[y/N] ')).trim().toLowerCase();
286
+ if (answer !== 'y' && answer !== 'yes') {
287
+ return false;
288
+ }
289
+ }
290
+ }
291
+ catch (e) {
292
+ console.log(` ⚠ 飞书凭证验证跳过: ${e.message?.slice(0, 100) || e}`);
293
+ }
294
+ config.channels.feishu.appId = appId;
295
+ config.channels.feishu.appSecret = appSecret;
296
+ config.channels.feishu.enabled = true;
297
+ return true;
298
+ }
249
299
  // ==================== Main ====================
250
300
  export async function cmdInit() {
251
301
  const p = resolvePaths();
252
302
  ensureDataDirs();
253
- // 检查服务是否在运行
254
303
  if (fs.existsSync(p.pid)) {
255
304
  const pid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
256
305
  try {
@@ -278,49 +327,11 @@ export async function cmdInit() {
278
327
  return;
279
328
  }
280
329
  console.log('📝 交互式配置\n');
281
- // feishu.appId
282
- let appId = '';
283
- while (!appId) {
284
- appId = (await ask(rl, ' 飞书 App ID: ')).trim();
285
- if (!appId)
286
- console.log(' ⚠ 不能为空');
287
- }
288
- // feishu.appSecret
289
- let appSecret = '';
290
- while (!appSecret) {
291
- appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
292
- if (!appSecret)
293
- console.log(' ⚠ 不能为空');
294
- }
295
- // 验证飞书凭证
296
- console.log(' 正在验证飞书凭证...');
297
- try {
298
- const lark = await import('@larksuiteoapi/node-sdk');
299
- const client = new lark.Client({ appId, appSecret });
300
- const res = await client.auth.tenantAccessToken.internal({
301
- data: { app_id: appId, app_secret: appSecret },
302
- });
303
- if (res.code === 0) {
304
- console.log(' ✓ 飞书凭证验证通过');
305
- }
306
- else {
307
- console.log(` ✗ 飞书凭证验证失败: ${res.msg}`);
308
- const answer = (await ask(rl, ' → 是否继续?[y/N] ')).trim().toLowerCase();
309
- if (answer !== 'y' && answer !== 'yes') {
310
- console.log(' 已取消');
311
- return;
312
- }
313
- }
314
- }
315
- catch (e) {
316
- console.log(` ⚠ 飞书凭证验证跳过: ${e.message?.slice(0, 100) || e}`);
317
- }
318
- // projects.defaultPath
330
+ // 通用配置
319
331
  const defaultSuggestion = path.join(os.homedir(), 'evolclaw-project');
320
332
  let defaultPath = (await ask(rl, ` 默认项目路径 [${defaultSuggestion}]: `)).trim();
321
- if (!defaultPath) {
333
+ if (!defaultPath)
322
334
  defaultPath = defaultSuggestion;
323
- }
324
335
  if (defaultPath.startsWith('~/')) {
325
336
  defaultPath = path.join(os.homedir(), defaultPath.slice(2));
326
337
  }
@@ -331,19 +342,61 @@ export async function cmdInit() {
331
342
  fs.mkdirSync(defaultPath, { recursive: true });
332
343
  console.log(` ✓ 已创建目录: ${defaultPath}`);
333
344
  }
334
- // anthropic.model
335
345
  const modelInput = (await ask(rl, ' 模型 [sonnet(默认)/opus/haiku]: ')).trim().toLowerCase();
336
346
  const model = ['opus', 'haiku'].includes(modelInput) ? modelInput : 'sonnet';
337
- // Generate config
347
+ // 渠道选择
348
+ console.log('\n选择消息渠道:');
349
+ console.log(' 1. 飞书 (Feishu)');
350
+ console.log(' 2. 微信 (WeChat)');
351
+ const channelChoice = (await ask(rl, '请选择 [1]: ')).trim() || '1';
338
352
  const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
339
- config.feishu.appId = appId;
340
- config.feishu.appSecret = appSecret;
341
353
  config.projects.defaultPath = defaultPath;
342
354
  config.projects.list = { [path.basename(defaultPath)]: defaultPath };
343
- config.anthropic.model = model;
355
+ config.agents.anthropic.model = model;
356
+ if (channelChoice === '1') {
357
+ console.log('\n飞书配置方式:');
358
+ console.log(' 1. 扫码自动注册(推荐)');
359
+ console.log(' 2. 手动输入 App ID/Secret');
360
+ const feishuMethod = (await ask(rl, '请选择 [1]: ')).trim() || '1';
361
+ if (feishuMethod === '1') {
362
+ const { runFeishuQrFlow } = await import('./init-feishu.js');
363
+ const result = await runFeishuQrFlow();
364
+ if (!result) {
365
+ console.log('已取消');
366
+ return;
367
+ }
368
+ config.channels.feishu.appId = result.appId;
369
+ config.channels.feishu.appSecret = result.appSecret;
370
+ config.channels.feishu.enabled = true;
371
+ if (result.openId)
372
+ config.channels.feishu.owner = result.openId;
373
+ }
374
+ else {
375
+ if (!await initFeishuManual(rl, config)) {
376
+ console.log('已取消');
377
+ return;
378
+ }
379
+ }
380
+ }
381
+ else if (channelChoice === '2') {
382
+ const { runWechatQrFlow } = await import('./init-wechat.js');
383
+ const result = await runWechatQrFlow();
384
+ if (!result) {
385
+ console.log('已取消');
386
+ return;
387
+ }
388
+ config.channels.wechat = {
389
+ enabled: true,
390
+ baseUrl: result.baseUrl,
391
+ token: result.token,
392
+ };
393
+ }
394
+ else {
395
+ console.log('无效选择');
396
+ return;
397
+ }
344
398
  fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
345
399
  console.log(`\n✓ 已创建配置文件: ${p.config}`);
346
- // Setup EVOLCLAW_HOME in shell profile
347
400
  setupEnvVar(resolveRoot());
348
401
  }
349
402
  finally {
@@ -1,3 +1,4 @@
1
+ import { logger } from './logger.js';
1
2
  /**
2
3
  * 流式输出缓冲器
3
4
  * 按时间窗口批量推送文本和活动事件
@@ -137,10 +138,10 @@ export class StreamFlusher {
137
138
  const before = output;
138
139
  output = output.replace(this.fileMarkerPattern, '').trim();
139
140
  if (before !== output) {
140
- console.log('[StreamFlusher] Removed file markers, before length:', before.length, 'after:', output.length);
141
+ logger.debug('[StreamFlusher] Removed file markers, before length:', before.length, 'after:', output.length);
141
142
  }
142
143
  }
143
- console.log('[StreamFlusher] flush called, output length:', output.length, 'isEmpty:', !output, 'preview:', output.substring(0, 100));
144
+ logger.debug('[StreamFlusher] flush called, output length:', output.length, 'isEmpty:', !output, 'preview:', output.substring(0, 100));
144
145
  if (output) {
145
146
  await this.send(output, isFinal);
146
147
  this.sentContent = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,16 +23,18 @@
23
23
  "prepublishOnly": "npm run build && npm test"
24
24
  },
25
25
  "dependencies": {
26
- "@anthropic-ai/claude-agent-sdk": "^0.2.77",
26
+ "@anthropic-ai/claude-agent-sdk": "^0.2.81",
27
27
  "@larksuiteoapi/node-sdk": "^1.59.0",
28
- "dotenv": "^16.6.1",
29
- "image-type": "^6.0.0"
28
+ "dotenv": "^17.3.1",
29
+ "image-type": "^6.0.0",
30
+ "qrcode-terminal": "^0.12.0"
30
31
  },
31
32
  "devDependencies": {
32
- "@types/node": "^22.0.0",
33
- "@vitest/coverage-v8": "^2.1.9",
33
+ "@types/node": "^25.5.0",
34
+ "@types/qrcode-terminal": "^0.12.2",
35
+ "@vitest/coverage-v8": "^4.1.0",
34
36
  "tsx": "^4.19.0",
35
37
  "typescript": "^5.6.0",
36
- "vitest": "^2.1.9"
38
+ "vitest": "^4.1.0"
37
39
  }
38
40
  }