evolclaw 2.5.0 → 2.5.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.
@@ -259,14 +259,7 @@ export class AgentRunner {
259
259
  }
260
260
  // 等待用户交互
261
261
  const answer = await new Promise((resolve) => {
262
- const timer = setTimeout(() => {
263
- permCtx?.interactionRouter?.cancel(requestId);
264
- permCtx?.cancelIntercept?.(sessionId);
265
- logger.info(`[AgentRunner] AskUserQuestion timeout for ${requestId}`);
266
- resolve(null);
267
- }, 5 * 60 * 1000);
268
262
  permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
269
- clearTimeout(timer);
270
263
  if (action === 'cancel') {
271
264
  resolve(null);
272
265
  }
@@ -292,7 +285,7 @@ export class AgentRunner {
292
285
  });
293
286
  });
294
287
  if (answer === null) {
295
- // 超时或取消,自动选第一项
288
+ // 取消,自动选第一项
296
289
  const firstLabel = q.options[0]?.label || '';
297
290
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
298
291
  }
@@ -323,6 +316,54 @@ export class AgentRunner {
323
316
  const updatedInput = { ...input, answers };
324
317
  return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
325
318
  }
319
+ /**
320
+ * 处理 ExitPlanMode 工具调用:plan mode 审批,等待用户批准后才继续执行
321
+ */
322
+ async handleExitPlanMode(sessionId, input, options) {
323
+ const permCtx = this.permissionContexts.get(sessionId);
324
+ const sendPrompt = this.sendPromptFn;
325
+ // 无交互上下文,直接 allow(防御性兜底)
326
+ if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId || !sendPrompt) {
327
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
328
+ }
329
+ const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
330
+ const interaction = {
331
+ type: 'interaction',
332
+ id: requestId,
333
+ kind: {
334
+ kind: 'action',
335
+ title: '📋 计划审批',
336
+ body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
337
+ buttons: [
338
+ { key: 'approve', label: '✅ 批准执行', style: 'primary' },
339
+ { key: 'reject', label: '❌ 拒绝', style: 'danger' },
340
+ ],
341
+ },
342
+ channelId: permCtx.channelId,
343
+ sessionId,
344
+ };
345
+ let cardSent = false;
346
+ try {
347
+ const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
348
+ cardSent = !!result;
349
+ }
350
+ catch (err) {
351
+ logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
352
+ }
353
+ if (!cardSent) {
354
+ await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n回复 /plan approve 批准执行 | /plan reject 拒绝');
355
+ }
356
+ return new Promise((resolve) => {
357
+ permCtx?.interactionRouter?.register(requestId, sessionId, (action) => {
358
+ if (action === 'approve') {
359
+ resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
360
+ }
361
+ else {
362
+ resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
363
+ }
364
+ });
365
+ });
366
+ }
326
367
  /**
327
368
  * SDK 原始事件 → 标准 AgentEvent 转换
328
369
  * 所有 SDK 特有的事件类型引用封装在此方法内
@@ -538,6 +579,10 @@ export class AgentRunner {
538
579
  if (toolName === 'AskUserQuestion') {
539
580
  return await this.handleAskUserQuestion(sessionId, input, options);
540
581
  }
582
+ // 特殊处理:ExitPlanMode 工具(plan mode 审批)
583
+ if (toolName === 'ExitPlanMode') {
584
+ return await this.handleExitPlanMode(sessionId, input, options);
585
+ }
541
586
  // bypass 模式:一律 allow
542
587
  if (this.permissionMode === 'bypass') {
543
588
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
@@ -2,6 +2,7 @@ import { AUNClient, GatewayDiscovery } from '@eleans/aun-core-sdk';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
+ import os from 'os';
5
6
  import { logger, localTimestamp } from '../utils/logger.js';
6
7
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
7
8
  import { resolvePaths } from '../paths.js';
@@ -171,7 +172,7 @@ export class AUNChannel {
171
172
  }
172
173
  if (!gateway) {
173
174
  logger.error('[AUN] Cannot resolve gateway URL from AID');
174
- return;
175
+ throw new Error('Cannot resolve gateway URL from AID');
175
176
  }
176
177
  logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
177
178
  // Create client with FileSecretStore (AES-256-GCM)
@@ -251,7 +252,7 @@ export class AUNChannel {
251
252
  if (!accessToken) {
252
253
  logger.error(`[AUN] No accessToken fallback available, scheduling retry`);
253
254
  this.scheduleReconnect();
254
- return;
255
+ throw new Error('Authentication failed and no accessToken fallback available');
255
256
  }
256
257
  logger.warn(`[AUN] Using accessToken fallback`);
257
258
  }
@@ -275,11 +276,97 @@ export class AUNChannel {
275
276
  }
276
277
  }
277
278
  logger.info(`[AUN] Connected as ${this._aid}`);
279
+ // Send welcome message to owner after first connection
280
+ await this.sendWelcomeMessage();
278
281
  }
279
282
  catch (e) {
280
283
  logger.error(`[AUN] Connection failed: ${e}`);
281
284
  this.scheduleReconnect();
282
- return;
285
+ throw e;
286
+ }
287
+ }
288
+ async sendWelcomeMessage() {
289
+ try {
290
+ const owner = this.config.owner;
291
+ if (!owner) {
292
+ logger.info('[AUN] No owner configured, skipping welcome message');
293
+ return;
294
+ }
295
+ // Check agent.md initialized field
296
+ const aid = this.config.aid;
297
+ const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
298
+ const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
299
+ if (!fs.existsSync(agentMdPath)) {
300
+ logger.warn('[AUN] agent.md not found, skipping welcome message');
301
+ return;
302
+ }
303
+ const agentMdContent = fs.readFileSync(agentMdPath, 'utf-8');
304
+ const match = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
305
+ if (!match) {
306
+ logger.warn('[AUN] agent.md frontmatter not found');
307
+ return;
308
+ }
309
+ const frontmatter = match[1];
310
+ const initializedMatch = frontmatter.match(/^initialized:\s*(true|false)/m);
311
+ if (!initializedMatch || initializedMatch[1] === 'true') {
312
+ logger.info('[AUN] Agent already initialized, skipping welcome message');
313
+ return;
314
+ }
315
+ // Generate new agent.md with proper fields
316
+ const ownerShortId = owner.split('@')[0].slice(0, 8);
317
+ const newAgentMd = `---
318
+ aid: "${aid}"
319
+ name: "${ownerShortId}的Evol助手"
320
+ type: "codeagent"
321
+ version: "1.0.0"
322
+ description: "EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道"
323
+ tags:
324
+ - evolclaw
325
+ - ai-agent
326
+ - gateway
327
+ initialized: true
328
+ ---
329
+
330
+ # ${ownerShortId}的Evol助手
331
+
332
+ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
333
+ `;
334
+ // Write locally
335
+ fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
336
+ logger.info('[AUN] Updated agent.md with initialized=true');
337
+ // Publish to AUN network via auth.uploadAgentMd
338
+ try {
339
+ await this.client.auth.uploadAgentMd(newAgentMd);
340
+ logger.info('[AUN] Published agent.md to AUN network');
341
+ }
342
+ catch (e) {
343
+ logger.warn(`[AUN] Failed to publish agent.md: ${e}`);
344
+ }
345
+ // Send welcome message
346
+ const welcomeText = `🎉 欢迎使用 EvolClaw!
347
+
348
+ 我是您的 AI Agent 网关,已成功连接到 AUN 网络。
349
+
350
+ 📋 **日常使用方法**:
351
+
352
+ 1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
353
+ 2. **查看帮助**:发送 \`/help\` 查看所有可用命令
354
+ 3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
355
+ 4. **查看状态**:发送 \`/status\` 查看当前会话状态
356
+ 5. **查看 Agent 信息**:发送 \`/agentmd\` 查看 agent.md 内容
357
+ 6. **会话管理**:发送 \`/session\` 查看和切换会话
358
+
359
+ 💡 **提示**:
360
+ - 直接发送消息即可与 Claude/Codex 对话
361
+ - 支持多项目会话管理,每个项目独立会话
362
+ - 所有命令以 \`/\` 开头
363
+
364
+ 现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
365
+ await this.sendMessage(owner, welcomeText);
366
+ logger.info(`[AUN] Welcome message sent to owner: ${owner}`);
367
+ }
368
+ catch (e) {
369
+ logger.warn(`[AUN] Failed to send welcome message: ${e}`);
283
370
  }
284
371
  }
285
372
  // ── Event handlers ──────────────────────────────────────────
@@ -939,6 +1026,7 @@ export class AUNChannelPlugin {
939
1026
  accessToken: inst.accessToken,
940
1027
  flushDelay: inst.flushDelay,
941
1028
  encryptionSeed: inst.encryptionSeed,
1029
+ owner: inst.owner,
942
1030
  aunTrace: config.debug?.aunTrace,
943
1031
  });
944
1032
  const adapter = {
package/dist/cli.js CHANGED
@@ -1306,6 +1306,10 @@ async function cmdCtl(args) {
1306
1306
  }
1307
1307
  }
1308
1308
  // ==================== Main ====================
1309
+ function getArgValue(args, flag) {
1310
+ const idx = args.indexOf(flag);
1311
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1312
+ }
1309
1313
  export async function main(args) {
1310
1314
  const cmd = args[0] || 'start';
1311
1315
  switch (cmd) {
@@ -1329,7 +1333,19 @@ export async function main(args) {
1329
1333
  await cmdInitWecom();
1330
1334
  }
1331
1335
  else {
1332
- await cmdInit();
1336
+ const nonInteractive = args.includes('--non-interactive');
1337
+ if (nonInteractive) {
1338
+ await cmdInit({
1339
+ nonInteractive: true,
1340
+ defaultPath: getArgValue(args, '--default-path') || process.cwd(),
1341
+ channel: getArgValue(args, '--channel') || 'aun',
1342
+ aunAid: getArgValue(args, '--aun-aid'),
1343
+ aunOwner: getArgValue(args, '--aun-owner'),
1344
+ });
1345
+ }
1346
+ else {
1347
+ await cmdInit();
1348
+ }
1333
1349
  }
1334
1350
  break;
1335
1351
  case 'start':
@@ -112,7 +112,7 @@ const aliases = {
112
112
  '/rw': '/rewind'
113
113
  };
114
114
  // 命令快速路径前缀(所有命令都不进入消息队列)
115
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity'];
115
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity'];
116
116
  export class CommandHandler {
117
117
  sessionManager;
118
118
  config;
@@ -338,23 +338,23 @@ export class CommandHandler {
338
338
  items.push({
339
339
  group: '项目管理',
340
340
  commands: [
341
- { cmd: '/pwd', label: '显示当前项目路径' },
342
- { cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
343
- ...(isOwner ? [{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' }] : []),
341
+ { cmd: '/pwd', label: '显示当前项目路径', desc: '查看当前会话绑定的项目目录' },
342
+ { cmd: '/p', label: '列出或切换项目', desc: '切换到其他已配置的项目', next: { type: 'select', dynamic: true } },
343
+ ...(isOwner ? [{ cmd: '/bind', label: '绑定新项目目录', desc: '将当前会话绑定到指定项目路径', next: { type: 'text' } }] : []),
344
344
  ]
345
345
  });
346
346
  }
347
347
  items.push({
348
348
  group: '会话管理',
349
349
  commands: [
350
- { cmd: '/new', args: '[name]', label: '创建新会话(清空历史请用此命令)' },
351
- { cmd: '/s', args: '[cli|name|index|uuid]', label: '列出或切换会话(cli 查看未导入的 CLI 会话)' },
352
- { cmd: '/name', args: '<name>', label: '重命名当前会话' },
353
- { cmd: '/del', args: '<name>', label: '删除指定会话' },
350
+ { cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
351
+ { cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
352
+ { cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
353
+ { cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
354
354
  ...(isAdmin ? [
355
- { cmd: '/fork', args: '[name]', label: '分支当前会话' },
356
- { cmd: '/rewind', args: '[N] [chat|file|all]', label: '查看历史/撤销指定轮次 (别名: /rw)' },
357
- { cmd: '/compact', label: '压缩会话上下文' },
355
+ { cmd: '/fork', label: '分支当前会话', desc: '基于当前会话创建独立分支', next: { type: 'text' } },
356
+ { cmd: '/rewind', label: '查看历史/撤销指定轮次', desc: '回退会话到指定轮次,可选择撤销文件改动' },
357
+ { cmd: '/compact', label: '压缩会话上下文', desc: '将长对话压缩为摘要以节省 token' },
358
358
  ] : []),
359
359
  ]
360
360
  });
@@ -362,31 +362,55 @@ export class CommandHandler {
362
362
  items.push({
363
363
  group: 'Agent 与模型',
364
364
  commands: [
365
- { cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
366
- { cmd: '/model', args: '[model]', label: '查看或切换模型' },
367
- { cmd: '/effort', args: '[level]', label: '查看或切换推理强度' },
365
+ { cmd: '/agent', label: '切换 Agent 后端', desc: '切换当前会话使用的 AI 后端', next: { type: 'select', dynamic: true } },
366
+ { cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
367
+ { cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
368
+ { value: 'low', label: 'Low' },
369
+ { value: 'medium', label: 'Medium' },
370
+ { value: 'high', label: 'High' },
371
+ { value: 'max', label: 'Max' },
372
+ ] } },
368
373
  ]
369
374
  });
370
375
  items.push({
371
376
  group: '权限管理',
372
377
  commands: [
373
- { cmd: '/perm', args: isOwner ? '[mode|allow|always|deny]' : '[allow|always|deny]', label: '权限模式管理' },
378
+ { cmd: '/perm', label: '权限模式管理', desc: '控制工具调用的审批策略', next: { type: 'select', items: [
379
+ ...(isOwner ? [
380
+ { value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
381
+ { value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
382
+ { value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
383
+ { value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
384
+ { value: 'request', label: '请求模式', desc: '所有操作均需审批' },
385
+ { value: 'noask', label: '静默模式', desc: '不弹出审批,自动拒绝未授权操作' },
386
+ ] : []),
387
+ { value: 'allow', label: '允许此操作', desc: '本次允许当前待审批操作' },
388
+ { value: 'always', label: '始终允许', desc: '永久允许同类操作' },
389
+ { value: 'deny', label: '拒绝此操作', desc: '拒绝当前待审批操作' },
390
+ ] } },
374
391
  ]
375
392
  });
376
393
  items.push({
377
394
  group: '运维',
378
395
  commands: [
379
- { cmd: '/status', label: '显示会话状态' },
380
- { cmd: '/stop', label: '中断当前任务' },
381
- { cmd: '/check', label: '检查渠道状态' },
382
- { cmd: '/activity', args: '[all|dm|owner|none]', label: '查看/控制中间输出显示模式' },
396
+ { cmd: '/status', label: '显示会话状态', desc: '查看当前会话、项目、Agent 的详细状态' },
397
+ { cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' },
398
+ { cmd: '/check', label: '检查渠道状态', desc: '检查各消息渠道的连接健康状态' },
399
+ { cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
400
+ { value: 'all', label: '全部显示', desc: '所有用户均可见中间输出' },
401
+ { value: 'dm', label: '仅私聊', desc: '仅私聊中显示中间输出' },
402
+ { value: 'owner', label: '仅 owner 私聊', desc: '仅 owner 的私聊中显示' },
403
+ { value: 'none', label: '不显示', desc: '关闭所有中间输出' },
404
+ ] } },
383
405
  ...(isAdmin ? [
384
- { cmd: '/restart', args: '<channel>', label: '重连指定渠道' },
406
+ { cmd: '/restart', label: '重启/重连', desc: '重启服务或重连指定渠道', next: { type: 'select', dynamic: true } },
385
407
  ] : []),
386
408
  ...(isOwner ? [
387
- { cmd: '/restart', label: '重启服务' },
388
- { cmd: '/send', args: '[channel] <path>', label: '发送项目内文件' },
389
- { cmd: '/agentmd', args: '[put|set <内容>]', label: '管理 agent.md' },
409
+ { cmd: '/send', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
410
+ { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
411
+ { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
412
+ { value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
413
+ ] } },
390
414
  ] : []),
391
415
  ]
392
416
  });
@@ -395,22 +419,67 @@ export class CommandHandler {
395
419
  items.push({
396
420
  group: '其他',
397
421
  commands: [
398
- { cmd: '/status', label: '显示会话状态' },
399
- { cmd: '/check', label: '检查渠道健康' },
422
+ { cmd: '/status', label: '显示会话状态', desc: '查看当前会话的基本状态' },
423
+ { cmd: '/check', label: '检查渠道健康', desc: '检查消息渠道连接状态' },
400
424
  ]
401
425
  });
402
426
  }
403
427
  items.push({
404
428
  group: '帮助',
405
429
  commands: [
406
- { cmd: '/help', label: '显示帮助信息' },
430
+ { cmd: '/help', label: '显示帮助信息', desc: '列出所有可用命令及说明' },
407
431
  ]
408
432
  });
409
433
  return items;
410
434
  }
411
- /**
412
- * 快速判断是否为命令(不进队列的命令)
413
- */
435
+ /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
436
+ async getSubMenuItems(cmd, channel, channelId, userId) {
437
+ const session = await this.sessionManager.getActiveSession(channel, channelId);
438
+ if (cmd === '/s' || cmd === '/del') {
439
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
440
+ const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
441
+ const items = sessions
442
+ .filter(s => !active || s.id !== active.id)
443
+ .map(s => {
444
+ const shortId = s.agentSessionId ? s.agentSessionId.substring(0, 8) : '';
445
+ const time = s.updatedAt ? formatIdleTime(Date.now() - s.updatedAt) : '';
446
+ const parts = [shortId, time].filter(Boolean).join(' · ');
447
+ return {
448
+ value: s.name || s.id.slice(0, 8),
449
+ label: s.name || s.id.slice(0, 8),
450
+ desc: parts || undefined,
451
+ };
452
+ });
453
+ if (cmd === '/s') {
454
+ items.push({ value: 'cli', label: '查看 CLI 会话', desc: '列出未导入的 CLI 本地会话' });
455
+ }
456
+ return items;
457
+ }
458
+ if (cmd === '/p') {
459
+ const list = this.config.projects?.list || {};
460
+ return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
461
+ }
462
+ if (cmd === '/agent') {
463
+ return [...this.agentMap.keys()].map(name => ({ value: name, label: name }));
464
+ }
465
+ if (cmd === '/model') {
466
+ const agent = this.getAgent(session?.agentId);
467
+ if (hasModelSwitcher(agent) && agent.listModels) {
468
+ const models = await agent.listModels() ?? [];
469
+ if (models.length > 0)
470
+ return models.map((m) => ({ value: m, label: m }));
471
+ }
472
+ return null;
473
+ }
474
+ if (cmd === '/restart') {
475
+ const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
476
+ const channels = [...this.adapters.keys()].map(name => ({ value: name, label: name, desc: '重连此渠道' }));
477
+ if (isOwner)
478
+ channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
479
+ return channels;
480
+ }
481
+ return null;
482
+ }
414
483
  isCommand(content) {
415
484
  return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
416
485
  }
@@ -1879,6 +1948,8 @@ export class CommandHandler {
1879
1948
  return response;
1880
1949
  }
1881
1950
  // /bind 命令:持久化项目到配置(不切换)(owner only)
1951
+ if (normalizedContent === '/bind')
1952
+ return '用法: /bind <路径>';
1882
1953
  if (normalizedContent.startsWith('/bind ')) {
1883
1954
  if (!isOwner)
1884
1955
  return '❌ 无权限:此命令仅限 owner 使用';
@@ -2205,6 +2276,9 @@ export class CommandHandler {
2205
2276
  return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
2206
2277
  }
2207
2278
  // /rename 或 /name 命令:重命名当前会话
2279
+ if (normalizedContent === '/rename' || normalizedContent === '/name') {
2280
+ return '用法: /name <新名称> 或 /rename <新名称>';
2281
+ }
2208
2282
  if (normalizedContent.startsWith('/rename ')) {
2209
2283
  const newName = normalizedContent.slice(8).trim();
2210
2284
  if (!newName)
@@ -152,13 +152,27 @@ export class MessageBridge {
152
152
  return false;
153
153
  if (parsed.type === 'menu.query') {
154
154
  const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
155
- const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
156
- const response = JSON.stringify({ type: 'menu.response', items });
157
- if (adapter?.sendCustomPayload) {
158
- adapter.sendCustomPayload(msg.channelId, response);
155
+ if (parsed.cmd) {
156
+ // 动态子菜单查询
157
+ const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
158
+ const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
159
+ if (adapter?.sendCustomPayload) {
160
+ adapter.sendCustomPayload(msg.channelId, response);
161
+ }
162
+ else {
163
+ await sendReply(msg.channelId, response);
164
+ }
159
165
  }
160
166
  else {
161
- await sendReply(msg.channelId, response);
167
+ // 全量菜单
168
+ const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
169
+ const response = JSON.stringify({ type: 'menu.response', items });
170
+ if (adapter?.sendCustomPayload) {
171
+ adapter.sendCustomPayload(msg.channelId, response);
172
+ }
173
+ else {
174
+ await sendReply(msg.channelId, response);
175
+ }
162
176
  }
163
177
  return true;
164
178
  }
@@ -337,15 +337,18 @@ export class MessageProcessor {
337
337
  // 保存当前 flusher,用于 compact 事件
338
338
  this.currentFlusher = flusher;
339
339
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
340
+ // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
341
+ const capturedChannelId = message.channelId;
342
+ const capturedReplyContext = this.getReplyContext(message);
340
343
  // 设置权限审批的消息发送回调(指向当前渠道)
341
344
  agent.setSendPrompt(async (text) => {
342
- await adapter.sendText(message.channelId, text, this.getReplyContext(message));
345
+ await adapter.sendText(capturedChannelId, text, capturedReplyContext);
343
346
  });
344
347
  // 设置权限审批的交互上下文(支持交互卡片)
345
348
  agent.setPermissionContext?.(session.id, {
346
349
  adapter,
347
- channelId: message.channelId,
348
- replyContext: this.getReplyContext(message),
350
+ channelId: capturedChannelId,
351
+ replyContext: capturedReplyContext,
349
352
  interactionRouter: this.interactionRouter,
350
353
  interceptNextMessage: this.messageQueue
351
354
  ? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
@@ -94,6 +94,12 @@ export function summarizeToolInput(toolName, input) {
94
94
  'Glob': (i) => `pattern: ${i.pattern}`,
95
95
  'Agent': (i) => i.description || i.prompt?.substring(0, 80),
96
96
  'Skill': (i) => i.skill ? `${i.skill}${i.args ? ' ' + i.args : ''}` : undefined,
97
+ 'ExitPlanMode': (i) => {
98
+ if (i.allowedPrompts?.length) {
99
+ return `计划包含 ${i.allowedPrompts.length} 项操作权限`;
100
+ }
101
+ return '计划审批';
102
+ },
97
103
  'TodoWrite': (i) => {
98
104
  if (Array.isArray(i.todos)) {
99
105
  return i.todos.map((t) => t.content || t.task || t.text).filter(Boolean).join(', ').substring(0, 80);
@@ -186,7 +192,6 @@ export class PermissionGateway {
186
192
  },
187
193
  channelId: context?.channelId || '',
188
194
  sessionId,
189
- expiresAt: Date.now() + this.timeout,
190
195
  };
191
196
  // 尝试富交互
192
197
  let interactionSent = false;
@@ -204,16 +209,7 @@ export class PermissionGateway {
204
209
  await sendPrompt(`🔐 权限请求\n工具:${toolName}\n操作:${displaySummary}${reasonLine}\n\n回复 /perm allow 本次允许 | always 始终允许 | deny 拒绝`);
205
210
  }
206
211
  return new Promise((resolve) => {
207
- const timer = setTimeout(() => {
208
- this.pending.delete(requestId);
209
- // 清理 router 注册(仅删除本次请求,不影响其他交互)
210
- if (interactionSent && context?.interactionRouter) {
211
- context.interactionRouter.cancel(requestId);
212
- }
213
- this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId });
214
- resolve('deny');
215
- }, this.timeout);
216
- this.pending.set(requestId, { sessionId, toolName, resolve, timer });
212
+ this.pending.set(requestId, { sessionId, toolName, resolve, timer: setTimeout(() => { }, 0) });
217
213
  // 如果发了交互卡片,同时注册到 InteractionRouter
218
214
  if (interactionSent && context?.interactionRouter) {
219
215
  context.interactionRouter.register(requestId, sessionId, (action) => {
@@ -11,16 +11,17 @@ import path from 'path';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
13
  import { createRequire } from 'module';
14
- import { execFile } from 'child_process';
14
+ import { execFile, execFileSync } from 'child_process';
15
15
  import { promisify } from 'util';
16
16
  import { resolvePaths } from '../paths.js';
17
17
  import { normalizeChannelInstances } from '../config.js';
18
18
  import { selectInstance } from './init.js';
19
19
  import { isWindows } from './cross-platform.js';
20
20
  const execFileAsync = promisify(execFile);
21
- async function npmInstallGlobal(pkg) {
21
+ export async function npmInstallGlobal(pkg) {
22
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
22
23
  try {
23
- await execFileAsync('npm', ['install', '-g', pkg], { timeout: 180000 });
24
+ await execFileAsync(npmCmd, ['install', '-g', pkg], { timeout: 180000 });
24
25
  }
25
26
  catch (e) {
26
27
  if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
@@ -575,7 +576,8 @@ function compareVersion(a, min) {
575
576
  return parts[1] > min[1];
576
577
  return parts[2] >= min[2];
577
578
  }
578
- function resolveAunCoreSdkPkg() {
579
+ export function resolveAunCoreSdkPkg() {
580
+ // Strategy 1: createRequire (works when evolclaw and SDK share the same node_modules)
579
581
  try {
580
582
  const esmRequire = createRequire(import.meta.url);
581
583
  const entry = esmRequire.resolve(AUN_CORE_SDK_PKG);
@@ -592,14 +594,25 @@ function resolveAunCoreSdkPkg() {
592
594
  }
593
595
  dir = path.dirname(dir);
594
596
  }
595
- return null;
596
597
  }
597
- const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
598
- return { version: data.version, path: pkgPath };
598
+ else {
599
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
600
+ return { version: data.version, path: pkgPath };
601
+ }
599
602
  }
600
- catch {
601
- return null;
603
+ catch { /* fall through to strategy 2 */ }
604
+ // Strategy 2: npm root -g fallback (handles Windows / path mismatch)
605
+ try {
606
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
607
+ const globalRoot = execFileSync(npmCmd, ['root', '-g'], { encoding: 'utf-8', timeout: 10000 }).trim();
608
+ const pkgPath = path.join(globalRoot, AUN_CORE_SDK_PKG, 'package.json');
609
+ if (fs.existsSync(pkgPath)) {
610
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
611
+ return { version: data.version, path: pkgPath };
612
+ }
602
613
  }
614
+ catch { /* not found */ }
615
+ return null;
603
616
  }
604
617
  export async function checkAunEnvironment(rl) {
605
618
  console.log('\n🔍 AUN 环境检查...\n');
@@ -699,11 +712,28 @@ export async function setupAunAid(rl, _config) {
699
712
  client._gatewayUrl = `wss://gateway.${domain}:${port}/aun`;
700
713
  const result = await client.auth.createAid({ aid });
701
714
  console.log(` ✓ AID ${result.aid} 创建成功`);
715
+ // 下载 CA 根证书(如果本地不存在)
716
+ const caDir = path.join(aunPath, 'CA', 'root');
717
+ const caCertPath = path.join(caDir, 'root.crt');
718
+ if (!fs.existsSync(caCertPath)) {
719
+ try {
720
+ fs.mkdirSync(caDir, { recursive: true });
721
+ const httpPort = gatewayPort === 443 ? 20001 : gatewayPort;
722
+ const resp = await fetch(`https://gateway.${domain}:${httpPort}/pki/chain`);
723
+ if (resp.ok) {
724
+ fs.writeFileSync(caCertPath, await resp.text());
725
+ console.log(' ✓ CA 根证书已下载');
726
+ }
727
+ }
728
+ catch (e) {
729
+ console.warn(` ⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
730
+ }
731
+ }
702
732
  // Collect agent.md info and publish
703
733
  const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
704
734
  const agentType = typeInput === 'human' ? 'human' : 'ai';
705
735
  const agentName = aid.split('.')[0];
706
- const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\n---\n`;
736
+ const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
707
737
  try {
708
738
  await client.auth.uploadAgentMd(agentMdContent);
709
739
  console.log(' ✓ agent.md 已发布');
@@ -732,7 +762,24 @@ export async function setupAunAid(rl, _config) {
732
762
  break;
733
763
  // default: retry with new AID
734
764
  }
735
- return { aid };
765
+ // Owner 必填
766
+ console.log('\n📋 Owner 配置');
767
+ console.log(' Owner 将接收欢迎消息并拥有管理权限');
768
+ let owner = '';
769
+ while (!owner) {
770
+ const ownerInput = (await ask(rl, ' Owner AID (必填): ')).trim();
771
+ if (!ownerInput) {
772
+ console.log(' ⚠ Owner AID 不能为空');
773
+ continue;
774
+ }
775
+ if (!isValidAid(ownerInput)) {
776
+ console.log(' ⚠ Owner AID 格式无效');
777
+ continue;
778
+ }
779
+ owner = ownerInput;
780
+ console.log(` ✓ Owner 已设置: ${owner}`);
781
+ }
782
+ return { aid, owner };
736
783
  }
737
784
  export async function cmdInitAun() {
738
785
  const p = resolvePaths();
@@ -761,6 +808,7 @@ export async function cmdInitAun() {
761
808
  config.channels.aun = {
762
809
  enabled: true,
763
810
  aid: result.aid,
811
+ owner: result.owner,
764
812
  };
765
813
  if (!config.channels.defaultChannel)
766
814
  config.channels.defaultChannel = 'aun';
@@ -376,7 +376,7 @@ async function offerRichContentRenderer(rl, config) {
376
376
  // ==================== AUN AID Helpers ====================
377
377
  // Moved to init-channel.ts
378
378
  // ==================== Main ====================
379
- export async function cmdInit() {
379
+ export async function cmdInit(options) {
380
380
  const p = resolvePaths();
381
381
  ensureDataDirs();
382
382
  if (fs.existsSync(p.pid)) {
@@ -393,6 +393,75 @@ export async function cmdInit() {
393
393
  console.log(`❌ 找不到示例配置: ${sampleSrc}`);
394
394
  return;
395
395
  }
396
+ // 非交互式模式
397
+ if (options?.nonInteractive) {
398
+ const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
399
+ const defaultPath = options.defaultPath || path.join(os.homedir(), 'evolclaw-project');
400
+ if (!fs.existsSync(defaultPath))
401
+ fs.mkdirSync(defaultPath, { recursive: true });
402
+ config.projects.defaultPath = defaultPath;
403
+ config.projects.list = { [path.basename(defaultPath)]: defaultPath };
404
+ if (options.channel === 'aun' && !options.aunAid) {
405
+ throw new Error('--aun-aid is required for AUN channel (e.g. --aun-aid mybot.agentid.pub)');
406
+ }
407
+ if (options.channel === 'aun' && options.aunAid) {
408
+ // 自动安装 AUN SDK
409
+ const { resolveAunCoreSdkPkg, npmInstallGlobal } = await import('./init-channel.js');
410
+ if (!resolveAunCoreSdkPkg()) {
411
+ console.log('正在安装 @eleans/aun-core-sdk...');
412
+ await npmInstallGlobal('@eleans/aun-core-sdk@latest');
413
+ }
414
+ // 创建 AID(如果本地不存在)
415
+ const aunPath = path.join(os.homedir(), '.aun');
416
+ const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
417
+ if (!fs.existsSync(path.join(aidDir, 'private'))) {
418
+ const { AUNClient } = await import('@eleans/aun-core-sdk');
419
+ const client = new AUNClient({ aun_path: aunPath });
420
+ const domain = options.aunAid.split('.').slice(1).join('.');
421
+ client._gatewayUrl = `wss://gateway.${domain}:443/aun`;
422
+ await client.auth.createAid({ aid: options.aunAid });
423
+ // 下载 CA 根证书(如果本地不存在)
424
+ const caDir = path.join(aunPath, 'CA', 'root');
425
+ const caCertPath = path.join(caDir, 'root.crt');
426
+ if (!fs.existsSync(caCertPath)) {
427
+ try {
428
+ fs.mkdirSync(caDir, { recursive: true });
429
+ const resp = await fetch(`https://gateway.${domain}:20001/pki/chain`);
430
+ if (resp.ok) {
431
+ fs.writeFileSync(caCertPath, await resp.text());
432
+ console.log('✓ CA 根证书已下载');
433
+ }
434
+ }
435
+ catch (e) {
436
+ console.warn(`⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
437
+ }
438
+ }
439
+ // 写入初始 agent.md(initialized: false)
440
+ const agentName = options.aunAid.split('.')[0];
441
+ const agentMd = `---\naid: "${options.aunAid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
442
+ try {
443
+ await client.auth.uploadAgentMd(agentMd);
444
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMd, 'utf-8');
445
+ }
446
+ catch { }
447
+ try {
448
+ await client.close();
449
+ }
450
+ catch { }
451
+ }
452
+ config.channels.aun = {
453
+ enabled: true,
454
+ aid: options.aunAid,
455
+ ...(options.aunOwner && { owner: options.aunOwner }),
456
+ };
457
+ config.channels.defaultChannel = 'aun';
458
+ }
459
+ fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
460
+ console.log(`✓ 已创建配置文件: ${p.config}`);
461
+ setupEnvVar(resolveRoot());
462
+ return;
463
+ }
464
+ // 交互式模式
396
465
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
397
466
  try {
398
467
  if (fs.existsSync(p.config)) {
@@ -490,6 +559,7 @@ export async function cmdInit() {
490
559
  config.channels.aun = {
491
560
  enabled: true,
492
561
  aid: result.aid,
562
+ owner: result.owner,
493
563
  };
494
564
  channelConfigured = true;
495
565
  config.channels.defaultChannel = 'aun';
@@ -0,0 +1,148 @@
1
+ # EvolClaw AUN 通道安装指南
2
+
3
+ EvolClaw 是一个开源的 AI Agent 网关,将 Claude/Codex 等 AI 后端连接到 AUN(AgentUnion.Network)消息通道。
4
+
5
+ - **npm**: https://www.npmjs.com/package/evolclaw
6
+ - **GitHub**: https://github.com/eLeanwang/evolclaw
7
+ - **AUN**: https://agentunion.network
8
+
9
+ ---
10
+
11
+ ## 前置条件
12
+
13
+ - Node.js >= 18,npm >= 8
14
+ - 一个 AUN Agent ID(AID)——标识你的 Agent
15
+ - 一个 Owner AID——接收欢迎消息和管理权限
16
+
17
+ ---
18
+
19
+ ## 安装步骤
20
+
21
+ ### 步骤 1:检查环境
22
+
23
+ ```bash
24
+ node --version # 需要 >= 18
25
+ npm --version # 需要 >= 8
26
+ ```
27
+
28
+ 版本不满足请先升级:https://nodejs.org
29
+
30
+ ### 步骤 2:安装 EvolClaw
31
+
32
+ ```bash
33
+ npm install -g evolclaw
34
+ evolclaw --version
35
+ ```
36
+
37
+ 遇到权限错误(EACCES):
38
+
39
+ ```bash
40
+ # 方案 A
41
+ sudo npm install -g evolclaw
42
+
43
+ # 方案 B(推荐,无需 sudo)
44
+ npm config set prefix ~/.npm-global
45
+ export PATH=~/.npm-global/bin:$PATH
46
+ npm install -g evolclaw
47
+ ```
48
+
49
+ ### 步骤 3:准备配置信息
50
+
51
+ | 参数 | 说明 | 示例 |
52
+ |------|------|------|
53
+ | `--aun-aid` | **必填**,你的 AUN Agent ID | `mybot.agentid.pub` |
54
+ | `--aun-owner` | **必填**,Owner 的 AUN ID | `alice.agentid.pub` |
55
+ | `--default-path` | 默认项目路径(可选,默认当前目录) | `/home/user/myproject` |
56
+
57
+ 如果用户在触发安装时已提供 Owner AID(例如消息中包含"将 xxx 添加为 Owner ID"),直接使用该值,否则询问用户。
58
+
59
+ ### 步骤 4:非交互式初始化
60
+
61
+ ```bash
62
+ evolclaw init --non-interactive \
63
+ --default-path "$PWD" \
64
+ --channel aun \
65
+ --aun-aid <AID> \
66
+ --aun-owner <Owner AID>
67
+ ```
68
+
69
+ 此命令自动完成:
70
+ 1. 创建配置文件 `~/.evolclaw/data/evolclaw.json`
71
+ 2. 安装 `@eleans/aun-core-sdk`
72
+ 3. 创建 AID 密钥对(如本地不存在)
73
+ 4. 下载 CA 根证书到 `~/.aun/CA/root/root.crt`
74
+ 5. 写入初始 `agent.md`(`initialized: false`)
75
+ 6. 写入配置(含 owner 字段)
76
+
77
+ ### 步骤 5:启动前验证
78
+
79
+ 读取配置文件确认关键字段:
80
+
81
+ ```bash
82
+ cat ~/.evolclaw/data/evolclaw.json
83
+ ```
84
+
85
+ 检查:
86
+ - `channels.aun.aid` — 你的 AID
87
+ - `channels.aun.owner` — Owner AID
88
+ - `projects.defaultPath` — 项目路径(目录需存在)
89
+ - `channels.defaultChannel` — 应为 `"aun"`
90
+
91
+ 如发现缺失或异常,向用户说明并提供修复方案。
92
+
93
+ ### 步骤 6:启动服务
94
+
95
+ ```bash
96
+ evolclaw start
97
+ ```
98
+
99
+ ### 步骤 7:验证运行状态
100
+
101
+ ```bash
102
+ evolclaw status
103
+ tail -n 30 ~/.evolclaw/logs/evolclaw.log
104
+ ```
105
+
106
+ 日志中应出现:`[AUN] Connected as @<aid>`
107
+
108
+ 如未出现或有错误,读取完整日志分析原因并提供修复建议。
109
+
110
+ ---
111
+
112
+ ## 首次连接自动行为
113
+
114
+ EvolClaw 首次连接 AUN 网络时自动:
115
+
116
+ 1. 检测 `~/.aun/AIDs/<aid>/agent.md` 中的 `initialized` 字段
117
+ 2. 若为 `false`,生成完整 agent.md 并发布到 AUN 网络
118
+ 3. 向 Owner 发送欢迎消息
119
+
120
+ 无需手动触发,连接成功后自动完成。
121
+
122
+ ---
123
+
124
+ ## 安装完成
125
+
126
+ ```
127
+ ✅ EvolClaw 安装完成!
128
+
129
+ 服务已启动并连接到 AUN 网络。
130
+ 首次连接后你将在 AUN 客户端收到欢迎消息。
131
+ 使用 /bind <项目路径> 绑定工作目录即可开始。
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 常见问题
137
+
138
+ **Q: AID 已存在怎么办?**
139
+ `evolclaw init --non-interactive` 会检测本地密钥,已存在则跳过创建。
140
+
141
+ **Q: 启动失败怎么办?**
142
+ 查看 `~/.evolclaw/logs/evolclaw.log` 或运行 `evolclaw diagnose`。
143
+
144
+ **Q: 如何重启/查看日志?**
145
+ ```bash
146
+ evolclaw restart
147
+ evolclaw logs
148
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.5.0",
3
+ "version": "2.5.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",
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "dist/",
12
12
  "!dist/experimental/",
13
- "data/evolclaw.sample.json"
13
+ "data/evolclaw.sample.json",
14
+ "evolclaw-install-aun.md"
14
15
  ],
15
16
  "scripts": {
16
17
  "dev": "tsx watch src/index.ts",