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.
- package/README.md +217 -115
- package/dist/manager/sseConnectionManager.js +0 -1
- package/dist/mcp/tools/feishuBlockTools.js +120 -9
- package/dist/mcp/tools/feishuFolderTools.js +4 -6
- package/dist/mcp/tools/feishuTools.js +47 -4
- package/dist/services/baseService.js +18 -4
- package/dist/services/blockFactory.js +36 -10
- package/dist/services/feishuApiService.js +327 -53
- package/dist/types/feishuSchema.js +56 -20
- package/package.json +3 -2
- package/README.en.md +0 -201
- package/dist/config.js +0 -104
- package/dist/services/feishu.js +0 -498
- package/dist/services/feishuBlocks.js +0 -135
|
@@ -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
|
-
//
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
197
|
-
* @returns
|
|
213
|
+
* 创建图片块内容(空图片块,需要后续设置图片资源)
|
|
214
|
+
* @param options 图片块选项
|
|
215
|
+
* @returns 图片块内容对象
|
|
198
216
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
//
|
|
632
|
-
const
|
|
633
|
-
const
|
|
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
|
}
|