evolclaw 2.0.1 → 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
+ }
@@ -29,12 +29,9 @@ async function sudoExec(cmd, args) {
29
29
  // 让 n 安装到当前 node 所在的 prefix 目录
30
30
  const env = { ...process.env };
31
31
  if (cmd === 'n' && !env.N_PREFIX) {
32
- try {
33
- const nodePrefix = execFileSync('node', ['-e', 'process.stdout.write(process.config.variables.node_prefix)'], { encoding: 'utf-8' });
34
- if (nodePrefix)
35
- env.N_PREFIX = nodePrefix;
36
- }
37
- catch { }
32
+ const nodePrefix = process.config.variables?.node_prefix;
33
+ if (nodePrefix)
34
+ env.N_PREFIX = nodePrefix;
38
35
  }
39
36
  try {
40
37
  await execFileAsync(cmd, args, { timeout: 120000, env });
@@ -129,7 +126,8 @@ async function checkEnvironment(rl) {
129
126
  await npmInstallGlobal('n');
130
127
  console.log(' 正在升级 Node.js...');
131
128
  await sudoExec('n', ['22']);
132
- console.log(' ✓ Node.js 升级完成,请重新运行 evolclaw init');
129
+ console.log(' ✓ Node.js 升级完成');
130
+ console.log(' → 请打开新终端后重新运行 evolclaw init');
133
131
  return false;
134
132
  }
135
133
  catch (e) {
@@ -186,22 +184,10 @@ async function checkEnvironment(rl) {
186
184
  // @anthropic-ai/claude-agent-sdk >= 0.2.75
187
185
  let sdkAction = 'ok';
188
186
  try {
189
- let sdkPkgPath = null;
187
+ // require.resolve 找到 SDK 入口,推导 package.json 路径
190
188
  const esmRequire = createRequire(import.meta.url);
191
- try {
192
- sdkPkgPath = esmRequire.resolve('@anthropic-ai/claude-agent-sdk/package.json');
193
- }
194
- catch {
195
- try {
196
- const globalRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf-8' }).trim();
197
- const globalPath = path.join(globalRoot, '@anthropic-ai', 'claude-agent-sdk', 'package.json');
198
- if (fs.existsSync(globalPath))
199
- sdkPkgPath = globalPath;
200
- }
201
- catch { }
202
- }
203
- if (!sdkPkgPath)
204
- 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');
205
191
  const sdkPkg = JSON.parse(fs.readFileSync(sdkPkgPath, 'utf-8'));
206
192
  const sdkVer = sdkPkg.version;
207
193
  const parts = sdkVer.split('.').map(Number);
@@ -270,11 +256,50 @@ function setupEnvVar(home) {
270
256
  }
271
257
  console.log(' ⚠ 请重新打开终端或执行 source 使其生效');
272
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
+ }
273
299
  // ==================== Main ====================
274
300
  export async function cmdInit() {
275
301
  const p = resolvePaths();
276
302
  ensureDataDirs();
277
- // 检查服务是否在运行
278
303
  if (fs.existsSync(p.pid)) {
279
304
  const pid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
280
305
  try {
@@ -302,49 +327,11 @@ export async function cmdInit() {
302
327
  return;
303
328
  }
304
329
  console.log('📝 交互式配置\n');
305
- // feishu.appId
306
- let appId = '';
307
- while (!appId) {
308
- appId = (await ask(rl, ' 飞书 App ID: ')).trim();
309
- if (!appId)
310
- console.log(' ⚠ 不能为空');
311
- }
312
- // feishu.appSecret
313
- let appSecret = '';
314
- while (!appSecret) {
315
- appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
316
- if (!appSecret)
317
- console.log(' ⚠ 不能为空');
318
- }
319
- // 验证飞书凭证
320
- console.log(' 正在验证飞书凭证...');
321
- try {
322
- const lark = await import('@larksuiteoapi/node-sdk');
323
- const client = new lark.Client({ appId, appSecret });
324
- const res = await client.auth.tenantAccessToken.internal({
325
- data: { app_id: appId, app_secret: appSecret },
326
- });
327
- if (res.code === 0) {
328
- console.log(' ✓ 飞书凭证验证通过');
329
- }
330
- else {
331
- console.log(` ✗ 飞书凭证验证失败: ${res.msg}`);
332
- const answer = (await ask(rl, ' → 是否继续?[y/N] ')).trim().toLowerCase();
333
- if (answer !== 'y' && answer !== 'yes') {
334
- console.log(' 已取消');
335
- return;
336
- }
337
- }
338
- }
339
- catch (e) {
340
- console.log(` ⚠ 飞书凭证验证跳过: ${e.message?.slice(0, 100) || e}`);
341
- }
342
- // projects.defaultPath
330
+ // 通用配置
343
331
  const defaultSuggestion = path.join(os.homedir(), 'evolclaw-project');
344
332
  let defaultPath = (await ask(rl, ` 默认项目路径 [${defaultSuggestion}]: `)).trim();
345
- if (!defaultPath) {
333
+ if (!defaultPath)
346
334
  defaultPath = defaultSuggestion;
347
- }
348
335
  if (defaultPath.startsWith('~/')) {
349
336
  defaultPath = path.join(os.homedir(), defaultPath.slice(2));
350
337
  }
@@ -355,19 +342,61 @@ export async function cmdInit() {
355
342
  fs.mkdirSync(defaultPath, { recursive: true });
356
343
  console.log(` ✓ 已创建目录: ${defaultPath}`);
357
344
  }
358
- // anthropic.model
359
345
  const modelInput = (await ask(rl, ' 模型 [sonnet(默认)/opus/haiku]: ')).trim().toLowerCase();
360
346
  const model = ['opus', 'haiku'].includes(modelInput) ? modelInput : 'sonnet';
361
- // 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';
362
352
  const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
363
- config.feishu.appId = appId;
364
- config.feishu.appSecret = appSecret;
365
353
  config.projects.defaultPath = defaultPath;
366
354
  config.projects.list = { [path.basename(defaultPath)]: defaultPath };
367
- 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
+ }
368
398
  fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
369
399
  console.log(`\n✓ 已创建配置文件: ${p.config}`);
370
- // Setup EVOLCLAW_HOME in shell profile
371
400
  setupEnvVar(resolveRoot());
372
401
  }
373
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.1",
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
  }