evolclaw 3.1.2 → 3.1.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.
@@ -159,6 +159,15 @@ export class EvolAgent {
159
159
  this.persist();
160
160
  }
161
161
  // ── Baseagent 字段写入 ────────────────────────────────────────────────
162
+ /** 切换当前活跃 baseagent(写顶层 active_baseagent)。 */
163
+ setActiveBaseagent(value) {
164
+ if (value === undefined)
165
+ delete this.rawAgent.active_baseagent;
166
+ else
167
+ this.rawAgent.active_baseagent = value;
168
+ this.merged.active_baseagent = value;
169
+ this.persist();
170
+ }
162
171
  setBaseagentModel(value) {
163
172
  const ba = this.baseagent;
164
173
  if (!this.rawAgent.baseagents)
@@ -182,6 +191,28 @@ export class EvolAgent {
182
191
  block[fieldName] = value;
183
192
  this.persist();
184
193
  }
194
+ /** 设置私聊 chatmode(群聊/非 human 强制 proactive,无可写入项)。 */
195
+ setChatmodePrivate(value) {
196
+ if (!this.rawAgent.chatmode)
197
+ this.rawAgent.chatmode = {};
198
+ if (value === undefined)
199
+ delete this.rawAgent.chatmode.private;
200
+ else
201
+ this.rawAgent.chatmode.private = value;
202
+ if (!this.merged.chatmode)
203
+ this.merged.chatmode = {};
204
+ this.merged.chatmode.private = value;
205
+ this.persist();
206
+ }
207
+ /** 设置群聊 dispatch 默认值(mention | broadcast)。 */
208
+ setDispatch(value) {
209
+ if (value === undefined)
210
+ delete this.rawAgent.dispatch;
211
+ else
212
+ this.rawAgent.dispatch = value;
213
+ this.merged.dispatch = value;
214
+ this.persist();
215
+ }
185
216
  // ── Projects ──────────────────────────────────────────────────────────
186
217
  getProjects() {
187
218
  const list = this.merged.projects?.list;
@@ -142,6 +142,16 @@ export class IMRenderer {
142
142
  }
143
143
  }
144
144
  }
145
+ /** 清除上下文过长错误文本(从 buffer + allText 中移除) */
146
+ stripContextError(pattern) {
147
+ this.textBuffer = this.textBuffer.replace(pattern, '').trim();
148
+ this.allText = this.allText.replace(pattern, '').trim();
149
+ for (const item of this.itemsQueue) {
150
+ if (item.kind === 'text') {
151
+ item.text = item.text.replace(pattern, '');
152
+ }
153
+ }
154
+ }
145
155
  // ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
146
156
  /** 添加文本片段(流式 text) */
147
157
  addText(text, outputTokens, turn) {
@@ -207,7 +207,28 @@ export class MessageBridge {
207
207
  }
208
208
  });
209
209
  }
210
- /** 自定义消息快速路径:拦截 menu.query 等自定义 payload,返回 true 表示已处理 */
210
+ // ── Menu Protocol ──
211
+ static MENU_NAME_MAP = {
212
+ pwd: '/pwd',
213
+ session: '/session',
214
+ baseagent: '/baseagent',
215
+ model: '/model',
216
+ effort: '/effort',
217
+ chatmode: '/chatmode',
218
+ dispatch: '/dispatch',
219
+ permission: '/perm',
220
+ activity: '/activity',
221
+ system: '/system',
222
+ };
223
+ resolveCmd(name, cmd) {
224
+ if (cmd)
225
+ return cmd;
226
+ const mapped = MessageBridge.MENU_NAME_MAP[name];
227
+ if (!mapped)
228
+ throw { code: 'UNKNOWN_NAME', message: `未知操作: ${name}` };
229
+ return mapped;
230
+ }
231
+ /** 自定义消息快速路径:拦截 menu.* 协议 */
211
232
  async handleCustomPayload(content, channel, msg, sendReply, adapter) {
212
233
  let parsed;
213
234
  try {
@@ -218,30 +239,108 @@ export class MessageBridge {
218
239
  }
219
240
  if (!parsed || typeof parsed !== 'object' || !parsed.type)
220
241
  return false;
221
- if (parsed.type === 'menu.query') {
222
- if (parsed.cmd && (parsed.mode === 'query' || parsed.mode === 'update')) {
223
- // exec 模式:查询状态或执行命令
224
- const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
225
- const base = { type: 'menu.response', cmd: parsed.cmd };
226
- const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
227
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
228
- }
229
- else if (parsed.cmd) {
230
- // 动态子菜单查询
231
- const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
232
- const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
233
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
234
- }
235
- else {
236
- // 全量菜单
237
- const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
238
- const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
239
- const response = JSON.stringify({ type: 'menu.response', items });
240
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
241
- }
242
- return true;
242
+ switch (parsed.type) {
243
+ case 'menu.list':
244
+ await this.handleMenuList(parsed, channel, msg, adapter, sendReply);
245
+ return true;
246
+ case 'menu.query':
247
+ await this.handleMenuQuery(parsed, channel, msg, adapter, sendReply);
248
+ return true;
249
+ case 'menu.options':
250
+ await this.handleMenuOptions(parsed, channel, msg, adapter, sendReply);
251
+ return true;
252
+ case 'menu.update':
253
+ await this.handleMenuUpdate(parsed, channel, msg, adapter, sendReply);
254
+ return true;
255
+ case 'menu.action':
256
+ await this.handleMenuAction(parsed, channel, msg, adapter, sendReply);
257
+ return true;
258
+ default:
259
+ return false;
260
+ }
261
+ }
262
+ async handleMenuList(req, channel, msg, adapter, sendReply) {
263
+ const { id } = req;
264
+ try {
265
+ const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
266
+ const data = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
267
+ await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, data }, sendReply);
268
+ }
269
+ catch (err) {
270
+ await this.sendMenuResponse(adapter, channel, msg.channelId, {
271
+ type: 'menu.response', id,
272
+ error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
273
+ }, sendReply);
274
+ }
275
+ }
276
+ async handleMenuQuery(req, channel, msg, adapter, sendReply) {
277
+ const { id, name, cmd } = req;
278
+ try {
279
+ const resolvedCmd = this.resolveCmd(name, cmd);
280
+ const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId);
281
+ if ('error' in result)
282
+ throw { code: result.code || 'EXEC_FAILED', message: result.error };
283
+ await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
284
+ }
285
+ catch (err) {
286
+ await this.sendMenuResponse(adapter, channel, msg.channelId, {
287
+ type: 'menu.response', id, name,
288
+ error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
289
+ }, sendReply);
290
+ }
291
+ }
292
+ async handleMenuOptions(req, channel, msg, adapter, sendReply) {
293
+ const { id, name, cmd } = req;
294
+ try {
295
+ const resolvedCmd = this.resolveCmd(name, cmd);
296
+ const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId) ?? [];
297
+ await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data }, sendReply);
298
+ }
299
+ catch (err) {
300
+ await this.sendMenuResponse(adapter, channel, msg.channelId, {
301
+ type: 'menu.response', id, name,
302
+ error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
303
+ }, sendReply);
304
+ }
305
+ }
306
+ async handleMenuUpdate(req, channel, msg, adapter, sendReply) {
307
+ const { id, name, cmd, value } = req;
308
+ try {
309
+ if (!value)
310
+ throw { code: 'MISSING_VALUE', message: '缺少 value 参数' };
311
+ const resolvedCmd = this.resolveCmd(name, cmd);
312
+ const result = await this.cmdHandler.execMenuUpdate(resolvedCmd, value, channel, msg.channelId, msg.peerId);
313
+ if ('error' in result)
314
+ throw { code: result.code || 'EXEC_FAILED', message: result.error };
315
+ await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
316
+ }
317
+ catch (err) {
318
+ await this.sendMenuResponse(adapter, channel, msg.channelId, {
319
+ type: 'menu.response', id, name,
320
+ error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
321
+ }, sendReply);
322
+ }
323
+ }
324
+ async handleMenuAction(req, channel, msg, adapter, sendReply) {
325
+ const { id, name, cmd, action, args } = req;
326
+ try {
327
+ if (!action)
328
+ throw { code: 'MISSING_VALUE', message: '缺少 action 参数' };
329
+ const resolvedCmd = this.resolveCmd(name, cmd);
330
+ const result = await this.cmdHandler.execMenuAction(resolvedCmd, action, args, channel, msg.channelId, msg.peerId);
331
+ if ('error' in result)
332
+ throw { code: result.code || 'EXEC_FAILED', message: result.error };
333
+ await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
243
334
  }
244
- return false;
335
+ catch (err) {
336
+ await this.sendMenuResponse(adapter, channel, msg.channelId, {
337
+ type: 'menu.response', id, name,
338
+ error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
339
+ }, sendReply);
340
+ }
341
+ }
342
+ async sendMenuResponse(adapter, channel, channelId, response, sendReply) {
343
+ await this.sendCustomResponse(adapter, channel, channelId, JSON.stringify(response), sendReply);
245
344
  }
246
345
  /** menu.query 响应:优先走 adapter.send(custom),降级 sendReply */
247
346
  async sendCustomResponse(adapter, channel, channelId, response, sendReply) {
@@ -12,6 +12,21 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
12
12
  import { renderKitSections } from '../../agents/kit-renderer.js';
13
13
  import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
14
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
+ function getContextTooLongHint(agent) {
16
+ if (canCompactAgent(agent)) {
17
+ return '上下文过长,请精简提问或使用 /compact 压缩上下文';
18
+ }
19
+ return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
20
+ }
21
+ function getContextCompactFailedHint(agent) {
22
+ if (canCompactAgent(agent)) {
23
+ return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
24
+ }
25
+ return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
26
+ }
27
+ function canCompactAgent(agent) {
28
+ return hasCompact(agent) && agent.capabilities?.compact !== false;
29
+ }
15
30
  /**
16
31
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
17
32
  *
@@ -183,7 +198,7 @@ export class MessageProcessor {
183
198
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
184
199
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
185
200
  '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
186
- '/aid', '/agentmd', '/upgrade',
201
+ '/aid', '/upgrade', '/evolagent',
187
202
  ];
188
203
  /** 判断消息内容是否为已知命令 */
189
204
  isKnownCommand(content) {
@@ -549,7 +564,7 @@ export class MessageProcessor {
549
564
  venueUid: undefined,
550
565
  project: path.basename(absoluteProjectPath),
551
566
  sessionName: session.name || undefined,
552
- sessionMode: isProactive ? 'proactive' : 'interactive',
567
+ chatmode: isProactive ? 'proactive' : 'interactive',
553
568
  readonly: session.metadata?.permissionMode === 'readonly',
554
569
  canSendFile: !isProactive && currentCanSend,
555
570
  capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
@@ -571,7 +586,7 @@ export class MessageProcessor {
571
586
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
572
587
  agent.registerStream(streamKey, stream);
573
588
  streamRegistered = true;
574
- streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
589
+ streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
575
590
  break; // 成功,跳出重试循环
576
591
  }
577
592
  catch (retryError) {
@@ -591,7 +606,7 @@ export class MessageProcessor {
591
606
  }
592
607
  }
593
608
  catch (error) {
594
- if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
609
+ if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
595
610
  // 尝试 compact 压缩会话
596
611
  renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
597
612
  await renderer.flush();
@@ -601,7 +616,7 @@ export class MessageProcessor {
601
616
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
602
617
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
603
618
  agent.registerStream(streamKey, retryStream);
604
- streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
619
+ streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
605
620
  }
606
621
  else {
607
622
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -615,7 +630,7 @@ export class MessageProcessor {
615
630
  // 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
616
631
  const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
617
632
  const errorsText = streamResult.errors?.join(' ') || '';
618
- const isPromptTooLong = streamResult.isError && session.agentSessionId && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
633
+ const isPromptTooLong = streamResult.isError && session.agentSessionId && canCompactAgent(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
619
634
  contextTooLongPattern.test(streamResult.lastReplyText) ||
620
635
  contextTooLongPattern.test(errorsText) ||
621
636
  contextTooLongPattern.test(streamResult.fullText));
@@ -627,7 +642,17 @@ export class MessageProcessor {
627
642
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
628
643
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
629
644
  agent.registerStream(streamKey, retryStream);
630
- streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
645
+ streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
646
+ // 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
647
+ const retryErrorsText = streamResult.errors?.join(' ') || '';
648
+ const retryStillTooLong = streamResult.isError && (streamResult.terminalReason === 'prompt_too_long' ||
649
+ contextTooLongPattern.test(streamResult.lastReplyText) ||
650
+ contextTooLongPattern.test(retryErrorsText) ||
651
+ contextTooLongPattern.test(streamResult.fullText));
652
+ if (retryStillTooLong) {
653
+ renderer.stripContextError(contextTooLongPattern);
654
+ renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
655
+ }
631
656
  }
632
657
  else {
633
658
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -638,7 +663,7 @@ export class MessageProcessor {
638
663
  contextTooLongPattern.test(errorsText) ||
639
664
  contextTooLongPattern.test(streamResult.fullText))) {
640
665
  // 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
641
- renderer.addNotice('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
666
+ renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
642
667
  }
643
668
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
644
669
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
@@ -927,7 +952,7 @@ export class MessageProcessor {
927
952
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
928
953
  let sendOpts;
929
954
  try {
930
- await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId);
955
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
931
956
  sendOpts = this.getReplyContext(message);
932
957
  }
933
958
  catch { }
@@ -964,7 +989,7 @@ export class MessageProcessor {
964
989
  : path.resolve(process.cwd(), session.projectPath);
965
990
  return { session, absoluteProjectPath };
966
991
  }
967
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
992
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
968
993
  // 兜底纠正1:群聊强制 proactive
969
994
  if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
970
995
  logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
@@ -990,7 +1015,7 @@ export class MessageProcessor {
990
1015
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
991
1016
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
992
1017
  */
993
- async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
1018
+ async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
994
1019
  // Per-session agent name for stats bucketing
995
1020
  const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
996
1021
  let hasReceivedText = false;
@@ -1173,9 +1198,13 @@ export class MessageProcessor {
1173
1198
  if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
1174
1199
  const errorSummary = event.errors?.join('; ') || '任务执行失败';
1175
1200
  // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
1176
- const userFriendlyMessage = event.terminalReason
1177
- ? getErrorMessage(null, event.terminalReason, false)
1178
- : errorSummary;
1201
+ const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
1202
+ ? getContextTooLongHint(agent)
1203
+ : event.terminalReason === 'context_compact_failed'
1204
+ ? getContextCompactFailedHint(agent)
1205
+ : event.terminalReason
1206
+ ? getErrorMessage(null, event.terminalReason, false)
1207
+ : errorSummary;
1179
1208
  renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
1180
1209
  }
1181
1210
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示