timbot 2026.3.12 → 2026.3.19
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/README.en.md +3 -3
- package/README.md +5 -0
- package/README.zh-CN.md +294 -3
- package/dist/src/accounts.d.ts.map +1 -1
- package/dist/src/accounts.js +7 -9
- package/dist/src/accounts.js.map +1 -1
- package/dist/src/config-schema.js +2 -2
- package/dist/src/config-schema.js.map +1 -1
- package/dist/src/inbound-routing.d.ts +12 -0
- package/dist/src/inbound-routing.d.ts.map +1 -0
- package/dist/src/inbound-routing.js +112 -0
- package/dist/src/inbound-routing.js.map +1 -0
- package/dist/src/monitor.d.ts.map +1 -1
- package/dist/src/monitor.js +139 -295
- package/dist/src/monitor.js.map +1 -1
- package/dist/src/onboarding.d.ts.map +1 -1
- package/dist/src/onboarding.js +1 -3
- package/dist/src/onboarding.js.map +1 -1
- package/dist/src/stream-text.d.ts.map +1 -1
- package/dist/src/stream-text.js +44 -10
- package/dist/src/stream-text.js.map +1 -1
- package/dist/src/text-splitter.d.ts.map +1 -1
- package/dist/src/text-splitter.js +49 -1
- package/dist/src/text-splitter.js.map +1 -1
- package/dist/src/types.d.ts +2 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/src/monitor.js
CHANGED
|
@@ -2,10 +2,8 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { getTimbotRuntime } from "./runtime.js";
|
|
3
3
|
import { genTestUserSig } from "./debug/GenerateTestUserSig-es.js";
|
|
4
4
|
import { LOG_PREFIX, logSimple } from "./logger.js";
|
|
5
|
+
import { extractMentionedBotAccounts, extractTextFromMsgBody, matchTimbotWebhookTargetsBySdkAppId, selectTimbotWebhookTarget, } from "./inbound-routing.js";
|
|
5
6
|
import { allowsFinalTextRecovery, buildBotErrorPayload, buildBotStreamPayload, buildCustomMsgBody, buildStreamingMsgBody, buildTimStreamChunk, buildTimStreamMsgBody, buildTextMsgBody, } from "./streaming-policy.js";
|
|
6
|
-
import { isSelfInboundMessage, resolveOutboundFromAccount } from "./sender.js";
|
|
7
|
-
import { createPartialTextAccumulator, mergeStreamingText } from "./stream-text.js";
|
|
8
|
-
import { splitTextByPreferredBreaks } from "./text-splitter.js";
|
|
9
7
|
const webhookTargets = new Map();
|
|
10
8
|
// ============ 日志工具(带 target) ============
|
|
11
9
|
/** 带 target 的日志(有 runtime 回调) */
|
|
@@ -104,6 +102,9 @@ function buildTimbotApiUrl(account, service, action) {
|
|
|
104
102
|
const userSig = generateUserSig(account) ?? "";
|
|
105
103
|
return `https://${domain}/v4/${service}/${action}?sdkappid=${encodeURIComponent(account.sdkAppId ?? "")}&identifier=${encodeURIComponent(identifier)}&usersig=${encodeURIComponent(userSig)}&random=${random}&contenttype=json`;
|
|
106
104
|
}
|
|
105
|
+
function resolveOutboundSenderAccount(account) {
|
|
106
|
+
return account.botAccount || account.identifier || "administrator";
|
|
107
|
+
}
|
|
107
108
|
const TIMBOT_PARTIAL_STREAM_THROTTLE_MS = 1000;
|
|
108
109
|
const TIMBOT_STREAM_SOFT_LIMIT_BYTES = 11 * 1024;
|
|
109
110
|
const TIMBOT_FINAL_TEXT_CHUNK_LIMIT = 3500;
|
|
@@ -116,9 +117,51 @@ function buildReplyRuntimeConfig(config) {
|
|
|
116
117
|
disableBlockStreaming: true,
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
function mergeStreamingText(previousText, nextText) {
|
|
121
|
+
const previous = typeof previousText === "string" ? previousText : "";
|
|
122
|
+
const next = typeof nextText === "string" ? nextText : "";
|
|
123
|
+
if (!next) {
|
|
124
|
+
return previous;
|
|
125
|
+
}
|
|
126
|
+
if (!previous || next === previous) {
|
|
127
|
+
return next;
|
|
128
|
+
}
|
|
129
|
+
if (next.startsWith(previous)) {
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
if (previous.startsWith(next)) {
|
|
133
|
+
return previous;
|
|
134
|
+
}
|
|
135
|
+
if (next.includes(previous)) {
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
if (previous.includes(next)) {
|
|
139
|
+
return previous;
|
|
140
|
+
}
|
|
141
|
+
const maxOverlap = Math.min(previous.length, next.length);
|
|
142
|
+
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
|
143
|
+
if (previous.slice(-overlap) === next.slice(0, overlap)) {
|
|
144
|
+
return `${previous}${next.slice(overlap)}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return `${previous}${next}`;
|
|
148
|
+
}
|
|
119
149
|
function estimateMsgBodyBytes(msgBody) {
|
|
120
150
|
return Buffer.byteLength(JSON.stringify(msgBody), "utf8");
|
|
121
151
|
}
|
|
152
|
+
function splitTextByFixedLength(text, limit) {
|
|
153
|
+
if (!text) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (limit <= 0 || text.length <= limit) {
|
|
157
|
+
return [text];
|
|
158
|
+
}
|
|
159
|
+
const chunks = [];
|
|
160
|
+
for (let index = 0; index < text.length; index += limit) {
|
|
161
|
+
chunks.push(text.slice(index, index + limit));
|
|
162
|
+
}
|
|
163
|
+
return chunks;
|
|
164
|
+
}
|
|
122
165
|
function isMsgTooLongError(error) {
|
|
123
166
|
return /msg too long/i.test(error ?? "");
|
|
124
167
|
}
|
|
@@ -149,6 +192,35 @@ function buildSnapshotStreamingMsgBody(params) {
|
|
|
149
192
|
typingText: params.typingText,
|
|
150
193
|
});
|
|
151
194
|
}
|
|
195
|
+
function createPartialTextAccumulator() {
|
|
196
|
+
const committedSegments = [];
|
|
197
|
+
let currentPartialText = "";
|
|
198
|
+
const commitCurrentPartial = () => {
|
|
199
|
+
if (!currentPartialText) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (committedSegments.at(-1) !== currentPartialText) {
|
|
203
|
+
committedSegments.push(currentPartialText);
|
|
204
|
+
}
|
|
205
|
+
currentPartialText = "";
|
|
206
|
+
};
|
|
207
|
+
const getVisibleText = () => {
|
|
208
|
+
const segments = currentPartialText
|
|
209
|
+
? [...committedSegments, currentPartialText]
|
|
210
|
+
: [...committedSegments];
|
|
211
|
+
return segments.filter(Boolean).join("\n\n");
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
noteAssistantMessageStart: () => {
|
|
215
|
+
commitCurrentPartial();
|
|
216
|
+
},
|
|
217
|
+
absorbPartial: (text) => {
|
|
218
|
+
currentPartialText = mergeStreamingText(currentPartialText, text);
|
|
219
|
+
return getVisibleText();
|
|
220
|
+
},
|
|
221
|
+
getVisibleText,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
152
224
|
function createLatestTextThrottleLoop(params) {
|
|
153
225
|
let lastSentAt = 0;
|
|
154
226
|
let pendingText = "";
|
|
@@ -509,10 +581,6 @@ async function modifyC2CMsg(params) {
|
|
|
509
581
|
const { account, fromAccount, toAccount, msgKey, msgBody, target } = params;
|
|
510
582
|
const error = (msg) => target ? log(target, "error", msg) : logSimple("error", msg);
|
|
511
583
|
const verbose = (msg) => target ? logVerbose(target, msg) : undefined;
|
|
512
|
-
if (!fromAccount) {
|
|
513
|
-
error("C2C 消息修改失败: 缺少机器人账号,无法确定 From_Account");
|
|
514
|
-
return { ok: false, error: "missing outbound sender account for c2c modify" };
|
|
515
|
-
}
|
|
516
584
|
const url = buildTimbotApiUrl(account, "openim", "modify_c2c_msg");
|
|
517
585
|
const body = {
|
|
518
586
|
From_Account: fromAccount,
|
|
@@ -604,12 +672,6 @@ async function executeStreamingReply(params) {
|
|
|
604
672
|
let streamFailed = !useStreaming;
|
|
605
673
|
let streamFailureReason;
|
|
606
674
|
let streamOverflowed = false;
|
|
607
|
-
let overflowSplitActive = false;
|
|
608
|
-
let overflowVisibleText = "";
|
|
609
|
-
let overflowBufferedText = "";
|
|
610
|
-
let overflowFirstChunkCommitted = false;
|
|
611
|
-
let overflowTailMsgRef;
|
|
612
|
-
let overflowTailText = "";
|
|
613
675
|
let lastSentVisibleText = "";
|
|
614
676
|
const partialTextAccumulator = createPartialTextAccumulator();
|
|
615
677
|
const streamFallbackTexts = [];
|
|
@@ -708,193 +770,6 @@ async function executeStreamingReply(params) {
|
|
|
708
770
|
}
|
|
709
771
|
return true;
|
|
710
772
|
};
|
|
711
|
-
const syncOverflowSplitState = async (forceFlush) => {
|
|
712
|
-
const chunks = splitFinalText(overflowBufferedText).filter((chunk) => chunk.length > 0);
|
|
713
|
-
if (chunks.length === 0) {
|
|
714
|
-
overflowTailMsgRef = undefined;
|
|
715
|
-
overflowTailText = "";
|
|
716
|
-
return true;
|
|
717
|
-
}
|
|
718
|
-
const readyChunks = forceFlush ? chunks : chunks.slice(0, -1);
|
|
719
|
-
for (const chunk of readyChunks) {
|
|
720
|
-
if (!overflowFirstChunkCommitted && !useTimStream && streamMsgRef) {
|
|
721
|
-
const firstResult = await transport.modifyMsg({
|
|
722
|
-
ref: streamMsgRef,
|
|
723
|
-
msgBody: buildTextMsgBody(chunk),
|
|
724
|
-
});
|
|
725
|
-
if (firstResult.ok) {
|
|
726
|
-
overflowFirstChunkCommitted = true;
|
|
727
|
-
lastSentVisibleText = chunk;
|
|
728
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
log(target, "warn", `${L}超限续发首段覆盖占位消息失败,改为新消息发送: ${firstResult.error}`);
|
|
732
|
-
}
|
|
733
|
-
if (overflowTailMsgRef) {
|
|
734
|
-
const sealTailResult = await transport.modifyMsg({
|
|
735
|
-
ref: overflowTailMsgRef,
|
|
736
|
-
msgBody: buildTextMsgBody(chunk),
|
|
737
|
-
});
|
|
738
|
-
if (sealTailResult.ok) {
|
|
739
|
-
overflowTailMsgRef = undefined;
|
|
740
|
-
overflowTailText = "";
|
|
741
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
log(target, "warn", `${L}超限续发尾段封板失败,改为新消息发送: ${sealTailResult.error}`);
|
|
745
|
-
overflowTailMsgRef = undefined;
|
|
746
|
-
overflowTailText = "";
|
|
747
|
-
}
|
|
748
|
-
const result = await transport.sendText({ text: chunk });
|
|
749
|
-
if (!result.ok) {
|
|
750
|
-
streamFailureReason = result.error || streamFailureReason || `${L}超限续发失败`;
|
|
751
|
-
streamFailed = true;
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
755
|
-
}
|
|
756
|
-
const liveTail = forceFlush ? "" : (chunks.at(-1) ?? "");
|
|
757
|
-
overflowBufferedText = liveTail;
|
|
758
|
-
if (forceFlush) {
|
|
759
|
-
overflowTailMsgRef = undefined;
|
|
760
|
-
overflowTailText = "";
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
763
|
-
if (!liveTail) {
|
|
764
|
-
overflowTailMsgRef = undefined;
|
|
765
|
-
overflowTailText = "";
|
|
766
|
-
return true;
|
|
767
|
-
}
|
|
768
|
-
if (overflowTailMsgRef) {
|
|
769
|
-
if (overflowTailText === liveTail) {
|
|
770
|
-
return true;
|
|
771
|
-
}
|
|
772
|
-
const tailResult = await transport.modifyMsg({
|
|
773
|
-
ref: overflowTailMsgRef,
|
|
774
|
-
msgBody: buildTextMsgBody(liveTail),
|
|
775
|
-
});
|
|
776
|
-
if (tailResult.ok) {
|
|
777
|
-
overflowTailText = liveTail;
|
|
778
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
log(target, "warn", `${L}超限续发尾段更新失败,改为新尾消息: ${tailResult.error}`);
|
|
782
|
-
overflowTailMsgRef = undefined;
|
|
783
|
-
overflowTailText = "";
|
|
784
|
-
}
|
|
785
|
-
const createTailResult = await transport.sendMsgBody({
|
|
786
|
-
msgBody: buildTextMsgBody(liveTail),
|
|
787
|
-
});
|
|
788
|
-
if (createTailResult.ok && createTailResult.ref) {
|
|
789
|
-
overflowTailMsgRef = createTailResult.ref;
|
|
790
|
-
overflowTailText = liveTail;
|
|
791
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
792
|
-
return true;
|
|
793
|
-
}
|
|
794
|
-
log(target, "warn", `${L}超限续发尾段创建失败: ${createTailResult.error || "未返回消息引用"}`);
|
|
795
|
-
overflowTailMsgRef = undefined;
|
|
796
|
-
overflowTailText = "";
|
|
797
|
-
return true;
|
|
798
|
-
};
|
|
799
|
-
const absorbOverflowVisibleText = async (visibleText, forceFlush) => {
|
|
800
|
-
const mergedVisibleText = mergeStreamingText(overflowVisibleText, visibleText);
|
|
801
|
-
if (!forceFlush && mergedVisibleText === overflowVisibleText) {
|
|
802
|
-
return true;
|
|
803
|
-
}
|
|
804
|
-
// Only append newly observed text so sealed chunks never re-enter the splitter.
|
|
805
|
-
const appendedText = mergedVisibleText.startsWith(overflowVisibleText)
|
|
806
|
-
? mergedVisibleText.slice(overflowVisibleText.length)
|
|
807
|
-
: mergedVisibleText;
|
|
808
|
-
overflowVisibleText = mergedVisibleText;
|
|
809
|
-
overflowBufferedText += appendedText;
|
|
810
|
-
return syncOverflowSplitState(forceFlush);
|
|
811
|
-
};
|
|
812
|
-
const activateOverflowSplit = async (visibleText, reason) => {
|
|
813
|
-
streamOverflowed = true;
|
|
814
|
-
if (overflowPolicy !== "split") {
|
|
815
|
-
streamFailureReason = reason;
|
|
816
|
-
log(target, "warn", reason);
|
|
817
|
-
streamFailed = true;
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
if (!overflowSplitActive) {
|
|
821
|
-
overflowSplitActive = true;
|
|
822
|
-
overflowVisibleText = "";
|
|
823
|
-
overflowBufferedText = "";
|
|
824
|
-
overflowFirstChunkCommitted = false;
|
|
825
|
-
overflowTailMsgRef = undefined;
|
|
826
|
-
overflowTailText = "";
|
|
827
|
-
log(target, "warn", `${reason},切换为分段续发`);
|
|
828
|
-
if (useTimStream && timStreamMsgId) {
|
|
829
|
-
const closed = await closeTimStream(lastSentVisibleText || typingText);
|
|
830
|
-
if (closed) {
|
|
831
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
log(target, "warn", `${L}超限切换分段续发时流式收尾失败`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
else if (useCustomStreaming && streamMsgRef && lastSentVisibleText) {
|
|
838
|
-
const finalizeResult = await transport.modifyMsg({
|
|
839
|
-
ref: streamMsgRef,
|
|
840
|
-
msgBody: buildSnapshotStreamingMsgBody({
|
|
841
|
-
useCustomStreaming: true,
|
|
842
|
-
text: lastSentVisibleText,
|
|
843
|
-
isFinished: 1,
|
|
844
|
-
typingText,
|
|
845
|
-
}),
|
|
846
|
-
});
|
|
847
|
-
if (finalizeResult.ok) {
|
|
848
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
log(target, "warn", `${L}超限切换分段续发时 custom_modify 收尾失败: ${finalizeResult.error}`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return absorbOverflowVisibleText(visibleText, false);
|
|
856
|
-
};
|
|
857
|
-
const activateProgressiveSplit = async (visibleText, reason) => {
|
|
858
|
-
if (overflowPolicy !== "split") {
|
|
859
|
-
return true;
|
|
860
|
-
}
|
|
861
|
-
if (!overflowSplitActive) {
|
|
862
|
-
overflowSplitActive = true;
|
|
863
|
-
overflowVisibleText = "";
|
|
864
|
-
overflowBufferedText = "";
|
|
865
|
-
overflowFirstChunkCommitted = false;
|
|
866
|
-
overflowTailMsgRef = undefined;
|
|
867
|
-
overflowTailText = "";
|
|
868
|
-
logVerbose(target, `${reason} (visibleLen=${visibleText.length})`);
|
|
869
|
-
if (useTimStream && timStreamMsgId) {
|
|
870
|
-
const closed = await closeTimStream(lastSentVisibleText || typingText);
|
|
871
|
-
if (closed) {
|
|
872
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
log(target, "warn", `${L}滚动续发切换时流式收尾失败`);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
else if (useCustomStreaming && streamMsgRef && lastSentVisibleText) {
|
|
879
|
-
const finalizeResult = await transport.modifyMsg({
|
|
880
|
-
ref: streamMsgRef,
|
|
881
|
-
msgBody: buildSnapshotStreamingMsgBody({
|
|
882
|
-
useCustomStreaming: true,
|
|
883
|
-
text: lastSentVisibleText,
|
|
884
|
-
isFinished: 1,
|
|
885
|
-
typingText,
|
|
886
|
-
}),
|
|
887
|
-
});
|
|
888
|
-
if (finalizeResult.ok) {
|
|
889
|
-
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
log(target, "warn", `${L}滚动续发切换时 custom_modify 收尾失败: ${finalizeResult.error}`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
return absorbOverflowVisibleText(visibleText, false);
|
|
897
|
-
};
|
|
898
773
|
const sendOverflowNotice = async () => {
|
|
899
774
|
if (useTimStream && timStreamMsgId) {
|
|
900
775
|
const closed = await closeTimStream(lastSentVisibleText || typingText);
|
|
@@ -949,15 +824,6 @@ async function executeStreamingReply(params) {
|
|
|
949
824
|
if (!visibleText.trim() || visibleText === lastSentVisibleText) {
|
|
950
825
|
return true;
|
|
951
826
|
}
|
|
952
|
-
if (!overflowSplitActive && overflowPolicy === "split") {
|
|
953
|
-
const splitChunks = splitFinalText(visibleText).filter((chunk) => chunk.length > 0);
|
|
954
|
-
if (splitChunks.length > 1) {
|
|
955
|
-
return activateProgressiveSplit(visibleText, `${L}流式文本达到分段阈值,开始滚动续发`);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
if (overflowSplitActive) {
|
|
959
|
-
return absorbOverflowVisibleText(visibleText, false);
|
|
960
|
-
}
|
|
961
827
|
if (useTimStream) {
|
|
962
828
|
if (lastSentVisibleText && !visibleText.startsWith(lastSentVisibleText)) {
|
|
963
829
|
streamFailureReason = `${L}流式快照与已发送文本不连续,停止 TIM stream 增量更新`;
|
|
@@ -982,7 +848,11 @@ async function executeStreamingReply(params) {
|
|
|
982
848
|
streamMsgId: timStreamMsgId,
|
|
983
849
|
});
|
|
984
850
|
if (estimatedBytes > TIMBOT_STREAM_SOFT_LIMIT_BYTES) {
|
|
985
|
-
|
|
851
|
+
streamOverflowed = true;
|
|
852
|
+
streamFailureReason = `${L}流式消息接近长度上限,停止增量更新并改为最终分段发送 (bytes=${estimatedBytes})`;
|
|
853
|
+
log(target, "warn", streamFailureReason);
|
|
854
|
+
streamFailed = true;
|
|
855
|
+
return false;
|
|
986
856
|
}
|
|
987
857
|
const result = await sendTimStreamChunk({
|
|
988
858
|
markdown: deltaText,
|
|
@@ -999,7 +869,7 @@ async function executeStreamingReply(params) {
|
|
|
999
869
|
|| streamFailureReason
|
|
1000
870
|
|| (timStreamMsgId ? `${L}流式消息更新失败` : `${L}流式消息未返回 StreamMsgID`);
|
|
1001
871
|
if (isMsgTooLongError(result.error)) {
|
|
1002
|
-
|
|
872
|
+
streamOverflowed = true;
|
|
1003
873
|
}
|
|
1004
874
|
log(target, "warn", `${L}流式消息发送失败,等待最终收尾: ${streamFailureReason}`);
|
|
1005
875
|
streamFailed = true;
|
|
@@ -1013,7 +883,11 @@ async function executeStreamingReply(params) {
|
|
|
1013
883
|
});
|
|
1014
884
|
const estimatedBytes = estimateMsgBodyBytes(msgBody);
|
|
1015
885
|
if (estimatedBytes > TIMBOT_STREAM_SOFT_LIMIT_BYTES) {
|
|
1016
|
-
|
|
886
|
+
streamOverflowed = true;
|
|
887
|
+
streamFailureReason = `${L}流式消息接近长度上限,停止 modify 并改为最终分段发送 (bytes=${estimatedBytes})`;
|
|
888
|
+
log(target, "warn", streamFailureReason);
|
|
889
|
+
streamFailed = true;
|
|
890
|
+
return false;
|
|
1017
891
|
}
|
|
1018
892
|
if (streamMsgRef) {
|
|
1019
893
|
const result = await transport.modifyMsg({
|
|
@@ -1027,7 +901,7 @@ async function executeStreamingReply(params) {
|
|
|
1027
901
|
}
|
|
1028
902
|
streamFailureReason = result.error || streamFailureReason || `${L}流式消息更新失败`;
|
|
1029
903
|
if (isMsgTooLongError(result.error)) {
|
|
1030
|
-
|
|
904
|
+
streamOverflowed = true;
|
|
1031
905
|
}
|
|
1032
906
|
log(target, "warn", `${L}流式消息更新失败,等待最终收尾: ${streamFailureReason}`);
|
|
1033
907
|
streamFailed = true;
|
|
@@ -1042,7 +916,7 @@ async function executeStreamingReply(params) {
|
|
|
1042
916
|
}
|
|
1043
917
|
streamFailureReason = result.error || streamFailureReason || `${L}流式消息创建失败`;
|
|
1044
918
|
if (isMsgTooLongError(result.error)) {
|
|
1045
|
-
|
|
919
|
+
streamOverflowed = true;
|
|
1046
920
|
}
|
|
1047
921
|
log(target, "warn", `${L}流式消息创建失败,等待最终收尾: ${streamFailureReason}`);
|
|
1048
922
|
streamFailed = true;
|
|
@@ -1157,7 +1031,7 @@ async function executeStreamingReply(params) {
|
|
|
1157
1031
|
? Math.max(0, firstPartialReplyAt - assistantMessageStartAt)
|
|
1158
1032
|
: undefined;
|
|
1159
1033
|
if (useStreaming) {
|
|
1160
|
-
logVerbose(target, `[partialStream] ${L}summary: assistantStarts=${assistantMessageStartCount}, partialCount=${partialReplyCount}, firstPartialLatencyMs=${firstPartialLatencyMs ?? "n/a"}, visibleLen=${streamVisibleText.length}, fallbackLen=${streamFallbackText.length}, streamFailed=${streamFailed}, overflowed=${streamOverflowed},
|
|
1034
|
+
logVerbose(target, `[partialStream] ${L}summary: assistantStarts=${assistantMessageStartCount}, partialCount=${partialReplyCount}, firstPartialLatencyMs=${firstPartialLatencyMs ?? "n/a"}, visibleLen=${streamVisibleText.length}, fallbackLen=${streamFallbackText.length}, streamFailed=${streamFailed}, overflowed=${streamOverflowed}, t=${Date.now()}`);
|
|
1161
1035
|
}
|
|
1162
1036
|
if (useStreaming
|
|
1163
1037
|
&& ((useTimStream && (timStreamMsgId || streamVisibleText || streamFallbackText))
|
|
@@ -1168,10 +1042,6 @@ async function executeStreamingReply(params) {
|
|
|
1168
1042
|
}
|
|
1169
1043
|
let finalized = false;
|
|
1170
1044
|
let overflowStopHandled = false;
|
|
1171
|
-
if (overflowSplitActive) {
|
|
1172
|
-
overflowStopHandled = true;
|
|
1173
|
-
finalized = await absorbOverflowVisibleText(fullText, true);
|
|
1174
|
-
}
|
|
1175
1045
|
const handleOverflow = async () => {
|
|
1176
1046
|
if (overflowPolicy === "split" && fullText) {
|
|
1177
1047
|
return sendChunkedFinalText(fullText);
|
|
@@ -1372,42 +1242,6 @@ async function executeStreamingReply(params) {
|
|
|
1372
1242
|
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
1373
1243
|
}
|
|
1374
1244
|
}
|
|
1375
|
-
// 从 MsgBody 提取文本内容
|
|
1376
|
-
function extractTextFromMsgBody(msgBody) {
|
|
1377
|
-
if (!msgBody || !Array.isArray(msgBody))
|
|
1378
|
-
return "";
|
|
1379
|
-
const texts = [];
|
|
1380
|
-
for (const elem of msgBody) {
|
|
1381
|
-
if (elem.MsgType === "TIMTextElem" && elem.MsgContent?.Text) {
|
|
1382
|
-
texts.push(elem.MsgContent.Text);
|
|
1383
|
-
}
|
|
1384
|
-
else if (elem.MsgType === "TIMCustomElem") {
|
|
1385
|
-
texts.push("[custom]");
|
|
1386
|
-
}
|
|
1387
|
-
else if (elem.MsgType === "TIMImageElem") {
|
|
1388
|
-
texts.push("[image]");
|
|
1389
|
-
}
|
|
1390
|
-
else if (elem.MsgType === "TIMSoundElem") {
|
|
1391
|
-
texts.push("[voice]");
|
|
1392
|
-
}
|
|
1393
|
-
else if (elem.MsgType === "TIMFileElem") {
|
|
1394
|
-
texts.push("[file]");
|
|
1395
|
-
}
|
|
1396
|
-
else if (elem.MsgType === "TIMVideoFileElem") {
|
|
1397
|
-
texts.push("[video]");
|
|
1398
|
-
}
|
|
1399
|
-
else if (elem.MsgType === "TIMFaceElem") {
|
|
1400
|
-
texts.push("[face]");
|
|
1401
|
-
}
|
|
1402
|
-
else if (elem.MsgType === "TIMLocationElem") {
|
|
1403
|
-
texts.push("[location]");
|
|
1404
|
-
}
|
|
1405
|
-
else if (elem.MsgType === "TIMStreamElem") {
|
|
1406
|
-
texts.push("[stream]");
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
return texts.join("\n");
|
|
1410
|
-
}
|
|
1411
1245
|
// 处理消息并回复
|
|
1412
1246
|
async function processAndReply(params) {
|
|
1413
1247
|
const { target, msg } = params;
|
|
@@ -1415,9 +1249,8 @@ async function processAndReply(params) {
|
|
|
1415
1249
|
const config = target.config;
|
|
1416
1250
|
const account = target.account;
|
|
1417
1251
|
const fromAccount = msg.From_Account?.trim() || "unknown";
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
if (isSelfInboundMessage(account, fromAccount, inboundBotAccount ? [inboundBotAccount] : [])) {
|
|
1252
|
+
const outboundSender = resolveOutboundSenderAccount(account);
|
|
1253
|
+
if (fromAccount === outboundSender) {
|
|
1421
1254
|
log(target, "info", `跳过机器人自身消息 <- ${fromAccount}`);
|
|
1422
1255
|
return;
|
|
1423
1256
|
}
|
|
@@ -1491,7 +1324,7 @@ async function processAndReply(params) {
|
|
|
1491
1324
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, "timbot", account.accountId, {
|
|
1492
1325
|
fallbackLimit: TIMBOT_FINAL_TEXT_CHUNK_LIMIT,
|
|
1493
1326
|
});
|
|
1494
|
-
const splitFinalText = (text) =>
|
|
1327
|
+
const splitFinalText = (text) => splitTextByFixedLength(text, finalTextChunkLimit);
|
|
1495
1328
|
logVerbose(target, `开始生成回复 -> ${fromAccount}`);
|
|
1496
1329
|
logVerbose(target, `转发给 OpenClaw: RawBody=${rawBody.slice(0, 100)}, SessionKey=${ctxPayload.SessionKey}, From=${ctxPayload.From}`);
|
|
1497
1330
|
// C2C 传输适配器
|
|
@@ -1500,7 +1333,7 @@ async function processAndReply(params) {
|
|
|
1500
1333
|
sendStreamMsg: (p) => sendTimbotC2CStreamMessage({
|
|
1501
1334
|
account,
|
|
1502
1335
|
toAccount: fromAccount,
|
|
1503
|
-
fromAccount:
|
|
1336
|
+
fromAccount: outboundSender,
|
|
1504
1337
|
target,
|
|
1505
1338
|
...p,
|
|
1506
1339
|
}),
|
|
@@ -1508,7 +1341,7 @@ async function processAndReply(params) {
|
|
|
1508
1341
|
const ref = p.ref;
|
|
1509
1342
|
return modifyC2CMsg({
|
|
1510
1343
|
account,
|
|
1511
|
-
fromAccount:
|
|
1344
|
+
fromAccount: outboundSender,
|
|
1512
1345
|
toAccount: fromAccount,
|
|
1513
1346
|
msgKey: ref.msgKey,
|
|
1514
1347
|
msgBody: p.msgBody,
|
|
@@ -1520,7 +1353,7 @@ async function processAndReply(params) {
|
|
|
1520
1353
|
account,
|
|
1521
1354
|
toAccount: fromAccount,
|
|
1522
1355
|
msgBody: p.msgBody,
|
|
1523
|
-
fromAccount:
|
|
1356
|
+
fromAccount: outboundSender,
|
|
1524
1357
|
target,
|
|
1525
1358
|
});
|
|
1526
1359
|
return {
|
|
@@ -1533,7 +1366,7 @@ async function processAndReply(params) {
|
|
|
1533
1366
|
account,
|
|
1534
1367
|
toAccount: fromAccount,
|
|
1535
1368
|
text: p.text,
|
|
1536
|
-
fromAccount:
|
|
1369
|
+
fromAccount: outboundSender,
|
|
1537
1370
|
target,
|
|
1538
1371
|
}),
|
|
1539
1372
|
};
|
|
@@ -1569,9 +1402,8 @@ async function processGroupAndReply(params) {
|
|
|
1569
1402
|
const account = target.account;
|
|
1570
1403
|
const groupId = msg.GroupId?.trim() || "unknown";
|
|
1571
1404
|
const fromAccount = msg.From_Account?.trim() || "unknown";
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
if (isSelfInboundMessage(account, fromAccount, inboundBotAccount ? [inboundBotAccount] : [])) {
|
|
1405
|
+
const outboundSender = resolveOutboundSenderAccount(account);
|
|
1406
|
+
if (fromAccount === outboundSender) {
|
|
1575
1407
|
log(target, "info", `跳过机器人自身消息 <- group:${groupId}, from: ${fromAccount}`);
|
|
1576
1408
|
return;
|
|
1577
1409
|
}
|
|
@@ -1631,6 +1463,7 @@ async function processGroupAndReply(params) {
|
|
|
1631
1463
|
MessageSid: msg.MsgKey ?? String(msg.MsgSeq ?? ""),
|
|
1632
1464
|
OriginatingChannel: "timbot",
|
|
1633
1465
|
OriginatingTo: `timbot:group:${groupId}`,
|
|
1466
|
+
WasMentioned: true,
|
|
1634
1467
|
});
|
|
1635
1468
|
await core.channel.session.recordInboundSession({
|
|
1636
1469
|
storePath,
|
|
@@ -1648,7 +1481,7 @@ async function processGroupAndReply(params) {
|
|
|
1648
1481
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, "timbot", account.accountId, {
|
|
1649
1482
|
fallbackLimit: TIMBOT_FINAL_TEXT_CHUNK_LIMIT,
|
|
1650
1483
|
});
|
|
1651
|
-
const splitFinalText = (text) =>
|
|
1484
|
+
const splitFinalText = (text) => splitTextByFixedLength(text, finalTextChunkLimit);
|
|
1652
1485
|
logVerbose(target, `开始生成群回复 -> group:${groupId}`);
|
|
1653
1486
|
// Group 传输适配器
|
|
1654
1487
|
const transport = {
|
|
@@ -1656,7 +1489,7 @@ async function processGroupAndReply(params) {
|
|
|
1656
1489
|
sendStreamMsg: (p) => sendTimbotGroupStreamMessage({
|
|
1657
1490
|
account,
|
|
1658
1491
|
groupId,
|
|
1659
|
-
fromAccount:
|
|
1492
|
+
fromAccount: outboundSender,
|
|
1660
1493
|
target,
|
|
1661
1494
|
...p,
|
|
1662
1495
|
}),
|
|
@@ -1675,7 +1508,7 @@ async function processGroupAndReply(params) {
|
|
|
1675
1508
|
account,
|
|
1676
1509
|
groupId,
|
|
1677
1510
|
msgBody: p.msgBody,
|
|
1678
|
-
fromAccount:
|
|
1511
|
+
fromAccount: outboundSender,
|
|
1679
1512
|
target,
|
|
1680
1513
|
});
|
|
1681
1514
|
return {
|
|
@@ -1688,7 +1521,7 @@ async function processGroupAndReply(params) {
|
|
|
1688
1521
|
account,
|
|
1689
1522
|
groupId,
|
|
1690
1523
|
text: p.text,
|
|
1691
|
-
fromAccount:
|
|
1524
|
+
fromAccount: outboundSender,
|
|
1692
1525
|
target,
|
|
1693
1526
|
}),
|
|
1694
1527
|
};
|
|
@@ -1735,7 +1568,6 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1735
1568
|
const targets = webhookTargets.get(path);
|
|
1736
1569
|
if (!targets || targets.length === 0)
|
|
1737
1570
|
return false;
|
|
1738
|
-
const firstTarget = targets[0];
|
|
1739
1571
|
// 只处理 POST 请求
|
|
1740
1572
|
if (req.method !== "POST") {
|
|
1741
1573
|
logSimple("warn", `收到非 POST 请求: ${req.method} ${path}`);
|
|
@@ -1758,27 +1590,16 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1758
1590
|
return true;
|
|
1759
1591
|
}
|
|
1760
1592
|
const msg = bodyResult.value;
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
if (sdkAppId && candidate.account.sdkAppId !== sdkAppId)
|
|
1767
|
-
return false;
|
|
1768
|
-
// 如果配置了 botAccount,校验 To_Account 是否匹配
|
|
1769
|
-
if (candidate.account.botAccount && msg.To_Account) {
|
|
1770
|
-
return candidate.account.botAccount === msg.To_Account;
|
|
1771
|
-
}
|
|
1772
|
-
return true;
|
|
1773
|
-
}) ?? firstTarget;
|
|
1774
|
-
if (!target.account.configured) {
|
|
1775
|
-
logSimple("warn", `账号 ${target.account.accountId} 未配置,跳过处理`);
|
|
1776
|
-
// 即使未配置也返回成功,避免腾讯 IM 重试
|
|
1777
|
-
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1778
|
-
return true;
|
|
1593
|
+
logSimple("info", `webhook 消息详情: callback=${msg.CallbackCommand || "-"}, To_Account=${msg.To_Account || "-"}, AtRobots_Account=${JSON.stringify(msg.AtRobots_Account ?? [])}, From_Account=${msg.From_Account || "-"}, GroupId=${msg.GroupId || "-"}`);
|
|
1594
|
+
const sdkMatchedTargets = matchTimbotWebhookTargetsBySdkAppId(targets, sdkAppId);
|
|
1595
|
+
let target = selectTimbotWebhookTarget({ targets: sdkMatchedTargets, msg });
|
|
1596
|
+
if (!target && sdkMatchedTargets.length === 1) {
|
|
1597
|
+
target = sdkMatchedTargets[0];
|
|
1779
1598
|
}
|
|
1780
1599
|
// 签名验证
|
|
1781
|
-
|
|
1600
|
+
const signatureTargets = (target ? [target] : sdkMatchedTargets)
|
|
1601
|
+
.filter((candidate) => candidate.account.configured && candidate.account.token);
|
|
1602
|
+
if (signatureTargets.length > 0) {
|
|
1782
1603
|
// 1. 超时校验:RequestTime 与当前时间相差超过 60 秒则拒绝
|
|
1783
1604
|
const requestTimestamp = parseInt(requestTime, 10);
|
|
1784
1605
|
const nowTimestamp = Math.floor(Date.now() / 1000);
|
|
@@ -1790,18 +1611,41 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1790
1611
|
return true;
|
|
1791
1612
|
}
|
|
1792
1613
|
// 2. 签名校验:sha256(token + requestTime)
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1614
|
+
const verifiedTargets = signatureTargets.filter((candidate) => {
|
|
1615
|
+
const expectedSign = createHash("sha256")
|
|
1616
|
+
.update(candidate.account.token + requestTime)
|
|
1617
|
+
.digest("hex");
|
|
1618
|
+
return (sign.length === expectedSign.length
|
|
1619
|
+
&& timingSafeEqual(Buffer.from(sign), Buffer.from(expectedSign)));
|
|
1620
|
+
});
|
|
1621
|
+
if (verifiedTargets.length === 0) {
|
|
1622
|
+
const expectedSign = createHash("sha256")
|
|
1623
|
+
.update(signatureTargets[0].account.token + requestTime)
|
|
1624
|
+
.digest("hex");
|
|
1798
1625
|
logSimple("error", `签名验证失败: 收到=${sign.slice(0, 16)}..., 预期=${expectedSign.slice(0, 16)}...`);
|
|
1799
1626
|
res.statusCode = 403;
|
|
1800
1627
|
res.end("Signature verification failed");
|
|
1801
1628
|
return true;
|
|
1802
1629
|
}
|
|
1630
|
+
if (!target && verifiedTargets.length === 1) {
|
|
1631
|
+
target = verifiedTargets[0];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (!target) {
|
|
1635
|
+
const callbackCommand = msg.CallbackCommand ?? "";
|
|
1636
|
+
const mentions = extractMentionedBotAccounts(extractTextFromMsgBody(msg.MsgBody));
|
|
1637
|
+
logSimple("warn", `未能唯一匹配 webhook 账号,跳过处理: callback=${callbackCommand || "-"}, sdkAppId=${sdkAppId || "-"}, to=${msg.To_Account?.trim() || "-"}, group=${msg.GroupId?.trim() || "-"}, mentions=${mentions.join(",") || "-"}`);
|
|
1638
|
+
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1639
|
+
return true;
|
|
1640
|
+
}
|
|
1641
|
+
if (!target.account.configured) {
|
|
1642
|
+
logSimple("warn", `账号 ${target.account.accountId} 未配置,跳过处理`);
|
|
1643
|
+
// 即使未配置也返回成功,避免腾讯 IM 重试
|
|
1644
|
+
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1645
|
+
return true;
|
|
1803
1646
|
}
|
|
1804
1647
|
target.statusSink?.({ lastInboundAt: Date.now() });
|
|
1648
|
+
logSimple("info", `匹配到账号: ${target.account.accountId}, botAccount=${target.account.botAccount || "-"}`);
|
|
1805
1649
|
const callbackCommand = msg.CallbackCommand ?? "";
|
|
1806
1650
|
// 立即返回成功响应给腾讯 IM
|
|
1807
1651
|
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|