feishu-mcp 0.0.7 → 0.0.9-alpha
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.en.md +201 -0
- package/README.md +164 -160
- package/dist/cli.js +0 -0
- package/dist/config.js +100 -22
- package/dist/manager/sseConnectionManager.js +104 -0
- package/dist/mcp/feishuMcp.js +67 -0
- package/dist/mcp/tools/feishuBlockTools.js +394 -0
- package/dist/mcp/tools/feishuFolderTools.js +86 -0
- package/dist/mcp/tools/feishuTools.js +138 -0
- package/dist/server.js +39 -488
- package/dist/services/feishu.js +3 -0
- package/dist/services/feishuApiService.js +143 -1
- package/dist/types/feishuSchema.js +22 -0
- package/dist/utils/document.js +38 -0
- package/dist/utils/logger.js +25 -9
- package/package.json +74 -74
- package/dist/services/feishuBlockService.js +0 -179
- package/dist/services/feishuService.js +0 -475
|
@@ -377,9 +377,48 @@ export class FeishuApiService extends BaseApiService {
|
|
|
377
377
|
* @returns 创建结果
|
|
378
378
|
*/
|
|
379
379
|
async createMixedBlocks(documentId, parentBlockId, blocks, index = 0) {
|
|
380
|
-
const blockContents = this.blockFactory.
|
|
380
|
+
const blockContents = blocks.map(block => this.blockFactory.createBlock(block.type, block.options));
|
|
381
381
|
return this.createDocumentBlocks(documentId, parentBlockId, blockContents, index);
|
|
382
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* 删除文档中的块,支持批量删除
|
|
385
|
+
* @param documentId 文档ID或URL
|
|
386
|
+
* @param parentBlockId 父块ID(通常是文档ID)
|
|
387
|
+
* @param startIndex 起始索引
|
|
388
|
+
* @param endIndex 结束索引
|
|
389
|
+
* @returns 操作结果
|
|
390
|
+
*/
|
|
391
|
+
async deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex) {
|
|
392
|
+
try {
|
|
393
|
+
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
394
|
+
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children/batch_delete`;
|
|
395
|
+
// 确保索引有效
|
|
396
|
+
if (startIndex < 0 || endIndex < startIndex) {
|
|
397
|
+
throw new Error('无效的索引范围:起始索引必须大于等于0,结束索引必须大于等于起始索引');
|
|
398
|
+
}
|
|
399
|
+
const payload = {
|
|
400
|
+
start_index: startIndex,
|
|
401
|
+
end_index: endIndex
|
|
402
|
+
};
|
|
403
|
+
Logger.info(`开始删除文档块,文档ID: ${normalizedDocId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
|
|
404
|
+
const response = await this.delete(endpoint, payload);
|
|
405
|
+
Logger.info('文档块删除成功');
|
|
406
|
+
return response;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
this.handleApiError(error, '删除文档块失败');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 删除单个文档块(通过创建起始和结束索引相同的批量删除请求)
|
|
414
|
+
* @param documentId 文档ID或URL
|
|
415
|
+
* @param parentBlockId 父块ID
|
|
416
|
+
* @param blockIndex 块索引
|
|
417
|
+
* @returns 操作结果
|
|
418
|
+
*/
|
|
419
|
+
async deleteDocumentBlock(documentId, parentBlockId, blockIndex) {
|
|
420
|
+
return this.deleteDocumentBlocks(documentId, parentBlockId, blockIndex, blockIndex + 1);
|
|
421
|
+
}
|
|
383
422
|
/**
|
|
384
423
|
* 将飞书Wiki链接转换为文档ID
|
|
385
424
|
* @param wikiUrl Wiki链接或Token
|
|
@@ -485,4 +524,107 @@ export class FeishuApiService extends BaseApiService {
|
|
|
485
524
|
return null;
|
|
486
525
|
}
|
|
487
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* 获取飞书图片资源
|
|
529
|
+
* @param mediaId 图片媒体ID
|
|
530
|
+
* @param extra 额外参数,可选
|
|
531
|
+
* @returns 图片二进制数据
|
|
532
|
+
*/
|
|
533
|
+
async getImageResource(mediaId, extra = '') {
|
|
534
|
+
try {
|
|
535
|
+
Logger.info(`开始获取图片资源,媒体ID: ${mediaId}`);
|
|
536
|
+
if (!mediaId) {
|
|
537
|
+
throw new Error('媒体ID不能为空');
|
|
538
|
+
}
|
|
539
|
+
const endpoint = `/drive/v1/medias/${mediaId}/download`;
|
|
540
|
+
const params = {};
|
|
541
|
+
if (extra) {
|
|
542
|
+
params.extra = extra;
|
|
543
|
+
}
|
|
544
|
+
// 这里需要特殊处理,因为返回的是二进制数据,不是JSON
|
|
545
|
+
const token = await this.getAccessToken();
|
|
546
|
+
const url = `${this.getBaseUrl()}${endpoint}`;
|
|
547
|
+
const headers = {
|
|
548
|
+
'Authorization': `Bearer ${token}`
|
|
549
|
+
};
|
|
550
|
+
Logger.debug(`请求图片资源URL: ${url}`);
|
|
551
|
+
// 使用axios直接获取二进制响应
|
|
552
|
+
const response = await axios.get(url, {
|
|
553
|
+
params,
|
|
554
|
+
headers,
|
|
555
|
+
responseType: 'arraybuffer'
|
|
556
|
+
});
|
|
557
|
+
// 检查响应状态
|
|
558
|
+
if (response.status !== 200) {
|
|
559
|
+
throw new Error(`获取图片资源失败,状态码: ${response.status}`);
|
|
560
|
+
}
|
|
561
|
+
const imageBuffer = Buffer.from(response.data);
|
|
562
|
+
Logger.info(`图片资源获取成功,大小: ${imageBuffer.length} 字节`);
|
|
563
|
+
return imageBuffer;
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
this.handleApiError(error, '获取图片资源失败');
|
|
567
|
+
return Buffer.from([]); // 永远不会执行到这里
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 获取飞书根文件夹信息
|
|
572
|
+
* 获取用户的根文件夹的元数据信息,包括token、id和用户id
|
|
573
|
+
* @returns 根文件夹信息
|
|
574
|
+
*/
|
|
575
|
+
async getRootFolderInfo() {
|
|
576
|
+
try {
|
|
577
|
+
const endpoint = '/drive/explorer/v2/root_folder/meta';
|
|
578
|
+
const response = await this.get(endpoint);
|
|
579
|
+
Logger.debug('获取根文件夹信息成功:', response);
|
|
580
|
+
return response;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
this.handleApiError(error, '获取飞书根文件夹信息失败');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 获取文件夹中的文件清单
|
|
588
|
+
* @param folderToken 文件夹Token
|
|
589
|
+
* @param orderBy 排序方式,默认按修改时间排序
|
|
590
|
+
* @param direction 排序方向,默认降序
|
|
591
|
+
* @returns 文件清单信息
|
|
592
|
+
*/
|
|
593
|
+
async getFolderFileList(folderToken, orderBy = 'EditedTime', direction = 'DESC') {
|
|
594
|
+
try {
|
|
595
|
+
const endpoint = '/drive/v1/files';
|
|
596
|
+
const params = {
|
|
597
|
+
folder_token: folderToken,
|
|
598
|
+
order_by: orderBy,
|
|
599
|
+
direction: direction
|
|
600
|
+
};
|
|
601
|
+
const response = await this.get(endpoint, params);
|
|
602
|
+
Logger.debug(`获取文件夹(${folderToken})中的文件清单成功,文件数量: ${response.files?.length || 0}`);
|
|
603
|
+
return response;
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
this.handleApiError(error, '获取文件夹中的文件清单失败');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* 创建文件夹
|
|
611
|
+
* @param folderToken 父文件夹Token
|
|
612
|
+
* @param name 文件夹名称
|
|
613
|
+
* @returns 创建的文件夹信息
|
|
614
|
+
*/
|
|
615
|
+
async createFolder(folderToken, name) {
|
|
616
|
+
try {
|
|
617
|
+
const endpoint = '/drive/v1/files/create_folder';
|
|
618
|
+
const payload = {
|
|
619
|
+
folder_token: folderToken,
|
|
620
|
+
name: name
|
|
621
|
+
};
|
|
622
|
+
const response = await this.post(endpoint, payload);
|
|
623
|
+
Logger.debug(`文件夹创建成功, token: ${response.token}, url: ${response.url}`);
|
|
624
|
+
return response;
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
this.handleApiError(error, '创建文件夹失败');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
488
630
|
}
|
|
@@ -17,6 +17,10 @@ export const IndexSchema = z.number().describe('Insertion position index (requir
|
|
|
17
17
|
// 起始插入位置索引参数定义
|
|
18
18
|
export const StartIndexSchema = z.number().describe('Starting insertion position index (required). Specifies where the first block should be inserted. Use 0 to insert at the beginning. ' +
|
|
19
19
|
'Use get_feishu_document_blocks tool to understand document structure if unsure.');
|
|
20
|
+
// 结束位置索引参数定义
|
|
21
|
+
export const EndIndexSchema = z.number().describe('Ending position index (required). Specifies the end of the range for deletion (exclusive). ' +
|
|
22
|
+
'For example, to delete blocks 2, 3, and 4, use startIndex=2, endIndex=5. ' +
|
|
23
|
+
'To delete a single block at position 2, use startIndex=2, endIndex=3.');
|
|
20
24
|
// 文本对齐方式参数定义
|
|
21
25
|
export const AlignSchema = z.number().optional().default(1).describe('Text alignment: 1 for left (default), 2 for center, 3 for right.');
|
|
22
26
|
// 文本对齐方式参数定义(带验证)
|
|
@@ -95,3 +99,21 @@ export const BlockConfigSchema = z.object({
|
|
|
95
99
|
z.object({ list: ListBlockSchema }).describe("List block options. Only used when blockType is 'list'."),
|
|
96
100
|
]).describe('Options for the specific block type. Must provide the corresponding options object based on blockType.'),
|
|
97
101
|
});
|
|
102
|
+
// 媒体ID参数定义
|
|
103
|
+
export const MediaIdSchema = z.string().describe('Media ID (required). The unique identifier for a media resource (image, file, etc.) in Feishu. ' +
|
|
104
|
+
'Usually obtained from image blocks or file references in documents. ' +
|
|
105
|
+
'Format is typically like "boxcnrHpsg1QDqXAAAyachabcef".');
|
|
106
|
+
// 额外参数定义 - 用于媒体资源下载
|
|
107
|
+
export const MediaExtraSchema = z.string().optional().describe('Extra parameters for media download (optional). ' +
|
|
108
|
+
'These parameters are passed directly to the Feishu API and can modify how the media is returned.');
|
|
109
|
+
// 文件夹Token参数定义
|
|
110
|
+
export const FolderTokenSchema = z.string().describe('Folder token (required). The unique identifier for a folder in Feishu. ' +
|
|
111
|
+
'Format is an alphanumeric string like "FWK2fMleClICfodlHHWc4Mygnhb".');
|
|
112
|
+
// 文件夹名称参数定义
|
|
113
|
+
export const FolderNameSchema = z.string().describe('Folder name (required). The name for the new folder to be created.');
|
|
114
|
+
// 排序方式参数定义
|
|
115
|
+
export const OrderBySchema = z.string().optional().default('EditedTime').describe('Order by field (optional). Specifies how to sort the file list. Available values: ' +
|
|
116
|
+
'"EditedTime" (default), "CreatedTime", "Name". For user-friendly display, case insensitive.');
|
|
117
|
+
// 排序方向参数定义
|
|
118
|
+
export const DirectionSchema = z.string().optional().default('DESC').describe('Sort direction (optional). Specifies the sort order. Available values: ' +
|
|
119
|
+
'"DESC" (default) for descending order, "ASC" for ascending order. Case sensitive.');
|
package/dist/utils/document.js
CHANGED
|
@@ -72,3 +72,41 @@ export function normalizeWikiToken(input) {
|
|
|
72
72
|
}
|
|
73
73
|
return token;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 根据图片二进制数据检测MIME类型
|
|
77
|
+
* @param buffer 图片二进制数据
|
|
78
|
+
* @returns MIME类型字符串
|
|
79
|
+
*/
|
|
80
|
+
export function detectMimeType(buffer) {
|
|
81
|
+
// 简单的图片格式检测,根据文件头进行判断
|
|
82
|
+
if (buffer.length < 4) {
|
|
83
|
+
return 'application/octet-stream';
|
|
84
|
+
}
|
|
85
|
+
// JPEG格式
|
|
86
|
+
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) {
|
|
87
|
+
return 'image/jpeg';
|
|
88
|
+
}
|
|
89
|
+
// PNG格式
|
|
90
|
+
else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) {
|
|
91
|
+
return 'image/png';
|
|
92
|
+
}
|
|
93
|
+
// GIF格式
|
|
94
|
+
else if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
|
95
|
+
return 'image/gif';
|
|
96
|
+
}
|
|
97
|
+
// SVG格式 - 检查字符串前缀
|
|
98
|
+
else if (buffer.length > 5 && buffer.toString('ascii', 0, 5).toLowerCase() === '<?xml' ||
|
|
99
|
+
buffer.toString('ascii', 0, 4).toLowerCase() === '<svg') {
|
|
100
|
+
return 'image/svg+xml';
|
|
101
|
+
}
|
|
102
|
+
// WebP格式
|
|
103
|
+
else if (buffer.length > 12 &&
|
|
104
|
+
buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
|
|
105
|
+
buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) {
|
|
106
|
+
return 'image/webp';
|
|
107
|
+
}
|
|
108
|
+
// 默认二进制流
|
|
109
|
+
else {
|
|
110
|
+
return 'application/octet-stream';
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -25,13 +25,28 @@ export class Logger {
|
|
|
25
25
|
static configure(config) {
|
|
26
26
|
this.config = { ...this.config, ...config };
|
|
27
27
|
// 确保日志目录存在
|
|
28
|
-
if (this.config.logToFile) {
|
|
28
|
+
if (this.config.logToFile && this.config.enabled) {
|
|
29
29
|
const logDir = path.dirname(this.config.logFilePath);
|
|
30
30
|
if (!fs.existsSync(logDir)) {
|
|
31
31
|
fs.mkdirSync(logDir, { recursive: true });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* 设置日志开关
|
|
37
|
+
* @param enabled 是否启用日志
|
|
38
|
+
*/
|
|
39
|
+
static setEnabled(enabled) {
|
|
40
|
+
this.config.enabled = enabled;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 检查日志是否可输出
|
|
44
|
+
* @param level 日志级别
|
|
45
|
+
* @returns 是否可输出
|
|
46
|
+
*/
|
|
47
|
+
static canLog(level) {
|
|
48
|
+
return this.config.enabled && level >= this.config.minLevel;
|
|
49
|
+
}
|
|
35
50
|
/**
|
|
36
51
|
* 格式化日志消息
|
|
37
52
|
* @param level 日志级别
|
|
@@ -59,7 +74,7 @@ export class Logger {
|
|
|
59
74
|
* @param logParts 日志内容部分
|
|
60
75
|
*/
|
|
61
76
|
static writeToFile(logParts) {
|
|
62
|
-
if (!this.config.logToFile)
|
|
77
|
+
if (!this.config.enabled || !this.config.logToFile)
|
|
63
78
|
return;
|
|
64
79
|
try {
|
|
65
80
|
// 将日志内容转换为字符串
|
|
@@ -143,7 +158,7 @@ export class Logger {
|
|
|
143
158
|
* @param args 日志参数
|
|
144
159
|
*/
|
|
145
160
|
static debug(...args) {
|
|
146
|
-
if (this.
|
|
161
|
+
if (this.canLog(LogLevel.DEBUG)) {
|
|
147
162
|
const formattedMessage = this.formatLogMessage(LogLevel.DEBUG, args);
|
|
148
163
|
console.debug(...formattedMessage);
|
|
149
164
|
this.writeToFile(formattedMessage);
|
|
@@ -154,7 +169,7 @@ export class Logger {
|
|
|
154
169
|
* @param args 日志参数
|
|
155
170
|
*/
|
|
156
171
|
static info(...args) {
|
|
157
|
-
if (this.
|
|
172
|
+
if (this.canLog(LogLevel.INFO)) {
|
|
158
173
|
const formattedMessage = this.formatLogMessage(LogLevel.INFO, args);
|
|
159
174
|
console.info(...formattedMessage);
|
|
160
175
|
this.writeToFile(formattedMessage);
|
|
@@ -165,7 +180,7 @@ export class Logger {
|
|
|
165
180
|
* @param args 日志参数
|
|
166
181
|
*/
|
|
167
182
|
static log(...args) {
|
|
168
|
-
if (this.
|
|
183
|
+
if (this.canLog(LogLevel.LOG)) {
|
|
169
184
|
const formattedMessage = this.formatLogMessage(LogLevel.LOG, args);
|
|
170
185
|
console.log(...formattedMessage);
|
|
171
186
|
this.writeToFile(formattedMessage);
|
|
@@ -176,7 +191,7 @@ export class Logger {
|
|
|
176
191
|
* @param args 日志参数
|
|
177
192
|
*/
|
|
178
193
|
static warn(...args) {
|
|
179
|
-
if (this.
|
|
194
|
+
if (this.canLog(LogLevel.WARN)) {
|
|
180
195
|
const formattedMessage = this.formatLogMessage(LogLevel.WARN, args);
|
|
181
196
|
console.warn(...formattedMessage);
|
|
182
197
|
this.writeToFile(formattedMessage);
|
|
@@ -187,7 +202,7 @@ export class Logger {
|
|
|
187
202
|
* @param args 日志参数
|
|
188
203
|
*/
|
|
189
204
|
static error(...args) {
|
|
190
|
-
if (this.
|
|
205
|
+
if (this.canLog(LogLevel.ERROR)) {
|
|
191
206
|
const formattedMessage = this.formatLogMessage(LogLevel.ERROR, args);
|
|
192
207
|
console.error(...formattedMessage);
|
|
193
208
|
this.writeToFile(formattedMessage);
|
|
@@ -202,7 +217,7 @@ export class Logger {
|
|
|
202
217
|
* @param statusCode 响应状态码
|
|
203
218
|
*/
|
|
204
219
|
static logApiCall(method, url, data, response, statusCode) {
|
|
205
|
-
if (this.
|
|
220
|
+
if (this.canLog(LogLevel.DEBUG)) {
|
|
206
221
|
this.debug('API调用详情:');
|
|
207
222
|
this.debug(`请求: ${method} ${url}`);
|
|
208
223
|
// 简化请求数据记录
|
|
@@ -235,7 +250,7 @@ export class Logger {
|
|
|
235
250
|
this.debug('响应数据: None');
|
|
236
251
|
}
|
|
237
252
|
}
|
|
238
|
-
else {
|
|
253
|
+
else if (this.canLog(LogLevel.INFO)) {
|
|
239
254
|
this.info(`API调用: ${method} ${url} - 状态码: ${statusCode}`);
|
|
240
255
|
}
|
|
241
256
|
}
|
|
@@ -245,6 +260,7 @@ Object.defineProperty(Logger, "config", {
|
|
|
245
260
|
configurable: true,
|
|
246
261
|
writable: true,
|
|
247
262
|
value: {
|
|
263
|
+
enabled: true, // 默认开启日志
|
|
248
264
|
minLevel: LogLevel.DEBUG, // 修改为DEBUG级别,确保捕获所有日志
|
|
249
265
|
showTimestamp: true,
|
|
250
266
|
showLevel: true,
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "feishu-mcp",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Model Context Protocol server for Feishu integration",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"feishu-mcp": "./dist/cli.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc && tsc-alias",
|
|
16
|
-
"type-check": "tsc --noEmit",
|
|
17
|
-
"start": "node dist/index.js",
|
|
18
|
-
"start:cli": "cross-env NODE_ENV=cli node dist/index.js",
|
|
19
|
-
"start:http": "node dist/index.js",
|
|
20
|
-
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
|
|
21
|
-
"dev:cli": "cross-env NODE_ENV=development tsx watch src/index.ts --stdio",
|
|
22
|
-
"lint": "eslint . --ext .ts",
|
|
23
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
24
|
-
"inspect": "pnpx @modelcontextprotocol/inspector",
|
|
25
|
-
"prepare": "pnpm run build",
|
|
26
|
-
"pub:release": "pnpm build && npm publish"
|
|
27
|
-
},
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": "^20.17.0"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "https://github.com/cso1z/Feishu-MCP.git"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"feishu",
|
|
37
|
-
"lark",
|
|
38
|
-
"mcp",
|
|
39
|
-
"typescript"
|
|
40
|
-
],
|
|
41
|
-
"author": "cso1z",
|
|
42
|
-
"license": "MIT",
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
45
|
-
"@types/yargs": "^17.0.33",
|
|
46
|
-
"axios": "^1.7.9",
|
|
47
|
-
"cross-env": "^7.0.3",
|
|
48
|
-
"dotenv": "^16.4.7",
|
|
49
|
-
"express": "^4.21.2",
|
|
50
|
-
"remeda": "^2.20.1",
|
|
51
|
-
"yargs": "^17.7.2",
|
|
52
|
-
"zod": "^3.24.2"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@types/express": "^5.0.0",
|
|
56
|
-
"@types/jest": "^29.5.11",
|
|
57
|
-
"@types/node": "^20.17.0",
|
|
58
|
-
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
59
|
-
"@typescript-eslint/parser": "^8.24.0",
|
|
60
|
-
"eslint": "^9.20.1",
|
|
61
|
-
"eslint-config-prettier": "^10.0.1",
|
|
62
|
-
"jest": "^29.7.0",
|
|
63
|
-
"prettier": "^3.5.0",
|
|
64
|
-
"ts-jest": "^29.2.5",
|
|
65
|
-
"tsc-alias": "^1.8.10",
|
|
66
|
-
"tsx": "^4.19.2",
|
|
67
|
-
"typescript": "^5.7.3"
|
|
68
|
-
},
|
|
69
|
-
"pnpm": {
|
|
70
|
-
"overrides": {
|
|
71
|
-
"feishu-mcp": "link:"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "feishu-mcp",
|
|
3
|
+
"version": "0.0.9-alpha",
|
|
4
|
+
"description": "Model Context Protocol server for Feishu integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"feishu-mcp": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && tsc-alias",
|
|
16
|
+
"type-check": "tsc --noEmit",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"start:cli": "cross-env NODE_ENV=cli node dist/index.js",
|
|
19
|
+
"start:http": "node dist/index.js",
|
|
20
|
+
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
|
|
21
|
+
"dev:cli": "cross-env NODE_ENV=development tsx watch src/index.ts --stdio",
|
|
22
|
+
"lint": "eslint . --ext .ts",
|
|
23
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
24
|
+
"inspect": "pnpx @modelcontextprotocol/inspector",
|
|
25
|
+
"prepare": "pnpm run build",
|
|
26
|
+
"pub:release": "pnpm build && npm publish"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": "^20.17.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/cso1z/Feishu-MCP.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"feishu",
|
|
37
|
+
"lark",
|
|
38
|
+
"mcp",
|
|
39
|
+
"typescript"
|
|
40
|
+
],
|
|
41
|
+
"author": "cso1z",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
45
|
+
"@types/yargs": "^17.0.33",
|
|
46
|
+
"axios": "^1.7.9",
|
|
47
|
+
"cross-env": "^7.0.3",
|
|
48
|
+
"dotenv": "^16.4.7",
|
|
49
|
+
"express": "^4.21.2",
|
|
50
|
+
"remeda": "^2.20.1",
|
|
51
|
+
"yargs": "^17.7.2",
|
|
52
|
+
"zod": "^3.24.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/express": "^5.0.0",
|
|
56
|
+
"@types/jest": "^29.5.11",
|
|
57
|
+
"@types/node": "^20.17.0",
|
|
58
|
+
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
59
|
+
"@typescript-eslint/parser": "^8.24.0",
|
|
60
|
+
"eslint": "^9.20.1",
|
|
61
|
+
"eslint-config-prettier": "^10.0.1",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"prettier": "^3.5.0",
|
|
64
|
+
"ts-jest": "^29.2.5",
|
|
65
|
+
"tsc-alias": "^1.8.10",
|
|
66
|
+
"tsx": "^4.19.2",
|
|
67
|
+
"typescript": "^5.7.3"
|
|
68
|
+
},
|
|
69
|
+
"pnpm": {
|
|
70
|
+
"overrides": {
|
|
71
|
+
"feishu-mcp": "link:"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated 这个文件已被弃用,所有功能已迁移到BlockFactory类。
|
|
3
|
-
* 请使用BlockFactory创建各种块内容。
|
|
4
|
-
* 所有接口定义已迁移到blockFactory.ts文件。
|
|
5
|
-
* 此文件仅保留为历史兼容性目的,将在后续版本中移除。
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* 构建批量创建块的请求数据
|
|
9
|
-
* @param blocks 块内容数组
|
|
10
|
-
* @param index 插入位置索引
|
|
11
|
-
* @returns 请求数据对象
|
|
12
|
-
* @deprecated 请使用BlockFactory.buildCreateBlocksRequest
|
|
13
|
-
*/
|
|
14
|
-
export function buildCreateBlocksRequest(blocks, index = 0) {
|
|
15
|
-
return {
|
|
16
|
-
children: blocks,
|
|
17
|
-
index
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* 创建文本块内容
|
|
22
|
-
* @param align 对齐方式:1左对齐,2居中,3右对齐
|
|
23
|
-
* @returns 文本块内容对象
|
|
24
|
-
* @deprecated 请使用BlockFactory.createTextBlock
|
|
25
|
-
*/
|
|
26
|
-
export function createTextBlockContent(textContents, align = 1) {
|
|
27
|
-
return {
|
|
28
|
-
block_type: 2, // 2表示文本块
|
|
29
|
-
text: {
|
|
30
|
-
elements: textContents.map(content => ({
|
|
31
|
-
text_run: {
|
|
32
|
-
content: content.text,
|
|
33
|
-
text_element_style: content.style || {}
|
|
34
|
-
}
|
|
35
|
-
})),
|
|
36
|
-
style: {
|
|
37
|
-
align: align // 1 居左,2 居中,3 居右
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* 创建代码块内容
|
|
44
|
-
* @param code 代码内容
|
|
45
|
-
* @param language 语言类型代码
|
|
46
|
-
* @param wrap 是否自动换行
|
|
47
|
-
* @returns 代码块内容对象
|
|
48
|
-
* @deprecated 请使用BlockFactory.createCodeBlock
|
|
49
|
-
*/
|
|
50
|
-
export function createCodeBlockContent(code, language = 0, wrap = false) {
|
|
51
|
-
return {
|
|
52
|
-
block_type: 14, // 14表示代码块
|
|
53
|
-
code: {
|
|
54
|
-
elements: [
|
|
55
|
-
{
|
|
56
|
-
text_run: {
|
|
57
|
-
content: code,
|
|
58
|
-
text_element_style: {
|
|
59
|
-
bold: false,
|
|
60
|
-
inline_code: false,
|
|
61
|
-
italic: false,
|
|
62
|
-
strikethrough: false,
|
|
63
|
-
underline: false
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
style: {
|
|
69
|
-
language: language,
|
|
70
|
-
wrap: wrap
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 创建标题块内容
|
|
77
|
-
* @param text 标题文本
|
|
78
|
-
* @param level 标题级别(1-9)
|
|
79
|
-
* @param align 对齐方式:1左对齐,2居中,3右对齐
|
|
80
|
-
* @returns 标题块内容对象
|
|
81
|
-
* @deprecated 请使用BlockFactory.createHeadingBlock
|
|
82
|
-
*/
|
|
83
|
-
export function createHeadingBlockContent(text, level = 1, align = 1) {
|
|
84
|
-
// 确保标题级别在有效范围内(1-9)
|
|
85
|
-
const safeLevel = Math.max(1, Math.min(9, level));
|
|
86
|
-
// 根据标题级别设置block_type和对应的属性名
|
|
87
|
-
// 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
|
|
88
|
-
const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
|
|
89
|
-
const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
|
|
90
|
-
// 构建块内容
|
|
91
|
-
const blockContent = {
|
|
92
|
-
block_type: blockType
|
|
93
|
-
};
|
|
94
|
-
// 设置对应级别的标题属性
|
|
95
|
-
blockContent[headingKey] = {
|
|
96
|
-
elements: [
|
|
97
|
-
{
|
|
98
|
-
text_run: {
|
|
99
|
-
content: text,
|
|
100
|
-
text_element_style: {}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
],
|
|
104
|
-
style: {
|
|
105
|
-
align: align,
|
|
106
|
-
folded: false
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
return blockContent;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* 创建列表块内容(有序或无序)
|
|
113
|
-
* @param text 列表项文本
|
|
114
|
-
* @param isOrdered 是否为有序列表
|
|
115
|
-
* @param align 对齐方式:1左对齐,2居中,3右对齐
|
|
116
|
-
* @returns 列表块内容对象
|
|
117
|
-
* @deprecated 请使用BlockFactory.createListBlock
|
|
118
|
-
*/
|
|
119
|
-
export function createListBlockContent(text, isOrdered = false, align = 1) {
|
|
120
|
-
// 确保 align 值在合法范围内(1-3)
|
|
121
|
-
const safeAlign = (align === 1 || align === 2 || align === 3) ? align : 1;
|
|
122
|
-
// 有序列表是 block_type: 13,无序列表是 block_type: 12
|
|
123
|
-
const blockType = isOrdered ? 13 : 12;
|
|
124
|
-
const propertyKey = isOrdered ? "ordered" : "bullet";
|
|
125
|
-
// 构建块内容
|
|
126
|
-
const blockContent = {
|
|
127
|
-
block_type: blockType
|
|
128
|
-
};
|
|
129
|
-
// 设置列表属性
|
|
130
|
-
blockContent[propertyKey] = {
|
|
131
|
-
elements: [
|
|
132
|
-
{
|
|
133
|
-
text_run: {
|
|
134
|
-
content: text,
|
|
135
|
-
text_element_style: {}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
],
|
|
139
|
-
style: {
|
|
140
|
-
align: safeAlign,
|
|
141
|
-
folded: false
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
return blockContent;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* 处理Markdown语法转换
|
|
148
|
-
* @param textContents 文本内容数组
|
|
149
|
-
* @returns 处理后的文本内容数组
|
|
150
|
-
* @deprecated 应当避免使用Markdown语法,请直接使用TextElementStyle设置样式
|
|
151
|
-
*/
|
|
152
|
-
export function processMarkdownSyntax(textContents) {
|
|
153
|
-
return textContents.map(content => {
|
|
154
|
-
let { text, style = {} } = content;
|
|
155
|
-
// 创建一个新的style对象,避免修改原始对象
|
|
156
|
-
const newStyle = { ...style };
|
|
157
|
-
// 处理粗体 **text**
|
|
158
|
-
if (text.match(/\*\*([^*]+)\*\*/g)) {
|
|
159
|
-
text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
|
|
160
|
-
newStyle.bold = true;
|
|
161
|
-
}
|
|
162
|
-
// 处理斜体 *text*
|
|
163
|
-
if (text.match(/(?<!\*)\*([^*]+)\*(?!\*)/g)) {
|
|
164
|
-
text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
165
|
-
newStyle.italic = true;
|
|
166
|
-
}
|
|
167
|
-
// 处理删除线 ~~text~~
|
|
168
|
-
if (text.match(/~~([^~]+)~~/g)) {
|
|
169
|
-
text = text.replace(/~~([^~]+)~~/g, "$1");
|
|
170
|
-
newStyle.strikethrough = true;
|
|
171
|
-
}
|
|
172
|
-
// 处理行内代码 `code`
|
|
173
|
-
if (text.match(/`([^`]+)`/g)) {
|
|
174
|
-
text = text.replace(/`([^`]+)`/g, "$1");
|
|
175
|
-
newStyle.inline_code = true;
|
|
176
|
-
}
|
|
177
|
-
return { text, style: newStyle };
|
|
178
|
-
});
|
|
179
|
-
}
|