feishu-mcp 0.1.6 → 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.
- package/README.md +73 -33
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/mcp/tools/feishuBlockTools.js +163 -35
- package/dist/mcp/tools/feishuFolderTools.js +95 -30
- package/dist/mcp/tools/feishuTools.js +84 -30
- package/dist/services/baseService.js +13 -2
- package/dist/services/blockFactory.js +17 -0
- package/dist/services/feishuApiService.js +738 -65
- package/dist/types/feishuSchema.js +60 -5
- package/dist/utils/auth/tokenCacheManager.js +112 -0
- package/dist/utils/error.js +24 -0
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
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';
|
|
8
7
|
import { AuthService } from './feishuAuthService.js';
|
|
8
|
+
import { ScopeInsufficientError } from '../utils/error.js';
|
|
9
9
|
import axios from 'axios';
|
|
10
10
|
import FormData from 'form-data';
|
|
11
11
|
import fs from 'fs';
|
|
@@ -20,12 +20,6 @@ export class FeishuApiService extends BaseApiService {
|
|
|
20
20
|
*/
|
|
21
21
|
constructor() {
|
|
22
22
|
super();
|
|
23
|
-
Object.defineProperty(this, "cacheManager", {
|
|
24
|
-
enumerable: true,
|
|
25
|
-
configurable: true,
|
|
26
|
-
writable: true,
|
|
27
|
-
value: void 0
|
|
28
|
-
});
|
|
29
23
|
Object.defineProperty(this, "blockFactory", {
|
|
30
24
|
enumerable: true,
|
|
31
25
|
configurable: true,
|
|
@@ -44,7 +38,6 @@ export class FeishuApiService extends BaseApiService {
|
|
|
44
38
|
writable: true,
|
|
45
39
|
value: void 0
|
|
46
40
|
});
|
|
47
|
-
this.cacheManager = CacheManager.getInstance();
|
|
48
41
|
this.blockFactory = BlockFactory.getInstance();
|
|
49
42
|
this.config = Config.getInstance();
|
|
50
43
|
this.authService = new AuthService();
|
|
@@ -84,13 +77,231 @@ export class FeishuApiService extends BaseApiService {
|
|
|
84
77
|
// 生成客户端缓存键
|
|
85
78
|
const clientKey = AuthUtils.generateClientKey(userKey);
|
|
86
79
|
Logger.debug(`[FeishuApiService] 获取访问令牌,userKey: ${userKey}, clientKey: ${clientKey}, authType: ${authType}`);
|
|
80
|
+
// 在使用token之前先校验scope(使用appId+appSecret获取临时tenant token来调用scope接口)
|
|
81
|
+
await this.validateScopeWithVersion(appId, appSecret, authType);
|
|
82
|
+
// 校验通过后,获取实际的token
|
|
87
83
|
if (authType === 'tenant') {
|
|
88
84
|
// 租户模式:获取租户访问令牌
|
|
89
|
-
return this.getTenantAccessToken(appId, appSecret, clientKey);
|
|
85
|
+
return await this.getTenantAccessToken(appId, appSecret, clientKey);
|
|
90
86
|
}
|
|
91
87
|
else {
|
|
92
88
|
// 用户模式:获取用户访问令牌
|
|
93
|
-
return this.authService.getUserAccessToken(clientKey, appId, appSecret);
|
|
89
|
+
return await this.authService.getUserAccessToken(clientKey, appId, appSecret);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 获取应用权限范围
|
|
94
|
+
* @param accessToken 访问令牌
|
|
95
|
+
* @param authType 认证类型(tenant或user)
|
|
96
|
+
* @returns 应用权限范围列表
|
|
97
|
+
*/
|
|
98
|
+
async getApplicationScopes(accessToken, authType) {
|
|
99
|
+
try {
|
|
100
|
+
const endpoint = '/application/v6/scopes';
|
|
101
|
+
const headers = {
|
|
102
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
103
|
+
'Content-Type': 'application/json'
|
|
104
|
+
};
|
|
105
|
+
Logger.debug('请求应用权限范围:', endpoint);
|
|
106
|
+
const response = await axios.get(`${this.getBaseUrl()}${endpoint}`, { headers });
|
|
107
|
+
const data = response.data;
|
|
108
|
+
if (data.code !== 0) {
|
|
109
|
+
throw new Error(`获取应用权限范围失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
|
|
110
|
+
}
|
|
111
|
+
// 提取权限列表
|
|
112
|
+
// API返回格式: { "data": { "scopes": [{ "grant_status": 1, "scope_name": "...", "scope_type": "tenant"|"user" }] } }
|
|
113
|
+
const scopes = [];
|
|
114
|
+
if (data.data && Array.isArray(data.data.scopes)) {
|
|
115
|
+
// 根据authType过滤,只取已授权的scope(grant_status === 1)
|
|
116
|
+
for (const scopeItem of data.data.scopes) {
|
|
117
|
+
if (scopeItem.grant_status === 1 && scopeItem.scope_type === authType && scopeItem.scope_name) {
|
|
118
|
+
scopes.push(scopeItem.scope_name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
Logger.debug(`获取应用权限范围成功,共 ${scopes.length} 个${authType}权限`);
|
|
123
|
+
return scopes;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
Logger.error('获取应用权限范围失败:', error);
|
|
127
|
+
throw new Error('获取应用权限范围失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 校验scope权限是否充足
|
|
132
|
+
* @param requiredScopes 所需的权限列表
|
|
133
|
+
* @param actualScopes 实际的权限列表
|
|
134
|
+
* @returns 是否权限充足,以及缺失的权限列表
|
|
135
|
+
*/
|
|
136
|
+
validateScopes(requiredScopes, actualScopes) {
|
|
137
|
+
const actualScopesSet = new Set(actualScopes);
|
|
138
|
+
const missingScopes = [];
|
|
139
|
+
for (const requiredScope of requiredScopes) {
|
|
140
|
+
if (!actualScopesSet.has(requiredScope)) {
|
|
141
|
+
missingScopes.push(requiredScope);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
isValid: missingScopes.length === 0,
|
|
146
|
+
missingScopes
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 获取所需的scope列表(根据认证类型)
|
|
151
|
+
* @param authType 认证类型
|
|
152
|
+
* @returns 所需的scope列表
|
|
153
|
+
*/
|
|
154
|
+
getRequiredScopes(authType) {
|
|
155
|
+
// 根据FEISHU_CONFIG.md中定义的权限列表,与用户提供的配置保持一致
|
|
156
|
+
const tenantScopes = [
|
|
157
|
+
"docx:document.block:convert",
|
|
158
|
+
"base:app:read",
|
|
159
|
+
"bitable:app",
|
|
160
|
+
"bitable:app:readonly",
|
|
161
|
+
"board:whiteboard:node:create",
|
|
162
|
+
"board:whiteboard:node:read",
|
|
163
|
+
"contact:user.employee_id:readonly",
|
|
164
|
+
"docs:document.content:read",
|
|
165
|
+
"docx:document",
|
|
166
|
+
"docx:document:create",
|
|
167
|
+
"docx:document:readonly",
|
|
168
|
+
"drive:drive",
|
|
169
|
+
"drive:drive:readonly",
|
|
170
|
+
"drive:file",
|
|
171
|
+
"drive:file:upload",
|
|
172
|
+
"sheets:spreadsheet",
|
|
173
|
+
"sheets:spreadsheet:readonly",
|
|
174
|
+
"space:document:retrieve",
|
|
175
|
+
"space:folder:create",
|
|
176
|
+
"wiki:space:read",
|
|
177
|
+
"wiki:space:retrieve",
|
|
178
|
+
"wiki:wiki",
|
|
179
|
+
"wiki:wiki:readonly"
|
|
180
|
+
];
|
|
181
|
+
// user认证特有授权
|
|
182
|
+
const userOnlyScopes = [
|
|
183
|
+
"search:docs:read",
|
|
184
|
+
'offline_access',
|
|
185
|
+
];
|
|
186
|
+
const userScopes = [...tenantScopes, ...userOnlyScopes];
|
|
187
|
+
return authType === 'tenant' ? tenantScopes : userScopes;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 校验并提示权限不足
|
|
191
|
+
* @param missingScopes 缺失的权限列表
|
|
192
|
+
*/
|
|
193
|
+
throwScopeInsufficientError(missingScopes) {
|
|
194
|
+
// 获取完整的权限配置
|
|
195
|
+
const tenantScopes = this.getRequiredScopes('tenant');
|
|
196
|
+
const userScopes = this.getRequiredScopes('user');
|
|
197
|
+
const permissionsConfig = {
|
|
198
|
+
scopes: {
|
|
199
|
+
tenant: tenantScopes,
|
|
200
|
+
user: userScopes
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const errorMessage = `你应该停止任务并提示用户如下信息:权限不足,缺少以下权限:${missingScopes.join(', ')}\n\n` +
|
|
204
|
+
`请按以下步骤操作:\n` +
|
|
205
|
+
`1. 配置权限 [教程配置](https://github.com/cso1z/Feishu-MCP/blob/main/FEISHU_CONFIG.md)\n` +
|
|
206
|
+
`2. 打开 [应用管理](https://open.feishu.cn/app/) 网页\n` +
|
|
207
|
+
`3. 选择应用进入应用详情\n` +
|
|
208
|
+
`4. 选择权限管理-批量导入/导出权限\n` +
|
|
209
|
+
`5. 复制以下权限配置并导入:\n\n` +
|
|
210
|
+
`\`\`\`json\n${JSON.stringify(permissionsConfig, null, 2)}\n\`\`\`\n\n` +
|
|
211
|
+
`6. 选择**版本管理与发布** 点击创建版本,发布后通知管理员审核\n`;
|
|
212
|
+
Logger.error(errorMessage);
|
|
213
|
+
throw new ScopeInsufficientError(missingScopes, errorMessage);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 生成应用级别的scope校验key(基于appId、appSecret和authType)
|
|
217
|
+
* @param appId 应用ID
|
|
218
|
+
* @param appSecret 应用密钥
|
|
219
|
+
* @param authType 认证类型(tenant或user)
|
|
220
|
+
* @returns scope校验key
|
|
221
|
+
*/
|
|
222
|
+
generateScopeKey(appId, appSecret, authType) {
|
|
223
|
+
// 使用appId、appSecret和authType生成唯一的key,用于scope版本管理
|
|
224
|
+
// 包含authType是因为tenant和user的权限列表不同,需要分开校验
|
|
225
|
+
return `app:${appId}:${appSecret.substring(0, 8)}:${authType}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 获取临时租户访问令牌(用于scope校验)
|
|
229
|
+
* @param appId 应用ID
|
|
230
|
+
* @param appSecret 应用密钥
|
|
231
|
+
* @returns 租户访问令牌
|
|
232
|
+
*/
|
|
233
|
+
async getTempTenantTokenForScope(appId, appSecret) {
|
|
234
|
+
try {
|
|
235
|
+
const requestData = {
|
|
236
|
+
app_id: appId,
|
|
237
|
+
app_secret: appSecret,
|
|
238
|
+
};
|
|
239
|
+
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
|
240
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
241
|
+
Logger.debug('获取临时租户token用于scope校验:', url);
|
|
242
|
+
const response = await axios.post(url, requestData, { headers });
|
|
243
|
+
const data = response.data;
|
|
244
|
+
if (data.code !== 0) {
|
|
245
|
+
throw new Error(`获取临时租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
|
|
246
|
+
}
|
|
247
|
+
if (!data.tenant_access_token) {
|
|
248
|
+
throw new Error('获取临时租户访问令牌失败:响应中没有token');
|
|
249
|
+
}
|
|
250
|
+
Logger.debug('临时租户token获取成功,用于scope校验');
|
|
251
|
+
return data.tenant_access_token;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
Logger.error('获取临时租户访问令牌失败:', error);
|
|
255
|
+
throw new Error('获取临时租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 校验scope权限(带版本管理)
|
|
260
|
+
* @param appId 应用ID
|
|
261
|
+
* @param appSecret 应用密钥
|
|
262
|
+
* @param authType 认证类型
|
|
263
|
+
*/
|
|
264
|
+
async validateScopeWithVersion(appId, appSecret, authType) {
|
|
265
|
+
const tokenCacheManager = TokenCacheManager.getInstance();
|
|
266
|
+
// 生成应用级别的scope校验key(包含authType,因为tenant和user权限不同)
|
|
267
|
+
const scopeKey = this.generateScopeKey(appId, appSecret, authType);
|
|
268
|
+
const scopeVersion = '2.0.0'; // 当前scope版本号,可以根据需要更新
|
|
269
|
+
// 检查是否需要校验
|
|
270
|
+
if (!tokenCacheManager.shouldValidateScope(scopeKey, scopeVersion)) {
|
|
271
|
+
Logger.debug(`Scope版本已校验过,跳过校验: ${scopeKey}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
Logger.info(`开始校验scope权限,版本: ${scopeVersion}, scopeKey: ${scopeKey}`);
|
|
275
|
+
try {
|
|
276
|
+
// 使用appId和appSecret获取临时tenant token来调用scope接口
|
|
277
|
+
const tempTenantToken = await this.getTempTenantTokenForScope(appId, appSecret);
|
|
278
|
+
// 获取实际权限范围(使用tenant token,但根据authType过滤scope_type)
|
|
279
|
+
const actualScopes = await this.getApplicationScopes(tempTenantToken, authType);
|
|
280
|
+
// 获取当前版本所需的scope列表
|
|
281
|
+
const requiredScopes = this.getRequiredScopes(authType);
|
|
282
|
+
// 校验权限
|
|
283
|
+
const validationResult = this.validateScopes(requiredScopes, actualScopes);
|
|
284
|
+
if (!validationResult.isValid) {
|
|
285
|
+
// 权限不足,抛出错误
|
|
286
|
+
this.throwScopeInsufficientError(validationResult.missingScopes);
|
|
287
|
+
}
|
|
288
|
+
// 权限充足,保存版本信息
|
|
289
|
+
const scopeVersionInfo = {
|
|
290
|
+
scopeVersion,
|
|
291
|
+
scopeList: requiredScopes,
|
|
292
|
+
validatedAt: Math.floor(Date.now() / 1000),
|
|
293
|
+
validatedVersion: scopeVersion
|
|
294
|
+
};
|
|
295
|
+
tokenCacheManager.saveScopeVersionInfo(scopeKey, scopeVersionInfo);
|
|
296
|
+
Logger.info(`Scope权限校验成功,版本: ${scopeVersion}`);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// 如果是权限不足错误,需要重新抛出,中断流程
|
|
300
|
+
if (error instanceof ScopeInsufficientError) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
// 如果获取权限范围失败(网络错误、API调用失败等),记录警告但不阻止token使用
|
|
304
|
+
Logger.warn(`Scope权限校验失败,但继续使用token: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
305
|
}
|
|
95
306
|
}
|
|
96
307
|
/**
|
|
@@ -163,16 +374,57 @@ export class FeishuApiService extends BaseApiService {
|
|
|
163
374
|
}
|
|
164
375
|
}
|
|
165
376
|
/**
|
|
166
|
-
*
|
|
167
|
-
* @param documentId 文档ID或URL
|
|
168
|
-
* @
|
|
377
|
+
* 获取文档信息(支持普通文档和Wiki文档)
|
|
378
|
+
* @param documentId 文档ID或URL(支持Wiki链接)
|
|
379
|
+
* @param documentType 文档类型(可选),'document' 或 'wiki',如果不指定则自动检测
|
|
380
|
+
* @returns 文档信息或Wiki节点信息
|
|
169
381
|
*/
|
|
170
|
-
async getDocumentInfo(documentId) {
|
|
382
|
+
async getDocumentInfo(documentId, documentType) {
|
|
171
383
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
428
|
}
|
|
177
429
|
catch (error) {
|
|
178
430
|
this.handleApiError(error, '获取文档信息失败');
|
|
@@ -586,33 +838,28 @@ export class FeishuApiService extends BaseApiService {
|
|
|
586
838
|
* @param wikiUrl Wiki链接或Token
|
|
587
839
|
* @returns 文档ID
|
|
588
840
|
*/
|
|
589
|
-
async convertWikiToDocumentId(wikiUrl) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
catch (error) {
|
|
612
|
-
this.handleApiError(error, 'Wiki转换为文档ID失败');
|
|
613
|
-
return ''; // 永远不会执行到这里
|
|
614
|
-
}
|
|
615
|
-
}
|
|
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
|
+
// }
|
|
616
863
|
/**
|
|
617
864
|
* 获取BlockFactory实例
|
|
618
865
|
* @returns BlockFactory实例
|
|
@@ -745,6 +992,21 @@ export class FeishuApiService extends BaseApiService {
|
|
|
745
992
|
};
|
|
746
993
|
}
|
|
747
994
|
break;
|
|
995
|
+
case BlockType.WHITEBOARD:
|
|
996
|
+
if ('whiteboard' in options && options.whiteboard) {
|
|
997
|
+
const whiteboardOptions = options.whiteboard;
|
|
998
|
+
blockConfig.options = {
|
|
999
|
+
align: (whiteboardOptions.align === 1 || whiteboardOptions.align === 2 || whiteboardOptions.align === 3)
|
|
1000
|
+
? whiteboardOptions.align : 1
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
// 默认画板块选项
|
|
1005
|
+
blockConfig.options = {
|
|
1006
|
+
align: 1
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
748
1010
|
default:
|
|
749
1011
|
Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
|
|
750
1012
|
if ('text' in options) {
|
|
@@ -815,6 +1077,14 @@ export class FeishuApiService extends BaseApiService {
|
|
|
815
1077
|
code: mermaidConfig.code,
|
|
816
1078
|
};
|
|
817
1079
|
}
|
|
1080
|
+
else if ("whiteboard" in options) {
|
|
1081
|
+
blockConfig.type = BlockType.WHITEBOARD;
|
|
1082
|
+
const whiteboardConfig = options.whiteboard;
|
|
1083
|
+
blockConfig.options = {
|
|
1084
|
+
align: (whiteboardConfig.align === 1 || whiteboardConfig.align === 2 || whiteboardConfig.align === 3)
|
|
1085
|
+
? whiteboardConfig.align : 1
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
818
1088
|
break;
|
|
819
1089
|
}
|
|
820
1090
|
// 记录调试信息
|
|
@@ -871,6 +1141,148 @@ export class FeishuApiService extends BaseApiService {
|
|
|
871
1141
|
this.handleApiError(error, '获取飞书根文件夹信息失败');
|
|
872
1142
|
}
|
|
873
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
|
+
}
|
|
874
1286
|
/**
|
|
875
1287
|
* 获取文件夹中的文件清单
|
|
876
1288
|
* @param folderToken 文件夹Token
|
|
@@ -916,51 +1328,282 @@ export class FeishuApiService extends BaseApiService {
|
|
|
916
1328
|
}
|
|
917
1329
|
}
|
|
918
1330
|
/**
|
|
919
|
-
*
|
|
1331
|
+
* 搜索飞书文档(支持分页和轮询)
|
|
920
1332
|
* @param searchKey 搜索关键字
|
|
921
|
-
* @param
|
|
922
|
-
* @
|
|
1333
|
+
* @param maxSize 最大返回数量,如果未指定则只返回一页
|
|
1334
|
+
* @param offset 偏移量,用于分页,默认0
|
|
1335
|
+
* @returns 搜索结果,包含数据和分页信息
|
|
923
1336
|
*/
|
|
924
|
-
async searchDocuments(searchKey,
|
|
1337
|
+
async searchDocuments(searchKey, maxSize, offset = 0) {
|
|
925
1338
|
try {
|
|
926
|
-
Logger.info(`开始搜索文档,关键字: ${searchKey}`);
|
|
1339
|
+
Logger.info(`开始搜索文档,关键字: ${searchKey}, maxSize: ${maxSize || '未指定'}, offset: ${offset}`);
|
|
927
1340
|
const endpoint = `/suite/docs-api/search/object`;
|
|
928
|
-
|
|
929
|
-
|
|
1341
|
+
const PAGE_SIZE = 50; // 文档API固定使用50
|
|
1342
|
+
const allResults = [];
|
|
1343
|
+
let currentOffset = offset;
|
|
930
1344
|
let hasMore = true;
|
|
931
|
-
//
|
|
932
|
-
while (hasMore &&
|
|
1345
|
+
// 如果指定了maxSize,轮询获取直到满足maxSize或没有更多数据
|
|
1346
|
+
while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
|
|
933
1347
|
const payload = {
|
|
934
1348
|
search_key: searchKey,
|
|
935
1349
|
docs_types: ["doc"],
|
|
936
|
-
count:
|
|
937
|
-
offset:
|
|
1350
|
+
count: PAGE_SIZE,
|
|
1351
|
+
offset: currentOffset
|
|
938
1352
|
};
|
|
939
|
-
Logger.debug(
|
|
1353
|
+
Logger.debug(`请求搜索文档,offset: ${currentOffset}, count: ${PAGE_SIZE}`);
|
|
940
1354
|
const response = await this.post(endpoint, payload);
|
|
941
1355
|
Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
|
|
942
1356
|
if (response && response.docs_entities) {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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}`);
|
|
948
1375
|
}
|
|
949
1376
|
else {
|
|
950
|
-
hasMore = false;
|
|
951
1377
|
Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
|
|
1378
|
+
hasMore = false;
|
|
952
1379
|
}
|
|
953
1380
|
}
|
|
954
1381
|
const resultCount = allResults.length;
|
|
955
|
-
Logger.info(`文档搜索完成,找到 ${resultCount}
|
|
1382
|
+
Logger.info(`文档搜索完成,找到 ${resultCount} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
|
|
956
1383
|
return {
|
|
957
|
-
|
|
1384
|
+
items: allResults,
|
|
1385
|
+
hasMore: hasMore,
|
|
1386
|
+
nextOffset: currentOffset
|
|
958
1387
|
};
|
|
959
1388
|
}
|
|
960
1389
|
catch (error) {
|
|
961
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;
|
|
962
1463
|
}
|
|
963
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
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return guide;
|
|
1606
|
+
}
|
|
964
1607
|
/**
|
|
965
1608
|
* 上传图片素材到飞书
|
|
966
1609
|
* @param imageBase64 图片的Base64编码
|
|
@@ -1162,6 +1805,36 @@ export class FeishuApiService extends BaseApiService {
|
|
|
1162
1805
|
return Buffer.from([]); // 永远不会执行到这里
|
|
1163
1806
|
}
|
|
1164
1807
|
}
|
|
1808
|
+
/**
|
|
1809
|
+
* 在画板中创建图表节点(支持 PlantUML 和 Mermaid)
|
|
1810
|
+
* @param whiteboardId 画板ID(token)
|
|
1811
|
+
* @param code 图表代码(PlantUML 或 Mermaid)
|
|
1812
|
+
* @param syntaxType 语法类型:1=PlantUML, 2=Mermaid
|
|
1813
|
+
* @returns 创建结果
|
|
1814
|
+
*/
|
|
1815
|
+
async createDiagramNode(whiteboardId, code, syntaxType) {
|
|
1816
|
+
try {
|
|
1817
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
1818
|
+
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes/plantuml`;
|
|
1819
|
+
const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
|
|
1820
|
+
Logger.info(`开始在画板中创建 ${syntaxTypeName} 节点,画板ID: ${normalizedWhiteboardId}`);
|
|
1821
|
+
Logger.debug(`${syntaxTypeName} 代码: ${code.substring(0, 200)}...`);
|
|
1822
|
+
const payload = {
|
|
1823
|
+
plant_uml_code: code,
|
|
1824
|
+
style_type: 1, // 画板样式(默认为2 经典样式) 示例值:1 可选值有: 1:画板样式(解析之后为多个画板节点,粘贴到画板中,不可对语法进行二次编辑) 2:经典样式(解析之后为一张图片,粘贴到画板中,可对语法进行二次编辑)(只有PlantUml语法支持经典样式
|
|
1825
|
+
syntax_type: syntaxType
|
|
1826
|
+
};
|
|
1827
|
+
Logger.debug(`请求载荷: ${JSON.stringify(payload, null, 2)}`);
|
|
1828
|
+
const response = await this.post(endpoint, payload);
|
|
1829
|
+
Logger.info(`${syntaxTypeName} 节点创建成功`);
|
|
1830
|
+
return response;
|
|
1831
|
+
}
|
|
1832
|
+
catch (error) {
|
|
1833
|
+
const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
|
|
1834
|
+
Logger.error(`创建 ${syntaxTypeName} 节点失败,画板ID: ${whiteboardId}`, error);
|
|
1835
|
+
this.handleApiError(error, `创建 ${syntaxTypeName} 节点失败`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1165
1838
|
/**
|
|
1166
1839
|
* 从路径或URL获取图片的Base64编码
|
|
1167
1840
|
* @param imagePathOrUrl 图片路径或URL
|