evolclaw 3.1.1 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +3 -7
  3. package/SKILLS.md +311 -0
  4. package/dist/agents/claude-runner.js +1 -1
  5. package/dist/agents/codex-runner.js +75 -19
  6. package/dist/agents/gemini-runner.js +0 -2
  7. package/dist/agents/kit-renderer.js +59 -10
  8. package/dist/aun/aid/agentmd.js +50 -27
  9. package/dist/aun/aid/client.js +5 -11
  10. package/dist/aun/aid/identity.js +32 -13
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/msg/group.js +1 -0
  13. package/dist/aun/msg/p2p.js +15 -2
  14. package/dist/aun/msg/upload.js +57 -18
  15. package/dist/aun/rpc/connection.js +3 -0
  16. package/dist/channels/aun.js +122 -48
  17. package/dist/channels/dingtalk.js +1 -0
  18. package/dist/channels/feishu.js +5 -4
  19. package/dist/channels/qqbot.js +1 -0
  20. package/dist/channels/wechat.js +1 -0
  21. package/dist/channels/wecom.js +1 -0
  22. package/dist/cli/agent.js +142 -40
  23. package/dist/cli/index.js +103 -58
  24. package/dist/cli/init-channel.js +4 -2
  25. package/dist/cli/init.js +55 -26
  26. package/dist/cli/watch-msg.js +3 -1
  27. package/dist/config-store.js +22 -1
  28. package/dist/core/channel-loader.js +4 -4
  29. package/dist/core/command-handler.js +626 -538
  30. package/dist/core/evolagent-registry.js +45 -9
  31. package/dist/core/evolagent.js +35 -4
  32. package/dist/core/message/im-renderer.js +14 -4
  33. package/dist/core/message/message-bridge.js +149 -25
  34. package/dist/core/message/message-processor.js +45 -38
  35. package/dist/core/session/session-fs-store.js +23 -0
  36. package/dist/core/session/session-manager.js +188 -42
  37. package/dist/index.js +15 -17
  38. package/dist/paths.js +35 -0
  39. package/dist/utils/cross-platform.js +2 -1
  40. package/kits/docs/INDEX.md +6 -0
  41. package/kits/eck_manifest.json +3 -3
  42. package/kits/rules/02-navigation.md +1 -0
  43. package/kits/rules/06-channel.md +2 -18
  44. package/kits/templates/system-fragments/baseagent.md +2 -2
  45. package/kits/templates/system-fragments/channel.md +18 -9
  46. package/kits/templates/system-fragments/eckruntime.md +14 -0
  47. package/kits/templates/system-fragments/identity.md +5 -6
  48. package/kits/templates/system-fragments/relation.md +7 -5
  49. package/kits/templates/system-fragments/venue.md +2 -3
  50. package/package.json +5 -2
  51. package/kits/templates/system-fragments/runtime.md +0 -19
@@ -51,7 +51,7 @@ export function detectDuplicates(agents) {
51
51
  export class EvolAgentRegistry {
52
52
  _agentsDir;
53
53
  agents = new Map();
54
- /** channel key (`<aid>#<type>#<name>`) → agent aid */
54
+ /** channel key (`<type>#<selfPeerId>#<name>`) → agent aid */
55
55
  channelIndex = new Map();
56
56
  /** 启动期被 ConfigStore 跳过的目录(命名非法 / 缺 config.json / 校验失败等) */
57
57
  skipped = [];
@@ -199,6 +199,12 @@ export class EvolAgentRegistry {
199
199
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${errs.join('; ')}`);
200
200
  return null;
201
201
  }
202
+ // Channel fingerprint 冲突检测(防止新 agent 复用已有 agent 的凭证)
203
+ const conflict = this.checkConflictForReload(raw, aid);
204
+ if (conflict) {
205
+ logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
206
+ return null;
207
+ }
202
208
  const defaults = loadDefaults();
203
209
  const merged = mergeForAgent(raw, defaults);
204
210
  const agent = new EvolAgent(raw, merged);
@@ -224,12 +230,47 @@ export class EvolAgentRegistry {
224
230
  throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
225
231
  const defaults = loadDefaults();
226
232
  const merged = mergeForAgent(raw, defaults);
233
+ // ── disabled → enabled 转换:需要完整启动流程 ──
234
+ if (oldAgent.status === 'disabled' && raw.enabled !== false) {
235
+ oldAgent.swapConfig(raw, merged);
236
+ const hotLoad = globalThis.__evolclaw_hotLoadAgent;
237
+ if (!hotLoad)
238
+ throw new Error(`Cannot enable agent "${aidOrName}": hot-load handler not initialized`);
239
+ // 从 registry 中移除旧的 disabled 实例,hotLoad 会重新创建
240
+ this.agents.delete(oldAgent.aid);
241
+ this.channelIndex.clear();
242
+ this.buildChannelIndex();
243
+ await hotLoad(oldAgent.aid);
244
+ logger.info(`[Reload] Agent "${aidOrName}" transitioned from disabled → enabled (full startup)`);
245
+ return;
246
+ }
247
+ // ── enabled → disabled 转换:断开所有 channel ──
248
+ if (oldAgent.status !== 'disabled' && raw.enabled === false) {
249
+ for (const ch of oldAgent.channelInstanceNames()) {
250
+ try {
251
+ await hooks.drainChannel(ch);
252
+ }
253
+ catch { }
254
+ try {
255
+ await hooks.disconnectChannel(ch);
256
+ }
257
+ catch { }
258
+ }
259
+ oldAgent.swapConfig(raw, merged);
260
+ oldAgent.status = 'disabled';
261
+ this.channelIndex.clear();
262
+ this.buildChannelIndex();
263
+ logger.info(`[Reload] Agent "${aidOrName}" disabled`);
264
+ return;
265
+ }
227
266
  const conflict = this.checkConflictForReload(raw, oldAgent.aid);
228
267
  if (conflict)
229
268
  throw new Error(`Channel conflict: ${conflict}`);
230
269
  const oldChannels = new Set(oldAgent.channelInstanceNames());
231
- // 计算新 channel keys(用 EvolAgent 的格式化)
232
- const newChannels = new Set(raw.channels.map(c => oldAgent.effectiveChannelName(c.type, c.name)));
270
+ // 计算新 channel keys:隐式 AUN + 显式非 AUN channels(与 channelInstanceNames 逻辑一致)
271
+ const aunKey = oldAgent.effectiveChannelName('aun', 'main');
272
+ const otherKeys = raw.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
273
+ const newChannels = new Set([aunKey, ...otherKeys]);
233
274
  const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
234
275
  const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
235
276
  const kept = [...oldChannels].filter(c => newChannels.has(c));
@@ -264,12 +305,7 @@ export class EvolAgentRegistry {
264
305
  addedSuccessfully.push(ch);
265
306
  }
266
307
  // truly kept 的 adapter 实例已经在 oldAgent.channels 里,无需迁移
267
- if (oldAgent.status === 'error' || oldAgent.status === 'disabled') {
268
- // 保持原态——swap 不改 status
269
- }
270
- else {
271
- oldAgent.status = 'running';
272
- }
308
+ oldAgent.status = 'running';
273
309
  // 重启触发器调度器(如果已初始化)
274
310
  if (oldAgent.triggerScheduler) {
275
311
  oldAgent.triggerScheduler.stop();
@@ -62,11 +62,11 @@ export class EvolAgent {
62
62
  }
63
63
  // ── Channels ──────────────────────────────────────────────────────────
64
64
  /**
65
- * effective channel key:`<aid>#<type>#<name>`。AUN 实例一个 agent 只有一条;
66
- * 其它类型靠 name 区分。
65
+ * effective channel key:`<type>#<urlEncode(selfPeerId)>#<name>`。
66
+ * AUN channel 的 selfPeerId 是 agent.aid,name 固定为 'main'。
67
67
  */
68
68
  effectiveChannelName(type, rawName) {
69
- return formatChannelKey({ aid: this.aid, type, name: rawName });
69
+ return formatChannelKey({ type, selfPeerId: this.aid, name: rawName });
70
70
  }
71
71
  channelInstanceNames() {
72
72
  // AUN channel 隐式存在(从 agent.aid 派生),不需要在 channels[] 里声明
@@ -97,7 +97,7 @@ export class EvolAgent {
97
97
  */
98
98
  isAunChannelKey(channelKey) {
99
99
  const parsed = tryParseChannelKey(channelKey);
100
- return parsed?.type === 'aun' && parsed.aid === this.aid;
100
+ return parsed?.type === 'aun' && parsed.selfPeerId === this.aid;
101
101
  }
102
102
  getOwner(channelKey) {
103
103
  if (this.isAunChannelKey(channelKey)) {
@@ -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) {
@@ -201,10 +211,10 @@ export class IMRenderer {
201
211
  call_id: callId || this.synthCallId(),
202
212
  name,
203
213
  ok,
204
- result,
205
- error,
206
- duration_ms: durationMs,
207
- text: descText,
214
+ ...(result !== undefined && { result }),
215
+ ...(error !== undefined && { error }),
216
+ ...(durationMs !== undefined && { duration_ms: durationMs }),
217
+ ...(descText !== undefined && { text: descText }),
208
218
  });
209
219
  this.messageTimestamps.push(Date.now());
210
220
  if (this.diagEnabled)
@@ -3,6 +3,8 @@ import { logger } from '../../utils/logger.js';
3
3
  import { StreamDebouncer } from './stream-debouncer.js';
4
4
  import { appendMessageLog, buildInboundEntry } from './message-log.js';
5
5
  import { buildEnvelope } from './message-processor.js';
6
+ import { chatDirPath } from '../session/session-fs-store.js';
7
+ import { resolvePaths } from '../../paths.js';
6
8
  /**
7
9
  * MessageBridge — Channel 与 Core 之间的消息桥梁
8
10
  *
@@ -74,8 +76,31 @@ export class MessageBridge {
74
76
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
75
77
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
76
78
  const cmdContent = contentForCmd || content;
77
- if (this.cmdHandler.isCommand(cmdContent)) {
79
+ const isCmd = this.cmdHandler.isCommand(cmdContent);
80
+ if (isCmd) {
78
81
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
82
+ // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
83
+ try {
84
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfId);
85
+ const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
86
+ const inboundChatmode = msg.replyContext?.metadata?.chatmode;
87
+ appendMessageLog(chatDir, buildInboundEntry({
88
+ from: msg.peerId || 'unknown',
89
+ to: msg.selfId || 'self',
90
+ chatType: msg.chatType || 'private',
91
+ groupId: msg.groupId ?? null,
92
+ msgId: msg.messageId ?? null,
93
+ content,
94
+ replyTo: msg.replyContext?.replyToMessageId ?? null,
95
+ permMode: null,
96
+ timestamp: Date.now(),
97
+ encrypt: inboundEncrypt,
98
+ chatmode: inboundChatmode,
99
+ }));
100
+ }
101
+ catch (e) {
102
+ logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
103
+ }
79
104
  }
80
105
  if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
81
106
  logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
@@ -182,7 +207,28 @@ export class MessageBridge {
182
207
  }
183
208
  });
184
209
  }
185
- /** 自定义消息快速路径:拦截 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.* 协议 */
186
232
  async handleCustomPayload(content, channel, msg, sendReply, adapter) {
187
233
  let parsed;
188
234
  try {
@@ -193,30 +239,108 @@ export class MessageBridge {
193
239
  }
194
240
  if (!parsed || typeof parsed !== 'object' || !parsed.type)
195
241
  return false;
196
- if (parsed.type === 'menu.query') {
197
- if (parsed.cmd && (parsed.mode === 'query' || parsed.mode === 'update')) {
198
- // exec 模式:查询状态或执行命令
199
- const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
200
- const base = { type: 'menu.response', cmd: parsed.cmd };
201
- const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
202
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
203
- }
204
- else if (parsed.cmd) {
205
- // 动态子菜单查询
206
- const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
207
- const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
208
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
209
- }
210
- else {
211
- // 全量菜单
212
- const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
213
- const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
214
- const response = JSON.stringify({ type: 'menu.response', items });
215
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
216
- }
217
- 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);
218
274
  }
219
- return false;
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);
334
+ }
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);
220
344
  }
221
345
  /** menu.query 响应:优先走 adapter.send(custom),降级 sendReply */
222
346
  async sendCustomResponse(adapter, channel, channelId, response, sendReply) {
@@ -2,7 +2,6 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import crypto from 'crypto';
4
4
  import { hasCompact } from '../../agents/claude-runner.js';
5
- import { appendMessageLog, buildOutboundEntry } from './message-log.js';
6
5
  import { IMRenderer } from './im-renderer.js';
7
6
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
8
7
  import { logger } from '../../utils/logger.js';
@@ -13,6 +12,21 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
13
12
  import { renderKitSections } from '../../agents/kit-renderer.js';
14
13
  import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
15
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
+ }
16
30
  /**
17
31
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
18
32
  *
@@ -184,7 +198,7 @@ export class MessageProcessor {
184
198
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
185
199
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
186
200
  '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
187
- '/aid', '/agentmd', '/upgrade',
201
+ '/aid', '/upgrade', '/evolagent',
188
202
  ];
189
203
  /** 判断消息内容是否为已知命令 */
190
204
  isKnownCommand(content) {
@@ -550,7 +564,7 @@ export class MessageProcessor {
550
564
  venueUid: undefined,
551
565
  project: path.basename(absoluteProjectPath),
552
566
  sessionName: session.name || undefined,
553
- sessionMode: isProactive ? 'proactive' : 'interactive',
567
+ chatmode: isProactive ? 'proactive' : 'interactive',
554
568
  readonly: session.metadata?.permissionMode === 'readonly',
555
569
  canSendFile: !isProactive && currentCanSend,
556
570
  capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
@@ -572,7 +586,7 @@ export class MessageProcessor {
572
586
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
573
587
  agent.registerStream(streamKey, stream);
574
588
  streamRegistered = true;
575
- streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
589
+ streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
576
590
  break; // 成功,跳出重试循环
577
591
  }
578
592
  catch (retryError) {
@@ -592,7 +606,7 @@ export class MessageProcessor {
592
606
  }
593
607
  }
594
608
  catch (error) {
595
- if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
609
+ if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
596
610
  // 尝试 compact 压缩会话
597
611
  renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
598
612
  await renderer.flush();
@@ -602,7 +616,7 @@ export class MessageProcessor {
602
616
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
603
617
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
604
618
  agent.registerStream(streamKey, retryStream);
605
- streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
619
+ streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
606
620
  }
607
621
  else {
608
622
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -616,7 +630,7 @@ export class MessageProcessor {
616
630
  // 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
617
631
  const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
618
632
  const errorsText = streamResult.errors?.join(' ') || '';
619
- 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' ||
620
634
  contextTooLongPattern.test(streamResult.lastReplyText) ||
621
635
  contextTooLongPattern.test(errorsText) ||
622
636
  contextTooLongPattern.test(streamResult.fullText));
@@ -628,7 +642,17 @@ export class MessageProcessor {
628
642
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
629
643
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
630
644
  agent.registerStream(streamKey, retryStream);
631
- 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
+ }
632
656
  }
633
657
  else {
634
658
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -639,7 +663,7 @@ export class MessageProcessor {
639
663
  contextTooLongPattern.test(errorsText) ||
640
664
  contextTooLongPattern.test(streamResult.fullText))) {
641
665
  // 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
642
- renderer.addNotice('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
666
+ renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
643
667
  }
644
668
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
645
669
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
@@ -928,7 +952,7 @@ export class MessageProcessor {
928
952
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
929
953
  let sendOpts;
930
954
  try {
931
- 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);
932
956
  sendOpts = this.getReplyContext(message);
933
957
  }
934
958
  catch { }
@@ -965,7 +989,7 @@ export class MessageProcessor {
965
989
  : path.resolve(process.cwd(), session.projectPath);
966
990
  return { session, absoluteProjectPath };
967
991
  }
968
- 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);
969
993
  // 兜底纠正1:群聊强制 proactive
970
994
  if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
971
995
  logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
@@ -991,7 +1015,7 @@ export class MessageProcessor {
991
1015
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
992
1016
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
993
1017
  */
994
- async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
1018
+ async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
995
1019
  // Per-session agent name for stats bucketing
996
1020
  const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
997
1021
  let hasReceivedText = false;
@@ -1161,29 +1185,8 @@ export class MessageProcessor {
1161
1185
  }
1162
1186
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1163
1187
  completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1164
- // proactive 模式:每轮 LLM 调用完成后写一条 thought 到 messages.jsonl
1165
- // 这样 thought = LLM 调用轮数,而不是 chunk 数
1166
- if (session.sessionMode === 'proactive' && lastReplyText) {
1167
- try {
1168
- const chatDir = this.sessionManager.getChatDir(session);
1169
- const sessionEncrypt = this.sessionManager.getSessionEncrypt(session.id);
1170
- appendMessageLog(chatDir, buildOutboundEntry({
1171
- from: session.selfId || 'self',
1172
- to: session.metadata?.peerId ?? session.channelId,
1173
- chatType: (session.chatType ?? 'private'),
1174
- groupId: session.metadata?.groupId ?? null,
1175
- msgId: `thought-${session.id}-${Date.now()}`,
1176
- content: lastReplyText,
1177
- agent: session.agentId || null,
1178
- model: null,
1179
- durationMs: null,
1180
- encrypt: sessionEncrypt ?? undefined,
1181
- chatmode: 'proactive',
1182
- msgType: 'thought',
1183
- }));
1184
- }
1185
- catch { }
1186
- }
1188
+ // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
1189
+ // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
1187
1190
  // 失败且无前置错误输出:显示 errors 摘要
1188
1191
  // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
1189
1192
  // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
@@ -1195,9 +1198,13 @@ export class MessageProcessor {
1195
1198
  if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
1196
1199
  const errorSummary = event.errors?.join('; ') || '任务执行失败';
1197
1200
  // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
1198
- const userFriendlyMessage = event.terminalReason
1199
- ? getErrorMessage(null, event.terminalReason, false)
1200
- : 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;
1201
1208
  renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
1202
1209
  }
1203
1210
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
@@ -146,6 +146,29 @@ export function scanChatDirs(sessionsDir) {
146
146
  if (!typeEntry.isDirectory())
147
147
  continue;
148
148
  const channelType = typeEntry.name;
149
+ // 包含 '#' 的目录是旧 channelKey 格式(如 'aun#dddd.agentid.pub#main'),
150
+ // 按通用 channel 布局扫描(sessionsDir/{channelKey}/{encodedChannelId}/),保持兼容
151
+ if (channelType.includes('#')) {
152
+ const typeDir = path.join(sessionsDir, channelType);
153
+ let chatEntries;
154
+ try {
155
+ chatEntries = fs.readdirSync(typeDir, { withFileTypes: true });
156
+ }
157
+ catch {
158
+ continue;
159
+ }
160
+ for (const chatEntry of chatEntries) {
161
+ if (!chatEntry.isDirectory())
162
+ continue;
163
+ results.push({
164
+ channelType,
165
+ selfId: null,
166
+ channelId: decodeSegment(chatEntry.name),
167
+ dirPath: path.join(typeDir, chatEntry.name),
168
+ });
169
+ }
170
+ continue;
171
+ }
149
172
  const typeDir = path.join(sessionsDir, channelType);
150
173
  if (channelType === 'aun') {
151
174
  // aun 下还有一层 selfId