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
package/dist/core/evolagent.js
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { validateDefaultChannelRef } from '../config.js';
|
|
5
|
+
const VALID_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
6
|
+
const VALID_CHANNEL_TYPES = new Set(['feishu', 'aun', 'wechat', 'wecom', 'dingtalk', 'qqbot']);
|
|
7
|
+
const VALID_CHATMODES = new Set(['interactive', 'proactive']);
|
|
8
|
+
export function validateEvolAgentConfig(raw) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
if (!raw || typeof raw !== 'object') {
|
|
11
|
+
return { valid: false, errors: ['config must be an object'] };
|
|
12
|
+
}
|
|
13
|
+
if (typeof raw.name !== 'string' || raw.name.trim() === '') {
|
|
14
|
+
errors.push('name is required and must be a non-empty string');
|
|
15
|
+
}
|
|
16
|
+
if (raw.enabled !== undefined && typeof raw.enabled !== 'boolean') {
|
|
17
|
+
errors.push('enabled must be a boolean if present');
|
|
18
|
+
}
|
|
19
|
+
if (!raw.agents || typeof raw.agents !== 'object') {
|
|
20
|
+
errors.push('agents must be an object with exactly one baseagent block');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const keys = Object.keys(raw.agents).filter(k => VALID_BASEAGENTS.has(k));
|
|
24
|
+
const unknownKeys = Object.keys(raw.agents).filter(k => !VALID_BASEAGENTS.has(k));
|
|
25
|
+
if (unknownKeys.length > 0) {
|
|
26
|
+
errors.push(`agents contains unknown baseagent keys: ${unknownKeys.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
if (keys.length === 0) {
|
|
29
|
+
errors.push('agents must contain exactly one of: claude | codex | gemini | hermes');
|
|
30
|
+
}
|
|
31
|
+
else if (keys.length > 1) {
|
|
32
|
+
errors.push(`agents must contain exactly one baseagent (single baseagent only), got: ${keys.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!raw.channels || typeof raw.channels !== 'object') {
|
|
36
|
+
errors.push('channels is required');
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const channelKeys = Object.keys(raw.channels).filter(k => k !== 'defaultChannel');
|
|
40
|
+
if (channelKeys.length === 0) {
|
|
41
|
+
errors.push('channels must contain at least one channel type');
|
|
42
|
+
}
|
|
43
|
+
for (const key of channelKeys) {
|
|
44
|
+
if (!VALID_CHANNEL_TYPES.has(key)) {
|
|
45
|
+
errors.push(`unknown channel type: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// defaultChannel reference validation (same rules as evolclaw.json)
|
|
49
|
+
let totalInstances = 0;
|
|
50
|
+
for (const key of channelKeys) {
|
|
51
|
+
const block = raw.channels[key];
|
|
52
|
+
const insts = Array.isArray(block) ? block : (block ? [block] : []);
|
|
53
|
+
totalInstances += insts.length;
|
|
54
|
+
}
|
|
55
|
+
const dc = raw.channels.defaultChannel;
|
|
56
|
+
if (dc) {
|
|
57
|
+
const err = validateDefaultChannelRef(dc, raw.channels);
|
|
58
|
+
if (err)
|
|
59
|
+
errors.push(err);
|
|
60
|
+
}
|
|
61
|
+
else if (totalInstances > 1) {
|
|
62
|
+
errors.push('channels.defaultChannel is required when multiple channel instances are configured (use "type" or "type/instanceName")');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!raw.projects || typeof raw.projects !== 'object') {
|
|
66
|
+
errors.push('projects is required');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const p = raw.projects.defaultPath;
|
|
70
|
+
if (typeof p !== 'string' || p === '') {
|
|
71
|
+
errors.push('projects.defaultPath is required');
|
|
72
|
+
}
|
|
73
|
+
else if (!path.isAbsolute(p)) {
|
|
74
|
+
errors.push(`projects.defaultPath must be absolute, got: ${p}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (raw.chatmode !== undefined) {
|
|
78
|
+
if (typeof raw.chatmode !== 'object' || raw.chatmode === null) {
|
|
79
|
+
errors.push('chatmode must be an object if present');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
for (const key of ['private', 'group']) {
|
|
83
|
+
const val = raw.chatmode[key];
|
|
84
|
+
if (val !== undefined && !VALID_CHATMODES.has(val)) {
|
|
85
|
+
errors.push(`chatmode.${key} must be 'interactive' or 'proactive'`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { valid: errors.length === 0, errors };
|
|
91
|
+
}
|
|
1
92
|
export class EvolAgent {
|
|
2
93
|
name;
|
|
3
94
|
configPath;
|
|
@@ -28,6 +119,22 @@ export class EvolAgent {
|
|
|
28
119
|
get projectPath() {
|
|
29
120
|
return this.config.projects.defaultPath;
|
|
30
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Compute the effective channel-instance name (used as registry key, session.channel, etc).
|
|
124
|
+
*
|
|
125
|
+
* - DefaultAgent: rawName ?? type (preserves backward-compat with evolclaw.json)
|
|
126
|
+
* - EvolAgent:
|
|
127
|
+
* - rawName present → `${agent.name}-${type}-${rawName}`
|
|
128
|
+
* - rawName absent → `${agent.name}-${type}`
|
|
129
|
+
*
|
|
130
|
+
* The agent-name prefix avoids collisions with DefaultAgent channels, e.g.
|
|
131
|
+
* test-bot's aun → "test-bot-aun" instead of "aun".
|
|
132
|
+
*/
|
|
133
|
+
effectiveChannelName(type, rawName) {
|
|
134
|
+
if (this.isDefault)
|
|
135
|
+
return rawName ?? type;
|
|
136
|
+
return rawName ? `${this.name}-${type}-${rawName}` : `${this.name}-${type}`;
|
|
137
|
+
}
|
|
31
138
|
channelInstanceNames() {
|
|
32
139
|
const names = [];
|
|
33
140
|
for (const [type, raw] of Object.entries(this.config.channels || {})) {
|
|
@@ -35,11 +142,153 @@ export class EvolAgent {
|
|
|
35
142
|
for (const inst of instances) {
|
|
36
143
|
if (!inst || typeof inst !== 'object')
|
|
37
144
|
continue;
|
|
38
|
-
names.push(inst.name
|
|
145
|
+
names.push(this.effectiveChannelName(type, inst.name));
|
|
39
146
|
}
|
|
40
147
|
}
|
|
41
148
|
return names;
|
|
42
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Locate a channel-instance config block within this agent's config by
|
|
152
|
+
* matching the effective channel name (with agent prefix for EvolAgents).
|
|
153
|
+
* Returns the raw mutable instance object, or `null` if not found.
|
|
154
|
+
*/
|
|
155
|
+
findChannelInstance(channelName) {
|
|
156
|
+
const channels = this.config.channels || {};
|
|
157
|
+
for (const [type, raw] of Object.entries(channels)) {
|
|
158
|
+
if (type === 'defaultChannel')
|
|
159
|
+
continue;
|
|
160
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
161
|
+
for (const inst of instances) {
|
|
162
|
+
if (!inst || typeof inst !== 'object')
|
|
163
|
+
continue;
|
|
164
|
+
const effName = this.effectiveChannelName(type, inst.name);
|
|
165
|
+
if (effName === channelName)
|
|
166
|
+
return inst;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
/** Get owner of a specific channel instance owned by this agent. */
|
|
172
|
+
getOwner(channelName) {
|
|
173
|
+
const inst = this.findChannelInstance(channelName);
|
|
174
|
+
return inst?.owner;
|
|
175
|
+
}
|
|
176
|
+
/** True when `userId` is the owner of `channelName`. */
|
|
177
|
+
isOwner(channelName, userId) {
|
|
178
|
+
return this.getOwner(channelName) === userId;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* True when `userId` is admin (or owner) of `channelName`.
|
|
182
|
+
* Owner implicitly has admin rights.
|
|
183
|
+
*/
|
|
184
|
+
isAdmin(channelName, userId) {
|
|
185
|
+
if (this.isOwner(channelName, userId))
|
|
186
|
+
return true;
|
|
187
|
+
const inst = this.findChannelInstance(channelName);
|
|
188
|
+
const admins = inst?.admins || [];
|
|
189
|
+
return admins.includes(userId);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Set owner for a channel instance and persist to agent.json.
|
|
193
|
+
* Throws when called on DefaultAgent (no configPath) — callers must use
|
|
194
|
+
* the global config setter for default channels.
|
|
195
|
+
*/
|
|
196
|
+
setOwner(channelName, userId) {
|
|
197
|
+
const inst = this.findChannelInstance(channelName);
|
|
198
|
+
if (!inst) {
|
|
199
|
+
logger.warn(`[EvolAgent] setOwner: channel "${channelName}" not found in agent "${this.name}"`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
inst.owner = userId;
|
|
203
|
+
this.persist();
|
|
204
|
+
}
|
|
205
|
+
/** Get showActivities mode for a channel instance owned by this agent. */
|
|
206
|
+
getShowActivities(channelName) {
|
|
207
|
+
const inst = this.findChannelInstance(channelName);
|
|
208
|
+
return inst?.showActivities ?? 'all';
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set showActivities for a channel instance and persist to agent.json.
|
|
212
|
+
* Throws when called on DefaultAgent — callers must use the global setter.
|
|
213
|
+
*/
|
|
214
|
+
setShowActivities(channelName, mode) {
|
|
215
|
+
const inst = this.findChannelInstance(channelName);
|
|
216
|
+
if (!inst) {
|
|
217
|
+
logger.warn(`[EvolAgent] setShowActivities: channel "${channelName}" not found in agent "${this.name}"`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
inst.showActivities = mode;
|
|
221
|
+
this.persist();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Set this agent's baseagent.model and persist to agent.json.
|
|
225
|
+
* Refuses for DefaultAgent. Writes to config.agents[baseagent].model.
|
|
226
|
+
*/
|
|
227
|
+
setBaseagentModel(value) {
|
|
228
|
+
const ba = this.baseagent;
|
|
229
|
+
if (!this.config.agents[ba])
|
|
230
|
+
this.config.agents[ba] = {};
|
|
231
|
+
if (value === undefined) {
|
|
232
|
+
delete this.config.agents[ba].model;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.config.agents[ba].model = value;
|
|
236
|
+
}
|
|
237
|
+
this.persist();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the agent's project list (defaults to a single entry derived from
|
|
241
|
+
* projects.defaultPath when projects.list is empty/absent).
|
|
242
|
+
*/
|
|
243
|
+
getProjects() {
|
|
244
|
+
const list = this.config.projects?.list;
|
|
245
|
+
if (list && Object.keys(list).length > 0)
|
|
246
|
+
return { ...list };
|
|
247
|
+
const dp = this.config.projects?.defaultPath;
|
|
248
|
+
if (dp)
|
|
249
|
+
return { [path.basename(dp)]: dp };
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Add (or update) a named project in this agent's projects.list and persist.
|
|
254
|
+
* Throws for DefaultAgent (caller should write to evolclaw.json instead).
|
|
255
|
+
*/
|
|
256
|
+
addProject(name, projectPath) {
|
|
257
|
+
if (!this.config.projects)
|
|
258
|
+
this.config.projects = { defaultPath: projectPath, list: {} };
|
|
259
|
+
if (!this.config.projects.list)
|
|
260
|
+
this.config.projects.list = {};
|
|
261
|
+
this.config.projects.list[name] = projectPath;
|
|
262
|
+
this.persist();
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Set this agent's baseagent.effort and persist to agent.json.
|
|
266
|
+
* For codex, the field is named `reasoning` (alias). Refuses for DefaultAgent.
|
|
267
|
+
*/
|
|
268
|
+
setBaseagentEffort(value) {
|
|
269
|
+
const ba = this.baseagent;
|
|
270
|
+
if (!this.config.agents[ba])
|
|
271
|
+
this.config.agents[ba] = {};
|
|
272
|
+
const fieldName = ba === 'codex' ? 'reasoning' : 'effort';
|
|
273
|
+
if (value === undefined) {
|
|
274
|
+
delete this.config.agents[ba][fieldName];
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
this.config.agents[ba][fieldName] = value;
|
|
278
|
+
}
|
|
279
|
+
this.persist();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Persist the in-memory config back to the agent.json file.
|
|
283
|
+
* Refuses for DefaultAgent: it is built from evolclaw.json and has no
|
|
284
|
+
* dedicated file — callers must route writes through the global config.
|
|
285
|
+
*/
|
|
286
|
+
persist() {
|
|
287
|
+
if (!this.configPath) {
|
|
288
|
+
throw new Error('Cannot persist DefaultAgent config; use global config setters');
|
|
289
|
+
}
|
|
290
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2) + '\n', 'utf-8');
|
|
291
|
+
}
|
|
43
292
|
getContext(channelName, chatType, globalChatmode) {
|
|
44
293
|
const chatMode = this.resolveChatMode(chatType, globalChatmode);
|
|
45
294
|
return {
|
|
@@ -16,6 +16,7 @@ export class MessageBridge {
|
|
|
16
16
|
eventBus;
|
|
17
17
|
debouncers = new Map();
|
|
18
18
|
defaultDebounce;
|
|
19
|
+
agentRegistry;
|
|
19
20
|
constructor(config, sessionManager, processor, messageQueue, cmdHandler, eventBus) {
|
|
20
21
|
this.config = config;
|
|
21
22
|
this.sessionManager = sessionManager;
|
|
@@ -25,6 +26,10 @@ export class MessageBridge {
|
|
|
25
26
|
this.eventBus = eventBus;
|
|
26
27
|
this.defaultDebounce = config.debounce ?? 2;
|
|
27
28
|
}
|
|
29
|
+
/** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
|
|
30
|
+
setAgentRegistry(registry) {
|
|
31
|
+
this.agentRegistry = registry;
|
|
32
|
+
}
|
|
28
33
|
getDebouncer(channelName, channelType) {
|
|
29
34
|
let d = this.debouncers.get(channelName);
|
|
30
35
|
if (!d) {
|
|
@@ -88,7 +93,13 @@ export class MessageBridge {
|
|
|
88
93
|
if (msg.peerName)
|
|
89
94
|
metadata.peerName = msg.peerName;
|
|
90
95
|
}
|
|
91
|
-
|
|
96
|
+
// Resolve effective project path: agent's projectPath when channel is agent-owned,
|
|
97
|
+
// otherwise fall back to global config.projects.defaultPath
|
|
98
|
+
const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
|
|
99
|
+
const effectiveProjectPath = (owningAgent && !owningAgent.isDefault)
|
|
100
|
+
? owningAgent.projectPath
|
|
101
|
+
: (this.config.projects?.defaultPath || process.cwd());
|
|
102
|
+
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType);
|
|
92
103
|
// 4. 消息前缀(由 policy 决定)
|
|
93
104
|
const channelInfo = this.processor.getChannelInfo?.(channelName);
|
|
94
105
|
if (channelInfo?.policy) {
|
|
@@ -114,9 +125,11 @@ export class MessageBridge {
|
|
|
114
125
|
if (fullMessage.messageId)
|
|
115
126
|
adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
|
|
116
127
|
const isInterrupt = chatType !== 'group';
|
|
128
|
+
const enqueueAgentName = (owningAgent && !owningAgent.isDefault) ? owningAgent.name : '[default]';
|
|
117
129
|
const doEnqueue = async (m) => {
|
|
118
130
|
return this.messageQueue.enqueue(session.id, m, session.projectPath, {
|
|
119
131
|
interruptible: isInterrupt,
|
|
132
|
+
agentName: enqueueAgentName,
|
|
120
133
|
});
|
|
121
134
|
};
|
|
122
135
|
if (isInterrupt) {
|
|
@@ -192,12 +205,19 @@ export class MessageBridge {
|
|
|
192
205
|
}
|
|
193
206
|
/** 首次交互自动绑定 owner */
|
|
194
207
|
async autoBindOwner(channel, userId) {
|
|
208
|
+
// Registry-first: route owner queries/writes to the agent that owns this channel.
|
|
209
|
+
// Falls back to evolclaw.json for default-agent channels.
|
|
195
210
|
const { getOwner, setOwner } = await import('../../config.js');
|
|
196
|
-
const currentOwner = getOwner(this.config, channel);
|
|
211
|
+
const currentOwner = this.agentRegistry?.getOwner?.(channel) ?? getOwner(this.config, channel);
|
|
197
212
|
// currentOwner === undefined means either no owner set, or instance not found
|
|
198
213
|
// In both cases, try to set — setOwner is a no-op for unknown instances
|
|
199
214
|
if (currentOwner === undefined) {
|
|
200
|
-
|
|
215
|
+
if (this.agentRegistry?.setChannelOwner) {
|
|
216
|
+
this.agentRegistry.setChannelOwner(channel, userId);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
setOwner(this.config, channel, userId);
|
|
220
|
+
}
|
|
201
221
|
logger.info(`[Owner] Auto-bound ${channel} owner: ${userId}`);
|
|
202
222
|
this.eventBus.publish({ type: 'channel:owner-bound', channel, userId });
|
|
203
223
|
}
|
|
@@ -11,7 +11,7 @@ import { summarizeToolInput } from '../permission.js';
|
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
12
|
import { getOwner } from '../../config.js';
|
|
13
13
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
14
|
-
import { renderPromptSection } from '../../
|
|
14
|
+
import { renderPromptSection } from '../../agents/templates.js';
|
|
15
15
|
/**
|
|
16
16
|
* 统一消息处理器
|
|
17
17
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
@@ -32,11 +32,25 @@ export class MessageProcessor {
|
|
|
32
32
|
interactionRouter;
|
|
33
33
|
messageQueue;
|
|
34
34
|
skillsEnsured = false; // 全局 SKILLS.md 是否已确保
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Get the runner for a given (channel, baseagent) pair.
|
|
37
|
+
*
|
|
38
|
+
* - `channel` is used to look up the owning EvolAgent (via registry).
|
|
39
|
+
* - `baseagent` (e.g. 'claude') comes from `session.agentId`.
|
|
40
|
+
*
|
|
41
|
+
* Falls back to `defaultAgentId` (a composite key, e.g. `[default]::claude`)
|
|
42
|
+
* when no match is found.
|
|
43
|
+
*/
|
|
44
|
+
getAgent(channel, baseagent) {
|
|
45
|
+
if (channel && baseagent) {
|
|
46
|
+
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
|
|
47
|
+
const key = `${evolName}::${baseagent}`;
|
|
48
|
+
if (this.agentMap.has(key))
|
|
49
|
+
return this.agentMap.get(key);
|
|
50
|
+
}
|
|
51
|
+
if (this.agentMap.has(this.defaultAgentId))
|
|
52
|
+
return this.agentMap.get(this.defaultAgentId);
|
|
53
|
+
return this.agentMap.values().next().value;
|
|
40
54
|
}
|
|
41
55
|
/** 获取可用 agent 列表 */
|
|
42
56
|
getAvailableAgents() {
|
|
@@ -57,12 +71,12 @@ export class MessageProcessor {
|
|
|
57
71
|
this.commandHandler = commandHandler;
|
|
58
72
|
if (agentRunnerOrMap instanceof Map) {
|
|
59
73
|
this.agentMap = agentRunnerOrMap;
|
|
60
|
-
this.defaultAgentId = defaultAgentId || 'claude';
|
|
74
|
+
this.defaultAgentId = defaultAgentId || '[default]::claude';
|
|
61
75
|
}
|
|
62
76
|
else {
|
|
63
|
-
//
|
|
64
|
-
this.agentMap = new Map([[agentRunnerOrMap.name
|
|
65
|
-
this.defaultAgentId = agentRunnerOrMap.name
|
|
77
|
+
// Backward-compat single-runner path.
|
|
78
|
+
this.agentMap = new Map([[`[default]::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
|
|
79
|
+
this.defaultAgentId = `[default]::${agentRunnerOrMap.name}`;
|
|
66
80
|
}
|
|
67
81
|
// 监听中断事件,标记被中断的 session
|
|
68
82
|
this.eventBus.subscribe('message:interrupted', (event) => {
|
|
@@ -77,6 +91,19 @@ export class MessageProcessor {
|
|
|
77
91
|
setMessageQueue(queue) {
|
|
78
92
|
this.messageQueue = queue;
|
|
79
93
|
}
|
|
94
|
+
agentRegistry;
|
|
95
|
+
setAgentRegistry(registry) {
|
|
96
|
+
this.agentRegistry = registry;
|
|
97
|
+
}
|
|
98
|
+
getAgentContext(channelName, chatType) {
|
|
99
|
+
if (!this.agentRegistry)
|
|
100
|
+
return null;
|
|
101
|
+
const agent = this.agentRegistry.resolveByChannel(channelName);
|
|
102
|
+
if (!agent)
|
|
103
|
+
return null;
|
|
104
|
+
const globalCm = this.config.chatmode;
|
|
105
|
+
return agent.getContext(channelName, chatType, globalCm);
|
|
106
|
+
}
|
|
80
107
|
/**
|
|
81
108
|
* 注册渠道适配器
|
|
82
109
|
*/
|
|
@@ -157,8 +184,13 @@ export class MessageProcessor {
|
|
|
157
184
|
const streamKey = session.id;
|
|
158
185
|
const chatType = message.chatType || 'private';
|
|
159
186
|
const identityRole = session.identity?.role || 'anonymous';
|
|
187
|
+
// Resolve agent context from registry (Phase 2 foundation)
|
|
188
|
+
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
189
|
+
if (agentContext) {
|
|
190
|
+
logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
|
|
191
|
+
}
|
|
160
192
|
// 按 session.agentId 选择 agent 后端
|
|
161
|
-
const agent = this.getAgent(session.agentId);
|
|
193
|
+
const agent = this.getAgent(channelKey, session.agentId);
|
|
162
194
|
const monitorEnabled = this.config.idleMonitor?.enabled !== false;
|
|
163
195
|
const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
|
|
164
196
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
@@ -270,6 +302,8 @@ export class MessageProcessor {
|
|
|
270
302
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
271
303
|
const channelKey = session.metadata?.channelName || message.channel;
|
|
272
304
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
305
|
+
// Per-method agent name for stats bucketing (agent.name or '[default]')
|
|
306
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '[default]';
|
|
273
307
|
if (!channelInfo) {
|
|
274
308
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
275
309
|
return;
|
|
@@ -282,7 +316,7 @@ export class MessageProcessor {
|
|
|
282
316
|
return;
|
|
283
317
|
}
|
|
284
318
|
const { adapter, options } = channelInfo;
|
|
285
|
-
const agent = this.getAgent(session.agentId);
|
|
319
|
+
const agent = this.getAgent(channelKey, session.agentId);
|
|
286
320
|
const streamKey = session.id;
|
|
287
321
|
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
288
322
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
@@ -312,6 +346,7 @@ export class MessageProcessor {
|
|
|
312
346
|
channel: message.channel,
|
|
313
347
|
channelId: message.channelId,
|
|
314
348
|
content: message.content,
|
|
349
|
+
agentName: agentNameForStats,
|
|
315
350
|
timestamp: Date.now()
|
|
316
351
|
});
|
|
317
352
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
@@ -584,7 +619,7 @@ export class MessageProcessor {
|
|
|
584
619
|
if (isCrossChannel) {
|
|
585
620
|
const targetAdapterName = targetInfo.adapter.channelName;
|
|
586
621
|
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
587
|
-
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
622
|
+
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName) ?? getOwner(this.config, targetAdapterName);
|
|
588
623
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
589
624
|
if (!targetChannelId) {
|
|
590
625
|
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
|
|
@@ -642,6 +677,12 @@ export class MessageProcessor {
|
|
|
642
677
|
// 清除处理中状态
|
|
643
678
|
this.sessionManager.clearProcessing(session.id);
|
|
644
679
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
680
|
+
// 更新 EvolAgent.lastActivity
|
|
681
|
+
if (this.agentRegistry) {
|
|
682
|
+
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
683
|
+
if (owningAgent)
|
|
684
|
+
owningAgent.lastActivity = Date.now();
|
|
685
|
+
}
|
|
645
686
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
646
687
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
647
688
|
if (streamResult.isError) {
|
|
@@ -655,6 +696,7 @@ export class MessageProcessor {
|
|
|
655
696
|
sessionId: session.id,
|
|
656
697
|
error: errorSummary,
|
|
657
698
|
errorType,
|
|
699
|
+
agentName: agentNameForStats,
|
|
658
700
|
terminalReason: streamResult.terminalReason
|
|
659
701
|
});
|
|
660
702
|
// 系统级 subtype 仍累计错误计数,供 /status 诊断使用
|
|
@@ -687,6 +729,7 @@ export class MessageProcessor {
|
|
|
687
729
|
terminalReason: streamResult.terminalReason,
|
|
688
730
|
finalText: streamResult.lastReplyText || undefined,
|
|
689
731
|
durationMs: Date.now() - startTime,
|
|
732
|
+
agentName: agentNameForStats,
|
|
690
733
|
timestamp: Date.now()
|
|
691
734
|
});
|
|
692
735
|
// 记录处理完成
|
|
@@ -749,7 +792,8 @@ export class MessageProcessor {
|
|
|
749
792
|
type: 'message:error',
|
|
750
793
|
sessionId: session.id,
|
|
751
794
|
error: errorMsg,
|
|
752
|
-
errorType
|
|
795
|
+
errorType,
|
|
796
|
+
agentName: agentNameForStats,
|
|
753
797
|
});
|
|
754
798
|
// 记录处理失败
|
|
755
799
|
logger.message({
|
|
@@ -822,6 +866,8 @@ export class MessageProcessor {
|
|
|
822
866
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
823
867
|
*/
|
|
824
868
|
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
|
|
869
|
+
// Per-session agent name for stats bucketing
|
|
870
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '[default]';
|
|
825
871
|
let hasReceivedText = false;
|
|
826
872
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
827
873
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -942,6 +988,7 @@ export class MessageProcessor {
|
|
|
942
988
|
toolName: event.name,
|
|
943
989
|
isError: event.isError,
|
|
944
990
|
content: event.result,
|
|
991
|
+
agentName: agentNameForStats,
|
|
945
992
|
timestamp: Date.now()
|
|
946
993
|
});
|
|
947
994
|
if (event.isError && !shouldSuppress()) {
|
|
@@ -1027,6 +1074,7 @@ export class MessageProcessor {
|
|
|
1027
1074
|
channelId: session.channelId,
|
|
1028
1075
|
finalText: lastReplyText || event.result || undefined,
|
|
1029
1076
|
durationMs: event.durationMs,
|
|
1077
|
+
agentName: agentNameForStats,
|
|
1030
1078
|
timestamp: Date.now()
|
|
1031
1079
|
});
|
|
1032
1080
|
}
|
|
@@ -1045,7 +1093,8 @@ export class MessageProcessor {
|
|
|
1045
1093
|
type: 'message:error',
|
|
1046
1094
|
sessionId: session.id,
|
|
1047
1095
|
error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
|
|
1048
|
-
errorType: bgErrorType
|
|
1096
|
+
errorType: bgErrorType,
|
|
1097
|
+
agentName: agentNameForStats,
|
|
1049
1098
|
});
|
|
1050
1099
|
}
|
|
1051
1100
|
}
|
|
@@ -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,20 +79,26 @@ 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
|
-
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
101
|
+
this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(queueKey)).catch(() => { });
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
else {
|
|
@@ -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,9 +275,14 @@ 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
|
-
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
285
|
+
this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(this.currentSessionKey)).catch(() => { });
|
|
256
286
|
}
|
|
257
287
|
return true;
|
|
258
288
|
}
|
|
@@ -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
|
}
|