evolclaw 2.8.2 → 3.0.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.
- package/README.md +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
|
@@ -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
|
-
|
|
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(
|
|
21
|
-
this.
|
|
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 =
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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);
|
|
@@ -78,7 +75,7 @@ export class MessageBridge {
|
|
|
78
75
|
if (this.cmdHandler.isCommand(cmdContent)) {
|
|
79
76
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
80
77
|
}
|
|
81
|
-
if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId))
|
|
78
|
+
if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext))
|
|
82
79
|
return;
|
|
83
80
|
// 3. session 解析(使用 Channel 层填充的 chatType)
|
|
84
81
|
const chatType = msg.chatType || 'private';
|
|
@@ -93,13 +90,21 @@ export class MessageBridge {
|
|
|
93
90
|
if (msg.peerName)
|
|
94
91
|
metadata.peerName = msg.peerName;
|
|
95
92
|
}
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if (chatType === 'group') {
|
|
94
|
+
// 群聊:peerId 是当前消息发送者;groupId 在 channel 提供时存到 metadata
|
|
95
|
+
if (msg.peerId)
|
|
96
|
+
metadata.peerId = msg.peerId;
|
|
97
|
+
if (msg.peerName)
|
|
98
|
+
metadata.peerName = msg.peerName;
|
|
99
|
+
if (msg.groupId)
|
|
100
|
+
metadata.groupId = msg.groupId;
|
|
101
|
+
}
|
|
102
|
+
// Resolve effective project path: 用通道所属 agent 的 projectPath;
|
|
103
|
+
// 通道找不到归属时退回到 globalConfig(一般是测试场景)
|
|
98
104
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
|
|
99
|
-
const effectiveProjectPath =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType);
|
|
105
|
+
const effectiveProjectPath = owningAgent?.projectPath
|
|
106
|
+
?? this.defaultProjectPath;
|
|
107
|
+
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);
|
|
103
108
|
// 4. 消息前缀(由 policy 决定)
|
|
104
109
|
const channelInfo = this.processor.getChannelInfo?.(channelName);
|
|
105
110
|
if (channelInfo?.policy) {
|
|
@@ -109,7 +114,10 @@ export class MessageBridge {
|
|
|
109
114
|
}
|
|
110
115
|
// 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
|
|
111
116
|
const fullMessage = {
|
|
112
|
-
channel: channelName,
|
|
117
|
+
channel: channelName,
|
|
118
|
+
channelType: msg.channelType || effectiveChannelType,
|
|
119
|
+
channelId: msg.channelId, content,
|
|
120
|
+
selfId: msg.selfId,
|
|
113
121
|
chatType,
|
|
114
122
|
images: msg.images, timestamp: Date.now(),
|
|
115
123
|
peerId: msg.peerId, peerName: msg.peerName,
|
|
@@ -118,6 +126,19 @@ export class MessageBridge {
|
|
|
118
126
|
mentions: msg.mentions, threadId: msg.threadId,
|
|
119
127
|
replyContext: msg.replyContext,
|
|
120
128
|
};
|
|
129
|
+
// 5.5 写入消息记录(入方向)
|
|
130
|
+
const chatDir = this.sessionManager.getChatDir(session);
|
|
131
|
+
appendMessageLog(chatDir, buildInboundEntry({
|
|
132
|
+
from: msg.peerId || 'unknown',
|
|
133
|
+
to: msg.selfId || 'self',
|
|
134
|
+
chatType,
|
|
135
|
+
groupId: msg.groupId ?? null,
|
|
136
|
+
msgId: msg.messageId ?? null,
|
|
137
|
+
content,
|
|
138
|
+
replyTo: msg.replyContext?.replyToMessageId ?? null,
|
|
139
|
+
permMode: session.identity?.role ?? null,
|
|
140
|
+
timestamp: fullMessage.timestamp,
|
|
141
|
+
}));
|
|
121
142
|
// 6. ACK + debounce/enqueue
|
|
122
143
|
// ACK 在到达时立即做(每条独立 ACK),不等合并
|
|
123
144
|
// Interrupt 模式(单聊)→ 入队前 debounce 合并
|
|
@@ -125,7 +146,7 @@ export class MessageBridge {
|
|
|
125
146
|
if (fullMessage.messageId)
|
|
126
147
|
adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
|
|
127
148
|
const isInterrupt = chatType !== 'group';
|
|
128
|
-
const enqueueAgentName =
|
|
149
|
+
const enqueueAgentName = owningAgent?.name ?? '<unknown>';
|
|
129
150
|
const doEnqueue = async (m) => {
|
|
130
151
|
return this.messageQueue.enqueue(session.id, m, session.projectPath, {
|
|
131
152
|
interruptible: isInterrupt,
|
|
@@ -169,71 +190,98 @@ export class MessageBridge {
|
|
|
169
190
|
const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
|
|
170
191
|
const base = { type: 'menu.response', cmd: parsed.cmd };
|
|
171
192
|
const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
|
|
172
|
-
|
|
173
|
-
adapter.sendCustomPayload(msg.channelId, response);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
await sendReply(msg.channelId, response);
|
|
177
|
-
}
|
|
193
|
+
await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
|
|
178
194
|
}
|
|
179
195
|
else if (parsed.cmd) {
|
|
180
196
|
// 动态子菜单查询
|
|
181
197
|
const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
|
|
182
198
|
const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
|
|
183
|
-
|
|
184
|
-
adapter.sendCustomPayload(msg.channelId, response);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
await sendReply(msg.channelId, response);
|
|
188
|
-
}
|
|
199
|
+
await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
|
|
189
200
|
}
|
|
190
201
|
else {
|
|
191
202
|
// 全量菜单
|
|
192
203
|
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
193
204
|
const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
|
|
194
205
|
const response = JSON.stringify({ type: 'menu.response', items });
|
|
195
|
-
|
|
196
|
-
adapter.sendCustomPayload(msg.channelId, response);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
await sendReply(msg.channelId, response);
|
|
200
|
-
}
|
|
206
|
+
await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
|
|
201
207
|
}
|
|
202
208
|
return true;
|
|
203
209
|
}
|
|
204
210
|
return false;
|
|
205
211
|
}
|
|
206
|
-
/**
|
|
212
|
+
/** menu.query 响应:优先走 adapter.send(custom),降级 sendReply */
|
|
213
|
+
async sendCustomResponse(adapter, channel, channelId, response, sendReply) {
|
|
214
|
+
if (adapter?.send) {
|
|
215
|
+
const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
|
|
216
|
+
const envelope = buildEnvelope({
|
|
217
|
+
taskId: `menu-${randomBytes(4).toString('hex')}`,
|
|
218
|
+
channel,
|
|
219
|
+
channelId,
|
|
220
|
+
agentName,
|
|
221
|
+
});
|
|
222
|
+
await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await sendReply(channelId, response);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** 首次交互自动绑定 owner —— 通过 channel-routed self-agent 完成 */
|
|
207
229
|
async autoBindOwner(channel, userId) {
|
|
208
|
-
|
|
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
|
|
230
|
+
const currentOwner = this.agentRegistry?.getOwner?.(channel);
|
|
214
231
|
if (currentOwner === undefined) {
|
|
215
232
|
if (this.agentRegistry?.setChannelOwner) {
|
|
216
233
|
this.agentRegistry.setChannelOwner(channel, userId);
|
|
217
234
|
}
|
|
218
235
|
else {
|
|
219
|
-
|
|
236
|
+
logger.warn(`[Owner] no agentRegistry; skip auto-bind for ${channel}`);
|
|
237
|
+
return;
|
|
220
238
|
}
|
|
221
239
|
logger.info(`[Owner] Auto-bound ${channel} owner: ${userId}`);
|
|
222
240
|
this.eventBus.publish({ type: 'channel:owner-bound', channel, userId });
|
|
223
241
|
}
|
|
224
242
|
}
|
|
225
243
|
/** 命令快速路径:返回 true 表示已处理 */
|
|
226
|
-
async handleCommand(content, channel, channelId, sendReply, userId, threadId) {
|
|
244
|
+
async handleCommand(content, channel, channelId, sendReply, userId, threadId, chatType, source, replyContext) {
|
|
227
245
|
if (!this.cmdHandler.isCommand(content))
|
|
228
246
|
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}
|
|
247
|
+
logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
|
|
248
|
+
const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source);
|
|
249
|
+
logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
|
|
232
250
|
if (cmdResult === undefined)
|
|
233
251
|
return false;
|
|
234
252
|
if (cmdResult) {
|
|
253
|
+
// 规范化为 OutboundPayload:string → command.result 包装;object → 透传
|
|
254
|
+
let payload;
|
|
255
|
+
if (typeof cmdResult === 'string') {
|
|
256
|
+
payload = { kind: 'command.result', text: cmdResult };
|
|
257
|
+
}
|
|
258
|
+
else if (typeof cmdResult === 'object' && cmdResult !== null && 'kind' in cmdResult) {
|
|
259
|
+
payload = cmdResult;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// 不识别的返回值,按已处理但无回显处理
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
// 出站走 adapter.send 统一入口
|
|
266
|
+
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
267
|
+
const envelope = buildEnvelope({
|
|
268
|
+
taskId: `cmd-${randomBytes(5).toString('hex')}`,
|
|
269
|
+
channel,
|
|
270
|
+
channelId,
|
|
271
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
272
|
+
chatmode: 'interactive',
|
|
273
|
+
replyContext,
|
|
274
|
+
});
|
|
235
275
|
try {
|
|
236
|
-
|
|
276
|
+
if (adapter?.send) {
|
|
277
|
+
await adapter.send(envelope, payload);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// 降级路径:渠道未实现 send 时回退到原有 sendReply(仅文本)
|
|
281
|
+
const fallbackText = ('text' in payload && typeof payload.text === 'string') ? payload.text : '';
|
|
282
|
+
if (fallbackText)
|
|
283
|
+
await sendReply(fallbackText);
|
|
284
|
+
}
|
|
237
285
|
}
|
|
238
286
|
catch (error) {
|
|
239
287
|
logger.error(`[${channel}] Failed to send command response:`, error);
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
};
|
|
91
|
+
}
|