UNPKG

feishu-mcp

Version:

Model Context Protocol server for Feishu integration

522 lines (521 loc) 21.2 kB
import { Logger } from '../utils/logger.js'; /** * 块类型枚举 */ export var BlockType; (function (BlockType) { BlockType["TEXT"] = "text"; BlockType["CODE"] = "code"; BlockType["HEADING"] = "heading"; BlockType["LIST"] = "list"; BlockType["IMAGE"] = "image"; BlockType["MERMAID"] = "mermaid"; BlockType["WHITEBOARD"] = "whiteboard"; })(BlockType || (BlockType = {})); /** * 对齐方式枚举 */ export var AlignType; (function (AlignType) { AlignType[AlignType["LEFT"] = 1] = "LEFT"; AlignType[AlignType["CENTER"] = 2] = "CENTER"; AlignType[AlignType["RIGHT"] = 3] = "RIGHT"; })(AlignType || (AlignType = {})); /** * 块工厂类 * 提供统一接口创建不同类型的块内容 */ export class BlockFactory { constructor() { } /** * 获取块工厂实例 * @returns 块工厂实例 */ static getInstance() { if (!BlockFactory.instance) { BlockFactory.instance = new BlockFactory(); } return BlockFactory.instance; } /** * 获取默认的文本元素样式 * @returns 默认文本元素样式 */ static getDefaultTextElementStyle() { return { bold: false, inline_code: false, italic: false, strikethrough: false, underline: false }; } /** * 应用默认文本样式 * @param style 已有样式(可选) * @returns 合并后的样式 */ static applyDefaultTextStyle(style) { const defaultStyle = BlockFactory.getDefaultTextElementStyle(); return style ? { ...defaultStyle, ...style } : defaultStyle; } /** * 创建块内容 * @param type 块类型 * @param options 块选项 * @returns 块内容对象 */ createBlock(type, options) { switch (type) { case BlockType.TEXT: return this.createTextBlock(options); case BlockType.CODE: return this.createCodeBlock(options); case BlockType.HEADING: return this.createHeadingBlock(options); case BlockType.LIST: return this.createListBlock(options); case BlockType.IMAGE: return this.createImageBlock(options); case BlockType.MERMAID: return this.createMermaidBlock(options); case BlockType.WHITEBOARD: return this.createWhiteboardBlock(options); default: Logger.error(`不支持的块类型: ${type}`); throw new Error(`不支持的块类型: ${type}`); } } /** * 创建文本块内容 * @param options 文本块选项 * @returns 文本块内容对象 */ createTextBlock(options) { const { textContents, align = AlignType.LEFT } = options; return { block_type: 2, // 2表示文本块 text: { elements: textContents.map(content => { // 检查是否是公式元素 if ('equation' in content) { return { equation: { content: content.equation, text_element_style: BlockFactory.applyDefaultTextStyle(content.style) } }; } else { // 普通文本元素 return { text_run: { content: content.text, text_element_style: BlockFactory.applyDefaultTextStyle(content.style) } }; } }), style: { align: align, // 1 居左,2 居中,3 居右 folded: false } } }; } /** * 创建代码块内容 * @param options 代码块选项 * @returns 代码块内容对象 */ createCodeBlock(options) { const { code, language = 0, wrap = false } = options; // 校验 language 合法性,飞书API只允许1~75 const safeLanguage = language >= 1 && language <= 75 ? language : 1; return { block_type: 14, // 14表示代码块 code: { elements: [ { text_run: { content: code, text_element_style: BlockFactory.getDefaultTextElementStyle() } } ], style: { language: safeLanguage, wrap: wrap } } }; } /** * 创建标题块内容 * @param options 标题块选项 * @returns 标题块内容对象 */ createHeadingBlock(options) { const { text, level = 1, align = AlignType.LEFT } = options; // 确保标题级别在有效范围内(1-9) const safeLevel = Math.max(1, Math.min(9, level)); // 根据标题级别设置block_type和对应的属性名 // 飞书API中,一级标题的block_type为3,二级标题为4,以此类推 const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推 const headingKey = `heading${safeLevel}`; // heading1, heading2, ... // 构建块内容 const blockContent = { block_type: blockType }; // 设置对应级别的标题属性 blockContent[headingKey] = { elements: [ { text_run: { content: text, text_element_style: BlockFactory.getDefaultTextElementStyle() } } ], style: { align: align, folded: false } }; return blockContent; } /** * 创建列表块内容(有序或无序) * @param options 列表块选项 * @returns 列表块内容对象 */ createListBlock(options) { const { text, isOrdered = false, align = AlignType.LEFT } = options; // 有序列表是 block_type: 13,无序列表是 block_type: 12 const blockType = isOrdered ? 13 : 12; const propertyKey = isOrdered ? "ordered" : "bullet"; // 构建块内容 const blockContent = { block_type: blockType }; // 设置列表属性 blockContent[propertyKey] = { elements: [ { text_run: { content: text, text_element_style: BlockFactory.getDefaultTextElementStyle() } } ], style: { align: align, folded: false } }; return blockContent; } /** * 创建图片块内容(空图片块,需要后续设置图片资源) * @param options 图片块选项 * @returns 图片块内容对象 */ createImageBlock(options = {}) { const { width = 100, height = 100 } = options; return { block_type: 27, // 27表示图片块 image: { width: width, height: height, token: "" // 空token,需要后续通过API设置 } }; } /** * 创建Mermaid * @param options Mermaid块选项 * @returns Mermaid块内容对象 */ createMermaidBlock(options = {}) { const { code } = options; return { block_type: 40, add_ons: { component_id: '', component_type_id: 'blk_631fefbbae02400430b8f9f4', record: JSON.stringify({ data: code, }), }, }; } /** * 创建画板块内容(空画板块,需要后续填充内容) * @param options 画板块选项 * @returns 画板块内容对象 */ createWhiteboardBlock(options = {}) { const { align = AlignType.CENTER } = options; return { block_type: 43, // 43表示画板块 board: { align: align // 1 居左,2 居中,3 居右 } }; } /** * 根据 blockType 字符串和 options 对象创建块内容 * 将 MCP 工具传入的高级选项转换为 BlockFactory.createBlock 所需格式 */ createBlockContentFromOptions(blockType, options) { try { // 处理特殊的 heading 格式,如 heading1, heading2 等 if (typeof blockType === 'string' && blockType.startsWith('heading')) { const headingMatch = blockType.match(/^heading([1-9])$/); if (headingMatch) { const level = parseInt(headingMatch[1], 10); if (level >= 1 && level <= 9) { if (!options || Object.keys(options).length === 0) { options = { heading: { level, content: '', align: 1 } }; } else if (!('heading' in options)) { options = { heading: { level, content: '', align: 1 } }; } else if (options.heading && !('level' in options.heading)) { options.heading.level = level; } blockType = BlockType.HEADING; Logger.info(`转换特殊标题格式: heading${level} -> standard heading with level=${level}`); } } } const blockTypeEnum = blockType; const blockConfig = { type: blockTypeEnum, options: {} }; switch (blockTypeEnum) { case BlockType.TEXT: if ('text' in options && options.text) { const textOptions = options.text; const textStyles = textOptions.textStyles || []; const processedTextStyles = textStyles.map((item) => { if (item.equation !== undefined) { return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) }; } return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) }; }); blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 }; } break; case BlockType.CODE: if ('code' in options && options.code) { const codeOptions = options.code; blockConfig.options = { code: codeOptions.code || '', language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0), wrap: codeOptions.wrap || false }; } break; case BlockType.HEADING: if ('heading' in options && options.heading) { const headingOptions = options.heading; blockConfig.options = { text: headingOptions.content || '', level: headingOptions.level || 1, align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1 }; } break; case BlockType.LIST: if ('list' in options && options.list) { const listOptions = options.list; blockConfig.options = { text: listOptions.content || '', isOrdered: listOptions.isOrdered || false, align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1 }; } break; case BlockType.IMAGE: if ('image' in options && options.image) { const imageOptions = options.image; blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 }; } else { blockConfig.options = { width: 100, height: 100 }; } break; case BlockType.MERMAID: if ('mermaid' in options && options.mermaid) { blockConfig.options = { code: options.mermaid.code }; } break; case BlockType.WHITEBOARD: if ('whiteboard' in options && options.whiteboard) { const whiteboardOptions = options.whiteboard; blockConfig.options = { align: [1, 2, 3].includes(whiteboardOptions.align) ? whiteboardOptions.align : 1 }; } else { blockConfig.options = { align: 1 }; } break; default: Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`); if ('text' in options) { blockConfig.type = BlockType.TEXT; const textOptions = options.text; const textStyles = textOptions.textStyles || []; const processedTextStyles = textStyles.map((item) => { if (item.equation !== undefined) { return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) }; } return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) }; }); blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 }; } else if ('code' in options) { blockConfig.type = BlockType.CODE; const codeOptions = options.code; blockConfig.options = { code: codeOptions.code || '', language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0), wrap: codeOptions.wrap || false }; } else if ('heading' in options) { blockConfig.type = BlockType.HEADING; const headingOptions = options.heading; blockConfig.options = { text: headingOptions.content || '', level: headingOptions.level || 1, align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1 }; } else if ('list' in options) { blockConfig.type = BlockType.LIST; const listOptions = options.list; blockConfig.options = { text: listOptions.content || '', isOrdered: listOptions.isOrdered || false, align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1 }; } else if ('image' in options) { blockConfig.type = BlockType.IMAGE; const imageOptions = options.image; blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 }; } else if ('mermaid' in options) { blockConfig.type = BlockType.MERMAID; blockConfig.options = { code: options.mermaid.code }; } else if ('whiteboard' in options) { blockConfig.type = BlockType.WHITEBOARD; const whiteboardConfig = options.whiteboard; blockConfig.options = { align: [1, 2, 3].includes(whiteboardConfig.align) ? whiteboardConfig.align : 1 }; } break; } Logger.debug(`创建块内容: 类型=${blockConfig.type}, 选项=${JSON.stringify(blockConfig.options)}`); return this.createBlock(blockConfig.type, blockConfig.options); } catch (error) { Logger.error(`创建块内容对象失败: ${error}`); return null; } } /** * 创建表格块 * @param options 表格块选项 * @returns 表格块内容对象 */ createTableBlock(options) { const { columnSize, rowSize, cells = [] } = options; // 生成表格ID const tableId = `table_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const imageBlocks = Array(); // 创建表格单元格 const tableCells = []; const descendants = []; for (let row = 0; row < rowSize; row++) { for (let col = 0; col < columnSize; col++) { const cellId = `table_cell${row}_${col}`; // 查找是否有配置的单元格内容 const cellConfigs = cells.filter(cell => cell.coordinate.row === row && cell.coordinate.column === col); // 创建单元格内容 const cellContentBlocks = []; const cellContentIds = []; if (cellConfigs.length > 0) { // 处理多个内容块 cellConfigs.forEach((cellConfig, index) => { const cellContentId = `${cellId}_child_${index}`; const cellContentBlock = { block_id: cellContentId, ...cellConfig.content, children: [] }; cellContentBlocks.push(cellContentBlock); cellContentIds.push(cellContentId); Logger.info(`处理块:${JSON.stringify(cellConfig)} ${index}`); if (cellConfig.content.block_type === 27) { //把图片块保存起来,用于后续获取该图片块的token imageBlocks.push({ coordinate: cellConfig.coordinate, localBlockId: cellContentId, }); } }); } else { // 创建空的文本块 const cellContentId = `${cellId}_child`; const cellContentBlock = { block_id: cellContentId, ...this.createTextBlock({ textContents: [{ text: "" }] }), children: [] }; cellContentBlocks.push(cellContentBlock); cellContentIds.push(cellContentId); } // 创建表格单元格块 const tableCell = { block_id: cellId, block_type: 32, // 表格单元格类型 table_cell: {}, children: cellContentIds }; tableCells.push(cellId); descendants.push(tableCell); descendants.push(...cellContentBlocks); } } // 创建表格主体 const tableBlock = { block_id: tableId, block_type: 31, // 表格块类型 table: { property: { row_size: rowSize, column_size: columnSize } }, children: tableCells }; descendants.unshift(tableBlock); // 过滤并记录 block_type 为 27 的元素 Logger.info(`发现 ${imageBlocks.length} 个图片块 (block_type: 27): ${JSON.stringify(imageBlocks)}`); return { children_id: [tableId], descendants: descendants, imageBlocks: imageBlocks }; } }