feishu-mcp 0.1.0 → 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.
@@ -8,10 +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
+ // MermaidCodeSchema,
12
13
  // ImageWidthSchema,
13
14
  // ImageHeightSchema
14
- } from '../../types/feishuSchema.js';
15
+ TableCreateSchema } from '../../types/feishuSchema.js';
15
16
  /**
16
17
  * 注册飞书块相关的MCP工具
17
18
  * @param server MCP服务器实例
@@ -396,6 +397,38 @@ export function registerFeishuBlockTools(server, feishuService) {
396
397
  // }
397
398
  // },
398
399
  // );
400
+ // 添加创建飞书Mermaid块工具
401
+ // server.tool(
402
+ // "create_feishu_mermaid_block",
403
+ // "Creates a new Mermaid block in a Feishu document. This tool allows you to insert a Mermaid diagram by specifying the Mermaid code. 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.",
404
+ // {
405
+ // documentId: DocumentIdSchema,
406
+ // parentBlockId: ParentBlockIdSchema,
407
+ // mermaidCode: MermaidCodeSchema,
408
+ // index: IndexSchema
409
+ // },
410
+ // async ({ documentId, parentBlockId, mermaidCode, index }) => {
411
+ // try {
412
+ // if (!feishuService) {
413
+ // return {
414
+ // content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
415
+ // };
416
+ // }
417
+ // Logger.info(`开始创建飞书Mermaid块,文档ID: ${documentId},父块ID: ${parentBlockId},插入位置: ${index}`);
418
+ // const result = await (feishuService as any).createMermaidBlock(documentId, parentBlockId, mermaidCode, index);
419
+ // Logger.info(`飞书Mermaid块创建成功`);
420
+ // return {
421
+ // content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
422
+ // };
423
+ // } catch (error) {
424
+ // Logger.error(`创建飞书Mermaid块失败:`, error);
425
+ // const errorMessage = formatErrorMessage(error);
426
+ // return {
427
+ // content: [{ type: "text", text: `创建飞书Mermaid块失败: ${errorMessage}` }],
428
+ // };
429
+ // }
430
+ // },
431
+ // );
399
432
  // 添加飞书Wiki文档ID转换工具
400
433
  server.tool('convert_feishu_wiki_to_document_id', 'Converts a Feishu Wiki document link to a compatible document ID. This conversion is required before using wiki links with any other Feishu document tools.', {
401
434
  wikiUrl: z.string().describe('Wiki URL or Token (required). Supports complete URL formats like https://xxx.feishu.cn/wiki/xxxxx or direct use of the Token portion'),
@@ -585,4 +618,45 @@ export function registerFeishuBlockTools(server, feishuService) {
585
618
  };
586
619
  }
587
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
+ });
588
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 }],
@@ -196,7 +213,7 @@ export function registerFeishuTools(server, feishuService) {
196
213
  }
197
214
  });
198
215
  // 添加获取画板内容工具
199
- server.tool('get_feishu_whiteboard_content', 'Retrieves the content and structure of a Feishu whiteboard. This tool fetches all nodes (elements) from a whiteboard, including shapes, text, mind maps, and other graphical elements. 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.', {
216
+ 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.', {
200
217
  whiteboardId: WhiteboardIdSchema,
201
218
  }, async ({ whiteboardId }) => {
202
219
  try {
@@ -207,7 +224,28 @@ export function registerFeishuTools(server, feishuService) {
207
224
  }
208
225
  Logger.info(`开始获取飞书画板内容,画板ID: ${whiteboardId}`);
209
226
  const whiteboardContent = await feishuService.getWhiteboardContent(whiteboardId);
210
- Logger.info(`飞书画板内容获取成功,节点数量: ${whiteboardContent.nodes?.length || 0}`);
227
+ const nodeCount = whiteboardContent.nodes?.length || 0;
228
+ Logger.info(`飞书画板内容获取成功,节点数量: ${nodeCount}`);
229
+ // 检查节点数量是否超过100
230
+ if (nodeCount > 200) {
231
+ Logger.info(`画板节点数量过多 (${nodeCount} > 200),返回缩略图`);
232
+ try {
233
+ const thumbnailBuffer = await feishuService.getWhiteboardThumbnail(whiteboardId);
234
+ const thumbnailBase64 = thumbnailBuffer.toString('base64');
235
+ return {
236
+ content: [
237
+ {
238
+ type: 'image',
239
+ data: thumbnailBase64,
240
+ mimeType: 'image/png'
241
+ }
242
+ ],
243
+ };
244
+ }
245
+ catch (thumbnailError) {
246
+ Logger.warn(`获取画板缩略图失败,返回基本信息: ${thumbnailError}`);
247
+ }
248
+ }
211
249
  return {
212
250
  content: [{ type: 'text', text: JSON.stringify(whiteboardContent, null, 2) }],
213
251
  };
package/dist/server.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import express from 'express';
2
2
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
5
+ import { randomUUID } from 'node:crypto';
3
6
  import { Logger } from './utils/logger.js';
4
7
  import { SSEConnectionManager } from './manager/sseConnectionManager.js';
5
8
  import { FeishuMcp } from './mcp/feishuMcp.js';
@@ -27,6 +30,118 @@ export class FeishuMcpServer {
27
30
  }
28
31
  async startHttpServer(port) {
29
32
  const app = express();
33
+ const transports = {};
34
+ // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint
35
+ app.use("/mcp", express.json());
36
+ app.post('/mcp', async (req, res) => {
37
+ try {
38
+ Logger.log("Received StreamableHTTP request", {
39
+ method: req.method,
40
+ url: req.url,
41
+ headers: req.headers,
42
+ body: req.body,
43
+ query: req.query,
44
+ params: req.params
45
+ });
46
+ // Check for existing session ID
47
+ const sessionId = req.headers['mcp-session-id'];
48
+ let transport;
49
+ if (sessionId && transports[sessionId]) {
50
+ // Reuse existing transport
51
+ Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId);
52
+ transport = transports[sessionId];
53
+ }
54
+ else if (!sessionId && isInitializeRequest(req.body)) {
55
+ // New initialization request
56
+ transport = new StreamableHTTPServerTransport({
57
+ sessionIdGenerator: () => randomUUID(),
58
+ onsessioninitialized: (sessionId) => {
59
+ // Store the transport by session ID
60
+ Logger.log(`[StreamableHTTP connection] ${sessionId}`);
61
+ transports[sessionId] = transport;
62
+ }
63
+ });
64
+ // Clean up transport and server when closed
65
+ transport.onclose = () => {
66
+ if (transport.sessionId) {
67
+ Logger.log(`[StreamableHTTP delete] ${transports[transport.sessionId]}`);
68
+ delete transports[transport.sessionId];
69
+ }
70
+ };
71
+ // Create and connect server instance
72
+ const server = new FeishuMcp();
73
+ await server.connect(transport);
74
+ }
75
+ else {
76
+ // Invalid request
77
+ res.status(400).json({
78
+ jsonrpc: '2.0',
79
+ error: {
80
+ code: -32000,
81
+ message: 'Bad Request: No valid session ID provided',
82
+ },
83
+ id: null,
84
+ });
85
+ return;
86
+ }
87
+ // Handle the request
88
+ await transport.handleRequest(req, res, req.body);
89
+ }
90
+ catch (error) {
91
+ console.error('Error handling MCP request:', error);
92
+ if (!res.headersSent) {
93
+ res.status(500).json({
94
+ jsonrpc: '2.0',
95
+ error: {
96
+ code: -32603,
97
+ message: 'Internal server error',
98
+ },
99
+ id: null,
100
+ });
101
+ }
102
+ }
103
+ });
104
+ // Handle GET requests for server-to-client notifications via Streamable HTTP
105
+ app.get('/mcp', async (req, res) => {
106
+ try {
107
+ Logger.log("Received StreamableHTTP request get");
108
+ const sessionId = req.headers['mcp-session-id'];
109
+ if (!sessionId || !transports[sessionId]) {
110
+ res.status(400).send('Invalid or missing session ID');
111
+ return;
112
+ }
113
+ const transport = transports[sessionId];
114
+ await transport.handleRequest(req, res);
115
+ }
116
+ catch (error) {
117
+ console.error('Error handling GET request:', error);
118
+ if (!res.headersSent) {
119
+ res.status(500).send('Internal server error');
120
+ }
121
+ }
122
+ });
123
+ // Handle DELETE requests for session termination
124
+ app.delete('/mcp', async (req, res) => {
125
+ try {
126
+ const sessionId = req.headers['mcp-session-id'];
127
+ if (!sessionId || !transports[sessionId]) {
128
+ res.status(400).send('Invalid or missing session ID');
129
+ return;
130
+ }
131
+ const transport = transports[sessionId];
132
+ await transport.handleRequest(req, res);
133
+ // Clean up resources after session termination
134
+ if (transport.sessionId) {
135
+ delete transports[transport.sessionId];
136
+ }
137
+ }
138
+ catch (error) {
139
+ console.error('Error handling DELETE request:', error);
140
+ if (!res.headersSent) {
141
+ res.status(500).send('Internal server error');
142
+ }
143
+ }
144
+ });
30
145
  app.get('/sse', async (req, res) => {
31
146
  const sseTransport = new SSEServerTransport('/messages', res);
32
147
  const sessionId = sseTransport.sessionId;
@@ -82,10 +197,11 @@ export class FeishuMcpServer {
82
197
  res.status(500).json({ code: 500, msg: e.message || '获取token失败' });
83
198
  }
84
199
  });
85
- app.listen(port, () => {
200
+ app.listen(port, '0.0.0.0', () => {
86
201
  Logger.info(`HTTP server listening on port ${port}`);
87
202
  Logger.info(`SSE endpoint available at http://localhost:${port}/sse`);
88
203
  Logger.info(`Message endpoint available at http://localhost:${port}/messages`);
204
+ Logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
89
205
  });
90
206
  }
91
207
  }
@@ -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})`);
@@ -9,6 +9,7 @@ export var BlockType;
9
9
  BlockType["HEADING"] = "heading";
10
10
  BlockType["LIST"] = "list";
11
11
  BlockType["IMAGE"] = "image";
12
+ BlockType["MERMAID"] = "mermaid";
12
13
  })(BlockType || (BlockType = {}));
13
14
  /**
14
15
  * 对齐方式枚举
@@ -75,6 +76,8 @@ export class BlockFactory {
75
76
  return this.createListBlock(options);
76
77
  case BlockType.IMAGE:
77
78
  return this.createImageBlock(options);
79
+ case BlockType.MERMAID:
80
+ return this.createMermaidBlock(options);
78
81
  default:
79
82
  Logger.error(`不支持的块类型: ${type}`);
80
83
  throw new Error(`不支持的块类型: ${type}`);
@@ -225,4 +228,110 @@ export class BlockFactory {
225
228
  }
226
229
  };
227
230
  }
231
+ /**
232
+ * 创建Mermaid
233
+ * @param options Mermaid块选项
234
+ * @returns Mermaid块内容对象
235
+ */
236
+ createMermaidBlock(options = {}) {
237
+ const { code } = options;
238
+ return {
239
+ block_type: 40,
240
+ add_ons: {
241
+ component_id: '',
242
+ component_type_id: 'blk_631fefbbae02400430b8f9f4',
243
+ record: JSON.stringify({
244
+ data: code,
245
+ }),
246
+ },
247
+ };
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
+ }
228
337
  }
@@ -382,6 +382,120 @@ export class FeishuApiService extends BaseApiService {
382
382
  });
383
383
  return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
384
384
  }
385
+ /**
386
+ * 创建Mermaid块
387
+ * @param documentId 文档ID或URL
388
+ * @param parentBlockId 父块ID
389
+ * @param mermaidCode Mermaid代码
390
+ * @param index 插入位置索引
391
+ * @returns 创建结果
392
+ */
393
+ async createMermaidBlock(documentId, parentBlockId, mermaidCode, index = 0) {
394
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
395
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
396
+ const blockContent = {
397
+ block_type: 40,
398
+ add_ons: {
399
+ component_id: "",
400
+ component_type_id: "blk_631fefbbae02400430b8f9f4",
401
+ record: JSON.stringify({
402
+ data: mermaidCode,
403
+ })
404
+ }
405
+ };
406
+ const payload = {
407
+ children: [blockContent],
408
+ index
409
+ };
410
+ Logger.info(`请求创建Mermaid块: ${JSON.stringify(payload).slice(0, 500)}...`);
411
+ const response = await this.post(endpoint, payload);
412
+ return response;
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
+ }
385
499
  /**
386
500
  * 删除文档中的块,支持批量删除
387
501
  * @param documentId 文档ID或URL
@@ -577,6 +691,14 @@ export class FeishuApiService extends BaseApiService {
577
691
  };
578
692
  }
579
693
  break;
694
+ case BlockType.MERMAID:
695
+ if ('mermaid' in options && options.mermaid) {
696
+ const mermaidOptions = options.mermaid;
697
+ blockConfig.options = {
698
+ code: mermaidOptions.code,
699
+ };
700
+ }
701
+ break;
580
702
  default:
581
703
  Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
582
704
  if ('text' in options) {
@@ -640,6 +762,13 @@ export class FeishuApiService extends BaseApiService {
640
762
  height: imageOptions.height || 100
641
763
  };
642
764
  }
765
+ else if ("mermaid" in options) {
766
+ blockConfig.type = BlockType.MERMAID;
767
+ const mermaidConfig = options.mermaid;
768
+ blockConfig.options = {
769
+ code: mermaidConfig.code,
770
+ };
771
+ }
643
772
  break;
644
773
  }
645
774
  // 记录调试信息
@@ -955,15 +1084,7 @@ export class FeishuApiService extends BaseApiService {
955
1084
  */
956
1085
  async getWhiteboardContent(whiteboardId) {
957
1086
  try {
958
- // 从URL中提取画板ID
959
- let normalizedWhiteboardId = whiteboardId;
960
- if (whiteboardId.includes('feishu.cn/board/')) {
961
- // 从URL中提取画板ID
962
- const matches = whiteboardId.match(/board\/([^\/\?]+)/);
963
- if (matches) {
964
- normalizedWhiteboardId = matches[1];
965
- }
966
- }
1087
+ const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
967
1088
  const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes`;
968
1089
  Logger.info(`开始获取画板内容,画板ID: ${normalizedWhiteboardId}`);
969
1090
  const response = await this.get(endpoint);
@@ -974,6 +1095,27 @@ export class FeishuApiService extends BaseApiService {
974
1095
  this.handleApiError(error, '获取画板内容失败');
975
1096
  }
976
1097
  }
1098
+ /**
1099
+ * 获取画板缩略图
1100
+ * @param whiteboardId 画板ID或URL
1101
+ * @returns 画板缩略图的二进制数据
1102
+ */
1103
+ async getWhiteboardThumbnail(whiteboardId) {
1104
+ try {
1105
+ const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
1106
+ const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/download_as_image`;
1107
+ Logger.info(`开始获取画板缩略图,画板ID: ${normalizedWhiteboardId}`);
1108
+ // 使用通用的request方法获取二进制响应
1109
+ const response = await this.request(endpoint, 'GET', {}, true, {}, 'arraybuffer');
1110
+ const thumbnailBuffer = Buffer.from(response);
1111
+ Logger.info(`画板缩略图获取成功,大小: ${thumbnailBuffer.length} 字节`);
1112
+ return thumbnailBuffer;
1113
+ }
1114
+ catch (error) {
1115
+ this.handleApiError(error, '获取画板缩略图失败');
1116
+ return Buffer.from([]); // 永远不会执行到这里
1117
+ }
1118
+ }
977
1119
  /**
978
1120
  * 从路径或URL获取图片的Base64编码
979
1121
  * @param imagePathOrUrl 图片路径或URL