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
|
@@ -2,7 +2,6 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { hasCompact } from '../../agents/claude-runner.js';
|
|
5
|
-
import { appendMessageLog, buildOutboundEntry } from './message-log.js';
|
|
6
5
|
import { IMRenderer } from './im-renderer.js';
|
|
7
6
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
8
7
|
import { logger } from '../../utils/logger.js';
|
|
@@ -396,7 +395,8 @@ export class MessageProcessor {
|
|
|
396
395
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
397
396
|
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
397
|
// 记录开始处理
|
|
399
|
-
|
|
398
|
+
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
399
|
+
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode: session.sessionMode || 'interactive' });
|
|
400
400
|
// 触发器消息不发 processing status(无需通知用户)
|
|
401
401
|
if (message.source !== 'trigger') {
|
|
402
402
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
@@ -581,7 +581,7 @@ export class MessageProcessor {
|
|
|
581
581
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
582
582
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
583
583
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
584
|
-
renderer.addNotice(
|
|
584
|
+
renderer.addNotice(`API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
|
|
585
585
|
await renderer.flush();
|
|
586
586
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
587
587
|
continue;
|
|
@@ -593,12 +593,12 @@ export class MessageProcessor {
|
|
|
593
593
|
catch (error) {
|
|
594
594
|
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
|
|
595
595
|
// 尝试 compact 压缩会话
|
|
596
|
-
renderer.addNotice('
|
|
596
|
+
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
597
597
|
await renderer.flush();
|
|
598
598
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
599
599
|
if (compacted) {
|
|
600
600
|
// compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
|
|
601
|
-
renderer.addNotice('
|
|
601
|
+
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
602
602
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
603
603
|
agent.registerStream(streamKey, retryStream);
|
|
604
604
|
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
@@ -620,11 +620,11 @@ export class MessageProcessor {
|
|
|
620
620
|
contextTooLongPattern.test(errorsText) ||
|
|
621
621
|
contextTooLongPattern.test(streamResult.fullText));
|
|
622
622
|
if (isPromptTooLong) {
|
|
623
|
-
renderer.addNotice('
|
|
623
|
+
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
624
624
|
await renderer.flush();
|
|
625
625
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
626
626
|
if (compacted) {
|
|
627
|
-
renderer.addNotice('✅
|
|
627
|
+
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
628
628
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
629
629
|
agent.registerStream(streamKey, retryStream);
|
|
630
630
|
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
@@ -638,7 +638,7 @@ export class MessageProcessor {
|
|
|
638
638
|
contextTooLongPattern.test(errorsText) ||
|
|
639
639
|
contextTooLongPattern.test(streamResult.fullText))) {
|
|
640
640
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
641
|
-
renderer.addNotice('
|
|
641
|
+
renderer.addNotice('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
|
|
642
642
|
}
|
|
643
643
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
644
644
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
@@ -829,6 +829,7 @@ export class MessageProcessor {
|
|
|
829
829
|
finalText: streamResult.lastReplyText || undefined,
|
|
830
830
|
durationMs: Date.now() - startTime,
|
|
831
831
|
agentName: agentNameForStats,
|
|
832
|
+
numTurns: streamResult.numTurns,
|
|
832
833
|
timestamp: Date.now()
|
|
833
834
|
});
|
|
834
835
|
// 记录处理完成
|
|
@@ -839,24 +840,8 @@ export class MessageProcessor {
|
|
|
839
840
|
status: 'completed',
|
|
840
841
|
duration: Date.now() - startTime
|
|
841
842
|
});
|
|
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
|
-
}
|
|
843
|
+
// 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
|
|
844
|
+
// 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
|
|
860
845
|
}
|
|
861
846
|
const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
862
847
|
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
@@ -980,7 +965,13 @@ export class MessageProcessor {
|
|
|
980
965
|
return { session, absoluteProjectPath };
|
|
981
966
|
}
|
|
982
967
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
|
|
983
|
-
//
|
|
968
|
+
// 兜底纠正1:群聊强制 proactive
|
|
969
|
+
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
970
|
+
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
971
|
+
session.sessionMode = 'proactive';
|
|
972
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
973
|
+
}
|
|
974
|
+
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
984
975
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
985
976
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
986
977
|
logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
|
|
@@ -1076,7 +1067,7 @@ export class MessageProcessor {
|
|
|
1076
1067
|
lastReplyText += event.text;
|
|
1077
1068
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
|
|
1078
1069
|
if (!shouldSuppress()) {
|
|
1079
|
-
renderer.addText(event.text);
|
|
1070
|
+
renderer.addText(event.text, event.outputTokens, event.turn);
|
|
1080
1071
|
}
|
|
1081
1072
|
}
|
|
1082
1073
|
// compact 完成
|
|
@@ -1118,7 +1109,7 @@ export class MessageProcessor {
|
|
|
1118
1109
|
if (event.callId) {
|
|
1119
1110
|
toolDescByCallId.set(event.callId, desc);
|
|
1120
1111
|
}
|
|
1121
|
-
renderer.addToolCall(event.name, event.input, event.callId, desc);
|
|
1112
|
+
renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
|
|
1122
1113
|
}
|
|
1123
1114
|
}
|
|
1124
1115
|
// 工具结果
|
|
@@ -1154,7 +1145,7 @@ export class MessageProcessor {
|
|
|
1154
1145
|
const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
|
|
1155
1146
|
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1156
1147
|
hasErrorResult = true;
|
|
1157
|
-
renderer.addNotice(
|
|
1148
|
+
renderer.addNotice(`${event.error}`, 'warn', 'runtime-error', true);
|
|
1158
1149
|
}
|
|
1159
1150
|
}
|
|
1160
1151
|
// 完成事件
|
|
@@ -1169,6 +1160,8 @@ export class MessageProcessor {
|
|
|
1169
1160
|
}
|
|
1170
1161
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1171
1162
|
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1163
|
+
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1164
|
+
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1172
1165
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
1173
1166
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
1174
1167
|
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
@@ -1178,11 +1171,11 @@ export class MessageProcessor {
|
|
|
1178
1171
|
|| /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
|
|
1179
1172
|
|| /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
|
|
1180
1173
|
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1181
|
-
const errorSummary = event.errors?.join('; ') || '
|
|
1182
|
-
// 使用 terminalReason
|
|
1174
|
+
const errorSummary = event.errors?.join('; ') || '任务执行失败';
|
|
1175
|
+
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
1183
1176
|
const userFriendlyMessage = event.terminalReason
|
|
1184
|
-
? getErrorMessage(null, event.terminalReason)
|
|
1185
|
-
:
|
|
1177
|
+
? getErrorMessage(null, event.terminalReason, false)
|
|
1178
|
+
: errorSummary;
|
|
1186
1179
|
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
1187
1180
|
}
|
|
1188
1181
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
@@ -1229,6 +1222,7 @@ export class MessageProcessor {
|
|
|
1229
1222
|
finalText: lastReplyText || event.result || undefined,
|
|
1230
1223
|
durationMs: event.durationMs,
|
|
1231
1224
|
agentName: agentNameForStats,
|
|
1225
|
+
numTurns: event.numTurns,
|
|
1232
1226
|
timestamp: Date.now()
|
|
1233
1227
|
});
|
|
1234
1228
|
}
|
|
@@ -1282,7 +1276,7 @@ export class MessageProcessor {
|
|
|
1282
1276
|
logger.error('[MessageProcessor] Stream processing error:', error);
|
|
1283
1277
|
}
|
|
1284
1278
|
if (error instanceof Error && error.message.includes('process exited')) {
|
|
1285
|
-
renderer.addNotice('
|
|
1279
|
+
renderer.addNotice('Claude Code 进程异常退出,请重试', 'warn', 'process-exit', true);
|
|
1286
1280
|
}
|
|
1287
1281
|
// Flush any pending error activities before re-throwing,
|
|
1288
1282
|
// 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
|
+
}
|
|
@@ -146,6 +146,29 @@ export function scanChatDirs(sessionsDir) {
|
|
|
146
146
|
if (!typeEntry.isDirectory())
|
|
147
147
|
continue;
|
|
148
148
|
const channelType = typeEntry.name;
|
|
149
|
+
// 包含 '#' 的目录是旧 channelKey 格式(如 'aun#dddd.agentid.pub#main'),
|
|
150
|
+
// 按通用 channel 布局扫描(sessionsDir/{channelKey}/{encodedChannelId}/),保持兼容
|
|
151
|
+
if (channelType.includes('#')) {
|
|
152
|
+
const typeDir = path.join(sessionsDir, channelType);
|
|
153
|
+
let chatEntries;
|
|
154
|
+
try {
|
|
155
|
+
chatEntries = fs.readdirSync(typeDir, { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const chatEntry of chatEntries) {
|
|
161
|
+
if (!chatEntry.isDirectory())
|
|
162
|
+
continue;
|
|
163
|
+
results.push({
|
|
164
|
+
channelType,
|
|
165
|
+
selfId: null,
|
|
166
|
+
channelId: decodeSegment(chatEntry.name),
|
|
167
|
+
dirPath: path.join(typeDir, chatEntry.name),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
149
172
|
const typeDir = path.join(sessionsDir, channelType);
|
|
150
173
|
if (channelType === 'aun') {
|
|
151
174
|
// aun 下还有一层 selfId
|
|
@@ -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) {
|
|
@@ -536,7 +540,10 @@ export class SessionManager {
|
|
|
536
540
|
this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
|
|
537
541
|
}
|
|
538
542
|
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType) {
|
|
539
|
-
|
|
543
|
+
// 优先使用精确路径(channelType + selfId),避免 fallback 到错误目录
|
|
544
|
+
const chatDir = (channelType && selfId)
|
|
545
|
+
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfId); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
|
|
546
|
+
: this.ensureResolvedChatDir(channelType || channel, channelId);
|
|
540
547
|
const threadIndex = readThreadIndex(chatDir);
|
|
541
548
|
const existingMetaId = threadIndex[threadId];
|
|
542
549
|
if (existingMetaId) {
|
|
@@ -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);
|