feishu-mcp 0.0.12 → 0.0.14

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.
@@ -1,4 +1,5 @@
1
1
  import axios, { AxiosError } from 'axios';
2
+ import FormData from 'form-data';
2
3
  import { Logger } from '../utils/logger.js';
3
4
  import { formatErrorMessage } from '../utils/error.js';
4
5
  /**
@@ -74,17 +75,25 @@ export class BaseApiService {
74
75
  * @param data 请求数据
75
76
  * @param needsAuth 是否需要认证
76
77
  * @param additionalHeaders 附加请求头
78
+ * @param responseType 响应类型
77
79
  * @returns 响应数据
78
80
  */
79
- async request(endpoint, method = 'GET', data, needsAuth = true, additionalHeaders) {
81
+ async request(endpoint, method = 'GET', data, needsAuth = true, additionalHeaders, responseType) {
80
82
  try {
81
83
  // 构建请求URL
82
84
  const url = `${this.getBaseUrl()}${endpoint}`;
83
85
  // 准备请求头
84
86
  const headers = {
85
- 'Content-Type': 'application/json',
86
87
  ...additionalHeaders
87
88
  };
89
+ // 如果数据是FormData,合并FormData的headers
90
+ // 否则设置为application/json
91
+ if (data instanceof FormData) {
92
+ Object.assign(headers, data.getHeaders());
93
+ }
94
+ else {
95
+ headers['Content-Type'] = 'application/json';
96
+ }
88
97
  // 添加认证令牌
89
98
  if (needsAuth) {
90
99
  const accessToken = await this.getAccessToken();
@@ -103,7 +112,8 @@ export class BaseApiService {
103
112
  url,
104
113
  headers,
105
114
  data: method !== 'GET' ? data : undefined,
106
- params: method === 'GET' ? data : undefined
115
+ params: method === 'GET' ? data : undefined,
116
+ responseType: responseType || 'json'
107
117
  };
108
118
  // 发送请求
109
119
  const response = await axios(config);
@@ -112,7 +122,11 @@ export class BaseApiService {
112
122
  Logger.debug(`响应状态码: ${response.status}`);
113
123
  Logger.debug(`响应头:`, response.headers);
114
124
  Logger.debug(`响应数据:`, response.data);
115
- // 检查API错误
125
+ // 对于非JSON响应,直接返回数据
126
+ if (responseType && responseType !== 'json') {
127
+ return response.data;
128
+ }
129
+ // 检查API错误(仅对JSON响应)
116
130
  if (response.data && typeof response.data.code === 'number' && response.data.code !== 0) {
117
131
  Logger.error(`API返回错误码: ${response.data.code}, 错误消息: ${response.data.msg}`);
118
132
  throw {
@@ -8,6 +8,7 @@ export var BlockType;
8
8
  BlockType["CODE"] = "code";
9
9
  BlockType["HEADING"] = "heading";
10
10
  BlockType["LIST"] = "list";
11
+ BlockType["IMAGE"] = "image";
11
12
  })(BlockType || (BlockType = {}));
12
13
  /**
13
14
  * 对齐方式枚举
@@ -72,6 +73,8 @@ export class BlockFactory {
72
73
  return this.createHeadingBlock(options);
73
74
  case BlockType.LIST:
74
75
  return this.createListBlock(options);
76
+ case BlockType.IMAGE:
77
+ return this.createImageBlock(options);
75
78
  default:
76
79
  Logger.error(`不支持的块类型: ${type}`);
77
80
  throw new Error(`不支持的块类型: ${type}`);
@@ -87,14 +90,29 @@ export class BlockFactory {
87
90
  return {
88
91
  block_type: 2, // 2表示文本块
89
92
  text: {
90
- elements: textContents.map(content => ({
91
- text_run: {
92
- content: content.text,
93
- text_element_style: BlockFactory.applyDefaultTextStyle(content.style)
93
+ elements: textContents.map(content => {
94
+ // 检查是否是公式元素
95
+ if ('equation' in content) {
96
+ return {
97
+ equation: {
98
+ content: content.equation,
99
+ text_element_style: BlockFactory.applyDefaultTextStyle(content.style)
100
+ }
101
+ };
102
+ }
103
+ else {
104
+ // 普通文本元素
105
+ return {
106
+ text_run: {
107
+ content: content.text,
108
+ text_element_style: BlockFactory.applyDefaultTextStyle(content.style)
109
+ }
110
+ };
94
111
  }
95
- })),
112
+ }),
96
113
  style: {
97
114
  align: align, // 1 居左,2 居中,3 居右
115
+ folded: false
98
116
  }
99
117
  }
100
118
  };
@@ -192,11 +210,19 @@ export class BlockFactory {
192
210
  return blockContent;
193
211
  }
194
212
  /**
195
- * 创建批量块内容
196
- * @param blocks 块配置数组
197
- * @returns 块内容数组
213
+ * 创建图片块内容(空图片块,需要后续设置图片资源)
214
+ * @param options 图片块选项
215
+ * @returns 图片块内容对象
198
216
  */
199
- createBatchBlocks(blocks) {
200
- return blocks.map(block => this.createBlock(block.type, block.options));
217
+ createImageBlock(options = {}) {
218
+ const { width = 100, height = 100 } = options;
219
+ return {
220
+ block_type: 27, // 27表示图片块
221
+ image: {
222
+ width: width,
223
+ height: height,
224
+ token: "" // 空token,需要后续通过API设置
225
+ }
226
+ };
201
227
  }
202
228
  }
@@ -5,6 +5,9 @@ import { CacheManager } from '../utils/cache.js';
5
5
  import { ParamUtils } from '../utils/paramUtils.js';
6
6
  import { BlockFactory, BlockType } from './blockFactory.js';
7
7
  import axios from 'axios';
8
+ import FormData from 'form-data';
9
+ import fs from 'fs';
10
+ import path from 'path';
8
11
  /**
9
12
  * 飞书API服务类
10
13
  * 提供飞书API的所有基础操作,包括认证、请求和缓存管理
@@ -218,7 +221,7 @@ export class FeishuApiService extends BaseApiService {
218
221
  * 更新块文本内容
219
222
  * @param documentId 文档ID或URL
220
223
  * @param blockId 块ID
221
- * @param textElements 文本元素数组
224
+ * @param textElements 文本元素数组,支持普通文本和公式元素
222
225
  * @returns 更新结果
223
226
  */
224
227
  async updateBlockTextContent(documentId, blockId, textElements) {
@@ -226,12 +229,24 @@ export class FeishuApiService extends BaseApiService {
226
229
  const docId = ParamUtils.processDocumentId(documentId);
227
230
  const endpoint = `/docx/v1/documents/${docId}/blocks/${blockId}?document_revision_id=-1`;
228
231
  Logger.debug(`准备请求API端点: ${endpoint}`);
229
- const elements = textElements.map(item => ({
230
- text_run: {
231
- content: item.text,
232
- text_element_style: BlockFactory.applyDefaultTextStyle(item.style)
232
+ const elements = textElements.map(item => {
233
+ if (item.equation !== undefined) {
234
+ return {
235
+ equation: {
236
+ content: item.equation,
237
+ text_element_style: BlockFactory.applyDefaultTextStyle(item.style)
238
+ }
239
+ };
240
+ }
241
+ else {
242
+ return {
243
+ text_run: {
244
+ content: item.text || '',
245
+ text_element_style: BlockFactory.applyDefaultTextStyle(item.style)
246
+ }
247
+ };
233
248
  }
234
- }));
249
+ });
235
250
  const data = {
236
251
  update_text_elements: {
237
252
  elements: elements
@@ -302,17 +317,27 @@ export class FeishuApiService extends BaseApiService {
302
317
  * 创建文本块
303
318
  * @param documentId 文档ID或URL
304
319
  * @param parentBlockId 父块ID
305
- * @param textContents 文本内容数组
320
+ * @param textContents 文本内容数组,支持普通文本和公式元素
306
321
  * @param align 对齐方式,1为左对齐,2为居中,3为右对齐
307
322
  * @param index 插入位置索引
308
323
  * @returns 创建结果
309
324
  */
310
325
  async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) {
311
- // 处理文本内容样式
312
- const processedTextContents = textContents.map(item => ({
313
- text: item.text,
314
- style: BlockFactory.applyDefaultTextStyle(item.style)
315
- }));
326
+ // 处理文本内容样式,支持普通文本和公式元素
327
+ const processedTextContents = textContents.map(item => {
328
+ if (item.equation !== undefined) {
329
+ return {
330
+ equation: item.equation,
331
+ style: BlockFactory.applyDefaultTextStyle(item.style)
332
+ };
333
+ }
334
+ else {
335
+ return {
336
+ text: item.text || '',
337
+ style: BlockFactory.applyDefaultTextStyle(item.style)
338
+ };
339
+ }
340
+ });
316
341
  const blockContent = this.blockFactory.createTextBlock({
317
342
  textContents: processedTextContents,
318
343
  align
@@ -373,18 +398,6 @@ export class FeishuApiService extends BaseApiService {
373
398
  });
374
399
  return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
375
400
  }
376
- /**
377
- * 创建混合块
378
- * @param documentId 文档ID或URL
379
- * @param parentBlockId 父块ID
380
- * @param blocks 块配置数组
381
- * @param index 插入位置索引
382
- * @returns 创建结果
383
- */
384
- async createMixedBlocks(documentId, parentBlockId, blocks, index = 0) {
385
- const blockContents = blocks.map(block => this.blockFactory.createBlock(block.type, block.options));
386
- return this.createDocumentBlocks(documentId, parentBlockId, blockContents, index);
387
- }
388
401
  /**
389
402
  * 删除文档中的块,支持批量删除
390
403
  * @param documentId 文档ID或URL
@@ -501,7 +514,7 @@ export class FeishuApiService extends BaseApiService {
501
514
  // 使用枚举类型来避免字符串错误
502
515
  const blockTypeEnum = blockType;
503
516
  // 构建块配置
504
- let blockConfig = {
517
+ const blockConfig = {
505
518
  type: blockTypeEnum,
506
519
  options: {}
507
520
  };
@@ -510,12 +523,22 @@ export class FeishuApiService extends BaseApiService {
510
523
  case BlockType.TEXT:
511
524
  if ('text' in options && options.text) {
512
525
  const textOptions = options.text;
513
- // 处理文本样式,应用默认样式
526
+ // 处理文本样式,应用默认样式,支持普通文本和公式元素
514
527
  const textStyles = textOptions.textStyles || [];
515
- const processedTextStyles = textStyles.map((item) => ({
516
- text: item.text,
517
- style: BlockFactory.applyDefaultTextStyle(item.style)
518
- }));
528
+ const processedTextStyles = textStyles.map((item) => {
529
+ if (item.equation !== undefined) {
530
+ return {
531
+ equation: item.equation,
532
+ style: BlockFactory.applyDefaultTextStyle(item.style)
533
+ };
534
+ }
535
+ else {
536
+ return {
537
+ text: item.text || '',
538
+ style: BlockFactory.applyDefaultTextStyle(item.style)
539
+ };
540
+ }
541
+ });
519
542
  blockConfig.options = {
520
543
  textContents: processedTextStyles,
521
544
  align: textOptions.align || 1
@@ -554,17 +577,43 @@ export class FeishuApiService extends BaseApiService {
554
577
  };
555
578
  }
556
579
  break;
580
+ case BlockType.IMAGE:
581
+ if ('image' in options && options.image) {
582
+ const imageOptions = options.image;
583
+ blockConfig.options = {
584
+ width: imageOptions.width || 100,
585
+ height: imageOptions.height || 100
586
+ };
587
+ }
588
+ else {
589
+ // 默认图片块选项
590
+ blockConfig.options = {
591
+ width: 100,
592
+ height: 100
593
+ };
594
+ }
595
+ break;
557
596
  default:
558
597
  Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
559
598
  if ('text' in options) {
560
599
  blockConfig.type = BlockType.TEXT;
561
600
  const textOptions = options.text;
562
- // 处理文本样式,应用默认样式
601
+ // 处理文本样式,应用默认样式,支持普通文本和公式元素
563
602
  const textStyles = textOptions.textStyles || [];
564
- const processedTextStyles = textStyles.map((item) => ({
565
- text: item.text,
566
- style: BlockFactory.applyDefaultTextStyle(item.style)
567
- }));
603
+ const processedTextStyles = textStyles.map((item) => {
604
+ if (item.equation !== undefined) {
605
+ return {
606
+ equation: item.equation,
607
+ style: BlockFactory.applyDefaultTextStyle(item.style)
608
+ };
609
+ }
610
+ else {
611
+ return {
612
+ text: item.text || '',
613
+ style: BlockFactory.applyDefaultTextStyle(item.style)
614
+ };
615
+ }
616
+ });
568
617
  blockConfig.options = {
569
618
  textContents: processedTextStyles,
570
619
  align: textOptions.align || 1
@@ -599,6 +648,14 @@ export class FeishuApiService extends BaseApiService {
599
648
  ? listOptions.align : 1
600
649
  };
601
650
  }
651
+ else if ('image' in options) {
652
+ blockConfig.type = BlockType.IMAGE;
653
+ const imageOptions = options.image;
654
+ blockConfig.options = {
655
+ width: imageOptions.width || 100,
656
+ height: imageOptions.height || 100
657
+ };
658
+ }
602
659
  break;
603
660
  }
604
661
  // 记录调试信息
@@ -628,24 +685,9 @@ export class FeishuApiService extends BaseApiService {
628
685
  if (extra) {
629
686
  params.extra = extra;
630
687
  }
631
- // 这里需要特殊处理,因为返回的是二进制数据,不是JSON
632
- const token = await this.getAccessToken();
633
- const url = `${this.getBaseUrl()}${endpoint}`;
634
- const headers = {
635
- 'Authorization': `Bearer ${token}`
636
- };
637
- Logger.debug(`请求图片资源URL: ${url}`);
638
- // 使用axios直接获取二进制响应
639
- const response = await axios.get(url, {
640
- params,
641
- headers,
642
- responseType: 'arraybuffer'
643
- });
644
- // 检查响应状态
645
- if (response.status !== 200) {
646
- throw new Error(`获取图片资源失败,状态码: ${response.status}`);
647
- }
648
- const imageBuffer = Buffer.from(response.data);
688
+ // 使用通用的request方法获取二进制响应
689
+ const response = await this.request(endpoint, 'GET', params, true, {}, 'arraybuffer');
690
+ const imageBuffer = Buffer.from(response);
649
691
  Logger.info(`图片资源获取成功,大小: ${imageBuffer.length} 字节`);
650
692
  return imageBuffer;
651
693
  }
@@ -760,4 +802,236 @@ export class FeishuApiService extends BaseApiService {
760
802
  this.handleApiError(error, '搜索文档失败');
761
803
  }
762
804
  }
805
+ /**
806
+ * 上传图片素材到飞书
807
+ * @param imageBase64 图片的Base64编码
808
+ * @param fileName 图片文件名,如果不提供则自动生成
809
+ * @param parentBlockId 图片块ID
810
+ * @returns 上传结果,包含file_token
811
+ */
812
+ async uploadImageMedia(imageBase64, fileName, parentBlockId) {
813
+ try {
814
+ const endpoint = '/drive/v1/medias/upload_all';
815
+ // 将Base64转换为Buffer
816
+ const imageBuffer = Buffer.from(imageBase64, 'base64');
817
+ const imageSize = imageBuffer.length;
818
+ // 如果没有提供文件名,根据Base64数据生成默认文件名
819
+ if (!fileName) {
820
+ // 简单检测图片格式
821
+ if (imageBase64.startsWith('/9j/')) {
822
+ fileName = `image_${Date.now()}.jpg`;
823
+ }
824
+ else if (imageBase64.startsWith('iVBORw0KGgo')) {
825
+ fileName = `image_${Date.now()}.png`;
826
+ }
827
+ else if (imageBase64.startsWith('R0lGODlh')) {
828
+ fileName = `image_${Date.now()}.gif`;
829
+ }
830
+ else {
831
+ fileName = `image_${Date.now()}.png`; // 默认PNG格式
832
+ }
833
+ }
834
+ Logger.info(`开始上传图片素材,文件名: ${fileName},大小: ${imageSize} 字节,关联块ID: ${parentBlockId}`);
835
+ // 验证图片大小(可选的业务检查)
836
+ if (imageSize > 20 * 1024 * 1024) {
837
+ // 20MB限制
838
+ Logger.warn(`图片文件过大: ${imageSize} 字节,建议小于20MB`);
839
+ }
840
+ // 使用FormData构建multipart/form-data请求
841
+ const formData = new FormData();
842
+ // file字段传递图片的二进制数据流
843
+ // Buffer是Node.js中的二进制数据类型,form-data库会将其作为文件流处理
844
+ formData.append('file', imageBuffer, {
845
+ filename: fileName,
846
+ contentType: this.getMimeTypeFromFileName(fileName),
847
+ knownLength: imageSize, // 明确指定文件大小,避免流读取问题
848
+ });
849
+ // 飞书API要求的其他表单字段
850
+ formData.append('file_name', fileName);
851
+ formData.append('parent_type', 'docx_image'); // 固定值:文档图片类型
852
+ formData.append('parent_node', parentBlockId); // 关联的图片块ID
853
+ formData.append('size', imageSize.toString()); // 文件大小(字节,字符串格式)
854
+ // 使用通用的post方法发送请求
855
+ const response = await this.post(endpoint, formData);
856
+ Logger.info(`图片素材上传成功,file_token: ${response.file_token}`);
857
+ return response;
858
+ }
859
+ catch (error) {
860
+ this.handleApiError(error, '上传图片素材失败');
861
+ }
862
+ }
863
+ /**
864
+ * 设置图片块的素材内容
865
+ * @param documentId 文档ID
866
+ * @param imageBlockId 图片块ID
867
+ * @param fileToken 图片素材的file_token
868
+ * @returns 设置结果
869
+ */
870
+ async setImageBlockContent(documentId, imageBlockId, fileToken) {
871
+ try {
872
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
873
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${imageBlockId}`;
874
+ const payload = {
875
+ replace_image: {
876
+ token: fileToken,
877
+ },
878
+ };
879
+ Logger.info(`开始设置图片块内容,文档ID: ${normalizedDocId},块ID: ${imageBlockId},file_token: ${fileToken}`);
880
+ const response = await this.patch(endpoint, payload);
881
+ Logger.info('图片块内容设置成功');
882
+ return response;
883
+ }
884
+ catch (error) {
885
+ this.handleApiError(error, '设置图片块内容失败');
886
+ }
887
+ }
888
+ /**
889
+ * 创建完整的图片块(包括创建空块、上传图片、设置内容的完整流程)
890
+ * @param documentId 文档ID
891
+ * @param parentBlockId 父块ID
892
+ * @param imagePathOrUrl 图片路径或URL
893
+ * @param options 图片选项
894
+ * @returns 创建结果
895
+ */
896
+ async createImageBlock(documentId, parentBlockId, imagePathOrUrl, options = {}) {
897
+ try {
898
+ const { fileName: providedFileName, width, height, index = 0 } = options;
899
+ Logger.info(`开始创建图片块,文档ID: ${documentId},父块ID: ${parentBlockId},图片源: ${imagePathOrUrl},插入位置: ${index}`);
900
+ // 从路径或URL获取图片的Base64编码
901
+ const { base64: imageBase64, fileName: detectedFileName } = await this.getImageBase64FromPathOrUrl(imagePathOrUrl);
902
+ // 使用提供的文件名或检测到的文件名
903
+ const finalFileName = providedFileName || detectedFileName;
904
+ // 第1步:创建空图片块
905
+ Logger.info('第1步:创建空图片块');
906
+ const imageBlockContent = this.blockFactory.createImageBlock({
907
+ width,
908
+ height,
909
+ });
910
+ const createBlockResult = await this.createDocumentBlock(documentId, parentBlockId, imageBlockContent, index);
911
+ if (!createBlockResult?.children?.[0]?.block_id) {
912
+ throw new Error('创建空图片块失败:无法获取块ID');
913
+ }
914
+ const imageBlockId = createBlockResult.children[0].block_id;
915
+ Logger.info(`空图片块创建成功,块ID: ${imageBlockId}`);
916
+ // 第2步:上传图片素材
917
+ Logger.info('第2步:上传图片素材');
918
+ const uploadResult = await this.uploadImageMedia(imageBase64, finalFileName, imageBlockId);
919
+ if (!uploadResult?.file_token) {
920
+ throw new Error('上传图片素材失败:无法获取file_token');
921
+ }
922
+ Logger.info(`图片素材上传成功,file_token: ${uploadResult.file_token}`);
923
+ // 第3步:设置图片块内容
924
+ Logger.info('第3步:设置图片块内容');
925
+ const setContentResult = await this.setImageBlockContent(documentId, imageBlockId, uploadResult.file_token);
926
+ Logger.info('图片块创建完成');
927
+ // 返回综合结果
928
+ return {
929
+ imageBlock: createBlockResult.children[0],
930
+ imageBlockId: imageBlockId,
931
+ fileToken: uploadResult.file_token,
932
+ uploadResult: uploadResult,
933
+ setContentResult: setContentResult,
934
+ documentRevisionId: setContentResult.document_revision_id ||
935
+ createBlockResult.document_revision_id,
936
+ };
937
+ }
938
+ catch (error) {
939
+ this.handleApiError(error, '创建图片块失败');
940
+ }
941
+ }
942
+ /**
943
+ * 根据文件名获取MIME类型
944
+ * @param fileName 文件名
945
+ * @returns MIME类型
946
+ */
947
+ getMimeTypeFromFileName(fileName) {
948
+ const extension = fileName.toLowerCase().split('.').pop();
949
+ switch (extension) {
950
+ case 'jpg':
951
+ case 'jpeg':
952
+ return 'image/jpeg';
953
+ case 'png':
954
+ return 'image/png';
955
+ case 'gif':
956
+ return 'image/gif';
957
+ case 'webp':
958
+ return 'image/webp';
959
+ case 'bmp':
960
+ return 'image/bmp';
961
+ case 'svg':
962
+ return 'image/svg+xml';
963
+ default:
964
+ return 'image/png'; // 默认PNG
965
+ }
966
+ }
967
+ /**
968
+ * 获取画板内容
969
+ * @param whiteboardId 画板ID或URL
970
+ * @returns 画板节点数据
971
+ */
972
+ async getWhiteboardContent(whiteboardId) {
973
+ try {
974
+ // 从URL中提取画板ID
975
+ let normalizedWhiteboardId = whiteboardId;
976
+ if (whiteboardId.includes('feishu.cn/board/')) {
977
+ // 从URL中提取画板ID
978
+ const matches = whiteboardId.match(/board\/([^\/\?]+)/);
979
+ if (matches) {
980
+ normalizedWhiteboardId = matches[1];
981
+ }
982
+ }
983
+ const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes`;
984
+ Logger.info(`开始获取画板内容,画板ID: ${normalizedWhiteboardId}`);
985
+ const response = await this.get(endpoint);
986
+ Logger.info(`画板内容获取成功,节点数量: ${response.nodes?.length || 0}`);
987
+ return response;
988
+ }
989
+ catch (error) {
990
+ this.handleApiError(error, '获取画板内容失败');
991
+ }
992
+ }
993
+ /**
994
+ * 从路径或URL获取图片的Base64编码
995
+ * @param imagePathOrUrl 图片路径或URL
996
+ * @returns 图片的Base64编码和文件名
997
+ */
998
+ async getImageBase64FromPathOrUrl(imagePathOrUrl) {
999
+ try {
1000
+ let imageBuffer;
1001
+ let fileName;
1002
+ // 判断是否为HTTP/HTTPS URL
1003
+ if (imagePathOrUrl.startsWith('http://') || imagePathOrUrl.startsWith('https://')) {
1004
+ Logger.info(`从URL获取图片: ${imagePathOrUrl}`);
1005
+ // 从URL下载图片
1006
+ const response = await axios.get(imagePathOrUrl, {
1007
+ responseType: 'arraybuffer',
1008
+ timeout: 30000, // 30秒超时
1009
+ });
1010
+ imageBuffer = Buffer.from(response.data);
1011
+ // 从URL中提取文件名
1012
+ const urlPath = new URL(imagePathOrUrl).pathname;
1013
+ fileName = path.basename(urlPath) || `image_${Date.now()}.png`;
1014
+ Logger.info(`从URL成功获取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
1015
+ }
1016
+ else {
1017
+ // 本地文件路径
1018
+ Logger.info(`从本地路径读取图片: ${imagePathOrUrl}`);
1019
+ // 检查文件是否存在
1020
+ if (!fs.existsSync(imagePathOrUrl)) {
1021
+ throw new Error(`图片文件不存在: ${imagePathOrUrl}`);
1022
+ }
1023
+ // 读取文件
1024
+ imageBuffer = fs.readFileSync(imagePathOrUrl);
1025
+ fileName = path.basename(imagePathOrUrl);
1026
+ Logger.info(`从本地路径成功读取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
1027
+ }
1028
+ // 转换为Base64
1029
+ const base64 = imageBuffer.toString('base64');
1030
+ return { base64, fileName };
1031
+ }
1032
+ catch (error) {
1033
+ Logger.error(`获取图片失败: ${error}`);
1034
+ throw new Error(`获取图片失败: ${error instanceof Error ? error.message : String(error)}`);
1035
+ }
1036
+ }
763
1037
  }