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.
Files changed (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +356 -266
  3. package/dist/cli/auth.js +117 -0
  4. package/dist/cli/commands/auth.js +141 -0
  5. package/dist/cli/commands/config.js +86 -0
  6. package/dist/cli/commands/guide.js +68 -0
  7. package/dist/cli/dispatcher.js +96 -0
  8. package/dist/cli/index.js +95 -0
  9. package/dist/manager/sseConnectionManager.js +2 -0
  10. package/dist/mcp/feishuMcp.js +25 -25
  11. package/dist/mcp/tools/blockTools.js +295 -0
  12. package/dist/mcp/tools/documentTools.js +105 -0
  13. package/dist/mcp/tools/feishuBlockTools.js +1 -285
  14. package/dist/mcp/tools/feishuTools.js +1 -67
  15. package/dist/mcp/tools/folderTools.js +99 -0
  16. package/dist/mcp/tools/toolHelpers.js +155 -0
  17. package/dist/modules/FeatureModule.js +1 -0
  18. package/dist/modules/ModuleRegistry.js +63 -0
  19. package/dist/modules/calendar/index.js +11 -0
  20. package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
  21. package/dist/modules/calendar/tools/calendarTools.js +6 -0
  22. package/dist/modules/document/index.js +15 -0
  23. package/dist/modules/document/services/FeishuBlockService.js +410 -0
  24. package/dist/modules/document/services/FeishuDocumentService.js +110 -0
  25. package/dist/modules/document/services/FeishuFoldService.js +187 -0
  26. package/dist/modules/document/services/FeishuSearchService.js +232 -0
  27. package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
  28. package/dist/modules/document/services/blockFactory.js +521 -0
  29. package/dist/modules/document/toolApi/blockToolApi.js +160 -0
  30. package/dist/modules/document/toolApi/documentToolApi.js +65 -0
  31. package/dist/modules/document/toolApi/folderToolApi.js +73 -0
  32. package/dist/modules/document/toolApi/index.js +3 -0
  33. package/dist/modules/document/tools/blockTools.js +138 -0
  34. package/dist/modules/document/tools/documentTools.js +64 -0
  35. package/dist/modules/document/tools/folderTools.js +46 -0
  36. package/dist/modules/document/tools/toolHelpers.js +155 -0
  37. package/dist/modules/index.js +5 -0
  38. package/dist/modules/member/index.js +11 -0
  39. package/dist/modules/member/services/FeishuMemberService.js +41 -0
  40. package/dist/modules/member/toolApi/index.js +1 -0
  41. package/dist/modules/member/toolApi/memberToolApi.js +54 -0
  42. package/dist/modules/member/tools/memberTools.js +26 -0
  43. package/dist/modules/task/index.js +11 -0
  44. package/dist/modules/task/services/FeishuTaskService.js +271 -0
  45. package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
  46. package/dist/modules/task/toolApi/index.js +1 -0
  47. package/dist/modules/task/toolApi/taskToolApi.js +128 -0
  48. package/dist/modules/task/tools/taskTools.js +93 -0
  49. package/dist/server.js +43 -24
  50. package/dist/services/baseService.js +11 -2
  51. package/dist/services/blockFactory.js +167 -0
  52. package/dist/services/callbackService.js +1 -1
  53. package/dist/services/constants/feishuScopes.js +94 -0
  54. package/dist/services/feishu/FeishuBaseApiService.js +47 -0
  55. package/dist/services/feishu/FeishuBlockService.js +410 -0
  56. package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
  57. package/dist/services/feishu/FeishuDocumentService.js +110 -0
  58. package/dist/services/feishu/FeishuDriveService.js +62 -0
  59. package/dist/services/feishu/FeishuFoldService.js +187 -0
  60. package/dist/services/feishu/FeishuImageService.js +219 -0
  61. package/dist/services/feishu/FeishuScopeValidator.js +177 -0
  62. package/dist/services/feishu/FeishuSearchService.js +232 -0
  63. package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
  64. package/dist/services/feishu/FeishuWikiService.js +134 -0
  65. package/dist/services/feishuApiService.js +246 -1760
  66. package/dist/services/feishuAuthService.js +43 -0
  67. package/dist/types/documentSchema.js +232 -0
  68. package/dist/types/feishuSchema.js +54 -77
  69. package/dist/types/memberSchema.js +35 -0
  70. package/dist/types/taskSchema.js +166 -0
  71. package/dist/utils/auth/tokenCacheManager.js +16 -3
  72. package/dist/utils/auth/tokenRefreshManager.js +2 -1
  73. package/dist/utils/config.js +47 -0
  74. package/dist/utils/document.js +116 -116
  75. package/dist/utils/error.js +0 -11
  76. package/dist/utils/paramUtils.js +0 -31
  77. package/package.json +77 -76
@@ -0,0 +1,54 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ import { UserSearchQueriesSchema, GetFeishuUsersUserIdsParamSchema, } from '../../../types/memberSchema.js';
3
+ /**
4
+ * 按名称搜索或按 ID 批量获取飞书用户
5
+ */
6
+ export async function getUsers(params, api) {
7
+ const byQuery = 'queries' in params && params.queries != null && params.queries.length > 0;
8
+ const byId = 'userIdsParam' in params && params.userIdsParam != null && params.userIdsParam.length > 0;
9
+ if (!byQuery && !byId) {
10
+ throw new Error('Provide exactly one of: queries (search by name) or userIdsParam (get by ID list).');
11
+ }
12
+ if (byQuery && byId) {
13
+ throw new Error('Provide exactly one of: queries or userIdsParam, not both.');
14
+ }
15
+ if (byQuery) {
16
+ const queries = params.queries;
17
+ const parsed = UserSearchQueriesSchema.safeParse(queries);
18
+ if (!parsed.success) {
19
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
20
+ }
21
+ Logger.info(`getUsers invoked: by query, ${parsed.data.length} queries`);
22
+ const results = [];
23
+ for (const { query, pageToken } of parsed.data) {
24
+ const q = query.trim();
25
+ const result = await api.searchUsers(q, pageToken);
26
+ const users = Array.isArray(result) ? result : result?.users ?? result?.data?.users ?? [];
27
+ const nextToken = result?.page_token ?? result?.pageToken;
28
+ results.push({ query: q, users, ...(nextToken && { pageToken: nextToken }) });
29
+ }
30
+ return results;
31
+ }
32
+ if (byId) {
33
+ const userIdsParam = params.userIdsParam;
34
+ const parsed = GetFeishuUsersUserIdsParamSchema.safeParse(userIdsParam);
35
+ if (!parsed.success) {
36
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
37
+ }
38
+ Logger.info(`getUsers invoked: by id, ${parsed.data.length} items`);
39
+ const byType = new Map();
40
+ for (const { id, idType } of parsed.data) {
41
+ const t = idType ?? 'open_id';
42
+ if (!byType.has(t))
43
+ byType.set(t, []);
44
+ byType.get(t).push(id);
45
+ }
46
+ const allItems = [];
47
+ for (const [idType, ids] of byType) {
48
+ const res = await api.getUsersBatch(ids, idType);
49
+ allItems.push(...(res.items ?? []));
50
+ }
51
+ return { items: allItems };
52
+ }
53
+ throw new Error('Unexpected state.');
54
+ }
@@ -0,0 +1,26 @@
1
+ import { formatErrorMessage } from '../../../utils/error.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ import { errorResponse } from '../../document/tools/toolHelpers.js';
4
+ import { getUsers } from '../toolApi/index.js';
5
+ import { UserSearchQueriesSchema, GetFeishuUsersUserIdsParamSchema, } from '../../../types/memberSchema.js';
6
+ /**
7
+ * 注册飞书成员 MCP 工具
8
+ * get_feishu_users:统一按「名称搜索」或「ID 批量获取」两种方式查询用户
9
+ */
10
+ export function registerMemberTools(server, feishuService) {
11
+ server.tool('get_feishu_users', 'Get Feishu users by either (1) name search or (2) user IDs. Provide exactly one: queries [{ query, pageToken? }, ...] (1–20 items; pageToken per query for pagination), OR userIdsParam [{ id, idType }, ...] (1–50 items) to batch get by ID. Returns user list with basic info: open_id, user_id, name, department_ids, email, mobile, status, avatar, etc.', {
12
+ queries: UserSearchQueriesSchema.optional(),
13
+ userIdsParam: GetFeishuUsersUserIdsParamSchema.optional(),
14
+ }, async ({ queries, userIdsParam }) => {
15
+ try {
16
+ const params = queries != null && queries.length > 0 ? { queries } : { userIdsParam: userIdsParam };
17
+ const result = await getUsers(params, feishuService);
18
+ const text = Array.isArray(result) ? JSON.stringify(result, null, 2) : JSON.stringify(result, null, 2);
19
+ return { content: [{ type: 'text', text }] };
20
+ }
21
+ catch (error) {
22
+ Logger.error('get_feishu_users 失败:', error);
23
+ return errorResponse(`get_feishu_users 失败: ${formatErrorMessage(error)}`);
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,11 @@
1
+ import { MODULE_SCOPES } from '../../services/constants/feishuScopes.js';
2
+ import { registerTaskTools } from './tools/taskTools.js';
3
+ export const taskModule = {
4
+ id: 'task',
5
+ name: '飞书任务',
6
+ description: '飞书任务查询、创建、更新、删除功能',
7
+ requiredScopes: MODULE_SCOPES.task,
8
+ registerTools(server, apiService) {
9
+ registerTaskTools(server, apiService);
10
+ },
11
+ };
@@ -0,0 +1,271 @@
1
+ import { FeishuBaseApiService } from '../../../services/feishu/FeishuBaseApiService.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ /**
4
+ * 飞书任务 API 服务
5
+ * 封装飞书任务 v2 API(/task/v2/...)
6
+ */
7
+ export class FeishuTaskService extends FeishuBaseApiService {
8
+ constructor(authService) {
9
+ super(authService);
10
+ }
11
+ async createTask(params) {
12
+ Logger.info(`创建任务: ${params.summary}`);
13
+ const body = { summary: params.summary };
14
+ if (params.description !== undefined)
15
+ body.description = params.description;
16
+ if (params.due !== undefined)
17
+ body.due = params.due;
18
+ if (params.completed_at !== undefined)
19
+ body.completed_at = params.completed_at;
20
+ if (params.members?.length)
21
+ body.members = params.members;
22
+ if (params.repeat_rule !== undefined)
23
+ body.repeat_rule = params.repeat_rule;
24
+ if (params.start !== undefined)
25
+ body.start = params.start;
26
+ if (params.mode !== undefined)
27
+ body.mode = params.mode;
28
+ if (params.is_milestone !== undefined)
29
+ body.is_milestone = params.is_milestone;
30
+ return this.post('/task/v2/tasks', body);
31
+ }
32
+ /**
33
+ * 在父任务下创建子任务。请求体与创建任务一致。
34
+ * POST /task/v2/tasks/:task_guid/subtasks
35
+ */
36
+ async createSubtask(parentTaskGuid, params) {
37
+ Logger.info(`创建子任务: ${params.summary}, parent: ${parentTaskGuid}`);
38
+ const body = { summary: params.summary };
39
+ if (params.description !== undefined)
40
+ body.description = params.description;
41
+ if (params.due !== undefined)
42
+ body.due = params.due;
43
+ if (params.completed_at !== undefined)
44
+ body.completed_at = params.completed_at;
45
+ if (params.members?.length)
46
+ body.members = params.members;
47
+ if (params.repeat_rule !== undefined)
48
+ body.repeat_rule = params.repeat_rule;
49
+ if (params.start !== undefined)
50
+ body.start = params.start;
51
+ if (params.mode !== undefined)
52
+ body.mode = params.mode;
53
+ if (params.is_milestone !== undefined)
54
+ body.is_milestone = params.is_milestone;
55
+ return this.post(`/task/v2/tasks/${parentTaskGuid}/subtasks`, body);
56
+ }
57
+ /**
58
+ * 按嵌套结构创建任务(含 subTasks)。深度优先:先创建父任务再递归创建子任务。
59
+ * 根节点可带 parentTaskGuid 挂到已有任务下。返回嵌套结果及按路径索引的错误列表。
60
+ */
61
+ async createTasksNested(rootItems, options) {
62
+ const maxDepth = options?.maxDepth ?? FeishuTaskService.DEFAULT_MAX_NESTING_DEPTH;
63
+ const errors = [];
64
+ const createOne = async (item, parentGuid, path, depth) => {
65
+ const params = {
66
+ summary: item.summary,
67
+ description: item.description,
68
+ due: item.due,
69
+ completed_at: item.completed_at,
70
+ members: item.members,
71
+ repeat_rule: item.repeat_rule,
72
+ start: item.start,
73
+ mode: item.mode,
74
+ is_milestone: item.is_milestone,
75
+ };
76
+ let created;
77
+ try {
78
+ const res = parentGuid
79
+ ? await this.createSubtask(parentGuid, params)
80
+ : await this.createTask(params);
81
+ created = parentGuid ? (res?.subtask ?? res) : (res?.task ?? res);
82
+ }
83
+ catch (e) {
84
+ const msg = e instanceof Error ? e.message : String(e);
85
+ errors.push({ path, error: msg });
86
+ Logger.warn(`创建任务失败 [${path}]: ${msg}`);
87
+ return null;
88
+ }
89
+ const result = { task: created };
90
+ const children = item.subTasks;
91
+ if (children?.length && depth < maxDepth) {
92
+ result.subTasks = [];
93
+ for (let i = 0; i < children.length; i++) {
94
+ const childPath = `${path}.subTasks[${i}]`;
95
+ const childResult = await createOne(children[i], created.guid, childPath, depth + 1);
96
+ if (childResult)
97
+ result.subTasks.push(childResult);
98
+ }
99
+ }
100
+ else if (children?.length && depth >= maxDepth) {
101
+ Logger.warn(`跳过嵌套子任务 [${path}]: 超过最大深度 ${maxDepth}`);
102
+ }
103
+ return result;
104
+ };
105
+ const results = [];
106
+ for (let i = 0; i < rootItems.length; i++) {
107
+ const item = rootItems[i];
108
+ const one = await createOne(item, item.parentTaskGuid, `[${i}]`, 0);
109
+ if (one)
110
+ results.push(one);
111
+ }
112
+ return { results, errors };
113
+ }
114
+ async updateTask(taskGuid, params) {
115
+ Logger.info(`更新任务: ${taskGuid}`);
116
+ const task = {};
117
+ const updateFields = [];
118
+ if (params.summary !== undefined) {
119
+ task.summary = params.summary;
120
+ updateFields.push('summary');
121
+ }
122
+ if (params.description !== undefined) {
123
+ task.description = params.description;
124
+ updateFields.push('description');
125
+ }
126
+ if (params.due !== undefined) {
127
+ task.due = params.due;
128
+ updateFields.push('due');
129
+ }
130
+ if (params.completed_at !== undefined) {
131
+ task.completed_at = params.completed_at;
132
+ updateFields.push('completed_at');
133
+ }
134
+ if (params.repeat_rule !== undefined) {
135
+ task.repeat_rule = params.repeat_rule;
136
+ updateFields.push('repeat_rule');
137
+ }
138
+ if (params.start !== undefined) {
139
+ task.start = params.start;
140
+ updateFields.push('start');
141
+ }
142
+ if (params.mode !== undefined) {
143
+ task.mode = params.mode;
144
+ updateFields.push('mode');
145
+ }
146
+ if (params.is_milestone !== undefined) {
147
+ task.is_milestone = params.is_milestone;
148
+ updateFields.push('is_milestone');
149
+ }
150
+ if (updateFields.length === 0) {
151
+ throw new Error('update_task requires at least one field to update');
152
+ }
153
+ return this.patch(`/task/v2/tasks/${taskGuid}`, { task, update_fields: updateFields });
154
+ }
155
+ /**
156
+ * 添加任务成员(负责人或关注人)。单次最多 50 个(去重后);已存在成员会被忽略。
157
+ * POST /task/v2/tasks/:task_guid/add_members
158
+ */
159
+ async addTaskMembers(taskGuid, members) {
160
+ if (!members.length)
161
+ throw new Error('addTaskMembers requires at least one member');
162
+ Logger.info(`添加任务成员: ${taskGuid}, ${members.length} 人`);
163
+ const res = await this.post(`/task/v2/tasks/${taskGuid}/add_members`, { members });
164
+ return res?.data?.task ?? res?.task ?? res;
165
+ }
166
+ /**
167
+ * 移除任务成员(负责人或关注人)。单次 1~500 个;非任务成员会被忽略。
168
+ * POST /task/v2/tasks/:task_guid/remove_members
169
+ */
170
+ async removeTaskMembers(taskGuid, members) {
171
+ if (!members.length)
172
+ throw new Error('removeTaskMembers requires at least one member');
173
+ Logger.info(`移除任务成员: ${taskGuid}, ${members.length} 人`);
174
+ const res = await this.post(`/task/v2/tasks/${taskGuid}/remove_members`, { members });
175
+ return res?.data?.task ?? res?.task ?? res;
176
+ }
177
+ /**
178
+ * 添加任务提醒。任务须已设置截止时间(due);当前每任务仅支持 1 个提醒,已有提醒时需先调用 removeTaskReminders。
179
+ * POST /task/v2/tasks/:task_guid/add_reminders
180
+ */
181
+ async addTaskReminder(taskGuid, relativeFireMinute) {
182
+ if (relativeFireMinute < 0)
183
+ throw new Error('addTaskReminder relativeFireMinute must be >= 0');
184
+ Logger.info(`添加任务提醒: ${taskGuid}, 截止前 ${relativeFireMinute} 分钟`);
185
+ const res = await this.post(`/task/v2/tasks/${taskGuid}/add_reminders`, {
186
+ reminders: [{ relative_fire_minute: relativeFireMinute }],
187
+ });
188
+ return res?.data?.task ?? res?.task ?? res;
189
+ }
190
+ /**
191
+ * 移除任务提醒。reminder_ids 来自任务详情的 reminders[].id;若提醒不存在会直接返回成功。
192
+ * POST /task/v2/tasks/:task_guid/remove_reminders
193
+ */
194
+ async removeTaskReminders(taskGuid, reminderIds) {
195
+ if (!reminderIds.length)
196
+ throw new Error('removeTaskReminders requires at least one reminder id');
197
+ Logger.info(`移除任务提醒: ${taskGuid}, ${reminderIds.length} 个`);
198
+ const res = await this.post(`/task/v2/tasks/${taskGuid}/remove_reminders`, { reminder_ids: reminderIds });
199
+ return res?.data?.task ?? res?.task ?? res;
200
+ }
201
+ /**
202
+ * 删除任务。删除后无法再获取。需任务可编辑权限。
203
+ * DELETE /task/v2/tasks/:task_guid
204
+ */
205
+ async deleteTask(taskGuid) {
206
+ Logger.info(`删除任务: ${taskGuid}`);
207
+ await this.delete(`/task/v2/tasks/${taskGuid}`);
208
+ }
209
+ /** 列取“我负责的”任务,分页。GET /task/v2/tasks,需 user_access_token。 */
210
+ async listTasks(pageToken, completed, pageSize = 50) {
211
+ const params = { page_size: pageSize, type: 'my_tasks' };
212
+ if (pageToken)
213
+ params.page_token = pageToken;
214
+ if (completed !== undefined)
215
+ params.completed = completed;
216
+ const res = await this.get('/task/v2/tasks', params);
217
+ return {
218
+ items: res?.items ?? [],
219
+ page_token: res?.page_token,
220
+ has_more: Boolean(res?.has_more),
221
+ };
222
+ }
223
+ /** 精简任务项,仅保留常用字段,减少干扰。 */
224
+ static slimTaskItem(task) {
225
+ if (!task || typeof task !== 'object')
226
+ return task;
227
+ return {
228
+ guid: task.guid,
229
+ summary: task.summary,
230
+ description: task.description,
231
+ due: task.due,
232
+ reminders: task.reminders,
233
+ creator: task.creator,
234
+ members: task.members,
235
+ completed_at: task.completed_at,
236
+ status: task.status,
237
+ task_id: task.task_id,
238
+ created_at: task.created_at,
239
+ updated_at: task.updated_at,
240
+ url: task.url,
241
+ start: task.start,
242
+ repeat_rule: task.repeat_rule,
243
+ parent_task_guid: task.parent_task_guid,
244
+ subtask_count: task.subtask_count,
245
+ is_milestone: task.is_milestone,
246
+ mode: task.mode,
247
+ };
248
+ }
249
+ /** 列取任务,每次拉取 2 页(共最多 100 条),返回精简后的 items。 */
250
+ async listTasksTwoPages(pageToken, completed) {
251
+ const PAGE_SIZE = 50;
252
+ const first = await this.listTasks(pageToken, completed, PAGE_SIZE);
253
+ const items = first.items.map(FeishuTaskService.slimTaskItem);
254
+ let nextToken = first.page_token;
255
+ let hasMore = first.has_more;
256
+ if (first.has_more && first.items.length === PAGE_SIZE) {
257
+ const second = await this.listTasks(first.page_token, completed, PAGE_SIZE);
258
+ items.push(...second.items.map(FeishuTaskService.slimTaskItem));
259
+ nextToken = second.page_token;
260
+ hasMore = second.has_more;
261
+ }
262
+ return { items, page_token: nextToken, has_more: hasMore };
263
+ }
264
+ }
265
+ /** subTasks 最大嵌套层数,防止递归过深。 */
266
+ Object.defineProperty(FeishuTaskService, "DEFAULT_MAX_NESTING_DEPTH", {
267
+ enumerable: true,
268
+ configurable: true,
269
+ writable: true,
270
+ value: 10
271
+ });
@@ -0,0 +1,98 @@
1
+ import { createTasks, listTasks, updateTask, deleteTasks } from '../taskToolApi.js';
2
+ function createMockApi() {
3
+ const calls = [];
4
+ return {
5
+ createTasksNested: async (rootItems) => {
6
+ calls.push({ method: 'createTasksNested', args: [rootItems] });
7
+ return { results: rootItems.map((r) => ({ task: { guid: 'mock', summary: r.summary } })), errors: [] };
8
+ },
9
+ listTasks: async (pageToken, completed) => {
10
+ calls.push({ method: 'listTasks', args: [pageToken, completed] });
11
+ return { items: [], has_more: false };
12
+ },
13
+ updateTask: async (taskGuid, params) => {
14
+ calls.push({ method: 'updateTask', args: [taskGuid, params] });
15
+ return { task: { guid: taskGuid } };
16
+ },
17
+ addTaskMembers: async () => ({}),
18
+ removeTaskMembers: async () => ({}),
19
+ addTaskReminder: async () => ({}),
20
+ removeTaskReminders: async () => ({}),
21
+ deleteTasks: async (guids) => {
22
+ calls.push({ method: 'deleteTasks', args: [guids] });
23
+ return { deleted: guids, errors: [] };
24
+ },
25
+ get _calls() {
26
+ return calls;
27
+ },
28
+ };
29
+ }
30
+ describe('taskToolApi', () => {
31
+ describe('createTasks', () => {
32
+ it('应校验参数并转换 TaskCreateItem 为 RootNestedCreateItem', async () => {
33
+ const mockApi = createMockApi();
34
+ const taskItems = [
35
+ {
36
+ summary: '任务1',
37
+ assigneeIds: ['ou_xxx'],
38
+ description: '描述',
39
+ },
40
+ ];
41
+ const result = await createTasks(taskItems, mockApi);
42
+ const calls = mockApi._calls;
43
+ expect(calls).toHaveLength(1);
44
+ expect(calls[0].method).toBe('createTasksNested');
45
+ const rootItems = calls[0].args[0];
46
+ expect(rootItems).toHaveLength(1);
47
+ expect(rootItems[0].summary).toBe('任务1');
48
+ expect(rootItems[0].members).toEqual([{ id: 'ou_xxx', type: 'user', role: 'assignee' }]);
49
+ expect(rootItems[0].description).toBe('描述');
50
+ expect(result.results).toHaveLength(1);
51
+ });
52
+ it('空数组应抛出校验错误', async () => {
53
+ const mockApi = createMockApi();
54
+ await expect(createTasks([], mockApi)).rejects.toThrow(/参数校验失败/);
55
+ expect(mockApi._calls).toHaveLength(0);
56
+ });
57
+ });
58
+ describe('listTasks', () => {
59
+ it('应透传 pageToken 和 completed', async () => {
60
+ const mockApi = createMockApi();
61
+ await listTasks({ pageToken: 'pt', completed: true }, mockApi);
62
+ const calls = mockApi._calls;
63
+ expect(calls[0].args).toEqual(['pt', true]);
64
+ });
65
+ });
66
+ describe('updateTask', () => {
67
+ it('至少需要一项更新', async () => {
68
+ const mockApi = createMockApi();
69
+ await expect(updateTask({ taskGuid: 'g1' }, mockApi)).rejects.toThrow(/至少需要一项更新/);
70
+ });
71
+ it('应正确转换字段更新', async () => {
72
+ const mockApi = createMockApi();
73
+ await updateTask({
74
+ taskGuid: 'g1',
75
+ summary: '新标题',
76
+ dueTimestamp: '123',
77
+ isDueAllDay: true,
78
+ }, mockApi);
79
+ const calls = mockApi._calls;
80
+ expect(calls[0].args).toEqual([
81
+ 'g1',
82
+ { summary: '新标题', due: { timestamp: '123', is_all_day: true } },
83
+ ]);
84
+ });
85
+ });
86
+ describe('deleteTasks', () => {
87
+ it('应校验 taskGuids 并调用 deleteTasks', async () => {
88
+ const mockApi = createMockApi();
89
+ const result = await deleteTasks(['g1'], mockApi);
90
+ expect(mockApi._calls[0].args[0]).toEqual(['g1']);
91
+ expect(result.deleted).toEqual(['g1']);
92
+ });
93
+ it('空数组应抛出校验错误', async () => {
94
+ const mockApi = createMockApi();
95
+ await expect(deleteTasks([], mockApi)).rejects.toThrow(/参数校验失败/);
96
+ });
97
+ });
98
+ });
@@ -0,0 +1 @@
1
+ export { createTasks, listTasks, updateTask, deleteTasks } from './taskToolApi.js';
@@ -0,0 +1,128 @@
1
+ import { Logger } from '../../../utils/logger.js';
2
+ import { TaskCreateBatchSchema, TaskDeleteBatchSchema, } from '../../../types/taskSchema.js';
3
+ function toNested(item) {
4
+ const members = [];
5
+ if (item.assigneeIds?.length) {
6
+ members.push(...item.assigneeIds.map((id) => ({ id, type: 'user', role: 'assignee' })));
7
+ }
8
+ if (item.followerIds?.length) {
9
+ members.push(...item.followerIds.map((id) => ({ id, type: 'user', role: 'follower' })));
10
+ }
11
+ return {
12
+ summary: item.summary,
13
+ ...(item.parentTaskGuid !== undefined && { parentTaskGuid: item.parentTaskGuid }),
14
+ ...(item.description !== undefined && { description: item.description }),
15
+ ...(item.dueTimestamp !== undefined && { due: { timestamp: item.dueTimestamp, is_all_day: item.isDueAllDay } }),
16
+ ...(item.completedAt !== undefined && { completed_at: item.completedAt }),
17
+ ...(members.length > 0 && { members }),
18
+ ...(item.repeatRule !== undefined && { repeat_rule: item.repeatRule }),
19
+ ...(item.startTimestamp !== undefined && {
20
+ start: { timestamp: item.startTimestamp, is_all_day: item.isStartAllDay },
21
+ }),
22
+ ...(item.mode !== undefined && { mode: item.mode }),
23
+ ...(item.isMilestone !== undefined && { is_milestone: item.isMilestone }),
24
+ ...(item.subTasks?.length && { subTasks: item.subTasks.map(toNested) }),
25
+ };
26
+ }
27
+ /**
28
+ * 批量创建飞书任务(含嵌套子任务)
29
+ */
30
+ export async function createTasks(taskItems, api) {
31
+ const parsed = TaskCreateBatchSchema.safeParse(taskItems);
32
+ if (!parsed.success) {
33
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
34
+ }
35
+ Logger.info(`createTasks invoked: ${parsed.data.length} top-level items`);
36
+ const rootItems = parsed.data.map(toNested);
37
+ const result = await api.createTasksNested(rootItems);
38
+ return result;
39
+ }
40
+ /**
41
+ * 列取飞书任务(我负责的)
42
+ */
43
+ export async function listTasks(options, api) {
44
+ Logger.info(`listTasks invoked: pageToken=${options?.pageToken ? 'yes' : 'no'}, completed=${options?.completed ?? 'all'}`);
45
+ return api.listTasks(options?.pageToken, options?.completed);
46
+ }
47
+ /**
48
+ * 更新飞书任务
49
+ */
50
+ export async function updateTask(params, api) {
51
+ const { taskGuid, ...rest } = params;
52
+ const hasFieldUpdate = rest.summary !== undefined ||
53
+ rest.description !== undefined ||
54
+ rest.dueTimestamp !== undefined ||
55
+ rest.completedAt !== undefined ||
56
+ rest.repeatRule !== undefined ||
57
+ rest.startTimestamp !== undefined ||
58
+ rest.mode !== undefined ||
59
+ rest.isMilestone !== undefined;
60
+ const hasAdd = (rest.addAssigneeIds?.length ?? 0) + (rest.addFollowerIds?.length ?? 0) > 0;
61
+ const hasRemove = (rest.removeAssigneeIds?.length ?? 0) + (rest.removeFollowerIds?.length ?? 0) > 0;
62
+ const hasAddReminder = rest.addReminderRelativeMinutes !== undefined;
63
+ const hasRemoveReminder = (rest.removeReminderIds?.length ?? 0) > 0;
64
+ if (!hasFieldUpdate && !hasAdd && !hasRemove && !hasAddReminder && !hasRemoveReminder) {
65
+ throw new Error('至少需要一项更新:summary/description/due/completed_at/repeat_rule/start/mode/is_milestone、成员 add/remove、或提醒 addReminderRelativeMinutes/removeReminderIds');
66
+ }
67
+ Logger.info(`updateTask invoked: taskGuid=${taskGuid}`);
68
+ let result;
69
+ if (hasFieldUpdate) {
70
+ const updateParams = {};
71
+ if (rest.summary !== undefined)
72
+ updateParams.summary = rest.summary;
73
+ if (rest.description !== undefined)
74
+ updateParams.description = rest.description;
75
+ if (rest.dueTimestamp !== undefined)
76
+ updateParams.due = { timestamp: rest.dueTimestamp, is_all_day: rest.isDueAllDay };
77
+ if (rest.completedAt !== undefined)
78
+ updateParams.completed_at = rest.completedAt;
79
+ if (rest.repeatRule !== undefined)
80
+ updateParams.repeat_rule = rest.repeatRule;
81
+ if (rest.startTimestamp !== undefined)
82
+ updateParams.start = { timestamp: rest.startTimestamp, is_all_day: rest.isStartAllDay };
83
+ if (rest.mode !== undefined)
84
+ updateParams.mode = rest.mode;
85
+ if (rest.isMilestone !== undefined)
86
+ updateParams.is_milestone = rest.isMilestone;
87
+ result = await api.updateTask(taskGuid, updateParams);
88
+ }
89
+ if (hasAdd) {
90
+ const members = [];
91
+ if (rest.addAssigneeIds?.length) {
92
+ members.push(...rest.addAssigneeIds.map((id) => ({ id, type: 'user', role: 'assignee' })));
93
+ }
94
+ if (rest.addFollowerIds?.length) {
95
+ members.push(...rest.addFollowerIds.map((id) => ({ id, type: 'user', role: 'follower' })));
96
+ }
97
+ result = await api.addTaskMembers(taskGuid, members);
98
+ }
99
+ if (hasRemove) {
100
+ const members = [];
101
+ if (rest.removeAssigneeIds?.length) {
102
+ members.push(...rest.removeAssigneeIds.map((id) => ({ id, type: 'user', role: 'assignee' })));
103
+ }
104
+ if (rest.removeFollowerIds?.length) {
105
+ members.push(...rest.removeFollowerIds.map((id) => ({ id, type: 'user', role: 'follower' })));
106
+ }
107
+ result = await api.removeTaskMembers(taskGuid, members);
108
+ }
109
+ if (hasRemoveReminder) {
110
+ result = await api.removeTaskReminders(taskGuid, rest.removeReminderIds);
111
+ }
112
+ if (hasAddReminder) {
113
+ result = await api.addTaskReminder(taskGuid, rest.addReminderRelativeMinutes);
114
+ }
115
+ const taskEntity = result != null && typeof result === 'object' && 'task' in result && result.task != null ? result.task : result;
116
+ return taskEntity;
117
+ }
118
+ /**
119
+ * 批量删除飞书任务
120
+ */
121
+ export async function deleteTasks(taskGuids, api) {
122
+ const parsed = TaskDeleteBatchSchema.safeParse(taskGuids);
123
+ if (!parsed.success) {
124
+ throw new Error(`参数校验失败: ${parsed.error.message}`);
125
+ }
126
+ Logger.info(`deleteTasks invoked: ${parsed.data.length} tasks`);
127
+ return api.deleteTasks(parsed.data);
128
+ }