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.
@@ -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
- return activateOverflowSplit(visibleText, `${L}流式消息接近长度上限,停止增量更新 (bytes=${estimatedBytes})`);
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
- return activateOverflowSplit(visibleText, `${L}流式消息更新超限,停止增量更新 (${result.error})`);
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
- return activateOverflowSplit(visibleText, `${L}流式消息接近长度上限,停止 modify (bytes=${estimatedBytes})`);
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
- return activateOverflowSplit(visibleText, `${L}流式消息 modify 超限,停止更新 (${result.error})`);
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
- return activateOverflowSplit(visibleText, `${L}流式消息创建超限,切换分段续发 (${result.error})`);
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}, splitActive=${overflowSplitActive}, bufferedLen=${overflowBufferedText.length}, t=${Date.now()}`);
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 inboundBotAccount = msg.To_Account?.trim() || undefined;
1419
- const outboundFromAccount = resolveOutboundFromAccount(account, inboundBotAccount);
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) => splitTextByPreferredBreaks(text, finalTextChunkLimit);
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: outboundFromAccount,
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: outboundFromAccount,
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: outboundFromAccount,
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: outboundFromAccount,
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 inboundBotAccount = msg.To_Account?.trim() || undefined;
1573
- const outboundFromAccount = resolveOutboundFromAccount(account, inboundBotAccount);
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) => splitTextByPreferredBreaks(text, finalTextChunkLimit);
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: outboundFromAccount,
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: outboundFromAccount,
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: outboundFromAccount,
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
- // 根据 SdkAppid To_Account 匹配目标账号
1762
- const target = targets.find((candidate) => {
1763
- if (!candidate.account.configured)
1764
- return false;
1765
- // 如果 URL 带了 SdkAppid,校验是否匹配
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
- if (target.account.token) {
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 expectedSign = createHash("sha256")
1794
- .update(target.account.token + requestTime)
1795
- .digest("hex");
1796
- if (sign.length !== expectedSign.length
1797
- || !timingSafeEqual(Buffer.from(sign), Buffer.from(expectedSign))) {
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: "" });