@soimy/dingtalk 3.3.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.
Files changed (36) hide show
  1. package/README.md +141 -12
  2. package/index.ts +71 -66
  3. package/package.json +6 -5
  4. package/src/access-control.ts +65 -0
  5. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
  6. package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
  7. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
  8. package/src/ack-reaction-classifier.ts +17 -4
  9. package/src/ack-reaction-service.ts +66 -19
  10. package/src/attachment-text-extractor.ts +2 -1
  11. package/src/card-service.ts +145 -257
  12. package/src/channel.ts +106 -47
  13. package/src/config-schema.ts +28 -6
  14. package/src/config.ts +30 -6
  15. package/src/connection-manager.ts +16 -5
  16. package/src/inbound-handler.ts +694 -520
  17. package/src/media-utils.ts +99 -36
  18. package/src/message-context-store.ts +787 -0
  19. package/src/message-utils.ts +221 -42
  20. package/src/messaging/quoted-context.ts +269 -0
  21. package/src/messaging/quoted-ref.ts +97 -0
  22. package/src/onboarding.ts +381 -269
  23. package/src/reply-strategy-card.ts +225 -0
  24. package/src/reply-strategy-markdown.ts +55 -0
  25. package/src/reply-strategy-with-reaction.ts +190 -0
  26. package/src/reply-strategy.ts +72 -0
  27. package/src/runtime.ts +5 -7
  28. package/src/send-service.ts +164 -62
  29. package/src/targeting/agent-name-matcher.ts +148 -0
  30. package/src/targeting/agent-routing.ts +181 -0
  31. package/src/targeting/target-directory-adapter.ts +152 -0
  32. package/src/targeting/target-directory-store.ts +396 -0
  33. package/src/targeting/target-input.ts +62 -0
  34. package/src/types.ts +124 -21
  35. package/src/quote-journal.ts +0 -242
  36. package/src/quoted-msg-cache.ts +0 -226
package/src/onboarding.ts CHANGED
@@ -1,8 +1,14 @@
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";
9
+ import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store.js";
3
10
  import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
4
11
  import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
5
- import { DEFAULT_JOURNAL_TTL_DAYS } from "./quote-journal.js";
6
12
 
7
13
  const channel = "dingtalk" as const;
8
14
 
@@ -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,6 +121,13 @@ 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 } : {}),
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 }
130
+ : {}),
107
131
  ...(input.messageType ? { messageType: input.messageType } : {}),
108
132
  ...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
109
133
  ...(input.cardTemplateKey ? { cardTemplateKey: input.cardTemplateKey } : {}),
@@ -155,288 +179,376 @@ function applyAccountConfig(params: {
155
179
  };
156
180
  }
157
181
 
158
- export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
159
- channel,
160
- getStatus: ({ cfg }) => {
161
- const accountIds = listDingTalkAccountIds(cfg);
162
- const configured =
163
- accountIds.length > 0
164
- ? accountIds.some((accountId) => isConfigured(resolveDingTalkAccount(cfg, accountId)))
165
- : 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
+ }
166
199
 
167
- return Promise.resolve({
168
- channel,
169
- configured,
170
- statusLines: [`DingTalk: ${configured ? "configured" : "needs setup"}`],
171
- selectionHint: configured ? "configured" : "钉钉企业机器人",
172
- quickstartScore: configured ? 1 : 4,
173
- });
174
- },
175
- configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
176
- const override = accountOverrides[channel]?.trim();
177
- let accountId = override ? normalizeAccountId(override) : DEFAULT_ACCOUNT_ID;
178
-
179
- if (shouldPromptAccountIds && !override) {
180
- accountId = await promptDingTalkAccountId({
181
- cfg,
182
- prompter,
183
- label: "DingTalk",
184
- currentId: accountId,
185
- listAccountIds: listDingTalkAccountIds,
186
- defaultAccountId: DEFAULT_ACCOUNT_ID,
187
- });
188
- }
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
+ }
189
211
 
190
- const resolved = resolveDingTalkAccount(cfg, accountId);
191
- 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);
192
219
 
193
- const clientId = await prompter.text({
194
- message: "Client ID (AppKey)",
195
- placeholder: "dingxxxxxxxx",
196
- initialValue: resolved.clientId ?? undefined,
197
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
198
- });
220
+ await noteDingTalkHelp(prompter);
199
221
 
200
- const clientSecret = await prompter.text({
201
- message: "Client Secret (AppSecret)",
202
- placeholder: "xxx-xxx-xxx-xxx",
203
- initialValue: resolved.clientSecret ?? undefined,
204
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
205
- });
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
+ });
206
228
 
207
- const wantsFullConfig = await prompter.confirm({
208
- message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
209
- initialValue: false,
210
- });
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
+ });
211
235
 
212
- let robotCode: string | undefined;
213
- let corpId: string | undefined;
214
- let agentId: string | undefined;
215
-
216
- if (wantsFullConfig) {
217
- robotCode =
218
- String(
219
- await prompter.text({
220
- message: "Robot Code",
221
- placeholder: "dingxxxxxxxx",
222
- initialValue: resolved.robotCode ?? undefined,
223
- }),
224
- ).trim() || undefined;
225
-
226
- corpId =
227
- String(
228
- await prompter.text({
229
- message: "Corp ID",
230
- placeholder: "dingxxxxxxxx",
231
- initialValue: resolved.corpId ?? undefined,
232
- }),
233
- ).trim() || undefined;
234
-
235
- agentId =
236
- String(
237
- await prompter.text({
238
- message: "Agent ID",
239
- placeholder: "123456789",
240
- initialValue: resolved.agentId ? String(resolved.agentId) : undefined,
241
- }),
242
- ).trim() || undefined;
243
- }
236
+ const wantsFullConfig = await prompter.confirm({
237
+ message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
238
+ initialValue: false,
239
+ });
244
240
 
245
- const wantsCardMode = await prompter.confirm({
246
- message: "Enable AI interactive card mode? (for streaming AI responses)",
247
- initialValue: resolved.messageType === "card",
248
- });
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
+ }
249
273
 
250
- let cardTemplateId: string | undefined;
251
- let cardTemplateKey: string | undefined;
252
- let messageType: "markdown" | "card" = "markdown";
253
-
254
- if (wantsCardMode) {
255
- await prompter.note(
256
- [
257
- "Create an AI card template in DingTalk Developer Console:",
258
- "https://open-dev.dingtalk.com/fe/card",
259
- "1. Go to 'My Templates' > 'Create Template'",
260
- "2. Select 'AI Card' scenario",
261
- "3. Design your card and publish",
262
- "4. Copy the Template ID (e.g., xxx.schema)",
263
- ].join("\n"),
264
- "Card Template Setup",
265
- );
266
-
267
- cardTemplateId =
268
- String(
269
- await prompter.text({
270
- message: "Card Template ID",
271
- placeholder: "xxxxx-xxxxx-xxxxx.schema",
272
- initialValue: resolved.cardTemplateId ?? undefined,
273
- }),
274
- ).trim() || undefined;
275
-
276
- cardTemplateKey =
277
- String(
278
- await prompter.text({
279
- message: "Card Template Key (content field name)",
280
- placeholder: "content",
281
- initialValue: resolved.cardTemplateKey ?? "content",
282
- }),
283
- ).trim() || "content";
284
-
285
- messageType = "card";
286
- }
274
+ const wantsCardMode = await prompter.confirm({
275
+ message: "Enable AI interactive card mode? (for streaming AI responses)",
276
+ initialValue: resolved.messageType === "card",
277
+ });
287
278
 
288
- const dmPolicyValue = await prompter.select({
289
- message: "Direct message policy",
290
- options: [
291
- { label: "Open - anyone can DM", value: "open" },
292
- { label: "Allowlist - only allowed users", value: "allowlist" },
293
- ],
294
- initialValue: resolved.dmPolicy ?? "open",
295
- });
279
+ let cardTemplateId: string | undefined;
280
+ let cardTemplateKey: string | undefined;
281
+ let messageType: "markdown" | "card" = "markdown";
282
+
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
+ );
295
+
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
+ }
296
316
 
297
- let allowFrom: string[] | undefined;
298
- if (dmPolicyValue === "allowlist") {
299
- const entry = await prompter.text({
300
- message: "Allowed user IDs (comma-separated)",
301
- placeholder: "user1, user2",
302
- });
303
- const parsed = parseList(String(entry ?? ""));
304
- allowFrom = parsed.length > 0 ? parsed : undefined;
305
- }
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
+ });
306
325
 
307
- const mediaUrlAllowlistEntry = await prompter.text({
308
- message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
309
- placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
310
- initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
311
- });
312
- const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
313
- const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
314
-
315
- const groupPolicyValue = await prompter.select({
316
- message: "Group message policy",
317
- options: [
318
- { label: "Open - any group can use bot", value: "open" },
319
- { label: "Allowlist - only allowed groups", value: "allowlist" },
320
- ],
321
- initialValue: resolved.groupPolicy ?? "open",
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",
322
331
  });
332
+ const parsed = parseList(String(entry ?? ""));
333
+ allowFrom = parsed.length > 0 ? parsed : undefined;
334
+ }
323
335
 
324
- let maxReconnectCycles: number | undefined;
325
- const wantsReconnectLimits = await prompter.confirm({
326
- message: "Configure runtime reconnect cycle limit? (recommended)",
327
- initialValue: typeof resolved.maxReconnectCycles === "number",
328
- });
329
- if (wantsReconnectLimits) {
330
- const parsedCycles = Number(
331
- String(
332
- await prompter.text({
333
- message: "Max runtime reconnect cycles",
334
- placeholder: "10",
335
- initialValue: String(resolved.maxReconnectCycles ?? 10),
336
- validate: (value) => {
337
- const raw = String(value ?? "").trim();
338
- const num = Number(raw);
339
- if (!raw) {
340
- return "Required";
341
- }
342
- if (!Number.isInteger(num) || num < 1) {
343
- return "Must be an integer >= 1";
344
- }
345
- return undefined;
346
- },
347
- }),
348
- ).trim(),
349
- );
350
- maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
351
- }
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
 
353
- let mediaMaxMb: number | undefined;
354
- const wantsMediaMax = await prompter.confirm({
355
- message: "Configure inbound media max size in MB? (optional)",
356
- initialValue: typeof resolved.mediaMaxMb === "number",
357
- });
358
- if (wantsMediaMax) {
359
- const parsedMediaMax = Number(
360
- String(
361
- await prompter.text({
362
- message: "Max inbound media size (MB)",
363
- placeholder: "20",
364
- initialValue:
365
- typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
366
- validate: (value) => {
367
- const raw = String(value ?? "").trim();
368
- const num = Number(raw);
369
- if (!raw) {
370
- return "Required";
371
- }
372
- if (!Number.isInteger(num) || num < 1) {
373
- return "Must be an integer >= 1";
374
- }
375
- return undefined;
376
- },
377
- }),
378
- ).trim(),
379
- );
380
- mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
381
- }
354
+ if (groupPolicyValue === "allowlist") {
355
+ await prompter.note(
356
+ [
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.',
363
+ ].join("\n"),
364
+ );
365
+ }
382
366
 
383
- let journalTTLDays: number | undefined;
384
- const wantsJournalTTL = await prompter.confirm({
385
- message: "Configure quote journal retention in days?",
386
- initialValue: typeof resolved.journalTTLDays === "number",
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,
387
373
  });
388
- if (wantsJournalTTL) {
389
- const parsedJournalTTL = Number(
390
- String(
391
- await prompter.text({
392
- message: "Quote journal retention days",
393
- placeholder: String(DEFAULT_JOURNAL_TTL_DAYS),
394
- initialValue:
395
- typeof resolved.journalTTLDays === "number"
396
- ? String(resolved.journalTTLDays)
397
- : String(DEFAULT_JOURNAL_TTL_DAYS),
398
- validate: (value) => {
399
- const raw = String(value ?? "").trim();
400
- const num = Number(raw);
401
- if (!raw) {
402
- return "Required";
403
- }
404
- if (!Number.isInteger(num) || num < 1) {
405
- return "Must be an integer >= 1";
406
- }
407
- return undefined;
408
- },
409
- }),
410
- ).trim(),
411
- );
412
- journalTTLDays =
413
- Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
414
- ? parsedJournalTTL
415
- : DEFAULT_JOURNAL_TTL_DAYS;
416
- }
374
+ const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
375
+ groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
376
+ }
377
+
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
+ );
388
+
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
+ });
403
+
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
+ }
417
422
 
418
- const next = applyAccountConfig({
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
+ }
442
+
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({
419
499
  cfg,
420
500
  accountId,
421
- input: {
422
- clientId: String(clientId).trim(),
423
- clientSecret: String(clientSecret).trim(),
424
- robotCode,
425
- corpId,
426
- agentId,
427
- dmPolicy: dmPolicyValue as "open" | "allowlist",
428
- groupPolicy: groupPolicyValue as "open" | "allowlist",
429
- allowFrom,
430
- mediaUrlAllowlist,
431
- messageType,
432
- cardTemplateId,
433
- cardTemplateKey,
434
- maxReconnectCycles,
435
- mediaMaxMb,
436
- journalTTLDays,
437
- },
438
- });
501
+ input,
502
+ }),
503
+ };
439
504
 
440
- 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
+ });
441
546
  },
547
+ finalize: async ({ cfg, accountId, prompter }) => ({
548
+ cfg: await configureDingTalkAccount({
549
+ cfg,
550
+ accountId,
551
+ prompter,
552
+ }),
553
+ }),
442
554
  };