@soimy/dingtalk 3.5.1 → 3.5.3

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.
Files changed (36) hide show
  1. package/README.md +13 -24
  2. package/openclaw.plugin.json +695 -0
  3. package/package.json +12 -7
  4. package/src/ack-reaction-service.ts +1 -1
  5. package/src/auth.ts +1 -1
  6. package/src/card/card-action-handler.ts +1 -1
  7. package/src/card/card-stop-handler.ts +1 -1
  8. package/src/card/card-streaming-mode.ts +30 -0
  9. package/src/card/reasoning-answer-split.ts +162 -0
  10. package/src/card/reasoning-block-assembler.ts +157 -0
  11. package/src/card-callback-service.ts +1 -1
  12. package/src/card-draft-controller.ts +117 -6
  13. package/src/card-service.ts +112 -1
  14. package/src/channel.ts +131 -96
  15. package/src/command/card-stop-command.ts +4 -22
  16. package/src/command/inbound-command-dispatch-service.ts +464 -0
  17. package/src/config-schema.ts +62 -38
  18. package/src/config.ts +25 -3
  19. package/src/docs-service.ts +5 -5
  20. package/src/http-client.ts +20 -0
  21. package/src/inbound-handler.ts +475 -501
  22. package/src/logger-context.ts +16 -2
  23. package/src/media-utils.ts +166 -10
  24. package/src/message-utils.ts +33 -5
  25. package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
  26. package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
  27. package/src/onboarding.ts +29 -0
  28. package/src/plugin-sdk-channel-actions-augment.ts +11 -0
  29. package/src/reply-strategy-card.ts +294 -28
  30. package/src/reply-strategy-markdown.ts +124 -19
  31. package/src/reply-strategy.ts +22 -2
  32. package/src/send-service.ts +178 -7
  33. package/src/targeting/agent-routing.ts +55 -32
  34. package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
  35. package/src/types.ts +60 -4
  36. package/src/utils.ts +190 -0
package/src/channel.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
3
+ import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
3
4
  import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
4
5
  import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core";
5
- import { jsonResult } from "openclaw/plugin-sdk/telegram-core";
6
6
  import { readStringParam } from "openclaw/plugin-sdk/param-readers";
7
7
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
8
8
  import { getAccessToken } from "./auth";
@@ -33,7 +33,7 @@ import {
33
33
  recordExplicitFeedbackLearning,
34
34
  } from "./feedback-learning-service";
35
35
  import { handleDingTalkMessage } from "./inbound-handler";
36
- import { getLogger } from "./logger-context";
36
+ import { getLogger, setCurrentLogger } from "./logger-context";
37
37
  import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
38
38
  import { dingtalkSetupAdapter, dingtalkSetupWizard } from "./onboarding.js";
39
39
  import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
@@ -62,11 +62,14 @@ import type {
62
62
  } from "./types";
63
63
  import { ConnectionState } from "./types";
64
64
  import {
65
+ closePluginDebugLog,
65
66
  cleanupOrphanedTempFiles,
66
67
  createResolve4FallbackLookup,
67
68
  formatDingTalkConnectionErrorLog,
68
69
  formatDingTalkErrorPayloadLog,
69
70
  getCurrentTimestamp,
71
+ parseBooleanLike,
72
+ resolvePluginDebugLog,
70
73
  } from "./utils";
71
74
 
72
75
  type InstrumentedDWClient = {
@@ -186,29 +189,15 @@ function logInboundCounters(log: any, accountId: string, reason: string): void {
186
189
  }
187
190
 
188
191
  function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
189
- const value = params[key];
190
- if (typeof value === "boolean") {
191
- return value;
192
- }
193
- if (typeof value === "number") {
194
- if (value === 1) {
195
- return true;
196
- }
197
- if (value === 0) {
198
- return false;
199
- }
200
- return undefined;
201
- }
202
- if (typeof value === "string") {
203
- const normalized = value.trim().toLowerCase();
204
- if (["1", "true", "yes", "y", "on"].includes(normalized)) {
205
- return true;
206
- }
207
- if (["0", "false", "no", "n", "off"].includes(normalized)) {
208
- return false;
209
- }
192
+ return parseBooleanLike(params[key]);
193
+ }
194
+
195
+ function readSharedAudioAsVoiceParam(params: Record<string, unknown>): boolean {
196
+ const sharedValue = readBooleanLikeParam(params, "audioAsVoice");
197
+ if (sharedValue !== undefined) {
198
+ return sharedValue;
210
199
  }
211
- return undefined;
200
+ return readBooleanLikeParam(params, "asVoice") === true;
212
201
  }
213
202
 
214
203
  function describeDingTalkMessageTool(cfg: OpenClawConfig): {
@@ -259,7 +248,7 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
259
248
  message = caption;
260
249
  }
261
250
 
262
- const asVoice = readBooleanLikeParam(params, "asVoice") === true;
251
+ const asVoice = readSharedAudioAsVoiceParam(params);
263
252
  const requestedMediaType = readStringParam(params, "mediaType");
264
253
 
265
254
  const target = resolveOriginalPeerId(stripTargetPrefix(to).targetId);
@@ -460,14 +449,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
460
449
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
461
450
  agentId: accountId,
462
451
  });
452
+ const effectiveLog = getLogger(accountId) || log;
463
453
  try {
464
454
  const result = await sendMessage(config, to, text, {
465
- log,
455
+ log: effectiveLog,
466
456
  accountId,
467
457
  storePath,
468
458
  conversationId: to,
469
459
  });
470
- getLogger()?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
460
+ effectiveLog?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
471
461
  if (!result.ok) {
472
462
  throw new Error(result.error || "sendText failed");
473
463
  }
@@ -487,7 +477,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
487
477
  };
488
478
  } catch (err: any) {
489
479
  if (err?.response?.data !== undefined) {
490
- log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
480
+ effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
491
481
  }
492
482
  throw new Error(
493
483
  typeof err?.response?.data === "string"
@@ -504,6 +494,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
504
494
  filePath,
505
495
  mediaUrl,
506
496
  mediaType: providedMediaType,
497
+ audioAsVoice,
507
498
  asVoice,
508
499
  accountId,
509
500
  mediaLocalRoots,
@@ -514,6 +505,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
514
505
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
515
506
  agentId: accountId,
516
507
  });
508
+ const effectiveLog = getLogger(accountId) || log;
517
509
  if (!config.clientId) {
518
510
  throw new Error("DingTalk not configured");
519
511
  }
@@ -521,7 +513,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
521
513
  // Support mediaPath/filePath/mediaUrl aliases for better CLI compatibility.
522
514
  const rawMediaPath = mediaPath || filePath || mediaUrl;
523
515
 
524
- getLogger()?.debug?.(
516
+ effectiveLog?.debug?.(
525
517
  `[DingTalk] sendMedia called: to=${to}, mediaPath=${mediaPath}, filePath=${filePath}, mediaUrl=${mediaUrl}, rawMediaPath=${rawMediaPath}`,
526
518
  );
527
519
 
@@ -539,10 +531,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
539
531
  let preparedMedia;
540
532
  try {
541
533
  try {
542
- preparedMedia = await prepareMediaInput(rawMediaPath, log, config.mediaUrlAllowlist);
534
+ preparedMedia = await prepareMediaInput(rawMediaPath, effectiveLog, config.mediaUrlAllowlist);
543
535
  } catch (err: any) {
544
536
  if (err?.response?.data !== undefined) {
545
- log?.error?.(
537
+ effectiveLog?.error?.(
546
538
  formatDingTalkErrorPayloadLog("outbound.sendMedia.prepare", err.response.data),
547
539
  );
548
540
  }
@@ -559,19 +551,19 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
559
551
  ? preparedMedia.path
560
552
  : resolveRelativePath(preparedMedia.path);
561
553
 
562
- getLogger()?.debug?.(
554
+ effectiveLog?.debug?.(
563
555
  `[DingTalk] sendMedia resolved path: rawMediaPath=${rawMediaPath}, actualMediaPath=${actualMediaPath}`,
564
556
  );
565
557
 
566
558
  const mediaType = resolveOutboundMediaType({
567
559
  mediaType: typeof providedMediaType === "string" ? providedMediaType : undefined,
568
560
  mediaPath: actualMediaPath,
569
- asVoice: asVoice === true,
561
+ asVoice: readSharedAudioAsVoiceParam({ audioAsVoice, asVoice }),
570
562
  });
571
563
  let result;
572
564
  try {
573
565
  result = await sendProactiveMedia(config, to, actualMediaPath, mediaType, {
574
- log,
566
+ log: effectiveLog,
575
567
  accountId,
576
568
  storePath,
577
569
  conversationId: to,
@@ -579,7 +571,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
579
571
  });
580
572
  } catch (err: any) {
581
573
  if (err?.response?.data !== undefined) {
582
- log?.error?.(
574
+ effectiveLog?.error?.(
583
575
  formatDingTalkErrorPayloadLog("outbound.sendMedia.send", err.response.data),
584
576
  );
585
577
  }
@@ -587,7 +579,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
587
579
  cause: err,
588
580
  });
589
581
  }
590
- getLogger()?.debug?.(
582
+ effectiveLog?.debug?.(
591
583
  `[DingTalk] sendMedia: ${mediaType} file=${actualMediaPath} result: ${JSON.stringify(result)}`,
592
584
  );
593
585
 
@@ -609,7 +601,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
609
601
  );
610
602
  } catch (err: any) {
611
603
  if (err?.response?.data !== undefined) {
612
- log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
604
+ effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
613
605
  }
614
606
  throw new Error(
615
607
  typeof err?.response?.data === "string"
@@ -639,34 +631,48 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
639
631
  accountStorePath = undefined;
640
632
  }
641
633
 
642
- ctx.log?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
634
+ const pluginLog = resolvePluginDebugLog({
635
+ accountId: account.accountId,
636
+ storePath: accountStorePath,
637
+ debug: config.debug,
638
+ baseLog: ctx.log,
639
+ });
640
+ setCurrentLogger(pluginLog, account.accountId);
641
+
642
+ pluginLog?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
643
643
 
644
644
  // Preload known peer IDs from sessions so outbound delivery (e.g. cron
645
645
  // jobs that fire immediately after startup) can resolve the original
646
646
  // case-sensitive conversationId before any inbound message has arrived.
647
647
  preloadPeerIdsFromSessions();
648
- ctx.log?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
648
+ pluginLog?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
649
649
 
650
- cleanupOrphanedTempFiles(ctx.log);
650
+ cleanupOrphanedTempFiles(pluginLog);
651
651
  try {
652
652
  const recovered = await recoverPendingCardsForAccount(
653
653
  config,
654
654
  account.accountId,
655
655
  accountStorePath,
656
- ctx.log,
656
+ pluginLog,
657
657
  );
658
658
  if (recovered > 0) {
659
- ctx.log?.info?.(
659
+ pluginLog?.info?.(
660
660
  `[${account.accountId}] Recovered and finalized ${recovered} unfinished card(s) from previous runtime`,
661
661
  );
662
662
  }
663
663
  } catch (err: any) {
664
- ctx.log?.warn?.(
664
+ pluginLog?.warn?.(
665
665
  `[${account.accountId}] Failed to recover unfinished cards: ${err.message}`,
666
666
  );
667
667
  }
668
668
 
669
669
  const useConnectionManager = config.useConnectionManager ?? true;
670
+ const applyStatusPatch = (patch: Record<string, unknown>) => {
671
+ ctx.setStatus({
672
+ ...ctx.getStatus(),
673
+ ...patch,
674
+ });
675
+ };
670
676
 
671
677
  // Factory that creates a fresh DWClient with the TOPIC_ROBOT callback
672
678
  // already registered. Each client captures its own reference for
@@ -683,7 +689,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
683
689
  });
684
690
  (c as any).sslopts = {
685
691
  ...(c as any).sslopts,
686
- lookup: createResolve4FallbackLookup(ctx.log, account.accountId),
692
+ lookup: createResolve4FallbackLookup(pluginLog, account.accountId),
687
693
  };
688
694
 
689
695
  instrumentConnectionStages(c);
@@ -702,13 +708,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
702
708
  c.socketCallBackResponse(messageId, { success: true });
703
709
  stats.acked += 1;
704
710
  } catch (ackError: any) {
705
- ctx.log?.warn?.(
711
+ pluginLog?.warn?.(
706
712
  `[${account.accountId}] Failed to acknowledge callback ${messageId}: ${ackError.message}`,
707
713
  );
708
714
  }
709
715
  };
710
716
  try {
711
717
  const data = JSON.parse(res.data) as DingTalkInboundMessage;
718
+ // Record the latest inbound callback arrival for status/UI projection.
719
+ // This intentionally tracks "message reached the plugin callback" rather
720
+ // than "message passed dedup and completed processing".
721
+ applyStatusPatch({
722
+ connected: true,
723
+ lastInboundAt: getCurrentTimestamp(),
724
+ lastEventAt: getCurrentTimestamp(),
725
+ });
712
726
 
713
727
  const robotKey = resolveRobotCode(config) || account.accountId;
714
728
  const msgId = data.msgId || messageId;
@@ -723,38 +737,38 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
723
737
  accountId: account.accountId,
724
738
  data,
725
739
  sessionWebhook: data.sessionWebhook,
726
- log: ctx.log,
740
+ log: pluginLog,
727
741
  dingtalkConfig: config,
728
742
  });
729
743
  stats.processed += 1;
730
744
  if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
731
- logInboundCounters(ctx.log, account.accountId, "periodic");
745
+ logInboundCounters(pluginLog, account.accountId, "periodic");
732
746
  }
733
747
  return;
734
748
  }
735
749
 
736
750
  if (isMessageProcessed(dedupKey)) {
737
- ctx.log?.debug?.(`[${account.accountId}] Skipping duplicate message: ${dedupKey}`);
751
+ pluginLog?.debug?.(`[${account.accountId}] Skipping duplicate message: ${dedupKey}`);
738
752
  stats.dedupSkipped += 1;
739
753
  acknowledge();
740
- logInboundCounters(ctx.log, account.accountId, "dedup-skipped");
754
+ logInboundCounters(pluginLog, account.accountId, "dedup-skipped");
741
755
  return;
742
756
  }
743
757
 
744
758
  const inflightSince = processingDedupKeys.get(dedupKey);
745
759
  if (inflightSince !== undefined) {
746
760
  if (Date.now() - inflightSince > INFLIGHT_TTL_MS) {
747
- ctx.log?.warn?.(
761
+ pluginLog?.warn?.(
748
762
  `[${account.accountId}] Releasing stale in-flight lock for ${dedupKey} (held ${Date.now() - inflightSince}ms > TTL ${INFLIGHT_TTL_MS}ms)`,
749
763
  );
750
764
  processingDedupKeys.delete(dedupKey);
751
765
  } else {
752
- ctx.log?.debug?.(
766
+ pluginLog?.debug?.(
753
767
  `[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`,
754
768
  );
755
769
  stats.inflightSkipped += 1;
756
770
  acknowledge();
757
- logInboundCounters(ctx.log, account.accountId, "inflight-skipped");
771
+ logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
758
772
  return;
759
773
  }
760
774
  }
@@ -767,21 +781,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
767
781
  accountId: account.accountId,
768
782
  data,
769
783
  sessionWebhook: data.sessionWebhook,
770
- log: ctx.log,
784
+ log: pluginLog,
771
785
  dingtalkConfig: config,
772
786
  });
773
787
  stats.processed += 1;
774
788
  markMessageProcessed(dedupKey);
775
789
  if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
776
- logInboundCounters(ctx.log, account.accountId, "periodic");
790
+ logInboundCounters(pluginLog, account.accountId, "periodic");
777
791
  }
778
792
  } finally {
779
793
  processingDedupKeys.delete(dedupKey);
780
794
  }
781
795
  } catch (error: any) {
782
796
  stats.failed += 1;
783
- logInboundCounters(ctx.log, account.accountId, "failed");
784
- ctx.log?.error?.(`[${account.accountId}] Error processing message: ${error.message}`);
797
+ logInboundCounters(pluginLog, account.accountId, "failed");
798
+ pluginLog?.error?.(`[${account.accountId}] Error processing message: ${error.message}`);
785
799
  }
786
800
  });
787
801
 
@@ -794,7 +808,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
794
808
  try {
795
809
  c.socketCallBackResponse(messageId, { success: true });
796
810
  } catch (ackError: any) {
797
- ctx.log?.warn?.(
811
+ pluginLog?.warn?.(
798
812
  `[${account.accountId}] Failed to acknowledge card callback ${messageId}: ${ackError.message}`,
799
813
  );
800
814
  }
@@ -803,7 +817,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
803
817
  try {
804
818
  const payload = JSON.parse(res.data);
805
819
  const analysis = analyzeCardCallback(payload);
806
- ctx.log?.info?.(
820
+ pluginLog?.info?.(
807
821
  `[${account.accountId}] [DingTalk][CardCallback] action=${analysis.summary} raw=${JSON.stringify(payload)}`,
808
822
  );
809
823
 
@@ -826,14 +840,14 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
826
840
  analysis.feedbackAckText,
827
841
  {
828
842
  accountId: account.accountId,
829
- log: ctx.log,
843
+ log: pluginLog,
830
844
  },
831
845
  );
832
- ctx.log?.info?.(
846
+ pluginLog?.info?.(
833
847
  `[${account.accountId}] [DingTalk][CardCallback] feedback ack sent to ${analysis.feedbackTarget}`,
834
848
  );
835
849
  } catch (sendErr: any) {
836
- ctx.log?.warn?.(
850
+ pluginLog?.warn?.(
837
851
  `[${account.accountId}] [DingTalk][CardCallback] Failed to send feedback ack: ${sendErr?.message || String(sendErr)}`,
838
852
  );
839
853
  }
@@ -843,15 +857,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
843
857
  cfg,
844
858
  accountId: account.accountId,
845
859
  config,
846
- log: ctx.log,
860
+ log: pluginLog,
847
861
  });
848
862
  if (!actionResult.handled && analysis.actionId && analysis.actionId !== "feedback_up" && analysis.actionId !== "feedback_down") {
849
- ctx.log?.debug?.(
863
+ pluginLog?.debug?.(
850
864
  `[${account.accountId}] [DingTalk][CardCallback] Unhandled actionId=${analysis.actionId}`,
851
865
  );
852
866
  }
853
867
  } catch (error: any) {
854
- ctx.log?.error?.(
868
+ pluginLog?.error?.(
855
869
  `[${account.accountId}] [DingTalk][CardCallback] Failed to parse callback: ${error.message}`,
856
870
  );
857
871
  } finally {
@@ -877,15 +891,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
877
891
  return;
878
892
  }
879
893
  stopped = true;
880
- ctx.log?.info?.(`[${account.accountId}] Stopping DingTalk Stream client...`);
894
+ pluginLog?.info?.(`[${account.accountId}] Stopping DingTalk Stream client...`);
881
895
  void finalizeActiveCardsForAccount(
882
896
  config,
883
897
  account.accountId,
884
898
  "⚠️ 服务正在重启,当前回复已中断。请重新发送你的问题。",
885
899
  accountStorePath,
886
- ctx.log,
900
+ pluginLog,
887
901
  ).catch((err: any) => {
888
- ctx.log?.debug?.(
902
+ pluginLog?.debug?.(
889
903
  `[${account.accountId}] Failed to finalize active cards during stop: ${err.message}`,
890
904
  );
891
905
  });
@@ -895,29 +909,35 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
895
909
  try {
896
910
  client.disconnect();
897
911
  } catch (err: any) {
898
- ctx.log?.warn?.(`[${account.accountId}] Error during disconnect: ${err.message}`);
912
+ pluginLog?.warn?.(`[${account.accountId}] Error during disconnect: ${err.message}`);
899
913
  }
900
914
  nativeStopResolve?.();
901
915
  }
902
916
 
903
- ctx.setStatus({
904
- ...ctx.getStatus(),
917
+ applyStatusPatch({
905
918
  running: false,
919
+ connected: false,
920
+ lastEventAt: getCurrentTimestamp(),
906
921
  lastStopAt: getCurrentTimestamp(),
907
922
  });
908
923
 
909
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client stopped`);
924
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client stopped`);
925
+ closePluginDebugLog({
926
+ accountId: account.accountId,
927
+ storePath: accountStorePath,
928
+ });
910
929
  };
911
930
 
912
931
  if (abortSignal) {
913
932
  if (abortSignal.aborted) {
914
- ctx.log?.warn?.(
933
+ pluginLog?.warn?.(
915
934
  `[${account.accountId}] Abort signal already active, skipping connection`,
916
935
  );
917
936
 
918
- ctx.setStatus({
919
- ...ctx.getStatus(),
937
+ applyStatusPatch({
920
938
  running: false,
939
+ connected: false,
940
+ lastEventAt: getCurrentTimestamp(),
921
941
  lastStopAt: getCurrentTimestamp(),
922
942
  lastError: "Connection aborted before start",
923
943
  });
@@ -929,7 +949,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
929
949
  if (stopped) {
930
950
  return;
931
951
  }
932
- ctx.log?.info?.(
952
+ pluginLog?.info?.(
933
953
  `[${account.accountId}] Abort signal received, stopping DingTalk Stream client...`,
934
954
  );
935
955
  stopClient();
@@ -940,17 +960,19 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
940
960
  try {
941
961
  await client.connect();
942
962
  if (!stopped) {
943
- ctx.setStatus({
944
- ...ctx.getStatus(),
963
+ applyStatusPatch({
945
964
  running: true,
965
+ connected: true,
966
+ lastConnectedAt: getCurrentTimestamp(),
967
+ lastEventAt: getCurrentTimestamp(),
946
968
  lastStartAt: getCurrentTimestamp(),
947
969
  lastError: null,
948
970
  });
949
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
971
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
950
972
  await nativeStopPromise;
951
973
  }
952
974
  } catch (err: any) {
953
- ctx.log?.error?.(
975
+ pluginLog?.error?.(
954
976
  formatDingTalkConnectionErrorLog(
955
977
  // Use connect.open as base scope; instrumentation can override to connect.websocket
956
978
  "connect.open",
@@ -958,9 +980,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
958
980
  `[${account.accountId}] Failed to establish connection: ${err.message}`,
959
981
  ) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
960
982
  );
961
- ctx.setStatus({
962
- ...ctx.getStatus(),
983
+ applyStatusPatch({
963
984
  running: false,
985
+ connected: false,
986
+ lastEventAt: getCurrentTimestamp(),
964
987
  lastError: err.message || "Connection failed",
965
988
  });
966
989
  throw err;
@@ -984,13 +1007,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
984
1007
  if (stopped) {
985
1008
  return;
986
1009
  }
987
- ctx.log?.debug?.(
1010
+ pluginLog?.debug?.(
988
1011
  `[${account.accountId}] Connection state changed to: ${state}${error ? ` (${error})` : ""}`,
989
1012
  );
990
1013
  if (state === ConnectionState.CONNECTED) {
991
- ctx.setStatus({
992
- ...ctx.getStatus(),
1014
+ applyStatusPatch({
993
1015
  running: true,
1016
+ connected: true,
1017
+ lastConnectedAt: getCurrentTimestamp(),
1018
+ lastEventAt: getCurrentTimestamp(),
994
1019
  lastStartAt: getCurrentTimestamp(),
995
1020
  lastError: null,
996
1021
  });
@@ -1007,20 +1032,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1007
1032
  }
1008
1033
  }
1009
1034
  if (cleared > 0) {
1010
- ctx.log?.info?.(
1035
+ pluginLog?.info?.(
1011
1036
  `[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`,
1012
1037
  );
1013
1038
  }
1014
- ctx.setStatus({
1015
- ...ctx.getStatus(),
1039
+ applyStatusPatch({
1016
1040
  running: false,
1041
+ connected: false,
1042
+ lastEventAt: getCurrentTimestamp(),
1017
1043
  lastError: error || `Connection ${state.toLowerCase()}`,
1018
1044
  });
1019
1045
  }
1020
1046
  },
1021
1047
  };
1022
1048
 
1023
- ctx.log?.debug?.(
1049
+ pluginLog?.debug?.(
1024
1050
  `[${account.accountId}] Connection config: maxAttempts=${connectionConfig.maxAttempts}, ` +
1025
1051
  `initialDelay=${connectionConfig.initialDelay}ms, maxDelay=${connectionConfig.maxDelay}ms, ` +
1026
1052
  `jitter=${connectionConfig.jitter}`,
@@ -1030,7 +1056,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1030
1056
  client,
1031
1057
  account.accountId,
1032
1058
  connectionConfig,
1033
- ctx.log,
1059
+ pluginLog,
1034
1060
  createStreamClient,
1035
1061
  );
1036
1062
 
@@ -1038,23 +1064,25 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1038
1064
  await connectionManager.connect();
1039
1065
 
1040
1066
  if (!stopped && connectionManager.isConnected()) {
1041
- ctx.setStatus({
1042
- ...ctx.getStatus(),
1067
+ applyStatusPatch({
1043
1068
  running: true,
1069
+ connected: true,
1070
+ lastConnectedAt: getCurrentTimestamp(),
1071
+ lastEventAt: getCurrentTimestamp(),
1044
1072
  lastStartAt: getCurrentTimestamp(),
1045
1073
  lastError: null,
1046
1074
  });
1047
- ctx.log?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
1075
+ pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
1048
1076
 
1049
1077
  await connectionManager.waitForStop();
1050
1078
  } else {
1051
- ctx.log?.info?.(
1079
+ pluginLog?.info?.(
1052
1080
  `[${account.accountId}] DingTalk Stream client connect() completed but channel is ` +
1053
1081
  `not running (stopped=${stopped}, connected=${connectionManager.isConnected()})`,
1054
1082
  );
1055
1083
  }
1056
1084
  } catch (err: any) {
1057
- ctx.log?.error?.(
1085
+ pluginLog?.error?.(
1058
1086
  formatDingTalkConnectionErrorLog(
1059
1087
  // Use connect.open as base scope; instrumentation can override to connect.websocket
1060
1088
  "connect.open",
@@ -1063,9 +1091,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1063
1091
  ) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
1064
1092
  );
1065
1093
 
1066
- ctx.setStatus({
1067
- ...ctx.getStatus(),
1094
+ applyStatusPatch({
1068
1095
  running: false,
1096
+ connected: false,
1097
+ lastEventAt: getCurrentTimestamp(),
1069
1098
  lastError: err.message || "Connection failed",
1070
1099
  });
1071
1100
  throw err;
@@ -1082,7 +1111,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1082
1111
  defaultRuntime: {
1083
1112
  accountId: "default",
1084
1113
  running: false,
1114
+ connected: false,
1085
1115
  lastEventAt: null,
1116
+ lastConnectedAt: null,
1117
+ lastInboundAt: null,
1086
1118
  lastStartAt: null,
1087
1119
  lastStopAt: null,
1088
1120
  lastError: null,
@@ -1139,7 +1171,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1139
1171
  configured: account.configured,
1140
1172
  clientId: account.config?.clientId ?? null,
1141
1173
  running,
1174
+ connected: runtime?.connected ?? snapshot?.connected ?? null,
1142
1175
  lastEventAt: running ? getCurrentTimestamp() : persistedLastEventAt,
1176
+ lastConnectedAt: runtime?.lastConnectedAt ?? snapshot?.lastConnectedAt ?? null,
1177
+ lastInboundAt: runtime?.lastInboundAt ?? snapshot?.lastInboundAt ?? null,
1143
1178
  lastStartAt: runtime?.lastStartAt ?? snapshot?.lastStartAt ?? null,
1144
1179
  lastStopAt: runtime?.lastStopAt ?? snapshot?.lastStopAt ?? null,
1145
1180
  lastError: runtime?.lastError ?? snapshot?.lastError ?? null,
@@ -1,26 +1,8 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
3
  import { getDingTalkRuntime } from "../runtime";
3
4
  import type { Logger } from "../types";
4
5
 
5
- /**
6
- * Local implementation of the same logic as
7
- * `resolveNativeCommandSessionTargets` from `openclaw/plugin-sdk/command-auth`.
8
- *
9
- * Inlined because the CI openclaw package does not yet export that sub-path.
10
- * Replace with a direct import once the upstream package is updated.
11
- */
12
- function resolveNativeCommandSessionTargets(params: {
13
- agentId: string;
14
- sessionPrefix: string;
15
- userId: string;
16
- targetSessionKey: string;
17
- }): { sessionKey: string; commandTargetSessionKey: string } {
18
- return {
19
- sessionKey: `agent:${params.agentId}:${params.sessionPrefix}:${params.userId}`,
20
- commandTargetSessionKey: params.targetSessionKey,
21
- };
22
- }
23
-
24
6
  /**
25
7
  * Dispatch a native targeted `/stop` command through the OpenClaw SDK,
26
8
  * replacing the previous self-built Gateway WebSocket `chat.abort` approach.
@@ -35,8 +17,8 @@ function resolveNativeCommandSessionTargets(params: {
35
17
  * and executes `abortEmbeddedPiRun` + `clearSessionQueues`.
36
18
  *
37
19
  * Accesses SDK functions via `getDingTalkRuntime().channel.reply` — the same
38
- * pattern used by `inbound-handler.ts` — to avoid direct sub-path imports
39
- * that may not be available in the CI openclaw package version.
20
+ * pattern used by `inbound-handler.ts` — while reusing the upstream native
21
+ * command session-target helper exported by the plugin SDK.
40
22
  */
41
23
  export async function dispatchDingTalkCardStopCommand(params: {
42
24
  cfg: OpenClawConfig;