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.
- package/LICENSE +21 -21
- package/README.md +356 -266
- package/dist/cli/auth.js +117 -0
- package/dist/cli/commands/auth.js +141 -0
- package/dist/cli/commands/config.js +86 -0
- package/dist/cli/commands/guide.js +68 -0
- package/dist/cli/dispatcher.js +96 -0
- package/dist/cli/index.js +95 -0
- package/dist/manager/sseConnectionManager.js +2 -0
- package/dist/mcp/feishuMcp.js +25 -25
- package/dist/mcp/tools/blockTools.js +295 -0
- package/dist/mcp/tools/documentTools.js +105 -0
- package/dist/mcp/tools/feishuBlockTools.js +1 -285
- package/dist/mcp/tools/feishuTools.js +1 -67
- package/dist/mcp/tools/folderTools.js +99 -0
- package/dist/mcp/tools/toolHelpers.js +155 -0
- package/dist/modules/FeatureModule.js +1 -0
- package/dist/modules/ModuleRegistry.js +63 -0
- package/dist/modules/calendar/index.js +11 -0
- package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
- package/dist/modules/calendar/tools/calendarTools.js +6 -0
- package/dist/modules/document/index.js +15 -0
- package/dist/modules/document/services/FeishuBlockService.js +410 -0
- package/dist/modules/document/services/FeishuDocumentService.js +110 -0
- package/dist/modules/document/services/FeishuFoldService.js +187 -0
- package/dist/modules/document/services/FeishuSearchService.js +232 -0
- package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
- package/dist/modules/document/services/blockFactory.js +521 -0
- package/dist/modules/document/toolApi/blockToolApi.js +160 -0
- package/dist/modules/document/toolApi/documentToolApi.js +65 -0
- package/dist/modules/document/toolApi/folderToolApi.js +73 -0
- package/dist/modules/document/toolApi/index.js +3 -0
- package/dist/modules/document/tools/blockTools.js +138 -0
- package/dist/modules/document/tools/documentTools.js +64 -0
- package/dist/modules/document/tools/folderTools.js +46 -0
- package/dist/modules/document/tools/toolHelpers.js +155 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/member/index.js +11 -0
- package/dist/modules/member/services/FeishuMemberService.js +41 -0
- package/dist/modules/member/toolApi/index.js +1 -0
- package/dist/modules/member/toolApi/memberToolApi.js +54 -0
- package/dist/modules/member/tools/memberTools.js +26 -0
- package/dist/modules/task/index.js +11 -0
- package/dist/modules/task/services/FeishuTaskService.js +271 -0
- package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
- package/dist/modules/task/toolApi/index.js +1 -0
- package/dist/modules/task/toolApi/taskToolApi.js +128 -0
- package/dist/modules/task/tools/taskTools.js +93 -0
- package/dist/server.js +43 -24
- package/dist/services/baseService.js +11 -2
- package/dist/services/blockFactory.js +167 -0
- package/dist/services/callbackService.js +1 -1
- package/dist/services/constants/feishuScopes.js +94 -0
- package/dist/services/feishu/FeishuBaseApiService.js +47 -0
- package/dist/services/feishu/FeishuBlockService.js +410 -0
- package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
- package/dist/services/feishu/FeishuDocumentService.js +110 -0
- package/dist/services/feishu/FeishuDriveService.js +62 -0
- package/dist/services/feishu/FeishuFoldService.js +187 -0
- package/dist/services/feishu/FeishuImageService.js +219 -0
- package/dist/services/feishu/FeishuScopeValidator.js +177 -0
- package/dist/services/feishu/FeishuSearchService.js +232 -0
- package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
- package/dist/services/feishu/FeishuWikiService.js +134 -0
- package/dist/services/feishuApiService.js +246 -1760
- package/dist/services/feishuAuthService.js +43 -0
- package/dist/types/documentSchema.js +232 -0
- package/dist/types/feishuSchema.js +54 -77
- package/dist/types/memberSchema.js +35 -0
- package/dist/types/taskSchema.js +166 -0
- package/dist/utils/auth/tokenCacheManager.js +16 -3
- package/dist/utils/auth/tokenRefreshManager.js +2 -1
- package/dist/utils/config.js +47 -0
- package/dist/utils/document.js +116 -116
- package/dist/utils/error.js +0 -11
- package/dist/utils/paramUtils.js +0 -31
- package/package.json +77 -76
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { formatErrorMessage } from '../../../utils/error.js';
|
|
2
|
+
import { Logger } from '../../../utils/logger.js';
|
|
3
|
+
import { errorResponse } from '../../document/tools/toolHelpers.js';
|
|
4
|
+
import { createTasks, listTasks, updateTask, deleteTasks } from '../toolApi/index.js';
|
|
5
|
+
import { TaskGuidSchema, TaskSummarySchema, TaskDescriptionSchema, TaskTimestampSchema, TaskIsAllDaySchema, TaskRepeatRuleSchema, TaskCompletedAtSchema, TaskModeSchema, TaskIsMilestoneSchema, TaskCreateBatchSchema, TaskAssigneeIdsSchema, TaskFollowerIdsSchema, TaskReminderRelativeMinutesSchema, TaskReminderIdsSchema, TaskListPageTokenSchema, TaskListCompletedSchema, TaskDeleteBatchSchema, } from '../../../types/taskSchema.js';
|
|
6
|
+
/**
|
|
7
|
+
* Registers Feishu task MCP tools.
|
|
8
|
+
*/
|
|
9
|
+
export function registerTaskTools(server, feishuService) {
|
|
10
|
+
server.tool('list_feishu_tasks', 'Lists tasks assigned to the current user ("我负责的"). Returns up to 100 items per call (2 pages of 50). Optional pageToken for next page, optional completed (true=done only, false=todo only). Response includes slimmed task fields: guid, summary, description, due, reminders, creator, members, status, task_id, url, etc. Requires user_access_token.', {
|
|
11
|
+
pageToken: TaskListPageTokenSchema,
|
|
12
|
+
completed: TaskListCompletedSchema,
|
|
13
|
+
}, async ({ pageToken, completed }) => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await listTasks({ pageToken, completed }, feishuService);
|
|
16
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
Logger.error('列取任务失败:', error);
|
|
20
|
+
return errorResponse(`列取任务失败: ${formatErrorMessage(error)}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
server.tool('create_feishu_task', 'Batch creates Feishu tasks or nested subtasks. Pass an array of task items; each item has summary (required), optional description/due/members/repeat_rule/start/mode/is_milestone, optional parentTaskGuid (create under existing task), and optional subTasks (nested subtasks, multi-level). Use open_id from get_feishu_users for assignees. Min 1, max 50 top-level items; max 50 subTasks per level. Returns nested results and path-indexed errors.', {
|
|
24
|
+
tasks: TaskCreateBatchSchema,
|
|
25
|
+
}, async ({ tasks: taskItems }) => {
|
|
26
|
+
try {
|
|
27
|
+
const result = await createTasks(taskItems, feishuService);
|
|
28
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
Logger.error('批量创建任务失败:', error);
|
|
32
|
+
return errorResponse(`批量创建任务失败: ${formatErrorMessage(error)}`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
server.tool('update_feishu_task', 'Updates an existing Feishu task. Provide task_guid and the fields to change. Supports summary, description, due, completed_at (use "0" to restore to unfinished), repeat_rule, start, mode, is_milestone. Also supports addAssigneeIds/addFollowerIds and removeAssigneeIds/removeFollowerIds for members; addReminderRelativeMinutes to add a reminder (task must have due; only one per task—remove first if needed) and removeReminderIds (from task.reminders[].id) to remove reminders. At least one update is required.', {
|
|
36
|
+
taskGuid: TaskGuidSchema,
|
|
37
|
+
summary: TaskSummarySchema.optional(),
|
|
38
|
+
description: TaskDescriptionSchema,
|
|
39
|
+
dueTimestamp: TaskTimestampSchema,
|
|
40
|
+
isDueAllDay: TaskIsAllDaySchema,
|
|
41
|
+
completedAt: TaskCompletedAtSchema,
|
|
42
|
+
repeatRule: TaskRepeatRuleSchema,
|
|
43
|
+
startTimestamp: TaskTimestampSchema,
|
|
44
|
+
isStartAllDay: TaskIsAllDaySchema,
|
|
45
|
+
mode: TaskModeSchema,
|
|
46
|
+
isMilestone: TaskIsMilestoneSchema,
|
|
47
|
+
addAssigneeIds: TaskAssigneeIdsSchema,
|
|
48
|
+
addFollowerIds: TaskFollowerIdsSchema,
|
|
49
|
+
removeAssigneeIds: TaskAssigneeIdsSchema,
|
|
50
|
+
removeFollowerIds: TaskFollowerIdsSchema,
|
|
51
|
+
addReminderRelativeMinutes: TaskReminderRelativeMinutesSchema,
|
|
52
|
+
removeReminderIds: TaskReminderIdsSchema,
|
|
53
|
+
}, async ({ taskGuid, summary, description, dueTimestamp, isDueAllDay, completedAt, repeatRule, startTimestamp, isStartAllDay, mode, isMilestone, addAssigneeIds, addFollowerIds, removeAssigneeIds, removeFollowerIds, addReminderRelativeMinutes, removeReminderIds, }) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await updateTask({
|
|
56
|
+
taskGuid,
|
|
57
|
+
summary,
|
|
58
|
+
description,
|
|
59
|
+
dueTimestamp,
|
|
60
|
+
isDueAllDay,
|
|
61
|
+
completedAt,
|
|
62
|
+
repeatRule,
|
|
63
|
+
startTimestamp,
|
|
64
|
+
isStartAllDay,
|
|
65
|
+
mode,
|
|
66
|
+
isMilestone,
|
|
67
|
+
addAssigneeIds,
|
|
68
|
+
addFollowerIds,
|
|
69
|
+
removeAssigneeIds,
|
|
70
|
+
removeFollowerIds,
|
|
71
|
+
addReminderRelativeMinutes,
|
|
72
|
+
removeReminderIds,
|
|
73
|
+
}, feishuService);
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
Logger.error('更新任务失败:', error);
|
|
78
|
+
return errorResponse(`更新任务失败: ${formatErrorMessage(error)}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
server.tool('delete_feishu_task', 'Deletes one or more Feishu tasks by task_guid. Pass an array of task GUIDs (min 1, max 50). Requires edit permission on each task. Returns deleted guids and per-item errors. Deleted tasks cannot be retrieved.', {
|
|
82
|
+
taskGuids: TaskDeleteBatchSchema,
|
|
83
|
+
}, async ({ taskGuids }) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await deleteTasks(taskGuids, feishuService);
|
|
86
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
Logger.error('删除任务失败:', error);
|
|
90
|
+
return errorResponse(`删除任务失败: ${formatErrorMessage(error)}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -47,25 +47,20 @@ export class FeishuMcpServer {
|
|
|
47
47
|
async connect(transport) {
|
|
48
48
|
const server = new FeishuMcp();
|
|
49
49
|
await server.connect(transport);
|
|
50
|
+
const shutdown = () => {
|
|
51
|
+
TokenRefreshManager.getInstance().stop();
|
|
52
|
+
this.stopCallbackServer();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
};
|
|
50
55
|
// 监听 transport 关闭事件,清理 callback 服务器
|
|
51
56
|
// 对于 stdio 模式,监听 stdin 关闭事件
|
|
52
57
|
if (process.stdin && typeof process.stdin.on === 'function') {
|
|
53
|
-
process.stdin.on('close',
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
process.stdin.on('end', () => {
|
|
57
|
-
this.stopCallbackServer();
|
|
58
|
-
});
|
|
58
|
+
process.stdin.on('close', shutdown);
|
|
59
|
+
process.stdin.on('end', shutdown);
|
|
59
60
|
}
|
|
60
61
|
// 监听进程退出事件,确保清理资源
|
|
61
|
-
process.on('SIGINT',
|
|
62
|
-
|
|
63
|
-
process.exit(0);
|
|
64
|
-
});
|
|
65
|
-
process.on('SIGTERM', () => {
|
|
66
|
-
this.stopCallbackServer();
|
|
67
|
-
process.exit(0);
|
|
68
|
-
});
|
|
62
|
+
process.on('SIGINT', shutdown);
|
|
63
|
+
process.on('SIGTERM', shutdown);
|
|
69
64
|
// 注意:在 stdio 模式下,Logger 会自动禁用输出,避免污染 MCP 协议
|
|
70
65
|
// 如果需要日志,可以通过 MCP 协议的 logging 消息传递
|
|
71
66
|
Logger.info('Server connected and ready to process requests');
|
|
@@ -98,32 +93,41 @@ export class FeishuMcpServer {
|
|
|
98
93
|
});
|
|
99
94
|
// Check for existing session ID
|
|
100
95
|
const sessionId = req.headers['mcp-session-id'];
|
|
96
|
+
// 优先使用客户端显式传入的 ?userKey=,若不传则在 session 初始化时 fallback 到 sessionId
|
|
97
|
+
const queryUserKey = req.query.userKey;
|
|
101
98
|
let transport;
|
|
99
|
+
let userKey;
|
|
102
100
|
if (sessionId && transports[sessionId]) {
|
|
103
|
-
// Reuse existing transport
|
|
101
|
+
// Reuse existing transport,从 userAuthManager 取回持久化的 userKey
|
|
104
102
|
Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId);
|
|
105
103
|
transport = transports[sessionId];
|
|
104
|
+
userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId;
|
|
106
105
|
}
|
|
107
106
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
108
107
|
// New initialization request
|
|
109
108
|
transport = new StreamableHTTPServerTransport({
|
|
110
109
|
sessionIdGenerator: () => randomUUID(),
|
|
111
|
-
onsessioninitialized: (
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
onsessioninitialized: (newSessionId) => {
|
|
111
|
+
// 建立 sessionId → userKey 持久映射:优先用客户端传入的,否则用 sessionId 兜底
|
|
112
|
+
const resolvedUserKey = queryUserKey || newSessionId;
|
|
113
|
+
this.userAuthManager.createSession(newSessionId, resolvedUserKey);
|
|
114
|
+
Logger.log(`[StreamableHTTP connection] ${newSessionId}, userKey: ${resolvedUserKey}`);
|
|
115
|
+
transports[newSessionId] = transport;
|
|
115
116
|
}
|
|
116
117
|
});
|
|
117
118
|
// Clean up transport and server when closed
|
|
118
119
|
transport.onclose = () => {
|
|
119
120
|
if (transport.sessionId) {
|
|
120
|
-
Logger.log(`[StreamableHTTP delete] ${
|
|
121
|
+
Logger.log(`[StreamableHTTP delete] ${transport.sessionId}`);
|
|
122
|
+
this.userAuthManager.removeSession(transport.sessionId);
|
|
121
123
|
delete transports[transport.sessionId];
|
|
122
124
|
}
|
|
123
125
|
};
|
|
124
126
|
// Create and connect server instance
|
|
125
127
|
const server = new FeishuMcp();
|
|
126
128
|
await server.connect(transport);
|
|
129
|
+
// 初始化握手阶段 sessionId 尚未分配,使用 queryUserKey(若有)或临时占位符
|
|
130
|
+
userKey = queryUserKey || 'http-client';
|
|
127
131
|
}
|
|
128
132
|
else {
|
|
129
133
|
// Invalid request
|
|
@@ -137,8 +141,12 @@ export class FeishuMcpServer {
|
|
|
137
141
|
});
|
|
138
142
|
return;
|
|
139
143
|
}
|
|
140
|
-
//
|
|
141
|
-
|
|
144
|
+
// 获取 baseUrl 并在用户上下文中处理请求
|
|
145
|
+
const baseUrl = getBaseUrl(req);
|
|
146
|
+
// Handle the request with user context
|
|
147
|
+
await this.userContextManager.run({ userKey, baseUrl }, async () => {
|
|
148
|
+
await transport.handleRequest(req, res, req.body);
|
|
149
|
+
});
|
|
142
150
|
}
|
|
143
151
|
catch (error) {
|
|
144
152
|
Logger.error('Error handling MCP request:', error);
|
|
@@ -164,7 +172,12 @@ export class FeishuMcpServer {
|
|
|
164
172
|
return;
|
|
165
173
|
}
|
|
166
174
|
const transport = transports[sessionId];
|
|
167
|
-
|
|
175
|
+
// 获取 baseUrl 并在用户上下文中处理请求
|
|
176
|
+
const baseUrl = getBaseUrl(req);
|
|
177
|
+
const userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId;
|
|
178
|
+
await this.userContextManager.run({ userKey, baseUrl }, async () => {
|
|
179
|
+
await transport.handleRequest(req, res);
|
|
180
|
+
});
|
|
168
181
|
}
|
|
169
182
|
catch (error) {
|
|
170
183
|
Logger.error('Error handling GET request:', error);
|
|
@@ -182,9 +195,15 @@ export class FeishuMcpServer {
|
|
|
182
195
|
return;
|
|
183
196
|
}
|
|
184
197
|
const transport = transports[sessionId];
|
|
185
|
-
|
|
198
|
+
// 获取 baseUrl 并在用户上下文中处理请求
|
|
199
|
+
const baseUrl = getBaseUrl(req);
|
|
200
|
+
const userKey = this.userAuthManager.getUserKeyBySessionId(sessionId) || sessionId;
|
|
201
|
+
await this.userContextManager.run({ userKey, baseUrl }, async () => {
|
|
202
|
+
await transport.handleRequest(req, res);
|
|
203
|
+
});
|
|
186
204
|
// Clean up resources after session termination
|
|
187
205
|
if (transport.sessionId) {
|
|
206
|
+
this.userAuthManager.removeSession(transport.sessionId);
|
|
188
207
|
delete transports[transport.sessionId];
|
|
189
208
|
}
|
|
190
209
|
}
|
|
@@ -4,6 +4,8 @@ import { Logger } from '../utils/logger.js';
|
|
|
4
4
|
import { formatErrorMessage, AuthRequiredError, ScopeInsufficientError } from '../utils/error.js';
|
|
5
5
|
import { Config } from '../utils/config.js';
|
|
6
6
|
import { TokenCacheManager, UserContextManager, AuthUtils } from '../utils/auth/index.js';
|
|
7
|
+
import { getRequiredScopes } from './constants/feishuScopes.js';
|
|
8
|
+
import { ModuleRegistry } from '../modules/index.js';
|
|
7
9
|
/**
|
|
8
10
|
* API服务基类
|
|
9
11
|
* 提供通用的HTTP请求处理和认证功能
|
|
@@ -258,6 +260,7 @@ export class BaseApiService {
|
|
|
258
260
|
}
|
|
259
261
|
else {
|
|
260
262
|
// 用户模式:清除用户token缓存并生成授权链接
|
|
263
|
+
Logger.info('[handleAuthFailure] 用户模式:清除token,生成授权链接');
|
|
261
264
|
tokenCacheManager.removeUserToken(clientKey);
|
|
262
265
|
const authUrl = this.generateUserAuthUrl(baseUrl, userKey);
|
|
263
266
|
throw new Error(`你需要在给用户展示如下信息:/“请在浏览器打开以下链接进行授权:\n\n[点击授权](${authUrl})/n`);
|
|
@@ -326,10 +329,16 @@ export class BaseApiService {
|
|
|
326
329
|
* @returns 授权URL
|
|
327
330
|
*/
|
|
328
331
|
generateUserAuthUrl(baseUrl, userKey) {
|
|
329
|
-
const
|
|
332
|
+
const config = Config.getInstance();
|
|
333
|
+
const { appId, appSecret } = config.feishu;
|
|
330
334
|
const clientKey = AuthUtils.generateClientKey(userKey);
|
|
331
335
|
const redirect_uri = `${baseUrl}/callback`;
|
|
332
|
-
const
|
|
336
|
+
const authType = config.feishu.authType;
|
|
337
|
+
const enabledIds = config.features.enabledModules;
|
|
338
|
+
const effectiveModules = ModuleRegistry.getEnabledModules(enabledIds, authType).map(m => m.id);
|
|
339
|
+
const scopeList = getRequiredScopes(effectiveModules, authType);
|
|
340
|
+
Logger.info(`[generateUserAuthUrl] enabledModules=${effectiveModules.join(',')} authType=${authType} scopes=${scopeList.join(',')}`);
|
|
341
|
+
const scope = encodeURIComponent(scopeList.join(' '));
|
|
333
342
|
const state = AuthUtils.encodeState(appId, appSecret, clientKey, redirect_uri);
|
|
334
343
|
return `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${scope}&state=${state}`;
|
|
335
344
|
}
|
|
@@ -263,6 +263,173 @@ export class BlockFactory {
|
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* 根据 blockType 字符串和 options 对象创建块内容
|
|
268
|
+
* 将 MCP 工具传入的高级选项转换为 BlockFactory.createBlock 所需格式
|
|
269
|
+
*/
|
|
270
|
+
createBlockContentFromOptions(blockType, options) {
|
|
271
|
+
try {
|
|
272
|
+
// 处理特殊的 heading 格式,如 heading1, heading2 等
|
|
273
|
+
if (typeof blockType === 'string' && blockType.startsWith('heading')) {
|
|
274
|
+
const headingMatch = blockType.match(/^heading([1-9])$/);
|
|
275
|
+
if (headingMatch) {
|
|
276
|
+
const level = parseInt(headingMatch[1], 10);
|
|
277
|
+
if (level >= 1 && level <= 9) {
|
|
278
|
+
if (!options || Object.keys(options).length === 0) {
|
|
279
|
+
options = { heading: { level, content: '', align: 1 } };
|
|
280
|
+
}
|
|
281
|
+
else if (!('heading' in options)) {
|
|
282
|
+
options = { heading: { level, content: '', align: 1 } };
|
|
283
|
+
}
|
|
284
|
+
else if (options.heading && !('level' in options.heading)) {
|
|
285
|
+
options.heading.level = level;
|
|
286
|
+
}
|
|
287
|
+
blockType = BlockType.HEADING;
|
|
288
|
+
Logger.info(`转换特殊标题格式: heading${level} -> standard heading with level=${level}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const blockTypeEnum = blockType;
|
|
293
|
+
const blockConfig = {
|
|
294
|
+
type: blockTypeEnum,
|
|
295
|
+
options: {}
|
|
296
|
+
};
|
|
297
|
+
switch (blockTypeEnum) {
|
|
298
|
+
case BlockType.TEXT:
|
|
299
|
+
if ('text' in options && options.text) {
|
|
300
|
+
const textOptions = options.text;
|
|
301
|
+
const textStyles = textOptions.textStyles || [];
|
|
302
|
+
const processedTextStyles = textStyles.map((item) => {
|
|
303
|
+
if (item.equation !== undefined) {
|
|
304
|
+
return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) };
|
|
305
|
+
}
|
|
306
|
+
return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) };
|
|
307
|
+
});
|
|
308
|
+
blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 };
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case BlockType.CODE:
|
|
312
|
+
if ('code' in options && options.code) {
|
|
313
|
+
const codeOptions = options.code;
|
|
314
|
+
blockConfig.options = {
|
|
315
|
+
code: codeOptions.code || '',
|
|
316
|
+
language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
|
|
317
|
+
wrap: codeOptions.wrap || false
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case BlockType.HEADING:
|
|
322
|
+
if ('heading' in options && options.heading) {
|
|
323
|
+
const headingOptions = options.heading;
|
|
324
|
+
blockConfig.options = {
|
|
325
|
+
text: headingOptions.content || '',
|
|
326
|
+
level: headingOptions.level || 1,
|
|
327
|
+
align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
case BlockType.LIST:
|
|
332
|
+
if ('list' in options && options.list) {
|
|
333
|
+
const listOptions = options.list;
|
|
334
|
+
blockConfig.options = {
|
|
335
|
+
text: listOptions.content || '',
|
|
336
|
+
isOrdered: listOptions.isOrdered || false,
|
|
337
|
+
align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
case BlockType.IMAGE:
|
|
342
|
+
if ('image' in options && options.image) {
|
|
343
|
+
const imageOptions = options.image;
|
|
344
|
+
blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 };
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
blockConfig.options = { width: 100, height: 100 };
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
case BlockType.MERMAID:
|
|
351
|
+
if ('mermaid' in options && options.mermaid) {
|
|
352
|
+
blockConfig.options = { code: options.mermaid.code };
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
case BlockType.WHITEBOARD:
|
|
356
|
+
if ('whiteboard' in options && options.whiteboard) {
|
|
357
|
+
const whiteboardOptions = options.whiteboard;
|
|
358
|
+
blockConfig.options = {
|
|
359
|
+
align: [1, 2, 3].includes(whiteboardOptions.align) ? whiteboardOptions.align : 1
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
blockConfig.options = { align: 1 };
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
|
|
368
|
+
if ('text' in options) {
|
|
369
|
+
blockConfig.type = BlockType.TEXT;
|
|
370
|
+
const textOptions = options.text;
|
|
371
|
+
const textStyles = textOptions.textStyles || [];
|
|
372
|
+
const processedTextStyles = textStyles.map((item) => {
|
|
373
|
+
if (item.equation !== undefined) {
|
|
374
|
+
return { equation: item.equation, style: BlockFactory.applyDefaultTextStyle(item.style) };
|
|
375
|
+
}
|
|
376
|
+
return { text: item.text || '', style: BlockFactory.applyDefaultTextStyle(item.style) };
|
|
377
|
+
});
|
|
378
|
+
blockConfig.options = { textContents: processedTextStyles, align: textOptions.align || 1 };
|
|
379
|
+
}
|
|
380
|
+
else if ('code' in options) {
|
|
381
|
+
blockConfig.type = BlockType.CODE;
|
|
382
|
+
const codeOptions = options.code;
|
|
383
|
+
blockConfig.options = {
|
|
384
|
+
code: codeOptions.code || '',
|
|
385
|
+
language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
|
|
386
|
+
wrap: codeOptions.wrap || false
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
else if ('heading' in options) {
|
|
390
|
+
blockConfig.type = BlockType.HEADING;
|
|
391
|
+
const headingOptions = options.heading;
|
|
392
|
+
blockConfig.options = {
|
|
393
|
+
text: headingOptions.content || '',
|
|
394
|
+
level: headingOptions.level || 1,
|
|
395
|
+
align: [1, 2, 3].includes(headingOptions.align) ? headingOptions.align : 1
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
else if ('list' in options) {
|
|
399
|
+
blockConfig.type = BlockType.LIST;
|
|
400
|
+
const listOptions = options.list;
|
|
401
|
+
blockConfig.options = {
|
|
402
|
+
text: listOptions.content || '',
|
|
403
|
+
isOrdered: listOptions.isOrdered || false,
|
|
404
|
+
align: [1, 2, 3].includes(listOptions.align) ? listOptions.align : 1
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
else if ('image' in options) {
|
|
408
|
+
blockConfig.type = BlockType.IMAGE;
|
|
409
|
+
const imageOptions = options.image;
|
|
410
|
+
blockConfig.options = { width: imageOptions.width || 100, height: imageOptions.height || 100 };
|
|
411
|
+
}
|
|
412
|
+
else if ('mermaid' in options) {
|
|
413
|
+
blockConfig.type = BlockType.MERMAID;
|
|
414
|
+
blockConfig.options = { code: options.mermaid.code };
|
|
415
|
+
}
|
|
416
|
+
else if ('whiteboard' in options) {
|
|
417
|
+
blockConfig.type = BlockType.WHITEBOARD;
|
|
418
|
+
const whiteboardConfig = options.whiteboard;
|
|
419
|
+
blockConfig.options = {
|
|
420
|
+
align: [1, 2, 3].includes(whiteboardConfig.align) ? whiteboardConfig.align : 1
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
Logger.debug(`创建块内容: 类型=${blockConfig.type}, 选项=${JSON.stringify(blockConfig.options)}`);
|
|
426
|
+
return this.createBlock(blockConfig.type, blockConfig.options);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
Logger.error(`创建块内容对象失败: ${error}`);
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
266
433
|
/**
|
|
267
434
|
* 创建表格块
|
|
268
435
|
* @param options 表格块选项
|
|
@@ -64,7 +64,7 @@ export async function callback(req, res) {
|
|
|
64
64
|
});
|
|
65
65
|
const data = (tokenResp && typeof tokenResp === 'object') ? tokenResp : undefined;
|
|
66
66
|
Logger.debug('[callback] feishu response:', data);
|
|
67
|
-
if (!data ||
|
|
67
|
+
if (!data || !data.access_token) {
|
|
68
68
|
return sendFail(res, `获取 access_token 失败,飞书返回: ${JSON.stringify(tokenResp)}`, CODE.CUSTOM);
|
|
69
69
|
}
|
|
70
70
|
// 使用TokenCacheManager缓存token信息
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书各功能模块所需权限 Scope 定义
|
|
3
|
+
* 按模块拆分,通过 getRequiredScopes 动态计算最小权限集
|
|
4
|
+
*/
|
|
5
|
+
export const MODULE_SCOPES = {
|
|
6
|
+
document: {
|
|
7
|
+
tenant: [
|
|
8
|
+
"docx:document.block:convert",
|
|
9
|
+
"base:app:read",
|
|
10
|
+
"bitable:app",
|
|
11
|
+
"bitable:app:readonly",
|
|
12
|
+
"board:whiteboard:node:create",
|
|
13
|
+
"board:whiteboard:node:read",
|
|
14
|
+
"contact:user.employee_id:readonly",
|
|
15
|
+
"docs:document.content:read",
|
|
16
|
+
"docx:document",
|
|
17
|
+
"docx:document:create",
|
|
18
|
+
"docx:document:readonly",
|
|
19
|
+
"drive:drive",
|
|
20
|
+
"drive:drive:readonly",
|
|
21
|
+
"drive:file",
|
|
22
|
+
"drive:file:upload",
|
|
23
|
+
"sheets:spreadsheet",
|
|
24
|
+
"sheets:spreadsheet:readonly",
|
|
25
|
+
"space:document:retrieve",
|
|
26
|
+
"space:folder:create",
|
|
27
|
+
"wiki:space:read",
|
|
28
|
+
"wiki:space:retrieve",
|
|
29
|
+
"wiki:wiki",
|
|
30
|
+
"wiki:wiki:readonly",
|
|
31
|
+
],
|
|
32
|
+
userOnly: [
|
|
33
|
+
"search:docs:read",
|
|
34
|
+
"offline_access",
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
task: {
|
|
38
|
+
tenant: [
|
|
39
|
+
"task:task:write",
|
|
40
|
+
],
|
|
41
|
+
userOnly: [],
|
|
42
|
+
},
|
|
43
|
+
calendar: {
|
|
44
|
+
tenant: [],
|
|
45
|
+
userOnly: [],
|
|
46
|
+
},
|
|
47
|
+
member: {
|
|
48
|
+
tenant: [
|
|
49
|
+
// 调用批量获取用户信息 API 所需(任选其一即可)
|
|
50
|
+
"contact:contact.base:readonly",
|
|
51
|
+
// 字段权限:返回完整用户信息
|
|
52
|
+
// "contact:user.assign_info:read",
|
|
53
|
+
"contact:user.base:readonly",
|
|
54
|
+
// "contact:user.department:readonly",
|
|
55
|
+
// "contact:user.department_path:readonly",
|
|
56
|
+
// "contact:user.dotted_line_leader_info.read",
|
|
57
|
+
// "contact:user.employee:readonly",
|
|
58
|
+
// "contact:user.employee_id:readonly",
|
|
59
|
+
// "contact:user.employee_number:read",
|
|
60
|
+
// "contact:user.gender:readonly",
|
|
61
|
+
// "contact:user.user_geo",
|
|
62
|
+
// "contact:user.phone:readonly",
|
|
63
|
+
// "contact:user.email:readonly",
|
|
64
|
+
// "contact:user.job_family:readonly",
|
|
65
|
+
// "contact:user.job_level:readonly",
|
|
66
|
+
],
|
|
67
|
+
userOnly: [
|
|
68
|
+
"contact:user:search",
|
|
69
|
+
"contact:contact.base:readonly",
|
|
70
|
+
"contact:user.employee_id:readonly",
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 根据已启用模块和认证类型,计算所需的最小 Scope 集合
|
|
76
|
+
* @param enabledModules 已启用的模块 ID 列表(含 'all' 则返回全量)
|
|
77
|
+
* @param authType 认证类型
|
|
78
|
+
*/
|
|
79
|
+
export function getRequiredScopes(enabledModules, authType) {
|
|
80
|
+
const moduleIds = enabledModules.includes('all')
|
|
81
|
+
? Object.keys(MODULE_SCOPES)
|
|
82
|
+
: enabledModules;
|
|
83
|
+
const scopes = new Set();
|
|
84
|
+
for (const moduleId of moduleIds) {
|
|
85
|
+
const moduleScopes = MODULE_SCOPES[moduleId];
|
|
86
|
+
if (!moduleScopes)
|
|
87
|
+
continue;
|
|
88
|
+
moduleScopes.tenant.forEach(s => scopes.add(s));
|
|
89
|
+
if (authType === 'user') {
|
|
90
|
+
moduleScopes.userOnly.forEach(s => scopes.add(s));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Array.from(scopes);
|
|
94
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseApiService } from '../baseService.js';
|
|
2
|
+
import { Config } from '../../utils/config.js';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import { AuthUtils } from '../../utils/auth/index.js';
|
|
5
|
+
import { FeishuScopeValidator } from './FeishuScopeValidator.js';
|
|
6
|
+
/**
|
|
7
|
+
* 飞书 API 服务基类
|
|
8
|
+
* 封装 getBaseUrl 和 getAccessToken(委托给 AuthService),供各领域服务继承
|
|
9
|
+
*/
|
|
10
|
+
export class FeishuBaseApiService extends BaseApiService {
|
|
11
|
+
constructor(authService) {
|
|
12
|
+
super();
|
|
13
|
+
Object.defineProperty(this, "authService", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: authService
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "scopeValidator", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
this.scopeValidator = new FeishuScopeValidator();
|
|
26
|
+
}
|
|
27
|
+
getBaseUrl() {
|
|
28
|
+
return Config.getInstance().feishu.baseUrl;
|
|
29
|
+
}
|
|
30
|
+
async getAccessToken(userKey) {
|
|
31
|
+
const { appId, appSecret, authType, enableScopeValidation } = Config.getInstance().feishu;
|
|
32
|
+
const clientKey = AuthUtils.generateClientKey(userKey);
|
|
33
|
+
Logger.debug(`[FeishuBaseApiService] 获取访问令牌,authType: ${authType}, clientKey: ${clientKey}`);
|
|
34
|
+
if (enableScopeValidation) {
|
|
35
|
+
await this.scopeValidator.validateScopeWithVersion(appId, appSecret, authType, clientKey);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
Logger.debug('权限检查已禁用,跳过scope校验');
|
|
39
|
+
}
|
|
40
|
+
if (authType === 'tenant') {
|
|
41
|
+
return this.authService.getTenantAccessToken(appId, appSecret, clientKey);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return this.authService.getUserAccessToken(clientKey, appId, appSecret);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|