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.
Files changed (45) hide show
  1. package/CHANGELOG.md +407 -0
  2. package/README.md +1 -1
  3. package/SKILLS.md +311 -0
  4. package/dist/agents/claude-runner.js +40 -3
  5. package/dist/aun/aid/agentmd.js +7 -6
  6. package/dist/aun/aid/client.js +5 -11
  7. package/dist/aun/aid/identity.js +32 -13
  8. package/dist/aun/msg/group.js +1 -0
  9. package/dist/aun/msg/p2p.js +51 -0
  10. package/dist/aun/msg/upload.js +57 -18
  11. package/dist/channels/aun.js +124 -50
  12. package/dist/channels/dingtalk.js +2 -0
  13. package/dist/channels/feishu.js +15 -6
  14. package/dist/channels/qqbot.js +2 -0
  15. package/dist/channels/wechat.js +2 -0
  16. package/dist/channels/wecom.js +2 -0
  17. package/dist/cli/agent.js +130 -35
  18. package/dist/cli/index.js +221 -48
  19. package/dist/cli/init-channel.js +4 -2
  20. package/dist/cli/init.js +44 -23
  21. package/dist/cli/watch-msg.js +109 -30
  22. package/dist/config-store.js +67 -1
  23. package/dist/core/channel-loader.js +4 -4
  24. package/dist/core/command-handler.js +95 -84
  25. package/dist/core/evolagent-registry.js +45 -9
  26. package/dist/core/evolagent.js +4 -4
  27. package/dist/core/message/im-renderer.js +47 -8
  28. package/dist/core/message/message-bridge.js +30 -1
  29. package/dist/core/message/message-log.js +6 -1
  30. package/dist/core/message/message-processor.js +29 -35
  31. package/dist/core/relation/peer-identity.js +161 -0
  32. package/dist/core/session/session-fs-store.js +23 -0
  33. package/dist/core/session/session-manager.js +11 -4
  34. package/dist/core/trigger/manager.js +16 -0
  35. package/dist/core/trigger/parser.js +110 -0
  36. package/dist/core/trigger/scheduler.js +6 -0
  37. package/dist/index.js +64 -20
  38. package/dist/paths.js +35 -0
  39. package/dist/utils/cross-platform.js +2 -1
  40. package/dist/utils/error-utils.js +17 -13
  41. package/dist/utils/stats.js +216 -2
  42. package/kits/docs/INDEX.md +6 -0
  43. package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
  44. package/kits/rules/06-channel.md +30 -0
  45. 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
- this.eventBus.publish({ type: 'task:started', sessionId: session.id });
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(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
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('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
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('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
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('⚠️ 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
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('✅ 压缩完成,正在重试...', 'info', 'compact-retry', true);
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('⚠️ 上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
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
- 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
- }
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
- // 兜底纠正:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive
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(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
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('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
1182
- // 使用 terminalReason 提供更友好的错误提示
1174
+ const errorSummary = event.errors?.join('; ') || '任务执行失败';
1175
+ // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
1183
1176
  const userFriendlyMessage = event.terminalReason
1184
- ? getErrorMessage(null, event.terminalReason)
1185
- : `\u274c ${errorSummary}`;
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('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
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
- // 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) {
@@ -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
- const chatDir = this.ensureResolvedChatDir(channel, channelId);
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);