evolclaw 2.6.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -51,7 +51,7 @@ function loadCodexSettings() {
51
51
  export function resolveAnthropicConfig(config) {
52
52
  const settings = loadClaudeSettings();
53
53
  // 过滤占位符,视为未配置
54
- const configApiKey = config.agents?.anthropic?.apiKey;
54
+ const configApiKey = config.agents?.claude?.apiKey;
55
55
  const isPlaceholderKey = !configApiKey ||
56
56
  configApiKey.includes('your-') ||
57
57
  configApiKey.includes('placeholder');
@@ -59,21 +59,21 @@ export function resolveAnthropicConfig(config) {
59
59
  || process.env.ANTHROPIC_AUTH_TOKEN
60
60
  || settings.env?.ANTHROPIC_AUTH_TOKEN;
61
61
  if (!apiKey) {
62
- throw new Error('No API key found. Set one of: agents.anthropic.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
62
+ throw new Error('No API key found. Set one of: agents.claude.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
63
63
  }
64
64
  // baseUrl 也过滤占位符
65
- const configBaseUrl = config.agents?.anthropic?.baseUrl;
65
+ const configBaseUrl = config.agents?.claude?.baseUrl;
66
66
  const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
67
67
  const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
68
68
  || process.env.ANTHROPIC_BASE_URL
69
69
  || settings.env?.ANTHROPIC_BASE_URL;
70
- const model = config.agents?.anthropic?.model
70
+ const model = config.agents?.claude?.model
71
71
  || settings.model
72
72
  || 'sonnet';
73
- const effort = config.agents?.anthropic?.effort
73
+ const effort = config.agents?.claude?.effort
74
74
  || settings.effortLevel
75
75
  || undefined;
76
- const configExecPath = config.agents?.anthropic?.pathToClaudeCodeExecutable;
76
+ const configExecPath = config.agents?.claude?.pathToClaudeCodeExecutable;
77
77
  const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
78
78
  const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
79
79
  return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
@@ -81,7 +81,7 @@ export function resolveAnthropicConfig(config) {
81
81
  export function resolveOpenaiConfig(config) {
82
82
  const codexSettings = loadCodexSettings();
83
83
  // 过滤占位符,视为未配置
84
- const configApiKey = config.agents?.openai?.apiKey;
84
+ const configApiKey = config.agents?.codex?.apiKey;
85
85
  const isPlaceholderKey = !configApiKey ||
86
86
  configApiKey.includes('your-') ||
87
87
  configApiKey.includes('placeholder');
@@ -89,23 +89,23 @@ export function resolveOpenaiConfig(config) {
89
89
  || process.env.OPENAI_API_KEY
90
90
  || codexSettings.apiKey;
91
91
  if (!apiKey) {
92
- throw new Error('No OpenAI API key found. Set one of: agents.openai.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
92
+ throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
93
93
  }
94
94
  // baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
95
- const configBaseUrl = config.agents?.openai?.baseUrl;
95
+ const configBaseUrl = config.agents?.codex?.baseUrl;
96
96
  const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
97
97
  const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
98
98
  || process.env.OPENAI_BASE_URL
99
99
  || codexSettings.baseUrl
100
100
  || undefined;
101
- const model = config.agents?.openai?.model
101
+ const model = config.agents?.codex?.model
102
102
  || codexSettings.model
103
103
  || 'gpt-5.2-codex';
104
- const effort = config.agents?.openai?.effort || config.agents?.openai?.reasoning || undefined;
104
+ const effort = config.agents?.codex?.effort || config.agents?.codex?.reasoning || undefined;
105
105
  return { apiKey, baseUrl, model, effort };
106
106
  }
107
107
  export function resolveGoogleConfig(config) {
108
- const googleCfg = config.agents?.google;
108
+ const googleCfg = config.agents?.gemini;
109
109
  // CLI path: config → which gemini
110
110
  let cliPath = googleCfg?.cliPath || '';
111
111
  if (!cliPath) {
@@ -170,9 +170,41 @@ export function loadConfig(configPath = resolvePaths().config) {
170
170
  }
171
171
  const content = fs.readFileSync(configPath, 'utf-8');
172
172
  const config = JSON.parse(content);
173
+ if (migrateAgentsKeys(config)) {
174
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
175
+ logger.warn(`Config migrated: agents.{anthropic,openai,google} → {claude,codex,gemini} in ${configPath}`);
176
+ }
173
177
  validateConfig(config);
174
178
  return config;
175
179
  }
180
+ /**
181
+ * Rename legacy agent config keys to runner names.
182
+ * Returns true if any rename happened.
183
+ */
184
+ function migrateAgentsKeys(config) {
185
+ const agents = config?.agents;
186
+ if (!agents || typeof agents !== 'object')
187
+ return false;
188
+ const renames = [
189
+ ['anthropic', 'claude'],
190
+ ['openai', 'codex'],
191
+ ['google', 'gemini'],
192
+ ];
193
+ let changed = false;
194
+ for (const [oldKey, newKey] of renames) {
195
+ if (agents[oldKey] === undefined)
196
+ continue;
197
+ if (agents[newKey] === undefined) {
198
+ agents[newKey] = agents[oldKey];
199
+ }
200
+ else {
201
+ logger.warn(`Config has both agents.${oldKey} and agents.${newKey}; keeping new key, dropping legacy one`);
202
+ }
203
+ delete agents[oldKey];
204
+ changed = true;
205
+ }
206
+ return changed;
207
+ }
176
208
  export function saveConfig(config, configPath = resolvePaths().config) {
177
209
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
178
210
  }
@@ -309,26 +341,16 @@ export function setChannelShowActivities(config, instanceName, mode) {
309
341
  }
310
342
  }
311
343
  /**
312
- * 读取通道实例的 sessionMode 锁定配置
313
- * 返回 undefined 表示未配置(由 session-manager chatType 默认决定)
344
+ * 读取全局 chatmode 配置的默认 sessionMode
345
+ * chatType 返回对应模式,未配置时返回 undefined(由 session-manager 回退到 'interactive')
314
346
  */
315
- export function getChannelSessionMode(config, instanceName) {
316
- for (const type of channelTypes) {
317
- const raw = config.channels?.[type];
318
- if (raw === undefined)
319
- continue;
320
- if (Array.isArray(raw)) {
321
- const inst = raw.find((item) => item.name === instanceName);
322
- if (inst)
323
- return inst.sessionMode;
324
- }
325
- else {
326
- const effectiveName = raw.name ?? type;
327
- if (effectiveName === instanceName)
328
- return raw.sessionMode;
329
- }
330
- }
331
- return undefined;
347
+ export function getDefaultSessionMode(config, chatType) {
348
+ const cm = config.chatmode;
349
+ if (!cm)
350
+ return undefined;
351
+ if (chatType === 'group')
352
+ return cm.group;
353
+ return cm.private;
332
354
  }
333
355
  export function isOwner(config, channelOrType, userId) {
334
356
  // 按实例名精确匹配
@@ -441,29 +463,33 @@ export function ensureDir(dirPath) {
441
463
  fs.mkdirSync(dirPath, { recursive: true });
442
464
  }
443
465
  }
444
- // agents.defaultAgent → config key 映射
445
- const agentKeyMap = { claude: 'anthropic', codex: 'openai', gemini: 'google' };
446
466
  /**
447
467
  * 配置结构完整性校验(不校验凭据有效性)。
448
468
  * 要求 agents/channels/projects 三段同时具备必要的锚点字段。
449
469
  */
450
470
  export function validateConfigIntegrity(config) {
451
471
  const reasons = [];
452
- // agents
472
+ // agents — 单 agent 时自动推断,无需显式 defaultAgent
453
473
  const defaultAgent = config.agents?.defaultAgent;
454
474
  if (!defaultAgent) {
455
- reasons.push('Missing agents.defaultAgent');
475
+ const agentKeys = Object.keys(config.agents || {}).filter(k => k !== 'defaultAgent');
476
+ const configuredAgents = agentKeys.filter(k => config.agents?.[k]);
477
+ if (configuredAgents.length === 0 && agentKeys.length !== 1) {
478
+ reasons.push('Missing agents.defaultAgent (multiple or no agents configured)');
479
+ }
456
480
  }
457
481
  else {
458
- const key = agentKeyMap[defaultAgent] || defaultAgent;
459
- if (!config.agents?.[key]) {
460
- reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${key} does not exist`);
482
+ if (!config.agents?.[defaultAgent]) {
483
+ reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${defaultAgent} does not exist`);
461
484
  }
462
485
  }
463
- // channels
486
+ // channels — 单通道时自动推断,无需显式 defaultChannel
464
487
  const defaultChannel = config.channels?.defaultChannel;
465
488
  if (!defaultChannel) {
466
- reasons.push('Missing channels.defaultChannel');
489
+ const channelKeys = channelTypes.filter(t => config.channels?.[t]);
490
+ if (channelKeys.length === 0) {
491
+ reasons.push('Missing channels.defaultChannel (no channels configured)');
492
+ }
467
493
  }
468
494
  else {
469
495
  if (!config.channels?.[defaultChannel]) {
@@ -1,5 +1,5 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
- import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities, getChannelSessionMode } from '../config.js';
2
+ import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import crypto from 'crypto';
5
5
  import path from 'path';
@@ -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', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode'];
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', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd'];
116
116
  export class CommandHandler {
117
117
  sessionManager;
118
118
  config;
@@ -150,9 +150,15 @@ export class CommandHandler {
150
150
  this.defaultAgentId = agentRunnerOrMap.name;
151
151
  }
152
152
  }
153
- /** 项目列表快捷访问 */
153
+ /** 项目列表快捷访问(list 缺失时用 defaultPath 作为唯一项目) */
154
154
  get projects() {
155
- return this.config.projects?.list || {};
155
+ const list = this.config.projects?.list;
156
+ if (list && Object.keys(list).length > 0)
157
+ return list;
158
+ const dp = this.config.projects?.defaultPath;
159
+ if (dp)
160
+ return { [path.basename(dp)]: dp };
161
+ return {};
156
162
  }
157
163
  /** 根据项目路径查找配置中的项目名称 */
158
164
  getConfiguredProjectName(projectPath) {
@@ -532,6 +538,7 @@ export class CommandHandler {
532
538
  if (normalizedContent !== content) {
533
539
  logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
534
540
  }
541
+ logger.info(`[CommandHandler] handle: channel=${channel} channelId=${channelId} cmd="${normalizedContent.split(' ')[0]}" user=${userId ?? 'n/a'} role=${identity?.role ?? 'n/a'}`);
535
542
  // 话题内禁用部分命令
536
543
  if (threadId) {
537
544
  const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
@@ -701,7 +708,7 @@ export class CommandHandler {
701
708
  if (!hasPermissionController(permAgent)) {
702
709
  return '❌ 权限控制不可用';
703
710
  }
704
- const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
711
+ const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'auto';
705
712
  const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
706
713
  const modes = permAgent.listModes();
707
714
  // 尝试发送交互卡片
@@ -737,6 +744,10 @@ export class CommandHandler {
737
744
  const adapter = this.adapters.get(channel);
738
745
  adapter?.sendText(channelId, result, replyCtx);
739
746
  }
747
+ else {
748
+ // 切换成功后重新发新卡片(会自动 invalidate 旧卡片)
749
+ await this.handle('/perm', channel, channelId, undefined, userId, threadId);
750
+ }
740
751
  }
741
752
  },
742
753
  });
@@ -1008,14 +1019,14 @@ export class CommandHandler {
1008
1019
  // evolclaw.json 配了 → 写 evolclaw.json
1009
1020
  // evolclaw.json 没配 → 写 agent 全局配置
1010
1021
  if (isCodexAgent) {
1011
- const configuredInEvolclaw = !!(this.config.agents?.openai?.model || this.config.agents?.openai?.reasoning);
1022
+ const configuredInEvolclaw = !!(this.config.agents?.codex?.model || this.config.agents?.codex?.reasoning);
1012
1023
  if (configuredInEvolclaw) {
1013
- if (!this.config.agents.openai)
1014
- this.config.agents.openai = {};
1024
+ if (!this.config.agents.codex)
1025
+ this.config.agents.codex = {};
1015
1026
  if (newModel)
1016
- this.config.agents.openai.model = newModel;
1027
+ this.config.agents.codex.model = newModel;
1017
1028
  if (newEffort)
1018
- this.config.agents.openai.reasoning = newEffort;
1029
+ this.config.agents.codex.reasoning = newEffort;
1019
1030
  try {
1020
1031
  saveConfig(this.config);
1021
1032
  }
@@ -1025,12 +1036,12 @@ export class CommandHandler {
1025
1036
  }
1026
1037
  else {
1027
1038
  // Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
1028
- if (!this.config.agents.openai)
1029
- this.config.agents.openai = {};
1039
+ if (!this.config.agents.codex)
1040
+ this.config.agents.codex = {};
1030
1041
  if (newModel)
1031
- this.config.agents.openai.model = newModel;
1042
+ this.config.agents.codex.model = newModel;
1032
1043
  if (newEffort)
1033
- this.config.agents.openai.reasoning = newEffort;
1044
+ this.config.agents.codex.reasoning = newEffort;
1034
1045
  try {
1035
1046
  saveConfig(this.config);
1036
1047
  }
@@ -1040,14 +1051,14 @@ export class CommandHandler {
1040
1051
  }
1041
1052
  }
1042
1053
  else {
1043
- const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
1054
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1044
1055
  if (configuredInEvolclaw) {
1045
- if (!this.config.agents.anthropic)
1046
- this.config.agents.anthropic = {};
1056
+ if (!this.config.agents.claude)
1057
+ this.config.agents.claude = {};
1047
1058
  if (newModel)
1048
- this.config.agents.anthropic.model = newModel;
1059
+ this.config.agents.claude.model = newModel;
1049
1060
  if (newEffort)
1050
- this.config.agents.anthropic.effort = newEffort;
1061
+ this.config.agents.claude.effort = newEffort;
1051
1062
  try {
1052
1063
  saveConfig(this.config);
1053
1064
  }
@@ -1146,8 +1157,8 @@ export class CommandHandler {
1146
1157
  effortAgent.setEffort?.(undefined);
1147
1158
  const isCodex = effortAgent.name === 'codex';
1148
1159
  if (isCodex) {
1149
- if (this.config.agents?.openai?.reasoning) {
1150
- delete this.config.agents.openai.reasoning;
1160
+ if (this.config.agents?.codex?.reasoning) {
1161
+ delete this.config.agents.codex.reasoning;
1151
1162
  try {
1152
1163
  saveConfig(this.config);
1153
1164
  }
@@ -1155,9 +1166,9 @@ export class CommandHandler {
1155
1166
  }
1156
1167
  }
1157
1168
  else {
1158
- const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
1169
+ const configuredInEvolclaw = !!this.config.agents?.claude?.effort;
1159
1170
  if (configuredInEvolclaw) {
1160
- delete this.config.agents.anthropic.effort;
1171
+ delete this.config.agents.claude.effort;
1161
1172
  try {
1162
1173
  saveConfig(this.config);
1163
1174
  }
@@ -1183,20 +1194,20 @@ export class CommandHandler {
1183
1194
  this.config.agents = {};
1184
1195
  const isCodex = effortAgent.name === 'codex';
1185
1196
  if (isCodex) {
1186
- if (!this.config.agents.openai)
1187
- this.config.agents.openai = {};
1188
- this.config.agents.openai.reasoning = newEffort;
1197
+ if (!this.config.agents.codex)
1198
+ this.config.agents.codex = {};
1199
+ this.config.agents.codex.reasoning = newEffort;
1189
1200
  try {
1190
1201
  saveConfig(this.config);
1191
1202
  }
1192
1203
  catch { }
1193
1204
  }
1194
1205
  else {
1195
- const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
1206
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1196
1207
  if (configuredInEvolclaw) {
1197
- if (!this.config.agents.anthropic)
1198
- this.config.agents.anthropic = {};
1199
- this.config.agents.anthropic.effort = newEffort;
1208
+ if (!this.config.agents.claude)
1209
+ this.config.agents.claude = {};
1210
+ this.config.agents.claude.effort = newEffort;
1200
1211
  try {
1201
1212
  saveConfig(this.config);
1202
1213
  }
@@ -1456,16 +1467,14 @@ export class CommandHandler {
1456
1467
  if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1457
1468
  if (!activeSession)
1458
1469
  return '❌ 当前无活跃会话';
1459
- const lockedMode = getChannelSessionMode(this.config, channel);
1460
1470
  const arg = normalizedContent.slice(9).trim();
1461
1471
  const currentMode = activeSession.sessionMode || 'interactive';
1462
1472
  if (!arg) {
1463
- const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
1464
1473
  const canSwitch = activeChatType !== 'group' || isAdmin;
1465
- if (canSwitch && !lockedMode) {
1466
- return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1474
+ if (canSwitch) {
1475
+ return `📋 当前会话模式: ${currentMode}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1467
1476
  }
1468
- return `📋 当前会话模式: ${currentMode}${lockHint}`;
1477
+ return `📋 当前会话模式: ${currentMode}`;
1469
1478
  }
1470
1479
  if (arg !== 'interactive' && arg !== 'proactive') {
1471
1480
  return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
@@ -1473,9 +1482,6 @@ export class CommandHandler {
1473
1482
  if (activeChatType === 'group' && !isAdmin) {
1474
1483
  return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
1475
1484
  }
1476
- if (lockedMode) {
1477
- return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
1478
- }
1479
1485
  if (arg === currentMode) {
1480
1486
  return `📋 当前会话模式已是 ${arg}`;
1481
1487
  }
@@ -1637,8 +1643,7 @@ export class CommandHandler {
1637
1643
  }
1638
1644
  const lines = [];
1639
1645
  const sessionMode = session.sessionMode || 'interactive';
1640
- const lockedMode = getChannelSessionMode(this.config, channel);
1641
- const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
1646
+ const chatModeLine = `会话模式: ${sessionMode}`;
1642
1647
  if (isAdmin) {
1643
1648
  lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
1644
1649
  if (health.consecutiveErrors > 0) {
@@ -2175,7 +2180,12 @@ export class CommandHandler {
2175
2180
  return '❌ 项目路径必须是绝对路径';
2176
2181
  }
2177
2182
  if (!fs.existsSync(projectPath)) {
2178
- return `❌ 路径不存在: ${projectPath}`;
2183
+ if (this.config.projects?.autoCreate) {
2184
+ fs.mkdirSync(projectPath, { recursive: true });
2185
+ }
2186
+ else {
2187
+ return `❌ 路径不存在: ${projectPath}`;
2188
+ }
2179
2189
  }
2180
2190
  // 生成项目名称(使用目录名)
2181
2191
  const projectName = path.basename(projectPath);