@@ -4,7 +4,12 @@ import { feishuClient } from '../client.js';
44import { logger } from '../../utils/logger.js' ;
55import { validateToken } from './validation.js' ;
66import { grantOwnerPermission , grantChatMembersPermission } from './permissions.js' ;
7- import { markdownToBlocks , batchBlocks , parseInlineMarkdown } from './markdown-to-blocks.js' ;
7+ import {
8+ markdownToSegments ,
9+ buildTableDescendants ,
10+ batchBlocks ,
11+ parseInlineMarkdown ,
12+ } from './markdown-to-blocks.js' ;
813
914/** Max lines returned by read before truncation (matches Claude Code's Read tool default) */
1015const READ_LINE_LIMIT = 2000 ;
@@ -14,16 +19,16 @@ const BLOCK_TYPE_NAMES: Record<number, string> = {
1419 1 : 'page' , 2 : 'text' , 3 : 'heading1' , 4 : 'heading2' , 5 : 'heading3' ,
1520 6 : 'heading4' , 7 : 'heading5' , 8 : 'heading6' , 9 : 'heading7' ,
1621 10 : 'heading8' , 11 : 'heading9' , 12 : 'bullet' , 13 : 'ordered' ,
17- 14 : 'code' , 15 : 'quote' , 16 : 'todo' , 17 : 'bitable' , 18 : 'callout' ,
18- 19 : 'chat_card' , 20 : 'diagram' , 21 : 'divider' , 22 : 'file' ,
19- 23 : 'grid' , 24 : 'grid_column' , 25 : 'iframe' , 26 : 'image' ,
20- 27 : 'isv' , 28 : 'mindnote' , 29 : 'sheet' , 30 : 'table' ,
21- 31 : 'table_cell' , 32 : 'view' , 33 : 'undefined' , 999 : 'virtual_merge ',
22- 34 : 'quote_container' , 40 : 'task' , 41 : 'okr' ,
22+ 14 : 'code' , 15 : 'quote' , 17 : 'todo' , 18 : 'bitable' , 19 : 'callout' ,
23+ 20 : 'chat_card' , 21 : 'diagram' , 22 : 'divider' , 23 : 'file' ,
24+ 24 : 'grid' , 25 : 'grid_column' , 26 : 'iframe' , 27 : 'image' ,
25+ 28 : 'isv' , 29 : 'mindnote' , 30 : 'sheet' , 31 : 'table' ,
26+ 32 : 'table_cell' , 33 : 'view' , 34 : 'quote_container ' ,
27+ 40 : 'task' , 41 : 'okr' ,
2328 42 : 'okr_objective' , 43 : 'okr_key_result' , 44 : 'okr_progress' ,
2429 46 : 'add_ons' , 48 : 'jira_issue' , 49 : 'wiki_catalog' ,
2530 51 : 'board' , 52 : 'agenda' , 53 : 'agenda_item' ,
26- 54 : 'agenda_item_content' ,
31+ 54 : 'agenda_item_content' , 999 : 'undefined' ,
2732} ;
2833
2934/**
@@ -58,6 +63,58 @@ function extractBlockText(block: Record<string, unknown>): string {
5863 return '' ;
5964}
6065
66+ /**
67+ * 将 Markdown 内容写入文档指定父 block,支持渲染原生飞书表格。
68+ *
69+ * 普通 block 走 `documentBlockChildren.create`(分批);每个 Markdown 表格通过
70+ * `documentBlockDescendant.create` 创建为原生 `table`。segment 按顺序写入,当传入
71+ * `startIndex` 时,插入位置按已写入的顶层 block 数量递增,从而保证混排内容的顺序。
72+ *
73+ * @returns 写入的顶层 block 数量。
74+ */
75+ async function writeMarkdownContent (
76+ client : typeof feishuClient . raw ,
77+ documentId : string ,
78+ parentBlockId : string ,
79+ markdown : string ,
80+ startIndex ?: number ,
81+ ) : Promise < number > {
82+ const segments = markdownToSegments ( markdown ) ;
83+ let index = startIndex ;
84+ let written = 0 ;
85+
86+ for ( const seg of segments ) {
87+ if ( seg . type === 'blocks' ) {
88+ for ( const batch of batchBlocks ( seg . blocks ) ) {
89+ const resp = await client . docx . documentBlockChildren . create ( {
90+ path : { document_id : documentId , block_id : parentBlockId } ,
91+ data : { children : batch , ...( index != null ? { index } : { } ) } ,
92+ } ) ;
93+ if ( resp . code !== 0 ) throw new Error ( `写入 blocks 失败 (${ resp . code } ): ${ resp . msg } ` ) ;
94+ if ( index != null ) index += batch . length ;
95+ written += batch . length ;
96+ }
97+ } else {
98+ const { childrenId, descendants } = buildTableDescendants ( seg . table ) ;
99+ const resp = await client . docx . documentBlockDescendant . create ( {
100+ path : { document_id : documentId , block_id : parentBlockId } ,
101+ data : {
102+ children_id : childrenId ,
103+ descendants : descendants as unknown as NonNullable <
104+ Parameters < typeof client . docx . documentBlockDescendant . create > [ 0 ]
105+ > [ 'data' ] [ 'descendants' ] ,
106+ ...( index != null ? { index } : { } ) ,
107+ } ,
108+ } ) ;
109+ if ( resp . code !== 0 ) throw new Error ( `写入表格失败 (${ resp . code } ): ${ resp . msg } ` ) ;
110+ if ( index != null ) index += childrenId . length ;
111+ written += childrenId . length ;
112+ }
113+ }
114+
115+ return written ;
116+ }
117+
61118/**
62119 * 飞书文档 MCP 工具
63120 *
@@ -80,7 +137,7 @@ export function feishuDocTool(chatId?: string) {
80137 '- insert_blocks: 在指定位置插入新 block (需要 block_id 作为父 block,index 指定位置)' ,
81138 '- delete_blocks: 删除指定 block (需要 block_id)' ,
82139 '' ,
83- 'write/append/insert_blocks 支持的 Markdown 语法: 标题(#)、加粗(**)、斜体(*)、删除线(~~)、行内代码(`)、链接、无序列表(-)、有序列表(1.)、代码块(```)、待办(- [ ])、分隔线(---)。' ,
140+ 'write/append/insert_blocks 支持的 Markdown 语法: 标题(#)、加粗(**)、斜体(*)、删除线(~~)、行内代码(`)、链接、无序列表(-)、有序列表(1.)、代码块(```)、待办(- [ ])、分隔线(---)、表格(| 列1 | 列2 |,会转换为原生飞书表格,首行为表头) 。' ,
84141 '' ,
85142 '读取文档的推荐流程: read (自动截断大文档) → 如需查看被截断部分,用 list_blocks 定位 → read_blocks 按需读取' ,
86143 '编辑他人文档的推荐流程: list_blocks → 找到目标 block_id → update_block/insert_blocks/delete_blocks' ,
@@ -197,15 +254,7 @@ export function feishuDocTool(chatId?: string) {
197254 const pageBlock2 = ( listResp . data ?. items ?? [ ] ) . find ( ( b ) => b . block_type === 1 ) ;
198255 const pageBlockId2 = pageBlock2 ?. block_id ?? args . doc_token ;
199256
200- const blocks = markdownToBlocks ( args . content ) ;
201- const batches = batchBlocks ( blocks ) ;
202- for ( const batch of batches ) {
203- const createResp1 = await client . docx . documentBlockChildren . create ( {
204- path : { document_id : args . doc_token , block_id : pageBlockId2 } ,
205- data : { children : batch } ,
206- } ) ;
207- if ( createResp1 . code !== 0 ) throw new Error ( `写入 blocks 失败 (${ createResp1 . code } ): ${ createResp1 . msg } ` ) ;
208- }
257+ await writeMarkdownContent ( client , args . doc_token , pageBlockId2 , args . content ) ;
209258 return { content : [ { type : 'text' as const , text : '文档已更新' } ] } ;
210259 }
211260
@@ -220,15 +269,7 @@ export function feishuDocTool(chatId?: string) {
220269 const pageBlock3 = ( listResp2 . data ?. items ?? [ ] ) . find ( ( b ) => b . block_type === 1 ) ;
221270 const pageBlockId3 = pageBlock3 ?. block_id ?? args . doc_token ;
222271
223- const appendBlocks = markdownToBlocks ( args . content ) ;
224- const appendBatches = batchBlocks ( appendBlocks ) ;
225- for ( const batch of appendBatches ) {
226- const createResp2 = await client . docx . documentBlockChildren . create ( {
227- path : { document_id : args . doc_token , block_id : pageBlockId3 } ,
228- data : { children : batch } ,
229- } ) ;
230- if ( createResp2 . code !== 0 ) throw new Error ( `追加 blocks 失败 (${ createResp2 . code } ): ${ createResp2 . msg } ` ) ;
231- }
272+ await writeMarkdownContent ( client , args . doc_token , pageBlockId3 , args . content ) ;
232273 return { content : [ { type : 'text' as const , text : '内容已追加' } ] } ;
233274 }
234275
@@ -248,16 +289,11 @@ export function feishuDocTool(chatId?: string) {
248289
249290 // 如果提供了 content,创建后自动写入,避免空文档
250291 if ( args . content ) {
251- const blocks = markdownToBlocks ( args . content ) ;
252- const batches = batchBlocks ( blocks ) ;
253- for ( const batch of batches ) {
254- const writeResp = await client . docx . documentBlockChildren . create ( {
255- path : { document_id : doc . document_id , block_id : doc . document_id } ,
256- data : { children : batch } ,
257- } ) ;
258- if ( writeResp . code !== 0 ) {
259- logger . warn ( { code : writeResp . code , msg : writeResp . msg } , 'create: 写入内容失败,文档已创建但为空' ) ;
260- }
292+ try {
293+ await writeMarkdownContent ( client , doc . document_id , doc . document_id , args . content ) ;
294+ } catch ( writeErr ) {
295+ const wmsg = writeErr instanceof Error ? writeErr . message : String ( writeErr ) ;
296+ logger . warn ( { err : wmsg } , 'create: 写入内容失败,文档已创建但内容可能不完整' ) ;
261297 }
262298 }
263299 }
@@ -344,7 +380,7 @@ export function feishuDocTool(chatId?: string) {
344380 const typeId = block . block_type as number ;
345381 const typeName = BLOCK_TYPE_NAMES [ typeId ] ?? `type_${ typeId } ` ;
346382 const text = extractBlockText ( block ) ;
347- if ( text || typeId === 21 /* divider */ ) {
383+ if ( text || typeId === 22 /* divider */ ) {
348384 blockTexts . push ( `[${ typeName } ] ${ text } ` ) ;
349385 }
350386 }
@@ -376,21 +412,10 @@ export function feishuDocTool(chatId?: string) {
376412 if ( ! args . doc_token ) throw new Error ( 'insert_blocks 操作需要 doc_token' ) ;
377413 if ( ! args . block_id ) throw new Error ( 'insert_blocks 操作需要 block_id (父 block)' ) ;
378414 if ( ! args . content ) throw new Error ( 'insert_blocks 操作需要 content' ) ;
379- const insertedBlocks = markdownToBlocks ( args . content ) ;
380- const insertBatches = batchBlocks ( insertedBlocks ) ;
381- let currentIndex = args . index ;
382- for ( const batch of insertBatches ) {
383- const createResp3 = await client . docx . documentBlockChildren . create ( {
384- path : { document_id : args . doc_token , block_id : args . block_id } ,
385- data : {
386- children : batch ,
387- ...( currentIndex != null ? { index : currentIndex } : { } ) ,
388- } ,
389- } ) ;
390- if ( createResp3 . code !== 0 ) throw new Error ( `插入 blocks 失败 (${ createResp3 . code } ): ${ createResp3 . msg } ` ) ;
391- if ( currentIndex != null ) currentIndex += batch . length ;
392- }
393- return { content : [ { type : 'text' as const , text : `已插入 ${ insertedBlocks . length } 个 block` } ] } ;
415+ const insertedCount = await writeMarkdownContent (
416+ client , args . doc_token , args . block_id , args . content , args . index ,
417+ ) ;
418+ return { content : [ { type : 'text' as const , text : `已插入 ${ insertedCount } 个 block` } ] } ;
394419 }
395420
396421 case 'delete_blocks' : {
0 commit comments