@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
@@ -4,8 +4,12 @@ import {
4
4
  hasConfiguredSecretInput,
5
5
  resolveDingTalkSecretConfig,
6
6
  } from "./secret-input";
7
- import type { DingTalkChannelConfig, DingTalkConfig } from "./types";
8
- export { resolveRelativePath, resolveUserPath } from "./path-utils";
7
+ import type {
8
+ DingTalkChannelConfig,
9
+ DingTalkConfig,
10
+ DingTalkGatewayCapabilitiesConfig,
11
+ } from "./types";
12
+ export { resolveRelativePath, resolveUserPath } from "../shared/path-utils";
9
13
  const DEFAULT_LEARNING_NOTE_TTL_MS = 6 * 60 * 60 * 1000;
10
14
  export type RuntimeDingTalkConfig = Omit<DingTalkConfig, "clientSecret"> & { clientSecret: string };
11
15
 
@@ -15,16 +19,22 @@ function normalizeLearningConfig(
15
19
  ): DingTalkConfig {
16
20
  return {
17
21
  ...config,
18
- learningEnabled: options.applyDefaults ? config.learningEnabled ?? false : config.learningEnabled,
22
+ learningEnabled: options.applyDefaults
23
+ ? (config.learningEnabled ?? false)
24
+ : config.learningEnabled,
19
25
  learningAutoApply: options.applyDefaults
20
- ? config.learningAutoApply ?? false
26
+ ? (config.learningAutoApply ?? false)
21
27
  : config.learningAutoApply,
22
28
  learningNoteTtlMs: options.applyDefaults
23
- ? config.learningNoteTtlMs ?? DEFAULT_LEARNING_NOTE_TTL_MS
29
+ ? (config.learningNoteTtlMs ?? DEFAULT_LEARNING_NOTE_TTL_MS)
24
30
  : config.learningNoteTtlMs,
25
31
  cardStreamingMode: options.applyDefaults
26
32
  ? (config.cardStreamingMode ?? (config.cardRealTimeStream === true ? "all" : "off"))
27
33
  : config.cardStreamingMode,
34
+ // Derived, internal-only: defaults below turn an omitted mode into "off",
35
+ // which would otherwise be indistinguishable from an explicit "off".
36
+ cardStreamingModeConfigured:
37
+ config.cardStreamingModeConfigured ?? config.cardStreamingMode !== undefined,
28
38
  };
29
39
  }
30
40
 
@@ -53,6 +63,32 @@ function stripRemovedLegacyFields(config: DingTalkConfig): DingTalkConfig {
53
63
  return rest as DingTalkConfig;
54
64
  }
55
65
 
66
+ /**
67
+ * Merge channel-level and account-level `gatewayCapabilities` gates by sub-key.
68
+ *
69
+ * `mergeAccountWithDefaults` is a shallow merge, so without this helper an
70
+ * account-level object would replace the whole channel-level `gatewayCapabilities` and
71
+ * silently drop a channel-level allowlist (fail-open). Merging `tools` /
72
+ * `docs` / `send` separately keeps channel-level restrictions in force unless
73
+ * the account explicitly overrides that exact sub-key.
74
+ */
75
+ export function mergeGatewayCapabilitiesConfig(
76
+ channelLevel: DingTalkGatewayCapabilitiesConfig | undefined,
77
+ accountLevel: DingTalkGatewayCapabilitiesConfig | undefined,
78
+ ): DingTalkGatewayCapabilitiesConfig | undefined {
79
+ if (!channelLevel) {
80
+ return accountLevel;
81
+ }
82
+ if (!accountLevel) {
83
+ return channelLevel;
84
+ }
85
+ return {
86
+ tools: { ...channelLevel.tools, ...accountLevel.tools },
87
+ docs: { ...channelLevel.docs, ...accountLevel.docs },
88
+ send: { ...channelLevel.send, ...accountLevel.send },
89
+ };
90
+ }
91
+
56
92
  /**
57
93
  * Merge channel-level defaults into an account-specific config.
58
94
  * Account-level values take precedence; `accounts` key is excluded to avoid recursion.
@@ -61,8 +97,10 @@ export function mergeAccountWithDefaults(
61
97
  channelCfg: DingTalkConfig,
62
98
  accountCfg: DingTalkConfig,
63
99
  ): DingTalkConfig {
64
- const { accounts: _accounts, ...defaultCandidate } =
65
- channelCfg as DingTalkConfig & { accounts?: unknown; verboseRealtimeStream?: unknown };
100
+ const { accounts: _accounts, ...defaultCandidate } = channelCfg as DingTalkConfig & {
101
+ accounts?: unknown;
102
+ verboseRealtimeStream?: unknown;
103
+ };
66
104
  const defaults = stripRemovedLegacyFields(defaultCandidate as DingTalkConfig);
67
105
  const normalizedAccountCfg = stripRemovedLegacyFields(
68
106
  normalizeLearningConfig(accountCfg, { applyDefaults: false }),
@@ -73,13 +111,23 @@ export function mergeAccountWithDefaults(
73
111
  Object.assign(overrides, { [key]: value });
74
112
  }
75
113
  }
76
- return normalizeLearningConfig(
77
- {
78
- ...defaults,
79
- ...overrides,
80
- },
81
- { applyDefaults: true },
114
+ const merged: DingTalkConfig = {
115
+ ...defaults,
116
+ ...overrides,
117
+ };
118
+ // Explicitness is inherited: the account opt-out only counts when the account
119
+ // (or the channel) actually wrote `cardStreamingMode` in its config.
120
+ merged.cardStreamingModeConfigured =
121
+ normalizedAccountCfg.cardStreamingModeConfigured === true ||
122
+ channelCfg.cardStreamingMode !== undefined;
123
+ const gatewayCapabilities = mergeGatewayCapabilitiesConfig(
124
+ defaults.gatewayCapabilities,
125
+ overrides.gatewayCapabilities,
82
126
  );
127
+ if (gatewayCapabilities) {
128
+ merged.gatewayCapabilities = gatewayCapabilities;
129
+ }
130
+ return normalizeLearningConfig(merged, { applyDefaults: true });
83
131
  }
84
132
 
85
133
  /**
@@ -113,6 +161,107 @@ export function isConfigured(cfg: OpenClawConfig, accountId?: string): boolean {
113
161
  return Boolean(config.clientId && hasConfiguredSecretInput(config.clientSecret));
114
162
  }
115
163
 
164
+ /**
165
+ * Resolved Gateway RPC capability settings (Issue #608, 问题 3).
166
+ * All capabilities default to enabled; allowlists default to unrestricted.
167
+ */
168
+ export interface ResolvedGatewayCapabilities {
169
+ /** `dingtalk.docs.*` / `dingtalk-connector.docs.*` RPCs enabled (default: true) */
170
+ docsEnabled: boolean;
171
+ /** `dingtalk-connector.sendToUser/sendToGroup/send` RPCs enabled (default: true) */
172
+ proactiveSendEnabled: boolean;
173
+ /** When set, docs RPCs only accept these spaceId values */
174
+ allowedSpaceIds?: string[];
175
+ /** When set, proactive-send RPCs only accept these `user:*` / `group:*` targets */
176
+ allowedTargets?: string[];
177
+ }
178
+
179
+ /** Denial reason returned when `gatewayCapabilities.tools.docs` is explicitly false. */
180
+ export const DOCS_GATE_DISABLED_REASON =
181
+ "dingtalk docs Gateway RPC is disabled by config (gatewayCapabilities.tools.docs = false)";
182
+
183
+ /** Denial reason returned when `gatewayCapabilities.tools.proactiveSend` is explicitly false. */
184
+ export const PROACTIVE_SEND_GATE_DISABLED_REASON =
185
+ "dingtalk proactive-send Gateway RPC is disabled by config (gatewayCapabilities.tools.proactiveSend = false)";
186
+
187
+ const DEFAULT_GATEWAY_CAPABILITIES: ResolvedGatewayCapabilities = Object.freeze({
188
+ docsEnabled: true,
189
+ proactiveSendEnabled: true,
190
+ });
191
+
192
+ /**
193
+ * Resolve Gateway RPC capability configuration for an account.
194
+ * Account-level `gatewayCapabilities` is merged with channel-level defaults by sub-key
195
+ * (see `mergeGatewayCapabilitiesConfig`); both default to all capabilities enabled.
196
+ */
197
+ export function resolveGatewayCapabilityConfig(
198
+ cfg: OpenClawConfig,
199
+ accountId?: string,
200
+ ): ResolvedGatewayCapabilities {
201
+ const config = getConfig(cfg, accountId);
202
+ const gatewayCapabilities = config.gatewayCapabilities;
203
+ if (!gatewayCapabilities) {
204
+ return DEFAULT_GATEWAY_CAPABILITIES;
205
+ }
206
+ const tools = gatewayCapabilities.tools ?? {};
207
+ const docs = gatewayCapabilities.docs ?? {};
208
+ const send = gatewayCapabilities.send ?? {};
209
+ return {
210
+ docsEnabled: tools.docs !== false,
211
+ proactiveSendEnabled: tools.proactiveSend !== false,
212
+ allowedSpaceIds: docs.allowedSpaceIds,
213
+ allowedTargets: send.allowedTargets,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Check a docs RPC request against the configured capability gates.
219
+ * Returns null when allowed, or a human-readable denial reason.
220
+ *
221
+ * A configured allowlist is fail-closed: an empty list (possible only when
222
+ * config validation was bypassed) denies every docs RPC, and a request without
223
+ * a spaceId is denied as well because its doc space cannot be verified.
224
+ */
225
+ export function checkDocsGatewayCapability(
226
+ caps: ResolvedGatewayCapabilities,
227
+ spaceId: string | undefined,
228
+ ): string | null {
229
+ if (!caps.docsEnabled) {
230
+ return DOCS_GATE_DISABLED_REASON;
231
+ }
232
+ if (caps.allowedSpaceIds) {
233
+ if (!spaceId) {
234
+ return "docs RPC denied: this request carries no spaceId while gatewayCapabilities.docs.allowedSpaceIds is configured (dingtalk.docs.append never carries a spaceId)";
235
+ }
236
+ if (!caps.allowedSpaceIds.includes(spaceId)) {
237
+ return "spaceId is not in gatewayCapabilities.docs.allowedSpaceIds allowlist";
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+
243
+ /**
244
+ * Check a proactive-send RPC request against the configured capability gates.
245
+ * Returns null when allowed, or a human-readable denial reason.
246
+ *
247
+ * A configured allowlist is fail-closed: an empty list (possible only when
248
+ * config validation was bypassed) denies every proactive-send RPC.
249
+ */
250
+ export function checkProactiveSendGatewayCapability(
251
+ caps: ResolvedGatewayCapabilities,
252
+ target: string,
253
+ ): string | null {
254
+ if (!caps.proactiveSendEnabled) {
255
+ return PROACTIVE_SEND_GATE_DISABLED_REASON;
256
+ }
257
+ if (caps.allowedTargets) {
258
+ if (!caps.allowedTargets.includes(target)) {
259
+ return "target is not in gatewayCapabilities.send.allowedTargets allowlist";
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+
116
265
  export async function resolveRuntimeConfig(
117
266
  config: DingTalkConfig,
118
267
  log?: { warn?: (message: string, data?: unknown) => void },
@@ -154,7 +303,10 @@ function hasOwn(obj: unknown, key: string): boolean {
154
303
  return typeof obj === "object" && obj !== null && Object.prototype.hasOwnProperty.call(obj, key);
155
304
  }
156
305
 
157
- function resolveAgentIdentityEmoji(cfg: OpenClawConfig, agentId?: string | null): string | undefined {
306
+ function resolveAgentIdentityEmoji(
307
+ cfg: OpenClawConfig,
308
+ agentId?: string | null,
309
+ ): string | undefined {
158
310
  const targetAgentId = String(agentId || "").trim();
159
311
  if (!targetAgentId) {
160
312
  return undefined;
@@ -234,7 +386,6 @@ export function stripTargetPrefix(target: string): { targetId: string; isExplici
234
386
 
235
387
  const DEFAULT_ACCOUNT_ID = "default";
236
388
 
237
-
238
389
  /**
239
390
  * List all DingTalk account IDs from config
240
391
  */
@@ -326,10 +477,7 @@ export function resolveDingTalkAccount(
326
477
 
327
478
  const accountConfig = dingtalk?.accounts?.[id];
328
479
  if (accountConfig) {
329
- const merged = mergeAccountWithDefaults(
330
- dingtalk as DingTalkConfig,
331
- accountConfig,
332
- );
480
+ const merged = mergeAccountWithDefaults(dingtalk as DingTalkConfig, accountConfig);
333
481
  const publicMerged = stripRemovedLegacyFields(merged);
334
482
  return {
335
483
  ...publicMerged,
@@ -1,4 +1,4 @@
1
- import httpClient from "./http-client.js";
1
+ import httpClient from "../shared/http-client.js";
2
2
 
3
3
  // ── Constants ──────────────────────────────────────────────────────────────
4
4
 
@@ -52,7 +52,9 @@ async function apiPost(
52
52
  const errcode = data.errcode;
53
53
  if (errcode !== undefined && errcode !== 0) {
54
54
  const errmsg = asString(data.errmsg) || "unknown error";
55
- throw new RegistrationError(`API error [${path}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`);
55
+ throw new RegistrationError(
56
+ `API error [${path}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`,
57
+ );
56
58
  }
57
59
  return data;
58
60
  }
@@ -137,8 +139,7 @@ export async function beginDeviceRegistration(): Promise<DeviceRegistrationSessi
137
139
  // Suppress unhandled rejection when abort fires outside Promise.race
138
140
  abortPromise?.catch(() => {});
139
141
 
140
- const sleep = () =>
141
- new Promise((resolve) => setTimeout(resolve, interval * 1000));
142
+ const sleep = () => new Promise((resolve) => setTimeout(resolve, interval * 1000));
142
143
 
143
144
  try {
144
145
  while (Date.now() < deadline) {
@@ -7,10 +7,7 @@ import type {
7
7
  } from "openclaw/plugin-sdk/setup";
8
8
  import { DEFAULT_ACCOUNT_ID, formatDocsLink, normalizeAccountId } from "openclaw/plugin-sdk/setup";
9
9
  import { listDingTalkAccountIds, resolveDingTalkAccount } from "./config.js";
10
- import {
11
- beginDeviceRegistration,
12
- RegistrationError,
13
- } from "./device-registration.js";
10
+ import { beginDeviceRegistration, RegistrationError } from "./device-registration.js";
14
11
  import {
15
12
  hasConfiguredSecretInput,
16
13
  normalizeSecretInputString,
@@ -1,10 +1,11 @@
1
1
  import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
2
2
 
3
- type JsonResultReturn = NonNullable<ChannelMessageActionAdapter["handleAction"]> extends (
4
- ...args: unknown[]
5
- ) => Promise<infer TResult>
6
- ? TResult
7
- : never;
3
+ type JsonResultReturn =
4
+ NonNullable<ChannelMessageActionAdapter["handleAction"]> extends (
5
+ ...args: unknown[]
6
+ ) => Promise<infer TResult>
7
+ ? TResult
8
+ : never;
8
9
 
9
10
  declare module "openclaw/plugin-sdk/channel-actions" {
10
11
  export function jsonResult(payload: unknown): JsonResultReturn;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Cross-domain runtime agent-event infrastructure.
3
+ *
4
+ * Owns the generic OpenClaw `events.onAgentEvent` surface that several
5
+ * domains consume: event typing, reference-counted fan-out, field accessors,
6
+ * and run correlation. Domain owners (ack reactions, AI card task progress)
7
+ * build their own behavior on top of these primitives instead of importing
8
+ * each other's private modules.
9
+ */
10
+
11
+ import { getErrorMessage } from "../shared/utils";
12
+
13
+ export type RuntimeEventsLogger = {
14
+ debug?: (msg: string) => void;
15
+ info?: (msg: string) => void;
16
+ warn?: (msg: string) => void;
17
+ };
18
+
19
+ export type RuntimeAgentEvent = {
20
+ stream?: string;
21
+ runId?: string;
22
+ sessionKey?: string;
23
+ data?: {
24
+ phase?: string;
25
+ name?: string;
26
+ args?: unknown;
27
+ runId?: string;
28
+ sessionKey?: string;
29
+ toolCallId?: string;
30
+ meta?: {
31
+ runId?: string;
32
+ sessionKey?: string;
33
+ } | null;
34
+ };
35
+ };
36
+
37
+ export type RuntimeEventsSurface = {
38
+ onAgentEvent?: (listener: (event: unknown) => void) => () => void;
39
+ };
40
+
41
+ /**
42
+ * Fan one upstream `onAgentEvent` subscription out to multiple local consumers.
43
+ *
44
+ * The upstream subscription is reference-counted: it is created with the first
45
+ * local listener and released with the last one. Each listener is isolated so a
46
+ * throwing consumer can neither starve the remaining consumers nor bubble back
47
+ * into the host event emitter.
48
+ */
49
+ export function createRuntimeEventsFanout(
50
+ upstream: RuntimeEventsSurface | undefined,
51
+ options: { log?: RuntimeEventsLogger } = {},
52
+ ): RuntimeEventsSurface {
53
+ const listeners = new Set<(event: unknown) => void>();
54
+ let unsubscribeUpstream: (() => void) | undefined;
55
+
56
+ const ensureUpstreamSubscription = () => {
57
+ if (unsubscribeUpstream || !upstream?.onAgentEvent) {
58
+ return;
59
+ }
60
+ unsubscribeUpstream = upstream.onAgentEvent((event: unknown) => {
61
+ // Dispatch against a snapshot so a listener that unsubscribes another
62
+ // listener during this event still receives the current event.
63
+ const currentListeners = Array.from(listeners);
64
+ for (const listener of currentListeners) {
65
+ try {
66
+ listener(event);
67
+ } catch (error: unknown) {
68
+ options.log?.warn?.(
69
+ `[DingTalk][RuntimeEvents] Listener failed: ${getErrorMessage(error)}`,
70
+ );
71
+ }
72
+ }
73
+ });
74
+ };
75
+
76
+ return {
77
+ onAgentEvent(listener: (event: unknown) => void) {
78
+ listeners.add(listener);
79
+ ensureUpstreamSubscription();
80
+ return () => {
81
+ listeners.delete(listener);
82
+ if (listeners.size === 0 && unsubscribeUpstream) {
83
+ unsubscribeUpstream();
84
+ unsubscribeUpstream = undefined;
85
+ }
86
+ };
87
+ },
88
+ };
89
+ }
90
+
91
+ function firstTrimmedString(...values: unknown[]): string | undefined {
92
+ for (const value of values) {
93
+ if (typeof value === "string" && value.trim()) {
94
+ return value.trim();
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ export function getEventRunId(event: RuntimeAgentEvent | undefined): string | undefined {
101
+ return firstTrimmedString(event?.runId, event?.data?.runId, event?.data?.meta?.runId);
102
+ }
103
+
104
+ export function getEventSessionKey(event: RuntimeAgentEvent | undefined): string | undefined {
105
+ return firstTrimmedString(
106
+ event?.sessionKey,
107
+ event?.data?.sessionKey,
108
+ event?.data?.meta?.sessionKey,
109
+ );
110
+ }
111
+
112
+ export function describeEvent(event: RuntimeAgentEvent | undefined): string {
113
+ const stream = firstTrimmedString(event?.stream) || "-";
114
+ const phase = firstTrimmedString(event?.data?.phase) || "-";
115
+ const toolName = firstTrimmedString(event?.data?.name) || "-";
116
+ const toolCallId = firstTrimmedString(event?.data?.toolCallId) || "-";
117
+ return (
118
+ `stream=${stream} phase=${phase} runId=${getEventRunId(event) || "-"} ` +
119
+ `sessionKey=${getEventSessionKey(event) || "-"} toolCallId=${toolCallId} toolName=${toolName}`
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Build a predicate that decides whether an agent event belongs to one reply.
125
+ *
126
+ * Correlation prefers an already captured `runId`; otherwise it accepts events
127
+ * whose `sessionKey` matches, and finally falls back to optimistically
128
+ * capturing the first session-less lifecycle start inside a short window.
129
+ *
130
+ * `consumer` labels the debug logs so two consumers sharing the same session
131
+ * (ack reactions and card task progress) stay distinguishable while triaging.
132
+ */
133
+ export function createAgentEventCorrelator(params: {
134
+ /** Short label identifying the consuming domain in debug logs. */
135
+ consumer: string;
136
+ sessionKey: string;
137
+ enabled: boolean;
138
+ createdAt: number;
139
+ optimisticCaptureWindowMs: number;
140
+ log?: RuntimeEventsLogger;
141
+ }) {
142
+ const logPrefix = `[DingTalk][AgentEventCorrelation][${params.consumer}]`;
143
+ let activeRunId: string | undefined;
144
+ let correlationUnavailableLogged = false;
145
+ let optimisticCaptureCount = 0;
146
+
147
+ return (event: RuntimeAgentEvent | undefined): boolean => {
148
+ const eventRunId = getEventRunId(event);
149
+ const eventSessionKey = getEventSessionKey(event);
150
+ const eventStream = firstTrimmedString(event?.stream) || "";
151
+ const eventPhase = firstTrimmedString(event?.data?.phase) || "";
152
+
153
+ if (activeRunId) {
154
+ const matched = eventRunId === activeRunId;
155
+ params.log?.debug?.(
156
+ `${logPrefix} correlation by runId matched=${matched} activeRunId=${activeRunId} ` +
157
+ `eventRunId=${eventRunId || "-"} eventSessionKey=${eventSessionKey || "-"}`,
158
+ );
159
+ return matched;
160
+ }
161
+
162
+ if (eventSessionKey === params.sessionKey) {
163
+ if (eventRunId) {
164
+ activeRunId = eventRunId;
165
+ params.log?.debug?.(
166
+ `${logPrefix} captured active runId=${activeRunId} from sessionKey=${params.sessionKey}`,
167
+ );
168
+ } else {
169
+ params.log?.debug?.(
170
+ `${logPrefix} correlated by sessionKey=${params.sessionKey} without runId`,
171
+ );
172
+ }
173
+ return true;
174
+ }
175
+
176
+ if (
177
+ optimisticCaptureCount === 0 &&
178
+ eventStream === "lifecycle" &&
179
+ eventPhase === "start" &&
180
+ eventRunId &&
181
+ !eventSessionKey &&
182
+ Date.now() - params.createdAt <= params.optimisticCaptureWindowMs
183
+ ) {
184
+ optimisticCaptureCount += 1;
185
+ activeRunId = eventRunId;
186
+ params.log?.debug?.(
187
+ `${logPrefix} optimistically captured active runId=${activeRunId} ` +
188
+ `from first lifecycle event without sessionKey windowMs=${params.optimisticCaptureWindowMs}`,
189
+ );
190
+ return true;
191
+ }
192
+
193
+ if (!correlationUnavailableLogged && params.enabled) {
194
+ correlationUnavailableLogged = true;
195
+ params.log?.debug?.(
196
+ `${logPrefix} ignored uncorrelated agent events; ` +
197
+ `reason=${getErrorMessage(eventRunId || eventSessionKey || "waiting for sessionKey/runId match")}`,
198
+ );
199
+ }
200
+ return false;
201
+ };
202
+ }
@@ -1,8 +1,10 @@
1
1
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
2
  import {
3
+ coerceSecretRef,
3
4
  hasConfiguredSecretInput as hasConfiguredOpenClawSecretInput,
4
5
  resolveConfiguredSecretInputString,
5
6
  } from "openclaw/plugin-sdk/secret-input-runtime";
7
+ import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
6
8
  import { z } from "zod";
7
9
  import { getDingTalkRuntime } from "./runtime";
8
10
 
@@ -28,6 +30,16 @@ type SecretInputLog = {
28
30
  const SECRET_INPUT_PROVIDER_PATTERN = /^[^:>]+$/;
29
31
  const SECRET_INPUT_ID_PATTERN = /^[^>]+$/;
30
32
 
33
+ const SECRET_INPUT_PATH = "channels.dingtalk.clientSecret";
34
+
35
+ function normalizeOptionalSecretString(value: unknown): string | undefined {
36
+ if (typeof value !== "string") {
37
+ return undefined;
38
+ }
39
+ const trimmed = value.trim();
40
+ return trimmed || undefined;
41
+ }
42
+
31
43
  function buildSecretInputFailure(
32
44
  value: SecretInputRef,
33
45
  reason: string,
@@ -40,6 +52,105 @@ function buildSecretInputFailure(
40
52
  };
41
53
  }
42
54
 
55
+ /**
56
+ * Resolve one `env` SecretInput reference without handing the whole ambient
57
+ * environment to the host resolver.
58
+ *
59
+ * Mirrors the official channel pattern (`openclaw/plugin-sdk/secret-ref-readonly`):
60
+ * inspect the reference, require the host to authorize this provider/id for a
61
+ * read-only path, and only then read the single `process.env[id]`. The SDK's
62
+ * own `resolveReadOnlyEnvSecretRef` collapses "not authorized" and "authorized
63
+ * but the variable is unset" into one `blocked` status, which would tell an
64
+ * operator with a correct allowlist to change it; re-deriving the same checks
65
+ * here keeps the failure reason actionable. Both branches fail closed.
66
+ */
67
+ function resolveEnvSecretInputRef(
68
+ value: SecretInputRef,
69
+ hostConfig: OpenClawConfig,
70
+ ): { value?: string; failure?: SecretInputResolutionFailure } {
71
+ const refLabel = `${value.source}:${value.provider}:${value.id}`;
72
+ const unresolved = (detail?: string) =>
73
+ buildSecretInputFailure(
74
+ value,
75
+ `${SECRET_INPUT_PATH} SecretRef is unresolved (${refLabel}).${detail ? ` ${detail}` : ""}`,
76
+ );
77
+
78
+ const ref = coerceSecretRef(value, hostConfig.secrets?.defaults);
79
+ if (!ref) {
80
+ return { failure: unresolved("SecretRef shape is not coercible.") };
81
+ }
82
+
83
+ if (ref.source !== "env") {
84
+ return {
85
+ failure: unresolved(
86
+ `Source "${ref.source}" is not usable from the DingTalk clientSecret env path.`,
87
+ ),
88
+ };
89
+ }
90
+
91
+ const providerConfig = hostConfig.secrets?.providers?.[ref.provider];
92
+ if (providerConfig && providerConfig.source !== "env") {
93
+ return {
94
+ failure: unresolved(
95
+ `Secret provider "${ref.provider}" has source "${providerConfig.source}" but the ref requests "env".`,
96
+ ),
97
+ };
98
+ }
99
+
100
+ if (
101
+ !canResolveEnvSecretRefInReadOnlyPath({
102
+ cfg: hostConfig,
103
+ provider: ref.provider,
104
+ id: ref.id,
105
+ })
106
+ ) {
107
+ return {
108
+ failure: unresolved(
109
+ `Environment variable "${value.id}" is not authorized for a read-only path: ` +
110
+ `set secrets.providers.${value.provider}.source to "env" and include ` +
111
+ `"${value.id}" in its allowlist.`,
112
+ ),
113
+ };
114
+ }
115
+
116
+ const envValue = normalizeOptionalSecretString(process.env[ref.id]);
117
+ if (envValue) {
118
+ return { value: envValue };
119
+ }
120
+ return {
121
+ failure: unresolved(`Environment variable "${value.id}" is authorized but unset or empty.`),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Resolve a `file` SecretInput reference with a scoped resolver call.
127
+ *
128
+ * The explicit empty `env` means this branch never reads ambient credentials.
129
+ * It does not stop the host from expanding a configured provider path (for
130
+ * example a leading `~`) against its own environment.
131
+ */
132
+ async function resolveFileSecretInputRef(
133
+ value: SecretInputRef,
134
+ hostConfig: OpenClawConfig,
135
+ ): Promise<{ value?: string; failure?: SecretInputResolutionFailure }> {
136
+ const resolved = await resolveConfiguredSecretInputString({
137
+ config: hostConfig,
138
+ env: {},
139
+ value,
140
+ path: SECRET_INPUT_PATH,
141
+ unresolvedReasonStyle: "detailed",
142
+ });
143
+ if (resolved.value) {
144
+ return { value: resolved.value };
145
+ }
146
+ return {
147
+ failure: buildSecretInputFailure(
148
+ value,
149
+ resolved.unresolvedRefReason || "secret reference is unresolved",
150
+ ),
151
+ };
152
+ }
153
+
43
154
  export function formatSecretInputResolutionFailure(failure: SecretInputResolutionFailure): string {
44
155
  return `${failure.source}:${failure.provider}:${failure.id} - ${failure.reason}`;
45
156
  }
@@ -133,20 +244,15 @@ export async function resolveSecretInputStringWithFailure(
133
244
  }
134
245
  }
135
246
  try {
136
- const resolved = await resolveConfiguredSecretInputString({
137
- config: hostConfig,
138
- env: process.env,
139
- value,
140
- path: "channels.dingtalk.clientSecret",
141
- unresolvedReasonStyle: "detailed",
142
- });
247
+ const resolved =
248
+ value.source === "env"
249
+ ? resolveEnvSecretInputRef(value, hostConfig)
250
+ : await resolveFileSecretInputRef(value, hostConfig);
143
251
  if (resolved.value) {
144
252
  return { value: resolved.value };
145
253
  }
146
- const failure = buildSecretInputFailure(
147
- value,
148
- resolved.unresolvedRefReason || "secret reference is unresolved",
149
- );
254
+ const failure =
255
+ resolved.failure ?? buildSecretInputFailure(value, "secret reference is unresolved");
150
256
  log?.warn?.("[DingTalk][SecretInput] Failed to resolve secret", {
151
257
  provider: value.provider,
152
258
  id: value.id,