@53ai/53ai-openclaw 1.0.9 → 1.1.1
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 +15 -0
- package/dist/index.cjs.js +393 -83
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +393 -83
- package/dist/index.esm.js.map +1 -1
- package/dist/src/const.d.ts +21 -2
- package/dist/src/message-cache.d.ts +119 -0
- package/dist/src/state-manager.d.ts +1 -1
- package/openclaw.plugin.json +6 -1
- package/package.json +2 -2
package/dist/index.cjs.js
CHANGED
|
@@ -58,8 +58,32 @@ const WS_RECONNECT_BASE_DELAY_MS = 1000;
|
|
|
58
58
|
// ============================================================================
|
|
59
59
|
/** 文本分块限制 */
|
|
60
60
|
const TEXT_CHUNK_LIMIT = 4000;
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* 请求分析软阈值(毫秒)
|
|
63
|
+
* 超过该时间后只告警并继续等待,不直接中断长分析任务。
|
|
64
|
+
*/
|
|
65
|
+
const REQUEST_ANALYSIS_TIMEOUT_MS = 600000;
|
|
66
|
+
/**
|
|
67
|
+
* 读取请求分析软阈值的运行时覆盖值。
|
|
68
|
+
* 仅用于本地或特定部署快速调参;未设置时回退到默认值。
|
|
69
|
+
*/
|
|
70
|
+
function getRequestAnalysisTimeoutMs() {
|
|
71
|
+
const raw = process.env.OPENCLAW_REQUEST_ANALYSIS_TIMEOUT_MS?.trim();
|
|
72
|
+
if (!raw) {
|
|
73
|
+
return REQUEST_ANALYSIS_TIMEOUT_MS;
|
|
74
|
+
}
|
|
75
|
+
const parsed = Number(raw);
|
|
76
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
77
|
+
return REQUEST_ANALYSIS_TIMEOUT_MS;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 消息缓存 TTL(毫秒)
|
|
83
|
+
* 设为 35 分钟,确保覆盖最大重连时间(约 28.5 分钟)+ 缓冲
|
|
84
|
+
* @see WS_MAX_RECONNECT_ATTEMPTS - 60 次,指数退避最大 30s
|
|
85
|
+
*/
|
|
86
|
+
const MESSAGE_CACHE_TTL_MS = 35 * 60 * 1000;
|
|
63
87
|
// ============================================================================
|
|
64
88
|
// 媒体处理配置
|
|
65
89
|
// ============================================================================
|
|
@@ -338,8 +362,9 @@ async function sendReply(params) {
|
|
|
338
362
|
const reqId = replyToMsgId || streamId;
|
|
339
363
|
runtime.log?.(`[53aihub] sendReply START: reqId=${reqId}, finish=${finish}, isError=${isError}, textLen=${text?.length || 0}, wsReadyState=${wsClient.readyState}`);
|
|
340
364
|
if (wsClient.readyState !== 1) {
|
|
341
|
-
|
|
342
|
-
|
|
365
|
+
const errorMsg = `WebSocket not connected (readyState=${wsClient.readyState}), cannot send message to ${toChatId}`;
|
|
366
|
+
runtime.error?.(`[53aihub] ${errorMsg}`);
|
|
367
|
+
throw new Error(`[53aihub] ${errorMsg}`);
|
|
343
368
|
}
|
|
344
369
|
if (isError) {
|
|
345
370
|
runtime.error?.(`[53aihub] sendReply ERROR: reqId=${reqId}, code=${errorCode}, text=${text?.substring(0, 100)}`);
|
|
@@ -450,8 +475,9 @@ async function sendThinkingMessage(wsClient, text, msgId, streamId, runtime) {
|
|
|
450
475
|
const wsState = wsClient.readyState;
|
|
451
476
|
runtime?.log?.(`[53aihub] sendThinkingMessage CALLED: msgId=${msgId}, streamId=${streamId}, text=${text}, wsReadyState=${wsState}`);
|
|
452
477
|
if (wsState !== 1) {
|
|
453
|
-
|
|
454
|
-
|
|
478
|
+
const errorMsg = `WebSocket not ready (state=${wsState})`;
|
|
479
|
+
runtime?.error?.(`[53aihub] sendThinkingMessage FAILED: ${errorMsg}`);
|
|
480
|
+
throw new Error(`[53aihub] ${errorMsg}`);
|
|
455
481
|
}
|
|
456
482
|
// 使用 OpenAI 兼容格式,确保 Go 后端能正确解析
|
|
457
483
|
// 关键:req_id 必须使用原始消息的 msgId,这样 Go 后端才能关联请求和响应
|
|
@@ -686,7 +712,7 @@ async function warmupReqIdStore(accountId = "default", log) {
|
|
|
686
712
|
const store = getOrCreateReqIdStore(accountId);
|
|
687
713
|
return store.warmup((err) => log?.(`ReqId warmup error: ${String(err)}`));
|
|
688
714
|
}
|
|
689
|
-
async function cleanupAccount(accountId) {
|
|
715
|
+
async function cleanupAccount(accountId, clearCache = false) {
|
|
690
716
|
const wsClient = wsClientInstances.get(accountId);
|
|
691
717
|
if (wsClient) {
|
|
692
718
|
try {
|
|
@@ -700,6 +726,10 @@ async function cleanupAccount(accountId) {
|
|
|
700
726
|
const store = reqIdStores.get(accountId);
|
|
701
727
|
if (store)
|
|
702
728
|
await store.flush();
|
|
729
|
+
if (clearCache) {
|
|
730
|
+
const { clearAccountCache } = await Promise.resolve().then(function () { return messageCache; });
|
|
731
|
+
clearAccountCache(accountId);
|
|
732
|
+
}
|
|
703
733
|
}
|
|
704
734
|
|
|
705
735
|
/**
|
|
@@ -807,41 +837,288 @@ async function downloadAndSaveFiles(params) {
|
|
|
807
837
|
return mediaList;
|
|
808
838
|
}
|
|
809
839
|
|
|
810
|
-
|
|
840
|
+
const DEFAULT_CONFIG = {
|
|
841
|
+
maxEntries: 100,
|
|
842
|
+
ttlMs: MESSAGE_CACHE_TTL_MS,
|
|
843
|
+
maxRetries: 3,
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* 按 accountId 隔离的消息缓存管理器
|
|
847
|
+
*/
|
|
848
|
+
class MessageCache {
|
|
849
|
+
cache = new Map();
|
|
850
|
+
config;
|
|
851
|
+
accountId;
|
|
852
|
+
runtime;
|
|
853
|
+
constructor(accountId, config = {}, runtime) {
|
|
854
|
+
this.accountId = accountId;
|
|
855
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
856
|
+
this.runtime = runtime;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* 生成唯一缓存 key
|
|
860
|
+
*/
|
|
861
|
+
generateCacheKey(reqId) {
|
|
862
|
+
return `${this.accountId}:${reqId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* 添加消息到缓存,返回缓存 key
|
|
866
|
+
*/
|
|
867
|
+
add(params) {
|
|
868
|
+
this.prune();
|
|
869
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
870
|
+
// 优先淘汰非终态消息(finish=false 且 isError=false),保留终态消息
|
|
871
|
+
const entries = [...this.cache.entries()];
|
|
872
|
+
// 先找最老的非终态消息
|
|
873
|
+
const oldestNonFinal = entries
|
|
874
|
+
.filter(([, entry]) => !entry.finish && !entry.isError)
|
|
875
|
+
.sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
|
|
876
|
+
if (oldestNonFinal) {
|
|
877
|
+
this.cache.delete(oldestNonFinal[0]);
|
|
878
|
+
this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest non-final entry ${oldestNonFinal[0]}`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
// 所有消息都是终态,才淘汰最老的
|
|
882
|
+
const oldest = entries.sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
|
|
883
|
+
if (oldest) {
|
|
884
|
+
this.cache.delete(oldest[0]);
|
|
885
|
+
this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest final entry ${oldest[0]} (all entries are final)`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const cacheKey = this.generateCacheKey(params.reqId);
|
|
890
|
+
this.cache.set(cacheKey, {
|
|
891
|
+
cacheKey,
|
|
892
|
+
...params,
|
|
893
|
+
createdAt: Date.now(),
|
|
894
|
+
retryCount: 0,
|
|
895
|
+
});
|
|
896
|
+
this.runtime?.log?.(`[53aihub] MessageCache: cached message reqId=${params.reqId}, cacheKey=${cacheKey}, cacheSize=${this.cache.size}`);
|
|
897
|
+
return cacheKey;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* 获取所有待重试的消息,返回 cacheKey + entry
|
|
901
|
+
*/
|
|
902
|
+
getPendingMessages() {
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
const pending = [];
|
|
905
|
+
for (const [key, entry] of this.cache) {
|
|
906
|
+
if (now - entry.createdAt > this.config.ttlMs) {
|
|
907
|
+
this.cache.delete(key);
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (entry.retryCount >= this.config.maxRetries) {
|
|
911
|
+
this.cache.delete(key);
|
|
912
|
+
this.runtime?.log?.(`[53aihub] MessageCache: dropped message after max retries cacheKey=${key}`);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
pending.push(entry);
|
|
916
|
+
}
|
|
917
|
+
return pending;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* 按 cacheKey 标记消息为已发送
|
|
921
|
+
*/
|
|
922
|
+
markSent(cacheKey) {
|
|
923
|
+
if (this.cache.has(cacheKey)) {
|
|
924
|
+
this.cache.delete(cacheKey);
|
|
925
|
+
this.runtime?.log?.(`[53aihub] MessageCache: marked as sent cacheKey=${cacheKey}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* 按 cacheKey 增加重试计数
|
|
930
|
+
*/
|
|
931
|
+
incrementRetry(cacheKey) {
|
|
932
|
+
const entry = this.cache.get(cacheKey);
|
|
933
|
+
if (entry) {
|
|
934
|
+
entry.retryCount++;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* 清理过期条目
|
|
939
|
+
*/
|
|
940
|
+
prune() {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
let pruned = 0;
|
|
943
|
+
for (const [key, entry] of this.cache) {
|
|
944
|
+
if (now - entry.createdAt > this.config.ttlMs) {
|
|
945
|
+
this.cache.delete(key);
|
|
946
|
+
pruned++;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (pruned > 0) {
|
|
950
|
+
this.runtime?.log?.(`[53aihub] MessageCache: pruned ${pruned} expired entries`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
size() {
|
|
954
|
+
return this.cache.size;
|
|
955
|
+
}
|
|
956
|
+
clear() {
|
|
957
|
+
this.cache.clear();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// 按 accountId 隔离的缓存实例
|
|
961
|
+
const cacheInstances = new Map();
|
|
962
|
+
/**
|
|
963
|
+
* 获取或创建按账号隔离的缓存实例
|
|
964
|
+
*/
|
|
965
|
+
function getMessageCache(accountId, runtime) {
|
|
966
|
+
let cache = cacheInstances.get(accountId);
|
|
967
|
+
if (!cache) {
|
|
968
|
+
cache = new MessageCache(accountId, {}, runtime);
|
|
969
|
+
cacheInstances.set(accountId, cache);
|
|
970
|
+
}
|
|
971
|
+
return cache;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* 清理指定账号的缓存
|
|
975
|
+
*/
|
|
976
|
+
function clearAccountCache(accountId) {
|
|
977
|
+
const cache = cacheInstances.get(accountId);
|
|
978
|
+
if (cache) {
|
|
979
|
+
cache.clear();
|
|
980
|
+
cacheInstances.delete(accountId);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* 缓存感知的消息发送器
|
|
985
|
+
* 返回 [success, cacheKey?] - 成功时 cacheKey 为 undefined,失败时返回缓存 key
|
|
986
|
+
*/
|
|
987
|
+
async function sendWithCache(accountId, params, sendFn) {
|
|
988
|
+
const { wsClient, replyToMsgId, streamId, text, toChatId, finish, isError, errorCode, errorDetails, isThinking, runtime } = params;
|
|
989
|
+
const reqId = replyToMsgId || streamId;
|
|
990
|
+
if (wsClient.readyState !== 1) {
|
|
991
|
+
const cache = getMessageCache(accountId, runtime);
|
|
992
|
+
const cacheKey = cache.add({
|
|
993
|
+
reqId,
|
|
994
|
+
toChatId,
|
|
995
|
+
text,
|
|
996
|
+
finish,
|
|
997
|
+
isError,
|
|
998
|
+
errorCode,
|
|
999
|
+
errorDetails,
|
|
1000
|
+
isThinking,
|
|
1001
|
+
});
|
|
1002
|
+
runtime.error?.(`[53aihub] sendWithCache: WebSocket not ready, cached message cacheKey=${cacheKey}`);
|
|
1003
|
+
return { success: false, cacheKey };
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
await sendFn(params);
|
|
1007
|
+
return { success: true };
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
const cache = getMessageCache(accountId, runtime);
|
|
1011
|
+
const cacheKey = cache.add({
|
|
1012
|
+
reqId,
|
|
1013
|
+
toChatId,
|
|
1014
|
+
text,
|
|
1015
|
+
finish,
|
|
1016
|
+
isError,
|
|
1017
|
+
errorCode,
|
|
1018
|
+
errorDetails,
|
|
1019
|
+
isThinking,
|
|
1020
|
+
});
|
|
1021
|
+
runtime.error?.(`[53aihub] sendWithCache: send failed, cached message cacheKey=${cacheKey}, error=${String(err)}`);
|
|
1022
|
+
return { success: false, cacheKey };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* 重放缓存的消息
|
|
1027
|
+
*/
|
|
1028
|
+
async function replayCachedMessages(accountId, wsClient, runtime, sendFn) {
|
|
1029
|
+
const cache = getMessageCache(accountId, runtime);
|
|
1030
|
+
const pending = cache.getPendingMessages();
|
|
1031
|
+
if (pending.length === 0) {
|
|
1032
|
+
return 0;
|
|
1033
|
+
}
|
|
1034
|
+
runtime.log?.(`[53aihub] replayCachedMessages: replaying ${pending.length} cached messages for account=${accountId}`);
|
|
1035
|
+
let successCount = 0;
|
|
1036
|
+
for (const entry of pending) {
|
|
1037
|
+
if (wsClient.readyState !== 1) {
|
|
1038
|
+
runtime.error?.(`[53aihub] replayCachedMessages: WebSocket not ready, stopping replay`);
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
await sendFn({
|
|
1043
|
+
wsClient,
|
|
1044
|
+
text: entry.text,
|
|
1045
|
+
toChatId: entry.toChatId,
|
|
1046
|
+
replyToMsgId: entry.reqId,
|
|
1047
|
+
runtime,
|
|
1048
|
+
finish: entry.finish,
|
|
1049
|
+
streamId: entry.reqId,
|
|
1050
|
+
isError: entry.isError,
|
|
1051
|
+
errorCode: entry.errorCode,
|
|
1052
|
+
errorDetails: entry.errorDetails,
|
|
1053
|
+
isThinking: entry.isThinking,
|
|
1054
|
+
});
|
|
1055
|
+
cache.markSent(entry.cacheKey);
|
|
1056
|
+
successCount++;
|
|
1057
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1058
|
+
}
|
|
1059
|
+
catch (err) {
|
|
1060
|
+
const newRetryCount = entry.retryCount + 1;
|
|
1061
|
+
cache.incrementRetry(entry.cacheKey);
|
|
1062
|
+
runtime.error?.(`[53aihub] replayCachedMessages: failed to send cacheKey=${entry.cacheKey}, retryCount=${newRetryCount}`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
runtime.log?.(`[53aihub] replayCachedMessages: completed, ${successCount}/${pending.length} sent`);
|
|
1066
|
+
return successCount;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
var messageCache = /*#__PURE__*/Object.freeze({
|
|
1070
|
+
__proto__: null,
|
|
1071
|
+
clearAccountCache: clearAccountCache,
|
|
1072
|
+
getMessageCache: getMessageCache,
|
|
1073
|
+
replayCachedMessages: replayCachedMessages,
|
|
1074
|
+
sendWithCache: sendWithCache
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// 按 chatId 分队列的消息处理器 - 同一会话串行,不同会话并行
|
|
811
1078
|
class MessageQueue {
|
|
812
|
-
|
|
813
|
-
processing =
|
|
1079
|
+
queues = new Map();
|
|
1080
|
+
processing = new Map();
|
|
814
1081
|
runtime;
|
|
815
1082
|
constructor(runtime) {
|
|
816
1083
|
this.runtime = runtime;
|
|
817
1084
|
}
|
|
818
|
-
enqueue(task) {
|
|
819
|
-
this.
|
|
820
|
-
|
|
1085
|
+
enqueue(chatId, task) {
|
|
1086
|
+
if (!this.queues.has(chatId)) {
|
|
1087
|
+
this.queues.set(chatId, []);
|
|
1088
|
+
this.processing.set(chatId, false);
|
|
1089
|
+
}
|
|
1090
|
+
this.queues.get(chatId).push(task);
|
|
1091
|
+
this.process(chatId);
|
|
821
1092
|
}
|
|
822
|
-
async process() {
|
|
823
|
-
|
|
1093
|
+
async process(chatId) {
|
|
1094
|
+
const queue = this.queues.get(chatId);
|
|
1095
|
+
const isProcessing = this.processing.get(chatId);
|
|
1096
|
+
if (!queue || isProcessing || queue.length === 0)
|
|
824
1097
|
return;
|
|
825
|
-
this.processing
|
|
1098
|
+
this.processing.set(chatId, true);
|
|
826
1099
|
try {
|
|
827
|
-
const task =
|
|
1100
|
+
const task = queue.shift();
|
|
828
1101
|
if (task) {
|
|
829
1102
|
await task();
|
|
830
1103
|
}
|
|
831
1104
|
}
|
|
832
1105
|
catch (err) {
|
|
833
|
-
this.runtime.error?.(`[53aihub] MessageQueue error: ${String(err)}`);
|
|
1106
|
+
this.runtime.error?.(`[53aihub] MessageQueue error (chatId=${chatId}): ${String(err)}`);
|
|
834
1107
|
}
|
|
835
1108
|
finally {
|
|
836
|
-
this.processing
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1109
|
+
this.processing.set(chatId, false);
|
|
1110
|
+
if (queue.length > 0) {
|
|
1111
|
+
this.process(chatId);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
this.queues.delete(chatId);
|
|
1115
|
+
this.processing.delete(chatId);
|
|
840
1116
|
}
|
|
841
1117
|
}
|
|
842
1118
|
}
|
|
843
1119
|
clear() {
|
|
844
|
-
this.
|
|
1120
|
+
this.queues.clear();
|
|
1121
|
+
this.processing.clear();
|
|
845
1122
|
}
|
|
846
1123
|
}
|
|
847
1124
|
function buildMessageContext(body, account, config, mediaList) {
|
|
@@ -906,20 +1183,26 @@ async function processMessage(params) {
|
|
|
906
1183
|
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
1184
|
const core = getRuntime();
|
|
908
1185
|
const streamId = `stream-${Date.now()}`;
|
|
1186
|
+
// 缓存感知的消息发送器 - 必须在所有 sendReply 调用前定义
|
|
1187
|
+
const cachedSend = async (sendParams) => {
|
|
1188
|
+
await sendWithCache(account.accountId, {
|
|
1189
|
+
...sendParams,
|
|
1190
|
+
wsClient,
|
|
1191
|
+
runtime,
|
|
1192
|
+
}, sendReply);
|
|
1193
|
+
};
|
|
909
1194
|
const accessResult = await checkAccessPolicy({
|
|
910
1195
|
userId: body.userId,
|
|
911
1196
|
account,
|
|
912
1197
|
runtime,
|
|
913
1198
|
});
|
|
914
1199
|
if (!accessResult.allowed) {
|
|
915
|
-
await
|
|
916
|
-
wsClient,
|
|
1200
|
+
await cachedSend({
|
|
917
1201
|
text: accessResult.reason === "Pairing required"
|
|
918
1202
|
? "您尚未获得授权使用此机器人,请联系管理员进行审核。"
|
|
919
1203
|
: `⚠️ 访问被拒绝: ${accessResult.reason || "未知原因"}`,
|
|
920
1204
|
toChatId: chatId,
|
|
921
1205
|
replyToMsgId: body.msgId,
|
|
922
|
-
runtime,
|
|
923
1206
|
finish: true,
|
|
924
1207
|
streamId,
|
|
925
1208
|
isError: true,
|
|
@@ -960,6 +1243,8 @@ async function processMessage(params) {
|
|
|
960
1243
|
const mediaList = [...imageMediaList, ...fileMediaList];
|
|
961
1244
|
const ctxPayload = buildMessageContext(body, account, config, mediaList);
|
|
962
1245
|
let cleanedUp = false;
|
|
1246
|
+
let analysisTimeoutTimer = null;
|
|
1247
|
+
const analysisTimeoutMs = getRequestAnalysisTimeoutMs();
|
|
963
1248
|
const safeCleanup = () => {
|
|
964
1249
|
if (!cleanedUp) {
|
|
965
1250
|
cleanedUp = true;
|
|
@@ -968,7 +1253,20 @@ async function processMessage(params) {
|
|
|
968
1253
|
};
|
|
969
1254
|
runtime.log?.(`[53aihub] processMessage: Starting dispatchReplyWithBufferedBlockDispatcher for msgId=${body.msgId}`);
|
|
970
1255
|
try {
|
|
971
|
-
|
|
1256
|
+
analysisTimeoutTimer = setTimeout(() => {
|
|
1257
|
+
runtime.log?.(`[53aihub] processMessage: analysis exceeded soft threshold ${analysisTimeoutMs}ms for msgId=${body.msgId}, keep waiting for completion`);
|
|
1258
|
+
if (wsClient.readyState !== WebSocket.OPEN) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
wsClient.ping();
|
|
1263
|
+
runtime.log?.(`[53aihub] processMessage: timeout keepalive ping sent for msgId=${body.msgId}`);
|
|
1264
|
+
}
|
|
1265
|
+
catch (err) {
|
|
1266
|
+
runtime.error?.(`[53aihub] processMessage: timeout keepalive ping failed for msgId=${body.msgId}: ${String(err)}`);
|
|
1267
|
+
}
|
|
1268
|
+
}, analysisTimeoutMs);
|
|
1269
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
972
1270
|
ctx: ctxPayload,
|
|
973
1271
|
cfg: config,
|
|
974
1272
|
dispatcherOptions: {
|
|
@@ -978,12 +1276,10 @@ async function processMessage(params) {
|
|
|
978
1276
|
const errorMsg = payload.text || "Unknown error";
|
|
979
1277
|
const errorCode = inferErrorCode(errorMsg);
|
|
980
1278
|
runtime.error?.(`[53aihub] deliver ERROR: ${errorMsg}`);
|
|
981
|
-
await
|
|
982
|
-
wsClient,
|
|
1279
|
+
await cachedSend({
|
|
983
1280
|
text: `⚠️ ${errorMsg}`,
|
|
984
1281
|
toChatId: chatId,
|
|
985
1282
|
replyToMsgId: body.msgId,
|
|
986
|
-
runtime,
|
|
987
1283
|
finish: true,
|
|
988
1284
|
streamId: state.streamId,
|
|
989
1285
|
isError: true,
|
|
@@ -996,12 +1292,10 @@ async function processMessage(params) {
|
|
|
996
1292
|
state.accumulatedText.startsWith("🧹 Compacting context");
|
|
997
1293
|
if (isCompaction && info.kind !== "final") {
|
|
998
1294
|
runtime.log?.(`[53aihub] deliver COMPACTION: text preview=${payload.text?.substring(0, 50)}...`);
|
|
999
|
-
await
|
|
1000
|
-
wsClient,
|
|
1295
|
+
await cachedSend({
|
|
1001
1296
|
text: payload.text || "",
|
|
1002
1297
|
toChatId: chatId,
|
|
1003
1298
|
replyToMsgId: body.msgId,
|
|
1004
|
-
runtime,
|
|
1005
1299
|
finish: false,
|
|
1006
1300
|
streamId: state.streamId,
|
|
1007
1301
|
isThinking: true,
|
|
@@ -1011,12 +1305,10 @@ async function processMessage(params) {
|
|
|
1011
1305
|
state.accumulatedText += payload.text;
|
|
1012
1306
|
if (info.kind !== "final") {
|
|
1013
1307
|
runtime.log?.(`[53aihub] deliver STREAMING: accumulatedText preview=${state.accumulatedText.substring(0, 50)}...`);
|
|
1014
|
-
await
|
|
1015
|
-
wsClient,
|
|
1308
|
+
await cachedSend({
|
|
1016
1309
|
text: state.accumulatedText,
|
|
1017
1310
|
toChatId: chatId,
|
|
1018
1311
|
replyToMsgId: body.msgId,
|
|
1019
|
-
runtime,
|
|
1020
1312
|
finish: false,
|
|
1021
1313
|
streamId: state.streamId,
|
|
1022
1314
|
});
|
|
@@ -1026,47 +1318,36 @@ async function processMessage(params) {
|
|
|
1026
1318
|
runtime.error?.(`[53aihub] onError: kind=${info.kind}, error=${String(err)}`);
|
|
1027
1319
|
const errorText = String(err);
|
|
1028
1320
|
const errorCode = inferErrorCode(errorText);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
}
|
|
1321
|
+
await cachedSend({
|
|
1322
|
+
text: `⚠️ 系统错误: ${errorText}`,
|
|
1323
|
+
toChatId: chatId,
|
|
1324
|
+
replyToMsgId: body.msgId,
|
|
1325
|
+
finish: true,
|
|
1326
|
+
streamId: state.streamId,
|
|
1327
|
+
isError: true,
|
|
1328
|
+
errorCode,
|
|
1329
|
+
errorDetails: `kind=${info.kind}, error=${errorText}`,
|
|
1330
|
+
});
|
|
1046
1331
|
},
|
|
1047
1332
|
},
|
|
1048
|
-
})
|
|
1333
|
+
});
|
|
1049
1334
|
runtime.log?.(`[53aihub] processMessage: dispatchReply completed, accumulatedTextLen=${state.accumulatedText.length}`);
|
|
1050
1335
|
if (state.accumulatedText) {
|
|
1051
1336
|
runtime.log?.(`[53aihub] processMessage: Sending final reply with accumulatedText`);
|
|
1052
|
-
await
|
|
1053
|
-
wsClient,
|
|
1337
|
+
await cachedSend({
|
|
1054
1338
|
text: state.accumulatedText,
|
|
1055
1339
|
toChatId: chatId,
|
|
1056
1340
|
replyToMsgId: body.msgId,
|
|
1057
|
-
runtime,
|
|
1058
1341
|
finish: true,
|
|
1059
1342
|
streamId: state.streamId,
|
|
1060
1343
|
});
|
|
1061
1344
|
}
|
|
1062
1345
|
else {
|
|
1063
1346
|
runtime.log?.(`[53aihub] processMessage: No accumulatedText, sending empty final reply`);
|
|
1064
|
-
await
|
|
1065
|
-
wsClient,
|
|
1347
|
+
await cachedSend({
|
|
1066
1348
|
text: "",
|
|
1067
1349
|
toChatId: chatId,
|
|
1068
1350
|
replyToMsgId: body.msgId,
|
|
1069
|
-
runtime,
|
|
1070
1351
|
finish: true,
|
|
1071
1352
|
streamId: state.streamId,
|
|
1072
1353
|
});
|
|
@@ -1079,12 +1360,10 @@ async function processMessage(params) {
|
|
|
1079
1360
|
const errorCode = inferErrorCode(errorText);
|
|
1080
1361
|
if (!cleanedUp) {
|
|
1081
1362
|
try {
|
|
1082
|
-
await
|
|
1083
|
-
wsClient,
|
|
1363
|
+
await cachedSend({
|
|
1084
1364
|
text: `⚠️ 处理请求时发生异常: ${errorText}`,
|
|
1085
1365
|
toChatId: chatId,
|
|
1086
1366
|
replyToMsgId: body.msgId,
|
|
1087
|
-
runtime,
|
|
1088
1367
|
finish: true,
|
|
1089
1368
|
streamId: state.streamId,
|
|
1090
1369
|
isError: true,
|
|
@@ -1098,6 +1377,12 @@ async function processMessage(params) {
|
|
|
1098
1377
|
}
|
|
1099
1378
|
safeCleanup();
|
|
1100
1379
|
}
|
|
1380
|
+
finally {
|
|
1381
|
+
if (analysisTimeoutTimer) {
|
|
1382
|
+
clearTimeout(analysisTimeoutTimer);
|
|
1383
|
+
analysisTimeoutTimer = null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1101
1386
|
}
|
|
1102
1387
|
async function monitorProvider(options) {
|
|
1103
1388
|
const { account, config, runtime, abortSignal } = options;
|
|
@@ -1109,17 +1394,6 @@ async function monitorProvider(options) {
|
|
|
1109
1394
|
let pingInterval = null;
|
|
1110
1395
|
let isAborted = false;
|
|
1111
1396
|
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
1397
|
const connect = () => {
|
|
1124
1398
|
if (isAborted)
|
|
1125
1399
|
return;
|
|
@@ -1144,9 +1418,18 @@ async function monitorProvider(options) {
|
|
|
1144
1418
|
setWebSocket(account.accountId, wsClient);
|
|
1145
1419
|
// 初始化消息队列
|
|
1146
1420
|
messageQueue = new MessageQueue(runtime);
|
|
1147
|
-
wsClient.on("open", () => {
|
|
1421
|
+
wsClient.on("open", async () => {
|
|
1148
1422
|
runtime.log?.(`[${account.accountId}] WebSocket connected successfully`);
|
|
1149
1423
|
reconnectAttempts = 0;
|
|
1424
|
+
try {
|
|
1425
|
+
const replayedCount = await replayCachedMessages(account.accountId, wsClient, runtime, sendReply);
|
|
1426
|
+
if (replayedCount > 0) {
|
|
1427
|
+
runtime.log?.(`[${account.accountId}] Replayed ${replayedCount} cached messages`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
catch (err) {
|
|
1431
|
+
runtime.error?.(`[${account.accountId}] Failed to replay cached messages: ${String(err)}`);
|
|
1432
|
+
}
|
|
1150
1433
|
pingInterval = setInterval(() => {
|
|
1151
1434
|
if (wsClient?.readyState === WebSocket.OPEN) {
|
|
1152
1435
|
wsClient.ping();
|
|
@@ -1156,8 +1439,20 @@ async function monitorProvider(options) {
|
|
|
1156
1439
|
wsClient.on("message", (data) => {
|
|
1157
1440
|
const rawPayload = data.toString();
|
|
1158
1441
|
runtime.log?.(`[${account.accountId}] Received WS message: ${rawPayload.substring(0, 200)}...`);
|
|
1159
|
-
|
|
1160
|
-
|
|
1442
|
+
let chatId = "unknown";
|
|
1443
|
+
try {
|
|
1444
|
+
const msg = JSON.parse(rawPayload);
|
|
1445
|
+
if (msg.action === "chat") {
|
|
1446
|
+
chatId = msg.data?.conversation_id || msg.data?.user || "unknown";
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
chatId = msg.data?.chatId || msg.data?.userId || "unknown";
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
chatId = "unknown";
|
|
1454
|
+
}
|
|
1455
|
+
messageQueue?.enqueue(chatId, async () => {
|
|
1161
1456
|
await processMessage({
|
|
1162
1457
|
rawPayload,
|
|
1163
1458
|
account,
|
|
@@ -1188,8 +1483,8 @@ async function monitorProvider(options) {
|
|
|
1188
1483
|
setTimeout(connect, backoff);
|
|
1189
1484
|
}
|
|
1190
1485
|
else if (!isAborted) {
|
|
1191
|
-
runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`);
|
|
1192
|
-
await
|
|
1486
|
+
runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached, preserving cache for external recovery`);
|
|
1487
|
+
await cleanupAccount(account.accountId, false);
|
|
1193
1488
|
reject(new Error(`Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`));
|
|
1194
1489
|
}
|
|
1195
1490
|
});
|
|
@@ -1197,15 +1492,23 @@ async function monitorProvider(options) {
|
|
|
1197
1492
|
if (abortSignal) {
|
|
1198
1493
|
abortSignal.addEventListener("abort", async () => {
|
|
1199
1494
|
isAborted = true;
|
|
1200
|
-
|
|
1495
|
+
if (pingInterval) {
|
|
1496
|
+
clearInterval(pingInterval);
|
|
1497
|
+
pingInterval = null;
|
|
1498
|
+
}
|
|
1499
|
+
if (messageQueue) {
|
|
1500
|
+
messageQueue.clear();
|
|
1501
|
+
messageQueue = null;
|
|
1502
|
+
}
|
|
1503
|
+
await cleanupAccount(account.accountId, true);
|
|
1201
1504
|
resolve();
|
|
1202
1505
|
});
|
|
1203
1506
|
}
|
|
1204
1507
|
warmupReqIdStore(account.accountId, (msg) => runtime.log?.(msg))
|
|
1205
1508
|
.then(() => connect())
|
|
1206
1509
|
.catch(async (err) => {
|
|
1207
|
-
runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}`);
|
|
1208
|
-
await
|
|
1510
|
+
runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}, preserving cache for retry`);
|
|
1511
|
+
await cleanupAccount(account.accountId, false);
|
|
1209
1512
|
reject(err);
|
|
1210
1513
|
});
|
|
1211
1514
|
});
|
|
@@ -1298,6 +1601,12 @@ async function promptWSUrl(prompter, account) {
|
|
|
1298
1601
|
},
|
|
1299
1602
|
})).trim();
|
|
1300
1603
|
}
|
|
1604
|
+
async function promptSendThinkingMessage(prompter, account) {
|
|
1605
|
+
return await prompter.confirm({
|
|
1606
|
+
message: "Send a thinking message while the agent is processing?",
|
|
1607
|
+
initialValue: account?.sendThinkingMessage ?? true,
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1301
1610
|
function setAccessPolicy(cfg, accessPolicy) {
|
|
1302
1611
|
const account = resolveAccount(cfg);
|
|
1303
1612
|
const existingAllowFrom = account.config.allowFrom ?? [];
|
|
@@ -1357,6 +1666,7 @@ const aiHubOnboardingAdapter = {
|
|
|
1357
1666
|
const botId = await promptBotId(prompter, account);
|
|
1358
1667
|
const secret = await promptSecret(prompter, account);
|
|
1359
1668
|
const WSUrl = await promptWSUrl(prompter, account);
|
|
1669
|
+
const sendThinkingMessage = await promptSendThinkingMessage(prompter, account);
|
|
1360
1670
|
const cfgWithAccount = setAccount(cfg, {
|
|
1361
1671
|
botId,
|
|
1362
1672
|
secret,
|
|
@@ -1364,7 +1674,7 @@ const aiHubOnboardingAdapter = {
|
|
|
1364
1674
|
enabled: true,
|
|
1365
1675
|
accessPolicy: account.config.accessPolicy ?? "open",
|
|
1366
1676
|
allowFrom: account.config.allowFrom ?? [],
|
|
1367
|
-
sendThinkingMessage
|
|
1677
|
+
sendThinkingMessage,
|
|
1368
1678
|
});
|
|
1369
1679
|
return { cfg: cfgWithAccount };
|
|
1370
1680
|
},
|