@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
@@ -2,109 +2,114 @@ import fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound";
4
4
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/reply-runtime";
5
- import { parseInlineDirectives } from "./messaging/inline-directives";
6
- import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
7
- import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
8
- import { attachNativeAckReaction } from "./ack-reaction-service";
9
- import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
10
- import { getAccessToken } from "./auth";
11
- import {
12
- createAICard,
13
- commitAICardBlocks,
14
- isCardInTerminalState,
15
- recallAICardMessage,
16
- } 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";
17
8
  import {
18
9
  invalidateAskUserQuestionsForScope,
19
10
  syncInvalidatedAskUserQuestionCards,
20
- } from "./card/ask-user-question";
11
+ } from "../card/ask-user-question";
21
12
  import {
22
13
  getDingTalkQuestionContext,
23
14
  withDingTalkQuestionContext,
24
- } from "./card/ask-user-question-context";
25
- import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry";
26
- import { renderStatusLine } from "./card/statusline-renderer";
27
- import { resolveConfiguredTaskModelMetadata } from "./card/task-model-metadata";
28
- import { dispatchDingTalkCardStopCommand } from "./command/card-stop-command";
29
- 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";
30
17
  import {
31
- resolveAckReactionSetting,
32
- resolveGroupConfig,
33
- resolveRelativePath,
34
- resolveRobotCode,
35
- } from "./config";
36
- import { buildLearningContextBlock, isLearningEnabled } from "./feedback-learning-service";
37
- import axios from "./http-client";
38
- import { dispatchInboundViaSessionQueue } from "./gateway/inbound-session-queue-dispatcher";
39
- import { setCurrentLogger } from "./logger-context";
40
- 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";
41
32
  import {
42
33
  DEFAULT_MEDIA_CONTEXT_TTL_MS,
43
34
  DEFAULT_MESSAGE_CONTEXT_TTL_DAYS,
44
35
  upsertInboundMessageContext,
45
- } from "./message-context-store";
46
- import { extractMessageContent } from "./message-utils";
47
- import { extractAttachmentText } from "./messaging/attachment-text-extractor";
48
- import { deliverBtwReply, stripLeadingMentions } from "./messaging/btw-deliver";
49
- 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";
50
43
  import {
51
44
  downloadGroupFile,
52
45
  getUnionIdByStaffId,
53
46
  resolveQuotedFile,
54
- } from "./messaging/quoted-file-service";
47
+ } from "../messaging/quoted-file-service";
55
48
  import {
56
49
  buildInboundQuotedRef,
57
50
  createReplyQuotedRef,
58
51
  resolveQuotedRecord,
59
- } from "./messaging/quoted-ref";
60
- 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";
61
56
  import {
62
- clearProactiveRiskObservationsForTest,
63
- getProactiveRiskObservationForAny,
64
- } from "./proactive-risk-registry";
65
- import {
66
- isReplySessionConflictError,
67
- withReplySessionConflictRetry,
68
- } from "./gateway/reply-session-conflict";
69
- import { createReplyStrategy } from "./reply-strategy";
70
- import type { DeliverPayload } from "./reply-strategy-types";
71
- import { getDingTalkRuntime } from "./runtime";
72
- import { sendBySession, sendMessage, sendProactiveMedia } from "./send-service";
73
- import { acquireSessionLock } from "./session-lock";
74
- import {
75
- clearSessionPeerOverride,
76
- getSessionPeerOverride,
77
- setSessionPeerOverride,
78
- } from "./session-peer-store";
79
- import { resolveDingTalkSessionPeer } from "./session-routing";
80
- import { getSessionState, initSessionState } from "./session-state";
81
- import { getAgentDisplayName } from "./targeting/agent-name-matcher";
82
- import {
83
- buildAgentSessionKey,
84
- dispatchSubAgents,
85
- resolveMessageTarget,
86
- sendUnmatchedAgentNotice,
87
- } from "./targeting/agent-routing";
88
- 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";
89
62
  import {
90
- upsertObservedGroupTarget,
91
- upsertObservedUserTarget,
92
- } from "./targeting/target-directory-store";
93
- 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";
94
73
  import type {
95
74
  DingTalkConfig,
96
75
  HandleDingTalkMessageParams,
97
76
  Logger,
98
77
  MediaFile,
99
78
  ResolvedDingTalkRoute,
100
- } from "./types";
79
+ } from "../platform/types";
80
+ import axios from "../shared/http-client";
101
81
  import {
102
82
  formatDingTalkErrorPayloadLog,
103
83
  getErrorMessage,
104
84
  getErrorResponseData,
105
85
  maskSensitiveData,
106
86
  parseBooleanLike,
107
- } 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";
108
113
 
109
114
  const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
110
115
  const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
@@ -432,14 +437,25 @@ function buildGroupTurnContextPrompt(params: {
432
437
  conversationId: string;
433
438
  senderDingtalkId: string;
434
439
  senderName?: string;
440
+ mentionedDingtalkIds?: readonly string[];
435
441
  }): string {
436
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
+ ];
437
450
  return [
438
451
  "Current DingTalk group turn context:",
439
452
  `- conversationId: ${params.conversationId}`,
440
453
  `- senderDingtalkId: ${params.senderDingtalkId}`,
441
454
  `- senderName: ${sanitizedSenderName}`,
455
+ `- mentionedDingtalkIds: ${mentionedDingtalkIds.length > 0 ? mentionedDingtalkIds.join(", ") : "(none provided)"}`,
442
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.",
443
459
  ].join("\n");
444
460
  }
445
461
 
@@ -1008,6 +1024,17 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1008
1024
  const quotedRef = buildInboundQuotedRef(data, extractedContent);
1009
1025
  const replyQuotedRef = createReplyQuotedRef(data.msgId);
1010
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
+ ];
1011
1038
  // Decide control-message bypasses once from the user-authored text. Reusing
1012
1039
  // the result below keeps /stop out of the FIFO queue without calling the SDK
1013
1040
  // classifier a second time after attachment/OCR enrichment.
@@ -1082,7 +1109,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1082
1109
  // abort branch finalizes the card with the abort confirmation text instead of
1083
1110
  // sending a separate plain-text message.
1084
1111
  let useCardMode = dingtalkConfig.messageType === "card";
1085
- let currentAICard: import("./types").AICardInstance | undefined;
1112
+ let currentAICard: import("../platform/types").AICardInstance | undefined;
1086
1113
  let questionCardTookOver = false;
1087
1114
 
1088
1115
  let cardFlightKey: string | undefined;
@@ -1248,6 +1275,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1248
1275
  quotedRef,
1249
1276
  senderId,
1250
1277
  senderName,
1278
+ mentions: content.atMentions?.map((mention) => mention.name),
1279
+ mentionUserIds,
1280
+ chatType: isDirect ? "direct" : "group",
1251
1281
  createdAt: data.createAt,
1252
1282
  ttlMs: ttlDaysToMs(journalTTLDays),
1253
1283
  ttlReferenceMs: data.createAt,
@@ -1780,6 +1810,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1780
1810
  conversationId: groupId,
1781
1811
  senderDingtalkId: senderId,
1782
1812
  senderName,
1813
+ mentionedDingtalkIds: mentionUserIds,
1783
1814
  }),
1784
1815
  groupConfig?.systemPrompt?.trim(),
1785
1816
  ]
@@ -2264,7 +2295,12 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2264
2295
  };
2265
2296
  }
2266
2297
  ).events;
2298
+ const replyRuntimeEvents = createRuntimeEventsFanout(runtimeEvents, { log });
2267
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;
2268
2304
  const dynamicAckReactionController = createDynamicAckReactionController({
2269
2305
  enabled: shouldTrackDynamicAckReaction,
2270
2306
  initialReaction: resolvedAckReaction || "",
@@ -2275,7 +2311,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2275
2311
  conversationId: groupId,
2276
2312
  sessionKey: route.sessionKey,
2277
2313
  log,
2278
- runtimeEvents,
2314
+ runtimeEvents: replyRuntimeEvents,
2279
2315
  onReactionDisposed: () => {
2280
2316
  ackReactionAttached = false;
2281
2317
  },
@@ -2350,7 +2386,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2350
2386
  isStopRequested: isCurrentCardStopRequested,
2351
2387
  inboundText: rawInboundText,
2352
2388
  taskMeta,
2389
+ runtimeEvents: replyRuntimeEvents,
2353
2390
  });
2391
+ strategyForCleanup = strategy;
2354
2392
 
2355
2393
  let deliveredFinalCount = 0;
2356
2394
  // Extracted as a thunk so reply-session init conflicts (raised by the
@@ -2534,19 +2572,36 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
2534
2572
  }
2535
2573
  await strategy.finalize();
2536
2574
  } finally {
2537
- // Only remove the registry entry if no stop was requested. When a stop is
2538
- // in progress, card-stop-handler may still be running async operations
2539
- // (finalize card, hide button, gateway abort) that read the record.
2540
- // In that case, let the 30-minute TTL sweep handle cleanup.
2541
- if (currentOutTrackId && !isCardRunStopRequested(currentOutTrackId)) {
2542
- 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();
2543
2604
  }
2544
- await waitForDynamicAckDispose({
2545
- dispose: () => dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS),
2546
- log,
2547
- sessionKey: route.sessionKey,
2548
- });
2549
- releaseSessionLock();
2550
2605
  }
2551
2606
  } finally {
2552
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"