evolclaw 2.8.1 → 2.8.3
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/agents/claude-runner.js +18 -7
- package/dist/agents/codex-runner.js +16 -5
- package/dist/agents/gemini-runner.js +15 -4
- package/dist/agents/templates.js +122 -0
- package/dist/channels/aun-ops.js +1 -1
- package/dist/channels/aun.js +22 -0
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/cli.js +345 -48
- package/dist/config.js +152 -65
- package/dist/core/agent-loader.js +34 -19
- package/dist/core/agent-registry.js +287 -1
- package/dist/core/command-handler.js +643 -192
- package/dist/core/evolagent-registry.js +514 -0
- package/dist/core/evolagent.js +250 -1
- package/dist/core/message/message-bridge.js +23 -3
- package/dist/core/message/message-processor.js +64 -15
- package/dist/core/message/message-queue.js +61 -6
- package/dist/core/reload-hooks.js +87 -0
- package/dist/index.js +140 -21
- package/dist/ipc.js +47 -0
- package/dist/types.js +2 -0
- package/dist/utils/reload-hooks.js +87 -0
- package/dist/utils/rich-content-renderer.js +33 -0
- package/dist/utils/stats-collector.js +15 -10
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reload Hooks
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts main() for testability. Builds the ReloadHooks
|
|
5
|
+
* implementation used by AgentRegistry.reload() to drain/disconnect/start
|
|
6
|
+
* channels during a hot reload.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
export function buildReloadHooks(deps) {
|
|
10
|
+
const { channelLoader, channelInstances, registerChannelInstance, messageQueue } = deps;
|
|
11
|
+
const drainDelayMs = deps.drainDelayMs ?? 500;
|
|
12
|
+
const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
|
|
13
|
+
return {
|
|
14
|
+
async drainChannel(channelName) {
|
|
15
|
+
logger.info(`[Reload] Draining channel: ${channelName}`);
|
|
16
|
+
if (messageQueue) {
|
|
17
|
+
// Real drain: poll until empty or timeout
|
|
18
|
+
const pollMs = 100;
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
while (messageQueue.isChannelProcessing(channelName)) {
|
|
21
|
+
if (Date.now() - start > drainTimeoutMs) {
|
|
22
|
+
logger.warn(`[Reload] Drain timeout (${drainTimeoutMs}ms) for channel: ${channelName}, proceeding anyway`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
26
|
+
}
|
|
27
|
+
logger.info(`[Reload] Drain complete: ${channelName}`);
|
|
28
|
+
}
|
|
29
|
+
else if (drainDelayMs > 0) {
|
|
30
|
+
await new Promise(r => setTimeout(r, drainDelayMs));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async disconnectChannel(channelName) {
|
|
34
|
+
const inst = channelInstances.find(i => i.adapter.channelName === channelName);
|
|
35
|
+
if (!inst) {
|
|
36
|
+
logger.warn(`[Reload] Channel ${channelName} not found, skipping disconnect`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await inst.disconnect();
|
|
41
|
+
const idx = channelInstances.indexOf(inst);
|
|
42
|
+
if (idx >= 0)
|
|
43
|
+
channelInstances.splice(idx, 1);
|
|
44
|
+
logger.info(`[Reload] Disconnected channel: ${channelName}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
logger.error(`[Reload] Failed to disconnect ${channelName}: ${e}`);
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async startChannel(agent, channelName) {
|
|
52
|
+
const channels = agent.config.channels;
|
|
53
|
+
let channelType = null;
|
|
54
|
+
for (const [type, raw] of Object.entries(channels)) {
|
|
55
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
56
|
+
for (const inst of instances) {
|
|
57
|
+
const name = inst.name ?? type;
|
|
58
|
+
if (name === channelName) {
|
|
59
|
+
channelType = type;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (channelType)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (!channelType) {
|
|
67
|
+
const msg = `[Reload] Channel ${channelName} not found in agent ${agent.name} config`;
|
|
68
|
+
logger.error(msg);
|
|
69
|
+
throw new Error(msg);
|
|
70
|
+
}
|
|
71
|
+
const partialConfig = {
|
|
72
|
+
agents: agent.config.agents,
|
|
73
|
+
channels: { [channelType]: channels[channelType] },
|
|
74
|
+
projects: agent.config.projects,
|
|
75
|
+
};
|
|
76
|
+
const newInstances = await channelLoader.createAll(partialConfig);
|
|
77
|
+
const newInst = newInstances.find(i => i.adapter.channelName === channelName);
|
|
78
|
+
if (!newInst) {
|
|
79
|
+
throw new Error(`[Reload] Failed to create instance ${channelName}`);
|
|
80
|
+
}
|
|
81
|
+
registerChannelInstance(newInst);
|
|
82
|
+
await newInst.connect();
|
|
83
|
+
channelInstances.push(newInst);
|
|
84
|
+
logger.info(`[Reload] Started channel: ${channelName}`);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
|
|
2
2
|
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
3
|
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
|
-
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode } from './config.js';
|
|
4
|
+
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode, setOwner as setOwnerInGlobalConfig, setChannelShowActivities as setShowActivitiesInGlobalConfig } from './config.js';
|
|
5
5
|
import { SessionManager } from './core/session/session-manager.js';
|
|
6
6
|
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
7
7
|
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
@@ -23,10 +23,12 @@ import { PermissionGateway } from './core/permission.js';
|
|
|
23
23
|
import { InteractionRouter } from './core/interaction-router.js';
|
|
24
24
|
import { ChannelLoader } from './core/channel-loader.js';
|
|
25
25
|
import { AgentLoader } from './core/agent-loader.js';
|
|
26
|
+
import { EvolAgentRegistry } from './core/evolagent-registry.js';
|
|
27
|
+
import { buildReloadHooks } from './utils/reload-hooks.js';
|
|
26
28
|
import { IpcServer } from './ipc.js';
|
|
27
29
|
import { logger, setLogLevel } from './utils/logger.js';
|
|
28
|
-
import { detectDuplicates } from './
|
|
29
|
-
import { loadPromptTemplates } from './
|
|
30
|
+
import { detectDuplicates } from './core/evolagent-registry.js';
|
|
31
|
+
import { loadPromptTemplates } from './agents/templates.js';
|
|
30
32
|
import path from 'path';
|
|
31
33
|
import fs from 'fs';
|
|
32
34
|
async function main() {
|
|
@@ -82,13 +84,45 @@ async function main() {
|
|
|
82
84
|
if (anthropic.baseUrl) {
|
|
83
85
|
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
84
86
|
}
|
|
87
|
+
// EvolAgent Registry
|
|
88
|
+
const agentRegistry = new EvolAgentRegistry(paths.agentsDir, {
|
|
89
|
+
setOwner: (channelName, userId) => {
|
|
90
|
+
setOwnerInGlobalConfig(config, channelName, userId);
|
|
91
|
+
},
|
|
92
|
+
setShowActivities: (channelName, mode) => {
|
|
93
|
+
setShowActivitiesInGlobalConfig(config, channelName, mode);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
agentRegistry.loadAll(config);
|
|
97
|
+
const agentInfos = agentRegistry.list();
|
|
98
|
+
const evolagentCount = agentInfos.filter(i => !i.isDefault).length;
|
|
99
|
+
if (evolagentCount > 0) {
|
|
100
|
+
logger.info(`✓ Loaded ${evolagentCount} evolagent(s)`);
|
|
101
|
+
for (const info of agentInfos) {
|
|
102
|
+
if (info.isDefault)
|
|
103
|
+
continue;
|
|
104
|
+
if (info.status === 'error') {
|
|
105
|
+
logger.error(` ✗ [${info.name}] ${info.error}`);
|
|
106
|
+
}
|
|
107
|
+
else if (info.status === 'disabled') {
|
|
108
|
+
logger.info(` ○ [${info.name}] disabled`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
logger.info(` ● [${info.name}] ${info.baseagent} @ ${path.basename(info.projectPath)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Store for IPC access (T10 will wire this)
|
|
116
|
+
// M4: removed dead globalThis.__evolclaw_agentRegistry assignment
|
|
85
117
|
// 创建事件总线
|
|
86
118
|
const eventBus = new EventBus();
|
|
87
119
|
logger.info('✓ Event bus initialized');
|
|
88
120
|
// 统计收集器(近 1 小时滚动统计)
|
|
89
121
|
const statsCollector = new StatsCollector(eventBus);
|
|
90
122
|
// 初始化数据库(带 ownerResolver)
|
|
91
|
-
|
|
123
|
+
// Registry-first: agent-owned channels resolve via EvolAgent.isOwner/isAdmin,
|
|
124
|
+
// default-agent channels fall back to global config (evolclaw.json).
|
|
125
|
+
const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId, (ch, uid) => isOwner(config, ch, uid)), (channel, userId) => agentRegistry.isAdmin(channel, userId, (ch, uid) => isAdmin(config, ch, uid)));
|
|
92
126
|
// sessionMode 解析:全局 chatmode 配置 > 默认 'interactive'
|
|
93
127
|
sessionManager.setSessionModeResolver((_channel, chatType) => {
|
|
94
128
|
return getDefaultSessionMode(config, chatType);
|
|
@@ -98,27 +132,28 @@ async function main() {
|
|
|
98
132
|
sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
|
|
99
133
|
sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
|
|
100
134
|
sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
|
|
101
|
-
// Agent
|
|
135
|
+
// Agent 插件系统:每个 EvolAgent × 每个 baseagent 一个独立 runner(H1/H2 修复)
|
|
102
136
|
const agentLoader = new AgentLoader();
|
|
103
137
|
agentLoader.register(new ClaudeAgentPlugin());
|
|
104
138
|
agentLoader.register(new CodexAgentPlugin());
|
|
105
139
|
agentLoader.register(new GeminiAgentPlugin());
|
|
106
|
-
const agentInstances = agentLoader.createAll(config, {
|
|
140
|
+
const agentInstances = agentLoader.createAll(config, agentRegistry, {
|
|
107
141
|
onSessionIdUpdate: async (sessionId, agentSessionId) => {
|
|
108
142
|
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
109
143
|
},
|
|
110
144
|
});
|
|
111
|
-
//
|
|
145
|
+
// agentMap 复合键:${evolagentName}::${baseagent}
|
|
112
146
|
const agentMap = new Map();
|
|
113
147
|
for (const inst of agentInstances) {
|
|
114
|
-
agentMap.set(inst.
|
|
148
|
+
agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
|
|
115
149
|
}
|
|
116
150
|
const defaultAgent = config.agents?.defaultAgent || 'claude';
|
|
117
|
-
const
|
|
151
|
+
const defaultAgentKey = `[default]::${defaultAgent}`;
|
|
152
|
+
const agentRunner = agentMap.get(defaultAgentKey) || agentInstances[0]?.agent;
|
|
118
153
|
if (!agentRunner) {
|
|
119
|
-
throw new Error('No agent backend available. Check agents config.');
|
|
154
|
+
throw new Error('No agent backend available. Check agents config (no runners created).');
|
|
120
155
|
}
|
|
121
|
-
logger.info(`✓
|
|
156
|
+
logger.info(`✓ Runners ready (default key: ${defaultAgentKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
|
|
122
157
|
// 权限审批网关
|
|
123
158
|
const permissionGateway = new PermissionGateway();
|
|
124
159
|
permissionGateway.setEventBus(eventBus);
|
|
@@ -143,12 +178,52 @@ async function main() {
|
|
|
143
178
|
channelLoader.register(new DingtalkChannelPlugin());
|
|
144
179
|
channelLoader.register(new QQBotChannelPlugin());
|
|
145
180
|
channelLoader.register(new WecomChannelPlugin());
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
// Create channel instances: default (from evolclaw.json) + each evolagent
|
|
182
|
+
const defaultInstances = await channelLoader.createAll(config);
|
|
183
|
+
const evolagentInstances = [];
|
|
184
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
185
|
+
// Rewrite channel instance names with agent prefix to avoid collisions
|
|
186
|
+
// with DefaultAgent and other EvolAgents.
|
|
187
|
+
// Rule (EvolAgent only):
|
|
188
|
+
// - explicit name → `${agent.name}-${type}-${name}`
|
|
189
|
+
// - omitted name → `${agent.name}-${type}`
|
|
190
|
+
const rewrittenChannels = {};
|
|
191
|
+
for (const [type, raw] of Object.entries(agent.config.channels || {})) {
|
|
192
|
+
if (type === 'defaultChannel') {
|
|
193
|
+
rewrittenChannels[type] = raw;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
197
|
+
const rewritten = instances.map((inst) => {
|
|
198
|
+
if (!inst || typeof inst !== 'object')
|
|
199
|
+
return inst;
|
|
200
|
+
const effName = agent.effectiveChannelName(type, inst.name);
|
|
201
|
+
return { ...inst, name: effName };
|
|
202
|
+
});
|
|
203
|
+
// Preserve original shape (array vs single object)
|
|
204
|
+
rewrittenChannels[type] = Array.isArray(raw) ? rewritten : rewritten[0];
|
|
205
|
+
}
|
|
206
|
+
const agentConfig = {
|
|
207
|
+
agents: agent.config.agents,
|
|
208
|
+
channels: rewrittenChannels,
|
|
209
|
+
projects: agent.config.projects,
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
const instances = await channelLoader.createAll(agentConfig);
|
|
213
|
+
evolagentInstances.push(...instances);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
logger.error(`[EvolAgent] Failed to create channels for ${agent.name}: ${e}`);
|
|
217
|
+
agent.status = 'error';
|
|
218
|
+
agent.error = `Channel creation failed: ${e}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const channelInstances = [...defaultInstances, ...evolagentInstances];
|
|
222
|
+
logger.info(`✓ Created ${channelInstances.length} channel instance(s)${evolagentInstances.length > 0 ? ` (${defaultInstances.length} default + ${evolagentInstances.length} agent)` : ''}`);
|
|
148
223
|
// 启动迁移:将 sessions.channel 从 channelType 回填为实例名
|
|
149
224
|
sessionManager.migrateChannelToInstanceName();
|
|
150
225
|
// 创建命令处理器
|
|
151
|
-
const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus,
|
|
226
|
+
const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgentKey);
|
|
152
227
|
cmdHandler.setPermissionGateway(permissionGateway);
|
|
153
228
|
cmdHandler.setInteractionRouter(interactionRouter);
|
|
154
229
|
cmdHandler.setStatsCollector(statsCollector);
|
|
@@ -163,9 +238,16 @@ async function main() {
|
|
|
163
238
|
}
|
|
164
239
|
};
|
|
165
240
|
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
166
|
-
},
|
|
241
|
+
}, defaultAgentKey);
|
|
167
242
|
// 回填 processor 和 messageQueue 的引用
|
|
168
243
|
cmdHandler.setProcessor(processor);
|
|
244
|
+
// Inject EvolAgentRegistry (methods added by T6/T7)
|
|
245
|
+
if (processor.setAgentRegistry) {
|
|
246
|
+
processor.setAgentRegistry(agentRegistry);
|
|
247
|
+
}
|
|
248
|
+
if (cmdHandler.setAgentRegistry) {
|
|
249
|
+
cmdHandler.setAgentRegistry(agentRegistry);
|
|
250
|
+
}
|
|
169
251
|
// 设置交互路由器
|
|
170
252
|
processor.setInteractionRouter(interactionRouter);
|
|
171
253
|
// 设置 compact 开始回调(对所有支持的 agent)
|
|
@@ -179,8 +261,11 @@ async function main() {
|
|
|
179
261
|
await processor.processMessage(message);
|
|
180
262
|
});
|
|
181
263
|
// 设置中断回调(精确中断正在处理的 agent)
|
|
182
|
-
messageQueue.setInterruptCallback(async (sessionKey, agentId) => {
|
|
183
|
-
const
|
|
264
|
+
messageQueue.setInterruptCallback(async (sessionKey, agentId, evolagentName) => {
|
|
265
|
+
const baseagent = agentId || defaultAgent;
|
|
266
|
+
const evol = evolagentName || '[default]';
|
|
267
|
+
const agent = agentMap.get(`${evol}::${baseagent}`)
|
|
268
|
+
|| agentMap.get(defaultAgentKey);
|
|
184
269
|
if (agent?.hasActiveStream(sessionKey)) {
|
|
185
270
|
await agent.interrupt(sessionKey);
|
|
186
271
|
}
|
|
@@ -203,12 +288,18 @@ async function main() {
|
|
|
203
288
|
};
|
|
204
289
|
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
205
290
|
const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
|
|
291
|
+
msgBridge.setAgentRegistry(agentRegistry);
|
|
206
292
|
// ── Channel instance registration (shared by startup and hot-load) ──
|
|
207
293
|
function registerChannelInstance(inst) {
|
|
208
294
|
// 1. 项目路径提供器
|
|
209
295
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
210
296
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
211
|
-
|
|
297
|
+
// Effective default path: agent's projectPath if agent-owned, else global
|
|
298
|
+
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
299
|
+
const effectiveDefault = (owningAgent && !owningAgent.isDefault)
|
|
300
|
+
? owningAgent.projectPath
|
|
301
|
+
: (config.projects?.defaultPath || process.cwd());
|
|
302
|
+
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
|
|
212
303
|
return path.isAbsolute(session.projectPath)
|
|
213
304
|
? session.projectPath
|
|
214
305
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -350,6 +441,18 @@ async function main() {
|
|
|
350
441
|
}
|
|
351
442
|
// ── 连接所有渠道 ──
|
|
352
443
|
const connected = await channelLoader.connectAll(channelInstances);
|
|
444
|
+
// Bind connected adapters to their owning agents
|
|
445
|
+
// I1: only mark 'running' if a channel actually connected for that agent
|
|
446
|
+
const connectedSet = new Set(connected);
|
|
447
|
+
for (const inst of channelInstances) {
|
|
448
|
+
const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
449
|
+
if (!agent || agent.status === 'error')
|
|
450
|
+
continue;
|
|
451
|
+
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
452
|
+
if (agent.status === 'stopped' && connectedSet.has(inst.adapter.channelName)) {
|
|
453
|
+
agent.status = 'running';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
353
456
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
354
457
|
for (const inst of channelInstances) {
|
|
355
458
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
@@ -384,7 +487,7 @@ async function main() {
|
|
|
384
487
|
continue; // 跳过同类型通道
|
|
385
488
|
if (notified.has(otherType))
|
|
386
489
|
continue; // 同类型已通知过
|
|
387
|
-
const ownerId = getOwner(config, other.adapter.channelName);
|
|
490
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelName) ?? getOwner(config, other.adapter.channelName);
|
|
388
491
|
if (!ownerId)
|
|
389
492
|
continue;
|
|
390
493
|
notified.add(otherType);
|
|
@@ -423,12 +526,16 @@ async function main() {
|
|
|
423
526
|
sessionManager.clearProcessing(session.id);
|
|
424
527
|
continue;
|
|
425
528
|
}
|
|
426
|
-
|
|
529
|
+
// 复合键:${evolagentName}::${baseagent},从 channel 反查 evolagent
|
|
530
|
+
const owningAgent = agentRegistry.resolveByChannel(session.channel);
|
|
531
|
+
const evolName = owningAgent?.name || '[default]';
|
|
532
|
+
const baseagentName = session.agentId || defaultAgent;
|
|
533
|
+
const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(defaultAgentKey);
|
|
427
534
|
if (!agent) {
|
|
428
535
|
sessionManager.clearProcessing(session.id);
|
|
429
536
|
continue;
|
|
430
537
|
}
|
|
431
|
-
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${
|
|
538
|
+
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
|
|
432
539
|
const resumeMessage = {
|
|
433
540
|
channel: session.channel,
|
|
434
541
|
channelId: session.channelId,
|
|
@@ -499,6 +606,18 @@ async function main() {
|
|
|
499
606
|
},
|
|
500
607
|
};
|
|
501
608
|
}, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
|
|
609
|
+
// M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
|
|
610
|
+
ipcServer.setAgentRegistry(agentRegistry);
|
|
611
|
+
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
612
|
+
const reloadHooks = buildReloadHooks({
|
|
613
|
+
channelLoader,
|
|
614
|
+
channelInstances,
|
|
615
|
+
registerChannelInstance,
|
|
616
|
+
messageQueue,
|
|
617
|
+
});
|
|
618
|
+
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
619
|
+
globalThis.__evolclaw_reloadHooks = reloadHooks;
|
|
620
|
+
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
502
621
|
ipcServer.start();
|
|
503
622
|
// 运行时配置文件监控
|
|
504
623
|
const configPath = resolvePaths().config;
|
package/dist/ipc.js
CHANGED
|
@@ -8,11 +8,16 @@ export class IpcServer {
|
|
|
8
8
|
getStatus;
|
|
9
9
|
commandExecutor;
|
|
10
10
|
server = null;
|
|
11
|
+
agentRegistry;
|
|
11
12
|
constructor(socketPath, getStatus, commandExecutor) {
|
|
12
13
|
this.socketPath = socketPath;
|
|
13
14
|
this.getStatus = getStatus;
|
|
14
15
|
this.commandExecutor = commandExecutor;
|
|
15
16
|
}
|
|
17
|
+
/** Inject EvolAgentRegistry for evolagent.* IPC handlers */
|
|
18
|
+
setAgentRegistry(registry) {
|
|
19
|
+
this.agentRegistry = registry;
|
|
20
|
+
}
|
|
16
21
|
start() {
|
|
17
22
|
// Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
|
|
18
23
|
if (!isNamedPipe(this.socketPath)) {
|
|
@@ -82,6 +87,48 @@ export class IpcServer {
|
|
|
82
87
|
return { ok: false, error: 'missing cmd or sessionId' };
|
|
83
88
|
return await this.commandExecutor(slashCmd, sessionId);
|
|
84
89
|
}
|
|
90
|
+
case 'evolagent.list': {
|
|
91
|
+
if (!this.agentRegistry)
|
|
92
|
+
return { ok: false, error: 'EvolAgentRegistry not available' };
|
|
93
|
+
return { ok: true, agents: this.agentRegistry.list() };
|
|
94
|
+
}
|
|
95
|
+
case 'evolagent.show': {
|
|
96
|
+
if (!this.agentRegistry)
|
|
97
|
+
return { ok: false, error: 'EvolAgentRegistry not available' };
|
|
98
|
+
const name = cmd.name;
|
|
99
|
+
if (!name || typeof name !== 'string')
|
|
100
|
+
return { ok: false, error: 'missing name' };
|
|
101
|
+
const agent = this.agentRegistry.get(name);
|
|
102
|
+
if (!agent)
|
|
103
|
+
return { ok: false, error: `Agent "${name}" not found` };
|
|
104
|
+
const info = this.agentRegistry.list().find((i) => i.name === name);
|
|
105
|
+
// I7: null-guard list().find() result
|
|
106
|
+
if (!info)
|
|
107
|
+
return { ok: false, error: `Agent "${name}" found but info missing (race?)` };
|
|
108
|
+
return { ok: true, agent: info };
|
|
109
|
+
}
|
|
110
|
+
case 'evolagent.reload': {
|
|
111
|
+
if (!this.agentRegistry)
|
|
112
|
+
return { ok: false, error: 'EvolAgentRegistry not available' };
|
|
113
|
+
const name = cmd.name;
|
|
114
|
+
if (!name || typeof name !== 'string')
|
|
115
|
+
return { ok: false, error: 'missing name' };
|
|
116
|
+
const hooks = globalThis.__evolclaw_reloadHooks;
|
|
117
|
+
if (!hooks)
|
|
118
|
+
return { ok: false, error: 'Reload hooks not initialized' };
|
|
119
|
+
try {
|
|
120
|
+
const a = this.agentRegistry.get(name);
|
|
121
|
+
if (!a)
|
|
122
|
+
return { ok: false, error: `Agent "${name}" not found` };
|
|
123
|
+
if (!this.agentRegistry.reload)
|
|
124
|
+
return { ok: false, error: 'EvolAgentRegistry.reload not available' };
|
|
125
|
+
await this.agentRegistry.reload(name, hooks);
|
|
126
|
+
return { ok: true, result: `Agent "${name}" reloaded` };
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
return { ok: false, error: e?.message || String(e) };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
85
132
|
default:
|
|
86
133
|
return { error: `unknown command: ${cmd.type}` };
|
|
87
134
|
}
|
package/dist/types.js
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
// Array form: `name` is required to distinguish instances.
|
|
4
4
|
/** Default permission mode applied to new sessions. Change here to affect all roles. */
|
|
5
5
|
export const DEFAULT_PERMISSION_MODE = 'bypass';
|
|
6
|
+
/** Reserved agent name used for DefaultAgent (no agent.json file). */
|
|
7
|
+
export const DEFAULT_AGENT_NAME = '[default]';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reload Hooks
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts main() for testability. Builds the ReloadHooks
|
|
5
|
+
* implementation used by EvolAgentRegistry.reload() to drain/disconnect/start
|
|
6
|
+
* channels during a hot reload.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
export function buildReloadHooks(deps) {
|
|
10
|
+
const { channelLoader, channelInstances, registerChannelInstance, messageQueue } = deps;
|
|
11
|
+
const drainDelayMs = deps.drainDelayMs ?? 500;
|
|
12
|
+
const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
|
|
13
|
+
return {
|
|
14
|
+
async drainChannel(channelName) {
|
|
15
|
+
logger.info(`[Reload] Draining channel: ${channelName}`);
|
|
16
|
+
if (messageQueue) {
|
|
17
|
+
// Real drain: poll until empty or timeout
|
|
18
|
+
const pollMs = 100;
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
while (messageQueue.isChannelProcessing(channelName)) {
|
|
21
|
+
if (Date.now() - start > drainTimeoutMs) {
|
|
22
|
+
logger.warn(`[Reload] Drain timeout (${drainTimeoutMs}ms) for channel: ${channelName}, proceeding anyway`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
26
|
+
}
|
|
27
|
+
logger.info(`[Reload] Drain complete: ${channelName}`);
|
|
28
|
+
}
|
|
29
|
+
else if (drainDelayMs > 0) {
|
|
30
|
+
await new Promise(r => setTimeout(r, drainDelayMs));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async disconnectChannel(channelName) {
|
|
34
|
+
const inst = channelInstances.find(i => i.adapter.channelName === channelName);
|
|
35
|
+
if (!inst) {
|
|
36
|
+
logger.warn(`[Reload] Channel ${channelName} not found, skipping disconnect`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await inst.disconnect();
|
|
41
|
+
const idx = channelInstances.indexOf(inst);
|
|
42
|
+
if (idx >= 0)
|
|
43
|
+
channelInstances.splice(idx, 1);
|
|
44
|
+
logger.info(`[Reload] Disconnected channel: ${channelName}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
logger.error(`[Reload] Failed to disconnect ${channelName}: ${e}`);
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async startChannel(agent, channelName) {
|
|
52
|
+
const channels = agent.config.channels;
|
|
53
|
+
let channelType = null;
|
|
54
|
+
for (const [type, raw] of Object.entries(channels)) {
|
|
55
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
56
|
+
for (const inst of instances) {
|
|
57
|
+
const name = inst.name ?? type;
|
|
58
|
+
if (name === channelName) {
|
|
59
|
+
channelType = type;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (channelType)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (!channelType) {
|
|
67
|
+
const msg = `[Reload] Channel ${channelName} not found in agent ${agent.name} config`;
|
|
68
|
+
logger.error(msg);
|
|
69
|
+
throw new Error(msg);
|
|
70
|
+
}
|
|
71
|
+
const partialConfig = {
|
|
72
|
+
agents: agent.config.agents,
|
|
73
|
+
channels: { [channelType]: channels[channelType] },
|
|
74
|
+
projects: agent.config.projects,
|
|
75
|
+
};
|
|
76
|
+
const newInstances = await channelLoader.createAll(partialConfig);
|
|
77
|
+
const newInst = newInstances.find(i => i.adapter.channelName === channelName);
|
|
78
|
+
if (!newInst) {
|
|
79
|
+
throw new Error(`[Reload] Failed to create instance ${channelName}`);
|
|
80
|
+
}
|
|
81
|
+
registerChannelInstance(newInst);
|
|
82
|
+
await newInst.connect();
|
|
83
|
+
channelInstances.push(newInst);
|
|
84
|
+
logger.info(`[Reload] Started channel: ${channelName}`);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -226,3 +226,36 @@ export async function closeBrowser() {
|
|
|
226
226
|
browserInstance = null;
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
|
+
// ── Markdown → Plain Text ───────────────────────────────────────────────────
|
|
230
|
+
// 用于不渲染 markdown 的渠道(微信、QQ)将 Agent 输出降级为纯文本。
|
|
231
|
+
export function markdownToPlainText(text) {
|
|
232
|
+
let result = text;
|
|
233
|
+
// Code blocks: strip fences, keep content
|
|
234
|
+
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
|
|
235
|
+
// Images: remove entirely
|
|
236
|
+
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
237
|
+
// Links: keep display text only
|
|
238
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
|
239
|
+
// Tables: remove separator rows
|
|
240
|
+
result = result.replace(/^\|[\s:|-]+\|$/gm, '');
|
|
241
|
+
result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
|
|
242
|
+
// Bold/italic
|
|
243
|
+
result = result.replace(/\*\*(.+?)\*\*/g, '$1');
|
|
244
|
+
result = result.replace(/\*(.+?)\*/g, '$1');
|
|
245
|
+
result = result.replace(/__(.+?)__/g, '$1');
|
|
246
|
+
result = result.replace(/_(.+?)_/g, '$1');
|
|
247
|
+
// Strikethrough
|
|
248
|
+
result = result.replace(/~~(.+?)~~/g, '$1');
|
|
249
|
+
// Inline code
|
|
250
|
+
result = result.replace(/`([^`]+)`/g, '$1');
|
|
251
|
+
// Headers
|
|
252
|
+
result = result.replace(/^#{1,6}\s+/gm, '');
|
|
253
|
+
// Blockquotes
|
|
254
|
+
result = result.replace(/^>\s?/gm, '');
|
|
255
|
+
// Horizontal rules
|
|
256
|
+
result = result.replace(/^[-*_]{3,}$/gm, '');
|
|
257
|
+
// List markers
|
|
258
|
+
result = result.replace(/^(\s*)[-*+]\s/gm, '$1');
|
|
259
|
+
result = result.replace(/^(\s*)\d+\.\s/gm, '$1');
|
|
260
|
+
return result.trim();
|
|
261
|
+
}
|
|
@@ -7,23 +7,24 @@ export class StatsCollector {
|
|
|
7
7
|
// 订阅相关事件
|
|
8
8
|
eventBus.subscribe('message:received', (event) => {
|
|
9
9
|
const e = event;
|
|
10
|
-
this.recordEvent({ type: 'received', timestamp: e.timestamp || Date.now() });
|
|
10
|
+
this.recordEvent({ type: 'received', timestamp: e.timestamp || Date.now(), agentName: e.agentName });
|
|
11
11
|
});
|
|
12
12
|
eventBus.subscribe('message:completed', (event) => {
|
|
13
13
|
const e = event;
|
|
14
|
-
this.recordEvent({ type: 'completed', timestamp: e.timestamp || Date.now(), durationMs: e.durationMs });
|
|
14
|
+
this.recordEvent({ type: 'completed', timestamp: e.timestamp || Date.now(), durationMs: e.durationMs, agentName: e.agentName });
|
|
15
15
|
});
|
|
16
16
|
eventBus.subscribe('message:error', (event) => {
|
|
17
17
|
const e = event;
|
|
18
|
-
this.recordEvent({ type: 'error', timestamp: Date.now(), errorType: e.errorType });
|
|
18
|
+
this.recordEvent({ type: 'error', timestamp: Date.now(), errorType: e.errorType, agentName: e.agentName });
|
|
19
19
|
});
|
|
20
|
-
eventBus.subscribe('message:interrupted', (
|
|
21
|
-
|
|
20
|
+
eventBus.subscribe('message:interrupted', (event) => {
|
|
21
|
+
const e = event;
|
|
22
|
+
this.recordEvent({ type: 'interrupted', timestamp: Date.now(), agentName: e.agentName });
|
|
22
23
|
});
|
|
23
24
|
eventBus.subscribe('tool:result', (event) => {
|
|
24
25
|
const e = event;
|
|
25
26
|
if (e.isError) {
|
|
26
|
-
this.recordEvent({ type: 'tool-error', timestamp: Date.now(), toolName: e.toolName });
|
|
27
|
+
this.recordEvent({ type: 'tool-error', timestamp: Date.now(), toolName: e.toolName, agentName: e.agentName });
|
|
27
28
|
}
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -31,14 +32,18 @@ export class StatsCollector {
|
|
|
31
32
|
this.events.push(record);
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* 获取统计快照。可选 agentName 过滤:未传则全局;传入则只统计该 agent。
|
|
36
|
+
* 自动裁剪 >1h 的事件。
|
|
35
37
|
*/
|
|
36
|
-
getSnapshot() {
|
|
38
|
+
getSnapshot(agentName) {
|
|
37
39
|
const now = Date.now();
|
|
38
40
|
const cutoff = now - this.HOUR_MS;
|
|
39
41
|
// 裁剪过期事件
|
|
40
42
|
this.events = this.events.filter(e => e.timestamp >= cutoff);
|
|
41
|
-
//
|
|
43
|
+
// 聚合统计(可按 agent 过滤)
|
|
44
|
+
const filtered = agentName === undefined
|
|
45
|
+
? this.events
|
|
46
|
+
: this.events.filter(e => (e.agentName ?? '[default]') === agentName);
|
|
42
47
|
let received = 0;
|
|
43
48
|
let completed = 0;
|
|
44
49
|
let errors = 0;
|
|
@@ -48,7 +53,7 @@ export class StatsCollector {
|
|
|
48
53
|
let interrupts = 0;
|
|
49
54
|
let totalDuration = 0;
|
|
50
55
|
let durationCount = 0;
|
|
51
|
-
for (const event of
|
|
56
|
+
for (const event of filtered) {
|
|
52
57
|
switch (event.type) {
|
|
53
58
|
case 'received':
|
|
54
59
|
received++;
|
package/package.json
CHANGED