feat(electron-mcp): 对齐 dootask-mcp,新增 send_task_ai_message 等

- 新增 send_task_ai_message 工具(dialog/msg/send_ai_assistant),支持
  自定义发送者昵称 nickname(≤20)与 silence
- complete_task 增加 flow_item_id 参数及多结束状态(-4005)重选处理
- update_task 增加 flow_item_id 参数及多结束/开始状态(-4005/-4006)处理
- request() 捕获 ret/data 并对 -4005/-4006 放行交工具处理(向后兼容)
- 同步头部工具清单注释(27→29)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-02 07:10:36 +00:00
parent c706c515ee
commit 4eab130313

125
electron/lib/mcp.js vendored
View File

@ -4,7 +4,7 @@
* DooTask Electron 客户端集成了 Model Context Protocol (MCP) 服务
* 允许 AI 助手( Claude)直接与 DooTask 任务进行交互
*
* 提供的工具 27 :
* 提供的工具 29 :
*
* === 用户管理 ===
* - get_users_basic - 批量获取用户基础信息1-50便于匹配负责人/协助人
@ -43,6 +43,7 @@
* === 消息通知 ===
* - search_dialogs - 按名称搜索群聊或联系人返回 dialog_id/userid
* - send_message - 发送消息到对话支持 dialog_id userid
* - send_task_ai_message - 以AI助手身份发送消息到任务对话支持自定义发送者昵称
* - get_message_list - 获取对话消息记录支持 dialog_id userid
*
* === 智能搜索 ===
@ -228,7 +229,7 @@ class DooTaskMCP {
return { error: 'Result contains non-serializable data' };
}
} catch (error) {
return { error: error.msg || error.message || String(error) || 'API request failed' };
return { error: error.msg || error.message || String(error) || 'API request failed', ret: error.ret, data: error.data };
}
})()
`);
@ -242,6 +243,10 @@ class DooTaskMCP {
const result = await Promise.race([executePromise, timeoutPromise]);
if (result && result.error) {
// 多结束/开始状态(-4005/-4006):保留 ret 与 flow_items 交给工具处理,不直接抛错
if (result.ret === -4005 || result.ret === -4006) {
return result;
}
throw new Error(result.error);
}
@ -591,14 +596,38 @@ class DooTaskMCP {
task_id: z.number()
.min(1)
.describe('要标记完成的任务ID'),
flow_item_id: z.number()
.optional()
.describe('工作流状态ID'),
}),
execute: async (params) => {
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
const result = await this.request('POST', 'project/task/update', {
const requestData = {
task_id: params.task_id,
complete_at: now,
});
};
if (params.flow_item_id) {
requestData.flow_item_id = params.flow_item_id;
}
const result = await this.request('POST', 'project/task/update', requestData);
// 处理多结束状态的情况
if (result.ret === -4005) {
const flowItems = result.data?.flow_items || [];
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
message: '存在多个结束状态请选择要使用的状态后重新调用此工具并指定flow_item_id参数',
task_id: params.task_id,
flow_items: flowItems,
}, null, 2)
}]
};
}
if (result.error) {
throw new Error(result.error);
@ -720,6 +749,9 @@ class DooTaskMCP {
complete_at: z.union([z.string(), z.boolean()])
.optional()
.describe('完成时间。传时间字符串标记完成传false标记未完成'),
flow_item_id: z.number()
.optional()
.describe('工作流状态ID'),
}),
execute: async (params) => {
const requestData = {
@ -734,9 +766,42 @@ class DooTaskMCP {
if (params.start_at !== undefined) requestData.start_at = params.start_at;
if (params.end_at !== undefined) requestData.end_at = params.end_at;
if (params.complete_at !== undefined) requestData.complete_at = params.complete_at;
if (params.flow_item_id !== undefined) requestData.flow_item_id = params.flow_item_id;
const result = await this.request('POST', 'project/task/update', requestData);
// 处理多结束状态的情况(标记完成时)
if (result.ret === -4005) {
const flowItems = result.data?.flow_items || [];
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
message: '存在多个结束状态请选择要使用的状态后重新调用此工具并指定flow_item_id参数',
task_id: params.task_id,
flow_items: flowItems,
}, null, 2)
}]
};
}
// 处理多开始状态的情况(取消完成时)
if (result.ret === -4006) {
const flowItems = result.data?.flow_items || [];
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
message: '存在多个开始状态请选择要使用的状态后重新调用此工具并指定flow_item_id参数',
task_id: params.task_id,
flow_items: flowItems,
}, null, 2)
}]
};
}
if (result.error) {
throw new Error(result.error);
}
@ -1291,6 +1356,58 @@ class DooTaskMCP {
}
});
// 以AI助手身份发送消息到任务对话
this.mcp.addTool({
name: 'send_task_ai_message',
description: '以AI助手身份发送消息到任务对话。应在每个重要里程碑、遇到阻塞、以及全部完成时主动调用。',
parameters: z.object({
task_id: z.number()
.describe('目标任务ID'),
text: z.string()
.min(1)
.describe('消息内容,支持 Markdown'),
nickname: z.string()
.max(20)
.optional()
.describe('自定义发送者昵称最多20字不传或留空时默认显示“AI 助手”'),
silence: z.boolean()
.optional()
.describe('静默发送,不触发推送提醒'),
}),
execute: async (params) => {
const payload = {
task_id: params.task_id,
text: params.text,
text_type: 'md',
};
if (params.nickname !== undefined) {
payload.nickname = params.nickname;
}
if (params.silence !== undefined) {
payload.silence = params.silence ? 'yes' : 'no';
}
const result = await this.request('POST', 'dialog/msg/send_ai_assistant', payload);
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
task_id: params.task_id,
message: result.data,
}, null, 2)
}]
};
}
});
// 获取对话消息列表
this.mcp.addTool({
name: 'get_message_list',