evolclaw 2.5.0 → 2.5.1

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';
@@ -275,6 +276,8 @@ 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}`);
@@ -282,6 +285,90 @@ export class AUNChannel {
282
285
  return;
283
286
  }
284
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}`);
370
+ }
371
+ }
285
372
  // ── Event handlers ──────────────────────────────────────────
286
373
  async downloadAttachment(att, channelId) {
287
374
  const ownerAid = att.owner_aid || this._aid || '';
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) => {
@@ -18,7 +18,7 @@ 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
22
  try {
23
23
  await execFileAsync('npm', ['install', '-g', pkg], { timeout: 180000 });
24
24
  }
@@ -575,7 +575,7 @@ function compareVersion(a, min) {
575
575
  return parts[1] > min[1];
576
576
  return parts[2] >= min[2];
577
577
  }
578
- function resolveAunCoreSdkPkg() {
578
+ export function resolveAunCoreSdkPkg() {
579
579
  try {
580
580
  const esmRequire = createRequire(import.meta.url);
581
581
  const entry = esmRequire.resolve(AUN_CORE_SDK_PKG);
@@ -703,7 +703,7 @@ export async function setupAunAid(rl, _config) {
703
703
  const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
704
704
  const agentType = typeInput === 'human' ? 'human' : 'ai';
705
705
  const agentName = aid.split('.')[0];
706
- const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\n---\n`;
706
+ const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
707
707
  try {
708
708
  await client.auth.uploadAgentMd(agentMdContent);
709
709
  console.log(' ✓ agent.md 已发布');
@@ -732,7 +732,24 @@ export async function setupAunAid(rl, _config) {
732
732
  break;
733
733
  // default: retry with new AID
734
734
  }
735
- return { aid };
735
+ // Owner 必填
736
+ console.log('\n📋 Owner 配置');
737
+ console.log(' Owner 将接收欢迎消息并拥有管理权限');
738
+ let owner = '';
739
+ while (!owner) {
740
+ const ownerInput = (await ask(rl, ' Owner AID (必填): ')).trim();
741
+ if (!ownerInput) {
742
+ console.log(' ⚠ Owner AID 不能为空');
743
+ continue;
744
+ }
745
+ if (!isValidAid(ownerInput)) {
746
+ console.log(' ⚠ Owner AID 格式无效');
747
+ continue;
748
+ }
749
+ owner = ownerInput;
750
+ console.log(` ✓ Owner 已设置: ${owner}`);
751
+ }
752
+ return { aid, owner };
736
753
  }
737
754
  export async function cmdInitAun() {
738
755
  const p = resolvePaths();
@@ -761,6 +778,7 @@ export async function cmdInitAun() {
761
778
  config.channels.aun = {
762
779
  enabled: true,
763
780
  aid: result.aid,
781
+ owner: result.owner,
764
782
  };
765
783
  if (!config.channels.defaultChannel)
766
784
  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,56 @@ 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
+ // 自动安装 AUN SDK
406
+ const { resolveAunCoreSdkPkg, npmInstallGlobal } = await import('./init-channel.js');
407
+ if (!resolveAunCoreSdkPkg()) {
408
+ console.log('正在安装 @eleans/aun-core-sdk...');
409
+ await npmInstallGlobal('@eleans/aun-core-sdk@latest');
410
+ }
411
+ // 创建 AID(如果本地不存在)
412
+ const aunPath = path.join(os.homedir(), '.aun');
413
+ const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
414
+ if (!fs.existsSync(path.join(aidDir, 'private'))) {
415
+ const { AUNClient } = await import('@eleans/aun-core-sdk');
416
+ const client = new AUNClient({ aun_path: aunPath });
417
+ const domain = options.aunAid.split('.').slice(1).join('.');
418
+ client._gatewayUrl = `wss://gateway.${domain}:443/aun`;
419
+ await client.auth.createAid({ aid: options.aunAid });
420
+ // 写入初始 agent.md(initialized: false)
421
+ const agentName = options.aunAid.split('.')[0];
422
+ const agentMd = `---\naid: "${options.aunAid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
423
+ try {
424
+ await client.auth.uploadAgentMd(agentMd);
425
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMd, 'utf-8');
426
+ }
427
+ catch { }
428
+ try {
429
+ await client.close();
430
+ }
431
+ catch { }
432
+ }
433
+ config.channels.aun = {
434
+ enabled: true,
435
+ aid: options.aunAid,
436
+ ...(options.aunOwner && { owner: options.aunOwner }),
437
+ };
438
+ config.channels.defaultChannel = 'aun';
439
+ }
440
+ fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
441
+ console.log(`✓ 已创建配置文件: ${p.config}`);
442
+ setupEnvVar(resolveRoot());
443
+ return;
444
+ }
445
+ // 交互式模式
396
446
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
397
447
  try {
398
448
  if (fs.existsSync(p.config)) {
@@ -490,6 +540,7 @@ export async function cmdInit() {
490
540
  config.channels.aun = {
491
541
  enabled: true,
492
542
  aid: result.aid,
543
+ owner: result.owner,
493
544
  };
494
545
  channelConfigured = true;
495
546
  config.channels.defaultChannel = 'aun';
@@ -0,0 +1,54 @@
1
+ 请引导我完成 EvolClaw 的安装和配置。EvolClaw 是一个轻量级 AI Agent 网关,连接 Claude/Codex 到飞书、微信等 IM 通道。
2
+
3
+ **重要提示**:安装完成后,首次在 IM 渠道(飞书/微信)中与 EvolClaw 交互的用户将自动成为 owner,拥有完整管理权限。
4
+
5
+ 按以下步骤执行:
6
+
7
+ ## 第 1 步:环境检查
8
+
9
+ 运行 `node --version` 和 `npm --version`,确认 Node.js >= 18、npm >= 8。
10
+ 如果不符合要求,提示用户先升级 Node.js(https://nodejs.org)后停止。
11
+
12
+ ## 第 2 步:确认安装
13
+
14
+ 使用 AskUserQuestion 询问:
15
+ - 问题:是否立即安装 EvolClaw?
16
+ - 选项 A:立即安装
17
+ - 选项 B:仅显示命令,我稍后手动执行
18
+
19
+ 如果选择 B,输出 `npm install -g evolclaw` 后结束。
20
+
21
+ ## 第 3 步:执行安装
22
+
23
+ 运行 `npm install -g evolclaw`,然后 `evolclaw --version` 验证。
24
+
25
+ 如果失败且是权限问题,提示:
26
+ ```
27
+ sudo npm install -g evolclaw
28
+ # 或配置 npm prefix(推荐):
29
+ npm config set prefix ~/.npm-global && export PATH=~/.npm-global/bin:$PATH
30
+ npm install -g evolclaw
31
+ ```
32
+
33
+ ## 第 4 步:初始化配置
34
+
35
+ 运行 `evolclaw init`。
36
+
37
+ 提示用户:EvolClaw 自动继承 Claude Code CLI 的 API Key,无需重复配置。
38
+
39
+ ## 第 5 步:配置消息渠道
40
+
41
+ 使用 AskUserQuestion(multiSelect: true)询问需要配置哪些渠道:
42
+ - 飞书:运行 `evolclaw init feishu`(扫码登录)
43
+ - 微信:运行 `evolclaw init wechat`(扫码登录)
44
+ - 暂不配置:跳过
45
+
46
+ ## 第 6 步:启动并验证
47
+
48
+ 运行 `evolclaw start`,再运行 `evolclaw status` 确认状态为 Running。
49
+
50
+ 安装完成后告知用户:
51
+ - 在飞书/微信中发消息验证连接
52
+ - 发送 `/help` 查看所有命令
53
+ - 发送 `/bind <项目路径>` 绑定项目目录
54
+ - 常用命令:`evolclaw stop/restart/logs`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
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.md"
14
15
  ],
15
16
  "scripts": {
16
17
  "dev": "tsx watch src/index.ts",