@soimy/dingtalk 3.5.1 → 3.5.2

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.
@@ -1,12 +1,14 @@
1
- import axios from "axios";
1
+ import fs from "node:fs";
2
+ import axios from "./http-client";
2
3
  import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
3
4
  import { buildAgentSessionKey, resolveSubAgentRoute, dispatchSubAgents } from "./targeting/agent-routing";
4
5
  import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
5
6
  import { attachNativeAckReaction } from "./ack-reaction-service";
6
7
  import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
7
- import { extractAttachmentText } from "./attachment-text-extractor";
8
+ import { extractAttachmentText } from "./messaging/attachment-text-extractor";
8
9
  import { getAccessToken } from "./auth";
9
10
  import { createAICard, finishAICard, isCardInTerminalState } from "./card-service";
11
+ import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
10
12
  import { resolveAckReactionSetting, resolveGroupConfig, resolveRobotCode } from "./config";
11
13
  import { AICardStatus } from "./types";
12
14
  import {
@@ -15,35 +17,10 @@ import {
15
17
  removeCardRun,
16
18
  } from "./card/card-run-registry";
17
19
  import {
18
- applyManualTargetLearningRule,
19
- applyManualTargetsLearningRule,
20
- applyManualGlobalLearningRule,
21
- applyManualSessionLearningNote,
22
- applyTargetSetLearningRule,
23
20
  buildLearningContextBlock,
24
- createOrUpdateTargetSet,
25
- deleteManualRule,
26
- disableManualRule,
27
21
  isLearningEnabled,
28
- listLearningTargetSets,
29
- listScopedLearningRules,
30
- resolveManualForcedReply,
31
22
  } from "./feedback-learning-service";
32
- import { formatGroupMembers, noteGroupMember } from "./group-members-store";
33
- import {
34
- formatLearnAppliedReply,
35
- formatLearnCommandHelp,
36
- formatLearnDeletedReply,
37
- formatLearnDisabledReply,
38
- formatLearnListReply,
39
- formatOwnerOnlyDeniedReply,
40
- formatOwnerStatusReply,
41
- formatTargetSetSavedReply,
42
- formatWhereAmIReply,
43
- formatWhoAmIReply,
44
- isLearningOwner,
45
- parseLearnCommand,
46
- } from "./learning-command-service";
23
+ import { formatGroupMembers, noteGroupMember } from "./targeting/group-members-store";
47
24
  import { setCurrentLogger } from "./logger-context";
48
25
  import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
49
26
  import {
@@ -63,21 +40,11 @@ import {
63
40
  clearProactiveRiskObservationsForTest,
64
41
  getProactiveRiskObservationForAny,
65
42
  } from "./proactive-risk-registry";
66
- import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./quoted-file-service";
43
+ import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./messaging/quoted-file-service";
67
44
  import { createReplyStrategy } from "./reply-strategy";
68
45
  import type { DeliverPayload } from "./reply-strategy";
69
46
  import { getDingTalkRuntime } from "./runtime";
70
47
  import { sendBySession, sendMessage, sendProactiveMedia } from "./send-service";
71
- import {
72
- formatSessionAliasBoundReply,
73
- formatSessionAliasClearedReply,
74
- formatSessionAliasReply,
75
- formatSessionAliasSetReply,
76
- formatSessionAliasUnboundReply,
77
- formatSessionAliasValidationErrorReply,
78
- parseSessionCommand,
79
- validateSessionAlias,
80
- } from "./session-command-service";
81
48
  import { acquireSessionLock } from "./session-lock";
82
49
  import {
83
50
  clearSessionPeerOverride,
@@ -89,7 +56,7 @@ import {
89
56
  upsertObservedGroupTarget,
90
57
  upsertObservedUserTarget,
91
58
  } from "./targeting/target-directory-store";
92
- import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
59
+ import type { DingTalkConfig, HandleDingTalkMessageParams, Logger, MediaFile } from "./types";
93
60
  import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData } from "./utils";
94
61
  import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
95
62
 
@@ -98,6 +65,72 @@ const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
98
65
  const MAX_DYNAMIC_ACK_DISPOSE_WAIT_MS = 500;
99
66
  const ATTACHMENT_TEXT_PREFIX = "[附件内容摘录]";
100
67
  const proactiveHintLastSentAt = new Map<string, number>();
68
+ const sessionReasoningLevelCache = new Map<string, {
69
+ updatedAt?: number;
70
+ reasoningLevel?: string;
71
+ }>();
72
+ type ReplyMode = "card" | "markdown";
73
+
74
+ function readSessionReasoningLevel(params: {
75
+ storePath?: string;
76
+ sessionKey: string;
77
+ sessionUpdatedAt?: number;
78
+ log?: Logger;
79
+ }): string | undefined {
80
+ if (!params.storePath || !params.sessionKey) {
81
+ return undefined;
82
+ }
83
+ const cacheKey = `${params.storePath}:${params.sessionKey}`;
84
+ const cached = sessionReasoningLevelCache.get(cacheKey);
85
+ if (
86
+ cached
87
+ && params.sessionUpdatedAt !== undefined
88
+ && cached.updatedAt === params.sessionUpdatedAt
89
+ ) {
90
+ return cached.reasoningLevel;
91
+ }
92
+ try {
93
+ const raw = fs.readFileSync(params.storePath, "utf8");
94
+ const parsed = JSON.parse(raw) as Record<string, { reasoningLevel?: unknown }>;
95
+ const value = parsed?.[params.sessionKey]?.reasoningLevel;
96
+ const reasoningLevel = typeof value === "string" ? value.trim().toLowerCase() : undefined;
97
+ sessionReasoningLevelCache.set(cacheKey, {
98
+ updatedAt: params.sessionUpdatedAt,
99
+ reasoningLevel,
100
+ });
101
+ return reasoningLevel;
102
+ } catch (err: unknown) {
103
+ params.log?.debug?.(
104
+ `[DingTalk][Session] Failed to read session reasoning level from ${params.storePath}: ${getErrorMessage(err)}`,
105
+ );
106
+ return undefined;
107
+ }
108
+ }
109
+
110
+ function shouldDisableBlockStreamingForReplyMode(params: {
111
+ replyMode: ReplyMode;
112
+ reasoningLevel?: string;
113
+ sessionKey: string;
114
+ log?: Logger;
115
+ }): boolean {
116
+ if (params.replyMode === "markdown") {
117
+ const shouldDisable = params.reasoningLevel === "on" || params.reasoningLevel === "stream";
118
+ if (shouldDisable) {
119
+ params.log?.debug?.(
120
+ `[DingTalk][Markdown] Disable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
121
+ );
122
+ }
123
+ return shouldDisable;
124
+ }
125
+
126
+ const shouldDisable = params.reasoningLevel !== "on";
127
+ if (!shouldDisable) {
128
+ params.log?.debug?.(
129
+ `[DingTalk][Card] Enable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
130
+ );
131
+ }
132
+ return shouldDisable;
133
+ }
101
134
 
102
135
  function resolvePinnedMainDmOwner(params: {
103
136
  dmScope?: string;
@@ -239,6 +272,7 @@ function buildGroupTurnContextPrompt(params: {
239
272
 
240
273
  type ReplyStreamPayload = {
241
274
  text?: string;
275
+ isReasoning?: boolean;
242
276
  };
243
277
 
244
278
  type ReplyChunkInfo = {
@@ -365,7 +399,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
365
399
  const rt = getDingTalkRuntime();
366
400
 
367
401
  // Save logger globally so shared services can log consistently without threading log everywhere.
368
- setCurrentLogger(log);
402
+ setCurrentLogger(log, accountId);
369
403
 
370
404
  log?.debug?.("[DingTalk] Full Inbound Data: " + JSON.stringify(maskSensitiveData(data)));
371
405
 
@@ -628,423 +662,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
628
662
  });
629
663
 
630
664
  const to = isDirect ? senderId : groupId;
631
- const parsedLearnCommand = parseLearnCommand(extractedContent.text);
632
- const parsedSessionCommand = parseSessionCommand(extractedContent.text);
633
- const isOwner = isLearningOwner({
665
+ const commandHandled = await handleInboundCommandDispatch({
634
666
  cfg,
635
- config: dingtalkConfig,
636
- senderId,
637
- rawSenderId: data.senderId,
638
- });
639
- if (isDirect && parsedLearnCommand.scope === "whoami") {
640
- await sendBySession(
641
- dingtalkConfig,
642
- sessionWebhook,
643
- formatWhoAmIReply({
644
- senderId,
645
- rawSenderId: data.senderId,
646
- senderStaffId: data.senderStaffId,
647
- isOwner,
648
- }),
649
- { log },
650
- );
651
- return;
652
- }
653
- if (parsedLearnCommand.scope === "whereami") {
654
- await sendBySession(
655
- dingtalkConfig,
656
- sessionWebhook,
657
- formatWhereAmIReply({
658
- conversationId: data.conversationId,
659
- conversationType: isDirect ? "dm" : "group",
660
- peerId: sessionPeer.peerId,
661
- }),
662
- { log },
663
- );
664
- return;
665
- }
666
- if (isDirect && parsedLearnCommand.scope === "owner-status") {
667
- await sendBySession(
668
- dingtalkConfig,
669
- sessionWebhook,
670
- formatOwnerStatusReply({
671
- senderId,
672
- rawSenderId: data.senderId,
673
- isOwner,
674
- }),
675
- { log },
676
- );
677
- return;
678
- }
679
- if (parsedLearnCommand.scope === "help") {
680
- await sendBySession(dingtalkConfig, sessionWebhook, formatLearnCommandHelp(), { log });
681
- return;
682
- }
683
- if (
684
- (parsedLearnCommand.scope === "global" ||
685
- parsedLearnCommand.scope === "session" ||
686
- parsedLearnCommand.scope === "here" ||
687
- parsedLearnCommand.scope === "target" ||
688
- parsedLearnCommand.scope === "targets" ||
689
- parsedLearnCommand.scope === "list" ||
690
- parsedLearnCommand.scope === "disable" ||
691
- parsedLearnCommand.scope === "delete" ||
692
- parsedLearnCommand.scope === "target-set-create" ||
693
- parsedLearnCommand.scope === "target-set-apply" ||
694
- parsedSessionCommand.scope === "session-alias-show" ||
695
- parsedSessionCommand.scope === "session-alias-set" ||
696
- parsedSessionCommand.scope === "session-alias-clear" ||
697
- parsedSessionCommand.scope === "session-alias-bind" ||
698
- parsedSessionCommand.scope === "session-alias-unbind") &&
699
- !isOwner
700
- ) {
701
- await sendBySession(dingtalkConfig, sessionWebhook, formatOwnerOnlyDeniedReply(), { log });
702
- return;
703
- }
704
- if (isOwner) {
705
- if (parsedSessionCommand.scope === "session-alias-show") {
706
- await sendBySession(
707
- dingtalkConfig,
708
- sessionWebhook,
709
- formatSessionAliasReply({
710
- sourceKind: currentSessionSourceKind,
711
- sourceId: currentSessionSourceId,
712
- peerId: sessionPeer.peerId,
713
- aliasSource: peerIdOverride ? "override" : "default",
714
- }),
715
- { log },
716
- );
717
- return;
718
- }
719
- if (parsedSessionCommand.scope === "session-alias-set" && parsedSessionCommand.peerId) {
720
- const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
721
- if (aliasValidationError) {
722
- await sendBySession(
723
- dingtalkConfig,
724
- sessionWebhook,
725
- formatSessionAliasValidationErrorReply(aliasValidationError),
726
- { log },
727
- );
728
- return;
729
- }
730
- setSessionPeerOverride({
731
- storePath: accountStorePath,
732
- accountId,
733
- sourceKind: currentSessionSourceKind,
734
- sourceId: currentSessionSourceId,
735
- peerId: parsedSessionCommand.peerId,
736
- });
737
- await sendBySession(
738
- dingtalkConfig,
739
- sessionWebhook,
740
- formatSessionAliasSetReply({
741
- sourceKind: currentSessionSourceKind,
742
- sourceId: currentSessionSourceId,
743
- peerId: parsedSessionCommand.peerId,
744
- }),
745
- { log },
746
- );
747
- return;
748
- }
749
- if (parsedSessionCommand.scope === "session-alias-clear") {
750
- clearSessionPeerOverride({
751
- storePath: accountStorePath,
752
- accountId,
753
- sourceKind: currentSessionSourceKind,
754
- sourceId: currentSessionSourceId,
755
- });
756
- await sendBySession(
757
- dingtalkConfig,
758
- sessionWebhook,
759
- formatSessionAliasClearedReply({
760
- sourceKind: currentSessionSourceKind,
761
- sourceId: currentSessionSourceId,
762
- }),
763
- { log },
764
- );
765
- return;
766
- }
767
- if (
768
- parsedSessionCommand.scope === "session-alias-bind" &&
769
- parsedSessionCommand.sourceKind &&
770
- parsedSessionCommand.sourceId &&
771
- parsedSessionCommand.peerId
772
- ) {
773
- const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
774
- if (aliasValidationError) {
775
- await sendBySession(
776
- dingtalkConfig,
777
- sessionWebhook,
778
- formatSessionAliasValidationErrorReply(aliasValidationError),
779
- { log },
780
- );
781
- return;
782
- }
783
- setSessionPeerOverride({
784
- storePath: accountStorePath,
785
- accountId,
786
- sourceKind: parsedSessionCommand.sourceKind,
787
- sourceId: parsedSessionCommand.sourceId,
788
- peerId: parsedSessionCommand.peerId,
789
- });
790
- await sendBySession(
791
- dingtalkConfig,
792
- sessionWebhook,
793
- formatSessionAliasBoundReply({
794
- sourceKind: parsedSessionCommand.sourceKind,
795
- sourceId: parsedSessionCommand.sourceId,
796
- peerId: parsedSessionCommand.peerId,
797
- }),
798
- { log },
799
- );
800
- return;
801
- }
802
- if (
803
- parsedSessionCommand.scope === "session-alias-unbind" &&
804
- parsedSessionCommand.sourceKind &&
805
- parsedSessionCommand.sourceId
806
- ) {
807
- const existed = clearSessionPeerOverride({
808
- storePath: accountStorePath,
809
- accountId,
810
- sourceKind: parsedSessionCommand.sourceKind,
811
- sourceId: parsedSessionCommand.sourceId,
812
- });
813
- await sendBySession(
814
- dingtalkConfig,
815
- sessionWebhook,
816
- formatSessionAliasUnboundReply({
817
- sourceKind: parsedSessionCommand.sourceKind,
818
- sourceId: parsedSessionCommand.sourceId,
819
- existed,
820
- }),
821
- { log },
822
- );
823
- return;
824
- }
825
- if (parsedLearnCommand.scope === "global" && parsedLearnCommand.instruction) {
826
- const applied = applyManualGlobalLearningRule({
827
- storePath: accountStorePath,
828
- accountId,
829
- instruction: parsedLearnCommand.instruction,
830
- });
831
- await sendBySession(
832
- dingtalkConfig,
833
- sessionWebhook,
834
- formatLearnAppliedReply({
835
- scope: "global",
836
- instruction: parsedLearnCommand.instruction,
837
- ruleId: applied?.ruleId,
838
- }),
839
- { log },
840
- );
841
- return;
842
- }
843
- if (parsedLearnCommand.scope === "session" && parsedLearnCommand.instruction) {
844
- applyManualSessionLearningNote({
845
- storePath: accountStorePath,
846
- accountId,
847
- targetId: data.conversationId,
848
- instruction: parsedLearnCommand.instruction,
849
- });
850
- await sendBySession(
851
- dingtalkConfig,
852
- sessionWebhook,
853
- formatLearnAppliedReply({
854
- scope: "session",
855
- instruction: parsedLearnCommand.instruction,
856
- }),
857
- { log },
858
- );
859
- return;
860
- }
861
- if (parsedLearnCommand.scope === "here" && parsedLearnCommand.instruction) {
862
- const applied = applyManualTargetLearningRule({
863
- storePath: accountStorePath,
864
- accountId,
865
- targetId: data.conversationId,
866
- instruction: parsedLearnCommand.instruction,
867
- });
868
- await sendBySession(
869
- dingtalkConfig,
870
- sessionWebhook,
871
- formatLearnAppliedReply({
872
- scope: "target",
873
- targetId: data.conversationId,
874
- instruction: parsedLearnCommand.instruction,
875
- ruleId: applied?.ruleId,
876
- }),
877
- { log },
878
- );
879
- return;
880
- }
881
- if (
882
- parsedLearnCommand.scope === "target" &&
883
- parsedLearnCommand.targetId &&
884
- parsedLearnCommand.instruction
885
- ) {
886
- const applied = applyManualTargetLearningRule({
887
- storePath: accountStorePath,
888
- accountId,
889
- targetId: parsedLearnCommand.targetId,
890
- instruction: parsedLearnCommand.instruction,
891
- });
892
- await sendBySession(
893
- dingtalkConfig,
894
- sessionWebhook,
895
- formatLearnAppliedReply({
896
- scope: "target",
897
- targetId: parsedLearnCommand.targetId,
898
- instruction: parsedLearnCommand.instruction,
899
- ruleId: applied?.ruleId,
900
- }),
901
- { log },
902
- );
903
- return;
904
- }
905
- if (
906
- parsedLearnCommand.scope === "targets" &&
907
- parsedLearnCommand.targetIds?.length &&
908
- parsedLearnCommand.instruction
909
- ) {
910
- const applied = applyManualTargetsLearningRule({
911
- storePath: accountStorePath,
912
- accountId,
913
- targetIds: parsedLearnCommand.targetIds,
914
- instruction: parsedLearnCommand.instruction,
915
- });
916
- await sendBySession(
917
- dingtalkConfig,
918
- sessionWebhook,
919
- formatLearnAppliedReply({
920
- scope: "targets",
921
- targetIds: parsedLearnCommand.targetIds,
922
- instruction: parsedLearnCommand.instruction,
923
- ruleId: applied[0]?.ruleId,
924
- }),
925
- { log },
926
- );
927
- return;
928
- }
929
- if (
930
- parsedLearnCommand.scope === "target-set-create" &&
931
- parsedLearnCommand.setName &&
932
- parsedLearnCommand.targetIds?.length
933
- ) {
934
- const saved = createOrUpdateTargetSet({
935
- storePath: accountStorePath,
936
- accountId,
937
- name: parsedLearnCommand.setName,
938
- targetIds: parsedLearnCommand.targetIds,
939
- });
940
- await sendBySession(
941
- dingtalkConfig,
942
- sessionWebhook,
943
- saved
944
- ? formatTargetSetSavedReply({
945
- setName: parsedLearnCommand.setName,
946
- targetIds: parsedLearnCommand.targetIds,
947
- })
948
- : "目标组保存失败,请检查名称和目标列表。",
949
- { log },
950
- );
951
- return;
952
- }
953
- if (
954
- parsedLearnCommand.scope === "target-set-apply" &&
955
- parsedLearnCommand.setName &&
956
- parsedLearnCommand.instruction
957
- ) {
958
- const applied = applyTargetSetLearningRule({
959
- storePath: accountStorePath,
960
- accountId,
961
- name: parsedLearnCommand.setName,
962
- instruction: parsedLearnCommand.instruction,
963
- });
964
- await sendBySession(
965
- dingtalkConfig,
966
- sessionWebhook,
967
- applied.length > 0
968
- ? formatLearnAppliedReply({
969
- scope: "target-set",
970
- setName: parsedLearnCommand.setName,
971
- targetIds: applied.map((item) => item.targetId),
972
- instruction: parsedLearnCommand.instruction,
973
- ruleId: applied[0]?.ruleId,
974
- })
975
- : `未找到目标组 \`${parsedLearnCommand.setName}\`,或该目标组为空。`,
976
- { log },
977
- );
978
- return;
979
- }
980
- if (parsedLearnCommand.scope === "list") {
981
- const rules = listScopedLearningRules({ storePath: accountStorePath, accountId })
982
- .slice(0, 20)
983
- .map((rule) => {
984
- const scope = rule.scope === "target" ? `target(${rule.targetId})` : "global";
985
- const status = rule.enabled ? "enabled" : "disabled";
986
- return `- [${scope}] ${rule.ruleId} (${status}) => ${rule.instruction}`;
987
- });
988
- const targetSets = listLearningTargetSets({ storePath: accountStorePath, accountId })
989
- .slice(0, 10)
990
- .map(
991
- (targetSet) => `- [target-set] ${targetSet.name} => ${targetSet.targetIds.join(", ")}`,
992
- );
993
- await sendBySession(
994
- dingtalkConfig,
995
- sessionWebhook,
996
- formatLearnListReply([...rules, ...targetSets]),
997
- { log },
998
- );
999
- return;
1000
- }
1001
- if (parsedLearnCommand.scope === "disable" && parsedLearnCommand.ruleId) {
1002
- const result = disableManualRule({
1003
- storePath: accountStorePath,
1004
- accountId,
1005
- ruleId: parsedLearnCommand.ruleId,
1006
- });
1007
- await sendBySession(
1008
- dingtalkConfig,
1009
- sessionWebhook,
1010
- formatLearnDisabledReply({
1011
- ruleId: parsedLearnCommand.ruleId,
1012
- existed: result.existed,
1013
- scope: result.scope,
1014
- targetId: result.targetId,
1015
- }),
1016
- { log },
1017
- );
1018
- return;
1019
- }
1020
- if (parsedLearnCommand.scope === "delete" && parsedLearnCommand.ruleId) {
1021
- const result = deleteManualRule({
1022
- storePath: accountStorePath,
1023
- accountId,
1024
- ruleId: parsedLearnCommand.ruleId,
1025
- });
1026
- await sendBySession(
1027
- dingtalkConfig,
1028
- sessionWebhook,
1029
- formatLearnDeletedReply({
1030
- ruleId: parsedLearnCommand.ruleId,
1031
- existed: result.existed,
1032
- scope: result.scope,
1033
- targetId: result.targetId,
1034
- }),
1035
- { log },
1036
- );
1037
- return;
1038
- }
1039
- }
1040
- const manualForcedReply = resolveManualForcedReply({
1041
- storePath: accountStorePath,
1042
667
  accountId,
1043
- targetId: data.conversationId,
1044
- content: extractedContent,
668
+ dingtalkConfig,
669
+ senderId,
670
+ isDirect,
671
+ extractedText: extractedContent.text,
672
+ messageType: extractedContent.messageType,
673
+ data: {
674
+ conversationId: data.conversationId,
675
+ senderId: data.senderId,
676
+ senderStaffId: data.senderStaffId,
677
+ },
678
+ accountStorePath,
679
+ currentSessionSourceKind,
680
+ currentSessionSourceId,
681
+ peerIdOverride,
682
+ sessionPeer,
683
+ sendReply: async (text: string) => {
684
+ await sendBySession(dingtalkConfig, sessionWebhook, text, { log });
685
+ },
686
+ clearSessionPeerOverride,
687
+ setSessionPeerOverride,
1045
688
  });
1046
- if (manualForcedReply) {
1047
- await sendBySession(dingtalkConfig, sessionWebhook, manualForcedReply, { log });
689
+ if (commandHandled) {
1048
690
  return;
1049
691
  }
1050
692
  // 3) Select response mode (card vs markdown).
@@ -1634,13 +1276,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
1634
1276
  // tryFastAbortFromMessage (inside the SDK) kill any in-flight generation immediately,
1635
1277
  // rather than waiting for it to finish before the stop message is processed.
1636
1278
  //
1637
- // In group chats, DingTalk typically strips @BotName from text.content at the
1638
- // protocol level before delivery, but as a defensive measure we also strip leading
1639
- // @mention tokens here (e.g. "@Bot 停止" "停止") to match the SDK's own behavior
1640
- // in tryFastAbortFromMessage (which calls stripMentions for group messages).
1641
- const textForAbortCheck = !isDirect
1642
- ? inboundText.replace(/^(?:@\S+\s+)*/u, "").trim()
1643
- : inboundText;
1279
+ // Strip leading @mention tokens before the abort check so that messages like
1280
+ // "@Agent /stop" are correctly recognised as abort requests in both DM and group
1281
+ // chats. In groups DingTalk usually strips @BotName at the protocol level, but
1282
+ // in DMs with multi-agent routing the @mention prefix survives all the way here.
1283
+ const textForAbortCheck = inboundText.replace(/^(?:@\S+\s+)*/u, "").trim();
1644
1284
  if (isAbortRequestText(textForAbortCheck)) {
1645
1285
  log?.info?.(
1646
1286
  `[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`,
@@ -1856,16 +1496,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
1856
1496
  }
1857
1497
 
1858
1498
  // ---- Create reply strategy (card or markdown) ----
1499
+ const replyMode: ReplyMode = useCardMode && !!currentAICard ? "card" : "markdown";
1500
+ const sessionReasoningLevel = readSessionReasoningLevel({
1501
+ storePath,
1502
+ sessionKey: route.sessionKey,
1503
+ sessionUpdatedAt: previousTimestamp,
1504
+ log,
1505
+ });
1859
1506
  const strategy = createReplyStrategy({
1860
1507
  config: dingtalkConfig,
1861
1508
  card: currentAICard,
1862
- useCardMode: useCardMode && !!currentAICard,
1509
+ useCardMode: replyMode === "card",
1863
1510
  to,
1864
1511
  sessionWebhook,
1865
1512
  senderId,
1866
1513
  isDirect,
1867
1514
  accountId,
1868
1515
  storePath: accountStorePath,
1516
+ sessionKey: route.sessionKey,
1517
+ sessionAgentId: route.agentId,
1518
+ disableBlockStreaming: shouldDisableBlockStreamingForReplyMode({
1519
+ replyMode,
1520
+ sessionKey: route.sessionKey,
1521
+ reasoningLevel: sessionReasoningLevel,
1522
+ log,
1523
+ }),
1869
1524
  groupId,
1870
1525
  log,
1871
1526
  replyQuotedRef,
@@ -1886,10 +1541,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
1886
1541
  }
1887
1542
  try {
1888
1543
  const mediaUrls = extractMediaUrls(payload);
1544
+ const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
1889
1545
  await strategy.deliver({
1890
1546
  text: payload.text,
1891
1547
  mediaUrls,
1892
1548
  kind: (info?.kind as DeliverPayload["kind"]) || "block",
1549
+ isReasoning: richPayload.isReasoning === true,
1893
1550
  });
1894
1551
  } catch (err: unknown) {
1895
1552
  log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);