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
|
@@ -0,0 +1,315 @@
|
|
|
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
|
+
}
|
|
92
|
+
export class EvolAgent {
|
|
93
|
+
name;
|
|
94
|
+
configPath;
|
|
95
|
+
config;
|
|
96
|
+
isDefault;
|
|
97
|
+
channels = new Map();
|
|
98
|
+
activeSessions = 0;
|
|
99
|
+
lastActivity;
|
|
100
|
+
status;
|
|
101
|
+
error;
|
|
102
|
+
constructor(configPath, config, opts = {}) {
|
|
103
|
+
this.configPath = configPath;
|
|
104
|
+
this.config = config;
|
|
105
|
+
this.name = config.name;
|
|
106
|
+
this.isDefault = opts.isDefault === true;
|
|
107
|
+
this.status = config.enabled === false ? 'disabled' : 'stopped';
|
|
108
|
+
}
|
|
109
|
+
get baseagent() {
|
|
110
|
+
const keys = Object.keys(this.config.agents);
|
|
111
|
+
return keys[0] || 'claude';
|
|
112
|
+
}
|
|
113
|
+
get model() {
|
|
114
|
+
return this.config.agents[this.baseagent]?.model;
|
|
115
|
+
}
|
|
116
|
+
get effort() {
|
|
117
|
+
return this.config.agents[this.baseagent]?.effort;
|
|
118
|
+
}
|
|
119
|
+
get projectPath() {
|
|
120
|
+
return this.config.projects.defaultPath;
|
|
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
|
+
}
|
|
138
|
+
channelInstanceNames() {
|
|
139
|
+
const names = [];
|
|
140
|
+
for (const [type, raw] of Object.entries(this.config.channels || {})) {
|
|
141
|
+
const instances = Array.isArray(raw) ? raw : [raw];
|
|
142
|
+
for (const inst of instances) {
|
|
143
|
+
if (!inst || typeof inst !== 'object')
|
|
144
|
+
continue;
|
|
145
|
+
names.push(this.effectiveChannelName(type, inst.name));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return names;
|
|
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
|
+
}
|
|
292
|
+
getContext(channelName, chatType, globalChatmode) {
|
|
293
|
+
const chatMode = this.resolveChatMode(chatType, globalChatmode);
|
|
294
|
+
return {
|
|
295
|
+
name: this.name,
|
|
296
|
+
isOwned: !this.isDefault,
|
|
297
|
+
baseagent: this.baseagent,
|
|
298
|
+
model: this.model,
|
|
299
|
+
effort: this.effort,
|
|
300
|
+
chatMode,
|
|
301
|
+
projectPath: this.projectPath,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
resolveChatMode(chatType, globalChatmode) {
|
|
305
|
+
const agentCm = this.config.chatmode;
|
|
306
|
+
const key = chatType === 'group' ? 'group' : 'private';
|
|
307
|
+
if (agentCm) {
|
|
308
|
+
return (agentCm[key] || 'interactive');
|
|
309
|
+
}
|
|
310
|
+
if (globalChatmode) {
|
|
311
|
+
return (globalChatmode[key] || 'interactive');
|
|
312
|
+
}
|
|
313
|
+
return 'interactive';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -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
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
@@ -77,6 +77,19 @@ export class MessageProcessor {
|
|
|
77
77
|
setMessageQueue(queue) {
|
|
78
78
|
this.messageQueue = queue;
|
|
79
79
|
}
|
|
80
|
+
agentRegistry;
|
|
81
|
+
setAgentRegistry(registry) {
|
|
82
|
+
this.agentRegistry = registry;
|
|
83
|
+
}
|
|
84
|
+
getAgentContext(channelName, chatType) {
|
|
85
|
+
if (!this.agentRegistry)
|
|
86
|
+
return null;
|
|
87
|
+
const agent = this.agentRegistry.resolveByChannel(channelName);
|
|
88
|
+
if (!agent)
|
|
89
|
+
return null;
|
|
90
|
+
const globalCm = this.config.chatmode;
|
|
91
|
+
return agent.getContext(channelName, chatType, globalCm);
|
|
92
|
+
}
|
|
80
93
|
/**
|
|
81
94
|
* 注册渠道适配器
|
|
82
95
|
*/
|
|
@@ -157,6 +170,11 @@ export class MessageProcessor {
|
|
|
157
170
|
const streamKey = session.id;
|
|
158
171
|
const chatType = message.chatType || 'private';
|
|
159
172
|
const identityRole = session.identity?.role || 'anonymous';
|
|
173
|
+
// Resolve agent context from registry (Phase 2 foundation)
|
|
174
|
+
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
175
|
+
if (agentContext) {
|
|
176
|
+
logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
|
|
177
|
+
}
|
|
160
178
|
// 按 session.agentId 选择 agent 后端
|
|
161
179
|
const agent = this.getAgent(session.agentId);
|
|
162
180
|
const monitorEnabled = this.config.idleMonitor?.enabled !== false;
|
|
@@ -270,6 +288,8 @@ export class MessageProcessor {
|
|
|
270
288
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
271
289
|
const channelKey = session.metadata?.channelName || message.channel;
|
|
272
290
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
291
|
+
// Per-method agent name for stats bucketing (agent.name or '[default]')
|
|
292
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '[default]';
|
|
273
293
|
if (!channelInfo) {
|
|
274
294
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
275
295
|
return;
|
|
@@ -312,16 +332,22 @@ export class MessageProcessor {
|
|
|
312
332
|
channel: message.channel,
|
|
313
333
|
channelId: message.channelId,
|
|
314
334
|
content: message.content,
|
|
335
|
+
agentName: agentNameForStats,
|
|
315
336
|
timestamp: Date.now()
|
|
316
337
|
});
|
|
317
338
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
318
339
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
319
340
|
const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
|
|
320
341
|
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
|
|
321
|
-
|
|
342
|
+
// 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
|
|
343
|
+
const peerName = session.metadata?.peerName ?? message.peerName;
|
|
344
|
+
const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
|
|
345
|
+
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
346
|
+
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
347
|
+
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
322
348
|
// 记录开始处理
|
|
323
349
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
324
|
-
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId,
|
|
350
|
+
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, taskReplyContext());
|
|
325
351
|
logger.message({
|
|
326
352
|
msgId: messageId,
|
|
327
353
|
sessionId: session.id,
|
|
@@ -531,8 +557,13 @@ export class MessageProcessor {
|
|
|
531
557
|
for (const match of fileMatches) {
|
|
532
558
|
// 兼容旧格式 (1组) 和新格式 (2组)
|
|
533
559
|
const hasChannelGroup = match.length >= 3;
|
|
534
|
-
|
|
535
|
-
|
|
560
|
+
let targetSpec = hasChannelGroup ? (match[1] ?? undefined) : undefined;
|
|
561
|
+
let filePath = (hasChannelGroup ? match[2] : match[1]).trim();
|
|
562
|
+
// 白名单校验:targetSpec 必须是已注册通道,否则视为路径的一部分(如 Windows 盘符 C:)
|
|
563
|
+
if (targetSpec && !this.channels.has(targetSpec) && !this.channelTypeMap.has(targetSpec)) {
|
|
564
|
+
filePath = `${targetSpec}:${filePath}`;
|
|
565
|
+
targetSpec = undefined;
|
|
566
|
+
}
|
|
536
567
|
if (this.isPlaceholderPath(filePath)) {
|
|
537
568
|
logger.info(`[${adapter.channelName}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
|
|
538
569
|
continue;
|
|
@@ -574,7 +605,7 @@ export class MessageProcessor {
|
|
|
574
605
|
if (isCrossChannel) {
|
|
575
606
|
const targetAdapterName = targetInfo.adapter.channelName;
|
|
576
607
|
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
577
|
-
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
608
|
+
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName) ?? getOwner(this.config, targetAdapterName);
|
|
578
609
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
579
610
|
if (!targetChannelId) {
|
|
580
611
|
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
|
|
@@ -632,6 +663,12 @@ export class MessageProcessor {
|
|
|
632
663
|
// 清除处理中状态
|
|
633
664
|
this.sessionManager.clearProcessing(session.id);
|
|
634
665
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
666
|
+
// 更新 EvolAgent.lastActivity
|
|
667
|
+
if (this.agentRegistry) {
|
|
668
|
+
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
669
|
+
if (owningAgent)
|
|
670
|
+
owningAgent.lastActivity = Date.now();
|
|
671
|
+
}
|
|
635
672
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
636
673
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
637
674
|
if (streamResult.isError) {
|
|
@@ -639,12 +676,13 @@ export class MessageProcessor {
|
|
|
639
676
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
640
677
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
641
678
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
642
|
-
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId,
|
|
679
|
+
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, taskReplyContext());
|
|
643
680
|
this.eventBus.publish({
|
|
644
681
|
type: 'message:error',
|
|
645
682
|
sessionId: session.id,
|
|
646
683
|
error: errorSummary,
|
|
647
684
|
errorType,
|
|
685
|
+
agentName: agentNameForStats,
|
|
648
686
|
terminalReason: streamResult.terminalReason
|
|
649
687
|
});
|
|
650
688
|
// 系统级 subtype 仍累计错误计数,供 /status 诊断使用
|
|
@@ -667,7 +705,7 @@ export class MessageProcessor {
|
|
|
667
705
|
}
|
|
668
706
|
else {
|
|
669
707
|
// 真正的成功
|
|
670
|
-
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId,
|
|
708
|
+
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, taskReplyContext());
|
|
671
709
|
await this.sessionManager.recordSuccess(session.id);
|
|
672
710
|
this.eventBus.publish({
|
|
673
711
|
type: 'message:completed',
|
|
@@ -677,6 +715,7 @@ export class MessageProcessor {
|
|
|
677
715
|
terminalReason: streamResult.terminalReason,
|
|
678
716
|
finalText: streamResult.lastReplyText || undefined,
|
|
679
717
|
durationMs: Date.now() - startTime,
|
|
718
|
+
agentName: agentNameForStats,
|
|
680
719
|
timestamp: Date.now()
|
|
681
720
|
});
|
|
682
721
|
// 记录处理完成
|
|
@@ -722,7 +761,7 @@ export class MessageProcessor {
|
|
|
722
761
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
723
762
|
if (!isUserInterrupt) {
|
|
724
763
|
try {
|
|
725
|
-
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId,
|
|
764
|
+
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, taskReplyContext());
|
|
726
765
|
}
|
|
727
766
|
catch { }
|
|
728
767
|
}
|
|
@@ -739,7 +778,8 @@ export class MessageProcessor {
|
|
|
739
778
|
type: 'message:error',
|
|
740
779
|
sessionId: session.id,
|
|
741
780
|
error: errorMsg,
|
|
742
|
-
errorType
|
|
781
|
+
errorType,
|
|
782
|
+
agentName: agentNameForStats,
|
|
743
783
|
});
|
|
744
784
|
// 记录处理失败
|
|
745
785
|
logger.message({
|
|
@@ -812,6 +852,8 @@ export class MessageProcessor {
|
|
|
812
852
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
813
853
|
*/
|
|
814
854
|
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
|
|
855
|
+
// Per-session agent name for stats bucketing
|
|
856
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '[default]';
|
|
815
857
|
let hasReceivedText = false;
|
|
816
858
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
817
859
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -932,6 +974,7 @@ export class MessageProcessor {
|
|
|
932
974
|
toolName: event.name,
|
|
933
975
|
isError: event.isError,
|
|
934
976
|
content: event.result,
|
|
977
|
+
agentName: agentNameForStats,
|
|
935
978
|
timestamp: Date.now()
|
|
936
979
|
});
|
|
937
980
|
if (event.isError && !shouldSuppress()) {
|
|
@@ -1017,6 +1060,7 @@ export class MessageProcessor {
|
|
|
1017
1060
|
channelId: session.channelId,
|
|
1018
1061
|
finalText: lastReplyText || event.result || undefined,
|
|
1019
1062
|
durationMs: event.durationMs,
|
|
1063
|
+
agentName: agentNameForStats,
|
|
1020
1064
|
timestamp: Date.now()
|
|
1021
1065
|
});
|
|
1022
1066
|
}
|
|
@@ -1035,7 +1079,8 @@ export class MessageProcessor {
|
|
|
1035
1079
|
type: 'message:error',
|
|
1036
1080
|
sessionId: session.id,
|
|
1037
1081
|
error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
|
|
1038
|
-
errorType: bgErrorType
|
|
1082
|
+
errorType: bgErrorType,
|
|
1083
|
+
agentName: agentNameForStats,
|
|
1039
1084
|
});
|
|
1040
1085
|
}
|
|
1041
1086
|
}
|