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,410 @@
1
+ import axios from 'axios';
2
+ import FormData from 'form-data';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { Logger } from '../../utils/logger.js';
6
+ import { ParamUtils } from '../../utils/paramUtils.js';
7
+ import { BlockFactory } from '../blockFactory.js';
8
+ import { FeishuBaseApiService } from './FeishuBaseApiService.js';
9
+ /**
10
+ * 飞书块服务
11
+ * 负责文档块的增删改查及图片块的完整操作
12
+ */
13
+ export class FeishuBlockService extends FeishuBaseApiService {
14
+ constructor(authService) {
15
+ super(authService);
16
+ Object.defineProperty(this, "blockFactory", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: BlockFactory.getInstance()
21
+ });
22
+ }
23
+ /**
24
+ * 更新块的文本内容,支持普通文本和行内公式混排
25
+ * @param documentId 文档 ID 或 URL
26
+ * @param blockId 目标块的 ID
27
+ * @param textElements 文本元素数组,每个元素可包含 text(普通文本)或 equation(LaTeX 公式),
28
+ * 以及可选的 style 样式(bold、italic、underline 等)
29
+ * @returns 更新后的块信息
30
+ */
31
+ async updateBlockTextContent(documentId, blockId, textElements) {
32
+ try {
33
+ const docId = ParamUtils.processDocumentId(documentId);
34
+ const endpoint = `/docx/v1/documents/${docId}/blocks/${blockId}?document_revision_id=-1`;
35
+ Logger.debug(`准备请求API端点: ${endpoint}`);
36
+ const elements = textElements.map(item => {
37
+ if (item.equation !== undefined) {
38
+ return { equation: { content: item.equation, text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } };
39
+ }
40
+ return { text_run: { content: item.text || '', text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } };
41
+ });
42
+ const data = { update_text_elements: { elements } };
43
+ Logger.debug(`请求数据: ${JSON.stringify(data, null, 2)}`);
44
+ return await this.patch(endpoint, data);
45
+ }
46
+ catch (error) {
47
+ this.handleApiError(error, '更新块文本内容失败');
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * 批量更新多个块的文本内容(一次 API 调用)
53
+ * @param documentId 文档 ID 或 URL
54
+ * @param updates 更新项数组,每项包含 blockId 和 textElements
55
+ * @returns 更新结果
56
+ */
57
+ async batchUpdateBlocksTextContent(documentId, updates) {
58
+ const docId = ParamUtils.processDocumentId(documentId);
59
+ const endpoint = `/docx/v1/documents/${docId}/blocks/batch_update?document_revision_id=-1`;
60
+ const requests = updates.map(({ blockId, textElements }) => ({
61
+ block_id: blockId,
62
+ update_text_elements: {
63
+ elements: textElements.map(item => item.equation !== undefined
64
+ ? { equation: { content: item.equation, text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } }
65
+ : { text_run: { content: item.text || '', text_element_style: BlockFactory.applyDefaultTextStyle(item.style) } }),
66
+ },
67
+ }));
68
+ Logger.debug(`批量更新块文本请求数据: ${JSON.stringify({ requests }, null, 2)}`);
69
+ return await this.patch(endpoint, { requests });
70
+ }
71
+ /**
72
+ * 在指定父块下创建单个子块
73
+ * @param documentId 文档 ID 或 URL
74
+ * @param parentBlockId 父块 ID,子块将插入到该块的子节点列表中
75
+ * @param blockContent 块内容对象,使用 BlockFactory 或 createBlockContent 生成
76
+ * @param index 插入位置的索引,0 表示插入到第一个子节点,默认 0
77
+ * @returns 创建结果,包含新块的 block_id、block_type 等信息
78
+ */
79
+ async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) {
80
+ try {
81
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
82
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
83
+ Logger.debug(`准备请求API端点: ${endpoint}`);
84
+ const payload = { children: [blockContent], index };
85
+ Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
86
+ return await this.post(endpoint, payload);
87
+ }
88
+ catch (error) {
89
+ this.handleApiError(error, '创建文档块失败');
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * 在指定父块下批量创建多个子块,一次 API 调用插入全部内容
95
+ * @param documentId 文档 ID 或 URL
96
+ * @param parentBlockId 父块 ID
97
+ * @param blockContents 块内容对象数组,按顺序插入
98
+ * @param index 起始插入位置的索引,默认 0
99
+ * @returns 创建结果,包含各新块的 block_id 等信息
100
+ */
101
+ async createDocumentBlocks(documentId, parentBlockId, blockContents, index = 0) {
102
+ try {
103
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
104
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
105
+ Logger.debug(`准备请求API端点: ${endpoint}`);
106
+ const payload = { children: blockContents, index };
107
+ Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
108
+ return await this.post(endpoint, payload);
109
+ }
110
+ catch (error) {
111
+ this.handleApiError(error, '批量创建文档块失败');
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * 创建表格块,支持自定义单元格内容
117
+ * 使用 descendant API 一次性创建表格及所有子块,并返回图片单元格的 blockId 映射
118
+ * @param documentId 文档 ID 或 URL
119
+ * @param parentBlockId 父块 ID
120
+ * @param tableConfig 表格配置
121
+ * @param tableConfig.columnSize 表格列数
122
+ * @param tableConfig.rowSize 表格行数
123
+ * @param tableConfig.cells 单元格内容配置,每项指定坐标(row/column)和内容(blockType/options)
124
+ * @param index 插入位置索引,默认 0
125
+ * @returns 创建结果,额外附加 imageTokens 字段,包含图片单元格的坐标与 blockId 映射
126
+ */
127
+ async createTableBlock(documentId, parentBlockId, tableConfig, index = 0) {
128
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
129
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/descendant?document_revision_id=-1`;
130
+ const processedTableConfig = {
131
+ ...tableConfig,
132
+ cells: tableConfig.cells?.map(cell => ({
133
+ ...cell,
134
+ content: this.blockFactory.createBlockContentFromOptions(cell.content.blockType, cell.content.options)
135
+ }))
136
+ };
137
+ const tableStructure = this.blockFactory.createTableBlock(processedTableConfig);
138
+ const payload = { children_id: tableStructure.children_id, descendants: tableStructure.descendants, index };
139
+ Logger.info(`请求创建表格块: ${tableConfig.rowSize}x${tableConfig.columnSize},单元格数量: ${tableConfig.cells?.length || 0}`);
140
+ const response = await this.post(endpoint, payload);
141
+ const imageTokens = await this.extractImageTokensFromTable(response, tableStructure.imageBlocks);
142
+ return { ...response, imageTokens };
143
+ }
144
+ async extractImageTokensFromTable(tableResponse, cells) {
145
+ try {
146
+ const imageTokens = [];
147
+ Logger.info(`tableResponse: ${JSON.stringify(tableResponse)}`);
148
+ if (!cells || cells.length === 0) {
149
+ Logger.info('表格中没有图片单元格,跳过图片块信息提取');
150
+ return imageTokens;
151
+ }
152
+ const blockIdMap = new Map();
153
+ if (tableResponse?.block_id_relations) {
154
+ for (const relation of tableResponse.block_id_relations) {
155
+ blockIdMap.set(relation.temporary_block_id, relation.block_id);
156
+ }
157
+ Logger.debug(`创建了 ${blockIdMap.size} 个块ID映射关系`);
158
+ }
159
+ for (const cell of cells) {
160
+ const { coordinate, localBlockId } = cell;
161
+ const blockId = blockIdMap.get(localBlockId);
162
+ if (!blockId) {
163
+ Logger.warn(`未找到 localBlockId ${localBlockId} 对应的 block_id`);
164
+ continue;
165
+ }
166
+ imageTokens.push({ row: coordinate.row, column: coordinate.column, blockId });
167
+ Logger.info(`提取到图片块信息: 位置(${coordinate.row}, ${coordinate.column}),blockId: ${blockId}`);
168
+ }
169
+ Logger.info(`成功提取 ${imageTokens.length} 个图片块信息`);
170
+ return imageTokens;
171
+ }
172
+ catch (error) {
173
+ Logger.error(`提取表格图片块信息失败: ${error}`);
174
+ return [];
175
+ }
176
+ }
177
+ /**
178
+ * 批量删除指定父块下的连续子块(按索引范围)
179
+ * @param documentId 文档 ID 或 URL
180
+ * @param parentBlockId 父块 ID
181
+ * @param startIndex 起始索引(含),必须 >= 0
182
+ * @param endIndex 结束索引(含),必须 >= startIndex
183
+ * @returns 操作结果
184
+ */
185
+ async deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex) {
186
+ try {
187
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
188
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children/batch_delete`;
189
+ if (startIndex < 0 || endIndex < startIndex) {
190
+ throw new Error('无效的索引范围:起始索引必须大于等于0,结束索引必须大于等于起始索引');
191
+ }
192
+ Logger.info(`开始删除文档块,文档ID: ${normalizedDocId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
193
+ const response = await this.delete(endpoint, { start_index: startIndex, end_index: endIndex });
194
+ Logger.info('文档块删除成功');
195
+ return response;
196
+ }
197
+ catch (error) {
198
+ this.handleApiError(error, '删除文档块失败');
199
+ }
200
+ }
201
+ /**
202
+ * 根据块类型字符串和选项对象创建块内容对象
203
+ * 委托给 BlockFactory.createBlockContentFromOptions 完成实际转换
204
+ * @param blockType 块类型字符串,如 'text'、'code'、'heading1'~'heading9'、'list'、'image' 等
205
+ * @param options 块配置选项对象,结构因 blockType 而异
206
+ * @returns 可传入 createDocumentBlock / createDocumentBlocks 的块内容对象,失败时返回 null
207
+ */
208
+ createBlockContent(blockType, options) {
209
+ return this.blockFactory.createBlockContentFromOptions(blockType, options);
210
+ }
211
+ /**
212
+ * 获取当前服务持有的 BlockFactory 实例
213
+ * @returns BlockFactory 单例实例
214
+ */
215
+ getBlockFactory() {
216
+ return this.blockFactory;
217
+ }
218
+ // ─── 图片块操作 ───────────────────────────────────────────────────
219
+ /**
220
+ * 下载飞书云文档中的图片素材,返回二进制数据
221
+ * @param mediaId 图片的媒体 ID(media_id / file_token)
222
+ * @param extra 额外参数,部分场景需要传递(如加密图片),默认为空字符串
223
+ * @returns 图片的二进制 Buffer 数据
224
+ */
225
+ async getImageResource(mediaId, extra = '') {
226
+ try {
227
+ Logger.info(`开始获取图片资源,媒体ID: ${mediaId}`);
228
+ if (!mediaId)
229
+ throw new Error('媒体ID不能为空');
230
+ const endpoint = `/drive/v1/medias/${mediaId}/download`;
231
+ const params = {};
232
+ if (extra)
233
+ params.extra = extra;
234
+ const response = await this.request(endpoint, 'GET', params, true, {}, 'arraybuffer');
235
+ const imageBuffer = Buffer.from(response);
236
+ Logger.info(`图片资源获取成功,大小: ${imageBuffer.length} 字节`);
237
+ return imageBuffer;
238
+ }
239
+ catch (error) {
240
+ this.handleApiError(error, '获取图片资源失败');
241
+ return Buffer.from([]);
242
+ }
243
+ }
244
+ /**
245
+ * 将图片素材上传到飞书云端,关联到指定的图片块
246
+ * @param imageBase64 图片的 Base64 编码字符串(不含 data:image/xxx;base64, 前缀)
247
+ * @param fileName 图片文件名(含扩展名),若为空字符串则根据 Base64 内容自动检测格式并生成文件名
248
+ * @param parentBlockId 关联的图片块 ID(parent_node),用于告知飞书该素材归属的块
249
+ * @returns 上传结果,包含 file_token 字段,用于后续 setImageBlockContent 调用
250
+ */
251
+ async uploadImageMedia(imageBase64, fileName, parentBlockId) {
252
+ try {
253
+ const endpoint = '/drive/v1/medias/upload_all';
254
+ const imageBuffer = Buffer.from(imageBase64, 'base64');
255
+ const imageSize = imageBuffer.length;
256
+ if (!fileName) {
257
+ if (imageBase64.startsWith('/9j/')) {
258
+ fileName = `image_${Date.now()}.jpg`;
259
+ }
260
+ else if (imageBase64.startsWith('iVBORw0KGgo')) {
261
+ fileName = `image_${Date.now()}.png`;
262
+ }
263
+ else if (imageBase64.startsWith('R0lGODlh')) {
264
+ fileName = `image_${Date.now()}.gif`;
265
+ }
266
+ else {
267
+ fileName = `image_${Date.now()}.png`;
268
+ }
269
+ }
270
+ Logger.info(`开始上传图片素材,文件名: ${fileName},大小: ${imageSize} 字节,关联块ID: ${parentBlockId}`);
271
+ if (imageSize > 20 * 1024 * 1024) {
272
+ Logger.warn(`图片文件过大: ${imageSize} 字节,建议小于20MB`);
273
+ }
274
+ const formData = new FormData();
275
+ formData.append('file', imageBuffer, {
276
+ filename: fileName,
277
+ contentType: this.getMimeTypeFromFileName(fileName),
278
+ knownLength: imageSize
279
+ });
280
+ formData.append('file_name', fileName);
281
+ formData.append('parent_type', 'docx_image');
282
+ formData.append('parent_node', parentBlockId);
283
+ formData.append('size', imageSize.toString());
284
+ const response = await this.post(endpoint, formData);
285
+ Logger.info(`图片素材上传成功,file_token: ${response.file_token}`);
286
+ return response;
287
+ }
288
+ catch (error) {
289
+ this.handleApiError(error, '上传图片素材失败');
290
+ }
291
+ }
292
+ /**
293
+ * 将已上传的图片素材绑定到指定的图片块,完成图片块的最终渲染
294
+ * @param documentId 文档 ID 或 URL
295
+ * @param imageBlockId 目标图片块的 block_id
296
+ * @param fileToken 图片素材的 file_token(由 uploadImageMedia 返回)
297
+ * @returns 更新结果,包含 document_revision_id 等字段
298
+ */
299
+ async setImageBlockContent(documentId, imageBlockId, fileToken) {
300
+ try {
301
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
302
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${imageBlockId}`;
303
+ const payload = { replace_image: { token: fileToken } };
304
+ Logger.info(`开始设置图片块内容,文档ID: ${normalizedDocId},块ID: ${imageBlockId},file_token: ${fileToken}`);
305
+ const response = await this.patch(endpoint, payload);
306
+ Logger.info('图片块内容设置成功');
307
+ return response;
308
+ }
309
+ catch (error) {
310
+ this.handleApiError(error, '设置图片块内容失败');
311
+ }
312
+ }
313
+ /**
314
+ * 完整创建图片块的三步流程:创建空块 → 上传素材 → 绑定素材
315
+ * 支持本地文件路径和 HTTP/HTTPS URL 两种图片来源
316
+ * @param documentId 文档 ID 或 URL
317
+ * @param parentBlockId 父块 ID,图片块将插入到该块的子节点列表中
318
+ * @param imagePathOrUrl 图片来源,支持本地绝对路径或 HTTP/HTTPS URL
319
+ * @param options 可选配置
320
+ * @param options.fileName 自定义文件名(含扩展名),不传则从路径/URL 自动提取
321
+ * @param options.width 图片显示宽度(像素),不传则使用默认值
322
+ * @param options.height 图片显示高度(像素),不传则使用默认值
323
+ * @param options.index 插入位置索引,默认 0
324
+ * @returns 综合结果对象,包含 imageBlockId、fileToken、imageBlock、uploadResult、setContentResult、documentRevisionId
325
+ */
326
+ async createImageBlock(documentId, parentBlockId, imagePathOrUrl, options = {}) {
327
+ try {
328
+ const { fileName: providedFileName, width, height, index = 0 } = options;
329
+ Logger.info(`开始创建图片块,文档ID: ${documentId},父块ID: ${parentBlockId},图片源: ${imagePathOrUrl},插入位置: ${index}`);
330
+ const { base64: imageBase64, fileName: detectedFileName } = await this.getImageBase64FromPathOrUrl(imagePathOrUrl);
331
+ const finalFileName = providedFileName || detectedFileName;
332
+ Logger.info('第1步:创建空图片块');
333
+ const imageBlockContent = this.blockFactory.createImageBlock({ width, height });
334
+ const createBlockResult = await this.createDocumentBlock(documentId, parentBlockId, imageBlockContent, index);
335
+ if (!createBlockResult?.children?.[0]?.block_id) {
336
+ throw new Error('创建空图片块失败:无法获取块ID');
337
+ }
338
+ const imageBlockId = createBlockResult.children[0].block_id;
339
+ Logger.info(`空图片块创建成功,块ID: ${imageBlockId}`);
340
+ Logger.info('第2步:上传图片素材');
341
+ const uploadResult = await this.uploadImageMedia(imageBase64, finalFileName, imageBlockId);
342
+ if (!uploadResult?.file_token) {
343
+ throw new Error('上传图片素材失败:无法获取file_token');
344
+ }
345
+ Logger.info(`图片素材上传成功,file_token: ${uploadResult.file_token}`);
346
+ Logger.info('第3步:设置图片块内容');
347
+ const setContentResult = await this.setImageBlockContent(documentId, imageBlockId, uploadResult.file_token);
348
+ Logger.info('图片块创建完成');
349
+ return {
350
+ imageBlock: createBlockResult.children[0],
351
+ imageBlockId,
352
+ fileToken: uploadResult.file_token,
353
+ uploadResult,
354
+ setContentResult,
355
+ documentRevisionId: setContentResult.document_revision_id || createBlockResult.document_revision_id
356
+ };
357
+ }
358
+ catch (error) {
359
+ this.handleApiError(error, '创建图片块失败');
360
+ }
361
+ }
362
+ getMimeTypeFromFileName(fileName) {
363
+ const extension = fileName.toLowerCase().split('.').pop();
364
+ switch (extension) {
365
+ case 'jpg':
366
+ case 'jpeg':
367
+ return 'image/jpeg';
368
+ case 'png':
369
+ return 'image/png';
370
+ case 'gif':
371
+ return 'image/gif';
372
+ case 'webp':
373
+ return 'image/webp';
374
+ case 'bmp':
375
+ return 'image/bmp';
376
+ case 'svg':
377
+ return 'image/svg+xml';
378
+ default:
379
+ return 'image/png';
380
+ }
381
+ }
382
+ async getImageBase64FromPathOrUrl(imagePathOrUrl) {
383
+ try {
384
+ let imageBuffer;
385
+ let fileName;
386
+ if (imagePathOrUrl.startsWith('http://') || imagePathOrUrl.startsWith('https://')) {
387
+ Logger.info(`从URL获取图片: ${imagePathOrUrl}`);
388
+ const response = await axios.get(imagePathOrUrl, { responseType: 'arraybuffer', timeout: 30000 });
389
+ imageBuffer = Buffer.from(response.data);
390
+ const urlPath = new URL(imagePathOrUrl).pathname;
391
+ fileName = path.basename(urlPath) || `image_${Date.now()}.png`;
392
+ Logger.info(`从URL成功获取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
393
+ }
394
+ else {
395
+ Logger.info(`从本地路径读取图片: ${imagePathOrUrl}`);
396
+ if (!fs.existsSync(imagePathOrUrl)) {
397
+ throw new Error(`图片文件不存在: ${imagePathOrUrl}`);
398
+ }
399
+ imageBuffer = fs.readFileSync(imagePathOrUrl);
400
+ fileName = path.basename(imagePathOrUrl);
401
+ Logger.info(`从本地路径成功读取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
402
+ }
403
+ return { base64: imageBuffer.toString('base64'), fileName };
404
+ }
405
+ catch (error) {
406
+ Logger.error(`获取图片失败: ${error}`);
407
+ throw new Error(`获取图片失败: ${error instanceof Error ? error.message : String(error)}`);
408
+ }
409
+ }
410
+ }