@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.
package/src/channel.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
3
+ import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
3
4
  import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
4
5
  import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core";
5
- import { jsonResult } from "openclaw/plugin-sdk/telegram-core";
6
6
  import { readStringParam } from "openclaw/plugin-sdk/param-readers";
7
7
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
8
8
  import { getAccessToken } from "./auth";
@@ -68,6 +68,7 @@ import {
68
68
  formatDingTalkConnectionErrorLog,
69
69
  formatDingTalkErrorPayloadLog,
70
70
  getCurrentTimestamp,
71
+ parseBooleanLike,
71
72
  resolvePluginDebugLog,
72
73
  } from "./utils";
73
74
 
@@ -188,29 +189,15 @@ function logInboundCounters(log: any, accountId: string, reason: string): void {
188
189
  }
189
190
 
190
191
  function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
191
- const value = params[key];
192
- if (typeof value === "boolean") {
193
- return value;
194
- }
195
- if (typeof value === "number") {
196
- if (value === 1) {
197
- return true;
198
- }
199
- if (value === 0) {
200
- return false;
201
- }
202
- return undefined;
203
- }
204
- if (typeof value === "string") {
205
- const normalized = value.trim().toLowerCase();
206
- if (["1", "true", "yes", "y", "on"].includes(normalized)) {
207
- return true;
208
- }
209
- if (["0", "false", "no", "n", "off"].includes(normalized)) {
210
- return false;
211
- }
192
+ return parseBooleanLike(params[key]);
193
+ }
194
+
195
+ function readSharedAudioAsVoiceParam(params: Record<string, unknown>): boolean {
196
+ const sharedValue = readBooleanLikeParam(params, "audioAsVoice");
197
+ if (sharedValue !== undefined) {
198
+ return sharedValue;
212
199
  }
213
- return undefined;
200
+ return readBooleanLikeParam(params, "asVoice") === true;
214
201
  }
215
202
 
216
203
  function describeDingTalkMessageTool(cfg: OpenClawConfig): {
@@ -261,7 +248,7 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
261
248
  message = caption;
262
249
  }
263
250
 
264
- const asVoice = readBooleanLikeParam(params, "asVoice") === true;
251
+ const asVoice = readSharedAudioAsVoiceParam(params);
265
252
  const requestedMediaType = readStringParam(params, "mediaType");
266
253
 
267
254
  const target = resolveOriginalPeerId(stripTargetPrefix(to).targetId);
@@ -507,6 +494,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
507
494
  filePath,
508
495
  mediaUrl,
509
496
  mediaType: providedMediaType,
497
+ audioAsVoice,
510
498
  asVoice,
511
499
  accountId,
512
500
  mediaLocalRoots,
@@ -570,7 +558,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
570
558
  const mediaType = resolveOutboundMediaType({
571
559
  mediaType: typeof providedMediaType === "string" ? providedMediaType : undefined,
572
560
  mediaPath: actualMediaPath,
573
- asVoice: asVoice === true,
561
+ asVoice: readSharedAudioAsVoiceParam({ audioAsVoice, asVoice }),
574
562
  });
575
563
  let result;
576
564
  try {
@@ -679,6 +667,12 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
679
667
  }
680
668
 
681
669
  const useConnectionManager = config.useConnectionManager ?? true;
670
+ const applyStatusPatch = (patch: Record<string, unknown>) => {
671
+ ctx.setStatus({
672
+ ...ctx.getStatus(),
673
+ ...patch,
674
+ });
675
+ };
682
676
 
683
677
  // Factory that creates a fresh DWClient with the TOPIC_ROBOT callback
684
678
  // already registered. Each client captures its own reference for
@@ -721,6 +715,14 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
721
715
  };
722
716
  try {
723
717
  const data = JSON.parse(res.data) as DingTalkInboundMessage;
718
+ // Record the latest inbound callback arrival for status/UI projection.
719
+ // This intentionally tracks "message reached the plugin callback" rather
720
+ // than "message passed dedup and completed processing".
721
+ applyStatusPatch({
722
+ connected: true,
723
+ lastInboundAt: getCurrentTimestamp(),
724
+ lastEventAt: getCurrentTimestamp(),
725
+ });
724
726
 
725
727
  const robotKey = resolveRobotCode(config) || account.accountId;
726
728
  const msgId = data.msgId || messageId;
@@ -912,9 +914,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
912
914
  nativeStopResolve?.();
913
915
  }
914
916
 
915
- ctx.setStatus({
916
- ...ctx.getStatus(),
917
+ applyStatusPatch({
917
918
  running: false,
919
+ connected: false,
920
+ lastEventAt: getCurrentTimestamp(),
918
921
  lastStopAt: getCurrentTimestamp(),
919
922
  });
920
923
 
@@ -931,9 +934,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
931
934
  `[${account.accountId}] Abort signal already active, skipping connection`,
932
935
  );
933
936
 
934
- ctx.setStatus({
935
- ...ctx.getStatus(),
937
+ applyStatusPatch({
936
938
  running: false,
939
+ connected: false,
940
+ lastEventAt: getCurrentTimestamp(),
937
941
  lastStopAt: getCurrentTimestamp(),
938
942
  lastError: "Connection aborted before start",
939
943
  });
@@ -956,9 +960,11 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
956
960
  try {
957
961
  await client.connect();
958
962
  if (!stopped) {
959
- ctx.setStatus({
960
- ...ctx.getStatus(),
963
+ applyStatusPatch({
961
964
  running: true,
965
+ connected: true,
966
+ lastConnectedAt: getCurrentTimestamp(),
967
+ lastEventAt: getCurrentTimestamp(),
962
968
  lastStartAt: getCurrentTimestamp(),
963
969
  lastError: null,
964
970
  });
@@ -974,9 +980,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
974
980
  `[${account.accountId}] Failed to establish connection: ${err.message}`,
975
981
  ) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
976
982
  );
977
- ctx.setStatus({
978
- ...ctx.getStatus(),
983
+ applyStatusPatch({
979
984
  running: false,
985
+ connected: false,
986
+ lastEventAt: getCurrentTimestamp(),
980
987
  lastError: err.message || "Connection failed",
981
988
  });
982
989
  throw err;
@@ -1004,9 +1011,11 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1004
1011
  `[${account.accountId}] Connection state changed to: ${state}${error ? ` (${error})` : ""}`,
1005
1012
  );
1006
1013
  if (state === ConnectionState.CONNECTED) {
1007
- ctx.setStatus({
1008
- ...ctx.getStatus(),
1014
+ applyStatusPatch({
1009
1015
  running: true,
1016
+ connected: true,
1017
+ lastConnectedAt: getCurrentTimestamp(),
1018
+ lastEventAt: getCurrentTimestamp(),
1010
1019
  lastStartAt: getCurrentTimestamp(),
1011
1020
  lastError: null,
1012
1021
  });
@@ -1027,9 +1036,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1027
1036
  `[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`,
1028
1037
  );
1029
1038
  }
1030
- ctx.setStatus({
1031
- ...ctx.getStatus(),
1039
+ applyStatusPatch({
1032
1040
  running: false,
1041
+ connected: false,
1042
+ lastEventAt: getCurrentTimestamp(),
1033
1043
  lastError: error || `Connection ${state.toLowerCase()}`,
1034
1044
  });
1035
1045
  }
@@ -1054,9 +1064,11 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1054
1064
  await connectionManager.connect();
1055
1065
 
1056
1066
  if (!stopped && connectionManager.isConnected()) {
1057
- ctx.setStatus({
1058
- ...ctx.getStatus(),
1067
+ applyStatusPatch({
1059
1068
  running: true,
1069
+ connected: true,
1070
+ lastConnectedAt: getCurrentTimestamp(),
1071
+ lastEventAt: getCurrentTimestamp(),
1060
1072
  lastStartAt: getCurrentTimestamp(),
1061
1073
  lastError: null,
1062
1074
  });
@@ -1079,9 +1091,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1079
1091
  ) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
1080
1092
  );
1081
1093
 
1082
- ctx.setStatus({
1083
- ...ctx.getStatus(),
1094
+ applyStatusPatch({
1084
1095
  running: false,
1096
+ connected: false,
1097
+ lastEventAt: getCurrentTimestamp(),
1085
1098
  lastError: err.message || "Connection failed",
1086
1099
  });
1087
1100
  throw err;
@@ -1098,7 +1111,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1098
1111
  defaultRuntime: {
1099
1112
  accountId: "default",
1100
1113
  running: false,
1114
+ connected: false,
1101
1115
  lastEventAt: null,
1116
+ lastConnectedAt: null,
1117
+ lastInboundAt: null,
1102
1118
  lastStartAt: null,
1103
1119
  lastStopAt: null,
1104
1120
  lastError: null,
@@ -1155,7 +1171,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
1155
1171
  configured: account.configured,
1156
1172
  clientId: account.config?.clientId ?? null,
1157
1173
  running,
1174
+ connected: runtime?.connected ?? snapshot?.connected ?? null,
1158
1175
  lastEventAt: running ? getCurrentTimestamp() : persistedLastEventAt,
1176
+ lastConnectedAt: runtime?.lastConnectedAt ?? snapshot?.lastConnectedAt ?? null,
1177
+ lastInboundAt: runtime?.lastInboundAt ?? snapshot?.lastInboundAt ?? null,
1159
1178
  lastStartAt: runtime?.lastStartAt ?? snapshot?.lastStartAt ?? null,
1160
1179
  lastStopAt: runtime?.lastStopAt ?? snapshot?.lastStopAt ?? null,
1161
1180
  lastError: runtime?.lastError ?? snapshot?.lastError ?? null,
@@ -7,65 +7,78 @@ const AckReactionSchema = z.union([
7
7
  z.string().min(1),
8
8
  ]);
9
9
 
10
+ const CardStreamingModeSchema = z.enum(["off", "answer", "all"]);
11
+ const ContextVisibilitySchema = z.enum(["all", "allowlist", "allowlist_quote"]);
12
+
13
+ /**
14
+ * Runtime-parsed DingTalk account config.
15
+ *
16
+ * Compatibility note:
17
+ * - `agentId`, `corpId`, `showThinkingStream`, and `asyncMode` are intentionally
18
+ * not parsed here. They remain only in manifest metadata for legacy host/UI
19
+ * compatibility and are ignored by the current runtime.
20
+ */
10
21
  const DingTalkAccountConfigShape = {
11
22
  /** Account name (optional display name) */
12
23
  name: z.string().optional(),
13
24
 
14
- /** Whether this channel is enabled */
25
+ /** Enable or disable this DingTalk channel/account without deleting saved credentials. */
15
26
  enabled: z.boolean().optional().default(true),
16
27
 
17
- /** DingTalk App Key (Client ID) - required for authentication */
28
+ /** DingTalk App Key (Client ID) used to authenticate API and Stream connections. */
18
29
  clientId: z.string().optional(),
19
30
 
20
- /** DingTalk App Secret (Client Secret) - required for authentication */
31
+ /** DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. */
21
32
  clientSecret: z.string().optional(),
22
33
 
23
- /** Direct message policy: open, pairing, or allowlist */
34
+ /** Direct-message access policy: open, pairing, or allowlist. */
24
35
  dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("open"),
25
36
 
26
- /** Group message policy: open, allowlist, or disabled */
37
+ /** Group-message access policy: open, allowlist, or disabled. */
27
38
  groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("open"),
28
39
 
29
- /** List of allowed user IDs for allowlist policy */
40
+ /** User IDs allowed when `dmPolicy` is `allowlist`. */
30
41
  allowFrom: z.array(z.string()).optional(),
31
42
 
32
- /** List of allowed user IDs for group allowlist policy */
43
+ /** Sender IDs allowed when `groupPolicy` is `allowlist`. */
33
44
  groupAllowFrom: z.array(z.string()).optional(),
34
45
 
35
- /** Default disabled. Enabling "all" allows learned displayName lookup but may misroute on stale/duplicate names and is available to all callers until upstream exposes requester authz context. */
46
+ /** Default disabled. Enabling `all` allows learned displayName lookup but may misroute on stale or duplicate names and is available to all callers until upstream exposes requester authz context. */
36
47
  displayNameResolution: z.enum(["disabled", "all"]).optional().default("disabled"),
37
48
 
49
+ /** Controls how much supplemental host context remains visible to the reply runtime. `allowlist_quote` is the safest advanced mode when only explicit quotes or replies should remain visible. */
50
+ contextVisibility: ContextVisibilitySchema.optional(),
51
+
52
+ /** Allowed remote media download hosts, IPs, or CIDRs for media fetches. */
38
53
  mediaUrlAllowlist: z.array(z.string()).optional(),
39
54
 
40
- /** Native ack reaction mode: off, emoji, or kaomoji */
55
+ /** Native acknowledgement reaction mode: off, emoji, kaomoji, or a custom compatibility string. */
41
56
  ackReaction: AckReactionSchema.optional(),
42
57
 
58
+ /** Retention window in days for short-lived message context used by quoting and media recovery. */
43
59
  journalTTLDays: z.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
44
- /** Enable debug logging */
60
+ /** Enable verbose DingTalk channel debug logging. */
45
61
  debug: z.boolean().optional().default(false),
46
62
 
47
- /** Message type for replies: markdown or card */
63
+ /** Default reply delivery mode: markdown or card. */
48
64
  messageType: z.enum(["markdown", "card"]).optional().default("markdown"),
49
65
 
50
- /** Card template ID for AI interactive cards
51
- * obtain the template ID from DingTalk Developer Console.
52
- * ref: https://github.com/soimy/openclaw-channel-dingtalk/blob/main/README.md#3-%E5%BB%BA%E7%AB%8B%E5%8D%A1%E7%89%87%E6%A8%A1%E6%9D%BF%E5%8F%AF%E9%80%89
53
- */
66
+ /** Deprecated and ignored. AI card replies now always use the built-in DingTalk template contract. Keep only for backward-compatible config parsing. */
54
67
  cardTemplateId: z.string().optional(),
55
68
 
56
- /** Card template key for streaming updates
57
- * Default: 'content' - maps to the content field in the card template
58
- * This key is used in the streaming API to update specific fields in the card.
59
- */
69
+ /** Deprecated and ignored. The built-in AI card contract owns the streaming field mapping. Keep only for backward-compatible config parsing. */
60
70
  cardTemplateKey: z.string().optional().default("content"),
61
71
 
62
- /** Per-group configuration, keyed by conversationId (supports "*" wildcard) */
72
+ /** Per-group overrides keyed by conversationId. Supports `*` as a wildcard fallback. */
63
73
  groups: z
64
74
  .record(
65
75
  z.string(),
66
76
  z.object({
77
+ /** Additional system prompt appended for this group. */
67
78
  systemPrompt: z.string().optional(),
79
+ /** Require an explicit @mention before the bot answers in this group. */
68
80
  requireMention: z.boolean().optional(),
81
+ /** Optional per-group sender allowlist for tighter access control than the channel default. */
69
82
  groupAllowFrom: z.array(z.string()).optional(),
70
83
  }),
71
84
  )
@@ -73,59 +86,70 @@ const DingTalkAccountConfigShape = {
73
86
 
74
87
  /** Connection robustness configuration */
75
88
 
76
- /** Maximum number of connection attempts before giving up (default: 10) */
89
+ /** Maximum connection attempts in a single reconnect cycle before backing off or giving up. */
77
90
  maxConnectionAttempts: z.number().int().min(1).optional().default(10),
78
91
 
79
- /** Initial reconnection delay in milliseconds (default: 1000ms) */
92
+ /** Initial reconnect backoff delay in milliseconds. */
80
93
  initialReconnectDelay: z.number().int().min(100).optional().default(1000),
81
94
 
82
- /** Maximum reconnection delay in milliseconds for exponential backoff (default: 60000ms = 1 minute) */
95
+ /** Upper bound for reconnect backoff delay in milliseconds. */
83
96
  maxReconnectDelay: z.number().int().min(1000).optional().default(60000),
84
97
 
85
- /** Jitter factor for reconnection delay randomization (0-1, default: 0.3) */
98
+ /** Randomization factor added to reconnect backoff to avoid synchronized reconnect storms. */
86
99
  reconnectJitter: z.number().min(0).max(1).optional().default(0.3),
87
100
 
88
- /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
101
+ /** Maximum reconnect cycles before the channel stops retrying and waits for the next lifecycle restart. */
89
102
  maxReconnectCycles: z.number().int().min(1).optional().default(10),
90
103
 
91
- /** Maximum time (ms) for a single reconnect cycle before starting a new cycle (default: 50000) */
104
+ /** Time limit in milliseconds for one reconnect cycle before starting a fresh cycle. */
92
105
  reconnectDeadlineMs: z.number().int().min(5000).optional().default(50000),
93
106
 
94
- /** Whether to use ConnectionManager (default: true). When false, rely on DWClient native keepAlive+autoReconnect. */
107
+ /** Enable the plugin connection manager. Disable only when you intentionally rely on DWClient native keepAlive plus autoReconnect behavior. */
95
108
  useConnectionManager: z.boolean().optional().default(true),
96
109
 
97
- /** Maximum inbound media file size in MB (overrides runtime default when set) */
110
+ /** Maximum inbound media size in MB accepted by the plugin. When omitted, the runtime default is used. */
98
111
  mediaMaxMb: z.number().int().min(1).optional(),
99
112
 
100
- /** Whether to enable underlying stream keepAlive heartbeat; defaults to !useConnectionManager when omitted */
113
+ /** Enable the underlying Stream client heartbeat. When omitted, runtime derives a default from `useConnectionManager`. */
101
114
  keepAlive: z.boolean().optional(),
102
- /** Bypass system/global HTTP(S) proxy for DingTalk outbound send/card/upload APIs */
115
+ /** Bypass global or system HTTP(S) proxy settings for DingTalk send, upload, and card APIs. */
103
116
  bypassProxyForSend: z.boolean().optional().default(false),
117
+ /** Controls the proactive-send permission reminder shown when a conversation has not granted send rights yet. */
104
118
  proactivePermissionHint: z
105
119
  .object({
120
+ /** Show the proactive-send permission hint when the runtime detects missing DingTalk proactive permission. */
106
121
  enabled: z.boolean().optional().default(true),
122
+ /** Minimum cooldown in hours before the same proactive permission hint can be shown again. */
107
123
  cooldownHours: z.number().int().min(1).max(24 * 30).optional().default(24),
108
124
  })
109
125
  .optional()
110
126
  .default({ enabled: true, cooldownHours: 24 }),
111
127
 
112
- /** Enable real-time card streaming (default: false).
113
- * When true, card updates are streamed per-token with 300ms throttle for a smoother experience, at the cost of more API calls. */
114
- cardRealTimeStream: z.boolean().optional().default(false),
128
+ /** Deprecated compatibility flag. When true and `cardStreamingMode` is unset, runtime resolves to `cardStreamingMode: "all"`. Do not use in new configs. */
129
+ cardRealTimeStream: z.boolean().optional(),
130
+
131
+ /** Card streaming mode:
132
+ * - off: disable incremental streaming
133
+ * - answer: stream answer text
134
+ * - all: stream answer + reasoning or thinking text */
135
+ cardStreamingMode: CardStreamingModeSchema.optional(),
136
+
137
+ /** Throttle interval in milliseconds between AI card streaming updates. */
138
+ cardStreamInterval: z.number().int().min(200).optional().default(1000),
115
139
 
116
- /** AICard degrade duration in milliseconds after trigger errors (default: 30 minutes) */
140
+ /** Cooldown window in milliseconds after AI card trigger errors. Replies fall back to non-card delivery during this period. */
117
141
  aicardDegradeMs: z.number().int().min(60_000).optional().default(30 * 60 * 1000),
118
142
 
119
- /** Enable local learning loop (default: false) */
143
+ /** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */
120
144
  learningEnabled: z.boolean().optional(),
121
145
 
122
- /** Auto-apply generated reflections into session notes/global rules (default: false) */
146
+ /** Automatically apply generated learning output into session notes or global rules when available. */
123
147
  learningAutoApply: z.boolean().optional(),
124
148
 
125
- /** Session learning note TTL in milliseconds (default: 6 hours) */
149
+ /** Retention window in milliseconds for temporary learning notes. */
126
150
  learningNoteTtlMs: z.number().int().min(60_000).optional(),
127
151
 
128
- /** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
152
+ /** Convert markdown tables to plain text before sending when you want more consistent DingTalk rendering. */
129
153
  convertMarkdownTables: z.boolean().optional().default(true),
130
154
 
131
155
  /** @mention the sender after card finalization in group chats.
package/src/config.ts CHANGED
@@ -26,12 +26,34 @@ function normalizeLearningConfig(
26
26
  learningNoteTtlMs: options.applyDefaults
27
27
  ? config.learningNoteTtlMs ?? DEFAULT_LEARNING_NOTE_TTL_MS
28
28
  : config.learningNoteTtlMs,
29
+ cardStreamingMode: options.applyDefaults
30
+ ? (config.cardStreamingMode ?? (config.cardRealTimeStream === true ? "all" : "off"))
31
+ : config.cardStreamingMode,
29
32
  };
30
33
  }
31
34
 
32
35
  function stripRemovedLegacyFields(config: DingTalkConfig): DingTalkConfig {
33
- const { verboseRealtimeStream: _verboseRealtimeStream, ...rest } =
34
- config as DingTalkConfig & { verboseRealtimeStream?: unknown };
36
+ const {
37
+ verboseRealtimeStream: _verboseRealtimeStream,
38
+ cardStreamReasoning: _cardStreamReasoning,
39
+ accounts,
40
+ ...rest
41
+ } = config as DingTalkConfig & {
42
+ verboseRealtimeStream?: unknown;
43
+ cardStreamReasoning?: unknown;
44
+ accounts?: Record<string, DingTalkConfig | undefined>;
45
+ };
46
+ const sanitizedAccounts = accounts
47
+ ? Object.fromEntries(
48
+ Object.entries(accounts).map(([accountId, accountConfig]) => [
49
+ accountId,
50
+ accountConfig ? stripRemovedLegacyFields(accountConfig) : accountConfig,
51
+ ]),
52
+ )
53
+ : undefined;
54
+ if (sanitizedAccounts) {
55
+ return { ...rest, accounts: sanitizedAccounts } as DingTalkConfig;
56
+ }
35
57
  return rest as DingTalkConfig;
36
58
  }
37
59
 
@@ -84,7 +106,7 @@ export function getConfig(cfg: OpenClawConfig, accountId?: string): DingTalkConf
84
106
  }
85
107
 
86
108
  if (dingtalkCfg.accounts && Object.keys(dingtalkCfg.accounts).length > 0) {
87
- return dingtalkCfg;
109
+ return stripRemovedLegacyFields(dingtalkCfg);
88
110
  }
89
111
 
90
112
  return stripRemovedLegacyFields(normalizeLearningConfig(dingtalkCfg, { applyDefaults: true }));