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.
Files changed (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +356 -266
  3. package/dist/cli/auth.js +117 -0
  4. package/dist/cli/commands/auth.js +141 -0
  5. package/dist/cli/commands/config.js +86 -0
  6. package/dist/cli/commands/guide.js +68 -0
  7. package/dist/cli/dispatcher.js +96 -0
  8. package/dist/cli/index.js +95 -0
  9. package/dist/manager/sseConnectionManager.js +2 -0
  10. package/dist/mcp/feishuMcp.js +25 -25
  11. package/dist/mcp/tools/blockTools.js +295 -0
  12. package/dist/mcp/tools/documentTools.js +105 -0
  13. package/dist/mcp/tools/feishuBlockTools.js +1 -285
  14. package/dist/mcp/tools/feishuTools.js +1 -67
  15. package/dist/mcp/tools/folderTools.js +99 -0
  16. package/dist/mcp/tools/toolHelpers.js +155 -0
  17. package/dist/modules/FeatureModule.js +1 -0
  18. package/dist/modules/ModuleRegistry.js +63 -0
  19. package/dist/modules/calendar/index.js +11 -0
  20. package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
  21. package/dist/modules/calendar/tools/calendarTools.js +6 -0
  22. package/dist/modules/document/index.js +15 -0
  23. package/dist/modules/document/services/FeishuBlockService.js +410 -0
  24. package/dist/modules/document/services/FeishuDocumentService.js +110 -0
  25. package/dist/modules/document/services/FeishuFoldService.js +187 -0
  26. package/dist/modules/document/services/FeishuSearchService.js +232 -0
  27. package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
  28. package/dist/modules/document/services/blockFactory.js +521 -0
  29. package/dist/modules/document/toolApi/blockToolApi.js +160 -0
  30. package/dist/modules/document/toolApi/documentToolApi.js +65 -0
  31. package/dist/modules/document/toolApi/folderToolApi.js +73 -0
  32. package/dist/modules/document/toolApi/index.js +3 -0
  33. package/dist/modules/document/tools/blockTools.js +138 -0
  34. package/dist/modules/document/tools/documentTools.js +64 -0
  35. package/dist/modules/document/tools/folderTools.js +46 -0
  36. package/dist/modules/document/tools/toolHelpers.js +155 -0
  37. package/dist/modules/index.js +5 -0
  38. package/dist/modules/member/index.js +11 -0
  39. package/dist/modules/member/services/FeishuMemberService.js +41 -0
  40. package/dist/modules/member/toolApi/index.js +1 -0
  41. package/dist/modules/member/toolApi/memberToolApi.js +54 -0
  42. package/dist/modules/member/tools/memberTools.js +26 -0
  43. package/dist/modules/task/index.js +11 -0
  44. package/dist/modules/task/services/FeishuTaskService.js +271 -0
  45. package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
  46. package/dist/modules/task/toolApi/index.js +1 -0
  47. package/dist/modules/task/toolApi/taskToolApi.js +128 -0
  48. package/dist/modules/task/tools/taskTools.js +93 -0
  49. package/dist/server.js +43 -24
  50. package/dist/services/baseService.js +11 -2
  51. package/dist/services/blockFactory.js +167 -0
  52. package/dist/services/callbackService.js +1 -1
  53. package/dist/services/constants/feishuScopes.js +94 -0
  54. package/dist/services/feishu/FeishuBaseApiService.js +47 -0
  55. package/dist/services/feishu/FeishuBlockService.js +410 -0
  56. package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
  57. package/dist/services/feishu/FeishuDocumentService.js +110 -0
  58. package/dist/services/feishu/FeishuDriveService.js +62 -0
  59. package/dist/services/feishu/FeishuFoldService.js +187 -0
  60. package/dist/services/feishu/FeishuImageService.js +219 -0
  61. package/dist/services/feishu/FeishuScopeValidator.js +177 -0
  62. package/dist/services/feishu/FeishuSearchService.js +232 -0
  63. package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
  64. package/dist/services/feishu/FeishuWikiService.js +134 -0
  65. package/dist/services/feishuApiService.js +246 -1760
  66. package/dist/services/feishuAuthService.js +43 -0
  67. package/dist/types/documentSchema.js +232 -0
  68. package/dist/types/feishuSchema.js +54 -77
  69. package/dist/types/memberSchema.js +35 -0
  70. package/dist/types/taskSchema.js +166 -0
  71. package/dist/utils/auth/tokenCacheManager.js +16 -3
  72. package/dist/utils/auth/tokenRefreshManager.js +2 -1
  73. package/dist/utils/config.js +47 -0
  74. package/dist/utils/document.js +116 -116
  75. package/dist/utils/error.js +0 -11
  76. package/dist/utils/paramUtils.js +0 -31
  77. package/package.json +77 -76
@@ -0,0 +1,65 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ import { extractSpecialBlocks, appendSpecialBlockTextHints } from '../tools/toolHelpers.js';
3
+ /**
4
+ * 创建飞书文档(文件夹模式或知识库节点模式)
5
+ */
6
+ export async function createDocument(params, api) {
7
+ const { title, folderToken, wikiContext } = params;
8
+ if (folderToken && wikiContext) {
9
+ throw new Error('错误:不能同时提供 folderToken 和 wikiContext 参数,请选择其中一种模式。\n' +
10
+ '- 使用 folderToken 在飞书文档目录中操作\n' +
11
+ '- 使用 wikiContext 在知识库中操作');
12
+ }
13
+ if (!folderToken && !wikiContext) {
14
+ throw new Error('错误:必须提供 folderToken(飞书文档目录模式)或 wikiContext(知识库节点模式)参数之一。');
15
+ }
16
+ if (folderToken) {
17
+ Logger.info(`createDocument invoked: folder mode, title=${title}`);
18
+ const newDoc = await api.createDocument(title, folderToken);
19
+ if (!newDoc)
20
+ throw new Error('创建文档失败,未返回文档信息');
21
+ return newDoc;
22
+ }
23
+ if (!wikiContext)
24
+ throw new Error('错误:内部参数状态异常。');
25
+ const { spaceId, parentNodeToken } = wikiContext;
26
+ if (!spaceId) {
27
+ throw new Error('错误:使用 wikiContext 模式时,必须提供 spaceId。');
28
+ }
29
+ Logger.info(`createDocument invoked: wiki mode, title=${title}, spaceId=${spaceId}`);
30
+ const node = await api.createWikiSpaceNode(spaceId, title, parentNodeToken);
31
+ if (!node)
32
+ throw new Error('创建知识库节点失败,未返回节点信息');
33
+ return {
34
+ ...node,
35
+ _note: '知识库节点既是节点又是文档:node_token 可作为父节点使用,obj_token 可用于文档编辑操作',
36
+ };
37
+ }
38
+ /**
39
+ * 获取飞书文档信息(支持普通文档和 Wiki 文档)
40
+ */
41
+ export async function getDocumentInfo(params, api) {
42
+ const { documentId, documentType } = params;
43
+ Logger.info(`getDocumentInfo invoked: documentId=${documentId}, type=${documentType ?? 'auto'}`);
44
+ const docInfo = await api.getDocumentInfo(documentId, documentType);
45
+ if (!docInfo)
46
+ throw new Error('获取文档信息失败,未返回数据');
47
+ return docInfo;
48
+ }
49
+ /**
50
+ * 获取飞书文档块结构
51
+ */
52
+ export async function getDocumentBlocks(documentId, api) {
53
+ Logger.info(`getDocumentBlocks invoked: documentId=${documentId}`);
54
+ const blocks = await api.getDocumentBlocks(documentId);
55
+ const { imageBlocks, whiteboardBlocks } = extractSpecialBlocks(blocks);
56
+ return appendSpecialBlockTextHints(JSON.stringify(blocks, null, 2), imageBlocks, whiteboardBlocks);
57
+ }
58
+ /**
59
+ * 搜索飞书文档和/或知识库节点
60
+ */
61
+ export async function searchDocuments(params, api) {
62
+ const { searchKey, searchType = 'both', offset, pageToken } = params;
63
+ Logger.info(`searchDocuments invoked: searchKey=${searchKey}, type=${searchType}`);
64
+ return api.search(searchKey, searchType, offset, pageToken);
65
+ }
@@ -0,0 +1,73 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ import { formatErrorMessage } from '../../../utils/error.js';
3
+ /**
4
+ * 获取飞书根文件夹信息、知识空间列表和我的知识库
5
+ */
6
+ export async function getRootFolderInfo(api) {
7
+ Logger.info('getRootFolderInfo invoked');
8
+ const result = {
9
+ root_folder: null,
10
+ wiki_spaces: [],
11
+ my_library: null,
12
+ };
13
+ try {
14
+ const folderInfo = await api.getRootFolderInfo();
15
+ result.root_folder = folderInfo?.data ?? folderInfo;
16
+ }
17
+ catch (error) {
18
+ Logger.error(`获取飞书根文件夹信息失败:`, error);
19
+ result.root_folder = { error: formatErrorMessage(error, '获取根文件夹信息失败') };
20
+ }
21
+ try {
22
+ result.wiki_spaces = (await api.getAllWikiSpacesList(20)) ?? [];
23
+ }
24
+ catch (error) {
25
+ Logger.error(`获取知识空间列表失败:`, error);
26
+ result.wiki_spaces = [];
27
+ }
28
+ try {
29
+ const myLibrary = await api.getWikiSpaceInfo('my_library', 'en');
30
+ const libraryData = myLibrary?.data ?? myLibrary;
31
+ result.my_library = libraryData?.space ?? libraryData;
32
+ }
33
+ catch (error) {
34
+ Logger.error(`获取我的知识库失败:`, error);
35
+ result.my_library = { error: formatErrorMessage(error, '获取我的知识库失败') };
36
+ }
37
+ return result;
38
+ }
39
+ /**
40
+ * 获取文件夹或知识空间节点下的文件列表
41
+ */
42
+ export async function getFolderFiles(params, api) {
43
+ const { folderToken, wikiContext } = params;
44
+ if (folderToken && wikiContext) {
45
+ throw new Error('错误:不能同时提供 folderToken 和 wikiContext 参数,请选择其中一种模式。\n' +
46
+ '- 使用 folderToken 在飞书文档目录中操作\n' +
47
+ '- 使用 wikiContext 在知识库中操作');
48
+ }
49
+ if (!folderToken && !wikiContext) {
50
+ throw new Error('错误:必须提供 folderToken(飞书文档目录模式)或 wikiContext(知识库节点模式)参数之一。');
51
+ }
52
+ if (folderToken) {
53
+ Logger.info(`getFolderFiles invoked: folder mode, folderToken=${folderToken}`);
54
+ return api.getFolderFileList(folderToken);
55
+ }
56
+ if (!wikiContext)
57
+ throw new Error('错误:内部参数状态异常。');
58
+ const { spaceId, parentNodeToken } = wikiContext;
59
+ if (!spaceId) {
60
+ throw new Error('错误:使用 wikiContext 模式时,必须提供 spaceId。');
61
+ }
62
+ Logger.info(`getFolderFiles invoked: wiki mode, spaceId=${spaceId}`);
63
+ const nodeList = await api.getAllWikiSpaceNodes(spaceId, parentNodeToken);
64
+ return { nodes: nodeList ?? [] };
65
+ }
66
+ /**
67
+ * 在指定文件夹下创建子文件夹
68
+ */
69
+ export async function createFolder(params, api) {
70
+ const { folderToken, folderName } = params;
71
+ Logger.info(`createFolder invoked: folderToken=${folderToken}, folderName=${folderName}`);
72
+ return api.createFolder(folderToken, folderName);
73
+ }
@@ -0,0 +1,3 @@
1
+ export { createDocument, getDocumentInfo, getDocumentBlocks, searchDocuments, } from './documentToolApi.js';
2
+ export { batchUpdateBlockText, batchCreateBlocks, deleteDocumentBlocks, getImageResource, uploadAndBindImageToBlock, createTable, getWhiteboardContent, fillWhiteboardWithPlantuml, } from './blockToolApi.js';
3
+ export { getRootFolderInfo, getFolderFiles, createFolder, } from './folderToolApi.js';
@@ -0,0 +1,138 @@
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 { batchUpdateBlockText, batchCreateBlocks, deleteDocumentBlocks, getImageResource, uploadAndBindImageToBlock, createTable, getWhiteboardContent, fillWhiteboardWithPlantuml, } from '../toolApi/blockToolApi.js';
6
+ import { DocumentIdSchema, ParentBlockIdSchema, IndexSchema, StartIndexSchema, EndIndexSchema, BlockTextUpdatesArraySchema, BlockConfigSchema, MediaIdSchema, MediaExtraSchema, ImagesArraySchema, TableCreateSchema, WhiteboardFillArraySchema, WhiteboardIdSchema, } from '../../../types/documentSchema.js';
7
+ import { WIKI_NOTE, errorResponse } from './toolHelpers.js';
8
+ /**
9
+ * 注册飞书块相关的MCP工具
10
+ */
11
+ export function registerBlockTools(server, feishuService) {
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
+ const result = await batchUpdateBlockText({ documentId, updates }, feishuService);
18
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
19
+ }
20
+ catch (error) {
21
+ Logger.error(`批量更新飞书块文本内容失败:`, error);
22
+ return errorResponse(`批量更新飞书块文本内容失败: ${formatErrorMessage(error)}`);
23
+ }
24
+ });
25
+ 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, {
26
+ documentId: DocumentIdSchema,
27
+ parentBlockId: ParentBlockIdSchema,
28
+ index: IndexSchema,
29
+ blocks: z.array(BlockConfigSchema).describe('Array of block configurations to create. Pass as a JSON array, not a serialized string.\n' +
30
+ 'Example: [{blockType:"text",options:{text:{textStyles:[{text:"Hello",style:{bold:true}}]}}},{blockType:"heading",options:{heading:{level:1,content:"My Title"}}}]'),
31
+ }, async ({ documentId, parentBlockId, index = 0, blocks }) => {
32
+ try {
33
+ const result = await batchCreateBlocks({ documentId, parentBlockId, index, blocks }, feishuService);
34
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
35
+ }
36
+ catch (error) {
37
+ Logger.error(`批量创建飞书块失败:`, error);
38
+ return errorResponse(`批量创建飞书块失败: ${formatErrorMessage(error)}\n\n` +
39
+ `建议使用 get_feishu_document_blocks 工具获取文档当前状态,确认是否有部分内容已创建成功。`);
40
+ }
41
+ });
42
+ 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, {
43
+ documentId: DocumentIdSchema,
44
+ parentBlockId: ParentBlockIdSchema,
45
+ startIndex: StartIndexSchema,
46
+ endIndex: EndIndexSchema,
47
+ }, async ({ documentId, parentBlockId, startIndex, endIndex }) => {
48
+ try {
49
+ const result = await deleteDocumentBlocks({ documentId, parentBlockId, startIndex, endIndex }, feishuService);
50
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
51
+ }
52
+ catch (error) {
53
+ Logger.error(`删除飞书文档块失败:`, error);
54
+ return errorResponse(`删除飞书文档块失败: ${formatErrorMessage(error)}`);
55
+ }
56
+ });
57
+ 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.', {
58
+ mediaId: MediaIdSchema,
59
+ extra: MediaExtraSchema,
60
+ }, async ({ mediaId, extra = '' }) => {
61
+ try {
62
+ const imageBuffer = await getImageResource(mediaId, extra, feishuService);
63
+ const base64Image = imageBuffer.toString('base64');
64
+ const mimeType = detectMimeType(imageBuffer);
65
+ return { content: [{ type: 'image', mimeType, data: base64Image }] };
66
+ }
67
+ catch (error) {
68
+ Logger.error(`获取飞书图片资源失败:`, error);
69
+ return errorResponse(`获取飞书图片资源失败: ${formatErrorMessage(error)}`);
70
+ }
71
+ });
72
+ 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.', {
73
+ documentId: DocumentIdSchema,
74
+ images: ImagesArraySchema,
75
+ }, async ({ documentId, images }) => {
76
+ try {
77
+ const results = await uploadAndBindImageToBlock({ documentId, images }, feishuService);
78
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
79
+ }
80
+ catch (error) {
81
+ Logger.error(`批量上传图片并绑定到块失败:`, error);
82
+ return errorResponse(`批量上传图片并绑定到块失败: ${formatErrorMessage(error)}`);
83
+ }
84
+ });
85
+ 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, {
86
+ documentId: DocumentIdSchema,
87
+ parentBlockId: ParentBlockIdSchema,
88
+ index: IndexSchema,
89
+ tableConfig: TableCreateSchema,
90
+ }, async ({ documentId, parentBlockId, index = 0, tableConfig }) => {
91
+ try {
92
+ const result = await createTable({ documentId, parentBlockId, index, tableConfig }, feishuService);
93
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
94
+ }
95
+ catch (error) {
96
+ Logger.error(`创建飞书表格失败:`, error);
97
+ return errorResponse(`创建飞书表格失败: ${formatErrorMessage(error)}`);
98
+ }
99
+ });
100
+ 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.', {
101
+ whiteboardId: WhiteboardIdSchema,
102
+ }, async ({ whiteboardId }) => {
103
+ try {
104
+ const result = await getWhiteboardContent(whiteboardId, feishuService);
105
+ if (result.type === 'thumbnail') {
106
+ return {
107
+ content: [
108
+ { type: 'image', data: result.buffer.toString('base64'), mimeType: 'image/png' },
109
+ ],
110
+ };
111
+ }
112
+ return { content: [{ type: 'text', text: JSON.stringify(result.content, null, 2) }] };
113
+ }
114
+ catch (error) {
115
+ Logger.error(`获取飞书画板内容失败:`, error);
116
+ return errorResponse(`获取飞书画板内容失败: ${formatErrorMessage(error)}`);
117
+ }
118
+ });
119
+ 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.', {
120
+ whiteboards: WhiteboardFillArraySchema,
121
+ }, async ({ whiteboards }) => {
122
+ try {
123
+ const result = await fillWhiteboardWithPlantuml({ whiteboards }, feishuService);
124
+ return {
125
+ content: [
126
+ {
127
+ type: 'text',
128
+ text: JSON.stringify({ total: result.total, success: result.success, failed: result.failed, results: result.results }, null, 2),
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ catch (error) {
134
+ Logger.error(`批量填充画板内容失败:`, error);
135
+ return errorResponse(`批量填充画板内容失败: ${formatErrorMessage(error)}\n\n错误详情: ${JSON.stringify(error, null, 2)}`);
136
+ }
137
+ });
138
+ }
@@ -0,0 +1,64 @@
1
+ import { formatErrorMessage } from '../../../utils/error.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ import { createDocument, getDocumentInfo, getDocumentBlocks, searchDocuments, } from '../toolApi/documentToolApi.js';
4
+ import { WIKI_NOTE, errorResponse, } from './toolHelpers.js';
5
+ import { DocumentIdSchema, DocumentIdOrWikiIdSchema, DocumentTypeSchema, SearchKeySchema, SearchTypeSchema, PageTokenSchema, OffsetSchema, DocumentTitleSchema, FolderTokenOptionalSchema, WikiSpaceNodeContextSchema, } from '../../../types/documentSchema.js';
6
+ /**
7
+ * 注册飞书文档相关的MCP工具
8
+ */
9
+ export function registerDocumentTools(server, feishuService) {
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 result = await createDocument({ title, folderToken, wikiContext }, feishuService);
17
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
18
+ }
19
+ catch (error) {
20
+ Logger.error(`创建文档失败:`, error);
21
+ return errorResponse(`创建文档失败: ${formatErrorMessage(error)}`);
22
+ }
23
+ });
24
+ 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.', {
25
+ documentId: DocumentIdOrWikiIdSchema,
26
+ documentType: DocumentTypeSchema,
27
+ }, async ({ documentId, documentType }) => {
28
+ try {
29
+ const docInfo = await getDocumentInfo({ documentId, documentType }, feishuService);
30
+ return { content: [{ type: 'text', text: JSON.stringify(docInfo, null, 2) }] };
31
+ }
32
+ catch (error) {
33
+ Logger.error(`获取飞书文档信息失败:`, error);
34
+ return errorResponse(formatErrorMessage(error, '获取飞书文档信息失败'));
35
+ }
36
+ });
37
+ server.tool('get_feishu_document_blocks', 'Retrieves the block hierarchy of a Feishu document, including block IDs, types, and content. ' + WIKI_NOTE, {
38
+ documentId: DocumentIdSchema,
39
+ }, async ({ documentId }) => {
40
+ try {
41
+ const responseText = await getDocumentBlocks(documentId, feishuService);
42
+ return { content: [{ type: 'text', text: responseText }] };
43
+ }
44
+ catch (error) {
45
+ Logger.error(`获取飞书文档块失败:`, error);
46
+ return errorResponse(`获取飞书文档块失败: ${formatErrorMessage(error)}`);
47
+ }
48
+ });
49
+ 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.', {
50
+ searchKey: SearchKeySchema,
51
+ searchType: SearchTypeSchema,
52
+ offset: OffsetSchema,
53
+ pageToken: PageTokenSchema,
54
+ }, async ({ searchKey, searchType, offset, pageToken }) => {
55
+ try {
56
+ const searchResult = await searchDocuments({ searchKey, searchType, offset, pageToken }, feishuService);
57
+ return { content: [{ type: 'text', text: JSON.stringify(searchResult, null, 2) }] };
58
+ }
59
+ catch (error) {
60
+ Logger.error(`搜索失败:`, error);
61
+ return errorResponse(`搜索失败: ${formatErrorMessage(error)}`);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,46 @@
1
+ import { formatErrorMessage } from '../../../utils/error.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ import { getRootFolderInfo, getFolderFiles, createFolder, } from '../toolApi/folderToolApi.js';
4
+ import { FolderTokenSchema, FolderTokenOptionalSchema, FolderNameSchema, WikiSpaceNodeContextSchema, } from '../../../types/documentSchema.js';
5
+ import { errorResponse } from './toolHelpers.js';
6
+ /**
7
+ * 注册飞书文件夹相关的MCP工具
8
+ */
9
+ export function registerFolderTools(server, feishuService) {
10
+ server.tool('get_feishu_root_folder_info', 'Retrieves the root folder in Feishu Drive, wiki spaces list, and "My Library". Use this when you need to browse folders or wiki spaces from the root. If you know the wiki node name, you can also use search_feishu_documents to directly locate specific wiki nodes instead of traversing from root. Returns root folder token, all wiki spaces, and personal library information.', {}, async () => {
11
+ try {
12
+ const result = await getRootFolderInfo(feishuService);
13
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
14
+ }
15
+ catch (error) {
16
+ Logger.error(`获取飞书信息失败:`, error);
17
+ return errorResponse(formatErrorMessage(error, '获取飞书信息失败'));
18
+ }
19
+ });
20
+ server.tool('get_feishu_folder_files', 'Retrieves a list of files and subfolders in a specified folder or wiki space node. Supports two modes: (1) Feishu Drive folder mode: use folderToken to get files in a Feishu Drive folder. (2) Wiki space node mode: use wikiContext with spaceId (and optional parentNodeToken) to get documents under a wiki space node. If parentNodeToken is not provided, retrieves nodes from the root of the wiki space. Only one mode can be used at a time - provide either folderToken OR wikiContext.', {
21
+ folderToken: FolderTokenOptionalSchema,
22
+ wikiContext: WikiSpaceNodeContextSchema,
23
+ }, async ({ folderToken, wikiContext }) => {
24
+ try {
25
+ const result = await getFolderFiles({ folderToken, wikiContext }, feishuService);
26
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
27
+ }
28
+ catch (error) {
29
+ Logger.error(`获取文件列表失败:`, error);
30
+ return errorResponse(`获取文件列表失败: ${formatErrorMessage(error)}`);
31
+ }
32
+ });
33
+ server.tool('create_feishu_folder', 'Creates a new folder in a specified parent folder. Use this to organize documents and files within your Feishu Drive structure. Returns the token and URL of the newly created folder.', {
34
+ folderToken: FolderTokenSchema,
35
+ folderName: FolderNameSchema,
36
+ }, async ({ folderToken, folderName }) => {
37
+ try {
38
+ const result = await createFolder({ folderToken, folderName }, feishuService);
39
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
40
+ }
41
+ catch (error) {
42
+ Logger.error(`创建飞书文件夹失败:`, error);
43
+ return errorResponse(`创建飞书文件夹失败: ${formatErrorMessage(error)}`);
44
+ }
45
+ });
46
+ }
@@ -0,0 +1,155 @@
1
+ import { formatErrorMessage } from '../../../utils/error.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ export const WHITEBOARD_NODE_THUMBNAIL_THRESHOLD = 200;
4
+ export const BATCH_SIZE = 50;
5
+ /** wiki 链接转换提示,在所有需要 documentId 的编辑类工具中复用 */
6
+ export const WIKI_NOTE = 'For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx), ' +
7
+ 'use get_feishu_document_info first to obtain the documentId, then use that ID for editing operations.';
8
+ const BLOCK_TYPE_IMAGE = 27;
9
+ const BLOCK_TYPE_WHITEBOARD = 43;
10
+ export function errorResponse(text) {
11
+ return { content: [{ type: 'text', text }] };
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // 参数校验
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * 校验 folderToken 与 wikiContext 的互斥关系(文件夹模式 vs 知识库模式)。
18
+ * 返回 null 表示校验通过,否则返回可直接 return 的错误响应。
19
+ */
20
+ export function validateFolderOrWikiContext(folderToken, wikiContext) {
21
+ if (folderToken && wikiContext) {
22
+ return errorResponse('错误:不能同时提供 folderToken 和 wikiContext 参数,请选择其中一种模式。\n' +
23
+ '- 使用 folderToken 在飞书文档目录中操作\n' +
24
+ '- 使用 wikiContext 在知识库中操作');
25
+ }
26
+ if (!folderToken && !wikiContext) {
27
+ return errorResponse('错误:必须提供 folderToken(飞书文档目录模式)或 wikiContext(知识库节点模式)参数之一。');
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * 将 BlockConfig 数组转换为飞书 API 所需的块内容数组。
33
+ * 遇到单个块处理失败时立即终止。
34
+ *
35
+ * 注:contents 类型为 any[] 以匹配 FeishuApiService.createDocumentBlocks 的参数类型。
36
+ */
37
+ export function prepareBlockContents(configs, feishuService) {
38
+ const contents = [];
39
+ for (const { blockType, options = {} } of configs) {
40
+ try {
41
+ const blockContent = feishuService.createBlockContent(blockType, options);
42
+ if (blockContent) {
43
+ contents.push(blockContent);
44
+ Logger.info(`已准备 ${blockType} 块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
45
+ }
46
+ else {
47
+ Logger.warn(`创建 ${blockType} 块失败,跳过此块`);
48
+ }
49
+ }
50
+ catch (err) {
51
+ Logger.error(`处理块类型 ${blockType} 时出错: ${err}`);
52
+ return {
53
+ ok: false,
54
+ error: errorResponse(`处理块类型"${blockType}"时出错: ${err}\n请检查该块类型的配置是否正确。`),
55
+ };
56
+ }
57
+ }
58
+ return { ok: true, contents };
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // 特殊块提取与提示
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * 从块列表中提取图片块(block_type=27)和画板块(block_type=43)。
65
+ */
66
+ export function extractSpecialBlocks(blocks) {
67
+ return {
68
+ imageBlocks: blocks.filter(b => b.block_type === BLOCK_TYPE_IMAGE),
69
+ whiteboardBlocks: blocks.filter(b => b.block_type === BLOCK_TYPE_WHITEBOARD),
70
+ };
71
+ }
72
+ /**
73
+ * 构建特殊块的 JSON 提示对象,用于展开合并到 JSON 响应中。
74
+ */
75
+ export function buildSpecialBlockHints(imageBlocks, whiteboardBlocks) {
76
+ const hints = {};
77
+ if (imageBlocks.length > 0) {
78
+ hints.imageBlocksInfo = {
79
+ count: imageBlocks.length,
80
+ blockIds: imageBlocks.map(b => b.block_id),
81
+ reminder: '检测到图片块已创建!请使用 upload_and_bind_image_to_block 工具上传图片并绑定到对应的块ID。',
82
+ };
83
+ }
84
+ if (whiteboardBlocks.length > 0) {
85
+ hints.whiteboardBlocksInfo = {
86
+ count: whiteboardBlocks.length,
87
+ blocks: whiteboardBlocks.map(b => ({
88
+ blockId: b.block_id,
89
+ token: b.board?.token,
90
+ align: b.board?.align,
91
+ })),
92
+ reminder: '检测到画板块已创建!请使用 fill_whiteboard_with_plantuml 工具填充画板内容,使用返回的 token 作为 whiteboardId 参数。支持 PlantUML (syntax_type: "plantuml") 和 Mermaid (syntax_type: "mermaid") 两种格式。',
93
+ };
94
+ }
95
+ return hints;
96
+ }
97
+ /**
98
+ * 构建文本形式的特殊块提示,追加到现有响应文本后。
99
+ */
100
+ export function appendSpecialBlockTextHints(base, imageBlocks, whiteboardBlocks) {
101
+ const sections = [base];
102
+ if (imageBlocks.length > 0) {
103
+ const lines = [
104
+ `\n\n🖼️ 检测到 ${imageBlocks.length} 个图片块 (block_type: 27)!`,
105
+ '💡 提示:如需查看图片内容,可使用 get_feishu_image_resource 工具下载图片。',
106
+ '图片信息:',
107
+ ...imageBlocks.map((b, idx) => {
108
+ const parts = [` ${idx + 1}. 块ID: ${b.block_id}`];
109
+ if (b.image?.token)
110
+ parts.push(`媒体ID: ${b.image.token}`);
111
+ return parts.join(',');
112
+ }),
113
+ '📝 注意:只有在需要查看图片内容时才调用上述工具,仅了解文档结构时无需获取。',
114
+ ];
115
+ sections.push(lines.join('\n'));
116
+ }
117
+ if (whiteboardBlocks.length > 0) {
118
+ const lines = [
119
+ `\n\n⚠️ 检测到 ${whiteboardBlocks.length} 个画板块 (block_type: 43)!`,
120
+ '💡 提示:如需获取画板具体内容,可使用 get_feishu_whiteboard_content 工具。',
121
+ '画板信息:',
122
+ ...whiteboardBlocks.map((b, idx) => {
123
+ const parts = [` ${idx + 1}. 块ID: ${b.block_id}`];
124
+ if (b.board?.token)
125
+ parts.push(`画板ID: ${b.board.token}`);
126
+ return parts.join(',');
127
+ }),
128
+ '📝 注意:只有在需要分析画板内容时才调用上述工具,仅了解文档结构时无需获取。',
129
+ ];
130
+ sections.push(lines.join('\n'));
131
+ }
132
+ return sections.join('');
133
+ }
134
+ /**
135
+ * 从飞书 API 抛出的错误对象中提取结构化错误信息。
136
+ * 飞书 API 错误可能包含 apiError.code / apiError.msg / apiError.log_id 等字段。
137
+ */
138
+ export function extractFeishuApiError(err) {
139
+ if (err && typeof err === 'object') {
140
+ const e = err;
141
+ const apiError = e.apiError;
142
+ if (apiError && apiError.code !== undefined && apiError.msg) {
143
+ return {
144
+ message: String(apiError.msg),
145
+ code: Number(apiError.code),
146
+ logId: apiError.log_id != null ? String(apiError.log_id) : undefined,
147
+ };
148
+ }
149
+ if (typeof e.err === 'string')
150
+ return { message: e.err };
151
+ if (typeof e.message === 'string')
152
+ return { message: e.message };
153
+ }
154
+ return { message: formatErrorMessage(err) };
155
+ }
@@ -0,0 +1,5 @@
1
+ export { ModuleRegistry } from './ModuleRegistry.js';
2
+ export { documentModule } from './document/index.js';
3
+ export { taskModule } from './task/index.js';
4
+ export { calendarModule } from './calendar/index.js';
5
+ export { memberModule } from './member/index.js';
@@ -0,0 +1,11 @@
1
+ import { MODULE_SCOPES } from '../../services/constants/feishuScopes.js';
2
+ import { registerMemberTools } from './tools/memberTools.js';
3
+ export const memberModule = {
4
+ id: 'member',
5
+ name: '飞书成员',
6
+ description: '通过用户名关键词搜索用户,返回头像、部门、open_id 等,用于任务指派(1 个工具)',
7
+ requiredScopes: MODULE_SCOPES.member,
8
+ registerTools(server, apiService) {
9
+ registerMemberTools(server, apiService);
10
+ },
11
+ };
@@ -0,0 +1,41 @@
1
+ import { FeishuBaseApiService } from '../../../services/feishu/FeishuBaseApiService.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ const PAGE_SIZE = 200;
4
+ /**
5
+ * 飞书成员搜索 API 服务
6
+ * 封装飞书搜索用户接口(/search/v1/user)、批量获取用户(/contact/v3/users/batch)
7
+ */
8
+ export class FeishuMemberService extends FeishuBaseApiService {
9
+ constructor(authService) {
10
+ super(authService);
11
+ }
12
+ /**
13
+ * 通过用户名关键词搜索用户
14
+ * 返回用户头像、用户名、部门、open_id、user_id 等
15
+ * @param query 搜索关键词
16
+ * @param pageToken 分页 token
17
+ */
18
+ async searchUsers(query, pageToken) {
19
+ Logger.info(`搜索用户,关键词: ${query}`);
20
+ const params = {
21
+ query,
22
+ page_size: PAGE_SIZE,
23
+ };
24
+ if (pageToken)
25
+ params.page_token = pageToken;
26
+ return this.get('/search/v1/user', params);
27
+ }
28
+ /**
29
+ * 批量获取用户信息(通讯录)
30
+ * GET /contact/v3/users/batch,返回用户 ID、名称、邮箱、手机、状态、部门等基本信息。
31
+ * 权限:contact:contact.base:readonly。单次最多 50 个 user_id。
32
+ */
33
+ async batchGetUsers(userIds, userIdType = 'open_id') {
34
+ Logger.info(`批量获取用户,数量: ${userIds.length}, user_id_type: ${userIdType}`);
35
+ const qs = new URLSearchParams();
36
+ userIds.forEach((id) => qs.append('user_ids', id));
37
+ qs.set('user_id_type', userIdType);
38
+ const res = await this.get(`/contact/v3/users/batch?${qs.toString()}`, {});
39
+ return res ?? { items: [] };
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export { getUsers } from './memberToolApi.js';