validate([ 'image' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:10240', ]); try { $file = $request->file('image'); $filename = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension(); // 存储到public目录 $path = $file->storeAs('uploads', $filename, 'public'); return response()->json([ 'success' => true, 'path' => $path, 'url' => asset('storage/' . $path), ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => '图片上传失败: ' . $e->getMessage(), ], 500); } } /** * 生成周报 */ public function generate(Request $request) { // 设置较长的执行时间,因为AI生成可能需要较长时间 set_time_limit(300); // 5分钟 $request->validate([ 'tasks' => 'required|array|min:1', 'tasks.*.description' => 'required|string|max:2000', 'tasks.*.images' => 'nullable|array', // 支持多图 'tasks.*.images.*' => 'nullable|string', // 每张图片路径 'week_start' => 'nullable|date', 'week_end' => 'nullable|date', 'author' => 'nullable|string|max:100', 'next_week_tasks' => 'nullable|array', 'next_week_tasks.*' => 'nullable|string|max:2000', ]); $tasks = $request->input('tasks'); $weekStart = $request->input('week_start') ?? date('Y-m-d', strtotime('monday this week')); $weekEnd = $request->input('week_end') ?? date('Y-m-d', strtotime('sunday this week')); $author = (string) ($request->input('author') ?? ''); $nextWeekTasks = $request->input('next_week_tasks', []); // 构建提示词 $taskList = $this->buildTaskList($tasks); $nextWeekList = $this->buildNextWeekTaskList($nextWeekTasks); $prompt = $this->buildPrompt($taskList, $weekStart, $weekEnd, $author, $nextWeekList); try { // 调用千问API $reportContent = $this->callQwenApi($prompt); // 插入图片到报告中 $reportWithImages = $this->insertImagesToReport($reportContent, $tasks); return response()->json([ 'success' => true, 'report' => $reportWithImages, 'raw_report' => $reportContent, ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => '生成周报失败: ' . $e->getMessage(), ], 500); } } /** * 构建任务列表文本 */ private function buildTaskList(array $tasks): string { $list = []; foreach ($tasks as $index => $task) { $num = $index + 1; $desc = trim($task['description']); // 支持多图:检查 images 数组 $images = $task['images'] ?? []; $imageCount = count(array_filter($images)); $hasImage = $imageCount > 0 ? "(有{$imageCount}张截图)" : ''; $list[] = "{$num}. {$desc}{$hasImage}"; } return implode("\n", $list); } /** * 构建下周任务列表文本 */ private function buildNextWeekTaskList(array $tasks): string { if (empty($tasks)) { return ''; } $list = []; foreach ($tasks as $index => $desc) { if (!empty(trim($desc))) { $num = $index + 1; $list[] = "{$num}. " . trim($desc); } } return implode("\n", $list); } /** * 构建完整提示词 */ private function buildPrompt(string $taskList, string $weekStart, string $weekEnd, string $author, string $nextWeekList = ''): string { $authorInfo = $author ? "作者:{$author}" : ""; // 根据是否有下周计划来调整要求 $nextWeekSection = ''; $nextWeekRequirement = ''; if (!empty(trim($nextWeekList))) { $nextWeekSection = "\n\n下周计划任务:\n{$nextWeekList}"; $nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,简要列出计划要做的事项"; } else { $nextWeekRequirement = "\n8. 不要添加下周工作计划部分"; } return <<withoutVerifying() ->withHeaders([ 'Authorization' => "Bearer {$apiKey}", 'Content-Type' => 'application/json', ]) ->post($apiUrl, [ 'model' => $model, 'messages' => [ [ 'role' => 'system', 'content' => '你是一位专业的工作周报撰写助手,擅长将简单的任务描述转化为专业、详细的周报内容。' ], [ 'role' => 'user', 'content' => $prompt ] ], 'temperature' => 0.7, 'max_tokens' => 4000, ]); if (!$response->successful()) { $error = $response->json('error.message') ?? $response->body(); throw new \Exception("API调用失败: {$error}"); } $data = $response->json(); if (!isset($data['choices'][0]['message']['content'])) { throw new \Exception('API返回数据格式错误'); } return $data['choices'][0]['message']['content']; } /** * 在报告中插入图片(支持多图) */ private function insertImagesToReport(string $report, array $tasks): string { // 首先尝试替换AI生成的占位符 foreach ($tasks as $index => $task) { $num = $index + 1; // 支持多种占位符格式 $placeholders = [ "[图片占位符-任务{$num}]", "[图片占位符-{$num}]", "[图片占位符{$num}]", "【图片占位符-任务{$num}】", "【图片占位符-{$num}】", "【图片占位符{$num}】", ]; // 获取该任务的所有图片 $images = $task['images'] ?? []; $validImages = array_filter($images); if (!empty($validImages)) { // 构建多图Markdown $imagesMarkdown = "\n\n"; foreach ($validImages as $imgIndex => $imagePath) { $imageUrl = asset('storage/' . $imagePath); $imgNum = $imgIndex + 1; $imagesMarkdown .= "![任务{$num}-截图{$imgNum}]({$imageUrl})\n\n"; } // 替换占位符 foreach ($placeholders as $placeholder) { $report = str_replace($placeholder, $imagesMarkdown, $report); } } else { // 没有图片时移除所有占位符 foreach ($placeholders as $placeholder) { $report = str_replace($placeholder, '', $report); } } } // 清理所有未使用的占位符(各种格式) $report = preg_replace('/\[图片占位符[-]?任务?\d*\]/', '', $report); $report = preg_replace('/【图片占位符[-]?任务?\d*】/', '', $report); $report = preg_replace('/\[?图片占位符[-—]?\d+\]?/', '', $report); // 清理可能产生的多余空行 $report = preg_replace('/\n{3,}/', "\n\n", $report); return $report; } /** * 下载Markdown格式 */ public function downloadMarkdown(Request $request) { $request->validate([ 'content' => 'required|string', 'filename' => 'nullable|string|max:100', ]); $content = $request->input('content'); $filename = $request->input('filename', '周报_' . date('Y-m-d')) . '.md'; return response($content) ->header('Content-Type', 'text/markdown; charset=utf-8') ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); } /** * 下载PDF格式 */ public function downloadPdf(Request $request) { $request->validate([ 'content' => 'required|string', 'filename' => 'nullable|string|max:100', ]); $markdownContent = $request->input('content'); $filename = $request->input('filename', '周报_' . date('Y-m-d')) . '.pdf'; // 将Markdown转换为HTML $parsedown = new Parsedown(); $htmlContent = $parsedown->text($markdownContent); // 添加样式 $styledHtml = $this->wrapHtmlWithStyle($htmlContent); // 生成PDF - 启用远程字体和图片 $pdf = Pdf::loadHTML($styledHtml); $pdf->setPaper('A4', 'portrait'); $pdf->setOption('isRemoteEnabled', true); $pdf->setOption('defaultFont', 'sans-serif'); return $pdf->download($filename); } /** * 为HTML添加样式 */ private function wrapHtmlWithStyle(string $html): string { return << {$html} HTML; } }