evolclaw 3.1.0 → 3.1.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/CHANGELOG.md +407 -0
- package/README.md +1 -1
- package/SKILLS.md +311 -0
- package/dist/agents/claude-runner.js +40 -3
- package/dist/aun/aid/agentmd.js +7 -6
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +51 -0
- package/dist/aun/msg/upload.js +57 -18
- package/dist/channels/aun.js +124 -50
- package/dist/channels/dingtalk.js +2 -0
- package/dist/channels/feishu.js +15 -6
- package/dist/channels/qqbot.js +2 -0
- package/dist/channels/wechat.js +2 -0
- package/dist/channels/wecom.js +2 -0
- package/dist/cli/agent.js +130 -35
- package/dist/cli/index.js +221 -48
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +44 -23
- package/dist/cli/watch-msg.js +109 -30
- package/dist/config-store.js +67 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +95 -84
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +4 -4
- package/dist/core/message/im-renderer.js +47 -8
- package/dist/core/message/message-bridge.js +30 -1
- package/dist/core/message/message-log.js +6 -1
- package/dist/core/message/message-processor.js +29 -35
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +11 -4
- package/dist/core/trigger/manager.js +16 -0
- package/dist/core/trigger/parser.js +110 -0
- package/dist/core/trigger/scheduler.js +6 -0
- package/dist/index.js +64 -20
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/dist/utils/error-utils.js +17 -13
- package/dist/utils/stats.js +216 -2
- package/kits/docs/INDEX.md +6 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
- package/kits/rules/06-channel.md +30 -0
- package/package.json +6 -3
|
@@ -8,7 +8,7 @@ import crypto from 'crypto';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import os from 'os';
|
|
11
|
-
import { parseTriggerSet } from './trigger/parser.js';
|
|
11
|
+
import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
|
|
12
12
|
import { calcNextFireAt } from './trigger/scheduler.js';
|
|
13
13
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
|
|
14
14
|
const allEfforts = ['low', 'medium', 'high', 'max'];
|
|
@@ -24,17 +24,8 @@ function getAvailableEfforts(agent, model) {
|
|
|
24
24
|
}
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
|
-
function formatModelUsage(
|
|
28
|
-
|
|
29
|
-
const lines = [
|
|
30
|
-
'用法:',
|
|
31
|
-
' /model <模型> 切换模型',
|
|
32
|
-
];
|
|
33
|
-
if (efforts.length > 0) {
|
|
34
|
-
lines.push(' /model <模型> <强度> 切换模型+推理强度');
|
|
35
|
-
lines.push(' /effort [level] 查看或切换推理强度');
|
|
36
|
-
}
|
|
37
|
-
return lines.join('\n');
|
|
27
|
+
function formatModelUsage(_agent, _model) {
|
|
28
|
+
return '用法: /model <模型>';
|
|
38
29
|
}
|
|
39
30
|
/**
|
|
40
31
|
* 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
@@ -341,8 +332,6 @@ export class CommandHandler {
|
|
|
341
332
|
return renderCommandCardAsText(card);
|
|
342
333
|
if (!adapter?.send)
|
|
343
334
|
return renderCommandCardAsText(card);
|
|
344
|
-
if (this.isSessionBusy(opts.interaction.sessionId))
|
|
345
|
-
return renderCommandCardAsText(card);
|
|
346
335
|
try {
|
|
347
336
|
const envelope = buildEnvelope({
|
|
348
337
|
channel: opts.channel,
|
|
@@ -382,14 +371,6 @@ export class CommandHandler {
|
|
|
382
371
|
});
|
|
383
372
|
return { matched: true, result: '✓ 已回答' };
|
|
384
373
|
}
|
|
385
|
-
/** 判断指定 session 是否有活跃流(用于 idle 守卫和卡片降级) */
|
|
386
|
-
isSessionBusy(sessionId) {
|
|
387
|
-
for (const agent of this.agentMap.values()) {
|
|
388
|
-
if (agent.hasActiveStream(sessionId))
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
374
|
/** 获取活跃会话,无会话时自动创建(话题除外) */
|
|
394
375
|
async ensureSession(channel, channelId, threadId, chatType) {
|
|
395
376
|
if (threadId) {
|
|
@@ -726,6 +707,10 @@ export class CommandHandler {
|
|
|
726
707
|
return result;
|
|
727
708
|
}
|
|
728
709
|
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
|
|
710
|
+
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
711
|
+
// 不应覆盖 session 中已有的正确值
|
|
712
|
+
if (source === 'card-trigger')
|
|
713
|
+
chatType = undefined;
|
|
729
714
|
// 解析身份(按实例名)
|
|
730
715
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
731
716
|
const policy = this.getPolicy(channel);
|
|
@@ -1029,9 +1014,9 @@ export class CommandHandler {
|
|
|
1029
1014
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
1030
1015
|
}).join('\n');
|
|
1031
1016
|
if (isOwner) {
|
|
1032
|
-
return { kind: 'command.result', text:
|
|
1017
|
+
return { kind: 'command.result', text: `权限模式: ${currentMode}\n\n${modeList}\n\n用法: /perm <模式> 或 allow|always|deny` };
|
|
1033
1018
|
}
|
|
1034
|
-
return { kind: 'command.result', text:
|
|
1019
|
+
return { kind: 'command.result', text: `当前权限模式: ${currentMode}` };
|
|
1035
1020
|
}
|
|
1036
1021
|
const parts = args.split(/\s+/);
|
|
1037
1022
|
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
@@ -1235,7 +1220,7 @@ export class CommandHandler {
|
|
|
1235
1220
|
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
1236
1221
|
const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
|
|
1237
1222
|
if (canSwitchAgent) {
|
|
1238
|
-
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n
|
|
1223
|
+
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n用法: /agent <name>` };
|
|
1239
1224
|
}
|
|
1240
1225
|
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}` };
|
|
1241
1226
|
}
|
|
@@ -1373,7 +1358,7 @@ export class CommandHandler {
|
|
|
1373
1358
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
1374
1359
|
: '';
|
|
1375
1360
|
if (isAdmin) {
|
|
1376
|
-
return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n
|
|
1361
|
+
return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n用法: /model <模型>` };
|
|
1377
1362
|
}
|
|
1378
1363
|
return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}` };
|
|
1379
1364
|
}
|
|
@@ -1494,12 +1479,11 @@ export class CommandHandler {
|
|
|
1494
1479
|
}
|
|
1495
1480
|
// 降级:文本
|
|
1496
1481
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
1497
|
-
const
|
|
1498
|
-
const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
|
|
1482
|
+
const effortOptions = [...efforts, 'auto'].join(' / ');
|
|
1499
1483
|
if (isAdmin) {
|
|
1500
|
-
return { kind: 'command.result', text:
|
|
1484
|
+
return { kind: 'command.result', text: `推理强度: ${effortDisplay} 可选: ${effortOptions} 用法: /effort <level>` };
|
|
1501
1485
|
}
|
|
1502
|
-
return { kind: 'command.result', text:
|
|
1486
|
+
return { kind: 'command.result', text: `推理强度: ${effortDisplay}` };
|
|
1503
1487
|
}
|
|
1504
1488
|
// 带参(切换)需 admin+;无参查询已在上方返回
|
|
1505
1489
|
if (!isAdmin)
|
|
@@ -1534,9 +1518,7 @@ export class CommandHandler {
|
|
|
1534
1518
|
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
1535
1519
|
// 无参数时返回用法说明
|
|
1536
1520
|
if (normalizedContent === '/aid') {
|
|
1537
|
-
return { kind: 'command.result', text:
|
|
1538
|
-
|
|
1539
|
-
用法:
|
|
1521
|
+
return { kind: 'command.result', text: `用法:
|
|
1540
1522
|
/aid list 列出本地所有 AID
|
|
1541
1523
|
/aid show <aid> 查看 AID 详情
|
|
1542
1524
|
/aid new <aid> 创建新 AID
|
|
@@ -1546,27 +1528,16 @@ export class CommandHandler {
|
|
|
1546
1528
|
/aid agentmd get <aid> 下载并验签 agent.md` };
|
|
1547
1529
|
}
|
|
1548
1530
|
if (normalizedContent === '/rpc') {
|
|
1549
|
-
return { kind: 'command.result', text:
|
|
1550
|
-
|
|
1551
|
-
用法:
|
|
1552
|
-
/rpc --as <aid> --params <json>
|
|
1553
|
-
|
|
1554
|
-
参数格式:
|
|
1555
|
-
单行 JSON 单次调用
|
|
1556
|
-
多行 JSONL 逐行执行,失败即停
|
|
1557
|
-
|
|
1558
|
-
示例:
|
|
1559
|
-
/rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
|
|
1531
|
+
return { kind: 'command.result', text: `用法: /rpc --as <aid> --params <json>
|
|
1532
|
+
示例: /rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
|
|
1560
1533
|
}
|
|
1561
1534
|
if (normalizedContent === '/storage') {
|
|
1562
|
-
return { kind: 'command.result', text:
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
/storage
|
|
1566
|
-
/storage
|
|
1567
|
-
/storage
|
|
1568
|
-
/storage rm <aid> <path> 删文件
|
|
1569
|
-
/storage quota <aid> 查配额` };
|
|
1535
|
+
return { kind: 'command.result', text: `用法:
|
|
1536
|
+
/storage upload <aid> <file> <path> [--public]
|
|
1537
|
+
/storage download <aid> <url> [local-path]
|
|
1538
|
+
/storage ls <aid> [prefix]
|
|
1539
|
+
/storage rm <aid> <path>
|
|
1540
|
+
/storage quota <aid>` };
|
|
1570
1541
|
}
|
|
1571
1542
|
const cliArgs = normalizedContent.slice(1); // strip leading /
|
|
1572
1543
|
try {
|
|
@@ -1644,9 +1615,9 @@ export class CommandHandler {
|
|
|
1644
1615
|
return ` ${prefix} ${m.key} — ${m.label}`;
|
|
1645
1616
|
}).join('\n');
|
|
1646
1617
|
if (isOwner) {
|
|
1647
|
-
return { kind: 'command.result', text:
|
|
1618
|
+
return { kind: 'command.result', text: `中间输出: ${currentMode} 用法: /activity <all|dm|owner|none>` };
|
|
1648
1619
|
}
|
|
1649
|
-
return { kind: 'command.result', text:
|
|
1620
|
+
return { kind: 'command.result', text: `中间输出: ${currentMode}` };
|
|
1650
1621
|
}
|
|
1651
1622
|
const newMode = modeMap[activityArg];
|
|
1652
1623
|
if (!newMode) {
|
|
@@ -1678,8 +1649,12 @@ export class CommandHandler {
|
|
|
1678
1649
|
const arg = normalizedContent.slice(9).trim();
|
|
1679
1650
|
const currentMode = chatmodeSession.sessionMode || 'interactive';
|
|
1680
1651
|
const chatmodeChatType = chatmodeSession.chatType || activeChatType;
|
|
1681
|
-
const
|
|
1652
|
+
const isGroup = chatmodeChatType === 'group';
|
|
1653
|
+
const canSwitch = !isGroup;
|
|
1682
1654
|
if (!arg) {
|
|
1655
|
+
if (isGroup) {
|
|
1656
|
+
return { kind: 'command.result', text: `📋 会话模式: proactive(群聊强制)` };
|
|
1657
|
+
}
|
|
1683
1658
|
// 尝试发送 CommandCard 卡片
|
|
1684
1659
|
if (canSwitch) {
|
|
1685
1660
|
const modes = [
|
|
@@ -1712,23 +1687,16 @@ export class CommandHandler {
|
|
|
1712
1687
|
}
|
|
1713
1688
|
// 降级:文本
|
|
1714
1689
|
if (canSwitch) {
|
|
1715
|
-
return { kind: 'command.result', text:
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
'模式说明:',
|
|
1719
|
-
' • interactive — 交互模式:收到消息时才回复,回复直接显示',
|
|
1720
|
-
' • proactive — 主动模式:流式输出静默,由 Agent 自调 ctl send 发声',
|
|
1721
|
-
'',
|
|
1722
|
-
'用法: /chatmode <interactive|proactive>',
|
|
1723
|
-
].join('\n') };
|
|
1724
|
-
}
|
|
1725
|
-
return { kind: 'command.result', text: `📋 会话模式: ${currentMode}` };
|
|
1690
|
+
return { kind: 'command.result', text: `会话模式: ${currentMode} 用法: /chatmode <interactive|proactive>` };
|
|
1691
|
+
}
|
|
1692
|
+
return { kind: 'command.result', text: `会话模式: ${currentMode}` };
|
|
1726
1693
|
}
|
|
1727
1694
|
if (arg !== 'interactive' && arg !== 'proactive') {
|
|
1728
1695
|
return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: interactive / proactive` };
|
|
1729
1696
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1697
|
+
// 群聊强制 proactive,不可切换
|
|
1698
|
+
if ((chatmodeSession.chatType || activeChatType) === 'group') {
|
|
1699
|
+
return { kind: 'command.error', text: '❌ 群聊强制 proactive 模式,不可切换' };
|
|
1732
1700
|
}
|
|
1733
1701
|
if (arg === currentMode) {
|
|
1734
1702
|
return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
|
|
@@ -1796,17 +1764,10 @@ export class CommandHandler {
|
|
|
1796
1764
|
// 卡片降级:fall through 到下方文本输出
|
|
1797
1765
|
}
|
|
1798
1766
|
// 降级:文本
|
|
1799
|
-
const lines = [];
|
|
1800
|
-
lines.push(`📋 分发模式: ${displayMode}`);
|
|
1801
|
-
lines.push('');
|
|
1802
|
-
lines.push('模式说明:');
|
|
1803
|
-
lines.push(' • mention — 提及模式:仅当被@提及时响应群消息(含@all)');
|
|
1804
|
-
lines.push(' • broadcast — 广播模式:群内所有消息都触发响应');
|
|
1805
1767
|
if (isAdmin) {
|
|
1806
|
-
|
|
1807
|
-
lines.push('用法: /dispatch <mention|broadcast>');
|
|
1768
|
+
return { kind: 'command.result', text: `分发模式: ${displayMode} 用法: /dispatch <mention|broadcast>` };
|
|
1808
1769
|
}
|
|
1809
|
-
return { kind: 'command.result', text:
|
|
1770
|
+
return { kind: 'command.result', text: `分发模式: ${displayMode}` };
|
|
1810
1771
|
}
|
|
1811
1772
|
if (arg !== 'mention' && arg !== 'broadcast') {
|
|
1812
1773
|
return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / broadcast\n用法: /dispatch <模式>` };
|
|
@@ -1815,7 +1776,7 @@ export class CommandHandler {
|
|
|
1815
1776
|
return { kind: 'command.error', text: '❌ 无权限:群聊中切换分发模式仅限管理员使用' };
|
|
1816
1777
|
}
|
|
1817
1778
|
if (arg === currentMode) {
|
|
1818
|
-
return { kind: 'command.result', text:
|
|
1779
|
+
return { kind: 'command.result', text: `当前已是 ${arg}` };
|
|
1819
1780
|
}
|
|
1820
1781
|
const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
|
|
1821
1782
|
await this.sessionManager.updateSession(dispatchSession.id, { metadata });
|
|
@@ -3043,8 +3004,10 @@ export class CommandHandler {
|
|
|
3043
3004
|
return null;
|
|
3044
3005
|
}
|
|
3045
3006
|
handleTrigger(content, channel, channelId, peerId, isAdmin) {
|
|
3046
|
-
|
|
3047
|
-
const
|
|
3007
|
+
// Resolve trigger manager/scheduler from the owning agent of this channel
|
|
3008
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
3009
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
3010
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
3048
3011
|
// Bare /trigger → list active
|
|
3049
3012
|
if (content === '/trigger') {
|
|
3050
3013
|
if (!manager)
|
|
@@ -3111,6 +3074,45 @@ export class CommandHandler {
|
|
|
3111
3074
|
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: peerId });
|
|
3112
3075
|
return `✅ 触发器已取消:**${trigger.name}**`;
|
|
3113
3076
|
}
|
|
3077
|
+
// /trigger update <name|id> [--参数...]
|
|
3078
|
+
if (sub.startsWith('update ')) {
|
|
3079
|
+
if (!manager || !scheduler)
|
|
3080
|
+
return '⚠️ 触发器功能未启用';
|
|
3081
|
+
const args = sub.slice('update '.length);
|
|
3082
|
+
const result = parseTriggerUpdate(args);
|
|
3083
|
+
if (!result.ok)
|
|
3084
|
+
return `❌ ${result.error}`;
|
|
3085
|
+
const { nameOrId, value: patch } = result;
|
|
3086
|
+
// Find trigger: non-admin lookup is scoped
|
|
3087
|
+
let trigger;
|
|
3088
|
+
if (isAdmin) {
|
|
3089
|
+
trigger = manager.getByName(nameOrId) ?? manager.getById(nameOrId);
|
|
3090
|
+
}
|
|
3091
|
+
else {
|
|
3092
|
+
trigger = manager.getByNameScoped(nameOrId, peerId, channel)
|
|
3093
|
+
?? manager.getByIdScoped(nameOrId, peerId, channel);
|
|
3094
|
+
}
|
|
3095
|
+
if (!trigger) {
|
|
3096
|
+
return isAdmin
|
|
3097
|
+
? `❌ 未找到触发器:${nameOrId}`
|
|
3098
|
+
: `❌ 未找到触发器 "${nameOrId}",或无权限修改`;
|
|
3099
|
+
}
|
|
3100
|
+
// If schedule changed, recalculate nextFireAt
|
|
3101
|
+
if (patch.scheduleType && patch.scheduleValue) {
|
|
3102
|
+
const now = Date.now();
|
|
3103
|
+
patch.nextFireAt = calcNextFireAt(patch.scheduleType, patch.scheduleValue, now);
|
|
3104
|
+
}
|
|
3105
|
+
let updated;
|
|
3106
|
+
try {
|
|
3107
|
+
updated = manager.update(trigger.id, patch);
|
|
3108
|
+
scheduler.update(updated);
|
|
3109
|
+
}
|
|
3110
|
+
catch (err) {
|
|
3111
|
+
return `❌ 更新失败:${err.message}`;
|
|
3112
|
+
}
|
|
3113
|
+
const nextStr = new Date(updated.nextFireAt).toLocaleString();
|
|
3114
|
+
return `✅ 触发器已更新:**${updated.name}**\n下次触发:${nextStr}`;
|
|
3115
|
+
}
|
|
3114
3116
|
// /trigger set ...
|
|
3115
3117
|
if (sub.startsWith('set ')) {
|
|
3116
3118
|
if (!manager || !scheduler)
|
|
@@ -3155,7 +3157,7 @@ export class CommandHandler {
|
|
|
3155
3157
|
const nextStr = new Date(nextFireAt).toLocaleString();
|
|
3156
3158
|
return `✅ 触发器已注册:**${name}**\n下次触发:${nextStr}`;
|
|
3157
3159
|
}
|
|
3158
|
-
return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger cancel <名称> — 取消触发器`;
|
|
3160
|
+
return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger update <名称|ID> <参数> — 修改触发器\n/trigger cancel <名称> — 取消触发器`;
|
|
3159
3161
|
}
|
|
3160
3162
|
// ── /rewind helpers ──
|
|
3161
3163
|
async handleRewindList(session, agent) {
|
|
@@ -3376,7 +3378,10 @@ export class CommandHandler {
|
|
|
3376
3378
|
}
|
|
3377
3379
|
// 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
|
|
3378
3380
|
if (cmd.startsWith('/send ') || cmd === '/send') {
|
|
3379
|
-
|
|
3381
|
+
// 解析 --encrypt 标志和消息文本
|
|
3382
|
+
const raw = cmd.startsWith('/send ') ? cmd.slice(6).trim() : '';
|
|
3383
|
+
const forceEncrypt = raw.startsWith('--encrypt ');
|
|
3384
|
+
const text = forceEncrypt ? raw.slice(10).trim() : raw;
|
|
3380
3385
|
if (!text)
|
|
3381
3386
|
return { ok: false, error: '消息内容不能为空' };
|
|
3382
3387
|
const adapter = this.adapters.get(session.channel);
|
|
@@ -3386,8 +3391,14 @@ export class CommandHandler {
|
|
|
3386
3391
|
const replyContext = this.buildCtlReplyContext(session);
|
|
3387
3392
|
const taskId = replyContext?.metadata?.taskId;
|
|
3388
3393
|
const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
|
|
3389
|
-
|
|
3390
|
-
|
|
3394
|
+
// --encrypt 覆盖 session 加密状态
|
|
3395
|
+
// 添加 source: 'ctl' 标记(用于区分 ec ctl send)
|
|
3396
|
+
const enrichedReplyContext = forceEncrypt
|
|
3397
|
+
? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true, source: 'ctl' } }
|
|
3398
|
+
: { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), source: 'ctl' } };
|
|
3399
|
+
await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext: enrichedReplyContext }), { kind: 'result.text', text, isFinal: true });
|
|
3400
|
+
// 出方向 jsonl 写入已下沉到 aun.ts:deliverTextEntry,message.send 成功后统一写入。
|
|
3401
|
+
return { ok: true, result: 'ok' };
|
|
3391
3402
|
}
|
|
3392
3403
|
catch (err) {
|
|
3393
3404
|
return { ok: false, error: err.message || String(err) };
|
|
@@ -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)) {
|
|
@@ -3,6 +3,21 @@ import { summarizeToolInput } from '../permission.js';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { resolvePaths } from '../../paths.js';
|
|
6
|
+
/**
|
|
7
|
+
* 检测是否为上下文过长错误
|
|
8
|
+
* 统一的检测逻辑,覆盖所有已知的错误文本模式
|
|
9
|
+
*/
|
|
10
|
+
function isContextTooLongError(text) {
|
|
11
|
+
if (!text)
|
|
12
|
+
return false;
|
|
13
|
+
const lower = text.toLowerCase();
|
|
14
|
+
return (lower.includes('prompt is too long') ||
|
|
15
|
+
lower.includes('input is too long') ||
|
|
16
|
+
lower.includes('context too long') ||
|
|
17
|
+
lower.includes('context limit') ||
|
|
18
|
+
lower.includes('context_length_exceeded') ||
|
|
19
|
+
text.includes('上下文过长'));
|
|
20
|
+
}
|
|
6
21
|
let diagStream = null;
|
|
7
22
|
function getDiagStream() {
|
|
8
23
|
if (!diagStream) {
|
|
@@ -129,7 +144,8 @@ export class IMRenderer {
|
|
|
129
144
|
}
|
|
130
145
|
// ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
|
|
131
146
|
/** 添加文本片段(流式 text) */
|
|
132
|
-
addText(text) {
|
|
147
|
+
addText(text, outputTokens, turn) {
|
|
148
|
+
this.emitProgress('text', outputTokens, turn);
|
|
133
149
|
if (this.opts.envelope.chatmode === 'proactive')
|
|
134
150
|
return;
|
|
135
151
|
if (!text)
|
|
@@ -155,7 +171,8 @@ export class IMRenderer {
|
|
|
155
171
|
this.scheduleFlush();
|
|
156
172
|
}
|
|
157
173
|
/** 添加工具调用 */
|
|
158
|
-
addToolCall(name, input, callId, descText) {
|
|
174
|
+
addToolCall(name, input, callId, descText, turn, outputTokens) {
|
|
175
|
+
this.emitProgress('tool_call', outputTokens, turn);
|
|
159
176
|
if (this.opts.envelope.chatmode === 'proactive')
|
|
160
177
|
return;
|
|
161
178
|
if (this.opts.suppressActivities)
|
|
@@ -174,6 +191,7 @@ export class IMRenderer {
|
|
|
174
191
|
}
|
|
175
192
|
/** 添加工具结果 */
|
|
176
193
|
addToolResult(name, ok, result, error, callId, durationMs, descText) {
|
|
194
|
+
this.emitProgress('tool_result');
|
|
177
195
|
if (this.opts.envelope.chatmode === 'proactive')
|
|
178
196
|
return;
|
|
179
197
|
if (this.opts.suppressActivities)
|
|
@@ -183,10 +201,10 @@ export class IMRenderer {
|
|
|
183
201
|
call_id: callId || this.synthCallId(),
|
|
184
202
|
name,
|
|
185
203
|
ok,
|
|
186
|
-
result,
|
|
187
|
-
error,
|
|
188
|
-
duration_ms: durationMs,
|
|
189
|
-
text: descText,
|
|
204
|
+
...(result !== undefined && { result }),
|
|
205
|
+
...(error !== undefined && { error }),
|
|
206
|
+
...(durationMs !== undefined && { duration_ms: durationMs }),
|
|
207
|
+
...(descText !== undefined && { text: descText }),
|
|
190
208
|
});
|
|
191
209
|
this.messageTimestamps.push(Date.now());
|
|
192
210
|
if (this.diagEnabled)
|
|
@@ -351,6 +369,11 @@ export class IMRenderer {
|
|
|
351
369
|
this.flushCount++;
|
|
352
370
|
}
|
|
353
371
|
}
|
|
372
|
+
// ── 内部:status.progress 发送 ──
|
|
373
|
+
emitProgress(activityType, outputTokens, turn) {
|
|
374
|
+
const payload = { kind: 'status.progress', metadata: { activityType, ...(turn != null && { turn }), ...(outputTokens != null && { outputTokens }) } };
|
|
375
|
+
this.opts.send(payload).catch(() => { });
|
|
376
|
+
}
|
|
354
377
|
// ── 内部:proactive 模式(逐事件 activity.batch[1 item]) ──
|
|
355
378
|
emitProactive(event) {
|
|
356
379
|
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
@@ -367,6 +390,10 @@ export class IMRenderer {
|
|
|
367
390
|
this.hasEmittedText = true;
|
|
368
391
|
this.allText += item.text;
|
|
369
392
|
}
|
|
393
|
+
const outputTokens = event.outputTokens;
|
|
394
|
+
const turn = event.turn;
|
|
395
|
+
const activityType = item.kind === 'text' ? 'text' : item.kind === 'tool_call' ? 'tool_call' : 'tool_result';
|
|
396
|
+
this.emitProgress(activityType, outputTokens, turn);
|
|
370
397
|
const payload = { kind: 'activity.batch', items: [item] };
|
|
371
398
|
// fire-and-forget
|
|
372
399
|
this.opts.send(payload).catch(err => {
|
|
@@ -430,9 +457,20 @@ export class IMRenderer {
|
|
|
430
457
|
duration_ms: event.durationMs,
|
|
431
458
|
};
|
|
432
459
|
}
|
|
433
|
-
case 'error':
|
|
460
|
+
case 'error': {
|
|
461
|
+
// 上下文过长错误不输出(留给外层 auto-compact 处理)
|
|
462
|
+
if (isContextTooLongError(event.error || ''))
|
|
463
|
+
return null;
|
|
434
464
|
return { kind: 'notice', text: event.error, severity: 'warn' };
|
|
435
|
-
|
|
465
|
+
}
|
|
466
|
+
case 'complete': {
|
|
467
|
+
// 上下文过长错误不输出(留给外层 auto-compact 处理)
|
|
468
|
+
const hasContextError = event.terminalReason === 'prompt_too_long'
|
|
469
|
+
|| isContextTooLongError(event.errors?.join(' ') || '')
|
|
470
|
+
|| isContextTooLongError(event.result || '');
|
|
471
|
+
if (event.isError && hasContextError) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
436
474
|
if (event.isError) {
|
|
437
475
|
const errText = event.errors?.join('; ') || event.result || '任务失败';
|
|
438
476
|
return {
|
|
@@ -452,6 +490,7 @@ export class IMRenderer {
|
|
|
452
490
|
};
|
|
453
491
|
}
|
|
454
492
|
return null;
|
|
493
|
+
}
|
|
455
494
|
case 'session_id':
|
|
456
495
|
case 'state_changed':
|
|
457
496
|
case 'status':
|
|
@@ -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 } });
|
|
@@ -133,6 +158,8 @@ export class MessageBridge {
|
|
|
133
158
|
};
|
|
134
159
|
// 5.5 写入消息记录(入方向)
|
|
135
160
|
const chatDir = this.sessionManager.getChatDir(session);
|
|
161
|
+
const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
|
|
162
|
+
const inboundChatmode = msg.replyContext?.metadata?.chatmode;
|
|
136
163
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
137
164
|
from: msg.peerId || 'unknown',
|
|
138
165
|
to: msg.selfId || 'self',
|
|
@@ -143,6 +170,8 @@ export class MessageBridge {
|
|
|
143
170
|
replyTo: msg.replyContext?.replyToMessageId ?? null,
|
|
144
171
|
permMode: session.identity?.role ?? null,
|
|
145
172
|
timestamp: fullMessage.timestamp,
|
|
173
|
+
encrypt: inboundEncrypt,
|
|
174
|
+
chatmode: inboundChatmode,
|
|
146
175
|
}));
|
|
147
176
|
// 6. ACK + debounce/enqueue
|
|
148
177
|
// ACK 在到达时立即做(每条独立 ACK),不等合并
|
|
@@ -66,6 +66,8 @@ export function buildInboundEntry(opts) {
|
|
|
66
66
|
permMode: opts.permMode ?? null,
|
|
67
67
|
cmdParsed: isCommand ? opts.content.split(/\s/)[0] : null,
|
|
68
68
|
durationMs: null,
|
|
69
|
+
encrypt: opts.encrypt,
|
|
70
|
+
chatmode: opts.chatmode,
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
export function buildOutboundEntry(opts) {
|
|
@@ -79,7 +81,7 @@ export function buildOutboundEntry(opts) {
|
|
|
79
81
|
chatType: opts.chatType,
|
|
80
82
|
groupId: opts.groupId ?? null,
|
|
81
83
|
msgId: opts.msgId ?? null,
|
|
82
|
-
msgType: 'text',
|
|
84
|
+
msgType: opts.msgType ?? 'text',
|
|
83
85
|
content: opts.content,
|
|
84
86
|
replyTo: opts.replyTo ?? null,
|
|
85
87
|
agent: opts.agent ?? null,
|
|
@@ -89,5 +91,8 @@ export function buildOutboundEntry(opts) {
|
|
|
89
91
|
durationMs: opts.durationMs ?? null,
|
|
90
92
|
numTurns: opts.numTurns ?? null,
|
|
91
93
|
usage: opts.usage ?? null,
|
|
94
|
+
encrypt: opts.encrypt,
|
|
95
|
+
chatmode: opts.chatmode,
|
|
96
|
+
source: opts.source ?? 'daemon',
|
|
92
97
|
};
|
|
93
98
|
}
|