@soimy/dingtalk 3.6.9 → 3.6.11

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 (182) hide show
  1. package/dist/index.d.ts +3 -12
  2. package/dist/index.js +1262 -320
  3. package/dist/src/access-control.d.ts +0 -1
  4. package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +0 -1
  5. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -1
  6. package/dist/src/ack-reaction/dynamic-ack-reaction-progress.d.ts +0 -1
  7. package/dist/src/ack-reaction-classifier.d.ts +0 -1
  8. package/dist/src/ack-reaction-service.d.ts +0 -1
  9. package/dist/src/auth.d.ts +0 -1
  10. package/dist/src/card/ask-user-question-context.d.ts +0 -1
  11. package/dist/src/card/ask-user-question-store.d.ts +0 -1
  12. package/dist/src/card/ask-user-question.d.ts +0 -1
  13. package/dist/src/card/card-action-handler.d.ts +0 -1
  14. package/dist/src/card/card-markdown-image-reroute.d.ts +0 -1
  15. package/dist/src/card/card-run-registry.d.ts +0 -1
  16. package/dist/src/card/card-stop-handler.d.ts +0 -1
  17. package/dist/src/card/card-streaming-mode.d.ts +0 -1
  18. package/dist/src/card/card-template.d.ts +0 -1
  19. package/dist/src/card/reasoning-answer-split.d.ts +0 -1
  20. package/dist/src/card/reasoning-block-assembler.d.ts +0 -1
  21. package/dist/src/card/statusline-renderer.d.ts +0 -1
  22. package/dist/src/card/task-model-metadata.d.ts +0 -1
  23. package/dist/src/card-callback-service.d.ts +0 -1
  24. package/dist/src/card-draft-controller.d.ts +0 -1
  25. package/dist/src/card-service.d.ts +0 -1
  26. package/dist/src/channel.d.ts +0 -1
  27. package/dist/src/command/card-stop-command.d.ts +0 -1
  28. package/dist/src/command/inbound-command-dispatch-service.d.ts +0 -1
  29. package/dist/src/config-schema.d.ts +0 -1
  30. package/dist/src/config.d.ts +0 -1
  31. package/dist/src/connection-manager.d.ts +0 -1
  32. package/dist/src/dedup.d.ts +0 -1
  33. package/dist/src/device-registration.d.ts +0 -1
  34. package/dist/src/docs-service.d.ts +4 -5
  35. package/dist/src/draft-stream-loop.d.ts +0 -1
  36. package/dist/src/feedback-learning-service.d.ts +0 -1
  37. package/dist/src/feedback-learning-store.d.ts +0 -1
  38. package/dist/src/gateway/channel-gateway.d.ts +0 -1
  39. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +0 -1
  40. package/dist/src/gateway/inbound-session-queue.d.ts +0 -1
  41. package/dist/src/gateway/reply-session-conflict.d.ts +0 -1
  42. package/dist/src/http-client.d.ts +0 -1
  43. package/dist/src/inbound-handler.d.ts +0 -1
  44. package/dist/src/learning-command-service.d.ts +0 -1
  45. package/dist/src/logger-context.d.ts +0 -1
  46. package/dist/src/media-utils.d.ts +0 -1
  47. package/dist/src/message-context-store.d.ts +0 -1
  48. package/dist/src/message-utils.d.ts +0 -1
  49. package/dist/src/messaging/attachment-text-extractor.d.ts +0 -1
  50. package/dist/src/messaging/btw-deliver.d.ts +0 -1
  51. package/dist/src/messaging/channel-actions.d.ts +0 -1
  52. package/dist/src/messaging/channel-outbound.d.ts +0 -1
  53. package/dist/src/messaging/inline-directives.d.ts +42 -0
  54. package/dist/src/messaging/quoted-context.d.ts +0 -1
  55. package/dist/src/messaging/quoted-file-service.d.ts +0 -1
  56. package/dist/src/messaging/quoted-ref.d.ts +0 -1
  57. package/dist/src/onboarding.d.ts +0 -1
  58. package/dist/src/path-utils.d.ts +0 -1
  59. package/dist/src/peer-id-registry.d.ts +0 -12
  60. package/dist/src/persistence-store.d.ts +0 -1
  61. package/dist/src/platform/channel-status.d.ts +0 -1
  62. package/dist/src/plugin-sdk-channel-actions-augment.d.ts +0 -1
  63. package/dist/src/proactive-risk-registry.d.ts +0 -1
  64. package/dist/src/reply-strategy-card.d.ts +0 -1
  65. package/dist/src/reply-strategy-markdown.d.ts +0 -1
  66. package/dist/src/reply-strategy-types.d.ts +0 -1
  67. package/dist/src/reply-strategy-with-reaction.d.ts +0 -1
  68. package/dist/src/reply-strategy.d.ts +0 -1
  69. package/dist/src/run-usage-store.d.ts +0 -1
  70. package/dist/src/runtime.d.ts +0 -1
  71. package/dist/src/secret-input.d.ts +3 -3
  72. package/dist/src/send-service.d.ts +0 -1
  73. package/dist/src/session-command-service.d.ts +0 -1
  74. package/dist/src/session-lock.d.ts +0 -1
  75. package/dist/src/session-peer-store.d.ts +0 -1
  76. package/dist/src/session-routing.d.ts +0 -1
  77. package/dist/src/session-state.d.ts +0 -1
  78. package/dist/src/signature.d.ts +0 -1
  79. package/dist/src/targeting/agent-name-matcher.d.ts +0 -1
  80. package/dist/src/targeting/agent-routing.d.ts +0 -1
  81. package/dist/src/targeting/group-members-store.d.ts +0 -1
  82. package/dist/src/targeting/target-directory-adapter.d.ts +0 -1
  83. package/dist/src/targeting/target-directory-store.d.ts +0 -1
  84. package/dist/src/targeting/target-input.d.ts +0 -1
  85. package/dist/src/types.d.ts +1 -2
  86. package/dist/src/utils.d.ts +0 -1
  87. package/index.ts +8 -2
  88. package/openclaw.plugin.json +4 -3
  89. package/package.json +10 -8
  90. package/src/channel.ts +4 -3
  91. package/src/gateway/channel-gateway.ts +67 -4
  92. package/src/inbound-handler.ts +6 -2
  93. package/src/messaging/channel-actions.ts +1 -4
  94. package/src/messaging/inline-directives.ts +342 -0
  95. package/src/peer-id-registry.ts +0 -100
  96. package/src/secret-input.ts +33 -33
  97. package/src/types.ts +1 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -7
  100. package/dist/src/access-control.d.ts.map +0 -1
  101. package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts.map +0 -1
  102. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts.map +0 -1
  103. package/dist/src/ack-reaction/dynamic-ack-reaction-progress.d.ts.map +0 -1
  104. package/dist/src/ack-reaction-classifier.d.ts.map +0 -1
  105. package/dist/src/ack-reaction-service.d.ts.map +0 -1
  106. package/dist/src/auth.d.ts.map +0 -1
  107. package/dist/src/card/ask-user-question-context.d.ts.map +0 -1
  108. package/dist/src/card/ask-user-question-store.d.ts.map +0 -1
  109. package/dist/src/card/ask-user-question.d.ts.map +0 -1
  110. package/dist/src/card/card-action-handler.d.ts.map +0 -1
  111. package/dist/src/card/card-markdown-image-reroute.d.ts.map +0 -1
  112. package/dist/src/card/card-run-registry.d.ts.map +0 -1
  113. package/dist/src/card/card-stop-handler.d.ts.map +0 -1
  114. package/dist/src/card/card-streaming-mode.d.ts.map +0 -1
  115. package/dist/src/card/card-template.d.ts.map +0 -1
  116. package/dist/src/card/reasoning-answer-split.d.ts.map +0 -1
  117. package/dist/src/card/reasoning-block-assembler.d.ts.map +0 -1
  118. package/dist/src/card/statusline-renderer.d.ts.map +0 -1
  119. package/dist/src/card/task-model-metadata.d.ts.map +0 -1
  120. package/dist/src/card-callback-service.d.ts.map +0 -1
  121. package/dist/src/card-draft-controller.d.ts.map +0 -1
  122. package/dist/src/card-service.d.ts.map +0 -1
  123. package/dist/src/channel.d.ts.map +0 -1
  124. package/dist/src/command/card-stop-command.d.ts.map +0 -1
  125. package/dist/src/command/inbound-command-dispatch-service.d.ts.map +0 -1
  126. package/dist/src/config-schema.d.ts.map +0 -1
  127. package/dist/src/config.d.ts.map +0 -1
  128. package/dist/src/connection-manager.d.ts.map +0 -1
  129. package/dist/src/dedup.d.ts.map +0 -1
  130. package/dist/src/device-registration.d.ts.map +0 -1
  131. package/dist/src/docs-service.d.ts.map +0 -1
  132. package/dist/src/draft-stream-loop.d.ts.map +0 -1
  133. package/dist/src/feedback-learning-service.d.ts.map +0 -1
  134. package/dist/src/feedback-learning-store.d.ts.map +0 -1
  135. package/dist/src/gateway/channel-gateway.d.ts.map +0 -1
  136. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts.map +0 -1
  137. package/dist/src/gateway/inbound-session-queue.d.ts.map +0 -1
  138. package/dist/src/gateway/reply-session-conflict.d.ts.map +0 -1
  139. package/dist/src/http-client.d.ts.map +0 -1
  140. package/dist/src/inbound-handler.d.ts.map +0 -1
  141. package/dist/src/learning-command-service.d.ts.map +0 -1
  142. package/dist/src/logger-context.d.ts.map +0 -1
  143. package/dist/src/media-utils.d.ts.map +0 -1
  144. package/dist/src/message-context-store.d.ts.map +0 -1
  145. package/dist/src/message-utils.d.ts.map +0 -1
  146. package/dist/src/messaging/attachment-text-extractor.d.ts.map +0 -1
  147. package/dist/src/messaging/btw-deliver.d.ts.map +0 -1
  148. package/dist/src/messaging/channel-actions.d.ts.map +0 -1
  149. package/dist/src/messaging/channel-outbound.d.ts.map +0 -1
  150. package/dist/src/messaging/quoted-context.d.ts.map +0 -1
  151. package/dist/src/messaging/quoted-file-service.d.ts.map +0 -1
  152. package/dist/src/messaging/quoted-ref.d.ts.map +0 -1
  153. package/dist/src/onboarding.d.ts.map +0 -1
  154. package/dist/src/path-utils.d.ts.map +0 -1
  155. package/dist/src/peer-id-registry.d.ts.map +0 -1
  156. package/dist/src/persistence-store.d.ts.map +0 -1
  157. package/dist/src/platform/channel-status.d.ts.map +0 -1
  158. package/dist/src/plugin-sdk-channel-actions-augment.d.ts.map +0 -1
  159. package/dist/src/proactive-risk-registry.d.ts.map +0 -1
  160. package/dist/src/reply-strategy-card.d.ts.map +0 -1
  161. package/dist/src/reply-strategy-markdown.d.ts.map +0 -1
  162. package/dist/src/reply-strategy-types.d.ts.map +0 -1
  163. package/dist/src/reply-strategy-with-reaction.d.ts.map +0 -1
  164. package/dist/src/reply-strategy.d.ts.map +0 -1
  165. package/dist/src/run-usage-store.d.ts.map +0 -1
  166. package/dist/src/runtime.d.ts.map +0 -1
  167. package/dist/src/secret-input.d.ts.map +0 -1
  168. package/dist/src/send-service.d.ts.map +0 -1
  169. package/dist/src/session-command-service.d.ts.map +0 -1
  170. package/dist/src/session-lock.d.ts.map +0 -1
  171. package/dist/src/session-peer-store.d.ts.map +0 -1
  172. package/dist/src/session-routing.d.ts.map +0 -1
  173. package/dist/src/session-state.d.ts.map +0 -1
  174. package/dist/src/signature.d.ts.map +0 -1
  175. package/dist/src/targeting/agent-name-matcher.d.ts.map +0 -1
  176. package/dist/src/targeting/agent-routing.d.ts.map +0 -1
  177. package/dist/src/targeting/group-members-store.d.ts.map +0 -1
  178. package/dist/src/targeting/target-directory-adapter.d.ts.map +0 -1
  179. package/dist/src/targeting/target-directory-store.d.ts.map +0 -1
  180. package/dist/src/targeting/target-input.d.ts.map +0 -1
  181. package/dist/src/types.d.ts.map +0 -1
  182. package/dist/src/utils.d.ts.map +0 -1
@@ -8,7 +8,7 @@
8
8
  * - Media files and streams
9
9
  * - Session and token management
10
10
  */
11
- import type { ChannelAccountSnapshot as SDKChannelAccountSnapshot, ChannelGatewayContext as SDKChannelGatewayContext, ChannelLogSink as SDKChannelLogSink } from "openclaw/plugin-sdk/channel-runtime";
11
+ import type { ChannelAccountSnapshot as SDKChannelAccountSnapshot, ChannelGatewayContext as SDKChannelGatewayContext, ChannelLogSink as SDKChannelLogSink } from "openclaw/plugin-sdk/channel-contract";
12
12
  import type { ChannelPlugin as SDKChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
13
13
  import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
14
14
  import type { SecretInput } from "./secret-input";
@@ -769,4 +769,3 @@ export type CardBlock = {
769
769
  mediaId: string;
770
770
  text?: string;
771
771
  };
772
- //# sourceMappingURL=types.d.ts.map
@@ -51,4 +51,3 @@ export declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: Retr
51
51
  */
52
52
  export declare function getCurrentTimestamp(): number;
53
53
  export {};
54
- //# sourceMappingURL=utils.d.ts.map
package/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { defineChannelPluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
1
+ import {
2
+ defineChannelPluginEntry,
3
+ type OpenClawPluginApi,
4
+ type OpenClawPluginDefinition,
5
+ } from "openclaw/plugin-sdk/core";
2
6
  import { readStringParam } from "openclaw/plugin-sdk/param-readers";
3
7
  import { getAccessToken } from "./src/auth";
4
8
  import { registerDingTalkAskUserQuestionTool } from "./src/card/ask-user-question";
@@ -311,7 +315,7 @@ const dingtalkEntry = defineChannelPluginEntry({
311
315
  },
312
316
  });
313
317
 
314
- export default {
318
+ const pluginDefinition: OpenClawPluginDefinition = {
315
319
  ...dingtalkEntry,
316
320
  register(api: OpenClawPluginApi) {
317
321
  const registrationMode = api.registrationMode as string | undefined;
@@ -325,3 +329,5 @@ export default {
325
329
  dingtalkEntry.register(api);
326
330
  },
327
331
  };
332
+
333
+ export default pluginDefinition;
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "id": "dingtalk",
3
+ "name": "DingTalk",
3
4
  "channels": ["dingtalk"],
4
5
  "contracts": {
5
6
  "tools": ["dingtalk_ask_user_question"]
@@ -62,7 +63,7 @@
62
63
  }
63
64
  }
64
65
  ],
65
- "description": "DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. Supports SecretInput references such as env/file. Security boundary: file reads the configured local path; use file only with trusted plugin configuration."
66
+ "description": "DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. Supports env/file SecretInput references resolved by configured OpenClaw secret providers."
66
67
  },
67
68
  "dmPolicy": {
68
69
  "type": "string",
@@ -368,7 +369,7 @@
368
369
  }
369
370
  }
370
371
  ],
371
- "description": "DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. Supports SecretInput references such as env/file. Security boundary: file reads the configured local path; use file only with trusted plugin configuration."
372
+ "description": "DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. Supports env/file SecretInput references resolved by configured OpenClaw secret providers."
372
373
  },
373
374
  "dmPolicy": {
374
375
  "type": "string",
@@ -654,7 +655,7 @@
654
655
  },
655
656
  "clientSecret": {
656
657
  "label": "DingTalk Client Secret",
657
- "help": "App Secret used to obtain DingTalk access tokens. SecretInput file references are for trusted plugin configuration because file reads a local path.",
658
+ "help": "App Secret used to obtain DingTalk access tokens. SecretInput references are resolved by configured OpenClaw secret providers.",
658
659
  "sensitive": true
659
660
  },
660
661
  "dmPolicy": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soimy/dingtalk",
3
- "version": "3.6.9",
3
+ "version": "3.6.11",
4
4
  "description": "DingTalk (钉钉) channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
@@ -20,9 +20,7 @@
20
20
  },
21
21
  "files": [
22
22
  "dist/**/*.js",
23
- "dist/**/*.js.map",
24
23
  "dist/**/*.d.ts",
25
- "dist/**/*.d.ts.map",
26
24
  "index.ts",
27
25
  "src/**/*.ts",
28
26
  "docs/assets/dingtalk-ask-user-card-template.json",
@@ -31,6 +29,9 @@
31
29
  "LICENSE"
32
30
  ],
33
31
  "type": "module",
32
+ "engines": {
33
+ "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
34
+ },
34
35
  "packageManager": "pnpm@10.33.0",
35
36
  "main": "./dist/index.js",
36
37
  "types": "./dist/index.d.ts",
@@ -69,12 +70,13 @@
69
70
  "form-data": "^4.0.0",
70
71
  "mammoth": "^1.12.0",
71
72
  "pdf-parse": "^2.4.5",
72
- "zod": "^4.3.6"
73
+ "zod": "^4.4.3"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@types/node": "^25.2.0",
76
77
  "@vitest/coverage-v8": "^3.2.4",
77
78
  "esbuild": "^0.27.3",
79
+ "openclaw": "2026.7.1-2",
78
80
  "oxfmt": "0.34.0",
79
81
  "oxlint": "1.49.0",
80
82
  "oxlint-tsgolint": "0.18.1",
@@ -83,7 +85,7 @@
83
85
  "vitest": "^3.2.4"
84
86
  },
85
87
  "peerDependencies": {
86
- "openclaw": ">=2026.3.28"
88
+ "openclaw": ">=2026.7.1-2"
87
89
  },
88
90
  "peerDependenciesMeta": {
89
91
  "openclaw": {
@@ -92,10 +94,10 @@
92
94
  },
93
95
  "openclaw": {
94
96
  "compat": {
95
- "pluginApi": ">=2026.3.28"
97
+ "pluginApi": ">=2026.7.1-2"
96
98
  },
97
99
  "build": {
98
- "openclawVersion": "2026.3.28"
100
+ "openclawVersion": "2026.7.1-2"
99
101
  },
100
102
  "extensions": [
101
103
  "./index.ts"
@@ -121,7 +123,7 @@
121
123
  ]
122
124
  },
123
125
  "install": {
124
- "minHostVersion": ">=2026.3.28",
126
+ "minHostVersion": ">=2026.7.1-2",
125
127
  "npmSpec": "@soimy/dingtalk",
126
128
  "localPath": ".",
127
129
  "defaultChoice": "npm"
package/src/channel.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core";
1
+ import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
+ import pluginManifest from "../openclaw.plugin.json";
2
4
  import { getConfig, isConfigured, mergeAccountWithDefaults, resolveGroupConfig } from "./config";
3
- import { DingTalkConfigSchema } from "./config-schema.js";
4
5
  import {
5
6
  CHANNEL_INFLIGHT_NAMESPACE_POLICY,
6
7
  createDingTalkGateway,
@@ -29,7 +30,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
29
30
  blurb: "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP。",
30
31
  aliases: ["dd", "ding"],
31
32
  },
32
- configSchema: buildChannelConfigSchema(DingTalkConfigSchema),
33
+ configSchema: buildJsonChannelConfigSchema(pluginManifest.channelConfigs.dingtalk.schema),
33
34
  setup: dingtalkSetupAdapter,
34
35
  setupWizard: dingtalkSetupWizard,
35
36
  capabilities: {
@@ -1,4 +1,5 @@
1
1
  import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
2
+ import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
2
3
  import { analyzeCardCallback } from "../card-callback-service";
3
4
  import { finalizeActiveCardsForAccount, recoverPendingCardsForAccount } from "../card-service";
4
5
  import { recoverAskUserQuestionsForAccount } from "../card/ask-user-question";
@@ -13,9 +14,14 @@ import {
13
14
  } from "../feedback-learning-service";
14
15
  import { handleDingTalkMessage } from "../inbound-handler";
15
16
  import { setCurrentLogger } from "../logger-context";
16
- import { preloadPeerIdsFromSessions } from "../peer-id-registry";
17
+ import { registerPeerId } from "../peer-id-registry";
17
18
  import { getDingTalkRuntime } from "../runtime";
18
19
  import { sendProactiveTextOrMarkdown } from "../send-service";
20
+ import {
21
+ listKnownGroupTargets,
22
+ listKnownUserTargets,
23
+ upsertObservedGroupTarget,
24
+ } from "../targeting/target-directory-store";
19
25
  import type {
20
26
  ConnectionManagerConfig,
21
27
  DingTalkChannelPlugin,
@@ -180,6 +186,66 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
180
186
  debug: config.debug,
181
187
  baseLog: ctx.log,
182
188
  });
189
+ try {
190
+ const knownGroupIds = new Set<string>();
191
+ for (const group of listKnownGroupTargets({
192
+ storePath: accountStorePath,
193
+ accountId: account.accountId,
194
+ })) {
195
+ registerPeerId(group.conversationId);
196
+ knownGroupIds.add(group.conversationId.toLowerCase());
197
+ }
198
+ const runtime = getDingTalkRuntime();
199
+ for (const agentId of listAgentIds(cfg)) {
200
+ const sessionStorePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
201
+ agentId,
202
+ });
203
+ for (const { entry } of runtime.agent.session.listSessionEntries({
204
+ agentId,
205
+ storePath: sessionStorePath,
206
+ readConsistency: "latest",
207
+ })) {
208
+ const isDingTalkSession = [
209
+ entry.lastChannel,
210
+ entry.channel,
211
+ entry.origin?.provider,
212
+ entry.origin?.surface,
213
+ ].some((value) => value === "dingtalk");
214
+ const sessionAccountId = entry.lastAccountId || entry.origin?.accountId;
215
+ if (!isDingTalkSession || sessionAccountId !== account.accountId) {
216
+ continue;
217
+ }
218
+ for (const peerId of [entry.lastTo, entry.origin?.from, entry.origin?.to]) {
219
+ if (typeof peerId !== "string" || !peerId.startsWith("cid")) {
220
+ continue;
221
+ }
222
+ registerPeerId(peerId);
223
+ const normalizedPeerId = peerId.toLowerCase();
224
+ if (!knownGroupIds.has(normalizedPeerId)) {
225
+ upsertObservedGroupTarget({
226
+ storePath: accountStorePath,
227
+ accountId: account.accountId,
228
+ conversationId: peerId,
229
+ seenAt: entry.updatedAt,
230
+ });
231
+ knownGroupIds.add(normalizedPeerId);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ for (const user of listKnownUserTargets({
237
+ storePath: accountStorePath,
238
+ accountId: account.accountId,
239
+ })) {
240
+ registerPeerId(user.canonicalUserId);
241
+ registerPeerId(user.staffId || "");
242
+ registerPeerId(user.senderId);
243
+ }
244
+ } catch (err: unknown) {
245
+ pluginLog?.warn?.(
246
+ `[${account.accountId}] Failed to restore peer IDs: ${err instanceof Error ? err.message : String(err)}`,
247
+ );
248
+ }
183
249
  // Stream credentials are resolved once per account start. If a file
184
250
  // SecretInput rotates, restart the gateway/account so reconnects use the
185
251
  // new secret.
@@ -188,9 +254,6 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
188
254
 
189
255
  pluginLog?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
190
256
 
191
- preloadPeerIdsFromSessions();
192
- pluginLog?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
193
-
194
257
  cleanupOrphanedTempFiles(pluginLog);
195
258
  try {
196
259
  const recovered = await recoverPendingCardsForAccount(
@@ -1,7 +1,8 @@
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 { parseInlineDirectives } from "./messaging/inline-directives";
5
6
  import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
6
7
  import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
7
8
  import { attachNativeAckReaction } from "./ack-reaction-service";
@@ -1792,7 +1793,10 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
1792
1793
  const groupMembers = !isDirect ? formatGroupMembers(storePath, groupId) : undefined;
1793
1794
 
1794
1795
  const fromLabel = isDirect ? `${senderName} (${senderId})` : `${groupName} - ${senderName}`;
1795
- const body = rt.channel.reply.formatInboundEnvelope({
1796
+ // `rt.channel.reply.formatInboundEnvelope` was removed from the runtime
1797
+ // facade in OpenClaw 2026.8.x; the helper is still exported from the
1798
+ // `channel-inbound` SDK subpath.
1799
+ const body = formatInboundEnvelope({
1796
1800
  channel: "DingTalk",
1797
1801
  from: fromLabel,
1798
1802
  timestamp: data.createAt,
@@ -55,12 +55,9 @@ function describeDingTalkMessageTool(cfg: OpenClawConfig) {
55
55
  if (!configured && !(config.accounts && Object.keys(config.accounts).length > 0)) {
56
56
  return { actions: [], capabilities: [], schema: null };
57
57
  }
58
- const hasCardMode =
59
- config.messageType === "card" ||
60
- (config.accounts && Object.values(config.accounts).some((account) => account?.messageType === "card"));
61
58
  return {
62
59
  actions: ["send"] as const,
63
- capabilities: hasCardMode ? (["cards"] as const) : [],
60
+ capabilities: [],
64
61
  schema: null,
65
62
  };
66
63
  }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Inline directive parsing (local implementation).
3
+ *
4
+ * OpenClaw retired the `openclaw/plugin-sdk/text-runtime` subpath in
5
+ * 2026.8.x (docs/plugins/sdk-migration.md: "The August 15 compatibility
6
+ * subpaths ... `text-runtime` ... were retired early"). Its replacement
7
+ * `openclaw/plugin-sdk/text-chunking` only re-exports the directive-tag
8
+ * *stripping* helpers and the `InlineDirectiveParseResult` type - not
9
+ * `parseInlineDirectives` itself.
10
+ *
11
+ * This module reproduces the previous host behaviour inside the plugin so no
12
+ * removed SDK internals are imported. Code-region awareness (fenced,
13
+ * indented, and inline code) is sourced from the public SDK helpers
14
+ * `findCodeRegions` / `isInsideCode` (available since 2026.7.1-2), plus a
15
+ * lenient indented-block fallback matching the host whitespace normalizer so
16
+ * directives inside code are never treated as real voice/reply commands.
17
+ *
18
+ * The shipped 2026.7.1-2 SDK `findCodeRegions` is a simplified heuristic: it
19
+ * pairs backtick runs of different lengths, lets shorter fences close longer
20
+ * ones, treats escaped backticks as real delimiters, and pairs backticks
21
+ * inside indented code with prose. `findDelimiterCodeRegions` therefore layers
22
+ * a CommonMark-style fence/inline-code supplement over the SDK regions (union,
23
+ * conservative: in doubt the text stays literal) so protection holds on every
24
+ * supported host. Ponytail: remove the supplement once the minimum host is
25
+ * 2026.8+ and its SDK regions are verified to cover these cases.
26
+ */
27
+
28
+ import { findCodeRegions, isInsideCode, type CodeRegion } from "openclaw/plugin-sdk/text-chunking";
29
+
30
+ const AUDIO_TAG_RE = /\[\[\s*audio_as_voice\s*\]\]/gi;
31
+ const REPLY_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
32
+ const MAX_REPLY_DIRECTIVE_ID_LENGTH = 256;
33
+ const BLOCK_SENTINEL_SEED = "\uE000";
34
+ /** Lenient indented-code-block lines (4 spaces or tab), same rule as the host whitespace normalizer. */
35
+ const INDENTED_CODE_LINE_RE = /(?:^|\n)((?: {4}|\t)[^\n]*)(?:\n(?: {4}|\t)[^\n]*)*/g;
36
+
37
+ export type ParseInlineDirectivesOptions = {
38
+ currentMessageId?: string;
39
+ stripAudioTag?: boolean;
40
+ stripReplyTags?: boolean;
41
+ };
42
+
43
+ export type InlineDirectiveParseResult = {
44
+ text: string;
45
+ audioAsVoice: boolean;
46
+ replyToId?: string;
47
+ replyToExplicitId?: string;
48
+ replyToCurrent: boolean;
49
+ hasAudioTag: boolean;
50
+ hasReplyTag: boolean;
51
+ };
52
+
53
+ function normalizeOptionalString(value: unknown): string | undefined {
54
+ if (typeof value !== "string") {
55
+ return undefined;
56
+ }
57
+ const trimmed = value.trim();
58
+ return trimmed ? trimmed : undefined;
59
+ }
60
+
61
+ /**
62
+ * CommonMark code regions (fenced / indented / inline) from the SDK, extended
63
+ * with a lenient indented-block fallback for lines indented by 4 spaces or a
64
+ * tab that follow a paragraph without a blank separator (the host normalizer
65
+ * protects those too).
66
+ */
67
+ function resolveCodeRegions(text: string): CodeRegion[] {
68
+ const indentedRegions: CodeRegion[] = [];
69
+ const indentRe = new RegExp(INDENTED_CODE_LINE_RE.source, "g");
70
+ let match: RegExpExecArray | null;
71
+ while ((match = indentRe.exec(text)) !== null) {
72
+ const start = match.index + (match[0].charCodeAt(0) === 10 ? 1 : 0);
73
+ indentedRegions.push({ start, end: match.index + match[0].length });
74
+ }
75
+ const sdkRegions = findCodeRegions(text).filter(
76
+ (region) =>
77
+ (text[region.start] !== "`" || !isEscaped(text, region.start)) &&
78
+ !indentedRegions.some(
79
+ (indented) => region.start >= indented.start && region.start < indented.end,
80
+ ),
81
+ );
82
+ const knownRegions = mergeCodeRegions([...sdkRegions, ...indentedRegions]);
83
+ return mergeCodeRegions([...knownRegions, ...findDelimiterCodeRegions(text, knownRegions)]);
84
+ }
85
+
86
+ function isEscaped(text: string, offset: number): boolean {
87
+ let slashes = 0;
88
+ for (let index = offset - 1; text[index] === "\\"; index--) {
89
+ slashes++;
90
+ }
91
+ return slashes % 2 === 1;
92
+ }
93
+
94
+ function isStrictlyInsideCode(offset: number, regions: CodeRegion[]): boolean {
95
+ return regions.some((region) => offset > region.start && offset < region.end);
96
+ }
97
+
98
+ // ponytail: small 2026.7.x delimiter supplement; delete when the minimum host is 2026.8+.
99
+ function findDelimiterCodeRegions(text: string, knownRegions: CodeRegion[]): CodeRegion[] {
100
+ const regions: CodeRegion[] = [];
101
+ let match: RegExpExecArray | null;
102
+ const fences = /^( {0,3})(`{3,}|~{3,})[^\n]*(?:\n|$)/gm;
103
+ let open: { char: string; length: number; start: number } | undefined;
104
+ while ((match = fences.exec(text)) !== null) {
105
+ const delimiter = match[2];
106
+ if (!open) {
107
+ const suffix = match[0].slice(match[1].length + delimiter.length).trim();
108
+ if (
109
+ isStrictlyInsideCode(match.index, knownRegions) ||
110
+ (delimiter[0] === "`" && suffix.includes("`"))
111
+ ) {
112
+ continue;
113
+ }
114
+ open = { char: delimiter[0], length: delimiter.length, start: match.index };
115
+ } else if (
116
+ delimiter[0] === open.char &&
117
+ delimiter.length >= open.length &&
118
+ match[0].slice(match[1].length + delimiter.length).trim() === ""
119
+ ) {
120
+ regions.push({ start: open.start, end: fences.lastIndex });
121
+ open = undefined;
122
+ }
123
+ }
124
+ if (open) {
125
+ regions.push({ start: open.start, end: text.length });
126
+ }
127
+
128
+ const fencedRegions = [...regions];
129
+ const pendingTicks = new Map<number, number>();
130
+ const ticks = /`+/g;
131
+ while ((match = ticks.exec(text)) !== null) {
132
+ const length = match[0].length;
133
+ if (
134
+ isEscaped(text, match.index) ||
135
+ isInsideCode(match.index, fencedRegions) ||
136
+ (pendingTicks.get(length) === undefined && isStrictlyInsideCode(match.index, knownRegions))
137
+ ) {
138
+ continue;
139
+ }
140
+ const start = pendingTicks.get(length);
141
+ if (start === undefined) {
142
+ pendingTicks.set(length, match.index);
143
+ continue;
144
+ }
145
+ regions.push({ start, end: ticks.lastIndex });
146
+ for (const [pendingLength, pendingStart] of pendingTicks) {
147
+ if (pendingStart >= start) {
148
+ pendingTicks.delete(pendingLength);
149
+ }
150
+ }
151
+ }
152
+ return regions;
153
+ }
154
+
155
+ function mergeCodeRegions(regions: CodeRegion[]): CodeRegion[] {
156
+ if (regions.length <= 1) {
157
+ return regions;
158
+ }
159
+ const sorted = [...regions].toSorted((a, b) => a.start - b.start || a.end - b.end);
160
+ const merged: CodeRegion[] = [];
161
+ for (const region of sorted) {
162
+ const last = merged[merged.length - 1];
163
+ if (last && region.start <= last.end) {
164
+ last.end = Math.max(last.end, region.end);
165
+ } else {
166
+ merged.push({ start: region.start, end: region.end });
167
+ }
168
+ }
169
+ return merged;
170
+ }
171
+
172
+ function createBlockSentinel(text: string): string {
173
+ let sentinel = BLOCK_SENTINEL_SEED;
174
+ while (text.includes(sentinel)) {
175
+ sentinel += BLOCK_SENTINEL_SEED;
176
+ }
177
+ return sentinel;
178
+ }
179
+
180
+ function normalizeDirectiveWhitespace(text: string, codeRegions: CodeRegion[] = []): string {
181
+ const blockSentinel = createBlockSentinel(text);
182
+ const blockPlaceholderRe = new RegExp(`${blockSentinel}(\\d+)${blockSentinel}`, "g");
183
+ const blocks: string[] = [];
184
+
185
+ let masked = "";
186
+ let cursor = 0;
187
+ for (const region of codeRegions) {
188
+ if (region.start < cursor) {
189
+ continue;
190
+ }
191
+ blocks.push(text.slice(region.start, region.end));
192
+ masked += `${text.slice(cursor, region.start)}${blockSentinel}${blocks.length - 1}${blockSentinel}`;
193
+ cursor = region.end;
194
+ }
195
+ masked += text.slice(cursor);
196
+
197
+ return masked
198
+ .replace(/\r\n/g, "\n")
199
+ .replace(/([^\s])[ \t]{2,}([^\s])/g, "$1 $2")
200
+ .replace(/^\n+/, "")
201
+ .replace(/^[ \t](?=\S)/, "")
202
+ .replace(/[ \t]+\n/g, "\n")
203
+ .replace(/\n{3,}/g, "\n\n")
204
+ .trimEnd()
205
+ .replace(blockPlaceholderRe, (_full, index: string) => blocks[Number(index)] ?? "");
206
+ }
207
+
208
+ function replacementPreservesWordBoundary(source: string, offset: number, length: number): string {
209
+ const before = source[offset - 1];
210
+ const after = source[offset + length];
211
+ return before && after && !/\s/u.test(before) && !/\s/u.test(after) ? " " : "";
212
+ }
213
+
214
+ function stripUnsafeReplyDirectiveChars(value: string): string {
215
+ const chars: string[] = [];
216
+ for (const ch of value) {
217
+ const code = ch.charCodeAt(0);
218
+ if (
219
+ (code >= 0 && code <= 31) ||
220
+ code === 127 ||
221
+ (code >= 128 && code <= 159) ||
222
+ ch === "[" ||
223
+ ch === "]"
224
+ ) {
225
+ continue;
226
+ }
227
+ chars.push(ch);
228
+ }
229
+ return chars.join("");
230
+ }
231
+
232
+ export function sanitizeReplyDirectiveId(rawReplyToId: string | undefined): string | undefined {
233
+ const trimmed = rawReplyToId?.trim();
234
+ if (!trimmed) {
235
+ return undefined;
236
+ }
237
+ const sanitized = stripUnsafeReplyDirectiveChars(trimmed).trim();
238
+ if (!sanitized) {
239
+ return undefined;
240
+ }
241
+ const chars = Array.from(sanitized);
242
+ if (chars.length > MAX_REPLY_DIRECTIVE_ID_LENGTH) {
243
+ return chars.slice(0, MAX_REPLY_DIRECTIVE_ID_LENGTH).join("");
244
+ }
245
+ return sanitized;
246
+ }
247
+
248
+ export function parseInlineDirectives(
249
+ text: string | undefined,
250
+ options: ParseInlineDirectivesOptions = {},
251
+ ): InlineDirectiveParseResult {
252
+ const { currentMessageId, stripAudioTag = true, stripReplyTags = true } = options;
253
+
254
+ if (!text) {
255
+ return {
256
+ text: "",
257
+ audioAsVoice: false,
258
+ replyToCurrent: false,
259
+ hasAudioTag: false,
260
+ hasReplyTag: false,
261
+ };
262
+ }
263
+
264
+ if (!text.includes("[[")) {
265
+ return {
266
+ text: normalizeDirectiveWhitespace(text, resolveCodeRegions(text)),
267
+ audioAsVoice: false,
268
+ replyToCurrent: false,
269
+ hasAudioTag: false,
270
+ hasReplyTag: false,
271
+ };
272
+ }
273
+
274
+ // Directives inside code regions are literal text: never voice/reply semantics.
275
+ // Stripping a tag shortens the string, so every pass re-derives regions from
276
+ // the text it actually runs against; offsets from an earlier pass are stale
277
+ // once a preceding tag has been removed.
278
+ const codeRegions = resolveCodeRegions(text);
279
+ let cleaned = text;
280
+ let audioAsVoice = false;
281
+ let hasAudioTag = false;
282
+ let hasReplyTag = false;
283
+ let sawCurrent = false;
284
+ let lastExplicitId: string | undefined;
285
+
286
+ cleaned = cleaned.replace(AUDIO_TAG_RE, (match: string, offset: number, source: string) => {
287
+ if (isInsideCode(offset, codeRegions)) {
288
+ return match;
289
+ }
290
+ audioAsVoice = true;
291
+ hasAudioTag = true;
292
+ return stripAudioTag ? replacementPreservesWordBoundary(source, offset, match.length) : match;
293
+ });
294
+ const codeRegionsAfterAudioStrip = resolveCodeRegions(cleaned);
295
+
296
+ cleaned = cleaned.replace(
297
+ REPLY_TAG_RE,
298
+ (match: string, idRaw: string | undefined, offset: number, source: string) => {
299
+ if (isInsideCode(offset, codeRegionsAfterAudioStrip)) {
300
+ return match;
301
+ }
302
+ hasReplyTag = true;
303
+ if (idRaw === undefined) {
304
+ sawCurrent = true;
305
+ } else {
306
+ const id = sanitizeReplyDirectiveId(idRaw);
307
+ if (id) {
308
+ lastExplicitId = id;
309
+ }
310
+ }
311
+ return stripReplyTags
312
+ ? replacementPreservesWordBoundary(source, offset, match.length)
313
+ : match;
314
+ },
315
+ );
316
+
317
+ // Early return when `[[...]]` matched no known directive: keep the original
318
+ // text untouched instead of running whitespace normalization.
319
+ if (!hasAudioTag && !hasReplyTag) {
320
+ return {
321
+ text,
322
+ audioAsVoice: false,
323
+ replyToCurrent: false,
324
+ hasAudioTag: false,
325
+ hasReplyTag: false,
326
+ };
327
+ }
328
+
329
+ cleaned = normalizeDirectiveWhitespace(cleaned, resolveCodeRegions(cleaned));
330
+ const replyToId =
331
+ lastExplicitId ?? (sawCurrent ? normalizeOptionalString(currentMessageId) : undefined);
332
+
333
+ return {
334
+ text: cleaned,
335
+ audioAsVoice,
336
+ replyToId,
337
+ replyToExplicitId: lastExplicitId,
338
+ replyToCurrent: sawCurrent,
339
+ hasAudioTag,
340
+ hasReplyTag,
341
+ };
342
+ }