diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 003efef69..d3de528b5 100755 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -618,6 +618,68 @@ class ReportController extends AbstractController ]); } + /** + * @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 13d1325a4..3a1101142 100644 --- a/app/Module/AI.php +++ b/app/Module/AI.php @@ -635,9 +635,8 @@ class AI return Base::retError("报告内容为空,无法进行分析"); } - $model = "gpt-5-mini"; $post = json_encode([ - "model" => $model, + "model" => "gpt-5-mini", "reasoning_effort" => "minimal", "messages" => [ [ @@ -679,7 +678,76 @@ class AI return Base::retSuccess("success", [ 'text' => $content, - 'model' => $model, + ]); + } + + /** + * 整理优化工作汇报内容 + * @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, ]); } @@ -1010,6 +1078,48 @@ class AI 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/resources/assets/js/pages/manage/components/ReportEdit.vue b/resources/assets/js/pages/manage/components/ReportEdit.vue index 94c26dd2a..b651ed375 100644 --- a/resources/assets/js/pages/manage/components/ReportEdit.vue +++ b/resources/assets/js/pages/manage/components/ReportEdit.vue @@ -1,50 +1,76 @@ + \ No newline at end of file diff --git a/resources/assets/sass/components/report.scss b/resources/assets/sass/components/report.scss index e08c17cd0..fa4515557 100644 --- a/resources/assets/sass/components/report.scss +++ b/resources/assets/sass/components/report.scss @@ -109,62 +109,68 @@ } } } +} - .report-content { - border-top: 1px solid #eeeeee; - padding-top: 24px; - margin-top: 24px; +.report-content { + border-top: 1px solid #eeeeee; + padding-top: 24px; + margin-top: 24px; + width: 100%; + + &.organize-preview { + border-top: none; + padding-top: 0; + margin-top: 0; + } + + ul, ol, li { + margin: revert; + padding: revert; + } + + h2 { + font-size: 20px; + margin-bottom: 10px; + } + + table { width: 100%; + border-collapse: collapse; + border-spacing: 0; - ul, ol, li { - margin: revert; - padding: revert; + th, td { + line-height: 20px; + padding: 10px; + border: 1px solid #e8e8e8; } - h2 { - font-size: 20px; - margin-bottom: 10px; + th { + background: #f8f8f8; + padding: 10px 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-align: center; } - table { - width: 100%; - border-collapse: collapse; - border-spacing: 0; - - th, td { - line-height: 20px; - padding: 10px; - border: 1px solid #e8e8e8; - } - - th { - background: #f8f8f8; - padding: 10px 16px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - text-align: center; - } - - td { - background: #fff; - text-align: left; - } + td { + background: #fff; + text-align: left; } + } - ol { - margin-bottom: 20px; - padding-left: 18px; + ol { + margin-bottom: 20px; + padding-left: 18px; - li { - font-size: 14px; - line-height: 24px; - } + li { + font-size: 14px; + line-height: 24px; } + } - img { - max-width: 100%; - } + img { + max-width: 100%; } } @@ -307,6 +313,12 @@ .report-foot { margin-bottom: 0; + } + + .report-bottoms { + display: flex; + align-items: center; + gap: 12px; .report-bottom { height: 38px; line-height: 36px;