evolclaw 2.8.0 → 2.8.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.
@@ -128,9 +128,9 @@ export class CommandHandler {
128
128
  permissionGateway;
129
129
  interactionRouter;
130
130
  statsCollector;
131
- hotLoadChannel;
132
131
  agentMap;
133
132
  defaultAgentId;
133
+ agentRegistry;
134
134
  /** 按 agentId 获取 agent,回退到默认 */
135
135
  getAgent(agentId) {
136
136
  if (agentId && this.agentMap.has(agentId))
@@ -151,6 +151,193 @@ export class CommandHandler {
151
151
  this.defaultAgentId = agentRunnerOrMap.name;
152
152
  }
153
153
  }
154
+ /** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
155
+ setAgentRegistry(registry) {
156
+ this.agentRegistry = registry;
157
+ }
158
+ /** 返回管理当前通道的 EvolAgent(非 default),无则返回 null */
159
+ getOwningAgent(channel) {
160
+ if (!this.agentRegistry)
161
+ return null;
162
+ const agent = this.agentRegistry.resolveByChannel(channel);
163
+ if (!agent || agent.isDefault)
164
+ return null;
165
+ return agent;
166
+ }
167
+ /** 返回当前通道的有效项目路径:agent-owned 用 agent.projectPath;否则用全局 defaultPath。*/
168
+ getEffectiveDefaultPath(channel) {
169
+ const owning = this.getOwningAgent(channel);
170
+ if (owning)
171
+ return owning.projectPath;
172
+ return this.config.projects?.defaultPath || process.cwd();
173
+ }
174
+ /**
175
+ * 返回当前通道有效的 projects.list(agent-owned 用 agent.json 的;否则全局 evolclaw.json 的)。
176
+ * 都没配 list 时回退到 defaultPath 单项目。
177
+ */
178
+ getEffectiveProjects(channel) {
179
+ const owning = this.getOwningAgent(channel);
180
+ if (owning) {
181
+ return owning.getProjects();
182
+ }
183
+ return this.projects;
184
+ }
185
+ /**
186
+ * 添加项目到当前通道范围(agent-owned 写 agent.json;default 写 evolclaw.json)。
187
+ */
188
+ async addProjectInScope(channel, name, projectPath) {
189
+ const owning = this.getOwningAgent(channel);
190
+ if (owning) {
191
+ try {
192
+ owning.addProject(name, projectPath);
193
+ }
194
+ catch (e) {
195
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
196
+ }
197
+ return undefined;
198
+ }
199
+ if (!this.config.projects) {
200
+ this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
201
+ }
202
+ if (!this.config.projects.list) {
203
+ this.config.projects.list = {};
204
+ }
205
+ this.config.projects.list[name] = projectPath;
206
+ try {
207
+ const { saveConfig } = await import('../config.js');
208
+ saveConfig(this.config);
209
+ }
210
+ catch (e) {
211
+ return `⚠️ 写入 evolclaw.json 失败: ${e?.message || e}`;
212
+ }
213
+ // Refresh in-memory list cache (this.projects getter reads from this.config)
214
+ return undefined;
215
+ }
216
+ /**
217
+ * 持久化 baseagent.model:agent-owned 写到 agent.json;否则写 evolclaw.json 或 ~/.claude/settings.json。
218
+ * 返回错误信息或 undefined。
219
+ */
220
+ persistBaseagentModel(channel, baseagentName, newModel) {
221
+ const owning = this.getOwningAgent(channel);
222
+ if (owning) {
223
+ try {
224
+ owning.setBaseagentModel(newModel);
225
+ }
226
+ catch (e) {
227
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
228
+ }
229
+ return undefined;
230
+ }
231
+ // DefaultAgent / 无 owning agent:保留原"就近原则"
232
+ if (!this.config.agents)
233
+ this.config.agents = {};
234
+ const isCodex = baseagentName === 'codex';
235
+ if (isCodex) {
236
+ if (!this.config.agents.codex)
237
+ this.config.agents.codex = {};
238
+ if (newModel)
239
+ this.config.agents.codex.model = newModel;
240
+ try {
241
+ saveConfig(this.config);
242
+ }
243
+ catch (e) {
244
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
245
+ }
246
+ return undefined;
247
+ }
248
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
249
+ if (configuredInEvolclaw) {
250
+ if (!this.config.agents.claude)
251
+ this.config.agents.claude = {};
252
+ if (newModel)
253
+ this.config.agents.claude.model = newModel;
254
+ try {
255
+ saveConfig(this.config);
256
+ }
257
+ catch (e) {
258
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
259
+ }
260
+ return undefined;
261
+ }
262
+ // Fallback: ~/.claude/settings.json
263
+ const updates = {};
264
+ if (newModel)
265
+ updates.model = newModel;
266
+ const writeResult = writeUserSettings(updates);
267
+ if (!writeResult.success) {
268
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
269
+ }
270
+ return undefined;
271
+ }
272
+ /**
273
+ * 持久化 baseagent.effort:agent-owned 写到 agent.json;否则就近原则。
274
+ */
275
+ persistBaseagentEffort(channel, baseagentName, newEffort) {
276
+ const owning = this.getOwningAgent(channel);
277
+ if (owning) {
278
+ try {
279
+ owning.setBaseagentEffort(newEffort);
280
+ }
281
+ catch (e) {
282
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
283
+ }
284
+ return undefined;
285
+ }
286
+ if (!this.config.agents)
287
+ this.config.agents = {};
288
+ const isCodex = baseagentName === 'codex';
289
+ if (isCodex) {
290
+ if (newEffort === undefined) {
291
+ if (this.config.agents.codex?.reasoning) {
292
+ delete this.config.agents.codex.reasoning;
293
+ try {
294
+ saveConfig(this.config);
295
+ }
296
+ catch { }
297
+ }
298
+ }
299
+ else {
300
+ if (!this.config.agents.codex)
301
+ this.config.agents.codex = {};
302
+ this.config.agents.codex.reasoning = newEffort;
303
+ try {
304
+ saveConfig(this.config);
305
+ }
306
+ catch (e) {
307
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
308
+ }
309
+ }
310
+ return undefined;
311
+ }
312
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
313
+ if (configuredInEvolclaw) {
314
+ if (newEffort === undefined) {
315
+ delete this.config.agents.claude.effort;
316
+ try {
317
+ saveConfig(this.config);
318
+ }
319
+ catch { }
320
+ }
321
+ else {
322
+ if (!this.config.agents.claude)
323
+ this.config.agents.claude = {};
324
+ this.config.agents.claude.effort = newEffort;
325
+ try {
326
+ saveConfig(this.config);
327
+ }
328
+ catch (e) {
329
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
330
+ }
331
+ }
332
+ return undefined;
333
+ }
334
+ const updates = { effortLevel: newEffort ?? null };
335
+ const writeResult = writeUserSettings(updates);
336
+ if (!writeResult.success) {
337
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
338
+ }
339
+ return undefined;
340
+ }
154
341
  /** 项目列表快捷访问(list 缺失时用 defaultPath 作为唯一项目) */
155
342
  get projects() {
156
343
  const list = this.config.projects?.list;
@@ -294,9 +481,6 @@ export class CommandHandler {
294
481
  setMessageQueue(messageQueue) {
295
482
  this.messageQueue = messageQueue;
296
483
  }
297
- setHotLoadChannel(fn) {
298
- this.hotLoadChannel = fn;
299
- }
300
484
  setPermissionGateway(gateway) {
301
485
  this.permissionGateway = gateway;
302
486
  }
@@ -442,9 +626,9 @@ export class CommandHandler {
442
626
  ] : []),
443
627
  ...(isOwner ? [
444
628
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
445
- { cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
446
- { value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
447
- { value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
629
+ { cmd: '/aid', label: 'AID 身份管理', desc: '管理本地 AID 身份(创建/列表)', next: { type: 'select', items: [
630
+ { value: 'list', label: '列表', desc: '列出本地所有 AID' },
631
+ { value: 'new', label: '创建', desc: '创建新 AID 身份', next: { type: 'text' } },
448
632
  ] } },
449
633
  { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
450
634
  { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
@@ -495,7 +679,9 @@ export class CommandHandler {
495
679
  return items;
496
680
  }
497
681
  if (cmd === '/p') {
498
- const list = this.config.projects?.list || {};
682
+ // Use agent-scoped project list: agent-owned channels see their agent.json's
683
+ // projects.list; default channel sees evolclaw.json's projects.list
684
+ const list = this.getEffectiveProjects(channel);
499
685
  return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
500
686
  }
501
687
  if (cmd === '/agent') {
@@ -511,8 +697,19 @@ export class CommandHandler {
511
697
  return null;
512
698
  }
513
699
  if (cmd === '/restart') {
700
+ // /restart 是服务级操作(重连/重启进程),仅限 default 通道。
701
+ // EvolAgent 通道返回空菜单(用户在 agent-owned 通道上无可选项)
702
+ if (this.getOwningAgent(channel))
703
+ return [];
514
704
  const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
515
- const channels = [...this.adapters.keys()].map(name => ({ value: name, label: name, desc: '重连此渠道' }));
705
+ // 列出所有 channel type
706
+ const visibleTypes = new Set();
707
+ for (const [name] of this.adapters) {
708
+ const t = this.channelTypeMap.get(name);
709
+ if (t)
710
+ visibleTypes.add(t);
711
+ }
712
+ const channels = [...visibleTypes].map(type => ({ value: type, label: type, desc: '重连此类型所有渠道实例' }));
516
713
  if (isOwner)
517
714
  channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
518
715
  return channels;
@@ -603,6 +800,20 @@ export class CommandHandler {
603
800
  return '⚠️ 话题中不支持此命令';
604
801
  }
605
802
  }
803
+ // Agent-owned 通道:禁止项目切换和 agent 切换
804
+ const owningAgent = this.getOwningAgent(channel);
805
+ if (owningAgent) {
806
+ const isProjectCmd = normalizedContent === '/project' || normalizedContent.startsWith('/project ') ||
807
+ normalizedContent === '/bind' || normalizedContent.startsWith('/bind ') ||
808
+ normalizedContent === '/plist' ||
809
+ normalizedContent === '/p' || normalizedContent.startsWith('/p ');
810
+ if (isProjectCmd) {
811
+ return `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}`;
812
+ }
813
+ if (normalizedContent.startsWith('/agent ')) {
814
+ return `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}`;
815
+ }
816
+ }
606
817
  // 权限检查:区分用户级命令和管理级命令
607
818
  const isOwner = identity.role === 'owner';
608
819
  const isAdmin = identity.role === 'owner' || identity.role === 'admin';
@@ -736,12 +947,12 @@ export class CommandHandler {
736
947
  ' /check - 检查渠道状态',
737
948
  ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
738
949
  ...(isAdmin ? [
739
- ' /restart <channel> - 重连指定渠道',
950
+ ' /restart <type> - 重连该类型所有渠道实例(服务级,admin+)',
740
951
  ] : []),
741
952
  ...(isOwner ? [
742
953
  ' /restart - 重启服务',
743
954
  ' /file [channel] <path> - 发送项目内文件',
744
- ' /aid [list|new <aid>] - AID 管理',
955
+ ' /aid [list|new <aid>] - AID 身份管理',
745
956
  ' /agentmd [put|set <内容>] - 管理 agent.md',
746
957
  ] : []),
747
958
  '',
@@ -1095,68 +1306,16 @@ export class CommandHandler {
1095
1306
  modelAgent.setEffort?.(newEffort);
1096
1307
  changes.push(`推理强度: ${newEffort}`);
1097
1308
  }
1098
- // 持久化:写回来源(就近原则)
1099
- // evolclaw.json 配了 → 写 evolclaw.json
1100
- // evolclaw.json 没配 agent 全局配置
1101
- if (isCodexAgent) {
1102
- const configuredInEvolclaw = !!(this.config.agents?.codex?.model || this.config.agents?.codex?.reasoning);
1103
- if (configuredInEvolclaw) {
1104
- if (!this.config.agents.codex)
1105
- this.config.agents.codex = {};
1106
- if (newModel)
1107
- this.config.agents.codex.model = newModel;
1108
- if (newEffort)
1109
- this.config.agents.codex.reasoning = newEffort;
1110
- try {
1111
- saveConfig(this.config);
1112
- }
1113
- catch (error) {
1114
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1115
- }
1116
- }
1117
- else {
1118
- // Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
1119
- if (!this.config.agents.codex)
1120
- this.config.agents.codex = {};
1121
- if (newModel)
1122
- this.config.agents.codex.model = newModel;
1123
- if (newEffort)
1124
- this.config.agents.codex.reasoning = newEffort;
1125
- try {
1126
- saveConfig(this.config);
1127
- }
1128
- catch (error) {
1129
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1130
- }
1131
- }
1309
+ // 持久化:agent-owned channel 写到 agent.json;default 走原"就近原则"
1310
+ if (newModel) {
1311
+ const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
1312
+ if (err)
1313
+ return `${err}\n已更新运行时配置,但未持久化`;
1132
1314
  }
1133
- else {
1134
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1135
- if (configuredInEvolclaw) {
1136
- if (!this.config.agents.claude)
1137
- this.config.agents.claude = {};
1138
- if (newModel)
1139
- this.config.agents.claude.model = newModel;
1140
- if (newEffort)
1141
- this.config.agents.claude.effort = newEffort;
1142
- try {
1143
- saveConfig(this.config);
1144
- }
1145
- catch (error) {
1146
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1147
- }
1148
- }
1149
- else {
1150
- const updates = {};
1151
- if (newModel)
1152
- updates.model = newModel;
1153
- if (newEffort)
1154
- updates.effortLevel = newEffort;
1155
- const writeResult = writeUserSettings(updates);
1156
- if (!writeResult.success) {
1157
- return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
1158
- }
1159
- }
1315
+ if (newEffort) {
1316
+ const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
1317
+ if (err)
1318
+ return `${err}\n已更新运行时配置,但未持久化`;
1160
1319
  }
1161
1320
  return `✓ 已切换\n ${changes.join('\n ')}`;
1162
1321
  }
@@ -1235,29 +1394,9 @@ export class CommandHandler {
1235
1394
  // /effort auto:恢复 SDK 默认
1236
1395
  if (args === 'auto') {
1237
1396
  effortAgent.setEffort?.(undefined);
1238
- const isCodex = effortAgent.name === 'codex';
1239
- if (isCodex) {
1240
- if (this.config.agents?.codex?.reasoning) {
1241
- delete this.config.agents.codex.reasoning;
1242
- try {
1243
- saveConfig(this.config);
1244
- }
1245
- catch { }
1246
- }
1247
- }
1248
- else {
1249
- const configuredInEvolclaw = !!this.config.agents?.claude?.effort;
1250
- if (configuredInEvolclaw) {
1251
- delete this.config.agents.claude.effort;
1252
- try {
1253
- saveConfig(this.config);
1254
- }
1255
- catch { }
1256
- }
1257
- else {
1258
- writeUserSettings({ effortLevel: null });
1259
- }
1260
- }
1397
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
1398
+ if (err)
1399
+ return `${err}\n已更新运行时配置,但未持久化`;
1261
1400
  return '✓ 推理强度已恢复为 auto (SDK默认)';
1262
1401
  }
1263
1402
  // /effort <level>:切换推理强度
@@ -1269,117 +1408,56 @@ export class CommandHandler {
1269
1408
  }
1270
1409
  const newEffort = args;
1271
1410
  effortAgent.setEffort?.(newEffort);
1272
- // 持久化
1273
- if (!this.config.agents)
1274
- this.config.agents = {};
1275
- const isCodex = effortAgent.name === 'codex';
1276
- if (isCodex) {
1277
- if (!this.config.agents.codex)
1278
- this.config.agents.codex = {};
1279
- this.config.agents.codex.reasoning = newEffort;
1280
- try {
1281
- saveConfig(this.config);
1282
- }
1283
- catch { }
1284
- }
1285
- else {
1286
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1287
- if (configuredInEvolclaw) {
1288
- if (!this.config.agents.claude)
1289
- this.config.agents.claude = {};
1290
- this.config.agents.claude.effort = newEffort;
1291
- try {
1292
- saveConfig(this.config);
1293
- }
1294
- catch { }
1295
- }
1296
- else {
1297
- writeUserSettings({ effortLevel: newEffort });
1298
- }
1299
- }
1411
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1412
+ if (err)
1413
+ return `${err}\n已更新运行时配置,但未持久化`;
1300
1414
  return `✓ 推理强度: ${newEffort}`;
1301
1415
  }
1302
- // /aid 命令:AID 管理(list / new)
1416
+ // /aid 命令:AID 身份管理(list / new)
1303
1417
  if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
1304
1418
  if (!isOwner)
1305
1419
  return '❌ 无权限:此命令仅限 owner 使用';
1306
- const adapter = this.adapters.get(channel);
1307
- const channelType = this.channelTypeMap.get(channel);
1308
- if (channelType !== 'aun')
1309
- return '❌ 此命令仅在 AUN 通道中可用';
1310
1420
  const arg = normalizedContent.slice(4).trim();
1311
- // /aid /aid list 列出所有 AUN 实例
1421
+ const { aidList, aidCreate, agentmdPut, buildInitialAgentMd, isValidAid } = await import('../channels/aun-ops.js');
1422
+ // /aid 或 /aid list — 列出本地所有 AID
1312
1423
  if (!arg || arg === 'list') {
1313
- const { normalizeChannelInstances } = await import('../config.js');
1314
- const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1315
- if (instances.length === 0)
1316
- return '暂无 AUN 实例';
1317
- const lines = ['AUN 实例:'];
1318
- for (const inst of instances) {
1319
- if (inst.enabled === false || !inst.aid)
1320
- continue;
1321
- const channelObj = this.channelObjects.get(inst.name);
1322
- const status = channelObj?.getStatus?.();
1323
- const connected = status?.connected ?? false;
1324
- const icon = connected ? '' : '✗';
1325
- const state = connected ? '已连接' : '未连接';
1326
- lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
1327
- }
1424
+ const aids = aidList();
1425
+ if (aids.length === 0)
1426
+ return '本地无 AID';
1427
+ const lines = ['本地 AID:'];
1428
+ for (const a of aids) {
1429
+ const icons = [
1430
+ a.hasPrivateKey ? '🔑' : ' ',
1431
+ a.hasAgentMd ? '📄' : ' ',
1432
+ ].join('');
1433
+ lines.push(` ${icons} ${a.aid}`);
1434
+ }
1435
+ lines.push('\n🔑=私钥 📄=agent.md');
1328
1436
  return lines.join('\n');
1329
1437
  }
1330
- // /aid new <aid> — 创建新 AID 并热加载
1438
+ // /aid new <aid> — 创建 AID(纯身份,不动 config)
1331
1439
  if (arg.startsWith('new ')) {
1332
- const rawName = arg.slice(4).trim();
1333
- if (!rawName)
1334
- return '用法: /aid new <aid>\n例: /aid new reviewer';
1335
- if (!this.hotLoadChannel)
1336
- return '❌ 热加载未就绪';
1337
- // Derive full AID: if no dots, append domain from current AID
1338
- const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1339
- let fullAid = rawName;
1340
- if (!rawName.includes('.')) {
1341
- const domain = selfAid.split('.').slice(1).join('.');
1342
- if (!domain)
1343
- return '❌ 无法推导 AID 域(当前实例未连接)';
1344
- fullAid = `${rawName}.${domain}`;
1345
- }
1346
- // Validate AID format
1347
- const { isValidAid } = await import('../utils/init-channel.js');
1348
- if (!isValidAid(fullAid))
1349
- return `❌ 无效 AID 格式: ${fullAid}`;
1350
- // Check instance name conflict
1351
- const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
1352
- const { normalizeChannelInstances } = await import('../config.js');
1353
- const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1354
- if (existing.some(e => e.name === instName)) {
1355
- return `❌ 实例名 "${instName}" 已存在`;
1356
- }
1357
- if (existing.some(e => e.aid === fullAid)) {
1358
- return `❌ AID ${fullAid} 已在配置中`;
1359
- }
1360
- // Create AID (reuse init-channel.ts silent logic)
1440
+ const rawAid = arg.slice(4).trim();
1441
+ if (!rawAid)
1442
+ return '用法: /aid new <完整AID>\n例: /aid new reviewer.agentid.pub';
1443
+ if (!isValidAid(rawAid))
1444
+ return `❌ 无效 AID 格式: ${rawAid}`;
1361
1445
  try {
1362
- const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
1363
- const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
1364
- // Resolve owner from current AUN instance config
1365
- const owner = this.config.channels?.aun
1366
- ? (Array.isArray(this.config.channels.aun)
1367
- ? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
1368
- : this.config.channels.aun.owner)
1369
- : undefined;
1370
- // Hot-load: build and register new channel instance BEFORE writing config
1371
- const { AUNChannelPlugin } = await import('../channels/aun.js');
1372
- const plugin = new AUNChannelPlugin();
1373
- const tempConfig = JSON.parse(JSON.stringify(this.config));
1374
- tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
1375
- const newInstances = await plugin.createChannels(tempConfig);
1376
- if (newInstances.length === 0)
1377
- return '❌ 通道实例创建失败';
1378
- await this.hotLoadChannel(newInstances[0]);
1379
- // Write config only after successful hot-load
1380
- appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
1381
- const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
1382
- return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
1446
+ const result = await aidCreate(rawAid);
1447
+ if (!result.alreadyExisted) {
1448
+ const content = buildInitialAgentMd({ aid: rawAid });
1449
+ try {
1450
+ await agentmdPut(content, { aid: rawAid, client: result.client });
1451
+ }
1452
+ catch { /* non-fatal */ }
1453
+ }
1454
+ try {
1455
+ await result.client.close();
1456
+ }
1457
+ catch { /* ignore */ }
1458
+ const verb = result.alreadyExisted ? '已存在' : '已创建';
1459
+ return `✓ ${rawAid} ${verb}
1460
+ 如需上线 AUN 通道,运行 evolclaw init aun`;
1383
1461
  }
1384
1462
  catch (e) {
1385
1463
  return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
@@ -1387,7 +1465,7 @@ export class CommandHandler {
1387
1465
  }
1388
1466
  return '用法: /aid [list|new <aid>]';
1389
1467
  }
1390
- // /activity 命令:控制中间输出显示模式
1468
+ // /agentmd 命令:管理 agent.md 身份文件
1391
1469
  if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1392
1470
  if (!isOwner)
1393
1471
  return '❌ 无权限:此命令仅限 owner 使用';
@@ -1396,7 +1474,8 @@ export class CommandHandler {
1396
1474
  return '❌ 当前通道不支持 agent.md 操作';
1397
1475
  const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1398
1476
  const arg = normalizedContent.slice(9).trim();
1399
- // put read local ~/.aun/AIDs/{aid}/agent.md and upload
1477
+ const { agentmdGet, agentmdPut } = await import('../channels/aun-ops.js');
1478
+ // put — read local agent.md and upload to network
1400
1479
  if (arg === 'put') {
1401
1480
  if (!selfAid)
1402
1481
  return '❌ 未连接,无法确定本地 AID';
@@ -1405,15 +1484,17 @@ export class CommandHandler {
1405
1484
  const { join } = await import('node:path');
1406
1485
  const { homedir } = await import('node:os');
1407
1486
  const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
1487
+ if (!readFileSync)
1488
+ return '❌ 读取失败';
1408
1489
  const content = readFileSync(localPath, 'utf-8');
1409
- await adapter.uploadAgentMd(content);
1490
+ await agentmdPut(content, { aid: selfAid });
1410
1491
  return '✅ agent.md 已发布';
1411
1492
  }
1412
1493
  catch (e) {
1413
1494
  return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1414
1495
  }
1415
1496
  }
1416
- // set <content> — upload inline content and sync to local
1497
+ // set <content> — upload inline content
1417
1498
  if (arg.startsWith('set ')) {
1418
1499
  const content = arg.slice(4).trim();
1419
1500
  if (!content)
@@ -1421,13 +1502,7 @@ export class CommandHandler {
1421
1502
  if (!selfAid)
1422
1503
  return '❌ 未连接,无法确定本地 AID';
1423
1504
  try {
1424
- await adapter.uploadAgentMd(content);
1425
- const { writeFileSync, mkdirSync } = await import('node:fs');
1426
- const { join } = await import('node:path');
1427
- const { homedir } = await import('node:os');
1428
- const localDir = join(homedir(), '.aun', 'AIDs', selfAid);
1429
- mkdirSync(localDir, { recursive: true });
1430
- writeFileSync(join(localDir, 'agent.md'), content, 'utf-8');
1505
+ await agentmdPut(content, { aid: selfAid });
1431
1506
  return '✅ agent.md 已更新并发布到AUN网络';
1432
1507
  }
1433
1508
  catch (e) {
@@ -1439,7 +1514,7 @@ export class CommandHandler {
1439
1514
  if (!aidToView)
1440
1515
  return '用法:/agentmd [<aid>] | put | set <内容>';
1441
1516
  try {
1442
- const md = await adapter.downloadAgentMd(aidToView);
1517
+ const md = await agentmdGet(aidToView);
1443
1518
  if (!md || !md.trim())
1444
1519
  return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1445
1520
  return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
@@ -1467,7 +1542,7 @@ export class CommandHandler {
1467
1542
  owner: 'owner-dm-only',
1468
1543
  none: 'none',
1469
1544
  };
1470
- const currentMode = getChannelShowActivities(this.config, channel);
1545
+ const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? getChannelShowActivities(this.config, channel);
1471
1546
  // 模式描述列表(用于 body 和文本降级)
1472
1547
  const modeDescriptions = [
1473
1548
  { key: 'all', configVal: 'all', label: '全部显示' },
@@ -1538,7 +1613,12 @@ export class CommandHandler {
1538
1613
  // 切换操作仅 owner
1539
1614
  if (!isOwner)
1540
1615
  return '❌ 中间输出模式切换仅限 owner';
1541
- setChannelShowActivities(this.config, channel, newMode);
1616
+ if (this.agentRegistry?.setShowActivities) {
1617
+ this.agentRegistry.setShowActivities(channel, newMode);
1618
+ }
1619
+ else {
1620
+ setChannelShowActivities(this.config, channel, newMode);
1621
+ }
1542
1622
  return `✅ 中间输出模式: ${activityArg}(${label})`;
1543
1623
  }
1544
1624
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
@@ -1596,7 +1676,12 @@ export class CommandHandler {
1596
1676
  }
1597
1677
  await stopAgent.interrupt(sessionKey);
1598
1678
  // 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
1599
- this.eventBus.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'stop' });
1679
+ this.eventBus.publish({
1680
+ type: 'message:interrupted',
1681
+ sessionId: sessionKey,
1682
+ reason: 'stop',
1683
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '[default]',
1684
+ });
1600
1685
  // 强制清除 processing_state
1601
1686
  this.sessionManager.clearProcessing(sessionKey);
1602
1687
  return '✓ 已发送中断信号,任务将尽快停止';
@@ -1669,7 +1754,7 @@ export class CommandHandler {
1669
1754
  // 尝试获取活跃会话(话题时直接查找话题 session)
1670
1755
  let session;
1671
1756
  if (threadId) {
1672
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
1757
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
1673
1758
  }
1674
1759
  else {
1675
1760
  session = await this.sessionManager.getActiveSession(channel, channelId);
@@ -1681,7 +1766,7 @@ export class CommandHandler {
1681
1766
  normalizedContent.startsWith('/project') ||
1682
1767
  normalizedContent === '/pwd' ||
1683
1768
  normalizedContent === '/status')) {
1684
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd());
1769
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel));
1685
1770
  }
1686
1771
  // /status 命令:显示会话状态
1687
1772
  if (normalizedContent === '/status') {
@@ -1750,7 +1835,7 @@ export class CommandHandler {
1750
1835
  return `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`;
1751
1836
  }
1752
1837
  }
1753
- const projectPath = session?.projectPath || this.config.projects?.defaultPath || process.cwd();
1838
+ const projectPath = session?.projectPath || this.getEffectiveDefaultPath(channel);
1754
1839
  const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.defaultAgentId);
1755
1840
  this.eventBus.publish({
1756
1841
  type: 'session:created',
@@ -1772,11 +1857,30 @@ export class CommandHandler {
1772
1857
  // /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
1773
1858
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
1774
1859
  const subCmd = normalizedContent.slice('/check'.length).trim();
1860
+ // 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
1861
+ // default 通道也仅显示 default 的渠道(不再展示 evolagents 的渠道)
1862
+ const checkOwningAgent = this.getOwningAgent(channel);
1863
+ let allowedChannels;
1864
+ if (checkOwningAgent) {
1865
+ allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
1866
+ }
1867
+ else {
1868
+ // default 范围:所有 channel 中,不属于任何 evolagent 的
1869
+ const defaultNames = [];
1870
+ for (const [name] of this.adapters) {
1871
+ const owner = this.agentRegistry?.resolveByChannel(name);
1872
+ if (!owner || owner.isDefault)
1873
+ defaultNames.push(name);
1874
+ }
1875
+ allowedChannels = new Set(defaultNames);
1876
+ }
1775
1877
  // Default: show system health check (non-admin 仅看摘要)
1776
1878
  const lines = ['📡 渠道状态:'];
1777
1879
  // Group by channelType
1778
1880
  const groups = new Map();
1779
1881
  for (const [name] of this.adapters) {
1882
+ if (!allowedChannels.has(name))
1883
+ continue;
1780
1884
  const type = this.channelTypeMap.get(name) || name;
1781
1885
  const ch = this.channelObjects.get(name);
1782
1886
  let status;
@@ -1807,19 +1911,21 @@ export class CommandHandler {
1807
1911
  lines.push(` ${type}: [${parts.join(', ')}]`);
1808
1912
  }
1809
1913
  }
1810
- // 队列状态
1914
+ // 当前 agent 名(用于 agent 维度 stats / queue 查询)
1915
+ const currentAgentName = checkOwningAgent?.name ?? '[default]';
1916
+ // 队列状态(按当前 agent 维度)
1811
1917
  lines.push('', '📬 队列状态:');
1812
- lines.push(` 待处理消息: ${this.messageQueue.getGlobalQueueLength()}`);
1813
- lines.push(` 处理中队列: ${this.messageQueue.getGlobalProcessingCount()}`);
1814
- // 运行概况
1918
+ lines.push(` 待处理消息: ${this.messageQueue.getQueueLengthByAgent(currentAgentName)}`);
1919
+ lines.push(` 处理中队列: ${this.messageQueue.getProcessingCountByAgent(currentAgentName)}`);
1920
+ // 运行概况(全局,进程级)
1815
1921
  lines.push('', '🖥️ 运行概况:');
1816
1922
  const uptimeMs = this.statsCollector
1817
1923
  ? this.statsCollector.getSnapshot().uptimeMs
1818
1924
  : process.uptime() * 1000;
1819
1925
  lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
1820
- // 近 1 小时统计
1926
+ // 近 1 小时统计(按当前 agent 维度)
1821
1927
  if (this.statsCollector) {
1822
- const snap = this.statsCollector.getSnapshot();
1928
+ const snap = this.statsCollector.getSnapshot(currentAgentName);
1823
1929
  const h = snap.lastHour;
1824
1930
  lines.push('', '📊 近 1 小时统计:');
1825
1931
  lines.push(` 收到消息: ${h.received}`);
@@ -1845,23 +1951,50 @@ export class CommandHandler {
1845
1951
  // /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
1846
1952
  if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
1847
1953
  const restartArg = normalizedContent.slice('/restart'.length).trim();
1848
- // /restart <channel> — 重连指定渠道(admin only
1954
+ // /restart <type> — 重连指定类型的所有渠道(admin only,evolclaw 服务级操作)
1955
+ // 服务级操作仅可从 default 通道发起,避免 evolagent owner/admin 越权
1849
1956
  if (restartArg) {
1957
+ if (this.getOwningAgent(channel)) {
1958
+ return '❌ 渠道重连只能从 DefaultAgent 通道发起(服务级操作)';
1959
+ }
1850
1960
  if (!isAdmin)
1851
1961
  return '❌ 无权限:渠道重连仅限管理员使用';
1852
- const target = restartArg;
1853
- const ch = this.channelObjects.get(target);
1854
- if (!ch) {
1855
- const available = [...this.channelObjects.keys()].join(', ') || '无';
1856
- return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
1857
- }
1858
- if (!ch.reconnect) {
1859
- return `❌ 渠道 "${target}" 不支持重连`;
1962
+ const type = restartArg;
1963
+ // /restart 是服务级操作:重连该 type 下的所有实例(不分 agent)
1964
+ const scopedNames = [];
1965
+ for (const [name] of this.adapters) {
1966
+ if (this.channelTypeMap.get(name) === type)
1967
+ scopedNames.push(name);
1968
+ }
1969
+ if (scopedNames.length === 0) {
1970
+ return `❌ 没有类型为 "${type}" 的渠道`;
1971
+ }
1972
+ const results = [];
1973
+ for (const name of scopedNames) {
1974
+ const ch = this.channelObjects.get(name);
1975
+ if (!ch) {
1976
+ results.push(`${name}: 未找到渠道对象`);
1977
+ continue;
1978
+ }
1979
+ if (!ch.reconnect) {
1980
+ results.push(`${name}: 不支持重连`);
1981
+ continue;
1982
+ }
1983
+ try {
1984
+ const result = await ch.reconnect();
1985
+ results.push(`${name}: ${result}`);
1986
+ }
1987
+ catch (e) {
1988
+ results.push(`${name}: 重连失败 - ${e?.message || e}`);
1989
+ }
1860
1990
  }
1861
- const result = await ch.reconnect();
1862
- return `🔄 ${target} 重连: ${result}`;
1991
+ return `🔄 重连 ${type}:\n ${results.join('\n ')}`;
1992
+ }
1993
+ // /restart(无参数)— 重启整个服务(owner only,且仅可从 default 通道触发)
1994
+ // 防止 evolagent 通道的 owner 越权杀整个 evolclaw 进程(影响所有租户)
1995
+ if (this.getOwningAgent(channel)) {
1996
+ return '❌ 服务重启只能从 DefaultAgent 通道发起。EvolAgent 通道仅可执行 /restart <type> 重连特定类型渠道';
1863
1997
  }
1864
- // /restart(无参数)— 重启整个服务(owner only)
1865
1998
  if (!isOwner)
1866
1999
  return '❌ 无权限:服务重启仅限 owner 使用';
1867
2000
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
@@ -1875,7 +2008,7 @@ export class CommandHandler {
1875
2008
  const executeRestart = async () => {
1876
2009
  let replyContext;
1877
2010
  if (threadId) {
1878
- const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
2011
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
1879
2012
  replyContext = this.getReplyContext(threadSession);
1880
2013
  }
1881
2014
  const restartInfo = {
@@ -2014,7 +2147,7 @@ export class CommandHandler {
2014
2147
  // 找目标 channelId
2015
2148
  let targetChannelId = channelId;
2016
2149
  if (isCrossChannel) {
2017
- const ownerPeerId = getOwner(this.config, targetChannel);
2150
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel) ?? getOwner(this.config, targetChannel);
2018
2151
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
2019
2152
  if (!targetChannelId) {
2020
2153
  return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
@@ -2269,27 +2402,19 @@ export class CommandHandler {
2269
2402
  }
2270
2403
  // 生成项目名称(使用目录名)
2271
2404
  const projectName = path.basename(projectPath);
2272
- // 检查是否已存在
2273
- if (this.projects[projectName]) {
2274
- const existingPath = this.projects[projectName];
2275
- if (existingPath === projectPath) {
2405
+ // 检查在当前 scope 内是否已存在
2406
+ const scopeProjects = this.getEffectiveProjects(channel);
2407
+ const existing = scopeProjects[projectName];
2408
+ if (existing) {
2409
+ if (existing === projectPath) {
2276
2410
  return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2277
2411
  }
2278
- return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existingPath}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
2279
- }
2280
- // 添加到配置
2281
- if (!this.config.projects) {
2282
- this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
2283
- }
2284
- if (!this.config.projects.list) {
2285
- this.config.projects.list = {};
2412
+ return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
2286
2413
  }
2287
- this.config.projects.list[projectName] = projectPath;
2288
- // 保存配置
2289
- const { saveConfig } = await import('../config.js');
2290
- saveConfig(this.config);
2291
- // 更新内存中的项目列表
2292
- this.projects[projectName] = projectPath;
2414
+ // 写入:agent-owned channel → agent.json;default evolclaw.json
2415
+ const err = await this.addProjectInScope(channel, projectName, projectPath);
2416
+ if (err)
2417
+ return err;
2293
2418
  return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2294
2419
  }
2295
2420
  // /slist 命令:列出当前项目的会话
@@ -2530,11 +2655,11 @@ export class CommandHandler {
2530
2655
  return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2531
2656
  }
2532
2657
  }
2533
- if (!targetSession && sessionName.length === 8) {
2658
+ if (!targetSession && sessionName.length >= 8) {
2534
2659
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2535
2660
  }
2536
2661
  const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
2537
- if (!targetSession && sessionName.length === 8 && canImport) {
2662
+ if (!targetSession && sessionName.length >= 8 && canImport) {
2538
2663
  const projectPaths = Object.values(this.projects);
2539
2664
  if (session) {
2540
2665
  projectPaths.unshift(session.projectPath);
@@ -2637,7 +2762,7 @@ export class CommandHandler {
2637
2762
  return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2638
2763
  }
2639
2764
  }
2640
- if (!targetSession && sessionName.length === 8) {
2765
+ if (!targetSession && sessionName.length >= 8) {
2641
2766
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2642
2767
  }
2643
2768
  if (!targetSession) {
@@ -2881,7 +3006,7 @@ export class CommandHandler {
2881
3006
  '/help', '/status', '/check', '/pwd',
2882
3007
  '/model', '/effort', '/perm', '/agent',
2883
3008
  '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
2884
- '/rename', '/name',
3009
+ '/rename', '/name', '/evolagent',
2885
3010
  ];
2886
3011
  /** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
2887
3012
  static CTL_READONLY = new Set(['/agent']);
@@ -2935,6 +3060,48 @@ export class CommandHandler {
2935
3060
  }
2936
3061
  // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
2937
3062
  const userId = session.metadata?.peerId;
3063
+ // 3.1 /evolagent: EvolAgent 管理(show identity / reload)
3064
+ if (cmd === '/evolagent' || cmd.startsWith('/evolagent ')) {
3065
+ const arg = cmd.slice('/evolagent'.length).trim();
3066
+ if (!arg) {
3067
+ const owning = this.getOwningAgent(session.channel);
3068
+ if (owning) {
3069
+ return { ok: true, result: `当前 EvolAgent: ${owning.name} (${owning.baseagent})` };
3070
+ }
3071
+ return { ok: true, result: '当前为 DefaultAgent 模式' };
3072
+ }
3073
+ if (arg.startsWith('reload ') || arg === 'reload') {
3074
+ const name = arg === 'reload' ? '' : arg.slice('reload '.length).trim();
3075
+ if (!name)
3076
+ return { ok: false, error: '用法: evolclaw ctl evolagent reload <name>' };
3077
+ // I8: reload is a structural op, require admin or owner
3078
+ if (!userId) {
3079
+ return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
3080
+ }
3081
+ const identity = this.sessionManager.resolveIdentity(session.channel, userId);
3082
+ if (identity.role !== 'owner' && identity.role !== 'admin') {
3083
+ return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
3084
+ }
3085
+ if (!this.agentRegistry)
3086
+ return { ok: false, error: 'EvolAgentRegistry not available' };
3087
+ const a = this.agentRegistry.get(name);
3088
+ if (!a)
3089
+ return { ok: false, error: `Agent "${name}" not found` };
3090
+ const hooks = globalThis.__evolclaw_reloadHooks;
3091
+ if (!hooks)
3092
+ return { ok: false, error: 'Reload hooks not initialized' };
3093
+ if (!this.agentRegistry.reload)
3094
+ return { ok: false, error: 'EvolAgentRegistry.reload not available' };
3095
+ try {
3096
+ await this.agentRegistry.reload(name, hooks);
3097
+ return { ok: true, result: `Agent "${name}" reloaded` };
3098
+ }
3099
+ catch (e) {
3100
+ return { ok: false, error: `Reload failed: ${e?.message || e}` };
3101
+ }
3102
+ }
3103
+ return { ok: false, error: '用法: evolclaw ctl evolagent [reload <name>]' };
3104
+ }
2938
3105
  // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
2939
3106
  if (cmd.startsWith('/send ') || cmd === '/send') {
2940
3107
  const text = cmd.startsWith('/send ') ? cmd.slice(6).trim() : '';