feishu-mcp 0.2.2 → 0.2.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.
Files changed (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +356 -266
  3. package/dist/cli/auth.js +117 -0
  4. package/dist/cli/commands/auth.js +141 -0
  5. package/dist/cli/commands/config.js +86 -0
  6. package/dist/cli/commands/guide.js +68 -0
  7. package/dist/cli/dispatcher.js +96 -0
  8. package/dist/cli/index.js +95 -0
  9. package/dist/manager/sseConnectionManager.js +2 -0
  10. package/dist/mcp/feishuMcp.js +25 -25
  11. package/dist/mcp/tools/blockTools.js +295 -0
  12. package/dist/mcp/tools/documentTools.js +105 -0
  13. package/dist/mcp/tools/feishuBlockTools.js +1 -285
  14. package/dist/mcp/tools/feishuTools.js +1 -67
  15. package/dist/mcp/tools/folderTools.js +99 -0
  16. package/dist/mcp/tools/toolHelpers.js +155 -0
  17. package/dist/modules/FeatureModule.js +1 -0
  18. package/dist/modules/ModuleRegistry.js +63 -0
  19. package/dist/modules/calendar/index.js +11 -0
  20. package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
  21. package/dist/modules/calendar/tools/calendarTools.js +6 -0
  22. package/dist/modules/document/index.js +15 -0
  23. package/dist/modules/document/services/FeishuBlockService.js +410 -0
  24. package/dist/modules/document/services/FeishuDocumentService.js +110 -0
  25. package/dist/modules/document/services/FeishuFoldService.js +187 -0
  26. package/dist/modules/document/services/FeishuSearchService.js +232 -0
  27. package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
  28. package/dist/modules/document/services/blockFactory.js +521 -0
  29. package/dist/modules/document/toolApi/blockToolApi.js +160 -0
  30. package/dist/modules/document/toolApi/documentToolApi.js +65 -0
  31. package/dist/modules/document/toolApi/folderToolApi.js +73 -0
  32. package/dist/modules/document/toolApi/index.js +3 -0
  33. package/dist/modules/document/tools/blockTools.js +138 -0
  34. package/dist/modules/document/tools/documentTools.js +64 -0
  35. package/dist/modules/document/tools/folderTools.js +46 -0
  36. package/dist/modules/document/tools/toolHelpers.js +155 -0
  37. package/dist/modules/index.js +5 -0
  38. package/dist/modules/member/index.js +11 -0
  39. package/dist/modules/member/services/FeishuMemberService.js +41 -0
  40. package/dist/modules/member/toolApi/index.js +1 -0
  41. package/dist/modules/member/toolApi/memberToolApi.js +54 -0
  42. package/dist/modules/member/tools/memberTools.js +26 -0
  43. package/dist/modules/task/index.js +11 -0
  44. package/dist/modules/task/services/FeishuTaskService.js +271 -0
  45. package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
  46. package/dist/modules/task/toolApi/index.js +1 -0
  47. package/dist/modules/task/toolApi/taskToolApi.js +128 -0
  48. package/dist/modules/task/tools/taskTools.js +93 -0
  49. package/dist/server.js +43 -24
  50. package/dist/services/baseService.js +11 -2
  51. package/dist/services/blockFactory.js +167 -0
  52. package/dist/services/callbackService.js +1 -1
  53. package/dist/services/constants/feishuScopes.js +94 -0
  54. package/dist/services/feishu/FeishuBaseApiService.js +47 -0
  55. package/dist/services/feishu/FeishuBlockService.js +410 -0
  56. package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
  57. package/dist/services/feishu/FeishuDocumentService.js +110 -0
  58. package/dist/services/feishu/FeishuDriveService.js +62 -0
  59. package/dist/services/feishu/FeishuFoldService.js +187 -0
  60. package/dist/services/feishu/FeishuImageService.js +219 -0
  61. package/dist/services/feishu/FeishuScopeValidator.js +177 -0
  62. package/dist/services/feishu/FeishuSearchService.js +232 -0
  63. package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
  64. package/dist/services/feishu/FeishuWikiService.js +134 -0
  65. package/dist/services/feishuApiService.js +246 -1760
  66. package/dist/services/feishuAuthService.js +43 -0
  67. package/dist/types/documentSchema.js +232 -0
  68. package/dist/types/feishuSchema.js +54 -77
  69. package/dist/types/memberSchema.js +35 -0
  70. package/dist/types/taskSchema.js +166 -0
  71. package/dist/utils/auth/tokenCacheManager.js +16 -3
  72. package/dist/utils/auth/tokenRefreshManager.js +2 -1
  73. package/dist/utils/config.js +47 -0
  74. package/dist/utils/document.js +116 -116
  75. package/dist/utils/error.js +0 -11
  76. package/dist/utils/paramUtils.js +0 -31
  77. package/package.json +77 -76
@@ -0,0 +1,521 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ /**
3
+ * 块类型枚举
4
+ */
5
+ export var BlockType;
6
+ (function (BlockType) {
7
+ BlockType["TEXT"] = "text";
8
+ BlockType["CODE"] = "code";
9
+ BlockType["HEADING"] = "heading";
10
+ BlockType["LIST"] = "list";
11
+ BlockType["IMAGE"] = "image";
12
+ BlockType["MERMAID"] = "mermaid";
13
+ BlockType["WHITEBOARD"] = "whiteboard";
14
+ })(BlockType || (BlockType = {}));
15
+ /**
16
+ * 对齐方式枚举
17
+ */
18
+ export var AlignType;
19
+ (function (AlignType) {
20
+ AlignType[AlignType["LEFT"] = 1] = "LEFT";
21
+ AlignType[AlignType["CENTER"] = 2] = "CENTER";
22
+ AlignType[AlignType["RIGHT"] = 3] = "RIGHT";
23
+ })(AlignType || (AlignType = {}));
24
+ /**
25
+ * 块工厂类
26
+ * 提供统一接口创建不同类型的块内容
27
+ */
28
+ export class BlockFactory {
29
+ constructor() { }
30
+ /**
31
+ * 获取块工厂实例
32
+ * @returns 块工厂实例
33
+ */
34
+ static getInstance() {
35
+ if (!BlockFactory.instance) {
36
+ BlockFactory.instance = new BlockFactory();
37
+ }
38
+ return BlockFactory.instance;
39
+ }
40
+ /**
41
+ * 获取默认的文本元素样式
42
+ * @returns 默认文本元素样式
43
+ */
44
+ static getDefaultTextElementStyle() {
45
+ return {
46
+ bold: false,
47
+ inline_code: false,
48
+ italic: false,
49
+ strikethrough: false,
50
+ underline: false
51
+ };
52
+ }
53
+ /**
54
+ * 应用默认文本样式
55
+ * @param style 已有样式(可选)
56
+ * @returns 合并后的样式
57
+ */
58
+ static applyDefaultTextStyle(style) {
59
+ const defaultStyle = BlockFactory.getDefaultTextElementStyle();
60
+ return style ? { ...defaultStyle, ...style } : defaultStyle;
61
+ }
62
+ /**
63
+ * 创建块内容
64
+ * @param type 块类型
65
+ * @param options 块选项
66
+ * @returns 块内容对象
67
+ */
68
+ createBlock(type, options) {
69
+ switch (type) {
70
+ case BlockType.TEXT:
71
+ return this.createTextBlock(options);
72
+ case BlockType.CODE:
73
+ return this.createCodeBlock(options);
74
+ case BlockType.HEADING:
75
+ return this.createHeadingBlock(options);
76
+ case BlockType.LIST:
77
+ return this.createListBlock(options);
78
+ case BlockType.IMAGE:
79
+ return this.createImageBlock(options);
80
+ case BlockType.MERMAID:
81
+ return this.createMermaidBlock(options);
82
+ case BlockType.WHITEBOARD:
83
+ return this.createWhiteboardBlock(options);
84
+ default:
85
+ Logger.error(`不支持的块类型: ${type}`);
86
+ throw new Error(`不支持的块类型: ${type}`);
87
+ }
88
+ }
89
+ /**
90
+ * 创建文本块内容
91
+ * @param options 文本块选项
92
+ * @returns 文本块内容对象
93
+ */
94
+ createTextBlock(options) {
95
+ const { textContents, align = AlignType.LEFT } = options;
96
+ return {
97
+ block_type: 2, // 2表示文本块
98
+ text: {
99
+ elements: textContents.map(content => {
100
+ // 检查是否是公式元素
101
+ if ('equation' in content) {
102
+ return {
103
+ equation: {
104
+ content: content.equation,
105
+ text_element_style: BlockFactory.applyDefaultTextStyle(content.style)
106
+ }
107
+ };
108
+ }
109
+ else {
110
+ // 普通文本元素
111
+ return {
112
+ text_run: {
113
+ content: content.text,
114
+ text_element_style: BlockFactory.applyDefaultTextStyle(content.style)
115
+ }
116
+ };
117
+ }
118
+ }),
119
+ style: {
120
+ align: align, // 1 居左,2 居中,3 居右
121
+ folded: false
122
+ }
123
+ }
124
+ };
125
+ }
126
+ /**
127
+ * 创建代码块内容
128
+ * @param options 代码块选项
129
+ * @returns 代码块内容对象
130
+ */
131
+ createCodeBlock(options) {
132
+ const { code, language = 0, wrap = false } = options;
133
+ // 校验 language 合法性,飞书API只允许1~75
134
+ const safeLanguage = language >= 1 && language <= 75 ? language : 1;
135
+ return {
136
+ block_type: 14, // 14表示代码块
137
+ code: {
138
+ elements: [
139
+ {
140
+ text_run: {
141
+ content: code,
142
+ text_element_style: BlockFactory.getDefaultTextElementStyle()
143
+ }
144
+ }
145
+ ],
146
+ style: {
147
+ language: safeLanguage,
148
+ wrap: wrap
149
+ }
150
+ }
151
+ };
152
+ }
153
+ /**
154
+ * 创建标题块内容
155
+ * @param options 标题块选项
156
+ * @returns 标题块内容对象
157
+ */
158
+ createHeadingBlock(options) {
159
+ const { text, level = 1, align = AlignType.LEFT } = options;
160
+ // 确保标题级别在有效范围内(1-9)
161
+ const safeLevel = Math.max(1, Math.min(9, level));
162
+ // 根据标题级别设置block_type和对应的属性名
163
+ // 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
164
+ const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
165
+ const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
166
+ // 构建块内容
167
+ const blockContent = {
168
+ block_type: blockType
169
+ };
170
+ // 设置对应级别的标题属性
171
+ blockContent[headingKey] = {
172
+ elements: [
173
+ {
174
+ text_run: {
175
+ content: text,
176
+ text_element_style: BlockFactory.getDefaultTextElementStyle()
177
+ }
178
+ }
179
+ ],
180
+ style: {
181
+ align: align,
182
+ folded: false
183
+ }
184
+ };
185
+ return blockContent;
186
+ }
187
+ /**
188
+ * 创建列表块内容(有序或无序)
189
+ * @param options 列表块选项
190
+ * @returns 列表块内容对象
191
+ */
192
+ createListBlock(options) {
193
+ const { text, isOrdered = false, align = AlignType.LEFT } = options;
194
+ // 有序列表是 block_type: 13,无序列表是 block_type: 12
195
+ const blockType = isOrdered ? 13 : 12;
196
+ const propertyKey = isOrdered ? "ordered" : "bullet";
197
+ // 构建块内容
198
+ const blockContent = {
199
+ block_type: blockType
200
+ };
201
+ // 设置列表属性
202
+ blockContent[propertyKey] = {
203
+ elements: [
204
+ {
205
+ text_run: {
206
+ content: text,
207
+ text_element_style: BlockFactory.getDefaultTextElementStyle()
208
+ }
209
+ }
210
+ ],
211
+ style: {
212
+ align: align,
213
+ folded: false
214
+ }
215
+ };
216
+ return blockContent;
217
+ }
218
+ /**
219
+ * 创建图片块内容(空图片块,需要后续设置图片资源)
220
+ * @param options 图片块选项
221
+ * @returns 图片块内容对象
222
+ */
223
+ createImageBlock(options = {}) {
224
+ const { width = 100, height = 100 } = options;
225
+ return {
226
+ block_type: 27, // 27表示图片块
227
+ image: {
228
+ width: width,
229
+ height: height,
230
+ token: "" // 空token,需要后续通过API设置
231
+ }
232
+ };
233
+ }
234
+ /**
235
+ * 创建Mermaid
236
+ * @param options Mermaid块选项
237
+ * @returns Mermaid块内容对象
238
+ */
239
+ createMermaidBlock(options = {}) {
240
+ const { code } = options;
241
+ return {
242
+ block_type: 40,
243
+ add_ons: {
244
+ component_id: '',
245
+ component_type_id: 'blk_631fefbbae02400430b8f9f4',
246
+ record: JSON.stringify({
247
+ data: code,
248
+ }),
249
+ },
250
+ };
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
+ }
266
+ /**
267
+ * 根据 blockType 字符串和 options 对象创建块内容
268
+ * 将 MCP 工具传入的高级选项转换为 BlockFactory.createBlock 所需格式
269
+ */
270
+ createBlockContentFromOptions(blockType, options) {
271
+ try {
272
+ // 处理特殊的 heading 格式,如 heading1, heading2 等
273
+ if (typeof blockType === 'string' && blockType.startsWith('heading')) {
274
+ const headingMatch = blockType.match(/^heading([1-9])$/);
275
+ if (headingMatch) {
276
+ const level = parseInt(headingMatch[1], 10);
277
+ if (level >= 1 && level <= 9) {
278
+ if (!options || Object.keys(options).length === 0) {
279
+ options = { heading: { level, content: '', align: 1 } };
280
+ }
281
+ else if (!('heading' in options)) {
282
+ options = { heading: { level, content: '', align: 1 } };
283
+ }
284
+ else if (options.heading && !('level' in options.heading)) {
285
+ options.heading.level = level;
286
+ }
287
+ blockType = BlockType.HEADING;
288
+ Logger.info(`转换特殊标题格式: heading${level} -> standard heading with level=${level}`);
289
+ }
290
+ }
291
+ }
292
+ const blockTypeEnum = blockType;
293
+ const blockConfig = {
294
+ type: blockTypeEnum,
295
+ options: {}
296
+ };
297
+ switch (blockTypeEnum) {
298
+ case BlockType.TEXT:
299
+ if ('text' in options && options.text) {
300
+ const textOptions = options.text;
301
+ const textStyles = textOptions.textStyles || [];
302
+ const processedTextStyles = textStyles.map((item) => {
303
+ if (item.equation !== undefined) {
304
+ return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) };
305
+ }
306
+ return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) };
307
+ });
308
+ blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 };
309
+ }
310
+ break;
311
+ case BlockType.CODE:
312
+ if ('code' in options && options.code) {
313
+ const codeOptions = options.code;
314
+ blockConfig.options = {
315
+ code: codeOptions.code || '',
316
+ language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
317
+ wrap: codeOptions.wrap || false
318
+ };
319
+ }
320
+ break;
321
+ case BlockType.HEADING:
322
+ if ('heading' in options && options.heading) {
323
+ const headingOptions = options.heading;
324
+ blockConfig.options = {
325
+ text: headingOptions.content || '',
326
+ level: headingOptions.level || 1,
327
+ align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1
328
+ };
329
+ }
330
+ break;
331
+ case BlockType.LIST:
332
+ if ('list' in options && options.list) {
333
+ const listOptions = options.list;
334
+ blockConfig.options = {
335
+ text: listOptions.content || '',
336
+ isOrdered: listOptions.isOrdered || false,
337
+ align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1
338
+ };
339
+ }
340
+ break;
341
+ case BlockType.IMAGE:
342
+ if ('image' in options && options.image) {
343
+ const imageOptions = options.image;
344
+ blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 };
345
+ }
346
+ else {
347
+ blockConfig.options = { width: 100, height: 100 };
348
+ }
349
+ break;
350
+ case BlockType.MERMAID:
351
+ if ('mermaid' in options && options.mermaid) {
352
+ blockConfig.options = { code: options.mermaid.code };
353
+ }
354
+ break;
355
+ case BlockType.WHITEBOARD:
356
+ if ('whiteboard' in options && options.whiteboard) {
357
+ const whiteboardOptions = options.whiteboard;
358
+ blockConfig.options = {
359
+ align: [1, 2, 3].includes(whiteboardOptions.align) ? whiteboardOptions.align : 1
360
+ };
361
+ }
362
+ else {
363
+ blockConfig.options = { align: 1 };
364
+ }
365
+ break;
366
+ default:
367
+ Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
368
+ if ('text' in options) {
369
+ blockConfig.type = BlockType.TEXT;
370
+ const textOptions = options.text;
371
+ const textStyles = textOptions.textStyles || [];
372
+ const processedTextStyles = textStyles.map((item) => {
373
+ if (item.equation !== undefined) {
374
+ return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) };
375
+ }
376
+ return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) };
377
+ });
378
+ blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 };
379
+ }
380
+ else if ('code' in options) {
381
+ blockConfig.type = BlockType.CODE;
382
+ const codeOptions = options.code;
383
+ blockConfig.options = {
384
+ code: codeOptions.code || '',
385
+ language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
386
+ wrap: codeOptions.wrap || false
387
+ };
388
+ }
389
+ else if ('heading' in options) {
390
+ blockConfig.type = BlockType.HEADING;
391
+ const headingOptions = options.heading;
392
+ blockConfig.options = {
393
+ text: headingOptions.content || '',
394
+ level: headingOptions.level || 1,
395
+ align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1
396
+ };
397
+ }
398
+ else if ('list' in options) {
399
+ blockConfig.type = BlockType.LIST;
400
+ const listOptions = options.list;
401
+ blockConfig.options = {
402
+ text: listOptions.content || '',
403
+ isOrdered: listOptions.isOrdered || false,
404
+ align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1
405
+ };
406
+ }
407
+ else if ('image' in options) {
408
+ blockConfig.type = BlockType.IMAGE;
409
+ const imageOptions = options.image;
410
+ blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 };
411
+ }
412
+ else if ('mermaid' in options) {
413
+ blockConfig.type = BlockType.MERMAID;
414
+ blockConfig.options = { code: options.mermaid.code };
415
+ }
416
+ else if ('whiteboard' in options) {
417
+ blockConfig.type = BlockType.WHITEBOARD;
418
+ const whiteboardConfig = options.whiteboard;
419
+ blockConfig.options = {
420
+ align: [1, 2, 3].includes(whiteboardConfig.align) ? whiteboardConfig.align : 1
421
+ };
422
+ }
423
+ break;
424
+ }
425
+ Logger.debug(`创建块内容: 类型=${blockConfig.type}, 选项=${JSON.stringify(blockConfig.options)}`);
426
+ return this.createBlock(blockConfig.type, blockConfig.options);
427
+ }
428
+ catch (error) {
429
+ Logger.error(`创建块内容对象失败: ${error}`);
430
+ return null;
431
+ }
432
+ }
433
+ /**
434
+ * 创建表格块
435
+ * @param options 表格块选项
436
+ * @returns 表格块内容对象
437
+ */
438
+ createTableBlock(options) {
439
+ const { columnSize, rowSize, cells = [] } = options;
440
+ // 生成表格ID
441
+ const tableId = `table_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
442
+ const imageBlocks = Array();
443
+ // 创建表格单元格
444
+ const tableCells = [];
445
+ const descendants = [];
446
+ for (let row = 0; row < rowSize; row++) {
447
+ for (let col = 0; col < columnSize; col++) {
448
+ const cellId = `table_cell${row}_${col}`;
449
+ // 查找是否有配置的单元格内容
450
+ const cellConfigs = cells.filter(cell => cell.coordinate.row === row && cell.coordinate.column === col);
451
+ // 创建单元格内容
452
+ const cellContentBlocks = [];
453
+ const cellContentIds = [];
454
+ if (cellConfigs.length > 0) {
455
+ // 处理多个内容块
456
+ cellConfigs.forEach((cellConfig, index) => {
457
+ const cellContentId = `${cellId}_child_${index}`;
458
+ const cellContentBlock = {
459
+ block_id: cellContentId,
460
+ ...cellConfig.content,
461
+ children: []
462
+ };
463
+ cellContentBlocks.push(cellContentBlock);
464
+ cellContentIds.push(cellContentId);
465
+ Logger.info(`处理块:${JSON.stringify(cellConfig)} ${index}`);
466
+ if (cellConfig.content.block_type === 27) {
467
+ //把图片块保存起来,用于后续获取该图片块的token
468
+ imageBlocks.push({
469
+ coordinate: cellConfig.coordinate,
470
+ localBlockId: cellContentId,
471
+ });
472
+ }
473
+ });
474
+ }
475
+ else {
476
+ // 创建空的文本块
477
+ const cellContentId = `${cellId}_child`;
478
+ const cellContentBlock = {
479
+ block_id: cellContentId,
480
+ ...this.createTextBlock({
481
+ textContents: [{ text: "" }]
482
+ }),
483
+ children: []
484
+ };
485
+ cellContentBlocks.push(cellContentBlock);
486
+ cellContentIds.push(cellContentId);
487
+ }
488
+ // 创建表格单元格块
489
+ const tableCell = {
490
+ block_id: cellId,
491
+ block_type: 32, // 表格单元格类型
492
+ table_cell: {},
493
+ children: cellContentIds
494
+ };
495
+ tableCells.push(cellId);
496
+ descendants.push(tableCell);
497
+ descendants.push(...cellContentBlocks);
498
+ }
499
+ }
500
+ // 创建表格主体
501
+ const tableBlock = {
502
+ block_id: tableId,
503
+ block_type: 31, // 表格块类型
504
+ table: {
505
+ property: {
506
+ row_size: rowSize,
507
+ column_size: columnSize
508
+ }
509
+ },
510
+ children: tableCells
511
+ };
512
+ descendants.unshift(tableBlock);
513
+ // 过滤并记录 block_type 为 27 的元素
514
+ Logger.info(`发现 ${imageBlocks.length} 个图片块 (block_type: 27): ${JSON.stringify(imageBlocks)}`);
515
+ return {
516
+ children_id: [tableId],
517
+ descendants: descendants,
518
+ imageBlocks: imageBlocks
519
+ };
520
+ }
521
+ }
@@ -0,0 +1,160 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ import { BlockTextUpdatesArraySchema, WhiteboardFillArraySchema, } from '../../../types/documentSchema.js';
3
+ import { WHITEBOARD_NODE_THUMBNAIL_THRESHOLD, BATCH_SIZE, prepareBlockContents, extractSpecialBlocks, buildSpecialBlockHints, extractFeishuApiError, } from '../tools/toolHelpers.js';
4
+ export async function batchUpdateBlockText(params, api) {
5
+ const parsed = BlockTextUpdatesArraySchema.safeParse(params.updates);
6
+ if (!parsed.success)
7
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
8
+ Logger.info(`batchUpdateBlockText invoked: documentId=${params.documentId}, count=${parsed.data.length}`);
9
+ const result = await api.batchUpdateBlocksTextContent(params.documentId, parsed.data);
10
+ return {
11
+ updatedCount: parsed.data.length,
12
+ blockIds: parsed.data.map((u) => u.blockId),
13
+ document_revision_id: result?.document_revision_id,
14
+ };
15
+ }
16
+ export async function batchCreateBlocks(params, api) {
17
+ if (typeof params.blocks === 'string') {
18
+ throw new Error('错误:blocks 参数传入了字符串而不是数组,请直接传入 JSON 数组。\n' +
19
+ '正确:blocks:[{blockType:"text",options:{...}}]\n' +
20
+ '错误:blocks:"[{blockType:\\"text\\"...}]"');
21
+ }
22
+ const { documentId, parentBlockId, index = 0, blocks } = params;
23
+ const totalBatches = Math.ceil(blocks.length / BATCH_SIZE);
24
+ const results = [];
25
+ let createdBlocksCount = 0;
26
+ let currentStartIndex = index;
27
+ Logger.info(`batchCreateBlocks invoked: documentId=${documentId}, blocks=${blocks.length}, batches=${totalBatches}`);
28
+ for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
29
+ const currentBatch = blocks.slice(batchNum * BATCH_SIZE, (batchNum + 1) * BATCH_SIZE);
30
+ const prepared = prepareBlockContents(currentBatch, api);
31
+ if (!prepared.ok) {
32
+ const errText = prepared.error.content?.[0]?.text ?? 'prepareBlockContents failed';
33
+ throw new Error(errText);
34
+ }
35
+ const batchResult = await api.createDocumentBlocks(documentId, parentBlockId, prepared.contents, currentStartIndex);
36
+ results.push(batchResult);
37
+ createdBlocksCount += prepared.contents.length;
38
+ currentStartIndex = index + createdBlocksCount;
39
+ }
40
+ const allChildren = results.flatMap((r) => r.children ?? []);
41
+ const { imageBlocks, whiteboardBlocks } = extractSpecialBlocks(allChildren);
42
+ const hints = buildSpecialBlockHints(imageBlocks, whiteboardBlocks);
43
+ return {
44
+ totalBlocksCreated: createdBlocksCount,
45
+ nextIndex: currentStartIndex,
46
+ document_revision_id: results[results.length - 1]?.document_revision_id,
47
+ ...hints,
48
+ };
49
+ }
50
+ export async function deleteDocumentBlocks(params, api) {
51
+ Logger.info(`deleteDocumentBlocks invoked: documentId=${params.documentId}, range=${params.startIndex}-${params.endIndex}`);
52
+ const result = await api.deleteDocumentBlocks(params.documentId, params.parentBlockId, params.startIndex, params.endIndex);
53
+ return {
54
+ deletedRange: { startIndex: params.startIndex, endIndex: params.endIndex },
55
+ document_revision_id: result.document_revision_id,
56
+ };
57
+ }
58
+ export async function getImageResource(mediaId, extra, api) {
59
+ Logger.info(`getImageResource invoked: mediaId=${mediaId}`);
60
+ return api.getImageResource(mediaId, extra);
61
+ }
62
+ export async function uploadAndBindImageToBlock(params, api) {
63
+ Logger.info(`uploadAndBindImageToBlock invoked: documentId=${params.documentId}, count=${params.images.length}`);
64
+ const results = [];
65
+ for (const { blockId, imagePathOrUrl, fileName } of params.images) {
66
+ try {
67
+ const { base64: imageBase64, fileName: detectedFileName } = await api.getImageBase64FromPathOrUrl(imagePathOrUrl);
68
+ const finalFileName = fileName || detectedFileName;
69
+ const uploadResult = await api.uploadImageMedia(imageBase64, finalFileName, blockId);
70
+ if (!uploadResult?.file_token)
71
+ throw new Error('上传图片素材失败:无法获取file_token');
72
+ const setContentResult = await api.setImageBlockContent(params.documentId, blockId, uploadResult.file_token);
73
+ const { client_token: _ct, ...blockResult } = setContentResult?.block ?? {};
74
+ results.push({
75
+ blockId,
76
+ fileToken: uploadResult.file_token,
77
+ block: blockResult,
78
+ document_revision_id: setContentResult.document_revision_id,
79
+ });
80
+ }
81
+ catch (err) {
82
+ Logger.error(`上传图片并绑定到块失败 blockId=${blockId}:`, err);
83
+ results.push({ blockId, error: err.message });
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+ export async function createTable(params, api) {
89
+ Logger.info(`createTable invoked: documentId=${params.documentId}, size=${params.tableConfig.rowSize}x${params.tableConfig.columnSize}`);
90
+ const result = await api.createTableBlock(params.documentId, params.parentBlockId, params.tableConfig, params.index ?? 0);
91
+ const relations = result.block_id_relations ?? [];
92
+ const cellMap = [];
93
+ const tableBlockId = relations.find((r) => /^table_\d/.test(r.temporary_block_id))?.block_id;
94
+ for (const rel of relations) {
95
+ const m = rel.temporary_block_id.match(/^table_cell(\d+)_(\d+)$/);
96
+ if (m)
97
+ cellMap.push({ row: Number(m[1]), column: Number(m[2]), cellBlockId: rel.block_id });
98
+ }
99
+ const response = {
100
+ document_revision_id: result.document_revision_id,
101
+ tableBlockId,
102
+ cells: cellMap,
103
+ };
104
+ if (result.imageTokens?.length > 0) {
105
+ response.imageBlocks = result.imageTokens.map((t) => ({
106
+ row: t.row,
107
+ column: t.column,
108
+ blockId: t.blockId,
109
+ }));
110
+ response.imageReminder =
111
+ 'Use upload_and_bind_image_to_block to bind images to the listed blockIds.';
112
+ }
113
+ return response;
114
+ }
115
+ export async function getWhiteboardContent(whiteboardId, api) {
116
+ Logger.info(`getWhiteboardContent invoked: whiteboardId=${whiteboardId}`);
117
+ const whiteboardContent = await api.getWhiteboardContent(whiteboardId);
118
+ const nodeCount = whiteboardContent.nodes?.length ?? 0;
119
+ if (nodeCount > WHITEBOARD_NODE_THUMBNAIL_THRESHOLD) {
120
+ try {
121
+ const thumbnailBuffer = await api.getWhiteboardThumbnail(whiteboardId);
122
+ return { type: 'thumbnail', buffer: thumbnailBuffer };
123
+ }
124
+ catch {
125
+ // fallback to content
126
+ }
127
+ }
128
+ return { type: 'content', content: whiteboardContent };
129
+ }
130
+ export async function fillWhiteboardWithPlantuml(params, api) {
131
+ const parsed = WhiteboardFillArraySchema.safeParse(params.whiteboards);
132
+ if (!parsed.success)
133
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
134
+ if (parsed.data.length === 0)
135
+ throw new Error('错误:画板数组不能为空');
136
+ Logger.info(`fillWhiteboardWithPlantuml invoked: count=${parsed.data.length}`);
137
+ const results = [];
138
+ let successCount = 0;
139
+ let failCount = 0;
140
+ for (const { whiteboardId, code, syntax_type } of parsed.data) {
141
+ const syntaxTypeNumber = syntax_type === 'plantuml' ? 1 : 2;
142
+ const syntaxTypeName = syntax_type === 'plantuml' ? 'PlantUML' : 'Mermaid';
143
+ try {
144
+ const result = await api.createDiagramNode(whiteboardId, code, syntaxTypeNumber);
145
+ successCount++;
146
+ results.push({ whiteboardId, syntaxType: syntaxTypeName, status: 'success', nodeId: result.node_id });
147
+ }
148
+ catch (err) {
149
+ failCount++;
150
+ const { message, code: errorCode, logId } = extractFeishuApiError(err);
151
+ results.push({
152
+ whiteboardId,
153
+ syntaxType: syntaxTypeName,
154
+ status: 'failed',
155
+ error: { message, code: errorCode, logId },
156
+ });
157
+ }
158
+ }
159
+ return { total: parsed.data.length, success: successCount, failed: failCount, results };
160
+ }