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/.env +5 -0
- package/dist/index.d.ts +513 -0
- package/dist/index.js +322 -24
- package/dist/limiter.d.ts +17 -0
- package/dist/logger.d.ts +11 -0
- package/dist/src/index.d.ts +92 -8
- package/package.json +1 -1
- package/src/index.ts +462 -31
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
|
|
181
|
-
* @returns
|
|
293
|
+
* @param params 请求参数 { offset?, filter?, limit? }
|
|
294
|
+
* @returns 接口返回结果,包含 has_more 字段表示是否还有更多数据
|
|
182
295
|
*/
|
|
183
|
-
list: async (params
|
|
184
|
-
const
|
|
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: {
|
|
900
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
932
|
-
await
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
{
|
|
937
|
-
department_id_type,
|
|
938
|
-
department_ids: chunk
|
|
1196
|
+
return response.data.data || [];
|
|
1197
|
+
});
|
|
939
1198
|
},
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1199
|
+
retryOptions,
|
|
1200
|
+
`[department.batchExchange] Chunk ${index + 1}/${chunks.length}`,
|
|
1201
|
+
this.log.bind(this)
|
|
943
1202
|
);
|
|
944
1203
|
|
|
945
|
-
|
|
946
|
-
this.log(LoggerLevel.
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|