feishu-mcp 0.1.5 → 0.1.7

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.
@@ -12,7 +12,7 @@ BlockConfigSchema, MediaIdSchema, MediaExtraSchema, ImagesArraySchema,
12
12
  // MermaidCodeSchema,
13
13
  // ImageWidthSchema,
14
14
  // ImageHeightSchema
15
- TableCreateSchema } from '../../types/feishuSchema.js';
15
+ TableCreateSchema, WhiteboardFillArraySchema } from '../../types/feishuSchema.js';
16
16
  /**
17
17
  * 注册飞书块相关的MCP工具
18
18
  * @param server MCP服务器实例
@@ -47,7 +47,7 @@ export function registerFeishuBlockTools(server, feishuService) {
47
47
  }
48
48
  });
49
49
  // 添加通用飞书块创建工具(支持文本、代码、标题)
50
- server.tool('batch_create_feishu_blocks', 'PREFERRED: Efficiently creates multiple blocks (text, code, heading, list) in a single API call. USE THIS TOOL when creating multiple consecutive blocks at the same position - reduces API calls by up to 90%. KEY FEATURES: (1) Handles any number of blocks by auto-batching large requests (>50 blocks), (2) Creates blocks at consecutive positions in a document, (3) Supports direct heading level format (e.g. "heading1", "heading2") or standard "heading" type with level in options. CORRECT FORMAT: mcp_feishu_batch_create_feishu_blocks({documentId:"doc123",parentBlockId:"para123",startIndex:0,blocks:[{blockType:"text",options:{...}},{blockType:"heading1",options:{heading:{content:"Title"}}}]}). For separate positions, use individual block creation tools instead. For wiki links (https://xxx.feishu.cn/wiki/xxx), first convert with convert_feishu_wiki_to_document_id tool.', {
50
+ server.tool('batch_create_feishu_blocks', 'PREFERRED: Efficiently creates multiple blocks (text, code, heading, list, image, mermaid, whiteboard) in a single API call. USE THIS TOOL when creating multiple consecutive blocks at the same position - reduces API calls by up to 90%. KEY FEATURES: (1) Handles any number of blocks by auto-batching large requests (>50 blocks), (2) Creates blocks at consecutive positions in a document, (3) Supports direct heading level format (e.g. "heading1", "heading2") or standard "heading" type with level in options. CORRECT FORMAT: mcp_feishu_batch_create_feishu_blocks({documentId:"doc123",parentBlockId:"para123",startIndex:0,blocks:[{blockType:"text",options:{...}},{blockType:"heading1",options:{heading:{content:"Title"}}}]}). For whiteboard blocks, use blockType:"whiteboard" with options:{whiteboard:{align:1}}. After creating a whiteboard block, you will receive a token in the response (board.token field) which can be used with fill_whiteboard_with_plantuml tool. The fill_whiteboard_with_plantuml tool supports both PlantUML (syntax_type: 1) and Mermaid (syntax_type: 2) formats. For separate positions, use individual block creation tools instead. For wiki links (https://xxx.feishu.cn/wiki/xxx), first convert with convert_feishu_wiki_to_document_id tool.', {
51
51
  documentId: DocumentIdSchema,
52
52
  parentBlockId: ParentBlockIdSchema,
53
53
  index: IndexSchema,
@@ -110,6 +110,9 @@ export function registerFeishuBlockTools(server, feishuService) {
110
110
  // 检查是否有图片块(block_type=27)
111
111
  const imageBlocks = result.children?.filter((child) => child.block_type === 27) || [];
112
112
  const hasImageBlocks = imageBlocks.length > 0;
113
+ // 检查是否有画板块(block_type=43)
114
+ const whiteboardBlocks = result.children?.filter((child) => child.block_type === 43) || [];
115
+ const hasWhiteboardBlocks = whiteboardBlocks.length > 0;
113
116
  const responseData = {
114
117
  ...result,
115
118
  nextIndex: index + blockContents.length,
@@ -120,6 +123,17 @@ export function registerFeishuBlockTools(server, feishuService) {
120
123
  blockIds: imageBlocks.map((block) => block.block_id),
121
124
  reminder: "检测到图片块已创建!请使用 upload_and_bind_image_to_block 工具上传图片并绑定到对应的块ID。"
122
125
  }
126
+ }),
127
+ ...(hasWhiteboardBlocks && {
128
+ whiteboardBlocksInfo: {
129
+ count: whiteboardBlocks.length,
130
+ blocks: whiteboardBlocks.map((block) => ({
131
+ blockId: block.block_id,
132
+ token: block.board?.token,
133
+ align: block.board?.align
134
+ })),
135
+ reminder: "检测到画板块已创建!请使用 fill_whiteboard_with_plantuml 工具填充画板内容,使用返回的 token 作为 whiteboardId 参数。支持 PlantUML (syntax_type: 1) 和 Mermaid (syntax_type: 2) 两种格式。"
136
+ }
123
137
  })
124
138
  };
125
139
  return {
@@ -201,12 +215,26 @@ export function registerFeishuBlockTools(server, feishuService) {
201
215
  allImageBlocks.push(...imageBlocks);
202
216
  });
203
217
  const hasImageBlocks = allImageBlocks.length > 0;
204
- const responseText = `所有飞书块创建成功,共分 ${totalBatches} 批创建了 ${createdBlocksCount} 个块。\n\n` +
218
+ // 检查所有批次中是否有画板块(block_type=43)
219
+ const allWhiteboardBlocks = [];
220
+ results.forEach(batchResult => {
221
+ const whiteboardBlocks = batchResult.children?.filter((child) => child.block_type === 43) || [];
222
+ allWhiteboardBlocks.push(...whiteboardBlocks);
223
+ });
224
+ const hasWhiteboardBlocks = allWhiteboardBlocks.length > 0;
225
+ let responseText = `所有飞书块创建成功,共分 ${totalBatches} 批创建了 ${createdBlocksCount} 个块。\n\n` +
205
226
  `最后一批结果: ${JSON.stringify(results[results.length - 1], null, 2)}\n\n` +
206
- `下一个索引位置: ${currentStartIndex},总创建块数: ${createdBlocksCount}` +
207
- (hasImageBlocks ? `\n\n⚠️ 检测到 ${allImageBlocks.length} 个图片块已创建!\n` +
227
+ `下一个索引位置: ${currentStartIndex},总创建块数: ${createdBlocksCount}`;
228
+ if (hasImageBlocks) {
229
+ responseText += `\n\n⚠️ 检测到 ${allImageBlocks.length} 个图片块已创建!\n` +
208
230
  `图片块IDs: ${allImageBlocks.map(block => block.block_id).join(', ')}\n` +
209
- `请使用 upload_and_bind_image_to_block 工具上传图片并绑定到对应的块ID。` : '');
231
+ `请使用 upload_and_bind_image_to_block 工具上传图片并绑定到对应的块ID。`;
232
+ }
233
+ if (hasWhiteboardBlocks) {
234
+ responseText += `\n\n⚠️ 检测到 ${allWhiteboardBlocks.length} 个画板块已创建!\n` +
235
+ `画板块信息:\n${allWhiteboardBlocks.map((block) => ` - blockId: ${block.block_id}, token: ${block.board?.token || 'N/A'}\n`).join('')}` +
236
+ `请使用 fill_whiteboard_with_plantuml 工具填充画板内容,使用返回的 token 作为 whiteboardId 参数。支持 PlantUML (syntax_type: 1) 和 Mermaid (syntax_type: 2) 两种格式。`;
237
+ }
210
238
  return {
211
239
  content: [
212
240
  {
@@ -659,4 +687,97 @@ export function registerFeishuBlockTools(server, feishuService) {
659
687
  };
660
688
  }
661
689
  });
690
+ // 添加批量填充画板工具(支持 PlantUML 和 Mermaid)
691
+ server.tool('fill_whiteboard_with_plantuml', 'Batch fills multiple whiteboard blocks with diagram content (PlantUML or Mermaid). Use this tool after creating whiteboard blocks with batch_create_feishu_blocks tool. Each item in the array should contain whiteboardId (the token from board.token field), code and syntax_type. Supports both PlantUML (syntax_type: 1) and Mermaid (syntax_type: 2) formats. Returns detailed results including which whiteboards were filled successfully and which failed, along with failure reasons. The same whiteboard can be filled multiple times.', {
692
+ whiteboards: WhiteboardFillArraySchema,
693
+ }, async ({ whiteboards }) => {
694
+ try {
695
+ if (!feishuService) {
696
+ return {
697
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
698
+ };
699
+ }
700
+ if (!whiteboards || whiteboards.length === 0) {
701
+ return {
702
+ content: [{ type: 'text', text: '错误:画板数组不能为空' }],
703
+ };
704
+ }
705
+ Logger.info(`开始批量填充画板内容,共 ${whiteboards.length} 个画板`);
706
+ const results = [];
707
+ let successCount = 0;
708
+ let failCount = 0;
709
+ // 逐个处理每个画板
710
+ for (let i = 0; i < whiteboards.length; i++) {
711
+ const item = whiteboards[i];
712
+ const { whiteboardId, code, syntax_type } = item;
713
+ const syntaxTypeName = syntax_type === 1 ? 'PlantUML' : 'Mermaid';
714
+ Logger.info(`处理第 ${i + 1}/${whiteboards.length} 个画板,画板ID: ${whiteboardId},语法类型: ${syntaxTypeName}`);
715
+ try {
716
+ const result = await feishuService.createDiagramNode(whiteboardId, code, syntax_type);
717
+ Logger.info(`画板填充成功,画板ID: ${whiteboardId}`);
718
+ successCount++;
719
+ results.push({
720
+ whiteboardId: whiteboardId,
721
+ syntaxType: syntaxTypeName,
722
+ status: 'success',
723
+ nodeId: result.node_id,
724
+ result: result
725
+ });
726
+ }
727
+ catch (error) {
728
+ Logger.error(`画板填充失败,画板ID: ${whiteboardId}`, error);
729
+ failCount++;
730
+ // 提取详细的错误信息
731
+ let errorMessage = formatErrorMessage(error);
732
+ let errorCode;
733
+ let logId;
734
+ if (error?.apiError) {
735
+ const apiError = error.apiError;
736
+ if (apiError.code !== undefined && apiError.msg) {
737
+ errorCode = apiError.code;
738
+ errorMessage = apiError.msg;
739
+ if (apiError.log_id) {
740
+ logId = apiError.log_id;
741
+ }
742
+ }
743
+ }
744
+ else if (error?.err) {
745
+ errorMessage = error.err;
746
+ }
747
+ else if (error?.message) {
748
+ errorMessage = error.message;
749
+ }
750
+ results.push({
751
+ whiteboardId: whiteboardId,
752
+ syntaxType: syntaxTypeName,
753
+ status: 'failed',
754
+ error: {
755
+ message: errorMessage,
756
+ code: errorCode,
757
+ logId: logId,
758
+ details: error
759
+ }
760
+ });
761
+ }
762
+ }
763
+ // 构建返回结果
764
+ const summary = {
765
+ total: whiteboards.length,
766
+ success: successCount,
767
+ failed: failCount,
768
+ results: results
769
+ };
770
+ Logger.info(`批量填充画板完成,成功: ${successCount},失败: ${failCount}`);
771
+ return {
772
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
773
+ };
774
+ }
775
+ catch (error) {
776
+ Logger.error(`批量填充画板内容失败:`, error);
777
+ const errorMessage = formatErrorMessage(error);
778
+ return {
779
+ content: [{ type: 'text', text: `批量填充画板内容失败: ${errorMessage}\n\n错误详情: ${JSON.stringify(error, null, 2)}` }],
780
+ };
781
+ }
782
+ });
662
783
  }
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { Logger } from './utils/logger.js';
7
7
  import { SSEConnectionManager } from './manager/sseConnectionManager.js';
8
8
  import { FeishuMcp } from './mcp/feishuMcp.js';
9
9
  import { callback } from './services/callbackService.js';
10
- import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager } from './utils/auth/index.js';
10
+ import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager, TokenRefreshManager } from './utils/auth/index.js';
11
11
  export class FeishuMcpServer {
12
12
  constructor() {
13
13
  Object.defineProperty(this, "connectionManager", {
@@ -33,6 +33,10 @@ export class FeishuMcpServer {
33
33
  this.userContextManager = UserContextManager.getInstance();
34
34
  // 初始化TokenCacheManager,确保在启动时从文件加载缓存
35
35
  TokenCacheManager.getInstance();
36
+ // 启动Token自动刷新管理器
37
+ const tokenRefreshManager = TokenRefreshManager.getInstance();
38
+ tokenRefreshManager.start();
39
+ Logger.info('Token自动刷新管理器已在服务器启动时初始化');
36
40
  }
37
41
  async connect(transport) {
38
42
  const server = new FeishuMcp();
@@ -1,7 +1,7 @@
1
1
  import axios, { AxiosError } from 'axios';
2
2
  import FormData from 'form-data';
3
3
  import { Logger } from '../utils/logger.js';
4
- import { formatErrorMessage, AuthRequiredError } from '../utils/error.js';
4
+ import { formatErrorMessage, AuthRequiredError, ScopeInsufficientError } from '../utils/error.js';
5
5
  import { Config } from '../utils/config.js';
6
6
  import { TokenCacheManager, UserContextManager, AuthUtils } from '../utils/auth/index.js';
7
7
  /**
@@ -126,6 +126,10 @@ export class BaseApiService {
126
126
  }
127
127
  catch (error) {
128
128
  const config = Config.getInstance().feishu;
129
+ // 优先处理权限不足异常
130
+ if (error instanceof ScopeInsufficientError) {
131
+ return this.handleScopeInsufficientError(error);
132
+ }
129
133
  // 处理授权异常
130
134
  if (error instanceof AuthRequiredError) {
131
135
  return this.handleAuthFailure(config.authType === "tenant", clientKey, baseUrl, userKey);
@@ -212,6 +216,13 @@ export class BaseApiService {
212
216
  async delete(endpoint, data, needsAuth = true) {
213
217
  return this.request(endpoint, 'DELETE', data, needsAuth);
214
218
  }
219
+ /**
220
+ * 处理权限不足异常
221
+ * @param error 权限不足错误
222
+ */
223
+ handleScopeInsufficientError(error) {
224
+ throw error;
225
+ }
215
226
  /**
216
227
  * 处理认证失败
217
228
  * @param tenant 是否是tenant
@@ -300,7 +311,7 @@ export class BaseApiService {
300
311
  const { appId, appSecret } = Config.getInstance().feishu;
301
312
  const clientKey = AuthUtils.generateClientKey(userKey);
302
313
  const redirect_uri = `${baseUrl}/callback`;
303
- const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
314
+ const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read board:whiteboard:node:create contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
304
315
  const state = AuthUtils.encodeState(appId, appSecret, clientKey, redirect_uri);
305
316
  return `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${scope}&state=${state}`;
306
317
  }
@@ -10,6 +10,7 @@ export var BlockType;
10
10
  BlockType["LIST"] = "list";
11
11
  BlockType["IMAGE"] = "image";
12
12
  BlockType["MERMAID"] = "mermaid";
13
+ BlockType["WHITEBOARD"] = "whiteboard";
13
14
  })(BlockType || (BlockType = {}));
14
15
  /**
15
16
  * 对齐方式枚举
@@ -78,6 +79,8 @@ export class BlockFactory {
78
79
  return this.createImageBlock(options);
79
80
  case BlockType.MERMAID:
80
81
  return this.createMermaidBlock(options);
82
+ case BlockType.WHITEBOARD:
83
+ return this.createWhiteboardBlock(options);
81
84
  default:
82
85
  Logger.error(`不支持的块类型: ${type}`);
83
86
  throw new Error(`不支持的块类型: ${type}`);
@@ -246,6 +249,20 @@ export class BlockFactory {
246
249
  },
247
250
  };
248
251
  }
252
+ /**
253
+ * 创建画板块内容(空画板块,需要后续填充内容)
254
+ * @param options 画板块选项
255
+ * @returns 画板块内容对象
256
+ */
257
+ createWhiteboardBlock(options = {}) {
258
+ const { align = AlignType.CENTER } = options;
259
+ return {
260
+ block_type: 43, // 43表示画板块
261
+ board: {
262
+ align: align // 1 居左,2 居中,3 居右
263
+ }
264
+ };
265
+ }
249
266
  /**
250
267
  * 创建表格块
251
268
  * @param options 表格块选项
@@ -74,6 +74,9 @@ export async function callback(req, res) {
74
74
  if (data.refresh_token_expires_in) {
75
75
  data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
76
76
  }
77
+ // 添加client_id和client_secret,用于后续刷新token
78
+ data.client_id = appId;
79
+ data.client_secret = appSecret;
77
80
  // 缓存token信息
78
81
  const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
79
82
  tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);