@soimy/dingtalk 3.6.10 → 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 (163) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +11655 -10324
  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} +5 -5
  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/messaging/inline-directives.d.ts +42 -0
  29. package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
  30. package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
  31. package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
  32. package/dist/src/messaging/quoted-context.d.ts +2 -2
  33. package/dist/src/messaging/quoted-file-service.d.ts +1 -1
  34. package/dist/src/messaging/quoted-ref.d.ts +2 -2
  35. package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
  36. package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
  37. package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
  38. package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
  39. package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
  40. package/dist/src/platform/channel-status.d.ts +1 -1
  41. package/dist/src/platform/config.d.ts +110 -0
  42. package/dist/src/platform/runtime-events.d.ts +67 -0
  43. package/dist/src/{types.d.ts → platform/types.d.ts} +75 -2
  44. package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
  45. package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
  46. package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
  47. package/dist/src/targeting/agent-routing.d.ts +3 -3
  48. package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
  49. package/index.ts +193 -69
  50. package/openclaw.plugin.json +158 -2
  51. package/package.json +16 -16
  52. package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
  53. package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
  54. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
  55. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
  56. package/src/card/ask-user-question-context.ts +1 -1
  57. package/src/card/ask-user-question-store.ts +2 -2
  58. package/src/card/ask-user-question.ts +11 -7
  59. package/src/card/card-action-handler.ts +2 -2
  60. package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
  61. package/src/card/card-draft-controller.ts +770 -0
  62. package/src/card/card-markdown-image-reroute.ts +8 -2
  63. package/src/card/card-run-registry.ts +17 -7
  64. package/src/{card-service.ts → card/card-service.ts} +91 -61
  65. package/src/card/card-stop-handler.ts +12 -8
  66. package/src/card/card-streaming-mode.ts +1 -1
  67. package/src/card/card-task-progress.ts +330 -0
  68. package/src/card/draft-stream-loop.ts +119 -0
  69. package/src/card/reasoning-answer-split.ts +125 -121
  70. package/src/card/reasoning-block-assembler.ts +122 -123
  71. package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
  72. package/src/card/statusline-renderer.ts +14 -5
  73. package/src/card/task-model-metadata.ts +8 -3
  74. package/src/channel.ts +14 -16
  75. package/src/command/card-stop-command.ts +2 -2
  76. package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
  77. package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
  78. package/src/command/inbound-command-dispatch-service.ts +12 -6
  79. package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
  80. package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
  81. package/src/gateway/channel-gateway.ts +106 -28
  82. package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
  83. package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
  84. package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +144 -85
  85. package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
  86. package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
  87. package/src/messaging/attachment-text-extractor.ts +21 -7
  88. package/src/messaging/btw-deliver.ts +2 -2
  89. package/src/messaging/channel-actions.ts +15 -7
  90. package/src/messaging/channel-outbound.ts +16 -11
  91. package/src/messaging/inline-directives.ts +342 -0
  92. package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
  93. package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
  94. package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
  95. package/src/messaging/quoted-context.ts +2 -2
  96. package/src/messaging/quoted-file-service.ts +311 -294
  97. package/src/messaging/quoted-ref.ts +11 -7
  98. package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
  99. package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
  100. package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
  101. package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
  102. package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
  103. package/src/{send-service.ts → messaging/send-service.ts} +100 -65
  104. package/src/{auth.ts → platform/auth.ts} +2 -2
  105. package/src/platform/channel-status.ts +3 -3
  106. package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
  107. package/src/{config.ts → platform/config.ts} +167 -19
  108. package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
  109. package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
  110. package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
  111. package/src/platform/runtime-events.ts +202 -0
  112. package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
  113. package/src/{types.ts → platform/types.ts} +77 -2
  114. package/src/{http-client.ts → shared/http-client.ts} +1 -3
  115. package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
  116. package/src/{utils.ts → shared/utils.ts} +30 -13
  117. package/src/targeting/agent-name-matcher.ts +1 -1
  118. package/src/targeting/agent-routing.ts +7 -7
  119. package/src/targeting/group-members-store.ts +1 -1
  120. package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
  121. package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
  122. package/src/targeting/target-directory-adapter.ts +3 -3
  123. package/src/targeting/target-directory-store.ts +1 -1
  124. package/src/targeting/target-input.ts +1 -1
  125. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
  126. package/dist/src/config.d.ts +0 -59
  127. package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
  128. package/src/card-draft-controller.ts +0 -637
  129. package/src/draft-stream-loop.ts +0 -119
  130. /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
  131. /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
  132. /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
  133. /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
  134. /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
  135. /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
  136. /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
  137. /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
  138. /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
  139. /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
  140. /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
  141. /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
  142. /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
  143. /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
  144. /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
  145. /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
  146. /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
  147. /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
  148. /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
  149. /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
  150. /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
  151. /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
  152. /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
  153. /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
  154. /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
  155. /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
  156. /package/src/{access-control.ts → platform/access-control.ts} +0 -0
  157. /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
  158. /package/src/{runtime.ts → platform/runtime.ts} +0 -0
  159. /package/src/{session-state.ts → platform/session-state.ts} +0 -0
  160. /package/src/{signature.ts → platform/signature.ts} +0 -0
  161. /package/src/{dedup.ts → shared/dedup.ts} +0 -0
  162. /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
  163. /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
@@ -1,109 +1,115 @@
1
1
  import fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound";
3
4
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/reply-runtime";
4
- import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
5
- import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
6
- import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
7
- import { attachNativeAckReaction } from "./ack-reaction-service";
8
- import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
9
- import { getAccessToken } from "./auth";
10
- import {
11
- createAICard,
12
- commitAICardBlocks,
13
- isCardInTerminalState,
14
- recallAICardMessage,
15
- } from "./card-service";
5
+ import { classifyAckReactionEmoji } from "../ack-reaction/ack-reaction-classifier";
6
+ import { attachNativeAckReaction } from "../ack-reaction/ack-reaction-service";
7
+ import { createDynamicAckReactionController } from "../ack-reaction/dynamic-ack-reaction-controller";
16
8
  import {
17
9
  invalidateAskUserQuestionsForScope,
18
10
  syncInvalidatedAskUserQuestionCards,
19
- } from "./card/ask-user-question";
11
+ } from "../card/ask-user-question";
20
12
  import {
21
13
  getDingTalkQuestionContext,
22
14
  withDingTalkQuestionContext,
23
- } from "./card/ask-user-question-context";
24
- import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry";
25
- import { renderStatusLine } from "./card/statusline-renderer";
26
- import { resolveConfiguredTaskModelMetadata } from "./card/task-model-metadata";
27
- import { dispatchDingTalkCardStopCommand } from "./command/card-stop-command";
28
- import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
15
+ } from "../card/ask-user-question-context";
16
+ import { isCardRunStopRequested, registerCardRun, removeCardRun } from "../card/card-run-registry";
29
17
  import {
30
- resolveAckReactionSetting,
31
- resolveGroupConfig,
32
- resolveRelativePath,
33
- resolveRobotCode,
34
- } from "./config";
35
- import { buildLearningContextBlock, isLearningEnabled } from "./feedback-learning-service";
36
- import axios from "./http-client";
37
- import { dispatchInboundViaSessionQueue } from "./gateway/inbound-session-queue-dispatcher";
38
- import { setCurrentLogger } from "./logger-context";
39
- import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
18
+ createAICard,
19
+ commitAICardBlocks,
20
+ isCardInTerminalState,
21
+ recallAICardMessage,
22
+ } from "../card/card-service";
23
+ import { renderStatusLine } from "../card/statusline-renderer";
24
+ import { resolveConfiguredTaskModelMetadata } from "../card/task-model-metadata";
25
+ import { dispatchDingTalkCardStopCommand } from "../command/card-stop-command";
26
+ import { buildLearningContextBlock, isLearningEnabled } from "../command/feedback-learning-service";
27
+ import { handleInboundCommandDispatch } from "../command/inbound-command-dispatch-service";
28
+ import { extractAttachmentText } from "../messaging/attachment-text-extractor";
29
+ import { deliverBtwReply, stripLeadingMentions } from "../messaging/btw-deliver";
30
+ import { parseInlineDirectives } from "../messaging/inline-directives";
31
+ import { prepareMediaInput, resolveOutboundMediaType } from "../messaging/media-utils";
40
32
  import {
41
33
  DEFAULT_MEDIA_CONTEXT_TTL_MS,
42
34
  DEFAULT_MESSAGE_CONTEXT_TTL_DAYS,
43
35
  upsertInboundMessageContext,
44
- } from "./message-context-store";
45
- import { extractMessageContent } from "./message-utils";
46
- import { extractAttachmentText } from "./messaging/attachment-text-extractor";
47
- import { deliverBtwReply, stripLeadingMentions } from "./messaging/btw-deliver";
48
- import { resolveQuotedRuntimeContext } from "./messaging/quoted-context";
36
+ } from "../messaging/message-context-store";
37
+ import { extractMessageContent } from "../messaging/message-utils";
38
+ import {
39
+ clearProactiveRiskObservationsForTest,
40
+ getProactiveRiskObservationForAny,
41
+ } from "../messaging/proactive-risk-registry";
42
+ import { resolveQuotedRuntimeContext } from "../messaging/quoted-context";
49
43
  import {
50
44
  downloadGroupFile,
51
45
  getUnionIdByStaffId,
52
46
  resolveQuotedFile,
53
- } from "./messaging/quoted-file-service";
47
+ } from "../messaging/quoted-file-service";
54
48
  import {
55
49
  buildInboundQuotedRef,
56
50
  createReplyQuotedRef,
57
51
  resolveQuotedRecord,
58
- } from "./messaging/quoted-ref";
59
- import { registerPeerId } from "./peer-id-registry";
52
+ } from "../messaging/quoted-ref";
53
+ import { createReplyStrategy } from "../messaging/reply-strategy";
54
+ import type { DeliverPayload, ReplyStrategy } from "../messaging/reply-strategy-types";
55
+ import { sendBySession, sendMessage, sendProactiveMedia } from "../messaging/send-service";
60
56
  import {
61
- clearProactiveRiskObservationsForTest,
62
- getProactiveRiskObservationForAny,
63
- } from "./proactive-risk-registry";
64
- import {
65
- isReplySessionConflictError,
66
- withReplySessionConflictRetry,
67
- } from "./gateway/reply-session-conflict";
68
- import { createReplyStrategy } from "./reply-strategy";
69
- import type { DeliverPayload } from "./reply-strategy-types";
70
- import { getDingTalkRuntime } from "./runtime";
71
- import { sendBySession, sendMessage, sendProactiveMedia } from "./send-service";
72
- import { acquireSessionLock } from "./session-lock";
73
- import {
74
- clearSessionPeerOverride,
75
- getSessionPeerOverride,
76
- setSessionPeerOverride,
77
- } from "./session-peer-store";
78
- import { resolveDingTalkSessionPeer } from "./session-routing";
79
- import { getSessionState, initSessionState } from "./session-state";
80
- import { getAgentDisplayName } from "./targeting/agent-name-matcher";
81
- import {
82
- buildAgentSessionKey,
83
- dispatchSubAgents,
84
- resolveMessageTarget,
85
- sendUnmatchedAgentNotice,
86
- } from "./targeting/agent-routing";
87
- import { formatGroupMembers, noteGroupMember } from "./targeting/group-members-store";
57
+ normalizeAllowFrom,
58
+ isSenderAllowed,
59
+ resolveGroupAccess,
60
+ } from "../platform/access-control";
61
+ import { getAccessToken } from "../platform/auth";
88
62
  import {
89
- upsertObservedGroupTarget,
90
- upsertObservedUserTarget,
91
- } from "./targeting/target-directory-store";
92
- import { AICardStatus } from "./types";
63
+ resolveAckReactionSetting,
64
+ resolveGroupConfig,
65
+ resolveRelativePath,
66
+ resolveRobotCode,
67
+ } from "../platform/config";
68
+ import { setCurrentLogger } from "../platform/logger-context";
69
+ import { getDingTalkRuntime } from "../platform/runtime";
70
+ import { createRuntimeEventsFanout } from "../platform/runtime-events";
71
+ import { getSessionState, initSessionState } from "../platform/session-state";
72
+ import { AICardStatus } from "../platform/types";
93
73
  import type {
94
74
  DingTalkConfig,
95
75
  HandleDingTalkMessageParams,
96
76
  Logger,
97
77
  MediaFile,
98
78
  ResolvedDingTalkRoute,
99
- } from "./types";
79
+ } from "../platform/types";
80
+ import axios from "../shared/http-client";
100
81
  import {
101
82
  formatDingTalkErrorPayloadLog,
102
83
  getErrorMessage,
103
84
  getErrorResponseData,
104
85
  maskSensitiveData,
105
86
  parseBooleanLike,
106
- } from "./utils";
87
+ } from "../shared/utils";
88
+ import { getAgentDisplayName } from "../targeting/agent-name-matcher";
89
+ import {
90
+ buildAgentSessionKey,
91
+ dispatchSubAgents,
92
+ resolveMessageTarget,
93
+ sendUnmatchedAgentNotice,
94
+ } from "../targeting/agent-routing";
95
+ import { formatGroupMembers, noteGroupMember } from "../targeting/group-members-store";
96
+ import { registerPeerId } from "../targeting/peer-id-registry";
97
+ import {
98
+ clearSessionPeerOverride,
99
+ getSessionPeerOverride,
100
+ setSessionPeerOverride,
101
+ } from "../targeting/session-peer-store";
102
+ import { resolveDingTalkSessionPeer } from "../targeting/session-routing";
103
+ import {
104
+ upsertObservedGroupTarget,
105
+ upsertObservedUserTarget,
106
+ } from "../targeting/target-directory-store";
107
+ import { dispatchInboundViaSessionQueue } from "./inbound-session-queue-dispatcher";
108
+ import {
109
+ isReplySessionConflictError,
110
+ withReplySessionConflictRetry,
111
+ } from "./reply-session-conflict";
112
+ import { acquireSessionLock } from "./session-lock";
107
113
 
108
114
  const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
109
115
  const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
@@ -431,14 +437,25 @@ function buildGroupTurnContextPrompt(params: {
431
437
  conversationId: string;
432
438
  senderDingtalkId: string;
433
439
  senderName?: string;
440
+ mentionedDingtalkIds?: readonly string[];
434
441
  }): string {
435
442
  const sanitizedSenderName = sanitizeGroupPromptName(params.senderName) || "Unknown";
443
+ const mentionedDingtalkIds = [
444
+ ...new Set(
445
+ (params.mentionedDingtalkIds || [])
446
+ .map((value) => value.replace(/[\r\n,=]/g, " ").trim())
447
+ .filter(Boolean),
448
+ ),
449
+ ];
436
450
  return [
437
451
  "Current DingTalk group turn context:",
438
452
  `- conversationId: ${params.conversationId}`,
439
453
  `- senderDingtalkId: ${params.senderDingtalkId}`,
440
454
  `- senderName: ${sanitizedSenderName}`,
455
+ `- mentionedDingtalkIds: ${mentionedDingtalkIds.length > 0 ? mentionedDingtalkIds.join(", ") : "(none provided)"}`,
441
456
  "Treat senderDingtalkId and senderName as the authoritative sender for this turn. Do not guess the current sender from GroupMembers.",
457
+ "Treat mentionedDingtalkIds as raw structured IDs only; do not infer a display name or map them to text mentions without payload evidence.",
458
+ "mentionedDingtalkIds are webhook atUsers/richText IDs (for example $:LWCP_v1:$...), not staff IDs; they are not comparable with senderDingtalkId or GroupMembers ids and must not be used as send targets.",
442
459
  ].join("\n");
443
460
  }
444
461
 
@@ -1007,6 +1024,17 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1007
1024
  const quotedRef = buildInboundQuotedRef(data, extractedContent);
1008
1025
  const replyQuotedRef = createReplyQuotedRef(data.msgId);
1009
1026
  const content = extractedContent;
1027
+ const mentionUserIds = [
1028
+ ...new Set(
1029
+ [
1030
+ ...(content.atUserDingtalkIds || []),
1031
+ ...(content.atMentions || []).map((mention) => mention.userId),
1032
+ ]
1033
+ .filter((value): value is string => typeof value === "string")
1034
+ .map((value) => value.trim())
1035
+ .filter(Boolean),
1036
+ ),
1037
+ ];
1010
1038
  // Decide control-message bypasses once from the user-authored text. Reusing
1011
1039
  // the result below keeps /stop out of the FIFO queue without calling the SDK
1012
1040
  // classifier a second time after attachment/OCR enrichment.
@@ -1081,7 +1109,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1081
1109
  // abort branch finalizes the card with the abort confirmation text instead of
1082
1110
  // sending a separate plain-text message.
1083
1111
  let useCardMode = dingtalkConfig.messageType === "card";
1084
- let currentAICard: import("./types").AICardInstance | undefined;
1112
+ let currentAICard: import("../platform/types").AICardInstance | undefined;
1085
1113
  let questionCardTookOver = false;
1086
1114
 
1087
1115
  let cardFlightKey: string | undefined;
@@ -1247,6 +1275,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1247
1275
  quotedRef,
1248
1276
  senderId,
1249
1277
  senderName,
1278
+ mentions: content.atMentions?.map((mention) => mention.name),
1279
+ mentionUserIds,
1280
+ chatType: isDirect ? "direct" : "group",
1250
1281
  createdAt: data.createAt,
1251
1282
  ttlMs: ttlDaysToMs(journalTTLDays),
1252
1283
  ttlReferenceMs: data.createAt,
@@ -1779,6 +1810,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1779
1810
  conversationId: groupId,
1780
1811
  senderDingtalkId: senderId,
1781
1812
  senderName,
1813
+ mentionedDingtalkIds: mentionUserIds,
1782
1814
  }),
1783
1815
  groupConfig?.systemPrompt?.trim(),
1784
1816
  ]
@@ -1792,7 +1824,10 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1792
1824
  const groupMembers = !isDirect ? formatGroupMembers(storePath, groupId) : undefined;
1793
1825
 
1794
1826
  const fromLabel = isDirect ? `${senderName} (${senderId})` : `${groupName} - ${senderName}`;
1795
- const body = rt.channel.reply.formatInboundEnvelope({
1827
+ // `rt.channel.reply.formatInboundEnvelope` was removed from the runtime
1828
+ // facade in OpenClaw 2026.8.x; the helper is still exported from the
1829
+ // `channel-inbound` SDK subpath.
1830
+ const body = formatInboundEnvelope({
1796
1831
  channel: "DingTalk",
1797
1832
  from: fromLabel,
1798
1833
  timestamp: data.createAt,
@@ -2260,7 +2295,12 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2260
2295
  };
2261
2296
  }
2262
2297
  ).events;
2298
+ const replyRuntimeEvents = createRuntimeEventsFanout(runtimeEvents, { log });
2263
2299
  const releaseSessionLock = await acquireSessionLock(route.sessionKey);
2300
+ // Declared outside the dispatch try-block so the finally below can always
2301
+ // release strategy-owned resources, including early returns that neither
2302
+ // finalize nor abort (ask-user question-card takeover).
2303
+ let strategyForCleanup: ReplyStrategy | undefined;
2264
2304
  const dynamicAckReactionController = createDynamicAckReactionController({
2265
2305
  enabled: shouldTrackDynamicAckReaction,
2266
2306
  initialReaction: resolvedAckReaction || "",
@@ -2271,7 +2311,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2271
2311
  conversationId: groupId,
2272
2312
  sessionKey: route.sessionKey,
2273
2313
  log,
2274
- runtimeEvents,
2314
+ runtimeEvents: replyRuntimeEvents,
2275
2315
  onReactionDisposed: () => {
2276
2316
  ackReactionAttached = false;
2277
2317
  },
@@ -2346,7 +2386,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2346
2386
  isStopRequested: isCurrentCardStopRequested,
2347
2387
  inboundText: rawInboundText,
2348
2388
  taskMeta,
2389
+ runtimeEvents: replyRuntimeEvents,
2349
2390
  });
2391
+ strategyForCleanup = strategy;
2350
2392
 
2351
2393
  let deliveredFinalCount = 0;
2352
2394
  // Extracted as a thunk so reply-session init conflicts (raised by the
@@ -2530,19 +2572,36 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2530
2572
  }
2531
2573
  await strategy.finalize();
2532
2574
  } finally {
2533
- // Only remove the registry entry if no stop was requested. When a stop is
2534
- // in progress, card-stop-handler may still be running async operations
2535
- // (finalize card, hide button, gateway abort) that read the record.
2536
- // In that case, let the 30-minute TTL sweep handle cleanup.
2537
- if (currentOutTrackId && !isCardRunStopRequested(currentOutTrackId)) {
2538
- removeCardRun(currentOutTrackId);
2575
+ try {
2576
+ // Only remove the registry entry if no stop was requested. When a stop is
2577
+ // in progress, card-stop-handler may still be running async operations
2578
+ // (finalize card, hide button, gateway abort) that read the record.
2579
+ // In that case, let the 30-minute TTL sweep handle cleanup.
2580
+ if (currentOutTrackId && !isCardRunStopRequested(currentOutTrackId)) {
2581
+ removeCardRun(currentOutTrackId);
2582
+ }
2583
+ // Guarantee strategy-owned cleanup on every exit path, including the
2584
+ // question-card takeover return above. `dispose()` is idempotent, so a
2585
+ // prior finalize()/abort() already having disposed is harmless. A
2586
+ // failing dispose is logged and swallowed: it must not skip the
2587
+ // remaining cleanup below.
2588
+ if (strategyForCleanup) {
2589
+ await strategyForCleanup.dispose().catch((disposeErr: unknown) => {
2590
+ log?.warn?.(
2591
+ `[DingTalk] Reply strategy cleanup failed for session ${route.sessionKey}: ${getErrorMessage(disposeErr)}`,
2592
+ );
2593
+ });
2594
+ }
2595
+ await waitForDynamicAckDispose({
2596
+ dispose: () => dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS),
2597
+ log,
2598
+ sessionKey: route.sessionKey,
2599
+ });
2600
+ } finally {
2601
+ // Unconditional: a session lock that never releases blocks every later
2602
+ // message on this session, so nothing above may prevent it.
2603
+ releaseSessionLock();
2539
2604
  }
2540
- await waitForDynamicAckDispose({
2541
- dispose: () => dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS),
2542
- log,
2543
- sessionKey: route.sessionKey,
2544
- });
2545
- releaseSessionLock();
2546
2605
  }
2547
2606
  } finally {
2548
2607
  if (cardFlightKey) {
@@ -21,13 +21,20 @@
21
21
  // orchestrator, adapted to soimy's blocking gateway contract (we await each
22
22
  // task so the gateway's per-message dedup stays correct).
23
23
 
24
- import { attachNativeAckReaction } from "../ack-reaction-service";
24
+ import { attachNativeAckReaction } from "../ack-reaction/ack-reaction-service";
25
25
  import {
26
26
  createAICard,
27
27
  isCardInTerminalState,
28
28
  recallAICardMessage,
29
29
  streamAICard,
30
- } from "../card-service";
30
+ } from "../card/card-service";
31
+ import { sendMessage } from "../messaging/send-service";
32
+ import type {
33
+ AICardInstance,
34
+ DingTalkConfig,
35
+ DingTalkInboundMessage,
36
+ Logger,
37
+ } from "../platform/types";
31
38
  import {
32
39
  chainInboundSessionTask,
33
40
  getInboundSessionQueueDepth,
@@ -37,8 +44,6 @@ import {
37
44
  MAX_INBOUND_SESSION_QUEUE_WAIT_MS,
38
45
  pickQueueBusyAckPhrase,
39
46
  } from "./inbound-session-queue";
40
- import { sendMessage } from "../send-service";
41
- import type { AICardInstance, DingTalkConfig, DingTalkInboundMessage, Logger } from "../types";
42
47
 
43
48
  export interface InboundQueueDispatchInput {
44
49
  accountId: string;
@@ -138,13 +143,14 @@ export async function dispatchInboundViaSessionQueue<T>(
138
143
  // slot below. Otherwise a burst of inbound messages can all observe the
139
144
  // same pre-await depth and each pass the cap check.
140
145
  let queuedAckState: "queued" | "timed-out" = "queued";
141
- const preCreatedCardPromise = wasBusy && shouldPrepareQueueAckCard(input)
142
- ? tryPrepareQueueAckCard(input, () =>
143
- queuedAckState === "timed-out"
144
- ? { content: QUEUE_WAIT_TIMEOUT_ACK, finished: true }
145
- : { content: pickQueueBusyAckPhrase(), finished: false },
146
- )
147
- : undefined;
146
+ const preCreatedCardPromise =
147
+ wasBusy && shouldPrepareQueueAckCard(input)
148
+ ? tryPrepareQueueAckCard(input, () =>
149
+ queuedAckState === "timed-out"
150
+ ? { content: QUEUE_WAIT_TIMEOUT_ACK, finished: true }
151
+ : { content: pickQueueBusyAckPhrase(), finished: false },
152
+ )
153
+ : undefined;
148
154
  // Chain onto the prior task for this conversation and AWAIT. Awaiting (rather
149
155
  // than fire-and-forget) preserves the gateway's per-message dedup:
150
156
  // `markMessageProcessed` runs only after this message truly completes, so a
@@ -153,9 +159,7 @@ export async function dispatchInboundViaSessionQueue<T>(
153
159
  return await chainInboundSessionTask(
154
160
  queueKey,
155
161
  async () => {
156
- const preCreatedCard = preCreatedCardPromise
157
- ? await preCreatedCardPromise
158
- : undefined;
162
+ const preCreatedCard = preCreatedCardPromise ? await preCreatedCardPromise : undefined;
159
163
  if (!preCreatedCard) {
160
164
  return handler(undefined);
161
165
  }
@@ -13,22 +13,22 @@ export const SESSION_LOCK_NAMESPACE_POLICY = "memory-only" as const;
13
13
  * Different session keys run in parallel; same key runs serially.
14
14
  */
15
15
  export async function acquireSessionLock(sessionKey: string): Promise<() => void> {
16
- let release!: () => void;
17
- const gate = new Promise<void>((r) => {
18
- release = r;
19
- });
20
- const prev = locks.get(sessionKey) ?? Promise.resolve();
21
- locks.set(sessionKey, gate);
22
- await prev;
23
- return () => {
24
- if (locks.get(sessionKey) === gate) {
25
- locks.delete(sessionKey);
26
- }
27
- release();
28
- };
16
+ let release!: () => void;
17
+ const gate = new Promise<void>((r) => {
18
+ release = r;
19
+ });
20
+ const prev = locks.get(sessionKey) ?? Promise.resolve();
21
+ locks.set(sessionKey, gate);
22
+ await prev;
23
+ return () => {
24
+ if (locks.get(sessionKey) === gate) {
25
+ locks.delete(sessionKey);
26
+ }
27
+ release();
28
+ };
29
29
  }
30
30
 
31
31
  /** Visible for testing only. */
32
32
  export function _getLocksMapForTest(): Map<string, Promise<void>> {
33
- return locks;
33
+ return locks;
34
34
  }
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { AttachmentTextSource } from "../types";
3
+ import type { AttachmentTextSource } from "../platform/types";
4
4
 
5
5
  const MAX_EXTRACTED_TEXT_CHARS = 6000;
6
6
  const MAX_ATTACHMENT_EXTRACT_BYTES = 2 * 1024 * 1024;
@@ -37,7 +37,8 @@ function isTextLikeMimeType(mimeType: string | undefined): boolean {
37
37
  function normalizeWhitespace(text: string): string {
38
38
  return text
39
39
  .replace(/\r\n/g, "\n")
40
- .split("\u0000").join("")
40
+ .split("\u0000")
41
+ .join("")
41
42
  .replace(/[ \t]{2,}/g, " ")
42
43
  .replace(/[ \t]+\n/g, "\n")
43
44
  .replace(/\n{3,}/g, "\n\n")
@@ -50,7 +51,9 @@ function limitExtractedText(text: string): AttachmentTextExtractionResult | null
50
51
  return null;
51
52
  }
52
53
  const truncated = normalized.length > MAX_EXTRACTED_TEXT_CHARS;
53
- const limited = truncated ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}\n\n[内容已截断]` : normalized;
54
+ const limited = truncated
55
+ ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}\n\n[内容已截断]`
56
+ : normalized;
54
57
  return {
55
58
  text: limited,
56
59
  truncated,
@@ -68,10 +71,12 @@ function stripHtml(html: string): string {
68
71
  .replace(/&gt;/gi, ">")
69
72
  .replace(/&amp;/gi, "&")
70
73
  .replace(/&#39;/gi, "'")
71
- .replace(/&quot;/gi, "\"");
74
+ .replace(/&quot;/gi, '"');
72
75
  }
73
76
 
74
- async function extractTextLikeFile(filePath: string): Promise<AttachmentTextExtractionResult | null> {
77
+ async function extractTextLikeFile(
78
+ filePath: string,
79
+ ): Promise<AttachmentTextExtractionResult | null> {
75
80
  const raw = await fs.readFile(filePath, "utf8");
76
81
  return limitExtractedText(raw);
77
82
  }
@@ -108,7 +113,11 @@ export async function extractAttachmentText(
108
113
  const mimeType = input.mimeType?.toLowerCase();
109
114
  const fileName = (input.fileName || path.basename(input.path)).toLowerCase();
110
115
 
111
- if (mimeType?.startsWith("image/") || mimeType?.startsWith("audio/") || mimeType?.startsWith("video/")) {
116
+ if (
117
+ mimeType?.startsWith("image/") ||
118
+ mimeType?.startsWith("audio/") ||
119
+ mimeType?.startsWith("video/")
120
+ ) {
112
121
  return null;
113
122
  }
114
123
 
@@ -127,7 +136,12 @@ export async function extractAttachmentText(
127
136
  return extractPdf(input.path);
128
137
  }
129
138
 
130
- if (mimeType === "text/html" || mimeType === "application/xhtml+xml" || fileName.endsWith(".html") || fileName.endsWith(".htm")) {
139
+ if (
140
+ mimeType === "text/html" ||
141
+ mimeType === "application/xhtml+xml" ||
142
+ fileName.endsWith(".html") ||
143
+ fileName.endsWith(".htm")
144
+ ) {
131
145
  return extractHtml(input.path);
132
146
  }
133
147
 
@@ -1,5 +1,5 @@
1
- import { sendMessage } from "../send-service";
2
- import type { DingTalkConfig, Logger } from "../types";
1
+ import type { DingTalkConfig, Logger } from "../platform/types";
2
+ import { sendMessage } from "./send-service";
3
3
 
4
4
  const MAX_QUESTION_LENGTH = 80;
5
5
  const LEADING_MENTIONS_RE = /^(?:@\S+\s+)*/u;
@@ -3,12 +3,12 @@ import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-co
3
3
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
4
4
  import { readStringParam } from "openclaw/plugin-sdk/param-readers";
5
5
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
6
- import { getConfig, stripTargetPrefix } from "../config";
7
- import { getLogger } from "../logger-context";
8
- import { resolveOriginalPeerId } from "../peer-id-registry";
9
- import { hasConfiguredSecretInput } from "../secret-input";
10
- import { sendMedia, sendMessage } from "../send-service";
11
- import { parseBooleanLike } from "../utils";
6
+ import { getConfig, stripTargetPrefix } from "../platform/config";
7
+ import { getLogger } from "../platform/logger-context";
8
+ import { hasConfiguredSecretInput } from "../platform/secret-input";
9
+ import { parseBooleanLike } from "../shared/utils";
10
+ import { resolveOriginalPeerId } from "../targeting/peer-id-registry";
11
+ import { sendMedia, sendMessage } from "./send-service";
12
12
 
13
13
  function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
14
14
  return parseBooleanLike(params[key]);
@@ -67,7 +67,15 @@ export function createDingTalkMessageActions(): ChannelMessageActionAdapter {
67
67
  describeMessageTool: ({ cfg }) => describeDingTalkMessageTool(cfg),
68
68
  supportsAction: ({ action }) => action === "send",
69
69
  extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
70
- handleAction: async ({ action, params, cfg, accountId, dryRun, mediaLocalRoots, sessionKey }) => {
70
+ handleAction: async ({
71
+ action,
72
+ params,
73
+ cfg,
74
+ accountId,
75
+ dryRun,
76
+ mediaLocalRoots,
77
+ sessionKey,
78
+ }) => {
71
79
  if (action !== "send") {
72
80
  throw new Error(`Action ${action} is not supported for provider dingtalk.`);
73
81
  }
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { getConfig } from "../config";
3
- import { getLogger } from "../logger-context";
4
- import { getDingTalkRuntime } from "../runtime";
5
- import { sendMedia, sendMessage } from "../send-service";
2
+ import { getConfig } from "../platform/config";
3
+ import { getLogger } from "../platform/logger-context";
4
+ import { getDingTalkRuntime } from "../platform/runtime";
5
+ import type { DingTalkChannelPlugin } from "../platform/types";
6
+ import { formatDingTalkErrorPayloadLog, parseBooleanLike } from "../shared/utils";
6
7
  import { normalizeResolvedDingTalkTarget } from "../targeting/target-directory-adapter";
7
- import type { DingTalkChannelPlugin } from "../types";
8
- import { formatDingTalkErrorPayloadLog, parseBooleanLike } from "../utils";
8
+ import { sendMedia, sendMessage } from "./send-service";
9
9
 
10
10
  function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
11
11
  return parseBooleanLike(params[key]);
@@ -66,7 +66,9 @@ export function createDingTalkOutbound(): NonNullable<DingTalkChannelPlugin["out
66
66
  };
67
67
  } catch (err: any) {
68
68
  if (err?.response?.data !== undefined) {
69
- effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
69
+ effectiveLog?.error?.(
70
+ formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data),
71
+ );
70
72
  }
71
73
  throw new Error(
72
74
  typeof err?.response?.data === "string"
@@ -112,9 +114,10 @@ export function createDingTalkOutbound(): NonNullable<DingTalkChannelPlugin["out
112
114
  );
113
115
  }
114
116
 
115
- const requestedMediaType = typeof providedMediaType === "string"
116
- ? (providedMediaType as "image" | "voice" | "video" | "file")
117
- : undefined;
117
+ const requestedMediaType =
118
+ typeof providedMediaType === "string"
119
+ ? (providedMediaType as "image" | "voice" | "video" | "file")
120
+ : undefined;
118
121
 
119
122
  try {
120
123
  const result = await sendMedia(config, to, rawMediaPath, {
@@ -144,7 +147,9 @@ export function createDingTalkOutbound(): NonNullable<DingTalkChannelPlugin["out
144
147
  };
145
148
  } catch (err: any) {
146
149
  if (err?.response?.data !== undefined) {
147
- effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
150
+ effectiveLog?.error?.(
151
+ formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data),
152
+ );
148
153
  }
149
154
  throw new Error(
150
155
  typeof err?.response?.data === "string"