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,403 @@
1
+ import { Logger } from '../../utils/logger.js';
2
+ import { ParamUtils } from '../../utils/paramUtils.js';
3
+ import { BlockFactory } from '../blockFactory.js';
4
+ import { FeishuBaseApiService } from './FeishuBaseApiService.js';
5
+ /**
6
+ * 飞书文档块服务
7
+ * 负责文档的创建、查询及块的 CRUD 操作
8
+ */
9
+ export class FeishuDocumentBlockService extends FeishuBaseApiService {
10
+ constructor(authService, blockFactory) {
11
+ super(authService);
12
+ Object.defineProperty(this, "blockFactory", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: blockFactory
17
+ });
18
+ }
19
+ /**
20
+ * 创建飞书文档
21
+ * @param title 文档标题
22
+ * @param folderToken 目标文件夹的 Token,文档将创建在此文件夹下
23
+ * @returns 创建的文档信息,包含 document_id、title 等字段
24
+ */
25
+ async createDocument(title, folderToken) {
26
+ try {
27
+ const response = await this.post('/docx/v1/documents', { title, folder_token: folderToken });
28
+ return response;
29
+ }
30
+ catch (error) {
31
+ this.handleApiError(error, '创建飞书文档失败');
32
+ }
33
+ }
34
+ /**
35
+ * 获取文档信息,支持普通文档和 Wiki 文档
36
+ * 当 documentType 未指定时,自动根据 documentId 是否包含 /wiki/ 路径判断类型
37
+ * @param documentId 文档 ID、文档 URL 或 Wiki 节点 Token/URL
38
+ * @param documentType 文档类型,'document' 为普通文档,'wiki' 为知识库文档;不传则自动检测
39
+ * @returns 文档信息对象,额外附加 _type 字段标识来源('document' | 'wiki'),
40
+ * Wiki 文档额外附加 documentId 字段作为 obj_token 的别名
41
+ */
42
+ async getDocumentInfo(documentId, documentType) {
43
+ try {
44
+ let isWikiLink;
45
+ if (documentType === 'wiki') {
46
+ isWikiLink = true;
47
+ }
48
+ else if (documentType === 'document') {
49
+ isWikiLink = false;
50
+ }
51
+ else {
52
+ isWikiLink = documentId.includes('/wiki/');
53
+ }
54
+ if (isWikiLink) {
55
+ const wikiToken = ParamUtils.processWikiToken(documentId);
56
+ const response = await this.get('/wiki/v2/spaces/get_node', { token: wikiToken, obj_type: 'wiki' });
57
+ if (!response.node || !response.node.obj_token) {
58
+ throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
59
+ }
60
+ const node = response.node;
61
+ Logger.debug(`获取Wiki文档信息: ${wikiToken} -> documentId: ${node.obj_token}`);
62
+ return { ...node, documentId: node.obj_token, _type: 'wiki' };
63
+ }
64
+ else {
65
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
66
+ const response = await this.get(`/docx/v1/documents/${normalizedDocId}`);
67
+ Logger.debug(`获取普通文档信息: ${normalizedDocId}`);
68
+ return { ...response, _type: 'document' };
69
+ }
70
+ }
71
+ catch (error) {
72
+ this.handleApiError(error, '获取文档信息失败');
73
+ }
74
+ }
75
+ /**
76
+ * 获取文档的纯文本内容
77
+ * @param documentId 文档 ID 或 URL
78
+ * @param lang 语言,0 为中文,1 为英文,默认 0
79
+ * @returns 文档的纯文本内容字符串
80
+ */
81
+ async getDocumentContent(documentId, lang = 0) {
82
+ try {
83
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
84
+ const response = await this.get(`/docx/v1/documents/${normalizedDocId}/raw_content`, { lang });
85
+ return response.content;
86
+ }
87
+ catch (error) {
88
+ this.handleApiError(error, '获取文档内容失败');
89
+ }
90
+ }
91
+ /**
92
+ * 获取文档的所有块结构,自动处理分页直到获取全部数据
93
+ * @param documentId 文档 ID 或 URL
94
+ * @param pageSize 每次分页请求的块数量,默认 500
95
+ * @returns 文档的所有块对象数组
96
+ */
97
+ async getDocumentBlocks(documentId, pageSize = 500) {
98
+ try {
99
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
100
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks`;
101
+ let pageToken = '';
102
+ let allBlocks = [];
103
+ do {
104
+ const params = { page_size: pageSize, document_revision_id: -1 };
105
+ if (pageToken)
106
+ params.page_token = pageToken;
107
+ const response = await this.get(endpoint, params);
108
+ allBlocks = [...allBlocks, ...(response.items || [])];
109
+ pageToken = response.page_token;
110
+ } while (pageToken);
111
+ return allBlocks;
112
+ }
113
+ catch (error) {
114
+ this.handleApiError(error, '获取文档块结构失败');
115
+ }
116
+ }
117
+ /**
118
+ * 获取指定块的详细内容
119
+ * @param documentId 文档 ID 或 URL
120
+ * @param blockId 目标块的 ID
121
+ * @returns 块的详细信息,包含 block_type、block_id 及对应类型的内容字段
122
+ */
123
+ async getBlockContent(documentId, blockId) {
124
+ try {
125
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
126
+ const safeBlockId = ParamUtils.processBlockId(blockId);
127
+ return await this.get(`/docx/v1/documents/${normalizedDocId}/blocks/${safeBlockId}`, { document_revision_id: -1 });
128
+ }
129
+ catch (error) {
130
+ this.handleApiError(error, '获取块内容失败');
131
+ }
132
+ }
133
+ /**
134
+ * 更新块的文本内容,支持普通文本和行内公式混排
135
+ * @param documentId 文档 ID 或 URL
136
+ * @param blockId 目标块的 ID
137
+ * @param textElements 文本元素数组,每个元素可包含 text(普通文本)或 equation(LaTeX 公式),
138
+ * 以及可选的 style 样式(bold、italic、underline 等)
139
+ * @returns 更新后的块信息
140
+ */
141
+ async updateBlockTextContent(documentId, blockId, textElements) {
142
+ try {
143
+ const docId = ParamUtils.processDocumentId(documentId);
144
+ const endpoint = `/docx/v1/documents/${docId}/blocks/${blockId}?document_revision_id=-1`;
145
+ Logger.debug(`准备请求API端点: ${endpoint}`);
146
+ const elements = textElements.map(item => {
147
+ if (item.equation !== undefined) {
148
+ return { equation: { content: item.equation, text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } };
149
+ }
150
+ return { text_run: { content: item.text || '', text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } };
151
+ });
152
+ const data = { update_text_elements: { elements } };
153
+ Logger.debug(`请求数据: ${JSON.stringify(data, null, 2)}`);
154
+ return await this.patch(endpoint, data);
155
+ }
156
+ catch (error) {
157
+ this.handleApiError(error, '更新块文本内容失败');
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * 在指定父块下创建单个子块
163
+ * @param documentId 文档 ID 或 URL
164
+ * @param parentBlockId 父块 ID,子块将插入到该块的子节点列表中
165
+ * @param blockContent 块内容对象,使用 BlockFactory 或 createBlockContent 生成
166
+ * @param index 插入位置的索引,0 表示插入到第一个子节点,默认 0
167
+ * @returns 创建结果,包含新块的 block_id、block_type 等信息
168
+ */
169
+ async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) {
170
+ try {
171
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
172
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
173
+ Logger.debug(`准备请求API端点: ${endpoint}`);
174
+ const payload = { children: [blockContent], index };
175
+ Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
176
+ return await this.post(endpoint, payload);
177
+ }
178
+ catch (error) {
179
+ this.handleApiError(error, '创建文档块失败');
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * 在指定父块下批量创建多个子块,一次 API 调用插入全部内容
185
+ * @param documentId 文档 ID 或 URL
186
+ * @param parentBlockId 父块 ID
187
+ * @param blockContents 块内容对象数组,按顺序插入
188
+ * @param index 起始插入位置的索引,默认 0
189
+ * @returns 创建结果,包含各新块的 block_id 等信息
190
+ */
191
+ async createDocumentBlocks(documentId, parentBlockId, blockContents, index = 0) {
192
+ try {
193
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
194
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
195
+ Logger.debug(`准备请求API端点: ${endpoint}`);
196
+ const payload = { children: blockContents, index };
197
+ Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
198
+ return await this.post(endpoint, payload);
199
+ }
200
+ catch (error) {
201
+ this.handleApiError(error, '批量创建文档块失败');
202
+ return null;
203
+ }
204
+ }
205
+ /**
206
+ * 创建文本段落块,支持普通文本与行内公式混排
207
+ * @param documentId 文档 ID 或 URL
208
+ * @param parentBlockId 父块 ID
209
+ * @param textContents 文本内容数组,每项可包含 text(普通文本)或 equation(LaTeX 公式)及 style
210
+ * @param align 对齐方式,1 左对齐,2 居中,3 右对齐,默认 1
211
+ * @param index 插入位置索引,默认 0
212
+ * @returns 创建结果
213
+ */
214
+ async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) {
215
+ const processedTextContents = textContents.map(item => {
216
+ if (item.equation !== undefined) {
217
+ return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) };
218
+ }
219
+ return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) };
220
+ });
221
+ const blockContent = this.blockFactory.createTextBlock({ textContents: processedTextContents, align });
222
+ return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
223
+ }
224
+ /**
225
+ * 创建代码块
226
+ * @param documentId 文档 ID 或 URL
227
+ * @param parentBlockId 父块 ID
228
+ * @param code 代码内容字符串
229
+ * @param language 编程语言代码(飞书 API 枚举值,1-75),0 表示纯文本,默认 0
230
+ * @param wrap 是否开启自动换行,默认 false
231
+ * @param index 插入位置索引,默认 0
232
+ * @returns 创建结果
233
+ */
234
+ async createCodeBlock(documentId, parentBlockId, code, language = 0, wrap = false, index = 0) {
235
+ const blockContent = this.blockFactory.createCodeBlock({ code, language, wrap });
236
+ return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
237
+ }
238
+ /**
239
+ * 创建标题块
240
+ * @param documentId 文档 ID 或 URL
241
+ * @param parentBlockId 父块 ID
242
+ * @param text 标题文本内容
243
+ * @param level 标题级别,1-9,默认 1(一级标题)
244
+ * @param index 插入位置索引,默认 0
245
+ * @param align 对齐方式,1 左对齐,2 居中,3 右对齐,默认 1
246
+ * @returns 创建结果
247
+ */
248
+ async createHeadingBlock(documentId, parentBlockId, text, level = 1, index = 0, align = 1) {
249
+ const blockContent = this.blockFactory.createHeadingBlock({ text, level, align });
250
+ return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
251
+ }
252
+ /**
253
+ * 创建列表块(有序或无序)
254
+ * @param documentId 文档 ID 或 URL
255
+ * @param parentBlockId 父块 ID
256
+ * @param text 列表项文本内容
257
+ * @param isOrdered 是否为有序列表,true 为有序,false 为无序,默认 false
258
+ * @param index 插入位置索引,默认 0
259
+ * @param align 对齐方式,1 左对齐,2 居中,3 右对齐,默认 1
260
+ * @returns 创建结果
261
+ */
262
+ async createListBlock(documentId, parentBlockId, text, isOrdered = false, index = 0, align = 1) {
263
+ const blockContent = this.blockFactory.createListBlock({ text, isOrdered, align });
264
+ return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
265
+ }
266
+ /**
267
+ * 创建 Mermaid 图表块
268
+ * @param documentId 文档 ID 或 URL
269
+ * @param parentBlockId 父块 ID
270
+ * @param mermaidCode Mermaid 图表代码
271
+ * @param index 插入位置索引,默认 0
272
+ * @returns 创建结果
273
+ */
274
+ async createMermaidBlock(documentId, parentBlockId, mermaidCode, index = 0) {
275
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
276
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
277
+ const blockContent = {
278
+ block_type: 40,
279
+ add_ons: {
280
+ component_id: '',
281
+ component_type_id: 'blk_631fefbbae02400430b8f9f4',
282
+ record: JSON.stringify({ data: mermaidCode })
283
+ }
284
+ };
285
+ const payload = { children: [blockContent], index };
286
+ Logger.info(`请求创建Mermaid块: ${JSON.stringify(payload).slice(0, 500)}...`);
287
+ return await this.post(endpoint, payload);
288
+ }
289
+ /**
290
+ * 创建表格块,支持自定义单元格内容
291
+ * 使用 descendant API 一次性创建表格及所有子块,并返回图片单元格的 blockId 映射
292
+ * @param documentId 文档 ID 或 URL
293
+ * @param parentBlockId 父块 ID
294
+ * @param tableConfig 表格配置
295
+ * @param tableConfig.columnSize 表格列数
296
+ * @param tableConfig.rowSize 表格行数
297
+ * @param tableConfig.cells 单元格内容配置,每项指定坐标(row/column)和内容(blockType/options)
298
+ * @param index 插入位置索引,默认 0
299
+ * @returns 创建结果,额外附加 imageTokens 字段,包含图片单元格的坐标与 blockId 映射
300
+ */
301
+ async createTableBlock(documentId, parentBlockId, tableConfig, index = 0) {
302
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
303
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/descendant?document_revision_id=-1`;
304
+ const processedTableConfig = {
305
+ ...tableConfig,
306
+ cells: tableConfig.cells?.map(cell => ({
307
+ ...cell,
308
+ content: this.blockFactory.createBlockContentFromOptions(cell.content.blockType, cell.content.options)
309
+ }))
310
+ };
311
+ const tableStructure = this.blockFactory.createTableBlock(processedTableConfig);
312
+ const payload = { children_id: tableStructure.children_id, descendants: tableStructure.descendants, index };
313
+ Logger.info(`请求创建表格块: ${tableConfig.rowSize}x${tableConfig.columnSize},单元格数量: ${tableConfig.cells?.length || 0}`);
314
+ const response = await this.post(endpoint, payload);
315
+ const imageTokens = await this.extractImageTokensFromTable(response, tableStructure.imageBlocks);
316
+ return { ...response, imageTokens };
317
+ }
318
+ async extractImageTokensFromTable(tableResponse, cells) {
319
+ try {
320
+ const imageTokens = [];
321
+ Logger.info(`tableResponse: ${JSON.stringify(tableResponse)}`);
322
+ if (!cells || cells.length === 0) {
323
+ Logger.info('表格中没有图片单元格,跳过图片块信息提取');
324
+ return imageTokens;
325
+ }
326
+ const blockIdMap = new Map();
327
+ if (tableResponse?.block_id_relations) {
328
+ for (const relation of tableResponse.block_id_relations) {
329
+ blockIdMap.set(relation.temporary_block_id, relation.block_id);
330
+ }
331
+ Logger.debug(`创建了 ${blockIdMap.size} 个块ID映射关系`);
332
+ }
333
+ for (const cell of cells) {
334
+ const { coordinate, localBlockId } = cell;
335
+ const blockId = blockIdMap.get(localBlockId);
336
+ if (!blockId) {
337
+ Logger.warn(`未找到 localBlockId ${localBlockId} 对应的 block_id`);
338
+ continue;
339
+ }
340
+ imageTokens.push({ row: coordinate.row, column: coordinate.column, blockId });
341
+ Logger.info(`提取到图片块信息: 位置(${coordinate.row}, ${coordinate.column}),blockId: ${blockId}`);
342
+ }
343
+ Logger.info(`成功提取 ${imageTokens.length} 个图片块信息`);
344
+ return imageTokens;
345
+ }
346
+ catch (error) {
347
+ Logger.error(`提取表格图片块信息失败: ${error}`);
348
+ return [];
349
+ }
350
+ }
351
+ /**
352
+ * 批量删除指定父块下的连续子块(按索引范围)
353
+ * @param documentId 文档 ID 或 URL
354
+ * @param parentBlockId 父块 ID
355
+ * @param startIndex 起始索引(含),必须 >= 0
356
+ * @param endIndex 结束索引(含),必须 >= startIndex
357
+ * @returns 操作结果
358
+ */
359
+ async deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex) {
360
+ try {
361
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
362
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children/batch_delete`;
363
+ if (startIndex < 0 || endIndex < startIndex) {
364
+ throw new Error('无效的索引范围:起始索引必须大于等于0,结束索引必须大于等于起始索引');
365
+ }
366
+ Logger.info(`开始删除文档块,文档ID: ${normalizedDocId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
367
+ const response = await this.delete(endpoint, { start_index: startIndex, end_index: endIndex });
368
+ Logger.info('文档块删除成功');
369
+ return response;
370
+ }
371
+ catch (error) {
372
+ this.handleApiError(error, '删除文档块失败');
373
+ }
374
+ }
375
+ /**
376
+ * 删除指定父块下的单个子块
377
+ * 等价于 deleteDocumentBlocks(documentId, parentBlockId, blockIndex, blockIndex + 1)
378
+ * @param documentId 文档 ID 或 URL
379
+ * @param parentBlockId 父块 ID
380
+ * @param blockIndex 目标块在父块子节点列表中的索引
381
+ * @returns 操作结果
382
+ */
383
+ async deleteDocumentBlock(documentId, parentBlockId, blockIndex) {
384
+ return this.deleteDocumentBlocks(documentId, parentBlockId, blockIndex, blockIndex + 1);
385
+ }
386
+ /**
387
+ * 根据块类型字符串和选项对象创建块内容对象
388
+ * 委托给 BlockFactory.createBlockContentFromOptions 完成实际转换
389
+ * @param blockType 块类型字符串,如 'text'、'code'、'heading1'~'heading9'、'list'、'image' 等
390
+ * @param options 块配置选项对象,结构因 blockType 而异
391
+ * @returns 可传入 createDocumentBlock / createDocumentBlocks 的块内容对象,失败时返回 null
392
+ */
393
+ createBlockContent(blockType, options) {
394
+ return this.blockFactory.createBlockContentFromOptions(blockType, options);
395
+ }
396
+ /**
397
+ * 获取当前服务持有的 BlockFactory 实例
398
+ * @returns BlockFactory 单例实例
399
+ */
400
+ getBlockFactory() {
401
+ return this.blockFactory;
402
+ }
403
+ }
@@ -0,0 +1,110 @@
1
+ import { Logger } from '../../utils/logger.js';
2
+ import { ParamUtils } from '../../utils/paramUtils.js';
3
+ import { FeishuBaseApiService } from './FeishuBaseApiService.js';
4
+ /**
5
+ * 飞书文档服务
6
+ * 负责文档的创建与信息查询
7
+ */
8
+ export class FeishuDocumentService extends FeishuBaseApiService {
9
+ constructor(authService) {
10
+ super(authService);
11
+ }
12
+ /**
13
+ * 创建飞书文档
14
+ * @param title 文档标题
15
+ * @param folderToken 目标文件夹的 Token,文档将创建在此文件夹下
16
+ * @returns 创建的文档信息,包含 document_id、title 等字段
17
+ */
18
+ async createDocument(title, folderToken) {
19
+ try {
20
+ const response = await this.post('/docx/v1/documents', { title, folder_token: folderToken });
21
+ return response;
22
+ }
23
+ catch (error) {
24
+ this.handleApiError(error, '创建飞书文档失败');
25
+ }
26
+ }
27
+ /**
28
+ * 获取文档信息,支持普通文档和 Wiki 文档
29
+ * 当 documentType 未指定时,自动根据 documentId 是否包含 /wiki/ 路径判断类型
30
+ * @param documentId 文档 ID、文档 URL 或 Wiki 节点 Token/URL
31
+ * @param documentType 文档类型,'document' 为普通文档,'wiki' 为知识库文档;不传则自动检测
32
+ * @returns 文档信息对象,额外附加 _type 字段标识来源('document' | 'wiki'),
33
+ * Wiki 文档额外附加 documentId 字段作为 obj_token 的别名
34
+ */
35
+ async getDocumentInfo(documentId, documentType) {
36
+ try {
37
+ let isWikiLink;
38
+ if (documentType === 'wiki') {
39
+ isWikiLink = true;
40
+ }
41
+ else if (documentType === 'document') {
42
+ isWikiLink = false;
43
+ }
44
+ else {
45
+ isWikiLink = documentId.includes('/wiki/');
46
+ }
47
+ if (isWikiLink) {
48
+ const wikiToken = ParamUtils.processWikiToken(documentId);
49
+ const response = await this.get('/wiki/v2/spaces/get_node', { token: wikiToken, obj_type: 'wiki' });
50
+ if (!response.node || !response.node.obj_token) {
51
+ throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
52
+ }
53
+ const node = response.node;
54
+ Logger.debug(`获取Wiki文档信息: ${wikiToken} -> documentId: ${node.obj_token}`);
55
+ return { ...node, documentId: node.obj_token, _type: 'wiki' };
56
+ }
57
+ else {
58
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
59
+ const response = await this.get(`/docx/v1/documents/${normalizedDocId}`);
60
+ Logger.debug(`获取普通文档信息: ${normalizedDocId}`);
61
+ return { ...response, _type: 'document' };
62
+ }
63
+ }
64
+ catch (error) {
65
+ this.handleApiError(error, '获取文档信息失败');
66
+ }
67
+ }
68
+ /**
69
+ * 获取文档的纯文本内容
70
+ * @param documentId 文档 ID 或 URL
71
+ * @param lang 语言,0 为中文,1 为英文,默认 0
72
+ * @returns 文档的纯文本内容字符串
73
+ */
74
+ async getDocumentContent(documentId, lang = 0) {
75
+ try {
76
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
77
+ const response = await this.get(`/docx/v1/documents/${normalizedDocId}/raw_content`, { lang });
78
+ return response.content;
79
+ }
80
+ catch (error) {
81
+ this.handleApiError(error, '获取文档内容失败');
82
+ }
83
+ }
84
+ /**
85
+ * 获取文档的所有块结构,自动处理分页直到获取全部数据
86
+ * @param documentId 文档 ID 或 URL
87
+ * @param pageSize 每次分页请求的块数量,默认 500
88
+ * @returns 文档的所有块对象数组
89
+ */
90
+ async getDocumentBlocks(documentId, pageSize = 500) {
91
+ try {
92
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
93
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks`;
94
+ let pageToken = '';
95
+ let allBlocks = [];
96
+ do {
97
+ const params = { page_size: pageSize, document_revision_id: -1 };
98
+ if (pageToken)
99
+ params.page_token = pageToken;
100
+ const response = await this.get(endpoint, params);
101
+ allBlocks = [...allBlocks, ...(response.items || [])];
102
+ pageToken = response.page_token;
103
+ } while (pageToken);
104
+ return allBlocks;
105
+ }
106
+ catch (error) {
107
+ this.handleApiError(error, '获取文档块结构失败');
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,62 @@
1
+ import { Logger } from '../../utils/logger.js';
2
+ import { FeishuBaseApiService } from './FeishuBaseApiService.js';
3
+ /**
4
+ * 飞书云盘(Drive)服务
5
+ * 负责文件夹的查询与创建
6
+ */
7
+ export class FeishuDriveService extends FeishuBaseApiService {
8
+ constructor(authService) {
9
+ super(authService);
10
+ }
11
+ /**
12
+ * 获取当前用户根文件夹的元数据信息
13
+ * @returns 根文件夹信息,包含 token(文件夹 Token)、id 和 user_id 字段
14
+ */
15
+ async getRootFolderInfo() {
16
+ try {
17
+ const response = await this.get('/drive/explorer/v2/root_folder/meta');
18
+ Logger.debug('获取根文件夹信息成功:', response);
19
+ return response;
20
+ }
21
+ catch (error) {
22
+ this.handleApiError(error, '获取飞书根文件夹信息失败');
23
+ }
24
+ }
25
+ /**
26
+ * 获取指定文件夹内的文件和子文件夹列表
27
+ * @param folderToken 目标文件夹的 Token
28
+ * @param orderBy 排序字段,可选 'EditedTime'(编辑时间)、'CreatedTime'(创建时间)等,默认 'EditedTime'
29
+ * @param direction 排序方向,'ASC' 升序 / 'DESC' 降序,默认 'DESC'
30
+ * @returns 文件夹文件清单,包含 files 数组,每项含文件名、类型、token 等信息
31
+ */
32
+ async getFolderFileList(folderToken, orderBy = 'EditedTime', direction = 'DESC') {
33
+ try {
34
+ const response = await this.get('/drive/v1/files', {
35
+ folder_token: folderToken,
36
+ order_by: orderBy,
37
+ direction
38
+ });
39
+ Logger.debug(`获取文件夹(${folderToken})中的文件清单成功,文件数量: ${response.files?.length || 0}`);
40
+ return response;
41
+ }
42
+ catch (error) {
43
+ this.handleApiError(error, '获取文件夹中的文件清单失败');
44
+ }
45
+ }
46
+ /**
47
+ * 在指定文件夹下创建子文件夹
48
+ * @param folderToken 父文件夹的 Token
49
+ * @param name 新文件夹的名称
50
+ * @returns 创建结果,包含新文件夹的 token 和访问 url
51
+ */
52
+ async createFolder(folderToken, name) {
53
+ try {
54
+ const response = await this.post('/drive/v1/files/create_folder', { folder_token: folderToken, name });
55
+ Logger.debug(`文件夹创建成功, token: ${response.token}, url: ${response.url}`);
56
+ return response;
57
+ }
58
+ catch (error) {
59
+ this.handleApiError(error, '创建文件夹失败');
60
+ }
61
+ }
62
+ }