feishu-mcp 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { formatErrorMessage } from '../../utils/error.js';
3
3
  import { Logger } from '../../utils/logger.js';
4
- import { DocumentIdSchema, BlockIdSchema, } from '../../types/feishuSchema.js';
4
+ import { DocumentIdSchema, BlockIdSchema, SearchKeySchema, } from '../../types/feishuSchema.js';
5
5
  /**
6
6
  * 注册飞书相关的MCP工具
7
7
  * @param server MCP服务器实例
@@ -134,4 +134,33 @@ export function registerFeishuTools(server, feishuService) {
134
134
  };
135
135
  }
136
136
  });
137
+ // 添加搜索文档工具
138
+ server.tool('search_feishu_documents', 'Searches for documents in Feishu. Supports keyword-based search and returns document information including title, type, and owner. Use this tool to find specific content or related documents in your document library.', {
139
+ searchKey: SearchKeySchema,
140
+ }, async ({ searchKey }) => {
141
+ try {
142
+ if (!feishuService) {
143
+ return {
144
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration.' }],
145
+ };
146
+ }
147
+ Logger.info(`开始搜索飞书文档,关键字: ${searchKey},`);
148
+ const searchResult = await feishuService.searchDocuments(searchKey);
149
+ Logger.info(`文档搜索完成,找到 ${searchResult.size} 个结果`);
150
+ return {
151
+ content: [
152
+ { type: 'text', text: JSON.stringify(searchResult, null, 2) },
153
+ ],
154
+ };
155
+ }
156
+ catch (error) {
157
+ Logger.error(`搜索飞书文档失败:`, error);
158
+ const errorMessage = formatErrorMessage(error);
159
+ return {
160
+ content: [
161
+ { type: 'text', text: `搜索飞书文档失败: ${errorMessage}` },
162
+ ],
163
+ };
164
+ }
165
+ });
137
166
  }
@@ -1,4 +1,5 @@
1
1
  import axios, { AxiosError } from 'axios';
2
+ import FormData from 'form-data';
2
3
  import { Logger } from '../utils/logger.js';
3
4
  import { formatErrorMessage } from '../utils/error.js';
4
5
  /**
@@ -74,17 +75,25 @@ export class BaseApiService {
74
75
  * @param data 请求数据
75
76
  * @param needsAuth 是否需要认证
76
77
  * @param additionalHeaders 附加请求头
78
+ * @param responseType 响应类型
77
79
  * @returns 响应数据
78
80
  */
79
- async request(endpoint, method = 'GET', data, needsAuth = true, additionalHeaders) {
81
+ async request(endpoint, method = 'GET', data, needsAuth = true, additionalHeaders, responseType) {
80
82
  try {
81
83
  // 构建请求URL
82
84
  const url = `${this.getBaseUrl()}${endpoint}`;
83
85
  // 准备请求头
84
86
  const headers = {
85
- 'Content-Type': 'application/json',
86
87
  ...additionalHeaders
87
88
  };
89
+ // 如果数据是FormData,合并FormData的headers
90
+ // 否则设置为application/json
91
+ if (data instanceof FormData) {
92
+ Object.assign(headers, data.getHeaders());
93
+ }
94
+ else {
95
+ headers['Content-Type'] = 'application/json';
96
+ }
88
97
  // 添加认证令牌
89
98
  if (needsAuth) {
90
99
  const accessToken = await this.getAccessToken();
@@ -103,7 +112,8 @@ export class BaseApiService {
103
112
  url,
104
113
  headers,
105
114
  data: method !== 'GET' ? data : undefined,
106
- params: method === 'GET' ? data : undefined
115
+ params: method === 'GET' ? data : undefined,
116
+ responseType: responseType || 'json'
107
117
  };
108
118
  // 发送请求
109
119
  const response = await axios(config);
@@ -112,7 +122,11 @@ export class BaseApiService {
112
122
  Logger.debug(`响应状态码: ${response.status}`);
113
123
  Logger.debug(`响应头:`, response.headers);
114
124
  Logger.debug(`响应数据:`, response.data);
115
- // 检查API错误
125
+ // 对于非JSON响应,直接返回数据
126
+ if (responseType && responseType !== 'json') {
127
+ return response.data;
128
+ }
129
+ // 检查API错误(仅对JSON响应)
116
130
  if (response.data && typeof response.data.code === 'number' && response.data.code !== 0) {
117
131
  Logger.error(`API返回错误码: ${response.data.code}, 错误消息: ${response.data.msg}`);
118
132
  throw {
@@ -8,6 +8,7 @@ export var BlockType;
8
8
  BlockType["CODE"] = "code";
9
9
  BlockType["HEADING"] = "heading";
10
10
  BlockType["LIST"] = "list";
11
+ BlockType["IMAGE"] = "image";
11
12
  })(BlockType || (BlockType = {}));
12
13
  /**
13
14
  * 对齐方式枚举
@@ -72,6 +73,8 @@ export class BlockFactory {
72
73
  return this.createHeadingBlock(options);
73
74
  case BlockType.LIST:
74
75
  return this.createListBlock(options);
76
+ case BlockType.IMAGE:
77
+ return this.createImageBlock(options);
75
78
  default:
76
79
  Logger.error(`不支持的块类型: ${type}`);
77
80
  throw new Error(`不支持的块类型: ${type}`);
@@ -192,11 +195,19 @@ export class BlockFactory {
192
195
  return blockContent;
193
196
  }
194
197
  /**
195
- * 创建批量块内容
196
- * @param blocks 块配置数组
197
- * @returns 块内容数组
198
+ * 创建图片块内容(空图片块,需要后续设置图片资源)
199
+ * @param options 图片块选项
200
+ * @returns 图片块内容对象
198
201
  */
199
- createBatchBlocks(blocks) {
200
- return blocks.map(block => this.createBlock(block.type, block.options));
202
+ createImageBlock(options = {}) {
203
+ const { width = 100, height = 100 } = options;
204
+ return {
205
+ block_type: 27, // 27表示图片块
206
+ image: {
207
+ width: width,
208
+ height: height,
209
+ token: "" // 空token,需要后续通过API设置
210
+ }
211
+ };
201
212
  }
202
213
  }
@@ -5,6 +5,9 @@ import { CacheManager } from '../utils/cache.js';
5
5
  import { ParamUtils } from '../utils/paramUtils.js';
6
6
  import { BlockFactory, BlockType } from './blockFactory.js';
7
7
  import axios from 'axios';
8
+ import FormData from 'form-data';
9
+ import fs from 'fs';
10
+ import path from 'path';
8
11
  /**
9
12
  * 飞书API服务类
10
13
  * 提供飞书API的所有基础操作,包括认证、请求和缓存管理
@@ -373,18 +376,6 @@ export class FeishuApiService extends BaseApiService {
373
376
  });
374
377
  return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
375
378
  }
376
- /**
377
- * 创建混合块
378
- * @param documentId 文档ID或URL
379
- * @param parentBlockId 父块ID
380
- * @param blocks 块配置数组
381
- * @param index 插入位置索引
382
- * @returns 创建结果
383
- */
384
- async createMixedBlocks(documentId, parentBlockId, blocks, index = 0) {
385
- const blockContents = blocks.map(block => this.blockFactory.createBlock(block.type, block.options));
386
- return this.createDocumentBlocks(documentId, parentBlockId, blockContents, index);
387
- }
388
379
  /**
389
380
  * 删除文档中的块,支持批量删除
390
381
  * @param documentId 文档ID或URL
@@ -501,7 +492,7 @@ export class FeishuApiService extends BaseApiService {
501
492
  // 使用枚举类型来避免字符串错误
502
493
  const blockTypeEnum = blockType;
503
494
  // 构建块配置
504
- let blockConfig = {
495
+ const blockConfig = {
505
496
  type: blockTypeEnum,
506
497
  options: {}
507
498
  };
@@ -554,6 +545,22 @@ export class FeishuApiService extends BaseApiService {
554
545
  };
555
546
  }
556
547
  break;
548
+ case BlockType.IMAGE:
549
+ if ('image' in options && options.image) {
550
+ const imageOptions = options.image;
551
+ blockConfig.options = {
552
+ width: imageOptions.width || 100,
553
+ height: imageOptions.height || 100
554
+ };
555
+ }
556
+ else {
557
+ // 默认图片块选项
558
+ blockConfig.options = {
559
+ width: 100,
560
+ height: 100
561
+ };
562
+ }
563
+ break;
557
564
  default:
558
565
  Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
559
566
  if ('text' in options) {
@@ -599,6 +606,14 @@ export class FeishuApiService extends BaseApiService {
599
606
  ? listOptions.align : 1
600
607
  };
601
608
  }
609
+ else if ('image' in options) {
610
+ blockConfig.type = BlockType.IMAGE;
611
+ const imageOptions = options.image;
612
+ blockConfig.options = {
613
+ width: imageOptions.width || 100,
614
+ height: imageOptions.height || 100
615
+ };
616
+ }
602
617
  break;
603
618
  }
604
619
  // 记录调试信息
@@ -628,24 +643,9 @@ export class FeishuApiService extends BaseApiService {
628
643
  if (extra) {
629
644
  params.extra = extra;
630
645
  }
631
- // 这里需要特殊处理,因为返回的是二进制数据,不是JSON
632
- const token = await this.getAccessToken();
633
- const url = `${this.getBaseUrl()}${endpoint}`;
634
- const headers = {
635
- 'Authorization': `Bearer ${token}`
636
- };
637
- Logger.debug(`请求图片资源URL: ${url}`);
638
- // 使用axios直接获取二进制响应
639
- const response = await axios.get(url, {
640
- params,
641
- headers,
642
- responseType: 'arraybuffer'
643
- });
644
- // 检查响应状态
645
- if (response.status !== 200) {
646
- throw new Error(`获取图片资源失败,状态码: ${response.status}`);
647
- }
648
- const imageBuffer = Buffer.from(response.data);
646
+ // 使用通用的request方法获取二进制响应
647
+ const response = await this.request(endpoint, 'GET', params, true, {}, 'arraybuffer');
648
+ const imageBuffer = Buffer.from(response);
649
649
  Logger.info(`图片资源获取成功,大小: ${imageBuffer.length} 字节`);
650
650
  return imageBuffer;
651
651
  }
@@ -714,4 +714,256 @@ export class FeishuApiService extends BaseApiService {
714
714
  this.handleApiError(error, '创建文件夹失败');
715
715
  }
716
716
  }
717
+ /**
718
+ * 搜索飞书文档
719
+ * @param searchKey 搜索关键字
720
+ * @param count 每页数量,默认50
721
+ * @returns 搜索结果,包含所有页的数据
722
+ */
723
+ async searchDocuments(searchKey, count = 50) {
724
+ try {
725
+ Logger.info(`开始搜索文档,关键字: ${searchKey}`);
726
+ const endpoint = `//suite/docs-api/search/object`;
727
+ let offset = 0;
728
+ let allResults = [];
729
+ let hasMore = true;
730
+ // 循环获取所有页的数据
731
+ while (hasMore && offset + count < 200) {
732
+ const payload = {
733
+ search_key: searchKey,
734
+ docs_types: ["doc"],
735
+ count: count,
736
+ offset: offset
737
+ };
738
+ Logger.debug(`请求搜索,offset: ${offset}, count: ${count}`);
739
+ const response = await this.post(endpoint, payload);
740
+ Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
741
+ if (response && response.docs_entities) {
742
+ const newDocs = response.docs_entities;
743
+ allResults = [...allResults, ...newDocs];
744
+ hasMore = response.has_more || false;
745
+ offset += count;
746
+ Logger.debug(`当前页获取到 ${newDocs.length} 条数据,累计 ${allResults.length} 条,总计 ${response.total} 条,hasMore: ${hasMore}`);
747
+ }
748
+ else {
749
+ hasMore = false;
750
+ Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
751
+ }
752
+ }
753
+ const resultCount = allResults.length;
754
+ Logger.info(`文档搜索完成,找到 ${resultCount} 个结果`);
755
+ return {
756
+ data: allResults
757
+ };
758
+ }
759
+ catch (error) {
760
+ this.handleApiError(error, '搜索文档失败');
761
+ }
762
+ }
763
+ /**
764
+ * 上传图片素材到飞书
765
+ * @param imageBase64 图片的Base64编码
766
+ * @param fileName 图片文件名,如果不提供则自动生成
767
+ * @param parentBlockId 图片块ID
768
+ * @returns 上传结果,包含file_token
769
+ */
770
+ async uploadImageMedia(imageBase64, fileName, parentBlockId) {
771
+ try {
772
+ const endpoint = '/drive/v1/medias/upload_all';
773
+ // 将Base64转换为Buffer
774
+ const imageBuffer = Buffer.from(imageBase64, 'base64');
775
+ const imageSize = imageBuffer.length;
776
+ // 如果没有提供文件名,根据Base64数据生成默认文件名
777
+ if (!fileName) {
778
+ // 简单检测图片格式
779
+ if (imageBase64.startsWith('/9j/')) {
780
+ fileName = `image_${Date.now()}.jpg`;
781
+ }
782
+ else if (imageBase64.startsWith('iVBORw0KGgo')) {
783
+ fileName = `image_${Date.now()}.png`;
784
+ }
785
+ else if (imageBase64.startsWith('R0lGODlh')) {
786
+ fileName = `image_${Date.now()}.gif`;
787
+ }
788
+ else {
789
+ fileName = `image_${Date.now()}.png`; // 默认PNG格式
790
+ }
791
+ }
792
+ Logger.info(`开始上传图片素材,文件名: ${fileName},大小: ${imageSize} 字节,关联块ID: ${parentBlockId}`);
793
+ // 验证图片大小(可选的业务检查)
794
+ if (imageSize > 20 * 1024 * 1024) {
795
+ // 20MB限制
796
+ Logger.warn(`图片文件过大: ${imageSize} 字节,建议小于20MB`);
797
+ }
798
+ // 使用FormData构建multipart/form-data请求
799
+ const formData = new FormData();
800
+ // file字段传递图片的二进制数据流
801
+ // Buffer是Node.js中的二进制数据类型,form-data库会将其作为文件流处理
802
+ formData.append('file', imageBuffer, {
803
+ filename: fileName,
804
+ contentType: this.getMimeTypeFromFileName(fileName),
805
+ knownLength: imageSize, // 明确指定文件大小,避免流读取问题
806
+ });
807
+ // 飞书API要求的其他表单字段
808
+ formData.append('file_name', fileName);
809
+ formData.append('parent_type', 'docx_image'); // 固定值:文档图片类型
810
+ formData.append('parent_node', parentBlockId); // 关联的图片块ID
811
+ formData.append('size', imageSize.toString()); // 文件大小(字节,字符串格式)
812
+ // 使用通用的post方法发送请求
813
+ const response = await this.post(endpoint, formData);
814
+ Logger.info(`图片素材上传成功,file_token: ${response.file_token}`);
815
+ return response;
816
+ }
817
+ catch (error) {
818
+ this.handleApiError(error, '上传图片素材失败');
819
+ }
820
+ }
821
+ /**
822
+ * 设置图片块的素材内容
823
+ * @param documentId 文档ID
824
+ * @param imageBlockId 图片块ID
825
+ * @param fileToken 图片素材的file_token
826
+ * @returns 设置结果
827
+ */
828
+ async setImageBlockContent(documentId, imageBlockId, fileToken) {
829
+ try {
830
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
831
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${imageBlockId}`;
832
+ const payload = {
833
+ replace_image: {
834
+ token: fileToken,
835
+ },
836
+ };
837
+ Logger.info(`开始设置图片块内容,文档ID: ${normalizedDocId},块ID: ${imageBlockId},file_token: ${fileToken}`);
838
+ const response = await this.patch(endpoint, payload);
839
+ Logger.info('图片块内容设置成功');
840
+ return response;
841
+ }
842
+ catch (error) {
843
+ this.handleApiError(error, '设置图片块内容失败');
844
+ }
845
+ }
846
+ /**
847
+ * 创建完整的图片块(包括创建空块、上传图片、设置内容的完整流程)
848
+ * @param documentId 文档ID
849
+ * @param parentBlockId 父块ID
850
+ * @param imagePathOrUrl 图片路径或URL
851
+ * @param options 图片选项
852
+ * @returns 创建结果
853
+ */
854
+ async createImageBlock(documentId, parentBlockId, imagePathOrUrl, options = {}) {
855
+ try {
856
+ const { fileName: providedFileName, width, height, index = 0 } = options;
857
+ Logger.info(`开始创建图片块,文档ID: ${documentId},父块ID: ${parentBlockId},图片源: ${imagePathOrUrl},插入位置: ${index}`);
858
+ // 从路径或URL获取图片的Base64编码
859
+ const { base64: imageBase64, fileName: detectedFileName } = await this.getImageBase64FromPathOrUrl(imagePathOrUrl);
860
+ // 使用提供的文件名或检测到的文件名
861
+ const finalFileName = providedFileName || detectedFileName;
862
+ // 第1步:创建空图片块
863
+ Logger.info('第1步:创建空图片块');
864
+ const imageBlockContent = this.blockFactory.createImageBlock({
865
+ width,
866
+ height,
867
+ });
868
+ const createBlockResult = await this.createDocumentBlock(documentId, parentBlockId, imageBlockContent, index);
869
+ if (!createBlockResult?.children?.[0]?.block_id) {
870
+ throw new Error('创建空图片块失败:无法获取块ID');
871
+ }
872
+ const imageBlockId = createBlockResult.children[0].block_id;
873
+ Logger.info(`空图片块创建成功,块ID: ${imageBlockId}`);
874
+ // 第2步:上传图片素材
875
+ Logger.info('第2步:上传图片素材');
876
+ const uploadResult = await this.uploadImageMedia(imageBase64, finalFileName, imageBlockId);
877
+ if (!uploadResult?.file_token) {
878
+ throw new Error('上传图片素材失败:无法获取file_token');
879
+ }
880
+ Logger.info(`图片素材上传成功,file_token: ${uploadResult.file_token}`);
881
+ // 第3步:设置图片块内容
882
+ Logger.info('第3步:设置图片块内容');
883
+ const setContentResult = await this.setImageBlockContent(documentId, imageBlockId, uploadResult.file_token);
884
+ Logger.info('图片块创建完成');
885
+ // 返回综合结果
886
+ return {
887
+ imageBlock: createBlockResult.children[0],
888
+ imageBlockId: imageBlockId,
889
+ fileToken: uploadResult.file_token,
890
+ uploadResult: uploadResult,
891
+ setContentResult: setContentResult,
892
+ documentRevisionId: setContentResult.document_revision_id ||
893
+ createBlockResult.document_revision_id,
894
+ };
895
+ }
896
+ catch (error) {
897
+ this.handleApiError(error, '创建图片块失败');
898
+ }
899
+ }
900
+ /**
901
+ * 根据文件名获取MIME类型
902
+ * @param fileName 文件名
903
+ * @returns MIME类型
904
+ */
905
+ getMimeTypeFromFileName(fileName) {
906
+ const extension = fileName.toLowerCase().split('.').pop();
907
+ switch (extension) {
908
+ case 'jpg':
909
+ case 'jpeg':
910
+ return 'image/jpeg';
911
+ case 'png':
912
+ return 'image/png';
913
+ case 'gif':
914
+ return 'image/gif';
915
+ case 'webp':
916
+ return 'image/webp';
917
+ case 'bmp':
918
+ return 'image/bmp';
919
+ case 'svg':
920
+ return 'image/svg+xml';
921
+ default:
922
+ return 'image/png'; // 默认PNG
923
+ }
924
+ }
925
+ /**
926
+ * 从路径或URL获取图片的Base64编码
927
+ * @param imagePathOrUrl 图片路径或URL
928
+ * @returns 图片的Base64编码和文件名
929
+ */
930
+ async getImageBase64FromPathOrUrl(imagePathOrUrl) {
931
+ try {
932
+ let imageBuffer;
933
+ let fileName;
934
+ // 判断是否为HTTP/HTTPS URL
935
+ if (imagePathOrUrl.startsWith('http://') || imagePathOrUrl.startsWith('https://')) {
936
+ Logger.info(`从URL获取图片: ${imagePathOrUrl}`);
937
+ // 从URL下载图片
938
+ const response = await axios.get(imagePathOrUrl, {
939
+ responseType: 'arraybuffer',
940
+ timeout: 30000, // 30秒超时
941
+ });
942
+ imageBuffer = Buffer.from(response.data);
943
+ // 从URL中提取文件名
944
+ const urlPath = new URL(imagePathOrUrl).pathname;
945
+ fileName = path.basename(urlPath) || `image_${Date.now()}.png`;
946
+ Logger.info(`从URL成功获取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
947
+ }
948
+ else {
949
+ // 本地文件路径
950
+ Logger.info(`从本地路径读取图片: ${imagePathOrUrl}`);
951
+ // 检查文件是否存在
952
+ if (!fs.existsSync(imagePathOrUrl)) {
953
+ throw new Error(`图片文件不存在: ${imagePathOrUrl}`);
954
+ }
955
+ // 读取文件
956
+ imageBuffer = fs.readFileSync(imagePathOrUrl);
957
+ fileName = path.basename(imagePathOrUrl);
958
+ Logger.info(`从本地路径成功读取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
959
+ }
960
+ // 转换为Base64
961
+ const base64 = imageBuffer.toString('base64');
962
+ return { base64, fileName };
963
+ }
964
+ catch (error) {
965
+ Logger.error(`获取图片失败: ${error}`);
966
+ throw new Error(`获取图片失败: ${error instanceof Error ? error.message : String(error)}`);
967
+ }
968
+ }
717
969
  }
@@ -11,14 +11,26 @@ export const ParentBlockIdSchema = z.string().describe('Parent block ID (require
11
11
  // 块ID参数定义
12
12
  export const BlockIdSchema = z.string().describe('Block ID (required). The ID of the specific block to get content from. You can obtain block IDs using the get_feishu_document_blocks tool.');
13
13
  // 插入位置索引参数定义
14
- export const IndexSchema = z.number().describe('Insertion position index (required). Specifies where the block should be inserted. Use 0 to insert at the beginning. ' +
14
+ export const IndexSchema = z.number().describe('Insertion position index (required). This index is relative to the children array of the specified parentBlockId block (not the whole document).\n' +
15
+ 'If parentBlockId is the document root (i.e., the document ID), index refers to the position among the document content blocks (excluding the title block itself).\n' +
16
+ '0 means to insert as the first content block after the title.\n' +
17
+ 'If children is empty or missing, use 0 to insert the first content block.\n' +
18
+ 'For nested blocks, index is relative to the parent block\'s children.\n' +
19
+ 'Note: The title block itself is not part of the children array and cannot be operated on with index.' +
20
+ 'Specifies where the block should be inserted. Use 0 to insert at the beginning. ' +
15
21
  'Use get_feishu_document_blocks tool to understand document structure if unsure. ' +
16
22
  'For consecutive insertions, calculate next index as previous index + 1.');
17
23
  // 起始插入位置索引参数定义
18
- export const StartIndexSchema = z.number().describe('Starting insertion position index (required). Specifies where the first block should be inserted. Use 0 to insert at the beginning. ' +
24
+ export const StartIndexSchema = z.number().describe('Starting insertion position index (required). This index is relative to the children array of the specified parentBlockId block.\n' +
25
+ 'For the document root, this means the content blocks after the title. For other blocks, it means the sub-blocks under that block.\n' +
26
+ 'The index does not include the title block itself.' +
27
+ 'Specifies where the first block should be inserted or deleted. Use 0 to insert at the beginning. ' +
19
28
  'Use get_feishu_document_blocks tool to understand document structure if unsure.');
20
29
  // 结束位置索引参数定义
21
- export const EndIndexSchema = z.number().describe('Ending position index (required). Specifies the end of the range for deletion (exclusive). ' +
30
+ export const EndIndexSchema = z.number().describe('Ending position index (required). This index is relative to the children array of the specified parentBlockId block.\n' +
31
+ 'For the document root, this means the content blocks after the title. For other blocks, it means the sub-blocks under that block.\n' +
32
+ 'The index does not include the title block itself.' +
33
+ 'Specifies the end of the range for deletion (exclusive). ' +
22
34
  'For example, to delete blocks 2, 3, and 4, use startIndex=2, endIndex=5. ' +
23
35
  'To delete a single block at position 2, use startIndex=2, endIndex=3.');
24
36
  // 文本对齐方式参数定义
@@ -87,8 +99,14 @@ export const ListBlockSchema = z.object({
87
99
  align: AlignSchemaWithValidation,
88
100
  });
89
101
  // 块类型枚举 - 用于批量创建块工具
90
- export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', as well as 'heading1' through 'heading9'. " +
91
- "For headings, we recommend using 'heading' with level property, but 'heading1'-'heading9' are also supported.");
102
+ export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image', as well as 'heading1' through 'heading9'. " +
103
+ "For headings, we recommend using 'heading' with level property, but 'heading1'-'heading9' are also supported. " +
104
+ "For images, use 'image' to create empty image blocks that can be filled later.");
105
+ // 图片块内容定义 - 用于批量创建块工具
106
+ export const ImageBlockSchema = z.object({
107
+ width: z.number().optional().describe('Image width in pixels (optional). If not provided, default width will be used.'),
108
+ height: z.number().optional().describe('Image height in pixels (optional). If not provided, default height will be used.'),
109
+ });
92
110
  // 块配置定义 - 用于批量创建块工具
93
111
  export const BlockConfigSchema = z.object({
94
112
  blockType: BlockTypeEnum,
@@ -97,6 +115,7 @@ export const BlockConfigSchema = z.object({
97
115
  z.object({ code: CodeBlockSchema }).describe("Code block options. Used when blockType is 'code'."),
98
116
  z.object({ heading: HeadingBlockSchema }).describe("Heading block options. Used with both 'heading' and 'headingN' formats."),
99
117
  z.object({ list: ListBlockSchema }).describe("List block options. Used when blockType is 'list'."),
118
+ z.object({ image: ImageBlockSchema }).describe("Image block options. Used when blockType is 'image'. Creates empty image blocks."),
100
119
  z.record(z.any()).describe("Fallback for any other block options")
101
120
  ]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
102
121
  });
@@ -112,9 +131,17 @@ export const FolderTokenSchema = z.string().describe('Folder token (required). T
112
131
  'Format is an alphanumeric string like "FWK2fMleClICfodlHHWc4Mygnhb".');
113
132
  // 文件夹名称参数定义
114
133
  export const FolderNameSchema = z.string().describe('Folder name (required). The name for the new folder to be created.');
115
- // 排序方式参数定义
116
- export const OrderBySchema = z.string().optional().default('EditedTime').describe('Order by field (optional). Specifies how to sort the file list. Available values: ' +
117
- '"EditedTime" (default), "CreatedTime", "Name". For user-friendly display, case insensitive.');
118
- // 排序方向参数定义
119
- export const DirectionSchema = z.string().optional().default('DESC').describe('Sort direction (optional). Specifies the sort order. Available values: ' +
120
- '"DESC" (default) for descending order, "ASC" for ascending order. Case sensitive.');
134
+ // 搜索关键字参数定义
135
+ export const SearchKeySchema = z.string().describe('Search keyword (required). The keyword to search for in documents.');
136
+ // 图片路径或URL参数定义
137
+ export const ImagePathOrUrlSchema = z.string().describe('Image path or URL (required). Supports the following formats:\n' +
138
+ '1. Local file absolute path: e.g., "C:\\path\\to\\image.jpg"\n' +
139
+ '2. HTTP/HTTPS URL: e.g., "https://example.com/image.png"\n' +
140
+ 'The tool will automatically detect the format and handle accordingly.');
141
+ // 图片文件名参数定义
142
+ export const ImageFileNameSchema = z.string().optional().describe('Image file name (optional). If not provided, a default name will be generated based on the source. ' +
143
+ 'Should include the file extension, e.g., "image.png" or "photo.jpg".');
144
+ // 图片宽度参数定义
145
+ export const ImageWidthSchema = z.number().optional().describe('Image width in pixels (optional). If not provided, the original image width will be used.');
146
+ // 图片高度参数定义
147
+ export const ImageHeightSchema = z.number().optional().describe('Image height in pixels (optional). If not provided, the original image height will be used.');
@@ -265,9 +265,9 @@ Object.defineProperty(Logger, "config", {
265
265
  showTimestamp: true,
266
266
  showLevel: true,
267
267
  timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS',
268
- logToFile: true,
268
+ logToFile: false,
269
269
  logFilePath: 'log/log.txt',
270
- maxObjectDepth: 4, // 限制对象序列化深度
270
+ maxObjectDepth: 2, // 限制对象序列化深度
271
271
  maxObjectStringLength: 5000000 // 限制序列化后字符串长度
272
272
  }
273
273
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-mcp",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Model Context Protocol server for Feishu integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,12 +41,13 @@
41
41
  "author": "cso1z",
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.6.1",
44
+ "@modelcontextprotocol/sdk": "^1.13.1",
45
45
  "@types/yargs": "^17.0.33",
46
46
  "axios": "^1.7.9",
47
47
  "cross-env": "^7.0.3",
48
48
  "dotenv": "^16.4.7",
49
49
  "express": "^4.21.2",
50
+ "form-data": "^4.0.3",
50
51
  "remeda": "^2.20.1",
51
52
  "yargs": "^17.7.2",
52
53
  "zod": "^3.24.2"
package/dist/config.js DELETED
@@ -1,26 +0,0 @@
1
- import { Config, ConfigSource } from './utils/config.js';
2
- /**
3
- * 为了向后兼容,保留getServerConfig函数
4
- * 但内部使用Config类
5
- * @param isStdioMode 是否在stdio模式下
6
- * @returns 服务器配置
7
- */
8
- export function getServerConfig(isStdioMode) {
9
- const config = Config.getInstance();
10
- if (!isStdioMode) {
11
- config.printConfig(isStdioMode);
12
- }
13
- // 为了向后兼容,返回旧格式的配置对象
14
- return {
15
- port: config.server.port,
16
- feishuAppId: config.feishu.appId,
17
- feishuAppSecret: config.feishu.appSecret,
18
- configSources: {
19
- port: config.configSources['server.port'].toLowerCase(),
20
- feishuAppId: config.configSources['feishu.appId']?.toLowerCase(),
21
- feishuAppSecret: config.configSources['feishu.appSecret']?.toLowerCase()
22
- }
23
- };
24
- }
25
- // 导出Config类
26
- export { Config, ConfigSource };