feishu-mcp 0.2.2 → 0.2.7
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/LICENSE +21 -21
- package/README.md +356 -266
- package/dist/cli/auth.js +117 -0
- package/dist/cli/commands/auth.js +141 -0
- package/dist/cli/commands/config.js +86 -0
- package/dist/cli/commands/guide.js +68 -0
- package/dist/cli/dispatcher.js +96 -0
- package/dist/cli/index.js +95 -0
- package/dist/manager/sseConnectionManager.js +2 -0
- package/dist/mcp/feishuMcp.js +25 -25
- package/dist/mcp/tools/blockTools.js +295 -0
- package/dist/mcp/tools/documentTools.js +105 -0
- package/dist/mcp/tools/feishuBlockTools.js +1 -285
- package/dist/mcp/tools/feishuTools.js +1 -67
- package/dist/mcp/tools/folderTools.js +99 -0
- package/dist/mcp/tools/toolHelpers.js +155 -0
- package/dist/modules/FeatureModule.js +1 -0
- package/dist/modules/ModuleRegistry.js +63 -0
- package/dist/modules/calendar/index.js +11 -0
- package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
- package/dist/modules/calendar/tools/calendarTools.js +6 -0
- package/dist/modules/document/index.js +15 -0
- package/dist/modules/document/services/FeishuBlockService.js +410 -0
- package/dist/modules/document/services/FeishuDocumentService.js +110 -0
- package/dist/modules/document/services/FeishuFoldService.js +187 -0
- package/dist/modules/document/services/FeishuSearchService.js +232 -0
- package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
- package/dist/modules/document/services/blockFactory.js +521 -0
- package/dist/modules/document/toolApi/blockToolApi.js +160 -0
- package/dist/modules/document/toolApi/documentToolApi.js +65 -0
- package/dist/modules/document/toolApi/folderToolApi.js +73 -0
- package/dist/modules/document/toolApi/index.js +3 -0
- package/dist/modules/document/tools/blockTools.js +138 -0
- package/dist/modules/document/tools/documentTools.js +64 -0
- package/dist/modules/document/tools/folderTools.js +46 -0
- package/dist/modules/document/tools/toolHelpers.js +155 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/member/index.js +11 -0
- package/dist/modules/member/services/FeishuMemberService.js +41 -0
- package/dist/modules/member/toolApi/index.js +1 -0
- package/dist/modules/member/toolApi/memberToolApi.js +54 -0
- package/dist/modules/member/tools/memberTools.js +26 -0
- package/dist/modules/task/index.js +11 -0
- package/dist/modules/task/services/FeishuTaskService.js +271 -0
- package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
- package/dist/modules/task/toolApi/index.js +1 -0
- package/dist/modules/task/toolApi/taskToolApi.js +128 -0
- package/dist/modules/task/tools/taskTools.js +93 -0
- package/dist/server.js +43 -24
- package/dist/services/baseService.js +11 -2
- package/dist/services/blockFactory.js +167 -0
- package/dist/services/callbackService.js +1 -1
- package/dist/services/constants/feishuScopes.js +94 -0
- package/dist/services/feishu/FeishuBaseApiService.js +47 -0
- package/dist/services/feishu/FeishuBlockService.js +410 -0
- package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
- package/dist/services/feishu/FeishuDocumentService.js +110 -0
- package/dist/services/feishu/FeishuDriveService.js +62 -0
- package/dist/services/feishu/FeishuFoldService.js +187 -0
- package/dist/services/feishu/FeishuImageService.js +219 -0
- package/dist/services/feishu/FeishuScopeValidator.js +177 -0
- package/dist/services/feishu/FeishuSearchService.js +232 -0
- package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
- package/dist/services/feishu/FeishuWikiService.js +134 -0
- package/dist/services/feishuApiService.js +246 -1760
- package/dist/services/feishuAuthService.js +43 -0
- package/dist/types/documentSchema.js +232 -0
- package/dist/types/feishuSchema.js +54 -77
- package/dist/types/memberSchema.js +35 -0
- package/dist/types/taskSchema.js +166 -0
- package/dist/utils/auth/tokenCacheManager.js +16 -3
- package/dist/utils/auth/tokenRefreshManager.js +2 -1
- package/dist/utils/config.js +47 -0
- package/dist/utils/document.js +116 -116
- package/dist/utils/error.js +0 -11
- package/dist/utils/paramUtils.js +0 -31
- package/package.json +77 -76
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Logger } from '../../utils/logger.js';
|
|
2
|
+
import { Config } from '../../utils/config.js';
|
|
3
|
+
import { FeishuBaseApiService } from './FeishuBaseApiService.js';
|
|
4
|
+
/**
|
|
5
|
+
* 飞书搜索服务
|
|
6
|
+
* 负责文档和知识库的搜索
|
|
7
|
+
*/
|
|
8
|
+
export class FeishuSearchService extends FeishuBaseApiService {
|
|
9
|
+
constructor(authService) {
|
|
10
|
+
super(authService);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 搜索飞书文档(Docs),支持分页轮询直到获取指定数量的结果
|
|
14
|
+
* 注意:仅支持 tenant 和 user 两种认证模式
|
|
15
|
+
* @param searchKey 搜索关键字
|
|
16
|
+
* @param maxSize 最大返回数量;不指定时只返回第一页(约 50 条)
|
|
17
|
+
* @param offset 起始偏移量,用于分页续取,默认 0
|
|
18
|
+
* @returns 搜索结果对象,包含 items(文档列表)、hasMore(是否还有更多)、nextOffset(下次请求的偏移量)
|
|
19
|
+
*/
|
|
20
|
+
async searchDocuments(searchKey, maxSize, offset = 0) {
|
|
21
|
+
try {
|
|
22
|
+
Logger.info(`开始搜索文档,关键字: ${searchKey}, maxSize: ${maxSize || '未指定'}, offset: ${offset}`);
|
|
23
|
+
const endpoint = '/suite/docs-api/search/object';
|
|
24
|
+
const PAGE_SIZE = 50;
|
|
25
|
+
const allResults = [];
|
|
26
|
+
let currentOffset = offset;
|
|
27
|
+
let hasMore = true;
|
|
28
|
+
while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
|
|
29
|
+
const payload = {
|
|
30
|
+
search_key: searchKey,
|
|
31
|
+
docs_types: ['doc'],
|
|
32
|
+
count: PAGE_SIZE,
|
|
33
|
+
offset: currentOffset
|
|
34
|
+
};
|
|
35
|
+
Logger.debug(`请求搜索文档,offset: ${currentOffset}, count: ${PAGE_SIZE}`);
|
|
36
|
+
const response = await this.post(endpoint, payload);
|
|
37
|
+
Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
|
|
38
|
+
if (response?.docs_entities) {
|
|
39
|
+
const resultCount = response.docs_entities.length;
|
|
40
|
+
const apiHasMore = response.has_more || false;
|
|
41
|
+
currentOffset += resultCount;
|
|
42
|
+
if (resultCount > 0) {
|
|
43
|
+
allResults.push(...response.docs_entities);
|
|
44
|
+
hasMore = apiHasMore;
|
|
45
|
+
if (maxSize == undefined || allResults.length >= maxSize) {
|
|
46
|
+
Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
hasMore = false;
|
|
52
|
+
}
|
|
53
|
+
Logger.debug(`文档搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
|
|
57
|
+
hasMore = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
Logger.info(`文档搜索完成,找到 ${allResults.length} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
|
|
61
|
+
return { items: allResults, hasMore, nextOffset: currentOffset };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.handleApiError(error, '搜索文档失败');
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 搜索飞书知识库(Wiki)节点,支持分页轮询直到获取指定数量的结果
|
|
70
|
+
* 注意:Wiki 搜索需要 user 认证,tenant 模式下不可用
|
|
71
|
+
* @param query 搜索关键字
|
|
72
|
+
* @param maxSize 最大返回数量;不指定时只返回第一页(约 20 条)
|
|
73
|
+
* @param pageToken 分页 Token,用于续取下一页,从上次返回的 pageToken 传入
|
|
74
|
+
* @returns 搜索结果对象,包含 items(节点列表)、hasMore、pageToken(下次请求用)、count(本次数量)
|
|
75
|
+
*/
|
|
76
|
+
async searchWikiNodes(query, maxSize, pageToken) {
|
|
77
|
+
try {
|
|
78
|
+
Logger.info(`开始搜索知识库,关键字: ${query}, maxSize: ${maxSize || '未指定'}, pageToken: ${pageToken || '无'}`);
|
|
79
|
+
const endpoint = '/wiki/v1/nodes/search';
|
|
80
|
+
const PAGE_SIZE = 20;
|
|
81
|
+
const allResults = [];
|
|
82
|
+
let currentPageToken = pageToken;
|
|
83
|
+
let hasMore = true;
|
|
84
|
+
while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
|
|
85
|
+
const size = Math.min(PAGE_SIZE, 100);
|
|
86
|
+
let url = `${endpoint}?page_size=${size}`;
|
|
87
|
+
if (currentPageToken)
|
|
88
|
+
url += `&page_token=${currentPageToken}`;
|
|
89
|
+
const payload = { query };
|
|
90
|
+
Logger.debug(`请求搜索知识库,pageSize: ${size}, pageToken: ${currentPageToken || '无'}`);
|
|
91
|
+
const response = await this.post(url, payload);
|
|
92
|
+
Logger.debug('知识库搜索响应:', JSON.stringify(response, null, 2));
|
|
93
|
+
if (response?.items) {
|
|
94
|
+
const resultCount = response.items?.length || 0;
|
|
95
|
+
const apiHasMore = response.has_more || false;
|
|
96
|
+
currentPageToken = response.page_token || null;
|
|
97
|
+
if (resultCount > 0) {
|
|
98
|
+
allResults.push(...response.items);
|
|
99
|
+
hasMore = apiHasMore;
|
|
100
|
+
if (maxSize !== undefined) {
|
|
101
|
+
if (allResults.length >= maxSize) {
|
|
102
|
+
Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
hasMore = false;
|
|
112
|
+
}
|
|
113
|
+
Logger.debug(`知识库搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
Logger.warn('知识库搜索响应格式异常:', JSON.stringify(response, null, 2));
|
|
117
|
+
hasMore = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Logger.info(`知识库搜索完成,找到 ${allResults.length} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
|
|
121
|
+
return { items: allResults, hasMore, pageToken: currentPageToken, count: allResults.length };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.handleApiError(error, '搜索知识库失败');
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 统一搜索入口,可同时搜索文档和知识库节点
|
|
130
|
+
* 当 authType 为 tenant 时,wiki 搜索不可用,会自动降级为 document 搜索
|
|
131
|
+
* 单次调用最多返回 100 条合并结果(文档 + Wiki)
|
|
132
|
+
* @param searchKey 搜索关键字
|
|
133
|
+
* @param searchType 搜索范围:'document' 仅搜文档,'wiki' 仅搜知识库,'both' 同时搜索,默认 'both'
|
|
134
|
+
* @param offset 文档搜索的起始偏移量,用于分页续取,默认 0
|
|
135
|
+
* @param pageToken 知识库搜索的分页 Token,用于续取下一页
|
|
136
|
+
* @returns 搜索结果对象,包含 documents(文档列表)、wikis(知识库节点列表)和 paginationGuide(分页指导)
|
|
137
|
+
*/
|
|
138
|
+
async search(searchKey, searchType = 'both', offset, pageToken) {
|
|
139
|
+
try {
|
|
140
|
+
if (Config.getInstance().feishu.authType === 'tenant' && (searchType === 'wiki' || searchType === 'both')) {
|
|
141
|
+
Logger.info(`租户认证模式下wiki搜索不支持,强制将searchType从 ${searchType} 修改为 document`);
|
|
142
|
+
searchType = 'document';
|
|
143
|
+
}
|
|
144
|
+
const MAX_TOTAL_RESULTS = 100;
|
|
145
|
+
const docOffset = offset ?? 0;
|
|
146
|
+
Logger.info(`开始统一搜索,关键字: ${searchKey}, 类型: ${searchType}, offset: ${docOffset}, pageToken: ${pageToken || '无'}`);
|
|
147
|
+
const documents = [];
|
|
148
|
+
const wikis = [];
|
|
149
|
+
let documentOffset = docOffset;
|
|
150
|
+
let wikiPageToken = null;
|
|
151
|
+
let documentHasMore = false;
|
|
152
|
+
let wikiHasMore = false;
|
|
153
|
+
if (searchType === 'document' || searchType === 'both') {
|
|
154
|
+
const docResult = await this.searchDocuments(searchKey, MAX_TOTAL_RESULTS, docOffset);
|
|
155
|
+
if (docResult.items?.length > 0) {
|
|
156
|
+
documents.push(...docResult.items);
|
|
157
|
+
documentOffset = docResult.nextOffset;
|
|
158
|
+
documentHasMore = docResult.hasMore;
|
|
159
|
+
Logger.debug(`文档搜索: 获取 ${docResult.items.length} 条,新offset: ${documentOffset}, hasMore: ${documentHasMore}`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
documentHasMore = false;
|
|
163
|
+
Logger.debug('文档搜索: 无结果');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (searchType === 'wiki' || searchType === 'both') {
|
|
167
|
+
const remainingCount = MAX_TOTAL_RESULTS - documents.length;
|
|
168
|
+
if (remainingCount > 0) {
|
|
169
|
+
const wikiResult = await this.searchWikiNodes(searchKey, remainingCount, pageToken);
|
|
170
|
+
if (wikiResult.items?.length > 0) {
|
|
171
|
+
wikis.push(...wikiResult.items);
|
|
172
|
+
wikiPageToken = wikiResult.pageToken;
|
|
173
|
+
wikiHasMore = wikiResult.hasMore;
|
|
174
|
+
Logger.debug(`知识库搜索: 获取 ${wikiResult.items.length} 条,pageToken: ${wikiPageToken || '无'}, hasMore: ${wikiHasMore}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
wikiHasMore = false;
|
|
178
|
+
Logger.debug('知识库搜索: 无结果');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
Logger.info(`已达到总限制 ${MAX_TOTAL_RESULTS} 条,不再获取知识库`);
|
|
183
|
+
wikiHasMore = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const paginationGuide = this.generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken);
|
|
187
|
+
const total = documents.length + wikis.length;
|
|
188
|
+
const hasMore = documentHasMore || wikiHasMore;
|
|
189
|
+
Logger.info(`统一搜索完成,文档: ${documents.length} 条, 知识库: ${wikis.length} 条, 总计: ${total} 条, hasMore: ${hasMore}`);
|
|
190
|
+
const result = { paginationGuide };
|
|
191
|
+
if (searchType === 'document' || searchType === 'both')
|
|
192
|
+
result.documents = documents;
|
|
193
|
+
if (searchType === 'wiki' || searchType === 'both')
|
|
194
|
+
result.wikis = wikis;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.handleApiError(error, '统一搜索失败');
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken) {
|
|
203
|
+
const guide = { hasMore: documentHasMore || wikiHasMore, description: '' };
|
|
204
|
+
if (!guide.hasMore) {
|
|
205
|
+
guide.description = '没有更多结果了';
|
|
206
|
+
return guide;
|
|
207
|
+
}
|
|
208
|
+
if (searchType === 'document') {
|
|
209
|
+
if (documentHasMore) {
|
|
210
|
+
guide.nextPageParams = { searchType: 'document', offset: documentOffset };
|
|
211
|
+
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = document offset=${documentOffset} 获取文档的下一页`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (searchType === 'wiki') {
|
|
215
|
+
if (wikiHasMore && wikiPageToken) {
|
|
216
|
+
guide.nextPageParams = { searchType: 'wiki', pageToken: wikiPageToken };
|
|
217
|
+
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (searchType === 'both') {
|
|
221
|
+
if (documentHasMore) {
|
|
222
|
+
guide.nextPageParams = { searchType: 'both', offset: documentOffset };
|
|
223
|
+
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = both offset=${documentOffset} 获取文档的下一页`;
|
|
224
|
+
}
|
|
225
|
+
else if (wikiHasMore && wikiPageToken) {
|
|
226
|
+
guide.nextPageParams = { searchType: 'wiki', pageToken: wikiPageToken };
|
|
227
|
+
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页wiki结果`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return guide;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Logger } from '../../utils/logger.js';
|
|
2
|
+
import { ParamUtils } from '../../utils/paramUtils.js';
|
|
3
|
+
import { FeishuBaseApiService } from './FeishuBaseApiService.js';
|
|
4
|
+
/**
|
|
5
|
+
* 飞书画板(Whiteboard)服务
|
|
6
|
+
*/
|
|
7
|
+
export class FeishuWhiteboardService extends FeishuBaseApiService {
|
|
8
|
+
constructor(authService) {
|
|
9
|
+
super(authService);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 获取画板的所有节点内容
|
|
13
|
+
* @param whiteboardId 画板 ID、Token 或包含画板 ID 的 URL
|
|
14
|
+
* @returns 画板节点数据,包含 nodes 数组,每项含节点 ID、类型、位置等信息
|
|
15
|
+
*/
|
|
16
|
+
async getWhiteboardContent(whiteboardId) {
|
|
17
|
+
try {
|
|
18
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
19
|
+
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes`;
|
|
20
|
+
Logger.info(`开始获取画板内容,画板ID: ${normalizedWhiteboardId}`);
|
|
21
|
+
const response = await this.get(endpoint);
|
|
22
|
+
Logger.info(`画板内容获取成功,节点数量: ${response.nodes?.length || 0}`);
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
this.handleApiError(error, '获取画板内容失败');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 获取画板的缩略图,以二进制数据返回
|
|
31
|
+
* @param whiteboardId 画板 ID、Token 或包含画板 ID 的 URL
|
|
32
|
+
* @returns 缩略图的二进制 Buffer 数据(PNG 格式)
|
|
33
|
+
*/
|
|
34
|
+
async getWhiteboardThumbnail(whiteboardId) {
|
|
35
|
+
try {
|
|
36
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
37
|
+
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/download_as_image`;
|
|
38
|
+
Logger.info(`开始获取画板缩略图,画板ID: ${normalizedWhiteboardId}`);
|
|
39
|
+
const response = await this.request(endpoint, 'GET', {}, true, {}, 'arraybuffer');
|
|
40
|
+
const thumbnailBuffer = Buffer.from(response);
|
|
41
|
+
Logger.info(`画板缩略图获取成功,大小: ${thumbnailBuffer.length} 字节`);
|
|
42
|
+
return thumbnailBuffer;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.handleApiError(error, '获取画板缩略图失败');
|
|
46
|
+
return Buffer.from([]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 在画板中创建图表节点,支持 PlantUML 和 Mermaid 两种语法
|
|
51
|
+
* 注意:style_type 固定为 1(画板样式),会解析为多个画板节点
|
|
52
|
+
* @param whiteboardId 画板 ID、Token 或包含画板 ID 的 URL
|
|
53
|
+
* @param code 图表代码,PlantUML 或 Mermaid 语法字符串
|
|
54
|
+
* @param syntaxType 语法类型,1 = PlantUML,2 = Mermaid
|
|
55
|
+
* @returns 创建的图表节点信息
|
|
56
|
+
*/
|
|
57
|
+
async createDiagramNode(whiteboardId, code, syntaxType) {
|
|
58
|
+
try {
|
|
59
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
60
|
+
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes/plantuml`;
|
|
61
|
+
const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
|
|
62
|
+
Logger.info(`开始在画板中创建 ${syntaxTypeName} 节点,画板ID: ${normalizedWhiteboardId}`);
|
|
63
|
+
Logger.debug(`${syntaxTypeName} 代码: ${code.substring(0, 200)}...`);
|
|
64
|
+
const payload = {
|
|
65
|
+
plant_uml_code: code,
|
|
66
|
+
style_type: 1,
|
|
67
|
+
syntax_type: syntaxType
|
|
68
|
+
};
|
|
69
|
+
Logger.debug(`请求载荷: ${JSON.stringify(payload, null, 2)}`);
|
|
70
|
+
const response = await this.post(endpoint, payload);
|
|
71
|
+
Logger.info(`${syntaxTypeName} 节点创建成功`);
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
|
|
76
|
+
Logger.error(`创建 ${syntaxTypeName} 节点失败,画板ID: ${whiteboardId}`, error);
|
|
77
|
+
this.handleApiError(error, `创建 ${syntaxTypeName} 节点失败`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Logger } from '../../utils/logger.js';
|
|
2
|
+
import { FeishuBaseApiService } from './FeishuBaseApiService.js';
|
|
3
|
+
/**
|
|
4
|
+
* 飞书知识库(Wiki)服务
|
|
5
|
+
* 负责知识空间及节点的查询与创建
|
|
6
|
+
*/
|
|
7
|
+
export class FeishuWikiService extends FeishuBaseApiService {
|
|
8
|
+
constructor(authService) {
|
|
9
|
+
super(authService);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 获取当前用户可见的所有知识空间列表,自动处理分页直到获取全部数据
|
|
13
|
+
* @param pageSize 每次分页请求的数量,默认 20
|
|
14
|
+
* @returns 知识空间对象数组,每项包含 space_id、name、description 等字段
|
|
15
|
+
*/
|
|
16
|
+
async getAllWikiSpacesList(pageSize = 20) {
|
|
17
|
+
try {
|
|
18
|
+
Logger.info(`开始获取所有知识空间列表,每页数量: ${pageSize}`);
|
|
19
|
+
const endpoint = '/wiki/v2/spaces';
|
|
20
|
+
let allItems = [];
|
|
21
|
+
let pageToken = undefined;
|
|
22
|
+
let hasMore = true;
|
|
23
|
+
while (hasMore) {
|
|
24
|
+
const params = { page_size: pageSize };
|
|
25
|
+
if (pageToken)
|
|
26
|
+
params.page_token = pageToken;
|
|
27
|
+
Logger.debug(`请求知识空间列表,page_token: ${pageToken || 'null'}, page_size: ${pageSize}`);
|
|
28
|
+
const response = await this.get(endpoint, params);
|
|
29
|
+
if (response?.items) {
|
|
30
|
+
allItems = [...allItems, ...response.items];
|
|
31
|
+
hasMore = response.has_more || false;
|
|
32
|
+
pageToken = response.page_token;
|
|
33
|
+
Logger.debug(`当前页获取到 ${response.items.length} 个知识空间,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
hasMore = false;
|
|
37
|
+
Logger.warn('知识空间列表响应格式异常:', JSON.stringify(response, null, 2));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
Logger.info(`知识空间列表获取完成,共 ${allItems.length} 个空间`);
|
|
41
|
+
return allItems;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.handleApiError(error, '获取知识空间列表失败');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 获取指定知识空间下的所有子节点,自动处理分页直到获取全部数据
|
|
49
|
+
* @param spaceId 知识空间 ID
|
|
50
|
+
* @param parentNodeToken 父节点 Token,不传则获取根节点下的直属子节点
|
|
51
|
+
* @param pageSize 每次分页请求的数量,默认 20
|
|
52
|
+
* @returns 节点对象数组,每项包含 node_token、obj_token、title、obj_type 等字段
|
|
53
|
+
*/
|
|
54
|
+
async getAllWikiSpaceNodes(spaceId, parentNodeToken, pageSize = 20) {
|
|
55
|
+
try {
|
|
56
|
+
Logger.info(`开始获取知识空间子节点列表,space_id: ${spaceId}, parent_node_token: ${parentNodeToken || 'null'}`);
|
|
57
|
+
const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
|
|
58
|
+
let allItems = [];
|
|
59
|
+
let pageToken = undefined;
|
|
60
|
+
let hasMore = true;
|
|
61
|
+
while (hasMore) {
|
|
62
|
+
const params = { page_size: pageSize };
|
|
63
|
+
if (parentNodeToken)
|
|
64
|
+
params.parent_node_token = parentNodeToken;
|
|
65
|
+
if (pageToken)
|
|
66
|
+
params.page_token = pageToken;
|
|
67
|
+
Logger.debug(`请求知识空间子节点列表,page_token: ${pageToken || 'null'}`);
|
|
68
|
+
const response = await this.get(endpoint, params);
|
|
69
|
+
if (response?.items) {
|
|
70
|
+
allItems = [...allItems, ...response.items];
|
|
71
|
+
hasMore = response.has_more || false;
|
|
72
|
+
pageToken = response.page_token;
|
|
73
|
+
Logger.debug(`当前页获取到 ${response.items.length} 个子节点,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
hasMore = false;
|
|
77
|
+
Logger.warn('知识空间子节点列表响应格式异常:', JSON.stringify(response, null, 2));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Logger.info(`知识空间子节点列表获取完成,共 ${allItems.length} 个节点`);
|
|
81
|
+
return allItems;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.handleApiError(error, '获取知识空间子节点列表失败');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 获取指定知识空间的详细信息
|
|
89
|
+
* 当 spaceId 传入 'my_library' 时,获取当前用户的"我的知识库"信息
|
|
90
|
+
* @param spaceId 知识空间 ID,或传入 'my_library' 获取个人知识库
|
|
91
|
+
* @param lang 语言参数,仅当 spaceId 为 'my_library' 时有效,默认 'en'
|
|
92
|
+
* @returns 知识空间信息对象,包含 space_id、name、description 等字段
|
|
93
|
+
*/
|
|
94
|
+
async getWikiSpaceInfo(spaceId, lang = 'en') {
|
|
95
|
+
try {
|
|
96
|
+
const params = {};
|
|
97
|
+
if (spaceId === 'my_library')
|
|
98
|
+
params.lang = lang;
|
|
99
|
+
const response = await this.get(`/wiki/v2/spaces/${spaceId}`, params);
|
|
100
|
+
Logger.debug(`获取知识空间信息成功 (space_id: ${spaceId}):`, response);
|
|
101
|
+
return response?.space || response;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.handleApiError(error, `获取知识空间信息失败 (space_id: ${spaceId})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 在知识空间中创建一个新的文档节点(类型固定为 docx)
|
|
109
|
+
* @param spaceId 目标知识空间 ID
|
|
110
|
+
* @param title 节点标题
|
|
111
|
+
* @param parentNodeToken 父节点 Token,不传则在根目录下创建
|
|
112
|
+
* @returns 创建的节点信息,包含 node_token(节点 ID)和 obj_token(关联文档 ID)
|
|
113
|
+
*/
|
|
114
|
+
async createWikiSpaceNode(spaceId, title, parentNodeToken) {
|
|
115
|
+
try {
|
|
116
|
+
Logger.info(`开始创建知识空间节点,space_id: ${spaceId}, title: ${title}, parent_node_token: ${parentNodeToken || 'null(根节点)'}`);
|
|
117
|
+
const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
|
|
118
|
+
const payload = { title, obj_type: 'docx', node_type: 'origin' };
|
|
119
|
+
if (parentNodeToken)
|
|
120
|
+
payload.parent_node_token = parentNodeToken;
|
|
121
|
+
const response = await this.post(endpoint, payload);
|
|
122
|
+
if (response?.data?.node) {
|
|
123
|
+
const node = response.data.node;
|
|
124
|
+
Logger.info(`知识空间节点创建成功,node_token: ${node.node_token}, obj_token: ${node.obj_token}`);
|
|
125
|
+
return node;
|
|
126
|
+
}
|
|
127
|
+
Logger.info('知识空间节点创建成功');
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.handleApiError(error, '创建知识空间节点失败');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|