evolclaw 2.7.3 → 2.8.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.
@@ -104,7 +104,7 @@ function formatIdleTime(ms) {
104
104
  return '刚刚';
105
105
  }
106
106
  // 支持的命令列表
107
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode'];
107
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode', '/ask'];
108
108
  // 命令别名映射
109
109
  const aliases = {
110
110
  '/p': '/project',
@@ -113,7 +113,7 @@ const aliases = {
113
113
  '/rw': '/rewind'
114
114
  };
115
115
  // 命令快速路径前缀(所有命令都不进入消息队列)
116
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd'];
116
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd', '/ask'];
117
117
  export class CommandHandler {
118
118
  sessionManager;
119
119
  config;
@@ -128,7 +128,6 @@ export class CommandHandler {
128
128
  permissionGateway;
129
129
  interactionRouter;
130
130
  statsCollector;
131
- hotLoadChannel;
132
131
  agentMap;
133
132
  defaultAgentId;
134
133
  /** 按 agentId 获取 agent,回退到默认 */
@@ -294,9 +293,6 @@ export class CommandHandler {
294
293
  setMessageQueue(messageQueue) {
295
294
  this.messageQueue = messageQueue;
296
295
  }
297
- setHotLoadChannel(fn) {
298
- this.hotLoadChannel = fn;
299
- }
300
296
  setPermissionGateway(gateway) {
301
297
  this.permissionGateway = gateway;
302
298
  }
@@ -401,6 +397,10 @@ export class CommandHandler {
401
397
  { value: 'high', label: 'High' },
402
398
  { value: 'max', label: 'Max' },
403
399
  ] } },
400
+ { cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
401
+ { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
402
+ { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
403
+ ] } },
404
404
  ]
405
405
  });
406
406
  items.push({
@@ -438,9 +438,9 @@ export class CommandHandler {
438
438
  ] : []),
439
439
  ...(isOwner ? [
440
440
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
441
- { cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
442
- { value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
443
- { value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
441
+ { cmd: '/aid', label: 'AID 身份管理', desc: '管理本地 AID 身份(创建/列表)', next: { type: 'select', items: [
442
+ { value: 'list', label: '列表', desc: '列出本地所有 AID' },
443
+ { value: 'new', label: '创建', desc: '创建新 AID 身份', next: { type: 'text' } },
444
444
  ] } },
445
445
  { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
446
446
  { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
@@ -515,6 +515,57 @@ export class CommandHandler {
515
515
  }
516
516
  return null;
517
517
  }
518
+ /** 菜单 exec 模式:查询状态或执行命令,返回结构化数据 */
519
+ async execMenu(cmd, mode, channel, channelId, userId) {
520
+ const session = await this.sessionManager.getActiveSession(channel, channelId);
521
+ if (!session)
522
+ return { error: '当前无活跃会话' };
523
+ const trimmed = cmd.trim();
524
+ const cmdBase = trimmed.split(' ')[0];
525
+ if (!cmdBase)
526
+ return { error: '缺少命令' };
527
+ const arg = trimmed.slice(cmdBase.length).trim();
528
+ if (cmdBase === '/perm') {
529
+ const currentMode = session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
530
+ if (mode === 'query') {
531
+ return { data: { mode: currentMode } };
532
+ }
533
+ // update
534
+ if (!arg)
535
+ return { error: '缺少目标模式' };
536
+ const identity = this.sessionManager.resolveIdentity(channel, userId);
537
+ if (identity.role !== 'owner')
538
+ return { error: '无权限' };
539
+ const permAgent = this.getAgent(session.agentId);
540
+ const validModes = hasPermissionController(permAgent)
541
+ ? permAgent.listModes().filter(m => m.available).map(m => m.key)
542
+ : ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
543
+ if (!validModes.includes(arg))
544
+ return { error: `无效模式: ${arg}` };
545
+ const metadata = { ...(session.metadata || {}), permissionMode: arg };
546
+ await this.sessionManager.updateSession(session.id, { metadata });
547
+ return { data: { mode: arg } };
548
+ }
549
+ if (cmdBase === '/chatmode') {
550
+ const currentMode = session.sessionMode || 'interactive';
551
+ if (mode === 'query') {
552
+ return { data: { mode: currentMode } };
553
+ }
554
+ // update
555
+ if (!arg)
556
+ return { error: '缺少目标模式' };
557
+ if (arg !== 'interactive' && arg !== 'proactive')
558
+ return { error: `无效模式: ${arg}` };
559
+ const identity = this.sessionManager.resolveIdentity(channel, userId);
560
+ const chatType = session.chatType || 'private';
561
+ if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
562
+ return { error: '无权限:群聊中仅管理员可切换' };
563
+ }
564
+ await this.sessionManager.updateSession(session.id, { sessionMode: arg });
565
+ return { data: { mode: arg } };
566
+ }
567
+ return { error: `不支持 exec 模式: ${cmdBase}` };
568
+ }
518
569
  isCommand(content) {
519
570
  return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
520
571
  }
@@ -686,7 +737,7 @@ export class CommandHandler {
686
737
  ...(isOwner ? [
687
738
  ' /restart - 重启服务',
688
739
  ' /file [channel] <path> - 发送项目内文件',
689
- ' /aid [list|new <aid>] - AID 管理',
740
+ ' /aid [list|new <aid>] - AID 身份管理',
690
741
  ' /agentmd [put|set <内容>] - 管理 agent.md',
691
742
  ] : []),
692
743
  '',
@@ -817,6 +868,31 @@ export class CommandHandler {
817
868
  const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
818
869
  return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
819
870
  }
871
+ // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
872
+ if (normalizedContent.startsWith('/ask')) {
873
+ const args = normalizedContent.slice(4).trim();
874
+ if (!args) {
875
+ // 无参数:列出当前 pending 的交互请求
876
+ const askResult = await this.ensureSession(channel, channelId, threadId);
877
+ if ('error' in askResult)
878
+ return askResult.error;
879
+ const pendingIds = this.interactionRouter?.getPending(askResult.session.id) || [];
880
+ if (pendingIds.length === 0)
881
+ return '当前没有待回答的问题';
882
+ return `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>`;
883
+ }
884
+ const askResult = await this.ensureSession(channel, channelId, threadId);
885
+ if ('error' in askResult)
886
+ return askResult.error;
887
+ const { session: askSession } = askResult;
888
+ const pendingIds = this.interactionRouter?.getPending(askSession.id) || [];
889
+ if (pendingIds.length === 0)
890
+ return '❌ 当前没有待回答的问题';
891
+ // 路由到最早的 pending interaction
892
+ const targetId = pendingIds[0];
893
+ this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
894
+ return `✓ 已回答`;
895
+ }
820
896
  // /agent 命令:查看或切换 Agent 后端
821
897
  if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
822
898
  const args = normalizedContent.slice(6).trim();
@@ -1219,87 +1295,51 @@ export class CommandHandler {
1219
1295
  }
1220
1296
  return `✓ 推理强度: ${newEffort}`;
1221
1297
  }
1222
- // /aid 命令:AID 管理(list / new)
1298
+ // /aid 命令:AID 身份管理(list / new)
1223
1299
  if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
1224
1300
  if (!isOwner)
1225
1301
  return '❌ 无权限:此命令仅限 owner 使用';
1226
- const adapter = this.adapters.get(channel);
1227
- const channelType = this.channelTypeMap.get(channel);
1228
- if (channelType !== 'aun')
1229
- return '❌ 此命令仅在 AUN 通道中可用';
1230
1302
  const arg = normalizedContent.slice(4).trim();
1231
- // /aid /aid list 列出所有 AUN 实例
1303
+ const { aidList, aidCreate, agentmdPut, buildInitialAgentMd, isValidAid } = await import('../channels/aun-ops.js');
1304
+ // /aid 或 /aid list — 列出本地所有 AID
1232
1305
  if (!arg || arg === 'list') {
1233
- const { normalizeChannelInstances } = await import('../config.js');
1234
- const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1235
- if (instances.length === 0)
1236
- return '暂无 AUN 实例';
1237
- const lines = ['AUN 实例:'];
1238
- for (const inst of instances) {
1239
- if (inst.enabled === false || !inst.aid)
1240
- continue;
1241
- const channelObj = this.channelObjects.get(inst.name);
1242
- const status = channelObj?.getStatus?.();
1243
- const connected = status?.connected ?? false;
1244
- const icon = connected ? '' : '✗';
1245
- const state = connected ? '已连接' : '未连接';
1246
- lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
1247
- }
1306
+ const aids = aidList();
1307
+ if (aids.length === 0)
1308
+ return '本地无 AID';
1309
+ const lines = ['本地 AID:'];
1310
+ for (const a of aids) {
1311
+ const icons = [
1312
+ a.hasPrivateKey ? '🔑' : ' ',
1313
+ a.hasAgentMd ? '📄' : ' ',
1314
+ ].join('');
1315
+ lines.push(` ${icons} ${a.aid}`);
1316
+ }
1317
+ lines.push('\n🔑=私钥 📄=agent.md');
1248
1318
  return lines.join('\n');
1249
1319
  }
1250
- // /aid new <aid> — 创建新 AID 并热加载
1320
+ // /aid new <aid> — 创建 AID(纯身份,不动 config)
1251
1321
  if (arg.startsWith('new ')) {
1252
- const rawName = arg.slice(4).trim();
1253
- if (!rawName)
1254
- return '用法: /aid new <aid>\n例: /aid new reviewer';
1255
- if (!this.hotLoadChannel)
1256
- return '❌ 热加载未就绪';
1257
- // Derive full AID: if no dots, append domain from current AID
1258
- const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1259
- let fullAid = rawName;
1260
- if (!rawName.includes('.')) {
1261
- const domain = selfAid.split('.').slice(1).join('.');
1262
- if (!domain)
1263
- return '❌ 无法推导 AID 域(当前实例未连接)';
1264
- fullAid = `${rawName}.${domain}`;
1265
- }
1266
- // Validate AID format
1267
- const { isValidAid } = await import('../utils/init-channel.js');
1268
- if (!isValidAid(fullAid))
1269
- return `❌ 无效 AID 格式: ${fullAid}`;
1270
- // Check instance name conflict
1271
- const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
1272
- const { normalizeChannelInstances } = await import('../config.js');
1273
- const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1274
- if (existing.some(e => e.name === instName)) {
1275
- return `❌ 实例名 "${instName}" 已存在`;
1276
- }
1277
- if (existing.some(e => e.aid === fullAid)) {
1278
- return `❌ AID ${fullAid} 已在配置中`;
1279
- }
1280
- // Create AID (reuse init-channel.ts silent logic)
1322
+ const rawAid = arg.slice(4).trim();
1323
+ if (!rawAid)
1324
+ return '用法: /aid new <完整AID>\n例: /aid new reviewer.agentid.pub';
1325
+ if (!isValidAid(rawAid))
1326
+ return `❌ 无效 AID 格式: ${rawAid}`;
1281
1327
  try {
1282
- const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
1283
- const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
1284
- // Resolve owner from current AUN instance config
1285
- const owner = this.config.channels?.aun
1286
- ? (Array.isArray(this.config.channels.aun)
1287
- ? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
1288
- : this.config.channels.aun.owner)
1289
- : undefined;
1290
- // Hot-load: build and register new channel instance BEFORE writing config
1291
- const { AUNChannelPlugin } = await import('../channels/aun.js');
1292
- const plugin = new AUNChannelPlugin();
1293
- const tempConfig = JSON.parse(JSON.stringify(this.config));
1294
- tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
1295
- const newInstances = await plugin.createChannels(tempConfig);
1296
- if (newInstances.length === 0)
1297
- return '❌ 通道实例创建失败';
1298
- await this.hotLoadChannel(newInstances[0]);
1299
- // Write config only after successful hot-load
1300
- appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
1301
- const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
1302
- return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
1328
+ const result = await aidCreate(rawAid);
1329
+ if (!result.alreadyExisted) {
1330
+ const content = buildInitialAgentMd({ aid: rawAid });
1331
+ try {
1332
+ await agentmdPut(content, { aid: rawAid, client: result.client });
1333
+ }
1334
+ catch { /* non-fatal */ }
1335
+ }
1336
+ try {
1337
+ await result.client.close();
1338
+ }
1339
+ catch { /* ignore */ }
1340
+ const verb = result.alreadyExisted ? '已存在' : '已创建';
1341
+ return `✓ ${rawAid} ${verb}
1342
+ 如需上线 AUN 通道,运行 evolclaw init aun`;
1303
1343
  }
1304
1344
  catch (e) {
1305
1345
  return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
@@ -1307,7 +1347,7 @@ export class CommandHandler {
1307
1347
  }
1308
1348
  return '用法: /aid [list|new <aid>]';
1309
1349
  }
1310
- // /activity 命令:控制中间输出显示模式
1350
+ // /agentmd 命令:管理 agent.md 身份文件
1311
1351
  if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1312
1352
  if (!isOwner)
1313
1353
  return '❌ 无权限:此命令仅限 owner 使用';
@@ -1316,7 +1356,8 @@ export class CommandHandler {
1316
1356
  return '❌ 当前通道不支持 agent.md 操作';
1317
1357
  const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1318
1358
  const arg = normalizedContent.slice(9).trim();
1319
- // put read local ~/.aun/AIDs/{aid}/agent.md and upload
1359
+ const { agentmdGet, agentmdPut } = await import('../channels/aun-ops.js');
1360
+ // put — read local agent.md and upload to network
1320
1361
  if (arg === 'put') {
1321
1362
  if (!selfAid)
1322
1363
  return '❌ 未连接,无法确定本地 AID';
@@ -1325,15 +1366,17 @@ export class CommandHandler {
1325
1366
  const { join } = await import('node:path');
1326
1367
  const { homedir } = await import('node:os');
1327
1368
  const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
1369
+ if (!readFileSync)
1370
+ return '❌ 读取失败';
1328
1371
  const content = readFileSync(localPath, 'utf-8');
1329
- await adapter.uploadAgentMd(content);
1372
+ await agentmdPut(content, { aid: selfAid });
1330
1373
  return '✅ agent.md 已发布';
1331
1374
  }
1332
1375
  catch (e) {
1333
1376
  return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1334
1377
  }
1335
1378
  }
1336
- // set <content> — upload inline content and sync to local
1379
+ // set <content> — upload inline content
1337
1380
  if (arg.startsWith('set ')) {
1338
1381
  const content = arg.slice(4).trim();
1339
1382
  if (!content)
@@ -1341,13 +1384,7 @@ export class CommandHandler {
1341
1384
  if (!selfAid)
1342
1385
  return '❌ 未连接,无法确定本地 AID';
1343
1386
  try {
1344
- await adapter.uploadAgentMd(content);
1345
- const { writeFileSync, mkdirSync } = await import('node:fs');
1346
- const { join } = await import('node:path');
1347
- const { homedir } = await import('node:os');
1348
- const localDir = join(homedir(), '.aun', 'AIDs', selfAid);
1349
- mkdirSync(localDir, { recursive: true });
1350
- writeFileSync(join(localDir, 'agent.md'), content, 'utf-8');
1387
+ await agentmdPut(content, { aid: selfAid });
1351
1388
  return '✅ agent.md 已更新并发布到AUN网络';
1352
1389
  }
1353
1390
  catch (e) {
@@ -1359,7 +1396,7 @@ export class CommandHandler {
1359
1396
  if (!aidToView)
1360
1397
  return '用法:/agentmd [<aid>] | put | set <内容>';
1361
1398
  try {
1362
- const md = await adapter.downloadAgentMd(aidToView);
1399
+ const md = await agentmdGet(aidToView);
1363
1400
  if (!md || !md.trim())
1364
1401
  return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1365
1402
  return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
@@ -2450,11 +2487,11 @@ export class CommandHandler {
2450
2487
  return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2451
2488
  }
2452
2489
  }
2453
- if (!targetSession && sessionName.length === 8) {
2490
+ if (!targetSession && sessionName.length >= 8) {
2454
2491
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2455
2492
  }
2456
2493
  const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
2457
- if (!targetSession && sessionName.length === 8 && canImport) {
2494
+ if (!targetSession && sessionName.length >= 8 && canImport) {
2458
2495
  const projectPaths = Object.values(this.projects);
2459
2496
  if (session) {
2460
2497
  projectPaths.unshift(session.projectPath);
@@ -2557,7 +2594,7 @@ export class CommandHandler {
2557
2594
  return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2558
2595
  }
2559
2596
  }
2560
- if (!targetSession && sessionName.length === 8) {
2597
+ if (!targetSession && sessionName.length >= 8) {
2561
2598
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2562
2599
  }
2563
2600
  if (!targetSession) {
@@ -2809,6 +2846,7 @@ export class CommandHandler {
2809
2846
  * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
2810
2847
  * - 群聊话题:metadata.replyContext.{threadId,peerId}
2811
2848
  * - 私聊:metadata.peerId
2849
+ * - taskId/chatmode:从 processing_state 和 sessionMode 注入
2812
2850
  */
2813
2851
  buildCtlReplyContext(session) {
2814
2852
  const ctx = {};
@@ -2819,6 +2857,18 @@ export class CommandHandler {
2819
2857
  ctx.peerId = meta.replyContext.peerId;
2820
2858
  if (!ctx.peerId && meta?.peerId)
2821
2859
  ctx.peerId = meta.peerId;
2860
+ const taskId = this.sessionManager.getActiveTaskId(session.id);
2861
+ const chatmode = session.sessionMode || 'interactive';
2862
+ const encrypted = this.sessionManager.getSessionEncrypt(session.id);
2863
+ if (taskId || chatmode !== 'interactive' || encrypted != null) {
2864
+ ctx.metadata = {};
2865
+ if (taskId)
2866
+ ctx.metadata.taskId = taskId;
2867
+ if (chatmode !== 'interactive')
2868
+ ctx.metadata.chatmode = chatmode;
2869
+ if (encrypted != null)
2870
+ ctx.metadata.encrypted = encrypted;
2871
+ }
2822
2872
  return Object.keys(ctx).length > 0 ? ctx : undefined;
2823
2873
  }
2824
2874
  /**
@@ -0,0 +1,72 @@
1
+ import path from 'path';
2
+ const VALID_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
3
+ const VALID_CHANNEL_TYPES = new Set(['feishu', 'aun', 'wechat', 'wecom', 'dingtalk', 'qqbot']);
4
+ const VALID_CHATMODES = new Set(['interactive', 'proactive']);
5
+ export function validateEvolAgentConfig(raw) {
6
+ const errors = [];
7
+ if (!raw || typeof raw !== 'object') {
8
+ return { valid: false, errors: ['config must be an object'] };
9
+ }
10
+ if (typeof raw.name !== 'string' || raw.name.trim() === '') {
11
+ errors.push('name is required and must be a non-empty string');
12
+ }
13
+ if (raw.enabled !== undefined && typeof raw.enabled !== 'boolean') {
14
+ errors.push('enabled must be a boolean if present');
15
+ }
16
+ if (!raw.agents || typeof raw.agents !== 'object') {
17
+ errors.push('agents must be an object with exactly one baseagent block');
18
+ }
19
+ else {
20
+ const keys = Object.keys(raw.agents).filter(k => VALID_BASEAGENTS.has(k));
21
+ const unknownKeys = Object.keys(raw.agents).filter(k => !VALID_BASEAGENTS.has(k));
22
+ if (unknownKeys.length > 0) {
23
+ errors.push(`agents contains unknown baseagent keys: ${unknownKeys.join(', ')}`);
24
+ }
25
+ if (keys.length === 0) {
26
+ errors.push('agents must contain exactly one of: claude | codex | gemini | hermes');
27
+ }
28
+ else if (keys.length > 1) {
29
+ errors.push(`agents must contain exactly one baseagent (single baseagent only), got: ${keys.join(', ')}`);
30
+ }
31
+ }
32
+ if (!raw.channels || typeof raw.channels !== 'object') {
33
+ errors.push('channels is required');
34
+ }
35
+ else {
36
+ const channelKeys = Object.keys(raw.channels);
37
+ if (channelKeys.length === 0) {
38
+ errors.push('channels must contain at least one channel type');
39
+ }
40
+ for (const key of channelKeys) {
41
+ if (!VALID_CHANNEL_TYPES.has(key)) {
42
+ errors.push(`unknown channel type: ${key}`);
43
+ }
44
+ }
45
+ }
46
+ if (!raw.projects || typeof raw.projects !== 'object') {
47
+ errors.push('projects is required');
48
+ }
49
+ else {
50
+ const p = raw.projects.defaultPath;
51
+ if (typeof p !== 'string' || p === '') {
52
+ errors.push('projects.defaultPath is required');
53
+ }
54
+ else if (!path.isAbsolute(p)) {
55
+ errors.push(`projects.defaultPath must be absolute, got: ${p}`);
56
+ }
57
+ }
58
+ if (raw.chatmode !== undefined) {
59
+ if (typeof raw.chatmode !== 'object' || raw.chatmode === null) {
60
+ errors.push('chatmode must be an object if present');
61
+ }
62
+ else {
63
+ for (const key of ['private', 'group']) {
64
+ const val = raw.chatmode[key];
65
+ if (val !== undefined && !VALID_CHATMODES.has(val)) {
66
+ errors.push(`chatmode.${key} must be 'interactive' or 'proactive'`);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return { valid: errors.length === 0, errors };
72
+ }
@@ -0,0 +1,66 @@
1
+ export class EvolAgent {
2
+ name;
3
+ configPath;
4
+ config;
5
+ isDefault;
6
+ channels = new Map();
7
+ activeSessions = 0;
8
+ lastActivity;
9
+ status;
10
+ error;
11
+ constructor(configPath, config, opts = {}) {
12
+ this.configPath = configPath;
13
+ this.config = config;
14
+ this.name = config.name;
15
+ this.isDefault = opts.isDefault === true;
16
+ this.status = config.enabled === false ? 'disabled' : 'stopped';
17
+ }
18
+ get baseagent() {
19
+ const keys = Object.keys(this.config.agents);
20
+ return keys[0] || 'claude';
21
+ }
22
+ get model() {
23
+ return this.config.agents[this.baseagent]?.model;
24
+ }
25
+ get effort() {
26
+ return this.config.agents[this.baseagent]?.effort;
27
+ }
28
+ get projectPath() {
29
+ return this.config.projects.defaultPath;
30
+ }
31
+ channelInstanceNames() {
32
+ const names = [];
33
+ for (const [type, raw] of Object.entries(this.config.channels || {})) {
34
+ const instances = Array.isArray(raw) ? raw : [raw];
35
+ for (const inst of instances) {
36
+ if (!inst || typeof inst !== 'object')
37
+ continue;
38
+ names.push(inst.name ?? type);
39
+ }
40
+ }
41
+ return names;
42
+ }
43
+ getContext(channelName, chatType, globalChatmode) {
44
+ const chatMode = this.resolveChatMode(chatType, globalChatmode);
45
+ return {
46
+ name: this.name,
47
+ isOwned: !this.isDefault,
48
+ baseagent: this.baseagent,
49
+ model: this.model,
50
+ effort: this.effort,
51
+ chatMode,
52
+ projectPath: this.projectPath,
53
+ };
54
+ }
55
+ resolveChatMode(chatType, globalChatmode) {
56
+ const agentCm = this.config.chatmode;
57
+ const key = chatType === 'group' ? 'group' : 'private';
58
+ if (agentCm) {
59
+ return (agentCm[key] || 'interactive');
60
+ }
61
+ if (globalChatmode) {
62
+ return (globalChatmode[key] || 'interactive');
63
+ }
64
+ return 'interactive';
65
+ }
66
+ }
@@ -151,8 +151,19 @@ export class MessageBridge {
151
151
  if (!parsed || typeof parsed !== 'object' || !parsed.type)
152
152
  return false;
153
153
  if (parsed.type === 'menu.query') {
154
- const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
155
- if (parsed.cmd) {
154
+ if (parsed.cmd && (parsed.mode === 'query' || parsed.mode === 'update')) {
155
+ // exec 模式:查询状态或执行命令
156
+ const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
157
+ const base = { type: 'menu.response', cmd: parsed.cmd };
158
+ const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
159
+ if (adapter?.sendCustomPayload) {
160
+ adapter.sendCustomPayload(msg.channelId, response);
161
+ }
162
+ else {
163
+ await sendReply(msg.channelId, response);
164
+ }
165
+ }
166
+ else if (parsed.cmd) {
156
167
  // 动态子菜单查询
157
168
  const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
158
169
  const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
@@ -165,6 +176,7 @@ export class MessageBridge {
165
176
  }
166
177
  else {
167
178
  // 全量菜单
179
+ const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
168
180
  const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
169
181
  const response = JSON.stringify({ type: 'menu.response', items });
170
182
  if (adapter?.sendCustomPayload) {