feishu-mcp 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,10 @@
24
24
  <img src="image/demo.png" alt="飞书 MCP 使用演示" width="800"/>
25
25
  </a>
26
26
 
27
+ <a href="https://www.bilibili.com/video/BV18z3gzdE1w/?vd_source=94c14da5a71aeb01f665f159dd3d89c8">
28
+ <img src="image/demo_1.png" alt="飞书 MCP 使用演示" width="800"/>
29
+ </a>
30
+
27
31
  > ⭐ **Star 本项目,第一时间获取最新功能和重要更新!** 关注项目可以让你不错过任何新特性、修复和优化,助你持续高效使用。你的支持也将帮助我们更好地完善和发展项目。⭐
28
32
 
29
33
  ---
@@ -44,13 +48,12 @@
44
48
  | **工具功能** | `convert_feishu_wiki_to_document_id` | Wiki链接转换 | 将Wiki链接转为文档ID | ✅ 已完成 |
45
49
  | | `get_feishu_image_resource` | 获取图片资源 | 下载文档中的图片 | ✅ 已完成 |
46
50
  | | `get_feishu_whiteboard_content` | 获取画板内容 | 获取画板中的图形元素和结构(流程图、思维导图等) | ✅ 已完成 |
47
- | **高级功能** | 表格操作 | 创建和编辑表格 | 结构化数据展示 | 🚧 计划中 |
48
- | | 图表插入 | 支持各类数据可视化图表 | 数据展示和分析 | 🚧 计划中 |
51
+ | **高级功能** | `create_feishu_table` | 创建和编辑表格 | 结构化数据展示 | 已完成 |
49
52
  | | 流程图插入 | 支持流程图和思维导图 | 流程梳理和可视化 | ✅ 已完成 |
50
53
  | 图片插入 | `upload_and_bind_image_to_block` | 支持插入本地和远程图片 | 修改文档内容 | ✅ 已完成 |
51
54
  | | 公式支持 | 支持数学公式 | 学术和技术文档 | ✅ 已完成 |
52
55
 
53
- ### 🎨 支持的样式功能
56
+ ### 🎨 支持的样式功能(基本支持md所有格式)
54
57
 
55
58
  - **文本样式**:粗体、斜体、下划线、删除线、行内代码
56
59
  - **文本颜色**:灰色、棕色、橙色、黄色、绿色、蓝色、紫色
@@ -61,6 +64,7 @@
61
64
  - **图片**:支持本地图片和网络图片
62
65
  - **公式**:在文本块中插入数学公式,支持LaTeX语法
63
66
  - **mermaid图表**:支持流程图、时序图、思维导图、类图、饼图等等
67
+ - **表格**:支持创建多行列表格,单元格可包含文本、标题、列表、代码块等多种内容类型
64
68
 
65
69
  ---
66
70
 
@@ -73,6 +77,7 @@
73
77
  - ~~**支持多种凭证类型**:包括 tenant_access_token和 user_access_token,满足不同场景下的认证需求~~ (飞书应用配置发生变更) 0.0.16 ✅。
74
78
  - **支持cursor用户登录**:方便在cursor平台用户认证
75
79
  - ~~**支持mermaid图表**:流程图、时序图等等,丰富文档内容~~ 0.1.11 ✅
80
+ - ~~**支持表格创建**:创建包含各种块类型的复杂表格,支持样式控制~~ 0.1.2 ✅
76
81
 
77
82
  ---
78
83
 
@@ -6,7 +6,7 @@ import { registerFeishuBlockTools } from './tools/feishuBlockTools.js';
6
6
  import { registerFeishuFolderTools } from './tools/feishuFolderTools.js';
7
7
  const serverInfo = {
8
8
  name: "Feishu MCP Server",
9
- version: "0.1.0",
9
+ version: "0.1.2",
10
10
  };
11
11
  const serverOptions = {
12
12
  capabilities: { logging: {}, tools: {} },
@@ -8,11 +8,11 @@ import { DocumentIdSchema, ParentBlockIdSchema, BlockIdSchema, IndexSchema, Star
8
8
  TextElementsArraySchema,
9
9
  // CodeLanguageSchema,
10
10
  // CodeWrapSchema,
11
- BlockConfigSchema, MediaIdSchema, MediaExtraSchema, ImagesArraySchema,
11
+ BlockConfigSchema, MediaIdSchema, MediaExtraSchema, ImagesArraySchema,
12
12
  // MermaidCodeSchema,
13
13
  // ImageWidthSchema,
14
14
  // ImageHeightSchema
15
- } from '../../types/feishuSchema.js';
15
+ TableCreateSchema } from '../../types/feishuSchema.js';
16
16
  /**
17
17
  * 注册飞书块相关的MCP工具
18
18
  * @param server MCP服务器实例
@@ -618,4 +618,45 @@ export function registerFeishuBlockTools(server, feishuService) {
618
618
  };
619
619
  }
620
620
  });
621
+ // 添加创建飞书表格工具
622
+ server.tool('create_feishu_table', 'Creates a table block in a Feishu document with specified rows and columns. Each cell can contain different types of content blocks (text, lists, code, etc.). This tool creates the complete table structure including table cells and their content. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
623
+ documentId: DocumentIdSchema,
624
+ parentBlockId: ParentBlockIdSchema,
625
+ index: IndexSchema,
626
+ tableConfig: TableCreateSchema,
627
+ }, async ({ documentId, parentBlockId, index = 0, tableConfig }) => {
628
+ try {
629
+ if (!feishuService) {
630
+ return {
631
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
632
+ };
633
+ }
634
+ Logger.info(`开始创建飞书表格,文档ID: ${documentId},父块ID: ${parentBlockId},表格大小: ${tableConfig.rowSize}x${tableConfig.columnSize},插入位置: ${index}`);
635
+ const result = await feishuService.createTableBlock(documentId, parentBlockId, tableConfig, index);
636
+ // 构建返回信息
637
+ let resultText = `表格创建成功!\n\n表格大小: ${tableConfig.rowSize}x${tableConfig.columnSize}\n`;
638
+ // 如果有图片token,显示图片信息
639
+ if (result.imageTokens && result.imageTokens.length > 0) {
640
+ resultText += `\n\n📸 发现 ${result.imageTokens.length} 个图片:\n`;
641
+ result.imageTokens.forEach((imageToken, index) => {
642
+ resultText += `${index + 1}. 坐标(${imageToken.row}, ${imageToken.column}) - blockId: ${imageToken.blockId}\n`;
643
+ });
644
+ resultText += "你需要使用upload_and_bind_image_to_block工具绑定图片";
645
+ }
646
+ resultText += `\n\n完整结果:\n${JSON.stringify(result, null, 2)}`;
647
+ return {
648
+ content: [{
649
+ type: 'text',
650
+ text: resultText
651
+ }],
652
+ };
653
+ }
654
+ catch (error) {
655
+ Logger.error(`创建飞书表格失败:`, error);
656
+ const errorMessage = formatErrorMessage(error);
657
+ return {
658
+ content: [{ type: 'text', text: `创建飞书表格失败: ${errorMessage}` }],
659
+ };
660
+ }
661
+ });
621
662
  }
@@ -107,11 +107,14 @@ export function registerFeishuTools(server, feishuService) {
107
107
  // 检查是否有 block_type 为 43 的块(画板块)
108
108
  const whiteboardBlocks = blocks.filter((block) => block.block_type === 43);
109
109
  const hasWhiteboardBlocks = whiteboardBlocks.length > 0;
110
+ // 检查是否有 block_type 为 27 的块(图片块)
111
+ const imageBlocks = blocks.filter((block) => block.block_type === 27);
112
+ const hasImageBlocks = imageBlocks.length > 0;
110
113
  let responseText = JSON.stringify(blocks, null, 2);
111
114
  if (hasWhiteboardBlocks) {
112
115
  responseText += '\n\n⚠️ 检测到画板块 (block_type: 43)!\n';
113
- responseText += `发现 ${whiteboardBlocks.length} 个画板块。画板块包含丰富的图形内容,如形状、文本、思维导图等。\n`;
114
- responseText += '建议使用 get_feishu_whiteboard_content 工具来获取画板的具体内容和结构。\n';
116
+ responseText += `发现 ${whiteboardBlocks.length} 个画板块。\n`;
117
+ responseText += '💡 提示:如果您需要获取画板的具体内容(如流程图、思维导图等),可以使用 get_feishu_whiteboard_content 工具。\n';
115
118
  responseText += '画板信息:\n';
116
119
  whiteboardBlocks.forEach((block, index) => {
117
120
  responseText += ` ${index + 1}. 块ID: ${block.block_id}`;
@@ -120,7 +123,21 @@ export function registerFeishuTools(server, feishuService) {
120
123
  }
121
124
  responseText += '\n';
122
125
  });
123
- responseText += '请使用上述画板ID调用 get_feishu_whiteboard_content 工具。';
126
+ responseText += '📝 注意:只有在需要分析画板内容时才调用上述工具,仅了解文档结构时无需获取。';
127
+ }
128
+ if (hasImageBlocks) {
129
+ responseText += '\n\n🖼️ 检测到图片块 (block_type: 27)!\n';
130
+ responseText += `发现 ${imageBlocks.length} 个图片块。\n`;
131
+ responseText += '💡 提示:如果您需要查看图片的具体内容,可以使用 get_feishu_image_resource 工具下载图片。\n';
132
+ responseText += '图片信息:\n';
133
+ imageBlocks.forEach((block, index) => {
134
+ responseText += ` ${index + 1}. 块ID: ${block.block_id}`;
135
+ if (block.image && block.image.token) {
136
+ responseText += `, 媒体ID: ${block.image.token}`;
137
+ }
138
+ responseText += '\n';
139
+ });
140
+ responseText += '📝 注意:只有在需要查看图片内容时才调用上述工具,仅了解文档结构时无需获取。';
124
141
  }
125
142
  return {
126
143
  content: [{ type: 'text', text: responseText }],
@@ -2,6 +2,8 @@ import axios, { AxiosError } from 'axios';
2
2
  import FormData from 'form-data';
3
3
  import { Logger } from '../utils/logger.js';
4
4
  import { formatErrorMessage } from '../utils/error.js';
5
+ import { CacheManager } from '../utils/cache.js';
6
+ import { Config } from '../utils/config.js';
5
7
  /**
6
8
  * API服务基类
7
9
  * 提供通用的HTTP请求处理和认证功能
@@ -135,7 +137,9 @@ export class BaseApiService {
135
137
  // 清除当前令牌,下次请求会重新获取
136
138
  this.accessToken = '';
137
139
  this.tokenExpireTime = null;
138
- Logger.warn('访问令牌可能已过期,已清除缓存的令牌');
140
+ Logger.warn(`访问令牌可能已过期,已清除缓存的令牌`);
141
+ const clientKey = await CacheManager.getClientKey(Config.getInstance().feishu.appId, Config.getInstance().feishu.appSecret);
142
+ CacheManager.getInstance().removeUserToken(clientKey);
139
143
  // 如果这是重试请求,避免无限循环
140
144
  if (error.isRetry) {
141
145
  this.handleApiError(error, `API请求失败 (${endpoint})`);
@@ -246,4 +246,92 @@ export class BlockFactory {
246
246
  },
247
247
  };
248
248
  }
249
+ /**
250
+ * 创建表格块
251
+ * @param options 表格块选项
252
+ * @returns 表格块内容对象
253
+ */
254
+ createTableBlock(options) {
255
+ const { columnSize, rowSize, cells = [] } = options;
256
+ // 生成表格ID
257
+ const tableId = `table_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
258
+ const imageBlocks = Array();
259
+ // 创建表格单元格
260
+ const tableCells = [];
261
+ const descendants = [];
262
+ for (let row = 0; row < rowSize; row++) {
263
+ for (let col = 0; col < columnSize; col++) {
264
+ const cellId = `table_cell${row}_${col}`;
265
+ // 查找是否有配置的单元格内容
266
+ const cellConfigs = cells.filter(cell => cell.coordinate.row === row && cell.coordinate.column === col);
267
+ // 创建单元格内容
268
+ const cellContentBlocks = [];
269
+ const cellContentIds = [];
270
+ if (cellConfigs.length > 0) {
271
+ // 处理多个内容块
272
+ cellConfigs.forEach((cellConfig, index) => {
273
+ const cellContentId = `${cellId}_child_${index}`;
274
+ const cellContentBlock = {
275
+ block_id: cellContentId,
276
+ ...cellConfig.content,
277
+ children: []
278
+ };
279
+ cellContentBlocks.push(cellContentBlock);
280
+ cellContentIds.push(cellContentId);
281
+ Logger.info(`处理块:${JSON.stringify(cellConfig)} ${index}`);
282
+ if (cellConfig.content.block_type === 27) {
283
+ //把图片块保存起来,用于后续获取该图片块的token
284
+ imageBlocks.push({
285
+ coordinate: cellConfig.coordinate,
286
+ localBlockId: cellContentId,
287
+ });
288
+ }
289
+ });
290
+ }
291
+ else {
292
+ // 创建空的文本块
293
+ const cellContentId = `${cellId}_child`;
294
+ const cellContentBlock = {
295
+ block_id: cellContentId,
296
+ ...this.createTextBlock({
297
+ textContents: [{ text: "" }]
298
+ }),
299
+ children: []
300
+ };
301
+ cellContentBlocks.push(cellContentBlock);
302
+ cellContentIds.push(cellContentId);
303
+ }
304
+ // 创建表格单元格块
305
+ const tableCell = {
306
+ block_id: cellId,
307
+ block_type: 32, // 表格单元格类型
308
+ table_cell: {},
309
+ children: cellContentIds
310
+ };
311
+ tableCells.push(cellId);
312
+ descendants.push(tableCell);
313
+ descendants.push(...cellContentBlocks);
314
+ }
315
+ }
316
+ // 创建表格主体
317
+ const tableBlock = {
318
+ block_id: tableId,
319
+ block_type: 31, // 表格块类型
320
+ table: {
321
+ property: {
322
+ row_size: rowSize,
323
+ column_size: columnSize
324
+ }
325
+ },
326
+ children: tableCells
327
+ };
328
+ descendants.unshift(tableBlock);
329
+ // 过滤并记录 block_type 为 27 的元素
330
+ Logger.info(`发现 ${imageBlocks.length} 个图片块 (block_type: 27): ${JSON.stringify(imageBlocks)}`);
331
+ return {
332
+ children_id: [tableId],
333
+ descendants: descendants,
334
+ imageBlocks: imageBlocks
335
+ };
336
+ }
249
337
  }
@@ -411,6 +411,91 @@ export class FeishuApiService extends BaseApiService {
411
411
  const response = await this.post(endpoint, payload);
412
412
  return response;
413
413
  }
414
+ /**
415
+ * 创建表格块
416
+ * @param documentId 文档ID或URL
417
+ * @param parentBlockId 父块ID
418
+ * @param tableConfig 表格配置
419
+ * @param index 插入位置索引
420
+ * @returns 创建结果
421
+ */
422
+ async createTableBlock(documentId, parentBlockId, tableConfig, index = 0) {
423
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
424
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/descendant?document_revision_id=-1`;
425
+ // 处理表格配置,为每个单元格创建正确的内容块
426
+ const processedTableConfig = {
427
+ ...tableConfig,
428
+ cells: tableConfig.cells?.map(cell => ({
429
+ ...cell,
430
+ content: this.createBlockContent(cell.content.blockType, cell.content.options)
431
+ }))
432
+ };
433
+ // 使用 BlockFactory 创建表格块内容
434
+ const tableStructure = this.blockFactory.createTableBlock(processedTableConfig);
435
+ const payload = {
436
+ children_id: tableStructure.children_id,
437
+ descendants: tableStructure.descendants,
438
+ index
439
+ };
440
+ Logger.info(`请求创建表格块: ${tableConfig.rowSize}x${tableConfig.columnSize},单元格数量: ${tableConfig.cells?.length || 0}`);
441
+ const response = await this.post(endpoint, payload);
442
+ // 创建表格成功后,获取单元格中的图片token
443
+ const imageTokens = await this.extractImageTokensFromTable(response, tableStructure.imageBlocks);
444
+ return {
445
+ ...response,
446
+ imageTokens: imageTokens
447
+ };
448
+ }
449
+ /**
450
+ * 从表格中提取图片块信息(优化版本)
451
+ * @param tableResponse 创建表格的响应数据
452
+ * @param cells 表格配置,包含原始cells信息
453
+ * @returns 图片块信息数组,包含坐标和块ID信息
454
+ */
455
+ async extractImageTokensFromTable(tableResponse, cells) {
456
+ try {
457
+ const imageTokens = [];
458
+ Logger.info(`tableResponse: ${JSON.stringify(tableResponse)}`);
459
+ // 判断 cells 是否为空
460
+ if (!cells || cells.length === 0) {
461
+ Logger.info('表格中没有图片单元格,跳过图片块信息提取');
462
+ return imageTokens;
463
+ }
464
+ // 创建 localBlockId 到 block_id 的映射
465
+ const blockIdMap = new Map();
466
+ if (tableResponse && tableResponse.block_id_relations) {
467
+ for (const relation of tableResponse.block_id_relations) {
468
+ blockIdMap.set(relation.temporary_block_id, relation.block_id);
469
+ }
470
+ Logger.debug(`创建了 ${blockIdMap.size} 个块ID映射关系`);
471
+ }
472
+ // 遍历所有图片单元格
473
+ for (const cell of cells) {
474
+ const { coordinate, localBlockId } = cell;
475
+ const { row, column } = coordinate;
476
+ // 根据 localBlockId 在创建表格的返回数据中找到 block_id
477
+ const blockId = blockIdMap.get(localBlockId);
478
+ if (!blockId) {
479
+ Logger.warn(`未找到 localBlockId ${localBlockId} 对应的 block_id`);
480
+ continue;
481
+ }
482
+ Logger.debug(`处理单元格 (${row}, ${column}),localBlockId: ${localBlockId},blockId: ${blockId}`);
483
+ // 直接添加块信息
484
+ imageTokens.push({
485
+ row,
486
+ column,
487
+ blockId
488
+ });
489
+ Logger.info(`提取到图片块信息: 位置(${row}, ${column}),blockId: ${blockId}`);
490
+ }
491
+ Logger.info(`成功提取 ${imageTokens.length} 个图片块信息`);
492
+ return imageTokens;
493
+ }
494
+ catch (error) {
495
+ Logger.error(`提取表格图片块信息失败: ${error}`);
496
+ return [];
497
+ }
498
+ }
414
499
  /**
415
500
  * 删除文档中的块,支持批量删除
416
501
  * @param documentId 文档ID或URL
@@ -137,6 +137,33 @@ export const BlockConfigSchema = z.object({
137
137
  z.record(z.any()).describe("Fallback for any other block options")
138
138
  ]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
139
139
  });
140
+ // 表格列数参数定义
141
+ export const TableColumnSizeSchema = z.number().min(1).describe('Table column size (required). The number of columns in the table. Must be at least 1.');
142
+ // 表格行数参数定义
143
+ export const TableRowSizeSchema = z.number().min(1).describe('Table row size (required). The number of rows in the table. Must be at least 1.');
144
+ // 表格单元格坐标参数定义
145
+ export const TableCellCoordinateSchema = z.object({
146
+ row: z.number().min(0).describe('Row coordinate (0-based). The row position of the cell in the table.'),
147
+ column: z.number().min(0).describe('Column coordinate (0-based). The column position of the cell in the table.')
148
+ });
149
+ // 表格单元格内容配置定义
150
+ export const TableCellContentSchema = z.object({
151
+ coordinate: TableCellCoordinateSchema,
152
+ content: BlockConfigSchema
153
+ });
154
+ // 表格创建参数定义 - 专门用于创建表格块工具
155
+ export const TableCreateSchema = z.object({
156
+ columnSize: TableColumnSizeSchema,
157
+ rowSize: TableRowSizeSchema,
158
+ cells: z.array(TableCellContentSchema).optional().describe('Array of cell configurations (optional). Each cell specifies its position (row, column) and content block configuration. ' +
159
+ 'If not provided, empty text blocks will be created for all cells. ' +
160
+ 'IMPORTANT: Multiple cells can have the same coordinates (row, column) - when this happens, ' +
161
+ 'the content blocks will be added sequentially to the same cell, allowing you to create rich content ' +
162
+ 'with multiple blocks (text, code, images, etc.) within a single cell. ' +
163
+ 'Example: [{coordinate:{row:0,column:0}, content:{blockType:"text", options:{text:{textStyles:[{text:"Header"}]}}}, ' +
164
+ '{coordinate:{row:0,column:0}, content:{blockType:"code", options:{code:{code:"console.log(\'hello\')", language:30}}}}] ' +
165
+ 'will add both a text block and a code block to cell (0,0).')
166
+ });
140
167
  // 媒体ID参数定义
141
168
  export const MediaIdSchema = z.string().describe('Media ID (required). The unique identifier for a media resource (image, file, etc.) in Feishu. ' +
142
169
  'Usually obtained from image blocks or file references in documents. ' +
@@ -241,6 +241,9 @@ export class CacheManager {
241
241
  getUserToken(key) {
242
242
  return this.get(`user_access_token:${key}`);
243
243
  }
244
+ removeUserToken(key) {
245
+ return this.delete(`user_access_token:${key}`);
246
+ }
244
247
  /**
245
248
  * 缓存访问令牌
246
249
  * @param token 访问令牌
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Model Context Protocol server for Feishu integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",