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
package/dist/mcp/feishuMcp.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { FeishuApiService } from '../services/feishuApiService.js';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { registerFeishuFolderTools } from './tools/feishuFolderTools.js';
|
|
4
|
+
import { Config } from '../utils/config.js';
|
|
5
|
+
import { ModuleRegistry } from '../modules/ModuleRegistry.js';
|
|
7
6
|
export const serverInfo = {
|
|
8
7
|
name: "Feishu MCP Server",
|
|
9
|
-
version: "0.2.
|
|
8
|
+
version: "0.2.7",
|
|
10
9
|
};
|
|
11
10
|
const serverOptions = {
|
|
12
11
|
capabilities: { logging: {}, tools: {} },
|
|
13
12
|
};
|
|
14
13
|
/**
|
|
15
14
|
* 飞书MCP服务类
|
|
16
|
-
* 继承自McpServer
|
|
15
|
+
* 继承自McpServer,根据 FEISHU_ENABLED_MODULES 配置动态注册工具
|
|
17
16
|
*/
|
|
18
17
|
export class FeishuMcp extends McpServer {
|
|
19
|
-
/**
|
|
20
|
-
* 构造函数
|
|
21
|
-
*/
|
|
22
18
|
constructor() {
|
|
23
19
|
super(serverInfo, serverOptions);
|
|
24
20
|
Object.defineProperty(this, "feishuService", {
|
|
@@ -27,23 +23,15 @@ export class FeishuMcp extends McpServer {
|
|
|
27
23
|
writable: true,
|
|
28
24
|
value: null
|
|
29
25
|
});
|
|
30
|
-
// 初始化飞书服务
|
|
31
26
|
this.initFeishuService();
|
|
32
|
-
|
|
33
|
-
if (this.feishuService) {
|
|
34
|
-
this.registerAllTools();
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
27
|
+
if (!this.feishuService) {
|
|
37
28
|
Logger.error('无法注册飞书工具: 飞书服务初始化失败');
|
|
38
29
|
throw new Error('飞书服务初始化失败');
|
|
39
30
|
}
|
|
31
|
+
this.registerModuleTools(this.feishuService);
|
|
40
32
|
}
|
|
41
|
-
/**
|
|
42
|
-
* 初始化飞书API服务
|
|
43
|
-
*/
|
|
44
33
|
initFeishuService() {
|
|
45
34
|
try {
|
|
46
|
-
// 使用单例模式获取飞书服务实例
|
|
47
35
|
this.feishuService = FeishuApiService.getInstance();
|
|
48
36
|
Logger.info('飞书服务初始化成功');
|
|
49
37
|
}
|
|
@@ -53,15 +41,27 @@ export class FeishuMcp extends McpServer {
|
|
|
53
41
|
}
|
|
54
42
|
}
|
|
55
43
|
/**
|
|
56
|
-
*
|
|
44
|
+
* 根据已启用模块动态注册 MCP 工具
|
|
45
|
+
* task、calendar、member 仅 user 认证时加载
|
|
57
46
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
registerModuleTools(service) {
|
|
48
|
+
const config = Config.getInstance();
|
|
49
|
+
const enabledIds = config.features.enabledModules;
|
|
50
|
+
const authType = config.feishu.authType;
|
|
51
|
+
const enabledModules = ModuleRegistry.getEnabledModules(enabledIds, authType);
|
|
52
|
+
if (authType === 'tenant' && (enabledIds.includes('task') || enabledIds.includes('calendar') || enabledIds.includes('all'))) {
|
|
53
|
+
Logger.info('task、calendar、member 模块需 user 认证,当前为 tenant 模式,已跳过');
|
|
54
|
+
}
|
|
55
|
+
if (enabledModules.length === 0) {
|
|
56
|
+
Logger.warn(`未找到有效的功能模块,请检查 FEISHU_ENABLED_MODULES 配置(当前值: ${enabledIds.join(', ')})`);
|
|
57
|
+
Logger.warn(`可用模块: ${ModuleRegistry.getAllModuleIds().join(', ')}`);
|
|
60
58
|
return;
|
|
61
59
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
const toolCounts = [];
|
|
61
|
+
for (const mod of enabledModules) {
|
|
62
|
+
mod.registerTools(this, service);
|
|
63
|
+
toolCounts.push(`${mod.name}(${mod.id})`);
|
|
64
|
+
}
|
|
65
|
+
Logger.info(`已加载 ${enabledModules.length} 个功能模块: ${toolCounts.join(', ')}`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatErrorMessage } from '../../utils/error.js';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import { detectMimeType } from '../../utils/document.js';
|
|
5
|
+
import { DocumentIdSchema, ParentBlockIdSchema, IndexSchema, StartIndexSchema, EndIndexSchema, BlockTextUpdatesArraySchema, BlockConfigSchema, MediaIdSchema, MediaExtraSchema, ImagesArraySchema, TableCreateSchema, WhiteboardFillArraySchema, WhiteboardIdSchema, } from '../../types/feishuSchema.js';
|
|
6
|
+
import { WHITEBOARD_NODE_THUMBNAIL_THRESHOLD, BATCH_SIZE, WIKI_NOTE, errorResponse, prepareBlockContents, extractSpecialBlocks, buildSpecialBlockHints, extractFeishuApiError, } from './toolHelpers.js';
|
|
7
|
+
/**
|
|
8
|
+
* 注册飞书块相关的MCP工具
|
|
9
|
+
*/
|
|
10
|
+
export function registerBlockTools(server, feishuService) {
|
|
11
|
+
// 批量更新块文本内容工具
|
|
12
|
+
server.tool('batch_update_feishu_block_text', 'Updates text content and styling of multiple document blocks. ' + WIKI_NOTE, {
|
|
13
|
+
documentId: DocumentIdSchema,
|
|
14
|
+
updates: BlockTextUpdatesArraySchema,
|
|
15
|
+
}, async ({ documentId, updates }) => {
|
|
16
|
+
try {
|
|
17
|
+
Logger.info(`开始批量更新飞书块文本,文档ID: ${documentId},块数量: ${updates.length}`);
|
|
18
|
+
const result = await feishuService.batchUpdateBlocksTextContent(documentId, updates);
|
|
19
|
+
Logger.info(`飞书块文本批量更新成功,共更新 ${updates.length} 个块`);
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
updatedCount: updates.length,
|
|
25
|
+
blockIds: updates.map(u => u.blockId),
|
|
26
|
+
document_revision_id: result?.document_revision_id,
|
|
27
|
+
}, null, 2),
|
|
28
|
+
}],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
Logger.error(`批量更新飞书块文本内容失败:`, error);
|
|
33
|
+
return errorResponse(`批量更新飞书块文本内容失败: ${formatErrorMessage(error)}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// 添加通用飞书块创建工具(支持文本、代码、标题)
|
|
37
|
+
server.tool('batch_create_feishu_blocks', 'Creates one or more blocks at a specified position within a Feishu document. Supports text, code, heading, list, image, mermaid, and whiteboard block types. Accepts any number of blocks. ' + WIKI_NOTE, {
|
|
38
|
+
documentId: DocumentIdSchema,
|
|
39
|
+
parentBlockId: ParentBlockIdSchema,
|
|
40
|
+
index: IndexSchema,
|
|
41
|
+
blocks: z.array(BlockConfigSchema).describe('Array of block configurations to create. Pass as a JSON array, not a serialized string.\n' +
|
|
42
|
+
'Example: [{blockType:"text",options:{text:{textStyles:[{text:"Hello",style:{bold:true}}]}}},{blockType:"heading",options:{heading:{level:1,content:"My Title"}}}]'),
|
|
43
|
+
}, async ({ documentId, parentBlockId, index = 0, blocks }) => {
|
|
44
|
+
try {
|
|
45
|
+
// 防御性检查:AI 客户端有时会错误地将数组序列化为字符串传入
|
|
46
|
+
if (typeof blocks === 'string') {
|
|
47
|
+
return errorResponse('错误:blocks 参数传入了字符串而不是数组,请直接传入 JSON 数组。\n' +
|
|
48
|
+
'正确:blocks:[{blockType:"text",options:{...}}]\n' +
|
|
49
|
+
'错误:blocks:"[{blockType:\\"text\\"...}]"');
|
|
50
|
+
}
|
|
51
|
+
const totalBatches = Math.ceil(blocks.length / BATCH_SIZE);
|
|
52
|
+
const results = [];
|
|
53
|
+
let currentStartIndex = index;
|
|
54
|
+
let createdBlocksCount = 0;
|
|
55
|
+
Logger.info(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${blocks.length},分批数: ${totalBatches}`);
|
|
56
|
+
for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
|
|
57
|
+
const currentBatch = blocks.slice(batchNum * BATCH_SIZE, (batchNum + 1) * BATCH_SIZE);
|
|
58
|
+
Logger.info(`处理第 ${batchNum + 1}/${totalBatches} 批,起始位置: ${currentStartIndex},块数量: ${currentBatch.length}`);
|
|
59
|
+
const prepared = prepareBlockContents(currentBatch, feishuService);
|
|
60
|
+
if (!prepared.ok)
|
|
61
|
+
return prepared.error;
|
|
62
|
+
try {
|
|
63
|
+
const batchResult = await feishuService.createDocumentBlocks(documentId, parentBlockId, prepared.contents, currentStartIndex);
|
|
64
|
+
results.push(batchResult);
|
|
65
|
+
createdBlocksCount += prepared.contents.length;
|
|
66
|
+
currentStartIndex = index + createdBlocksCount;
|
|
67
|
+
Logger.info(`第 ${batchNum + 1}/${totalBatches} 批创建成功,当前已创建 ${createdBlocksCount} 个块`);
|
|
68
|
+
}
|
|
69
|
+
catch (batchError) {
|
|
70
|
+
Logger.error(`第 ${batchNum + 1}/${totalBatches} 批创建失败:`, batchError);
|
|
71
|
+
return errorResponse(`批量创建飞书块失败:已成功创建 ${createdBlocksCount} 个块,还有 ${blocks.length - createdBlocksCount} 个块未能创建。\n\n` +
|
|
72
|
+
`错误信息: ${formatErrorMessage(batchError)}\n\n` +
|
|
73
|
+
`如需继续,请使用 get_feishu_document_blocks 确认当前状态后,从索引位置 ${currentStartIndex} 继续创建剩余块。`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
Logger.info(`所有批次创建成功,共创建 ${createdBlocksCount} 个块`);
|
|
77
|
+
const allChildren = results.flatMap(r => r.children ?? []);
|
|
78
|
+
const { imageBlocks, whiteboardBlocks } = extractSpecialBlocks(allChildren);
|
|
79
|
+
return {
|
|
80
|
+
content: [{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: JSON.stringify({
|
|
83
|
+
totalBlocksCreated: createdBlocksCount,
|
|
84
|
+
nextIndex: currentStartIndex,
|
|
85
|
+
document_revision_id: results[results.length - 1]?.document_revision_id,
|
|
86
|
+
...buildSpecialBlockHints(imageBlocks, whiteboardBlocks),
|
|
87
|
+
}, null, 2),
|
|
88
|
+
}],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
Logger.error(`批量创建飞书块失败:`, error);
|
|
93
|
+
return errorResponse(`批量创建飞书块失败: ${formatErrorMessage(error)}\n\n` +
|
|
94
|
+
`建议使用 get_feishu_document_blocks 工具获取文档当前状态,确认是否有部分内容已创建成功。`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// 添加删除文档块工具
|
|
98
|
+
server.tool('delete_feishu_document_blocks', 'Deletes a consecutive range of blocks from a Feishu document identified by startIndex (inclusive) and endIndex (exclusive). ' + WIKI_NOTE, {
|
|
99
|
+
documentId: DocumentIdSchema,
|
|
100
|
+
parentBlockId: ParentBlockIdSchema,
|
|
101
|
+
startIndex: StartIndexSchema,
|
|
102
|
+
endIndex: EndIndexSchema,
|
|
103
|
+
}, async ({ documentId, parentBlockId, startIndex, endIndex }) => {
|
|
104
|
+
try {
|
|
105
|
+
Logger.info(`开始删除飞书文档块,文档ID: ${documentId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
|
|
106
|
+
const result = await feishuService.deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex);
|
|
107
|
+
Logger.info(`飞书文档块删除成功,文档修订ID: ${result.document_revision_id}`);
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: JSON.stringify({
|
|
112
|
+
deletedRange: { startIndex, endIndex },
|
|
113
|
+
document_revision_id: result.document_revision_id,
|
|
114
|
+
}, null, 2),
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
Logger.error(`删除飞书文档块失败:`, error);
|
|
120
|
+
return errorResponse(`删除飞书文档块失败: ${formatErrorMessage(error)}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// 添加获取图片资源工具
|
|
124
|
+
server.tool('get_feishu_image_resource', 'Downloads an image resource from Feishu by its media ID and returns binary image data. To get the mediaId, extract block.image.token from an image block (block_type=27) returned by get_feishu_document_blocks.', {
|
|
125
|
+
mediaId: MediaIdSchema,
|
|
126
|
+
extra: MediaExtraSchema,
|
|
127
|
+
}, async ({ mediaId, extra = '' }) => {
|
|
128
|
+
try {
|
|
129
|
+
Logger.info(`开始获取飞书图片资源,媒体ID: ${mediaId}`);
|
|
130
|
+
const imageBuffer = await feishuService.getImageResource(mediaId, extra);
|
|
131
|
+
Logger.info(`飞书图片资源获取成功,大小: ${imageBuffer.length} 字节`);
|
|
132
|
+
// 将图片数据转为Base64编码,以便在MCP协议中传输
|
|
133
|
+
const base64Image = imageBuffer.toString('base64');
|
|
134
|
+
const mimeType = detectMimeType(imageBuffer);
|
|
135
|
+
return { content: [{ type: 'image', mimeType, data: base64Image }] };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
Logger.error(`获取飞书图片资源失败:`, error);
|
|
139
|
+
return errorResponse(`获取飞书图片资源失败: ${formatErrorMessage(error)}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// 添加图片上传绑定工具
|
|
143
|
+
server.tool('upload_and_bind_image_to_block', 'Uploads images from local paths or URLs and binds them to existing empty image blocks. This tool is used after creating image blocks with batch_create_feishu_blocks tool. It handles uploading the image media and setting the image content to the specified block IDs. Supports local file paths and HTTP/HTTPS URLs. Each image upload and binding is processed independently, and all results are returned in order.', {
|
|
144
|
+
documentId: DocumentIdSchema,
|
|
145
|
+
images: ImagesArraySchema,
|
|
146
|
+
}, async ({ documentId, images }) => {
|
|
147
|
+
try {
|
|
148
|
+
const results = [];
|
|
149
|
+
for (const { blockId, imagePathOrUrl, fileName } of images) {
|
|
150
|
+
Logger.info(`开始上传图片并绑定到块,文档ID: ${documentId},块ID: ${blockId},图片源: ${imagePathOrUrl}`);
|
|
151
|
+
try {
|
|
152
|
+
const { base64: imageBase64, fileName: detectedFileName } = await feishuService.getImageBase64FromPathOrUrl(imagePathOrUrl);
|
|
153
|
+
const finalFileName = fileName || detectedFileName;
|
|
154
|
+
Logger.info('第1步:上传图片素材');
|
|
155
|
+
const uploadResult = await feishuService.uploadImageMedia(imageBase64, finalFileName, blockId);
|
|
156
|
+
if (!uploadResult?.file_token) {
|
|
157
|
+
throw new Error('上传图片素材失败:无法获取file_token');
|
|
158
|
+
}
|
|
159
|
+
Logger.info(`图片素材上传成功,file_token: ${uploadResult.file_token}`);
|
|
160
|
+
Logger.info('第2步:设置图片块内容');
|
|
161
|
+
const setContentResult = await feishuService.setImageBlockContent(documentId, blockId, uploadResult.file_token);
|
|
162
|
+
Logger.info('图片上传并绑定完成');
|
|
163
|
+
const { client_token: _ct, ...blockResult } = setContentResult?.block ?? {};
|
|
164
|
+
results.push({
|
|
165
|
+
blockId,
|
|
166
|
+
fileToken: uploadResult.file_token,
|
|
167
|
+
block: blockResult,
|
|
168
|
+
document_revision_id: setContentResult.document_revision_id,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
Logger.error(`上传图片并绑定到块失败:`, err);
|
|
173
|
+
results.push({ blockId, error: formatErrorMessage(err) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
Logger.error(`批量上传图片并绑定到块失败:`, error);
|
|
180
|
+
return errorResponse(`批量上传图片并绑定到块失败: ${formatErrorMessage(error)}`);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// 添加创建飞书表格工具
|
|
184
|
+
server.tool('create_feishu_table', 'Creates a table block with specified rows and columns in a Feishu document. Each cell can contain text, list, code, or other block types. ' + WIKI_NOTE, {
|
|
185
|
+
documentId: DocumentIdSchema,
|
|
186
|
+
parentBlockId: ParentBlockIdSchema,
|
|
187
|
+
index: IndexSchema,
|
|
188
|
+
tableConfig: TableCreateSchema,
|
|
189
|
+
}, async ({ documentId, parentBlockId, index = 0, tableConfig }) => {
|
|
190
|
+
try {
|
|
191
|
+
Logger.info(`开始创建飞书表格,文档ID: ${documentId},父块ID: ${parentBlockId},表格大小: ${tableConfig.rowSize}x${tableConfig.columnSize},插入位置: ${index}`);
|
|
192
|
+
const result = await feishuService.createTableBlock(documentId, parentBlockId, tableConfig, index);
|
|
193
|
+
// 从 block_id_relations 中提取坐标 → cellBlockId 的映射(排除子块和表格本身)
|
|
194
|
+
const relations = result.block_id_relations ?? [];
|
|
195
|
+
const cellMap = [];
|
|
196
|
+
const tableBlockId = relations.find((r) => /^table_\d/.test(r.temporary_block_id))?.block_id;
|
|
197
|
+
for (const rel of relations) {
|
|
198
|
+
const m = rel.temporary_block_id.match(/^table_cell(\d+)_(\d+)$/);
|
|
199
|
+
if (m)
|
|
200
|
+
cellMap.push({ row: Number(m[1]), column: Number(m[2]), cellBlockId: rel.block_id });
|
|
201
|
+
}
|
|
202
|
+
const response = {
|
|
203
|
+
document_revision_id: result.document_revision_id,
|
|
204
|
+
tableBlockId,
|
|
205
|
+
cells: cellMap,
|
|
206
|
+
};
|
|
207
|
+
if (result.imageTokens?.length > 0) {
|
|
208
|
+
response.imageBlocks = result.imageTokens.map((t) => ({
|
|
209
|
+
row: t.row, column: t.column, blockId: t.blockId,
|
|
210
|
+
}));
|
|
211
|
+
response.imageReminder = 'Use upload_and_bind_image_to_block to bind images to the listed blockIds.';
|
|
212
|
+
}
|
|
213
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
Logger.error(`创建飞书表格失败:`, error);
|
|
217
|
+
return errorResponse(`创建飞书表格失败: ${formatErrorMessage(error)}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// 添加获取画板内容工具
|
|
221
|
+
server.tool('get_feishu_whiteboard_content', 'Retrieves the content and structure of a Feishu whiteboard. Use this to analyze whiteboard content, extract information, or understand the structure of collaborative diagrams. The whiteboard ID can be obtained from the board.token field when getting document blocks with block_type: 43.', {
|
|
222
|
+
whiteboardId: WhiteboardIdSchema,
|
|
223
|
+
}, async ({ whiteboardId }) => {
|
|
224
|
+
try {
|
|
225
|
+
Logger.info(`开始获取飞书画板内容,画板ID: ${whiteboardId}`);
|
|
226
|
+
const whiteboardContent = await feishuService.getWhiteboardContent(whiteboardId);
|
|
227
|
+
const nodeCount = whiteboardContent.nodes?.length ?? 0;
|
|
228
|
+
Logger.info(`飞书画板内容获取成功,节点数量: ${nodeCount}`);
|
|
229
|
+
if (nodeCount > WHITEBOARD_NODE_THUMBNAIL_THRESHOLD) {
|
|
230
|
+
Logger.info(`画板节点数量过多 (${nodeCount} > ${WHITEBOARD_NODE_THUMBNAIL_THRESHOLD}),返回缩略图`);
|
|
231
|
+
try {
|
|
232
|
+
const thumbnailBuffer = await feishuService.getWhiteboardThumbnail(whiteboardId);
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: 'image', data: thumbnailBuffer.toString('base64'), mimeType: 'image/png' }],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (thumbnailError) {
|
|
238
|
+
Logger.warn(`获取画板缩略图失败,返回基本信息: ${thumbnailError}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { content: [{ type: 'text', text: JSON.stringify(whiteboardContent, null, 2) }] };
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
Logger.error(`获取飞书画板内容失败:`, error);
|
|
245
|
+
return errorResponse(`获取飞书画板内容失败: ${formatErrorMessage(error)}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// 添加批量填充画板工具(支持 PlantUML 和 Mermaid)
|
|
249
|
+
server.tool('fill_whiteboard_with_plantuml', 'Fills whiteboard blocks with PlantUML or Mermaid diagram code. Accepts any number of whiteboards. Returns per-item success/failure details.', {
|
|
250
|
+
whiteboards: WhiteboardFillArraySchema,
|
|
251
|
+
}, async ({ whiteboards }) => {
|
|
252
|
+
try {
|
|
253
|
+
if (whiteboards.length === 0) {
|
|
254
|
+
return errorResponse('错误:画板数组不能为空');
|
|
255
|
+
}
|
|
256
|
+
Logger.info(`开始批量填充画板内容,共 ${whiteboards.length} 个画板`);
|
|
257
|
+
const results = [];
|
|
258
|
+
let successCount = 0;
|
|
259
|
+
let failCount = 0;
|
|
260
|
+
for (const [i, { whiteboardId, code, syntax_type }] of whiteboards.entries()) {
|
|
261
|
+
const syntaxTypeNumber = syntax_type === 'plantuml' ? 1 : 2;
|
|
262
|
+
const syntaxTypeName = syntax_type === 'plantuml' ? 'PlantUML' : 'Mermaid';
|
|
263
|
+
Logger.info(`处理第 ${i + 1}/${whiteboards.length} 个画板,画板ID: ${whiteboardId},语法类型: ${syntaxTypeName}`);
|
|
264
|
+
try {
|
|
265
|
+
const result = await feishuService.createDiagramNode(whiteboardId, code, syntaxTypeNumber);
|
|
266
|
+
Logger.info(`画板填充成功,画板ID: ${whiteboardId}`);
|
|
267
|
+
successCount++;
|
|
268
|
+
results.push({ whiteboardId, syntaxType: syntaxTypeName, status: 'success', nodeId: result.node_id });
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
Logger.error(`画板填充失败,画板ID: ${whiteboardId}`, err);
|
|
272
|
+
failCount++;
|
|
273
|
+
const { message, code: errorCode, logId } = extractFeishuApiError(err);
|
|
274
|
+
results.push({
|
|
275
|
+
whiteboardId,
|
|
276
|
+
syntaxType: syntaxTypeName,
|
|
277
|
+
status: 'failed',
|
|
278
|
+
error: { message, code: errorCode, logId },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
Logger.info(`批量填充画板完成,成功: ${successCount},失败: ${failCount}`);
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: 'text',
|
|
286
|
+
text: JSON.stringify({ total: whiteboards.length, success: successCount, failed: failCount, results }, null, 2),
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
Logger.error(`批量填充画板内容失败:`, error);
|
|
292
|
+
return errorResponse(`批量填充画板内容失败: ${formatErrorMessage(error)}\n\n错误详情: ${JSON.stringify(error, null, 2)}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { formatErrorMessage } from '../../utils/error.js';
|
|
2
|
+
import { Logger } from '../../utils/logger.js';
|
|
3
|
+
import { DocumentIdSchema, DocumentIdOrWikiIdSchema, DocumentTypeSchema, SearchKeySchema, SearchTypeSchema, PageTokenSchema, OffsetSchema, DocumentTitleSchema, FolderTokenOptionalSchema, WikiSpaceNodeContextSchema, } from '../../types/feishuSchema.js';
|
|
4
|
+
import { WIKI_NOTE, errorResponse, validateFolderOrWikiContext, extractSpecialBlocks, appendSpecialBlockTextHints, } from './toolHelpers.js';
|
|
5
|
+
/**
|
|
6
|
+
* 注册飞书文档相关的MCP工具
|
|
7
|
+
*/
|
|
8
|
+
export function registerDocumentTools(server, feishuService) {
|
|
9
|
+
// 添加创建飞书文档工具
|
|
10
|
+
server.tool('create_feishu_document', 'Creates a new Feishu document and returns its information. Supports two modes: (1) Feishu Drive folder mode: use folderToken to create a document in a folder. (2) Wiki space node mode: use wikiContext with spaceId (and optional parentNodeToken) to create a node (document) in a wiki space. IMPORTANT: In wiki spaces, documents are nodes themselves - they can act as parent nodes containing child documents, and can also be edited as regular documents. The created node returns both node_token (node ID, can be used as parentNodeToken for creating child nodes) and obj_token (document ID, can be used for document editing operations like get_feishu_document_blocks, batch_create_feishu_blocks, etc.). Only one mode can be used at a time - provide either folderToken OR wikiContext, not both.', {
|
|
11
|
+
title: DocumentTitleSchema,
|
|
12
|
+
folderToken: FolderTokenOptionalSchema,
|
|
13
|
+
wikiContext: WikiSpaceNodeContextSchema,
|
|
14
|
+
}, async ({ title, folderToken, wikiContext }) => {
|
|
15
|
+
try {
|
|
16
|
+
const validationError = validateFolderOrWikiContext(folderToken, wikiContext);
|
|
17
|
+
if (validationError)
|
|
18
|
+
return validationError;
|
|
19
|
+
// 模式一:飞书文档目录模式
|
|
20
|
+
if (folderToken) {
|
|
21
|
+
Logger.info(`开始创建飞书文档(文件夹模式),标题: ${title},文件夹Token: ${folderToken}`);
|
|
22
|
+
const newDoc = await feishuService.createDocument(title, folderToken);
|
|
23
|
+
if (!newDoc)
|
|
24
|
+
throw new Error('创建文档失败,未返回文档信息');
|
|
25
|
+
Logger.info(`飞书文档创建成功,文档ID: ${newDoc.objToken || newDoc.document_id}`);
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify(newDoc, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
// 模式二:知识库节点模式
|
|
29
|
+
if (!wikiContext)
|
|
30
|
+
return errorResponse('错误:内部参数状态异常。');
|
|
31
|
+
const { spaceId, parentNodeToken } = wikiContext;
|
|
32
|
+
if (!spaceId) {
|
|
33
|
+
return errorResponse('错误:使用 wikiContext 模式时,必须提供 spaceId。');
|
|
34
|
+
}
|
|
35
|
+
Logger.info(`开始创建知识库节点,标题: ${title},知识空间ID: ${spaceId},父节点Token: ${parentNodeToken ?? 'null(根节点)'}`);
|
|
36
|
+
const node = await feishuService.createWikiSpaceNode(spaceId, title, parentNodeToken);
|
|
37
|
+
if (!node)
|
|
38
|
+
throw new Error('创建知识库节点失败,未返回节点信息');
|
|
39
|
+
const result = {
|
|
40
|
+
...node,
|
|
41
|
+
_note: '知识库节点既是节点又是文档:node_token 可作为父节点使用,obj_token 可用于文档编辑操作',
|
|
42
|
+
};
|
|
43
|
+
Logger.info(`知识库节点创建成功,node_token: ${node.node_token}, obj_token: ${node.obj_token}`);
|
|
44
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
Logger.error(`创建文档失败:`, error);
|
|
48
|
+
return errorResponse(`创建文档失败: ${formatErrorMessage(error)}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// 添加获取飞书文档信息工具(支持普通文档和Wiki文档)
|
|
52
|
+
server.tool('get_feishu_document_info', 'Retrieves basic information about a Feishu document or Wiki node. Supports both regular documents (via document ID/URL) and Wiki documents (via Wiki URL/token). Use this to verify a document exists, check access permissions, or get metadata like title, type, and creation information. For Wiki documents, returns complete node information including documentId (obj_token) for document editing operations, and space_id and node_token for creating child nodes.', {
|
|
53
|
+
documentId: DocumentIdOrWikiIdSchema,
|
|
54
|
+
documentType: DocumentTypeSchema,
|
|
55
|
+
}, async ({ documentId, documentType }) => {
|
|
56
|
+
try {
|
|
57
|
+
Logger.info(`开始获取飞书文档信息,文档ID: ${documentId}, 类型: ${documentType ?? 'auto'}`);
|
|
58
|
+
const docInfo = await feishuService.getDocumentInfo(documentId, documentType);
|
|
59
|
+
if (!docInfo)
|
|
60
|
+
throw new Error('获取文档信息失败,未返回数据');
|
|
61
|
+
const title = docInfo.title || docInfo.document?.title || '未知标题';
|
|
62
|
+
Logger.info(`飞书文档信息获取成功,标题: ${title}, 类型: ${docInfo._type ?? 'document'}`);
|
|
63
|
+
return { content: [{ type: 'text', text: JSON.stringify(docInfo, null, 2) }] };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
Logger.error(`获取飞书文档信息失败:`, error);
|
|
67
|
+
return errorResponse(formatErrorMessage(error, '获取飞书文档信息失败'));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// 添加获取飞书文档块工具
|
|
71
|
+
server.tool('get_feishu_document_blocks', 'Retrieves the block hierarchy of a Feishu document, including block IDs, types, and content. ' + WIKI_NOTE, {
|
|
72
|
+
documentId: DocumentIdSchema,
|
|
73
|
+
}, async ({ documentId }) => {
|
|
74
|
+
try {
|
|
75
|
+
Logger.info(`开始获取飞书文档块,文档ID: ${documentId}`);
|
|
76
|
+
const blocks = await feishuService.getDocumentBlocks(documentId);
|
|
77
|
+
Logger.info(`飞书文档块获取成功,共 ${blocks.length} 个块`);
|
|
78
|
+
const { imageBlocks, whiteboardBlocks } = extractSpecialBlocks(blocks);
|
|
79
|
+
const responseText = appendSpecialBlockTextHints(JSON.stringify(blocks, null, 2), imageBlocks, whiteboardBlocks);
|
|
80
|
+
return { content: [{ type: 'text', text: responseText }] };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
Logger.error(`获取飞书文档块失败:`, error);
|
|
84
|
+
return errorResponse(`获取飞书文档块失败: ${formatErrorMessage(error)}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// 添加搜索文档工具(支持文档和知识库搜索)
|
|
88
|
+
server.tool('search_feishu_documents', 'Searches for documents and/or Wiki knowledge base nodes in Feishu. Supports keyword-based search with type filtering (document, wiki, or both). Returns document and wiki information including title, type, and owner. Supports pagination: use offset for document search pagination and pageToken for wiki search pagination. Each type (document or wiki) can return up to 100 results maximum per search. Default page size is 20 items.', {
|
|
89
|
+
searchKey: SearchKeySchema,
|
|
90
|
+
searchType: SearchTypeSchema,
|
|
91
|
+
offset: OffsetSchema,
|
|
92
|
+
pageToken: PageTokenSchema,
|
|
93
|
+
}, async ({ searchKey, searchType, offset, pageToken }) => {
|
|
94
|
+
try {
|
|
95
|
+
Logger.info(`开始搜索,关键字: ${searchKey}, 类型: ${searchType ?? 'both'}, offset: ${offset ?? 0}, pageToken: ${pageToken ?? '无'}`);
|
|
96
|
+
const searchResult = await feishuService.search(searchKey, searchType ?? 'both', offset, pageToken);
|
|
97
|
+
Logger.info(`搜索完成,文档: ${searchResult.documents?.length ?? 0} 条,知识库: ${searchResult.wikis?.length ?? 0} 条`);
|
|
98
|
+
return { content: [{ type: 'text', text: JSON.stringify(searchResult, null, 2) }] };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
Logger.error(`搜索失败:`, error);
|
|
102
|
+
return errorResponse(`搜索失败: ${formatErrorMessage(error)}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|