evolclaw 2.6.0 → 2.6.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/channels/aun.js +127 -17
- package/dist/channels/feishu.js +4 -3
- package/dist/cli.js +104 -7
- package/dist/config.js +1 -1
- package/dist/core/command-handler.js +111 -8
- package/dist/core/message/message-processor.js +64 -50
- package/dist/core/message/message-queue.js +10 -2
- package/dist/core/message/stream-debouncer.js +9 -1
- package/dist/core/message/thought-emitter.js +153 -0
- package/dist/index.js +51 -30
- package/dist/templates/skills.md +5 -3
- package/dist/utils/init-channel.js +78 -5
- package/dist/utils/init.js +3 -3
- package/dist/utils/upgrade.js +100 -0
- package/package.json +3 -3
|
@@ -103,7 +103,7 @@ function formatIdleTime(ms) {
|
|
|
103
103
|
return '刚刚';
|
|
104
104
|
}
|
|
105
105
|
// 支持的命令列表
|
|
106
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/agentmd', '/chatmode'];
|
|
106
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode'];
|
|
107
107
|
// 命令别名映射
|
|
108
108
|
const aliases = {
|
|
109
109
|
'/p': '/project',
|
|
@@ -127,6 +127,7 @@ export class CommandHandler {
|
|
|
127
127
|
permissionGateway;
|
|
128
128
|
interactionRouter;
|
|
129
129
|
statsCollector;
|
|
130
|
+
hotLoadChannel;
|
|
130
131
|
agentMap;
|
|
131
132
|
defaultAgentId;
|
|
132
133
|
/** 按 agentId 获取 agent,回退到默认 */
|
|
@@ -266,6 +267,9 @@ export class CommandHandler {
|
|
|
266
267
|
setMessageQueue(messageQueue) {
|
|
267
268
|
this.messageQueue = messageQueue;
|
|
268
269
|
}
|
|
270
|
+
setHotLoadChannel(fn) {
|
|
271
|
+
this.hotLoadChannel = fn;
|
|
272
|
+
}
|
|
269
273
|
setPermissionGateway(gateway) {
|
|
270
274
|
this.permissionGateway = gateway;
|
|
271
275
|
}
|
|
@@ -407,6 +411,10 @@ export class CommandHandler {
|
|
|
407
411
|
] : []),
|
|
408
412
|
...(isOwner ? [
|
|
409
413
|
{ cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
414
|
+
{ cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
|
|
415
|
+
{ value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
|
|
416
|
+
{ value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
|
|
417
|
+
] } },
|
|
410
418
|
{ cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
|
|
411
419
|
{ value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
|
|
412
420
|
{ value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
|
|
@@ -517,10 +525,10 @@ export class CommandHandler {
|
|
|
517
525
|
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
518
526
|
const activeChatType = activeSession?.chatType || 'private';
|
|
519
527
|
if (normalizedContent.startsWith('/')) {
|
|
520
|
-
const guestGroupCommands = ['/status', '/help', '/check'];
|
|
528
|
+
const guestGroupCommands = ['/status', '/help', '/check', '/chatmode'];
|
|
521
529
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
522
530
|
? guestGroupCommands
|
|
523
|
-
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
|
|
531
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check', '/chatmode'];
|
|
524
532
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
525
533
|
if (!isUserCommand && !isAdmin) {
|
|
526
534
|
return activeChatType === 'group'
|
|
@@ -634,6 +642,7 @@ export class CommandHandler {
|
|
|
634
642
|
...(isOwner ? [
|
|
635
643
|
' /restart - 重启服务',
|
|
636
644
|
' /file [channel] <path> - 发送项目内文件',
|
|
645
|
+
' /aid [list|new <aid>] - AID 管理',
|
|
637
646
|
' /agentmd [put|set <内容>] - 管理 agent.md',
|
|
638
647
|
] : []),
|
|
639
648
|
'',
|
|
@@ -1139,6 +1148,94 @@ export class CommandHandler {
|
|
|
1139
1148
|
}
|
|
1140
1149
|
return `✓ 推理强度: ${newEffort}`;
|
|
1141
1150
|
}
|
|
1151
|
+
// /aid 命令:AID 管理(list / new)
|
|
1152
|
+
if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
|
|
1153
|
+
if (!isOwner)
|
|
1154
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1155
|
+
const adapter = this.adapters.get(channel);
|
|
1156
|
+
const channelType = this.channelTypeMap.get(channel);
|
|
1157
|
+
if (channelType !== 'aun')
|
|
1158
|
+
return '❌ 此命令仅在 AUN 通道中可用';
|
|
1159
|
+
const arg = normalizedContent.slice(4).trim();
|
|
1160
|
+
// /aid 或 /aid list — 列出所有 AUN 实例
|
|
1161
|
+
if (!arg || arg === 'list') {
|
|
1162
|
+
const { normalizeChannelInstances } = await import('../config.js');
|
|
1163
|
+
const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
|
|
1164
|
+
if (instances.length === 0)
|
|
1165
|
+
return '暂无 AUN 实例';
|
|
1166
|
+
const lines = ['AUN 实例:'];
|
|
1167
|
+
for (const inst of instances) {
|
|
1168
|
+
if (inst.enabled === false || !inst.aid)
|
|
1169
|
+
continue;
|
|
1170
|
+
const channelObj = this.channelObjects.get(inst.name);
|
|
1171
|
+
const status = channelObj?.getStatus?.();
|
|
1172
|
+
const connected = status?.connected ?? false;
|
|
1173
|
+
const icon = connected ? '✓' : '✗';
|
|
1174
|
+
const state = connected ? '已连接' : '未连接';
|
|
1175
|
+
lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
|
|
1176
|
+
}
|
|
1177
|
+
return lines.join('\n');
|
|
1178
|
+
}
|
|
1179
|
+
// /aid new <aid> — 创建新 AID 并热加载
|
|
1180
|
+
if (arg.startsWith('new ')) {
|
|
1181
|
+
const rawName = arg.slice(4).trim();
|
|
1182
|
+
if (!rawName)
|
|
1183
|
+
return '用法: /aid new <aid>\n例: /aid new reviewer';
|
|
1184
|
+
if (!this.hotLoadChannel)
|
|
1185
|
+
return '❌ 热加载未就绪';
|
|
1186
|
+
// Derive full AID: if no dots, append domain from current AID
|
|
1187
|
+
const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
|
|
1188
|
+
let fullAid = rawName;
|
|
1189
|
+
if (!rawName.includes('.')) {
|
|
1190
|
+
const domain = selfAid.split('.').slice(1).join('.');
|
|
1191
|
+
if (!domain)
|
|
1192
|
+
return '❌ 无法推导 AID 域(当前实例未连接)';
|
|
1193
|
+
fullAid = `${rawName}.${domain}`;
|
|
1194
|
+
}
|
|
1195
|
+
// Validate AID format
|
|
1196
|
+
const { isValidAid } = await import('../utils/init-channel.js');
|
|
1197
|
+
if (!isValidAid(fullAid))
|
|
1198
|
+
return `❌ 无效 AID 格式: ${fullAid}`;
|
|
1199
|
+
// Check instance name conflict
|
|
1200
|
+
const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
|
|
1201
|
+
const { normalizeChannelInstances } = await import('../config.js');
|
|
1202
|
+
const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
|
|
1203
|
+
if (existing.some(e => e.name === instName)) {
|
|
1204
|
+
return `❌ 实例名 "${instName}" 已存在`;
|
|
1205
|
+
}
|
|
1206
|
+
if (existing.some(e => e.aid === fullAid)) {
|
|
1207
|
+
return `❌ AID ${fullAid} 已在配置中`;
|
|
1208
|
+
}
|
|
1209
|
+
// Create AID (reuse init-channel.ts silent logic)
|
|
1210
|
+
try {
|
|
1211
|
+
const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
|
|
1212
|
+
const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
|
|
1213
|
+
// Resolve owner from current AUN instance config
|
|
1214
|
+
const owner = this.config.channels?.aun
|
|
1215
|
+
? (Array.isArray(this.config.channels.aun)
|
|
1216
|
+
? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
|
|
1217
|
+
: this.config.channels.aun.owner)
|
|
1218
|
+
: undefined;
|
|
1219
|
+
// Hot-load: build and register new channel instance BEFORE writing config
|
|
1220
|
+
const { AUNChannelPlugin } = await import('../channels/aun.js');
|
|
1221
|
+
const plugin = new AUNChannelPlugin();
|
|
1222
|
+
const tempConfig = JSON.parse(JSON.stringify(this.config));
|
|
1223
|
+
tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
|
|
1224
|
+
const newInstances = await plugin.createChannels(tempConfig);
|
|
1225
|
+
if (newInstances.length === 0)
|
|
1226
|
+
return '❌ 通道实例创建失败';
|
|
1227
|
+
await this.hotLoadChannel(newInstances[0]);
|
|
1228
|
+
// Write config only after successful hot-load
|
|
1229
|
+
appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
|
|
1230
|
+
const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
|
|
1231
|
+
return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
|
|
1232
|
+
}
|
|
1233
|
+
catch (e) {
|
|
1234
|
+
return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return '用法: /aid [list|new <aid>]';
|
|
1238
|
+
}
|
|
1142
1239
|
// /activity 命令:控制中间输出显示模式
|
|
1143
1240
|
if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
|
|
1144
1241
|
if (!isOwner)
|
|
@@ -1289,9 +1386,9 @@ export class CommandHandler {
|
|
|
1289
1386
|
return `✅ 中间输出模式: ${activityArg}(${label})`;
|
|
1290
1387
|
}
|
|
1291
1388
|
// /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
|
|
1389
|
+
// - 查看:所有人可用
|
|
1390
|
+
// - 设置:单聊任何角色可设置;群聊仅管理员可设置
|
|
1292
1391
|
if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
|
|
1293
|
-
if (!isAdmin)
|
|
1294
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1295
1392
|
if (!activeSession)
|
|
1296
1393
|
return '❌ 当前无活跃会话';
|
|
1297
1394
|
const lockedMode = getChannelSessionMode(this.config, channel);
|
|
@@ -1304,6 +1401,9 @@ export class CommandHandler {
|
|
|
1304
1401
|
if (arg !== 'interactive' && arg !== 'proactive') {
|
|
1305
1402
|
return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
|
|
1306
1403
|
}
|
|
1404
|
+
if (activeChatType === 'group' && !isAdmin) {
|
|
1405
|
+
return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
|
|
1406
|
+
}
|
|
1307
1407
|
if (lockedMode) {
|
|
1308
1408
|
return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
|
|
1309
1409
|
}
|
|
@@ -1454,15 +1554,18 @@ export class CommandHandler {
|
|
|
1454
1554
|
}
|
|
1455
1555
|
}
|
|
1456
1556
|
const lines = [];
|
|
1557
|
+
const sessionMode = session.sessionMode || 'interactive';
|
|
1558
|
+
const lockedMode = getChannelSessionMode(this.config, channel);
|
|
1559
|
+
const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
|
|
1457
1560
|
if (isAdmin) {
|
|
1458
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
1561
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
|
|
1459
1562
|
if (health.consecutiveErrors > 0) {
|
|
1460
1563
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1461
1564
|
}
|
|
1462
1565
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
1463
1566
|
}
|
|
1464
1567
|
else {
|
|
1465
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1568
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1466
1569
|
}
|
|
1467
1570
|
if (health.lastError) {
|
|
1468
1571
|
lines.push('');
|
|
@@ -2601,7 +2704,7 @@ export class CommandHandler {
|
|
|
2601
2704
|
static CTL_COMMANDS = [
|
|
2602
2705
|
'/help', '/status', '/check',
|
|
2603
2706
|
'/model', '/effort', '/perm',
|
|
2604
|
-
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd',
|
|
2707
|
+
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
|
|
2605
2708
|
];
|
|
2606
2709
|
/**
|
|
2607
2710
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import crypto from 'crypto';
|
|
3
4
|
import { hasCompact } from '../../agents/claude-runner.js';
|
|
4
5
|
import { StreamFlusher } from './stream-flusher.js';
|
|
6
|
+
import { ThoughtEmitter } from './thought-emitter.js';
|
|
5
7
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
6
8
|
import { logger } from '../../utils/logger.js';
|
|
7
9
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
@@ -27,7 +29,6 @@ export class MessageProcessor {
|
|
|
27
29
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
28
30
|
interactionRouter;
|
|
29
31
|
messageQueue;
|
|
30
|
-
skillsHintDesc = undefined; // undefined=未加载, null=无模板, string=缓存描述
|
|
31
32
|
skillsEnsured = false; // 全局 SKILLS.md 是否已确保
|
|
32
33
|
/** 按 agentId 获取 agent,回退到默认 */
|
|
33
34
|
getAgent(agentId) {
|
|
@@ -279,6 +280,8 @@ export class MessageProcessor {
|
|
|
279
280
|
const { adapter, options } = channelInfo;
|
|
280
281
|
const agent = this.getAgent(session.agentId);
|
|
281
282
|
const streamKey = session.id;
|
|
283
|
+
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
284
|
+
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
282
285
|
try {
|
|
283
286
|
const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
284
287
|
// 记录收到消息
|
|
@@ -302,7 +305,7 @@ export class MessageProcessor {
|
|
|
302
305
|
logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
303
306
|
// 记录开始处理
|
|
304
307
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
305
|
-
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
|
|
308
|
+
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
|
|
306
309
|
logger.message({
|
|
307
310
|
msgId: messageId,
|
|
308
311
|
sessionId: session.id,
|
|
@@ -338,6 +341,12 @@ export class MessageProcessor {
|
|
|
338
341
|
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
|
|
339
342
|
// 保存当前 flusher,用于 compact 事件
|
|
340
343
|
this.currentFlusher = flusher;
|
|
344
|
+
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
345
|
+
// selector: context = { type: 'task', id: taskId }
|
|
346
|
+
let thoughtEmitter = null;
|
|
347
|
+
if (isProactive && adapter.putThought) {
|
|
348
|
+
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
|
|
349
|
+
}
|
|
341
350
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
342
351
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
343
352
|
const capturedChannelId = message.channelId;
|
|
@@ -380,15 +389,28 @@ export class MessageProcessor {
|
|
|
380
389
|
const peerLabel = session.identity?.role || 'unknown';
|
|
381
390
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
382
391
|
const peerType = message.peerType;
|
|
392
|
+
const peerId = message.peerId;
|
|
393
|
+
const adapterAny = channelInfo.adapter;
|
|
394
|
+
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
395
|
+
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
396
|
+
const formatIdentity = (name, id) => {
|
|
397
|
+
if (name && id)
|
|
398
|
+
return `${name} (${id})`;
|
|
399
|
+
return name || id || undefined;
|
|
400
|
+
};
|
|
401
|
+
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
402
|
+
const peerIdentity = formatIdentity(peerName, peerId);
|
|
383
403
|
const envParts = [
|
|
384
404
|
`会话通道: ${currentChannelType}`,
|
|
385
405
|
`当前项目: ${path.basename(absoluteProjectPath)}`,
|
|
386
406
|
];
|
|
387
407
|
if (session.name)
|
|
388
408
|
envParts.push(`会话名称: ${session.name}`);
|
|
409
|
+
if (selfIdentity)
|
|
410
|
+
envParts.push(`当前名称: ${selfIdentity}`);
|
|
389
411
|
envParts.push(`对端身份: ${peerLabel}`);
|
|
390
|
-
if (
|
|
391
|
-
envParts.push(`对端名称: ${
|
|
412
|
+
if (peerIdentity)
|
|
413
|
+
envParts.push(`对端名称: ${peerIdentity}`);
|
|
392
414
|
if (peerType && peerType !== 'unknown')
|
|
393
415
|
envParts.push(`对端类型: ${peerType}`);
|
|
394
416
|
if (session.chatType)
|
|
@@ -439,14 +461,16 @@ export class MessageProcessor {
|
|
|
439
461
|
contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
|
|
440
462
|
}
|
|
441
463
|
// 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
464
|
+
// 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
|
|
465
|
+
// 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
|
|
466
|
+
// if (!this.skillsEnsured) {
|
|
467
|
+
// this.ensureSkillsFile();
|
|
468
|
+
// this.skillsEnsured = true;
|
|
469
|
+
// }
|
|
470
|
+
// const skillsHint = this.getSkillsHint();
|
|
471
|
+
// if (skillsHint) {
|
|
472
|
+
// contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
|
|
473
|
+
// }
|
|
450
474
|
// 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
|
|
451
475
|
if (isProactive) {
|
|
452
476
|
contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
|
|
@@ -463,7 +487,7 @@ export class MessageProcessor {
|
|
|
463
487
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
464
488
|
agent.registerStream(streamKey, stream);
|
|
465
489
|
streamRegistered = true;
|
|
466
|
-
streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress);
|
|
490
|
+
streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
|
|
467
491
|
break; // 成功,跳出重试循环
|
|
468
492
|
}
|
|
469
493
|
catch (retryError) {
|
|
@@ -493,7 +517,7 @@ export class MessageProcessor {
|
|
|
493
517
|
flusher.addActivity('\u2705 压缩完成,正在重试...');
|
|
494
518
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
|
|
495
519
|
agent.registerStream(streamKey, retryStream);
|
|
496
|
-
streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress);
|
|
520
|
+
streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
|
|
497
521
|
}
|
|
498
522
|
else {
|
|
499
523
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -607,7 +631,7 @@ export class MessageProcessor {
|
|
|
607
631
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
608
632
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
609
633
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
610
|
-
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(message));
|
|
634
|
+
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, this.getReplyContext(message));
|
|
611
635
|
this.eventBus.publish({
|
|
612
636
|
type: 'message:error',
|
|
613
637
|
sessionId: session.id,
|
|
@@ -635,7 +659,7 @@ export class MessageProcessor {
|
|
|
635
659
|
}
|
|
636
660
|
else {
|
|
637
661
|
// 真正的成功
|
|
638
|
-
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(message));
|
|
662
|
+
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, this.getReplyContext(message));
|
|
639
663
|
await this.sessionManager.recordSuccess(session.id);
|
|
640
664
|
this.eventBus.publish({
|
|
641
665
|
type: 'message:completed',
|
|
@@ -688,7 +712,7 @@ export class MessageProcessor {
|
|
|
688
712
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
689
713
|
if (!isUserInterrupt) {
|
|
690
714
|
try {
|
|
691
|
-
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(message));
|
|
715
|
+
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, this.getReplyContext(message));
|
|
692
716
|
}
|
|
693
717
|
catch { }
|
|
694
718
|
}
|
|
@@ -764,7 +788,7 @@ export class MessageProcessor {
|
|
|
764
788
|
* 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
|
|
765
789
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
766
790
|
*/
|
|
767
|
-
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
|
|
791
|
+
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
|
|
768
792
|
let hasReceivedText = false;
|
|
769
793
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
770
794
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -777,6 +801,10 @@ export class MessageProcessor {
|
|
|
777
801
|
resetTimer(event.type, toolName);
|
|
778
802
|
// 记录所有事件类型
|
|
779
803
|
logger.info(`[MessageProcessor] Event: type=${event.type}`);
|
|
804
|
+
// Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
|
|
805
|
+
if (thoughtEmitter) {
|
|
806
|
+
thoughtEmitter.emit(event).catch(() => { });
|
|
807
|
+
}
|
|
780
808
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
781
809
|
if (event.type === 'session_id') {
|
|
782
810
|
logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -1077,42 +1105,28 @@ export class MessageProcessor {
|
|
|
1077
1105
|
return 0;
|
|
1078
1106
|
}
|
|
1079
1107
|
/**
|
|
1080
|
-
*
|
|
1108
|
+
* 从 data/SKILLS.md 读取 frontmatter 并生成提示。
|
|
1109
|
+
* 不缓存:每次读取保证用户编辑立即生效。
|
|
1110
|
+
* 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
|
|
1081
1111
|
*/
|
|
1082
1112
|
getSkillsHint() {
|
|
1083
|
-
if (this.skillsHintDesc === undefined) {
|
|
1084
|
-
this.skillsHintDesc = this.loadSkillsHint();
|
|
1085
|
-
}
|
|
1086
|
-
return this.skillsHintDesc;
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* 从包模板源读取 frontmatter 并生成提示(仅执行一次)
|
|
1090
|
-
*/
|
|
1091
|
-
loadSkillsHint() {
|
|
1092
1113
|
try {
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1114
|
+
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1115
|
+
if (!fs.existsSync(skillsPath))
|
|
1116
|
+
return null;
|
|
1117
|
+
const content = fs.readFileSync(skillsPath, 'utf-8');
|
|
1118
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1119
|
+
if (!frontmatterMatch)
|
|
1120
|
+
return null;
|
|
1121
|
+
const fm = frontmatterMatch[1];
|
|
1122
|
+
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1123
|
+
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1124
|
+
const parts = [
|
|
1125
|
+
`可通过 Bash 指令管理运行时,${desc}。`,
|
|
1126
|
+
trigger ? `触发时机:${trigger}。` : '',
|
|
1127
|
+
`完整文档见 ${skillsPath}`,
|
|
1096
1128
|
];
|
|
1097
|
-
|
|
1098
|
-
if (!fs.existsSync(templatePath))
|
|
1099
|
-
continue;
|
|
1100
|
-
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
1101
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1102
|
-
if (!frontmatterMatch)
|
|
1103
|
-
continue;
|
|
1104
|
-
const fm = frontmatterMatch[1];
|
|
1105
|
-
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1106
|
-
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1107
|
-
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1108
|
-
const parts = [
|
|
1109
|
-
`可通过 Bash 执行 \`evolclaw ctl <cmd>\` 管理运行时:${desc}`,
|
|
1110
|
-
trigger ? `触发时机:${trigger}` : '',
|
|
1111
|
-
`完整文档见 ${skillsPath}`,
|
|
1112
|
-
];
|
|
1113
|
-
return parts.filter(Boolean).join('\n');
|
|
1114
|
-
}
|
|
1115
|
-
return null;
|
|
1129
|
+
return parts.filter(Boolean).join('');
|
|
1116
1130
|
}
|
|
1117
1131
|
catch {
|
|
1118
1132
|
return null;
|
|
@@ -169,7 +169,7 @@ export class MessageQueue {
|
|
|
169
169
|
* 合并多条同 peerId 消息:
|
|
170
170
|
* - content: \n 连接
|
|
171
171
|
* - images / mentions: 扁平合并
|
|
172
|
-
* - messageId:
|
|
172
|
+
* - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
173
173
|
* - replyContext / peerName / 其余字段: 取最后一条
|
|
174
174
|
*/
|
|
175
175
|
mergeItems(items) {
|
|
@@ -185,12 +185,20 @@ export class MessageQueue {
|
|
|
185
185
|
allMentions.push(...m.mentions);
|
|
186
186
|
}
|
|
187
187
|
const last = items[items.length - 1];
|
|
188
|
+
// 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
|
|
189
|
+
let latestMessageId;
|
|
190
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
191
|
+
if (items[i].message.messageId) {
|
|
192
|
+
latestMessageId = items[i].message.messageId;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
188
196
|
const merged = {
|
|
189
197
|
...last.message,
|
|
190
198
|
content: contents.join('\n'),
|
|
191
199
|
images: allImages.length > 0 ? allImages : undefined,
|
|
192
200
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
193
|
-
messageId:
|
|
201
|
+
messageId: latestMessageId,
|
|
194
202
|
};
|
|
195
203
|
return {
|
|
196
204
|
message: merged,
|
|
@@ -95,13 +95,21 @@ export class StreamDebouncer {
|
|
|
95
95
|
allMentions.push(...e.mentions);
|
|
96
96
|
}
|
|
97
97
|
const last = entries[entries.length - 1];
|
|
98
|
+
// 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
99
|
+
let latestMessageId;
|
|
100
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
101
|
+
if (entries[i].messageId) {
|
|
102
|
+
latestMessageId = entries[i].messageId;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
98
106
|
const merged = {
|
|
99
107
|
...last.rest,
|
|
100
108
|
content: contents.join('\n'),
|
|
101
109
|
images: allImages.length > 0 ? allImages : undefined,
|
|
102
110
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
103
111
|
replyContext: last.replyContext,
|
|
104
|
-
messageId:
|
|
112
|
+
messageId: latestMessageId,
|
|
105
113
|
};
|
|
106
114
|
const resolves = entries.map(e => e.resolve);
|
|
107
115
|
const rejects = entries.map(e => e.reject);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { logger } from '../../utils/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* ThoughtEmitter — 将 Proactive 模式下的流式 AgentEvent 实时发送为 thought
|
|
4
|
+
*
|
|
5
|
+
* 设计特点:
|
|
6
|
+
* - 不做聚合/batching,逐事件调用 adapter.putThought()
|
|
7
|
+
* - 不感知 group vs P2P,通道差异由 adapter 内部处理
|
|
8
|
+
* - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
|
|
9
|
+
* - fire-and-forget:调用方不 await emit(),错误被内部捕获
|
|
10
|
+
*/
|
|
11
|
+
export class ThoughtEmitter {
|
|
12
|
+
adapter;
|
|
13
|
+
channelId;
|
|
14
|
+
taskId;
|
|
15
|
+
hasEmittedText = false;
|
|
16
|
+
constructor(adapter, channelId, taskId) {
|
|
17
|
+
if (!taskId) {
|
|
18
|
+
throw new Error('[ThoughtEmitter] taskId is required at construction');
|
|
19
|
+
}
|
|
20
|
+
this.adapter = adapter;
|
|
21
|
+
this.channelId = channelId;
|
|
22
|
+
this.taskId = taskId;
|
|
23
|
+
}
|
|
24
|
+
async emit(event) {
|
|
25
|
+
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
26
|
+
if (event.type === 'complete' &&
|
|
27
|
+
!event.isError &&
|
|
28
|
+
event.result &&
|
|
29
|
+
this.hasEmittedText) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const payload = this.mapEventToPayload(event);
|
|
33
|
+
if (!payload)
|
|
34
|
+
return;
|
|
35
|
+
if (!this.adapter.putThought)
|
|
36
|
+
return;
|
|
37
|
+
if (payload.stage === 'thinking') {
|
|
38
|
+
this.hasEmittedText = true;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await this.adapter.putThought(this.channelId, this.taskId, payload);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
mapEventToPayload(event) {
|
|
48
|
+
switch (event.type) {
|
|
49
|
+
case 'text':
|
|
50
|
+
if (!event.text)
|
|
51
|
+
return null;
|
|
52
|
+
return { type: 'thought', text: event.text, stage: 'thinking' };
|
|
53
|
+
case 'tool_use': {
|
|
54
|
+
const desc = this.summarizeInput(event.input, event.name);
|
|
55
|
+
return {
|
|
56
|
+
type: 'thought',
|
|
57
|
+
text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
|
|
58
|
+
stage: 'tool',
|
|
59
|
+
metadata: { tool: event.name, input: desc },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case 'tool_result':
|
|
63
|
+
if (event.isError) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'thought',
|
|
66
|
+
text: `⚠️ ${event.name}: ${event.error || '执行失败'}`,
|
|
67
|
+
stage: 'tool',
|
|
68
|
+
metadata: { tool: event.name, ok: false },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
const resultText = this.truncate(this.stringifyResult(event.result), 200);
|
|
73
|
+
return {
|
|
74
|
+
type: 'thought',
|
|
75
|
+
text: resultText ? `✅ ${event.name}: ${resultText}` : `✅ ${event.name}`,
|
|
76
|
+
stage: 'tool',
|
|
77
|
+
metadata: { tool: event.name, ok: true },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case 'compact':
|
|
81
|
+
return {
|
|
82
|
+
type: 'thought',
|
|
83
|
+
text: `💡 会话压缩完成 (压缩前 tokens: ${event.preTokens})`,
|
|
84
|
+
stage: 'system',
|
|
85
|
+
};
|
|
86
|
+
case 'task_progress': {
|
|
87
|
+
const stats = this.formatTaskStats(event);
|
|
88
|
+
const text = event.summary
|
|
89
|
+
? `⏳ 子任务: ${event.summary}${stats ? ` (${stats})` : ''}`
|
|
90
|
+
: `⏳ 子任务进行中${stats ? `: ${stats}` : ''}`;
|
|
91
|
+
return { type: 'thought', text, stage: 'planning' };
|
|
92
|
+
}
|
|
93
|
+
case 'error':
|
|
94
|
+
return { type: 'thought', text: `❌ ${event.error}`, stage: 'error' };
|
|
95
|
+
case 'complete':
|
|
96
|
+
if (event.isError) {
|
|
97
|
+
const errText = event.errors?.join('; ') || event.result || '任务失败';
|
|
98
|
+
return { type: 'thought', text: `❌ ${errText}`, stage: 'error' };
|
|
99
|
+
}
|
|
100
|
+
if (event.result) {
|
|
101
|
+
return { type: 'thought', text: event.result, stage: 'summary' };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
case 'session_id':
|
|
105
|
+
case 'state_changed':
|
|
106
|
+
case 'status':
|
|
107
|
+
return null;
|
|
108
|
+
default:
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
summarizeInput(input, toolName) {
|
|
113
|
+
if (!input || typeof input !== 'object')
|
|
114
|
+
return '';
|
|
115
|
+
// Bash + ctl send/file: 显示完整命令内容(含发送的消息正文)
|
|
116
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
117
|
+
const cmd = input.command;
|
|
118
|
+
if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
|
|
119
|
+
return cmd;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return (input.description ||
|
|
123
|
+
input.file_path ||
|
|
124
|
+
input.pattern ||
|
|
125
|
+
(typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
|
|
126
|
+
(typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
|
|
127
|
+
(typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
|
|
128
|
+
'');
|
|
129
|
+
}
|
|
130
|
+
stringifyResult(result) {
|
|
131
|
+
if (result === null || result === undefined)
|
|
132
|
+
return '';
|
|
133
|
+
if (typeof result === 'string')
|
|
134
|
+
return result;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.stringify(result);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return String(result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
truncate(text, maxLen) {
|
|
143
|
+
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
|
|
144
|
+
}
|
|
145
|
+
formatTaskStats(event) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
if (event.toolUses)
|
|
148
|
+
parts.push(`${event.toolUses} tools`);
|
|
149
|
+
if (event.durationMs)
|
|
150
|
+
parts.push(`${Math.round(event.durationMs / 1000)}s`);
|
|
151
|
+
return parts.join(', ');
|
|
152
|
+
}
|
|
153
|
+
}
|