evolclaw 2.8.3 → 3.0.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.
Files changed (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -10,48 +10,11 @@ import readline from 'readline';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
- import { execFile } from 'child_process';
14
- import { promisify } from 'util';
15
- import { resolvePaths } from '../paths.js';
16
- import { normalizeChannelInstances } from '../config.js';
17
13
  import { selectInstance } from './init.js';
18
- import { isWindows } from './cross-platform.js';
19
- import { AUN_CORE_SDK_PKG, MIN_AUN_CORE_SDK, resolveAunCoreSdkPkg, isAunSdkVersionOk, isValidAid, aidCreate, agentmdPut, buildInitialAgentMd, } from '../channels/aun-ops.js';
20
- const execFileAsync = promisify(execFile);
21
- export async function npmInstallGlobal(pkg) {
22
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
23
- const execOpts = { timeout: 180000, shell: isWindows };
24
- try {
25
- await execFileAsync(npmCmd, ['install', '-g', pkg], execOpts);
26
- }
27
- catch (e) {
28
- if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
29
- if (isWindows) {
30
- throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
31
- }
32
- await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 180000 });
33
- }
34
- else {
35
- throw e;
36
- }
37
- }
38
- }
39
- /** Dynamic import with auto-install fallback for optional dependencies */
40
- export async function requireOptional(pkg, autoInstall = true) {
41
- try {
42
- return await import(pkg);
43
- }
44
- catch (e) {
45
- if (e.code !== 'ERR_MODULE_NOT_FOUND' && e.code !== 'MODULE_NOT_FOUND')
46
- throw e;
47
- if (!autoInstall)
48
- throw new Error(`依赖 ${pkg} 未安装。请运行: npm install -g ${pkg}`);
49
- const { logger } = await import('./logger.js');
50
- logger.info(`正在安装可选依赖 ${pkg}...`);
51
- await npmInstallGlobal(pkg);
52
- return await import(pkg);
53
- }
54
- }
14
+ import { npmInstallGlobal } from '../utils/npm-ops.js';
15
+ import { loadAllAgents, loadAgent } from '../config-store.js';
16
+ import { agentChannelUpsert } from './agent.js';
17
+ import { AUN_CORE_SDK_PKG, MIN_AUN_CORE_SDK, resolveAunCoreSdkPkg, isAunSdkVersionOk, isValidAid, aidCreate, agentmdPut, buildInitialAgentMd, } from '../aun/aid/index.js';
55
18
  function ask(rl, question) {
56
19
  return new Promise(resolve => rl.question(question, resolve));
57
20
  }
@@ -221,38 +184,28 @@ export async function runFeishuQrFlow() {
221
184
  }
222
185
  }
223
186
  export async function cmdInitFeishu() {
224
- const p = resolvePaths();
225
- if (!fs.existsSync(p.config)) {
226
- console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
227
- return;
228
- }
229
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
230
- // Normalize existing instances and filter out placeholders
231
- const allInstances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
232
- const validInstances = [];
233
- for (let i = 0; i < allInstances.length; i++) {
234
- const inst = allInstances[i];
235
- if (!inst.appId || !inst.appSecret)
236
- continue;
237
- if (inst.appId.includes('your-') || inst.appId.includes('placeholder'))
238
- continue;
239
- if (inst.appSecret.includes('your-') || inst.appSecret.includes('placeholder'))
240
- continue;
241
- validInstances.push({ ...inst, originalIndex: i });
242
- }
187
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
188
+ let aid = null;
189
+ let agentConfig = null;
243
190
  let choice = null;
244
- if (validInstances.length > 0) {
245
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
246
- try {
247
- choice = await selectInstance(rl, 'feishu', validInstances);
248
- if (choice === null)
249
- return; // user cancelled
250
- }
251
- finally {
252
- rl.close();
191
+ try {
192
+ aid = await pickAgentForChannel(rl);
193
+ if (!aid)
194
+ return;
195
+ agentConfig = loadAgent(aid);
196
+ if (!agentConfig) {
197
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
198
+ return;
253
199
  }
200
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'feishu');
201
+ choice = await pickInstanceWithinAgent(rl, 'feishu', existing);
202
+ if (choice === null)
203
+ return;
204
+ }
205
+ finally {
206
+ rl.close();
254
207
  }
255
- console.log('正在获取飞书登录二维码...\n');
208
+ console.log('\n正在获取飞书登录二维码...\n');
256
209
  let result;
257
210
  try {
258
211
  const flowResult = await runQrRegistrationFlow();
@@ -261,12 +214,12 @@ export async function cmdInitFeishu() {
261
214
  return;
262
215
  }
263
216
  if (flowResult === SKIP) {
264
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
217
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
265
218
  try {
266
- result = await manualInput(rl);
219
+ result = await manualInput(rl2);
267
220
  }
268
221
  finally {
269
- rl.close();
222
+ rl2.close();
270
223
  }
271
224
  }
272
225
  else {
@@ -277,77 +230,20 @@ export async function cmdInitFeishu() {
277
230
  console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
278
231
  process.exit(1);
279
232
  }
280
- // Write config to the correct slot
281
- if (!config.channels)
282
- config.channels = {};
283
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.feishu)) {
284
- // Overwrite existing instance in array — use originalIndex to find the right slot
285
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
286
- config.channels.feishu[idx].appId = result.appId;
287
- config.channels.feishu[idx].appSecret = result.appSecret;
288
- config.channels.feishu[idx].enabled = true;
289
- if (result.openId)
290
- config.channels.feishu[idx].owner = result.openId;
291
- }
292
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.feishu)) {
293
- // Overwrite single-object
294
- config.channels.feishu = config.channels.feishu || {};
295
- config.channels.feishu.appId = result.appId;
296
- config.channels.feishu.appSecret = result.appSecret;
297
- config.channels.feishu.enabled = true;
298
- if (result.openId)
299
- config.channels.feishu.owner = result.openId;
300
- else
301
- delete config.channels.feishu.owner;
302
- }
303
- else if (choice && choice.action === 'add') {
304
- // Add new instance — upgrade to array if needed
305
- const newInst = {
306
- name: choice.name,
307
- appId: result.appId,
308
- appSecret: result.appSecret,
309
- enabled: true,
310
- ...(result.openId ? { owner: result.openId } : {}),
311
- };
312
- if (Array.isArray(config.channels.feishu)) {
313
- config.channels.feishu.push(newInst);
314
- }
315
- else if (config.channels.feishu) {
316
- // Upgrade single object to array
317
- const oldInst = { ...config.channels.feishu, name: config.channels.feishu.name || 'feishu' };
318
- config.channels.feishu = [oldInst, newInst];
319
- }
320
- else {
321
- config.channels.feishu = [newInst];
322
- }
323
- }
324
- else {
325
- // First instance — single object format (backward compat)
326
- config.channels.feishu = config.channels.feishu || {};
327
- config.channels.feishu.appId = result.appId;
328
- config.channels.feishu.appSecret = result.appSecret;
329
- config.channels.feishu.enabled = true;
330
- if (result.openId)
331
- config.channels.feishu.owner = result.openId;
332
- else
333
- delete config.channels.feishu.owner;
334
- }
335
- if (!config.channels.defaultChannel)
336
- config.channels.defaultChannel = 'feishu';
337
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
338
- console.log(`\n✅ 飞书连接成功!`);
233
+ const channel = {
234
+ type: 'feishu',
235
+ name: choice.name,
236
+ enabled: true,
237
+ appId: result.appId,
238
+ appSecret: result.appSecret,
239
+ ...(result.openId ? { owners: [result.openId] } : {}),
240
+ };
241
+ await commitChannel(aid, channel, choice.action);
339
242
  console.log(` App ID: ${result.appId}`);
340
- if (result.openId) {
243
+ if (result.openId)
341
244
  console.log(` Owner: ${result.openId}`);
342
- }
343
- if (result.domain !== 'unknown') {
245
+ if (result.domain !== 'unknown')
344
246
  console.log(` Domain: ${result.domain}`);
345
- }
346
- if (choice) {
347
- console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
348
- }
349
- console.log(` 配置已写入: ${p.config}`);
350
- console.log(`\n现在可以启动服务: evolclaw restart`);
351
247
  }
352
248
  // ==================== WeChat ====================
353
249
  const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
@@ -439,145 +335,41 @@ export async function runWechatQrFlow() {
439
335
  return null;
440
336
  }
441
337
  export async function cmdInitWechat() {
442
- const p = resolvePaths();
443
- if (!fs.existsSync(p.config)) {
444
- console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
445
- return;
446
- }
447
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
448
- // Normalize existing instances and filter out placeholders
449
- const allInstances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
450
- const validInstances = [];
451
- for (let i = 0; i < allInstances.length; i++) {
452
- const inst = allInstances[i];
453
- if (!inst.token)
454
- continue;
455
- if (inst.token.includes('your-') || inst.token.includes('placeholder'))
456
- continue;
457
- validInstances.push({ ...inst, originalIndex: i });
458
- }
338
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
339
+ let aid = null;
340
+ let agentConfig = null;
459
341
  let choice = null;
460
- if (validInstances.length > 0) {
461
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
462
- try {
463
- choice = await selectInstance(rl, 'wechat', validInstances);
464
- if (choice === null)
465
- return; // user cancelled
466
- }
467
- finally {
468
- rl.close();
469
- }
470
- }
471
- console.log('正在获取微信登录二维码...\n');
472
- const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
473
- // 终端显示二维码
474
342
  try {
475
- const qrterm = await import('qrcode-terminal');
476
- await new Promise(resolve => {
477
- qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
478
- console.log(qr);
479
- resolve();
480
- });
481
- });
343
+ aid = await pickAgentForChannel(rl);
344
+ if (!aid)
345
+ return;
346
+ agentConfig = loadAgent(aid);
347
+ if (!agentConfig) {
348
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
349
+ return;
350
+ }
351
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'wechat');
352
+ choice = await pickInstanceWithinAgent(rl, 'wechat', existing);
353
+ if (choice === null)
354
+ return;
482
355
  }
483
- catch {
484
- console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
356
+ finally {
357
+ rl.close();
485
358
  }
486
- console.log('请用微信扫描上方二维码...\n');
487
- const deadline = Date.now() + LOGIN_TIMEOUT_MS;
488
- let scannedPrinted = false;
489
- let currentPollUrl = DEFAULT_BASE_URL;
490
- while (Date.now() < deadline) {
491
- const status = await pollQRStatus(currentPollUrl, qrResp.qrcode);
492
- switch (status.status) {
493
- case 'wait':
494
- process.stdout.write('.');
495
- break;
496
- case 'scaned':
497
- if (!scannedPrinted) {
498
- console.log('\n\ud83d\udc40 已扫码,请在微信中确认...');
499
- scannedPrinted = true;
500
- }
501
- break;
502
- case 'scaned_but_redirect':
503
- if (status.redirect_host) {
504
- currentPollUrl = `https://${status.redirect_host}`;
505
- }
506
- break;
507
- case 'expired':
508
- console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
509
- process.exit(1);
510
- break;
511
- case 'confirmed': {
512
- if (!status.ilink_bot_id || !status.bot_token) {
513
- console.error('\n登录失败:服务器未返回完整信息');
514
- process.exit(1);
515
- }
516
- const baseUrl = status.baseurl || DEFAULT_BASE_URL;
517
- const token = status.bot_token;
518
- // Write config to the correct slot
519
- if (!config.channels)
520
- config.channels = {};
521
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.wechat)) {
522
- // Overwrite existing instance in array — use originalIndex to find the right slot
523
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
524
- config.channels.wechat[idx].enabled = true;
525
- config.channels.wechat[idx].baseUrl = baseUrl;
526
- config.channels.wechat[idx].token = token;
527
- }
528
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.wechat)) {
529
- // Overwrite single-object
530
- config.channels.wechat = config.channels.wechat || {};
531
- config.channels.wechat.enabled = true;
532
- config.channels.wechat.baseUrl = baseUrl;
533
- config.channels.wechat.token = token;
534
- }
535
- else if (choice && choice.action === 'add') {
536
- // Add new instance — upgrade to array if needed
537
- const newInst = {
538
- name: choice.name,
539
- enabled: true,
540
- baseUrl,
541
- token,
542
- };
543
- if (Array.isArray(config.channels.wechat)) {
544
- config.channels.wechat.push(newInst);
545
- }
546
- else if (config.channels.wechat) {
547
- // Upgrade single object to array
548
- const oldInst = { ...config.channels.wechat, name: config.channels.wechat.name || 'wechat' };
549
- config.channels.wechat = [oldInst, newInst];
550
- }
551
- else {
552
- config.channels.wechat = [newInst];
553
- }
554
- }
555
- else {
556
- // First instance — single object format (backward compat)
557
- config.channels.wechat = {
558
- enabled: true,
559
- baseUrl,
560
- token,
561
- };
562
- }
563
- if (!config.channels.defaultChannel)
564
- config.channels.defaultChannel = 'wechat';
565
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
566
- console.log(`\n✅ 微信连接成功!`);
567
- console.log(` Bot ID: ${status.ilink_bot_id}`);
568
- console.log(` User ID: ${status.ilink_user_id}`);
569
- if (choice) {
570
- console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
571
- }
572
- console.log(` 配置已写入: ${p.config}`);
573
- console.log(`\n现在可以启动服务: evolclaw restart`);
574
- return;
575
- }
576
- }
577
- await new Promise(r => setTimeout(r, 1000));
359
+ console.log('\n正在获取微信登录二维码...\n');
360
+ const result = await runWechatQrFlow();
361
+ if (!result) {
362
+ console.log('已取消');
363
+ return;
578
364
  }
579
- console.log('\n登录超时,请重新运行');
580
- process.exit(1);
365
+ const channel = {
366
+ type: 'wechat',
367
+ name: choice.name,
368
+ enabled: true,
369
+ baseUrl: result.baseUrl,
370
+ token: result.token,
371
+ };
372
+ await commitChannel(aid, channel, choice.action);
581
373
  }
582
374
  // ==================== AUN ====================
583
375
  //
@@ -726,90 +518,6 @@ export async function setupAunAid(rl, _config) {
726
518
  }
727
519
  return { aid, owner };
728
520
  }
729
- export async function cmdInitAun() {
730
- const p = resolvePaths();
731
- if (!fs.existsSync(p.config)) {
732
- console.log('❌ 配置文件不存在,请先运行 evolclaw init');
733
- return;
734
- }
735
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
736
- // Normalize existing instances and filter out placeholders
737
- const allInstances = normalizeChannelInstances(config.channels?.aun, 'aun');
738
- const validInstances = [];
739
- for (let i = 0; i < allInstances.length; i++) {
740
- const inst = allInstances[i];
741
- if (!inst.aid || inst.aid.includes('your-') || inst.aid.includes('placeholder'))
742
- continue;
743
- validInstances.push({ ...inst, originalIndex: i });
744
- }
745
- let choice = null;
746
- if (validInstances.length > 0) {
747
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
748
- try {
749
- choice = await selectInstance(rl, 'aun', validInstances);
750
- if (choice === null)
751
- return;
752
- }
753
- finally {
754
- rl.close();
755
- }
756
- }
757
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
758
- try {
759
- if (!await checkAunEnvironment(rl)) {
760
- return;
761
- }
762
- const result = await setupAunAid(rl, config);
763
- if (!result)
764
- return;
765
- if (!config.channels)
766
- config.channels = {};
767
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.aun)) {
768
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
769
- config.channels.aun[idx].aid = result.aid;
770
- config.channels.aun[idx].owner = result.owner;
771
- config.channels.aun[idx].enabled = true;
772
- }
773
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.aun)) {
774
- config.channels.aun = config.channels.aun || {};
775
- config.channels.aun.aid = result.aid;
776
- config.channels.aun.owner = result.owner;
777
- config.channels.aun.enabled = true;
778
- }
779
- else if (choice && choice.action === 'add') {
780
- const newInst = {
781
- name: choice.name,
782
- enabled: true,
783
- aid: result.aid,
784
- owner: result.owner,
785
- };
786
- if (Array.isArray(config.channels.aun)) {
787
- config.channels.aun.push(newInst);
788
- }
789
- else if (config.channels.aun) {
790
- const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
791
- config.channels.aun = [oldInst, newInst];
792
- }
793
- else {
794
- config.channels.aun = [newInst];
795
- }
796
- }
797
- else {
798
- config.channels.aun = {
799
- enabled: true,
800
- aid: result.aid,
801
- owner: result.owner,
802
- };
803
- }
804
- if (!config.channels.defaultChannel)
805
- config.channels.defaultChannel = 'aun';
806
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
807
- console.log('\n✓ AUN 配置已写入');
808
- }
809
- finally {
810
- rl.close();
811
- }
812
- }
813
521
  // ==================== DingTalk ====================
814
522
  const DINGTALK_BASE_URL = 'https://oapi.dingtalk.com';
815
523
  const DINGTALK_SOURCE = 'openClaw';
@@ -929,38 +637,28 @@ export async function runDingtalkQrFlowSimple() {
929
637
  }
930
638
  }
931
639
  export async function cmdInitDingtalk() {
932
- const p = resolvePaths();
933
- if (!fs.existsSync(p.config)) {
934
- console.log('❌ 配置文件不存在,请先运行 evolclaw init');
935
- return;
936
- }
937
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
938
- // Normalize existing instances and filter out placeholders
939
- const allInstances = normalizeChannelInstances(config.channels?.dingtalk, 'dingtalk');
940
- const validInstances = [];
941
- for (let i = 0; i < allInstances.length; i++) {
942
- const inst = allInstances[i];
943
- if (!inst.clientId || !inst.clientSecret)
944
- continue;
945
- if (inst.clientId.includes('your-') || inst.clientId.includes('placeholder'))
946
- continue;
947
- if (inst.clientSecret.includes('your-') || inst.clientSecret.includes('placeholder'))
948
- continue;
949
- validInstances.push({ ...inst, originalIndex: i });
950
- }
640
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
641
+ let aid = null;
642
+ let agentConfig = null;
951
643
  let choice = null;
952
- if (validInstances.length > 0) {
953
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
954
- try {
955
- choice = await selectInstance(rl, 'dingtalk', validInstances);
956
- if (choice === null)
957
- return;
958
- }
959
- finally {
960
- rl.close();
644
+ try {
645
+ aid = await pickAgentForChannel(rl);
646
+ if (!aid)
647
+ return;
648
+ agentConfig = loadAgent(aid);
649
+ if (!agentConfig) {
650
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
651
+ return;
961
652
  }
653
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'dingtalk');
654
+ choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing);
655
+ if (choice === null)
656
+ return;
962
657
  }
963
- console.log('正在获取钉钉登录二维码...\n');
658
+ finally {
659
+ rl.close();
660
+ }
661
+ console.log('\n正在获取钉钉登录二维码...\n');
964
662
  let result;
965
663
  try {
966
664
  const flowResult = await runDingtalkQrFlow();
@@ -969,25 +667,25 @@ export async function cmdInitDingtalk() {
969
667
  return;
970
668
  }
971
669
  if (flowResult === SKIP) {
972
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
670
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
973
671
  try {
974
672
  console.log('\n手动输入模式:\n');
975
673
  let clientId = '';
976
674
  while (!clientId) {
977
- clientId = (await ask(rl, ' 钉钉 Client ID (AppKey): ')).trim();
675
+ clientId = (await ask(rl2, ' 钉钉 Client ID (AppKey): ')).trim();
978
676
  if (!clientId)
979
677
  console.log(' ⚠ 不能为空');
980
678
  }
981
679
  let clientSecret = '';
982
680
  while (!clientSecret) {
983
- clientSecret = (await ask(rl, ' 钉钉 Client Secret (AppSecret): ')).trim();
681
+ clientSecret = (await ask(rl2, ' 钉钉 Client Secret (AppSecret): ')).trim();
984
682
  if (!clientSecret)
985
683
  console.log(' ⚠ 不能为空');
986
684
  }
987
685
  result = { clientId, clientSecret };
988
686
  }
989
687
  finally {
990
- rl.close();
688
+ rl2.close();
991
689
  }
992
690
  }
993
691
  else {
@@ -998,56 +696,15 @@ export async function cmdInitDingtalk() {
998
696
  console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
999
697
  process.exit(1);
1000
698
  }
1001
- // Write config to the correct slot
1002
- if (!config.channels)
1003
- config.channels = {};
1004
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.dingtalk)) {
1005
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
1006
- config.channels.dingtalk[idx].clientId = result.clientId;
1007
- config.channels.dingtalk[idx].clientSecret = result.clientSecret;
1008
- config.channels.dingtalk[idx].enabled = true;
1009
- }
1010
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.dingtalk)) {
1011
- config.channels.dingtalk = config.channels.dingtalk || {};
1012
- config.channels.dingtalk.clientId = result.clientId;
1013
- config.channels.dingtalk.clientSecret = result.clientSecret;
1014
- config.channels.dingtalk.enabled = true;
1015
- }
1016
- else if (choice && choice.action === 'add') {
1017
- const newInst = {
1018
- name: choice.name,
1019
- clientId: result.clientId,
1020
- clientSecret: result.clientSecret,
1021
- enabled: true,
1022
- };
1023
- if (Array.isArray(config.channels.dingtalk)) {
1024
- config.channels.dingtalk.push(newInst);
1025
- }
1026
- else if (config.channels.dingtalk) {
1027
- const oldInst = { ...config.channels.dingtalk, name: config.channels.dingtalk.name || 'dingtalk' };
1028
- config.channels.dingtalk = [oldInst, newInst];
1029
- }
1030
- else {
1031
- config.channels.dingtalk = [newInst];
1032
- }
1033
- }
1034
- else {
1035
- config.channels.dingtalk = {
1036
- clientId: result.clientId,
1037
- clientSecret: result.clientSecret,
1038
- enabled: true,
1039
- };
1040
- }
1041
- if (!config.channels.defaultChannel)
1042
- config.channels.defaultChannel = 'dingtalk';
1043
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
1044
- console.log(`\n✅ 钉钉连接成功!`);
699
+ const channel = {
700
+ type: 'dingtalk',
701
+ name: choice.name,
702
+ enabled: true,
703
+ clientId: result.clientId,
704
+ clientSecret: result.clientSecret,
705
+ };
706
+ await commitChannel(aid, channel, choice.action);
1045
707
  console.log(` Client ID: ${result.clientId}`);
1046
- if (choice) {
1047
- console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
1048
- }
1049
- console.log(` 配置已写入: ${p.config}`);
1050
- console.log(`\n现在可以启动服务: evolclaw restart`);
1051
708
  }
1052
709
  // ==================== QQBot ====================
1053
710
  const QQBOT_PORTAL_HOST = 'q.qq.com';
@@ -1185,38 +842,28 @@ export async function runQQBotBindFlowSimple() {
1185
842
  }
1186
843
  }
1187
844
  export async function cmdInitQQBot() {
1188
- const p = resolvePaths();
1189
- if (!fs.existsSync(p.config)) {
1190
- console.log('❌ 配置文件不存在,请先运行 evolclaw init');
1191
- return;
1192
- }
1193
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
1194
- // Normalize existing instances and filter out placeholders
1195
- const allInstances = normalizeChannelInstances(config.channels?.qqbot, 'qqbot');
1196
- const validInstances = [];
1197
- for (let i = 0; i < allInstances.length; i++) {
1198
- const inst = allInstances[i];
1199
- if (!inst.appId || !inst.clientSecret)
1200
- continue;
1201
- if (inst.appId.includes('your-') || inst.appId.includes('placeholder'))
1202
- continue;
1203
- if (inst.clientSecret.includes('your-') || inst.clientSecret.includes('placeholder'))
1204
- continue;
1205
- validInstances.push({ ...inst, originalIndex: i });
1206
- }
845
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
846
+ let aid = null;
847
+ let agentConfig = null;
1207
848
  let choice = null;
1208
- if (validInstances.length > 0) {
1209
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1210
- try {
1211
- choice = await selectInstance(rl, 'qqbot', validInstances);
1212
- if (choice === null)
1213
- return;
1214
- }
1215
- finally {
1216
- rl.close();
849
+ try {
850
+ aid = await pickAgentForChannel(rl);
851
+ if (!aid)
852
+ return;
853
+ agentConfig = loadAgent(aid);
854
+ if (!agentConfig) {
855
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
856
+ return;
1217
857
  }
858
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'qqbot');
859
+ choice = await pickInstanceWithinAgent(rl, 'qqbot', existing);
860
+ if (choice === null)
861
+ return;
862
+ }
863
+ finally {
864
+ rl.close();
1218
865
  }
1219
- console.log('正在创建 QQ 机器人绑定任务...\n');
866
+ console.log('\n正在创建 QQ 机器人绑定任务...\n');
1220
867
  let result;
1221
868
  try {
1222
869
  const flowResult = await runQQBotBindFlow();
@@ -1225,25 +872,25 @@ export async function cmdInitQQBot() {
1225
872
  return;
1226
873
  }
1227
874
  if (flowResult === SKIP) {
1228
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
875
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
1229
876
  try {
1230
877
  console.log('\n手动输入模式:\n');
1231
878
  let appId = '';
1232
879
  while (!appId) {
1233
- appId = (await ask(rl, ' QQ 机器人 App ID: ')).trim();
880
+ appId = (await ask(rl2, ' QQ 机器人 App ID: ')).trim();
1234
881
  if (!appId)
1235
882
  console.log(' ⚠ 不能为空');
1236
883
  }
1237
884
  let clientSecret = '';
1238
885
  while (!clientSecret) {
1239
- clientSecret = (await ask(rl, ' QQ 机器人 Client Secret: ')).trim();
886
+ clientSecret = (await ask(rl2, ' QQ 机器人 Client Secret: ')).trim();
1240
887
  if (!clientSecret)
1241
888
  console.log(' ⚠ 不能为空');
1242
889
  }
1243
890
  result = { appId, clientSecret };
1244
891
  }
1245
892
  finally {
1246
- rl.close();
893
+ rl2.close();
1247
894
  }
1248
895
  }
1249
896
  else {
@@ -1254,163 +901,121 @@ export async function cmdInitQQBot() {
1254
901
  console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
1255
902
  process.exit(1);
1256
903
  }
1257
- // Write config to the correct slot
1258
- if (!config.channels)
1259
- config.channels = {};
1260
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.qqbot)) {
1261
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
1262
- config.channels.qqbot[idx].appId = result.appId;
1263
- config.channels.qqbot[idx].clientSecret = result.clientSecret;
1264
- config.channels.qqbot[idx].enabled = true;
1265
- }
1266
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.qqbot)) {
1267
- config.channels.qqbot = config.channels.qqbot || {};
1268
- config.channels.qqbot.appId = result.appId;
1269
- config.channels.qqbot.clientSecret = result.clientSecret;
1270
- config.channels.qqbot.enabled = true;
1271
- }
1272
- else if (choice && choice.action === 'add') {
1273
- const newInst = {
1274
- name: choice.name,
1275
- appId: result.appId,
1276
- clientSecret: result.clientSecret,
1277
- enabled: true,
1278
- };
1279
- if (Array.isArray(config.channels.qqbot)) {
1280
- config.channels.qqbot.push(newInst);
1281
- }
1282
- else if (config.channels.qqbot) {
1283
- const oldInst = { ...config.channels.qqbot, name: config.channels.qqbot.name || 'qqbot' };
1284
- config.channels.qqbot = [oldInst, newInst];
1285
- }
1286
- else {
1287
- config.channels.qqbot = [newInst];
1288
- }
1289
- }
1290
- else {
1291
- config.channels.qqbot = {
1292
- appId: result.appId,
1293
- clientSecret: result.clientSecret,
1294
- enabled: true,
1295
- };
1296
- }
1297
- if (!config.channels.defaultChannel)
1298
- config.channels.defaultChannel = 'qqbot';
1299
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
1300
- console.log(`\n✅ QQ 机器人绑定成功!`);
904
+ const channel = {
905
+ type: 'qqbot',
906
+ name: choice.name,
907
+ enabled: true,
908
+ appId: result.appId,
909
+ clientSecret: result.clientSecret,
910
+ };
911
+ await commitChannel(aid, channel, choice.action);
1301
912
  console.log(` App ID: ${result.appId}`);
1302
- if (choice) {
1303
- console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
1304
- }
1305
- console.log(` 配置已写入: ${p.config}`);
1306
- console.log(`\n现在可以启动服务: evolclaw restart`);
1307
913
  }
1308
914
  // ==================== WeCom (企业微信) ====================
1309
915
  export async function cmdInitWecom() {
1310
- const p = resolvePaths();
1311
- if (!fs.existsSync(p.config)) {
1312
- console.log('❌ 配置文件不存在,请先运行 evolclaw init');
1313
- return;
1314
- }
1315
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
1316
- // Normalize existing instances and filter out placeholders
1317
- const allInstances = normalizeChannelInstances(config.channels?.wecom, 'wecom');
1318
- const validInstances = [];
1319
- for (let i = 0; i < allInstances.length; i++) {
1320
- const inst = allInstances[i];
1321
- if (!inst.botId || !inst.secret)
1322
- continue;
1323
- if (inst.botId.includes('your-') || inst.botId.includes('placeholder'))
1324
- continue;
1325
- if (inst.secret.includes('your-') || inst.secret.includes('placeholder'))
1326
- continue;
1327
- validInstances.push({ ...inst, originalIndex: i });
1328
- }
1329
- let choice = null;
1330
- if (validInstances.length > 0) {
1331
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1332
- try {
1333
- choice = await selectInstance(rl, 'wecom', validInstances);
1334
- if (choice === null)
1335
- return;
1336
- }
1337
- finally {
1338
- rl.close();
1339
- }
1340
- }
1341
- // WeCom uses manual input only (no QR flow)
1342
916
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1343
- let result;
917
+ let aid = null;
918
+ let agentConfig = null;
919
+ let choice = null;
920
+ let botId = '';
921
+ let secret = '';
1344
922
  try {
1345
- console.log('企业微信 AI Bot 配置\n');
923
+ aid = await pickAgentForChannel(rl);
924
+ if (!aid)
925
+ return;
926
+ agentConfig = loadAgent(aid);
927
+ if (!agentConfig) {
928
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
929
+ return;
930
+ }
931
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'wecom');
932
+ choice = await pickInstanceWithinAgent(rl, 'wecom', existing);
933
+ if (choice === null)
934
+ return;
935
+ console.log('\n企业微信 AI Bot 配置');
1346
936
  console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1347
- let botId = '';
1348
937
  while (!botId) {
1349
938
  botId = (await ask(rl, ' Bot ID: ')).trim();
1350
939
  if (!botId)
1351
940
  console.log(' ⚠ 不能为空');
1352
941
  }
1353
- let secret = '';
1354
942
  while (!secret) {
1355
943
  secret = (await ask(rl, ' Secret: ')).trim();
1356
944
  if (!secret)
1357
945
  console.log(' ⚠ 不能为空');
1358
946
  }
1359
- result = { botId, secret };
1360
947
  }
1361
948
  finally {
1362
949
  rl.close();
1363
950
  }
1364
- // Write config to the correct slot
1365
- if (!config.channels)
1366
- config.channels = {};
1367
- if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.wecom)) {
1368
- const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
1369
- config.channels.wecom[idx].botId = result.botId;
1370
- config.channels.wecom[idx].secret = result.secret;
1371
- config.channels.wecom[idx].enabled = true;
1372
- }
1373
- else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.wecom)) {
1374
- config.channels.wecom = config.channels.wecom || {};
1375
- config.channels.wecom.botId = result.botId;
1376
- config.channels.wecom.secret = result.secret;
1377
- config.channels.wecom.enabled = true;
1378
- }
1379
- else if (choice && choice.action === 'add') {
1380
- const newInst = {
1381
- name: choice.name,
1382
- botId: result.botId,
1383
- secret: result.secret,
1384
- enabled: true,
1385
- };
1386
- if (Array.isArray(config.channels.wecom)) {
1387
- config.channels.wecom.push(newInst);
1388
- }
1389
- else if (config.channels.wecom) {
1390
- const oldInst = { ...config.channels.wecom, name: config.channels.wecom.name || 'wecom' };
1391
- config.channels.wecom = [oldInst, newInst];
1392
- }
1393
- else {
1394
- config.channels.wecom = [newInst];
951
+ const channel = {
952
+ type: 'wecom',
953
+ name: choice.name,
954
+ enabled: true,
955
+ botId,
956
+ secret,
957
+ };
958
+ await commitChannel(aid, channel, choice.action);
959
+ console.log(` Bot ID: ${botId}`);
960
+ }
961
+ // ==================== Shared helpers for per-agent init <channel> ====================
962
+ /**
963
+ * Pick the target agent for an `evolclaw init <channel>` flow.
964
+ *
965
+ * - 0 agents → print guidance and return null
966
+ * - ≥1 letter menu; with 1 agent the prompt accepts Enter (defaults to 'a')
967
+ */
968
+ async function pickAgentForChannel(rl) {
969
+ const { agents } = loadAllAgents();
970
+ if (agents.length === 0) {
971
+ console.log('❌ 暂无 agent,请先创建:');
972
+ console.log(' evolclaw agent new <aid>.agentid.pub');
973
+ return null;
974
+ }
975
+ const letters = 'abcdefghijklmnopqrstuvwxyz';
976
+ console.log(`共 ${agents.length} 个 agent:`);
977
+ for (let i = 0; i < agents.length; i++) {
978
+ console.log(` ${letters[i]}. ${agents[i].aid}`);
979
+ }
980
+ const valid = letters.slice(0, agents.length).split('');
981
+ const promptSuffix = agents.length === 1 ? ' [a]' : '';
982
+ let choice = '';
983
+ while (!valid.includes(choice)) {
984
+ choice = (await ask(rl, `请选择${promptSuffix}: `)).trim().toLowerCase();
985
+ if (agents.length === 1 && choice === '')
986
+ choice = 'a';
987
+ if (!valid.includes(choice)) {
988
+ console.log(`无效选择,请输入 ${valid.join('/')}`);
1395
989
  }
1396
990
  }
1397
- else {
1398
- config.channels.wecom = {
1399
- botId: result.botId,
1400
- secret: result.secret,
1401
- enabled: true,
1402
- };
991
+ return agents[letters.indexOf(choice)].aid;
992
+ }
993
+ /**
994
+ * Pick "add new instance" or "overwrite existing instance" within a single agent
995
+ * for the given channel type. Returns null if user cancels.
996
+ *
997
+ * If existing.length === 0 → returns { action:'add', name:'main' } directly.
998
+ */
999
+ async function pickInstanceWithinAgent(rl, channelType, existing) {
1000
+ if (existing.length === 0) {
1001
+ return { action: 'add', name: 'main' };
1403
1002
  }
1404
- if (!config.channels.defaultChannel)
1405
- config.channels.defaultChannel = 'wecom';
1406
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
1407
- console.log(`\n✅ 企业微信 AI Bot 配置成功!`);
1408
- console.log(` Bot ID: ${result.botId}`);
1409
- if (choice) {
1410
- console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
1003
+ const view = existing.map((c, i) => ({ ...c, name: c.name, originalIndex: i }));
1004
+ return await selectInstance(rl, channelType, view);
1005
+ }
1006
+ /**
1007
+ * Persist the new/overwritten channel and trigger hot-reload.
1008
+ */
1009
+ async function commitChannel(aid, channel, mode) {
1010
+ const result = await agentChannelUpsert({ aid, channel, mode });
1011
+ if (result.ok !== true) {
1012
+ console.error(`❌ ${result.error || 'channel upsert failed'}`);
1013
+ return;
1411
1014
  }
1412
- console.log(` 配置已写入: ${p.config}`);
1413
- console.log(`\n现在可以启动服务: evolclaw restart`);
1015
+ console.log(`\n✓ 已写入 agents/${aid}/config.json`);
1016
+ console.log(result.reloaded
1017
+ ? ' ✓ 已热重载'
1018
+ : ' ⚠ 服务未运行(或热重载失败),下次 evolclaw start 时生效');
1414
1019
  }
1415
1020
  export function getChannelCredentialCollector(type) {
1416
1021
  switch (type) {
@@ -1428,21 +1033,6 @@ export function getChannelCredentialCollector(type) {
1428
1033
  return null;
1429
1034
  return { baseUrl: result.baseUrl, token: result.token, enabled: true };
1430
1035
  };
1431
- case 'aun':
1432
- return async () => {
1433
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1434
- try {
1435
- if (!await checkAunEnvironment(rl))
1436
- return null;
1437
- const result = await setupAunAid(rl, {});
1438
- if (!result)
1439
- return null;
1440
- return { aid: result.aid, owner: result.owner, enabled: true };
1441
- }
1442
- finally {
1443
- rl.close();
1444
- }
1445
- };
1446
1036
  case 'dingtalk':
1447
1037
  return async () => {
1448
1038
  const result = await runDingtalkQrFlowSimple();