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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务模块 Schema 定义(task/v2 API)
|
|
3
|
+
*
|
|
4
|
+
* ## Schema 描述规范
|
|
5
|
+
*
|
|
6
|
+
* 明确必填/可选、格式与示例,表述简洁。
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
/** 任务 GUID(必填)。任务全局唯一标识,最大 100 字符。 */
|
|
10
|
+
export const TaskGuidSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(100)
|
|
14
|
+
.describe('Task GUID (required). Global unique ID of the task. Max 100 characters.');
|
|
15
|
+
/** 父任务 GUID(可选)。填写则在父任务下创建子任务,需具备父任务编辑权限。 */
|
|
16
|
+
export const TaskParentTaskGuidSchema = TaskGuidSchema.optional().describe('Parent task GUID (optional). When set, creates a subtask under this task; requires edit permission on the parent.');
|
|
17
|
+
/** 批量删除时的任务 GUID 列表。最少 1 个,最多 50 个。 */
|
|
18
|
+
export const TaskDeleteBatchSchema = z
|
|
19
|
+
.array(TaskGuidSchema)
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(50)
|
|
22
|
+
.describe('Task GUIDs to delete (required). Min 1, max 50 per call. Requires edit permission on each task.');
|
|
23
|
+
/** 任务标题(必填)。最大 3000 UTF-8 字符。 */
|
|
24
|
+
export const TaskSummarySchema = z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1)
|
|
27
|
+
.max(3000)
|
|
28
|
+
.describe('Task title (required). Max 3000 UTF-8 characters.');
|
|
29
|
+
/** 任务描述(可选)。最大 3000 UTF-8 字符。 */
|
|
30
|
+
export const TaskDescriptionSchema = z
|
|
31
|
+
.string()
|
|
32
|
+
.max(3000)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Task description (optional). Max 3000 UTF-8 characters.');
|
|
35
|
+
/** 时间戳,毫秒,自 1970-01-01 00:00:00 UTC 起。 */
|
|
36
|
+
export const TaskTimestampSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Timestamp in milliseconds since 1970-01-01 00:00:00 UTC. Example: "1675454764000".');
|
|
40
|
+
/** 是否全天(仅日期)。 */
|
|
41
|
+
export const TaskIsAllDaySchema = z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.default(false)
|
|
45
|
+
.describe('Whether the time is all-day. If true, only the date part of timestamp is used.');
|
|
46
|
+
/** 任务截止时间。 */
|
|
47
|
+
export const TaskDueSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
timestamp: TaskTimestampSchema,
|
|
50
|
+
is_all_day: TaskIsAllDaySchema,
|
|
51
|
+
})
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Task due time (optional). Set timestamp and is_all_day.');
|
|
54
|
+
/** 任务开始时间。若与 due 同时设置,start 须 <= due 且 is_all_day 一致。 */
|
|
55
|
+
export const TaskStartSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
timestamp: TaskTimestampSchema,
|
|
58
|
+
is_all_day: TaskIsAllDaySchema,
|
|
59
|
+
})
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Task start time (optional). If set with due, start must be <= due and is_all_day must match.');
|
|
62
|
+
/** 成员角色:assignee(负责人)或 follower(关注者)。 */
|
|
63
|
+
export const TaskMemberRoleSchema = z
|
|
64
|
+
.enum(['assignee', 'follower'])
|
|
65
|
+
.describe('Member role: "assignee" for owner, "follower" for watcher.');
|
|
66
|
+
/** 任务成员(负责人或关注者)。id 为 get_feishu_users 返回的 open_id。 */
|
|
67
|
+
export const TaskMemberSchema = z.object({
|
|
68
|
+
id: z.string().min(1).describe('User open_id (required). Use open_id from get_feishu_users.'),
|
|
69
|
+
type: z.literal('user').optional().default('user'),
|
|
70
|
+
role: TaskMemberRoleSchema,
|
|
71
|
+
name: z.string().optional().describe('Member display name (optional).'),
|
|
72
|
+
});
|
|
73
|
+
/** 任务成员列表,最多 50 人(去重)。 */
|
|
74
|
+
export const TaskMembersSchema = z
|
|
75
|
+
.array(TaskMemberSchema)
|
|
76
|
+
.max(50)
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Task members: assignees and followers (optional). Max 50. Use open_id from get_feishu_users.');
|
|
79
|
+
/** 负责人 open_id 列表(可选)。来自 search_feishu_users,最多 50。 */
|
|
80
|
+
export const TaskAssigneeIdsSchema = z
|
|
81
|
+
.array(z.string().min(1))
|
|
82
|
+
.max(50)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe('Assignee open_ids (optional). Use open_id from get_feishu_users. Max 50.');
|
|
85
|
+
/** 关注者 open_id 列表(可选)。来自 search_feishu_users,最多 50。 */
|
|
86
|
+
export const TaskFollowerIdsSchema = z
|
|
87
|
+
.array(z.string().min(1))
|
|
88
|
+
.max(50)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Follower open_ids (optional). Use open_id from get_feishu_users. Max 50.');
|
|
91
|
+
/** 提醒相对截止时间的分钟数(可选)。0=截止时提醒,30=截止前30分钟。非负整数。任务需先有 due;当前每任务仅支持 1 个提醒,已有提醒时需先移除再添加。 */
|
|
92
|
+
export const TaskReminderRelativeMinutesSchema = z
|
|
93
|
+
.number()
|
|
94
|
+
.int()
|
|
95
|
+
.min(0)
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('Reminder: minutes before due (0 = at due time, 30 = 30 min before). Task must have due; only one reminder per task—remove existing first if needed.');
|
|
98
|
+
/** 要移除的提醒 id 列表(可选)。id 来自任务详情的 reminders[].id。 */
|
|
99
|
+
export const TaskReminderIdsSchema = z
|
|
100
|
+
.array(z.string().min(1))
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Reminder ids to remove (from task.reminders[].id).');
|
|
103
|
+
/** 列取任务列表的分页标记(可选)。首次不传,后续用上响应的 page_token。 */
|
|
104
|
+
export const TaskListPageTokenSchema = z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Page token for list tasks (omit on first request; use page_token from previous response for next page).');
|
|
108
|
+
/** 列取任务时按完成状态过滤(可选)。true=仅已完成,false=仅未完成,不传=不过滤。 */
|
|
109
|
+
export const TaskListCompletedSchema = z
|
|
110
|
+
.boolean()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Filter by completion: true = only completed, false = only todo, omit = no filter.');
|
|
113
|
+
/** 重复规则。如 "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR"。 */
|
|
114
|
+
export const TaskRepeatRuleSchema = z
|
|
115
|
+
.string()
|
|
116
|
+
.max(1000)
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Recurrence rule (optional). Example: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR".');
|
|
119
|
+
/** 完成时间戳(毫秒)。不填或 "0" 表示未完成。 */
|
|
120
|
+
export const TaskCompletedAtSchema = z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('Completion timestamp in ms (optional). Omit or "0" = unfinished; set to create a completed task.');
|
|
124
|
+
/** 任务模式:1 = 会签(均需完成),2 = 或签(任一人完成即可)。默认 2。 */
|
|
125
|
+
export const TaskModeSchema = z
|
|
126
|
+
.union([z.literal(1), z.literal(2)])
|
|
127
|
+
.optional()
|
|
128
|
+
.default(2)
|
|
129
|
+
.describe('Task mode (optional): 1 = all assignees must complete, 2 = any assignee can complete. Default 2.');
|
|
130
|
+
/** 是否为里程碑任务。 */
|
|
131
|
+
export const TaskIsMilestoneSchema = z
|
|
132
|
+
.boolean()
|
|
133
|
+
.optional()
|
|
134
|
+
.default(false)
|
|
135
|
+
.describe('Whether the task is a milestone (optional). Default false.');
|
|
136
|
+
/** 批量创建时的单条任务入参。可设 parentTaskGuid 在已有任务下建子任务,或设 subTasks 建嵌套子任务。 */
|
|
137
|
+
export const TaskCreateItemSchema = z
|
|
138
|
+
.object({
|
|
139
|
+
parentTaskGuid: TaskParentTaskGuidSchema,
|
|
140
|
+
summary: TaskSummarySchema,
|
|
141
|
+
description: TaskDescriptionSchema,
|
|
142
|
+
dueTimestamp: TaskTimestampSchema,
|
|
143
|
+
isDueAllDay: TaskIsAllDaySchema,
|
|
144
|
+
completedAt: TaskCompletedAtSchema,
|
|
145
|
+
assigneeIds: TaskAssigneeIdsSchema,
|
|
146
|
+
followerIds: TaskFollowerIdsSchema,
|
|
147
|
+
repeatRule: TaskRepeatRuleSchema,
|
|
148
|
+
startTimestamp: TaskTimestampSchema,
|
|
149
|
+
isStartAllDay: TaskIsAllDaySchema,
|
|
150
|
+
mode: TaskModeSchema,
|
|
151
|
+
isMilestone: TaskIsMilestoneSchema,
|
|
152
|
+
subTasks: z.lazy(() => TaskSubTasksSchema),
|
|
153
|
+
})
|
|
154
|
+
.describe('Task item: summary (required), optional description/due/members/subTasks etc. Use subTasks for nested subtasks.');
|
|
155
|
+
/** 嵌套子任务数组(可选)。与父任务同结构,支持多层,每层最多 50 个。 */
|
|
156
|
+
export const TaskSubTasksSchema = z
|
|
157
|
+
.array(z.lazy(() => TaskCreateItemSchema))
|
|
158
|
+
.max(50)
|
|
159
|
+
.optional()
|
|
160
|
+
.describe('Nested subtasks (optional). Same shape as parent; supports multi-level nesting. Max 50 per level.');
|
|
161
|
+
/** 待创建任务列表。顶层 1~50 条,每条可有 subTasks 做嵌套创建。 */
|
|
162
|
+
export const TaskCreateBatchSchema = z
|
|
163
|
+
.array(TaskCreateItemSchema)
|
|
164
|
+
.min(1)
|
|
165
|
+
.max(50)
|
|
166
|
+
.describe('Array of tasks to create (required). Each item has summary, description?, parentTaskGuid? (for subtask under existing task), subTasks? (nested subtasks). Min 1, max 50 top-level.');
|
|
@@ -42,6 +42,12 @@ export class TokenCacheManager {
|
|
|
42
42
|
writable: true,
|
|
43
43
|
value: void 0
|
|
44
44
|
});
|
|
45
|
+
Object.defineProperty(this, "cleanupTimerId", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: null
|
|
50
|
+
});
|
|
45
51
|
this.cache = new Map();
|
|
46
52
|
this.cacheDir = this.resolveCacheDir();
|
|
47
53
|
this.userTokenCacheFile = path.join(this.cacheDir, 'user_token_cache.json');
|
|
@@ -147,7 +153,12 @@ export class TokenCacheManager {
|
|
|
147
153
|
*/
|
|
148
154
|
getUserTokenInfo(key) {
|
|
149
155
|
const cacheKey = `user_access_token:${key}`;
|
|
150
|
-
|
|
156
|
+
let cacheItem = this.cache.get(cacheKey);
|
|
157
|
+
// 内存未命中时,尝试从文件重新加载(应对多进程 stdio 场景:callback 在进程 A,API 调用在进程 B)
|
|
158
|
+
if (!cacheItem && fs.existsSync(this.userTokenCacheFile)) {
|
|
159
|
+
this.loadUserTokenCache();
|
|
160
|
+
cacheItem = this.cache.get(cacheKey);
|
|
161
|
+
}
|
|
151
162
|
if (!cacheItem) {
|
|
152
163
|
Logger.debug(`用户token信息未找到: ${key}`);
|
|
153
164
|
return null;
|
|
@@ -215,7 +226,7 @@ export class TokenCacheManager {
|
|
|
215
226
|
const cacheKey = `tenant_access_token:${key}`;
|
|
216
227
|
const result = this.cache.delete(cacheKey);
|
|
217
228
|
if (result) {
|
|
218
|
-
this.
|
|
229
|
+
this.saveTenantTokenCache();
|
|
219
230
|
Logger.debug(`租户token删除成功: ${key}`);
|
|
220
231
|
}
|
|
221
232
|
return result;
|
|
@@ -455,9 +466,11 @@ export class TokenCacheManager {
|
|
|
455
466
|
*/
|
|
456
467
|
startCacheCleanupTimer() {
|
|
457
468
|
// 每5分钟清理一次过期缓存
|
|
458
|
-
setInterval(() => {
|
|
469
|
+
this.cleanupTimerId = setInterval(() => {
|
|
459
470
|
this.cleanExpiredTokens();
|
|
460
471
|
}, 5 * 60 * 1000);
|
|
472
|
+
// 不阻止进程在 stdio 模式下自然退出
|
|
473
|
+
this.cleanupTimerId.unref();
|
|
461
474
|
Logger.info('Token缓存清理定时器已启动,每5分钟执行一次');
|
|
462
475
|
}
|
|
463
476
|
/**
|
|
@@ -50,10 +50,11 @@ export class TokenRefreshManager {
|
|
|
50
50
|
Logger.info(`启动Token自动刷新管理器,检查间隔: ${this.checkInterval / 1000}秒`);
|
|
51
51
|
// 立即执行一次检查
|
|
52
52
|
this.checkAndRefreshTokens();
|
|
53
|
-
//
|
|
53
|
+
// 设置定时器(不阻止进程在 stdio 模式下自然退出)
|
|
54
54
|
this.intervalId = setInterval(() => {
|
|
55
55
|
this.checkAndRefreshTokens();
|
|
56
56
|
}, this.checkInterval);
|
|
57
|
+
this.intervalId.unref();
|
|
57
58
|
this.isRunning = true;
|
|
58
59
|
Logger.info('Token自动刷新管理器已启动');
|
|
59
60
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { hideBin } from 'yargs/helpers';
|
|
|
3
3
|
import yargs from 'yargs';
|
|
4
4
|
import { Logger, LogLevel } from './logger.js';
|
|
5
5
|
import { serverInfo } from '../mcp/feishuMcp.js';
|
|
6
|
+
import { ModuleRegistry } from '../modules/ModuleRegistry.js';
|
|
6
7
|
/**
|
|
7
8
|
* 配置来源枚举
|
|
8
9
|
*/
|
|
@@ -46,6 +47,12 @@ export class Config {
|
|
|
46
47
|
writable: true,
|
|
47
48
|
value: void 0
|
|
48
49
|
});
|
|
50
|
+
Object.defineProperty(this, "features", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: void 0
|
|
55
|
+
});
|
|
49
56
|
Object.defineProperty(this, "configSources", {
|
|
50
57
|
enumerable: true,
|
|
51
58
|
configurable: true,
|
|
@@ -66,6 +73,8 @@ export class Config {
|
|
|
66
73
|
this.log = this.initLogConfig(argv);
|
|
67
74
|
// 配置缓存
|
|
68
75
|
this.cache = this.initCacheConfig(argv);
|
|
76
|
+
// 配置功能模块
|
|
77
|
+
this.features = this.initFeaturesConfig(argv);
|
|
69
78
|
}
|
|
70
79
|
/**
|
|
71
80
|
* 获取配置单例
|
|
@@ -127,6 +136,10 @@ export class Config {
|
|
|
127
136
|
'user-key': {
|
|
128
137
|
type: 'string',
|
|
129
138
|
description: 'stdio 模式下的用户标识,默认 stdio'
|
|
139
|
+
},
|
|
140
|
+
'enabled-modules': {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: '启用的功能模块列表(逗号分隔),可选值: document,task,calendar 或 all,默认 document'
|
|
130
143
|
}
|
|
131
144
|
})
|
|
132
145
|
.help()
|
|
@@ -347,6 +360,24 @@ export class Config {
|
|
|
347
360
|
}
|
|
348
361
|
return cacheConfig;
|
|
349
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* 初始化功能模块配置
|
|
365
|
+
* @param argv 命令行参数
|
|
366
|
+
* @returns 功能模块配置
|
|
367
|
+
*/
|
|
368
|
+
initFeaturesConfig(argv) {
|
|
369
|
+
const featuresConfig = {
|
|
370
|
+
enabledModules: ['document'],
|
|
371
|
+
};
|
|
372
|
+
const rawValue = argv['enabled-modules'] ?? process.env.FEISHU_ENABLED_MODULES;
|
|
373
|
+
if (rawValue) {
|
|
374
|
+
featuresConfig.enabledModules = String(rawValue)
|
|
375
|
+
.split(',')
|
|
376
|
+
.map(s => s.trim())
|
|
377
|
+
.filter(Boolean);
|
|
378
|
+
}
|
|
379
|
+
return featuresConfig;
|
|
380
|
+
}
|
|
350
381
|
/**
|
|
351
382
|
* 从字符串获取日志级别
|
|
352
383
|
* @param levelStr 日志级别字符串
|
|
@@ -368,6 +399,8 @@ export class Config {
|
|
|
368
399
|
* @param isStdioMode 是否在stdio模式下
|
|
369
400
|
*/
|
|
370
401
|
printConfig(isStdioMode = false) {
|
|
402
|
+
// 模块配置信息始终输出(含认证类型导致的跳过提醒)
|
|
403
|
+
this.printModuleConfig();
|
|
371
404
|
if (isStdioMode)
|
|
372
405
|
return;
|
|
373
406
|
Logger.info(`应用版本: ${serverInfo.version}`);
|
|
@@ -394,6 +427,20 @@ export class Config {
|
|
|
394
427
|
Logger.info(`- 缓存TTL: ${this.cache.ttl}秒 (来源: ${this.configSources['cache.ttl']})`);
|
|
395
428
|
Logger.info(`- 最大缓存条目: ${this.cache.maxSize} (来源: ${this.configSources['cache.maxSize']})`);
|
|
396
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* 输出模块配置及认证限制提醒(stdio 模式下也会输出)
|
|
432
|
+
*/
|
|
433
|
+
printModuleConfig() {
|
|
434
|
+
const authType = this.feishu.authType;
|
|
435
|
+
const effectiveModules = ModuleRegistry.getEnabledModules(this.features.enabledModules, authType);
|
|
436
|
+
Logger.info(`[模块] 配置: ${this.features.enabledModules.join(', ') || 'document'} → 实际加载: ${effectiveModules.map(m => m.id).join(', ') || '(无)'}`);
|
|
437
|
+
if (authType === 'tenant') {
|
|
438
|
+
const userOnlyRequested = this.features.enabledModules.some(id => ['task', 'calendar', 'member', 'all'].includes(id));
|
|
439
|
+
if (userOnlyRequested) {
|
|
440
|
+
Logger.warn('[模块] task、calendar、member 需 user 认证,当前为 tenant 模式已跳过。设置 FEISHU_AUTH_TYPE=user 以启用。');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
397
444
|
/**
|
|
398
445
|
* 掩盖API密钥
|
|
399
446
|
* @param key API密钥
|
package/dist/utils/document.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @param input 文档URL或ID
|
|
9
9
|
* @returns 提取的文档ID或null
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
function extractDocumentId(input) {
|
|
12
12
|
// 移除首尾空白
|
|
13
13
|
input = input.trim();
|
|
14
14
|
// 处理各种URL格式
|
|
@@ -29,7 +29,7 @@ export function extractDocumentId(input) {
|
|
|
29
29
|
* @param input Wiki URL或Token
|
|
30
30
|
* @returns 提取的Wiki Token或null
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
function extractWikiToken(input) {
|
|
33
33
|
// 移除首尾空白
|
|
34
34
|
input = input.trim();
|
|
35
35
|
// 处理Wiki URL格式
|
|
@@ -136,127 +136,127 @@ export function renderFeishuAuthResultHtml(data) {
|
|
|
136
136
|
expiresIn = expiresIn - now;
|
|
137
137
|
if (refreshExpiresIn && refreshExpiresIn > 1000000000)
|
|
138
138
|
refreshExpiresIn = refreshExpiresIn - now;
|
|
139
|
-
const tokenBlock = data && !isError ? `
|
|
140
|
-
<div class="card">
|
|
141
|
-
<h3>Token 信息</h3>
|
|
142
|
-
<ul class="kv-list">
|
|
143
|
-
<li><b>token_type:</b> <span>${data.token_type || ''}</span></li>
|
|
144
|
-
<li><b>access_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.access_token || ''}</pre></li>
|
|
145
|
-
<li><b>expires_in:</b> <span>${formatExpire(expiresIn)}</span></li>
|
|
146
|
-
<li><b>refresh_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.refresh_token || ''}</pre></li>
|
|
147
|
-
<li><b>refresh_token_expires_in:</b> <span>${formatExpire(refreshExpiresIn)}</span></li>
|
|
148
|
-
<li><b>scope:</b> <pre class="scope">${(data.scope || '').replace(/ /g, '\n')}</pre></li>
|
|
149
|
-
</ul>
|
|
150
|
-
<div class="success-action">
|
|
151
|
-
<span class="success-msg">授权成功,继续完成任务</span>
|
|
152
|
-
<button class="copy-btn" onclick="copySuccessMsg(this)">点击复制到粘贴板</button>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
139
|
+
const tokenBlock = data && !isError ? `
|
|
140
|
+
<div class="card">
|
|
141
|
+
<h3>Token 信息</h3>
|
|
142
|
+
<ul class="kv-list">
|
|
143
|
+
<li><b>token_type:</b> <span>${data.token_type || ''}</span></li>
|
|
144
|
+
<li><b>access_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.access_token || ''}</pre></li>
|
|
145
|
+
<li><b>expires_in:</b> <span>${formatExpire(expiresIn)}</span></li>
|
|
146
|
+
<li><b>refresh_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.refresh_token || ''}</pre></li>
|
|
147
|
+
<li><b>refresh_token_expires_in:</b> <span>${formatExpire(refreshExpiresIn)}</span></li>
|
|
148
|
+
<li><b>scope:</b> <pre class="scope">${(data.scope || '').replace(/ /g, '\n')}</pre></li>
|
|
149
|
+
</ul>
|
|
150
|
+
<div class="success-action">
|
|
151
|
+
<span class="success-msg">授权成功,继续完成任务</span>
|
|
152
|
+
<button class="copy-btn" onclick="copySuccessMsg(this)">点击复制到粘贴板</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
155
|
` : '';
|
|
156
156
|
let userBlock = '';
|
|
157
157
|
const userInfo = data && data.userInfo && data.userInfo.data;
|
|
158
158
|
if (userInfo) {
|
|
159
|
-
userBlock = `
|
|
160
|
-
<div class="card user-card">
|
|
161
|
-
<div class="avatar-wrap">
|
|
162
|
-
<img src="${userInfo.avatar_big || userInfo.avatar_thumb || userInfo.avatar_url || ''}" class="avatar" />
|
|
163
|
-
</div>
|
|
164
|
-
<div class="user-info">
|
|
165
|
-
<div class="user-name">${userInfo.name || ''}</div>
|
|
166
|
-
<div class="user-en">${userInfo.en_name || ''}</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
159
|
+
userBlock = `
|
|
160
|
+
<div class="card user-card">
|
|
161
|
+
<div class="avatar-wrap">
|
|
162
|
+
<img src="${userInfo.avatar_big || userInfo.avatar_thumb || userInfo.avatar_url || ''}" class="avatar" />
|
|
163
|
+
</div>
|
|
164
|
+
<div class="user-info">
|
|
165
|
+
<div class="user-name">${userInfo.name || ''}</div>
|
|
166
|
+
<div class="user-en">${userInfo.en_name || ''}</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
169
|
`;
|
|
170
170
|
}
|
|
171
|
-
const errorBlock = isError ? `
|
|
172
|
-
<div class="card error-card">
|
|
173
|
-
<h3>授权失败</h3>
|
|
174
|
-
<div class="error-msg">${escapeHtml(data.error || '')}</div>
|
|
175
|
-
<div class="error-code">错误码: ${data.code || ''}</div>
|
|
176
|
-
</div>
|
|
171
|
+
const errorBlock = isError ? `
|
|
172
|
+
<div class="card error-card">
|
|
173
|
+
<h3>授权失败</h3>
|
|
174
|
+
<div class="error-msg">${escapeHtml(data.error || '')}</div>
|
|
175
|
+
<div class="error-code">错误码: ${data.code || ''}</div>
|
|
176
|
+
</div>
|
|
177
177
|
` : '';
|
|
178
|
-
return `
|
|
179
|
-
<html>
|
|
180
|
-
<head>
|
|
181
|
-
<title>飞书授权结果</title>
|
|
182
|
-
<meta charset="utf-8"/>
|
|
183
|
-
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
184
|
-
<style>
|
|
185
|
-
body { background: #f7f8fa; font-family: 'Segoe UI', Arial, sans-serif; margin:0; padding:0; }
|
|
186
|
-
.container { max-width: 600px; margin: 40px auto; padding: 16px; }
|
|
187
|
-
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px #0001; margin-bottom: 24px; padding: 24px 20px; }
|
|
188
|
-
.user-card { display: flex; align-items: center; gap: 24px; }
|
|
189
|
-
.avatar-wrap { flex-shrink: 0; }
|
|
190
|
-
.avatar { width: 96px; height: 96px; border-radius: 50%; box-shadow: 0 2px 8px #0002; display: block; margin: 0 auto; }
|
|
191
|
-
.user-info { flex: 1; }
|
|
192
|
-
.user-name { font-size: 1.5em; font-weight: bold; margin-bottom: 4px; }
|
|
193
|
-
.user-en { color: #888; margin-bottom: 10px; }
|
|
194
|
-
.kv-list { list-style: none; padding: 0; margin: 0; }
|
|
195
|
-
.kv-list li { margin-bottom: 6px; word-break: break-all; }
|
|
196
|
-
.kv-list b { color: #1976d2; }
|
|
197
|
-
.scope { background: #f0f4f8; border-radius: 4px; padding: 6px; font-size: 0.95em; white-space: pre-line; }
|
|
198
|
-
.foldable { color: #1976d2; cursor: pointer; text-decoration: underline; margin-left: 8px; }
|
|
199
|
-
.fold { display: none; background: #f6f6f6; border-radius: 4px; padding: 6px; margin: 4px 0; font-size: 0.92em; max-width: 100%; overflow-x: auto; word-break: break-all; }
|
|
200
|
-
.scrollable { max-width: 100%; overflow-x: auto; font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace; font-size: 0.93em; }
|
|
201
|
-
.success-action { margin-top: 18px; display: flex; align-items: center; gap: 16px; }
|
|
202
|
-
.success-msg { color: #388e3c; font-weight: bold; }
|
|
203
|
-
.copy-btn { background: #1976d2; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 1em; cursor: pointer; transition: background 0.2s; }
|
|
204
|
-
.copy-btn:hover { background: #125ea2; }
|
|
205
|
-
.error-card { border-left: 6px solid #e53935; background: #fff0f0; color: #b71c1c; }
|
|
206
|
-
.error-msg { font-size: 1.1em; margin-bottom: 8px; }
|
|
207
|
-
.error-code { color: #b71c1c; font-size: 0.95em; }
|
|
208
|
-
.raw-block { margin-top: 24px; }
|
|
209
|
-
.raw-toggle { color: #1976d2; cursor: pointer; text-decoration: underline; margin-bottom: 8px; display: inline-block; }
|
|
210
|
-
.raw-pre { display: none; background: #23272e; color: #fff; border-radius: 6px; padding: 12px; font-size: 0.95em; overflow-x: auto; max-width: 100%; }
|
|
211
|
-
@media (max-width: 700px) {
|
|
212
|
-
.container { max-width: 98vw; padding: 4vw; }
|
|
213
|
-
.card { padding: 4vw 3vw; }
|
|
214
|
-
.avatar { width: 64px; height: 64px; }
|
|
215
|
-
}
|
|
216
|
-
</style>
|
|
217
|
-
<script>
|
|
218
|
-
function toggleFold(el) {
|
|
219
|
-
var pre = el.nextElementSibling;
|
|
220
|
-
if (pre.style.display === 'block') {
|
|
221
|
-
pre.style.display = 'none';
|
|
222
|
-
} else {
|
|
223
|
-
pre.style.display = 'block';
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function toggleRaw() {
|
|
227
|
-
var pre = document.getElementById('raw-pre');
|
|
228
|
-
if (pre.style.display === 'block') {
|
|
229
|
-
pre.style.display = 'none';
|
|
230
|
-
} else {
|
|
231
|
-
pre.style.display = 'block';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function copySuccessMsg(btn) {
|
|
235
|
-
var text = '授权成功,继续完成任务';
|
|
236
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
237
|
-
btn.innerText = '已复制';
|
|
238
|
-
btn.disabled = true;
|
|
239
|
-
setTimeout(function() {
|
|
240
|
-
btn.innerText = '点击复制到粘贴板';
|
|
241
|
-
btn.disabled = false;
|
|
242
|
-
}, 2000);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
</script>
|
|
246
|
-
</head>
|
|
247
|
-
<body>
|
|
248
|
-
<div class="container">
|
|
249
|
-
<h2 style="margin-bottom:24px;">飞书授权结果</h2>
|
|
250
|
-
${errorBlock}
|
|
251
|
-
${tokenBlock}
|
|
252
|
-
${userBlock}
|
|
253
|
-
<div class="card raw-block">
|
|
254
|
-
<span class="raw-toggle" onclick="toggleRaw()">点击展开/收起原始数据</span>
|
|
255
|
-
<pre id="raw-pre" class="raw-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</body>
|
|
259
|
-
</html>
|
|
178
|
+
return `
|
|
179
|
+
<html>
|
|
180
|
+
<head>
|
|
181
|
+
<title>飞书授权结果</title>
|
|
182
|
+
<meta charset="utf-8"/>
|
|
183
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
184
|
+
<style>
|
|
185
|
+
body { background: #f7f8fa; font-family: 'Segoe UI', Arial, sans-serif; margin:0; padding:0; }
|
|
186
|
+
.container { max-width: 600px; margin: 40px auto; padding: 16px; }
|
|
187
|
+
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px #0001; margin-bottom: 24px; padding: 24px 20px; }
|
|
188
|
+
.user-card { display: flex; align-items: center; gap: 24px; }
|
|
189
|
+
.avatar-wrap { flex-shrink: 0; }
|
|
190
|
+
.avatar { width: 96px; height: 96px; border-radius: 50%; box-shadow: 0 2px 8px #0002; display: block; margin: 0 auto; }
|
|
191
|
+
.user-info { flex: 1; }
|
|
192
|
+
.user-name { font-size: 1.5em; font-weight: bold; margin-bottom: 4px; }
|
|
193
|
+
.user-en { color: #888; margin-bottom: 10px; }
|
|
194
|
+
.kv-list { list-style: none; padding: 0; margin: 0; }
|
|
195
|
+
.kv-list li { margin-bottom: 6px; word-break: break-all; }
|
|
196
|
+
.kv-list b { color: #1976d2; }
|
|
197
|
+
.scope { background: #f0f4f8; border-radius: 4px; padding: 6px; font-size: 0.95em; white-space: pre-line; }
|
|
198
|
+
.foldable { color: #1976d2; cursor: pointer; text-decoration: underline; margin-left: 8px; }
|
|
199
|
+
.fold { display: none; background: #f6f6f6; border-radius: 4px; padding: 6px; margin: 4px 0; font-size: 0.92em; max-width: 100%; overflow-x: auto; word-break: break-all; }
|
|
200
|
+
.scrollable { max-width: 100%; overflow-x: auto; font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace; font-size: 0.93em; }
|
|
201
|
+
.success-action { margin-top: 18px; display: flex; align-items: center; gap: 16px; }
|
|
202
|
+
.success-msg { color: #388e3c; font-weight: bold; }
|
|
203
|
+
.copy-btn { background: #1976d2; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 1em; cursor: pointer; transition: background 0.2s; }
|
|
204
|
+
.copy-btn:hover { background: #125ea2; }
|
|
205
|
+
.error-card { border-left: 6px solid #e53935; background: #fff0f0; color: #b71c1c; }
|
|
206
|
+
.error-msg { font-size: 1.1em; margin-bottom: 8px; }
|
|
207
|
+
.error-code { color: #b71c1c; font-size: 0.95em; }
|
|
208
|
+
.raw-block { margin-top: 24px; }
|
|
209
|
+
.raw-toggle { color: #1976d2; cursor: pointer; text-decoration: underline; margin-bottom: 8px; display: inline-block; }
|
|
210
|
+
.raw-pre { display: none; background: #23272e; color: #fff; border-radius: 6px; padding: 12px; font-size: 0.95em; overflow-x: auto; max-width: 100%; }
|
|
211
|
+
@media (max-width: 700px) {
|
|
212
|
+
.container { max-width: 98vw; padding: 4vw; }
|
|
213
|
+
.card { padding: 4vw 3vw; }
|
|
214
|
+
.avatar { width: 64px; height: 64px; }
|
|
215
|
+
}
|
|
216
|
+
</style>
|
|
217
|
+
<script>
|
|
218
|
+
function toggleFold(el) {
|
|
219
|
+
var pre = el.nextElementSibling;
|
|
220
|
+
if (pre.style.display === 'block') {
|
|
221
|
+
pre.style.display = 'none';
|
|
222
|
+
} else {
|
|
223
|
+
pre.style.display = 'block';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function toggleRaw() {
|
|
227
|
+
var pre = document.getElementById('raw-pre');
|
|
228
|
+
if (pre.style.display === 'block') {
|
|
229
|
+
pre.style.display = 'none';
|
|
230
|
+
} else {
|
|
231
|
+
pre.style.display = 'block';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function copySuccessMsg(btn) {
|
|
235
|
+
var text = '授权成功,继续完成任务';
|
|
236
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
237
|
+
btn.innerText = '已复制';
|
|
238
|
+
btn.disabled = true;
|
|
239
|
+
setTimeout(function() {
|
|
240
|
+
btn.innerText = '点击复制到粘贴板';
|
|
241
|
+
btn.disabled = false;
|
|
242
|
+
}, 2000);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
</script>
|
|
246
|
+
</head>
|
|
247
|
+
<body>
|
|
248
|
+
<div class="container">
|
|
249
|
+
<h2 style="margin-bottom:24px;">飞书授权结果</h2>
|
|
250
|
+
${errorBlock}
|
|
251
|
+
${tokenBlock}
|
|
252
|
+
${userBlock}
|
|
253
|
+
<div class="card raw-block">
|
|
254
|
+
<span class="raw-toggle" onclick="toggleRaw()">点击展开/收起原始数据</span>
|
|
255
|
+
<pre id="raw-pre" class="raw-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
260
260
|
`;
|
|
261
261
|
}
|
|
262
262
|
function escapeHtml(str) {
|
package/dist/utils/error.js
CHANGED
|
@@ -164,17 +164,6 @@ export function formatErrorMessage(error, context) {
|
|
|
164
164
|
return typeof error === 'string' ? error : '发生未知错误';
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
/**
|
|
168
|
-
* 包装错误为标准格式
|
|
169
|
-
*
|
|
170
|
-
* @param message 错误消息前缀
|
|
171
|
-
* @param originalError 原始错误
|
|
172
|
-
* @returns 包装后的错误对象
|
|
173
|
-
*/
|
|
174
|
-
export function wrapError(message, originalError) {
|
|
175
|
-
const errorMessage = formatErrorMessage(originalError);
|
|
176
|
-
return new Error(`${message}: ${errorMessage}`);
|
|
177
|
-
}
|
|
178
167
|
/**
|
|
179
168
|
* 授权异常类
|
|
180
169
|
* 用于处理需要用户授权的情况
|
package/dist/utils/paramUtils.js
CHANGED
|
@@ -197,35 +197,4 @@ export class ParamUtils {
|
|
|
197
197
|
throw new ParamValidationError('whiteboardId', formatErrorMessage(error));
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
/**
|
|
201
|
-
* 批量处理通用参数
|
|
202
|
-
* 验证并规范化常用参数集
|
|
203
|
-
*
|
|
204
|
-
* @param params 通用参数对象
|
|
205
|
-
* @returns 处理后的参数对象
|
|
206
|
-
*/
|
|
207
|
-
static processCommonParams(params) {
|
|
208
|
-
const result = { ...params };
|
|
209
|
-
// 处理文档ID
|
|
210
|
-
if (params.documentId) {
|
|
211
|
-
result.documentId = ParamUtils.processDocumentId(params.documentId);
|
|
212
|
-
}
|
|
213
|
-
// 处理块ID
|
|
214
|
-
if (params.blockId) {
|
|
215
|
-
result.blockId = ParamUtils.processBlockId(params.blockId);
|
|
216
|
-
}
|
|
217
|
-
// 处理父块ID
|
|
218
|
-
if (params.parentBlockId) {
|
|
219
|
-
result.parentBlockId = ParamUtils.processParentBlockId(params.parentBlockId);
|
|
220
|
-
}
|
|
221
|
-
// 处理索引
|
|
222
|
-
if (params.index !== undefined) {
|
|
223
|
-
result.index = ParamUtils.processIndex(params.index);
|
|
224
|
-
}
|
|
225
|
-
// 处理起始索引
|
|
226
|
-
if (params.startIndex !== undefined) {
|
|
227
|
-
result.startIndex = ParamUtils.processIndex(params.startIndex);
|
|
228
|
-
}
|
|
229
|
-
return result;
|
|
230
|
-
}
|
|
231
200
|
}
|