feishu-mcp 0.1.6 → 0.1.8
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 +73 -33
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/mcp/tools/feishuBlockTools.js +163 -35
- package/dist/mcp/tools/feishuFolderTools.js +95 -30
- package/dist/mcp/tools/feishuTools.js +84 -30
- package/dist/services/baseService.js +13 -2
- package/dist/services/blockFactory.js +17 -0
- package/dist/services/feishuApiService.js +738 -65
- package/dist/types/feishuSchema.js +60 -5
- package/dist/utils/auth/tokenCacheManager.js +112 -0
- package/dist/utils/error.js +24 -0
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
//
|
|
2
|
+
// 文档类型枚举(用于 get_feishu_document_info)
|
|
3
|
+
export const DocumentTypeSchema = z.enum(['document', 'wiki']).optional().describe('Document type (optional). "document" for regular document, "wiki" for Wiki document.');
|
|
4
|
+
// 文档ID或URL参数定义(仅支持普通文档)
|
|
3
5
|
export const DocumentIdSchema = z.string().describe('Document ID or URL (required). Supports the following formats:\n' +
|
|
4
6
|
'1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n' +
|
|
5
|
-
'2. Direct document ID: e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf
|
|
6
|
-
|
|
7
|
+
'2. Direct document ID: e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf');
|
|
8
|
+
// 文档ID或Wiki ID参数定义(用于 get_feishu_document_info,支持普通文档和Wiki文档)
|
|
9
|
+
export const DocumentIdOrWikiIdSchema = z.string().describe('Document ID, URL, or Wiki ID/URL (required). Supports regular document formats (https://xxx.feishu.cn/docx/xxx or direct ID) and Wiki formats (https://xxx.feishu.cn/wiki/xxxxx or Wiki token).');
|
|
7
10
|
// 父块ID参数定义
|
|
8
11
|
export const ParentBlockIdSchema = z.string().describe('Parent block ID (required). Target block ID where content will be added, without any URL prefix. ' +
|
|
9
12
|
'For page-level (root level) insertion, extract and use only the document ID portion (not the full URL) as parentBlockId. ' +
|
|
@@ -103,9 +106,10 @@ export const ListBlockSchema = z.object({
|
|
|
103
106
|
align: AlignSchemaWithValidation,
|
|
104
107
|
});
|
|
105
108
|
// 块类型枚举 - 用于批量创建块工具
|
|
106
|
-
export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image','mermaid',as well as 'heading1' through 'heading9'. " +
|
|
109
|
+
export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image','mermaid','whiteboard',as well as 'heading1' through 'heading9'. " +
|
|
107
110
|
"For headings, we recommend using 'heading' with level property, but 'heading1'-'heading9' are also supported. " +
|
|
108
111
|
"For images, use 'image' to create empty image blocks that can be filled later. " +
|
|
112
|
+
"For whiteboards, use 'whiteboard' to create empty whiteboard blocks that return a token for filling content. " +
|
|
109
113
|
"For text blocks, you can include both regular text and equation elements in the same block.");
|
|
110
114
|
// 图片宽度参数定义
|
|
111
115
|
export const ImageWidthSchema = z.number().optional().describe('Image width in pixels (optional). If not provided, the original image width will be used.');
|
|
@@ -124,6 +128,12 @@ export const MermaidCodeSchema = z.string().describe('Mermaid code (required). T
|
|
|
124
128
|
export const MermaidBlockSchema = z.object({
|
|
125
129
|
code: MermaidCodeSchema,
|
|
126
130
|
});
|
|
131
|
+
// 画板对齐方式参数定义
|
|
132
|
+
export const WhiteboardAlignSchema = z.number().optional().default(2).describe('Whiteboard alignment: 1 for left, 2 for center (default), 3 for right.');
|
|
133
|
+
// 画板块内容定义 - 用于批量创建块工具
|
|
134
|
+
export const WhiteboardBlockSchema = z.object({
|
|
135
|
+
align: WhiteboardAlignSchema,
|
|
136
|
+
});
|
|
127
137
|
// 块配置定义 - 用于批量创建块工具
|
|
128
138
|
export const BlockConfigSchema = z.object({
|
|
129
139
|
blockType: BlockTypeEnum,
|
|
@@ -134,6 +144,7 @@ export const BlockConfigSchema = z.object({
|
|
|
134
144
|
z.object({ list: ListBlockSchema }).describe("List block options. Used when blockType is 'list'."),
|
|
135
145
|
z.object({ image: ImageBlockSchema }).describe("Image block options. Used when blockType is 'image'. Creates empty image blocks."),
|
|
136
146
|
z.object({ mermaid: MermaidBlockSchema }).describe("Mermaid block options. Used when blockType is 'mermaid'."),
|
|
147
|
+
z.object({ whiteboard: WhiteboardBlockSchema }).describe("Whiteboard block options. Used when blockType is 'whiteboard'. Creates empty whiteboard blocks that return a token for filling content."),
|
|
137
148
|
z.record(z.any()).describe("Fallback for any other block options")
|
|
138
149
|
]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
|
|
139
150
|
});
|
|
@@ -171,13 +182,36 @@ export const MediaIdSchema = z.string().describe('Media ID (required). The uniqu
|
|
|
171
182
|
// 额外参数定义 - 用于媒体资源下载
|
|
172
183
|
export const MediaExtraSchema = z.string().optional().describe('Extra parameters for media download (optional). ' +
|
|
173
184
|
'These parameters are passed directly to the Feishu API and can modify how the media is returned.');
|
|
174
|
-
// 文件夹Token
|
|
185
|
+
// 文件夹Token参数定义(必传)
|
|
175
186
|
export const FolderTokenSchema = z.string().describe('Folder token (required). The unique identifier for a folder in Feishu. ' +
|
|
176
187
|
'Format is an alphanumeric string like "FWK2fMleClICfodlHHWc4Mygnhb".');
|
|
188
|
+
// 文件夹Token参数定义(可选,用于文档创建、获取文件列表等场景)
|
|
189
|
+
export const FolderTokenOptionalSchema = z.string().optional().describe('Folder token (optional, for Feishu Drive folder mode). The unique identifier for a folder in Feishu Drive. ' +
|
|
190
|
+
'Format is an alphanumeric string like "FWK2fMleClICfodlHHWc4Mygnhb". ');
|
|
177
191
|
// 文件夹名称参数定义
|
|
178
192
|
export const FolderNameSchema = z.string().describe('Folder name (required). The name for the new folder to be created.');
|
|
193
|
+
// 知识空间ID参数定义
|
|
194
|
+
export const SpaceIdSchema = z.string().describe('Space ID (optional, required for wiki space mode). The unique identifier for a wiki space in Feishu. ' +
|
|
195
|
+
'Can be obtained from get_feishu_root_folder_info (wiki_spaces array or my_library.space_id). ' +
|
|
196
|
+
'Format is typically like "74812***88644".');
|
|
197
|
+
// 父节点Token参数定义
|
|
198
|
+
export const ParentNodeTokenSchema = z.string().optional().describe('Parent node token (optional, used with spaceId). The token of the parent node in a wiki space. ' +
|
|
199
|
+
'If not provided or empty, will retrieve nodes from the root of the wiki space. ' +
|
|
200
|
+
'Format is typically like "PdDWwIHD6****MhcIOY7npg".');
|
|
201
|
+
// 知识库节点上下文参数定义(包装 spaceId 和 parentNodeToken)
|
|
202
|
+
export const WikiSpaceNodeContextSchema = z.object({
|
|
203
|
+
spaceId: SpaceIdSchema.optional(),
|
|
204
|
+
parentNodeToken: ParentNodeTokenSchema,
|
|
205
|
+
}).optional().describe('Wiki space node context object. Contains spaceId (required when using this object) and optional parentNodeToken. ' +
|
|
206
|
+
'Used for wiki space operations instead of folderToken.');
|
|
179
207
|
// 搜索关键字参数定义
|
|
180
208
|
export const SearchKeySchema = z.string().describe('Search keyword (required). The keyword to search for in documents.');
|
|
209
|
+
// 搜索类型枚举
|
|
210
|
+
export const SearchTypeSchema = z.enum(['document', 'wiki', 'both']).optional().default('both').describe('Search type (optional, default: "both"). "document": only documents, "wiki": only wiki nodes, "both": both (default)');
|
|
211
|
+
// 知识库分页token参数定义
|
|
212
|
+
export const PageTokenSchema = z.string().optional().describe('Wiki page token (optional). Token from previous wiki search result for pagination. Only needed when fetching next page of wiki results.');
|
|
213
|
+
// 文档分页偏移量参数定义
|
|
214
|
+
export const OffsetSchema = z.number().optional().describe('Document offset (optional). Offset for document search pagination. Only needed when fetching next page of document results.');
|
|
181
215
|
// 图片路径或URL参数定义
|
|
182
216
|
export const ImagePathOrUrlSchema = z.string().describe('Image path or URL (required). Supports the following formats:\n' +
|
|
183
217
|
'1. Local file absolute path: e.g., "C:\\path\\to\\image.jpg"\n' +
|
|
@@ -196,5 +230,26 @@ export const ImagesArraySchema = z.array(z.object({
|
|
|
196
230
|
export const WhiteboardIdSchema = z.string().describe('Whiteboard ID (required). This is the token value from the board.token field when getting document blocks.\n' +
|
|
197
231
|
'When you find a block with block_type: 43, the whiteboard ID is located in board.token field.\n' +
|
|
198
232
|
'Example: "EPJKwvY5ghe3pVbKj9RcT2msnBX"');
|
|
233
|
+
// 画板代码参数定义(支持 PlantUML 和 Mermaid)
|
|
234
|
+
export const WhiteboardCodeSchema = z.string().describe('Diagram code (required). The complete diagram code to create in the whiteboard.\n' +
|
|
235
|
+
'Supports both PlantUML and Mermaid formats.\n' +
|
|
236
|
+
'PlantUML example: "@startuml\nAlice -> Bob: Hello\n@enduml"\n' +
|
|
237
|
+
'Mermaid example: "graph TD\nA[Start] --> B[End]"');
|
|
238
|
+
// 语法类型参数定义
|
|
239
|
+
export const SyntaxTypeSchema = z.number().describe('Syntax type (required). Specifies the diagram syntax format.\n' +
|
|
240
|
+
'1: PlantUML syntax\n' +
|
|
241
|
+
'2: Mermaid syntax');
|
|
242
|
+
// 画板内容配置定义(包含画板ID和内容配置)
|
|
243
|
+
export const WhiteboardContentSchema = z.object({
|
|
244
|
+
whiteboardId: WhiteboardIdSchema,
|
|
245
|
+
code: WhiteboardCodeSchema,
|
|
246
|
+
syntax_type: SyntaxTypeSchema,
|
|
247
|
+
}).describe('Whiteboard content configuration. Contains the whiteboard ID, diagram code and syntax type.\n' +
|
|
248
|
+
'whiteboardId: The token value from board.token field when creating whiteboard block (required)\n' +
|
|
249
|
+
'code: The diagram code (PlantUML or Mermaid format) (required)\n' +
|
|
250
|
+
'syntax_type: 1 for PlantUML, 2 for Mermaid (required)');
|
|
251
|
+
// 批量填充画板数组定义
|
|
252
|
+
export const WhiteboardFillArraySchema = z.array(WhiteboardContentSchema).describe('Array of whiteboard fill items (required). Each item must include whiteboardId, code and syntax_type.\n' +
|
|
253
|
+
'Example: [{whiteboardId:"token1", code:"@startuml...", syntax_type:1}, {whiteboardId:"token2", code:"graph TD...", syntax_type:2}]');
|
|
199
254
|
// 文档标题参数定义
|
|
200
255
|
export const DocumentTitleSchema = z.string().describe('Document title (required). This will be displayed in the Feishu document list and document header.');
|
|
@@ -28,9 +28,16 @@ export class TokenCacheManager {
|
|
|
28
28
|
writable: true,
|
|
29
29
|
value: void 0
|
|
30
30
|
});
|
|
31
|
+
Object.defineProperty(this, "scopeVersionCacheFile", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
31
37
|
this.cache = new Map();
|
|
32
38
|
this.userTokenCacheFile = path.resolve(process.cwd(), 'user_token_cache.json');
|
|
33
39
|
this.tenantTokenCacheFile = path.resolve(process.cwd(), 'tenant_token_cache.json');
|
|
40
|
+
this.scopeVersionCacheFile = path.resolve(process.cwd(), 'scope_version_cache.json');
|
|
34
41
|
this.loadTokenCaches();
|
|
35
42
|
this.startCacheCleanupTimer();
|
|
36
43
|
}
|
|
@@ -49,6 +56,7 @@ export class TokenCacheManager {
|
|
|
49
56
|
loadTokenCaches() {
|
|
50
57
|
this.loadUserTokenCache();
|
|
51
58
|
this.loadTenantTokenCache();
|
|
59
|
+
this.loadScopeVersionCache();
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* 加载用户token缓存
|
|
@@ -433,4 +441,108 @@ export class TokenCacheManager {
|
|
|
433
441
|
Logger.debug(`获取到 ${keys.length} 个用户token keys`);
|
|
434
442
|
return keys;
|
|
435
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* 获取scope版本信息
|
|
446
|
+
* @param clientKey 客户端缓存键
|
|
447
|
+
* @returns scope版本信息,如果未找到则返回null
|
|
448
|
+
*/
|
|
449
|
+
getScopeVersionInfo(clientKey) {
|
|
450
|
+
const cacheKey = `scope_version:${clientKey}`;
|
|
451
|
+
const cacheItem = this.cache.get(cacheKey);
|
|
452
|
+
if (!cacheItem) {
|
|
453
|
+
Logger.debug(`Scope版本信息未找到: ${clientKey}`);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
Logger.debug(`获取Scope版本信息成功: ${clientKey}`);
|
|
457
|
+
return cacheItem.data;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 保存scope版本信息
|
|
461
|
+
* @param clientKey 客户端缓存键
|
|
462
|
+
* @param scopeVersionInfo scope版本信息
|
|
463
|
+
* @returns 是否成功保存
|
|
464
|
+
*/
|
|
465
|
+
saveScopeVersionInfo(clientKey, scopeVersionInfo) {
|
|
466
|
+
try {
|
|
467
|
+
const cacheKey = `scope_version:${clientKey}`;
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
// scope版本信息永久有效,不设置过期时间
|
|
470
|
+
const cacheItem = {
|
|
471
|
+
data: scopeVersionInfo,
|
|
472
|
+
timestamp: now,
|
|
473
|
+
expiresAt: Number.MAX_SAFE_INTEGER // 永久有效
|
|
474
|
+
};
|
|
475
|
+
this.cache.set(cacheKey, cacheItem);
|
|
476
|
+
this.saveScopeVersionCache();
|
|
477
|
+
Logger.debug(`Scope版本信息保存成功: ${clientKey}, 版本: ${scopeVersionInfo.scopeVersion}`);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
Logger.error(`保存Scope版本信息失败: ${clientKey}`, error);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* 检查scope版本是否需要校验
|
|
487
|
+
* @param clientKey 客户端缓存键
|
|
488
|
+
* @param currentScopeVersion 当前scope版本号
|
|
489
|
+
* @returns 是否需要校验
|
|
490
|
+
*/
|
|
491
|
+
shouldValidateScope(clientKey, currentScopeVersion) {
|
|
492
|
+
const scopeVersionInfo = this.getScopeVersionInfo(clientKey);
|
|
493
|
+
if (!scopeVersionInfo) {
|
|
494
|
+
Logger.debug(`Scope版本信息不存在,需要校验: ${clientKey}`);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
// 如果版本号不同,需要重新校验
|
|
498
|
+
if (scopeVersionInfo.validatedVersion !== currentScopeVersion) {
|
|
499
|
+
Logger.debug(`Scope版本号已更新,需要重新校验: ${clientKey}, 旧版本: ${scopeVersionInfo.validatedVersion}, 新版本: ${currentScopeVersion}`);
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
Logger.debug(`Scope版本已校验过,无需重复校验: ${clientKey}, 版本: ${currentScopeVersion}`);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 加载scope版本缓存
|
|
507
|
+
*/
|
|
508
|
+
loadScopeVersionCache() {
|
|
509
|
+
if (fs.existsSync(this.scopeVersionCacheFile)) {
|
|
510
|
+
try {
|
|
511
|
+
const raw = fs.readFileSync(this.scopeVersionCacheFile, 'utf-8');
|
|
512
|
+
const cacheData = JSON.parse(raw);
|
|
513
|
+
let loadedCount = 0;
|
|
514
|
+
for (const key in cacheData) {
|
|
515
|
+
if (key.startsWith('scope_version:')) {
|
|
516
|
+
this.cache.set(key, cacheData[key]);
|
|
517
|
+
loadedCount++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
Logger.info(`已加载Scope版本缓存,共 ${loadedCount} 条记录`);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
Logger.warn('加载Scope版本缓存失败:', error);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
Logger.info('Scope版本缓存文件不存在,将创建新的缓存');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 保存scope版本缓存到文件
|
|
532
|
+
*/
|
|
533
|
+
saveScopeVersionCache() {
|
|
534
|
+
const cacheData = {};
|
|
535
|
+
for (const [key, value] of this.cache.entries()) {
|
|
536
|
+
if (key.startsWith('scope_version:')) {
|
|
537
|
+
cacheData[key] = value;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
fs.writeFileSync(this.scopeVersionCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
542
|
+
Logger.debug('Scope版本缓存已保存到文件');
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
Logger.warn('保存Scope版本缓存失败:', error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
436
548
|
}
|
package/dist/utils/error.js
CHANGED
|
@@ -206,3 +206,27 @@ export class AuthRequiredError extends Error {
|
|
|
206
206
|
this.message = message;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* 权限不足异常类
|
|
211
|
+
* 用于处理应用权限范围不足的情况
|
|
212
|
+
*/
|
|
213
|
+
export class ScopeInsufficientError extends Error {
|
|
214
|
+
constructor(missingScopes, message) {
|
|
215
|
+
super(message);
|
|
216
|
+
Object.defineProperty(this, "missingScopes", {
|
|
217
|
+
enumerable: true,
|
|
218
|
+
configurable: true,
|
|
219
|
+
writable: true,
|
|
220
|
+
value: void 0
|
|
221
|
+
});
|
|
222
|
+
Object.defineProperty(this, "message", {
|
|
223
|
+
enumerable: true,
|
|
224
|
+
configurable: true,
|
|
225
|
+
writable: true,
|
|
226
|
+
value: void 0
|
|
227
|
+
});
|
|
228
|
+
this.name = 'ScopeInsufficientError';
|
|
229
|
+
this.missingScopes = missingScopes;
|
|
230
|
+
this.message = message;
|
|
231
|
+
}
|
|
232
|
+
}
|