diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 2d595d01d..2e8dc9445 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -3851,6 +3851,11 @@ class ProjectController extends AbstractController $type = trim(Request::input('type')); $data = Request::input('data', []); + // 验证建议类型 + if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) { + return Base::retError('无效的建议类型'); + } + // 验证任务 $task = ProjectTask::userTask($taskId); if (!$task) { @@ -3884,6 +3889,13 @@ class ProjectController extends AbstractController case ProjectTaskAiEvent::EVENT_SUBTASKS: // 创建子任务 $subtasks = $result['content'] ?? []; + // 检查子任务数量限制 + $existingCount = ProjectTask::where('parent_id', $task->id) + ->whereNull('deleted_at') + ->count(); + if ($existingCount + count($subtasks) > 50) { + return Base::retError('子任务数量超过限制(最多50个)'); + } \DB::transaction(function () use ($task, $subtasks) { foreach ($subtasks as $name) { ProjectTask::addTask([ @@ -3963,12 +3975,27 @@ class ProjectController extends AbstractController $msgId = intval(Request::input('msg_id')); $type = trim(Request::input('type')); + // 验证建议类型 + if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) { + return Base::retError('无效的建议类型'); + } + // 验证任务 $task = ProjectTask::userTask($taskId); if (!$task) { return Base::retError('任务不存在或无权限'); } + // 验证事件记录存在 + $event = ProjectTaskAiEvent::where('task_id', $taskId) + ->where('event_type', $type) + ->where('msg_id', $msgId) + ->first(); + + if (!$event || $event->status !== ProjectTaskAiEvent::STATUS_COMPLETED) { + return Base::retError('建议不存在或已处理'); + } + // 更新消息状态 if ($msgId > 0 && $task->dialog_id) { AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'dismissed'); diff --git a/app/Module/AiTaskSuggestion.php b/app/Module/AiTaskSuggestion.php index 9680ba7b8..efc329f95 100644 --- a/app/Module/AiTaskSuggestion.php +++ b/app/Module/AiTaskSuggestion.php @@ -47,8 +47,8 @@ class AiTaskSuggestion return !$hasOwner; case ProjectTaskAiEvent::EVENT_SIMILAR: - // 始终执行 - return true; + // 向量搜索暂未实现,跳过 + return false; default: return false; @@ -164,17 +164,29 @@ class AiTaskSuggestion } } + /** + * 转义用户输入以防止 Prompt 注入 + */ + private static function escapeUserInput(string $input): string + { + // 移除可能影响 AI Prompt 解析的特殊字符 + $input = str_replace(['```', '---', '==='], '', $input); + // 截断过长的输入 + return mb_substr(trim($input), 0, 500); + } + /** * 构建描述生成 Prompt */ private static function buildDescriptionPrompt(ProjectTask $task): string { - $projectName = $task->project->name ?? '未知项目'; + $taskName = self::escapeUserInput($task->name); + $projectName = self::escapeUserInput($task->project->name ?? '未知项目'); return <<name} +任务标题:{$taskName} 所属项目:{$projectName} 请按以下格式生成任务描述(使用 Markdown): @@ -198,12 +210,13 @@ PROMPT; */ private static function buildSubtasksPrompt(ProjectTask $task): string { - $content = $task->content ?? ''; + $taskName = self::escapeUserInput($task->name); + $content = self::escapeUserInput($task->content ?? ''); return <<name} +任务标题:{$taskName} 任务描述:{$content} 请返回 3-5 个子任务,每行一个,格式如下: @@ -226,9 +239,11 @@ PROMPT; { $membersText = ''; foreach ($members as $member) { - $membersText .= "- {$member['nickname']}(ID:{$member['userid']})"; + $nickname = self::escapeUserInput($member['nickname']); + $membersText .= "- {$nickname}(ID:{$member['userid']})"; if (!empty($member['profession'])) { - $membersText .= ",职位:{$member['profession']}"; + $profession = self::escapeUserInput($member['profession']); + $membersText .= ",职位:{$profession}"; } $membersText .= ",进行中任务:{$member['in_progress_count']}个"; $membersText .= ",近期完成:{$member['completed_count']}个"; @@ -238,11 +253,14 @@ PROMPT; $membersText .= "\n"; } + $taskName = self::escapeUserInput($task->name); + $taskContent = self::escapeUserInput($task->content ?? ''); + return <<name} -任务描述:{$task->content} +任务标题:{$taskName} +任务描述:{$taskContent} 团队成员: {$membersText} @@ -552,7 +570,10 @@ MD; */ public static function updateMessageStatus(int $msgId, int $dialogId, string $type, string $status): void { - $msg = WebSocketDialogMsg::find($msgId); + // 验证消息存在且属于指定对话 + $msg = WebSocketDialogMsg::where('id', $msgId) + ->where('dialog_id', $dialogId) + ->first(); if (!$msg) { return; } diff --git a/app/Tasks/AiTaskAnalyzeTask.php b/app/Tasks/AiTaskAnalyzeTask.php index e72f03235..75a83d08c 100644 --- a/app/Tasks/AiTaskAnalyzeTask.php +++ b/app/Tasks/AiTaskAnalyzeTask.php @@ -53,8 +53,16 @@ class AiTaskAnalyzeTask extends AbstractTask continue; } - // 标记为处理中 - $event->markProcessing(); + // 使用原子操作标记为处理中(防止并发重复处理) + $updated = ProjectTaskAiEvent::where('id', $event->id) + ->whereIn('status', [ProjectTaskAiEvent::STATUS_PENDING, ProjectTaskAiEvent::STATUS_FAILED]) + ->update(['status' => ProjectTaskAiEvent::STATUS_PROCESSING]); + + if (!$updated) { + // 已被其他进程处理 + continue; + } + $event->status = ProjectTaskAiEvent::STATUS_PROCESSING; try { // 检查是否满足执行条件 diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index deb37de51..718e0d896 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -5229,15 +5229,9 @@ export default { * 采纳 AI 建议 */ applyAiSuggestion({}, params) { - return new Promise((resolve, reject) => { - this.dispatch('call', { - url: 'project/task/ai-apply', - data: params, - }).then(result => { - resolve(result); - }).catch(e => { - reject(e); - }); + return this.dispatch('call', { + url: 'project/task/ai-apply', + data: params, }); }, @@ -5245,15 +5239,9 @@ export default { * 忽略 AI 建议 */ dismissAiSuggestion({}, params) { - return new Promise((resolve, reject) => { - this.dispatch('call', { - url: 'project/task/ai-dismiss', - data: params, - }).then(result => { - resolve(result); - }).catch(e => { - reject(e); - }); + return this.dispatch('call', { + url: 'project/task/ai-dismiss', + data: params, }); }