apaas-oapi-client 0.1.30 → 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 ADDED
@@ -0,0 +1,5 @@
1
+ CLIENT_APP_ID=c_fc919c38b7ab41a5ab36
2
+ CLIENT_APP_SECRET=cad5106b819a43f286fb46a51b6ea2c8
3
+
4
+ LARK_APP_ID=cli_a77cb9cd64b81013
5
+ LARK_APP_SECRET=AEq56P8ntETGxNucLHTgbdLR2kNkXRNd
@@ -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 = 100;
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 results = [];
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
- const res = await functionLimiter(async () => {
662
- await this.ensureTokenValid();
663
- const response = await this.axiosInstance.post(url, {
664
- department_id_type,
665
- department_ids: chunk
666
- }, {
667
- headers: { Authorization: `${this.accessToken}` }
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
- this.log(LoggerLevel.debug, `[department.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
670
- this.log(LoggerLevel.trace, `[department.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
671
- if (response.data.code !== '0') {
672
- this.log(LoggerLevel.error, `[department.batchExchange] Error exchanging departments: code=${response.data.code}, msg=${response.data.msg}`);
673
- throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
674
- }
675
- return response.data.data || [];
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
- results.push(...res);
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
- return results;
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>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 日志等级枚举
3
+ */
4
+ export declare enum LoggerLevel {
5
+ fatal = 0,
6
+ error = 1,
7
+ warn = 2,
8
+ info = 3,
9
+ debug = 4,
10
+ trace = 5
11
+ }
@@ -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
- }) => Promise<any[]>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apaas-oapi-client",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "main": "dist/index.js",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
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: { department_id_type: 'department_id' | 'external_department_id' | 'external_open_department_id'; department_ids: string[] }): Promise<any[]> => {
900
- const { department_id_type, department_ids } = params;
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 = 100;
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 results: any[] = [];
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
- const res = await functionLimiter(async () => {
932
- await this.ensureTokenValid();
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
- const response = await this.axiosInstance.post(
935
- url,
936
- {
937
- department_id_type,
938
- department_ids: chunk
1076
+ return response.data.data || [];
1077
+ });
939
1078
  },
940
- {
941
- headers: { Authorization: `${this.accessToken}` }
942
- }
1079
+ retryOptions,
1080
+ `[department.batchExchange] Chunk ${index + 1}/${chunks.length}`,
1081
+ this.log.bind(this)
943
1082
  );
944
1083
 
945
- this.log(LoggerLevel.debug, `[department.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
946
- this.log(LoggerLevel.trace, `[department.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
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
- if (response.data.code !== '0') {
949
- this.log(LoggerLevel.error, `[department.batchExchange] Error exchanging departments: code=${response.data.code}, msg=${response.data.msg}`);
950
- throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
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
- return response.data.data || [];
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
- results.push(...res);
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
- return results;
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 };