evolclaw 3.1.0 → 3.1.1
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/agents/claude-runner.js +40 -3
- package/dist/aun/msg/p2p.js +38 -0
- package/dist/channels/aun.js +80 -27
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -2
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/index.js +147 -29
- package/dist/cli/init.js +3 -4
- package/dist/cli/watch-msg.js +107 -30
- package/dist/config-store.js +45 -0
- package/dist/core/command-handler.js +86 -82
- package/dist/core/message/im-renderer.js +43 -4
- package/dist/core/message/message-bridge.js +4 -0
- package/dist/core/message/message-log.js +6 -1
- package/dist/core/message/message-processor.js +50 -34
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-manager.js +7 -3
- 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 +49 -3
- package/dist/utils/error-utils.js +17 -13
- package/dist/utils/stats.js +216 -2
- package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
- package/kits/rules/06-channel.md +30 -0
- package/package.json +2 -2
|
@@ -396,7 +396,8 @@ export class MessageProcessor {
|
|
|
396
396
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
397
397
|
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
398
398
|
// 记录开始处理
|
|
399
|
-
|
|
399
|
+
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
400
|
+
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode: session.sessionMode || 'interactive' });
|
|
400
401
|
// 触发器消息不发 processing status(无需通知用户)
|
|
401
402
|
if (message.source !== 'trigger') {
|
|
402
403
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
@@ -581,7 +582,7 @@ export class MessageProcessor {
|
|
|
581
582
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
582
583
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
583
584
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
584
|
-
renderer.addNotice(
|
|
585
|
+
renderer.addNotice(`API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
|
|
585
586
|
await renderer.flush();
|
|
586
587
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
587
588
|
continue;
|
|
@@ -593,12 +594,12 @@ export class MessageProcessor {
|
|
|
593
594
|
catch (error) {
|
|
594
595
|
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
|
|
595
596
|
// 尝试 compact 压缩会话
|
|
596
|
-
renderer.addNotice('
|
|
597
|
+
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
597
598
|
await renderer.flush();
|
|
598
599
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
599
600
|
if (compacted) {
|
|
600
601
|
// compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
|
|
601
|
-
renderer.addNotice('
|
|
602
|
+
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
602
603
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
603
604
|
agent.registerStream(streamKey, retryStream);
|
|
604
605
|
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
@@ -620,11 +621,11 @@ export class MessageProcessor {
|
|
|
620
621
|
contextTooLongPattern.test(errorsText) ||
|
|
621
622
|
contextTooLongPattern.test(streamResult.fullText));
|
|
622
623
|
if (isPromptTooLong) {
|
|
623
|
-
renderer.addNotice('
|
|
624
|
+
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
624
625
|
await renderer.flush();
|
|
625
626
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
626
627
|
if (compacted) {
|
|
627
|
-
renderer.addNotice('✅
|
|
628
|
+
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
628
629
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
629
630
|
agent.registerStream(streamKey, retryStream);
|
|
630
631
|
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
@@ -638,7 +639,7 @@ export class MessageProcessor {
|
|
|
638
639
|
contextTooLongPattern.test(errorsText) ||
|
|
639
640
|
contextTooLongPattern.test(streamResult.fullText))) {
|
|
640
641
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
641
|
-
renderer.addNotice('
|
|
642
|
+
renderer.addNotice('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
|
|
642
643
|
}
|
|
643
644
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
644
645
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
@@ -829,6 +830,7 @@ export class MessageProcessor {
|
|
|
829
830
|
finalText: streamResult.lastReplyText || undefined,
|
|
830
831
|
durationMs: Date.now() - startTime,
|
|
831
832
|
agentName: agentNameForStats,
|
|
833
|
+
numTurns: streamResult.numTurns,
|
|
832
834
|
timestamp: Date.now()
|
|
833
835
|
});
|
|
834
836
|
// 记录处理完成
|
|
@@ -839,24 +841,8 @@ export class MessageProcessor {
|
|
|
839
841
|
status: 'completed',
|
|
840
842
|
duration: Date.now() - startTime
|
|
841
843
|
});
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
const chatDir = this.sessionManager.getChatDir(session);
|
|
845
|
-
appendMessageLog(chatDir, buildOutboundEntry({
|
|
846
|
-
from: message.selfId || session.selfId || 'self',
|
|
847
|
-
to: message.peerId || message.channelId,
|
|
848
|
-
chatType: (message.chatType || session.chatType || 'private'),
|
|
849
|
-
groupId: session.metadata?.groupId ?? null,
|
|
850
|
-
msgId: `${messageId}_reply`,
|
|
851
|
-
content: streamResult.lastReplyText || streamResult.fullText,
|
|
852
|
-
replyTo: message.messageId ?? null,
|
|
853
|
-
agent: session.agentId || null,
|
|
854
|
-
model: agent.getModel?.() || null,
|
|
855
|
-
durationMs: Date.now() - startTime,
|
|
856
|
-
numTurns: streamResult.numTurns,
|
|
857
|
-
usage: streamResult.usage,
|
|
858
|
-
}));
|
|
859
|
-
}
|
|
844
|
+
// 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
|
|
845
|
+
// 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
|
|
860
846
|
}
|
|
861
847
|
const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
862
848
|
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
@@ -980,7 +966,13 @@ export class MessageProcessor {
|
|
|
980
966
|
return { session, absoluteProjectPath };
|
|
981
967
|
}
|
|
982
968
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
|
|
983
|
-
//
|
|
969
|
+
// 兜底纠正1:群聊强制 proactive
|
|
970
|
+
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
971
|
+
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
972
|
+
session.sessionMode = 'proactive';
|
|
973
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
974
|
+
}
|
|
975
|
+
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
984
976
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
985
977
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
986
978
|
logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
|
|
@@ -1076,7 +1068,7 @@ export class MessageProcessor {
|
|
|
1076
1068
|
lastReplyText += event.text;
|
|
1077
1069
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
|
|
1078
1070
|
if (!shouldSuppress()) {
|
|
1079
|
-
renderer.addText(event.text);
|
|
1071
|
+
renderer.addText(event.text, event.outputTokens, event.turn);
|
|
1080
1072
|
}
|
|
1081
1073
|
}
|
|
1082
1074
|
// compact 完成
|
|
@@ -1118,7 +1110,7 @@ export class MessageProcessor {
|
|
|
1118
1110
|
if (event.callId) {
|
|
1119
1111
|
toolDescByCallId.set(event.callId, desc);
|
|
1120
1112
|
}
|
|
1121
|
-
renderer.addToolCall(event.name, event.input, event.callId, desc);
|
|
1113
|
+
renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
|
|
1122
1114
|
}
|
|
1123
1115
|
}
|
|
1124
1116
|
// 工具结果
|
|
@@ -1154,7 +1146,7 @@ export class MessageProcessor {
|
|
|
1154
1146
|
const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
|
|
1155
1147
|
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1156
1148
|
hasErrorResult = true;
|
|
1157
|
-
renderer.addNotice(
|
|
1149
|
+
renderer.addNotice(`${event.error}`, 'warn', 'runtime-error', true);
|
|
1158
1150
|
}
|
|
1159
1151
|
}
|
|
1160
1152
|
// 完成事件
|
|
@@ -1169,6 +1161,29 @@ export class MessageProcessor {
|
|
|
1169
1161
|
}
|
|
1170
1162
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1171
1163
|
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1164
|
+
// proactive 模式:每轮 LLM 调用完成后写一条 thought 到 messages.jsonl
|
|
1165
|
+
// 这样 thought 数 = LLM 调用轮数,而不是 chunk 数
|
|
1166
|
+
if (session.sessionMode === 'proactive' && lastReplyText) {
|
|
1167
|
+
try {
|
|
1168
|
+
const chatDir = this.sessionManager.getChatDir(session);
|
|
1169
|
+
const sessionEncrypt = this.sessionManager.getSessionEncrypt(session.id);
|
|
1170
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
1171
|
+
from: session.selfId || 'self',
|
|
1172
|
+
to: session.metadata?.peerId ?? session.channelId,
|
|
1173
|
+
chatType: (session.chatType ?? 'private'),
|
|
1174
|
+
groupId: session.metadata?.groupId ?? null,
|
|
1175
|
+
msgId: `thought-${session.id}-${Date.now()}`,
|
|
1176
|
+
content: lastReplyText,
|
|
1177
|
+
agent: session.agentId || null,
|
|
1178
|
+
model: null,
|
|
1179
|
+
durationMs: null,
|
|
1180
|
+
encrypt: sessionEncrypt ?? undefined,
|
|
1181
|
+
chatmode: 'proactive',
|
|
1182
|
+
msgType: 'thought',
|
|
1183
|
+
}));
|
|
1184
|
+
}
|
|
1185
|
+
catch { }
|
|
1186
|
+
}
|
|
1172
1187
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
1173
1188
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
1174
1189
|
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
@@ -1178,11 +1193,11 @@ export class MessageProcessor {
|
|
|
1178
1193
|
|| /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
|
|
1179
1194
|
|| /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
|
|
1180
1195
|
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1181
|
-
const errorSummary = event.errors?.join('; ') || '
|
|
1182
|
-
// 使用 terminalReason
|
|
1196
|
+
const errorSummary = event.errors?.join('; ') || '任务执行失败';
|
|
1197
|
+
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
1183
1198
|
const userFriendlyMessage = event.terminalReason
|
|
1184
|
-
? getErrorMessage(null, event.terminalReason)
|
|
1185
|
-
:
|
|
1199
|
+
? getErrorMessage(null, event.terminalReason, false)
|
|
1200
|
+
: errorSummary;
|
|
1186
1201
|
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
1187
1202
|
}
|
|
1188
1203
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
@@ -1229,6 +1244,7 @@ export class MessageProcessor {
|
|
|
1229
1244
|
finalText: lastReplyText || event.result || undefined,
|
|
1230
1245
|
durationMs: event.durationMs,
|
|
1231
1246
|
agentName: agentNameForStats,
|
|
1247
|
+
numTurns: event.numTurns,
|
|
1232
1248
|
timestamp: Date.now()
|
|
1233
1249
|
});
|
|
1234
1250
|
}
|
|
@@ -1282,7 +1298,7 @@ export class MessageProcessor {
|
|
|
1282
1298
|
logger.error('[MessageProcessor] Stream processing error:', error);
|
|
1283
1299
|
}
|
|
1284
1300
|
if (error instanceof Error && error.message.includes('process exited')) {
|
|
1285
|
-
renderer.addNotice('
|
|
1301
|
+
renderer.addNotice('Claude Code 进程异常退出,请重试', 'warn', 'process-exit', true);
|
|
1286
1302
|
}
|
|
1287
1303
|
// Flush any pending error activities before re-throwing,
|
|
1288
1304
|
// and mark the error so outer catch won't send a duplicate message
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerIdentityCache - 对端身份缓存管理
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 从对端的 agent.md 确定身份(human / agent)
|
|
6
|
+
* 2. 缓存到关系层文件(30天时效)
|
|
7
|
+
* 3. 支持入站和出站消息的身份查询
|
|
8
|
+
*
|
|
9
|
+
* 信源:对端的 agent.md(通过 AUN SDK 下载并验签)
|
|
10
|
+
* 判定规则:type !== 'human' → agent
|
|
11
|
+
* 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import { logger } from '../../utils/logger.js';
|
|
17
|
+
/**
|
|
18
|
+
* 对端身份缓存管理器
|
|
19
|
+
*/
|
|
20
|
+
export class PeerIdentityCache {
|
|
21
|
+
/** 缓存最大时效:30 天 */
|
|
22
|
+
static CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* 获取 peer-identity.json 文件路径
|
|
25
|
+
*/
|
|
26
|
+
static getFilePath(channel, peerId, agentDir) {
|
|
27
|
+
const peerKey = `${channel}#${encodeURIComponent(peerId)}`;
|
|
28
|
+
return path.join(agentDir, 'relations', peerKey, 'peer-identity.json');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 从文件读取缓存
|
|
32
|
+
* @returns PeerIdentity | null(缓存不存在)
|
|
33
|
+
*/
|
|
34
|
+
static get(channel, peerId, agentDir) {
|
|
35
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 检查缓存是否需要刷新
|
|
46
|
+
* @param maxAgeMs 最大缓存时间(默认 30 天)
|
|
47
|
+
* @returns true=需要刷新
|
|
48
|
+
*/
|
|
49
|
+
static needsRefresh(channel, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
|
|
50
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
51
|
+
if (!cached)
|
|
52
|
+
return true;
|
|
53
|
+
return Date.now() - cached.lastCheckedAt > maxAgeMs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 从 agent.md 更新身份信息
|
|
57
|
+
* @param agentMd 已验签的 agent.md 内容
|
|
58
|
+
*/
|
|
59
|
+
static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
|
|
60
|
+
// 解析 type 和 name
|
|
61
|
+
const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
|
|
62
|
+
const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
63
|
+
const type = typeMatch?.[1] || 'unknown';
|
|
64
|
+
const isAgent = type !== 'human';
|
|
65
|
+
const name = nameMatch?.[1]?.trim();
|
|
66
|
+
// 计算 hash
|
|
67
|
+
const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
|
|
68
|
+
// 构建身份信息
|
|
69
|
+
const identity = {
|
|
70
|
+
aid: peerId,
|
|
71
|
+
type,
|
|
72
|
+
isAgent,
|
|
73
|
+
name,
|
|
74
|
+
agentMdHash,
|
|
75
|
+
verifiedAt,
|
|
76
|
+
lastCheckedAt: Date.now(),
|
|
77
|
+
source: 'agentmd',
|
|
78
|
+
};
|
|
79
|
+
// 写入文件
|
|
80
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
84
|
+
logger.debug(`[PeerIdentityCache] Updated: ${channel}#${peerId} type=${type} isAgent=${isAgent}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
|
|
88
|
+
}
|
|
89
|
+
return identity;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 标记为 unknown(验签失败或无 agent.md)
|
|
93
|
+
*/
|
|
94
|
+
static markUnknown(channel, peerId, agentDir) {
|
|
95
|
+
const identity = {
|
|
96
|
+
aid: peerId,
|
|
97
|
+
type: 'unknown',
|
|
98
|
+
isAgent: true, // 验签失败 → 当做 agent(安全策略)
|
|
99
|
+
agentMdHash: '',
|
|
100
|
+
verifiedAt: 0,
|
|
101
|
+
lastCheckedAt: Date.now(),
|
|
102
|
+
source: 'unknown',
|
|
103
|
+
};
|
|
104
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
107
|
+
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
108
|
+
logger.debug(`[PeerIdentityCache] Marked unknown: ${channel}#${peerId}`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
logger.warn(`[PeerIdentityCache] Failed to write unknown cache: ${filePath} err=${err}`);
|
|
112
|
+
}
|
|
113
|
+
return identity;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 完整流程:检查缓存 → 需要刷新则下载 agent.md → 更新缓存
|
|
117
|
+
*
|
|
118
|
+
* @param channel 渠道类型(如 'aun')
|
|
119
|
+
* @param peerId 对端 ID(AUN 是 AID)
|
|
120
|
+
* @param agentDir agent 数据根目录
|
|
121
|
+
* @param aunClient AUN SDK client(需要有 fetchAgentMd 方法)
|
|
122
|
+
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
123
|
+
* @returns PeerIdentity
|
|
124
|
+
*/
|
|
125
|
+
static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
|
|
126
|
+
// 1. 检查缓存
|
|
127
|
+
if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
|
|
128
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
129
|
+
if (cached) {
|
|
130
|
+
logger.debug(`[PeerIdentityCache] Cache hit: ${channel}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
|
|
131
|
+
return cached;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 2. 下载并验签 agent.md(SDK 自动验签)
|
|
135
|
+
try {
|
|
136
|
+
logger.debug(`[PeerIdentityCache] Fetching agent.md: ${channel}#${peerId}`);
|
|
137
|
+
const result = await aunClient.fetchAgentMd(peerId);
|
|
138
|
+
const agentMd = result.content;
|
|
139
|
+
// 3. 更新缓存
|
|
140
|
+
return this.updateFromAgentMd(channel, peerId, agentDir, agentMd, Date.now());
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
// 验签失败或下载失败 → 标记为 unknown,当做 agent
|
|
144
|
+
logger.warn(`[PeerIdentityCache] Failed to fetch agent.md: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
return this.markUnknown(channel, peerId, agentDir);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 清除指定对端的缓存
|
|
150
|
+
*/
|
|
151
|
+
static clear(channel, peerId, agentDir) {
|
|
152
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(filePath);
|
|
155
|
+
logger.debug(`[PeerIdentityCache] Cleared: ${channel}#${peerId}`);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// 文件不存在,忽略
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -32,11 +32,15 @@ export class SessionManager {
|
|
|
32
32
|
this.sessionModeResolver = resolver;
|
|
33
33
|
}
|
|
34
34
|
resolveDefaultSessionMode(channel, chatType, peerType) {
|
|
35
|
-
|
|
35
|
+
const ct = chatType || 'private';
|
|
36
|
+
// 来源2:群聊强制 proactive
|
|
37
|
+
if (ct === 'group')
|
|
38
|
+
return 'proactive';
|
|
39
|
+
// 来源3:非 human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
|
|
36
40
|
if (peerType && peerType !== 'human' && peerType !== 'unknown')
|
|
37
41
|
return 'proactive';
|
|
38
|
-
|
|
39
|
-
const resolved = this.sessionModeResolver?.(channel, ct);
|
|
42
|
+
// 来源1:agent 配置默认值
|
|
43
|
+
const resolved = this.sessionModeResolver?.(channel, ct, peerType);
|
|
40
44
|
return resolved || 'interactive';
|
|
41
45
|
}
|
|
42
46
|
registerFileAdapter(adapter) {
|
|
@@ -92,6 +92,22 @@ export class TriggerManager {
|
|
|
92
92
|
}
|
|
93
93
|
return { active, history };
|
|
94
94
|
}
|
|
95
|
+
update(id, patch) {
|
|
96
|
+
const t = this.triggers.get(id);
|
|
97
|
+
if (!t)
|
|
98
|
+
throw new Error(`触发器不存在:${id}`);
|
|
99
|
+
// Check name uniqueness if name is being changed
|
|
100
|
+
if (patch.name && patch.name !== t.name) {
|
|
101
|
+
for (const other of this.triggers.values()) {
|
|
102
|
+
if (other.id !== id && other.name === patch.name) {
|
|
103
|
+
throw new Error(`触发器名称已存在:${patch.name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
Object.assign(t, patch, { updatedAt: Date.now() });
|
|
108
|
+
this.save();
|
|
109
|
+
return t;
|
|
110
|
+
}
|
|
95
111
|
updateFireStats(id, firedAt) {
|
|
96
112
|
const t = this.triggers.get(id);
|
|
97
113
|
if (!t)
|
|
@@ -43,6 +43,116 @@ export function validateCronExpr(expr) {
|
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
export function parseTriggerUpdate(args) {
|
|
47
|
+
// First token is the trigger name/id, rest is flags
|
|
48
|
+
const trimmed = args.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
return { ok: false, error: '用法:/trigger update <名称|ID> [--参数...]' };
|
|
51
|
+
}
|
|
52
|
+
// Extract nameOrId: first non-flag token (could be quoted)
|
|
53
|
+
let nameOrId;
|
|
54
|
+
let rest;
|
|
55
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
56
|
+
const quote = trimmed[0];
|
|
57
|
+
const end = trimmed.indexOf(quote, 1);
|
|
58
|
+
if (end === -1) {
|
|
59
|
+
return { ok: false, error: '名称引号未闭合' };
|
|
60
|
+
}
|
|
61
|
+
nameOrId = trimmed.slice(1, end);
|
|
62
|
+
rest = trimmed.slice(end + 1).trim();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
66
|
+
if (spaceIdx === -1) {
|
|
67
|
+
return { ok: false, error: '至少需要指定一个修改参数(如 --prompt、--delay 等)' };
|
|
68
|
+
}
|
|
69
|
+
nameOrId = trimmed.slice(0, spaceIdx);
|
|
70
|
+
rest = trimmed.slice(spaceIdx + 1).trim();
|
|
71
|
+
}
|
|
72
|
+
if (!rest) {
|
|
73
|
+
return { ok: false, error: '至少需要指定一个修改参数(如 --prompt、--delay 等)' };
|
|
74
|
+
}
|
|
75
|
+
const flags = parseFlags(rest);
|
|
76
|
+
if (flags.size === 0) {
|
|
77
|
+
return { ok: false, error: '至少需要指定一个修改参数(如 --prompt、--delay 等)' };
|
|
78
|
+
}
|
|
79
|
+
const result = {};
|
|
80
|
+
// Parse schedule if provided (only one allowed)
|
|
81
|
+
const hasDelay = flags.has('delay');
|
|
82
|
+
const hasAt = flags.has('at');
|
|
83
|
+
const hasCron = flags.has('cron');
|
|
84
|
+
const timeCount = [hasDelay, hasAt, hasCron].filter(Boolean).length;
|
|
85
|
+
if (timeCount > 1) {
|
|
86
|
+
return { ok: false, error: '--delay、--at、--cron 互斥,只能指定一个' };
|
|
87
|
+
}
|
|
88
|
+
if (hasDelay) {
|
|
89
|
+
const raw = flags.get('delay');
|
|
90
|
+
const ms = parseDuration(raw);
|
|
91
|
+
if (ms === null)
|
|
92
|
+
return { ok: false, error: `无法解析 --delay "${raw}",支持格式:30m、2h、1d、2h30m` };
|
|
93
|
+
result.scheduleType = 'delay';
|
|
94
|
+
result.scheduleValue = String(ms);
|
|
95
|
+
}
|
|
96
|
+
else if (hasAt) {
|
|
97
|
+
const raw = flags.get('at');
|
|
98
|
+
const ts = parseIsoDate(raw);
|
|
99
|
+
if (ts === null)
|
|
100
|
+
return { ok: false, error: `无法解析 --at "${raw}",请使用 ISO 格式,如 2026-05-15T09:00` };
|
|
101
|
+
if (ts <= Date.now())
|
|
102
|
+
return { ok: false, error: `--at 时间已过期:${raw}` };
|
|
103
|
+
result.scheduleType = 'at';
|
|
104
|
+
result.scheduleValue = new Date(ts).toISOString();
|
|
105
|
+
}
|
|
106
|
+
else if (hasCron) {
|
|
107
|
+
const raw = flags.get('cron');
|
|
108
|
+
if (!validateCronExpr(raw))
|
|
109
|
+
return { ok: false, error: `无效的 cron 表达式:"${raw}"` };
|
|
110
|
+
result.scheduleType = 'cron';
|
|
111
|
+
result.scheduleValue = raw;
|
|
112
|
+
}
|
|
113
|
+
// Parse optional fields
|
|
114
|
+
if (flags.has('prompt')) {
|
|
115
|
+
const prompt = flags.get('prompt');
|
|
116
|
+
if (!prompt || prompt === true)
|
|
117
|
+
return { ok: false, error: '--prompt 不能为空' };
|
|
118
|
+
if (typeof prompt === 'string' && prompt.length > 4096)
|
|
119
|
+
return { ok: false, error: '--prompt 超过 4096 字符限制' };
|
|
120
|
+
result.prompt = prompt;
|
|
121
|
+
}
|
|
122
|
+
if (flags.has('name')) {
|
|
123
|
+
const name = flags.get('name');
|
|
124
|
+
if (!name || name === true)
|
|
125
|
+
return { ok: false, error: '--name 不能为空' };
|
|
126
|
+
result.name = name;
|
|
127
|
+
}
|
|
128
|
+
if (flags.has('session')) {
|
|
129
|
+
const sv = flags.get('session');
|
|
130
|
+
if (sv !== 'latest' && sv !== 'silent')
|
|
131
|
+
return { ok: false, error: '--session 只接受 latest 或 silent' };
|
|
132
|
+
result.targetSessionStrategy = sv;
|
|
133
|
+
}
|
|
134
|
+
if (flags.has('agent')) {
|
|
135
|
+
const agent = flags.get('agent');
|
|
136
|
+
if (!agent || agent === true)
|
|
137
|
+
return { ok: false, error: '--agent 不能为空' };
|
|
138
|
+
result.agentId = agent;
|
|
139
|
+
}
|
|
140
|
+
const hasChannel = flags.has('channel');
|
|
141
|
+
const hasChannelId = flags.has('channelid');
|
|
142
|
+
if (hasChannel !== hasChannelId) {
|
|
143
|
+
return { ok: false, error: '--channel 与 --channelid 必须同时指定或同时省略' };
|
|
144
|
+
}
|
|
145
|
+
if (hasChannel) {
|
|
146
|
+
result.targetChannel = flags.get('channel');
|
|
147
|
+
result.targetChannelId = flags.get('channelid');
|
|
148
|
+
}
|
|
149
|
+
if (flags.has('thread')) {
|
|
150
|
+
if (flags.has('session'))
|
|
151
|
+
return { ok: false, error: '--thread 与 --session 互斥' };
|
|
152
|
+
result.targetThreadId = flags.get('thread');
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, nameOrId, value: result };
|
|
155
|
+
}
|
|
46
156
|
export function parseTriggerSet(args) {
|
|
47
157
|
const flags = parseFlags(args);
|
|
48
158
|
const hasDelay = flags.has('delay');
|
|
@@ -137,6 +137,12 @@ export class TriggerScheduler {
|
|
|
137
137
|
this.inflightCron.delete(id);
|
|
138
138
|
this.resetTimer();
|
|
139
139
|
}
|
|
140
|
+
update(trigger) {
|
|
141
|
+
this.heap.remove(trigger.id);
|
|
142
|
+
this.heap.push(trigger);
|
|
143
|
+
this.resetTimer();
|
|
144
|
+
this.eventBus.publish({ type: 'trigger:updated', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId });
|
|
145
|
+
}
|
|
140
146
|
stop() {
|
|
141
147
|
if (this.timer) {
|
|
142
148
|
clearTimeout(this.timer);
|
package/dist/index.js
CHANGED
|
@@ -124,6 +124,46 @@ function readFastaunVersion() {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
async function main() {
|
|
127
|
+
// 启动信息:目录类型 + 版本号 + 代码最新时间戳
|
|
128
|
+
{
|
|
129
|
+
const pkgRoot = getPackageRoot();
|
|
130
|
+
const runDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'));
|
|
131
|
+
const isDist = runDir.includes(path.join(pkgRoot, 'dist'));
|
|
132
|
+
const isLinked = fs.existsSync(path.join(pkgRoot, '.git'));
|
|
133
|
+
const dirType = isDist ? (isLinked ? '开发仓/dist' : '安装路径/dist') : '源码(tsx)';
|
|
134
|
+
const scanDir = isDist ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
|
|
135
|
+
let latestMtime = 0;
|
|
136
|
+
const scanRecursive = (dir) => {
|
|
137
|
+
try {
|
|
138
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
139
|
+
if (entry.name === 'node_modules')
|
|
140
|
+
continue;
|
|
141
|
+
const full = path.join(dir, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
scanRecursive(full);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
147
|
+
const mt = fs.statSync(full).mtimeMs;
|
|
148
|
+
if (mt > latestMtime)
|
|
149
|
+
latestMtime = mt;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
};
|
|
155
|
+
scanRecursive(scanDir);
|
|
156
|
+
let version = '?';
|
|
157
|
+
try {
|
|
158
|
+
version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
const fmtTime = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; };
|
|
162
|
+
console.error(`[EvolClaw] EvolClaw v${version}`);
|
|
163
|
+
console.error(`[EvolClaw] 执行类型: ${dirType}`);
|
|
164
|
+
console.error(`[EvolClaw] 包路径: ${pkgRoot}`);
|
|
165
|
+
console.error(`[EvolClaw] 代码时间: ${latestMtime ? fmtTime(latestMtime) : '?'}`);
|
|
166
|
+
}
|
|
127
167
|
// 过滤飞书 SDK 的 info 日志
|
|
128
168
|
const originalLog = console.log;
|
|
129
169
|
const originalInfo = console.info;
|
|
@@ -261,16 +301,22 @@ async function main() {
|
|
|
261
301
|
// 统计收集器(近 1 小时滚动统计)
|
|
262
302
|
const statsCollector = new StatsCollector(eventBus);
|
|
263
303
|
// Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
|
|
264
|
-
const aidStatsCollector = new AidStatsCollector();
|
|
304
|
+
const aidStatsCollector = new AidStatsCollector(eventBus);
|
|
305
|
+
aidStatsCollector.setSessionsDir(paths.sessionsDir);
|
|
265
306
|
// 初始化 SessionManager(文件系统后端)
|
|
266
307
|
const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
|
|
267
308
|
// sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
|
|
268
|
-
sessionManager.setSessionModeResolver((channelKey, chatType) => {
|
|
309
|
+
sessionManager.setSessionModeResolver((channelKey, chatType, peerType) => {
|
|
269
310
|
const agent = agentRegistry.resolveByChannel(channelKey);
|
|
270
311
|
const cm = agent?.config.chatmode;
|
|
271
312
|
if (!cm)
|
|
272
313
|
return undefined;
|
|
273
|
-
|
|
314
|
+
// 优先级:群聊 > nothuman > private
|
|
315
|
+
if (chatType === 'group')
|
|
316
|
+
return cm.group;
|
|
317
|
+
if (peerType && peerType !== 'human' && peerType !== 'unknown')
|
|
318
|
+
return cm.nothuman;
|
|
319
|
+
return cm.private;
|
|
274
320
|
});
|
|
275
321
|
logger.info('✓ Database initialized');
|
|
276
322
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|