@soimy/dingtalk 3.4.0 → 3.4.1

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/onboarding.ts CHANGED
@@ -1,5 +1,11 @@
1
- import type { OpenClawConfig, ChannelOnboardingAdapter, WizardPrompter } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId, formatDocsLink } from "openclaw/plugin-sdk";
1
+ import type {
2
+ ChannelSetupAdapter,
3
+ ChannelSetupInput,
4
+ ChannelSetupWizard,
5
+ OpenClawConfig,
6
+ WizardPrompter,
7
+ } from "openclaw/plugin-sdk/setup";
8
+ import { DEFAULT_ACCOUNT_ID, formatDocsLink, normalizeAccountId } from "openclaw/plugin-sdk/setup";
3
9
  import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store.js";
4
10
  import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
5
11
  import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
@@ -53,8 +59,19 @@ async function promptDingTalkAccountId(options: {
53
59
  message: `Use existing ${options.label} account?`,
54
60
  initialValue: true,
55
61
  });
56
- if (useExisting && existingIds.includes(options.currentId)) {
57
- return options.currentId;
62
+ if (useExisting) {
63
+ if (existingIds.includes(options.currentId)) {
64
+ return options.currentId;
65
+ }
66
+ const selected = await options.prompter.select({
67
+ message: `Select existing ${options.label} account`,
68
+ options: existingIds.map((accountId) => ({
69
+ label: accountId,
70
+ value: accountId,
71
+ })),
72
+ initialValue: existingIds[0],
73
+ });
74
+ return normalizeAccountId(String(selected));
58
75
  }
59
76
  const newId = await options.prompter.text({
60
77
  message: `New ${options.label} account ID`,
@@ -73,7 +90,7 @@ async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
73
90
  "3. Enable 'Robot' capability",
74
91
  "4. Configure message receiving mode as 'Stream mode'",
75
92
  "5. Copy Client ID (AppKey) and Client Secret (AppSecret)",
76
- `Docs: ${formatDocsLink("/channels/dingtalk", "channels/dingtalk")}`,
93
+ `Docs: ${formatDocsLink("https://github.com/soimy/openclaw-channel-dingtalk", "plugin docs")}`,
77
94
  ].join("\n"),
78
95
  "DingTalk setup",
79
96
  );
@@ -104,9 +121,12 @@ function applyAccountConfig(params: {
104
121
  ...(input.dmPolicy ? { dmPolicy: input.dmPolicy } : {}),
105
122
  ...(input.groupPolicy ? { groupPolicy: input.groupPolicy } : {}),
106
123
  ...(input.allowFrom && input.allowFrom.length > 0 ? { allowFrom: input.allowFrom } : {}),
107
- ...(input.groupAllowFrom && input.groupAllowFrom.length > 0 ? { groupAllowFrom: input.groupAllowFrom } : {}),
108
- ...(input.displayNameResolution
109
- ? { displayNameResolution: input.displayNameResolution }
124
+ ...(input.groupAllowFrom && input.groupAllowFrom.length > 0
125
+ ? { groupAllowFrom: input.groupAllowFrom }
126
+ : {}),
127
+ ...(input.displayNameResolution ? { displayNameResolution: input.displayNameResolution } : {}),
128
+ ...(input.mediaUrlAllowlist && input.mediaUrlAllowlist.length > 0
129
+ ? { mediaUrlAllowlist: input.mediaUrlAllowlist }
110
130
  : {}),
111
131
  ...(input.messageType ? { messageType: input.messageType } : {}),
112
132
  ...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
@@ -159,341 +179,376 @@ function applyAccountConfig(params: {
159
179
  };
160
180
  }
161
181
 
162
- export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
163
- channel,
164
- getStatus: ({ cfg }) => {
165
- const accountIds = listDingTalkAccountIds(cfg);
166
- const configured =
167
- accountIds.length > 0
168
- ? accountIds.some((accountId) => isConfigured(resolveDingTalkAccount(cfg, accountId)))
169
- : isConfigured(resolveDingTalkAccount(cfg, DEFAULT_ACCOUNT_ID));
182
+ function applyGenericSetupInput(params: {
183
+ cfg: OpenClawConfig;
184
+ accountId: string;
185
+ input: ChannelSetupInput;
186
+ }): OpenClawConfig {
187
+ return applyAccountConfig({
188
+ cfg: params.cfg,
189
+ accountId: params.accountId,
190
+ input: {
191
+ name: params.input.name,
192
+ clientId: typeof params.input.token === "string" ? params.input.token.trim() : undefined,
193
+ clientSecret:
194
+ typeof params.input.password === "string" ? params.input.password.trim() : undefined,
195
+ robotCode: typeof params.input.code === "string" ? params.input.code.trim() : undefined,
196
+ },
197
+ });
198
+ }
170
199
 
171
- return Promise.resolve({
172
- channel,
173
- configured,
174
- statusLines: [`DingTalk: ${configured ? "configured" : "needs setup"}`],
175
- selectionHint: configured ? "configured" : "钉钉企业机器人",
176
- quickstartScore: configured ? 1 : 4,
177
- });
178
- },
179
- configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
180
- const override = accountOverrides[channel]?.trim();
181
- let accountId = override ? normalizeAccountId(override) : DEFAULT_ACCOUNT_ID;
182
-
183
- if (shouldPromptAccountIds && !override) {
184
- accountId = await promptDingTalkAccountId({
185
- cfg,
186
- prompter,
187
- label: "DingTalk",
188
- currentId: accountId,
189
- listAccountIds: listDingTalkAccountIds,
190
- defaultAccountId: DEFAULT_ACCOUNT_ID,
191
- });
192
- }
200
+ function validatePositiveInteger(value: string): string | undefined {
201
+ const raw = String(value ?? "").trim();
202
+ const num = Number(raw);
203
+ if (!raw) {
204
+ return "Required";
205
+ }
206
+ if (!Number.isInteger(num) || num < 1) {
207
+ return "Must be an integer >= 1";
208
+ }
209
+ return undefined;
210
+ }
193
211
 
194
- const resolved = resolveDingTalkAccount(cfg, accountId);
195
- await noteDingTalkHelp(prompter);
212
+ async function configureDingTalkAccount(params: {
213
+ cfg: OpenClawConfig;
214
+ accountId: string;
215
+ prompter: WizardPrompter;
216
+ }): Promise<OpenClawConfig> {
217
+ const { cfg, accountId, prompter } = params;
218
+ const resolved = resolveDingTalkAccount(cfg, accountId);
196
219
 
197
- const clientId = await prompter.text({
198
- message: "Client ID (AppKey)",
199
- placeholder: "dingxxxxxxxx",
200
- initialValue: resolved.clientId ?? undefined,
201
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
202
- });
220
+ await noteDingTalkHelp(prompter);
203
221
 
204
- const clientSecret = await prompter.text({
205
- message: "Client Secret (AppSecret)",
206
- placeholder: "xxx-xxx-xxx-xxx",
207
- initialValue: resolved.clientSecret ?? undefined,
208
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
209
- });
222
+ const clientId = await prompter.text({
223
+ message: "Client ID (AppKey)",
224
+ placeholder: "dingxxxxxxxx",
225
+ initialValue: resolved.clientId ?? undefined,
226
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
227
+ });
210
228
 
211
- const wantsFullConfig = await prompter.confirm({
212
- message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
213
- initialValue: false,
214
- });
229
+ const clientSecret = await prompter.text({
230
+ message: "Client Secret (AppSecret)",
231
+ placeholder: "xxx-xxx-xxx-xxx",
232
+ initialValue: resolved.clientSecret ?? undefined,
233
+ validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
234
+ });
215
235
 
216
- let robotCode: string | undefined;
217
- let corpId: string | undefined;
218
- let agentId: string | undefined;
219
-
220
- if (wantsFullConfig) {
221
- robotCode =
222
- String(
223
- await prompter.text({
224
- message: "Robot Code",
225
- placeholder: "dingxxxxxxxx",
226
- initialValue: resolved.robotCode ?? undefined,
227
- }),
228
- ).trim() || undefined;
229
-
230
- corpId =
231
- String(
232
- await prompter.text({
233
- message: "Corp ID",
234
- placeholder: "dingxxxxxxxx",
235
- initialValue: resolved.corpId ?? undefined,
236
- }),
237
- ).trim() || undefined;
238
-
239
- agentId =
240
- String(
241
- await prompter.text({
242
- message: "Agent ID",
243
- placeholder: "123456789",
244
- initialValue: resolved.agentId ? String(resolved.agentId) : undefined,
245
- }),
246
- ).trim() || undefined;
247
- }
236
+ const wantsFullConfig = await prompter.confirm({
237
+ message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
238
+ initialValue: false,
239
+ });
248
240
 
249
- const wantsCardMode = await prompter.confirm({
250
- message: "Enable AI interactive card mode? (for streaming AI responses)",
251
- initialValue: resolved.messageType === "card",
252
- });
241
+ let robotCode: string | undefined;
242
+ let corpId: string | undefined;
243
+ let agentId: string | undefined;
244
+
245
+ if (wantsFullConfig) {
246
+ robotCode =
247
+ String(
248
+ await prompter.text({
249
+ message: "Robot Code",
250
+ placeholder: "dingxxxxxxxx",
251
+ initialValue: resolved.robotCode ?? undefined,
252
+ }),
253
+ ).trim() || undefined;
254
+
255
+ corpId =
256
+ String(
257
+ await prompter.text({
258
+ message: "Corp ID",
259
+ placeholder: "dingxxxxxxxx",
260
+ initialValue: resolved.corpId ?? undefined,
261
+ }),
262
+ ).trim() || undefined;
263
+
264
+ agentId =
265
+ String(
266
+ await prompter.text({
267
+ message: "Agent ID",
268
+ placeholder: "123456789",
269
+ initialValue: resolved.agentId ? String(resolved.agentId) : undefined,
270
+ }),
271
+ ).trim() || undefined;
272
+ }
253
273
 
254
- let cardTemplateId: string | undefined;
255
- let cardTemplateKey: string | undefined;
256
- let messageType: "markdown" | "card" = "markdown";
257
-
258
- if (wantsCardMode) {
259
- await prompter.note(
260
- [
261
- "Create an AI card template in DingTalk Developer Console:",
262
- "https://open-dev.dingtalk.com/fe/card",
263
- "1. Go to 'My Templates' > 'Create Template'",
264
- "2. Select 'AI Card' scenario",
265
- "3. Design your card and publish",
266
- "4. Copy the Template ID (e.g., xxx.schema)",
267
- ].join("\n"),
268
- "Card Template Setup",
269
- );
270
-
271
- cardTemplateId =
272
- String(
273
- await prompter.text({
274
- message: "Card Template ID",
275
- placeholder: "xxxxx-xxxxx-xxxxx.schema",
276
- initialValue: resolved.cardTemplateId ?? undefined,
277
- }),
278
- ).trim() || undefined;
279
-
280
- cardTemplateKey =
281
- String(
282
- await prompter.text({
283
- message: "Card Template Key (content field name)",
284
- placeholder: "content",
285
- initialValue: resolved.cardTemplateKey ?? "content",
286
- }),
287
- ).trim() || "content";
288
-
289
- messageType = "card";
290
- }
274
+ const wantsCardMode = await prompter.confirm({
275
+ message: "Enable AI interactive card mode? (for streaming AI responses)",
276
+ initialValue: resolved.messageType === "card",
277
+ });
291
278
 
292
- const dmPolicyValue = await prompter.select({
293
- message: "Direct message policy",
294
- options: [
295
- { label: "Open - anyone can DM", value: "open" },
296
- { label: "Allowlist - only allowed users", value: "allowlist" },
297
- ],
298
- initialValue: resolved.dmPolicy ?? "open",
299
- });
279
+ let cardTemplateId: string | undefined;
280
+ let cardTemplateKey: string | undefined;
281
+ let messageType: "markdown" | "card" = "markdown";
300
282
 
301
- let allowFrom: string[] | undefined;
302
- if (dmPolicyValue === "allowlist") {
303
- const entry = await prompter.text({
304
- message: "Allowed user IDs (comma-separated)",
305
- placeholder: "user1, user2",
306
- });
307
- const parsed = parseList(String(entry ?? ""));
308
- allowFrom = parsed.length > 0 ? parsed : undefined;
309
- }
283
+ if (wantsCardMode) {
284
+ await prompter.note(
285
+ [
286
+ "Create an AI card template in DingTalk Developer Console:",
287
+ "https://open-dev.dingtalk.com/fe/card",
288
+ "1. Go to 'My Templates' > 'Create Template'",
289
+ "2. Select 'AI Card' scenario",
290
+ "3. Design your card and publish",
291
+ "4. Copy the Template ID (e.g., xxx.schema)",
292
+ ].join("\n"),
293
+ "Card Template Setup",
294
+ );
310
295
 
311
- const mediaUrlAllowlistEntry = await prompter.text({
312
- message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
313
- placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
314
- initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
315
- });
316
- const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
317
- const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
318
-
319
- const groupPolicyValue = await prompter.select({
320
- message: "Group message policy",
321
- options: [
322
- { label: "Open - any group can use bot", value: "open" },
323
- { label: "Allowlist - only allowed groups", value: "allowlist" },
324
- { label: "Disabled - block all group messages", value: "disabled" },
325
- ],
326
- initialValue: resolved.groupPolicy ?? "open",
327
- });
296
+ cardTemplateId =
297
+ String(
298
+ await prompter.text({
299
+ message: "Card Template ID",
300
+ placeholder: "xxxxx-xxxxx-xxxxx.schema",
301
+ initialValue: resolved.cardTemplateId ?? undefined,
302
+ }),
303
+ ).trim() || undefined;
304
+
305
+ cardTemplateKey =
306
+ String(
307
+ await prompter.text({
308
+ message: "Card Template Key (content field name)",
309
+ placeholder: "content",
310
+ initialValue: resolved.cardTemplateKey ?? "content",
311
+ }),
312
+ ).trim() || "content";
313
+
314
+ messageType = "card";
315
+ }
328
316
 
329
- if (groupPolicyValue === "allowlist") {
330
- await prompter.note(
331
- [
332
- 'groupPolicy=allowlist requires "groups" config to specify allowed group IDs.',
333
- "After setup, manually add group conversationIds to your config:",
334
- "",
335
- ' "groups": { "cidXXX": {}, "cidYYY": { "systemPrompt": "..." } }',
336
- "",
337
- "Groups not listed will be blocked. Use \"*\" as key to allow all groups.",
338
- ].join("\n"),
339
- );
340
- }
317
+ const dmPolicyValue = await prompter.select({
318
+ message: "Direct message policy",
319
+ options: [
320
+ { label: "Open - anyone can DM", value: "open" },
321
+ { label: "Allowlist - only allowed users", value: "allowlist" },
322
+ ],
323
+ initialValue: resolved.dmPolicy ?? "open",
324
+ });
341
325
 
342
- let groupAllowFrom: string[] | undefined;
343
- if (groupPolicyValue !== "disabled") {
344
- const groupAllowFromEntry = await prompter.text({
345
- message: "Group sender allowlist - user IDs allowed in groups (comma-separated, optional)",
346
- placeholder: "user1, user2",
347
- initialValue: (resolved.groupAllowFrom || []).join(", ") || undefined,
348
- });
349
- const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
350
- groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
351
- }
326
+ let allowFrom: string[] | undefined;
327
+ if (dmPolicyValue === "allowlist") {
328
+ const entry = await prompter.text({
329
+ message: "Allowed user IDs (comma-separated)",
330
+ placeholder: "user1, user2",
331
+ });
332
+ const parsed = parseList(String(entry ?? ""));
333
+ allowFrom = parsed.length > 0 ? parsed : undefined;
334
+ }
335
+
336
+ const mediaUrlAllowlistEntry = await prompter.text({
337
+ message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
338
+ placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
339
+ initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
340
+ });
341
+ const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
342
+ const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
343
+
344
+ const groupPolicyValue = await prompter.select({
345
+ message: "Group message policy",
346
+ options: [
347
+ { label: "Open - any group can use bot", value: "open" },
348
+ { label: "Allowlist - only allowed groups", value: "allowlist" },
349
+ { label: "Disabled - block all group messages", value: "disabled" },
350
+ ],
351
+ initialValue: resolved.groupPolicy ?? "open",
352
+ });
352
353
 
354
+ if (groupPolicyValue === "allowlist") {
353
355
  await prompter.note(
354
356
  [
355
- "Enabling learned displayName target resolution has tradeoffs:",
356
- "- learned names come from observed inbound messages and can become stale",
357
- "- duplicate display names can resolve to the wrong group or user",
358
- "- current upstream target resolution does not provide requester authz, so \"all\" applies to every caller that can reach the send flow",
359
- "Use explicit IDs for sensitive or high-risk deliveries.",
357
+ 'groupPolicy=allowlist requires "groups" config to specify allowed group IDs.',
358
+ "After setup, manually add group conversationIds to your config:",
359
+ "",
360
+ ' "groups": { "cidXXX": {}, "cidYYY": { "systemPrompt": "..." } }',
361
+ "",
362
+ 'Groups not listed will be blocked. Use "*" as key to allow all groups.',
360
363
  ].join("\n"),
361
- "displayName resolution risk",
362
364
  );
365
+ }
363
366
 
364
- const displayNameResolutionValue = await prompter.select({
365
- message: "Learned displayName target resolution",
366
- options: [
367
- {
368
- label: "Disabled - require explicit IDs",
369
- value: "disabled",
370
- },
371
- {
372
- label: "All - learned lookup for all callers (higher risk)",
373
- value: "all",
374
- },
375
- ],
376
- initialValue: resolved.displayNameResolution ?? "disabled",
367
+ let groupAllowFrom: string[] | undefined;
368
+ if (groupPolicyValue !== "disabled") {
369
+ const groupAllowFromEntry = await prompter.text({
370
+ message: "Group sender allowlist - user IDs allowed in groups (comma-separated, optional)",
371
+ placeholder: "user1, user2",
372
+ initialValue: (resolved.groupAllowFrom || []).join(", ") || undefined,
377
373
  });
374
+ const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
375
+ groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
376
+ }
378
377
 
379
- let maxReconnectCycles: number | undefined;
380
- const wantsReconnectLimits = await prompter.confirm({
381
- message: "Configure runtime reconnect cycle limit? (recommended)",
382
- initialValue: typeof resolved.maxReconnectCycles === "number",
383
- });
384
- if (wantsReconnectLimits) {
385
- const parsedCycles = Number(
386
- String(
387
- await prompter.text({
388
- message: "Max runtime reconnect cycles",
389
- placeholder: "10",
390
- initialValue: String(resolved.maxReconnectCycles ?? 10),
391
- validate: (value) => {
392
- const raw = String(value ?? "").trim();
393
- const num = Number(raw);
394
- if (!raw) {
395
- return "Required";
396
- }
397
- if (!Number.isInteger(num) || num < 1) {
398
- return "Must be an integer >= 1";
399
- }
400
- return undefined;
401
- },
402
- }),
403
- ).trim(),
404
- );
405
- maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
406
- }
378
+ await prompter.note(
379
+ [
380
+ "Enabling learned displayName target resolution has tradeoffs:",
381
+ "- learned names come from observed inbound messages and can become stale",
382
+ "- duplicate display names can resolve to the wrong group or user",
383
+ '- current upstream target resolution does not provide requester authz, so "all" applies to every caller that can reach the send flow',
384
+ "Use explicit IDs for sensitive or high-risk deliveries.",
385
+ ].join("\n"),
386
+ "displayName resolution risk",
387
+ );
407
388
 
408
- let mediaMaxMb: number | undefined;
409
- const wantsMediaMax = await prompter.confirm({
410
- message: "Configure inbound media max size in MB? (optional)",
411
- initialValue: typeof resolved.mediaMaxMb === "number",
412
- });
413
- if (wantsMediaMax) {
414
- const parsedMediaMax = Number(
415
- String(
416
- await prompter.text({
417
- message: "Max inbound media size (MB)",
418
- placeholder: "20",
419
- initialValue:
420
- typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
421
- validate: (value) => {
422
- const raw = String(value ?? "").trim();
423
- const num = Number(raw);
424
- if (!raw) {
425
- return "Required";
426
- }
427
- if (!Number.isInteger(num) || num < 1) {
428
- return "Must be an integer >= 1";
429
- }
430
- return undefined;
431
- },
432
- }),
433
- ).trim(),
434
- );
435
- mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
436
- }
389
+ const displayNameResolutionValue = await prompter.select({
390
+ message: "Learned displayName target resolution",
391
+ options: [
392
+ {
393
+ label: "Disabled - require explicit IDs",
394
+ value: "disabled",
395
+ },
396
+ {
397
+ label: "All - learned lookup for all callers (higher risk)",
398
+ value: "all",
399
+ },
400
+ ],
401
+ initialValue: resolved.displayNameResolution ?? "disabled",
402
+ });
437
403
 
438
- let journalTTLDays: number | undefined;
439
- const wantsJournalTTL = await prompter.confirm({
440
- message: "Configure quote journal retention in days?",
441
- initialValue: typeof resolved.journalTTLDays === "number",
442
- });
443
- if (wantsJournalTTL) {
444
- const parsedJournalTTL = Number(
445
- String(
446
- await prompter.text({
447
- message: "Quote journal retention days",
448
- placeholder: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
449
- initialValue:
450
- typeof resolved.journalTTLDays === "number"
451
- ? String(resolved.journalTTLDays)
452
- : String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
453
- validate: (value) => {
454
- const raw = String(value ?? "").trim();
455
- const num = Number(raw);
456
- if (!raw) {
457
- return "Required";
458
- }
459
- if (!Number.isInteger(num) || num < 1) {
460
- return "Must be an integer >= 1";
461
- }
462
- return undefined;
463
- },
464
- }),
465
- ).trim(),
466
- );
467
- journalTTLDays =
468
- Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
469
- ? parsedJournalTTL
470
- : DEFAULT_MESSAGE_CONTEXT_TTL_DAYS;
471
- }
404
+ let maxReconnectCycles: number | undefined;
405
+ const wantsReconnectLimits = await prompter.confirm({
406
+ message: "Configure runtime reconnect cycle limit? (recommended)",
407
+ initialValue: typeof resolved.maxReconnectCycles === "number",
408
+ });
409
+ if (wantsReconnectLimits) {
410
+ const parsedCycles = Number(
411
+ String(
412
+ await prompter.text({
413
+ message: "Max runtime reconnect cycles",
414
+ placeholder: "10",
415
+ initialValue: String(resolved.maxReconnectCycles ?? 10),
416
+ validate: (value: string) => validatePositiveInteger(value),
417
+ }),
418
+ ).trim(),
419
+ );
420
+ maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
421
+ }
422
+
423
+ let mediaMaxMb: number | undefined;
424
+ const wantsMediaMax = await prompter.confirm({
425
+ message: "Configure inbound media max size in MB? (optional)",
426
+ initialValue: typeof resolved.mediaMaxMb === "number",
427
+ });
428
+ if (wantsMediaMax) {
429
+ const parsedMediaMax = Number(
430
+ String(
431
+ await prompter.text({
432
+ message: "Max inbound media size (MB)",
433
+ placeholder: "20",
434
+ initialValue:
435
+ typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
436
+ validate: (value: string) => validatePositiveInteger(value),
437
+ }),
438
+ ).trim(),
439
+ );
440
+ mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
441
+ }
472
442
 
473
- const next = applyAccountConfig({
443
+ let journalTTLDays: number | undefined;
444
+ const wantsJournalTTL = await prompter.confirm({
445
+ message: "Configure quote journal retention in days?",
446
+ initialValue: typeof resolved.journalTTLDays === "number",
447
+ });
448
+ if (wantsJournalTTL) {
449
+ const parsedJournalTTL = Number(
450
+ String(
451
+ await prompter.text({
452
+ message: "Quote journal retention days",
453
+ placeholder: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
454
+ initialValue:
455
+ typeof resolved.journalTTLDays === "number"
456
+ ? String(resolved.journalTTLDays)
457
+ : String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
458
+ validate: (value: string) => validatePositiveInteger(value),
459
+ }),
460
+ ).trim(),
461
+ );
462
+ journalTTLDays =
463
+ Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
464
+ ? parsedJournalTTL
465
+ : DEFAULT_MESSAGE_CONTEXT_TTL_DAYS;
466
+ }
467
+
468
+ return applyAccountConfig({
469
+ cfg,
470
+ accountId,
471
+ input: {
472
+ clientId: String(clientId).trim(),
473
+ clientSecret: String(clientSecret).trim(),
474
+ robotCode,
475
+ corpId,
476
+ agentId,
477
+ dmPolicy: dmPolicyValue as "open" | "allowlist",
478
+ groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
479
+ allowFrom,
480
+ groupAllowFrom,
481
+ displayNameResolution: displayNameResolutionValue as "disabled" | "all",
482
+ mediaUrlAllowlist,
483
+ messageType,
484
+ cardTemplateId,
485
+ cardTemplateKey,
486
+ maxReconnectCycles,
487
+ mediaMaxMb,
488
+ journalTTLDays,
489
+ },
490
+ });
491
+ }
492
+
493
+ export const dingtalkSetupAdapter: ChannelSetupAdapter = {
494
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID),
495
+ applyAccountName: ({ cfg, accountId, name }) =>
496
+ applyAccountNameToChannelSection({ cfg, channelKey: channel, accountId, name }),
497
+ applyAccountConfig: ({ cfg, accountId, input }) =>
498
+ applyGenericSetupInput({
474
499
  cfg,
475
500
  accountId,
476
- input: {
477
- clientId: String(clientId).trim(),
478
- clientSecret: String(clientSecret).trim(),
479
- robotCode,
480
- corpId,
481
- agentId,
482
- dmPolicy: dmPolicyValue as "open" | "allowlist",
483
- groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
484
- allowFrom,
485
- groupAllowFrom,
486
- displayNameResolution: displayNameResolutionValue as "disabled" | "all",
487
- mediaUrlAllowlist,
488
- messageType,
489
- cardTemplateId,
490
- cardTemplateKey,
491
- maxReconnectCycles,
492
- mediaMaxMb,
493
- journalTTLDays,
494
- },
495
- });
501
+ input,
502
+ }),
503
+ };
496
504
 
497
- return { cfg: next, accountId };
505
+ export const dingtalkSetupWizard: ChannelSetupWizard = {
506
+ channel,
507
+ credentials: [],
508
+ status: {
509
+ configuredLabel: "configured",
510
+ unconfiguredLabel: "needs setup",
511
+ resolveConfigured: ({ cfg }) => {
512
+ const accountIds = listDingTalkAccountIds(cfg);
513
+ return accountIds.length > 0
514
+ ? accountIds.some((accountId) => isConfigured(resolveDingTalkAccount(cfg, accountId)))
515
+ : isConfigured(resolveDingTalkAccount(cfg, DEFAULT_ACCOUNT_ID));
516
+ },
517
+ resolveStatusLines: ({ configured }) => [
518
+ `DingTalk: ${configured ? "configured" : "needs setup"}`,
519
+ ],
520
+ resolveSelectionHint: ({ configured }) =>
521
+ configured ? "configured" : "钉钉企业机器人",
522
+ resolveQuickstartScore: ({ configured }) => (configured ? 1 : 4),
523
+ },
524
+ resolveAccountIdForConfigure: async ({
525
+ cfg,
526
+ prompter,
527
+ accountOverride,
528
+ shouldPromptAccountIds,
529
+ listAccountIds,
530
+ defaultAccountId,
531
+ }) => {
532
+ const resolvedAccountId = accountOverride
533
+ ? normalizeAccountId(accountOverride)
534
+ : defaultAccountId;
535
+ if (!shouldPromptAccountIds || accountOverride) {
536
+ return resolvedAccountId;
537
+ }
538
+ return await promptDingTalkAccountId({
539
+ cfg,
540
+ prompter,
541
+ label: "DingTalk",
542
+ currentId: resolvedAccountId,
543
+ listAccountIds,
544
+ defaultAccountId,
545
+ });
498
546
  },
547
+ finalize: async ({ cfg, accountId, prompter }) => ({
548
+ cfg: await configureDingTalkAccount({
549
+ cfg,
550
+ accountId,
551
+ prompter,
552
+ }),
553
+ }),
499
554
  };