evolclaw 2.8.1 → 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 +1 -1
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/cli.js +345 -48
- package/dist/config.js +93 -21
- package/dist/core/agent-registry.js +287 -1
- package/dist/core/command-handler.js +370 -160
- package/dist/core/evolagent-registry.js +503 -0
- package/dist/core/evolagent.js +250 -1
- package/dist/core/message/message-bridge.js +23 -3
- package/dist/core/message/message-processor.js +39 -4
- package/dist/core/message/message-queue.js +59 -4
- package/dist/core/reload-hooks.js +87 -0
- package/dist/index.js +119 -8
- 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);
|
|
@@ -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);
|
|
@@ -350,6 +437,18 @@ async function main() {
|
|
|
350
437
|
}
|
|
351
438
|
// ── 连接所有渠道 ──
|
|
352
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
|
+
}
|
|
353
452
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
354
453
|
for (const inst of channelInstances) {
|
|
355
454
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
@@ -384,7 +483,7 @@ async function main() {
|
|
|
384
483
|
continue; // 跳过同类型通道
|
|
385
484
|
if (notified.has(otherType))
|
|
386
485
|
continue; // 同类型已通知过
|
|
387
|
-
const ownerId = getOwner(config, other.adapter.channelName);
|
|
486
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelName) ?? getOwner(config, other.adapter.channelName);
|
|
388
487
|
if (!ownerId)
|
|
389
488
|
continue;
|
|
390
489
|
notified.add(otherType);
|
|
@@ -499,6 +598,18 @@ async function main() {
|
|
|
499
598
|
},
|
|
500
599
|
};
|
|
501
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
|
|
502
613
|
ipcServer.start();
|
|
503
614
|
// 运行时配置文件监控
|
|
504
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/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