@53ai/53ai-openclaw 1.0.9 → 1.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
 
7
+ ## [1.1.0] - 2026-03-27
8
+
9
+ ### Added
10
+ - 新增 `message-cache.ts` 模块,实现按账号隔离的消息缓存、重试机制与重连后的消息回放能力。
11
+ - 新增 `MESSAGE_CACHE_TTL_MS` 常量配置,用于统一控制消息缓存保留时长。
12
+
13
+ ### Changed
14
+ - `monitor.ts`:消息队列从全局串行改为按 `chatId` 分队列处理,实现同会话串行、跨会话并行,并在队列空闲时自动回收。
15
+ - `monitor.ts`:发送链路接入缓存感知发送器,WebSocket 不可用或发送失败时自动缓存消息,连接恢复后自动回放。
16
+ - `monitor.ts` / `state-manager.ts`:重连上限和预热失败场景默认保留缓存,便于外部恢复后继续补发消息。
17
+
18
+ ### Fixed
19
+ - `message-sender.ts`:`sendReply` 与 `sendThinkingMessage` 在 WebSocket 未就绪时改为抛出错误,避免发送失败被静默忽略。
20
+ - `message-cache.ts`:缓存满时优先淘汰非终态消息,降低最终完成态消息被淘汰导致会话状态不完整的风险。
21
+
7
22
  ## [1.0.9] - 2026-03-26
8
23
 
9
24
  ### Changed
package/dist/index.cjs.js CHANGED
@@ -60,6 +60,12 @@ const WS_RECONNECT_BASE_DELAY_MS = 1000;
60
60
  const TEXT_CHUNK_LIMIT = 4000;
61
61
  /** 消息处理超时(毫秒) */
62
62
  const MESSAGE_PROCESS_TIMEOUT_MS = 120000;
63
+ /**
64
+ * 消息缓存 TTL(毫秒)
65
+ * 设为 35 分钟,确保覆盖最大重连时间(约 28.5 分钟)+ 缓冲
66
+ * @see WS_MAX_RECONNECT_ATTEMPTS - 60 次,指数退避最大 30s
67
+ */
68
+ const MESSAGE_CACHE_TTL_MS = 35 * 60 * 1000;
63
69
  // ============================================================================
64
70
  // 媒体处理配置
65
71
  // ============================================================================
@@ -338,8 +344,9 @@ async function sendReply(params) {
338
344
  const reqId = replyToMsgId || streamId;
339
345
  runtime.log?.(`[53aihub] sendReply START: reqId=${reqId}, finish=${finish}, isError=${isError}, textLen=${text?.length || 0}, wsReadyState=${wsClient.readyState}`);
340
346
  if (wsClient.readyState !== 1) {
341
- runtime.error?.(`[53aihub] WebSocket is not open (readyState=${wsClient.readyState}). Cannot send message to ${toChatId}`);
342
- return;
347
+ const errorMsg = `WebSocket not connected (readyState=${wsClient.readyState}), cannot send message to ${toChatId}`;
348
+ runtime.error?.(`[53aihub] ${errorMsg}`);
349
+ throw new Error(`[53aihub] ${errorMsg}`);
343
350
  }
344
351
  if (isError) {
345
352
  runtime.error?.(`[53aihub] sendReply ERROR: reqId=${reqId}, code=${errorCode}, text=${text?.substring(0, 100)}`);
@@ -450,8 +457,9 @@ async function sendThinkingMessage(wsClient, text, msgId, streamId, runtime) {
450
457
  const wsState = wsClient.readyState;
451
458
  runtime?.log?.(`[53aihub] sendThinkingMessage CALLED: msgId=${msgId}, streamId=${streamId}, text=${text}, wsReadyState=${wsState}`);
452
459
  if (wsState !== 1) {
453
- runtime?.error?.(`[53aihub] sendThinkingMessage SKIPPED: WebSocket not ready (state=${wsState})`);
454
- return;
460
+ const errorMsg = `WebSocket not ready (state=${wsState})`;
461
+ runtime?.error?.(`[53aihub] sendThinkingMessage FAILED: ${errorMsg}`);
462
+ throw new Error(`[53aihub] ${errorMsg}`);
455
463
  }
456
464
  // 使用 OpenAI 兼容格式,确保 Go 后端能正确解析
457
465
  // 关键:req_id 必须使用原始消息的 msgId,这样 Go 后端才能关联请求和响应
@@ -686,7 +694,7 @@ async function warmupReqIdStore(accountId = "default", log) {
686
694
  const store = getOrCreateReqIdStore(accountId);
687
695
  return store.warmup((err) => log?.(`ReqId warmup error: ${String(err)}`));
688
696
  }
689
- async function cleanupAccount(accountId) {
697
+ async function cleanupAccount(accountId, clearCache = false) {
690
698
  const wsClient = wsClientInstances.get(accountId);
691
699
  if (wsClient) {
692
700
  try {
@@ -700,6 +708,10 @@ async function cleanupAccount(accountId) {
700
708
  const store = reqIdStores.get(accountId);
701
709
  if (store)
702
710
  await store.flush();
711
+ if (clearCache) {
712
+ const { clearAccountCache } = await Promise.resolve().then(function () { return messageCache; });
713
+ clearAccountCache(accountId);
714
+ }
703
715
  }
704
716
 
705
717
  /**
@@ -807,41 +819,288 @@ async function downloadAndSaveFiles(params) {
807
819
  return mediaList;
808
820
  }
809
821
 
810
- // 消息队列处理器 - 确保消息按顺序处理,避免竞态条件
822
+ const DEFAULT_CONFIG = {
823
+ maxEntries: 100,
824
+ ttlMs: MESSAGE_CACHE_TTL_MS,
825
+ maxRetries: 3,
826
+ };
827
+ /**
828
+ * 按 accountId 隔离的消息缓存管理器
829
+ */
830
+ class MessageCache {
831
+ cache = new Map();
832
+ config;
833
+ accountId;
834
+ runtime;
835
+ constructor(accountId, config = {}, runtime) {
836
+ this.accountId = accountId;
837
+ this.config = { ...DEFAULT_CONFIG, ...config };
838
+ this.runtime = runtime;
839
+ }
840
+ /**
841
+ * 生成唯一缓存 key
842
+ */
843
+ generateCacheKey(reqId) {
844
+ return `${this.accountId}:${reqId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
845
+ }
846
+ /**
847
+ * 添加消息到缓存,返回缓存 key
848
+ */
849
+ add(params) {
850
+ this.prune();
851
+ if (this.cache.size >= this.config.maxEntries) {
852
+ // 优先淘汰非终态消息(finish=false 且 isError=false),保留终态消息
853
+ const entries = [...this.cache.entries()];
854
+ // 先找最老的非终态消息
855
+ const oldestNonFinal = entries
856
+ .filter(([, entry]) => !entry.finish && !entry.isError)
857
+ .sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
858
+ if (oldestNonFinal) {
859
+ this.cache.delete(oldestNonFinal[0]);
860
+ this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest non-final entry ${oldestNonFinal[0]}`);
861
+ }
862
+ else {
863
+ // 所有消息都是终态,才淘汰最老的
864
+ const oldest = entries.sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
865
+ if (oldest) {
866
+ this.cache.delete(oldest[0]);
867
+ this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest final entry ${oldest[0]} (all entries are final)`);
868
+ }
869
+ }
870
+ }
871
+ const cacheKey = this.generateCacheKey(params.reqId);
872
+ this.cache.set(cacheKey, {
873
+ cacheKey,
874
+ ...params,
875
+ createdAt: Date.now(),
876
+ retryCount: 0,
877
+ });
878
+ this.runtime?.log?.(`[53aihub] MessageCache: cached message reqId=${params.reqId}, cacheKey=${cacheKey}, cacheSize=${this.cache.size}`);
879
+ return cacheKey;
880
+ }
881
+ /**
882
+ * 获取所有待重试的消息,返回 cacheKey + entry
883
+ */
884
+ getPendingMessages() {
885
+ const now = Date.now();
886
+ const pending = [];
887
+ for (const [key, entry] of this.cache) {
888
+ if (now - entry.createdAt > this.config.ttlMs) {
889
+ this.cache.delete(key);
890
+ continue;
891
+ }
892
+ if (entry.retryCount >= this.config.maxRetries) {
893
+ this.cache.delete(key);
894
+ this.runtime?.log?.(`[53aihub] MessageCache: dropped message after max retries cacheKey=${key}`);
895
+ continue;
896
+ }
897
+ pending.push(entry);
898
+ }
899
+ return pending;
900
+ }
901
+ /**
902
+ * 按 cacheKey 标记消息为已发送
903
+ */
904
+ markSent(cacheKey) {
905
+ if (this.cache.has(cacheKey)) {
906
+ this.cache.delete(cacheKey);
907
+ this.runtime?.log?.(`[53aihub] MessageCache: marked as sent cacheKey=${cacheKey}`);
908
+ }
909
+ }
910
+ /**
911
+ * 按 cacheKey 增加重试计数
912
+ */
913
+ incrementRetry(cacheKey) {
914
+ const entry = this.cache.get(cacheKey);
915
+ if (entry) {
916
+ entry.retryCount++;
917
+ }
918
+ }
919
+ /**
920
+ * 清理过期条目
921
+ */
922
+ prune() {
923
+ const now = Date.now();
924
+ let pruned = 0;
925
+ for (const [key, entry] of this.cache) {
926
+ if (now - entry.createdAt > this.config.ttlMs) {
927
+ this.cache.delete(key);
928
+ pruned++;
929
+ }
930
+ }
931
+ if (pruned > 0) {
932
+ this.runtime?.log?.(`[53aihub] MessageCache: pruned ${pruned} expired entries`);
933
+ }
934
+ }
935
+ size() {
936
+ return this.cache.size;
937
+ }
938
+ clear() {
939
+ this.cache.clear();
940
+ }
941
+ }
942
+ // 按 accountId 隔离的缓存实例
943
+ const cacheInstances = new Map();
944
+ /**
945
+ * 获取或创建按账号隔离的缓存实例
946
+ */
947
+ function getMessageCache(accountId, runtime) {
948
+ let cache = cacheInstances.get(accountId);
949
+ if (!cache) {
950
+ cache = new MessageCache(accountId, {}, runtime);
951
+ cacheInstances.set(accountId, cache);
952
+ }
953
+ return cache;
954
+ }
955
+ /**
956
+ * 清理指定账号的缓存
957
+ */
958
+ function clearAccountCache(accountId) {
959
+ const cache = cacheInstances.get(accountId);
960
+ if (cache) {
961
+ cache.clear();
962
+ cacheInstances.delete(accountId);
963
+ }
964
+ }
965
+ /**
966
+ * 缓存感知的消息发送器
967
+ * 返回 [success, cacheKey?] - 成功时 cacheKey 为 undefined,失败时返回缓存 key
968
+ */
969
+ async function sendWithCache(accountId, params, sendFn) {
970
+ const { wsClient, replyToMsgId, streamId, text, toChatId, finish, isError, errorCode, errorDetails, isThinking, runtime } = params;
971
+ const reqId = replyToMsgId || streamId;
972
+ if (wsClient.readyState !== 1) {
973
+ const cache = getMessageCache(accountId, runtime);
974
+ const cacheKey = cache.add({
975
+ reqId,
976
+ toChatId,
977
+ text,
978
+ finish,
979
+ isError,
980
+ errorCode,
981
+ errorDetails,
982
+ isThinking,
983
+ });
984
+ runtime.error?.(`[53aihub] sendWithCache: WebSocket not ready, cached message cacheKey=${cacheKey}`);
985
+ return { success: false, cacheKey };
986
+ }
987
+ try {
988
+ await sendFn(params);
989
+ return { success: true };
990
+ }
991
+ catch (err) {
992
+ const cache = getMessageCache(accountId, runtime);
993
+ const cacheKey = cache.add({
994
+ reqId,
995
+ toChatId,
996
+ text,
997
+ finish,
998
+ isError,
999
+ errorCode,
1000
+ errorDetails,
1001
+ isThinking,
1002
+ });
1003
+ runtime.error?.(`[53aihub] sendWithCache: send failed, cached message cacheKey=${cacheKey}, error=${String(err)}`);
1004
+ return { success: false, cacheKey };
1005
+ }
1006
+ }
1007
+ /**
1008
+ * 重放缓存的消息
1009
+ */
1010
+ async function replayCachedMessages(accountId, wsClient, runtime, sendFn) {
1011
+ const cache = getMessageCache(accountId, runtime);
1012
+ const pending = cache.getPendingMessages();
1013
+ if (pending.length === 0) {
1014
+ return 0;
1015
+ }
1016
+ runtime.log?.(`[53aihub] replayCachedMessages: replaying ${pending.length} cached messages for account=${accountId}`);
1017
+ let successCount = 0;
1018
+ for (const entry of pending) {
1019
+ if (wsClient.readyState !== 1) {
1020
+ runtime.error?.(`[53aihub] replayCachedMessages: WebSocket not ready, stopping replay`);
1021
+ break;
1022
+ }
1023
+ try {
1024
+ await sendFn({
1025
+ wsClient,
1026
+ text: entry.text,
1027
+ toChatId: entry.toChatId,
1028
+ replyToMsgId: entry.reqId,
1029
+ runtime,
1030
+ finish: entry.finish,
1031
+ streamId: entry.reqId,
1032
+ isError: entry.isError,
1033
+ errorCode: entry.errorCode,
1034
+ errorDetails: entry.errorDetails,
1035
+ isThinking: entry.isThinking,
1036
+ });
1037
+ cache.markSent(entry.cacheKey);
1038
+ successCount++;
1039
+ await new Promise((resolve) => setTimeout(resolve, 100));
1040
+ }
1041
+ catch (err) {
1042
+ const newRetryCount = entry.retryCount + 1;
1043
+ cache.incrementRetry(entry.cacheKey);
1044
+ runtime.error?.(`[53aihub] replayCachedMessages: failed to send cacheKey=${entry.cacheKey}, retryCount=${newRetryCount}`);
1045
+ }
1046
+ }
1047
+ runtime.log?.(`[53aihub] replayCachedMessages: completed, ${successCount}/${pending.length} sent`);
1048
+ return successCount;
1049
+ }
1050
+
1051
+ var messageCache = /*#__PURE__*/Object.freeze({
1052
+ __proto__: null,
1053
+ clearAccountCache: clearAccountCache,
1054
+ getMessageCache: getMessageCache,
1055
+ replayCachedMessages: replayCachedMessages,
1056
+ sendWithCache: sendWithCache
1057
+ });
1058
+
1059
+ // 按 chatId 分队列的消息处理器 - 同一会话串行,不同会话并行
811
1060
  class MessageQueue {
812
- queue = [];
813
- processing = false;
1061
+ queues = new Map();
1062
+ processing = new Map();
814
1063
  runtime;
815
1064
  constructor(runtime) {
816
1065
  this.runtime = runtime;
817
1066
  }
818
- enqueue(task) {
819
- this.queue.push(task);
820
- this.process();
1067
+ enqueue(chatId, task) {
1068
+ if (!this.queues.has(chatId)) {
1069
+ this.queues.set(chatId, []);
1070
+ this.processing.set(chatId, false);
1071
+ }
1072
+ this.queues.get(chatId).push(task);
1073
+ this.process(chatId);
821
1074
  }
822
- async process() {
823
- if (this.processing || this.queue.length === 0)
1075
+ async process(chatId) {
1076
+ const queue = this.queues.get(chatId);
1077
+ const isProcessing = this.processing.get(chatId);
1078
+ if (!queue || isProcessing || queue.length === 0)
824
1079
  return;
825
- this.processing = true;
1080
+ this.processing.set(chatId, true);
826
1081
  try {
827
- const task = this.queue.shift();
1082
+ const task = queue.shift();
828
1083
  if (task) {
829
1084
  await task();
830
1085
  }
831
1086
  }
832
1087
  catch (err) {
833
- this.runtime.error?.(`[53aihub] MessageQueue error: ${String(err)}`);
1088
+ this.runtime.error?.(`[53aihub] MessageQueue error (chatId=${chatId}): ${String(err)}`);
834
1089
  }
835
1090
  finally {
836
- this.processing = false;
837
- // 继续处理队列中的下一条消息
838
- if (this.queue.length > 0) {
839
- this.process();
1091
+ this.processing.set(chatId, false);
1092
+ if (queue.length > 0) {
1093
+ this.process(chatId);
1094
+ }
1095
+ else {
1096
+ this.queues.delete(chatId);
1097
+ this.processing.delete(chatId);
840
1098
  }
841
1099
  }
842
1100
  }
843
1101
  clear() {
844
- this.queue = [];
1102
+ this.queues.clear();
1103
+ this.processing.clear();
845
1104
  }
846
1105
  }
847
1106
  function buildMessageContext(body, account, config, mediaList) {
@@ -906,20 +1165,26 @@ async function processMessage(params) {
906
1165
  runtime.log?.(`[53aihub] processMessage: chatId=${chatId}, msgId=${body.msgId}, text=${parsed.textParts.join(" ").substring(0, 50)}... images=${parsed.imageUrls.length} files=${parsed.fileUrls.length}`);
907
1166
  const core = getRuntime();
908
1167
  const streamId = `stream-${Date.now()}`;
1168
+ // 缓存感知的消息发送器 - 必须在所有 sendReply 调用前定义
1169
+ const cachedSend = async (sendParams) => {
1170
+ await sendWithCache(account.accountId, {
1171
+ ...sendParams,
1172
+ wsClient,
1173
+ runtime,
1174
+ }, sendReply);
1175
+ };
909
1176
  const accessResult = await checkAccessPolicy({
910
1177
  userId: body.userId,
911
1178
  account,
912
1179
  runtime,
913
1180
  });
914
1181
  if (!accessResult.allowed) {
915
- await sendReply({
916
- wsClient,
1182
+ await cachedSend({
917
1183
  text: accessResult.reason === "Pairing required"
918
1184
  ? "您尚未获得授权使用此机器人,请联系管理员进行审核。"
919
1185
  : `⚠️ 访问被拒绝: ${accessResult.reason || "未知原因"}`,
920
1186
  toChatId: chatId,
921
1187
  replyToMsgId: body.msgId,
922
- runtime,
923
1188
  finish: true,
924
1189
  streamId,
925
1190
  isError: true,
@@ -978,12 +1243,10 @@ async function processMessage(params) {
978
1243
  const errorMsg = payload.text || "Unknown error";
979
1244
  const errorCode = inferErrorCode(errorMsg);
980
1245
  runtime.error?.(`[53aihub] deliver ERROR: ${errorMsg}`);
981
- await sendReply({
982
- wsClient,
1246
+ await cachedSend({
983
1247
  text: `⚠️ ${errorMsg}`,
984
1248
  toChatId: chatId,
985
1249
  replyToMsgId: body.msgId,
986
- runtime,
987
1250
  finish: true,
988
1251
  streamId: state.streamId,
989
1252
  isError: true,
@@ -996,12 +1259,10 @@ async function processMessage(params) {
996
1259
  state.accumulatedText.startsWith("🧹 Compacting context");
997
1260
  if (isCompaction && info.kind !== "final") {
998
1261
  runtime.log?.(`[53aihub] deliver COMPACTION: text preview=${payload.text?.substring(0, 50)}...`);
999
- await sendReply({
1000
- wsClient,
1262
+ await cachedSend({
1001
1263
  text: payload.text || "",
1002
1264
  toChatId: chatId,
1003
1265
  replyToMsgId: body.msgId,
1004
- runtime,
1005
1266
  finish: false,
1006
1267
  streamId: state.streamId,
1007
1268
  isThinking: true,
@@ -1011,12 +1272,10 @@ async function processMessage(params) {
1011
1272
  state.accumulatedText += payload.text;
1012
1273
  if (info.kind !== "final") {
1013
1274
  runtime.log?.(`[53aihub] deliver STREAMING: accumulatedText preview=${state.accumulatedText.substring(0, 50)}...`);
1014
- await sendReply({
1015
- wsClient,
1275
+ await cachedSend({
1016
1276
  text: state.accumulatedText,
1017
1277
  toChatId: chatId,
1018
1278
  replyToMsgId: body.msgId,
1019
- runtime,
1020
1279
  finish: false,
1021
1280
  streamId: state.streamId,
1022
1281
  });
@@ -1026,47 +1285,36 @@ async function processMessage(params) {
1026
1285
  runtime.error?.(`[53aihub] onError: kind=${info.kind}, error=${String(err)}`);
1027
1286
  const errorText = String(err);
1028
1287
  const errorCode = inferErrorCode(errorText);
1029
- try {
1030
- await sendReply({
1031
- wsClient,
1032
- text: `⚠️ 系统错误: ${errorText}`,
1033
- toChatId: chatId,
1034
- replyToMsgId: body.msgId,
1035
- runtime,
1036
- finish: true,
1037
- streamId: state.streamId,
1038
- isError: true,
1039
- errorCode,
1040
- errorDetails: `kind=${info.kind}, error=${errorText}`,
1041
- });
1042
- }
1043
- catch (sendErr) {
1044
- runtime.error?.(`[53aihub] Failed to send error notification: ${String(sendErr)}`);
1045
- }
1288
+ await cachedSend({
1289
+ text: `⚠️ 系统错误: ${errorText}`,
1290
+ toChatId: chatId,
1291
+ replyToMsgId: body.msgId,
1292
+ finish: true,
1293
+ streamId: state.streamId,
1294
+ isError: true,
1295
+ errorCode,
1296
+ errorDetails: `kind=${info.kind}, error=${errorText}`,
1297
+ });
1046
1298
  },
1047
1299
  },
1048
1300
  }), MESSAGE_PROCESS_TIMEOUT_MS, `Message processing timed out (msgId=${body.msgId})`);
1049
1301
  runtime.log?.(`[53aihub] processMessage: dispatchReply completed, accumulatedTextLen=${state.accumulatedText.length}`);
1050
1302
  if (state.accumulatedText) {
1051
1303
  runtime.log?.(`[53aihub] processMessage: Sending final reply with accumulatedText`);
1052
- await sendReply({
1053
- wsClient,
1304
+ await cachedSend({
1054
1305
  text: state.accumulatedText,
1055
1306
  toChatId: chatId,
1056
1307
  replyToMsgId: body.msgId,
1057
- runtime,
1058
1308
  finish: true,
1059
1309
  streamId: state.streamId,
1060
1310
  });
1061
1311
  }
1062
1312
  else {
1063
1313
  runtime.log?.(`[53aihub] processMessage: No accumulatedText, sending empty final reply`);
1064
- await sendReply({
1065
- wsClient,
1314
+ await cachedSend({
1066
1315
  text: "",
1067
1316
  toChatId: chatId,
1068
1317
  replyToMsgId: body.msgId,
1069
- runtime,
1070
1318
  finish: true,
1071
1319
  streamId: state.streamId,
1072
1320
  });
@@ -1079,12 +1327,10 @@ async function processMessage(params) {
1079
1327
  const errorCode = inferErrorCode(errorText);
1080
1328
  if (!cleanedUp) {
1081
1329
  try {
1082
- await sendReply({
1083
- wsClient,
1330
+ await cachedSend({
1084
1331
  text: `⚠️ 处理请求时发生异常: ${errorText}`,
1085
1332
  toChatId: chatId,
1086
1333
  replyToMsgId: body.msgId,
1087
- runtime,
1088
1334
  finish: true,
1089
1335
  streamId: state.streamId,
1090
1336
  isError: true,
@@ -1109,17 +1355,6 @@ async function monitorProvider(options) {
1109
1355
  let pingInterval = null;
1110
1356
  let isAborted = false;
1111
1357
  let messageQueue = null;
1112
- const cleanup = async () => {
1113
- if (pingInterval) {
1114
- clearInterval(pingInterval);
1115
- pingInterval = null;
1116
- }
1117
- if (messageQueue) {
1118
- messageQueue.clear();
1119
- messageQueue = null;
1120
- }
1121
- await cleanupAccount(account.accountId);
1122
- };
1123
1358
  const connect = () => {
1124
1359
  if (isAborted)
1125
1360
  return;
@@ -1144,9 +1379,18 @@ async function monitorProvider(options) {
1144
1379
  setWebSocket(account.accountId, wsClient);
1145
1380
  // 初始化消息队列
1146
1381
  messageQueue = new MessageQueue(runtime);
1147
- wsClient.on("open", () => {
1382
+ wsClient.on("open", async () => {
1148
1383
  runtime.log?.(`[${account.accountId}] WebSocket connected successfully`);
1149
1384
  reconnectAttempts = 0;
1385
+ try {
1386
+ const replayedCount = await replayCachedMessages(account.accountId, wsClient, runtime, sendReply);
1387
+ if (replayedCount > 0) {
1388
+ runtime.log?.(`[${account.accountId}] Replayed ${replayedCount} cached messages`);
1389
+ }
1390
+ }
1391
+ catch (err) {
1392
+ runtime.error?.(`[${account.accountId}] Failed to replay cached messages: ${String(err)}`);
1393
+ }
1150
1394
  pingInterval = setInterval(() => {
1151
1395
  if (wsClient?.readyState === WebSocket.OPEN) {
1152
1396
  wsClient.ping();
@@ -1156,8 +1400,20 @@ async function monitorProvider(options) {
1156
1400
  wsClient.on("message", (data) => {
1157
1401
  const rawPayload = data.toString();
1158
1402
  runtime.log?.(`[${account.accountId}] Received WS message: ${rawPayload.substring(0, 200)}...`);
1159
- // 使用消息队列确保顺序处理,避免竞态条件
1160
- messageQueue?.enqueue(async () => {
1403
+ let chatId = "unknown";
1404
+ try {
1405
+ const msg = JSON.parse(rawPayload);
1406
+ if (msg.action === "chat") {
1407
+ chatId = msg.data?.conversation_id || msg.data?.user || "unknown";
1408
+ }
1409
+ else {
1410
+ chatId = msg.data?.chatId || msg.data?.userId || "unknown";
1411
+ }
1412
+ }
1413
+ catch {
1414
+ chatId = "unknown";
1415
+ }
1416
+ messageQueue?.enqueue(chatId, async () => {
1161
1417
  await processMessage({
1162
1418
  rawPayload,
1163
1419
  account,
@@ -1188,8 +1444,8 @@ async function monitorProvider(options) {
1188
1444
  setTimeout(connect, backoff);
1189
1445
  }
1190
1446
  else if (!isAborted) {
1191
- runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`);
1192
- await cleanup();
1447
+ runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached, preserving cache for external recovery`);
1448
+ await cleanupAccount(account.accountId, false);
1193
1449
  reject(new Error(`Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`));
1194
1450
  }
1195
1451
  });
@@ -1197,15 +1453,23 @@ async function monitorProvider(options) {
1197
1453
  if (abortSignal) {
1198
1454
  abortSignal.addEventListener("abort", async () => {
1199
1455
  isAborted = true;
1200
- await cleanup();
1456
+ if (pingInterval) {
1457
+ clearInterval(pingInterval);
1458
+ pingInterval = null;
1459
+ }
1460
+ if (messageQueue) {
1461
+ messageQueue.clear();
1462
+ messageQueue = null;
1463
+ }
1464
+ await cleanupAccount(account.accountId, true);
1201
1465
  resolve();
1202
1466
  });
1203
1467
  }
1204
1468
  warmupReqIdStore(account.accountId, (msg) => runtime.log?.(msg))
1205
1469
  .then(() => connect())
1206
1470
  .catch(async (err) => {
1207
- runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}`);
1208
- await cleanup();
1471
+ runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}, preserving cache for retry`);
1472
+ await cleanupAccount(account.accountId, false);
1209
1473
  reject(err);
1210
1474
  });
1211
1475
  });