$msg]); } // ---- 基础校验 ---- if (!isset($OJ_LLM_ENABLED) || !$OJ_LLM_ENABLED) { // 返回 JSON(非 SSE),因为前端可能用 $.ajax 调用 header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "AI点评功能未开启"], JSON_UNESCAPED_UNICODE); exit; } // 检查登录 session_start(); if (!isset($_SESSION[$OJ_NAME . '_user_id'])) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "请先登录"], JSON_UNESCAPED_UNICODE); exit; } // 获取 solution_id if (!isset($_GET['sid'])) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "缺少参数 sid"], JSON_UNESCAPED_UNICODE); exit; } $sid = intval($_GET['sid']); if ($sid <= 0) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "无效的 solution_id"], JSON_UNESCAPED_UNICODE); exit; } // ---- 1. 检查缓存 ---- // 自动建表(首次调用时) pdo_query("CREATE TABLE IF NOT EXISTS `llm_review` ( `solution_id` INT NOT NULL, `review` TEXT NOT NULL, `create_time` DATETIME NOT NULL, PRIMARY KEY (`solution_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); $cache = pdo_query("SELECT `review` FROM `llm_review` WHERE `solution_id`=?", $sid); if ($cache !== -1 && !empty($cache)) { // 有缓存,走 SSE 立刻返回(前端统一用 SSE 接收) header("Content-Type: text/event-stream; charset=utf-8"); header("Cache-Control: no-cache"); header("X-Accel-Buffering: no"); sse_send("cached", ["text" => $cache[0]['review']]); sse_send("done", []); exit; } // ---- 设置 SSE 响应头 ---- header("Content-Type: text/event-stream; charset=utf-8"); header("Cache-Control: no-cache"); header("X-Accel-Buffering: no"); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); } @ini_set('zlib.output_compression', 0); @ini_set('implicit_flush', 1); while (ob_get_level()) ob_end_flush(); // ---- 2. 查询 solution 信息 ---- $solution = pdo_query("SELECT * FROM `solution` WHERE `solution_id`=?", $sid); if (empty($solution)) { sse_error("找不到该提交记录"); exit; } $sol = $solution[0]; $problem_id = intval($sol['problem_id']); $language = intval($sol['language']); $result_code = intval($sol['result']); $pass_rate = floatval($sol['pass_rate']) * 100; $lang_name = isset($language_name[$language]) ? $language_name[$language] : "unknown"; // ---- 3. 查询学生源码 ---- $src = pdo_query("SELECT `source` FROM `source_code_user` WHERE `solution_id`=?", $sid); $student_code = (!empty($src) && $src !== -1) ? $src[0]['source'] : ""; // ---- 4. 查询题目信息 ---- $prob = pdo_query("SELECT `title`,`description`,`input`,`output`,`sample_input`,`sample_output`,`hint` FROM `problem` WHERE `problem_id`=?", $problem_id); if (empty($prob) || $prob === -1) { sse_error("找不到该题目"); exit; } $p = $prob[0]; // ---- 5. 查询第一个 AC 代码(同语言) ---- $first_ac = pdo_query( "SELECT `solution_id` FROM `solution` WHERE `problem_id`=? AND `result`=4 AND `language`=? ORDER BY `in_date` ASC LIMIT 1", $problem_id, $language ); $reference_code = ""; if (!empty($first_ac) && $first_ac !== -1) { $ref_src = pdo_query("SELECT `source` FROM `source_code_user` WHERE `solution_id`=?", intval($first_ac[0]['solution_id'])); if (!empty($ref_src) && $ref_src !== -1) { $reference_code = $ref_src[0]['source']; } } // ---- 6. 查询 diff/error 信息 ---- $diff_info = ""; if ($result_code == 11) { $err = pdo_query("SELECT `error` FROM `compileinfo` WHERE `solution_id`=?", $sid); if (!empty($err) && $err !== -1) { $diff_info = $err[0]['error']; } } else { $err = pdo_query("SELECT `error` FROM `runtimeinfo` WHERE `solution_id`=?", $sid); if (!empty($err) && $err !== -1) { $diff_info = $err[0]['error']; } } // ---- 7. 组装 Prompt ---- $system_prompt = isset($OJ_LLM_SYSTEM_PROMPT) && $OJ_LLM_SYSTEM_PROMPT !== "" ? $OJ_LLM_SYSTEM_PROMPT : << $model, "max_tokens" => $max_tokens, "system" => $system_prompt, "temperature" => 1.0, "stream" => true, "messages" => [ ["role" => "user", "content" => $user_prompt] ] ]); // ---- 流式 SSE 转发 ---- // 追踪连接状态和累积文本 $GLOBALS['_llm_alive'] = true; $GLOBALS['_llm_full_text'] = ""; $GLOBALS['_llm_buffer'] = ""; $GLOBALS['_llm_sent_text'] = ""; // 已发送给前端的文本(用于增量比较) // 检测客户端断开 register_shutdown_function(function() { $GLOBALS['_llm_alive'] = false; }); $ch = curl_init($api_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $request_body, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "x-api-key: " . $api_key, "anthropic-version: 2023-06-01" ], CURLOPT_RETURNTRANSFER => false, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_WRITEFUNCTION => function($ch, $chunk) use (&$_llm_alive, &$_llm_full_text, &$_llm_buffer) { if (!$_llm_alive) return 0; $_llm_buffer .= $chunk; // 按行解析 SSE while (($pos = strpos($_llm_buffer, "\n")) !== false) { $line = substr($_llm_buffer, 0, $pos); $_llm_buffer = substr($_llm_buffer, $pos + 1); $line = trim($line); if (empty($line)) continue; // 解析 data: 行 if (strncmp($line, "data: ", 6) === 0) { $json_str = substr($line, 6); if ($json_str === "[DONE]") continue; $data = json_decode($json_str, true); if (!$data) continue; $type = isset($data['type']) ? $data['type'] : ''; // content_block_delta: 增量文本 if ($type === 'content_block_delta' && isset($data['delta']['text'])) { $_llm_full_text .= $data['delta']['text']; sse_send("chunk", ["text" => $data['delta']['text']]); // message_delta: 消息完成 } elseif ($type === 'message_delta' && isset($data['delta']['stop_reason'])) { if ($data['delta']['stop_reason'] === 'end_turn' || $data['delta']['stop_reason'] === 'max_tokens') { sse_send("done", []); return 0; // 停止 curl } // message_start / ping: 忽略 } elseif ($type === 'message_start' || $type === 'ping') { continue; // content_block_start / stop: 忽略 } elseif ($type === 'content_block_start' || $type === 'content_block_stop') { continue; // 错误事件 } elseif ($type === 'error') { $msg = isset($data['error']['message']) ? $data['error']['message'] : '未知错误'; sse_send("error", ["message" => "API错误: " . $msg]); return 0; } } } return strlen($chunk); } ]); $result = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); // 如果连接断开,不缓存也不报错 if (!$_llm_alive) exit; // ---- 处理错误 ---- if ($result === false && empty($_llm_full_text)) { sse_error("网络请求失败: " . ($curl_error ?: "未知错误")); exit; } // 检查 HTTP 错误(如果 write callback 没有收到数据) if (empty($_llm_full_text) && $http_code !== 200) { sse_error("API请求失败 (HTTP $http_code)"); exit; } if (empty($_llm_full_text)) { sse_error("API返回为空"); exit; } // ---- 9. 缓存完整结果 ---- $review_text = $_llm_full_text; $insert_ok = pdo_query("INSERT INTO `llm_review` (`solution_id`, `review`, `create_time`) VALUES (?, ?, NOW())", $sid, $review_text); if ($insert_ok === -1) { // 插入失败(可能已有记录),尝试更新 pdo_query("UPDATE `llm_review` SET `review`=?, `create_time`=NOW() WHERE `solution_id`=?", $review_text, $sid); }