@soimy/dingtalk 2.6.5
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/README.md +482 -0
- package/clawbot.plugin.json +9 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +79 -0
- package/src/AGENTS.md +63 -0
- package/src/channel.ts +1807 -0
- package/src/config-schema.ts +92 -0
- package/src/connection-manager.ts +434 -0
- package/src/media-utils.ts +132 -0
- package/src/onboarding.ts +325 -0
- package/src/openclaw-channel-dingtalk.code-workspace +17 -0
- package/src/peer-id-registry.ts +35 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +543 -0
- package/src/utils.ts +106 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for DingTalk Channel Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive type safety for:
|
|
5
|
+
* - Configuration objects
|
|
6
|
+
* - DingTalk API request/response models
|
|
7
|
+
* - Message content and formats
|
|
8
|
+
* - Media files and streams
|
|
9
|
+
* - Session and token management
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
OpenClawConfig,
|
|
14
|
+
ChannelLogSink as SDKChannelLogSink,
|
|
15
|
+
ChannelAccountSnapshot as SDKChannelAccountSnapshot,
|
|
16
|
+
ChannelGatewayContext as SDKChannelGatewayContext,
|
|
17
|
+
ChannelPlugin as SDKChannelPlugin,
|
|
18
|
+
} from "openclaw/plugin-sdk";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* DingTalk channel configuration (extends base OpenClaw config)
|
|
22
|
+
*/
|
|
23
|
+
export interface DingTalkConfig extends OpenClawConfig {
|
|
24
|
+
clientId: string;
|
|
25
|
+
clientSecret: string;
|
|
26
|
+
robotCode?: string;
|
|
27
|
+
corpId?: string;
|
|
28
|
+
agentId?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
32
|
+
groupPolicy?: "open" | "allowlist";
|
|
33
|
+
allowFrom?: string[];
|
|
34
|
+
showThinking?: boolean;
|
|
35
|
+
debug?: boolean;
|
|
36
|
+
messageType?: "markdown" | "card";
|
|
37
|
+
cardTemplateId?: string;
|
|
38
|
+
cardTemplateKey?: string;
|
|
39
|
+
groups?: Record<string, { systemPrompt?: string }>;
|
|
40
|
+
accounts?: Record<string, DingTalkConfig>;
|
|
41
|
+
// Connection robustness configuration
|
|
42
|
+
maxConnectionAttempts?: number;
|
|
43
|
+
initialReconnectDelay?: number;
|
|
44
|
+
maxReconnectDelay?: number;
|
|
45
|
+
reconnectJitter?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Multi-account DingTalk configuration wrapper
|
|
50
|
+
*/
|
|
51
|
+
export interface DingTalkChannelConfig {
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
clientId: string;
|
|
54
|
+
clientSecret: string;
|
|
55
|
+
robotCode?: string;
|
|
56
|
+
corpId?: string;
|
|
57
|
+
agentId?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
60
|
+
groupPolicy?: "open" | "allowlist";
|
|
61
|
+
allowFrom?: string[];
|
|
62
|
+
showThinking?: boolean;
|
|
63
|
+
debug?: boolean;
|
|
64
|
+
messageType?: "markdown" | "card";
|
|
65
|
+
cardTemplateId?: string;
|
|
66
|
+
cardTemplateKey?: string;
|
|
67
|
+
groups?: Record<string, { systemPrompt?: string }>;
|
|
68
|
+
accounts?: Record<string, DingTalkConfig>;
|
|
69
|
+
maxConnectionAttempts?: number;
|
|
70
|
+
initialReconnectDelay?: number;
|
|
71
|
+
maxReconnectDelay?: number;
|
|
72
|
+
reconnectJitter?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* DingTalk token info for caching
|
|
77
|
+
*/
|
|
78
|
+
export interface TokenInfo {
|
|
79
|
+
accessToken: string;
|
|
80
|
+
expireIn: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* DingTalk API token response
|
|
85
|
+
*/
|
|
86
|
+
export interface TokenResponse {
|
|
87
|
+
accessToken: string;
|
|
88
|
+
expireIn: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* DingTalk API generic response wrapper
|
|
93
|
+
*/
|
|
94
|
+
export interface DingTalkApiResponse<T = unknown> {
|
|
95
|
+
data?: T;
|
|
96
|
+
code?: string;
|
|
97
|
+
message?: string;
|
|
98
|
+
success?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Media download response from DingTalk API
|
|
103
|
+
*/
|
|
104
|
+
export interface MediaDownloadResponse {
|
|
105
|
+
downloadUrl?: string;
|
|
106
|
+
downloadCode?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Media file metadata
|
|
111
|
+
*/
|
|
112
|
+
export interface MediaFile {
|
|
113
|
+
path: string;
|
|
114
|
+
mimeType: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* DingTalk incoming message (Stream mode)
|
|
119
|
+
*/
|
|
120
|
+
export interface DingTalkInboundMessage {
|
|
121
|
+
msgId: string;
|
|
122
|
+
msgtype: string;
|
|
123
|
+
createAt: number;
|
|
124
|
+
text?: {
|
|
125
|
+
content: string;
|
|
126
|
+
};
|
|
127
|
+
content?: {
|
|
128
|
+
downloadCode?: string;
|
|
129
|
+
fileName?: string;
|
|
130
|
+
recognition?: string;
|
|
131
|
+
richText?: Array<{
|
|
132
|
+
type: string;
|
|
133
|
+
text?: string;
|
|
134
|
+
atName?: string;
|
|
135
|
+
downloadCode?: string; // For picture type in richText
|
|
136
|
+
}>;
|
|
137
|
+
};
|
|
138
|
+
conversationType: string;
|
|
139
|
+
conversationId: string;
|
|
140
|
+
conversationTitle?: string;
|
|
141
|
+
senderId: string;
|
|
142
|
+
senderStaffId?: string;
|
|
143
|
+
senderNick?: string;
|
|
144
|
+
chatbotUserId: string;
|
|
145
|
+
sessionWebhook: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extracted message content for unified processing
|
|
150
|
+
*/
|
|
151
|
+
export interface MessageContent {
|
|
152
|
+
text: string;
|
|
153
|
+
mediaPath?: string;
|
|
154
|
+
mediaType?: string;
|
|
155
|
+
messageType: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Send message options
|
|
160
|
+
*/
|
|
161
|
+
export interface SendMessageOptions {
|
|
162
|
+
title?: string;
|
|
163
|
+
useMarkdown?: boolean;
|
|
164
|
+
atUserId?: string | null;
|
|
165
|
+
log?: any;
|
|
166
|
+
mediaPath?: string;
|
|
167
|
+
filePath?: string;
|
|
168
|
+
mediaUrl?: string;
|
|
169
|
+
mediaType?: "image" | "voice" | "video" | "file";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Session webhook response
|
|
174
|
+
*/
|
|
175
|
+
export interface SessionWebhookResponse {
|
|
176
|
+
msgtype: string;
|
|
177
|
+
markdown?: {
|
|
178
|
+
title: string;
|
|
179
|
+
text: string;
|
|
180
|
+
};
|
|
181
|
+
text?: {
|
|
182
|
+
content: string;
|
|
183
|
+
};
|
|
184
|
+
at?: {
|
|
185
|
+
atUserIds: string[];
|
|
186
|
+
isAtAll: boolean;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Message handler parameters
|
|
192
|
+
*/
|
|
193
|
+
export interface HandleDingTalkMessageParams {
|
|
194
|
+
cfg: OpenClawConfig;
|
|
195
|
+
accountId: string;
|
|
196
|
+
data: DingTalkInboundMessage;
|
|
197
|
+
sessionWebhook: string;
|
|
198
|
+
log?: any;
|
|
199
|
+
dingtalkConfig: DingTalkConfig;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Proactive message payload
|
|
204
|
+
*/
|
|
205
|
+
export interface ProactiveMessagePayload {
|
|
206
|
+
robotCode: string;
|
|
207
|
+
msgKey: string;
|
|
208
|
+
msgParam: string;
|
|
209
|
+
openConversationId?: string;
|
|
210
|
+
userIds?: string[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Account descriptor
|
|
215
|
+
*/
|
|
216
|
+
export interface AccountDescriptor {
|
|
217
|
+
accountId: string;
|
|
218
|
+
config?: DingTalkConfig;
|
|
219
|
+
enabled?: boolean;
|
|
220
|
+
name?: string;
|
|
221
|
+
configured?: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Account resolver result
|
|
226
|
+
*/
|
|
227
|
+
export interface ResolvedAccount {
|
|
228
|
+
accountId: string;
|
|
229
|
+
config: DingTalkConfig;
|
|
230
|
+
enabled: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* HTTP request config for axios
|
|
235
|
+
*/
|
|
236
|
+
export interface AxiosRequestConfig {
|
|
237
|
+
url?: string;
|
|
238
|
+
method?: string;
|
|
239
|
+
data?: any;
|
|
240
|
+
headers?: Record<string, string>;
|
|
241
|
+
responseType?: "arraybuffer" | "json" | "text";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* HTTP response from axios
|
|
246
|
+
*/
|
|
247
|
+
export interface AxiosResponse<T = any> {
|
|
248
|
+
data: T;
|
|
249
|
+
status: number;
|
|
250
|
+
statusText: string;
|
|
251
|
+
headers: Record<string, string>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* DingTalk Stream callback listener types
|
|
256
|
+
*/
|
|
257
|
+
export interface StreamCallbackResponse {
|
|
258
|
+
headers?: {
|
|
259
|
+
messageId?: string;
|
|
260
|
+
};
|
|
261
|
+
data: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Reply dispatcher context
|
|
266
|
+
*/
|
|
267
|
+
export interface ReplyDispatchContext {
|
|
268
|
+
responsePrefix?: string;
|
|
269
|
+
deliver: (payload: any) => Promise<{ ok: boolean; error?: string }>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reply dispatcher result
|
|
274
|
+
*/
|
|
275
|
+
export interface ReplyDispatcherResult {
|
|
276
|
+
dispatcher: any;
|
|
277
|
+
replyOptions: any;
|
|
278
|
+
markDispatchIdle: () => void;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Retry options
|
|
283
|
+
*/
|
|
284
|
+
export interface RetryOptions {
|
|
285
|
+
maxRetries?: number;
|
|
286
|
+
baseDelayMs?: number;
|
|
287
|
+
log?: any;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Channel log sink
|
|
292
|
+
*/
|
|
293
|
+
export type ChannelLogSink = SDKChannelLogSink;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @deprecated Use ChannelLogSink instead
|
|
297
|
+
*/
|
|
298
|
+
export type Logger = ChannelLogSink;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Channel account snapshot
|
|
302
|
+
*/
|
|
303
|
+
export type ChannelAccountSnapshot = SDKChannelAccountSnapshot;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @deprecated Use ChannelAccountSnapshot instead
|
|
307
|
+
*/
|
|
308
|
+
export type ChannelSnapshot = ChannelAccountSnapshot;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Plugin gateway start context
|
|
312
|
+
*/
|
|
313
|
+
export type GatewayStartContext = SDKChannelGatewayContext<ResolvedAccount>;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Plugin gateway account stop result
|
|
317
|
+
*/
|
|
318
|
+
export interface GatewayStopResult {
|
|
319
|
+
stop: () => void;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* DingTalk channel plugin definition
|
|
324
|
+
*/
|
|
325
|
+
export type DingTalkChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }>;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Result of target resolution validation
|
|
329
|
+
*/
|
|
330
|
+
export interface TargetResolutionResult {
|
|
331
|
+
ok: boolean;
|
|
332
|
+
to?: string;
|
|
333
|
+
error?: Error;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Parameters for resolveTarget validation
|
|
338
|
+
*/
|
|
339
|
+
export interface ResolveTargetParams {
|
|
340
|
+
to?: string | null;
|
|
341
|
+
[key: string]: any;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parameters for sendText delivery
|
|
346
|
+
*/
|
|
347
|
+
export interface SendTextParams {
|
|
348
|
+
cfg: DingTalkConfig;
|
|
349
|
+
to: string;
|
|
350
|
+
text: string;
|
|
351
|
+
accountId?: string;
|
|
352
|
+
[key: string]: any;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Parameters for sendMedia delivery
|
|
357
|
+
*/
|
|
358
|
+
export interface SendMediaParams {
|
|
359
|
+
cfg: DingTalkConfig;
|
|
360
|
+
to: string;
|
|
361
|
+
mediaPath: string;
|
|
362
|
+
accountId?: string;
|
|
363
|
+
[key: string]: any;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* DingTalk outbound handler configuration
|
|
368
|
+
*/
|
|
369
|
+
export interface DingTalkOutboundHandler {
|
|
370
|
+
deliveryMode: "direct" | "queued" | "batch";
|
|
371
|
+
resolveTarget: (params: ResolveTargetParams) => TargetResolutionResult;
|
|
372
|
+
sendText: (params: SendTextParams) => Promise<{ ok: boolean; data?: any; error?: any }>;
|
|
373
|
+
sendMedia?: (params: SendMediaParams) => Promise<{ ok: boolean; data?: any; error?: any }>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* AI Card status constants
|
|
378
|
+
*/
|
|
379
|
+
export const AICardStatus = {
|
|
380
|
+
PROCESSING: "1",
|
|
381
|
+
INPUTING: "2",
|
|
382
|
+
FINISHED: "3",
|
|
383
|
+
FAILED: "5",
|
|
384
|
+
} as const;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* AI Card state type
|
|
388
|
+
*/
|
|
389
|
+
export type AICardState = (typeof AICardStatus)[keyof typeof AICardStatus];
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* AI Card instance
|
|
393
|
+
*/
|
|
394
|
+
export interface AICardInstance {
|
|
395
|
+
cardInstanceId: string;
|
|
396
|
+
accessToken: string;
|
|
397
|
+
conversationId: string;
|
|
398
|
+
createdAt: number;
|
|
399
|
+
lastUpdated: number;
|
|
400
|
+
state: AICardState; // Current card state: PROCESSING, INPUTING, FINISHED, FAILED
|
|
401
|
+
config?: DingTalkConfig; // Store config reference for token refresh
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* AI Card streaming update request (new API)
|
|
406
|
+
*/
|
|
407
|
+
export interface AICardStreamingRequest {
|
|
408
|
+
outTrackId: string;
|
|
409
|
+
guid: string;
|
|
410
|
+
key: string;
|
|
411
|
+
content: string;
|
|
412
|
+
isFull: boolean;
|
|
413
|
+
isFinalize: boolean;
|
|
414
|
+
isError: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Connection state enum for lifecycle management
|
|
419
|
+
*/
|
|
420
|
+
export enum ConnectionState {
|
|
421
|
+
DISCONNECTED = "DISCONNECTED",
|
|
422
|
+
CONNECTING = "CONNECTING",
|
|
423
|
+
CONNECTED = "CONNECTED",
|
|
424
|
+
DISCONNECTING = "DISCONNECTING",
|
|
425
|
+
FAILED = "FAILED",
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Connection manager configuration
|
|
430
|
+
*/
|
|
431
|
+
export interface ConnectionManagerConfig {
|
|
432
|
+
maxAttempts: number;
|
|
433
|
+
initialDelay: number;
|
|
434
|
+
maxDelay: number;
|
|
435
|
+
jitter: number;
|
|
436
|
+
/** Callback invoked when connection state changes */
|
|
437
|
+
onStateChange?: (state: ConnectionState, error?: string) => void;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Connection attempt result
|
|
442
|
+
*/
|
|
443
|
+
export interface ConnectionAttemptResult {
|
|
444
|
+
success: boolean;
|
|
445
|
+
attempt: number;
|
|
446
|
+
error?: Error;
|
|
447
|
+
nextDelay?: number;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============ Onboarding Helper Functions ============
|
|
451
|
+
|
|
452
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* List all DingTalk account IDs from config
|
|
456
|
+
*/
|
|
457
|
+
export function listDingTalkAccountIds(cfg: OpenClawConfig): string[] {
|
|
458
|
+
const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
|
|
459
|
+
if (!dingtalk) return [];
|
|
460
|
+
|
|
461
|
+
const accountIds: string[] = [];
|
|
462
|
+
|
|
463
|
+
// Check for direct configuration (default account)
|
|
464
|
+
if (dingtalk.clientId || dingtalk.clientSecret) {
|
|
465
|
+
accountIds.push(DEFAULT_ACCOUNT_ID);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check accounts object
|
|
469
|
+
if (dingtalk.accounts) {
|
|
470
|
+
accountIds.push(...Object.keys(dingtalk.accounts));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return accountIds;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Resolved DingTalk account with configuration status
|
|
478
|
+
*/
|
|
479
|
+
export interface ResolvedDingTalkAccount extends DingTalkConfig {
|
|
480
|
+
accountId: string;
|
|
481
|
+
configured: boolean;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Resolve a specific DingTalk account configuration
|
|
486
|
+
*/
|
|
487
|
+
export function resolveDingTalkAccount(
|
|
488
|
+
cfg: OpenClawConfig,
|
|
489
|
+
accountId?: string | null,
|
|
490
|
+
): ResolvedDingTalkAccount {
|
|
491
|
+
const id = accountId || DEFAULT_ACCOUNT_ID;
|
|
492
|
+
const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
|
|
493
|
+
|
|
494
|
+
// If default account, return top-level config
|
|
495
|
+
if (id === DEFAULT_ACCOUNT_ID) {
|
|
496
|
+
const config: DingTalkConfig = {
|
|
497
|
+
clientId: dingtalk?.clientId ?? "",
|
|
498
|
+
clientSecret: dingtalk?.clientSecret ?? "",
|
|
499
|
+
robotCode: dingtalk?.robotCode,
|
|
500
|
+
corpId: dingtalk?.corpId,
|
|
501
|
+
agentId: dingtalk?.agentId,
|
|
502
|
+
name: dingtalk?.name,
|
|
503
|
+
enabled: dingtalk?.enabled,
|
|
504
|
+
dmPolicy: dingtalk?.dmPolicy,
|
|
505
|
+
groupPolicy: dingtalk?.groupPolicy,
|
|
506
|
+
allowFrom: dingtalk?.allowFrom,
|
|
507
|
+
showThinking: dingtalk?.showThinking,
|
|
508
|
+
debug: dingtalk?.debug,
|
|
509
|
+
messageType: dingtalk?.messageType,
|
|
510
|
+
cardTemplateId: dingtalk?.cardTemplateId,
|
|
511
|
+
cardTemplateKey: dingtalk?.cardTemplateKey,
|
|
512
|
+
groups: dingtalk?.groups,
|
|
513
|
+
accounts: dingtalk?.accounts,
|
|
514
|
+
maxConnectionAttempts: dingtalk?.maxConnectionAttempts,
|
|
515
|
+
initialReconnectDelay: dingtalk?.initialReconnectDelay,
|
|
516
|
+
maxReconnectDelay: dingtalk?.maxReconnectDelay,
|
|
517
|
+
reconnectJitter: dingtalk?.reconnectJitter,
|
|
518
|
+
};
|
|
519
|
+
return {
|
|
520
|
+
...config,
|
|
521
|
+
accountId: id,
|
|
522
|
+
configured: Boolean(config.clientId && config.clientSecret),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// If named account, get from accounts object
|
|
527
|
+
const accountConfig = dingtalk?.accounts?.[id];
|
|
528
|
+
if (accountConfig) {
|
|
529
|
+
return {
|
|
530
|
+
...accountConfig,
|
|
531
|
+
accountId: id,
|
|
532
|
+
configured: Boolean(accountConfig.clientId && accountConfig.clientSecret),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Account doesn't exist, return empty config
|
|
537
|
+
return {
|
|
538
|
+
clientId: "",
|
|
539
|
+
clientSecret: "",
|
|
540
|
+
accountId: id,
|
|
541
|
+
configured: false,
|
|
542
|
+
};
|
|
543
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import type { Logger, RetryOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mask sensitive fields in data for safe logging
|
|
8
|
+
* Prevents PII leakage in debug logs
|
|
9
|
+
*/
|
|
10
|
+
export function maskSensitiveData(data: unknown): any {
|
|
11
|
+
if (data === null || data === undefined) {
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof data !== 'object') {
|
|
16
|
+
return data as string | number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const masked = JSON.parse(JSON.stringify(data)) as Record<string, any>;
|
|
20
|
+
const sensitiveFields = ['token', 'accessToken'];
|
|
21
|
+
|
|
22
|
+
function maskObj(obj: any): void {
|
|
23
|
+
for (const key in obj) {
|
|
24
|
+
if (sensitiveFields.includes(key)) {
|
|
25
|
+
const val = obj[key];
|
|
26
|
+
if (typeof val === 'string' && val.length > 6) {
|
|
27
|
+
obj[key] = val.slice(0, 3) + '*'.repeat(val.length - 6) + val.slice(-3);
|
|
28
|
+
} else if (typeof val === 'string') {
|
|
29
|
+
obj[key] = '*'.repeat(val.length);
|
|
30
|
+
}
|
|
31
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
32
|
+
maskObj(obj[key]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
maskObj(masked);
|
|
38
|
+
return masked;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cleanup orphaned temp files from dingtalk media
|
|
43
|
+
* Run at startup to clean up files from crashed processes
|
|
44
|
+
*/
|
|
45
|
+
export function cleanupOrphanedTempFiles(log?: Logger): number {
|
|
46
|
+
const tempDir = os.tmpdir();
|
|
47
|
+
const dingtalkPattern = /^dingtalk_\d+\..+$/;
|
|
48
|
+
let cleaned = 0;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const files = fs.readdirSync(tempDir);
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const maxAge = 24 * 60 * 60 * 1000;
|
|
54
|
+
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
if (!dingtalkPattern.test(file)) continue;
|
|
57
|
+
|
|
58
|
+
const filePath = path.join(tempDir, file);
|
|
59
|
+
try {
|
|
60
|
+
const stats = fs.statSync(filePath);
|
|
61
|
+
if (now - stats.mtime.getTime() > maxAge) {
|
|
62
|
+
fs.unlinkSync(filePath);
|
|
63
|
+
cleaned++;
|
|
64
|
+
log?.debug?.(`[DingTalk] Cleaned up orphaned temp file: ${file}`);
|
|
65
|
+
}
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
log?.debug?.(`[DingTalk] Failed to cleanup temp file ${file}: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (cleaned > 0) {
|
|
72
|
+
log?.info?.(`[DingTalk] Cleaned up ${cleaned} orphaned temp files`);
|
|
73
|
+
}
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
log?.debug?.(`[DingTalk] Failed to cleanup temp directory: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return cleaned;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Retry logic for API calls with exponential backoff
|
|
83
|
+
* Handles transient failures like 401 token expiry
|
|
84
|
+
*/
|
|
85
|
+
export async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
|
86
|
+
const { maxRetries = 3, baseDelayMs = 100, log } = options;
|
|
87
|
+
|
|
88
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
89
|
+
try {
|
|
90
|
+
return await fn();
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
const statusCode = err.response?.status;
|
|
93
|
+
const isRetryable = statusCode === 401 || statusCode === 429 || (statusCode && statusCode >= 500);
|
|
94
|
+
|
|
95
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
100
|
+
log?.debug?.(`[DingTalk] Retry attempt ${attempt}/${maxRetries} after ${delayMs}ms`);
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw new Error('Retry exhausted without returning');
|
|
106
|
+
}
|