Compare commits

...

2 Commits

3 changed files with 100 additions and 154 deletions

View File

@ -834,7 +834,7 @@ class WebSocketDialogMsg extends AbstractModel
switch ($this->type) { switch ($this->type) {
case "file": case "file":
// 提取文件消息 // 提取文件消息
$result = " 文件:{$this->msg['name']}、大小:{$this->msg['size']}、下载URL:{$this->msg['path']} "; $result = " 文件{$this->msg['name']}(大小:{$this->msg['size']}BURL{$this->msg['path']} ";
break; break;
case "text": case "text":
@ -858,30 +858,30 @@ class WebSocketDialogMsg extends AbstractModel
$result = preg_replace_callback_array([ $result = preg_replace_callback_array([
// 用户 // 用户
"/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/" => function ($match) { "/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/" => function ($match) {
return " 提及用户ID:{$match[1]} "; return "";
}, },
// 任务 // 任务
"/<span class=\"mention task\" data-id=\"(\d+)\">#?(.*?)<\/span>/" => function ($match) { "/<span class=\"mention task\" data-id=\"(\d+)\">#?(.*?)<\/span>/" => function ($match) {
return " 任务:{$match[2]} (任务ID:{$match[1]}) "; return " 任务{$match[2]} (任务ID{$match[1]}) ";
}, },
// 文件 // 文件
"/<a class=\"mention file\" href=\"([^\"']+?)\"[^>]*?>~?(.*?)<\/a>/" => function ($match) use (&$reserves) { "/<a class=\"mention file\" href=\"([^\"']+?)\"[^>]*?>~?(.*?)<\/a>/" => function ($match) use (&$reserves) {
$idOrCode = ""; $idOrCode = "";
if (preg_match("/single\/file\/(.*?)$/", $match[1], $subMatch)) { if (preg_match("/single\/file\/(.*?)$/", $match[1], $subMatch)) {
$idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "文件ID:{$subMatch[1]}" : "文件分享码:{$subMatch[1]}") . ")"; $idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "文件ID{$subMatch[1]}" : "文件分享码{$subMatch[1]}") . ")";
} }
return " 文件:{$match[2]}{$idOrCode} "; return " 文件{$match[2]}{$idOrCode} ";
}, },
// 报告 // 报告
"/<a class=\"mention report\" href=\"([^\"']+?)\"[^>]*?>%?(.*?)<\/a>/" => function ($match) use (&$reserves) { "/<a class=\"mention report\" href=\"([^\"']+?)\"[^>]*?>%?(.*?)<\/a>/" => function ($match) use (&$reserves) {
$idOrCode = ""; $idOrCode = "";
if (preg_match("/single\/report\/detail\/(.*?)$/", $match[1], $subMatch)) { if (preg_match("/single\/report\/detail\/(.*?)$/", $match[1], $subMatch)) {
$idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "报告ID:{$subMatch[1]}" : "报告分享码:{$subMatch[1]}") . ")"; $idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "报告ID{$subMatch[1]}" : "报告分享码{$subMatch[1]}") . ")";
} }
return " 工作汇报:{$match[2]}{$idOrCode} "; return " 工作汇报{$match[2]}{$idOrCode} ";
}, },
], $result); ], $result);

View File

@ -508,7 +508,7 @@ class BotReceiveMsgTask extends AbstractTask
if (empty($extras['api_key'])) { if (empty($extras['api_key'])) {
throw new Exception('机器人未启用。'); throw new Exception('机器人未启用。');
} }
$this->generateSystemPromptForAI($msg->userid, $dialog, $extras); $this->generateSystemPromptForAI($msg->userid, $dialog, $botUser, $extras);
// 转换提及格式 // 转换提及格式
if ($replyText) { if ($replyText) {
$sendText = <<<EOF $sendText = <<<EOF
@ -602,134 +602,80 @@ class BotReceiveMsgTask extends AbstractTask
* *
* @param int|null $userid 用户ID * @param int|null $userid 用户ID
* @param WebSocketDialog $dialog 对话对象 * @param WebSocketDialog $dialog 对话对象
* @param User $botUser 机器人用户对象
* @param array $extras 额外参数数组通过引用传递以修改system_message * @param array $extras 额外参数数组通过引用传递以修改system_message
* @return void * @return void
*/ */
private function generateSystemPromptForAI($userid, WebSocketDialog $dialog, array &$extras) private function generateSystemPromptForAI($userid, WebSocketDialog $dialog, User $botUser, array &$extras)
{ {
// 构建结构化的系统提示词 // 用户自定义提示词(私聊场景优先使用)
$sections = []; $customPrompt = null;
if ($dialog->type === 'user') {
// 基础角色设定(如果有) $customPrompt = WebSocketDialogConfig::where([
if (!empty($extras['system_message'])) { 'dialog_id' => $dialog->id,
$sections[] = <<<EOF 'userid' => $userid,
<role_setting> 'type' => 'ai_prompt',
{$extras['system_message']} ])->value('value');
</role_setting>
EOF;
} }
// 上下文信息(项目、任务、部门等)+ 操作指令 $prompt = [];
switch ($dialog->type) {
// 用户对话
case "user":
$aiPrompt = WebSocketDialogConfig::where([
'dialog_id' => $dialog->id,
'userid' => $userid,
'type' => 'ai_prompt',
])->value('value');
if ($aiPrompt) {
return $aiPrompt;
}
break;
// 群组对话 // 1. 基础角色(自定义提示词优先)
case "group": if ($customPrompt) {
switch ($dialog->group_type) { $prompt[] = $customPrompt;
// 用户群 } elseif (!empty($extras['system_message'])) {
case 'user': $prompt[] = $extras['system_message'];
break;
// 项目群
case 'project':
$projectInfo = Project::whereDialogId($dialog->id)->first();
if ($projectInfo) {
$currentTime = Carbon::now()->toDateTimeString();
$sections[] = <<<EOF
<context_info>
当前我在项目群聊中
项目ID{$projectInfo->id}
项目名称:{$projectInfo->name}
当前时间:{$currentTime}
</context_info>
EOF;
}
break;
// 任务群
case 'task':
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
if ($taskInfo) {
$currentTime = Carbon::now()->toDateTimeString();
$sections[] = <<<EOF
<context_info>
当前我在任务群聊中
任务ID{$taskInfo->id}
任务名称:{$taskInfo->name}
当前时间:{$currentTime}
</context_info>
EOF;
}
break;
// 部门群
case 'department':
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
if ($userDepartment) {
$sections[] = <<<EOF
<context_info>
当前我在部门群聊中
部门ID{$userDepartment->id}
部门名称:{$userDepartment->name}
</context_info>
EOF;
}
break;
// 全体成员群
case 'all':
$sections[] = <<<EOF
<context_info>
当前我在【全体成员】的群聊中
</context_info>
EOF;
break;
}
// 聊天历史
if ($dialog->type === 'group') {
$chatHistory = $this->getRecentChatHistory($dialog, 15);
if ($chatHistory) {
$sections[] = <<<EOF
<chat_history>
{$chatHistory}
</chat_history>
EOF;
}
}
break;
} }
// 更新系统提示词 // 2. 上下文信息
if (!empty($sections)) { $currentTime = Carbon::now()->toDateTimeString();
$extras['system_message'] = implode("\n\n", $sections); $contextLines = [
} "您是:{$botUser->nickname}ID: {$botUser->userid}",
"当前对话ID{$dialog->id}",
// 添加标签说明 "当前系统时间:{$currentTime}"
$tagDescs = [
'role_setting' => '你的基础角色和行为定义',
'context_info' => '当前环境和状态信息',
'chat_history' => '最近的对话历史记录',
]; ];
$useTags = [];
foreach ($tagDescs as $tag => $desc) { if ($dialog->type === 'group') {
if (str_contains($extras['system_message'], '<' . $tag . '>')) { switch ($dialog->group_type) {
$useTags[] = '- <' . $tag . '>: ' . $desc; case 'project':
$projectInfo = Project::whereDialogId($dialog->id)->first();
if ($projectInfo) {
$contextLines[] = "场景:项目群聊「{$projectInfo->name}ID: {$projectInfo->id}";
}
break;
case 'task':
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
if ($taskInfo) {
$contextLines[] = "场景:任务群聊「{$taskInfo->name}ID: {$taskInfo->id}";
}
break;
case 'department':
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
if ($userDepartment) {
$contextLines[] = "场景:部门群聊「{$userDepartment->name}";
}
break;
case 'all':
$contextLines[] = "场景:全体成员群聊";
break;
} }
// 3. 聊天历史(仅群聊)
$chatHistory = $this->getRecentChatHistory($dialog, 15);
if ($chatHistory) {
$prompt[] = implode("\n", $contextLines);
$prompt[] = "最近的对话记录:\n{$chatHistory}";
} else {
$prompt[] = implode("\n", $contextLines);
}
} else {
$prompt[] = implode("\n", $contextLines);
} }
if (!empty($useTags)) {
$extras['system_message'] = "以下信息按标签组织:\n" . implode("\n", $useTags) . "\n\n" . $extras['system_message']; $extras['system_message'] = implode("\n----\n", array_filter($prompt));
}
} }
/** /**
@ -766,14 +712,14 @@ class BotReceiveMsgTask extends AbstractTask
// 使用XML标签格式确保AI能清晰识别边界 // 使用XML标签格式确保AI能清晰识别边界
// 对用户名进行HTML转义防止特殊字符破坏格式 // 对用户名进行HTML转义防止特殊字符破坏格式
$safeUserName = htmlspecialchars($userName, ENT_QUOTES, 'UTF-8'); $safeUserName = htmlspecialchars($userName, ENT_QUOTES, 'UTF-8');
return "<message user=\"{$safeUserName}\">\n{$content}\n</message>"; return "<message userid=\"{$message->userid}\" nickname=\"{$safeUserName}\">\n{$content}\n</message>";
}) })
->reverse() // 反转集合,让时间顺序正确(最早的在前) ->reverse() // 反转集合,让时间顺序正确(最早的在前)
->filter() // 过滤掉空内容的消息 ->filter() // 过滤掉空内容的消息
->values() // 重新索引数组 ->values() // 重新索引数组
->toArray(); ->toArray();
return empty($chatMessages) ? null : implode("\n\n", $chatMessages); return empty($chatMessages) ? null : implode("\n", $chatMessages);
} }
/** /**

56
electron/lib/mcp.js vendored
View File

@ -4,36 +4,36 @@
* DooTask Electron 客户端集成了 Model Context Protocol (MCP) 服务 * DooTask Electron 客户端集成了 Model Context Protocol (MCP) 服务
* 允许 AI 助手( Claude)直接与 DooTask 任务进行交互 * 允许 AI 助手( Claude)直接与 DooTask 任务进行交互
* *
* 提供的工具 24 : * 提供的工具 25 :
* *
* === 用户管理 === * === 用户管理 ===
* - get_users_basic - 根据用户ID列表获取基础信息便于匹配负责人/协助人 * - get_users_basic - 批量获取用户基础信息1-50便于匹配负责人/协助人
* - search_user - 按关键字或项目筛选用户支持分页与更多过滤项 * - search_user - 按关键词搜索用户支持按项目/对话范围筛选用于不知道用户ID时的查找
* *
* === 任务管理 === * === 任务管理 ===
* - list_tasks - 获取任务列表支持按状态/项目/主任务筛选搜索分页 * - list_tasks - 获取当前用户相关的任务列表负责/协助/关注支持按状态/项目/时间筛选
* - get_task - 获取任务详情包含完整内容负责人协助人员标签等所有信息 * - get_task - 获取单个任务的完整详细信息 list_tasks 返回更详细
* - complete_task - 快速标记任务完成 * - complete_task - 快速标记任务完成
* - create_task - 创建新任务 * - create_task - 在指定项目中创建新任务
* - update_task - 更新任务支持修改名称内容负责人时间状态等所有属性 * - update_task - 更新任务的任意属性只需提供要修改的字段
* - create_sub_task - 为指定主任务创建子任务 * - create_sub_task - 为指定主任务创建子任务
* - get_task_files - 获取任务附件列表 * - get_task_files - 获取任务附件列表
* - delete_task - 删除或还原任务 * - delete_task - 删除或还原任务
* *
* === 项目管理 === * === 项目管理 ===
* - list_projects - 获取项目列表支持按归档状态筛选搜索 * - list_projects - 获取当前用户可访问的项目列表支持按归档状态筛选搜索
* - get_project - 获取项目详情包含列看板列成员等完整信息 * - get_project - 获取项目的完整详细信息 list_projects 返回更详细
* - create_project - 创建新项目 * - create_project - 创建新项目
* - update_project - 修改项目信息名称描述等 * - update_project - 修改项目信息名称描述等
* *
* === 文件管理 === * === 文件管理个人文件系统 ===
* - list_files - 获取项目文件列表支持按父级文件夹筛选 * - list_files - 浏览个人文件系统获取指定文件夹下的文件和子文件夹列表
* - search_files - 按关键词搜索文件支持搜索文件名称或ID * - search_files - 搜索用户文件系统中的文件自己创建的和共享给自己的
* - get_file_detail - 获取文件详情返回 content_url 可配合 WebFetch 读取文件内容 * - get_file_detail - 获取文件详情支持通过文件ID或分享码访问返回 content_url 可配合 WebFetch 读取
* *
* === 工作报告 === * === 工作报告 ===
* - list_received_reports - 获取我接收的汇报列表支持按类型/状态/部门/时间筛选 * - list_received_reports - 获取我接收的汇报列表支持按类型/状态/部门/时间筛选
* - get_report_detail - 获取汇报详情包括完整内容汇报人接收人AI分析等 * - get_report_detail - 获取汇报详情支持通过报告ID或分享码访问内容自动转为 Markdown
* - generate_report_template - 基于任务完成情况自动生成汇报模板已完成/未完成任务 * - generate_report_template - 基于任务完成情况自动生成汇报模板已完成/未完成任务
* - create_report - 创建并提交工作汇报 * - create_report - 创建并提交工作汇报
* - list_my_reports - 获取我发送的汇报列表支持按类型/时间筛选 * - list_my_reports - 获取我发送的汇报列表支持按类型/时间筛选
@ -41,7 +41,7 @@
* *
* === 消息通知 === * === 消息通知 ===
* - send_message_to_user - 给指定用户发送私信 * - send_message_to_user - 给指定用户发送私信
* - get_message_list - 获取对话消息或执行关键词搜索 * - get_message_list - 两种模式获取对话消息列表 按关键词搜索消息
* *
* 配置方法: * 配置方法:
* { * {
@ -222,7 +222,7 @@ class DooTaskMCP {
// 用户管理:获取用户基础信息 // 用户管理:获取用户基础信息
this.mcp.addTool({ this.mcp.addTool({
name: 'get_users_basic', name: 'get_users_basic',
description: '根据用户ID列表获取用户基础信息(昵称、邮箱、头像等),方便在分配任务前确认成员身份。', description: '根据用户ID列表批量获取用户基础信息(昵称、邮箱、头像等)。适用于分配任务前确认成员身份支持1-50个用户。',
parameters: z.object({ parameters: z.object({
userids: z.array(z.number()) userids: z.array(z.number())
.min(1) .min(1)
@ -269,7 +269,7 @@ class DooTaskMCP {
// 用户管理:搜索用户 // 用户管理:搜索用户
this.mcp.addTool({ this.mcp.addTool({
name: 'search_user', name: 'search_user',
description: '按关键词搜索或筛选用户,支持按项目/对话过滤并返回分页结果。', description: '按关键词搜索用户(昵称、邮箱、拼音),支持按项目/对话范围筛选。与 get_users_basic 不同此工具用于不知道具体用户ID时的查找场景。',
parameters: z.object({ parameters: z.object({
keyword: z.string() keyword: z.string()
.min(1) .min(1)
@ -376,7 +376,7 @@ class DooTaskMCP {
// 获取任务列表 // 获取任务列表
this.mcp.addTool({ this.mcp.addTool({
name: 'list_tasks', name: 'list_tasks',
description: '获取任务列表。可以按状态筛选(已完成/未完成)、搜索任务名称、按时间范围筛选等。', description: '获取当前用户相关的任务列表(负责/协助/关注),支持按状态、项目、时间范围筛选和搜索。',
parameters: z.object({ parameters: z.object({
status: z.enum(['all', 'completed', 'uncompleted']) status: z.enum(['all', 'completed', 'uncompleted'])
.optional() .optional()
@ -474,7 +474,7 @@ class DooTaskMCP {
// 获取任务详情 // 获取任务详情
this.mcp.addTool({ this.mcp.addTool({
name: 'get_task', name: 'get_task',
description: '获取指定任务的详细信息,包括任务描述、完整内容、负责人、协助人员、标签、时间等所有信息。返回的 content 字段为 Markdown 格式。', description: '获取单个任务的完整详细信息包括任务描述、完整内容content、负责人、协助人员、标签、时间等。比 list_tasks 返回更详细的信息。',
parameters: z.object({ parameters: z.object({
task_id: z.number() task_id: z.number()
.describe('任务ID'), .describe('任务ID'),
@ -587,7 +587,7 @@ class DooTaskMCP {
// 创建任务 // 创建任务
this.mcp.addTool({ this.mcp.addTool({
name: 'create_task', name: 'create_task',
description: '创建新任务。可以指定任务名称、内容、负责人、时间等信息。', description: '在指定项目中创建新任务。必需参数项目ID、任务名称。可选负责人、协助人、开始/结束时间、看板列等。',
parameters: z.object({ parameters: z.object({
project_id: z.number() project_id: z.number()
.describe('项目ID'), .describe('项目ID'),
@ -656,7 +656,7 @@ class DooTaskMCP {
// 更新任务 // 更新任务
this.mcp.addTool({ this.mcp.addTool({
name: 'update_task', name: 'update_task',
description: '更新任务信息。可以修改任务名称、内容、负责人、时间、状态等所有属性。', description: '更新任务的任意属性(名称、内容、负责人、协助人、时间、完成状态、看板列等)。只需提供要修改的字段。',
parameters: z.object({ parameters: z.object({
task_id: z.number() task_id: z.number()
.describe('任务ID'), .describe('任务ID'),
@ -864,7 +864,7 @@ class DooTaskMCP {
// 获取项目列表 // 获取项目列表
this.mcp.addTool({ this.mcp.addTool({
name: 'list_projects', name: 'list_projects',
description: '获取项目列表。可以按归档状态筛选、搜索项目名称等。', description: '获取当前用户可访问的项目列表,支持按归档状态筛选、搜索项目名称。',
parameters: z.object({ parameters: z.object({
archived: z.enum(['no', 'yes', 'all']) archived: z.enum(['no', 'yes', 'all'])
.optional() .optional()
@ -926,7 +926,7 @@ class DooTaskMCP {
// 获取项目详情 // 获取项目详情
this.mcp.addTool({ this.mcp.addTool({
name: 'get_project', name: 'get_project',
description: '获取指定项目的详细信息,包括项目的列(看板列)、成员等完整信息。', description: '获取指定项目的完整详细信息,包括项目描述、所有看板列、成员列表及权限等。比 list_projects 返回更详细的信息。',
parameters: z.object({ parameters: z.object({
project_id: z.number() project_id: z.number()
.describe('项目ID'), .describe('项目ID'),
@ -1188,7 +1188,7 @@ class DooTaskMCP {
// 获取消息列表或搜索消息 // 获取消息列表或搜索消息
this.mcp.addTool({ this.mcp.addTool({
name: 'get_message_list', name: 'get_message_list',
description: '获取指定对话的消息列表,或按关键字搜索消息位置/内容。', description: '两种模式1获取指定对话的消息列表需提供 dialog_id支持按类型筛选、分页加载2按关键词搜索消息提供 keyword可在单个对话或全局搜索。',
parameters: z.object({ parameters: z.object({
dialog_id: z.number() dialog_id: z.number()
.optional() .optional()
@ -1715,7 +1715,7 @@ class DooTaskMCP {
// 文件管理:获取文件列表 // 文件管理:获取文件列表
this.mcp.addTool({ this.mcp.addTool({
name: 'list_files', name: 'list_files',
description: '获取项目文件列表,支持按父级文件夹筛选。可以浏览文件夹结构,查看所有文件和子文件夹。', description: '获取用户文件列表个人文件系统支持按父级文件夹筛选。pid=0或不传表示获取根目录pid>0获取指定文件夹下的内容。可以浏览文件夹结构,查看所有文件和子文件夹。',
parameters: z.object({ parameters: z.object({
pid: z.number() pid: z.number()
.optional() .optional()
@ -1764,11 +1764,11 @@ class DooTaskMCP {
// 文件管理:搜索文件 // 文件管理:搜索文件
this.mcp.addTool({ this.mcp.addTool({
name: 'search_files', name: 'search_files',
description: '按关键词搜索文件支持搜索文件名称或ID。可以快速定位文件位置。', description: '按关键词搜索用户文件系统中的文件支持搜索文件名称、文件ID或分享链接。搜索范围包括自己创建的文件和共享给自己的文件。',
parameters: z.object({ parameters: z.object({
keyword: z.string() keyword: z.string()
.min(1) .min(1)
.describe('搜索关键词,支持文件名称或文件ID'), .describe('搜索关键词,支持文件名称、文件ID或分享链接'),
take: z.number() take: z.number()
.optional() .optional()
.describe('返回数量默认50最大100'), .describe('返回数量默认50最大100'),
@ -1817,7 +1817,7 @@ class DooTaskMCP {
// 文件管理:获取文件详情 // 文件管理:获取文件详情
this.mcp.addTool({ this.mcp.addTool({
name: 'get_file_detail', name: 'get_file_detail',
description: '获取指定文件的详细信息,包括类型、大小、共享状态、创建者等。返回的 content_url 可以配合 WebFetch 工具读取文件内容进行分析。支持通过文件ID或分享码访问。', description: '获取指定文件的详细信息,包括类型、大小、共享状态、创建者等。支持通过文件ID或分享码访问。返回的 content_url 可以配合 WebFetch 工具读取文件内容进行分析。',
parameters: z.object({ parameters: z.object({
file_id: z.union([z.number(), z.string()]) file_id: z.union([z.number(), z.string()])
.describe('文件ID数字或分享码字符串'), .describe('文件ID数字或分享码字符串'),