apaas-oapi-client 0.1.30 → 0.1.32

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/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
  */
@@ -177,11 +290,13 @@ class Client {
177
290
  public object = {
178
291
  /**
179
292
  * 列出所有对象(数据表)
180
- * @param params 请求参数 { offset, filter?, limit }
181
- * @returns 接口返回结果
293
+ * @param params 请求参数 { offset?, filter?, limit? }
294
+ * @returns 接口返回结果,包含 has_more 字段表示是否还有更多数据
182
295
  */
183
- list: async (params: { offset: number; filter?: { type?: string; quickQuery?: string }; limit: number }): Promise<any> => {
184
- const { offset, filter, limit } = params;
296
+ list: async (params?: { offset?: number; filter?: { type?: string; quickQuery?: string }; limit?: number }): Promise<any> => {
297
+ const offset = params?.offset ?? 0;
298
+ const limit = params?.limit ?? 50;
299
+ const filter = params?.filter;
185
300
  await this.ensureTokenValid();
186
301
  const url = `/api/data/v1/namespaces/${this.namespace}/meta/objects/list`;
187
302
 
@@ -198,9 +313,81 @@ class Client {
198
313
 
199
314
  this.log(LoggerLevel.debug, `[object.list] Objects list fetched successfully: code=${res.data.code}`);
200
315
  this.log(LoggerLevel.trace, `[object.list] Response: ${JSON.stringify(res.data)}`);
316
+
317
+ // 添加 has_more 字段判断是否还有更多数据
318
+ if (res.data && res.data.data) {
319
+ const total = res.data.data.total || 0;
320
+ const currentEnd = offset + limit;
321
+ res.data.has_more = currentEnd < total;
322
+ this.log(LoggerLevel.debug, `[object.list] has_more=${res.data.has_more}, total=${total}, currentEnd=${currentEnd}`);
323
+ }
324
+
201
325
  return res.data;
202
326
  },
203
327
 
328
+ /**
329
+ * 列出所有对象(数据表)- 支持自动分页查询
330
+ * @description 该方法会自动处理分页,直到没有更多数据为止
331
+ * @param params 请求参数 { filter?, limit? }
332
+ * @returns { total, items }
333
+ */
334
+ listWithIterator: async (params?: { filter?: { type?: string; quickQuery?: string }; limit?: number }): Promise<{ total: number; items: any[] }> => {
335
+ const filter = params?.filter;
336
+ const limit = params?.limit ?? 50;
337
+
338
+ let results: any[] = [];
339
+ let offset = 0;
340
+ let total = 0;
341
+ let hasMore = true;
342
+ let page = 0;
343
+ let totalPages = 0;
344
+
345
+ this.log(LoggerLevel.info, `[object.listWithIterator] Starting paginated query with limit=${limit}`);
346
+
347
+ while (hasMore) {
348
+ const res = await this.object.list({
349
+ offset,
350
+ limit,
351
+ filter
352
+ });
353
+
354
+ if (res.code !== '0') {
355
+ this.log(LoggerLevel.error, `[object.listWithIterator] Error querying objects: code=${res.code}, msg=${res.msg}`);
356
+ throw new Error(res.msg || `Query failed with code ${res.code}`);
357
+ }
358
+
359
+ page += 1;
360
+
361
+ if (res.data && Array.isArray(res.data.items)) {
362
+ results = results.concat(res.data.items);
363
+ }
364
+
365
+ if (res.data && (res.data.total !== undefined && res.data.total !== null)) {
366
+ total = res.data.total;
367
+ }
368
+
369
+ if (page === 1) {
370
+ totalPages = Math.ceil(total / limit);
371
+ this.log(LoggerLevel.info, `[object.listWithIterator] Total objects: ${total}, pages: ${totalPages}`);
372
+ }
373
+
374
+ // 判断是否还有更多数据
375
+ hasMore = res.has_more === true;
376
+ offset += limit;
377
+
378
+ const padLength = totalPages.toString().length;
379
+ const pageStr = page.toString().padStart(padLength, '0');
380
+ const totalPagesStr = totalPages.toString().padStart(padLength, '0');
381
+
382
+ this.log(LoggerLevel.info, `[object.listWithIterator] Page completed: [${pageStr}/${totalPagesStr}]`);
383
+ this.log(LoggerLevel.debug, `[object.listWithIterator] Page ${page} details: items=${res.data?.items?.length}, hasMore=${hasMore}`);
384
+ this.log(LoggerLevel.trace, `[object.listWithIterator] Page ${page} data: ${JSON.stringify(res.data?.items)}`);
385
+ }
386
+
387
+ this.log(LoggerLevel.info, `[object.listWithIterator] Completed: total=${total}, fetched=${results.length}`);
388
+ return { total, items: results };
389
+ },
390
+
204
391
  metadata: {
205
392
  /**
206
393
  * 获取指定对象下指定字段的元数据
@@ -366,6 +553,52 @@ class Client {
366
553
  }
367
554
 
368
555
  return { total, items: results };
556
+ },
557
+
558
+ /**
559
+ * 统计记录数量
560
+ * @description 统计指定对象中的记录总数,支持按条件统计
561
+ * @param params 请求参数 { object_name, data? }
562
+ * @returns 接口返回结果 { code, total, msg }
563
+ */
564
+ count: async (params: { object_name: string; data?: any }): Promise<any> => {
565
+ const { object_name, data } = params;
566
+
567
+ // 默认查询参数:最小化数据传输,只获取总数
568
+ const defaultData = {
569
+ offset: 0,
570
+ page_size: 1,
571
+ need_total_count: true,
572
+ use_page_token: true,
573
+ select: ['_id'],
574
+ query_deleted_record: false
575
+ };
576
+
577
+ // 合并用户传入的参数(用户参数优先)
578
+ const queryData = data ? { ...defaultData, ...data } : defaultData;
579
+
580
+ this.log(LoggerLevel.info, `[object.search.count] Counting records in: ${object_name}`);
581
+ this.log(LoggerLevel.debug, `[object.search.count] Query data: ${JSON.stringify(queryData)}`);
582
+
583
+ const res = await this.object.search.records({
584
+ object_name,
585
+ data: queryData
586
+ });
587
+
588
+ if (res.code !== '0') {
589
+ this.log(LoggerLevel.error, `[object.search.count] Error counting records: code=${res.code}, msg=${res.msg}`);
590
+ throw new Error(res.msg || `Count failed with code ${res.code}`);
591
+ }
592
+
593
+ const total = res.data?.total || 0;
594
+ this.log(LoggerLevel.info, `[object.search.count] Total records in ${object_name}: ${total}`);
595
+
596
+ // 返回格式:{ code, total, msg }
597
+ return {
598
+ code: res.code,
599
+ total: total,
600
+ msg: res.msg
601
+ };
369
602
  }
370
603
  },
371
604
 
@@ -894,10 +1127,14 @@ class Client {
894
1127
  /**
895
1128
  * 批量部门 ID 交换
896
1129
  * @param params 请求参数
897
- * @returns 所有子请求的返回结果数组
1130
+ * @returns 批量操作结果,包含成功和失败的详细信息
898
1131
  */
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;
1132
+ batchExchange: async (params: {
1133
+ department_id_type: 'department_id' | 'external_department_id' | 'external_open_department_id';
1134
+ department_ids: string[];
1135
+ retryOptions?: RetryOptions;
1136
+ }): Promise<BatchResult<any>> => {
1137
+ const { department_id_type, department_ids, retryOptions } = params;
901
1138
  // department_id_type 可选值:
902
1139
  // - 'department_id' (如 "1758534140403815")
903
1140
  // - 'external_department_id' (外部平台 department_id, 无固定格式)
@@ -908,15 +1145,15 @@ class Client {
908
1145
  this.log(LoggerLevel.error, '[department.batchExchange] Invalid department_ids parameter: must be a non-empty array');
909
1146
  throw new Error('参数 department_ids 必须是一个数组');
910
1147
  }
911
-
1148
+
912
1149
  if (department_ids.length === 0) {
913
1150
  this.log(LoggerLevel.warn, '[department.batchExchange] Empty department_ids array provided, returning empty result');
914
- return [];
1151
+ return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
915
1152
  }
916
1153
 
917
1154
  const url = '/api/integration/v2/feishu/getDepartments';
918
1155
 
919
- const chunkSize = 100;
1156
+ const chunkSize = 200; // 最大支持200个
920
1157
  const chunks: string[][] = [];
921
1158
  for (let i = 0; i < department_ids.length; i += chunkSize) {
922
1159
  chunks.push(department_ids.slice(i, i + chunkSize));
@@ -924,39 +1161,231 @@ class Client {
924
1161
 
925
1162
  this.log(LoggerLevel.info, `[department.batchExchange] Chunking ${department_ids.length} department IDs into ${chunks.length} groups of ${chunkSize}`);
926
1163
 
927
- const results: any[] = [];
1164
+ const successResults: any[] = [];
1165
+ const failedResults: Array<{ id: string; error: string }> = [];
1166
+
928
1167
  for (const [index, chunk] of chunks.entries()) {
929
1168
  this.log(LoggerLevel.info, `[department.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
930
1169
 
931
- const res = await functionLimiter(async () => {
932
- await this.ensureTokenValid();
1170
+ try {
1171
+ const res = await executeWithRetry(
1172
+ async () => {
1173
+ return await functionLimiter(async () => {
1174
+ await this.ensureTokenValid();
1175
+
1176
+ const response = await this.axiosInstance.post(
1177
+ url,
1178
+ {
1179
+ department_id_type,
1180
+ department_ids: chunk
1181
+ },
1182
+ {
1183
+ headers: { Authorization: `${this.accessToken}` },
1184
+ timeout: 30000 // 30秒超时
1185
+ }
1186
+ );
1187
+
1188
+ this.log(LoggerLevel.debug, `[department.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
1189
+ this.log(LoggerLevel.trace, `[department.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
1190
+
1191
+ if (response.data.code !== '0') {
1192
+ this.log(LoggerLevel.error, `[department.batchExchange] Error exchanging departments: code=${response.data.code}, msg=${response.data.msg}`);
1193
+ throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
1194
+ }
933
1195
 
934
- const response = await this.axiosInstance.post(
935
- url,
936
- {
937
- department_id_type,
938
- department_ids: chunk
1196
+ return response.data.data || [];
1197
+ });
939
1198
  },
940
- {
941
- headers: { Authorization: `${this.accessToken}` }
942
- }
1199
+ retryOptions,
1200
+ `[department.batchExchange] Chunk ${index + 1}/${chunks.length}`,
1201
+ this.log.bind(this)
943
1202
  );
944
1203
 
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)}`);
1204
+ successResults.push(...res);
1205
+ this.log(LoggerLevel.info, `[department.batchExchange] Chunk ${index + 1} succeeded: ${res.length} departments`);
1206
+ } catch (error) {
1207
+ // 部分失败:记录失败的chunk
1208
+ const errorMsg = error instanceof Error ? error.message : String(error);
1209
+ this.log(LoggerLevel.error, `[department.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
1210
+
1211
+ chunk.forEach(id => {
1212
+ failedResults.push({
1213
+ id,
1214
+ error: errorMsg
1215
+ });
1216
+ });
1217
+ }
1218
+ }
1219
+
1220
+ const result: BatchResult<any> = {
1221
+ success: successResults,
1222
+ failed: failedResults,
1223
+ successCount: successResults.length,
1224
+ failedCount: failedResults.length,
1225
+ total: department_ids.length
1226
+ };
1227
+
1228
+ this.log(LoggerLevel.info, `[department.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
947
1229
 
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}`);
1230
+ return result;
1231
+ }
1232
+ };
1233
+
1234
+ /**
1235
+ * 用户 ID 交换模块
1236
+ */
1237
+ public user = {
1238
+ /**
1239
+ * 单个用户 ID 交换
1240
+ * @param params 请求参数
1241
+ * @returns 单个用户映射结果
1242
+ */
1243
+ exchange: async (params: { user_id_type: 'user_id' | 'external_user_id' | 'external_open_id'; user_id: string; feishu_app_id: string }): Promise<any> => {
1244
+ const { user_id_type, user_id, feishu_app_id } = params;
1245
+ // user_id_type 可选值:
1246
+ // - 'user_id' (如 "1758534140403815")
1247
+ // - 'external_user_id' (外部平台 user_id, 无固定格式)
1248
+ // - 'external_open_id' (以 'ou_' 开头的 open_id)
1249
+
1250
+ const url = '/api/integration/v2/feishu/getUsers';
1251
+
1252
+ this.log(LoggerLevel.info, `[user.exchange] Exchanging user ID: ${user_id}`);
1253
+
1254
+ const res = await functionLimiter(async () => {
1255
+ await this.ensureTokenValid();
1256
+
1257
+ const response = await this.axiosInstance.post(
1258
+ url,
1259
+ {
1260
+ user_id_type,
1261
+ feishu_app_id,
1262
+ user_ids: [user_id]
1263
+ },
1264
+ {
1265
+ headers: { Authorization: `${this.accessToken}` }
951
1266
  }
1267
+ );
952
1268
 
953
- return response.data.data || [];
954
- });
1269
+ this.log(LoggerLevel.debug, `[user.exchange] User ID exchanged: ${user_id}, code=${response.data.code}`);
1270
+ this.log(LoggerLevel.trace, `[user.exchange] Response: ${JSON.stringify(response.data)}`);
1271
+
1272
+ if (response.data.code !== '0') {
1273
+ this.log(LoggerLevel.error, `[user.exchange] Error exchanging user: code=${response.data.code}, msg=${response.data.msg}`);
1274
+ throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
1275
+ }
1276
+
1277
+ return response.data.data && response.data.data[0]; // 返回第一个元素
1278
+ });
1279
+
1280
+ return res;
1281
+ },
1282
+
1283
+ /**
1284
+ * 批量用户 ID 交换
1285
+ * @param params 请求参数
1286
+ * @returns 批量操作结果,包含成功和失败的详细信息
1287
+ */
1288
+ batchExchange: async (params: {
1289
+ user_id_type: 'user_id' | 'external_user_id' | 'external_open_id';
1290
+ user_ids: string[];
1291
+ feishu_app_id: string;
1292
+ retryOptions?: RetryOptions;
1293
+ }): Promise<BatchResult<any>> => {
1294
+ const { user_id_type, user_ids, feishu_app_id, retryOptions } = params;
1295
+ // user_id_type 可选值:
1296
+ // - 'user_id' (如 "1758534140403815")
1297
+ // - 'external_user_id' (外部平台 user_id, 无固定格式)
1298
+ // - 'external_open_id' (以 'ou_' 开头的 open_id)
1299
+
1300
+ // 参数校验
1301
+ if (!user_ids || !Array.isArray(user_ids)) {
1302
+ this.log(LoggerLevel.error, '[user.batchExchange] Invalid user_ids parameter: must be a non-empty array');
1303
+ throw new Error('参数 user_ids 必须是一个数组');
1304
+ }
1305
+
1306
+ if (user_ids.length === 0) {
1307
+ this.log(LoggerLevel.warn, '[user.batchExchange] Empty user_ids array provided, returning empty result');
1308
+ return { success: [], failed: [], successCount: 0, failedCount: 0, total: 0 };
1309
+ }
1310
+
1311
+ const url = '/api/integration/v2/feishu/getUsers';
1312
+
1313
+ const chunkSize = 200; // 最大支持200个
1314
+ const chunks: string[][] = [];
1315
+ for (let i = 0; i < user_ids.length; i += chunkSize) {
1316
+ chunks.push(user_ids.slice(i, i + chunkSize));
1317
+ }
1318
+
1319
+ this.log(LoggerLevel.info, `[user.batchExchange] Chunking ${user_ids.length} user IDs into ${chunks.length} groups of ${chunkSize}`);
1320
+
1321
+ const successResults: any[] = [];
1322
+ const failedResults: Array<{ id: string; error: string }> = [];
1323
+
1324
+ for (const [index, chunk] of chunks.entries()) {
1325
+ this.log(LoggerLevel.info, `[user.batchExchange] Processing chunk ${index + 1}/${chunks.length}: ${chunk.length} IDs`);
1326
+
1327
+ try {
1328
+ const res = await executeWithRetry(
1329
+ async () => {
1330
+ return await functionLimiter(async () => {
1331
+ await this.ensureTokenValid();
1332
+
1333
+ const response = await this.axiosInstance.post(
1334
+ url,
1335
+ {
1336
+ user_id_type,
1337
+ feishu_app_id,
1338
+ user_ids: chunk
1339
+ },
1340
+ {
1341
+ headers: { Authorization: `${this.accessToken}` },
1342
+ timeout: 30000 // 30秒超时
1343
+ }
1344
+ );
1345
+
1346
+ this.log(LoggerLevel.debug, `[user.batchExchange] Chunk ${index + 1} completed: code=${response.data.code}`);
1347
+ this.log(LoggerLevel.trace, `[user.batchExchange] Chunk ${index + 1} response: ${JSON.stringify(response.data)}`);
1348
+
1349
+ if (response.data.code !== '0') {
1350
+ this.log(LoggerLevel.error, `[user.batchExchange] Error exchanging users: code=${response.data.code}, msg=${response.data.msg}`);
1351
+ throw new Error(response.data.msg || `Exchange failed with code ${response.data.code}`);
1352
+ }
955
1353
 
956
- results.push(...res);
1354
+ return response.data.data || [];
1355
+ });
1356
+ },
1357
+ retryOptions,
1358
+ `[user.batchExchange] Chunk ${index + 1}/${chunks.length}`,
1359
+ this.log.bind(this)
1360
+ );
1361
+
1362
+ successResults.push(...res);
1363
+ this.log(LoggerLevel.info, `[user.batchExchange] Chunk ${index + 1} succeeded: ${res.length} users`);
1364
+ } catch (error) {
1365
+ // 部分失败:记录失败的chunk
1366
+ const errorMsg = error instanceof Error ? error.message : String(error);
1367
+ this.log(LoggerLevel.error, `[user.batchExchange] Chunk ${index + 1} failed after retries: ${errorMsg}`);
1368
+
1369
+ chunk.forEach(id => {
1370
+ failedResults.push({
1371
+ id,
1372
+ error: errorMsg
1373
+ });
1374
+ });
1375
+ }
957
1376
  }
958
1377
 
959
- return results;
1378
+ const result: BatchResult<any> = {
1379
+ success: successResults,
1380
+ failed: failedResults,
1381
+ successCount: successResults.length,
1382
+ failedCount: failedResults.length,
1383
+ total: user_ids.length
1384
+ };
1385
+
1386
+ this.log(LoggerLevel.info, `[user.batchExchange] Completed: total=${result.total}, success=${result.successCount}, failed=${result.failedCount}`);
1387
+
1388
+ return result;
960
1389
  }
961
1390
  };
962
1391
 
@@ -1628,3 +2057,5 @@ class Client {
1628
2057
  export const apaas = {
1629
2058
  Client
1630
2059
  };
2060
+
2061
+ export type { BatchResult, RetryOptions };