@soimy/dingtalk 3.1.0 → 3.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soimy/dingtalk",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "DingTalk (钉钉) channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
package/src/channel.ts CHANGED
@@ -381,6 +381,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
381
381
  initialDelay: config.initialReconnectDelay ?? 1000,
382
382
  maxDelay: config.maxReconnectDelay ?? 60000,
383
383
  jitter: config.reconnectJitter ?? 0.3,
384
+ maxReconnectCycles: config.maxReconnectCycles,
384
385
  onStateChange: (state: ConnectionState, error?: string) => {
385
386
  if (stopped) {
386
387
  return;
@@ -75,6 +75,17 @@ const DingTalkAccountConfigSchema = z.object({
75
75
 
76
76
  /** Jitter factor for reconnection delay randomization (0-1, default: 0.3) */
77
77
  reconnectJitter: z.number().min(0).max(1).optional().default(0.3),
78
+
79
+ /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
80
+ maxReconnectCycles: z.number().int().min(1).optional().default(10),
81
+
82
+ proactivePermissionHint: z
83
+ .object({
84
+ enabled: z.boolean().optional().default(true),
85
+ cooldownHours: z.number().int().min(1).max(24 * 30).optional().default(24),
86
+ })
87
+ .optional()
88
+ .default({ enabled: true, cooldownHours: 24 }),
78
89
  });
79
90
 
80
91
  /**
@@ -37,6 +37,8 @@ export class ConnectionManager {
37
37
  private static readonly HEALTH_CHECK_INTERVAL_MS = 5000;
38
38
  private static readonly HEALTH_CHECK_GRACE_MS = 3000;
39
39
  private static readonly HEALTH_CHECK_UNHEALTHY_THRESHOLD = 2;
40
+ private static readonly DEFAULT_MAX_RECONNECT_CYCLES = 10;
41
+ private runtimeReconnectCycles: number = 0;
40
42
  private runtimeCounters = {
41
43
  healthUnhealthyChecks: 0,
42
44
  healthTriggeredReconnects: 0,
@@ -160,6 +162,9 @@ export class ConnectionManager {
160
162
 
161
163
  this.log?.info?.(`[${this.accountId}] DingTalk Stream client connected successfully`);
162
164
 
165
+ // Reset runtime reconnect cycle counter on successful connection
166
+ this.runtimeReconnectCycles = 0;
167
+
163
168
  return { success: true, attempt: successfulAttempt };
164
169
  } catch (err: any) {
165
170
  this.log?.error?.(
@@ -427,17 +432,34 @@ export class ConnectionManager {
427
432
  this.log?.error?.(`[${this.accountId}] Reconnection failed: ${err.message}`);
428
433
  this.runtimeCounters.reconnectFailures += 1;
429
434
  this.logRuntimeCounters("reconnect-failed");
435
+
436
+ // Track runtime reconnect cycles to prevent infinite loops
437
+ this.runtimeReconnectCycles += 1;
438
+ const maxCycles = this.config.maxReconnectCycles ?? ConnectionManager.DEFAULT_MAX_RECONNECT_CYCLES;
439
+
440
+ if (this.runtimeReconnectCycles >= maxCycles) {
441
+ this.log?.error?.(
442
+ `[${this.accountId}] Max runtime reconnect cycles (${maxCycles}) reached. Giving up. ` +
443
+ `Please check network connectivity or restart the gateway.`,
444
+ );
445
+ this.state = ConnectionStateEnum.FAILED;
446
+ this.connectedAt = undefined;
447
+ this.consecutiveUnhealthyChecks = 0;
448
+ this.notifyStateChange(`Max runtime reconnect cycles (${maxCycles}) reached`);
449
+ return;
450
+ }
451
+
430
452
  this.state = ConnectionStateEnum.FAILED;
431
453
  this.connectedAt = undefined;
432
454
  this.consecutiveUnhealthyChecks = 0;
433
455
  this.notifyStateChange(err.message);
434
456
 
435
- // Continue runtime recovery instead of getting stuck in FAILED.
436
- const delay = this.calculateNextDelay(0);
457
+ // Continue runtime recovery with exponential backoff based on cycle count
458
+ const delay = this.calculateNextDelay(Math.min(this.runtimeReconnectCycles - 1, 6)); // Cap at ~64x initial delay
437
459
  this.attemptCount = 0;
438
460
  this.clearReconnectTimer();
439
461
  this.log?.warn?.(
440
- `[${this.accountId}] Reconnection cycle failed; scheduling next reconnect in ${(delay / 1000).toFixed(2)}s`,
462
+ `[${this.accountId}] Reconnection cycle ${this.runtimeReconnectCycles}/${maxCycles} failed; scheduling next reconnect in ${(delay / 1000).toFixed(2)}s`,
441
463
  );
442
464
  this.reconnectTimer = setTimeout(() => {
443
465
  void this.reconnect();
@@ -16,12 +16,78 @@ import { formatGroupMembers, noteGroupMember } from "./group-members-store";
16
16
  import { setCurrentLogger } from "./logger-context";
17
17
  import { extractMessageContent } from "./message-utils";
18
18
  import { registerPeerId } from "./peer-id-registry";
19
+ import {
20
+ clearProactiveRiskObservationsForTest,
21
+ getProactiveRiskObservationForAny,
22
+ } from "./proactive-risk-registry";
19
23
  import { getDingTalkRuntime } from "./runtime";
20
24
  import { sendBySession, sendMessage } from "./send-service";
21
25
  import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
22
26
  import { AICardStatus } from "./types";
23
27
  import { formatDingTalkErrorPayloadLog, maskSensitiveData } from "./utils";
24
28
 
29
+ const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
30
+ const proactiveHintLastSentAt = new Map<string, number>();
31
+
32
+ export function resetProactivePermissionHintStateForTest(): void {
33
+ proactiveHintLastSentAt.clear();
34
+ clearProactiveRiskObservationsForTest();
35
+ }
36
+
37
+ function shouldSendProactivePermissionHint(params: {
38
+ isDirect: boolean;
39
+ accountId: string;
40
+ senderId: string;
41
+ senderStaffId?: string;
42
+ config: DingTalkConfig;
43
+ nowMs: number;
44
+ }): boolean {
45
+ if (!params.isDirect) {
46
+ return false;
47
+ }
48
+
49
+ const hintConfig = params.config.proactivePermissionHint;
50
+ if (hintConfig?.enabled === false) {
51
+ return false;
52
+ }
53
+
54
+ const targetId = (params.senderId || "").trim();
55
+ if (!targetId) {
56
+ return false;
57
+ }
58
+
59
+ const riskObservation = getProactiveRiskObservationForAny(
60
+ params.accountId,
61
+ [params.senderId, params.senderStaffId],
62
+ params.nowMs,
63
+ );
64
+ if (!riskObservation || riskObservation.source !== "proactive-api") {
65
+ return false;
66
+ }
67
+
68
+ const cooldownHours =
69
+ hintConfig?.cooldownHours && hintConfig.cooldownHours > 0
70
+ ? hintConfig.cooldownHours
71
+ : DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS;
72
+ const cooldownMs = cooldownHours * 60 * 60 * 1000;
73
+ const key = `${params.accountId}:${targetId}`;
74
+ const lastSentAt = proactiveHintLastSentAt.get(key) || 0;
75
+ if (params.nowMs - lastSentAt < cooldownMs) {
76
+ return false;
77
+ }
78
+
79
+ proactiveHintLastSentAt.set(key, params.nowMs);
80
+ return true;
81
+ }
82
+
83
+ function isUnhandledStopReasonText(value: string): boolean {
84
+ const normalized = value.trim();
85
+ if (!normalized) {
86
+ return false;
87
+ }
88
+ return /^Unhandled stop reason:\s*[A-Za-z0-9_-]+/i.test(normalized);
89
+ }
90
+
25
91
  /**
26
92
  * Download DingTalk media file via runtime media service (sandbox-compatible).
27
93
  * Files are stored in the global media inbound directory.
@@ -143,6 +209,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
143
209
  if (groupId) {
144
210
  registerPeerId(groupId);
145
211
  }
212
+
213
+ if (
214
+ shouldSendProactivePermissionHint({
215
+ isDirect,
216
+ accountId,
217
+ senderId,
218
+ senderStaffId: data.senderStaffId,
219
+ config: dingtalkConfig,
220
+ nowMs: Date.now(),
221
+ })
222
+ ) {
223
+ try {
224
+ await sendBySession(
225
+ dingtalkConfig,
226
+ sessionWebhook,
227
+ "⚠️ 主动推送可能失败\n\n检测到该用户最近一次主动发送调用返回了权限或目标不可达错误。当前会话回复仍可正常使用,但定时/主动发送可能失败。\n\n建议:\n1) 在钉钉开放平台确认应用已申请并获得主动发送相关权限\n2) 确认目标用户属于当前企业并在应用可见范围内\n3) 使用相同账号进行一次主动发送验证并检查错误码详情",
228
+ { log },
229
+ );
230
+ } catch (err: any) {
231
+ log?.debug?.(`[DingTalk] Failed to send proactive permission hint: ${err.message}`);
232
+ if (err?.response?.data !== undefined) {
233
+ log?.debug?.(formatDingTalkErrorPayloadLog("inbound.proactivePermissionHint", err.response.data));
234
+ }
235
+ }
236
+ }
146
237
  if (senderId) {
147
238
  registerPeerId(senderId);
148
239
  }
@@ -383,6 +474,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
383
474
  return;
384
475
  }
385
476
 
477
+ if (typeof textToSend === "string" && isUnhandledStopReasonText(textToSend)) {
478
+ log?.warn?.(`[DingTalk] Suppressed stop reason from outbound chat content: ${textToSend}`);
479
+ return;
480
+ }
481
+
386
482
  if (useCardMode && currentAICard && info?.kind === "final") {
387
483
  lastCardContent = textToSend;
388
484
  return;
@@ -468,12 +564,21 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
468
564
  const hasQueuedFinalString = isNonEmptyString(queuedFinal);
469
565
 
470
566
  if (hasLastCardContent || hasQueuedFinalString) {
471
- const finalContent =
567
+ const finalContentCandidate =
472
568
  hasLastCardContent && typeof lastCardContent === "string"
473
569
  ? lastCardContent
474
570
  : typeof queuedFinal === "string"
475
571
  ? queuedFinal
476
572
  : "";
573
+ if (isUnhandledStopReasonText(finalContentCandidate)) {
574
+ log?.warn?.(
575
+ `[DingTalk] Suppressed stop reason from AI Card final content: ${finalContentCandidate}`,
576
+ );
577
+ currentAICard.state = AICardStatus.FINISHED;
578
+ currentAICard.lastUpdated = Date.now();
579
+ return;
580
+ }
581
+ const finalContent = finalContentCandidate;
477
582
  await finishAICard(currentAICard, finalContent, log);
478
583
  } else {
479
584
  log?.debug?.(
package/src/onboarding.ts CHANGED
@@ -106,6 +106,9 @@ function applyAccountConfig(params: {
106
106
  ...(input.messageType ? { messageType: input.messageType } : {}),
107
107
  ...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
108
108
  ...(input.cardTemplateKey ? { cardTemplateKey: input.cardTemplateKey } : {}),
109
+ ...(typeof input.maxReconnectCycles === "number"
110
+ ? { maxReconnectCycles: input.maxReconnectCycles }
111
+ : {}),
109
112
  };
110
113
 
111
114
  if (useDefault) {
@@ -304,6 +307,35 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
304
307
  initialValue: resolved.groupPolicy ?? "open",
305
308
  });
306
309
 
310
+ let maxReconnectCycles: number | undefined;
311
+ const wantsReconnectLimits = await prompter.confirm({
312
+ message: "Configure runtime reconnect cycle limit? (recommended)",
313
+ initialValue: typeof resolved.maxReconnectCycles === "number",
314
+ });
315
+ if (wantsReconnectLimits) {
316
+ const parsedCycles = Number(
317
+ String(
318
+ await prompter.text({
319
+ message: "Max runtime reconnect cycles",
320
+ placeholder: "10",
321
+ initialValue: String(resolved.maxReconnectCycles ?? 10),
322
+ validate: (value) => {
323
+ const raw = String(value ?? "").trim();
324
+ const num = Number(raw);
325
+ if (!raw) {
326
+ return "Required";
327
+ }
328
+ if (!Number.isInteger(num) || num < 1) {
329
+ return "Must be an integer >= 1";
330
+ }
331
+ return undefined;
332
+ },
333
+ }),
334
+ ).trim(),
335
+ );
336
+ maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
337
+ }
338
+
307
339
  const next = applyAccountConfig({
308
340
  cfg,
309
341
  accountId,
@@ -319,6 +351,7 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
319
351
  messageType,
320
352
  cardTemplateId,
321
353
  cardTemplateKey,
354
+ maxReconnectCycles,
322
355
  },
323
356
  });
324
357
 
@@ -0,0 +1,89 @@
1
+ type ProactiveRiskLevel = "low" | "medium" | "high";
2
+
3
+ export interface ProactiveRiskObservation {
4
+ accountId: string;
5
+ targetId: string;
6
+ level: ProactiveRiskLevel;
7
+ reason: string;
8
+ source: string;
9
+ observedAtMs?: number;
10
+ }
11
+
12
+ export interface ProactiveRiskSnapshot {
13
+ level: ProactiveRiskLevel;
14
+ reason: string;
15
+ source: string;
16
+ observedAtMs: number;
17
+ }
18
+
19
+ const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
20
+ const store = new Map<string, ProactiveRiskSnapshot>();
21
+
22
+ function keyOf(accountId: string, targetId: string): string {
23
+ return `${accountId}:${targetId.trim()}`;
24
+ }
25
+
26
+ export function recordProactiveRiskObservation(observation: ProactiveRiskObservation): void {
27
+ const targetId = observation.targetId?.trim();
28
+ if (!observation.accountId || !targetId) {
29
+ return;
30
+ }
31
+
32
+ store.set(keyOf(observation.accountId, targetId), {
33
+ level: observation.level,
34
+ reason: observation.reason,
35
+ source: observation.source,
36
+ observedAtMs: observation.observedAtMs ?? Date.now(),
37
+ });
38
+ }
39
+
40
+ export function getProactiveRiskObservation(
41
+ accountId: string,
42
+ targetId: string,
43
+ nowMs = Date.now(),
44
+ ): ProactiveRiskSnapshot | null {
45
+ const target = targetId?.trim();
46
+ if (!accountId || !target) {
47
+ return null;
48
+ }
49
+
50
+ const key = keyOf(accountId, target);
51
+ const entry = store.get(key);
52
+ if (!entry) {
53
+ return null;
54
+ }
55
+ if (nowMs - entry.observedAtMs > DEFAULT_TTL_MS) {
56
+ store.delete(key);
57
+ return null;
58
+ }
59
+ return entry;
60
+ }
61
+
62
+ export function getProactiveRiskObservationForAny(
63
+ accountId: string,
64
+ targetIds: Array<string | null | undefined>,
65
+ nowMs = Date.now(),
66
+ ): ProactiveRiskSnapshot | null {
67
+ for (const candidate of targetIds) {
68
+ if (!candidate) {
69
+ continue;
70
+ }
71
+ const observation = getProactiveRiskObservation(accountId, candidate, nowMs);
72
+ if (observation) {
73
+ return observation;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export function deleteProactiveRiskObservation(accountId: string, targetId: string): void {
80
+ const target = targetId?.trim();
81
+ if (!accountId || !target) {
82
+ return;
83
+ }
84
+ store.delete(keyOf(accountId, target));
85
+ }
86
+
87
+ export function clearProactiveRiskObservationsForTest(): void {
88
+ store.clear();
89
+ }
@@ -13,6 +13,11 @@ import { getLogger } from "./logger-context";
13
13
  import { uploadMedia as uploadMediaUtil } from "./media-utils";
14
14
  import { detectMarkdownAndExtractTitle } from "./message-utils";
15
15
  import { resolveOriginalPeerId } from "./peer-id-registry";
16
+ import {
17
+ deleteProactiveRiskObservation,
18
+ getProactiveRiskObservation,
19
+ recordProactiveRiskObservation,
20
+ } from "./proactive-risk-registry";
16
21
  import { formatDingTalkErrorPayloadLog } from "./utils";
17
22
  import type {
18
23
  AxiosResponse,
@@ -26,6 +31,38 @@ import { AICardStatus } from "./types";
26
31
 
27
32
  export { detectMediaTypeFromExtension } from "./media-utils";
28
33
 
34
+ function extractErrorCodeFromResponseData(data: unknown): string | null {
35
+ if (!data || typeof data !== "object") {
36
+ return null;
37
+ }
38
+
39
+ const payload = data as Record<string, unknown>;
40
+ const code = payload.code;
41
+ if (typeof code === "string" && code.trim()) {
42
+ return code.trim();
43
+ }
44
+
45
+ const subCode = payload.subCode;
46
+ if (typeof subCode === "string" && subCode.trim()) {
47
+ return subCode.trim();
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ function isProactivePermissionOrScopeError(code: string | null): boolean {
54
+ if (!code) {
55
+ return false;
56
+ }
57
+ return (
58
+ code.startsWith("Forbidden.AccessDenied") ||
59
+ code === "invalidParameter.userIds.invalid" ||
60
+ code === "invalidParameter.userIds.empty" ||
61
+ code === "invalidParameter.openConversationId.invalid" ||
62
+ code === "invalidParameter.robotCode.empty"
63
+ );
64
+ }
65
+
29
66
  /**
30
67
  * Wrapper to upload media with shared getAccessToken binding.
31
68
  */
@@ -51,6 +88,12 @@ export async function sendProactiveTextOrMarkdown(
51
88
  const { targetId, isExplicitUser } = stripTargetPrefix(target);
52
89
  const resolvedTarget = resolveOriginalPeerId(targetId);
53
90
  const isGroup = !isExplicitUser && resolvedTarget.startsWith("cid");
91
+ const proactiveRisk = options.accountId
92
+ ? getProactiveRiskObservation(options.accountId, resolvedTarget)
93
+ : null;
94
+ const proactiveRiskTag = proactiveRisk
95
+ ? ` proactiveRisk=${proactiveRisk.level}:${proactiveRisk.reason}`
96
+ : "";
54
97
 
55
98
  const url = isGroup
56
99
  ? "https://api.dingtalk.com/v1.0/robot/groupMessages/send"
@@ -59,7 +102,7 @@ export async function sendProactiveTextOrMarkdown(
59
102
  const { useMarkdown, title } = detectMarkdownAndExtractTitle(text, options, "OpenClaw 提醒");
60
103
 
61
104
  log?.debug?.(
62
- `[DingTalk] Sending proactive message to ${isGroup ? "group" : "user"} ${resolvedTarget} with title "${title}"`,
105
+ `[DingTalk] Sending proactive message to ${isGroup ? "group" : "user"} ${resolvedTarget} with title "${title}"${proactiveRiskTag}`,
63
106
  );
64
107
 
65
108
  // DingTalk proactive API uses message templates (sampleMarkdown / sampleText).
@@ -87,6 +130,9 @@ export async function sendProactiveTextOrMarkdown(
87
130
  data: payload,
88
131
  headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
89
132
  });
133
+ if (options.accountId) {
134
+ deleteProactiveRiskObservation(options.accountId, resolvedTarget);
135
+ }
90
136
  return result.data;
91
137
  } catch (err: unknown) {
92
138
  const maybeAxiosError = err as {
@@ -94,13 +140,23 @@ export async function sendProactiveTextOrMarkdown(
94
140
  message?: string;
95
141
  };
96
142
  if (maybeAxiosError?.response) {
143
+ const errCode = extractErrorCodeFromResponseData(maybeAxiosError.response.data);
144
+ if (options.accountId && isProactivePermissionOrScopeError(errCode)) {
145
+ recordProactiveRiskObservation({
146
+ accountId: options.accountId,
147
+ targetId: resolvedTarget,
148
+ level: "high",
149
+ reason: errCode || "proactive-permission-error",
150
+ source: "proactive-api",
151
+ });
152
+ }
97
153
  const status = maybeAxiosError.response.status;
98
154
  const statusText = maybeAxiosError.response.statusText;
99
155
  const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
100
156
  log?.error?.(
101
157
  `[DingTalk] Failed to send proactive message:${statusLabel} message=${
102
158
  maybeAxiosError.message || String(err)
103
- }`,
159
+ }${proactiveRiskTag}`,
104
160
  );
105
161
  if (maybeAxiosError.response.data !== undefined) {
106
162
  log?.error?.(
@@ -183,16 +239,36 @@ export async function sendProactiveMedia(
183
239
  data: payload,
184
240
  headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
185
241
  });
242
+ if (options.accountId) {
243
+ deleteProactiveRiskObservation(options.accountId, resolvedTarget);
244
+ }
186
245
 
187
246
  const messageId = result.data?.processQueryKey || result.data?.messageId;
188
247
  return { ok: true, data: result.data, messageId };
189
248
  } catch (err: any) {
190
249
  log?.error?.(`[DingTalk] Failed to send proactive media: ${err.message}`);
250
+ const normalizedTarget = resolveOriginalPeerId(stripTargetPrefix(target).targetId);
251
+ const proactiveRisk = options.accountId
252
+ ? getProactiveRiskObservation(options.accountId, normalizedTarget)
253
+ : null;
254
+ const proactiveRiskTag = proactiveRisk
255
+ ? ` proactiveRisk=${proactiveRisk.level}:${proactiveRisk.reason}`
256
+ : "";
191
257
  if (axios.isAxiosError(err) && err.response) {
258
+ const errCode = extractErrorCodeFromResponseData(err.response.data);
259
+ if (options.accountId && isProactivePermissionOrScopeError(errCode)) {
260
+ recordProactiveRiskObservation({
261
+ accountId: options.accountId,
262
+ targetId: normalizedTarget,
263
+ level: "high",
264
+ reason: errCode || "proactive-permission-error",
265
+ source: "proactive-api",
266
+ });
267
+ }
192
268
  const status = err.response.status;
193
269
  const statusText = err.response.statusText;
194
270
  const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
195
- log?.error?.(`[DingTalk] Proactive media response${statusLabel}`);
271
+ log?.error?.(`[DingTalk] Proactive media response${statusLabel}${proactiveRiskTag}`);
196
272
  log?.error?.(formatDingTalkErrorPayloadLog("send.proactiveMedia", err.response.data));
197
273
  }
198
274
  return { ok: false, error: err.message };
package/src/types.ts CHANGED
@@ -52,6 +52,12 @@ export interface DingTalkConfig extends OpenClawConfig {
52
52
  initialReconnectDelay?: number;
53
53
  maxReconnectDelay?: number;
54
54
  reconnectJitter?: number;
55
+ /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
56
+ maxReconnectCycles?: number;
57
+ proactivePermissionHint?: {
58
+ enabled?: boolean;
59
+ cooldownHours?: number;
60
+ };
55
61
  }
56
62
 
57
63
  /**
@@ -79,6 +85,12 @@ export interface DingTalkChannelConfig {
79
85
  initialReconnectDelay?: number;
80
86
  maxReconnectDelay?: number;
81
87
  reconnectJitter?: number;
88
+ /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
89
+ maxReconnectCycles?: number;
90
+ proactivePermissionHint?: {
91
+ enabled?: boolean;
92
+ cooldownHours?: number;
93
+ };
82
94
  }
83
95
 
84
96
  /**
@@ -201,6 +213,7 @@ export interface SendMessageOptions {
201
213
  filePath?: string;
202
214
  mediaUrl?: string;
203
215
  mediaType?: "image" | "voice" | "video" | "file";
216
+ accountId?: string;
204
217
  }
205
218
 
206
219
  /**
@@ -467,6 +480,8 @@ export interface ConnectionManagerConfig {
467
480
  initialDelay: number;
468
481
  maxDelay: number;
469
482
  jitter: number;
483
+ /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
484
+ maxReconnectCycles?: number;
470
485
  /** Callback invoked when connection state changes */
471
486
  onStateChange?: (state: ConnectionState, error?: string) => void;
472
487
  }
@@ -551,6 +566,8 @@ export function resolveDingTalkAccount(
551
566
  initialReconnectDelay: dingtalk?.initialReconnectDelay,
552
567
  maxReconnectDelay: dingtalk?.maxReconnectDelay,
553
568
  reconnectJitter: dingtalk?.reconnectJitter,
569
+ maxReconnectCycles: dingtalk?.maxReconnectCycles,
570
+ proactivePermissionHint: dingtalk?.proactivePermissionHint,
554
571
  };
555
572
  return {
556
573
  ...config,