evolclaw 2.6.4 → 2.7.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.
- package/README.md +1 -1
- package/data/evolclaw.sample.json +3 -4
- package/dist/agents/claude-runner.js +15 -6
- package/dist/channels/aun.js +97 -30
- package/dist/channels/feishu.js +2 -0
- package/dist/cli.js +29 -1
- package/dist/config.js +66 -40
- package/dist/core/command-handler.js +51 -41
- package/dist/core/message/message-processor.js +43 -12
- package/dist/core/session/session-manager.js +9 -7
- package/dist/index.js +21 -25
- package/dist/templates/prompts.md +4 -4
- package/dist/types.js +2 -1
- package/dist/utils/channel-fingerprint.js +59 -0
- package/dist/utils/cross-platform.js +23 -12
- package/dist/utils/init.js +1 -1
- package/dist/utils/logger.js +15 -3
- package/package.json +1 -1
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?.
|
|
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.
|
|
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?.
|
|
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?.
|
|
70
|
+
const model = config.agents?.claude?.model
|
|
71
71
|
|| settings.model
|
|
72
72
|
|| 'sonnet';
|
|
73
|
-
const effort = config.agents?.
|
|
73
|
+
const effort = config.agents?.claude?.effort
|
|
74
74
|
|| settings.effortLevel
|
|
75
75
|
|| undefined;
|
|
76
|
-
const configExecPath = config.agents?.
|
|
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?.
|
|
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.
|
|
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?.
|
|
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?.
|
|
101
|
+
const model = config.agents?.codex?.model
|
|
102
102
|
|| codexSettings.model
|
|
103
103
|
|| 'gpt-5.2-codex';
|
|
104
|
-
const effort = config.agents?.
|
|
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?.
|
|
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
|
-
*
|
|
313
|
-
*
|
|
344
|
+
* 读取全局 chatmode 配置的默认 sessionMode
|
|
345
|
+
* 按 chatType 返回对应模式,未配置时返回 undefined(由 session-manager 回退到 'interactive')
|
|
314
346
|
*/
|
|
315
|
-
export function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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' :
|
|
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?.
|
|
1022
|
+
const configuredInEvolclaw = !!(this.config.agents?.codex?.model || this.config.agents?.codex?.reasoning);
|
|
1012
1023
|
if (configuredInEvolclaw) {
|
|
1013
|
-
if (!this.config.agents.
|
|
1014
|
-
this.config.agents.
|
|
1024
|
+
if (!this.config.agents.codex)
|
|
1025
|
+
this.config.agents.codex = {};
|
|
1015
1026
|
if (newModel)
|
|
1016
|
-
this.config.agents.
|
|
1027
|
+
this.config.agents.codex.model = newModel;
|
|
1017
1028
|
if (newEffort)
|
|
1018
|
-
this.config.agents.
|
|
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.
|
|
1029
|
-
this.config.agents.
|
|
1039
|
+
if (!this.config.agents.codex)
|
|
1040
|
+
this.config.agents.codex = {};
|
|
1030
1041
|
if (newModel)
|
|
1031
|
-
this.config.agents.
|
|
1042
|
+
this.config.agents.codex.model = newModel;
|
|
1032
1043
|
if (newEffort)
|
|
1033
|
-
this.config.agents.
|
|
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?.
|
|
1054
|
+
const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
|
|
1044
1055
|
if (configuredInEvolclaw) {
|
|
1045
|
-
if (!this.config.agents.
|
|
1046
|
-
this.config.agents.
|
|
1056
|
+
if (!this.config.agents.claude)
|
|
1057
|
+
this.config.agents.claude = {};
|
|
1047
1058
|
if (newModel)
|
|
1048
|
-
this.config.agents.
|
|
1059
|
+
this.config.agents.claude.model = newModel;
|
|
1049
1060
|
if (newEffort)
|
|
1050
|
-
this.config.agents.
|
|
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?.
|
|
1150
|
-
delete this.config.agents.
|
|
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?.
|
|
1169
|
+
const configuredInEvolclaw = !!this.config.agents?.claude?.effort;
|
|
1159
1170
|
if (configuredInEvolclaw) {
|
|
1160
|
-
delete this.config.agents.
|
|
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.
|
|
1187
|
-
this.config.agents.
|
|
1188
|
-
this.config.agents.
|
|
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?.
|
|
1206
|
+
const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
|
|
1196
1207
|
if (configuredInEvolclaw) {
|
|
1197
|
-
if (!this.config.agents.
|
|
1198
|
-
this.config.agents.
|
|
1199
|
-
this.config.agents.
|
|
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
|
|
1466
|
-
return `📋 当前会话模式: ${currentMode}
|
|
1474
|
+
if (canSwitch) {
|
|
1475
|
+
return `📋 当前会话模式: ${currentMode}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
|
|
1467
1476
|
}
|
|
1468
|
-
return `📋 当前会话模式: ${currentMode}
|
|
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
|
|
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
|
-
|
|
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);
|
|
@@ -8,6 +8,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
9
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
|
+
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
11
12
|
import { getOwner } from '../../config.js';
|
|
12
13
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
14
|
import { renderPromptSection } from '../../prompts/templates.js';
|
|
@@ -130,7 +131,8 @@ export class MessageProcessor {
|
|
|
130
131
|
'/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
|
|
131
132
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
132
133
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
133
|
-
'/p ', '/s ', '/name ',
|
|
134
|
+
'/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
135
|
+
'/aid', '/agentmd',
|
|
134
136
|
];
|
|
135
137
|
/** 判断消息内容是否为已知命令 */
|
|
136
138
|
isKnownCommand(content) {
|
|
@@ -193,6 +195,7 @@ export class MessageProcessor {
|
|
|
193
195
|
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
194
196
|
});
|
|
195
197
|
}
|
|
198
|
+
logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
|
|
196
199
|
agent.interrupt(streamKey).catch(e => {
|
|
197
200
|
logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
|
|
198
201
|
});
|
|
@@ -383,12 +386,11 @@ export class MessageProcessor {
|
|
|
383
386
|
? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
|
|
384
387
|
: undefined,
|
|
385
388
|
});
|
|
386
|
-
// 设置 per-session
|
|
387
|
-
|
|
388
|
-
const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'noask';
|
|
389
|
-
agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
|
|
389
|
+
// 设置 per-session 权限模式(默认 bypass,所有角色统一)
|
|
390
|
+
agent.setMode(session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE);
|
|
390
391
|
// 标记会话为处理中(实时持久化,重启后可恢复)
|
|
391
392
|
this.sessionManager.markProcessing(session.id);
|
|
393
|
+
logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
|
|
392
394
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
393
395
|
const prevInterruptReason = this.interruptedSessions.get(session.id);
|
|
394
396
|
this.interruptedSessions.delete(session.id);
|
|
@@ -468,6 +470,7 @@ export class MessageProcessor {
|
|
|
468
470
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
469
471
|
let streamRegistered = false;
|
|
470
472
|
try {
|
|
473
|
+
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
471
474
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
472
475
|
agent.registerStream(streamKey, stream);
|
|
473
476
|
streamRegistered = true;
|
|
@@ -594,8 +597,23 @@ export class MessageProcessor {
|
|
|
594
597
|
// 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
|
|
595
598
|
// 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
|
|
596
599
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
600
|
+
// 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
|
|
601
|
+
// 特征:无流式 text + complete.result 匹配已知模式
|
|
602
|
+
// 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
|
|
603
|
+
// Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
|
|
604
|
+
const isSdkFallbackMessage = !!finalReplyText
|
|
605
|
+
&& !streamResult.hasReceivedText
|
|
606
|
+
&& /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
|
|
597
607
|
if (finalReplyText) {
|
|
598
|
-
if (
|
|
608
|
+
if (isProactive && isSdkFallbackMessage) {
|
|
609
|
+
// Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
|
|
610
|
+
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
611
|
+
if (!isCurrentlyBackground) {
|
|
612
|
+
await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
|
|
613
|
+
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else if (shouldSuppress()) {
|
|
599
617
|
flusher.addText(finalReplyText);
|
|
600
618
|
}
|
|
601
619
|
else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
|
|
@@ -606,8 +624,10 @@ export class MessageProcessor {
|
|
|
606
624
|
await flusher.flush(true);
|
|
607
625
|
// 清理 activeStreams(正常完成)
|
|
608
626
|
agent.cleanupStream(streamKey);
|
|
627
|
+
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
609
628
|
// 清除处理中状态
|
|
610
629
|
this.sessionManager.clearProcessing(session.id);
|
|
630
|
+
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
611
631
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
612
632
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
613
633
|
if (streamResult.isError) {
|
|
@@ -681,8 +701,10 @@ export class MessageProcessor {
|
|
|
681
701
|
catch (error) {
|
|
682
702
|
// 清理流和处理中状态(异常时也要清除)
|
|
683
703
|
agent.cleanupStream(streamKey);
|
|
704
|
+
logger.info(`[MessageProcessor] agent.cleanupStream ok (on error): session=${session.id} task=${taskId}`);
|
|
684
705
|
try {
|
|
685
706
|
this.sessionManager.clearProcessing(session.id);
|
|
707
|
+
logger.info(`[MessageProcessor] session ${session.id} processing cleared (on error) task=${taskId}`);
|
|
686
708
|
}
|
|
687
709
|
catch { }
|
|
688
710
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
@@ -796,7 +818,8 @@ export class MessageProcessor {
|
|
|
796
818
|
// 每收到事件重置空闲超时
|
|
797
819
|
const toolName = event.type === 'tool_use' ? event.name : undefined;
|
|
798
820
|
resetTimer(event.type, toolName);
|
|
799
|
-
//
|
|
821
|
+
// 记录事件类型:高价值事件(text/tool_use/tool_result/complete/error/compact/task_progress)INFO,
|
|
822
|
+
// 框架事件(session_id/state_changed/status)DEBUG
|
|
800
823
|
let eventDetail = '';
|
|
801
824
|
if (event.type === 'text' && event.text) {
|
|
802
825
|
const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
|
|
@@ -813,25 +836,34 @@ export class MessageProcessor {
|
|
|
813
836
|
|| '';
|
|
814
837
|
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
815
838
|
}
|
|
816
|
-
|
|
839
|
+
else if (event.type === 'tool_result') {
|
|
840
|
+
eventDetail = ` tool=${event.name} ok=${!event.isError}`;
|
|
841
|
+
}
|
|
842
|
+
const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
|
|
843
|
+
if (frameworkEvents.has(event.type)) {
|
|
844
|
+
logger.debug(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
848
|
+
}
|
|
817
849
|
// Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
|
|
818
850
|
if (thoughtEmitter) {
|
|
819
851
|
thoughtEmitter.emit(event).catch(() => { });
|
|
820
852
|
}
|
|
821
853
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
822
854
|
if (event.type === 'session_id') {
|
|
823
|
-
logger.
|
|
855
|
+
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
824
856
|
continue;
|
|
825
857
|
}
|
|
826
858
|
// session 状态变更(idle/running/requires_action)
|
|
827
859
|
if (event.type === 'state_changed') {
|
|
828
|
-
logger.
|
|
860
|
+
logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
|
|
829
861
|
this.eventBus.publish({ type: 'agent:state-changed', sessionId: session.id, state: event.state });
|
|
830
862
|
continue;
|
|
831
863
|
}
|
|
832
864
|
// agent 状态通知(仅事件,不直出给用户)
|
|
833
865
|
if (event.type === 'status') {
|
|
834
|
-
logger.
|
|
866
|
+
logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
|
|
835
867
|
this.eventBus.publish({
|
|
836
868
|
type: 'agent:status',
|
|
837
869
|
sessionId: session.id,
|
|
@@ -890,7 +922,6 @@ export class MessageProcessor {
|
|
|
890
922
|
}
|
|
891
923
|
// 工具结果
|
|
892
924
|
if (event.type === 'tool_result') {
|
|
893
|
-
logger.debug(`[MessageProcessor] tool_result: name=${event.name}, is_error=${event.isError}`);
|
|
894
925
|
this.eventBus.publish({
|
|
895
926
|
type: 'tool:result',
|
|
896
927
|
sessionId: session.id,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
2
3
|
import { ensureDir } from '../../config.js';
|
|
3
4
|
import { resolvePaths } from '../../paths.js';
|
|
4
5
|
import { logger } from '../../utils/logger.js';
|
|
@@ -315,15 +316,15 @@ export class SessionManager {
|
|
|
315
316
|
logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
|
-
// Migration: readonly
|
|
319
|
+
// Migration: readonly 模式已禁用,历史会话统一转为 auto
|
|
319
320
|
if (hasMetadata && tableInfo.length > 0) {
|
|
320
321
|
const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
|
|
321
322
|
let migratedPerm = 0;
|
|
322
323
|
for (const row of rows) {
|
|
323
324
|
try {
|
|
324
325
|
const meta = JSON.parse(row.metadata);
|
|
325
|
-
if (meta.permissionMode === 'readonly') {
|
|
326
|
-
meta.permissionMode = '
|
|
326
|
+
if (meta.permissionMode === 'readonly' || meta.permissionMode === 'noask') {
|
|
327
|
+
meta.permissionMode = 'auto';
|
|
327
328
|
this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
|
|
328
329
|
.run(JSON.stringify(meta), row.id);
|
|
329
330
|
migratedPerm++;
|
|
@@ -332,7 +333,7 @@ export class SessionManager {
|
|
|
332
333
|
catch { /* skip malformed JSON */ }
|
|
333
334
|
}
|
|
334
335
|
if (migratedPerm > 0) {
|
|
335
|
-
logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode
|
|
336
|
+
logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode → auto`);
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
339
|
// 创建新表(首次初始化)
|
|
@@ -471,7 +472,7 @@ export class SessionManager {
|
|
|
471
472
|
session.identity = this.resolveIdentity(channel, userId);
|
|
472
473
|
// 新话题会话补写默认权限模式
|
|
473
474
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
474
|
-
session.metadata.permissionMode =
|
|
475
|
+
session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
475
476
|
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
476
477
|
.run(JSON.stringify(session.metadata), Date.now(), session.id);
|
|
477
478
|
}
|
|
@@ -580,9 +581,9 @@ export class SessionManager {
|
|
|
580
581
|
updatedAt: Date.now()
|
|
581
582
|
};
|
|
582
583
|
session.identity = this.resolveIdentity(channel, userId);
|
|
583
|
-
//
|
|
584
|
+
// 写入默认权限模式(统一 bypass,只在首次创建时设置)
|
|
584
585
|
if (!sessionMetadata.permissionMode) {
|
|
585
|
-
sessionMetadata.permissionMode =
|
|
586
|
+
sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
586
587
|
}
|
|
587
588
|
this.insertSession(session);
|
|
588
589
|
this.eventBus.publish({
|
|
@@ -681,6 +682,7 @@ export class SessionManager {
|
|
|
681
682
|
}
|
|
682
683
|
async switchProject(channel, channelId, newProjectPath, currentAgentId) {
|
|
683
684
|
const agentId = currentAgentId || 'claude';
|
|
685
|
+
logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
|
|
684
686
|
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
685
687
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
686
688
|
// 2. 取消当前活跃会话
|