mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-27 04:58:12 +00:00
fix(ai): address security and robustness issues from code review
Security fixes: - Add escapeUserInput() to prevent Prompt injection via user input - Validate msgId belongs to dialogId in updateMessageStatus() - Add type parameter whitelist validation in ai-apply/ai-dismiss - Add event record validation in task__ai_dismiss Robustness fixes: - Use atomic update for markProcessing to prevent concurrent processing - Add subtask count limit check before creation (max 50) - Disable similar task feature until vector search is implemented - Fix Promise anti-pattern in frontend actions Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
parent
d4d7a0d69f
commit
75073d4320
@ -3851,6 +3851,11 @@ class ProjectController extends AbstractController
|
|||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
$data = Request::input('data', []);
|
$data = Request::input('data', []);
|
||||||
|
|
||||||
|
// 验证建议类型
|
||||||
|
if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) {
|
||||||
|
return Base::retError('无效的建议类型');
|
||||||
|
}
|
||||||
|
|
||||||
// 验证任务
|
// 验证任务
|
||||||
$task = ProjectTask::userTask($taskId);
|
$task = ProjectTask::userTask($taskId);
|
||||||
if (!$task) {
|
if (!$task) {
|
||||||
@ -3884,6 +3889,13 @@ class ProjectController extends AbstractController
|
|||||||
case ProjectTaskAiEvent::EVENT_SUBTASKS:
|
case ProjectTaskAiEvent::EVENT_SUBTASKS:
|
||||||
// 创建子任务
|
// 创建子任务
|
||||||
$subtasks = $result['content'] ?? [];
|
$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) {
|
\DB::transaction(function () use ($task, $subtasks) {
|
||||||
foreach ($subtasks as $name) {
|
foreach ($subtasks as $name) {
|
||||||
ProjectTask::addTask([
|
ProjectTask::addTask([
|
||||||
@ -3963,12 +3975,27 @@ class ProjectController extends AbstractController
|
|||||||
$msgId = intval(Request::input('msg_id'));
|
$msgId = intval(Request::input('msg_id'));
|
||||||
$type = trim(Request::input('type'));
|
$type = trim(Request::input('type'));
|
||||||
|
|
||||||
|
// 验证建议类型
|
||||||
|
if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) {
|
||||||
|
return Base::retError('无效的建议类型');
|
||||||
|
}
|
||||||
|
|
||||||
// 验证任务
|
// 验证任务
|
||||||
$task = ProjectTask::userTask($taskId);
|
$task = ProjectTask::userTask($taskId);
|
||||||
if (!$task) {
|
if (!$task) {
|
||||||
return Base::retError('任务不存在或无权限');
|
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) {
|
if ($msgId > 0 && $task->dialog_id) {
|
||||||
AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'dismissed');
|
AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'dismissed');
|
||||||
|
|||||||
@ -47,8 +47,8 @@ class AiTaskSuggestion
|
|||||||
return !$hasOwner;
|
return !$hasOwner;
|
||||||
|
|
||||||
case ProjectTaskAiEvent::EVENT_SIMILAR:
|
case ProjectTaskAiEvent::EVENT_SIMILAR:
|
||||||
// 始终执行
|
// 向量搜索暂未实现,跳过
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
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
|
* 构建描述生成 Prompt
|
||||||
*/
|
*/
|
||||||
private static function buildDescriptionPrompt(ProjectTask $task): string
|
private static function buildDescriptionPrompt(ProjectTask $task): string
|
||||||
{
|
{
|
||||||
$projectName = $task->project->name ?? '未知项目';
|
$taskName = self::escapeUserInput($task->name);
|
||||||
|
$projectName = self::escapeUserInput($task->project->name ?? '未知项目');
|
||||||
|
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
你是一个专业的项目管理助手。请根据以下任务标题,生成结构化的任务描述。
|
你是一个专业的项目管理助手。请根据以下任务标题,生成结构化的任务描述。
|
||||||
|
|
||||||
任务标题:{$task->name}
|
任务标题:{$taskName}
|
||||||
所属项目:{$projectName}
|
所属项目:{$projectName}
|
||||||
|
|
||||||
请按以下格式生成任务描述(使用 Markdown):
|
请按以下格式生成任务描述(使用 Markdown):
|
||||||
@ -198,12 +210,13 @@ PROMPT;
|
|||||||
*/
|
*/
|
||||||
private static function buildSubtasksPrompt(ProjectTask $task): string
|
private static function buildSubtasksPrompt(ProjectTask $task): string
|
||||||
{
|
{
|
||||||
$content = $task->content ?? '';
|
$taskName = self::escapeUserInput($task->name);
|
||||||
|
$content = self::escapeUserInput($task->content ?? '');
|
||||||
|
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
你是一个专业的项目管理助手。请将以下任务拆分为可执行的子任务。
|
你是一个专业的项目管理助手。请将以下任务拆分为可执行的子任务。
|
||||||
|
|
||||||
任务标题:{$task->name}
|
任务标题:{$taskName}
|
||||||
任务描述:{$content}
|
任务描述:{$content}
|
||||||
|
|
||||||
请返回 3-5 个子任务,每行一个,格式如下:
|
请返回 3-5 个子任务,每行一个,格式如下:
|
||||||
@ -226,9 +239,11 @@ PROMPT;
|
|||||||
{
|
{
|
||||||
$membersText = '';
|
$membersText = '';
|
||||||
foreach ($members as $member) {
|
foreach ($members as $member) {
|
||||||
$membersText .= "- {$member['nickname']}(ID:{$member['userid']})";
|
$nickname = self::escapeUserInput($member['nickname']);
|
||||||
|
$membersText .= "- {$nickname}(ID:{$member['userid']})";
|
||||||
if (!empty($member['profession'])) {
|
if (!empty($member['profession'])) {
|
||||||
$membersText .= ",职位:{$member['profession']}";
|
$profession = self::escapeUserInput($member['profession']);
|
||||||
|
$membersText .= ",职位:{$profession}";
|
||||||
}
|
}
|
||||||
$membersText .= ",进行中任务:{$member['in_progress_count']}个";
|
$membersText .= ",进行中任务:{$member['in_progress_count']}个";
|
||||||
$membersText .= ",近期完成:{$member['completed_count']}个";
|
$membersText .= ",近期完成:{$member['completed_count']}个";
|
||||||
@ -238,11 +253,14 @@ PROMPT;
|
|||||||
$membersText .= "\n";
|
$membersText .= "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$taskName = self::escapeUserInput($task->name);
|
||||||
|
$taskContent = self::escapeUserInput($task->content ?? '');
|
||||||
|
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
你是一个专业的项目管理助手。请根据任务内容和团队成员情况,推荐最合适的负责人。
|
你是一个专业的项目管理助手。请根据任务内容和团队成员情况,推荐最合适的负责人。
|
||||||
|
|
||||||
任务标题:{$task->name}
|
任务标题:{$taskName}
|
||||||
任务描述:{$task->content}
|
任务描述:{$taskContent}
|
||||||
|
|
||||||
团队成员:
|
团队成员:
|
||||||
{$membersText}
|
{$membersText}
|
||||||
@ -552,7 +570,10 @@ MD;
|
|||||||
*/
|
*/
|
||||||
public static function updateMessageStatus(int $msgId, int $dialogId, string $type, string $status): void
|
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) {
|
if (!$msg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,8 +53,16 @@ class AiTaskAnalyzeTask extends AbstractTask
|
|||||||
continue;
|
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 {
|
try {
|
||||||
// 检查是否满足执行条件
|
// 检查是否满足执行条件
|
||||||
|
|||||||
24
resources/assets/js/store/actions.js
vendored
24
resources/assets/js/store/actions.js
vendored
@ -5229,15 +5229,9 @@ export default {
|
|||||||
* 采纳 AI 建议
|
* 采纳 AI 建议
|
||||||
*/
|
*/
|
||||||
applyAiSuggestion({}, params) {
|
applyAiSuggestion({}, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return this.dispatch('call', {
|
||||||
this.dispatch('call', {
|
url: 'project/task/ai-apply',
|
||||||
url: 'project/task/ai-apply',
|
data: params,
|
||||||
data: params,
|
|
||||||
}).then(result => {
|
|
||||||
resolve(result);
|
|
||||||
}).catch(e => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -5245,15 +5239,9 @@ export default {
|
|||||||
* 忽略 AI 建议
|
* 忽略 AI 建议
|
||||||
*/
|
*/
|
||||||
dismissAiSuggestion({}, params) {
|
dismissAiSuggestion({}, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return this.dispatch('call', {
|
||||||
this.dispatch('call', {
|
url: 'project/task/ai-dismiss',
|
||||||
url: 'project/task/ai-dismiss',
|
data: params,
|
||||||
data: params,
|
|
||||||
}).then(result => {
|
|
||||||
resolve(result);
|
|
||||||
}).catch(e => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user