lightclawbot 1.2.6-beta.0 → 1.2.7

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 (98) hide show
  1. package/dist/src/gateway.js +50 -6
  2. package/dist/src/group/constants/index.js +20 -0
  3. package/dist/src/group/inbound/index.js +254 -0
  4. package/dist/src/group/index.js +15 -0
  5. package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
  6. package/dist/src/group/orchestrator/execution/index.js +7 -0
  7. package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
  8. package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
  9. package/dist/src/group/orchestrator/execution/types.js +7 -0
  10. package/dist/src/group/orchestrator/index.js +14 -0
  11. package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
  12. package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
  13. package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
  14. package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
  15. package/dist/src/group/orchestrator/orchestrator.js +265 -0
  16. package/dist/src/group/orchestrator/planning/index.js +13 -0
  17. package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
  18. package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
  19. package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
  20. package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
  21. package/dist/src/group/orchestrator/routes/index.js +9 -0
  22. package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
  23. package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
  24. package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
  25. package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
  26. package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
  27. package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
  28. package/dist/src/group/orchestrator/routes/types.js +8 -0
  29. package/dist/src/group/services/group-cleanup-service.js +183 -0
  30. package/dist/src/group/services/group-creation-service.js +122 -0
  31. package/dist/src/group/services/group-deletion-service.js +111 -0
  32. package/dist/src/group/services/group-history-service.js +73 -0
  33. package/dist/src/group/services/group-member-service.js +169 -0
  34. package/dist/src/group/services/group-query-service.js +133 -0
  35. package/dist/src/group/services/group-update-service.js +144 -0
  36. package/dist/src/group/services/index.js +20 -0
  37. package/dist/src/group/storage/concurrency-manager.js +119 -0
  38. package/dist/src/group/storage/group-storage-core.js +227 -0
  39. package/dist/src/group/storage/index.js +12 -0
  40. package/dist/src/group/storage/message-reader.js +213 -0
  41. package/dist/src/group/storage/message-writer.js +229 -0
  42. package/dist/src/group/storage/slice-manager.js +165 -0
  43. package/dist/src/group/types/common.js +5 -0
  44. package/dist/src/group/types/index.js +5 -0
  45. package/dist/src/group/types/message.js +5 -0
  46. package/dist/src/group/types/orchestrator.js +5 -0
  47. package/dist/src/group/types/storage.js +5 -0
  48. package/dist/src/group/utils/id-generator.js +15 -0
  49. package/dist/src/group/utils/index.js +12 -0
  50. package/dist/src/group/utils/mime.js +36 -0
  51. package/dist/src/group/utils/normalize.js +32 -0
  52. package/dist/src/group/utils/run-helpers.js +36 -0
  53. package/dist/src/outbound.js +12 -19
  54. package/dist/src/shared.js +4 -3
  55. package/dist/src/socket/events/agents-request.js +147 -0
  56. package/dist/src/socket/events/chat-request.js +67 -0
  57. package/dist/src/socket/events/file-download.js +121 -0
  58. package/dist/src/socket/events/group-abort.js +59 -0
  59. package/dist/src/socket/events/group-history.js +59 -0
  60. package/dist/src/socket/events/group-member.js +83 -0
  61. package/dist/src/socket/events/group-request.js +91 -0
  62. package/dist/src/socket/events/history-request.js +95 -0
  63. package/dist/src/socket/events/index.js +39 -0
  64. package/dist/src/socket/events/message-private.js +82 -0
  65. package/dist/src/socket/handlers.js +53 -517
  66. package/dist/src/socket/native-socket.js +21 -20
  67. package/dist/src/socket/registry.js +6 -3
  68. package/dist/src/socket/reliable-emitter.js +16 -13
  69. package/dist/src/socket/service/chat-common.js +36 -0
  70. package/dist/src/socket/service/chat-create.js +75 -0
  71. package/dist/src/socket/service/chat-delete.js +94 -0
  72. package/dist/src/socket/service/chat-list.js +82 -0
  73. package/dist/src/socket/service/chat-update.js +83 -0
  74. package/dist/src/socket/service/group-abort.js +104 -0
  75. package/dist/src/socket/service/group-history.js +140 -0
  76. package/dist/src/socket/service/group-member.js +209 -0
  77. package/dist/src/socket/service/group.js +233 -0
  78. package/dist/src/socket/service/history.js +102 -0
  79. package/dist/src/socket/service/index.js +14 -0
  80. package/dist/src/socket/types/index.js +7 -0
  81. package/dist/src/socket/types/request.js +8 -0
  82. package/dist/src/socket/types/service.js +8 -0
  83. package/dist/src/socket/utils/agent-soul.js +95 -0
  84. package/dist/src/socket/utils/index.js +8 -0
  85. package/dist/src/socket/utils/message.js +83 -0
  86. package/dist/src/socket/utils/validate.js +42 -0
  87. package/dist/src/streaming/index.js +1 -0
  88. package/dist/src/streaming/stream-reply-sink.js +270 -14
  89. package/dist/src/streaming/types.js +20 -1
  90. package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
  91. package/dist/src/tools/group-history-tool.js +172 -0
  92. package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
  93. package/dist/src/tools.js +4 -3
  94. package/dist/src/utils/index.js +1 -0
  95. package/dist/src/utils/logger.js +38 -0
  96. package/openclaw.plugin.json +2 -1
  97. package/package.json +1 -1
  98. 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
- /** 日志对象(可选)。调用方通常传入 GatewayContext.log,用于打印连接/重连/错误等诊断信息。 */
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?.info(`${this.logPrefix} disconnect() called (wasConnected=${this._connected})`);
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?.debug?.(`${this.logPrefix} connect() skipped, already ${this._ws.readyState === WebSocket.OPEN ? 'OPEN' : 'CONNECTING'}`);
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?.info(`${this.logPrefix} connect() called, manually reconnecting`);
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?.info(`${this.logPrefix} Connecting to ${fullUrl}`);
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?.info(`${this.logPrefix} WebSocket opened`);
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?.info(`${this.logPrefix} WebSocket closed (code=${event.code}, reason="${event.reason}", wasClean=${event.wasClean}, wasConnected=${wasConnected})`);
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?.error(`${this.logPrefix} WebSocket error: ${detail}`);
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?.error(`${this.logPrefix} Failed to construct WebSocket: ${message}`);
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?.warn(`${this.logPrefix} Received non-JSON message, ignored`);
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?.warn(`${this.logPrefix} Received message without event field, ignored`);
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?.info(`${this.logPrefix} Received message ACK ${JSON.stringify(msg)}`);
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?.debug?.(`${this.logPrefix} Received ACK for unknown relatedMsgId=${relatedMsgId}`);
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?.info(`${this.logPrefix} Handshake received, socket id=${this._id}`);
290
+ this.log.info(`${this.logPrefix} Handshake received, socket id=${this._id}`);
290
291
  }
291
292
  return;
292
293
  }
293
294
  // 分发业务事件
294
- this.log?.debug?.(`${this.logPrefix} Dispatch event: ${msg.event}`);
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?.error(`${this.logPrefix} Max reconnection attempts (${SOCKET_RECONNECTION_ATTEMPTS}) reached, giving up`);
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?.warn(`${this.logPrefix} Scheduling reconnect attempt #${this._reconnectAttempts} in ${delay}ms`);
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, log) {
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?.warn(`[socket-registry] Failed to flush buffered message: msgId=${msg.msgId}`);
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?.info(`[socket-registry] Flushed pending messages: sent=${sent}, failed=${failed}`);
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
- constructor(getSocket, log, logPrefix = "[ReliableEmitter]") {
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?.info(`${this.logPrefix} emit: ${JSON.stringify(enrichedData)}`);
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?.info(`${this.logPrefix} Queued while paused: emitId=${emitId}, msgId=${msgId}`);
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?.info(`${this.logPrefix} Paused, ${this.pending.size} message(s) pending`);
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?.info(`${this.logPrefix} Resumed, re-emitting ${this.pending.size} pending message(s)`);
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?.info(`${this.logPrefix} Destroyed`);
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?.warn(`[ReliableEmitter] ACK error: emitId=${entry.id}, msgId=${entry.msgId}, err=${err.message}, retryCount=${entry.retryCount}`);
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?.info(
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?.error(`${this.logPrefix} Gave up after ${entry.retryCount} retries: emitId=${entry.id}, msgId=${entry.msgId}, ` +
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?.info(`${this.logPrefix} Retry #${entry.retryCount} in ${delay}ms: emitId=${entry.id}, msgId=${entry.msgId}`);
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?.warn(`${this.logPrefix} Evicted oldest pending: emitId=${oldest.id}, msgId=${oldest.msgId}`);
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
+ }