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.
- package/LICENSE +21 -21
- package/README.md +264 -257
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/mcp/tools/feishuBlockTools.js +76 -2
- package/dist/mcp/tools/feishuTools.js +43 -5
- package/dist/server.js +117 -1
- package/dist/services/baseService.js +5 -1
- package/dist/services/blockFactory.js +109 -0
- package/dist/services/feishuApiService.js +151 -9
- package/dist/types/feishuSchema.js +37 -1
- package/dist/utils/cache.js +3 -0
- package/dist/utils/document.js +114 -114
- package/dist/utils/paramUtils.js +38 -0
- package/package.json +75 -75
|
@@ -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}
|
|
114
|
-
responseText += '
|
|
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 += '
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|