@soimy/dingtalk 3.5.0 → 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.
package/src/channel.ts CHANGED
@@ -7,6 +7,7 @@ import { readStringParam } from "openclaw/plugin-sdk/param-readers";
7
7
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
8
8
  import { getAccessToken } from "./auth";
9
9
  import { analyzeCardCallback } from "./card-callback-service";
10
+ import { handleCardAction } from "./card/card-action-handler";
10
11
  import {
11
12
  createAICard,
12
13
  streamAICard,
@@ -32,7 +33,7 @@ import {
32
33
  recordExplicitFeedbackLearning,
33
34
  } from "./feedback-learning-service";
34
35
  import { handleDingTalkMessage } from "./inbound-handler";
35
- import { getLogger } from "./logger-context";
36
+ import { getLogger, setCurrentLogger } from "./logger-context";
36
37
  import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
37
38
  import { dingtalkSetupAdapter, dingtalkSetupWizard } from "./onboarding.js";
38
39
  import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
@@ -61,11 +62,13 @@ import type {
61
62
  } from "./types";
62
63
  import { ConnectionState } from "./types";
63
64
  import {
65
+ closePluginDebugLog,
64
66
  cleanupOrphanedTempFiles,
65
67
  createResolve4FallbackLookup,
66
68
  formatDingTalkConnectionErrorLog,
67
69
  formatDingTalkErrorPayloadLog,
68
70
  getCurrentTimestamp,
71
+ resolvePluginDebugLog,
69
72
  } from "./utils";
70
73
 
71
74
  type InstrumentedDWClient = {
@@ -459,14 +462,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
459
462
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
460
463
  agentId: accountId,
461
464
  });
465
+ const effectiveLog = getLogger(accountId) || log;
462
466
  try {
463
467
  const result = await sendMessage(config, to, text, {
464
- log,
468
+ log: effectiveLog,
465
469
  accountId,
466
470
  storePath,
467
471
  conversationId: to,
468
472
  });
469
- getLogger()?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
473
+ effectiveLog?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
470
474
  if (!result.ok) {
471
475
  throw new Error(result.error || "sendText failed");
472
476
  }
@@ -486,7 +490,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
486
490
  };
487
491
  } catch (err: any) {
488
492
  if (err?.response?.data !== undefined) {
489
- log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
493
+ effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
490
494
  }
491
495
  throw new Error(
492
496
  typeof err?.response?.data === "string"
@@ -513,6 +517,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
513
517
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
514
518
  agentId: accountId,
515
519
  });
520
+ const effectiveLog = getLogger(accountId) || log;
516
521
  if (!config.clientId) {
517
522
  throw new Error("DingTalk not configured");
518
523
  }
@@ -520,7 +525,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
520
525
  // Support mediaPath/filePath/mediaUrl aliases for better CLI compatibility.
521
526
  const rawMediaPath = mediaPath || filePath || mediaUrl;
522
527
 
523
- getLogger()?.debug?.(
528
+ effectiveLog?.debug?.(
524
529
  `[DingTalk] sendMedia called: to=${to}, mediaPath=${mediaPath}, filePath=${filePath}, mediaUrl=${mediaUrl}, rawMediaPath=${rawMediaPath}`,
525
530
  );
526
531
 
@@ -538,10 +543,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
538
543
  let preparedMedia;
539
544
  try {
540
545
  try {
541
- preparedMedia = await prepareMediaInput(rawMediaPath, log, config.mediaUrlAllowlist);
546
+ preparedMedia = await prepareMediaInput(rawMediaPath, effectiveLog, config.mediaUrlAllowlist);
542
547
  } catch (err: any) {
543
548
  if (err?.response?.data !== undefined) {
544
- log?.error?.(
549
+ effectiveLog?.error?.(
545
550
  formatDingTalkErrorPayloadLog("outbound.sendMedia.prepare", err.response.data),
546
551
  );
547
552
  }
@@ -558,7 +563,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
558
563
  ? preparedMedia.path
559
564
  : resolveRelativePath(preparedMedia.path);
560
565
 
561
- getLogger()?.debug?.(
566
+ effectiveLog?.debug?.(
562
567
  `[DingTalk] sendMedia resolved path: rawMediaPath=${rawMediaPath}, actualMediaPath=${actualMediaPath}`,
563
568
  );
564
569
 
@@ -570,7 +575,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
570
575
  let result;
571
576
  try {
572
577
  result = await sendProactiveMedia(config, to, actualMediaPath, mediaType, {
573
- log,
578
+ log: effectiveLog,
574
579
  accountId,
575
580
  storePath,
576
581
  conversationId: to,
@@ -578,7 +583,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
578
583
  });
579
584
  } catch (err: any) {
580
585
  if (err?.response?.data !== undefined) {
581
- log?.error?.(
586
+ effectiveLog?.error?.(
582
587
  formatDingTalkErrorPayloadLog("outbound.sendMedia.send", err.response.data),
583
588
  );
584
589
  }
@@ -586,7 +591,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
586
591
  cause: err,
587
592
  });
588
593
  }
589
- getLogger()?.debug?.(
594
+ effectiveLog?.debug?.(
590
595
  `[DingTalk] sendMedia: ${mediaType} file=${actualMediaPath} result: ${JSON.stringify(result)}`,
591
596
  );
592
597
 
@@ -608,7 +613,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
608
613
  );
609
614
  } catch (err: any) {
610
615
  if (err?.response?.data !== undefined) {
611
- log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
616
+ effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
612
617
  }
613
618
  throw new Error(
614
619
  typeof err?.response?.data === "string"
@@ -638,29 +643,37 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
638
643
  accountStorePath = undefined;
639
644
  }
640
645
 
641
- ctx.log?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
646
+ const pluginLog = resolvePluginDebugLog({
647
+ accountId: account.accountId,
648
+ storePath: accountStorePath,
649
+ debug: config.debug,
650
+ baseLog: ctx.log,
651
+ });
652
+ setCurrentLogger(pluginLog, account.accountId);
653
+
654
+ pluginLog?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
642
655
 
643
656
  // Preload known peer IDs from sessions so outbound delivery (e.g. cron
644
657
  // jobs that fire immediately after startup) can resolve the original
645
658
  // case-sensitive conversationId before any inbound message has arrived.
646
659
  preloadPeerIdsFromSessions();
647
- ctx.log?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
660
+ pluginLog?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
648
661
 
649
- cleanupOrphanedTempFiles(ctx.log);
662
+ cleanupOrphanedTempFiles(pluginLog);
650
663
  try {
651
664
  const recovered = await recoverPendingCardsForAccount(
652
665
  config,
653
666
  account.accountId,
654
667
  accountStorePath,
655
- ctx.log,
668
+ pluginLog,
656
669
  );
657
670
  if (recovered > 0) {
658
- ctx.log?.info?.(
671
+ pluginLog?.info?.(
659
672
  `[${account.accountId}] Recovered and finalized ${recovered} unfinished card(s) from previous runtime`,
660
673
  );
661
674
  }
662
675
  } catch (err: any) {
663
- ctx.log?.warn?.(
676
+ pluginLog?.warn?.(
664
677
  `[${account.accountId}] Failed to recover unfinished cards: ${err.message}`,
665
678
  );
666
679
  }
@@ -682,7 +695,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
682
695
  });
683
696
  (c as any).sslopts = {
684
697
  ...(c as any).sslopts,
685
- lookup: createResolve4FallbackLookup(ctx.log, account.accountId),
698
+ lookup: createResolve4FallbackLookup(pluginLog, account.accountId),
686
699
  };
687
700
 
688
701
  instrumentConnectionStages(c);
@@ -701,7 +714,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
701
714
  c.socketCallBackResponse(messageId, { success: true });
702
715
  stats.acked += 1;
703
716
  } catch (ackError: any) {
704
- ctx.log?.warn?.(
717
+ pluginLog?.warn?.(
705
718
  `[${account.accountId}] Failed to acknowledge callback ${messageId}: ${ackError.message}`,
706
719
  );
707
720
  }
@@ -722,38 +735,38 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
722
735
  accountId: account.accountId,
723
736
  data,
724
737
  sessionWebhook: data.sessionWebhook,
725
- log: ctx.log,
738
+ log: pluginLog,
726
739
  dingtalkConfig: config,
727
740
  });
728
741
  stats.processed += 1;
729
742
  if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
730
- logInboundCounters(ctx.log, account.accountId, "periodic");
743
+ logInboundCounters(pluginLog, account.accountId, "periodic");
731
744
  }
732
745
  return;
733
746
  }
734
747
 
735
748
  if (isMessageProcessed(dedupKey)) {
736
- ctx.log?.debug?.(`[${account.accountId}] Skipping duplicate message: ${dedupKey}`);
749
+ pluginLog?.debug?.(`[${account.accountId}] Skipping duplicate message: ${dedupKey}`);
737
750
  stats.dedupSkipped += 1;
738
751
  acknowledge();
739
- logInboundCounters(ctx.log, account.accountId, "dedup-skipped");
752
+ logInboundCounters(pluginLog, account.accountId, "dedup-skipped");
740
753
  return;
741
754
  }
742
755
 
743
756
  const inflightSince = processingDedupKeys.get(dedupKey);
744
757
  if (inflightSince !== undefined) {
745
758
  if (Date.now() - inflightSince > INFLIGHT_TTL_MS) {
746
- ctx.log?.warn?.(
759
+ pluginLog?.warn?.(
747
760
  `[${account.accountId}] Releasing stale in-flight lock for ${dedupKey} (held ${Date.now() - inflightSince}ms > TTL ${INFLIGHT_TTL_MS}ms)`,
748
761
  );
749
762
  processingDedupKeys.delete(dedupKey);
750
763
  } else {
751
- ctx.log?.debug?.(
764
+ pluginLog?.debug?.(
752
765
  `[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`,
753
766
  );
754
767
  stats.inflightSkipped += 1;
755
768
  acknowledge();
756
- logInboundCounters(ctx.log, account.accountId, "inflight-skipped");
769
+ logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
757
770
  return;
758
771
  }
759
772
  }
@@ -766,21 +779,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
766
779
  accountId: account.accountId,
767
780
  data,
768
781
  sessionWebhook: data.sessionWebhook,
769
- log: ctx.log,
782
+ log: pluginLog,
770
783
  dingtalkConfig: config,
771
784
  });
772
785
  stats.processed += 1;
773
786
  markMessageProcessed(dedupKey);
774
787
  if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
775
- logInboundCounters(ctx.log, account.accountId, "periodic");
788
+ logInboundCounters(pluginLog, account.accountId, "periodic");
776
789
  }
777
790
  } finally {
778
791
  processingDedupKeys.delete(dedupKey);
779
792
  }
780
793
  } catch (error: any) {
781
794
  stats.failed += 1;
782
- logInboundCounters(ctx.log, account.accountId, "failed");
783
- ctx.log?.error?.(`[${account.accountId}] Error processing message: ${error.message}`);
795
+ logInboundCounters(pluginLog, account.accountId, "failed");
796
+ pluginLog?.error?.(`[${account.accountId}] Error processing message: ${error.message}`);
784
797
  }
785
798
  });
786
799
 
@@ -793,7 +806,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
793
806
  try {
794
807
  c.socketCallBackResponse(messageId, { success: true });
795
808
  } catch (ackError: any) {
796
- ctx.log?.warn?.(
809
+ pluginLog?.warn?.(
797
810
  `[${account.accountId}] Failed to acknowledge card callback ${messageId}: ${ackError.message}`,
798
811
  );
799
812
  }
@@ -802,7 +815,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
802
815
  try {
803
816
  const payload = JSON.parse(res.data);
804
817
  const analysis = analyzeCardCallback(payload);
805
- ctx.log?.info?.(
818
+ pluginLog?.info?.(
806
819
  `[${account.accountId}] [DingTalk][CardCallback] action=${analysis.summary} raw=${JSON.stringify(payload)}`,
807
820
  );
808
821
 
@@ -825,20 +838,32 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
825
838
  analysis.feedbackAckText,
826
839
  {
827
840
  accountId: account.accountId,
828
- log: ctx.log,
841
+ log: pluginLog,
829
842
  },
830
843
  );
831
- ctx.log?.info?.(
844
+ pluginLog?.info?.(
832
845
  `[${account.accountId}] [DingTalk][CardCallback] feedback ack sent to ${analysis.feedbackTarget}`,
833
846
  );
834
847
  } catch (sendErr: any) {
835
- ctx.log?.warn?.(
848
+ pluginLog?.warn?.(
836
849
  `[${account.accountId}] [DingTalk][CardCallback] Failed to send feedback ack: ${sendErr?.message || String(sendErr)}`,
837
850
  );
838
851
  }
839
852
  }
853
+ const actionResult = await handleCardAction({
854
+ analysis,
855
+ cfg,
856
+ accountId: account.accountId,
857
+ config,
858
+ log: pluginLog,
859
+ });
860
+ if (!actionResult.handled && analysis.actionId && analysis.actionId !== "feedback_up" && analysis.actionId !== "feedback_down") {
861
+ pluginLog?.debug?.(
862
+ `[${account.accountId}] [DingTalk][CardCallback] Unhandled actionId=${analysis.actionId}`,
863
+ );
864
+ }
840
865
  } catch (error: any) {
841
- ctx.log?.error?.(
866
+ pluginLog?.error?.(
842
867
  `[${account.accountId}] [DingTalk][CardCallback] Failed to parse callback: ${error.message}`,
843
868
  );
844
869
  } finally {
@@ -864,15 +889,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
864
889
  return;
865
890
  }
866
891
  stopped = true;
867
- ctx.log?.info?.(`[${account.accountId}] Stopping DingTalk Stream client...`);
892
+ pluginLog?.info?.(`[${account.accountId}] Stopping DingTalk Stream client...`);
868
893
  void finalizeActiveCardsForAccount(
869
894
  config,
870
895
  account.accountId,
871
896
  "⚠️ 服务正在重启,当前回复已中断。请重新发送你的问题。",
872
897
  accountStorePath,
873
- ctx.log,
898
+ pluginLog,
874
899
  ).catch((err: any) => {
875
- ctx.log?.debug?.(
900
+ pluginLog?.debug?.(
876
901
  `[${account.accountId}] Failed to finalize active cards during stop: ${err.message}`,
877
902
  );
878
903
  });
@@ -882,7 +907,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
882
907
  try {
883
908
  client.disconnect();
884
909
  } catch (err: any) {
885
- ctx.log?.warn?.(`[${account.accountId}] Error during disconnect: ${err.message}`);
910
+ pluginLog?.warn?.(`[${account.accountId}] Error during disconnect: ${err.message}`);
886
911
  }
887
912
  nativeStopResolve?.();
888
913
  }
@@ -893,12 +918,16 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
893
918
  lastStopAt: getCurrentTimestamp(),
894
919
  });
895
920
 
896
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client stopped`);
921
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client stopped`);
922
+ closePluginDebugLog({
923
+ accountId: account.accountId,
924
+ storePath: accountStorePath,
925
+ });
897
926
  };
898
927
 
899
928
  if (abortSignal) {
900
929
  if (abortSignal.aborted) {
901
- ctx.log?.warn?.(
930
+ pluginLog?.warn?.(
902
931
  `[${account.accountId}] Abort signal already active, skipping connection`,
903
932
  );
904
933
 
@@ -916,7 +945,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
916
945
  if (stopped) {
917
946
  return;
918
947
  }
919
- ctx.log?.info?.(
948
+ pluginLog?.info?.(
920
949
  `[${account.accountId}] Abort signal received, stopping DingTalk Stream client...`,
921
950
  );
922
951
  stopClient();
@@ -933,11 +962,11 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
933
962
  lastStartAt: getCurrentTimestamp(),
934
963
  lastError: null,
935
964
  });
936
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
965
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
937
966
  await nativeStopPromise;
938
967
  }
939
968
  } catch (err: any) {
940
- ctx.log?.error?.(
969
+ pluginLog?.error?.(
941
970
  formatDingTalkConnectionErrorLog(
942
971
  // Use connect.open as base scope; instrumentation can override to connect.websocket
943
972
  "connect.open",
@@ -971,7 +1000,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
971
1000
  if (stopped) {
972
1001
  return;
973
1002
  }
974
- ctx.log?.debug?.(
1003
+ pluginLog?.debug?.(
975
1004
  `[${account.accountId}] Connection state changed to: ${state}${error ? ` (${error})` : ""}`,
976
1005
  );
977
1006
  if (state === ConnectionState.CONNECTED) {
@@ -994,7 +1023,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
994
1023
  }
995
1024
  }
996
1025
  if (cleared > 0) {
997
- ctx.log?.info?.(
1026
+ pluginLog?.info?.(
998
1027
  `[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`,
999
1028
  );
1000
1029
  }
@@ -1007,7 +1036,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1007
1036
  },
1008
1037
  };
1009
1038
 
1010
- ctx.log?.debug?.(
1039
+ pluginLog?.debug?.(
1011
1040
  `[${account.accountId}] Connection config: maxAttempts=${connectionConfig.maxAttempts}, ` +
1012
1041
  `initialDelay=${connectionConfig.initialDelay}ms, maxDelay=${connectionConfig.maxDelay}ms, ` +
1013
1042
  `jitter=${connectionConfig.jitter}`,
@@ -1017,7 +1046,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1017
1046
  client,
1018
1047
  account.accountId,
1019
1048
  connectionConfig,
1020
- ctx.log,
1049
+ pluginLog,
1021
1050
  createStreamClient,
1022
1051
  );
1023
1052
 
@@ -1031,17 +1060,17 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1031
1060
  lastStartAt: getCurrentTimestamp(),
1032
1061
  lastError: null,
1033
1062
  });
1034
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
1063
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
1035
1064
 
1036
1065
  await connectionManager.waitForStop();
1037
1066
  } else {
1038
- ctx.log?.info?.(
1067
+ pluginLog?.info?.(
1039
1068
  `[${account.accountId}] DingTalk Stream client connect() completed but channel is ` +
1040
1069
  `not running (stopped=${stopped}, connected=${connectionManager.isConnected()})`,
1041
1070
  );
1042
1071
  }
1043
1072
  } catch (err: any) {
1044
- ctx.log?.error?.(
1073
+ pluginLog?.error?.(
1045
1074
  formatDingTalkConnectionErrorLog(
1046
1075
  // Use connect.open as base scope; instrumentation can override to connect.websocket
1047
1076
  "connect.open",
@@ -0,0 +1,78 @@
1
+ import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
+ import { getDingTalkRuntime } from "../runtime";
4
+ import type { Logger } from "../types";
5
+
6
+ /**
7
+ * Dispatch a native targeted `/stop` command through the OpenClaw SDK,
8
+ * replacing the previous self-built Gateway WebSocket `chat.abort` approach.
9
+ *
10
+ * Uses the same `resolveNativeCommandSessionTargets` + `CommandSource: "native"`
11
+ * model as Telegram / Discord / Slack slash commands, producing:
12
+ * - A dedicated command SessionKey (`agent:<agentId>:dingtalk:card-stop:<userId>`)
13
+ * - A CommandTargetSessionKey pointing at the real conversation session
14
+ *
15
+ * Inside the SDK, `dispatch-from-config` → `tryFastAbortFromMessage` picks up
16
+ * the `/stop` body, resolves the target session via `CommandTargetSessionKey`,
17
+ * and executes `abortEmbeddedPiRun` + `clearSessionQueues`.
18
+ *
19
+ * Accesses SDK functions via `getDingTalkRuntime().channel.reply` — the same
20
+ * pattern used by `inbound-handler.ts` — while reusing the upstream native
21
+ * command session-target helper exported by the plugin SDK.
22
+ */
23
+ export async function dispatchDingTalkCardStopCommand(params: {
24
+ cfg: OpenClawConfig;
25
+ accountId: string;
26
+ agentId: string;
27
+ targetSessionKey: string;
28
+ clickerUserId: string;
29
+ log?: Logger;
30
+ }): Promise<{ ok: boolean }> {
31
+ const rt = getDingTalkRuntime();
32
+
33
+ const { sessionKey: commandSessionKey, commandTargetSessionKey } =
34
+ resolveNativeCommandSessionTargets({
35
+ agentId: params.agentId,
36
+ sessionPrefix: "dingtalk:card-stop",
37
+ userId: params.clickerUserId,
38
+ targetSessionKey: params.targetSessionKey,
39
+ });
40
+
41
+ const ctx = rt.channel.reply.finalizeInboundContext({
42
+ Body: "/stop",
43
+ RawBody: "/stop",
44
+ CommandBody: "/stop",
45
+ SessionKey: commandSessionKey,
46
+ CommandTargetSessionKey: commandTargetSessionKey,
47
+ CommandSource: "native" as const,
48
+ CommandAuthorized: true,
49
+ AccountId: params.accountId,
50
+ Provider: "dingtalk",
51
+ Surface: "dingtalk",
52
+ // "direct" because the synthetic /stop body contains no @mentions to strip.
53
+ // The actual chat type of the target session is irrelevant for abort routing.
54
+ ChatType: "direct",
55
+ From: `dingtalk:card-stop:${params.clickerUserId}`,
56
+ To: `card-stop:${params.clickerUserId}`,
57
+ SenderId: params.clickerUserId,
58
+ OriginatingChannel: "dingtalk",
59
+ });
60
+
61
+ // DispatchInboundResult = { queuedFinal, counts } — the return value does
62
+ // not expose whether tryFastAbortFromMessage took the fast-abort path.
63
+ // Treat successful dispatch as best-effort abort, consistent with the
64
+ // previous gateway chat.abort approach.
65
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
66
+ ctx,
67
+ cfg: params.cfg,
68
+ dispatcherOptions: {
69
+ responsePrefix: "",
70
+ deliver: async () => {
71
+ // SDK abort confirmation text is swallowed here; the card
72
+ // finalize path handles stopped content independently.
73
+ },
74
+ },
75
+ });
76
+
77
+ return { ok: true };
78
+ }