1756 lines
69 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* DooTask MCP Server
*
* DooTask 的 Electron 客户端集成了 Model Context Protocol (MCP) 服务,
* 允许 AI 助手(如 Claude)直接与 DooTask 任务进行交互。
*
* 提供的工具(共 21 个):
*
* === 用户管理 ===
* - get_users_basic - 根据用户ID列表获取基础信息便于匹配负责人/协助人
* - search_user - 按关键字或项目筛选用户,支持分页与更多过滤项
*
* === 任务管理 ===
* - list_tasks - 获取任务列表,支持按状态/项目/主任务筛选、搜索、分页
* - get_task - 获取任务详情,包含完整内容、负责人、协助人员、标签等所有信息
* - complete_task - 快速标记任务完成
* - create_task - 创建新任务
* - update_task - 更新任务,支持修改名称、内容、负责人、时间、状态等所有属性
* - create_sub_task - 为指定主任务创建子任务
* - get_task_files - 获取任务附件列表
* - delete_task - 删除或还原任务
*
* === 项目管理 ===
* - list_projects - 获取项目列表,支持按归档状态筛选、搜索
* - get_project - 获取项目详情,包含列(看板列)、成员等完整信息
* - create_project - 创建新项目
* - update_project - 修改项目信息(名称、描述等)
*
* === 工作报告 ===
* - list_received_reports - 获取我接收的汇报列表,支持按类型/状态/部门/时间筛选
* - get_report_detail - 获取汇报详情包括完整内容、汇报人、接收人、AI分析等
* - generate_report_template - 基于任务完成情况自动生成汇报模板(已完成/未完成任务)
* - create_report - 创建并提交工作汇报
* - list_my_reports - 获取我发送的汇报列表,支持按类型/时间筛选
* - mark_reports_read - 批量标记汇报为已读或未读状态
*
* === 消息通知 ===
* - send_message_to_user - 给指定用户发送私信
* - get_message_list - 获取对话消息或执行关键词搜索
*
* 配置方法:
* {
* "mcpServers": {
* "DooTask": {
* "url": "http://localhost:22224/mcp"
* }
* }
* }
*
* 使用示例:
*
* 任务管理:
* - "查看我未完成的任务"
* - "搜索包含'报告'的任务"
* - "显示任务123的详细信息"
* - "标记任务456为已完成"
* - "在项目1中创建任务完成用户手册负责人100协助人员101"
* - "把任务789的截止时间改为下周五并分配给用户200"
*
* 项目管理:
* - "我有哪些项目?"
* - "查看项目5的详情包括所有列和成员"
*
* 工作报告:
* - "查看未读的工作汇报"
* - "生成本周周报"
* - "查看我上周提交的日报"
* - "把周报提交给用户100和200"
* - "把所有未读报告标记为已读"
*/
const { FastMCP } = require('fastmcp');
const { z } = require('zod');
const loger = require("electron-log");
const TurndownService = require('turndown');
const { marked } = require('marked');
let mcpServer = null;
// 初始化 HTML 转 Markdown 工具
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
bulletListMarker: '-',
emDelimiter: '_',
strongDelimiter: '**',
linkStyle: 'inlined',
preformattedCode: true,
});
// HTML 转 Markdown
function htmlToMarkdown(html) {
if (!html) {
return '';
}
if (typeof html !== 'string') {
loger.warn(`HTML to Markdown: expected string, got ${typeof html}`);
return '';
}
try {
const markdown = turndownService.turndown(html);
return markdown.trim();
} catch (error) {
loger.error(`HTML to Markdown conversion failed: ${error.message}`, { html: html.substring(0, 100) });
// 返回清理后的纯文本作为降级方案
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}
}
// Markdown 转 HTML
function markdownToHtml(markdown) {
if (!markdown) {
return '';
}
if (typeof markdown !== 'string') {
loger.warn(`Markdown to HTML: expected string, got ${typeof markdown}`);
return '';
}
try {
// marked.parse 在某些版本可能返回 Promise,这里使用同步方法
const html = marked.parse(markdown, { async: false });
return html;
} catch (error) {
loger.error(`Markdown to HTML conversion failed: ${error.message}`, { markdown: markdown.substring(0, 100) });
// 返回原始 markdown 作为降级方案,至少保留内容
return markdown.replace(/\n/g, '<br>');
}
}
class DooTaskMCP {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.mcp = new FastMCP({
name: 'DooTask MCP Server',
version: '1.0.0',
description: 'DooTask 任务管理 MCP 接口',
});
this.setupTools();
}
/**
* 调用接口
*/
async request(method, path, data = {}) {
try {
// 通过主窗口执行前端代码来调用API
if (!this.mainWindow || !this.mainWindow.webContents) {
throw new Error('Main window not available, please open DooTask application first');
}
// 检查 webContents 是否 ready
if (this.mainWindow.webContents.isLoading()) {
loger.warn(`[MCP] WebContents is loading, waiting...`);
await new Promise(resolve => {
this.mainWindow.webContents.once('did-finish-load', resolve);
});
}
// 通过前端已有的API调用机制添加超时处理
const executePromise = this.mainWindow.webContents.executeJavaScript(`
(async () => {
try {
// 检查API是否可用
if (typeof $A === 'undefined') {
return { error: 'API not available - $A is undefined' };
}
if (typeof $A.apiCall !== 'function') {
return { error: 'API not available - $A.apiCall is not a function' };
}
// 调用 API
const result = await $A.apiCall({
url: '${path}',
data: ${JSON.stringify(data)},
method: '${method.toLowerCase()}',
});
try {
return { data: JSON.parse(JSON.stringify(result.data)) };
} catch (serError) {
return { error: 'Result contains non-serializable data' };
}
} catch (error) {
return { error: error.msg || error.message || String(error) || 'API request failed' };
}
})()
`);
// 添加超时处理30秒
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout after 30 seconds')), 30000);
});
// 返回结果
const result = await Promise.race([executePromise, timeoutPromise]);
if (result && result.error) {
throw new Error(result.error);
}
return result;
} catch (error) {
throw error;
}
}
// 设置 MCP 工具
setupTools() {
// 用户管理:获取用户基础信息
this.mcp.addTool({
name: 'get_users_basic',
description: '根据用户ID列表获取用户基础信息昵称、邮箱、头像等方便在分配任务前确认成员身份。',
parameters: z.object({
userids: z.array(z.number())
.min(1)
.max(50)
.describe('用户ID数组至少1个最多50个'),
}),
execute: async (params) => {
const ids = params.userids;
const requestData = {
userid: ids.length === 1 ? ids[0] : JSON.stringify(ids),
};
const result = await this.request('GET', 'users/basic', requestData);
if (result.error) {
throw new Error(result.error);
}
const rawList = Array.isArray(result.data)
? result.data
: (Array.isArray(result.data?.data) ? result.data.data : []);
const users = rawList.map(user => ({
userid: user.userid,
nickname: user.nickname || '',
email: user.email || '',
avatar: user.avatar || '',
identity: user.identity || '',
department: user.department || '',
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
count: users.length,
users: users,
}, null, 2)
}]
};
}
});
// 用户管理:搜索用户
this.mcp.addTool({
name: 'search_user',
description: '按关键词搜索或筛选用户,支持按项目/对话过滤并返回分页结果。',
parameters: z.object({
keyword: z.string()
.min(1)
.describe('搜索关键词,支持昵称、邮箱、拼音等'),
project_id: z.number()
.optional()
.describe('仅返回指定项目的成员'),
dialog_id: z.number()
.optional()
.describe('仅返回指定对话的成员'),
include_disabled: z.boolean()
.optional()
.describe('是否同时包含已离职/禁用用户'),
include_bot: z.boolean()
.optional()
.describe('是否同时包含机器人账号'),
with_department: z.boolean()
.optional()
.describe('是否返回部门信息'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20最大100'),
}),
execute: async (params) => {
const page = params.page && params.page > 0 ? params.page : 1;
const pagesize = params.pagesize && params.pagesize > 0 ? Math.min(params.pagesize, 100) : 20;
const keys = {
key: params.keyword,
};
if (params.project_id !== undefined) {
keys.project_id = params.project_id;
}
if (params.dialog_id !== undefined) {
keys.dialog_id = params.dialog_id;
}
if (params.include_disabled) {
keys.disable = 2;
}
if (params.include_bot) {
keys.bot = 2;
}
const requestData = {
page,
pagesize,
keys,
};
if (params.with_department) {
requestData.with_department = 1;
}
const result = await this.request('GET', 'users/search', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
let users = [];
let total = 0;
let perPage = pagesize;
let currentPage = page;
if (Array.isArray(data.data)) {
users = data.data;
total = data.total ?? users.length;
perPage = data.per_page ?? perPage;
currentPage = data.current_page ?? currentPage;
} else if (Array.isArray(data)) {
users = data;
total = users.length;
}
const simplified = users.map(user => ({
userid: user.userid,
nickname: user.nickname || '',
email: user.email || '',
tags: user.tags || [],
department: user.department_info || user.department || '',
online: user.online ?? null,
identity: user.identity || '',
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total,
page: currentPage,
pagesize: perPage,
users: simplified,
}, null, 2)
}]
};
}
});
// 获取任务列表
this.mcp.addTool({
name: 'list_tasks',
description: '获取任务列表。可以按状态筛选(已完成/未完成)、搜索任务名称、按时间范围筛选等。',
parameters: z.object({
status: z.enum(['all', 'completed', 'uncompleted'])
.optional()
.describe('任务状态: all(所有), completed(已完成), uncompleted(未完成)'),
search: z.string()
.optional()
.describe('搜索关键词(可搜索任务ID、名称、描述)'),
time: z.string()
.optional()
.describe('时间范围: today(今天), week(本周), month(本月), year(今年), 自定义时间范围,如2025-12-12,2025-12-30'),
project_id: z.number()
.optional()
.describe('项目ID,只获取指定项目的任务'),
parent_id: z.number()
.optional()
.describe('主任务ID。大于0:获取该主任务的子任务; -1:仅获取主任务; 不传:所有任务'),
page: z.number()
.optional()
.describe('页码,默认1'),
pagesize: z.number()
.optional()
.describe('每页数量,默认20,最大100'),
}),
execute: async (params) => {
const requestData = {
page: params.page || 1,
pagesize: params.pagesize || 20,
};
// 构建 keys 对象用于筛选
const keys = {};
if (params.search) {
keys.name = params.search;
}
if (params.status && params.status !== 'all') {
keys.status = params.status;
}
if (Object.keys(keys).length > 0) {
requestData.keys = keys;
}
// 其他筛选参数
if (params.time !== undefined) {
requestData.time = params.time;
}
if (params.project_id !== undefined) {
requestData.project_id = params.project_id;
}
if (params.parent_id !== undefined) {
requestData.parent_id = params.parent_id;
}
const result = await this.request('GET', 'project/task/lists', requestData);
if (result.error) {
throw new Error(result.error);
}
const tasks = result.data.data.map(task => ({
id: task.id,
name: task.name,
desc: task.desc || '无描述',
dialog_id: task.dialog_id,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
end_at: task.end_at || '无截止时间',
project_id: task.project_id,
project_name: task.project_name || '',
column_name: task.column_name || '',
parent_id: task.parent_id,
owners: task.taskUser?.filter(u => u.owner === 1).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
sub_num: task.sub_num || 0,
sub_complete: task.sub_complete || 0,
percent: task.percent || 0,
created_at: task.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total: result.data.total,
page: result.data.current_page,
pagesize: result.data.per_page,
tasks: tasks,
}, null, 2)
}]
};
}
});
// 获取任务详情
this.mcp.addTool({
name: 'get_task',
description: '获取指定任务的详细信息,包括任务描述、完整内容、负责人、协助人员、标签、时间等所有信息。返回的 content 字段为 Markdown 格式。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
}),
execute: async (params) => {
const taskResult = await this.request('GET', 'project/task/one', {
task_id: params.task_id,
});
if (taskResult.error) {
throw new Error(taskResult.error);
}
const task = taskResult.data;
// 获取任务完整内容
let fullContent = task.desc || '无描述';
try {
const contentResult = await this.request('GET', 'project/task/content', {
task_id: params.task_id,
});
if (contentResult && contentResult.data) {
if (typeof contentResult.data === 'object' && contentResult.data.content) {
fullContent = contentResult.data.content;
} else if (typeof contentResult.data === 'string') {
fullContent = contentResult.data;
}
}
} catch (error) {
loger.warn(`Failed to get task content: ${error.message}`);
}
// 将 HTML 内容转换为 Markdown
fullContent = htmlToMarkdown(fullContent);
const taskDetail = {
id: task.id,
name: task.name,
desc: task.desc || '无描述',
dialog_id: task.dialog_id,
content: fullContent,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
project_id: task.project_id,
project_name: task.project_name,
column_id: task.column_id,
column_name: task.column_name,
parent_id: task.parent_id,
start_at: task.start_at || '无开始时间',
end_at: task.end_at || '无截止时间',
flow_item_id: task.flow_item_id,
flow_item_name: task.flow_item_name,
visibility: task.visibility === 1 ? '公开' : '指定人员',
owners: task.taskUser?.filter(u => u.owner === 1).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
assistants: task.taskUser?.filter(u => u.owner === 0).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
tags: task.taskTag?.map(t => t.name) || [],
created_at: task.created_at,
updated_at: task.updated_at,
};
return {
content: [{
type: 'text',
text: JSON.stringify(taskDetail, null, 2)
}]
};
}
});
// 标记任务完成
this.mcp.addTool({
name: 'complete_task',
description: '快速标记任务完成(自动使用当前时间)。如需指定完成时间或取消完成,请使用 update_task。注意:主任务必须在所有子任务完成后才能标记完成。',
parameters: z.object({
task_id: z.number()
.describe('要标记完成的任务ID'),
}),
execute: async (params) => {
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
const result = await this.request('POST', 'project/task/update', {
task_id: params.task_id,
complete_at: now,
});
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: '任务已标记为完成',
task_id: params.task_id,
complete_at: result.data.complete_at,
}, null, 2)
}]
};
}
});
// 创建任务
this.mcp.addTool({
name: 'create_task',
description: '创建新任务。可以指定任务名称、内容、负责人、时间等信息。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
name: z.string()
.describe('任务名称'),
content: z.string()
.optional()
.describe('任务内容描述(Markdown 格式)'),
owner: z.array(z.number())
.optional()
.describe('负责人用户ID数组'),
assist: z.array(z.number())
.optional()
.describe('协助人员用户ID数组'),
column_id: z.number()
.optional()
.describe('列ID(看板列)'),
start_at: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD HH:mm:ss'),
end_at: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD HH:mm:ss'),
}),
execute: async (params) => {
const requestData = {
project_id: params.project_id,
name: params.name,
};
// 添加可选参数,将 Markdown 转换为 HTML
if (params.content) requestData.content = markdownToHtml(params.content);
if (params.owner) requestData.owner = params.owner;
if (params.assist) requestData.assist = params.assist;
if (params.column_id) requestData.column_id = params.column_id;
if (params.start_at) requestData.start_at = params.start_at;
if (params.end_at) requestData.end_at = params.end_at;
const result = await this.request('POST', 'project/task/add', requestData);
if (result.error) {
throw new Error(result.error);
}
const task = result.data;
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: '任务创建成功',
task: {
id: task.id,
name: task.name,
project_id: task.project_id,
column_id: task.column_id,
created_at: task.created_at,
}
}, null, 2)
}]
};
}
});
// 更新任务
this.mcp.addTool({
name: 'update_task',
description: '更新任务信息。可以修改任务名称、内容、负责人、时间、状态等所有属性。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
name: z.string()
.optional()
.describe('任务名称'),
content: z.string()
.optional()
.describe('任务内容描述(Markdown 格式)'),
owner: z.array(z.number())
.optional()
.describe('负责人用户ID数组'),
assist: z.array(z.number())
.optional()
.describe('协助人员用户ID数组'),
column_id: z.number()
.optional()
.describe('移动到指定列ID'),
start_at: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD HH:mm:ss'),
end_at: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD HH:mm:ss'),
complete_at: z.union([z.string(), z.boolean()])
.optional()
.describe('完成时间。传时间字符串标记完成传false标记未完成'),
}),
execute: async (params) => {
const requestData = {
task_id: params.task_id,
};
// 添加要更新的字段,将 Markdown 转换为 HTML
if (params.name !== undefined) requestData.name = params.name;
if (params.content !== undefined) requestData.content = markdownToHtml(params.content);
if (params.owner !== undefined) requestData.owner = params.owner;
if (params.assist !== undefined) requestData.assist = params.assist;
if (params.column_id !== undefined) requestData.column_id = params.column_id;
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;
const result = await this.request('POST', 'project/task/update', requestData);
if (result.error) {
throw new Error(result.error);
}
const task = result.data;
// 构建更新摘要
const updates = [];
if (params.name !== undefined) updates.push('名称');
if (params.content !== undefined) updates.push('内容');
if (params.owner !== undefined) updates.push('负责人');
if (params.assist !== undefined) updates.push('协助人员');
if (params.column_id !== undefined) updates.push('列');
if (params.start_at !== undefined || params.end_at !== undefined) updates.push('时间');
if (params.complete_at !== undefined) updates.push('完成状态');
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `任务已更新: ${updates.join('、')}`,
task: {
id: task.id,
name: task.name,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
updated_at: task.updated_at,
}
}, null, 2)
}]
};
}
});
// 创建子任务
this.mcp.addTool({
name: 'create_sub_task',
description: '为指定主任务新增子任务,自动继承主任务所属项目与看板列配置。',
parameters: z.object({
task_id: z.number()
.describe('主任务ID'),
name: z.string()
.min(1)
.describe('子任务名称'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/task/addsub', {
task_id: params.task_id,
name: params.name,
});
if (result.error) {
throw new Error(result.error);
}
const subTask = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
sub_task: {
id: subTask.id,
name: subTask.name,
project_id: subTask.project_id,
parent_id: subTask.parent_id,
column_id: subTask.column_id,
start_at: subTask.start_at,
end_at: subTask.end_at,
created_at: subTask.created_at,
}
}, null, 2)
}]
};
}
});
// 获取任务附件
this.mcp.addTool({
name: 'get_task_files',
description: '获取指定任务的附件列表,包含文件名称、大小、下载地址等信息。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/task/files', {
task_id: params.task_id,
});
if (result.error) {
throw new Error(result.error);
}
const files = Array.isArray(result.data) ? result.data : [];
const normalized = files.map(file => ({
id: file.id,
name: file.name,
ext: file.ext,
size: file.size,
url: file.path,
thumb: file.thumb,
userid: file.userid,
download: file.download,
created_at: file.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
task_id: params.task_id,
files: normalized,
}, null, 2)
}]
};
}
});
// 删除或还原任务
this.mcp.addTool({
name: 'delete_task',
description: '删除或还原任务。默认执行删除,可通过 action=recovery 将任务从回收站恢复。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
action: z.enum(['delete', 'recovery'])
.optional()
.describe('操作类型delete(默认) 删除recovery 还原'),
}),
execute: async (params) => {
const action = params.action || 'delete';
const result = await this.request('GET', 'project/task/remove', {
task_id: params.task_id,
type: action,
});
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
action: action,
task_id: params.task_id,
data: result.data,
}, null, 2)
}]
};
}
});
// 获取项目列表
this.mcp.addTool({
name: 'list_projects',
description: '获取项目列表。可以按归档状态筛选、搜索项目名称等。',
parameters: z.object({
archived: z.enum(['no', 'yes', 'all'])
.optional()
.describe('归档状态: no(未归档), yes(已归档), all(全部)默认no'),
search: z.string()
.optional()
.describe('搜索关键词(可搜索项目名称)'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20'),
}),
execute: async (params) => {
const requestData = {
archived: params.archived || 'no',
page: params.page || 1,
pagesize: params.pagesize || 20,
};
// 添加搜索参数
if (params.search) {
requestData.keys = {
name: params.search
};
}
const result = await this.request('GET', 'project/lists', requestData);
if (result.error) {
throw new Error(result.error);
}
const projects = result.data.data.map(project => ({
id: project.id,
name: project.name,
desc: project.desc || '无描述',
dialog_id: project.dialog_id,
archived_at: project.archived_at || '未归档',
owner_userid: project.owner_userid || 0,
created_at: project.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total: result.data.total,
page: result.data.current_page,
pagesize: result.data.per_page,
projects: projects,
}, null, 2)
}]
};
}
});
// 获取项目详情
this.mcp.addTool({
name: 'get_project',
description: '获取指定项目的详细信息,包括项目的列(看板列)、成员等完整信息。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/one', {
project_id: params.project_id,
});
if (result.error) {
throw new Error(result.error);
}
const project = result.data;
const projectDetail = {
id: project.id,
name: project.name,
desc: project.desc || '无描述',
dialog_id: project.dialog_id,
archived_at: project.archived_at || '未归档',
owner_userid: project.owner_userid,
owner_username: project.owner_username,
columns: project.projectColumn?.map(col => ({
id: col.id,
name: col.name,
sort: col.sort,
})) || [],
members: project.projectUser?.map(user => ({
userid: user.userid,
username: user.username,
owner: user.owner === 1 ? '管理员' : '成员',
})) || [],
created_at: project.created_at,
updated_at: project.updated_at,
};
return {
content: [{
type: 'text',
text: JSON.stringify(projectDetail, null, 2)
}]
};
}
});
// 创建项目
this.mcp.addTool({
name: 'create_project',
description: '创建新项目,可选设置项目描述、初始化列及流程状态。',
parameters: z.object({
name: z.string()
.min(2)
.describe('项目名称至少2个字符'),
desc: z.string()
.optional()
.describe('项目描述'),
columns: z.union([z.string(), z.array(z.string())])
.optional()
.describe('初始化列名称,字符串使用逗号分隔,也可直接传字符串数组'),
flow: z.enum(['open', 'close'])
.optional()
.describe('是否开启流程open/close默认close'),
personal: z.boolean()
.optional()
.describe('是否创建个人项目,仅支持创建一个个人项目'),
}),
execute: async (params) => {
const requestData = {
name: params.name,
};
if (params.desc !== undefined) {
requestData.desc = params.desc;
}
if (params.columns !== undefined) {
requestData.columns = Array.isArray(params.columns) ? params.columns.join(',') : params.columns;
}
if (params.flow !== undefined) {
requestData.flow = params.flow;
}
if (params.personal !== undefined) {
requestData.personal = params.personal ? 1 : 0;
}
const result = await this.request('GET', 'project/add', requestData);
if (result.error) {
throw new Error(result.error);
}
const project = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
project: {
id: project.id,
name: project.name,
desc: project.desc || '',
columns: project.projectColumn || [],
created_at: project.created_at,
}
}, null, 2)
}]
};
}
});
// 更新项目
this.mcp.addTool({
name: 'update_project',
description: '修改项目信息(名称、描述、归档策略等)。若未传 name 将自动沿用项目当前名称。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
name: z.string()
.optional()
.describe('项目名称'),
desc: z.string()
.optional()
.describe('项目描述'),
archive_method: z.string()
.optional()
.describe('归档方式'),
archive_days: z.number()
.optional()
.describe('自动归档天数'),
}),
execute: async (params) => {
const requestData = {
project_id: params.project_id,
};
if (params.name && params.name.trim().length > 0) {
requestData.name = params.name;
} else {
const projectResult = await this.request('GET', 'project/one', {
project_id: params.project_id,
});
if (projectResult.error) {
throw new Error(projectResult.error);
}
const currentName = projectResult.data?.name;
if (!currentName) {
throw new Error('无法获取项目名称,请手动提供 name 参数');
}
requestData.name = currentName;
}
if (params.desc !== undefined) {
requestData.desc = params.desc;
}
if (params.archive_method !== undefined) {
requestData.archive_method = params.archive_method;
}
if (params.archive_days !== undefined) {
requestData.archive_days = params.archive_days;
}
const result = await this.request('GET', 'project/update', requestData);
if (result.error) {
throw new Error(result.error);
}
const project = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
project: {
id: project.id,
name: project.name,
desc: project.desc || '',
archived_at: project.archived_at || null,
archive_method: project.archive_method ?? requestData.archive_method ?? null,
archive_days: project.archive_days ?? requestData.archive_days ?? null,
updated_at: project.updated_at,
}
}, null, 2)
}]
};
}
});
// 发送消息给用户
this.mcp.addTool({
name: 'send_message_to_user',
description: '给指定用户发送私信,可选择 Markdown 或 HTML 格式,并支持静默发送。',
parameters: z.object({
user_id: z.number()
.describe('接收方用户ID'),
text: z.string()
.min(1)
.describe('消息内容'),
text_type: z.enum(['md', 'html'])
.optional()
.describe('消息类型默认md可选md/html'),
silence: z.boolean()
.optional()
.describe('是否静默发送(不触发提醒)'),
}),
execute: async (params) => {
const dialogResult = await this.request('GET', 'dialog/open/user', {
userid: params.user_id,
});
if (dialogResult.error) {
throw new Error(dialogResult.error);
}
const dialogData = dialogResult.data || {};
const dialogId = dialogData.id;
if (!dialogId) {
throw new Error('未能获取会话ID无法发送消息');
}
const payload = {
dialog_id: dialogId,
text: params.text,
};
if (params.text_type) {
payload.text_type = params.text_type;
} else {
payload.text_type = 'md';
}
if (params.silence !== undefined) {
payload.silence = params.silence ? 'yes' : 'no';
}
const sendResult = await this.request('POST', 'dialog/msg/sendtext', payload);
if (sendResult.error) {
throw new Error(sendResult.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
dialog_id: dialogId,
data: sendResult.data,
}, null, 2)
}]
};
}
});
// 获取消息列表或搜索消息
this.mcp.addTool({
name: 'get_message_list',
description: '获取指定对话的消息列表,或按关键字搜索消息位置/内容。',
parameters: z.object({
dialog_id: z.number()
.optional()
.describe('对话ID获取消息列表时必填'),
keyword: z.string()
.optional()
.describe('搜索关键词,提供时执行消息搜索'),
msg_id: z.number()
.optional()
.describe('围绕某条消息加载相关内容'),
position_id: z.number()
.optional()
.describe('以position_id为中心加载消息'),
prev_id: z.number()
.optional()
.describe('获取此消息之前的历史'),
next_id: z.number()
.optional()
.describe('获取此消息之后的新消息'),
msg_type: z.enum(['tag', 'todo', 'link', 'text', 'image', 'file', 'record', 'meeting'])
.optional()
.describe('按消息类型筛选'),
take: z.number()
.optional()
.describe('获取条数列表模式最大100搜索模式受接口限制'),
}),
execute: async (params) => {
const keyword = params.keyword?.trim();
if (keyword) {
const searchPayload = {
key: keyword,
};
if (params.dialog_id) {
searchPayload.dialog_id = params.dialog_id;
}
if (params.take && params.take > 0) {
const takeValue = params.take;
searchPayload.take = params.dialog_id
? Math.min(takeValue, 200)
: Math.min(takeValue, 50);
}
const searchResult = await this.request('GET', 'dialog/msg/search', searchPayload);
if (searchResult.error) {
throw new Error(searchResult.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
mode: params.dialog_id ? 'position_search' : 'global_search',
keyword: keyword,
dialog_id: params.dialog_id || null,
data: searchResult.data,
}, null, 2)
}]
};
}
if (!params.dialog_id) {
throw new Error('请提供 dialog_id 以获取消息列表,或提供 keyword 执行搜索');
}
const requestData = {
dialog_id: params.dialog_id,
};
if (params.msg_id !== undefined) requestData.msg_id = params.msg_id;
if (params.position_id !== undefined) requestData.position_id = params.position_id;
if (params.prev_id !== undefined) requestData.prev_id = params.prev_id;
if (params.next_id !== undefined) requestData.next_id = params.next_id;
if (params.msg_type !== undefined) requestData.msg_type = params.msg_type;
if (params.take !== undefined) {
const takeValue = params.take > 0 ? params.take : 1;
requestData.take = Math.min(takeValue, 100);
}
const result = await this.request('GET', 'dialog/msg/list', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
const messages = Array.isArray(data.list) ? data.list : [];
return {
content: [{
type: 'text',
text: JSON.stringify({
dialog_id: params.dialog_id,
count: messages.length,
time: data.time,
dialog: data.dialog,
top: data.top,
todo: data.todo,
messages: messages,
}, null, 2)
}]
};
}
});
// 工作报告:获取我接收的汇报列表
this.mcp.addTool({
name: 'list_received_reports',
description: '获取我接收的工作汇报列表,支持按类型、已读状态、部门、时间筛选和搜索。适用于管理者查看团队成员提交的工作汇报。',
parameters: z.object({
search: z.string()
.optional()
.describe('搜索关键词可搜索标题、汇报人邮箱或用户ID'),
type: z.enum(['weekly', 'daily', 'all'])
.optional()
.describe('汇报类型: weekly(周报), daily(日报), all(全部)默认all'),
status: z.enum(['read', 'unread', 'all'])
.optional()
.describe('已读状态: read(已读), unread(未读), all(全部)默认all'),
department_id: z.number()
.optional()
.describe('部门ID筛选指定部门的汇报'),
created_at_start: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD'),
created_at_end: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20最大50'),
}),
execute: async (params) => {
const page = params.page && params.page > 0 ? params.page : 1;
const pagesize = params.pagesize && params.pagesize > 0 ? Math.min(params.pagesize, 50) : 20;
const keys = {};
if (params.search) {
keys.key = params.search;
}
if (params.type && params.type !== 'all') {
keys.type = params.type;
}
if (params.status && params.status !== 'all') {
keys.status = params.status;
}
if (params.department_id !== undefined) {
keys.department_id = params.department_id;
}
if (params.created_at_start || params.created_at_end) {
const dateRange = [];
if (params.created_at_start) {
dateRange.push(new Date(params.created_at_start).getTime());
} else {
dateRange.push(0);
}
if (params.created_at_end) {
dateRange.push(new Date(params.created_at_end).getTime());
} else {
dateRange.push(0);
}
keys.created_at = dateRange;
}
const requestData = {
page,
pagesize,
};
if (Object.keys(keys).length > 0) {
requestData.keys = keys;
}
const result = await this.request('GET', 'report/receive', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
const reports = Array.isArray(data.data) ? data.data : [];
const simplified = reports.map(report => {
const myReceive = Array.isArray(report.receives_user)
? report.receives_user.find(u => u.pivot && u.pivot.userid)
: null;
return {
id: report.id,
title: report.title,
type: report.type === 'daily' ? '日报' : '周报',
sender_id: report.userid,
sender_name: report.user ? (report.user.nickname || report.user.email) : '',
is_read: myReceive && myReceive.pivot ? (myReceive.pivot.read === 1) : false,
receive_at: report.receive_at || report.created_at,
created_at: report.created_at,
};
});
return {
content: [{
type: 'text',
text: JSON.stringify({
total: data.total || reports.length,
page: data.current_page || page,
pagesize: data.per_page || pagesize,
reports: simplified,
}, null, 2)
}]
};
}
});
// 工作报告:获取汇报详情
this.mcp.addTool({
name: 'get_report_detail',
description: '获取指定工作汇报的详细信息包括完整内容、汇报人、接收人列表、AI分析等。返回的 content 字段为 Markdown 格式。',
parameters: z.object({
report_id: z.number()
.describe('报告ID'),
}),
execute: async (params) => {
const result = await this.request('GET', 'report/detail', {
id: params.report_id,
});
if (result.error) {
throw new Error(result.error);
}
const report = result.data;
// 将 HTML 内容转换为 Markdown
const markdownContent = htmlToMarkdown(report.content || '');
const reportDetail = {
id: report.id,
title: report.title,
type: report.type === 'daily' ? '日报' : '周报',
type_value: report.type_val || report.type,
content: markdownContent,
sender_id: report.userid,
sender_name: report.user ? (report.user.nickname || report.user.email) : '',
receivers: Array.isArray(report.receives_user)
? report.receives_user.map(u => ({
userid: u.userid,
nickname: u.nickname || u.email,
is_read: u.pivot ? (u.pivot.read === 1) : false,
}))
: [],
ai_analysis: report.ai_analysis || null,
created_at: report.created_at,
updated_at: report.updated_at,
};
return {
content: [{
type: 'text',
text: JSON.stringify(reportDetail, null, 2)
}]
};
}
});
// 工作报告:生成汇报模板
this.mcp.addTool({
name: 'generate_report_template',
description: '基于用户的任务完成情况自动生成工作汇报模板,包括已完成工作、未完成工作等内容。支持生成当前周期或历史周期的汇报。返回的 content 字段为 Markdown 格式。',
parameters: z.object({
type: z.enum(['weekly', 'daily'])
.describe('汇报类型: weekly(周报), daily(日报)'),
offset: z.number()
.optional()
.describe('时间偏移量0表示当前周期-1表示上一周期-2表示上上周期以此类推。默认0'),
}),
execute: async (params) => {
const offset = params.offset !== undefined ? Math.abs(params.offset) : 0;
const result = await this.request('GET', 'report/template', {
type: params.type,
offset: offset,
});
if (result.error) {
throw new Error(result.error);
}
const template = result.data;
// 将 HTML 内容转换为 Markdown
const markdownContent = htmlToMarkdown(template.content || '');
const templateData = {
sign: template.sign,
title: template.title,
content: markdownContent,
existing_report_id: template.id || null,
message: template.id
? '该时间周期已有报告,如需修改请使用 update_report 或在界面中编辑'
: '模板已生成,可以直接使用或编辑 content 字段,然后使用 create_report 提交',
};
return {
content: [{
type: 'text',
text: JSON.stringify(templateData, null, 2)
}]
};
}
});
// 工作报告:创建汇报
this.mcp.addTool({
name: 'create_report',
description: '创建并提交工作汇报。通常先使用 generate_report_template 生成模板,然后使用此工具提交。',
parameters: z.object({
type: z.enum(['weekly', 'daily'])
.describe('汇报类型: weekly(周报), daily(日报)'),
title: z.string()
.describe('报告标题'),
content: z.string()
.describe('报告内容Markdown 格式),通常从 generate_report_template 返回的 content 字段获取'),
receive: z.array(z.number())
.optional()
.describe('接收人用户ID数组不包含自己'),
sign: z.string()
.optional()
.describe('唯一签名,从 generate_report_template 返回的 sign 字段获取'),
offset: z.number()
.optional()
.describe('时间偏移量应与生成模板时保持一致。默认0'),
}),
execute: async (params) => {
const requestData = {
id: 0,
title: params.title,
type: params.type,
content: markdownToHtml(params.content),
offset: params.offset !== undefined ? Math.abs(params.offset) : 0,
};
if (params.receive && Array.isArray(params.receive)) {
requestData.receive = params.receive;
}
if (params.sign) {
requestData.sign = params.sign;
}
const result = await this.request('POST', 'report/store', requestData);
if (result.error) {
throw new Error(result.error);
}
const report = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: '工作汇报创建成功',
report: {
id: report.id,
title: report.title,
type: report.type === 'daily' ? '日报' : '周报',
created_at: report.created_at,
}
}, null, 2)
}]
};
}
});
// 工作报告:获取我发送的汇报列表
this.mcp.addTool({
name: 'list_my_reports',
description: '获取我发送的工作汇报列表,支持按类型、时间筛选和搜索。适用于查看自己的历史汇报。',
parameters: z.object({
search: z.string()
.optional()
.describe('搜索关键词(可搜索标题)'),
type: z.enum(['weekly', 'daily', 'all'])
.optional()
.describe('汇报类型: weekly(周报), daily(日报), all(全部)默认all'),
created_at_start: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD'),
created_at_end: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20最大50'),
}),
execute: async (params) => {
const page = params.page && params.page > 0 ? params.page : 1;
const pagesize = params.pagesize && params.pagesize > 0 ? Math.min(params.pagesize, 50) : 20;
const keys = {};
if (params.search) {
keys.key = params.search;
}
if (params.type && params.type !== 'all') {
keys.type = params.type;
}
if (params.created_at_start || params.created_at_end) {
const dateRange = [];
if (params.created_at_start) {
dateRange.push(new Date(params.created_at_start).getTime());
} else {
dateRange.push(0);
}
if (params.created_at_end) {
dateRange.push(new Date(params.created_at_end).getTime());
} else {
dateRange.push(0);
}
keys.created_at = dateRange;
}
const requestData = {
page,
pagesize,
};
if (Object.keys(keys).length > 0) {
requestData.keys = keys;
}
const result = await this.request('GET', 'report/my', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
const reports = Array.isArray(data.data) ? data.data : [];
const simplified = reports.map(report => ({
id: report.id,
title: report.title,
type: report.type === 'daily' ? '日报' : '周报',
receivers: Array.isArray(report.receives) ? report.receives : [],
receiver_count: Array.isArray(report.receives) ? report.receives.length : 0,
created_at: report.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total: data.total || reports.length,
page: data.current_page || page,
pagesize: data.per_page || pagesize,
reports: simplified,
}, null, 2)
}]
};
}
});
// 工作报告:标记已读/未读
this.mcp.addTool({
name: 'mark_reports_read',
description: '批量标记工作汇报为已读或未读状态。支持单个或多个报告的状态管理。',
parameters: z.object({
report_ids: z.union([z.number(), z.array(z.number())])
.describe('报告ID或ID数组最多100个'),
action: z.enum(['read', 'unread'])
.optional()
.describe('操作类型: read(标记已读), unread(标记未读)默认read'),
}),
execute: async (params) => {
const action = params.action || 'read';
const ids = Array.isArray(params.report_ids) ? params.report_ids : [params.report_ids];
if (ids.length > 100) {
throw new Error('最多只能操作100条数据');
}
const result = await this.request('GET', 'report/mark', {
id: ids,
action: action,
});
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `已将 ${ids.length} 个报告标记为${action === 'read' ? '已读' : '未读'}`,
action: action,
affected_count: ids.length,
report_ids: ids,
}, null, 2)
}]
};
}
});
}
// 启动 MCP 服务器
async start(port = 22224) {
try {
await this.mcp.start({
transportType: 'httpStream',
httpStream: {
port: port
}
});
} catch (error) {
throw error;
}
}
// 停止服务器
async stop() {
if (this.mcp && typeof this.mcp.stop === 'function') {
await this.mcp.stop();
}
}
}
/**
* 启动 MCP 服务器
*/
function startMCPServer(mainWindow, mcpPort) {
if (mcpServer) {
loger.info('MCP server already running');
return;
}
mcpServer = new DooTaskMCP(mainWindow);
mcpServer.start(mcpPort).then(() => {
loger.info(`MCP Server started on http://localhost:${mcpPort}/mcp`);
loger.info(`Legacy SSE endpoint also available at http://localhost:${mcpPort}/sse`);
}).catch((error) => {
loger.error('Failed to start MCP server:', error);
mcpServer = null;
});
}
/**
* 停止 MCP 服务器
*/
function stopMCPServer() {
if (!mcpServer) {
loger.info('MCP server is not running');
return;
}
mcpServer.stop().then(() => {
loger.info('MCP server stopped');
}).catch((error) => {
loger.error('Failed to stop MCP server:', error);
}).finally(() => {
mcpServer = null;
});
}
module.exports = {
DooTaskMCP,
startMCPServer,
stopMCPServer,
};