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.
- package/CHANGELOG.md +428 -0
- package/README.md +3 -7
- package/SKILLS.md +311 -0
- package/dist/agents/claude-runner.js +1 -1
- package/dist/agents/codex-runner.js +75 -19
- package/dist/agents/gemini-runner.js +0 -2
- package/dist/agents/kit-renderer.js +59 -10
- package/dist/aun/aid/agentmd.js +50 -27
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +15 -2
- package/dist/aun/msg/upload.js +57 -18
- package/dist/aun/rpc/connection.js +3 -0
- package/dist/channels/aun.js +122 -48
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +5 -4
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +142 -40
- package/dist/cli/index.js +103 -58
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +55 -26
- package/dist/cli/watch-msg.js +3 -1
- package/dist/config-store.js +22 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +626 -538
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +35 -4
- package/dist/core/message/im-renderer.js +14 -4
- package/dist/core/message/message-bridge.js +149 -25
- package/dist/core/message/message-processor.js +45 -38
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +188 -42
- package/dist/index.js +15 -17
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/kits/docs/INDEX.md +6 -0
- package/kits/eck_manifest.json +3 -3
- package/kits/rules/02-navigation.md +1 -0
- package/kits/rules/06-channel.md +2 -18
- package/kits/templates/system-fragments/baseagent.md +2 -2
- package/kits/templates/system-fragments/channel.md +18 -9
- package/kits/templates/system-fragments/eckruntime.md +14 -0
- package/kits/templates/system-fragments/identity.md +5 -6
- package/kits/templates/system-fragments/relation.md +7 -5
- package/kits/templates/system-fragments/venue.md +2 -3
- package/package.json +5 -2
- 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 (`<
|
|
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
|
|
232
|
-
const
|
|
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
|
-
|
|
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();
|
package/dist/core/evolagent.js
CHANGED
|
@@ -62,11 +62,11 @@ export class EvolAgent {
|
|
|
62
62
|
}
|
|
63
63
|
// ── Channels ──────────────────────────────────────────────────────────
|
|
64
64
|
/**
|
|
65
|
-
* effective channel key:`<
|
|
66
|
-
*
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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', '/
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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(
|
|
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,
|
|
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
|
-
//
|
|
1165
|
-
//
|
|
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
|
-
?
|
|
1200
|
-
:
|
|
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
|