@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.esm.js
CHANGED
|
@@ -34,8 +34,32 @@ const WS_RECONNECT_BASE_DELAY_MS = 1000;
|
|
|
34
34
|
// ============================================================================
|
|
35
35
|
/** 文本分块限制 */
|
|
36
36
|
const TEXT_CHUNK_LIMIT = 4000;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* 请求分析软阈值(毫秒)
|
|
39
|
+
* 超过该时间后只告警并继续等待,不直接中断长分析任务。
|
|
40
|
+
*/
|
|
41
|
+
const REQUEST_ANALYSIS_TIMEOUT_MS = 600000;
|
|
42
|
+
/**
|
|
43
|
+
* 读取请求分析软阈值的运行时覆盖值。
|
|
44
|
+
* 仅用于本地或特定部署快速调参;未设置时回退到默认值。
|
|
45
|
+
*/
|
|
46
|
+
function getRequestAnalysisTimeoutMs() {
|
|
47
|
+
const raw = process.env.OPENCLAW_REQUEST_ANALYSIS_TIMEOUT_MS?.trim();
|
|
48
|
+
if (!raw) {
|
|
49
|
+
return REQUEST_ANALYSIS_TIMEOUT_MS;
|
|
50
|
+
}
|
|
51
|
+
const parsed = Number(raw);
|
|
52
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
53
|
+
return REQUEST_ANALYSIS_TIMEOUT_MS;
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 消息缓存 TTL(毫秒)
|
|
59
|
+
* 设为 35 分钟,确保覆盖最大重连时间(约 28.5 分钟)+ 缓冲
|
|
60
|
+
* @see WS_MAX_RECONNECT_ATTEMPTS - 60 次,指数退避最大 30s
|
|
61
|
+
*/
|
|
62
|
+
const MESSAGE_CACHE_TTL_MS = 35 * 60 * 1000;
|
|
39
63
|
// ============================================================================
|
|
40
64
|
// 媒体处理配置
|
|
41
65
|
// ============================================================================
|
|
@@ -314,8 +338,9 @@ async function sendReply(params) {
|
|
|
314
338
|
const reqId = replyToMsgId || streamId;
|
|
315
339
|
runtime.log?.(`[53aihub] sendReply START: reqId=${reqId}, finish=${finish}, isError=${isError}, textLen=${text?.length || 0}, wsReadyState=${wsClient.readyState}`);
|
|
316
340
|
if (wsClient.readyState !== 1) {
|
|
317
|
-
|
|
318
|
-
|
|
341
|
+
const errorMsg = `WebSocket not connected (readyState=${wsClient.readyState}), cannot send message to ${toChatId}`;
|
|
342
|
+
runtime.error?.(`[53aihub] ${errorMsg}`);
|
|
343
|
+
throw new Error(`[53aihub] ${errorMsg}`);
|
|
319
344
|
}
|
|
320
345
|
if (isError) {
|
|
321
346
|
runtime.error?.(`[53aihub] sendReply ERROR: reqId=${reqId}, code=${errorCode}, text=${text?.substring(0, 100)}`);
|
|
@@ -426,8 +451,9 @@ async function sendThinkingMessage(wsClient, text, msgId, streamId, runtime) {
|
|
|
426
451
|
const wsState = wsClient.readyState;
|
|
427
452
|
runtime?.log?.(`[53aihub] sendThinkingMessage CALLED: msgId=${msgId}, streamId=${streamId}, text=${text}, wsReadyState=${wsState}`);
|
|
428
453
|
if (wsState !== 1) {
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
const errorMsg = `WebSocket not ready (state=${wsState})`;
|
|
455
|
+
runtime?.error?.(`[53aihub] sendThinkingMessage FAILED: ${errorMsg}`);
|
|
456
|
+
throw new Error(`[53aihub] ${errorMsg}`);
|
|
431
457
|
}
|
|
432
458
|
// 使用 OpenAI 兼容格式,确保 Go 后端能正确解析
|
|
433
459
|
// 关键:req_id 必须使用原始消息的 msgId,这样 Go 后端才能关联请求和响应
|
|
@@ -662,7 +688,7 @@ async function warmupReqIdStore(accountId = "default", log) {
|
|
|
662
688
|
const store = getOrCreateReqIdStore(accountId);
|
|
663
689
|
return store.warmup((err) => log?.(`ReqId warmup error: ${String(err)}`));
|
|
664
690
|
}
|
|
665
|
-
async function cleanupAccount(accountId) {
|
|
691
|
+
async function cleanupAccount(accountId, clearCache = false) {
|
|
666
692
|
const wsClient = wsClientInstances.get(accountId);
|
|
667
693
|
if (wsClient) {
|
|
668
694
|
try {
|
|
@@ -676,6 +702,10 @@ async function cleanupAccount(accountId) {
|
|
|
676
702
|
const store = reqIdStores.get(accountId);
|
|
677
703
|
if (store)
|
|
678
704
|
await store.flush();
|
|
705
|
+
if (clearCache) {
|
|
706
|
+
const { clearAccountCache } = await Promise.resolve().then(function () { return messageCache; });
|
|
707
|
+
clearAccountCache(accountId);
|
|
708
|
+
}
|
|
679
709
|
}
|
|
680
710
|
|
|
681
711
|
/**
|
|
@@ -783,41 +813,288 @@ async function downloadAndSaveFiles(params) {
|
|
|
783
813
|
return mediaList;
|
|
784
814
|
}
|
|
785
815
|
|
|
786
|
-
|
|
816
|
+
const DEFAULT_CONFIG = {
|
|
817
|
+
maxEntries: 100,
|
|
818
|
+
ttlMs: MESSAGE_CACHE_TTL_MS,
|
|
819
|
+
maxRetries: 3,
|
|
820
|
+
};
|
|
821
|
+
/**
|
|
822
|
+
* 按 accountId 隔离的消息缓存管理器
|
|
823
|
+
*/
|
|
824
|
+
class MessageCache {
|
|
825
|
+
cache = new Map();
|
|
826
|
+
config;
|
|
827
|
+
accountId;
|
|
828
|
+
runtime;
|
|
829
|
+
constructor(accountId, config = {}, runtime) {
|
|
830
|
+
this.accountId = accountId;
|
|
831
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
832
|
+
this.runtime = runtime;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* 生成唯一缓存 key
|
|
836
|
+
*/
|
|
837
|
+
generateCacheKey(reqId) {
|
|
838
|
+
return `${this.accountId}:${reqId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 添加消息到缓存,返回缓存 key
|
|
842
|
+
*/
|
|
843
|
+
add(params) {
|
|
844
|
+
this.prune();
|
|
845
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
846
|
+
// 优先淘汰非终态消息(finish=false 且 isError=false),保留终态消息
|
|
847
|
+
const entries = [...this.cache.entries()];
|
|
848
|
+
// 先找最老的非终态消息
|
|
849
|
+
const oldestNonFinal = entries
|
|
850
|
+
.filter(([, entry]) => !entry.finish && !entry.isError)
|
|
851
|
+
.sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
|
|
852
|
+
if (oldestNonFinal) {
|
|
853
|
+
this.cache.delete(oldestNonFinal[0]);
|
|
854
|
+
this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest non-final entry ${oldestNonFinal[0]}`);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// 所有消息都是终态,才淘汰最老的
|
|
858
|
+
const oldest = entries.sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
|
|
859
|
+
if (oldest) {
|
|
860
|
+
this.cache.delete(oldest[0]);
|
|
861
|
+
this.runtime?.log?.(`[53aihub] MessageCache: evicted oldest final entry ${oldest[0]} (all entries are final)`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const cacheKey = this.generateCacheKey(params.reqId);
|
|
866
|
+
this.cache.set(cacheKey, {
|
|
867
|
+
cacheKey,
|
|
868
|
+
...params,
|
|
869
|
+
createdAt: Date.now(),
|
|
870
|
+
retryCount: 0,
|
|
871
|
+
});
|
|
872
|
+
this.runtime?.log?.(`[53aihub] MessageCache: cached message reqId=${params.reqId}, cacheKey=${cacheKey}, cacheSize=${this.cache.size}`);
|
|
873
|
+
return cacheKey;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* 获取所有待重试的消息,返回 cacheKey + entry
|
|
877
|
+
*/
|
|
878
|
+
getPendingMessages() {
|
|
879
|
+
const now = Date.now();
|
|
880
|
+
const pending = [];
|
|
881
|
+
for (const [key, entry] of this.cache) {
|
|
882
|
+
if (now - entry.createdAt > this.config.ttlMs) {
|
|
883
|
+
this.cache.delete(key);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (entry.retryCount >= this.config.maxRetries) {
|
|
887
|
+
this.cache.delete(key);
|
|
888
|
+
this.runtime?.log?.(`[53aihub] MessageCache: dropped message after max retries cacheKey=${key}`);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
pending.push(entry);
|
|
892
|
+
}
|
|
893
|
+
return pending;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* 按 cacheKey 标记消息为已发送
|
|
897
|
+
*/
|
|
898
|
+
markSent(cacheKey) {
|
|
899
|
+
if (this.cache.has(cacheKey)) {
|
|
900
|
+
this.cache.delete(cacheKey);
|
|
901
|
+
this.runtime?.log?.(`[53aihub] MessageCache: marked as sent cacheKey=${cacheKey}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* 按 cacheKey 增加重试计数
|
|
906
|
+
*/
|
|
907
|
+
incrementRetry(cacheKey) {
|
|
908
|
+
const entry = this.cache.get(cacheKey);
|
|
909
|
+
if (entry) {
|
|
910
|
+
entry.retryCount++;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* 清理过期条目
|
|
915
|
+
*/
|
|
916
|
+
prune() {
|
|
917
|
+
const now = Date.now();
|
|
918
|
+
let pruned = 0;
|
|
919
|
+
for (const [key, entry] of this.cache) {
|
|
920
|
+
if (now - entry.createdAt > this.config.ttlMs) {
|
|
921
|
+
this.cache.delete(key);
|
|
922
|
+
pruned++;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (pruned > 0) {
|
|
926
|
+
this.runtime?.log?.(`[53aihub] MessageCache: pruned ${pruned} expired entries`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
size() {
|
|
930
|
+
return this.cache.size;
|
|
931
|
+
}
|
|
932
|
+
clear() {
|
|
933
|
+
this.cache.clear();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// 按 accountId 隔离的缓存实例
|
|
937
|
+
const cacheInstances = new Map();
|
|
938
|
+
/**
|
|
939
|
+
* 获取或创建按账号隔离的缓存实例
|
|
940
|
+
*/
|
|
941
|
+
function getMessageCache(accountId, runtime) {
|
|
942
|
+
let cache = cacheInstances.get(accountId);
|
|
943
|
+
if (!cache) {
|
|
944
|
+
cache = new MessageCache(accountId, {}, runtime);
|
|
945
|
+
cacheInstances.set(accountId, cache);
|
|
946
|
+
}
|
|
947
|
+
return cache;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* 清理指定账号的缓存
|
|
951
|
+
*/
|
|
952
|
+
function clearAccountCache(accountId) {
|
|
953
|
+
const cache = cacheInstances.get(accountId);
|
|
954
|
+
if (cache) {
|
|
955
|
+
cache.clear();
|
|
956
|
+
cacheInstances.delete(accountId);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* 缓存感知的消息发送器
|
|
961
|
+
* 返回 [success, cacheKey?] - 成功时 cacheKey 为 undefined,失败时返回缓存 key
|
|
962
|
+
*/
|
|
963
|
+
async function sendWithCache(accountId, params, sendFn) {
|
|
964
|
+
const { wsClient, replyToMsgId, streamId, text, toChatId, finish, isError, errorCode, errorDetails, isThinking, runtime } = params;
|
|
965
|
+
const reqId = replyToMsgId || streamId;
|
|
966
|
+
if (wsClient.readyState !== 1) {
|
|
967
|
+
const cache = getMessageCache(accountId, runtime);
|
|
968
|
+
const cacheKey = cache.add({
|
|
969
|
+
reqId,
|
|
970
|
+
toChatId,
|
|
971
|
+
text,
|
|
972
|
+
finish,
|
|
973
|
+
isError,
|
|
974
|
+
errorCode,
|
|
975
|
+
errorDetails,
|
|
976
|
+
isThinking,
|
|
977
|
+
});
|
|
978
|
+
runtime.error?.(`[53aihub] sendWithCache: WebSocket not ready, cached message cacheKey=${cacheKey}`);
|
|
979
|
+
return { success: false, cacheKey };
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
await sendFn(params);
|
|
983
|
+
return { success: true };
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
const cache = getMessageCache(accountId, runtime);
|
|
987
|
+
const cacheKey = cache.add({
|
|
988
|
+
reqId,
|
|
989
|
+
toChatId,
|
|
990
|
+
text,
|
|
991
|
+
finish,
|
|
992
|
+
isError,
|
|
993
|
+
errorCode,
|
|
994
|
+
errorDetails,
|
|
995
|
+
isThinking,
|
|
996
|
+
});
|
|
997
|
+
runtime.error?.(`[53aihub] sendWithCache: send failed, cached message cacheKey=${cacheKey}, error=${String(err)}`);
|
|
998
|
+
return { success: false, cacheKey };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* 重放缓存的消息
|
|
1003
|
+
*/
|
|
1004
|
+
async function replayCachedMessages(accountId, wsClient, runtime, sendFn) {
|
|
1005
|
+
const cache = getMessageCache(accountId, runtime);
|
|
1006
|
+
const pending = cache.getPendingMessages();
|
|
1007
|
+
if (pending.length === 0) {
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
runtime.log?.(`[53aihub] replayCachedMessages: replaying ${pending.length} cached messages for account=${accountId}`);
|
|
1011
|
+
let successCount = 0;
|
|
1012
|
+
for (const entry of pending) {
|
|
1013
|
+
if (wsClient.readyState !== 1) {
|
|
1014
|
+
runtime.error?.(`[53aihub] replayCachedMessages: WebSocket not ready, stopping replay`);
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
await sendFn({
|
|
1019
|
+
wsClient,
|
|
1020
|
+
text: entry.text,
|
|
1021
|
+
toChatId: entry.toChatId,
|
|
1022
|
+
replyToMsgId: entry.reqId,
|
|
1023
|
+
runtime,
|
|
1024
|
+
finish: entry.finish,
|
|
1025
|
+
streamId: entry.reqId,
|
|
1026
|
+
isError: entry.isError,
|
|
1027
|
+
errorCode: entry.errorCode,
|
|
1028
|
+
errorDetails: entry.errorDetails,
|
|
1029
|
+
isThinking: entry.isThinking,
|
|
1030
|
+
});
|
|
1031
|
+
cache.markSent(entry.cacheKey);
|
|
1032
|
+
successCount++;
|
|
1033
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
const newRetryCount = entry.retryCount + 1;
|
|
1037
|
+
cache.incrementRetry(entry.cacheKey);
|
|
1038
|
+
runtime.error?.(`[53aihub] replayCachedMessages: failed to send cacheKey=${entry.cacheKey}, retryCount=${newRetryCount}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
runtime.log?.(`[53aihub] replayCachedMessages: completed, ${successCount}/${pending.length} sent`);
|
|
1042
|
+
return successCount;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
var messageCache = /*#__PURE__*/Object.freeze({
|
|
1046
|
+
__proto__: null,
|
|
1047
|
+
clearAccountCache: clearAccountCache,
|
|
1048
|
+
getMessageCache: getMessageCache,
|
|
1049
|
+
replayCachedMessages: replayCachedMessages,
|
|
1050
|
+
sendWithCache: sendWithCache
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// 按 chatId 分队列的消息处理器 - 同一会话串行,不同会话并行
|
|
787
1054
|
class MessageQueue {
|
|
788
|
-
|
|
789
|
-
processing =
|
|
1055
|
+
queues = new Map();
|
|
1056
|
+
processing = new Map();
|
|
790
1057
|
runtime;
|
|
791
1058
|
constructor(runtime) {
|
|
792
1059
|
this.runtime = runtime;
|
|
793
1060
|
}
|
|
794
|
-
enqueue(task) {
|
|
795
|
-
this.
|
|
796
|
-
|
|
1061
|
+
enqueue(chatId, task) {
|
|
1062
|
+
if (!this.queues.has(chatId)) {
|
|
1063
|
+
this.queues.set(chatId, []);
|
|
1064
|
+
this.processing.set(chatId, false);
|
|
1065
|
+
}
|
|
1066
|
+
this.queues.get(chatId).push(task);
|
|
1067
|
+
this.process(chatId);
|
|
797
1068
|
}
|
|
798
|
-
async process() {
|
|
799
|
-
|
|
1069
|
+
async process(chatId) {
|
|
1070
|
+
const queue = this.queues.get(chatId);
|
|
1071
|
+
const isProcessing = this.processing.get(chatId);
|
|
1072
|
+
if (!queue || isProcessing || queue.length === 0)
|
|
800
1073
|
return;
|
|
801
|
-
this.processing
|
|
1074
|
+
this.processing.set(chatId, true);
|
|
802
1075
|
try {
|
|
803
|
-
const task =
|
|
1076
|
+
const task = queue.shift();
|
|
804
1077
|
if (task) {
|
|
805
1078
|
await task();
|
|
806
1079
|
}
|
|
807
1080
|
}
|
|
808
1081
|
catch (err) {
|
|
809
|
-
this.runtime.error?.(`[53aihub] MessageQueue error: ${String(err)}`);
|
|
1082
|
+
this.runtime.error?.(`[53aihub] MessageQueue error (chatId=${chatId}): ${String(err)}`);
|
|
810
1083
|
}
|
|
811
1084
|
finally {
|
|
812
|
-
this.processing
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1085
|
+
this.processing.set(chatId, false);
|
|
1086
|
+
if (queue.length > 0) {
|
|
1087
|
+
this.process(chatId);
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
this.queues.delete(chatId);
|
|
1091
|
+
this.processing.delete(chatId);
|
|
816
1092
|
}
|
|
817
1093
|
}
|
|
818
1094
|
}
|
|
819
1095
|
clear() {
|
|
820
|
-
this.
|
|
1096
|
+
this.queues.clear();
|
|
1097
|
+
this.processing.clear();
|
|
821
1098
|
}
|
|
822
1099
|
}
|
|
823
1100
|
function buildMessageContext(body, account, config, mediaList) {
|
|
@@ -882,20 +1159,26 @@ async function processMessage(params) {
|
|
|
882
1159
|
runtime.log?.(`[53aihub] processMessage: chatId=${chatId}, msgId=${body.msgId}, text=${parsed.textParts.join(" ").substring(0, 50)}... images=${parsed.imageUrls.length} files=${parsed.fileUrls.length}`);
|
|
883
1160
|
const core = getRuntime();
|
|
884
1161
|
const streamId = `stream-${Date.now()}`;
|
|
1162
|
+
// 缓存感知的消息发送器 - 必须在所有 sendReply 调用前定义
|
|
1163
|
+
const cachedSend = async (sendParams) => {
|
|
1164
|
+
await sendWithCache(account.accountId, {
|
|
1165
|
+
...sendParams,
|
|
1166
|
+
wsClient,
|
|
1167
|
+
runtime,
|
|
1168
|
+
}, sendReply);
|
|
1169
|
+
};
|
|
885
1170
|
const accessResult = await checkAccessPolicy({
|
|
886
1171
|
userId: body.userId,
|
|
887
1172
|
account,
|
|
888
1173
|
runtime,
|
|
889
1174
|
});
|
|
890
1175
|
if (!accessResult.allowed) {
|
|
891
|
-
await
|
|
892
|
-
wsClient,
|
|
1176
|
+
await cachedSend({
|
|
893
1177
|
text: accessResult.reason === "Pairing required"
|
|
894
1178
|
? "您尚未获得授权使用此机器人,请联系管理员进行审核。"
|
|
895
1179
|
: `⚠️ 访问被拒绝: ${accessResult.reason || "未知原因"}`,
|
|
896
1180
|
toChatId: chatId,
|
|
897
1181
|
replyToMsgId: body.msgId,
|
|
898
|
-
runtime,
|
|
899
1182
|
finish: true,
|
|
900
1183
|
streamId,
|
|
901
1184
|
isError: true,
|
|
@@ -936,6 +1219,8 @@ async function processMessage(params) {
|
|
|
936
1219
|
const mediaList = [...imageMediaList, ...fileMediaList];
|
|
937
1220
|
const ctxPayload = buildMessageContext(body, account, config, mediaList);
|
|
938
1221
|
let cleanedUp = false;
|
|
1222
|
+
let analysisTimeoutTimer = null;
|
|
1223
|
+
const analysisTimeoutMs = getRequestAnalysisTimeoutMs();
|
|
939
1224
|
const safeCleanup = () => {
|
|
940
1225
|
if (!cleanedUp) {
|
|
941
1226
|
cleanedUp = true;
|
|
@@ -944,7 +1229,20 @@ async function processMessage(params) {
|
|
|
944
1229
|
};
|
|
945
1230
|
runtime.log?.(`[53aihub] processMessage: Starting dispatchReplyWithBufferedBlockDispatcher for msgId=${body.msgId}`);
|
|
946
1231
|
try {
|
|
947
|
-
|
|
1232
|
+
analysisTimeoutTimer = setTimeout(() => {
|
|
1233
|
+
runtime.log?.(`[53aihub] processMessage: analysis exceeded soft threshold ${analysisTimeoutMs}ms for msgId=${body.msgId}, keep waiting for completion`);
|
|
1234
|
+
if (wsClient.readyState !== WebSocket.OPEN) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
wsClient.ping();
|
|
1239
|
+
runtime.log?.(`[53aihub] processMessage: timeout keepalive ping sent for msgId=${body.msgId}`);
|
|
1240
|
+
}
|
|
1241
|
+
catch (err) {
|
|
1242
|
+
runtime.error?.(`[53aihub] processMessage: timeout keepalive ping failed for msgId=${body.msgId}: ${String(err)}`);
|
|
1243
|
+
}
|
|
1244
|
+
}, analysisTimeoutMs);
|
|
1245
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
948
1246
|
ctx: ctxPayload,
|
|
949
1247
|
cfg: config,
|
|
950
1248
|
dispatcherOptions: {
|
|
@@ -954,12 +1252,10 @@ async function processMessage(params) {
|
|
|
954
1252
|
const errorMsg = payload.text || "Unknown error";
|
|
955
1253
|
const errorCode = inferErrorCode(errorMsg);
|
|
956
1254
|
runtime.error?.(`[53aihub] deliver ERROR: ${errorMsg}`);
|
|
957
|
-
await
|
|
958
|
-
wsClient,
|
|
1255
|
+
await cachedSend({
|
|
959
1256
|
text: `⚠️ ${errorMsg}`,
|
|
960
1257
|
toChatId: chatId,
|
|
961
1258
|
replyToMsgId: body.msgId,
|
|
962
|
-
runtime,
|
|
963
1259
|
finish: true,
|
|
964
1260
|
streamId: state.streamId,
|
|
965
1261
|
isError: true,
|
|
@@ -972,12 +1268,10 @@ async function processMessage(params) {
|
|
|
972
1268
|
state.accumulatedText.startsWith("🧹 Compacting context");
|
|
973
1269
|
if (isCompaction && info.kind !== "final") {
|
|
974
1270
|
runtime.log?.(`[53aihub] deliver COMPACTION: text preview=${payload.text?.substring(0, 50)}...`);
|
|
975
|
-
await
|
|
976
|
-
wsClient,
|
|
1271
|
+
await cachedSend({
|
|
977
1272
|
text: payload.text || "",
|
|
978
1273
|
toChatId: chatId,
|
|
979
1274
|
replyToMsgId: body.msgId,
|
|
980
|
-
runtime,
|
|
981
1275
|
finish: false,
|
|
982
1276
|
streamId: state.streamId,
|
|
983
1277
|
isThinking: true,
|
|
@@ -987,12 +1281,10 @@ async function processMessage(params) {
|
|
|
987
1281
|
state.accumulatedText += payload.text;
|
|
988
1282
|
if (info.kind !== "final") {
|
|
989
1283
|
runtime.log?.(`[53aihub] deliver STREAMING: accumulatedText preview=${state.accumulatedText.substring(0, 50)}...`);
|
|
990
|
-
await
|
|
991
|
-
wsClient,
|
|
1284
|
+
await cachedSend({
|
|
992
1285
|
text: state.accumulatedText,
|
|
993
1286
|
toChatId: chatId,
|
|
994
1287
|
replyToMsgId: body.msgId,
|
|
995
|
-
runtime,
|
|
996
1288
|
finish: false,
|
|
997
1289
|
streamId: state.streamId,
|
|
998
1290
|
});
|
|
@@ -1002,47 +1294,36 @@ async function processMessage(params) {
|
|
|
1002
1294
|
runtime.error?.(`[53aihub] onError: kind=${info.kind}, error=${String(err)}`);
|
|
1003
1295
|
const errorText = String(err);
|
|
1004
1296
|
const errorCode = inferErrorCode(errorText);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
errorCode,
|
|
1016
|
-
errorDetails: `kind=${info.kind}, error=${errorText}`,
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
catch (sendErr) {
|
|
1020
|
-
runtime.error?.(`[53aihub] Failed to send error notification: ${String(sendErr)}`);
|
|
1021
|
-
}
|
|
1297
|
+
await cachedSend({
|
|
1298
|
+
text: `⚠️ 系统错误: ${errorText}`,
|
|
1299
|
+
toChatId: chatId,
|
|
1300
|
+
replyToMsgId: body.msgId,
|
|
1301
|
+
finish: true,
|
|
1302
|
+
streamId: state.streamId,
|
|
1303
|
+
isError: true,
|
|
1304
|
+
errorCode,
|
|
1305
|
+
errorDetails: `kind=${info.kind}, error=${errorText}`,
|
|
1306
|
+
});
|
|
1022
1307
|
},
|
|
1023
1308
|
},
|
|
1024
|
-
})
|
|
1309
|
+
});
|
|
1025
1310
|
runtime.log?.(`[53aihub] processMessage: dispatchReply completed, accumulatedTextLen=${state.accumulatedText.length}`);
|
|
1026
1311
|
if (state.accumulatedText) {
|
|
1027
1312
|
runtime.log?.(`[53aihub] processMessage: Sending final reply with accumulatedText`);
|
|
1028
|
-
await
|
|
1029
|
-
wsClient,
|
|
1313
|
+
await cachedSend({
|
|
1030
1314
|
text: state.accumulatedText,
|
|
1031
1315
|
toChatId: chatId,
|
|
1032
1316
|
replyToMsgId: body.msgId,
|
|
1033
|
-
runtime,
|
|
1034
1317
|
finish: true,
|
|
1035
1318
|
streamId: state.streamId,
|
|
1036
1319
|
});
|
|
1037
1320
|
}
|
|
1038
1321
|
else {
|
|
1039
1322
|
runtime.log?.(`[53aihub] processMessage: No accumulatedText, sending empty final reply`);
|
|
1040
|
-
await
|
|
1041
|
-
wsClient,
|
|
1323
|
+
await cachedSend({
|
|
1042
1324
|
text: "",
|
|
1043
1325
|
toChatId: chatId,
|
|
1044
1326
|
replyToMsgId: body.msgId,
|
|
1045
|
-
runtime,
|
|
1046
1327
|
finish: true,
|
|
1047
1328
|
streamId: state.streamId,
|
|
1048
1329
|
});
|
|
@@ -1055,12 +1336,10 @@ async function processMessage(params) {
|
|
|
1055
1336
|
const errorCode = inferErrorCode(errorText);
|
|
1056
1337
|
if (!cleanedUp) {
|
|
1057
1338
|
try {
|
|
1058
|
-
await
|
|
1059
|
-
wsClient,
|
|
1339
|
+
await cachedSend({
|
|
1060
1340
|
text: `⚠️ 处理请求时发生异常: ${errorText}`,
|
|
1061
1341
|
toChatId: chatId,
|
|
1062
1342
|
replyToMsgId: body.msgId,
|
|
1063
|
-
runtime,
|
|
1064
1343
|
finish: true,
|
|
1065
1344
|
streamId: state.streamId,
|
|
1066
1345
|
isError: true,
|
|
@@ -1074,6 +1353,12 @@ async function processMessage(params) {
|
|
|
1074
1353
|
}
|
|
1075
1354
|
safeCleanup();
|
|
1076
1355
|
}
|
|
1356
|
+
finally {
|
|
1357
|
+
if (analysisTimeoutTimer) {
|
|
1358
|
+
clearTimeout(analysisTimeoutTimer);
|
|
1359
|
+
analysisTimeoutTimer = null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1077
1362
|
}
|
|
1078
1363
|
async function monitorProvider(options) {
|
|
1079
1364
|
const { account, config, runtime, abortSignal } = options;
|
|
@@ -1085,17 +1370,6 @@ async function monitorProvider(options) {
|
|
|
1085
1370
|
let pingInterval = null;
|
|
1086
1371
|
let isAborted = false;
|
|
1087
1372
|
let messageQueue = null;
|
|
1088
|
-
const cleanup = async () => {
|
|
1089
|
-
if (pingInterval) {
|
|
1090
|
-
clearInterval(pingInterval);
|
|
1091
|
-
pingInterval = null;
|
|
1092
|
-
}
|
|
1093
|
-
if (messageQueue) {
|
|
1094
|
-
messageQueue.clear();
|
|
1095
|
-
messageQueue = null;
|
|
1096
|
-
}
|
|
1097
|
-
await cleanupAccount(account.accountId);
|
|
1098
|
-
};
|
|
1099
1373
|
const connect = () => {
|
|
1100
1374
|
if (isAborted)
|
|
1101
1375
|
return;
|
|
@@ -1120,9 +1394,18 @@ async function monitorProvider(options) {
|
|
|
1120
1394
|
setWebSocket(account.accountId, wsClient);
|
|
1121
1395
|
// 初始化消息队列
|
|
1122
1396
|
messageQueue = new MessageQueue(runtime);
|
|
1123
|
-
wsClient.on("open", () => {
|
|
1397
|
+
wsClient.on("open", async () => {
|
|
1124
1398
|
runtime.log?.(`[${account.accountId}] WebSocket connected successfully`);
|
|
1125
1399
|
reconnectAttempts = 0;
|
|
1400
|
+
try {
|
|
1401
|
+
const replayedCount = await replayCachedMessages(account.accountId, wsClient, runtime, sendReply);
|
|
1402
|
+
if (replayedCount > 0) {
|
|
1403
|
+
runtime.log?.(`[${account.accountId}] Replayed ${replayedCount} cached messages`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
runtime.error?.(`[${account.accountId}] Failed to replay cached messages: ${String(err)}`);
|
|
1408
|
+
}
|
|
1126
1409
|
pingInterval = setInterval(() => {
|
|
1127
1410
|
if (wsClient?.readyState === WebSocket.OPEN) {
|
|
1128
1411
|
wsClient.ping();
|
|
@@ -1132,8 +1415,20 @@ async function monitorProvider(options) {
|
|
|
1132
1415
|
wsClient.on("message", (data) => {
|
|
1133
1416
|
const rawPayload = data.toString();
|
|
1134
1417
|
runtime.log?.(`[${account.accountId}] Received WS message: ${rawPayload.substring(0, 200)}...`);
|
|
1135
|
-
|
|
1136
|
-
|
|
1418
|
+
let chatId = "unknown";
|
|
1419
|
+
try {
|
|
1420
|
+
const msg = JSON.parse(rawPayload);
|
|
1421
|
+
if (msg.action === "chat") {
|
|
1422
|
+
chatId = msg.data?.conversation_id || msg.data?.user || "unknown";
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
chatId = msg.data?.chatId || msg.data?.userId || "unknown";
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
chatId = "unknown";
|
|
1430
|
+
}
|
|
1431
|
+
messageQueue?.enqueue(chatId, async () => {
|
|
1137
1432
|
await processMessage({
|
|
1138
1433
|
rawPayload,
|
|
1139
1434
|
account,
|
|
@@ -1164,8 +1459,8 @@ async function monitorProvider(options) {
|
|
|
1164
1459
|
setTimeout(connect, backoff);
|
|
1165
1460
|
}
|
|
1166
1461
|
else if (!isAborted) {
|
|
1167
|
-
runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`);
|
|
1168
|
-
await
|
|
1462
|
+
runtime.error?.(`[${account.accountId}] Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached, preserving cache for external recovery`);
|
|
1463
|
+
await cleanupAccount(account.accountId, false);
|
|
1169
1464
|
reject(new Error(`Max reconnect attempts (${WS_MAX_RECONNECT_ATTEMPTS}) reached`));
|
|
1170
1465
|
}
|
|
1171
1466
|
});
|
|
@@ -1173,15 +1468,23 @@ async function monitorProvider(options) {
|
|
|
1173
1468
|
if (abortSignal) {
|
|
1174
1469
|
abortSignal.addEventListener("abort", async () => {
|
|
1175
1470
|
isAborted = true;
|
|
1176
|
-
|
|
1471
|
+
if (pingInterval) {
|
|
1472
|
+
clearInterval(pingInterval);
|
|
1473
|
+
pingInterval = null;
|
|
1474
|
+
}
|
|
1475
|
+
if (messageQueue) {
|
|
1476
|
+
messageQueue.clear();
|
|
1477
|
+
messageQueue = null;
|
|
1478
|
+
}
|
|
1479
|
+
await cleanupAccount(account.accountId, true);
|
|
1177
1480
|
resolve();
|
|
1178
1481
|
});
|
|
1179
1482
|
}
|
|
1180
1483
|
warmupReqIdStore(account.accountId, (msg) => runtime.log?.(msg))
|
|
1181
1484
|
.then(() => connect())
|
|
1182
1485
|
.catch(async (err) => {
|
|
1183
|
-
runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}`);
|
|
1184
|
-
await
|
|
1486
|
+
runtime.error?.(`[${account.accountId}] Failed to warmup ReqId store: ${String(err)}, preserving cache for retry`);
|
|
1487
|
+
await cleanupAccount(account.accountId, false);
|
|
1185
1488
|
reject(err);
|
|
1186
1489
|
});
|
|
1187
1490
|
});
|
|
@@ -1274,6 +1577,12 @@ async function promptWSUrl(prompter, account) {
|
|
|
1274
1577
|
},
|
|
1275
1578
|
})).trim();
|
|
1276
1579
|
}
|
|
1580
|
+
async function promptSendThinkingMessage(prompter, account) {
|
|
1581
|
+
return await prompter.confirm({
|
|
1582
|
+
message: "Send a thinking message while the agent is processing?",
|
|
1583
|
+
initialValue: account?.sendThinkingMessage ?? true,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1277
1586
|
function setAccessPolicy(cfg, accessPolicy) {
|
|
1278
1587
|
const account = resolveAccount(cfg);
|
|
1279
1588
|
const existingAllowFrom = account.config.allowFrom ?? [];
|
|
@@ -1333,6 +1642,7 @@ const aiHubOnboardingAdapter = {
|
|
|
1333
1642
|
const botId = await promptBotId(prompter, account);
|
|
1334
1643
|
const secret = await promptSecret(prompter, account);
|
|
1335
1644
|
const WSUrl = await promptWSUrl(prompter, account);
|
|
1645
|
+
const sendThinkingMessage = await promptSendThinkingMessage(prompter, account);
|
|
1336
1646
|
const cfgWithAccount = setAccount(cfg, {
|
|
1337
1647
|
botId,
|
|
1338
1648
|
secret,
|
|
@@ -1340,7 +1650,7 @@ const aiHubOnboardingAdapter = {
|
|
|
1340
1650
|
enabled: true,
|
|
1341
1651
|
accessPolicy: account.config.accessPolicy ?? "open",
|
|
1342
1652
|
allowFrom: account.config.allowFrom ?? [],
|
|
1343
|
-
sendThinkingMessage
|
|
1653
|
+
sendThinkingMessage,
|
|
1344
1654
|
});
|
|
1345
1655
|
return { cfg: cfgWithAccount };
|
|
1346
1656
|
},
|