lightclawbot 1.2.6 → 1.2.8
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/dist/src/config.js +30 -3
- package/dist/src/gateway.js +58 -12
- package/dist/src/group/constants/index.js +20 -0
- package/dist/src/group/inbound/index.js +254 -0
- package/dist/src/group/index.js +15 -0
- package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
- package/dist/src/group/orchestrator/execution/index.js +7 -0
- package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
- package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
- package/dist/src/group/orchestrator/execution/types.js +7 -0
- package/dist/src/group/orchestrator/index.js +14 -0
- package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
- package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
- package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
- package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
- package/dist/src/group/orchestrator/orchestrator.js +265 -0
- package/dist/src/group/orchestrator/planning/index.js +13 -0
- package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
- package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
- package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
- package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
- package/dist/src/group/orchestrator/routes/index.js +9 -0
- package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
- package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
- package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
- package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
- package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
- package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
- package/dist/src/group/orchestrator/routes/types.js +8 -0
- package/dist/src/group/services/group-cleanup-service.js +183 -0
- package/dist/src/group/services/group-creation-service.js +122 -0
- package/dist/src/group/services/group-deletion-service.js +111 -0
- package/dist/src/group/services/group-history-service.js +73 -0
- package/dist/src/group/services/group-member-service.js +169 -0
- package/dist/src/group/services/group-query-service.js +133 -0
- package/dist/src/group/services/group-update-service.js +144 -0
- package/dist/src/group/services/index.js +20 -0
- package/dist/src/group/storage/concurrency-manager.js +119 -0
- package/dist/src/group/storage/group-storage-core.js +227 -0
- package/dist/src/group/storage/index.js +12 -0
- package/dist/src/group/storage/message-reader.js +213 -0
- package/dist/src/group/storage/message-writer.js +229 -0
- package/dist/src/group/storage/slice-manager.js +165 -0
- package/dist/src/group/types/common.js +5 -0
- package/dist/src/group/types/index.js +5 -0
- package/dist/src/group/types/message.js +5 -0
- package/dist/src/group/types/orchestrator.js +5 -0
- package/dist/src/group/types/storage.js +5 -0
- package/dist/src/group/utils/id-generator.js +15 -0
- package/dist/src/group/utils/index.js +12 -0
- package/dist/src/group/utils/mime.js +36 -0
- package/dist/src/group/utils/normalize.js +32 -0
- package/dist/src/group/utils/run-helpers.js +36 -0
- package/dist/src/history/session-reader.js +8 -2
- package/dist/src/outbound.js +12 -19
- package/dist/src/shared.js +4 -3
- package/dist/src/socket/events/agents-request.js +147 -0
- package/dist/src/socket/events/chat-request.js +67 -0
- package/dist/src/socket/events/file-download.js +121 -0
- package/dist/src/socket/events/group-abort.js +59 -0
- package/dist/src/socket/events/group-history.js +59 -0
- package/dist/src/socket/events/group-member.js +83 -0
- package/dist/src/socket/events/group-request.js +91 -0
- package/dist/src/socket/events/history-request.js +95 -0
- package/dist/src/socket/events/index.js +39 -0
- package/dist/src/socket/events/message-private.js +82 -0
- package/dist/src/socket/handlers.js +53 -568
- package/dist/src/socket/native-socket.js +21 -20
- package/dist/src/socket/registry.js +6 -3
- package/dist/src/socket/reliable-emitter.js +16 -13
- package/dist/src/socket/service/chat-common.js +36 -0
- package/dist/src/socket/service/chat-create.js +75 -0
- package/dist/src/socket/service/chat-delete.js +94 -0
- package/dist/src/socket/service/chat-list.js +82 -0
- package/dist/src/socket/service/chat-update.js +83 -0
- package/dist/src/socket/service/group-abort.js +104 -0
- package/dist/src/socket/service/group-history.js +140 -0
- package/dist/src/socket/service/group-member.js +209 -0
- package/dist/src/socket/service/group.js +233 -0
- package/dist/src/socket/service/history.js +102 -0
- package/dist/src/socket/service/index.js +14 -0
- package/dist/src/socket/types/index.js +7 -0
- package/dist/src/socket/types/request.js +8 -0
- package/dist/src/socket/types/service.js +8 -0
- package/dist/src/socket/utils/agent-soul.js +95 -0
- package/dist/src/socket/utils/index.js +8 -0
- package/dist/src/socket/utils/message.js +83 -0
- package/dist/src/socket/utils/validate.js +42 -0
- package/dist/src/streaming/index.js +1 -0
- package/dist/src/streaming/stream-reply-sink.js +367 -20
- package/dist/src/streaming/thinking-formatter.js +325 -0
- package/dist/src/streaming/types.js +20 -1
- package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
- package/dist/src/tools/group-history-tool.js +172 -0
- package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
- package/dist/src/tools.js +4 -3
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/logger.js +38 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/dist/src/socket/agent-soul.js +0 -41
- package/dist/src/socket/chat.js +0 -257
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import WebSocket from 'ws';
|
|
16
16
|
import { SOCKET_RECONNECTION_DELAY, SOCKET_RECONNECTION_DELAY_MAX, SOCKET_RECONNECTION_ATTEMPTS } from '../config.js';
|
|
17
|
+
import { getModuleLogger } from '../utils/logger.js';
|
|
18
|
+
/** 模块级日志器 */
|
|
19
|
+
const moduleLog = getModuleLogger('socket.native-socket');
|
|
17
20
|
// ============================================================
|
|
18
21
|
// NativeSocketClient 实现
|
|
19
22
|
// ============================================================
|
|
@@ -59,21 +62,19 @@ export class NativeSocketClient {
|
|
|
59
62
|
// ----------------------------------------------------------
|
|
60
63
|
// 构造函数
|
|
61
64
|
// ----------------------------------------------------------
|
|
62
|
-
/**
|
|
63
|
-
log;
|
|
65
|
+
/** 日志对象,使用公共方法获取。 */
|
|
66
|
+
log = moduleLog;
|
|
64
67
|
/** 日志前缀,便于在多实例场景下区分日志来源。 */
|
|
65
68
|
logPrefix;
|
|
66
69
|
/**
|
|
67
70
|
* @param url WebSocket 服务端地址(ws:// 或 wss://),认证所需的 ticket 等参数请直接拼到 URL query 上
|
|
68
71
|
* @param options 连接选项
|
|
69
72
|
* - path: 连接路径(如 /claw-socket),会附加到 URL
|
|
70
|
-
* - log: 日志对象(可选),用于打印连接生命周期与异常信息
|
|
71
73
|
* - logPrefix: 日志前缀(可选),默认 `[NativeSocket]`
|
|
72
74
|
*/
|
|
73
75
|
constructor(url, options = {}) {
|
|
74
76
|
this.url = url;
|
|
75
77
|
this.options = options;
|
|
76
|
-
this.log = options.log;
|
|
77
78
|
this.logPrefix = options.logPrefix ?? '[NativeSocket]';
|
|
78
79
|
this._connect();
|
|
79
80
|
}
|
|
@@ -169,7 +170,7 @@ export class NativeSocketClient {
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
disconnect() {
|
|
172
|
-
this.log
|
|
173
|
+
this.log.info(`${this.logPrefix} disconnect() called (wasConnected=${this._connected})`);
|
|
173
174
|
this._manualDisconnect = true;
|
|
174
175
|
this._clearReconnectTimer();
|
|
175
176
|
const wasConnected = this._connected;
|
|
@@ -184,10 +185,10 @@ export class NativeSocketClient {
|
|
|
184
185
|
// 手动触发重连(用于服务端主动断开后的手动重连场景)
|
|
185
186
|
// 若当前已有活跃连接(CONNECTING 或 OPEN),直接返回,避免连接泄漏
|
|
186
187
|
if (this._ws && (this._ws.readyState === WebSocket.CONNECTING || this._ws.readyState === WebSocket.OPEN)) {
|
|
187
|
-
this.log
|
|
188
|
+
this.log.debug?.(`${this.logPrefix} connect() skipped, already ${this._ws.readyState === WebSocket.OPEN ? 'OPEN' : 'CONNECTING'}`);
|
|
188
189
|
return this;
|
|
189
190
|
}
|
|
190
|
-
this.log
|
|
191
|
+
this.log.info(`${this.logPrefix} connect() called, manually reconnecting`);
|
|
191
192
|
this._manualDisconnect = false;
|
|
192
193
|
this._reconnectAttempts = 0;
|
|
193
194
|
this._clearReconnectTimer();
|
|
@@ -200,14 +201,14 @@ export class NativeSocketClient {
|
|
|
200
201
|
_connect() {
|
|
201
202
|
// 构建完整 URL(仅拼接 path,认证通过 URL query 参数完成,无需 headers)
|
|
202
203
|
const fullUrl = this._buildUrl();
|
|
203
|
-
this.log
|
|
204
|
+
this.log.info(`${this.logPrefix} Connecting to ${fullUrl}`);
|
|
204
205
|
try {
|
|
205
206
|
const ws = new WebSocket(fullUrl);
|
|
206
207
|
this._ws = ws;
|
|
207
208
|
ws.onopen = () => {
|
|
208
209
|
this._reconnectAttempts = 0;
|
|
209
210
|
this._connected = true;
|
|
210
|
-
this.log
|
|
211
|
+
this.log.info(`${this.logPrefix} WebSocket opened`);
|
|
211
212
|
this._dispatch('connect');
|
|
212
213
|
};
|
|
213
214
|
ws.onmessage = (event) => {
|
|
@@ -218,7 +219,7 @@ export class NativeSocketClient {
|
|
|
218
219
|
this._connected = false;
|
|
219
220
|
this._id = undefined;
|
|
220
221
|
this._ws = null;
|
|
221
|
-
this.log
|
|
222
|
+
this.log.info(`${this.logPrefix} WebSocket closed (code=${event.code}, reason="${event.reason}", wasClean=${event.wasClean}, wasConnected=${wasConnected})`);
|
|
222
223
|
// 立即 flush 所有挂起的 ACK,以断开错误立即回调,而非等待超时
|
|
223
224
|
this._flushPendingAcks(new Error('disconnect'));
|
|
224
225
|
if (wasConnected) {
|
|
@@ -231,14 +232,14 @@ export class NativeSocketClient {
|
|
|
231
232
|
};
|
|
232
233
|
ws.onerror = (event) => {
|
|
233
234
|
const detail = event.error?.stack || event.message || 'unknown';
|
|
234
|
-
this.log
|
|
235
|
+
this.log.error(`${this.logPrefix} WebSocket error: ${detail}`);
|
|
235
236
|
// onerror 后必然触发 onclose,此处只派发 connect_error 事件
|
|
236
237
|
this._dispatch('connect_error', new Error('WebSocket error'));
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
catch (err) {
|
|
240
241
|
const message = err instanceof Error ? err.message : String(err);
|
|
241
|
-
this.log
|
|
242
|
+
this.log.error(`${this.logPrefix} Failed to construct WebSocket: ${message}`);
|
|
242
243
|
this._dispatch('connect_error', err instanceof Error ? err : new Error(String(err)));
|
|
243
244
|
if (!this._manualDisconnect) {
|
|
244
245
|
this._scheduleReconnect();
|
|
@@ -254,16 +255,16 @@ export class NativeSocketClient {
|
|
|
254
255
|
msg = JSON.parse(raw);
|
|
255
256
|
}
|
|
256
257
|
catch {
|
|
257
|
-
this.log
|
|
258
|
+
this.log.warn(`${this.logPrefix} Received non-JSON message, ignored`);
|
|
258
259
|
return; // 忽略非 JSON 消息
|
|
259
260
|
}
|
|
260
261
|
if (!msg.event) {
|
|
261
|
-
this.log
|
|
262
|
+
this.log.warn(`${this.logPrefix} Received message without event field, ignored`);
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
264
265
|
// 处理 ACK 回包
|
|
265
266
|
if (msg.event === 'message:ack') {
|
|
266
|
-
this.log
|
|
267
|
+
// this.log.info(`${this.logPrefix} Received message ACK ${JSON.stringify(msg)}`);
|
|
267
268
|
// 服务端通过 relatedMsgId 回传上行消息的 msgId,用于匹配挂起的 ACK 记录
|
|
268
269
|
// pendingKey = data.msgId(注册时),relatedMsgId = msgId(服务端回传),两者一致
|
|
269
270
|
const ackData = msg.data;
|
|
@@ -276,7 +277,7 @@ export class NativeSocketClient {
|
|
|
276
277
|
pending.callback(null);
|
|
277
278
|
}
|
|
278
279
|
else {
|
|
279
|
-
this.log
|
|
280
|
+
this.log.debug?.(`${this.logPrefix} Received ACK for unknown relatedMsgId=${relatedMsgId}`);
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
return;
|
|
@@ -286,12 +287,12 @@ export class NativeSocketClient {
|
|
|
286
287
|
const handshakeData = msg.data;
|
|
287
288
|
if (handshakeData?.id) {
|
|
288
289
|
this._id = handshakeData.id;
|
|
289
|
-
this.log
|
|
290
|
+
this.log.info(`${this.logPrefix} Handshake received, socket id=${this._id}`);
|
|
290
291
|
}
|
|
291
292
|
return;
|
|
292
293
|
}
|
|
293
294
|
// 分发业务事件
|
|
294
|
-
this.log
|
|
295
|
+
this.log.debug?.(`${this.logPrefix} Dispatch event: ${msg.event}`);
|
|
295
296
|
this._dispatch(msg.event, msg.data);
|
|
296
297
|
}
|
|
297
298
|
// ----------------------------------------------------------
|
|
@@ -318,14 +319,14 @@ export class NativeSocketClient {
|
|
|
318
319
|
_scheduleReconnect() {
|
|
319
320
|
// 超过最大重连次数时停止重连
|
|
320
321
|
if (SOCKET_RECONNECTION_ATTEMPTS !== Infinity && this._reconnectAttempts >= SOCKET_RECONNECTION_ATTEMPTS) {
|
|
321
|
-
this.log
|
|
322
|
+
this.log.error(`${this.logPrefix} Max reconnection attempts (${SOCKET_RECONNECTION_ATTEMPTS}) reached, giving up`);
|
|
322
323
|
this._dispatch('connect_error', new Error(`Max reconnection attempts (${SOCKET_RECONNECTION_ATTEMPTS}) reached`));
|
|
323
324
|
return;
|
|
324
325
|
}
|
|
325
326
|
// 指数退避延迟:delay = min(base * 2^attempts, max)
|
|
326
327
|
const delay = Math.min(SOCKET_RECONNECTION_DELAY * Math.pow(2, this._reconnectAttempts), SOCKET_RECONNECTION_DELAY_MAX);
|
|
327
328
|
this._reconnectAttempts++;
|
|
328
|
-
this.log
|
|
329
|
+
this.log.warn(`${this.logPrefix} Scheduling reconnect attempt #${this._reconnectAttempts} in ${delay}ms`);
|
|
329
330
|
this._reconnectTimer = setTimeout(() => {
|
|
330
331
|
this._reconnectTimer = null;
|
|
331
332
|
if (!this._manualDisconnect) {
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* 仅在 gateway 彻底销毁(cleanup)时才删除 entry。
|
|
12
12
|
*/
|
|
13
13
|
import { MAX_PENDING_MESSAGES, EVENT_MESSAGE_PRIVATE } from "../config.js";
|
|
14
|
+
import { getModuleLogger } from '../utils/logger.js';
|
|
15
|
+
/** 模块级日志器 */
|
|
16
|
+
const log = getModuleLogger('socket.registry');
|
|
14
17
|
/** accountId → SocketEntry */
|
|
15
18
|
const registry = new Map();
|
|
16
19
|
// ============================================================
|
|
@@ -81,7 +84,7 @@ export function bufferMessage(accountId, message) {
|
|
|
81
84
|
* 如果有 ReliableEmitter,走可靠发送;否则 fallback 直接 emit。
|
|
82
85
|
* 返回发送成功 / 失败的计数。
|
|
83
86
|
*/
|
|
84
|
-
export function flushPendingMessages(accountId
|
|
87
|
+
export function flushPendingMessages(accountId) {
|
|
85
88
|
const entry = registry.get(accountId);
|
|
86
89
|
if (!entry)
|
|
87
90
|
return { sent: 0, failed: 0 };
|
|
@@ -109,12 +112,12 @@ export function flushPendingMessages(accountId, log) {
|
|
|
109
112
|
}
|
|
110
113
|
catch {
|
|
111
114
|
failed++;
|
|
112
|
-
log
|
|
115
|
+
log.warn(`[socket-registry] Failed to flush buffered message: msgId=${msg.msgId}`);
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
if (sent > 0 || failed > 0) {
|
|
117
|
-
log
|
|
120
|
+
log.info(`[socket-registry] Flushed pending messages: sent=${sent}, failed=${failed}`);
|
|
118
121
|
}
|
|
119
122
|
return { sent, failed };
|
|
120
123
|
}
|
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
* @format
|
|
24
24
|
*/
|
|
25
25
|
import { EMIT_ACK_TIMEOUT, EMIT_MAX_RETRIES, EMIT_RETRY_BASE_DELAY, EMIT_RETRY_MAX_DELAY, EMIT_PENDING_MAX, } from '../config.js';
|
|
26
|
+
import { getModuleLogger } from '../utils/logger.js';
|
|
27
|
+
/** 模块级日志器 */
|
|
28
|
+
const moduleLog = getModuleLogger('socket.reliable-emitter');
|
|
26
29
|
// ============================================================
|
|
27
30
|
// ReliableEmitter 主类
|
|
28
31
|
// ============================================================
|
|
29
32
|
export class ReliableEmitter {
|
|
30
33
|
getSocket;
|
|
31
|
-
log;
|
|
32
34
|
/**
|
|
33
35
|
* 待确认消息队列。
|
|
34
36
|
* key = emitId(内部唯一标识),value = PendingMessage。
|
|
@@ -57,9 +59,10 @@ export class ReliableEmitter {
|
|
|
57
59
|
* 业务通道前缀(如 `[lightclawbot]`),使上游日志风格统一。
|
|
58
60
|
*/
|
|
59
61
|
logPrefix;
|
|
60
|
-
|
|
62
|
+
/** 模块级日志器引用 */
|
|
63
|
+
log = moduleLog;
|
|
64
|
+
constructor(getSocket, logPrefix = "[ReliableEmitter]") {
|
|
61
65
|
this.getSocket = getSocket;
|
|
62
|
-
this.log = log;
|
|
63
66
|
this.logPrefix = logPrefix;
|
|
64
67
|
}
|
|
65
68
|
// ----------------------------------------------------------
|
|
@@ -96,7 +99,7 @@ export class ReliableEmitter {
|
|
|
96
99
|
// - data.msgId 保持业务层原值不变,同一条消息的所有重试相同,服务端用它识别消息身份
|
|
97
100
|
const enrichedData = { ...data, idempotencyKey: emitId };
|
|
98
101
|
// 统一的发送日志(含最终 payload),业务层无需再打
|
|
99
|
-
this.log
|
|
102
|
+
this.log.info(`${this.logPrefix} emit: ${JSON.stringify(enrichedData)}`);
|
|
100
103
|
return new Promise((resolve) => {
|
|
101
104
|
const entry = {
|
|
102
105
|
id: emitId,
|
|
@@ -112,7 +115,7 @@ export class ReliableEmitter {
|
|
|
112
115
|
this.pending.set(emitId, entry);
|
|
113
116
|
if (this.paused) {
|
|
114
117
|
// 断线中,等 resume 统一重发
|
|
115
|
-
this.log
|
|
118
|
+
this.log.info(`${this.logPrefix} Queued while paused: emitId=${emitId}, msgId=${msgId}`);
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
// 正常状态:立即尝试发送
|
|
@@ -140,7 +143,7 @@ export class ReliableEmitter {
|
|
|
140
143
|
entry.retryTimer = null;
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
|
-
this.log
|
|
146
|
+
this.log.info(`${this.logPrefix} Paused, ${this.pending.size} message(s) pending`);
|
|
144
147
|
}
|
|
145
148
|
/**
|
|
146
149
|
* 重连时调用 — 立即重发所有待确认消息。
|
|
@@ -156,7 +159,7 @@ export class ReliableEmitter {
|
|
|
156
159
|
if (!this.paused)
|
|
157
160
|
return;
|
|
158
161
|
this.paused = false;
|
|
159
|
-
this.log
|
|
162
|
+
this.log.info(`${this.logPrefix} Resumed, re-emitting ${this.pending.size} pending message(s)`);
|
|
160
163
|
for (const entry of this.pending.values()) {
|
|
161
164
|
this.doEmit(entry);
|
|
162
165
|
}
|
|
@@ -176,7 +179,7 @@ export class ReliableEmitter {
|
|
|
176
179
|
entry.resolve(false);
|
|
177
180
|
}
|
|
178
181
|
this.pending.clear();
|
|
179
|
-
this.log
|
|
182
|
+
this.log.info(`${this.logPrefix} Destroyed`);
|
|
180
183
|
}
|
|
181
184
|
/**
|
|
182
185
|
* 当前待确认消息数(实时值)。
|
|
@@ -228,12 +231,12 @@ export class ReliableEmitter {
|
|
|
228
231
|
return;
|
|
229
232
|
if (err) {
|
|
230
233
|
// ACK 超时或服务端返回错误,安排下一次重试
|
|
231
|
-
this.log
|
|
234
|
+
this.log.warn(`[ReliableEmitter] ACK error: emitId=${entry.id}, msgId=${entry.msgId}, err=${err.message}, retryCount=${entry.retryCount}`);
|
|
232
235
|
this.scheduleRetry(entry);
|
|
233
236
|
}
|
|
234
237
|
else {
|
|
235
238
|
// 服务端已确认收到,从 pending 队列移除
|
|
236
|
-
// this.log
|
|
239
|
+
// this.log.info(
|
|
237
240
|
// `[ReliableEmitter] ACK success: emitId=${entry.id}, msgId=${entry.msgId}, retryCount=${entry.retryCount}`,
|
|
238
241
|
// );
|
|
239
242
|
this.confirm(entry.id);
|
|
@@ -274,7 +277,7 @@ export class ReliableEmitter {
|
|
|
274
277
|
// 重试次数耗尽,放弃该消息
|
|
275
278
|
this.pending.delete(entry.id);
|
|
276
279
|
this.stats.totalFailed++;
|
|
277
|
-
this.log
|
|
280
|
+
this.log.error(`${this.logPrefix} Gave up after ${entry.retryCount} retries: emitId=${entry.id}, msgId=${entry.msgId}, ` +
|
|
278
281
|
`elapsed=${Date.now() - entry.createdAt}ms`);
|
|
279
282
|
// 通知调用方:消息最终未被确认
|
|
280
283
|
entry.resolve(false);
|
|
@@ -284,7 +287,7 @@ export class ReliableEmitter {
|
|
|
284
287
|
entry.retryCount++;
|
|
285
288
|
this.stats.totalRetries++;
|
|
286
289
|
const delay = this.getRetryDelay(entry.retryCount);
|
|
287
|
-
this.log
|
|
290
|
+
this.log.info(`${this.logPrefix} Retry #${entry.retryCount} in ${delay}ms: emitId=${entry.id}, msgId=${entry.msgId}`);
|
|
288
291
|
entry.retryTimer = setTimeout(() => {
|
|
289
292
|
entry.retryTimer = null;
|
|
290
293
|
// 定时器触发时再次检查是否断线(重试等待期间可能发生断线)
|
|
@@ -333,7 +336,7 @@ export class ReliableEmitter {
|
|
|
333
336
|
clearTimeout(oldest.retryTimer);
|
|
334
337
|
this.pending.delete(oldest.id);
|
|
335
338
|
this.stats.totalFailed++;
|
|
336
|
-
this.log
|
|
339
|
+
this.log.warn(`${this.logPrefix} Evicted oldest pending: emitId=${oldest.id}, msgId=${oldest.msgId}`);
|
|
337
340
|
oldest.resolve(false);
|
|
338
341
|
}
|
|
339
342
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat handler 共享工具函数
|
|
3
|
+
*
|
|
4
|
+
* 提供 list / create / update / delete 四个 handler 共用的:
|
|
5
|
+
* - createChatErrorSender 工厂函数
|
|
6
|
+
* - 模块级日志器
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
import { CHANNEL_KEY, EVENT_CHAT_RESPONSE } from '../../config.js';
|
|
10
|
+
import { generateMsgId } from '../../dedup.js';
|
|
11
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
12
|
+
/** 模块级日志器 */
|
|
13
|
+
export const log = getModuleLogger('socket.chat');
|
|
14
|
+
/**
|
|
15
|
+
* 生成统一的错误响应发送器。
|
|
16
|
+
* @param ctx 共享上下文
|
|
17
|
+
* @param extra 额外字段(如 chatId、兜底 chats: [])
|
|
18
|
+
*/
|
|
19
|
+
export function createChatErrorSender(ctx, extra = {}) {
|
|
20
|
+
const { responseMsgId, botClientId, userId, agentId, type, reliableEmitter } = ctx;
|
|
21
|
+
return (error) => {
|
|
22
|
+
log.error(`[${CHANNEL_KEY}] Chat ${type} error: ${error}`);
|
|
23
|
+
reliableEmitter.emitWithAck(EVENT_CHAT_RESPONSE, {
|
|
24
|
+
msgId: responseMsgId,
|
|
25
|
+
from: botClientId,
|
|
26
|
+
to: userId,
|
|
27
|
+
type,
|
|
28
|
+
agentId,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
...extra,
|
|
31
|
+
error,
|
|
32
|
+
}, responseMsgId);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 生成响应 msgId 的快捷方法 */
|
|
36
|
+
export { generateMsgId };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file chat-create.ts
|
|
3
|
+
* @description Chat handler — create(新建会话)
|
|
4
|
+
*
|
|
5
|
+
* 新建一条会话元数据,chatId = randomUUID(),插入列表最前。
|
|
6
|
+
* 文件或目录不存在时由 writeChatsFile 递归创建。
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { CHANNEL_KEY, EVENT_CHAT_RESPONSE } from '../../config.js';
|
|
10
|
+
import { readChatsFile, resolveChatsFilePath, writeChatsFile } from '../../utils/common.js';
|
|
11
|
+
import { log, generateMsgId, createChatErrorSender } from './chat-common.js';
|
|
12
|
+
/**
|
|
13
|
+
* 创建新的会话元数据
|
|
14
|
+
* @returns 包含默认值的 ChatMeta 对象
|
|
15
|
+
*/
|
|
16
|
+
function buildNewChatMeta() {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
return {
|
|
19
|
+
chatId: randomUUID(),
|
|
20
|
+
title: '新会话',
|
|
21
|
+
titleLocked: false,
|
|
22
|
+
createdAt: now,
|
|
23
|
+
updatedAt: now,
|
|
24
|
+
pinned: false,
|
|
25
|
+
sessionIdHistory: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 发送创建成功响应
|
|
30
|
+
* @param params - handler 基础参数
|
|
31
|
+
* @param responseMsgId - 响应消息 ID
|
|
32
|
+
* @param newChat - 新创建的会话元数据
|
|
33
|
+
*/
|
|
34
|
+
function emitCreateResponse(params, responseMsgId, newChat) {
|
|
35
|
+
const { botClientId, userId, agentId, reliableEmitter } = params;
|
|
36
|
+
reliableEmitter.emitWithAck(EVENT_CHAT_RESPONSE, {
|
|
37
|
+
msgId: responseMsgId,
|
|
38
|
+
from: botClientId,
|
|
39
|
+
to: userId,
|
|
40
|
+
type: 'create',
|
|
41
|
+
agentId,
|
|
42
|
+
chats: [newChat],
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
}, responseMsgId);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 处理 EVENT_CHAT_REQUEST(type=create)
|
|
48
|
+
*
|
|
49
|
+
* 流程:解析存储路径 → 读取现有会话列表 → 创建新会话 → 写入文件 → 回包
|
|
50
|
+
*
|
|
51
|
+
* @param params - Chat handler 通用入参
|
|
52
|
+
*/
|
|
53
|
+
export function handleChatCreate(params) {
|
|
54
|
+
const { userId, agentId, botClientId, reliableEmitter } = params;
|
|
55
|
+
const chatsPath = resolveChatsFilePath(agentId, userId);
|
|
56
|
+
const responseMsgId = generateMsgId();
|
|
57
|
+
const sendError = createChatErrorSender({
|
|
58
|
+
responseMsgId,
|
|
59
|
+
botClientId,
|
|
60
|
+
userId,
|
|
61
|
+
agentId,
|
|
62
|
+
type: 'create',
|
|
63
|
+
reliableEmitter,
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const existing = readChatsFile(chatsPath);
|
|
67
|
+
const newChat = buildNewChatMeta();
|
|
68
|
+
writeChatsFile(chatsPath, [newChat, ...existing]);
|
|
69
|
+
log.info(`[${CHANNEL_KEY}] 会话创建成功: userId=${userId}, agentId=${agentId}, chatId=${newChat.chatId}`);
|
|
70
|
+
emitCreateResponse(params, responseMsgId, newChat);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
sendError(err instanceof Error ? err.message : String(err));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file chat-delete.ts
|
|
3
|
+
* @description Chat handler — delete(删除会话)
|
|
4
|
+
*
|
|
5
|
+
* 该模块负责处理会话删除请求:
|
|
6
|
+
* 1. 根据传入的 chatId 在本地 JSON 文件中定位目标会话
|
|
7
|
+
* 2. 将目标会话从列表中剔除并原子写回文件
|
|
8
|
+
* 3. 通过可靠消息通道将删除结果推送给客户端
|
|
9
|
+
*
|
|
10
|
+
* 异常处理:
|
|
11
|
+
* - 会话文件不存在时,返回 error 响应
|
|
12
|
+
* - 目标 chatId 未找到时,返回 error 响应
|
|
13
|
+
* - 文件读写异常时,捕获错误并返回 error 响应
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import { CHANNEL_KEY, EVENT_CHAT_RESPONSE } from '../../config.js';
|
|
17
|
+
import { readChatsFile, resolveChatsFilePath, writeChatsFile } from '../../utils/common.js';
|
|
18
|
+
import { log, generateMsgId, createChatErrorSender } from './chat-common.js';
|
|
19
|
+
/**
|
|
20
|
+
* 发送删除成功响应
|
|
21
|
+
*
|
|
22
|
+
* 通过 reliableEmitter 的 emitWithAck 方法发送带确认机制的响应消息,
|
|
23
|
+
* 确保客户端能够可靠接收到删除操作的结果。
|
|
24
|
+
*
|
|
25
|
+
* @param params - handler 基础参数,包含用户/Agent/Bot 标识及消息发射器
|
|
26
|
+
* @param responseMsgId - 本次响应的唯一消息 ID,用于客户端消息去重和确认
|
|
27
|
+
* @param deletedChat - 被删除的会话元数据,回传给客户端用于 UI 同步
|
|
28
|
+
*/
|
|
29
|
+
function emitDeleteResponse(params, responseMsgId, deletedChat) {
|
|
30
|
+
const { botClientId, userId, agentId, reliableEmitter } = params;
|
|
31
|
+
// 通过可靠消息通道发送删除成功响应,附带被删除的会话信息
|
|
32
|
+
reliableEmitter.emitWithAck(EVENT_CHAT_RESPONSE, {
|
|
33
|
+
msgId: responseMsgId, // 响应消息唯一标识
|
|
34
|
+
from: botClientId, // 发送方:Bot 客户端 ID
|
|
35
|
+
to: userId, // 接收方:用户 ID
|
|
36
|
+
type: 'delete', // 操作类型标识
|
|
37
|
+
agentId, // 关联的 Agent ID
|
|
38
|
+
chats: [deletedChat], // 被删除的会话列表(单条)
|
|
39
|
+
timestamp: Date.now(), // 响应时间戳
|
|
40
|
+
}, responseMsgId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 处理 EVENT_CHAT_REQUEST(type=delete)
|
|
44
|
+
*
|
|
45
|
+
* 完整处理流程:
|
|
46
|
+
* 1. 解构入参,获取用户/Agent/会话标识
|
|
47
|
+
* 2. 解析会话存储文件路径
|
|
48
|
+
* 3. 生成本次响应的唯一消息 ID
|
|
49
|
+
* 4. 创建统一的错误发送器(复用 chat-common 工具函数)
|
|
50
|
+
* 5. 校验会话文件是否存在(前置守卫)
|
|
51
|
+
* 6. 读取会话列表,查找目标会话
|
|
52
|
+
* 7. 过滤掉目标会话,将剩余列表写回文件
|
|
53
|
+
* 8. 记录操作日志,发送成功响应
|
|
54
|
+
*
|
|
55
|
+
* @param params - Chat delete 操作入参,继承自 ChatHandlerParams 并扩展 chatId 字段
|
|
56
|
+
*/
|
|
57
|
+
export function handleChatDelete(params) {
|
|
58
|
+
// 解构所需参数:用户ID、AgentID、目标会话ID、Bot客户端ID、可靠消息发射器
|
|
59
|
+
const { userId, agentId, chatId, botClientId, reliableEmitter } = params;
|
|
60
|
+
// 根据 agentId 和 userId 解析会话列表的本地 JSON 文件路径
|
|
61
|
+
const chatsPath = resolveChatsFilePath(agentId, userId);
|
|
62
|
+
// 生成本次响应的唯一消息 ID,用于消息追踪和 ACK 确认
|
|
63
|
+
const responseMsgId = generateMsgId();
|
|
64
|
+
// 创建统一的错误响应发送器,封装了错误消息的格式化和发送逻辑
|
|
65
|
+
const sendError = createChatErrorSender({ responseMsgId, botClientId, userId, agentId, type: 'delete', reliableEmitter }, { chatId });
|
|
66
|
+
// 前置守卫:检查会话文件是否存在,不存在则直接返回错误
|
|
67
|
+
if (!fs.existsSync(chatsPath)) {
|
|
68
|
+
sendError(`会话文件不存在: ${chatsPath}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// 读取当前会话列表(JSON 文件 → ChatMeta[] 数组)
|
|
73
|
+
const chats = readChatsFile(chatsPath);
|
|
74
|
+
// 在列表中查找目标会话对象
|
|
75
|
+
const targetChat = chats.find((c) => c.chatId === chatId);
|
|
76
|
+
// 目标会话不存在时返回错误响应
|
|
77
|
+
if (!targetChat) {
|
|
78
|
+
sendError(`会话不存在: chatId=${chatId}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 过滤掉目标会话,得到删除后的剩余列表
|
|
82
|
+
const remainingChats = chats.filter((c) => c.chatId !== chatId);
|
|
83
|
+
// 将剩余会话列表原子写回文件(writeChatsFile 内部保证写入的原子性)
|
|
84
|
+
writeChatsFile(chatsPath, remainingChats);
|
|
85
|
+
// 记录删除成功日志,包含关键上下文信息便于排查
|
|
86
|
+
log.info(`[${CHANNEL_KEY}] 会话删除成功: userId=${userId}, agentId=${agentId}, chatId=${chatId}, 剩余=${remainingChats.length}`);
|
|
87
|
+
// 向客户端发送删除成功响应
|
|
88
|
+
emitDeleteResponse(params, responseMsgId, targetChat);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// 捕获文件读写等异常,统一通过错误发送器返回给客户端
|
|
92
|
+
sendError(err instanceof Error ? err.message : String(err));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file chat-list.ts
|
|
3
|
+
* @description Chat handler — list(会话列表查询)
|
|
4
|
+
*
|
|
5
|
+
* 该模块负责处理会话列表查询请求:
|
|
6
|
+
* 1. 根据 agentId + userId 定位本地 JSON 存储文件
|
|
7
|
+
* 2. 读取并返回该用户在指定 Agent 下的全部会话列表
|
|
8
|
+
* 3. 通过可靠消息通道将查询结果推送给客户端
|
|
9
|
+
*
|
|
10
|
+
* 兜底策略:
|
|
11
|
+
* - 文件不存在时由 readChatsFileOrInitDefault 主动落盘一条默认会话条目
|
|
12
|
+
* - 保证前端拉列表永远拿到至少一项,避免空列表导致的 UI 异常
|
|
13
|
+
*/
|
|
14
|
+
// ─── 依赖导入 ────────────────────────────────────────────────────────────────
|
|
15
|
+
/** 通道标识常量和聊天响应事件名 */
|
|
16
|
+
import { CHANNEL_KEY, EVENT_CHAT_RESPONSE } from '../../config.js';
|
|
17
|
+
/** 文件读取工具:readChatsFileOrInitDefault 在文件缺失时自动创建默认条目 */
|
|
18
|
+
import { readChatsFileOrInitDefault, resolveChatsFilePath } from '../../utils/common.js';
|
|
19
|
+
/** 共享工具:模块日志器、消息 ID 生成器、错误发送器工厂 */
|
|
20
|
+
import { log, generateMsgId, createChatErrorSender } from './chat-common.js';
|
|
21
|
+
// ─── 内部辅助函数 ─────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* 发送列表查询成功响应
|
|
24
|
+
*
|
|
25
|
+
* 通过 reliableEmitter 的 emitWithAck 方法发送带确认机制的响应消息,
|
|
26
|
+
* 确保客户端能够可靠接收到会话列表数据。
|
|
27
|
+
*
|
|
28
|
+
* @param params - handler 基础参数,包含用户/Agent/Bot 标识及消息发射器
|
|
29
|
+
* @param responseMsgId - 本次响应的唯一消息 ID,用于客户端消息去重和 ACK 确认
|
|
30
|
+
* @param chats - 完整的会话列表数据,至少包含一条兜底记录
|
|
31
|
+
*/
|
|
32
|
+
function emitListResponse(params, responseMsgId, chats) {
|
|
33
|
+
const { botClientId, userId, agentId, reliableEmitter } = params;
|
|
34
|
+
// 通过可靠消息通道发送列表查询成功响应
|
|
35
|
+
reliableEmitter.emitWithAck(EVENT_CHAT_RESPONSE, {
|
|
36
|
+
msgId: responseMsgId, // 响应消息唯一标识
|
|
37
|
+
from: botClientId, // 发送方:Bot 客户端 ID
|
|
38
|
+
to: userId, // 接收方:用户 ID
|
|
39
|
+
type: 'list', // 操作类型标识
|
|
40
|
+
agentId, // 关联的 Agent ID
|
|
41
|
+
chats, // 会话列表数据(ChatMeta[])
|
|
42
|
+
timestamp: Date.now(), // 响应时间戳
|
|
43
|
+
}, responseMsgId);
|
|
44
|
+
}
|
|
45
|
+
// ─── 导出函数 ─────────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* 处理 EVENT_CHAT_REQUEST(type=list)
|
|
48
|
+
*
|
|
49
|
+
* 完整处理流程:
|
|
50
|
+
* 1. 解构入参,获取用户和 Agent 标识
|
|
51
|
+
* 2. 根据 agentId + userId 解析会话存储文件路径
|
|
52
|
+
* 3. 生成本次响应的唯一消息 ID
|
|
53
|
+
* 4. 创建统一的错误发送器(复用 chat-common 工具函数)
|
|
54
|
+
* 5. 读取会话列表(文件不存在时自动初始化默认条目)
|
|
55
|
+
* 6. 记录操作日志,发送成功响应
|
|
56
|
+
*
|
|
57
|
+
* @param params - Chat handler 通用入参,包含 userId、agentId、botClientId、reliableEmitter
|
|
58
|
+
*/
|
|
59
|
+
export function handleChatList(params) {
|
|
60
|
+
// 解构直接使用的字段
|
|
61
|
+
const { userId, agentId } = params;
|
|
62
|
+
// 根据 agentId 和 userId 解析会话列表的本地 JSON 文件路径
|
|
63
|
+
// 路径格式:~/.openclaw/agents/<agentId>/chats/<userId>/chats.json
|
|
64
|
+
const chatsPath = resolveChatsFilePath(agentId, userId);
|
|
65
|
+
// 生成本次响应的唯一消息 ID,用于消息追踪和 ACK 确认
|
|
66
|
+
const responseMsgId = generateMsgId();
|
|
67
|
+
// 创建统一的错误响应发送器
|
|
68
|
+
// 附加 chats: [] 确保客户端即使收到错误也能正确处理列表字段
|
|
69
|
+
const sendError = createChatErrorSender({ responseMsgId, botClientId: params.botClientId, userId, agentId, type: 'list', reliableEmitter: params.reliableEmitter }, { chats: [] });
|
|
70
|
+
try {
|
|
71
|
+
// 读取会话列表;文件不存在时自动创建并写入一条默认会话,保证返回非空数组
|
|
72
|
+
const chats = readChatsFileOrInitDefault(chatsPath);
|
|
73
|
+
// 记录查询成功日志,包含关键上下文信息便于排查
|
|
74
|
+
log.info(`[${CHANNEL_KEY}] 会话列表查询成功: userId=${userId}, agentId=${agentId}, count=${chats.length}`);
|
|
75
|
+
// 向客户端发送列表查询成功响应
|
|
76
|
+
emitListResponse(params, responseMsgId, chats);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// 捕获文件读写等异常,统一通过错误发送器返回给客户端
|
|
80
|
+
sendError(err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
}
|