@soimy/dingtalk 3.1.1 → 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 +1 -1
- package/src/channel.ts +1 -0
- package/src/config-schema.ts +3 -0
- package/src/connection-manager.ts +25 -3
- package/src/inbound-handler.ts +36 -15
- package/src/onboarding.ts +33 -0
- package/src/proactive-risk-registry.ts +25 -0
- package/src/send-service.ts +63 -1
- package/src/types.ts +7 -0
package/package.json
CHANGED
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;
|
package/src/config-schema.ts
CHANGED
|
@@ -76,6 +76,9 @@ const DingTalkAccountConfigSchema = z.object({
|
|
|
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
78
|
|
|
79
|
+
/** Maximum number of runtime reconnect cycles before giving up (default: 10) */
|
|
80
|
+
maxReconnectCycles: z.number().int().min(1).optional().default(10),
|
|
81
|
+
|
|
79
82
|
proactivePermissionHint: z
|
|
80
83
|
.object({
|
|
81
84
|
enabled: z.boolean().optional().default(true),
|
|
@@ -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
|
|
436
|
-
const delay = this.calculateNextDelay(
|
|
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();
|
package/src/inbound-handler.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { extractMessageContent } from "./message-utils";
|
|
|
18
18
|
import { registerPeerId } from "./peer-id-registry";
|
|
19
19
|
import {
|
|
20
20
|
clearProactiveRiskObservationsForTest,
|
|
21
|
-
|
|
21
|
+
getProactiveRiskObservationForAny,
|
|
22
22
|
} from "./proactive-risk-registry";
|
|
23
23
|
import { getDingTalkRuntime } from "./runtime";
|
|
24
24
|
import { sendBySession, sendMessage } from "./send-service";
|
|
@@ -51,8 +51,17 @@ function shouldSendProactivePermissionHint(params: {
|
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const targetId = (params.
|
|
55
|
-
if (!targetId
|
|
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") {
|
|
56
65
|
return false;
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -71,6 +80,14 @@ function shouldSendProactivePermissionHint(params: {
|
|
|
71
80
|
return true;
|
|
72
81
|
}
|
|
73
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
|
+
|
|
74
91
|
/**
|
|
75
92
|
* Download DingTalk media file via runtime media service (sandbox-compatible).
|
|
76
93
|
* Files are stored in the global media inbound directory.
|
|
@@ -207,7 +224,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
207
224
|
await sendBySession(
|
|
208
225
|
dingtalkConfig,
|
|
209
226
|
sessionWebhook,
|
|
210
|
-
|
|
227
|
+
"⚠️ 主动推送可能失败\n\n检测到该用户最近一次主动发送调用返回了权限或目标不可达错误。当前会话回复仍可正常使用,但定时/主动发送可能失败。\n\n建议:\n1) 在钉钉开放平台确认应用已申请并获得主动发送相关权限\n2) 确认目标用户属于当前企业并在应用可见范围内\n3) 使用相同账号进行一次主动发送验证并检查错误码详情",
|
|
211
228
|
{ log },
|
|
212
229
|
);
|
|
213
230
|
} catch (err: any) {
|
|
@@ -221,16 +238,6 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
221
238
|
registerPeerId(senderId);
|
|
222
239
|
}
|
|
223
240
|
|
|
224
|
-
if (isDirect && /^\d+$/.test((data.senderStaffId || senderId || "").trim())) {
|
|
225
|
-
recordProactiveRiskObservation({
|
|
226
|
-
accountId,
|
|
227
|
-
targetId: data.senderStaffId || senderId,
|
|
228
|
-
level: "high",
|
|
229
|
-
reason: "numeric-user-id",
|
|
230
|
-
source: "webhook-hint",
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
241
|
// 2) Authorization guard (DM/group policy).
|
|
235
242
|
let commandAuthorized = true;
|
|
236
243
|
if (isDirect) {
|
|
@@ -467,6 +474,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
467
474
|
return;
|
|
468
475
|
}
|
|
469
476
|
|
|
477
|
+
if (typeof textToSend === "string" && isUnhandledStopReasonText(textToSend)) {
|
|
478
|
+
log?.warn?.(`[DingTalk] Suppressed stop reason from outbound chat content: ${textToSend}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
470
482
|
if (useCardMode && currentAICard && info?.kind === "final") {
|
|
471
483
|
lastCardContent = textToSend;
|
|
472
484
|
return;
|
|
@@ -552,12 +564,21 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
552
564
|
const hasQueuedFinalString = isNonEmptyString(queuedFinal);
|
|
553
565
|
|
|
554
566
|
if (hasLastCardContent || hasQueuedFinalString) {
|
|
555
|
-
const
|
|
567
|
+
const finalContentCandidate =
|
|
556
568
|
hasLastCardContent && typeof lastCardContent === "string"
|
|
557
569
|
? lastCardContent
|
|
558
570
|
: typeof queuedFinal === "string"
|
|
559
571
|
? queuedFinal
|
|
560
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;
|
|
561
582
|
await finishAICard(currentAICard, finalContent, log);
|
|
562
583
|
} else {
|
|
563
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
|
|
|
@@ -59,6 +59,31 @@ export function getProactiveRiskObservation(
|
|
|
59
59
|
return entry;
|
|
60
60
|
}
|
|
61
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
|
+
|
|
62
87
|
export function clearProactiveRiskObservationsForTest(): void {
|
|
63
88
|
store.clear();
|
|
64
89
|
}
|
package/src/send-service.ts
CHANGED
|
@@ -13,7 +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 {
|
|
16
|
+
import {
|
|
17
|
+
deleteProactiveRiskObservation,
|
|
18
|
+
getProactiveRiskObservation,
|
|
19
|
+
recordProactiveRiskObservation,
|
|
20
|
+
} from "./proactive-risk-registry";
|
|
17
21
|
import { formatDingTalkErrorPayloadLog } from "./utils";
|
|
18
22
|
import type {
|
|
19
23
|
AxiosResponse,
|
|
@@ -27,6 +31,38 @@ import { AICardStatus } from "./types";
|
|
|
27
31
|
|
|
28
32
|
export { detectMediaTypeFromExtension } from "./media-utils";
|
|
29
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
|
+
|
|
30
66
|
/**
|
|
31
67
|
* Wrapper to upload media with shared getAccessToken binding.
|
|
32
68
|
*/
|
|
@@ -94,6 +130,9 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
94
130
|
data: payload,
|
|
95
131
|
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
96
132
|
});
|
|
133
|
+
if (options.accountId) {
|
|
134
|
+
deleteProactiveRiskObservation(options.accountId, resolvedTarget);
|
|
135
|
+
}
|
|
97
136
|
return result.data;
|
|
98
137
|
} catch (err: unknown) {
|
|
99
138
|
const maybeAxiosError = err as {
|
|
@@ -101,6 +140,16 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
101
140
|
message?: string;
|
|
102
141
|
};
|
|
103
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
|
+
}
|
|
104
153
|
const status = maybeAxiosError.response.status;
|
|
105
154
|
const statusText = maybeAxiosError.response.statusText;
|
|
106
155
|
const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
|
|
@@ -190,6 +239,9 @@ export async function sendProactiveMedia(
|
|
|
190
239
|
data: payload,
|
|
191
240
|
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
192
241
|
});
|
|
242
|
+
if (options.accountId) {
|
|
243
|
+
deleteProactiveRiskObservation(options.accountId, resolvedTarget);
|
|
244
|
+
}
|
|
193
245
|
|
|
194
246
|
const messageId = result.data?.processQueryKey || result.data?.messageId;
|
|
195
247
|
return { ok: true, data: result.data, messageId };
|
|
@@ -203,6 +255,16 @@ export async function sendProactiveMedia(
|
|
|
203
255
|
? ` proactiveRisk=${proactiveRisk.level}:${proactiveRisk.reason}`
|
|
204
256
|
: "";
|
|
205
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
|
+
}
|
|
206
268
|
const status = err.response.status;
|
|
207
269
|
const statusText = err.response.statusText;
|
|
208
270
|
const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
|
package/src/types.ts
CHANGED
|
@@ -52,6 +52,8 @@ 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;
|
|
55
57
|
proactivePermissionHint?: {
|
|
56
58
|
enabled?: boolean;
|
|
57
59
|
cooldownHours?: number;
|
|
@@ -83,6 +85,8 @@ export interface DingTalkChannelConfig {
|
|
|
83
85
|
initialReconnectDelay?: number;
|
|
84
86
|
maxReconnectDelay?: number;
|
|
85
87
|
reconnectJitter?: number;
|
|
88
|
+
/** Maximum number of runtime reconnect cycles before giving up (default: 10) */
|
|
89
|
+
maxReconnectCycles?: number;
|
|
86
90
|
proactivePermissionHint?: {
|
|
87
91
|
enabled?: boolean;
|
|
88
92
|
cooldownHours?: number;
|
|
@@ -476,6 +480,8 @@ export interface ConnectionManagerConfig {
|
|
|
476
480
|
initialDelay: number;
|
|
477
481
|
maxDelay: number;
|
|
478
482
|
jitter: number;
|
|
483
|
+
/** Maximum number of runtime reconnect cycles before giving up (default: 10) */
|
|
484
|
+
maxReconnectCycles?: number;
|
|
479
485
|
/** Callback invoked when connection state changes */
|
|
480
486
|
onStateChange?: (state: ConnectionState, error?: string) => void;
|
|
481
487
|
}
|
|
@@ -560,6 +566,7 @@ export function resolveDingTalkAccount(
|
|
|
560
566
|
initialReconnectDelay: dingtalk?.initialReconnectDelay,
|
|
561
567
|
maxReconnectDelay: dingtalk?.maxReconnectDelay,
|
|
562
568
|
reconnectJitter: dingtalk?.reconnectJitter,
|
|
569
|
+
maxReconnectCycles: dingtalk?.maxReconnectCycles,
|
|
563
570
|
proactivePermissionHint: dingtalk?.proactivePermissionHint,
|
|
564
571
|
};
|
|
565
572
|
return {
|