diff --git a/src/feishu/tools/__tests__/task.test.ts b/src/feishu/tools/__tests__/task.test.ts index a0661c6c..0fd05d45 100644 --- a/src/feishu/tools/__tests__/task.test.ts +++ b/src/feishu/tools/__tests__/task.test.ts @@ -593,6 +593,62 @@ describe('feishu_task tool', () => { }); }); +// ============================================================ +// create with requesterId — 自动将发起人加为关注者 +// ============================================================ + +describe('feishu_task tool with requesterId', () => { + let handlerWithRequester: (args: Record) => Promise; + + beforeEach(() => { + vi.clearAllMocks(); + feishuTaskTool(undefined, 'ou_requester_001'); + handlerWithRequester = capturedHandler; + }); + + it('should auto-add requester as follower when no members specified', async () => { + mockTaskCreate.mockResolvedValue({ + code: 0, + data: { task: { guid: 'TASK_AUTO', summary: '自动关注' } }, + }); + await handlerWithRequester({ action: 'create', summary: '自动关注' }); + const callData = mockTaskCreate.mock.calls[0][0].data; + expect(callData.members).toEqual([{ id: 'ou_requester_001', role: 'follower' }]); + }); + + it('should auto-add requester as follower alongside existing members', async () => { + mockTaskCreate.mockResolvedValue({ + code: 0, + data: { task: { guid: 'TASK_AUTO2', summary: '混合成员' } }, + }); + await handlerWithRequester({ + action: 'create', + summary: '混合成员', + members: '[{"id": "ou_other", "role": "assignee"}]', + }); + const callData = mockTaskCreate.mock.calls[0][0].data; + expect(callData.members).toEqual([ + { id: 'ou_other', role: 'assignee' }, + { id: 'ou_requester_001', role: 'follower' }, + ]); + }); + + it('should not duplicate requester if already in members', async () => { + mockTaskCreate.mockResolvedValue({ + code: 0, + data: { task: { guid: 'TASK_AUTO3', summary: '已存在' } }, + }); + await handlerWithRequester({ + action: 'create', + summary: '已存在', + members: '[{"id": "ou_requester_001", "role": "assignee"}]', + }); + const callData = mockTaskCreate.mock.calls[0][0].data; + // Should only have the original entry, not a duplicate + expect(callData.members).toEqual([{ id: 'ou_requester_001', role: 'assignee' }]); + }); +}); + // ============================================================ // list with user_access_token (Task v2 API) // ============================================================ diff --git a/src/feishu/tools/index.ts b/src/feishu/tools/index.ts index 18035dfc..6820805e 100644 --- a/src/feishu/tools/index.ts +++ b/src/feishu/tools/index.ts @@ -27,8 +27,9 @@ export function createFeishuToolsMcpServer(chatId?: string, userId?: string) { if (config.feishu.tools.chat) tools.push(feishuChatTool(chatId)); if (config.feishu.tools.contact) tools.push(feishuContactTool()); // 通过闭包绑定当前用户的 token 获取函数,task 工具可透明使用 user_access_token + // 同时传入 userId,用于创建任务时自动将发起人加为关注者 const getUserToken = userId ? () => getValidUserToken(userId) : undefined; - if (config.feishu.tools.task) tools.push(feishuTaskTool(getUserToken)); + if (config.feishu.tools.task) tools.push(feishuTaskTool(getUserToken, userId)); // 边界条件修复 (review 反馈): 所有子开关全 false 时不注入空 MCP 服务器 if (tools.length === 0) return undefined; diff --git a/src/feishu/tools/task.ts b/src/feishu/tools/task.ts index ef896be4..c8bed4bb 100644 --- a/src/feishu/tools/task.ts +++ b/src/feishu/tools/task.ts @@ -133,7 +133,7 @@ function validateTasklists(jsonStr: string): Array<{ tasklist_guid: string; sect * 有 user token 时 list 使用 Task v2 API(支持查看用户个人任务); * 无 user token 时降级为 Task v1 API(仅 bot 创建的任务)。 */ -export function feishuTaskTool(getUserToken?: () => Promise) { +export function feishuTaskTool(getUserToken?: () => Promise, requesterId?: string) { return tool( 'feishu_task', [ @@ -200,6 +200,15 @@ export function feishuTaskTool(getUserToken?: () => Promise) if (args.members) { data.members = validateMembers(args.members); } + // 自动将发起请求的用户加为关注者(如果尚未在 members 中) + if (requesterId) { + const members = (data.members ?? []) as Array<{ id: string; role: string; type?: string }>; + const alreadyIncluded = members.some((m) => m.id === requesterId); + if (!alreadyIncluded) { + members.push({ id: requesterId, role: 'follower' }); + data.members = members; + } + } if (args.tasklists) { data.tasklists = validateTasklists(args.tasklists); }