diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index d3de528b5..042925faf 100755 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -11,7 +11,6 @@ use App\Models\ReportLink; use App\Models\ReportReceive; use App\Models\User; use App\Models\WebSocketDialogMsg; -use App\Module\AI; use App\Module\Base; use App\Module\Doo; use App\Tasks\PushTask; @@ -523,33 +522,38 @@ class ReportController extends AbstractController } /** - * @api {post} api/report/ai_analyze 生成工作汇报 AI 分析 + * @api {post} api/report/analysave 保存工作汇报 AI 分析 * - * @apiDescription 需要token身份,仅支持报告提交人或接收人发起分析 + * @apiDescription 需要token身份,仅支持报告提交人或接收人保存分析 * @apiVersion 1.0.0 * @apiGroup report - * @apiName ai_analyze + * @apiName analysave * - * @apiParam {Number} id 报告ID - * @apiParam {Array|String} [focus] 分析关注点(可选) + * @apiParam {Number} id 报告ID + * @apiParam {String} text 分析内容(Markdown) + * @apiParam {String} [model] 分析使用的模型标识(可选) * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - * @apiSuccess {Number} data.id 分析记录ID - * @apiSuccess {String} data.text 分析内容(Markdown) + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + * @apiSuccess {Number} data.id 分析记录ID + * @apiSuccess {String} data.text 分析内容(Markdown) * @apiSuccess {String} data.updated_at 最近更新时间 */ - public function ai_analyze(): array + public function analysave(): array { $user = User::auth(); $id = intval(Request::input("id")); if ($id <= 0) { return Base::retError("缺少ID参数"); } + $text = trim((string)Request::input('text', '')); + if ($text === '') { + return Base::retError("分析内容不能为空"); + } + $model = trim((string)Request::input('model', '')); $report = Report::getOne($id); - if (!$this->userCanAccessReport($report, $user)) { return Base::retError("无权访问该工作汇报"); } @@ -559,31 +563,6 @@ class ReportController extends AbstractController ->whereUserid($user->userid) ->first(); - $context = [ - 'viewer_name' => $user->nickname ?? ('用户' . $user->userid), - ]; - if (!empty($user->profession)) { - $context['viewer_role'] = $user->profession; - } elseif (is_array($user->identity) && !empty($user->identity)) { - $context['viewer_role'] = implode('/', $user->identity); - } - if ($analysis && $analysis->analysis_text) { - $context['previous_feedback'] = $analysis->analysis_text; - } - - $focus = Request::input('focus'); - if (is_array($focus)) { - $context['focus'] = $focus; - } elseif (is_string($focus) && trim($focus) !== '') { - $context['focus_note'] = trim($focus); - } - - $result = AI::analyzeReport($report, $context); - if (Base::isError($result)) { - return Base::retError("生成AI分析失败", $result); - } - $data = $result['data']; - if (!$analysis) { $analysis = ReportAnalysis::fillInstance([ 'rid' => $report->id, @@ -591,10 +570,19 @@ class ReportController extends AbstractController ]); } + $viewerRole = $user->profession ?: (is_array($user->identity) && !empty($user->identity) ? implode('/', $user->identity) : null); + $focusMeta = null; + $focus = Request::input('focus'); + if (is_array($focus)) { + $focusMeta = array_filter(array_map('trim', $focus)); + } elseif (is_string($focus) && trim($focus) !== '') { + $focusMeta = [trim($focus)]; + } + $meta = array_filter([ - 'viewer_role' => $context['viewer_role'] ?? null, - 'viewer_name' => $context['viewer_name'] ?? null, - 'focus' => $context['focus'] ?? null, + 'viewer_role' => $viewerRole, + 'viewer_name' => $user->nickname ?? null, + 'focus' => $focusMeta, ], function ($value) { if (is_array($value)) { return !empty($value); @@ -603,8 +591,8 @@ class ReportController extends AbstractController }); $analysis->updateInstance([ - 'model' => $data['model'] ?? '', - 'analysis_text' => $data['text'], + 'model' => $model, + 'analysis_text' => $text, 'meta' => $meta, ]); $analysis->save(); @@ -614,72 +602,11 @@ class ReportController extends AbstractController return Base::retSuccess("success", [ 'id' => $analysis->id, 'text' => $analysis->analysis_text, + 'model' => $analysis->model, 'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null, ]); } - /** - * @api {post} api/report/ai_organize 整理工作汇报内容 - * - * @apiDescription 需要token身份,根据当前草稿重新整理工作汇报结构 - * @apiVersion 1.0.0 - * @apiGroup report - * @apiName ai_organize - * - * @apiParam {String} content 汇报内容(HTML) - * @apiParam {String} [title] 汇报标题 - * @apiParam {String} [type] 汇报类型(weekly/daily) - * @apiParam {Array|String} [focus] 整理关注点(可选) - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - * @apiSuccess {String} data.html 整理后的内容(HTML) - */ - public function ai_organize(): array - { - $user = User::auth(); - $content = trim((string)Request::input("content", "")); - if ($content === '') { - return Base::retError("汇报内容不能为空"); - } - - $title = trim((string)Request::input("title", "")); - $type = trim((string)Request::input("type", "")); - - $markdown = Base::html2markdown($content); - if ($markdown === '') { - return Base::retError("汇报内容解析失败"); - } - - $context = array_filter([ - 'title' => $title, - 'type' => $type, - ]); - - $focus = Request::input('focus'); - if (is_array($focus)) { - $context['focus'] = $focus; - } elseif (is_string($focus) && trim($focus) !== '') { - $context['focus'] = [trim($focus)]; - } - - $result = AI::organizeReportContent($markdown, $context); - if (Base::isError($result)) { - return Base::retError("整理汇报失败", $result); - } - - $data = $result['data']; - $html = Base::markdown2html($data['text']); - if (trim($html) === '') { - return Base::retError("整理后的内容为空"); - } - - return Base::retSuccess("success", [ - 'html' => $html, - ]); - } - /** * @api {get} api/report/mark 标记已读/未读 * diff --git a/app/Module/AI.php b/app/Module/AI.php index e81b39c02..4caa43d3b 100644 --- a/app/Module/AI.php +++ b/app/Module/AI.php @@ -2,7 +2,6 @@ namespace App\Module; -use App\Models\Report; use App\Models\Setting; use Cache; use Carbon\Carbon; @@ -318,135 +317,6 @@ class AI return $result; } - /** - * 对工作汇报内容进行分析 - * @param Report $report - * @param array $context - * @return array - */ - public static function analyzeReport(Report $report, array $context = []) - { - $prompt = self::buildReportAnalysisPrompt($report, $context); - if ($prompt === '') { - return Base::retError("报告内容为空,无法进行分析"); - } - - $post = json_encode([ - "model" => "gpt-5-mini", - "reasoning_effort" => "minimal", - "messages" => [ - [ - "role" => "system", - "content" => << "user", - "content" => $prompt, - ], - ], - ]); - - $ai = new self($post); - $ai->setTimeout(60); - - $res = $ai->request(); - if (Base::isError($res)) { - return Base::retError("工作汇报分析失败", $res); - } - - $content = trim($res['data']); - $content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content); - $content = preg_replace('/\s*```\s*$/', '', $content); - $content = trim($content); - - if ($content === '') { - return Base::retError("工作汇报分析结果为空"); - } - - return Base::retSuccess("success", [ - 'text' => $content, - ]); - } - - /** - * 整理优化工作汇报内容 - * @param string $markdown 用户当前的工作汇报(Markdown) - * @param array $context 上下文信息 - * @return array - */ - public static function organizeReportContent(string $markdown, array $context = []) - { - $markdown = trim((string)$markdown); - if ($markdown === '') { - return Base::retError("工作汇报内容不能为空"); - } - - $prompt = self::buildReportOrganizePrompt($markdown, $context); - if ($prompt === '') { - return Base::retError("整理内容为空"); - } - - $post = json_encode([ - "model" => "gpt-5-mini", - "reasoning_effort" => "minimal", - "messages" => [ - [ - "role" => "system", - "content" => << "user", - "content" => $prompt, - ], - ], - ]); - - $ai = new self($post); - $ai->setTimeout(60); - - $res = $ai->request(); - if (Base::isError($res)) { - return Base::retError("汇报整理失败", $res); - } - - $content = trim($res['data']); - $content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content); - $content = preg_replace('/\s*```\s*$/', '', $content); - $content = trim($content); - - if ($content === '') { - return Base::retError("汇报整理结果为空"); - } - - return Base::retSuccess("success", [ - 'text' => $content, - ]); - } - /** * 通过 openAI 生成职场笑话、心灵鸡汤 * @param bool $noCache 是否禁用缓存 @@ -546,106 +416,6 @@ class AI return $result; } - /** - * 构建工作汇报分析的提示词 - * @param Report $report - * @param array $context - * @return string - */ - private static function buildReportAnalysisPrompt(Report $report, array $context = []): string - { - $sections = []; - - $metaLines = []; - $metaLines[] = "标题:" . ($report->title ?: "(未填写)"); - $metaLines[] = "类型:" . match ($report->type) { - Report::WEEKLY => "周报", - Report::DAILY => "日报", - default => $report->type, - }; - if ($report->created_at) { - $createdAt = $report->created_at instanceof Carbon - ? $report->created_at->toDateTimeString() - : (string)$report->created_at; - $metaLines[] = "提交时间:" . $createdAt; - } - if (!empty($context['viewer_role'])) { - $metaLines[] = "查看人角色:" . trim($context['viewer_role']); - } - if (!empty($context['viewer_name'])) { - $metaLines[] = "查看人:" . trim($context['viewer_name']); - } - if (!empty($metaLines)) { - $sections[] = "### 基础信息\n" . implode("\n", array_map(function ($line) { - return "- " . $line; - }, $metaLines)); - } - - if (!empty($context['focus']) && is_array($context['focus'])) { - $focusItems = array_filter(array_map('trim', $context['focus'])); - if (!empty($focusItems)) { - $sections[] = "### 关注重点\n" . implode("\n", array_map(function ($line) { - return "- " . $line; - }, $focusItems)); - } - } elseif (!empty($context['focus_note'])) { - $sections[] = "### 关注重点\n- " . trim($context['focus_note']); - } - - if (!empty($context['previous_feedback'])) { - $sections[] = "### 历史反馈\n" . trim($context['previous_feedback']); - } - - $contentMarkdown = trim(Base::html2markdown($report->content)); - if ($contentMarkdown !== '') { - $sections[] = "### 汇报正文\n" . $contentMarkdown; - } - - return trim(implode("\n\n", array_filter($sections))); - } - - /** - * 构建工作汇报整理的提示词 - * @param string $markdown - * @param array $context - * @return string - */ - private static function buildReportOrganizePrompt(string $markdown, array $context = []): string - { - $sections = []; - - $infoLines = []; - if (!empty($context['title'])) { - $infoLines[] = "汇报标题:" . trim($context['title']); - } - if (!empty($context['type'])) { - $typeLabel = match ($context['type']) { - Report::WEEKLY => "周报", - Report::DAILY => "日报", - default => $context['type'], - }; - $infoLines[] = "汇报类型:" . $typeLabel; - } - if (!empty($context['focus']) && is_array($context['focus'])) { - $focusItems = array_filter(array_map('trim', $context['focus'])); - if (!empty($focusItems)) { - $sections[] = "### 需要重点整理的方向\n" . implode("\n", array_map(function ($line) { - return "- " . $line; - }, $focusItems)); - } - } - - if (!empty($infoLines)) { - $sections[] = "### 汇报背景(仅供参考,请勿写入输出)\n" . implode("\n", array_map(function ($line) { - return "- " . $line; - }, $infoLines)); - } - - $sections[] = "### 原始汇报草稿(请整理后仅输出正文内容)\n" . $markdown; - - return trim(implode("\n\n", array_filter($sections))); - } - /** * 获取 ollama 模型 * @param $baseUrl diff --git a/language/original-web.txt b/language/original-web.txt index fe4d7f023..3fe476d7a 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2218,7 +2218,6 @@ MCP 服务器已启动成功! AI 分析 重新分析 生成分析 -AI 正在生成分析... 最后更新: 暂无 AI 分析,点击右侧按钮生成。 AI 整理汇报 diff --git a/language/translate.json b/language/translate.json index f69446bdd..81270a403 100644 --- a/language/translate.json +++ b/language/translate.json @@ -31811,18 +31811,6 @@ "id": "Hasilkan analisis", "ru": "Сгенерировать анализ" }, - { - "key": "AI 正在生成分析...", - "zh": "", - "zh-CHT": "AI 正在生成分析...", - "en": "AI is generating analysis...", - "ko": "AI가 분석을 생성하는 중...", - "ja": "AIが分析を生成しています...", - "de": "KI erstellt die Analyse...", - "fr": "L’IA génère l’analyse...", - "id": "AI sedang menghasilkan analisis...", - "ru": "ИИ формирует анализ..." - }, { "key": "最后更新:", "zh": "", diff --git a/resources/assets/js/pages/manage/components/ReportDetail.vue b/resources/assets/js/pages/manage/components/ReportDetail.vue index 72c855244..e39214daa 100644 --- a/resources/assets/js/pages/manage/components/ReportDetail.vue +++ b/resources/assets/js/pages/manage/components/ReportDetail.vue @@ -49,16 +49,12 @@ -
- - {{ $L("AI 正在生成分析...") }} -
-
+
{{ $L("最后更新:") }}{{ aiAnalysis.updated_at }}
@@ -77,6 +73,9 @@ \ No newline at end of file + diff --git a/resources/assets/js/utils/ai.js b/resources/assets/js/utils/ai.js index 51a01819f..e020449a6 100644 --- a/resources/assets/js/utils/ai.js +++ b/resources/assets/js/utils/ai.js @@ -336,6 +336,15 @@ const REPORT_AI_SYSTEM_PROMPT = `你是一名资深团队管理教练,需要 - 若原文包含数据或里程碑,保留并突出这些数字 - 若某一章节没有信息,请输出“暂无”而非留空`; +const REPORT_ANALYSIS_SYSTEM_PROMPT = `你是一名经验丰富的团队管理顾问,擅长阅读和分析员工提交的工作汇报,能够快速提炼重点并给出可执行建议。 + +输出要求: +1. 使用简洁的 Markdown 结构(标题、无序列表、引用等),不要使用代码块或 JSON +2. 先给出整体概览,再列出具体亮点、风险或问题,以及明确的改进建议 +3. 如有数据或目标,应评估其完成情况和后续跟进要点 +4. 语气保持专业、客观、中立,不过度夸赞或批评 +5. 控制在 200-400 字之间,可视内容复杂度略微增减,但保持紧凑`; + export { AIModelNames, AINormalizeJsonContent, @@ -345,4 +354,5 @@ export { TASK_AI_SYSTEM_PROMPT, PROJECT_AI_SYSTEM_PROMPT, REPORT_AI_SYSTEM_PROMPT, + REPORT_ANALYSIS_SYSTEM_PROMPT, }