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.
@@ -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
- this.eventBus.publish({ type: 'task:started', sessionId: session.id });
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(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
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('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
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('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
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('⚠️ 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
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('✅ 压缩完成,正在重试...', 'info', 'compact-retry', true);
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('⚠️ 上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
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
- if (streamResult.lastReplyText || streamResult.fullText) {
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
- // 兜底纠正:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive
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(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
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('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
1182
- // 使用 terminalReason 提供更友好的错误提示
1196
+ const errorSummary = event.errors?.join('; ') || '任务执行失败';
1197
+ // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
1183
1198
  const userFriendlyMessage = event.terminalReason
1184
- ? getErrorMessage(null, event.terminalReason)
1185
- : `\u274c ${errorSummary}`;
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('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
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
- // human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
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
- const ct = chatType || 'private';
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
- return chatType === 'group' ? cm.group : cm.private;
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 各自的会话文件操作)