feishu-mcp
Version:
Model Context Protocol server for Feishu integration
156 lines (155 loc) • 6.96 kB
JavaScript
import { formatErrorMessage } from '../../../utils/error.js';
import { Logger } from '../../../utils/logger.js';
export const WHITEBOARD_NODE_THUMBNAIL_THRESHOLD = 200;
export const BATCH_SIZE = 50;
/** wiki 链接转换提示,在所有需要 documentId 的编辑类工具中复用 */
export const WIKI_NOTE = 'For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx), ' +
'use get_feishu_document_info first to obtain the documentId, then use that ID for editing operations.';
const BLOCK_TYPE_IMAGE = 27;
const BLOCK_TYPE_WHITEBOARD = 43;
export function errorResponse(text) {
return { content: [{ type: 'text', text }] };
}
// ---------------------------------------------------------------------------
// 参数校验
// ---------------------------------------------------------------------------
/**
* 校验 folderToken 与 wikiContext 的互斥关系(文件夹模式 vs 知识库模式)。
* 返回 null 表示校验通过,否则返回可直接 return 的错误响应。
*/
export function validateFolderOrWikiContext(folderToken, wikiContext) {
if (folderToken && wikiContext) {
return errorResponse('错误:不能同时提供 folderToken 和 wikiContext 参数,请选择其中一种模式。\n' +
'- 使用 folderToken 在飞书文档目录中操作\n' +
'- 使用 wikiContext 在知识库中操作');
}
if (!folderToken && !wikiContext) {
return errorResponse('错误:必须提供 folderToken(飞书文档目录模式)或 wikiContext(知识库节点模式)参数之一。');
}
return null;
}
/**
* 将 BlockConfig 数组转换为飞书 API 所需的块内容数组。
* 遇到单个块处理失败时立即终止。
*
* 注:contents 类型为 any[] 以匹配 FeishuApiService.createDocumentBlocks 的参数类型。
*/
export function prepareBlockContents(configs, feishuService) {
const contents = [];
for (const { blockType, options = {} } of configs) {
try {
const blockContent = feishuService.createBlockContent(blockType, options);
if (blockContent) {
contents.push(blockContent);
Logger.info(`已准备 ${blockType} 块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
}
else {
Logger.warn(`创建 ${blockType} 块失败,跳过此块`);
}
}
catch (err) {
Logger.error(`处理块类型 ${blockType} 时出错: ${err}`);
return {
ok: false,
error: errorResponse(`处理块类型"${blockType}"时出错: ${err}\n请检查该块类型的配置是否正确。`),
};
}
}
return { ok: true, contents };
}
// ---------------------------------------------------------------------------
// 特殊块提取与提示
// ---------------------------------------------------------------------------
/**
* 从块列表中提取图片块(block_type=27)和画板块(block_type=43)。
*/
export function extractSpecialBlocks(blocks) {
return {
imageBlocks: blocks.filter(b => b.block_type === BLOCK_TYPE_IMAGE),
whiteboardBlocks: blocks.filter(b => b.block_type === BLOCK_TYPE_WHITEBOARD),
};
}
/**
* 构建特殊块的 JSON 提示对象,用于展开合并到 JSON 响应中。
*/
export function buildSpecialBlockHints(imageBlocks, whiteboardBlocks) {
const hints = {};
if (imageBlocks.length > 0) {
hints.imageBlocksInfo = {
count: imageBlocks.length,
blockIds: imageBlocks.map(b => b.block_id),
reminder: '检测到图片块已创建!请使用 upload_and_bind_image_to_block 工具上传图片并绑定到对应的块ID。',
};
}
if (whiteboardBlocks.length > 0) {
hints.whiteboardBlocksInfo = {
count: whiteboardBlocks.length,
blocks: whiteboardBlocks.map(b => ({
blockId: b.block_id,
token: b.board?.token,
align: b.board?.align,
})),
reminder: '检测到画板块已创建!请使用 fill_whiteboard_with_plantuml 工具填充画板内容,使用返回的 token 作为 whiteboardId 参数。支持 PlantUML (syntax_type: "plantuml") 和 Mermaid (syntax_type: "mermaid") 两种格式。',
};
}
return hints;
}
/**
* 构建文本形式的特殊块提示,追加到现有响应文本后。
*/
export function appendSpecialBlockTextHints(base, imageBlocks, whiteboardBlocks) {
const sections = [base];
if (imageBlocks.length > 0) {
const lines = [
`\n\n🖼️ 检测到 ${imageBlocks.length} 个图片块 (block_type: 27)!`,
'💡 提示:如需查看图片内容,可使用 get_feishu_image_resource 工具下载图片。',
'图片信息:',
...imageBlocks.map((b, idx) => {
const parts = [` ${idx + 1}. 块ID: ${b.block_id}`];
if (b.image?.token)
parts.push(`媒体ID: ${b.image.token}`);
return parts.join(',');
}),
'📝 注意:只有在需要查看图片内容时才调用上述工具,仅了解文档结构时无需获取。',
];
sections.push(lines.join('\n'));
}
if (whiteboardBlocks.length > 0) {
const lines = [
`\n\n⚠️ 检测到 ${whiteboardBlocks.length} 个画板块 (block_type: 43)!`,
'💡 提示:如需获取画板具体内容,可使用 get_feishu_whiteboard_content 工具。',
'画板信息:',
...whiteboardBlocks.map((b, idx) => {
const parts = [` ${idx + 1}. 块ID: ${b.block_id}`];
if (b.board?.token)
parts.push(`画板ID: ${b.board.token}`);
return parts.join(',');
}),
'📝 注意:只有在需要分析画板内容时才调用上述工具,仅了解文档结构时无需获取。',
];
sections.push(lines.join('\n'));
}
return sections.join('');
}
/**
* 从飞书 API 抛出的错误对象中提取结构化错误信息。
* 飞书 API 错误可能包含 apiError.code / apiError.msg / apiError.log_id 等字段。
*/
export function extractFeishuApiError(err) {
if (err && typeof err === 'object') {
const e = err;
const apiError = e.apiError;
if (apiError && apiError.code !== undefined && apiError.msg) {
return {
message: String(apiError.msg),
code: Number(apiError.code),
logId: apiError.log_id != null ? String(apiError.log_id) : undefined,
};
}
if (typeof e.err === 'string')
return { message: e.err };
if (typeof e.message === 'string')
return { message: e.message };
}
return { message: formatErrorMessage(err) };
}