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.
- package/LICENSE +21 -21
- package/README.md +356 -266
- package/dist/cli/auth.js +117 -0
- package/dist/cli/commands/auth.js +141 -0
- package/dist/cli/commands/config.js +86 -0
- package/dist/cli/commands/guide.js +68 -0
- package/dist/cli/dispatcher.js +96 -0
- package/dist/cli/index.js +95 -0
- package/dist/manager/sseConnectionManager.js +2 -0
- package/dist/mcp/feishuMcp.js +25 -25
- package/dist/mcp/tools/blockTools.js +295 -0
- package/dist/mcp/tools/documentTools.js +105 -0
- package/dist/mcp/tools/feishuBlockTools.js +1 -285
- package/dist/mcp/tools/feishuTools.js +1 -67
- package/dist/mcp/tools/folderTools.js +99 -0
- package/dist/mcp/tools/toolHelpers.js +155 -0
- package/dist/modules/FeatureModule.js +1 -0
- package/dist/modules/ModuleRegistry.js +63 -0
- package/dist/modules/calendar/index.js +11 -0
- package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
- package/dist/modules/calendar/tools/calendarTools.js +6 -0
- package/dist/modules/document/index.js +15 -0
- package/dist/modules/document/services/FeishuBlockService.js +410 -0
- package/dist/modules/document/services/FeishuDocumentService.js +110 -0
- package/dist/modules/document/services/FeishuFoldService.js +187 -0
- package/dist/modules/document/services/FeishuSearchService.js +232 -0
- package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
- package/dist/modules/document/services/blockFactory.js +521 -0
- package/dist/modules/document/toolApi/blockToolApi.js +160 -0
- package/dist/modules/document/toolApi/documentToolApi.js +65 -0
- package/dist/modules/document/toolApi/folderToolApi.js +73 -0
- package/dist/modules/document/toolApi/index.js +3 -0
- package/dist/modules/document/tools/blockTools.js +138 -0
- package/dist/modules/document/tools/documentTools.js +64 -0
- package/dist/modules/document/tools/folderTools.js +46 -0
- package/dist/modules/document/tools/toolHelpers.js +155 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/member/index.js +11 -0
- package/dist/modules/member/services/FeishuMemberService.js +41 -0
- package/dist/modules/member/toolApi/index.js +1 -0
- package/dist/modules/member/toolApi/memberToolApi.js +54 -0
- package/dist/modules/member/tools/memberTools.js +26 -0
- package/dist/modules/task/index.js +11 -0
- package/dist/modules/task/services/FeishuTaskService.js +271 -0
- package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
- package/dist/modules/task/toolApi/index.js +1 -0
- package/dist/modules/task/toolApi/taskToolApi.js +128 -0
- package/dist/modules/task/tools/taskTools.js +93 -0
- package/dist/server.js +43 -24
- package/dist/services/baseService.js +11 -2
- package/dist/services/blockFactory.js +167 -0
- package/dist/services/callbackService.js +1 -1
- package/dist/services/constants/feishuScopes.js +94 -0
- package/dist/services/feishu/FeishuBaseApiService.js +47 -0
- package/dist/services/feishu/FeishuBlockService.js +410 -0
- package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
- package/dist/services/feishu/FeishuDocumentService.js +110 -0
- package/dist/services/feishu/FeishuDriveService.js +62 -0
- package/dist/services/feishu/FeishuFoldService.js +187 -0
- package/dist/services/feishu/FeishuImageService.js +219 -0
- package/dist/services/feishu/FeishuScopeValidator.js +177 -0
- package/dist/services/feishu/FeishuSearchService.js +232 -0
- package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
- package/dist/services/feishu/FeishuWikiService.js +134 -0
- package/dist/services/feishuApiService.js +246 -1760
- package/dist/services/feishuAuthService.js +43 -0
- package/dist/types/documentSchema.js +232 -0
- package/dist/types/feishuSchema.js +54 -77
- package/dist/types/memberSchema.js +35 -0
- package/dist/types/taskSchema.js +166 -0
- package/dist/utils/auth/tokenCacheManager.js +16 -3
- package/dist/utils/auth/tokenRefreshManager.js +2 -1
- package/dist/utils/config.js +47 -0
- package/dist/utils/document.js +116 -116
- package/dist/utils/error.js +0 -11
- package/dist/utils/paramUtils.js +0 -31
- 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
|
+
}
|