Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/feishu/tools/__tests__/task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,62 @@ describe('feishu_task tool', () => {
});
});

// ============================================================
// create with requesterId — 自动将发起人加为关注者
// ============================================================

describe('feishu_task tool with requesterId', () => {
let handlerWithRequester: (args: Record<string, unknown>) => Promise<unknown>;

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)
// ============================================================
Expand Down
3 changes: 2 additions & 1 deletion src/feishu/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion src/feishu/tools/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined>) {
export function feishuTaskTool(getUserToken?: () => Promise<string | undefined>, requesterId?: string) {
return tool(
'feishu_task',
[
Expand Down Expand Up @@ -200,6 +200,15 @@ export function feishuTaskTool(getUserToken?: () => Promise<string | undefined>)
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);
}
Expand Down
Loading