evolclaw 2.8.3 → 3.1.0

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 (142) hide show
  1. package/README.md +21 -12
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +108 -46
  5. package/dist/agents/codex-runner.js +13 -14
  6. package/dist/agents/gemini-runner.js +15 -17
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/agents/resolve.js +134 -0
  9. package/dist/aun/aid/agentmd.js +186 -0
  10. package/dist/aun/aid/client.js +134 -0
  11. package/dist/aun/aid/identity.js +159 -0
  12. package/dist/aun/aid/index.js +3 -0
  13. package/dist/aun/aid/lifecycle-log.js +33 -0
  14. package/dist/aun/aid/types.js +1 -0
  15. package/dist/aun/aid/validation.js +21 -0
  16. package/dist/aun/msg/group.js +293 -0
  17. package/dist/aun/msg/index.js +4 -0
  18. package/dist/aun/msg/p2p.js +147 -0
  19. package/dist/aun/msg/payload-type.js +27 -0
  20. package/dist/aun/msg/upload.js +98 -0
  21. package/dist/aun/outbox.js +138 -0
  22. package/dist/aun/rpc/caller.js +42 -0
  23. package/dist/aun/rpc/connection.js +34 -0
  24. package/dist/aun/rpc/index.js +2 -0
  25. package/dist/aun/storage/download.js +29 -0
  26. package/dist/aun/storage/index.js +3 -0
  27. package/dist/aun/storage/manage.js +10 -0
  28. package/dist/aun/storage/upload.js +35 -0
  29. package/dist/channels/aun.js +1340 -349
  30. package/dist/channels/dingtalk.js +59 -5
  31. package/dist/channels/feishu.js +381 -32
  32. package/dist/channels/qqbot.js +68 -12
  33. package/dist/channels/wechat.js +63 -4
  34. package/dist/channels/wecom.js +59 -5
  35. package/dist/cli/agent.js +800 -0
  36. package/dist/cli/bench.js +1219 -0
  37. package/dist/cli/index.js +4513 -0
  38. package/dist/{utils → cli}/init-channel.js +211 -621
  39. package/dist/cli/init.js +178 -0
  40. package/dist/cli/link-rules.js +245 -0
  41. package/dist/cli/net-check.js +640 -0
  42. package/dist/cli/watch-msg.js +589 -0
  43. package/dist/config-store.js +645 -0
  44. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  45. package/dist/core/channel-loader.js +176 -12
  46. package/dist/core/command-handler.js +883 -848
  47. package/dist/core/evolagent-registry.js +191 -371
  48. package/dist/core/evolagent.js +202 -238
  49. package/dist/core/interaction-router.js +52 -5
  50. package/dist/core/message/im-renderer.js +486 -0
  51. package/dist/core/message/items-formatter.js +68 -0
  52. package/dist/core/message/message-bridge.js +109 -56
  53. package/dist/core/message/message-log.js +93 -0
  54. package/dist/core/message/message-processor.js +430 -212
  55. package/dist/core/message/message-queue.js +13 -6
  56. package/dist/core/permission.js +116 -11
  57. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  58. package/dist/core/session/session-fs-store.js +230 -0
  59. package/dist/core/session/session-manager.js +740 -777
  60. package/dist/core/session/session-mapper.js +87 -0
  61. package/dist/core/trigger/manager.js +122 -0
  62. package/dist/core/trigger/parser.js +128 -0
  63. package/dist/core/trigger/scheduler.js +224 -0
  64. package/dist/data/error-dict.json +118 -0
  65. package/dist/eck/baseagent-caps.js +18 -0
  66. package/dist/eck/detect.js +47 -0
  67. package/dist/eck/init.js +77 -0
  68. package/dist/eck/rules-loader.js +28 -0
  69. package/dist/index.js +560 -283
  70. package/dist/ipc.js +49 -0
  71. package/dist/net-check.js +640 -0
  72. package/dist/paths.js +73 -9
  73. package/dist/types.js +8 -2
  74. package/dist/utils/aid-lifecycle-log.js +33 -0
  75. package/dist/utils/atomic-write.js +89 -0
  76. package/dist/utils/channel-helpers.js +46 -0
  77. package/dist/utils/cross-platform.js +17 -26
  78. package/dist/utils/error-utils.js +10 -2
  79. package/dist/utils/instance-registry.js +434 -0
  80. package/dist/utils/log-writer.js +217 -0
  81. package/dist/utils/logger.js +34 -77
  82. package/dist/utils/media-cache.js +23 -0
  83. package/dist/utils/npm-ops.js +163 -0
  84. package/dist/utils/process-introspect.js +122 -0
  85. package/dist/utils/stats.js +192 -0
  86. package/dist/watch-msg.js +544 -0
  87. package/evolclaw-install-aun.md +127 -47
  88. package/kits/docs/GUIDE.md +20 -0
  89. package/kits/docs/INDEX.md +52 -0
  90. package/kits/docs/aun/CHEATSHEET.md +17 -0
  91. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  92. package/kits/docs/channels/aun.md +25 -0
  93. package/kits/docs/channels/feishu.md +27 -0
  94. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  95. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  96. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  97. package/kits/docs/eck_templates/runtime.template.md +19 -0
  98. package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
  99. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  100. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  101. package/kits/docs/evolclaw/self-summary.md +29 -0
  102. package/kits/docs/evolclaw/tools.md +25 -0
  103. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  104. package/kits/docs/identity/PATH_OPS.md +16 -0
  105. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  106. package/kits/docs/identity/identity-tools.md +26 -0
  107. package/kits/docs/path-registry.md +43 -0
  108. package/kits/eck_manifest.json +95 -0
  109. package/kits/rules/01-overview.md +120 -0
  110. package/kits/rules/02-navigation.md +75 -0
  111. package/kits/rules/03-identity.md +34 -0
  112. package/kits/rules/04-relation.md +49 -0
  113. package/kits/rules/05-venue.md +45 -0
  114. package/kits/rules/06-channel.md +43 -0
  115. package/kits/templates/system-fragments/baseagent.md +2 -0
  116. package/kits/templates/system-fragments/channel.md +10 -0
  117. package/kits/templates/system-fragments/identity.md +12 -0
  118. package/kits/templates/system-fragments/relation.md +9 -0
  119. package/kits/templates/system-fragments/runtime.md +19 -0
  120. package/kits/templates/system-fragments/venue.md +5 -0
  121. package/package.json +10 -6
  122. package/data/evolclaw.sample.json +0 -60
  123. package/dist/agents/templates.js +0 -122
  124. package/dist/channels/aun-ops.js +0 -275
  125. package/dist/cli.js +0 -2178
  126. package/dist/config.js +0 -591
  127. package/dist/core/agent-registry.js +0 -450
  128. package/dist/core/evolagent-schema.js +0 -72
  129. package/dist/core/message/stream-flusher.js +0 -238
  130. package/dist/core/message/thought-emitter.js +0 -162
  131. package/dist/core/reload-hooks.js +0 -87
  132. package/dist/prompts/templates.js +0 -122
  133. package/dist/templates/prompts.md +0 -104
  134. package/dist/templates/skills.md +0 -66
  135. package/dist/utils/channel-fingerprint.js +0 -59
  136. package/dist/utils/error-dict.js +0 -63
  137. package/dist/utils/format.js +0 -32
  138. package/dist/utils/init.js +0 -645
  139. package/dist/utils/migrate-project.js +0 -122
  140. package/dist/utils/reload-hooks.js +0 -87
  141. package/dist/utils/stats-collector.js +0 -99
  142. package/dist/utils/upgrade.js +0 -100
@@ -1,5 +1,8 @@
1
+ import { randomBytes } from 'crypto';
1
2
  import { logger } from '../../utils/logger.js';
2
3
  import { StreamDebouncer } from './stream-debouncer.js';
4
+ import { appendMessageLog, buildInboundEntry } from './message-log.js';
5
+ import { buildEnvelope } from './message-processor.js';
3
6
  /**
4
7
  * MessageBridge — Channel 与 Core 之间的消息桥梁
5
8
  *
@@ -8,7 +11,7 @@ import { StreamDebouncer } from './stream-debouncer.js';
8
11
  * 出站:命令响应通过 sendReply 回调直接发送到渠道
9
12
  */
10
13
  export class MessageBridge {
11
- config;
14
+ defaultProjectPath;
12
15
  sessionManager;
13
16
  processor;
14
17
  messageQueue;
@@ -17,14 +20,14 @@ export class MessageBridge {
17
20
  debouncers = new Map();
18
21
  defaultDebounce;
19
22
  agentRegistry;
20
- constructor(config, sessionManager, processor, messageQueue, cmdHandler, eventBus) {
21
- this.config = config;
23
+ constructor(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, defaultDebounce) {
24
+ this.defaultProjectPath = defaultProjectPath;
22
25
  this.sessionManager = sessionManager;
23
26
  this.processor = processor;
24
27
  this.messageQueue = messageQueue;
25
28
  this.cmdHandler = cmdHandler;
26
29
  this.eventBus = eventBus;
27
- this.defaultDebounce = config.debounce ?? 2;
30
+ this.defaultDebounce = defaultDebounce ?? 2;
28
31
  }
29
32
  /** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
30
33
  setAgentRegistry(registry) {
@@ -33,19 +36,13 @@ export class MessageBridge {
33
36
  getDebouncer(channelName, channelType) {
34
37
  let d = this.debouncers.get(channelName);
35
38
  if (!d) {
39
+ // 从 owning agent 的 channel 配置取 debounce,找不到用全局默认
36
40
  let seconds = this.defaultDebounce;
37
- // 查找渠道级 debounce 配置:先用 channelType(如 'feishu')在 config.channels 里查
38
- const type = channelType || channelName;
39
- const raw = this.config.channels?.[type];
40
- if (raw) {
41
- if (Array.isArray(raw)) {
42
- const inst = raw.find((i) => (i.name || type) === channelName);
43
- if (inst?.debounce !== undefined)
44
- seconds = inst.debounce;
45
- }
46
- else if (raw.debounce !== undefined) {
47
- seconds = raw.debounce;
48
- }
41
+ const agent = this.agentRegistry?.resolveByChannel(channelName);
42
+ if (agent) {
43
+ const merged = agent.config;
44
+ if (merged?.debounce !== undefined)
45
+ seconds = merged.debounce;
49
46
  }
50
47
  d = new StreamDebouncer(seconds);
51
48
  this.debouncers.set(channelName, d);
@@ -66,6 +63,8 @@ export class MessageBridge {
66
63
  onMessage(async (msg) => {
67
64
  try {
68
65
  let content = msg.content.trim();
66
+ // 渠道入站日志
67
+ logger.channelIn({ channel: channelName, channelId: msg.channelId, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
69
68
  // 0. 自定义消息快速路径(menu.query 等)
70
69
  if (await this.handleCustomPayload(content, channelName, msg, sendReply, adapter))
71
70
  return;
@@ -78,7 +77,10 @@ export class MessageBridge {
78
77
  if (this.cmdHandler.isCommand(cmdContent)) {
79
78
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
80
79
  }
81
- if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId))
80
+ if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
81
+ logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
82
+ return sendReply(msg.channelId, text, msg.replyContext);
83
+ }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext))
82
84
  return;
83
85
  // 3. session 解析(使用 Channel 层填充的 chatType)
84
86
  const chatType = msg.chatType || 'private';
@@ -93,13 +95,21 @@ export class MessageBridge {
93
95
  if (msg.peerName)
94
96
  metadata.peerName = msg.peerName;
95
97
  }
96
- // Resolve effective project path: agent's projectPath when channel is agent-owned,
97
- // otherwise fall back to global config.projects.defaultPath
98
+ if (chatType === 'group') {
99
+ // 群聊:peerId 是当前消息发送者;groupId channel 提供时存到 metadata
100
+ if (msg.peerId)
101
+ metadata.peerId = msg.peerId;
102
+ if (msg.peerName)
103
+ metadata.peerName = msg.peerName;
104
+ if (msg.groupId)
105
+ metadata.groupId = msg.groupId;
106
+ }
107
+ // Resolve effective project path: 用通道所属 agent 的 projectPath;
108
+ // 通道找不到归属时退回到 globalConfig(一般是测试场景)
98
109
  const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
99
- const effectiveProjectPath = (owningAgent && !owningAgent.isDefault)
100
- ? owningAgent.projectPath
101
- : (this.config.projects?.defaultPath || process.cwd());
102
- const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType);
110
+ const effectiveProjectPath = owningAgent?.projectPath
111
+ ?? this.defaultProjectPath;
112
+ const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfId, msg.channelType || effectiveChannelType, msg.peerType);
103
113
  // 4. 消息前缀(由 policy 决定)
104
114
  const channelInfo = this.processor.getChannelInfo?.(channelName);
105
115
  if (channelInfo?.policy) {
@@ -109,7 +119,10 @@ export class MessageBridge {
109
119
  }
110
120
  // 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
111
121
  const fullMessage = {
112
- channel: channelName, channelId: msg.channelId, content,
122
+ channel: channelName,
123
+ channelType: msg.channelType || effectiveChannelType,
124
+ channelId: msg.channelId, content,
125
+ selfId: msg.selfId,
113
126
  chatType,
114
127
  images: msg.images, timestamp: Date.now(),
115
128
  peerId: msg.peerId, peerName: msg.peerName,
@@ -118,6 +131,19 @@ export class MessageBridge {
118
131
  mentions: msg.mentions, threadId: msg.threadId,
119
132
  replyContext: msg.replyContext,
120
133
  };
134
+ // 5.5 写入消息记录(入方向)
135
+ const chatDir = this.sessionManager.getChatDir(session);
136
+ appendMessageLog(chatDir, buildInboundEntry({
137
+ from: msg.peerId || 'unknown',
138
+ to: msg.selfId || 'self',
139
+ chatType,
140
+ groupId: msg.groupId ?? null,
141
+ msgId: msg.messageId ?? null,
142
+ content,
143
+ replyTo: msg.replyContext?.replyToMessageId ?? null,
144
+ permMode: session.identity?.role ?? null,
145
+ timestamp: fullMessage.timestamp,
146
+ }));
121
147
  // 6. ACK + debounce/enqueue
122
148
  // ACK 在到达时立即做(每条独立 ACK),不等合并
123
149
  // Interrupt 模式(单聊)→ 入队前 debounce 合并
@@ -125,7 +151,7 @@ export class MessageBridge {
125
151
  if (fullMessage.messageId)
126
152
  adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
127
153
  const isInterrupt = chatType !== 'group';
128
- const enqueueAgentName = (owningAgent && !owningAgent.isDefault) ? owningAgent.name : '[default]';
154
+ const enqueueAgentName = owningAgent?.name ?? '<unknown>';
129
155
  const doEnqueue = async (m) => {
130
156
  return this.messageQueue.enqueue(session.id, m, session.projectPath, {
131
157
  interruptible: isInterrupt,
@@ -169,71 +195,98 @@ export class MessageBridge {
169
195
  const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
170
196
  const base = { type: 'menu.response', cmd: parsed.cmd };
171
197
  const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
172
- if (adapter?.sendCustomPayload) {
173
- adapter.sendCustomPayload(msg.channelId, response);
174
- }
175
- else {
176
- await sendReply(msg.channelId, response);
177
- }
198
+ await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
178
199
  }
179
200
  else if (parsed.cmd) {
180
201
  // 动态子菜单查询
181
202
  const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
182
203
  const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
183
- if (adapter?.sendCustomPayload) {
184
- adapter.sendCustomPayload(msg.channelId, response);
185
- }
186
- else {
187
- await sendReply(msg.channelId, response);
188
- }
204
+ await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
189
205
  }
190
206
  else {
191
207
  // 全量菜单
192
208
  const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
193
209
  const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
194
210
  const response = JSON.stringify({ type: 'menu.response', items });
195
- if (adapter?.sendCustomPayload) {
196
- adapter.sendCustomPayload(msg.channelId, response);
197
- }
198
- else {
199
- await sendReply(msg.channelId, response);
200
- }
211
+ await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
201
212
  }
202
213
  return true;
203
214
  }
204
215
  return false;
205
216
  }
206
- /** 首次交互自动绑定 owner */
217
+ /** menu.query 响应:优先走 adapter.send(custom),降级 sendReply */
218
+ async sendCustomResponse(adapter, channel, channelId, response, sendReply) {
219
+ if (adapter?.send) {
220
+ const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
221
+ const envelope = buildEnvelope({
222
+ taskId: `menu-${randomBytes(4).toString('hex')}`,
223
+ channel,
224
+ channelId,
225
+ agentName,
226
+ });
227
+ await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
228
+ }
229
+ else {
230
+ await sendReply(channelId, response);
231
+ }
232
+ }
233
+ /** 首次交互自动绑定 owner —— 通过 channel-routed self-agent 完成 */
207
234
  async autoBindOwner(channel, userId) {
208
- // Registry-first: route owner queries/writes to the agent that owns this channel.
209
- // Falls back to evolclaw.json for default-agent channels.
210
- const { getOwner, setOwner } = await import('../../config.js');
211
- const currentOwner = this.agentRegistry?.getOwner?.(channel) ?? getOwner(this.config, channel);
212
- // currentOwner === undefined means either no owner set, or instance not found
213
- // In both cases, try to set — setOwner is a no-op for unknown instances
235
+ const currentOwner = this.agentRegistry?.getOwner?.(channel);
214
236
  if (currentOwner === undefined) {
215
237
  if (this.agentRegistry?.setChannelOwner) {
216
238
  this.agentRegistry.setChannelOwner(channel, userId);
217
239
  }
218
240
  else {
219
- setOwner(this.config, channel, userId);
241
+ logger.warn(`[Owner] no agentRegistry; skip auto-bind for ${channel}`);
242
+ return;
220
243
  }
221
244
  logger.info(`[Owner] Auto-bound ${channel} owner: ${userId}`);
222
245
  this.eventBus.publish({ type: 'channel:owner-bound', channel, userId });
223
246
  }
224
247
  }
225
248
  /** 命令快速路径:返回 true 表示已处理 */
226
- async handleCommand(content, channel, channelId, sendReply, userId, threadId) {
249
+ async handleCommand(content, channel, channelId, sendReply, userId, threadId, chatType, source, replyContext) {
227
250
  if (!this.cmdHandler.isCommand(content))
228
251
  return false;
229
- logger.info(`[${channel}] ${channelId}: ${content}`);
230
- const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId);
231
- logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}, value=${cmdResult === null ? 'null' : cmdResult === undefined ? 'undefined' : 'string'}`);
252
+ logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
253
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source);
254
+ logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
232
255
  if (cmdResult === undefined)
233
256
  return false;
234
257
  if (cmdResult) {
258
+ // 规范化为 OutboundPayload:string → command.result 包装;object → 透传
259
+ let payload;
260
+ if (typeof cmdResult === 'string') {
261
+ payload = { kind: 'command.result', text: cmdResult };
262
+ }
263
+ else if (typeof cmdResult === 'object' && cmdResult !== null && 'kind' in cmdResult) {
264
+ payload = cmdResult;
265
+ }
266
+ else {
267
+ // 不识别的返回值,按已处理但无回显处理
268
+ return true;
269
+ }
270
+ // 出站走 adapter.send 统一入口
271
+ const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
272
+ const envelope = buildEnvelope({
273
+ taskId: `cmd-${randomBytes(5).toString('hex')}`,
274
+ channel,
275
+ channelId,
276
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
277
+ chatmode: 'interactive',
278
+ replyContext,
279
+ });
235
280
  try {
236
- await sendReply(cmdResult);
281
+ if (adapter?.send) {
282
+ await adapter.send(envelope, payload);
283
+ }
284
+ else {
285
+ // 降级路径:渠道未实现 send 时回退到原有 sendReply(仅文本)
286
+ const fallbackText = ('text' in payload && typeof payload.text === 'string') ? payload.text : '';
287
+ if (fallbackText)
288
+ await sendReply(fallbackText);
289
+ }
237
290
  }
238
291
  catch (error) {
239
292
  logger.error(`[${channel}] Failed to send command response:`, error);
@@ -0,0 +1,93 @@
1
+ import path from 'path';
2
+ import { appendJsonl, chatDirPath } from '../session/session-fs-store.js';
3
+ import { logger } from '../../utils/logger.js';
4
+ const MESSAGE_LOG_FILE = 'messages.jsonl';
5
+ // 入方向去重:最近 msgId 缓存(LRU 风格,最多 200 条)
6
+ const recentMsgIds = new Set();
7
+ const DEDUP_MAX = 200;
8
+ function isDuplicate(msgId) {
9
+ if (!msgId)
10
+ return false;
11
+ if (recentMsgIds.has(msgId))
12
+ return true;
13
+ if (recentMsgIds.size >= DEDUP_MAX) {
14
+ const first = recentMsgIds.values().next().value;
15
+ recentMsgIds.delete(first);
16
+ }
17
+ recentMsgIds.add(msgId);
18
+ return false;
19
+ }
20
+ function formatTimestampMs(epochMs) {
21
+ const d = new Date(epochMs);
22
+ const yyyy = d.getFullYear();
23
+ const mo = String(d.getMonth() + 1).padStart(2, '0');
24
+ const dd = String(d.getDate()).padStart(2, '0');
25
+ const hh = String(d.getHours()).padStart(2, '0');
26
+ const mi = String(d.getMinutes()).padStart(2, '0');
27
+ const ss = String(d.getSeconds()).padStart(2, '0');
28
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
29
+ return `${yyyy}-${mo}-${dd} ${hh}:${mi}:${ss}.${ms}`;
30
+ }
31
+ export function messageLogPath(chatDir) {
32
+ return path.join(chatDir, MESSAGE_LOG_FILE);
33
+ }
34
+ export function resolveChatDir(sessionsDir, channelType, channelId, selfId) {
35
+ return chatDirPath(sessionsDir, channelType, channelId, selfId);
36
+ }
37
+ export function appendMessageLog(chatDir, entry) {
38
+ if (entry.dir === 'in' && isDuplicate(entry.msgId)) {
39
+ logger.debug(`[MessageLog] Duplicate msgId skipped: ${entry.msgId}`);
40
+ return;
41
+ }
42
+ try {
43
+ appendJsonl(messageLogPath(chatDir), entry);
44
+ }
45
+ catch (e) {
46
+ logger.warn(`[MessageLog] Failed to write message log: ${e}`);
47
+ }
48
+ }
49
+ export function buildInboundEntry(opts) {
50
+ const ts = opts.timestamp || Date.now();
51
+ const isCommand = opts.content.startsWith('/');
52
+ return {
53
+ ts,
54
+ time: formatTimestampMs(ts),
55
+ dir: 'in',
56
+ from: opts.from,
57
+ to: opts.to,
58
+ chatType: opts.chatType,
59
+ groupId: opts.groupId ?? null,
60
+ msgId: opts.msgId ?? null,
61
+ msgType: isCommand ? 'command' : 'text',
62
+ content: opts.content,
63
+ replyTo: opts.replyTo ?? null,
64
+ agent: null,
65
+ model: null,
66
+ permMode: opts.permMode ?? null,
67
+ cmdParsed: isCommand ? opts.content.split(/\s/)[0] : null,
68
+ durationMs: null,
69
+ };
70
+ }
71
+ export function buildOutboundEntry(opts) {
72
+ const ts = opts.timestamp || Date.now();
73
+ return {
74
+ ts,
75
+ time: formatTimestampMs(ts),
76
+ dir: 'out',
77
+ from: opts.from,
78
+ to: opts.to,
79
+ chatType: opts.chatType,
80
+ groupId: opts.groupId ?? null,
81
+ msgId: opts.msgId ?? null,
82
+ msgType: 'text',
83
+ content: opts.content,
84
+ replyTo: opts.replyTo ?? null,
85
+ agent: opts.agent ?? null,
86
+ model: opts.model ?? null,
87
+ permMode: null,
88
+ cmdParsed: null,
89
+ durationMs: opts.durationMs ?? null,
90
+ numTurns: opts.numTurns ?? null,
91
+ usage: opts.usage ?? null,
92
+ };
93
+ }