apaas-oapi-client 0.1.29 → 0.1.31
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/.env +5 -0
- package/README.md +7 -0
- package/dist/index.d.ts +513 -0
- package/dist/index.js +219 -21
- package/dist/limiter.d.ts +17 -0
- package/dist/logger.d.ts +11 -0
- package/dist/src/index.d.ts +61 -3
- package/package.json +1 -1
- package/src/index.ts +338 -27
package/.env
ADDED
package/README.md
CHANGED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { LoggerLevel } from './logger';
|
|
2
|
+
/**
|
|
3
|
+
* Client 初始化配置
|
|
4
|
+
*/
|
|
5
|
+
interface ClientOptions {
|
|
6
|
+
/** 命名空间, 例如 app_xxx */
|
|
7
|
+
namespace: string;
|
|
8
|
+
/** 应用 clientId */
|
|
9
|
+
clientId: string;
|
|
10
|
+
/** 应用 clientSecret */
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
/** 是否禁用 token 缓存, 每次调用强制刷新 token, 默认 false */
|
|
13
|
+
disableTokenCache?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* aPaaS OpenAPI 客户端
|
|
17
|
+
*/
|
|
18
|
+
declare class Client {
|
|
19
|
+
private clientId;
|
|
20
|
+
private clientSecret;
|
|
21
|
+
private namespace;
|
|
22
|
+
private disableTokenCache;
|
|
23
|
+
private accessToken;
|
|
24
|
+
private expireTime;
|
|
25
|
+
private axiosInstance;
|
|
26
|
+
private loggerLevel;
|
|
27
|
+
/**
|
|
28
|
+
* 构造函数
|
|
29
|
+
* @param options ClientOptions
|
|
30
|
+
*/
|
|
31
|
+
constructor(options: ClientOptions);
|
|
32
|
+
/**
|
|
33
|
+
* 设置日志等级
|
|
34
|
+
* @param level LoggerLevel
|
|
35
|
+
*/
|
|
36
|
+
setLoggerLevel(level: LoggerLevel): void;
|
|
37
|
+
/**
|
|
38
|
+
* 日志打印方法
|
|
39
|
+
* @param level LoggerLevel
|
|
40
|
+
* @param args 打印内容
|
|
41
|
+
*/
|
|
42
|
+
private log;
|
|
43
|
+
/**
|
|
44
|
+
* 初始化 client, 自动获取 token
|
|
45
|
+
*/
|
|
46
|
+
init(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* 获取 accessToken
|
|
49
|
+
*/
|
|
50
|
+
private getAccessToken;
|
|
51
|
+
/**
|
|
52
|
+
* 确保 token 有效, 若过期则刷新
|
|
53
|
+
*/
|
|
54
|
+
private ensureTokenValid;
|
|
55
|
+
/**
|
|
56
|
+
* 获取当前 accessToken
|
|
57
|
+
*/
|
|
58
|
+
get token(): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* 获取当前 token 剩余过期时间(单位:秒)
|
|
61
|
+
* @returns 剩余秒数,若无 token 则返回 null
|
|
62
|
+
*/
|
|
63
|
+
get tokenExpireTime(): number | null;
|
|
64
|
+
/**
|
|
65
|
+
* 获取当前 namespace
|
|
66
|
+
*/
|
|
67
|
+
get currentNamespace(): string;
|
|
68
|
+
/**
|
|
69
|
+
* 对象模块
|
|
70
|
+
*/
|
|
71
|
+
object: {
|
|
72
|
+
/**
|
|
73
|
+
* 列出所有对象(数据表)
|
|
74
|
+
* @param params 请求参数 { offset, filter?, limit }
|
|
75
|
+
* @returns 接口返回结果
|
|
76
|
+
*/
|
|
77
|
+
list: (params: {
|
|
78
|
+
offset: number;
|
|
79
|
+
filter?: {
|
|
80
|
+
type?: string;
|
|
81
|
+
quickQuery?: string;
|
|
82
|
+
};
|
|
83
|
+
limit: number;
|
|
84
|
+
}) => Promise<any>;
|
|
85
|
+
metadata: {
|
|
86
|
+
/**
|
|
87
|
+
* 获取指定对象下指定字段的元数据
|
|
88
|
+
* @description 查询指定对象下的单个字段元数据
|
|
89
|
+
* @param params 请求参数 { object_name, field_name }
|
|
90
|
+
* @returns 接口返回结果
|
|
91
|
+
*/
|
|
92
|
+
field: (params: {
|
|
93
|
+
object_name: string;
|
|
94
|
+
field_name: string;
|
|
95
|
+
}) => Promise<any>;
|
|
96
|
+
/**
|
|
97
|
+
* 获取指定对象的所有字段信息
|
|
98
|
+
* @description 查询指定对象下的所有字段元数据
|
|
99
|
+
* @param params 请求参数 { object_name }
|
|
100
|
+
* @returns 接口返回结果
|
|
101
|
+
*/
|
|
102
|
+
fields: (params: {
|
|
103
|
+
object_name: string;
|
|
104
|
+
}) => Promise<any>;
|
|
105
|
+
};
|
|
106
|
+
search: {
|
|
107
|
+
/**
|
|
108
|
+
* 单条记录查询
|
|
109
|
+
* @description 查询指定对象下的单条记录
|
|
110
|
+
* @param params 请求参数
|
|
111
|
+
* @returns 接口返回结果
|
|
112
|
+
*/
|
|
113
|
+
record: (params: {
|
|
114
|
+
object_name: string;
|
|
115
|
+
record_id: string;
|
|
116
|
+
select: string[];
|
|
117
|
+
}) => Promise<any>;
|
|
118
|
+
/**
|
|
119
|
+
* 多条记录查询 - 最多传入 100 条
|
|
120
|
+
* @description 查询指定对象下的多条记录
|
|
121
|
+
* @param params 请求参数
|
|
122
|
+
* @returns 接口返回结果
|
|
123
|
+
*/
|
|
124
|
+
records: (params: {
|
|
125
|
+
object_name: string;
|
|
126
|
+
data: any;
|
|
127
|
+
}) => Promise<any>;
|
|
128
|
+
/**
|
|
129
|
+
* 查询所有记录 - 支持超过 100 条数据,自动分页查询
|
|
130
|
+
* @description 该方法会自动处理分页,直到没有更多数据为止
|
|
131
|
+
* @param params 请求参数
|
|
132
|
+
* @returns { total, items }
|
|
133
|
+
*/
|
|
134
|
+
recordsWithIterator: (params: {
|
|
135
|
+
object_name: string;
|
|
136
|
+
data: any;
|
|
137
|
+
}) => Promise<{
|
|
138
|
+
total: number;
|
|
139
|
+
items: any[];
|
|
140
|
+
}>;
|
|
141
|
+
};
|
|
142
|
+
create: {
|
|
143
|
+
/**
|
|
144
|
+
* 单条记录创建
|
|
145
|
+
* @description 创建单条记录到指定对象中
|
|
146
|
+
* @param params 请求参数 { object_name, record }
|
|
147
|
+
* @returns 接口返回结果
|
|
148
|
+
*/
|
|
149
|
+
record: (params: {
|
|
150
|
+
object_name: string;
|
|
151
|
+
record: any;
|
|
152
|
+
}) => Promise<any>;
|
|
153
|
+
/**
|
|
154
|
+
* 批量创建记录 - 最多传入 100 条
|
|
155
|
+
* @description 创建多条记录到指定对象中
|
|
156
|
+
* @param params 请求参数 { object_name, records }
|
|
157
|
+
* @returns 接口返回结果
|
|
158
|
+
*/
|
|
159
|
+
records: (params: {
|
|
160
|
+
object_name: string;
|
|
161
|
+
records: any[];
|
|
162
|
+
}) => Promise<any>;
|
|
163
|
+
/**
|
|
164
|
+
* 分批创建所有记录 - 支持超过 100 条数据,自动拆分
|
|
165
|
+
* @description 创建多条记录到指定对象中,超过 100 条数据会自动拆分为多次请求
|
|
166
|
+
* @param params 请求参数 { object_name, records }
|
|
167
|
+
* @returns { total, items }
|
|
168
|
+
*/
|
|
169
|
+
recordsWithIterator: (params: {
|
|
170
|
+
object_name: string;
|
|
171
|
+
records: any[];
|
|
172
|
+
}) => Promise<{
|
|
173
|
+
total: number;
|
|
174
|
+
items: any[];
|
|
175
|
+
}>;
|
|
176
|
+
};
|
|
177
|
+
update: {
|
|
178
|
+
/**
|
|
179
|
+
* 单条更新
|
|
180
|
+
* @description 更新指定对象下的单条记录
|
|
181
|
+
* @param params 请求参数
|
|
182
|
+
* @returns 接口返回结果
|
|
183
|
+
*/
|
|
184
|
+
record: (params: {
|
|
185
|
+
object_name: string;
|
|
186
|
+
record_id: string;
|
|
187
|
+
record: any;
|
|
188
|
+
}) => Promise<any>;
|
|
189
|
+
/**
|
|
190
|
+
* 多条更新 - 最多传入 100 条
|
|
191
|
+
* @description 更新指定对象下的多条记录
|
|
192
|
+
* @param params 请求参数
|
|
193
|
+
* @returns 接口返回结果
|
|
194
|
+
*/
|
|
195
|
+
records: (params: {
|
|
196
|
+
object_name: string;
|
|
197
|
+
records: any[];
|
|
198
|
+
}) => Promise<any>;
|
|
199
|
+
/**
|
|
200
|
+
* 批量更新 - 支持超过 100 条数据,自动拆分
|
|
201
|
+
* @description 更新指定对象下的多条记录,超过 100 条数据会自动拆分为多次请求
|
|
202
|
+
* @param params 请求参数
|
|
203
|
+
* @returns 所有子请求的返回结果数组
|
|
204
|
+
*/
|
|
205
|
+
recordsWithIterator: (params: {
|
|
206
|
+
object_name: string;
|
|
207
|
+
records: any[];
|
|
208
|
+
}) => Promise<any[]>;
|
|
209
|
+
};
|
|
210
|
+
delete: {
|
|
211
|
+
/**
|
|
212
|
+
* 单条删除
|
|
213
|
+
* @description 删除指定对象下的单条记录
|
|
214
|
+
* @param params 请求参数
|
|
215
|
+
* @returns 接口返回结果
|
|
216
|
+
*/
|
|
217
|
+
record: (params: {
|
|
218
|
+
object_name: string;
|
|
219
|
+
record_id: string;
|
|
220
|
+
}) => Promise<any>;
|
|
221
|
+
/**
|
|
222
|
+
* 多条删除 - 最多传入 100 条
|
|
223
|
+
* @description 删除指定对象下的多条记录
|
|
224
|
+
* @param params 请求参数
|
|
225
|
+
* @returns 接口返回结果
|
|
226
|
+
*/
|
|
227
|
+
records: (params: {
|
|
228
|
+
object_name: string;
|
|
229
|
+
ids: string[];
|
|
230
|
+
}) => Promise<any>;
|
|
231
|
+
/**
|
|
232
|
+
* 批量删除
|
|
233
|
+
* @description 删除指定对象下的多条记录,超过 100 条数据会自动拆分为多次请求
|
|
234
|
+
* @param params 请求参数
|
|
235
|
+
* @returns 所有子请求的返回结果数组
|
|
236
|
+
*/
|
|
237
|
+
recordsWithIterator: (params: {
|
|
238
|
+
object_name: string;
|
|
239
|
+
ids: string[];
|
|
240
|
+
}) => Promise<any[]>;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* 部门 ID 交换模块
|
|
245
|
+
*/
|
|
246
|
+
department: {
|
|
247
|
+
/**
|
|
248
|
+
* 单个部门 ID 交换
|
|
249
|
+
* @param params 请求参数
|
|
250
|
+
* @returns 单个部门映射结果
|
|
251
|
+
*/
|
|
252
|
+
exchange: (params: {
|
|
253
|
+
department_id_type: "department_id" | "external_department_id" | "external_open_department_id";
|
|
254
|
+
department_id: string;
|
|
255
|
+
}) => Promise<any>;
|
|
256
|
+
/**
|
|
257
|
+
* 批量部门 ID 交换
|
|
258
|
+
* @param params 请求参数
|
|
259
|
+
* @returns 所有子请求的返回结果数组
|
|
260
|
+
*/
|
|
261
|
+
batchExchange: (params: {
|
|
262
|
+
department_id_type: "department_id" | "external_department_id" | "external_open_department_id";
|
|
263
|
+
department_ids: string[];
|
|
264
|
+
}) => Promise<any[]>;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* 云函数模块
|
|
268
|
+
*/
|
|
269
|
+
function: {
|
|
270
|
+
/**
|
|
271
|
+
* 调用云函数
|
|
272
|
+
* @param params 请求参数 { name: string; params: any }
|
|
273
|
+
* @returns 接口返回结果
|
|
274
|
+
*/
|
|
275
|
+
invoke: (params: {
|
|
276
|
+
name: string;
|
|
277
|
+
params: any;
|
|
278
|
+
}) => Promise<any>;
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* 页面模块
|
|
282
|
+
*/
|
|
283
|
+
page: {
|
|
284
|
+
/**
|
|
285
|
+
* 获取所有页面
|
|
286
|
+
* @param params 请求参数 { limit: number (max 200), offset: number }
|
|
287
|
+
* @returns 接口返回结果
|
|
288
|
+
*/
|
|
289
|
+
list: (params: {
|
|
290
|
+
limit: number;
|
|
291
|
+
offset: number;
|
|
292
|
+
}) => Promise<any>;
|
|
293
|
+
/**
|
|
294
|
+
* 获取所有页面 - 支持自动分页,获取全部数据
|
|
295
|
+
* @description 该方法会自动处理分页,直到获取所有页面数据
|
|
296
|
+
* @param params 请求参数 { limit?: number }
|
|
297
|
+
* @returns { total, items }
|
|
298
|
+
*/
|
|
299
|
+
listWithIterator: (params?: {
|
|
300
|
+
limit?: number;
|
|
301
|
+
}) => Promise<{
|
|
302
|
+
total: number;
|
|
303
|
+
items: any[];
|
|
304
|
+
}>;
|
|
305
|
+
/**
|
|
306
|
+
* 获取页面详情
|
|
307
|
+
* @param params 请求参数 { page_id: string }
|
|
308
|
+
* @returns 接口返回结果
|
|
309
|
+
*/
|
|
310
|
+
detail: (params: {
|
|
311
|
+
page_id: string;
|
|
312
|
+
}) => Promise<any>;
|
|
313
|
+
/**
|
|
314
|
+
* 获取页面访问地址
|
|
315
|
+
* @param params 请求参数 { page_id: string, pageParams?: any, parentPageParams?: any, navId?: string, tabId?: string }
|
|
316
|
+
* @returns 接口返回结果
|
|
317
|
+
*/
|
|
318
|
+
url: (params: {
|
|
319
|
+
page_id: string;
|
|
320
|
+
pageParams?: any;
|
|
321
|
+
parentPageParams?: any;
|
|
322
|
+
navId?: string;
|
|
323
|
+
tabId?: string;
|
|
324
|
+
}) => Promise<any>;
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* 附件模块
|
|
328
|
+
*/
|
|
329
|
+
attachment: {
|
|
330
|
+
/**
|
|
331
|
+
* 文件操作
|
|
332
|
+
*/
|
|
333
|
+
file: {
|
|
334
|
+
/**
|
|
335
|
+
* 上传文件
|
|
336
|
+
* @param params 请求参数 { file: any }
|
|
337
|
+
* @returns 接口返回结果
|
|
338
|
+
*/
|
|
339
|
+
upload: (params: {
|
|
340
|
+
file: any;
|
|
341
|
+
}) => Promise<any>;
|
|
342
|
+
/**
|
|
343
|
+
* 下载文件
|
|
344
|
+
* @param params 请求参数 { file_id: string }
|
|
345
|
+
* @returns 文件二进制流
|
|
346
|
+
*/
|
|
347
|
+
download: (params: {
|
|
348
|
+
file_id: string;
|
|
349
|
+
}) => Promise<any>;
|
|
350
|
+
/**
|
|
351
|
+
* 删除文件
|
|
352
|
+
* @param params 请求参数 { file_id: string }
|
|
353
|
+
* @returns 接口返回结果
|
|
354
|
+
*/
|
|
355
|
+
delete: (params: {
|
|
356
|
+
file_id: string;
|
|
357
|
+
}) => Promise<any>;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* 头像图片操作
|
|
361
|
+
*/
|
|
362
|
+
avatar: {
|
|
363
|
+
/**
|
|
364
|
+
* 上传头像图片
|
|
365
|
+
* @param params 请求参数 { image: any }
|
|
366
|
+
* @returns 接口返回结果
|
|
367
|
+
*/
|
|
368
|
+
upload: (params: {
|
|
369
|
+
image: any;
|
|
370
|
+
}) => Promise<any>;
|
|
371
|
+
/**
|
|
372
|
+
* 下载头像图片
|
|
373
|
+
* @param params 请求参数 { image_id: string }
|
|
374
|
+
* @returns 图片二进制流
|
|
375
|
+
*/
|
|
376
|
+
download: (params: {
|
|
377
|
+
image_id: string;
|
|
378
|
+
}) => Promise<any>;
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* 全局数据模块
|
|
383
|
+
*/
|
|
384
|
+
global: {
|
|
385
|
+
/**
|
|
386
|
+
* 全局选项
|
|
387
|
+
*/
|
|
388
|
+
options: {
|
|
389
|
+
/**
|
|
390
|
+
* 查询全局选项详情
|
|
391
|
+
* @param params 请求参数 { api_name: string }
|
|
392
|
+
* @returns 接口返回结果
|
|
393
|
+
*/
|
|
394
|
+
detail: (params: {
|
|
395
|
+
api_name: string;
|
|
396
|
+
}) => Promise<any>;
|
|
397
|
+
/**
|
|
398
|
+
* 查询全局选项列表
|
|
399
|
+
* @param params 请求参数 { limit: number, offset: number, filter?: { quickQuery?: string } }
|
|
400
|
+
* @returns 接口返回结果
|
|
401
|
+
*/
|
|
402
|
+
list: (params: {
|
|
403
|
+
limit: number;
|
|
404
|
+
offset: number;
|
|
405
|
+
filter?: {
|
|
406
|
+
quickQuery?: string;
|
|
407
|
+
};
|
|
408
|
+
}) => Promise<any>;
|
|
409
|
+
/**
|
|
410
|
+
* 查询所有全局选项 - 支持自动分页,获取全部数据
|
|
411
|
+
* @description 该方法会自动处理分页,直到获取所有全局选项数据
|
|
412
|
+
* @param params 请求参数 { limit?: number, filter?: { quickQuery?: string } }
|
|
413
|
+
* @returns { total, items }
|
|
414
|
+
*/
|
|
415
|
+
listWithIterator: (params?: {
|
|
416
|
+
limit?: number;
|
|
417
|
+
filter?: {
|
|
418
|
+
quickQuery?: string;
|
|
419
|
+
};
|
|
420
|
+
}) => Promise<{
|
|
421
|
+
total: number;
|
|
422
|
+
items: any[];
|
|
423
|
+
}>;
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* 环境变量
|
|
427
|
+
*/
|
|
428
|
+
variables: {
|
|
429
|
+
/**
|
|
430
|
+
* 查询环境变量详情
|
|
431
|
+
* @param params 请求参数 { api_name: string }
|
|
432
|
+
* @returns 接口返回结果
|
|
433
|
+
*/
|
|
434
|
+
detail: (params: {
|
|
435
|
+
api_name: string;
|
|
436
|
+
}) => Promise<any>;
|
|
437
|
+
/**
|
|
438
|
+
* 查询环境变量列表
|
|
439
|
+
* @param params 请求参数 { limit: number, offset: number, filter?: { quickQuery?: string } }
|
|
440
|
+
* @returns 接口返回结果
|
|
441
|
+
*/
|
|
442
|
+
list: (params: {
|
|
443
|
+
limit: number;
|
|
444
|
+
offset: number;
|
|
445
|
+
filter?: {
|
|
446
|
+
quickQuery?: string;
|
|
447
|
+
};
|
|
448
|
+
}) => Promise<any>;
|
|
449
|
+
/**
|
|
450
|
+
* 查询所有环境变量 - 支持自动分页,获取全部数据
|
|
451
|
+
* @description 该方法会自动处理分页,直到获取所有环境变量数据
|
|
452
|
+
* @param params 请求参数 { limit?: number, filter?: { quickQuery?: string } }
|
|
453
|
+
* @returns { total, items }
|
|
454
|
+
*/
|
|
455
|
+
listWithIterator: (params?: {
|
|
456
|
+
limit?: number;
|
|
457
|
+
filter?: {
|
|
458
|
+
quickQuery?: string;
|
|
459
|
+
};
|
|
460
|
+
}) => Promise<{
|
|
461
|
+
total: number;
|
|
462
|
+
items: any[];
|
|
463
|
+
}>;
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* 自动化流程模块
|
|
468
|
+
*/
|
|
469
|
+
automation: {
|
|
470
|
+
/**
|
|
471
|
+
* V1 版本
|
|
472
|
+
*/
|
|
473
|
+
v1: {
|
|
474
|
+
/**
|
|
475
|
+
* 执行流程
|
|
476
|
+
* @param params 请求参数 { flow_api_name: string, operator: { _id: number, email: string }, params: any }
|
|
477
|
+
* @returns 接口返回结果
|
|
478
|
+
*/
|
|
479
|
+
execute: (params: {
|
|
480
|
+
flow_api_name: string;
|
|
481
|
+
operator: {
|
|
482
|
+
_id: number;
|
|
483
|
+
email: string;
|
|
484
|
+
};
|
|
485
|
+
params: any;
|
|
486
|
+
}) => Promise<any>;
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* V2 版本
|
|
490
|
+
*/
|
|
491
|
+
v2: {
|
|
492
|
+
/**
|
|
493
|
+
* 执行流程
|
|
494
|
+
* @param params 请求参数 { flow_api_name: string, operator: { _id: number, email: string }, params: any, is_resubmit?: boolean, pre_instance_id?: string }
|
|
495
|
+
* @returns 接口返回结果
|
|
496
|
+
*/
|
|
497
|
+
execute: (params: {
|
|
498
|
+
flow_api_name: string;
|
|
499
|
+
operator: {
|
|
500
|
+
_id: number;
|
|
501
|
+
email: string;
|
|
502
|
+
};
|
|
503
|
+
params: any;
|
|
504
|
+
is_resubmit?: boolean;
|
|
505
|
+
pre_instance_id?: string;
|
|
506
|
+
}) => Promise<any>;
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
export declare const apaas: {
|
|
511
|
+
Client: typeof Client;
|
|
512
|
+
};
|
|
513
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,64 @@ async function functionLimiter(fn, options = {}) {
|
|
|
43
43
|
return wrapped();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 判断错误是否可重试
|
|
48
|
+
*/
|
|
49
|
+
function isRetryableError(error) {
|
|
50
|
+
// 网络错误
|
|
51
|
+
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// Axios 错误
|
|
55
|
+
if (axios.isAxiosError(error)) {
|
|
56
|
+
const axiosError = error;
|
|
57
|
+
// 没有响应(网络错误)
|
|
58
|
+
if (!axiosError.response) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// 5xx 服务器错误
|
|
62
|
+
if (axiosError.response.status >= 500) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// 429 限流
|
|
66
|
+
if (axiosError.response.status === 429) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 带重试的函数执行
|
|
74
|
+
*/
|
|
75
|
+
async function executeWithRetry(fn, options = {}, logContext = '', logger) {
|
|
76
|
+
const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000, backoffMultiplier = 2 } = options;
|
|
77
|
+
let lastError;
|
|
78
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
return await fn();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
lastError = error;
|
|
84
|
+
// 最后一次尝试,直接抛出
|
|
85
|
+
if (attempt === maxRetries) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// 判断是否可重试
|
|
89
|
+
if (!isRetryableError(error)) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
// 计算延迟时间(指数退避)
|
|
93
|
+
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt), maxDelay);
|
|
94
|
+
if (logger) {
|
|
95
|
+
logger(LoggerLevel.warn, `${logContext} Attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${delay}ms...`, error instanceof Error ? error.message : String(error));
|
|
96
|
+
}
|
|
97
|
+
// 等待后重试
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 所有重试都失败
|
|
102
|
+
throw lastError;
|
|
103
|
+
}
|
|
46
104
|
/**
|
|
47
105
|
* aPaaS OpenAPI 客户端
|
|
48
106
|
*/
|
|
@@ -631,10 +689,10 @@ class Client {
|
|
|
631
689
|
/**
|
|
632
690
|
* 批量部门 ID 交换
|
|
633
691
|
* @param params 请求参数
|
|
634
|
-
* @returns
|
|
692
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
635
693
|
*/
|
|
636
694
|
batchExchange: async (params) => {
|
|
637
|
-
const { department_id_type, department_ids } = params;
|
|
695
|
+
const { department_id_type, department_ids, retryOptions } = params;
|
|
638
696
|
// department_id_type 可选值:
|
|
639
697
|
// - 'department_id' (如 "1758534140403815")
|
|
640
698
|
// - 'external_department_id' (外部平台 department_id, 无固定格式)
|
|
@@ -646,37 +704,177 @@ class Client {
|
|
|
646
704
|
}
|
|
647
705
|
if (department_ids.length === 0) {
|
|
648
706
|
this.log(LoggerLevel.warn, '[department.batchExchange] Empty department_ids array provided, returning empty result');
|
|
649
|
-
return [];
|
|
707
|
+
return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
|
|
650
708
|
}
|
|
651
709
|
const url = '/api/integration/v2/feishu/getDepartments';
|
|
652
|
-
const chunkSize =
|
|
710
|
+
const chunkSize = 200; // 最大支持200个
|
|
653
711
|
const chunks = [];
|
|
654
712
|
for (let i = 0; i < department_ids.length; i += chunkSize) {
|
|
655
713
|
chunks.push(department_ids.slice(i, i + chunkSize));
|
|
656
714
|
}
|
|
657
715
|
this.log(LoggerLevel.info, `[department.batchExchange] Chunking ${department_ids.length} department IDs into ${chunks.length} groups of ${chunkSize}`);
|
|
658
|
-
const
|
|
716
|
+
const successResults = [];
|
|
717
|
+
const failedResults = [];
|
|
659
718
|
for (const [index, chunk] of chunks.entries()) {
|
|
660
719
|
this.log(LoggerLevel.info, `[department.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
|
|
661
|
-
|
|
662
|
-
await
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
720
|
+
try {
|
|
721
|
+
const res = await executeWithRetry(async () => {
|
|
722
|
+
return await functionLimiter(async () => {
|
|
723
|
+
await this.ensureTokenValid();
|
|
724
|
+
const response = await this.axiosInstance.post(url, {
|
|
725
|
+
department_id_type,
|
|
726
|
+
department_ids: chunk
|
|
727
|
+
}, {
|
|
728
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
729
|
+
timeout: 30000 // 30秒超时
|
|
730
|
+
});
|
|
731
|
+
this.log(LoggerLevel.debug, `[department.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
|
|
732
|
+
this.log(LoggerLevel.trace, `[department.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
|
|
733
|
+
if (response.data.code !== '0') {
|
|
734
|
+
this.log(LoggerLevel.error, `[department.batchExchange] Error exchanging departments: code=${response.data.code}, msg=${response.data.msg}`);
|
|
735
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
736
|
+
}
|
|
737
|
+
return response.data.data || [];
|
|
738
|
+
});
|
|
739
|
+
}, retryOptions, `[department.batchExchange] Chunk ${index + 1}/${chunks.length}`, this.log.bind(this));
|
|
740
|
+
successResults.push(...res);
|
|
741
|
+
this.log(LoggerLevel.info, `[department.batchExchange] Chunk ${index + 1} succeeded: ${res.length} departments`);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
// 部分失败:记录失败的chunk
|
|
745
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
746
|
+
this.log(LoggerLevel.error, `[department.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
|
|
747
|
+
chunk.forEach(id => {
|
|
748
|
+
failedResults.push({
|
|
749
|
+
id,
|
|
750
|
+
error: errorMsg
|
|
751
|
+
});
|
|
668
752
|
});
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const result = {
|
|
756
|
+
success: successResults,
|
|
757
|
+
failed: failedResults,
|
|
758
|
+
successCount: successResults.length,
|
|
759
|
+
failedCount: failedResults.length,
|
|
760
|
+
total: department_ids.length
|
|
761
|
+
};
|
|
762
|
+
this.log(LoggerLevel.info, `[department.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
/**
|
|
767
|
+
* 用户 ID 交换模块
|
|
768
|
+
*/
|
|
769
|
+
this.user = {
|
|
770
|
+
/**
|
|
771
|
+
* 单个用户 ID 交换
|
|
772
|
+
* @param params 请求参数
|
|
773
|
+
* @returns 单个用户映射结果
|
|
774
|
+
*/
|
|
775
|
+
exchange: async (params) => {
|
|
776
|
+
const { user_id_type, user_id, feishu_app_id } = params;
|
|
777
|
+
// user_id_type 可选值:
|
|
778
|
+
// - 'user_id' (如 "1758534140403815")
|
|
779
|
+
// - 'external_user_id' (外部平台 user_id, 无固定格式)
|
|
780
|
+
// - 'external_open_id' (以 'ou_' 开头的 open_id)
|
|
781
|
+
const url = '/api/integration/v2/feishu/getUsers';
|
|
782
|
+
this.log(LoggerLevel.info, `[user.exchange] Exchanging user ID: ${user_id}`);
|
|
783
|
+
const res = await functionLimiter(async () => {
|
|
784
|
+
await this.ensureTokenValid();
|
|
785
|
+
const response = await this.axiosInstance.post(url, {
|
|
786
|
+
user_id_type,
|
|
787
|
+
feishu_app_id,
|
|
788
|
+
user_ids: [user_id]
|
|
789
|
+
}, {
|
|
790
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
676
791
|
});
|
|
677
|
-
|
|
792
|
+
this.log(LoggerLevel.debug, `[user.exchange] User ID exchanged: ${user_id}, code=${response.data.code}`);
|
|
793
|
+
this.log(LoggerLevel.trace, `[user.exchange] Response: ${JSON.stringify(response.data)}`);
|
|
794
|
+
if (response.data.code !== '0') {
|
|
795
|
+
this.log(LoggerLevel.error, `[user.exchange] Error exchanging user: code=${response.data.code}, msg=${response.data.msg}`);
|
|
796
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
797
|
+
}
|
|
798
|
+
return response.data.data && response.data.data[0]; // 返回第一个元素
|
|
799
|
+
});
|
|
800
|
+
return res;
|
|
801
|
+
},
|
|
802
|
+
/**
|
|
803
|
+
* 批量用户 ID 交换
|
|
804
|
+
* @param params 请求参数
|
|
805
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
806
|
+
*/
|
|
807
|
+
batchExchange: async (params) => {
|
|
808
|
+
const { user_id_type, user_ids, feishu_app_id, retryOptions } = params;
|
|
809
|
+
// user_id_type 可选值:
|
|
810
|
+
// - 'user_id' (如 "1758534140403815")
|
|
811
|
+
// - 'external_user_id' (外部平台 user_id, 无固定格式)
|
|
812
|
+
// - 'external_open_id' (以 'ou_' 开头的 open_id)
|
|
813
|
+
// 参数校验
|
|
814
|
+
if (!user_ids || !Array.isArray(user_ids)) {
|
|
815
|
+
this.log(LoggerLevel.error, '[user.batchExchange] Invalid user_ids parameter: must be a non-empty array');
|
|
816
|
+
throw new Error('参数 user_ids 必须是一个数组');
|
|
817
|
+
}
|
|
818
|
+
if (user_ids.length === 0) {
|
|
819
|
+
this.log(LoggerLevel.warn, '[user.batchExchange] Empty user_ids array provided, returning empty result');
|
|
820
|
+
return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
|
|
821
|
+
}
|
|
822
|
+
const url = '/api/integration/v2/feishu/getUsers';
|
|
823
|
+
const chunkSize = 200; // 最大支持200个
|
|
824
|
+
const chunks = [];
|
|
825
|
+
for (let i = 0; i < user_ids.length; i += chunkSize) {
|
|
826
|
+
chunks.push(user_ids.slice(i, i + chunkSize));
|
|
827
|
+
}
|
|
828
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Chunking ${user_ids.length} user IDs into ${chunks.length} groups of ${chunkSize}`);
|
|
829
|
+
const successResults = [];
|
|
830
|
+
const failedResults = [];
|
|
831
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
832
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
|
|
833
|
+
try {
|
|
834
|
+
const res = await executeWithRetry(async () => {
|
|
835
|
+
return await functionLimiter(async () => {
|
|
836
|
+
await this.ensureTokenValid();
|
|
837
|
+
const response = await this.axiosInstance.post(url, {
|
|
838
|
+
user_id_type,
|
|
839
|
+
feishu_app_id,
|
|
840
|
+
user_ids: chunk
|
|
841
|
+
}, {
|
|
842
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
843
|
+
timeout: 30000 // 30秒超时
|
|
844
|
+
});
|
|
845
|
+
this.log(LoggerLevel.debug, `[user.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
|
|
846
|
+
this.log(LoggerLevel.trace, `[user.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
|
|
847
|
+
if (response.data.code !== '0') {
|
|
848
|
+
this.log(LoggerLevel.error, `[user.batchExchange] Error exchanging users: code=${response.data.code}, msg=${response.data.msg}`);
|
|
849
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
850
|
+
}
|
|
851
|
+
return response.data.data || [];
|
|
852
|
+
});
|
|
853
|
+
}, retryOptions, `[user.batchExchange] Chunk ${index + 1}/${chunks.length}`, this.log.bind(this));
|
|
854
|
+
successResults.push(...res);
|
|
855
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Chunk ${index + 1} succeeded: ${res.length} users`);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
// 部分失败:记录失败的chunk
|
|
859
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
860
|
+
this.log(LoggerLevel.error, `[user.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
|
|
861
|
+
chunk.forEach(id => {
|
|
862
|
+
failedResults.push({
|
|
863
|
+
id,
|
|
864
|
+
error: errorMsg
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
}
|
|
678
868
|
}
|
|
679
|
-
|
|
869
|
+
const result = {
|
|
870
|
+
success: successResults,
|
|
871
|
+
failed: failedResults,
|
|
872
|
+
successCount: successResults.length,
|
|
873
|
+
failedCount: failedResults.length,
|
|
874
|
+
total: user_ids.length
|
|
875
|
+
};
|
|
876
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
|
|
877
|
+
return result;
|
|
680
878
|
}
|
|
681
879
|
};
|
|
682
880
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Bottleneck from 'bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* 默认 apaas 限流配置
|
|
4
|
+
*/
|
|
5
|
+
export declare const apaasLimiterOptions: {
|
|
6
|
+
minTime: number;
|
|
7
|
+
reservoir: number;
|
|
8
|
+
reservoirRefreshAmount: number;
|
|
9
|
+
reservoirRefreshInterval: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 创建限流器
|
|
13
|
+
* @param fn 被限流函数
|
|
14
|
+
* @param options 自定义限流配置
|
|
15
|
+
* @returns 包装后的限流函数
|
|
16
|
+
*/
|
|
17
|
+
export declare function functionLimiter<T>(fn: () => Promise<T>, options?: Partial<Bottleneck.ConstructorOptions>): Promise<T>;
|
package/dist/logger.d.ts
ADDED
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
import { LoggerLevel } from './logger';
|
|
2
|
+
/**
|
|
3
|
+
* 批量操作结果
|
|
4
|
+
*/
|
|
5
|
+
interface BatchResult<T> {
|
|
6
|
+
/** 成功的项 */
|
|
7
|
+
success: T[];
|
|
8
|
+
/** 失败的项 */
|
|
9
|
+
failed: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
error: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** 成功数量 */
|
|
14
|
+
successCount: number;
|
|
15
|
+
/** 失败数量 */
|
|
16
|
+
failedCount: number;
|
|
17
|
+
/** 总数 */
|
|
18
|
+
total: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 重试配置
|
|
22
|
+
*/
|
|
23
|
+
interface RetryOptions {
|
|
24
|
+
/** 最大重试次数 */
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
/** 初始延迟时间(ms) */
|
|
27
|
+
initialDelay?: number;
|
|
28
|
+
/** 最大延迟时间(ms) */
|
|
29
|
+
maxDelay?: number;
|
|
30
|
+
/** 延迟倍数 */
|
|
31
|
+
backoffMultiplier?: number;
|
|
32
|
+
}
|
|
2
33
|
/**
|
|
3
34
|
* Client 初始化配置
|
|
4
35
|
*/
|
|
@@ -295,12 +326,39 @@ declare class Client {
|
|
|
295
326
|
/**
|
|
296
327
|
* 批量部门 ID 交换
|
|
297
328
|
* @param params 请求参数
|
|
298
|
-
* @returns
|
|
329
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
299
330
|
*/
|
|
300
331
|
batchExchange: (params: {
|
|
301
332
|
department_id_type: "department_id" | "external_department_id" | "external_open_department_id";
|
|
302
333
|
department_ids: string[];
|
|
303
|
-
|
|
334
|
+
retryOptions?: RetryOptions;
|
|
335
|
+
}) => Promise<BatchResult<any>>;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* 用户 ID 交换模块
|
|
339
|
+
*/
|
|
340
|
+
user: {
|
|
341
|
+
/**
|
|
342
|
+
* 单个用户 ID 交换
|
|
343
|
+
* @param params 请求参数
|
|
344
|
+
* @returns 单个用户映射结果
|
|
345
|
+
*/
|
|
346
|
+
exchange: (params: {
|
|
347
|
+
user_id_type: "user_id" | "external_user_id" | "external_open_id";
|
|
348
|
+
user_id: string;
|
|
349
|
+
feishu_app_id: string;
|
|
350
|
+
}) => Promise<any>;
|
|
351
|
+
/**
|
|
352
|
+
* 批量用户 ID 交换
|
|
353
|
+
* @param params 请求参数
|
|
354
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
355
|
+
*/
|
|
356
|
+
batchExchange: (params: {
|
|
357
|
+
user_id_type: "user_id" | "external_user_id" | "external_open_id";
|
|
358
|
+
user_ids: string[];
|
|
359
|
+
feishu_app_id: string;
|
|
360
|
+
retryOptions?: RetryOptions;
|
|
361
|
+
}) => Promise<BatchResult<any>>;
|
|
304
362
|
};
|
|
305
363
|
/**
|
|
306
364
|
* 云函数模块
|
|
@@ -549,4 +607,4 @@ declare class Client {
|
|
|
549
607
|
export declare const apaas: {
|
|
550
608
|
Client: typeof Client;
|
|
551
609
|
};
|
|
552
|
-
export {};
|
|
610
|
+
export type { BatchResult, RetryOptions };
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,8 +1,121 @@
|
|
|
1
1
|
import dayjs from 'dayjs';
|
|
2
|
-
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
3
3
|
import { LoggerLevel } from './logger';
|
|
4
4
|
import { functionLimiter } from './limiter';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* 批量操作结果
|
|
8
|
+
*/
|
|
9
|
+
interface BatchResult<T> {
|
|
10
|
+
/** 成功的项 */
|
|
11
|
+
success: T[];
|
|
12
|
+
/** 失败的项 */
|
|
13
|
+
failed: Array<{ id: string; error: string }>;
|
|
14
|
+
/** 成功数量 */
|
|
15
|
+
successCount: number;
|
|
16
|
+
/** 失败数量 */
|
|
17
|
+
failedCount: number;
|
|
18
|
+
/** 总数 */
|
|
19
|
+
total: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 重试配置
|
|
24
|
+
*/
|
|
25
|
+
interface RetryOptions {
|
|
26
|
+
/** 最大重试次数 */
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
/** 初始延迟时间(ms) */
|
|
29
|
+
initialDelay?: number;
|
|
30
|
+
/** 最大延迟时间(ms) */
|
|
31
|
+
maxDelay?: number;
|
|
32
|
+
/** 延迟倍数 */
|
|
33
|
+
backoffMultiplier?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 判断错误是否可重试
|
|
38
|
+
*/
|
|
39
|
+
function isRetryableError(error: any): boolean {
|
|
40
|
+
// 网络错误
|
|
41
|
+
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Axios 错误
|
|
46
|
+
if (axios.isAxiosError(error)) {
|
|
47
|
+
const axiosError = error as AxiosError;
|
|
48
|
+
// 没有响应(网络错误)
|
|
49
|
+
if (!axiosError.response) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// 5xx 服务器错误
|
|
53
|
+
if (axiosError.response.status >= 500) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// 429 限流
|
|
57
|
+
if (axiosError.response.status === 429) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 带重试的函数执行
|
|
67
|
+
*/
|
|
68
|
+
async function executeWithRetry<T>(
|
|
69
|
+
fn: () => Promise<T>,
|
|
70
|
+
options: RetryOptions = {},
|
|
71
|
+
logContext: string = '',
|
|
72
|
+
logger?: (level: LoggerLevel, ...args: any[]) => void
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
const {
|
|
75
|
+
maxRetries = 3,
|
|
76
|
+
initialDelay = 1000,
|
|
77
|
+
maxDelay = 10000,
|
|
78
|
+
backoffMultiplier = 2
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
let lastError: any;
|
|
82
|
+
|
|
83
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
return await fn();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
lastError = error;
|
|
88
|
+
|
|
89
|
+
// 最后一次尝试,直接抛出
|
|
90
|
+
if (attempt === maxRetries) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 判断是否可重试
|
|
95
|
+
if (!isRetryableError(error)) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 计算延迟时间(指数退避)
|
|
100
|
+
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt), maxDelay);
|
|
101
|
+
|
|
102
|
+
if (logger) {
|
|
103
|
+
logger(
|
|
104
|
+
LoggerLevel.warn,
|
|
105
|
+
`${logContext} Attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${delay}ms...`,
|
|
106
|
+
error instanceof Error ? error.message : String(error)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 等待后重试
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 所有重试都失败
|
|
116
|
+
throw lastError;
|
|
117
|
+
}
|
|
118
|
+
|
|
6
119
|
/**
|
|
7
120
|
* Client 初始化配置
|
|
8
121
|
*/
|
|
@@ -894,10 +1007,14 @@ class Client {
|
|
|
894
1007
|
/**
|
|
895
1008
|
* 批量部门 ID 交换
|
|
896
1009
|
* @param params 请求参数
|
|
897
|
-
* @returns
|
|
1010
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
898
1011
|
*/
|
|
899
|
-
batchExchange: async (params: {
|
|
900
|
-
|
|
1012
|
+
batchExchange: async (params: {
|
|
1013
|
+
department_id_type: 'department_id' | 'external_department_id' | 'external_open_department_id';
|
|
1014
|
+
department_ids: string[];
|
|
1015
|
+
retryOptions?: RetryOptions;
|
|
1016
|
+
}): Promise<BatchResult<any>> => {
|
|
1017
|
+
const { department_id_type, department_ids, retryOptions } = params;
|
|
901
1018
|
// department_id_type 可选值:
|
|
902
1019
|
// - 'department_id' (如 "1758534140403815")
|
|
903
1020
|
// - 'external_department_id' (外部平台 department_id, 无固定格式)
|
|
@@ -908,15 +1025,15 @@ class Client {
|
|
|
908
1025
|
this.log(LoggerLevel.error, '[department.batchExchange] Invalid department_ids parameter: must be a non-empty array');
|
|
909
1026
|
throw new Error('参数 department_ids 必须是一个数组');
|
|
910
1027
|
}
|
|
911
|
-
|
|
1028
|
+
|
|
912
1029
|
if (department_ids.length === 0) {
|
|
913
1030
|
this.log(LoggerLevel.warn, '[department.batchExchange] Empty department_ids array provided, returning empty result');
|
|
914
|
-
return [];
|
|
1031
|
+
return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
|
|
915
1032
|
}
|
|
916
1033
|
|
|
917
1034
|
const url = '/api/integration/v2/feishu/getDepartments';
|
|
918
1035
|
|
|
919
|
-
const chunkSize =
|
|
1036
|
+
const chunkSize = 200; // 最大支持200个
|
|
920
1037
|
const chunks: string[][] = [];
|
|
921
1038
|
for (let i = 0; i < department_ids.length; i += chunkSize) {
|
|
922
1039
|
chunks.push(department_ids.slice(i, i + chunkSize));
|
|
@@ -924,39 +1041,231 @@ class Client {
|
|
|
924
1041
|
|
|
925
1042
|
this.log(LoggerLevel.info, `[department.batchExchange] Chunking ${department_ids.length} department IDs into ${chunks.length} groups of ${chunkSize}`);
|
|
926
1043
|
|
|
927
|
-
const
|
|
1044
|
+
const successResults: any[] = [];
|
|
1045
|
+
const failedResults: Array<{ id: string; error: string }> = [];
|
|
1046
|
+
|
|
928
1047
|
for (const [index, chunk] of chunks.entries()) {
|
|
929
1048
|
this.log(LoggerLevel.info, `[department.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
|
|
930
1049
|
|
|
931
|
-
|
|
932
|
-
await
|
|
1050
|
+
try {
|
|
1051
|
+
const res = await executeWithRetry(
|
|
1052
|
+
async () => {
|
|
1053
|
+
return await functionLimiter(async () => {
|
|
1054
|
+
await this.ensureTokenValid();
|
|
1055
|
+
|
|
1056
|
+
const response = await this.axiosInstance.post(
|
|
1057
|
+
url,
|
|
1058
|
+
{
|
|
1059
|
+
department_id_type,
|
|
1060
|
+
department_ids: chunk
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
1064
|
+
timeout: 30000 // 30秒超时
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
this.log(LoggerLevel.debug, `[department.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
|
|
1069
|
+
this.log(LoggerLevel.trace, `[department.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
|
|
1070
|
+
|
|
1071
|
+
if (response.data.code !== '0') {
|
|
1072
|
+
this.log(LoggerLevel.error, `[department.batchExchange] Error exchanging departments: code=${response.data.code}, msg=${response.data.msg}`);
|
|
1073
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
1074
|
+
}
|
|
933
1075
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
{
|
|
937
|
-
department_id_type,
|
|
938
|
-
department_ids: chunk
|
|
1076
|
+
return response.data.data || [];
|
|
1077
|
+
});
|
|
939
1078
|
},
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1079
|
+
retryOptions,
|
|
1080
|
+
`[department.batchExchange] Chunk ${index + 1}/${chunks.length}`,
|
|
1081
|
+
this.log.bind(this)
|
|
943
1082
|
);
|
|
944
1083
|
|
|
945
|
-
|
|
946
|
-
this.log(LoggerLevel.
|
|
1084
|
+
successResults.push(...res);
|
|
1085
|
+
this.log(LoggerLevel.info, `[department.batchExchange] Chunk ${index + 1} succeeded: ${res.length} departments`);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
// 部分失败:记录失败的chunk
|
|
1088
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1089
|
+
this.log(LoggerLevel.error, `[department.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
|
|
1090
|
+
|
|
1091
|
+
chunk.forEach(id => {
|
|
1092
|
+
failedResults.push({
|
|
1093
|
+
id,
|
|
1094
|
+
error: errorMsg
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const result: BatchResult<any> = {
|
|
1101
|
+
success: successResults,
|
|
1102
|
+
failed: failedResults,
|
|
1103
|
+
successCount: successResults.length,
|
|
1104
|
+
failedCount: failedResults.length,
|
|
1105
|
+
total: department_ids.length
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
this.log(LoggerLevel.info, `[department.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
|
|
1109
|
+
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* 用户 ID 交换模块
|
|
1116
|
+
*/
|
|
1117
|
+
public user = {
|
|
1118
|
+
/**
|
|
1119
|
+
* 单个用户 ID 交换
|
|
1120
|
+
* @param params 请求参数
|
|
1121
|
+
* @returns 单个用户映射结果
|
|
1122
|
+
*/
|
|
1123
|
+
exchange: async (params: { user_id_type: 'user_id' | 'external_user_id' | 'external_open_id'; user_id: string; feishu_app_id: string }): Promise<any> => {
|
|
1124
|
+
const { user_id_type, user_id, feishu_app_id } = params;
|
|
1125
|
+
// user_id_type 可选值:
|
|
1126
|
+
// - 'user_id' (如 "1758534140403815")
|
|
1127
|
+
// - 'external_user_id' (外部平台 user_id, 无固定格式)
|
|
1128
|
+
// - 'external_open_id' (以 'ou_' 开头的 open_id)
|
|
1129
|
+
|
|
1130
|
+
const url = '/api/integration/v2/feishu/getUsers';
|
|
1131
|
+
|
|
1132
|
+
this.log(LoggerLevel.info, `[user.exchange] Exchanging user ID: ${user_id}`);
|
|
947
1133
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1134
|
+
const res = await functionLimiter(async () => {
|
|
1135
|
+
await this.ensureTokenValid();
|
|
1136
|
+
|
|
1137
|
+
const response = await this.axiosInstance.post(
|
|
1138
|
+
url,
|
|
1139
|
+
{
|
|
1140
|
+
user_id_type,
|
|
1141
|
+
feishu_app_id,
|
|
1142
|
+
user_ids: [user_id]
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
951
1146
|
}
|
|
1147
|
+
);
|
|
952
1148
|
|
|
953
|
-
|
|
954
|
-
});
|
|
1149
|
+
this.log(LoggerLevel.debug, `[user.exchange] User ID exchanged: ${user_id}, code=${response.data.code}`);
|
|
1150
|
+
this.log(LoggerLevel.trace, `[user.exchange] Response: ${JSON.stringify(response.data)}`);
|
|
1151
|
+
|
|
1152
|
+
if (response.data.code !== '0') {
|
|
1153
|
+
this.log(LoggerLevel.error, `[user.exchange] Error exchanging user: code=${response.data.code}, msg=${response.data.msg}`);
|
|
1154
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return response.data.data && response.data.data[0]; // 返回第一个元素
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
return res;
|
|
1161
|
+
},
|
|
955
1162
|
|
|
956
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* 批量用户 ID 交换
|
|
1165
|
+
* @param params 请求参数
|
|
1166
|
+
* @returns 批量操作结果,包含成功和失败的详细信息
|
|
1167
|
+
*/
|
|
1168
|
+
batchExchange: async (params: {
|
|
1169
|
+
user_id_type: 'user_id' | 'external_user_id' | 'external_open_id';
|
|
1170
|
+
user_ids: string[];
|
|
1171
|
+
feishu_app_id: string;
|
|
1172
|
+
retryOptions?: RetryOptions;
|
|
1173
|
+
}): Promise<BatchResult<any>> => {
|
|
1174
|
+
const { user_id_type, user_ids, feishu_app_id, retryOptions } = params;
|
|
1175
|
+
// user_id_type 可选值:
|
|
1176
|
+
// - 'user_id' (如 "1758534140403815")
|
|
1177
|
+
// - 'external_user_id' (外部平台 user_id, 无固定格式)
|
|
1178
|
+
// - 'external_open_id' (以 'ou_' 开头的 open_id)
|
|
1179
|
+
|
|
1180
|
+
// 参数校验
|
|
1181
|
+
if (!user_ids || !Array.isArray(user_ids)) {
|
|
1182
|
+
this.log(LoggerLevel.error, '[user.batchExchange] Invalid user_ids parameter: must be a non-empty array');
|
|
1183
|
+
throw new Error('参数 user_ids 必须是一个数组');
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (user_ids.length === 0) {
|
|
1187
|
+
this.log(LoggerLevel.warn, '[user.batchExchange] Empty user_ids array provided, returning empty result');
|
|
1188
|
+
return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const url = '/api/integration/v2/feishu/getUsers';
|
|
1192
|
+
|
|
1193
|
+
const chunkSize = 200; // 最大支持200个
|
|
1194
|
+
const chunks: string[][] = [];
|
|
1195
|
+
for (let i = 0; i < user_ids.length; i += chunkSize) {
|
|
1196
|
+
chunks.push(user_ids.slice(i, i + chunkSize));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Chunking ${user_ids.length} user IDs into ${chunks.length} groups of ${chunkSize}`);
|
|
1200
|
+
|
|
1201
|
+
const successResults: any[] = [];
|
|
1202
|
+
const failedResults: Array<{ id: string; error: string }> = [];
|
|
1203
|
+
|
|
1204
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
1205
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
const res = await executeWithRetry(
|
|
1209
|
+
async () => {
|
|
1210
|
+
return await functionLimiter(async () => {
|
|
1211
|
+
await this.ensureTokenValid();
|
|
1212
|
+
|
|
1213
|
+
const response = await this.axiosInstance.post(
|
|
1214
|
+
url,
|
|
1215
|
+
{
|
|
1216
|
+
user_id_type,
|
|
1217
|
+
feishu_app_id,
|
|
1218
|
+
user_ids: chunk
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
1222
|
+
timeout: 30000 // 30秒超时
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
this.log(LoggerLevel.debug, `[user.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
|
|
1227
|
+
this.log(LoggerLevel.trace, `[user.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
|
|
1228
|
+
|
|
1229
|
+
if (response.data.code !== '0') {
|
|
1230
|
+
this.log(LoggerLevel.error, `[user.batchExchange] Error exchanging users: code=${response.data.code}, msg=${response.data.msg}`);
|
|
1231
|
+
throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return response.data.data || [];
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
retryOptions,
|
|
1238
|
+
`[user.batchExchange] Chunk ${index + 1}/${chunks.length}`,
|
|
1239
|
+
this.log.bind(this)
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
successResults.push(...res);
|
|
1243
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Chunk ${index + 1} succeeded: ${res.length} users`);
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
// 部分失败:记录失败的chunk
|
|
1246
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1247
|
+
this.log(LoggerLevel.error, `[user.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
|
|
1248
|
+
|
|
1249
|
+
chunk.forEach(id => {
|
|
1250
|
+
failedResults.push({
|
|
1251
|
+
id,
|
|
1252
|
+
error: errorMsg
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
957
1256
|
}
|
|
958
1257
|
|
|
959
|
-
|
|
1258
|
+
const result: BatchResult<any> = {
|
|
1259
|
+
success: successResults,
|
|
1260
|
+
failed: failedResults,
|
|
1261
|
+
successCount: successResults.length,
|
|
1262
|
+
failedCount: failedResults.length,
|
|
1263
|
+
total: user_ids.length
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
this.log(LoggerLevel.info, `[user.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
|
|
1267
|
+
|
|
1268
|
+
return result;
|
|
960
1269
|
}
|
|
961
1270
|
};
|
|
962
1271
|
|
|
@@ -1628,3 +1937,5 @@ class Client {
|
|
|
1628
1937
|
export const apaas = {
|
|
1629
1938
|
Client
|
|
1630
1939
|
};
|
|
1940
|
+
|
|
1941
|
+
export type { BatchResult, RetryOptions };
|