@soimy/dingtalk 3.6.11 → 3.7.0

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 (161) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +11684 -10583
  3. package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
  4. package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
  5. package/dist/src/card/ask-user-question-context.d.ts +1 -1
  6. package/dist/src/card/ask-user-question-store.d.ts +1 -1
  7. package/dist/src/card/ask-user-question.d.ts +1 -1
  8. package/dist/src/card/card-action-handler.d.ts +2 -2
  9. package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
  10. package/dist/src/card/card-run-registry.d.ts +2 -2
  11. package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
  12. package/dist/src/card/card-stop-handler.d.ts +1 -1
  13. package/dist/src/card/card-streaming-mode.d.ts +1 -1
  14. package/dist/src/card/card-task-progress.d.ts +60 -0
  15. package/dist/src/channel.d.ts +6 -6
  16. package/dist/src/command/card-stop-command.d.ts +1 -1
  17. package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
  18. package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
  19. package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
  20. package/dist/src/gateway/channel-gateway.d.ts +1 -1
  21. package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
  22. package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +1 -1
  23. package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
  24. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
  25. package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
  26. package/dist/src/messaging/btw-deliver.d.ts +1 -1
  27. package/dist/src/messaging/channel-outbound.d.ts +1 -1
  28. package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
  29. package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
  30. package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
  31. package/dist/src/messaging/quoted-context.d.ts +2 -2
  32. package/dist/src/messaging/quoted-file-service.d.ts +1 -1
  33. package/dist/src/messaging/quoted-ref.d.ts +2 -2
  34. package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
  35. package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
  36. package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
  37. package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
  38. package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
  39. package/dist/src/platform/channel-status.d.ts +1 -1
  40. package/dist/src/platform/config.d.ts +110 -0
  41. package/dist/src/platform/runtime-events.d.ts +67 -0
  42. package/dist/src/{types.d.ts → platform/types.d.ts} +74 -1
  43. package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
  44. package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
  45. package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
  46. package/dist/src/targeting/agent-routing.d.ts +3 -3
  47. package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
  48. package/index.ts +193 -69
  49. package/openclaw.plugin.json +158 -2
  50. package/package.json +16 -16
  51. package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
  52. package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
  53. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
  54. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
  55. package/src/card/ask-user-question-context.ts +1 -1
  56. package/src/card/ask-user-question-store.ts +2 -2
  57. package/src/card/ask-user-question.ts +11 -7
  58. package/src/card/card-action-handler.ts +2 -2
  59. package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
  60. package/src/card/card-draft-controller.ts +770 -0
  61. package/src/card/card-markdown-image-reroute.ts +8 -2
  62. package/src/card/card-run-registry.ts +17 -7
  63. package/src/{card-service.ts → card/card-service.ts} +91 -61
  64. package/src/card/card-stop-handler.ts +12 -8
  65. package/src/card/card-streaming-mode.ts +1 -1
  66. package/src/card/card-task-progress.ts +330 -0
  67. package/src/card/draft-stream-loop.ts +119 -0
  68. package/src/card/reasoning-answer-split.ts +125 -121
  69. package/src/card/reasoning-block-assembler.ts +122 -123
  70. package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
  71. package/src/card/statusline-renderer.ts +14 -5
  72. package/src/card/task-model-metadata.ts +8 -3
  73. package/src/channel.ts +14 -16
  74. package/src/command/card-stop-command.ts +2 -2
  75. package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
  76. package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
  77. package/src/command/inbound-command-dispatch-service.ts +12 -6
  78. package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
  79. package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
  80. package/src/gateway/channel-gateway.ts +106 -28
  81. package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
  82. package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
  83. package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +139 -84
  84. package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
  85. package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
  86. package/src/messaging/attachment-text-extractor.ts +21 -7
  87. package/src/messaging/btw-deliver.ts +2 -2
  88. package/src/messaging/channel-actions.ts +15 -7
  89. package/src/messaging/channel-outbound.ts +16 -11
  90. package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
  91. package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
  92. package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
  93. package/src/messaging/quoted-context.ts +2 -2
  94. package/src/messaging/quoted-file-service.ts +311 -294
  95. package/src/messaging/quoted-ref.ts +11 -7
  96. package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
  97. package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
  98. package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
  99. package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
  100. package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
  101. package/src/{send-service.ts → messaging/send-service.ts} +100 -65
  102. package/src/{auth.ts → platform/auth.ts} +2 -2
  103. package/src/platform/channel-status.ts +3 -3
  104. package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
  105. package/src/{config.ts → platform/config.ts} +167 -19
  106. package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
  107. package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
  108. package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
  109. package/src/platform/runtime-events.ts +202 -0
  110. package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
  111. package/src/{types.ts → platform/types.ts} +76 -1
  112. package/src/{http-client.ts → shared/http-client.ts} +1 -3
  113. package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
  114. package/src/{utils.ts → shared/utils.ts} +30 -13
  115. package/src/targeting/agent-name-matcher.ts +1 -1
  116. package/src/targeting/agent-routing.ts +7 -7
  117. package/src/targeting/group-members-store.ts +1 -1
  118. package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
  119. package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
  120. package/src/targeting/target-directory-adapter.ts +3 -3
  121. package/src/targeting/target-directory-store.ts +1 -1
  122. package/src/targeting/target-input.ts +1 -1
  123. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
  124. package/dist/src/config.d.ts +0 -59
  125. package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
  126. package/src/card-draft-controller.ts +0 -637
  127. package/src/draft-stream-loop.ts +0 -119
  128. /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
  129. /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
  130. /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
  131. /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
  132. /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
  133. /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
  134. /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
  135. /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
  136. /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
  137. /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
  138. /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
  139. /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
  140. /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
  141. /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
  142. /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
  143. /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
  144. /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
  145. /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
  146. /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
  147. /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
  148. /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
  149. /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
  150. /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
  151. /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
  152. /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
  153. /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
  154. /package/src/{access-control.ts → platform/access-control.ts} +0 -0
  155. /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
  156. /package/src/{runtime.ts → platform/runtime.ts} +0 -0
  157. /package/src/{session-state.ts → platform/session-state.ts} +0 -0
  158. /package/src/{signature.ts → platform/signature.ts} +0 -0
  159. /package/src/{dedup.ts → shared/dedup.ts} +0 -0
  160. /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
  161. /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
@@ -1,8 +1,5 @@
1
- import {
2
- resolveByQuotedRef,
3
- type MessageRecord,
4
- } from "../message-context-store";
5
- import type { DingTalkInboundMessage, Logger, MessageContent, QuotedRef } from "../types";
1
+ import type { DingTalkInboundMessage, Logger, MessageContent, QuotedRef } from "../platform/types";
2
+ import { resolveByQuotedRef, type MessageRecord } from "./message-context-store";
6
3
 
7
4
  function firstTrimmedString(...candidates: Array<string | undefined>): string | undefined {
8
5
  for (const candidate of candidates) {
@@ -27,7 +24,11 @@ export function buildInboundQuotedRef(
27
24
  content: MessageContent,
28
25
  ): QuotedRef | undefined {
29
26
  const repliedMsg = data.text?.repliedMsg;
30
- const repliedMsgId = firstTrimmedString(repliedMsg?.msgId, data.originalMsgId, content.quoted?.msgId);
27
+ const repliedMsgId = firstTrimmedString(
28
+ repliedMsg?.msgId,
29
+ data.originalMsgId,
30
+ content.quoted?.msgId,
31
+ );
31
32
  const fallbackCreatedAt = firstFiniteNumber(
32
33
  repliedMsg?.createdAt,
33
34
  content.quoted?.cardCreatedAt,
@@ -38,7 +39,10 @@ export function buildInboundQuotedRef(
38
39
  repliedMsg?.senderId === data.chatbotUserId ||
39
40
  content.quoted?.isQuotedCard === true;
40
41
  if (isOutboundQuoted) {
41
- const processQueryKey = firstTrimmedString(data.originalProcessQueryKey, content.quoted?.processQueryKey);
42
+ const processQueryKey = firstTrimmedString(
43
+ data.originalProcessQueryKey,
44
+ content.quoted?.processQueryKey,
45
+ );
42
46
  if (processQueryKey) {
43
47
  return {
44
48
  targetDirection: "outbound",
@@ -6,34 +6,43 @@
6
6
  * inbound-handler only coordinates — it no longer owns card state.
7
7
  */
8
8
 
9
+ import { createCardDraftController } from "../card/card-draft-controller";
10
+ import {
11
+ buildImagePlaceholderText,
12
+ extractMarkdownImageCandidates,
13
+ } from "../card/card-markdown-image-reroute";
14
+ import { attachCardRunController } from "../card/card-run-registry";
9
15
  import {
10
16
  commitAICardBlocks,
11
17
  isCardInTerminalState,
12
18
  updateAICardStatusLine,
13
- } from "./card-service";
14
- import { splitCardReasoningAnswerText } from "./card/reasoning-answer-split";
15
- import { createReasoningBlockAssembler } from "./card/reasoning-block-assembler";
19
+ } from "../card/card-service";
16
20
  import {
17
21
  resolveCardStreamingMode,
18
22
  shouldWarnDeprecatedCardRealTimeStreamOnce,
19
- } from "./card/card-streaming-mode";
23
+ } from "../card/card-streaming-mode";
20
24
  import {
21
- buildImagePlaceholderText,
22
- extractMarkdownImageCandidates,
23
- } from "./card/card-markdown-image-reroute";
24
- import { createCardDraftController } from "./card-draft-controller";
25
- import { attachCardRunController } from "./card/card-run-registry";
26
- import type { DeliverPayload, ReplyOptions, ReplyStrategy, ReplyStrategyContext } from "./reply-strategy-types";
27
- import { resolveRelativePath } from "./config";
25
+ createCardTaskProgressController,
26
+ resolveCardTaskProgressEnabled,
27
+ } from "../card/card-task-progress";
28
+ import { splitCardReasoningAnswerText } from "../card/reasoning-answer-split";
29
+ import { createReasoningBlockAssembler } from "../card/reasoning-block-assembler";
30
+ import { recordRunStart, getAggregatedUsage, clearRuns } from "../card/run-usage-store";
31
+ import { renderStatusLine } from "../card/statusline-renderer";
32
+ import type { StatusLineData } from "../card/statusline-renderer";
33
+ import { resolveRelativePath } from "../platform/config";
34
+ import { getTaskTimeSeconds, updateSessionState } from "../platform/session-state";
35
+ import type { AICardInstance } from "../platform/types";
36
+ import { AICardStatus } from "../platform/types";
37
+ import { formatDingTalkErrorPayloadLog } from "../shared/utils";
28
38
  import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
29
- import { getTaskTimeSeconds, updateSessionState } from "./session-state";
30
- import { renderStatusLine } from "./card/statusline-renderer";
31
- import type { StatusLineData } from "./card/statusline-renderer";
32
- import { recordRunStart, getAggregatedUsage, clearRuns } from "./run-usage-store";
39
+ import type {
40
+ DeliverPayload,
41
+ ReplyOptions,
42
+ ReplyStrategy,
43
+ ReplyStrategyContext,
44
+ } from "./reply-strategy-types";
33
45
  import { sendBySession, sendMessage, sendProactiveMedia, uploadMedia } from "./send-service";
34
- import type { AICardInstance } from "./types";
35
- import { AICardStatus } from "./types";
36
- import { formatDingTalkErrorPayloadLog } from "./utils";
37
46
 
38
47
  const EMPTY_FINAL_REPLY = "✅ Done";
39
48
  const DEFAULT_CARD_FAILED_MESSAGE = "回复生成失败,请重试";
@@ -73,33 +82,38 @@ export function createCardReplyStrategy(
73
82
  ? getTaskTimeSeconds(sessionTaskStateScope)
74
83
  : undefined;
75
84
  const cardElapsedMs = Math.max(0, Date.now() - card.createdAt);
76
- const sessionElapsedMs = typeof sessionTaskTimeSeconds === "number"
77
- ? sessionTaskTimeSeconds * 1000
78
- : undefined;
79
- const metaElapsedMs = typeof ctx.taskMeta.elapsedMs === "number" && ctx.taskMeta.elapsedMs > 0
80
- ? ctx.taskMeta.elapsedMs
81
- : undefined;
82
- ctx.taskMeta.elapsedMs = Math.max(
83
- cardElapsedMs,
84
- sessionElapsedMs ?? 0,
85
- metaElapsedMs ?? 0,
86
- );
85
+ const sessionElapsedMs =
86
+ typeof sessionTaskTimeSeconds === "number" ? sessionTaskTimeSeconds * 1000 : undefined;
87
+ const metaElapsedMs =
88
+ typeof ctx.taskMeta.elapsedMs === "number" && ctx.taskMeta.elapsedMs > 0
89
+ ? ctx.taskMeta.elapsedMs
90
+ : undefined;
91
+ ctx.taskMeta.elapsedMs = Math.max(cardElapsedMs, sessionElapsedMs ?? 0, metaElapsedMs ?? 0);
87
92
 
88
93
  let inputTokens: number | undefined;
89
94
  let outputTokens: number | undefined;
90
95
  let cacheRead: number | undefined;
91
96
  if (ctx.taskMeta.runIds && ctx.taskMeta.runIds.size > 0) {
92
97
  const tokenUsage = getAggregatedUsage(ctx.taskMeta.runIds);
93
- if (typeof tokenUsage.input === "number") { inputTokens = tokenUsage.input; }
94
- if (typeof tokenUsage.output === "number") { outputTokens = tokenUsage.output; }
95
- if (typeof tokenUsage.cacheRead === "number") { cacheRead = tokenUsage.cacheRead; }
98
+ if (typeof tokenUsage.input === "number") {
99
+ inputTokens = tokenUsage.input;
100
+ }
101
+ if (typeof tokenUsage.output === "number") {
102
+ outputTokens = tokenUsage.output;
103
+ }
104
+ if (typeof tokenUsage.cacheRead === "number") {
105
+ cacheRead = tokenUsage.cacheRead;
106
+ }
96
107
  }
97
108
 
98
109
  const statusLineData: StatusLineData = {
99
110
  model: ctx.taskMeta.model,
100
111
  effort: ctx.taskMeta.effort,
101
112
  agent: ctx.taskMeta.agent,
102
- taskTime: typeof ctx.taskMeta.elapsedMs === "number" ? Math.round(ctx.taskMeta.elapsedMs / 1000) : undefined,
113
+ taskTime:
114
+ typeof ctx.taskMeta.elapsedMs === "number"
115
+ ? Math.round(ctx.taskMeta.elapsedMs / 1000)
116
+ : undefined,
103
117
  inputTokens,
104
118
  outputTokens,
105
119
  cacheRead,
@@ -131,6 +145,15 @@ export function createCardReplyStrategy(
131
145
  throttleMs: config.cardStreamInterval ?? 1000,
132
146
  getStatusLine: buildStatusLine,
133
147
  });
148
+ const taskProgressController = createCardTaskProgressController({
149
+ sessionKey: ctx.sessionKey ?? "",
150
+ enabled: resolveCardTaskProgressEnabled(config),
151
+ refresh: config.cardTaskProgressRefresh,
152
+ runtimeEvents: ctx.runtimeEvents,
153
+ updateProgress: controller.updateProgress,
154
+ clearProgress: controller.clearProgress,
155
+ log,
156
+ });
134
157
  const reasoningAssembler = createReasoningBlockAssembler();
135
158
  if (card.outTrackId) {
136
159
  attachCardRunController(card.outTrackId, controller);
@@ -145,7 +168,8 @@ export function createCardReplyStrategy(
145
168
  const processedMediaUrls = new Set<string>();
146
169
 
147
170
  const getRenderedTimeline = (options: { preferFinalAnswer?: boolean } = {}): string => {
148
- const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined);
171
+ const fallbackAnswer =
172
+ finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined);
149
173
  return controller.getRenderedContent({
150
174
  fallbackAnswer,
151
175
  overrideAnswer: options.preferFinalAnswer ? finalTextForFallback : undefined,
@@ -183,10 +207,7 @@ export function createCardReplyStrategy(
183
207
  }
184
208
  const blocks = reasoningAssembler.ingestSnapshot(text);
185
209
  const trimmed = text.trimStart();
186
- if (
187
- blocks.length === 0
188
- && !trimmed.startsWith("Reasoning:")
189
- ) {
210
+ if (blocks.length === 0 && !trimmed.startsWith("Reasoning:")) {
190
211
  if (trimmed.startsWith("Reason:")) {
191
212
  latestReasoningSnapshot = "";
192
213
  return;
@@ -308,8 +329,9 @@ export function createCardReplyStrategy(
308
329
  await applyModeAwareReasoningSnapshot(normalizedSnapshot.reasoningText);
309
330
  }
310
331
 
311
- const answerSnapshot = normalizedSnapshot.answerText
312
- ?? (!normalizedSnapshot.reasoningText ? rewrittenSnapshot : undefined);
332
+ const answerSnapshot =
333
+ normalizedSnapshot.answerText ??
334
+ (!normalizedSnapshot.reasoningText ? rewrittenSnapshot : undefined);
313
335
  if (!answerSnapshot) {
314
336
  return;
315
337
  }
@@ -364,9 +386,7 @@ export function createCardReplyStrategy(
364
386
  let prepared: Awaited<ReturnType<typeof prepareMediaInput>> | undefined;
365
387
  try {
366
388
  prepared = await prepareMediaInput(candidate.url, log, config.mediaUrlAllowlist);
367
- const mediaPath = prepared.cleanup
368
- ? prepared.path
369
- : resolveRelativePath(prepared.path);
389
+ const mediaPath = prepared.cleanup ? prepared.path : resolveRelativePath(prepared.path);
370
390
  const mediaType = resolveOutboundMediaType({ mediaPath, asVoice: false });
371
391
  if (mediaType !== "image") {
372
392
  continue;
@@ -379,7 +399,8 @@ export function createCardReplyStrategy(
379
399
 
380
400
  processedMediaUrls.add(candidate.url.trim());
381
401
  const placeholder = buildImagePlaceholderText({ alt: candidate.alt, url: candidate.url });
382
- const blockText = candidate.alt.trim() || placeholder.replace(/^见下图/, "").trim() || "图片";
402
+ const blockText =
403
+ candidate.alt.trim() || placeholder.replace(/^见下图/, "").trim() || "图片";
383
404
  successfulReroutes.push({
384
405
  start: candidate.start,
385
406
  end: candidate.end,
@@ -427,7 +448,9 @@ export function createCardReplyStrategy(
427
448
  }
428
449
  recordRunStart(runId);
429
450
  if (ctx.taskMeta) {
430
- if (!ctx.taskMeta.runIds) { ctx.taskMeta.runIds = new Set(); }
451
+ if (!ctx.taskMeta.runIds) {
452
+ ctx.taskMeta.runIds = new Set();
453
+ }
431
454
  ctx.taskMeta.runIds.add(runId);
432
455
  }
433
456
  },
@@ -472,7 +495,10 @@ export function createCardReplyStrategy(
472
495
  const textToSend = payload.text;
473
496
 
474
497
  // Empty-payload guard — card final is an exception (e.g. file-only response).
475
- if ((typeof textToSend !== "string" || textToSend.length === 0) && payload.mediaUrls.length === 0) {
498
+ if (
499
+ (typeof textToSend !== "string" || textToSend.length === 0) &&
500
+ payload.mediaUrls.length === 0
501
+ ) {
476
502
  if (payload.kind !== "final") {
477
503
  return;
478
504
  }
@@ -482,14 +508,15 @@ export function createCardReplyStrategy(
482
508
  if (payload.kind === "final") {
483
509
  // OpenClaw can append a non-terminal tool warning after a valid final.
484
510
  // Do not let that plain-text warning replace the answer already captured by the card.
485
- const hasPreservableReply = Boolean(
486
- finalTextForFallback?.trim() || controller.getFinalAnswerContent().trim(),
487
- ) || processedMediaUrls.size > 0 || pendingNonImageMedia.length > 0;
511
+ const hasPreservableReply =
512
+ Boolean(finalTextForFallback?.trim() || controller.getFinalAnswerContent().trim()) ||
513
+ processedMediaUrls.size > 0 ||
514
+ pendingNonImageMedia.length > 0;
488
515
  if (
489
- payload.isError === true
490
- && sawFinalDelivery
491
- && payload.mediaUrls.length === 0
492
- && hasPreservableReply
516
+ payload.isError === true &&
517
+ sawFinalDelivery &&
518
+ payload.mediaUrls.length === 0 &&
519
+ hasPreservableReply
493
520
  ) {
494
521
  log?.debug?.("[DingTalk][Finalize] Ignoring late error final after answer delivery");
495
522
  return;
@@ -502,10 +529,10 @@ export function createCardReplyStrategy(
502
529
  }
503
530
  log?.info?.(
504
531
  `[DingTalk][Finalize] deliver(final) received — cardState=${card.state} ` +
505
- `textLen=${typeof textToSend === "string" ? textToSend.length : "null"} ` +
506
- `mediaUrls=${payload.mediaUrls.length} ` +
507
- `lastAnswer="${(controller.getLastAnswerContent() ?? "").slice(0, 80)}" ` +
508
- `lastContent="${(controller.getLastContent() ?? "").slice(0, 80)}"`,
532
+ `textLen=${typeof textToSend === "string" ? textToSend.length : "null"} ` +
533
+ `mediaUrls=${payload.mediaUrls.length} ` +
534
+ `lastAnswer="${(controller.getLastAnswerContent() ?? "").slice(0, 80)}" ` +
535
+ `lastContent="${(controller.getLastContent() ?? "").slice(0, 80)}"`,
509
536
  );
510
537
  // Inline media upload → image blocks in card; defer non-image attachments
511
538
  if (payload.mediaUrls.length > 0) {
@@ -516,9 +543,14 @@ export function createCardReplyStrategy(
516
543
  }
517
544
  try {
518
545
  const prepared = await prepareMediaInput(url, log, config.mediaUrlAllowlist);
519
- const mediaType = resolveOutboundMediaType({ mediaPath: prepared.path, asVoice: false });
546
+ const mediaType = resolveOutboundMediaType({
547
+ mediaPath: prepared.path,
548
+ asVoice: false,
549
+ });
520
550
  if (mediaType !== "image") {
521
- log?.debug?.(`[DingTalk][Card] Deferring non-image media (${mediaType}) for out-of-card delivery: ${url}`);
551
+ log?.debug?.(
552
+ `[DingTalk][Card] Deferring non-image media (${mediaType}) for out-of-card delivery: ${url}`,
553
+ );
522
554
  // Collect non-image attachments for later delivery
523
555
  if (mediaType === "voice" || mediaType === "video" || mediaType === "file") {
524
556
  pendingNonImageMedia.push({ url, type: mediaType });
@@ -548,7 +580,11 @@ export function createCardReplyStrategy(
548
580
  const normalizedFinal = await applySplitTextToTimeline(rewrittenFinalText, {
549
581
  answerHandling: "capture",
550
582
  });
551
- if (isFirstFinalDelivery && !normalizedFinal.answerText && !normalizedFinal.reasoningText) {
583
+ if (
584
+ isFirstFinalDelivery &&
585
+ !normalizedFinal.answerText &&
586
+ !normalizedFinal.reasoningText
587
+ ) {
552
588
  finalTextForFallback = rewrittenFinalText;
553
589
  }
554
590
  await flushPendingReasoning();
@@ -560,7 +596,9 @@ export function createCardReplyStrategy(
560
596
  // ---- tool: append to card ----
561
597
  if (payload.kind === "tool") {
562
598
  if (controller.isFailed() || isCardInTerminalState(card.state)) {
563
- log?.debug?.("[DingTalk] Card failed, skipping tool result (will send full reply on final)");
599
+ log?.debug?.(
600
+ "[DingTalk] Card failed, skipping tool result (will send full reply on final)",
601
+ );
564
602
  return;
565
603
  }
566
604
  await flushPendingReasoning();
@@ -599,9 +637,14 @@ export function createCardReplyStrategy(
599
637
  }
600
638
  try {
601
639
  const prepared = await prepareMediaInput(url, log, config.mediaUrlAllowlist);
602
- const mediaType = resolveOutboundMediaType({ mediaPath: prepared.path, asVoice: false });
640
+ const mediaType = resolveOutboundMediaType({
641
+ mediaPath: prepared.path,
642
+ asVoice: false,
643
+ });
603
644
  if (mediaType !== "image") {
604
- log?.debug?.(`[DingTalk][Card] Deferring non-image media (${mediaType}) for out-of-card delivery: ${url}`);
645
+ log?.debug?.(
646
+ `[DingTalk][Card] Deferring non-image media (${mediaType}) for out-of-card delivery: ${url}`,
647
+ );
605
648
  pendingNonImageMedia.push({ url, type: mediaType });
606
649
  await prepared.cleanup?.();
607
650
  continue;
@@ -621,13 +664,14 @@ export function createCardReplyStrategy(
621
664
  },
622
665
 
623
666
  async finalize(): Promise<void> {
667
+ await taskProgressController.dispose();
624
668
  log?.info?.(
625
669
  `[DingTalk][Finalize] Step 5 entry — ` +
626
- `cardState=${card.state ?? "N/A"} ` +
627
- `controllerFailed=${controller.isFailed()} ` +
628
- `finalTextForFallback="${(finalTextForFallback ?? "").slice(0, 80)}" ` +
629
- `lastAnswer="${(controller.getLastAnswerContent() ?? "").slice(0, 80)}" ` +
630
- `lastContent="${(controller.getLastContent() ?? "").slice(0, 80)}"`,
670
+ `cardState=${card.state ?? "N/A"} ` +
671
+ `controllerFailed=${controller.isFailed()} ` +
672
+ `finalTextForFallback="${(finalTextForFallback ?? "").slice(0, 80)}" ` +
673
+ `lastAnswer="${(controller.getLastAnswerContent() ?? "").slice(0, 80)}" ` +
674
+ `lastContent="${(controller.getLastContent() ?? "").slice(0, 80)}"`,
631
675
  );
632
676
 
633
677
  if (isStopRequested?.()) {
@@ -645,14 +689,15 @@ export function createCardReplyStrategy(
645
689
  // deliver it as a markdown fallback so the user sees the final result.
646
690
  // The user may see partial overlap with the frozen card's content, but
647
691
  // delivering the full answer is preferred over silence.
648
- const recoveryText = getRenderedTimeline({ preferFinalAnswer: true })
649
- || finalTextForFallback
650
- || controller.getLastAnswerContent()
651
- || controller.getLastContent();
692
+ const recoveryText =
693
+ getRenderedTimeline({ preferFinalAnswer: true }) ||
694
+ finalTextForFallback ||
695
+ controller.getLastAnswerContent() ||
696
+ controller.getLastContent();
652
697
  if (recoveryText) {
653
698
  log?.info?.(
654
699
  `[DingTalk][Finalize] Card already FINISHED — sending markdown fallback for session-recovery content ` +
655
- `len=${recoveryText.length} preview="${recoveryText.slice(0, 80)}"`,
700
+ `len=${recoveryText.length} preview="${recoveryText.slice(0, 80)}"`,
656
701
  );
657
702
  const sendResult = await sendMessage(ctx.config, ctx.to, recoveryText, {
658
703
  sessionWebhook: ctx.sessionWebhook,
@@ -690,9 +735,10 @@ export function createCardReplyStrategy(
690
735
 
691
736
  // Card failed -> markdown fallback (bypass sendMessage to avoid duplicate card).
692
737
  if (card.state === AICardStatus.FAILED || controller.isFailed()) {
693
- const fallbackText = getRenderedTimeline({ preferFinalAnswer: true })
694
- || controller.getLastAnswerContent()
695
- || DEFAULT_CARD_FAILED_MESSAGE;
738
+ const fallbackText =
739
+ getRenderedTimeline({ preferFinalAnswer: true }) ||
740
+ controller.getLastAnswerContent() ||
741
+ DEFAULT_CARD_FAILED_MESSAGE;
696
742
  if (fallbackText) {
697
743
  log?.debug?.("[DingTalk] Card failed during streaming, sending markdown fallback");
698
744
  const sendResult = await sendMessage(ctx.config, ctx.to, fallbackText, {
@@ -727,38 +773,48 @@ export function createCardReplyStrategy(
727
773
  await controller.waitForInFlight();
728
774
 
729
775
  // Prepare finalize options for single instances API call
730
- const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined);
776
+ const fallbackAnswer =
777
+ finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined);
731
778
  const blockListJson = controller.getRenderedBlocks({
732
779
  fallbackAnswer,
733
780
  overrideAnswer: finalTextForFallback,
734
781
  });
735
- const content = controller.getRenderedContent({
736
- fallbackAnswer,
737
- overrideAnswer: finalTextForFallback,
738
- }) || fallbackAnswer || EMPTY_FINAL_REPLY;
782
+ const content =
783
+ controller.getRenderedContent({
784
+ fallbackAnswer,
785
+ overrideAnswer: finalTextForFallback,
786
+ }) ||
787
+ fallbackAnswer ||
788
+ EMPTY_FINAL_REPLY;
739
789
 
740
790
  controller.stop();
741
791
  log?.info?.(
742
792
  `[DingTalk][Finalize] Calling commitAICardBlocks — ` +
743
- `blockListLen=${blockListJson.length} contentLen=${content.length} ` +
744
- `source=${finalTextForFallback ? "final.payload" : controller.getFinalAnswerContent() ? "timeline.answer" : sawFinalDelivery ? "timeline.fileOnly" : "fallbackDone"} ` +
745
- `preview="${content.slice(0, 120)}"`,
793
+ `blockListLen=${blockListJson.length} contentLen=${content.length} ` +
794
+ `source=${finalTextForFallback ? "final.payload" : controller.getFinalAnswerContent() ? "timeline.answer" : sawFinalDelivery ? "timeline.fileOnly" : "fallbackDone"} ` +
795
+ `preview="${content.slice(0, 120)}"`,
746
796
  );
747
797
 
748
798
  // Build statusLine for card template
749
799
  const statusLine = buildStatusLine();
750
800
 
751
- await commitAICardBlocks(card, {
752
- blockListJson,
753
- content,
754
- statusLine,
755
- quotedRef: ctx.replyQuotedRef,
756
- }, log);
801
+ await commitAICardBlocks(
802
+ card,
803
+ {
804
+ blockListJson,
805
+ content,
806
+ statusLine,
807
+ quotedRef: ctx.replyQuotedRef,
808
+ },
809
+ log,
810
+ );
757
811
 
758
812
  // Send deferred non-image attachments after card finalize
759
813
  // Use sessionWebhook for reply-session semantics; fallback to proactive if unavailable.
760
814
  if (pendingNonImageMedia.length > 0) {
761
- log?.debug?.(`[DingTalk][Card] Sending ${pendingNonImageMedia.length} deferred non-image attachments`);
815
+ log?.debug?.(
816
+ `[DingTalk][Card] Sending ${pendingNonImageMedia.length} deferred non-image attachments`,
817
+ );
762
818
  for (const { url, type } of pendingNonImageMedia) {
763
819
  try {
764
820
  const prepared = await prepareMediaInput(url, log, config.mediaUrlAllowlist);
@@ -775,7 +831,9 @@ export function createCardReplyStrategy(
775
831
  storePath: ctx.storePath,
776
832
  });
777
833
  if (!sendResult.ok) {
778
- log?.warn?.(`[DingTalk][Card] Deferred media session send failed: ${sendResult.error || "unknown"}`);
834
+ log?.warn?.(
835
+ `[DingTalk][Card] Deferred media session send failed: ${sendResult.error || "unknown"}`,
836
+ );
779
837
  }
780
838
  } else {
781
839
  // Fallback: proactive send when no reply session available
@@ -784,7 +842,9 @@ export function createCardReplyStrategy(
784
842
  accountId: ctx.accountId,
785
843
  });
786
844
  if (!result.ok) {
787
- log?.warn?.(`[DingTalk][Card] Deferred media proactive send failed: ${result.error || "unknown"}`);
845
+ log?.warn?.(
846
+ `[DingTalk][Card] Deferred media proactive send failed: ${result.error || "unknown"}`,
847
+ );
788
848
  }
789
849
  }
790
850
 
@@ -832,6 +892,7 @@ export function createCardReplyStrategy(
832
892
  },
833
893
 
834
894
  async abort(_error: Error): Promise<void> {
895
+ await taskProgressController.dispose();
835
896
  lifecycleState = "sealed";
836
897
  if (card.accountId && card.conversationId) {
837
898
  clearRuns(ctx.taskMeta?.runIds);
@@ -842,22 +903,38 @@ export function createCardReplyStrategy(
842
903
  try {
843
904
  // For V2 template, finalize via instances API
844
905
  const errorBlockListJson = JSON.stringify([{ type: 0, markdown: "❌ 处理失败" }]);
845
- await commitAICardBlocks(card, {
846
- blockListJson: errorBlockListJson,
847
- content: "❌ 处理失败",
848
- }, log);
906
+ await commitAICardBlocks(
907
+ card,
908
+ {
909
+ blockListJson: errorBlockListJson,
910
+ content: "❌ 处理失败",
911
+ },
912
+ log,
913
+ );
849
914
  } catch (cardCloseErr: unknown) {
850
- log?.debug?.(`[DingTalk] Failed to finalize card after dispatch error: ${(cardCloseErr as Error).message}`);
915
+ log?.debug?.(
916
+ `[DingTalk] Failed to finalize card after dispatch error: ${(cardCloseErr as Error).message}`,
917
+ );
851
918
  card.state = AICardStatus.FAILED;
852
919
  card.lastUpdated = Date.now();
853
920
  }
854
921
  }
855
922
  },
856
923
 
924
+ async dispose(): Promise<void> {
925
+ // Release the progress heartbeat/subscription even when this strategy is
926
+ // dropped without finalize (for example ask-user question-card takeover).
927
+ // This path never commits the card, so a progress-only card must also
928
+ // clear its remote block list instead of leaving a live-looking card.
929
+ await taskProgressController.dispose({ clearRemoteWhenEmpty: true });
930
+ },
931
+
857
932
  getFinalText(): string | undefined {
858
- return finalTextForFallback
859
- || controller.getFinalAnswerContent()
860
- || (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined);
933
+ return (
934
+ finalTextForFallback ||
935
+ controller.getFinalAnswerContent() ||
936
+ (sawFinalDelivery ? EMPTY_FINAL_REPLY : undefined)
937
+ );
861
938
  },
862
939
  };
863
940
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import path from "node:path";
10
- import { resolveRelativePath } from "./config";
10
+ import { resolveRelativePath } from "../platform/config";
11
11
  import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
12
12
  import type {
13
13
  DeliverPayload,
@@ -90,7 +90,9 @@ export function createMarkdownReplyStrategy(ctx: ReplyStrategyContext): ReplyStr
90
90
  sentVisibleContent = true;
91
91
  };
92
92
 
93
- const prepareAnswerSuffix = (text: string | undefined): {
93
+ const prepareAnswerSuffix = (
94
+ text: string | undefined,
95
+ ): {
94
96
  text: string;
95
97
  markSent: () => void;
96
98
  } | null => {
@@ -272,6 +274,10 @@ export function createMarkdownReplyStrategy(ctx: ReplyStrategyContext): ReplyStr
272
274
  // Nothing to clean up.
273
275
  },
274
276
 
277
+ async dispose(): Promise<void> {
278
+ // Markdown delivery owns no timers or subscriptions.
279
+ },
280
+
275
281
  getFinalText(): string | undefined {
276
282
  return finalText || activeAnswerText || undefined;
277
283
  },
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
10
- import type { DingTalkConfig, Logger, QuotedRef } from "./types";
10
+ import type { RuntimeEventsSurface } from "../platform/runtime-events";
11
+ import type { DingTalkConfig, Logger, QuotedRef } from "../platform/types";
11
12
 
12
13
  // ---- Internal helper type ----
13
14
 
@@ -57,6 +58,14 @@ export interface ReplyStrategy {
57
58
  /** Called when dispatch throws an error. */
58
59
  abort(error: Error): Promise<void>;
59
60
 
61
+ /**
62
+ * Release strategy-owned resources (timers, event subscriptions) without
63
+ * touching the delivery surface. Idempotent, and safe to call after
64
+ * `finalize()`/`abort()`. Callers must invoke this even on early-return
65
+ * paths so a strategy that never finalized cannot leak.
66
+ */
67
+ dispose(): Promise<void>;
68
+
60
69
  /** Last known final text (for external consumers such as logging). */
61
70
  getFinalText(): string | undefined;
62
71
  }
@@ -94,4 +103,5 @@ export interface ReplyStrategyContext {
94
103
  isStopRequested?: () => boolean;
95
104
  inboundText?: string;
96
105
  taskMeta?: TaskMeta;
106
+ runtimeEvents?: RuntimeEventsSurface;
97
107
  }
@@ -24,8 +24,8 @@
24
24
  * concurrently. The `sessionFilter` parameter is required for this reason.
25
25
  */
26
26
 
27
+ import type { DingTalkConfig, Logger } from "../platform/types";
27
28
  import type { DeliverPayload, ReplyOptions, ReplyStrategy } from "./reply-strategy-types";
28
- import type { DingTalkConfig, Logger } from "./types";
29
29
 
30
30
  const TOOL_REACTION_SILENCE_MS = 55_000;
31
31
  const TOOL_HEARTBEAT_INTERVAL_MS = 60_000;
@@ -183,6 +183,11 @@ export function withDynamicReaction(
183
183
  await inner.abort(_error);
184
184
  },
185
185
 
186
+ async dispose(): Promise<void> {
187
+ await dispose();
188
+ await inner.dispose();
189
+ },
190
+
186
191
  getFinalText(): string | undefined {
187
192
  return inner.getFinalText();
188
193
  },
@@ -5,10 +5,10 @@
5
5
  * strategy implementations. Type definitions live in reply-strategy-types.ts.
6
6
  */
7
7
 
8
- import type { AICardInstance } from "./types";
9
- import type { ReplyStrategy, ReplyStrategyContext } from "./reply-strategy-types";
8
+ import type { AICardInstance } from "../platform/types";
10
9
  import { createCardReplyStrategy } from "./reply-strategy-card";
11
10
  import { createMarkdownReplyStrategy } from "./reply-strategy-markdown";
11
+ import type { ReplyStrategy, ReplyStrategyContext } from "./reply-strategy-types";
12
12
 
13
13
  // Re-export all types so existing consumers that import from "./reply-strategy"
14
14
  // continue to work without changes beyond the ones we explicitly migrate.