@soimy/dingtalk 3.5.2 → 3.5.3

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.
@@ -14,11 +14,20 @@ import { resolveRobotCode } from "../config";
14
14
  import { parseLearnCommand } from "../learning-command-service";
15
15
  import { getDingTalkRuntime } from "../runtime";
16
16
  import { sendBySession } from "../send-service";
17
+ import { getErrorMessage } from "../utils";
17
18
  import type { AgentNameMatch, DingTalkConfig, DingTalkInboundMessage, HandleDingTalkMessageParams, Logger, MessageContent } from "../types";
18
19
 
20
+ export class HostRoutingHelperUnavailableError extends Error {
21
+ constructor(message = "DingTalk sub-agent routing requires runtime.channel.routing.buildAgentSessionKey from the host runtime.") {
22
+ super(message);
23
+ this.name = "HostRoutingHelperUnavailableError";
24
+ }
25
+ }
26
+
19
27
  /**
20
28
  * Build a session key for a specific agent using the runtime API.
21
- * Falls back to framework's resolveAgentRoute if buildAgentSessionKey is unavailable.
29
+ * On supported host versions, sub-agent routing must use the shared helper
30
+ * instead of synthesizing plugin-local fallback keys.
22
31
  */
23
32
  export function buildAgentSessionKey(params: {
24
33
  rt: ReturnType<typeof getDingTalkRuntime>;
@@ -30,31 +39,19 @@ export function buildAgentSessionKey(params: {
30
39
  }): string {
31
40
  const { rt, cfg, accountId, agentId, peerKind, peerId } = params;
32
41
  const routing = rt.channel.routing as Record<string, unknown>;
33
- if (typeof routing.buildAgentSessionKey === "function") {
34
- return (
35
- (routing.buildAgentSessionKey as (p: unknown) => string)({
36
- agentId,
37
- channel: "dingtalk",
38
- accountId,
39
- peer: { kind: peerKind, id: peerId },
40
- dmScope: cfg.session?.dmScope,
41
- identityLinks: cfg.session?.identityLinks,
42
- })
43
- ).toLowerCase();
42
+ if (typeof routing.buildAgentSessionKey !== "function") {
43
+ throw new HostRoutingHelperUnavailableError();
44
44
  }
45
- // Fallback: derive a session key with agentId suffix to ensure isolation.
46
- // resolveAgentRoute routes to the default agent, so we append the target
47
- // agentId to prevent session key collisions between sub-agents.
48
- // @migration-note: When SDK exposes buildAgentSessionKey in type definitions,
49
- // sessions created via this fallback path will become orphaned. Remove this
50
- // fallback and the typeof check once the SDK is updated.
51
- const fallbackRoute = rt.channel.routing.resolveAgentRoute({
52
- cfg,
53
- channel: "dingtalk",
54
- accountId,
55
- peer: { kind: peerKind, id: peerId },
56
- });
57
- return `${fallbackRoute.sessionKey}:subagent:${agentId}`;
45
+ return (
46
+ (routing.buildAgentSessionKey as (p: unknown) => string)({
47
+ agentId,
48
+ channel: "dingtalk",
49
+ accountId,
50
+ peer: { kind: peerKind, id: peerId },
51
+ dmScope: cfg.session?.dmScope,
52
+ identityLinks: cfg.session?.identityLinks,
53
+ })
54
+ ).toLowerCase();
58
55
  }
59
56
 
60
57
  /**
@@ -131,8 +128,8 @@ export async function resolveSubAgentRoute(params: {
131
128
  await sendBySession(dingtalkConfig, sessionWebhook, `⚠️ ${fallbackReason}`, {
132
129
  ...sendOptions,
133
130
  });
134
- } catch (err: any) {
135
- log?.debug?.(`[DingTalk] Failed to send fallback notice: ${err.message}`);
131
+ } catch (err: unknown) {
132
+ log?.debug?.(`[DingTalk] Failed to send fallback notice: ${getErrorMessage(err)}`);
136
133
  }
137
134
  }
138
135
 
@@ -168,6 +165,7 @@ export async function dispatchSubAgents(params: {
168
165
  preDownloadedMedia = { mediaPath: media.path, mediaType: media.mimeType };
169
166
  }
170
167
  }
168
+ let helperMissingWarningSent = false;
171
169
 
172
170
  for (const agentMatch of matchedAgents) {
173
171
  try {
@@ -186,9 +184,27 @@ export async function dispatchSubAgents(params: {
186
184
  preDownloadedMedia,
187
185
  });
188
186
  } catch (error) {
187
+ const message = getErrorMessage(error);
189
188
  log?.error?.(
190
- `[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${error instanceof Error ? error.message : String(error)}`,
189
+ `[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`,
191
190
  );
191
+ if (error instanceof HostRoutingHelperUnavailableError && !helperMissingWarningSent) {
192
+ helperMissingWarningSent = true;
193
+ try {
194
+ const isGroup = data.conversationType !== "1";
195
+ const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
196
+ await sendBySession(
197
+ dingtalkConfig,
198
+ sessionWebhook,
199
+ "⚠️ 当前宿主版本不支持 DingTalk 子助手路由所需的 session helper,请升级 OpenClaw 后重试。",
200
+ sendOptions,
201
+ );
202
+ } catch (notifyError: unknown) {
203
+ log?.debug?.(
204
+ `[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`,
205
+ );
206
+ }
207
+ }
192
208
  }
193
209
  }
194
210
  }
package/src/types.ts CHANGED
@@ -25,6 +25,8 @@ export type AckReactionMode = "off" | "emoji" | "kaomoji";
25
25
  // Accept arbitrary strings for backward compatibility; the recommended
26
26
  // explicit modes remain: "off" | "emoji" | "kaomoji".
27
27
  export type AckReactionConfigValue = string;
28
+ export type CardStreamingMode = "off" | "answer" | "all";
29
+ export type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
28
30
 
29
31
  /**
30
32
  * DingTalk channel configuration (extends base OpenClaw config)
@@ -39,6 +41,7 @@ export interface DingTalkConfig extends OpenClawConfig {
39
41
  allowFrom?: string[];
40
42
  groupAllowFrom?: string[];
41
43
  displayNameResolution?: "disabled" | "all";
44
+ contextVisibility?: ContextVisibilityMode;
42
45
  mediaUrlAllowlist?: string[];
43
46
  journalTTLDays?: number;
44
47
  ackReaction?: AckReactionConfigValue;
@@ -71,8 +74,15 @@ export interface DingTalkConfig extends OpenClawConfig {
71
74
  enabled?: boolean;
72
75
  cooldownHours?: number;
73
76
  };
74
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
77
+ /** Card streaming mode.
78
+ * - off: disable incremental streaming
79
+ * - answer: stream answer text
80
+ * - all: stream answer + reasoning text */
81
+ cardStreamingMode?: CardStreamingMode;
82
+ /** @deprecated Use `cardStreamingMode` instead. */
75
83
  cardRealTimeStream?: boolean;
84
+ /** Throttle interval in ms for card stream updates (default 1000) */
85
+ cardStreamInterval?: number;
76
86
  /** AICard degrade duration in milliseconds after trigger errors (default 30m) */
77
87
  aicardDegradeMs?: number;
78
88
  /** Enable local learning loop (events/reflections/session notes/global rules) */
@@ -100,6 +110,7 @@ export interface DingTalkChannelConfig {
100
110
  allowFrom?: string[];
101
111
  groupAllowFrom?: string[];
102
112
  displayNameResolution?: "disabled" | "all";
113
+ contextVisibility?: ContextVisibilityMode;
103
114
  mediaUrlAllowlist?: string[];
104
115
  journalTTLDays?: number;
105
116
  ackReaction?: AckReactionConfigValue;
@@ -131,8 +142,15 @@ export interface DingTalkChannelConfig {
131
142
  enabled?: boolean;
132
143
  cooldownHours?: number;
133
144
  };
134
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
145
+ /** Card streaming mode.
146
+ * - off: disable incremental streaming
147
+ * - answer: stream answer text
148
+ * - all: stream answer + reasoning text */
149
+ cardStreamingMode?: CardStreamingMode;
150
+ /** @deprecated Use `cardStreamingMode` instead. */
135
151
  cardRealTimeStream?: boolean;
152
+ /** Throttle interval in ms for card stream updates (default 1000) */
153
+ cardStreamInterval?: number;
136
154
  /** AICard degrade duration in milliseconds after trigger errors (default 30m) */
137
155
  aicardDegradeMs?: number;
138
156
  /** Enable local learning loop (events/reflections/session notes/global rules) */
@@ -234,6 +252,10 @@ export interface DingTalkInboundMessage {
234
252
  atName?: string;
235
253
  downloadCode?: string;
236
254
  }>;
255
+ /** chatRecord 消息摘要 */
256
+ summary?: string;
257
+ /** chatRecord 消息标题 */
258
+ title?: string;
237
259
  };
238
260
  };
239
261
  };
@@ -243,6 +265,8 @@ export interface DingTalkInboundMessage {
243
265
  recognition?: string;
244
266
  spaceId?: string;
245
267
  fileId?: string;
268
+ text?: string;
269
+ title?: string;
246
270
  biz_custom_action_url?: string;
247
271
  richText?: Array<{
248
272
  type: string;
@@ -712,6 +736,33 @@ export interface ConnectionAttemptResult {
712
736
 
713
737
  const DEFAULT_ACCOUNT_ID = "default";
714
738
 
739
+ function stripRemovedLegacyFieldsFromPublicAccount(
740
+ config: DingTalkConfig,
741
+ ): DingTalkConfig {
742
+ const {
743
+ cardStreamReasoning: _cardStreamReasoning,
744
+ verboseRealtimeStream: _verboseRealtimeStream,
745
+ accounts,
746
+ ...rest
747
+ } = config as DingTalkConfig & {
748
+ cardStreamReasoning?: unknown;
749
+ verboseRealtimeStream?: unknown;
750
+ accounts?: Record<string, DingTalkConfig | undefined>;
751
+ };
752
+ const sanitizedAccounts = accounts
753
+ ? Object.fromEntries(
754
+ Object.entries(accounts).map(([accountId, accountConfig]) => [
755
+ accountId,
756
+ accountConfig ? stripRemovedLegacyFieldsFromPublicAccount(accountConfig) : accountConfig,
757
+ ]),
758
+ )
759
+ : undefined;
760
+ if (sanitizedAccounts) {
761
+ return { ...rest, accounts: sanitizedAccounts } as DingTalkConfig;
762
+ }
763
+ return rest as DingTalkConfig;
764
+ }
765
+
715
766
  /**
716
767
  * List all DingTalk account IDs from config
717
768
  */
@@ -756,7 +807,7 @@ export function resolveDingTalkAccount(
756
807
 
757
808
  // If default account, return top-level config
758
809
  if (id === DEFAULT_ACCOUNT_ID) {
759
- const config: DingTalkConfig = {
810
+ const rawConfig: DingTalkConfig = {
760
811
  clientId: dingtalk?.clientId ?? "",
761
812
  clientSecret: dingtalk?.clientSecret ?? "",
762
813
  name: dingtalk?.name,
@@ -766,6 +817,7 @@ export function resolveDingTalkAccount(
766
817
  allowFrom: dingtalk?.allowFrom,
767
818
  groupAllowFrom: dingtalk?.groupAllowFrom,
768
819
  displayNameResolution: dingtalk?.displayNameResolution,
820
+ contextVisibility: dingtalk?.contextVisibility,
769
821
  journalTTLDays: dingtalk?.journalTTLDays,
770
822
  ackReaction: dingtalk?.ackReaction,
771
823
  debug: dingtalk?.debug,
@@ -785,7 +837,9 @@ export function resolveDingTalkAccount(
785
837
  keepAlive: dingtalk?.keepAlive,
786
838
  bypassProxyForSend: dingtalk?.bypassProxyForSend,
787
839
  proactivePermissionHint: dingtalk?.proactivePermissionHint,
840
+ cardStreamingMode: dingtalk?.cardStreamingMode,
788
841
  cardRealTimeStream: dingtalk?.cardRealTimeStream,
842
+ cardStreamInterval: dingtalk?.cardStreamInterval,
789
843
  aicardDegradeMs: dingtalk?.aicardDegradeMs,
790
844
  learningEnabled: dingtalk?.learningEnabled,
791
845
  learningAutoApply: dingtalk?.learningAutoApply,
@@ -793,6 +847,7 @@ export function resolveDingTalkAccount(
793
847
  convertMarkdownTables: dingtalk?.convertMarkdownTables,
794
848
  cardAtSender: dingtalk?.cardAtSender,
795
849
  };
850
+ const config = stripRemovedLegacyFieldsFromPublicAccount(rawConfig);
796
851
  return {
797
852
  ...config,
798
853
  accountId: id,
@@ -807,8 +862,9 @@ export function resolveDingTalkAccount(
807
862
  dingtalk as DingTalkConfig,
808
863
  accountConfig,
809
864
  );
865
+ const publicMerged = stripRemovedLegacyFieldsFromPublicAccount(merged);
810
866
  return {
811
- ...merged,
867
+ ...publicMerged,
812
868
  accountId: id,
813
869
  configured: Boolean(merged.clientId && merged.clientSecret),
814
870
  };
package/src/utils.ts CHANGED
@@ -224,6 +224,31 @@ export function stringifyUnknown(value: unknown): string {
224
224
  }
225
225
  }
226
226
 
227
+ export function parseBooleanLike(value: unknown): boolean | undefined {
228
+ if (typeof value === "boolean") {
229
+ return value;
230
+ }
231
+ if (typeof value === "number") {
232
+ if (value === 1) {
233
+ return true;
234
+ }
235
+ if (value === 0) {
236
+ return false;
237
+ }
238
+ return undefined;
239
+ }
240
+ if (typeof value === "string") {
241
+ const normalized = value.trim().toLowerCase();
242
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) {
243
+ return true;
244
+ }
245
+ if (["0", "false", "no", "n", "off"].includes(normalized)) {
246
+ return false;
247
+ }
248
+ }
249
+ return undefined;
250
+ }
251
+
227
252
  export function getErrorMessage(err: unknown): string {
228
253
  if (err instanceof Error && err.message) {
229
254
  return err.message;