feishu-mcp 0.1.7 → 0.1.8

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.
@@ -1,7 +1,6 @@
1
1
  import { BaseApiService } from './baseService.js';
2
2
  import { Logger } from '../utils/logger.js';
3
3
  import { Config } from '../utils/config.js';
4
- import { CacheManager } from '../utils/cache.js';
5
4
  import { ParamUtils } from '../utils/paramUtils.js';
6
5
  import { BlockFactory, BlockType } from './blockFactory.js';
7
6
  import { AuthUtils, TokenCacheManager } from '../utils/auth/index.js';
@@ -21,12 +20,6 @@ export class FeishuApiService extends BaseApiService {
21
20
  */
22
21
  constructor() {
23
22
  super();
24
- Object.defineProperty(this, "cacheManager", {
25
- enumerable: true,
26
- configurable: true,
27
- writable: true,
28
- value: void 0
29
- });
30
23
  Object.defineProperty(this, "blockFactory", {
31
24
  enumerable: true,
32
25
  configurable: true,
@@ -45,7 +38,6 @@ export class FeishuApiService extends BaseApiService {
45
38
  writable: true,
46
39
  value: void 0
47
40
  });
48
- this.cacheManager = CacheManager.getInstance();
49
41
  this.blockFactory = BlockFactory.getInstance();
50
42
  this.config = Config.getInstance();
51
43
  this.authService = new AuthService();
@@ -186,10 +178,12 @@ export class FeishuApiService extends BaseApiService {
186
178
  "wiki:wiki",
187
179
  "wiki:wiki:readonly"
188
180
  ];
189
- const userScopes = [
190
- ...tenantScopes,
191
- 'offline_access'
181
+ // user认证特有授权
182
+ const userOnlyScopes = [
183
+ "search:docs:read",
184
+ 'offline_access',
192
185
  ];
186
+ const userScopes = [...tenantScopes, ...userOnlyScopes];
193
187
  return authType === 'tenant' ? tenantScopes : userScopes;
194
188
  }
195
189
  /**
@@ -271,7 +265,7 @@ export class FeishuApiService extends BaseApiService {
271
265
  const tokenCacheManager = TokenCacheManager.getInstance();
272
266
  // 生成应用级别的scope校验key(包含authType,因为tenant和user权限不同)
273
267
  const scopeKey = this.generateScopeKey(appId, appSecret, authType);
274
- const scopeVersion = '1.0.0'; // 当前scope版本号,可以根据需要更新
268
+ const scopeVersion = '2.0.0'; // 当前scope版本号,可以根据需要更新
275
269
  // 检查是否需要校验
276
270
  if (!tokenCacheManager.shouldValidateScope(scopeKey, scopeVersion)) {
277
271
  Logger.debug(`Scope版本已校验过,跳过校验: ${scopeKey}`);
@@ -380,16 +374,57 @@ export class FeishuApiService extends BaseApiService {
380
374
  }
381
375
  }
382
376
  /**
383
- * 获取文档信息
384
- * @param documentId 文档ID或URL
385
- * @returns 文档信息
377
+ * 获取文档信息(支持普通文档和Wiki文档)
378
+ * @param documentId 文档ID或URL(支持Wiki链接)
379
+ * @param documentType 文档类型(可选),'document' 或 'wiki',如果不指定则自动检测
380
+ * @returns 文档信息或Wiki节点信息
386
381
  */
387
- async getDocumentInfo(documentId) {
382
+ async getDocumentInfo(documentId, documentType) {
388
383
  try {
389
- const normalizedDocId = ParamUtils.processDocumentId(documentId);
390
- const endpoint = `/docx/v1/documents/${normalizedDocId}`;
391
- const response = await this.get(endpoint);
392
- return response;
384
+ let isWikiLink;
385
+ // 如果明确指定了类型,使用指定的类型
386
+ if (documentType === 'wiki') {
387
+ isWikiLink = true;
388
+ }
389
+ else if (documentType === 'document') {
390
+ isWikiLink = false;
391
+ }
392
+ else {
393
+ // 自动检测:检查是否是Wiki链接(包含 /wiki/ 路径)
394
+ isWikiLink = documentId.includes('/wiki/');
395
+ }
396
+ if (isWikiLink) {
397
+ // 处理Wiki文档
398
+ const wikiToken = ParamUtils.processWikiToken(documentId);
399
+ const endpoint = `/wiki/v2/spaces/get_node`;
400
+ const params = { token: wikiToken, obj_type: 'wiki' };
401
+ const response = await this.get(endpoint, params);
402
+ if (!response.node || !response.node.obj_token) {
403
+ throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
404
+ }
405
+ const node = response.node;
406
+ const docId = node.obj_token;
407
+ // 构建返回对象,包含完整节点信息和 documentId 字段
408
+ const result = {
409
+ ...node,
410
+ documentId: docId, // 添加 documentId 字段作为 obj_token 的别名
411
+ _type: 'wiki', // 标识这是Wiki文档
412
+ };
413
+ Logger.debug(`获取Wiki文档信息: ${wikiToken} -> documentId: ${docId}, space_id: ${node.space_id}, node_token: ${node.node_token}`);
414
+ return result;
415
+ }
416
+ else {
417
+ // 处理普通文档
418
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
419
+ const endpoint = `/docx/v1/documents/${normalizedDocId}`;
420
+ const response = await this.get(endpoint);
421
+ const result = {
422
+ ...response,
423
+ _type: 'document', // 标识这是普通文档
424
+ };
425
+ Logger.debug(`获取普通文档信息: ${normalizedDocId}`);
426
+ return result;
427
+ }
393
428
  }
394
429
  catch (error) {
395
430
  this.handleApiError(error, '获取文档信息失败');
@@ -803,33 +838,28 @@ export class FeishuApiService extends BaseApiService {
803
838
  * @param wikiUrl Wiki链接或Token
804
839
  * @returns 文档ID
805
840
  */
806
- async convertWikiToDocumentId(wikiUrl) {
807
- try {
808
- const wikiToken = ParamUtils.processWikiToken(wikiUrl);
809
- // 尝试从缓存获取
810
- const cachedDocId = this.cacheManager.getWikiToDocId(wikiToken);
811
- if (cachedDocId) {
812
- Logger.debug(`使用缓存的Wiki转换结果: ${wikiToken} -> ${cachedDocId}`);
813
- return cachedDocId;
814
- }
815
- // 获取Wiki节点信息
816
- const endpoint = `/wiki/v2/spaces/get_node`;
817
- const params = { token: wikiToken, obj_type: 'wiki' };
818
- const response = await this.get(endpoint, params);
819
- if (!response.node || !response.node.obj_token) {
820
- throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
821
- }
822
- const documentId = response.node.obj_token;
823
- // 缓存结果
824
- this.cacheManager.cacheWikiToDocId(wikiToken, documentId);
825
- Logger.debug(`Wiki转换为文档ID: ${wikiToken} -> ${documentId}`);
826
- return documentId;
827
- }
828
- catch (error) {
829
- this.handleApiError(error, 'Wiki转换为文档ID失败');
830
- return ''; // 永远不会执行到这里
831
- }
832
- }
841
+ // public async convertWikiToDocumentId(wikiUrl: string): Promise<string> {
842
+ // try {
843
+ // const wikiToken = ParamUtils.processWikiToken(wikiUrl);
844
+ //
845
+ // // 获取Wiki节点信息
846
+ // const endpoint = `/wiki/v2/spaces/get_node`;
847
+ // const params = { token: wikiToken, obj_type: 'wiki' };
848
+ // const response = await this.get(endpoint, params);
849
+ //
850
+ // if (!response.node || !response.node.obj_token) {
851
+ // throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
852
+ // }
853
+ //
854
+ // const documentId = response.node.obj_token;
855
+ //
856
+ // Logger.debug(`Wiki转换为文档ID: ${wikiToken} -> ${documentId}`);
857
+ // return documentId;
858
+ // } catch (error) {
859
+ // this.handleApiError(error, 'Wiki转换为文档ID失败');
860
+ // return ''; // 永远不会执行到这里
861
+ // }
862
+ // }
833
863
  /**
834
864
  * 获取BlockFactory实例
835
865
  * @returns BlockFactory实例
@@ -1111,6 +1141,148 @@ export class FeishuApiService extends BaseApiService {
1111
1141
  this.handleApiError(error, '获取飞书根文件夹信息失败');
1112
1142
  }
1113
1143
  }
1144
+ /**
1145
+ * 获取所有知识空间列表(遍历所有分页)
1146
+ * @param pageSize 每页数量,默认20
1147
+ * @returns 所有知识空间列表(仅包含 items 数组,不包含 has_more 和 page_token)
1148
+ */
1149
+ async getAllWikiSpacesList(pageSize = 20) {
1150
+ try {
1151
+ Logger.info(`开始获取所有知识空间列表,每页数量: ${pageSize}`);
1152
+ const endpoint = '/wiki/v2/spaces';
1153
+ let allItems = [];
1154
+ let pageToken = undefined;
1155
+ let hasMore = true;
1156
+ // 循环获取所有页的数据
1157
+ while (hasMore) {
1158
+ const params = { page_size: pageSize };
1159
+ if (pageToken) {
1160
+ params.page_token = pageToken;
1161
+ }
1162
+ Logger.debug(`请求知识空间列表,page_token: ${pageToken || 'null'}, page_size: ${pageSize}`);
1163
+ const response = await this.get(endpoint, params);
1164
+ if (response && response.items) {
1165
+ const newItems = response.items;
1166
+ allItems = [...allItems, ...newItems];
1167
+ hasMore = response.has_more || false;
1168
+ pageToken = response.page_token;
1169
+ Logger.debug(`当前页获取到 ${newItems.length} 个知识空间,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
1170
+ }
1171
+ else {
1172
+ hasMore = false;
1173
+ Logger.warn('知识空间列表响应格式异常:', JSON.stringify(response, null, 2));
1174
+ }
1175
+ }
1176
+ Logger.info(`知识空间列表获取完成,共 ${allItems.length} 个空间`);
1177
+ return allItems; // 直接返回数组,不包装在 items 中
1178
+ }
1179
+ catch (error) {
1180
+ this.handleApiError(error, '获取知识空间列表失败');
1181
+ }
1182
+ }
1183
+ /**
1184
+ * 获取所有知识空间子节点列表(遍历所有分页)
1185
+ * @param spaceId 知识空间ID
1186
+ * @param parentNodeToken 父节点Token(可选,为空时获取根节点)
1187
+ * @param pageSize 每页数量,默认20
1188
+ * @returns 所有子节点列表(仅包含 items 数组,不包含 has_more 和 page_token)
1189
+ */
1190
+ async getAllWikiSpaceNodes(spaceId, parentNodeToken, pageSize = 20) {
1191
+ try {
1192
+ Logger.info(`开始获取知识空间子节点列表,space_id: ${spaceId}, parent_node_token: ${parentNodeToken || 'null'}, 每页数量: ${pageSize}`);
1193
+ const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
1194
+ let allItems = [];
1195
+ let pageToken = undefined;
1196
+ let hasMore = true;
1197
+ // 循环获取所有页的数据
1198
+ while (hasMore) {
1199
+ const params = { page_size: pageSize };
1200
+ if (parentNodeToken) {
1201
+ params.parent_node_token = parentNodeToken;
1202
+ }
1203
+ if (pageToken) {
1204
+ params.page_token = pageToken;
1205
+ }
1206
+ Logger.debug(`请求知识空间子节点列表,page_token: ${pageToken || 'null'}, page_size: ${pageSize}`);
1207
+ const response = await this.get(endpoint, params);
1208
+ if (response && response.items) {
1209
+ const newItems = response.items;
1210
+ allItems = [...allItems, ...newItems];
1211
+ hasMore = response.has_more || false;
1212
+ pageToken = response.page_token;
1213
+ Logger.debug(`当前页获取到 ${newItems.length} 个子节点,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
1214
+ }
1215
+ else {
1216
+ hasMore = false;
1217
+ Logger.warn('知识空间子节点列表响应格式异常:', JSON.stringify(response, null, 2));
1218
+ }
1219
+ }
1220
+ Logger.info(`知识空间子节点列表获取完成,共 ${allItems.length} 个节点`);
1221
+ return allItems; // 直接返回数组,不包装在 items 中
1222
+ }
1223
+ catch (error) {
1224
+ this.handleApiError(error, '获取知识空间子节点列表失败');
1225
+ }
1226
+ }
1227
+ /**
1228
+ * 获取知识空间信息
1229
+ * @param spaceId 知识空间ID,传入 'my_library' 时获取"我的知识库"
1230
+ * @param lang 语言(仅当 spaceId 为 'my_library' 时有效),默认'en'
1231
+ * @returns 知识空间信息
1232
+ */
1233
+ async getWikiSpaceInfo(spaceId, lang = 'en') {
1234
+ try {
1235
+ const endpoint = `/wiki/v2/spaces/${spaceId}`;
1236
+ const params = {};
1237
+ // 当 spaceId 为 'my_library' 时,添加 lang 参数
1238
+ if (spaceId === 'my_library') {
1239
+ params.lang = lang;
1240
+ }
1241
+ const response = await this.get(endpoint, params);
1242
+ Logger.debug(`获取知识空间信息成功 (space_id: ${spaceId}):`, response);
1243
+ // 如果响应中包含 space 字段,直接返回 space 对象;否则返回整个响应
1244
+ if (response && response.space) {
1245
+ return response.space;
1246
+ }
1247
+ return response;
1248
+ }
1249
+ catch (error) {
1250
+ this.handleApiError(error, `获取知识空间信息失败 (space_id: ${spaceId})`);
1251
+ }
1252
+ }
1253
+ /**
1254
+ * 创建知识空间节点(知识库节点)
1255
+ * @param spaceId 知识空间ID
1256
+ * @param title 节点标题
1257
+ * @param parentNodeToken 父节点Token(可选,为空时在根节点下创建)
1258
+ * @returns 创建的节点信息,包含 node_token(节点ID)和 obj_token(文档ID)
1259
+ */
1260
+ async createWikiSpaceNode(spaceId, title, parentNodeToken) {
1261
+ try {
1262
+ Logger.info(`开始创建知识空间节点,space_id: ${spaceId}, title: ${title}, parent_node_token: ${parentNodeToken || 'null(根节点)'}`);
1263
+ const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
1264
+ const payload = {
1265
+ title,
1266
+ obj_type: 'docx',
1267
+ node_type: 'origin',
1268
+ };
1269
+ if (parentNodeToken) {
1270
+ payload.parent_node_token = parentNodeToken;
1271
+ }
1272
+ const response = await this.post(endpoint, payload);
1273
+ // 提取 node 对象,统一返回格式
1274
+ if (response && response.data && response.data.node) {
1275
+ const node = response.data.node;
1276
+ Logger.info(`知识空间节点创建成功,node_token: ${node.node_token}, obj_token: ${node.obj_token}`);
1277
+ return node;
1278
+ }
1279
+ Logger.info(`知识空间节点创建成功`);
1280
+ return response;
1281
+ }
1282
+ catch (error) {
1283
+ this.handleApiError(error, '创建知识空间节点失败');
1284
+ }
1285
+ }
1114
1286
  /**
1115
1287
  * 获取文件夹中的文件清单
1116
1288
  * @param folderToken 文件夹Token
@@ -1156,50 +1328,281 @@ export class FeishuApiService extends BaseApiService {
1156
1328
  }
1157
1329
  }
1158
1330
  /**
1159
- * 搜索飞书文档
1331
+ * 搜索飞书文档(支持分页和轮询)
1160
1332
  * @param searchKey 搜索关键字
1161
- * @param count 每页数量,默认50
1162
- * @returns 搜索结果,包含所有页的数据
1333
+ * @param maxSize 最大返回数量,如果未指定则只返回一页
1334
+ * @param offset 偏移量,用于分页,默认0
1335
+ * @returns 搜索结果,包含数据和分页信息
1163
1336
  */
1164
- async searchDocuments(searchKey, count = 50) {
1337
+ async searchDocuments(searchKey, maxSize, offset = 0) {
1165
1338
  try {
1166
- Logger.info(`开始搜索文档,关键字: ${searchKey}`);
1339
+ Logger.info(`开始搜索文档,关键字: ${searchKey}, maxSize: ${maxSize || '未指定'}, offset: ${offset}`);
1167
1340
  const endpoint = `/suite/docs-api/search/object`;
1168
- let offset = 0;
1169
- let allResults = [];
1341
+ const PAGE_SIZE = 50; // 文档API固定使用50
1342
+ const allResults = [];
1343
+ let currentOffset = offset;
1170
1344
  let hasMore = true;
1171
- // 循环获取所有页的数据
1172
- while (hasMore && offset + count < 200) {
1345
+ // 如果指定了maxSize,轮询获取直到满足maxSize或没有更多数据
1346
+ while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
1173
1347
  const payload = {
1174
1348
  search_key: searchKey,
1175
1349
  docs_types: ["doc"],
1176
- count: count,
1177
- offset: offset
1350
+ count: PAGE_SIZE,
1351
+ offset: currentOffset
1178
1352
  };
1179
- Logger.debug(`请求搜索,offset: ${offset}, count: ${count}`);
1353
+ Logger.debug(`请求搜索文档,offset: ${currentOffset}, count: ${PAGE_SIZE}`);
1180
1354
  const response = await this.post(endpoint, payload);
1181
1355
  Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
1182
1356
  if (response && response.docs_entities) {
1183
- const newDocs = response.docs_entities;
1184
- allResults = [...allResults, ...newDocs];
1185
- hasMore = response.has_more || false;
1186
- offset += count;
1187
- Logger.debug(`当前页获取到 ${newDocs.length} 条数据,累计 ${allResults.length} 条,总计 ${response.total} 条,hasMore: ${hasMore}`);
1357
+ const resultCount = response.docs_entities.length;
1358
+ const apiHasMore = response.has_more || false;
1359
+ // 更新offset
1360
+ currentOffset += resultCount;
1361
+ if (resultCount > 0) {
1362
+ allResults.push(...response.docs_entities);
1363
+ hasMore = apiHasMore; // 保持API返回的hasMore
1364
+ // 如果指定了maxSize,只取需要的数量
1365
+ if (maxSize == undefined || allResults.length >= maxSize) {
1366
+ // 如果已经达到maxSize,停止轮询,但保持API返回的hasMore值
1367
+ Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
1368
+ break; // 停止轮询
1369
+ }
1370
+ }
1371
+ else {
1372
+ hasMore = false;
1373
+ }
1374
+ Logger.debug(`文档搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
1188
1375
  }
1189
1376
  else {
1190
- hasMore = false;
1191
1377
  Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
1378
+ hasMore = false;
1192
1379
  }
1193
1380
  }
1194
1381
  const resultCount = allResults.length;
1195
- Logger.info(`文档搜索完成,找到 ${resultCount} 个结果`);
1382
+ Logger.info(`文档搜索完成,找到 ${resultCount} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
1196
1383
  return {
1197
- data: allResults
1384
+ items: allResults,
1385
+ hasMore: hasMore,
1386
+ nextOffset: currentOffset
1198
1387
  };
1199
1388
  }
1200
1389
  catch (error) {
1201
1390
  this.handleApiError(error, '搜索文档失败');
1391
+ throw error;
1392
+ }
1393
+ }
1394
+ /**
1395
+ * 搜索Wiki知识库节点(支持分页和轮询)
1396
+ * @param query 搜索关键字
1397
+ * @param maxSize 最大返回数量,如果未指定则只返回一页
1398
+ * @param pageToken 分页token,用于获取下一页,可选
1399
+ * @returns 搜索结果,包含数据和分页信息
1400
+ */
1401
+ async searchWikiNodes(query, maxSize, pageToken) {
1402
+ try {
1403
+ Logger.info(`开始搜索知识库,关键字: ${query}, maxSize: ${maxSize || '未指定'}, pageToken: ${pageToken || '无'}`);
1404
+ const endpoint = `/wiki/v1/nodes/search`;
1405
+ const PAGE_SIZE = 20; // Wiki API每页固定使用20
1406
+ const allResults = [];
1407
+ let currentPageToken = pageToken;
1408
+ let hasMore = true;
1409
+ // 如果指定了maxSize,轮询获取直到满足maxSize或没有更多数据
1410
+ while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
1411
+ const size = Math.min(PAGE_SIZE, 100); // Wiki API最大支持100
1412
+ let url = `${endpoint}?page_size=${size}`;
1413
+ if (currentPageToken) {
1414
+ url += `&page_token=${currentPageToken}`;
1415
+ }
1416
+ const payload = {
1417
+ query: query
1418
+ };
1419
+ Logger.debug(`请求搜索知识库,pageSize: ${size}, pageToken: ${currentPageToken || '无'}`);
1420
+ const response = await this.post(url, payload);
1421
+ Logger.debug('知识库搜索响应:', JSON.stringify(response, null, 2));
1422
+ // baseService的post方法已经提取了response.data.data,所以response直接就是data字段的内容
1423
+ if (response && response.items) {
1424
+ const resultCount = response.items?.length || 0;
1425
+ const apiHasMore = response.has_more || false;
1426
+ currentPageToken = response.page_token || null;
1427
+ if (resultCount > 0) {
1428
+ allResults.push(...response.items);
1429
+ hasMore = apiHasMore; // 保持API返回的hasMore,以便下次调用可以继续
1430
+ if (maxSize !== undefined) {
1431
+ // 如果已经达到maxSize,停止轮询,但保持API返回的hasMore值
1432
+ if (allResults.length >= maxSize) {
1433
+ Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
1434
+ break; // 停止轮询
1435
+ }
1436
+ }
1437
+ else {
1438
+ break; // 只返回一页
1439
+ }
1440
+ }
1441
+ else {
1442
+ hasMore = false;
1443
+ }
1444
+ Logger.debug(`知识库搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
1445
+ }
1446
+ else {
1447
+ Logger.warn('知识库搜索响应格式异常:', JSON.stringify(response, null, 2));
1448
+ hasMore = false;
1449
+ }
1450
+ }
1451
+ const resultCount = allResults.length;
1452
+ Logger.info(`知识库搜索完成,找到 ${resultCount} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
1453
+ return {
1454
+ items: allResults,
1455
+ hasMore: hasMore,
1456
+ pageToken: currentPageToken,
1457
+ count: resultCount
1458
+ };
1459
+ }
1460
+ catch (error) {
1461
+ this.handleApiError(error, '搜索知识库失败');
1462
+ throw error;
1463
+ }
1464
+ }
1465
+ /**
1466
+ * 统一搜索方法,支持文档和知识库搜索
1467
+ * @param searchKey 搜索关键字
1468
+ * @param searchType 搜索类型:'document' | 'wiki' | 'both',默认'both'
1469
+ * @param offset 文档搜索的偏移量,可选(用于分页)
1470
+ * @param pageToken 知识库搜索的分页token,可选(用于分页)
1471
+ * @returns 搜索结果,包含documents、wikis和分页信息
1472
+ */
1473
+ async search(searchKey, searchType = 'both', offset, pageToken) {
1474
+ try {
1475
+ // wiki搜索不支持tenant认证,如果是tenant模式则强制使用document搜索
1476
+ if (this.config.feishu.authType === 'tenant' && (searchType === 'wiki' || searchType === 'both')) {
1477
+ Logger.info(`租户认证模式下wiki搜索不支持,强制将searchType从 ${searchType} 修改为 document`);
1478
+ searchType = 'document';
1479
+ }
1480
+ const MAX_TOTAL_RESULTS = 100; // 总共最多200条(文档+wiki合计)
1481
+ const docOffset = offset ?? 0;
1482
+ Logger.info(`开始统一搜索,关键字: ${searchKey}, 类型: ${searchType}, offset: ${docOffset}, pageToken: ${pageToken || '无'}`);
1483
+ const documents = [];
1484
+ const wikis = [];
1485
+ // 用于生成分页指导的内部变量
1486
+ let documentOffset = docOffset;
1487
+ let wikiPageToken = null;
1488
+ let documentHasMore = false;
1489
+ let wikiHasMore = false;
1490
+ // 搜索文档
1491
+ if (searchType === 'document' || searchType === 'both') {
1492
+ // 计算文档的最大数量(不超过总限制)
1493
+ const maxDocCount = MAX_TOTAL_RESULTS;
1494
+ const docResult = await this.searchDocuments(searchKey, maxDocCount, docOffset);
1495
+ if (docResult.items && docResult.items.length > 0) {
1496
+ documents.push(...docResult.items);
1497
+ documentOffset = docResult.nextOffset;
1498
+ documentHasMore = docResult.hasMore;
1499
+ Logger.debug(`文档搜索: 获取 ${docResult.items.length} 条,新offset: ${documentOffset}, hasMore: ${documentHasMore}`);
1500
+ }
1501
+ else {
1502
+ documentHasMore = false;
1503
+ Logger.debug('文档搜索: 无结果');
1504
+ }
1505
+ }
1506
+ // 搜索知识库(仅在文档+wiki总数未达到100条时继续)
1507
+ if (searchType === 'wiki' || searchType === 'both') {
1508
+ const currentDocCount = documents.length;
1509
+ const remainingCount = MAX_TOTAL_RESULTS - currentDocCount;
1510
+ // 如果还有剩余空间,获取知识库
1511
+ if (remainingCount > 0) {
1512
+ const wikiResult = await this.searchWikiNodes(searchKey, remainingCount, pageToken);
1513
+ if (wikiResult.items && wikiResult.items.length > 0) {
1514
+ wikis.push(...wikiResult.items);
1515
+ wikiPageToken = wikiResult.pageToken;
1516
+ wikiHasMore = wikiResult.hasMore;
1517
+ Logger.debug(`知识库搜索: 获取 ${wikiResult.items.length} 条,pageToken: ${wikiPageToken || '无'}, hasMore: ${wikiHasMore}`);
1518
+ }
1519
+ else {
1520
+ wikiHasMore = false;
1521
+ Logger.debug('知识库搜索: 无结果');
1522
+ }
1523
+ }
1524
+ else {
1525
+ Logger.info(`已达到总限制 ${MAX_TOTAL_RESULTS} 条,不再获取知识库`);
1526
+ wikiHasMore = true;
1527
+ }
1528
+ }
1529
+ // 生成分页指导信息
1530
+ const paginationGuide = this.generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken);
1531
+ const total = documents.length + wikis.length;
1532
+ const hasMore = documentHasMore || wikiHasMore;
1533
+ Logger.info(`统一搜索完成,文档: ${documents.length} 条, 知识库: ${wikis.length} 条, 总计: ${total} 条, hasMore: ${hasMore}`);
1534
+ // 只返回必要字段,根据搜索类型动态添加
1535
+ const result = {
1536
+ paginationGuide
1537
+ };
1538
+ if (searchType === 'document' || searchType === 'both') {
1539
+ result.documents = documents;
1540
+ }
1541
+ if (searchType === 'wiki' || searchType === 'both') {
1542
+ result.wikis = wikis;
1543
+ }
1544
+ return result;
1545
+ }
1546
+ catch (error) {
1547
+ this.handleApiError(error, '统一搜索失败');
1548
+ throw error;
1549
+ }
1550
+ }
1551
+ /**
1552
+ * 生成分页指导信息
1553
+ * @param searchType 搜索类型
1554
+ * @param documentHasMore 文档是否还有更多
1555
+ * @param wikiHasMore 知识库是否还有更多
1556
+ * @param documentOffset 文档的下一offset
1557
+ * @param wikiPageToken 知识库的下一页token
1558
+ * @returns 分页指导信息
1559
+ */
1560
+ generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken) {
1561
+ const guide = {
1562
+ hasMore: documentHasMore || wikiHasMore,
1563
+ description: ''
1564
+ };
1565
+ if (!guide.hasMore) {
1566
+ guide.description = '没有更多结果了';
1567
+ return guide;
1568
+ }
1569
+ // 根据搜索类型和hasMore状态生成指导
1570
+ if (searchType === 'document') {
1571
+ if (documentHasMore) {
1572
+ guide.nextPageParams = {
1573
+ searchType: 'document',
1574
+ offset: documentOffset
1575
+ };
1576
+ guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = document offset=${documentOffset} 获取文档的下一页`;
1577
+ }
1578
+ }
1579
+ else if (searchType === 'wiki') {
1580
+ if (wikiHasMore && wikiPageToken) {
1581
+ guide.nextPageParams = {
1582
+ searchType: 'wiki',
1583
+ pageToken: wikiPageToken
1584
+ };
1585
+ guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页`;
1586
+ }
1587
+ }
1588
+ else if (searchType === 'both') {
1589
+ // both类型:优先返回文档的下一页,如果文档没有更多了,再返回知识库的下一页
1590
+ if (documentHasMore) {
1591
+ guide.nextPageParams = {
1592
+ searchType: 'both',
1593
+ offset: documentOffset
1594
+ };
1595
+ guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = both offset=${documentOffset} 获取文档的下一页`;
1596
+ }
1597
+ else if (wikiHasMore && wikiPageToken) {
1598
+ guide.nextPageParams = {
1599
+ searchType: 'wiki',
1600
+ pageToken: wikiPageToken
1601
+ };
1602
+ guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页wiki结果`;
1603
+ }
1202
1604
  }
1605
+ return guide;
1203
1606
  }
1204
1607
  /**
1205
1608
  * 上传图片素材到飞书