feishu-mcp
Version:
Model Context Protocol server for Feishu integration
522 lines (521 loc) • 21.2 kB
JavaScript
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
};
}
}