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.
- package/dist/agents/templates.js +122 -0
- package/dist/channels/aun-ops.js +275 -0
- package/dist/channels/aun.js +206 -103
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/cli.js +676 -20
- package/dist/config.js +94 -22
- package/dist/core/agent-registry.js +450 -0
- package/dist/core/command-handler.js +422 -255
- package/dist/core/evolagent-registry.js +503 -0
- package/dist/core/evolagent-schema.js +72 -0
- package/dist/core/evolagent.js +315 -0
- package/dist/core/message/message-bridge.js +23 -3
- package/dist/core/message/message-processor.js +56 -11
- package/dist/core/message/message-queue.js +59 -4
- package/dist/core/reload-hooks.js +87 -0
- package/dist/index.js +119 -20
- package/dist/ipc.js +47 -0
- package/dist/paths.js +2 -0
- package/dist/types.js +2 -0
- package/dist/utils/init-channel.js +91 -221
- package/dist/utils/init.js +18 -42
- package/dist/utils/logger.js +58 -2
- 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/evolclaw-install-aun.md +48 -7
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { logger } from '../../utils/logger.js';
|
|
3
|
+
const DEFAULT_AGENT_NAME = '[default]';
|
|
3
4
|
export class MessageQueue {
|
|
4
5
|
queues = new Map();
|
|
5
6
|
processing = new Set();
|
|
7
|
+
processingAgent = new Map(); // queueKey → agentName(处理中项目的 agent)
|
|
6
8
|
externalLocks = new Map();
|
|
7
9
|
handler;
|
|
8
10
|
currentSessionKey;
|
|
@@ -77,18 +79,24 @@ export class MessageQueue {
|
|
|
77
79
|
return Promise.resolve();
|
|
78
80
|
}
|
|
79
81
|
const queueKey = this.getQueueKey(sessionKey, projectPath);
|
|
80
|
-
|
|
82
|
+
const agentName = options?.agentName || DEFAULT_AGENT_NAME;
|
|
83
|
+
logger.debug(`[Queue] Enqueuing message for ${queueKey} (agent=${agentName})`);
|
|
81
84
|
return new Promise((resolve, reject) => {
|
|
82
85
|
if (!this.queues.has(queueKey)) {
|
|
83
86
|
this.queues.set(queueKey, []);
|
|
84
87
|
}
|
|
85
|
-
this.queues.get(queueKey).push({ message, projectPath, resolve, reject });
|
|
88
|
+
this.queues.get(queueKey).push({ message, projectPath, agentName, resolve, reject });
|
|
86
89
|
// 根据 interruptible 选项决定是否触发中断
|
|
87
90
|
if (this.processing.has(queueKey)) {
|
|
88
91
|
if (options?.interruptible !== false) {
|
|
89
92
|
// 单聊:保留中断行为
|
|
90
93
|
logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
|
|
91
|
-
this.eventBus?.publish({
|
|
94
|
+
this.eventBus?.publish({
|
|
95
|
+
type: 'message:interrupted',
|
|
96
|
+
sessionId: sessionKey,
|
|
97
|
+
reason: 'new_message',
|
|
98
|
+
agentName: this.processingAgent.get(queueKey),
|
|
99
|
+
});
|
|
92
100
|
if (this.interruptCallback) {
|
|
93
101
|
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
94
102
|
}
|
|
@@ -118,6 +126,7 @@ export class MessageQueue {
|
|
|
118
126
|
if (!queue || queue.length === 0) {
|
|
119
127
|
logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
|
|
120
128
|
this.processing.delete(queueKey);
|
|
129
|
+
this.processingAgent.delete(queueKey);
|
|
121
130
|
this.currentSessionKey = undefined;
|
|
122
131
|
this.currentProjectPath = undefined;
|
|
123
132
|
this.activeMessageIds.clear();
|
|
@@ -129,6 +138,7 @@ export class MessageQueue {
|
|
|
129
138
|
this.currentSessionKey = queueKey;
|
|
130
139
|
this.currentProjectPath = merged.projectPath;
|
|
131
140
|
this.currentAgentId = merged.message.agentId;
|
|
141
|
+
this.processingAgent.set(queueKey, merged.agentName);
|
|
132
142
|
// 记录正在执行的 messageId(用于撤回中断)
|
|
133
143
|
this.activeMessageIds.clear();
|
|
134
144
|
for (const item of items) {
|
|
@@ -203,6 +213,7 @@ export class MessageQueue {
|
|
|
203
213
|
return {
|
|
204
214
|
message: merged,
|
|
205
215
|
projectPath: last.projectPath,
|
|
216
|
+
agentName: last.agentName,
|
|
206
217
|
resolve: () => { }, // 由调用方管理
|
|
207
218
|
reject: () => { },
|
|
208
219
|
};
|
|
@@ -226,6 +237,20 @@ export class MessageQueue {
|
|
|
226
237
|
}
|
|
227
238
|
return false;
|
|
228
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* 检查指定 channel 下是否有任何 session 在处理。
|
|
242
|
+
* queueKey 格式为 `${sessionKey}::${projectPath}`,其中 sessionKey
|
|
243
|
+
* 形如 `${channelName}-${channelId}-${ts}`,因此匹配 `${channelName}-` 前缀。
|
|
244
|
+
*/
|
|
245
|
+
isChannelProcessing(channelName) {
|
|
246
|
+
const prefix = `${channelName}-`;
|
|
247
|
+
for (const key of this.processing.keys()) {
|
|
248
|
+
if (key.startsWith(prefix) || key.startsWith(`${channelName}::`)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
229
254
|
cancel(messageId) {
|
|
230
255
|
for (const queue of this.queues.values()) {
|
|
231
256
|
const idx = queue.findIndex(q => q.message.messageId === messageId);
|
|
@@ -250,7 +275,12 @@ export class MessageQueue {
|
|
|
250
275
|
// 从 queueKey 提取 sessionKey
|
|
251
276
|
const sessionKey = this.currentSessionKey.split('::')[0];
|
|
252
277
|
logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
|
|
253
|
-
this.eventBus?.publish({
|
|
278
|
+
this.eventBus?.publish({
|
|
279
|
+
type: 'message:interrupted',
|
|
280
|
+
sessionId: sessionKey,
|
|
281
|
+
reason: 'recalled',
|
|
282
|
+
agentName: this.processingAgent.get(this.currentSessionKey),
|
|
283
|
+
});
|
|
254
284
|
if (this.interruptCallback) {
|
|
255
285
|
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
256
286
|
}
|
|
@@ -293,4 +323,29 @@ export class MessageQueue {
|
|
|
293
323
|
getGlobalProcessingCount() {
|
|
294
324
|
return this.processing.size;
|
|
295
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* 获取指定 agent 的待处理消息数量。
|
|
328
|
+
* agent 维度按 enqueue 时传入的 agentName 计数。
|
|
329
|
+
*/
|
|
330
|
+
getQueueLengthByAgent(agentName) {
|
|
331
|
+
let total = 0;
|
|
332
|
+
for (const queue of this.queues.values()) {
|
|
333
|
+
for (const item of queue) {
|
|
334
|
+
if ((item.agentName || DEFAULT_AGENT_NAME) === agentName)
|
|
335
|
+
total++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return total;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 获取指定 agent 的处理中队列数量。
|
|
342
|
+
*/
|
|
343
|
+
getProcessingCountByAgent(agentName) {
|
|
344
|
+
let total = 0;
|
|
345
|
+
for (const a of this.processingAgent.values()) {
|
|
346
|
+
if ((a || DEFAULT_AGENT_NAME) === agentName)
|
|
347
|
+
total++;
|
|
348
|
+
}
|
|
349
|
+
return total;
|
|
350
|
+
}
|
|
296
351
|
}
|
|
@@ -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);
|
|
@@ -143,8 +177,48 @@ async function main() {
|
|
|
143
177
|
channelLoader.register(new DingtalkChannelPlugin());
|
|
144
178
|
channelLoader.register(new QQBotChannelPlugin());
|
|
145
179
|
channelLoader.register(new WecomChannelPlugin());
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
// Create channel instances: default (from evolclaw.json) + each evolagent
|
|
181
|
+
const defaultInstances = await channelLoader.createAll(config);
|
|
182
|
+
const evolagentInstances = [];
|
|
183
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
184
|
+
// Rewrite channel instance names with agent prefix to avoid collisions
|
|
185
|
+
// with DefaultAgent and other EvolAgents.
|
|
186
|
+
// Rule (EvolAgent only):
|
|
187
|
+
// - explicit name → `${agent.name}-${type}-${name}`
|
|
188
|
+
// - omitted name → `${agent.name}-${type}`
|
|
189
|
+
const rewrittenChannels = {};
|
|
190
|
+
for (const [type, raw] of Object.entries(agent.config.channels || {})) {
|
|
191
|
+
if (type === 'defaultChannel') {
|
|
192
|
+
rewrittenChannels[type] = raw;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
196
|
+
const rewritten = instances.map((inst) => {
|
|
197
|
+
if (!inst || typeof inst !== 'object')
|
|
198
|
+
return inst;
|
|
199
|
+
const effName = agent.effectiveChannelName(type, inst.name);
|
|
200
|
+
return { ...inst, name: effName };
|
|
201
|
+
});
|
|
202
|
+
// Preserve original shape (array vs single object)
|
|
203
|
+
rewrittenChannels[type] = Array.isArray(raw) ? rewritten : rewritten[0];
|
|
204
|
+
}
|
|
205
|
+
const agentConfig = {
|
|
206
|
+
agents: agent.config.agents,
|
|
207
|
+
channels: rewrittenChannels,
|
|
208
|
+
projects: agent.config.projects,
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
const instances = await channelLoader.createAll(agentConfig);
|
|
212
|
+
evolagentInstances.push(...instances);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
logger.error(`[EvolAgent] Failed to create channels for ${agent.name}: ${e}`);
|
|
216
|
+
agent.status = 'error';
|
|
217
|
+
agent.error = `Channel creation failed: ${e}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const channelInstances = [...defaultInstances, ...evolagentInstances];
|
|
221
|
+
logger.info(`✓ Created ${channelInstances.length} channel instance(s)${evolagentInstances.length > 0 ? ` (${defaultInstances.length} default + ${evolagentInstances.length} agent)` : ''}`);
|
|
148
222
|
// 启动迁移:将 sessions.channel 从 channelType 回填为实例名
|
|
149
223
|
sessionManager.migrateChannelToInstanceName();
|
|
150
224
|
// 创建命令处理器
|
|
@@ -166,6 +240,13 @@ async function main() {
|
|
|
166
240
|
}, defaultAgent);
|
|
167
241
|
// 回填 processor 和 messageQueue 的引用
|
|
168
242
|
cmdHandler.setProcessor(processor);
|
|
243
|
+
// Inject EvolAgentRegistry (methods added by T6/T7)
|
|
244
|
+
if (processor.setAgentRegistry) {
|
|
245
|
+
processor.setAgentRegistry(agentRegistry);
|
|
246
|
+
}
|
|
247
|
+
if (cmdHandler.setAgentRegistry) {
|
|
248
|
+
cmdHandler.setAgentRegistry(agentRegistry);
|
|
249
|
+
}
|
|
169
250
|
// 设置交互路由器
|
|
170
251
|
processor.setInteractionRouter(interactionRouter);
|
|
171
252
|
// 设置 compact 开始回调(对所有支持的 agent)
|
|
@@ -203,12 +284,18 @@ async function main() {
|
|
|
203
284
|
};
|
|
204
285
|
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
205
286
|
const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
|
|
287
|
+
msgBridge.setAgentRegistry(agentRegistry);
|
|
206
288
|
// ── Channel instance registration (shared by startup and hot-load) ──
|
|
207
289
|
function registerChannelInstance(inst) {
|
|
208
290
|
// 1. 项目路径提供器
|
|
209
291
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
210
292
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
211
|
-
|
|
293
|
+
// Effective default path: agent's projectPath if agent-owned, else global
|
|
294
|
+
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
295
|
+
const effectiveDefault = (owningAgent && !owningAgent.isDefault)
|
|
296
|
+
? owningAgent.projectPath
|
|
297
|
+
: (config.projects?.defaultPath || process.cwd());
|
|
298
|
+
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
|
|
212
299
|
return path.isAbsolute(session.projectPath)
|
|
213
300
|
? session.projectPath
|
|
214
301
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -348,20 +435,20 @@ async function main() {
|
|
|
348
435
|
for (const inst of channelInstances) {
|
|
349
436
|
registerChannelInstance(inst);
|
|
350
437
|
}
|
|
351
|
-
// ── 设置热加载回调 ──
|
|
352
|
-
cmdHandler.setHotLoadChannel(async (inst) => {
|
|
353
|
-
registerChannelInstance(inst);
|
|
354
|
-
channelInstances.push(inst);
|
|
355
|
-
await inst.connect();
|
|
356
|
-
eventBus.publish({
|
|
357
|
-
type: 'channel:connected',
|
|
358
|
-
channel: (inst.channelType || inst.adapter.channelName).toLowerCase(),
|
|
359
|
-
channelName: inst.adapter.channelName,
|
|
360
|
-
timestamp: Date.now(),
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
438
|
// ── 连接所有渠道 ──
|
|
364
439
|
const connected = await channelLoader.connectAll(channelInstances);
|
|
440
|
+
// Bind connected adapters to their owning agents
|
|
441
|
+
// I1: only mark 'running' if a channel actually connected for that agent
|
|
442
|
+
const connectedSet = new Set(connected);
|
|
443
|
+
for (const inst of channelInstances) {
|
|
444
|
+
const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
445
|
+
if (!agent || agent.status === 'error')
|
|
446
|
+
continue;
|
|
447
|
+
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
448
|
+
if (agent.status === 'stopped' && connectedSet.has(inst.adapter.channelName)) {
|
|
449
|
+
agent.status = 'running';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
365
452
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
366
453
|
for (const inst of channelInstances) {
|
|
367
454
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
@@ -396,7 +483,7 @@ async function main() {
|
|
|
396
483
|
continue; // 跳过同类型通道
|
|
397
484
|
if (notified.has(otherType))
|
|
398
485
|
continue; // 同类型已通知过
|
|
399
|
-
const ownerId = getOwner(config, other.adapter.channelName);
|
|
486
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelName) ?? getOwner(config, other.adapter.channelName);
|
|
400
487
|
if (!ownerId)
|
|
401
488
|
continue;
|
|
402
489
|
notified.add(otherType);
|
|
@@ -511,6 +598,18 @@ async function main() {
|
|
|
511
598
|
},
|
|
512
599
|
};
|
|
513
600
|
}, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
|
|
601
|
+
// M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
|
|
602
|
+
ipcServer.setAgentRegistry(agentRegistry);
|
|
603
|
+
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
604
|
+
const reloadHooks = buildReloadHooks({
|
|
605
|
+
channelLoader,
|
|
606
|
+
channelInstances,
|
|
607
|
+
registerChannelInstance,
|
|
608
|
+
messageQueue,
|
|
609
|
+
});
|
|
610
|
+
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
611
|
+
globalThis.__evolclaw_reloadHooks = reloadHooks;
|
|
612
|
+
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
514
613
|
ipcServer.start();
|
|
515
614
|
// 运行时配置文件监控
|
|
516
615
|
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/paths.js
CHANGED
|
@@ -32,6 +32,7 @@ export function resolvePaths() {
|
|
|
32
32
|
pid: path.join(root, 'logs', 'evolclaw.pid'),
|
|
33
33
|
dataDir: path.join(root, 'data'),
|
|
34
34
|
logs: path.join(root, 'logs'),
|
|
35
|
+
agentsDir: path.join(root, 'agents'),
|
|
35
36
|
lineStats: path.join(root, 'logs', 'line-stats.log'),
|
|
36
37
|
readySignal: path.join(root, 'logs', 'ready.signal'),
|
|
37
38
|
selfHealLog: path.join(root, 'logs', 'self-heal.md'),
|
|
@@ -49,6 +50,7 @@ export function ensureDataDirs() {
|
|
|
49
50
|
const p = resolvePaths();
|
|
50
51
|
fs.mkdirSync(p.dataDir, { recursive: true });
|
|
51
52
|
fs.mkdirSync(p.logs, { recursive: true });
|
|
53
|
+
fs.mkdirSync(p.agentsDir, { recursive: true });
|
|
52
54
|
}
|
|
53
55
|
export function getPackageRoot() {
|
|
54
56
|
// import.meta.dirname is available in Node.js 21.2+ and always returns
|
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]';
|