@soimy/dingtalk 3.5.2 → 3.6.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 (40) hide show
  1. package/README.md +6 -23
  2. package/index.ts +7 -0
  3. package/openclaw.plugin.json +799 -0
  4. package/package.json +5 -5
  5. package/src/card/card-markdown-image-reroute.ts +106 -0
  6. package/src/card/card-run-registry.ts +54 -1
  7. package/src/card/card-stop-handler.ts +10 -20
  8. package/src/card/card-streaming-mode.ts +30 -0
  9. package/src/card/card-template.ts +14 -3
  10. package/src/card/reasoning-answer-split.ts +162 -0
  11. package/src/card/statusline-renderer.ts +94 -0
  12. package/src/card-draft-controller.ts +326 -54
  13. package/src/card-service.ts +479 -8
  14. package/src/channel.ts +19 -1062
  15. package/src/config-schema.ts +81 -38
  16. package/src/config.ts +142 -4
  17. package/src/device-registration.ts +245 -0
  18. package/src/gateway/channel-gateway.ts +636 -0
  19. package/src/inbound-handler.ts +489 -49
  20. package/src/media-utils.ts +169 -7
  21. package/src/message-utils.ts +153 -17
  22. package/src/messaging/btw-deliver.ts +85 -0
  23. package/src/messaging/channel-actions.ts +173 -0
  24. package/src/messaging/channel-outbound.ts +158 -0
  25. package/src/messaging/quoted-file-service.ts +9 -4
  26. package/src/onboarding.ts +323 -205
  27. package/src/platform/channel-status.ts +81 -0
  28. package/src/plugin-sdk-channel-actions-augment.ts +11 -0
  29. package/src/reply-strategy-card.ts +568 -44
  30. package/src/reply-strategy-markdown.ts +2 -2
  31. package/src/reply-strategy-types.ts +93 -0
  32. package/src/reply-strategy-with-reaction.ts +1 -1
  33. package/src/reply-strategy.ts +14 -56
  34. package/src/run-usage-store.ts +59 -0
  35. package/src/send-service.ts +225 -7
  36. package/src/session-state.ts +62 -0
  37. package/src/targeting/agent-name-matcher.ts +28 -0
  38. package/src/targeting/agent-routing.ts +44 -28
  39. package/src/types.ts +49 -117
  40. package/src/utils.ts +25 -0
package/src/onboarding.ts CHANGED
@@ -6,9 +6,13 @@ import type {
6
6
  WizardPrompter,
7
7
  } from "openclaw/plugin-sdk/setup";
8
8
  import { DEFAULT_ACCOUNT_ID, formatDocsLink, normalizeAccountId } from "openclaw/plugin-sdk/setup";
9
- import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store.js";
9
+ import { listDingTalkAccountIds, resolveDingTalkAccount } from "./config.js";
10
+ import {
11
+ beginDeviceRegistration,
12
+ openUrlInBrowser,
13
+ RegistrationError,
14
+ } from "./device-registration.js";
10
15
  import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
11
- import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
12
16
 
13
17
  const channel = "dingtalk" as const;
14
18
 
@@ -16,13 +20,6 @@ function isConfigured(account: DingTalkConfig): boolean {
16
20
  return Boolean(account.clientId && account.clientSecret);
17
21
  }
18
22
 
19
- function parseList(value: string): string[] {
20
- return value
21
- .split(/[\n,;]+/g)
22
- .map((entry) => entry.trim())
23
- .filter(Boolean);
24
- }
25
-
26
23
  function applyAccountNameToChannelSection(params: {
27
24
  cfg: OpenClawConfig;
28
25
  channelKey: string;
@@ -52,33 +49,62 @@ async function promptDingTalkAccountId(options: {
52
49
  defaultAccountId: string;
53
50
  }): Promise<string> {
54
51
  const existingIds = options.listAccountIds(options.cfg);
55
- if (existingIds.length === 0) {
52
+ const hasDefault = existingIds.includes(options.defaultAccountId);
53
+ const namedIds = existingIds.filter((id) => id !== options.defaultAccountId);
54
+ const action = await options.prompter.select({
55
+ message: `Choose ${options.label} account`,
56
+ options: [
57
+ {
58
+ label: hasDefault ? "Configure default account" : "Add default account",
59
+ value: "default",
60
+ },
61
+ ...(namedIds.length > 0
62
+ ? [{ label: "Modify existing named account", value: "existing" }]
63
+ : []),
64
+ { label: "Add named account", value: "new" },
65
+ ],
66
+ initialValue: hasDefault ? "default" : namedIds.length > 0 ? "existing" : "default",
67
+ });
68
+
69
+ if (action === "default") {
56
70
  return options.defaultAccountId;
57
71
  }
58
- const useExisting = await options.prompter.confirm({
59
- message: `Use existing ${options.label} account?`,
60
- initialValue: true,
61
- });
62
- if (useExisting) {
63
- if (existingIds.includes(options.currentId)) {
64
- return options.currentId;
65
- }
72
+
73
+ if (action === "existing") {
66
74
  const selected = await options.prompter.select({
67
75
  message: `Select existing ${options.label} account`,
68
- options: existingIds.map((accountId) => ({
76
+ options: namedIds.map((accountId) => ({
69
77
  label: accountId,
70
78
  value: accountId,
71
79
  })),
72
- initialValue: existingIds[0],
80
+ initialValue: namedIds[0],
73
81
  });
74
82
  return normalizeAccountId(String(selected));
75
83
  }
76
- const newId = await options.prompter.text({
77
- message: `New ${options.label} account ID`,
78
- placeholder: options.defaultAccountId,
79
- initialValue: options.defaultAccountId,
80
- });
81
- return normalizeAccountId(String(newId));
84
+
85
+ while (true) {
86
+ const raw = await options.prompter.text({
87
+ message: `New ${options.label} account ID`,
88
+ placeholder: "work",
89
+ initialValue: "",
90
+ });
91
+ const normalized = normalizeAccountId(String(raw));
92
+ if (!normalized || normalized === options.defaultAccountId) {
93
+ await options.prompter.note(
94
+ "Enter a non-default account ID, for example: work",
95
+ "DingTalk account",
96
+ );
97
+ continue;
98
+ }
99
+ if (existingIds.includes(normalized)) {
100
+ await options.prompter.note(
101
+ `Account "${normalized}" already exists. Choose Modify existing named account to edit it.`,
102
+ "DingTalk account",
103
+ );
104
+ continue;
105
+ }
106
+ return normalized;
107
+ }
82
108
  }
83
109
 
84
110
  async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
@@ -96,6 +122,141 @@ async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
96
122
  );
97
123
  }
98
124
 
125
+ async function noteDmAllowlistGuidance(prompter: WizardPrompter): Promise<void> {
126
+ await prompter.note(
127
+ [
128
+ "DM allowlist requires DingTalk userId values.",
129
+ "Ask each target user to send a direct message to this bot.",
130
+ "The plugin will show the observed userId so an admin can add it to channels.dingtalk.allowFrom.",
131
+ ].join("\n"),
132
+ "DingTalk DM allowlist",
133
+ );
134
+ }
135
+
136
+ async function noteGroupAllowlistGuidance(prompter: WizardPrompter): Promise<void> {
137
+ await prompter.note(
138
+ [
139
+ "Group allowlist requires DingTalk conversationId values.",
140
+ "Ask a member to @mention this bot in the target group.",
141
+ "The plugin will show the observed group ID so an admin can configure channels.dingtalk.groups or related allowlist settings.",
142
+ ].join("\n"),
143
+ "DingTalk group allowlist",
144
+ );
145
+ }
146
+
147
+ async function noteDingTalkSetupComplete(prompter: WizardPrompter): Promise<void> {
148
+ await prompter.note(
149
+ [
150
+ "DingTalk configuration has been saved.",
151
+ "For named accounts, configuration lives under channels.dingtalk.accounts.",
152
+ "If you selected allowlist policies, ask the target user or group to message this bot first; the plugin will show IDs that an admin can add manually.",
153
+ "Advanced runtime settings can be edited in the config UI or openclaw.json.",
154
+ "Restart the gateway to apply changes:",
155
+ " openclaw gateway restart",
156
+ ].join("\n"),
157
+ "DingTalk setup complete",
158
+ );
159
+ }
160
+
161
+ function validateMinInteger(min: number) {
162
+ return (value: string): string | undefined => {
163
+ const raw = String(value ?? "").trim();
164
+ const num = Number(raw);
165
+ if (!raw) {
166
+ return "Required";
167
+ }
168
+ if (!Number.isInteger(num) || num < min) {
169
+ return `Must be an integer >= ${min}`;
170
+ }
171
+ return undefined;
172
+ };
173
+ }
174
+
175
+ type CardAdvancedConfig = Partial<
176
+ Pick<
177
+ DingTalkConfig,
178
+ "cardStreamingMode" | "cardStreamInterval" | "cardAtSender" | "cardStatusLine"
179
+ >
180
+ >;
181
+
182
+ async function promptCardAdvancedConfig(params: {
183
+ resolved: DingTalkConfig;
184
+ prompter: WizardPrompter;
185
+ }): Promise<CardAdvancedConfig> {
186
+ const { resolved, prompter } = params;
187
+ const cardStreamingMode = (await prompter.select({
188
+ message: "Card streaming mode",
189
+ options: [
190
+ { label: "Off - answer does not stream incrementally", value: "off" },
191
+ { label: "Answer - only answer streams incrementally", value: "answer" },
192
+ { label: "All - answer and thinking stream incrementally", value: "all" },
193
+ ],
194
+ initialValue: resolved.cardStreamingMode ?? (resolved.cardRealTimeStream ? "all" : "off"),
195
+ })) as DingTalkConfig["cardStreamingMode"];
196
+
197
+ const cardStreamInterval = Number(
198
+ String(
199
+ await prompter.text({
200
+ message: "Card stream interval (ms)",
201
+ placeholder: "1000",
202
+ initialValue: String(resolved.cardStreamInterval ?? 1000),
203
+ validate: validateMinInteger(200),
204
+ }),
205
+ ).trim(),
206
+ );
207
+
208
+ const cardAtSenderRaw = String(
209
+ await prompter.text({
210
+ message: "Card completion @mention text (optional)",
211
+ placeholder: "Reply complete",
212
+ initialValue: resolved.cardAtSender || undefined,
213
+ }),
214
+ ).trim();
215
+
216
+ const wantsStatusLine = await prompter.confirm({
217
+ message: "Customize AI card status line?",
218
+ initialValue: Boolean(resolved.cardStatusLine),
219
+ });
220
+
221
+ let cardStatusLine: DingTalkConfig["cardStatusLine"] | undefined;
222
+ if (wantsStatusLine) {
223
+ const current = resolved.cardStatusLine ?? {};
224
+ cardStatusLine = {
225
+ model: await prompter.confirm({
226
+ message: "Show model name?",
227
+ initialValue: current.model ?? true,
228
+ }),
229
+ effort: await prompter.confirm({
230
+ message: "Show thinking effort?",
231
+ initialValue: current.effort ?? true,
232
+ }),
233
+ agent: await prompter.confirm({
234
+ message: "Show agent name?",
235
+ initialValue: current.agent ?? true,
236
+ }),
237
+ taskTime: await prompter.confirm({
238
+ message: "Show task elapsed time?",
239
+ initialValue: current.taskTime ?? false,
240
+ }),
241
+ tokens: await prompter.confirm({
242
+ message: "Show token usage?",
243
+ initialValue: current.tokens ?? false,
244
+ }),
245
+ dapiUsage: await prompter.confirm({
246
+ message: "Show DingTalk API usage?",
247
+ initialValue: current.dapiUsage ?? false,
248
+ }),
249
+ };
250
+ }
251
+
252
+ return {
253
+ cardStreamingMode,
254
+ cardStreamInterval,
255
+ ...(cardAtSenderRaw ? { cardAtSender: cardAtSenderRaw } : {}),
256
+ ...(cardStatusLine ? { cardStatusLine } : {}),
257
+ };
258
+ }
259
+
99
260
  function applyAccountConfig(params: {
100
261
  cfg: OpenClawConfig;
101
262
  accountId: string;
@@ -122,10 +283,17 @@ function applyAccountConfig(params: {
122
283
  ? { groupAllowFrom: input.groupAllowFrom }
123
284
  : {}),
124
285
  ...(input.displayNameResolution ? { displayNameResolution: input.displayNameResolution } : {}),
286
+ ...(input.contextVisibility ? { contextVisibility: input.contextVisibility } : {}),
125
287
  ...(input.mediaUrlAllowlist && input.mediaUrlAllowlist.length > 0
126
288
  ? { mediaUrlAllowlist: input.mediaUrlAllowlist }
127
289
  : {}),
128
290
  ...(input.messageType ? { messageType: input.messageType } : {}),
291
+ ...(input.cardStreamingMode ? { cardStreamingMode: input.cardStreamingMode } : {}),
292
+ ...(typeof input.cardStreamInterval === "number"
293
+ ? { cardStreamInterval: input.cardStreamInterval }
294
+ : {}),
295
+ ...(input.cardAtSender ? { cardAtSender: input.cardAtSender } : {}),
296
+ ...(input.cardStatusLine ? { cardStatusLine: input.cardStatusLine } : {}),
129
297
  ...(typeof input.maxReconnectCycles === "number"
130
298
  ? { maxReconnectCycles: input.maxReconnectCycles }
131
299
  : {}),
@@ -191,18 +359,6 @@ function applyGenericSetupInput(params: {
191
359
  });
192
360
  }
193
361
 
194
- function validatePositiveInteger(value: string): string | undefined {
195
- const raw = String(value ?? "").trim();
196
- const num = Number(raw);
197
- if (!raw) {
198
- return "Required";
199
- }
200
- if (!Number.isInteger(num) || num < 1) {
201
- return "Must be an integer >= 1";
202
- }
203
- return undefined;
204
- }
205
-
206
362
  async function configureDingTalkAccount(params: {
207
363
  cfg: OpenClawConfig;
208
364
  accountId: string;
@@ -211,210 +367,171 @@ async function configureDingTalkAccount(params: {
211
367
  const { cfg, accountId, prompter } = params;
212
368
  const resolved = resolveDingTalkAccount(cfg, accountId);
213
369
 
214
- await noteDingTalkHelp(prompter);
215
-
216
- const clientId = await prompter.text({
217
- message: "Client ID (AppKey)",
218
- placeholder: "dingxxxxxxxx",
219
- initialValue: resolved.clientId ?? undefined,
220
- validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
221
- });
222
-
223
- const clientSecret = await prompter.text({
224
- message: "Client Secret (AppSecret)",
225
- placeholder: "xxx-xxx-xxx-xxx",
226
- initialValue: resolved.clientSecret ?? undefined,
227
- validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
228
- });
229
-
230
- const wantsCardMode = await prompter.confirm({
231
- message: "Enable AI interactive card mode? (for streaming AI responses)",
232
- initialValue: resolved.messageType === "card",
370
+ // ── Credential acquisition: auto-register or manual ────────────────────
371
+ const hasExistingCredentials = Boolean(resolved.clientId && resolved.clientSecret);
372
+ const credentialMethod = await prompter.select({
373
+ message: "How do you want to get DingTalk bot credentials?",
374
+ options: [
375
+ { label: "Auto-register an OpenClaw DingTalk bot", value: "auto" },
376
+ { label: "Enter an existing DingTalk bot Client ID / Client Secret", value: "manual" },
377
+ ],
378
+ initialValue: hasExistingCredentials ? "manual" : "auto",
233
379
  });
234
380
 
235
- let messageType: "markdown" | "card" = "markdown";
236
-
237
- if (wantsCardMode) {
238
- await prompter.note(
239
- [
240
- "AI interactive card mode now uses the built-in DingTalk template contract.",
241
- "No manual Template ID or content field configuration is required.",
242
- "Legacy cardTemplateId/cardTemplateKey config is deprecated and ignored.",
243
- ].join("\n"),
244
- "Built-in AI Card Template",
245
- );
246
- messageType = "card";
381
+ let clientId: string;
382
+ let clientSecret: string;
383
+
384
+ if (credentialMethod === "auto") {
385
+ try {
386
+ const session = await beginDeviceRegistration();
387
+
388
+ openUrlInBrowser(session.verificationUrl);
389
+
390
+ await prompter.note(
391
+ [
392
+ "Opened the authorization page in your browser.",
393
+ "Scan the authorization code in DingTalk to finish registration.",
394
+ "",
395
+ "If the browser did not open automatically, visit this link manually:",
396
+ session.verificationUrl,
397
+ ].join("\n"),
398
+ "DingTalk bot auto-registration",
399
+ );
400
+
401
+ let lastWaitingNote = 0;
402
+ const result = await session.waitForResult({
403
+ onWaiting: () => {
404
+ const now = Date.now();
405
+ if (now - lastWaitingNote >= 15_000) {
406
+ lastWaitingNote = now;
407
+ prompter
408
+ .note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling")
409
+ .catch(() => {});
410
+ }
411
+ },
412
+ });
413
+ clientId = result.clientId;
414
+ clientSecret = result.clientSecret;
415
+
416
+ await prompter.note(
417
+ [
418
+ "Registration succeeded!",
419
+ `Client ID: ${clientId}`,
420
+ "Client Secret: [captured; see config file]",
421
+ ].join("\n"),
422
+ "Registration complete",
423
+ );
424
+ } catch (err) {
425
+ const message = err instanceof RegistrationError ? err.message : String(err);
426
+ await prompter.note(
427
+ [`Auto-registration failed: ${message}`, "", "Falling back to manual input."].join("\n"),
428
+ "Registration failed",
429
+ );
430
+ // Fall through to manual path
431
+ await noteDingTalkHelp(prompter);
432
+ clientId = String(
433
+ await prompter.text({
434
+ message: "Client ID (AppKey)",
435
+ placeholder: "dingxxxxxxxx",
436
+ initialValue: resolved.clientId ?? undefined,
437
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
438
+ }),
439
+ ).trim();
440
+ clientSecret = String(
441
+ await prompter.text({
442
+ message: "Client Secret (AppSecret)",
443
+ placeholder: "xxx-xxx-xxx-xxx",
444
+ initialValue: resolved.clientSecret ?? undefined,
445
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
446
+ }),
447
+ ).trim();
448
+ }
449
+ } else {
450
+ // Manual path — existing behavior
451
+ await noteDingTalkHelp(prompter);
452
+ clientId = String(
453
+ await prompter.text({
454
+ message: "Client ID (AppKey)",
455
+ placeholder: "dingxxxxxxxx",
456
+ initialValue: resolved.clientId ?? undefined,
457
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
458
+ }),
459
+ ).trim();
460
+ clientSecret = String(
461
+ await prompter.text({
462
+ message: "Client Secret (AppSecret)",
463
+ placeholder: "xxx-xxx-xxx-xxx",
464
+ initialValue: resolved.clientSecret ?? undefined,
465
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
466
+ }),
467
+ ).trim();
247
468
  }
248
469
 
249
- const dmPolicyValue = await prompter.select({
470
+ const dmPolicyValue = (await prompter.select({
250
471
  message: "Direct message policy",
251
472
  options: [
252
473
  { label: "Open - anyone can DM", value: "open" },
253
- { label: "Allowlist - only allowed users", value: "allowlist" },
474
+ { label: "Pairing - require OpenClaw pairing approval", value: "pairing" },
475
+ { label: "Allowlist - only manually allowed users", value: "allowlist" },
254
476
  ],
255
477
  initialValue: resolved.dmPolicy ?? "open",
256
- });
478
+ })) as "open" | "pairing" | "allowlist";
257
479
 
258
- let allowFrom: string[] | undefined;
259
480
  if (dmPolicyValue === "allowlist") {
260
- const entry = await prompter.text({
261
- message: "Allowed user IDs (comma-separated)",
262
- placeholder: "user1, user2",
263
- });
264
- const parsed = parseList(String(entry ?? ""));
265
- allowFrom = parsed.length > 0 ? parsed : undefined;
481
+ await noteDmAllowlistGuidance(prompter);
266
482
  }
267
483
 
268
- const mediaUrlAllowlistEntry = await prompter.text({
269
- message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
270
- placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
271
- initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
272
- });
273
- const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
274
- const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
275
-
276
- const groupPolicyValue = await prompter.select({
484
+ const groupPolicyValue = (await prompter.select({
277
485
  message: "Group message policy",
278
486
  options: [
279
487
  { label: "Open - any group can use bot", value: "open" },
280
- { label: "Allowlist - only allowed groups", value: "allowlist" },
488
+ { label: "Allowlist - only manually configured groups", value: "allowlist" },
281
489
  { label: "Disabled - block all group messages", value: "disabled" },
282
490
  ],
283
491
  initialValue: resolved.groupPolicy ?? "open",
284
- });
492
+ })) as "open" | "allowlist" | "disabled";
285
493
 
286
494
  if (groupPolicyValue === "allowlist") {
287
- await prompter.note(
288
- [
289
- 'groupPolicy=allowlist requires "groups" config to specify allowed group IDs.',
290
- "After setup, manually add group conversationIds to your config:",
291
- "",
292
- ' "groups": { "cidXXX": {}, "cidYYY": { "systemPrompt": "..." } }',
293
- "",
294
- 'Groups not listed will be blocked. Use "*" as key to allow all groups.',
295
- ].join("\n"),
296
- );
495
+ await noteGroupAllowlistGuidance(prompter);
297
496
  }
298
497
 
299
- let groupAllowFrom: string[] | undefined;
300
- if (groupPolicyValue !== "disabled") {
301
- const groupAllowFromEntry = await prompter.text({
302
- message: "Group sender allowlist - user IDs allowed in groups (comma-separated, optional)",
303
- placeholder: "user1, user2",
304
- initialValue: (resolved.groupAllowFrom || []).join(", ") || undefined,
305
- });
306
- const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
307
- groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
308
- }
309
-
310
- await prompter.note(
311
- [
312
- "Enabling learned displayName target resolution has tradeoffs:",
313
- "- learned names come from observed inbound messages and can become stale",
314
- "- duplicate display names can resolve to the wrong group or user",
315
- '- current upstream target resolution does not provide requester authz, so "all" applies to every caller that can reach the send flow',
316
- "Use explicit IDs for sensitive or high-risk deliveries.",
317
- ].join("\n"),
318
- "displayName resolution risk",
319
- );
320
-
321
- const displayNameResolutionValue = await prompter.select({
322
- message: "Learned displayName target resolution",
498
+ const messageType = (await prompter.select({
499
+ message: "Reply message type",
323
500
  options: [
324
- {
325
- label: "Disabled - require explicit IDs",
326
- value: "disabled",
327
- },
328
- {
329
- label: "All - learned lookup for all callers (higher risk)",
330
- value: "all",
331
- },
501
+ { label: "Markdown - standard DingTalk messages", value: "markdown" },
502
+ { label: "AI Card - interactive card replies", value: "card" },
332
503
  ],
333
- initialValue: resolved.displayNameResolution ?? "disabled",
334
- });
335
-
336
- let maxReconnectCycles: number | undefined;
337
- const wantsReconnectLimits = await prompter.confirm({
338
- message: "Configure runtime reconnect cycle limit? (recommended)",
339
- initialValue: typeof resolved.maxReconnectCycles === "number",
340
- });
341
- if (wantsReconnectLimits) {
342
- const parsedCycles = Number(
343
- String(
344
- await prompter.text({
345
- message: "Max runtime reconnect cycles",
346
- placeholder: "10",
347
- initialValue: String(resolved.maxReconnectCycles ?? 10),
348
- validate: (value: string) => validatePositiveInteger(value),
349
- }),
350
- ).trim(),
351
- );
352
- maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
353
- }
354
-
355
- let mediaMaxMb: number | undefined;
356
- const wantsMediaMax = await prompter.confirm({
357
- message: "Configure inbound media max size in MB? (optional)",
358
- initialValue: typeof resolved.mediaMaxMb === "number",
359
- });
360
- if (wantsMediaMax) {
361
- const parsedMediaMax = Number(
362
- String(
363
- await prompter.text({
364
- message: "Max inbound media size (MB)",
365
- placeholder: "20",
366
- initialValue:
367
- typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
368
- validate: (value: string) => validatePositiveInteger(value),
369
- }),
370
- ).trim(),
371
- );
372
- mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
373
- }
504
+ initialValue: resolved.messageType ?? "markdown",
505
+ })) as "markdown" | "card";
374
506
 
375
- let journalTTLDays: number | undefined;
376
- const wantsJournalTTL = await prompter.confirm({
377
- message: "Configure quote journal retention in days?",
378
- initialValue: typeof resolved.journalTTLDays === "number",
507
+ const wantsAdvanced = await prompter.confirm({
508
+ message: "Configure advanced DingTalk options?",
509
+ initialValue: false,
379
510
  });
380
- if (wantsJournalTTL) {
381
- const parsedJournalTTL = Number(
382
- String(
383
- await prompter.text({
384
- message: "Quote journal retention days",
385
- placeholder: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
386
- initialValue:
387
- typeof resolved.journalTTLDays === "number"
388
- ? String(resolved.journalTTLDays)
389
- : String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
390
- validate: (value: string) => validatePositiveInteger(value),
391
- }),
392
- ).trim(),
511
+ let cardAdvanced: CardAdvancedConfig = {};
512
+ if (wantsAdvanced && messageType === "card") {
513
+ cardAdvanced = await promptCardAdvancedConfig({ resolved, prompter });
514
+ } else if (wantsAdvanced) {
515
+ await prompter.note(
516
+ "No markdown-specific advanced onboarding options are required. Other advanced settings can be edited in the config UI or openclaw.json.",
517
+ "DingTalk advanced options",
393
518
  );
394
- journalTTLDays =
395
- Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
396
- ? parsedJournalTTL
397
- : DEFAULT_MESSAGE_CONTEXT_TTL_DAYS;
398
519
  }
399
520
 
400
- return applyAccountConfig({
521
+ const nextCfg = applyAccountConfig({
401
522
  cfg,
402
523
  accountId,
403
524
  input: {
404
525
  clientId: String(clientId).trim(),
405
526
  clientSecret: String(clientSecret).trim(),
406
- dmPolicy: dmPolicyValue as "open" | "allowlist",
407
- groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
408
- allowFrom,
409
- groupAllowFrom,
410
- displayNameResolution: displayNameResolutionValue as "disabled" | "all",
411
- mediaUrlAllowlist,
527
+ dmPolicy: dmPolicyValue,
528
+ groupPolicy: groupPolicyValue,
412
529
  messageType,
413
- maxReconnectCycles,
414
- mediaMaxMb,
415
- journalTTLDays,
530
+ ...cardAdvanced,
416
531
  },
417
532
  });
533
+ await noteDingTalkSetupComplete(prompter);
534
+ return nextCfg;
418
535
  }
419
536
 
420
537
  export const dingtalkSetupAdapter: ChannelSetupAdapter = {
@@ -445,9 +562,10 @@ export const dingtalkSetupWizard: ChannelSetupWizard = {
445
562
  `DingTalk: ${configured ? "configured" : "needs setup"}`,
446
563
  ],
447
564
  resolveSelectionHint: ({ configured }) =>
448
- configured ? "configured" : "钉钉企业机器人",
565
+ configured ? "configured" : "DingTalk enterprise bot",
449
566
  resolveQuickstartScore: ({ configured }) => (configured ? 1 : 4),
450
567
  },
568
+ resolveShouldPromptAccountIds: () => true,
451
569
  resolveAccountIdForConfigure: async ({
452
570
  cfg,
453
571
  prompter,