diff --git a/core/sim/sim_3_01/clang.c b/core/sim/sim_3_01/clang.c
new file mode 100644
index 0000000..e69de29
diff --git a/web/include/db_info.inc.php b/web/include/db_info.inc.php
index 6291506..88f7181 100644
--- a/web/include/db_info.inc.php
+++ b/web/include/db_info.inc.php
@@ -146,7 +146,7 @@ static $OJ_SaaS_ENABLE=false;
static $OJ_MENU_NEWS=true;
/* LLM AI Review - AI错误点评功能 */
-static $OJ_LLM_ENABLED=false; // 总开关,设为true开启AI点评
+static $OJ_LLM_ENABLED=true; // 总开关,设为true开启AI点评
static $OJ_LLM_API_URL="https://api.minimaxi.com/anthropic/v1/messages"; // API地址(MiniMax Anthropic兼容格式)
static $OJ_LLM_API_KEY="sk-cp-w571CJpA_6WP5P6b_xGVbjOqXOGBWTQsiNY9xxAPambouIhD7ic3GHIoWce0ON_Y7Q0rVLzBWtgwwS8cQVUxS1guBxYkPJoJ33q0jEcx0Qei60bx5La4fB4"; // 填入你的MiniMax API Key
static $OJ_LLM_MODEL="MiniMax-M2.7"; // 模型名称,如 MiniMax-M2.7 / MiniMax-M2.5 / MiniMax-M2.5-highspeed
diff --git a/web/llm-chat-api.php b/web/llm-chat-api.php
new file mode 100644
index 0000000..9427969
--- /dev/null
+++ b/web/llm-chat-api.php
@@ -0,0 +1,275 @@
+ $msg]);
+}
+
+// ---- 基础校验 ----
+if (!isset($OJ_LLM_ENABLED) || !$OJ_LLM_ENABLED) {
+ 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;
+}
+
+$user_id = $_SESSION[$OJ_NAME . '_user_id'];
+
+// ---- 自动建表 ----
+pdo_query("CREATE TABLE IF NOT EXISTS `llm_chat_session` (
+ `session_id` INT AUTO_INCREMENT PRIMARY KEY,
+ `user_id` VARCHAR(48) NOT NULL,
+ `title` VARCHAR(200) NOT NULL DEFAULT '新对话',
+ `create_time` DATETIME NOT NULL,
+ `update_time` DATETIME NOT NULL,
+ INDEX `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
+
+pdo_query("CREATE TABLE IF NOT EXISTS `llm_chat_message` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `session_id` INT NOT NULL,
+ `role` VARCHAR(20) NOT NULL,
+ `content` TEXT NOT NULL,
+ `create_time` DATETIME NOT NULL,
+ INDEX `idx_session` (`session_id`),
+ INDEX `idx_session_role` (`session_id`, `id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
+
+// ---- GET 请求 ----
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+ header("Content-Type: application/json; charset=utf-8");
+ $action = isset($_GET['action']) ? $_GET['action'] : '';
+
+ if ($action === 'sessions') {
+ $sessions = pdo_query(
+ "SELECT `session_id`, `title`, `create_time`, `update_time` FROM `llm_chat_session` WHERE `user_id`=? ORDER BY `update_time` DESC",
+ $user_id
+ );
+ echo json_encode(["sessions" => $sessions !== -1 ? $sessions : []], JSON_UNESCAPED_UNICODE);
+
+ } elseif ($action === 'messages') {
+ $session_id = isset($_GET['session_id']) ? intval($_GET['session_id']) : 0;
+ if ($session_id <= 0) { echo json_encode(["error" => "无效的会话ID"]); exit; }
+
+ $check = pdo_query("SELECT 1 FROM `llm_chat_session` WHERE `session_id`=? AND `user_id`=?", $session_id, $user_id);
+ if (empty($check) || $check === -1) { echo json_encode(["error" => "无权访问"]); exit; }
+
+ $messages = pdo_query("SELECT `role`, `content` FROM `llm_chat_message` WHERE `session_id`=? ORDER BY `id` ASC", $session_id);
+ echo json_encode(["messages" => $messages !== -1 ? $messages : []], JSON_UNESCAPED_UNICODE);
+
+ } elseif ($action === 'last_session') {
+ $last = pdo_query("SELECT `session_id` FROM `llm_chat_session` WHERE `user_id`=? ORDER BY `update_time` DESC LIMIT 1", $user_id);
+ echo json_encode(["session_id" => (!empty($last) && $last !== -1) ? intval($last[0]['session_id']) : 0], JSON_UNESCAPED_UNICODE);
+ }
+ exit;
+}
+
+// ---- DELETE 请求 ----
+if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
+ header("Content-Type: application/json; charset=utf-8");
+ $input = json_decode(file_get_contents('php://input'), true);
+ $session_id = isset($input['session_id']) ? intval($input['session_id']) : 0;
+ if ($session_id <= 0) { echo json_encode(["error" => "无效的会话ID"]); exit; }
+
+ $check = pdo_query("SELECT 1 FROM `llm_chat_session` WHERE `session_id`=? AND `user_id`=?", $session_id, $user_id);
+ if (empty($check) || $check === -1) { echo json_encode(["error" => "无权删除"]); exit; }
+
+ pdo_query("DELETE FROM `llm_chat_message` WHERE `session_id`=?", $session_id);
+ pdo_query("DELETE FROM `llm_chat_session` WHERE `session_id`=?", $session_id);
+ echo json_encode(["success" => true], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+// ---- POST 请求 ----
+$input = json_decode(file_get_contents('php://input'), true);
+if (!$input) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "无效请求"]); exit; }
+
+$action = isset($input['action']) ? $input['action'] : 'chat';
+
+// 创建新会话
+if ($action === 'create_session') {
+ header("Content-Type: application/json; charset=utf-8");
+ $title = isset($input['title']) ? trim($input['title']) : '新对话';
+ $now = date('Y-m-d H:i:s');
+ $session_id = pdo_query("INSERT INTO `llm_chat_session` (`user_id`, `title`, `create_time`, `update_time`) VALUES (?, ?, ?, ?)", $user_id, $title, $now, $now);
+ echo json_encode(["session_id" => $session_id, "title" => $title], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+// 重命名会话
+if ($action === 'rename_session') {
+ header("Content-Type: application/json; charset=utf-8");
+ $session_id = isset($input['session_id']) ? intval($input['session_id']) : 0;
+ $title = isset($input['title']) ? trim($input['title']) : '';
+ if ($session_id <= 0 || empty($title)) { echo json_encode(["error" => "参数错误"]); exit; }
+ pdo_query("UPDATE `llm_chat_session` SET `title`=? WHERE `session_id`=? AND `user_id`=?", $title, $session_id, $user_id);
+ echo json_encode(["success" => true], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+// ---- 聊天(流式SSE) ----
+$message = isset($input['message']) ? trim($input['message']) : '';
+$session_id = isset($input['session_id']) ? intval($input['session_id']) : 0;
+
+if (empty($message)) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "消息不能为空"]); exit; }
+
+// 自动创建会话
+if ($session_id <= 0) {
+ $now = date('Y-m-d H:i:s');
+ $auto_title = mb_substr($message, 0, 20, 'UTF-8');
+ if (mb_strlen($message, 'UTF-8') > 20) $auto_title .= '...';
+ $session_id = pdo_query("INSERT INTO `llm_chat_session` (`user_id`, `title`, `create_time`, `update_time`) VALUES (?, ?, ?, ?)", $user_id, $auto_title, $now, $now);
+}
+
+// 验证归属
+$check = pdo_query("SELECT 1 FROM `llm_chat_session` WHERE `session_id`=? AND `user_id`=?", $session_id, $user_id);
+if (empty($check) || $check === -1) { header("Content-Type: application/json; charset=utf-8"); echo json_encode(["error" => "无权访问"]); exit; }
+
+// 保存用户消息
+$now = date('Y-m-d H:i:s');
+pdo_query("INSERT INTO `llm_chat_message` (`session_id`, `role`, `content`, `create_time`) VALUES (?, 'user', ?, ?)", $session_id, $message, $now);
+pdo_query("UPDATE `llm_chat_session` SET `update_time`=? WHERE `session_id`=?", $now, $session_id);
+
+// 获取历史(最近10条上下文)
+$history = pdo_query("SELECT `role`, `content` FROM `llm_chat_message` WHERE `session_id`=? ORDER BY `id` DESC LIMIT 11", $session_id);
+$messages = [];
+if ($history !== -1 && !empty($history)) {
+ $history = array_reverse($history);
+ $recent = array_slice($history, 0, -1);
+ $recent = array_slice($recent, -10);
+ foreach ($recent as $msg) {
+ $messages[] = ['role' => $msg['role'], 'content' => $msg['content']];
+ }
+}
+$messages[] = ['role' => 'user', 'content' => $message];
+
+// ---- 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();
+
+sse_send("session", ["session_id" => $session_id]);
+
+// ---- 系统提示词 ----
+$system_prompt = <<<'EOP'
+你是一位名叫"MiniMax老师"的AI助教,正在与学生进行日常对话。
+
+你的职责:
+1. 用亲切、友好的语气和学生交流
+2. 回答学生的各种问题,包括但不限于学习、生活、编程概念等
+3. 提供有帮助的信息和建议
+4. 使用中文回答
+
+严格禁止的行为:
+1. 绝对不要帮助学生解答任何编程题目、算法题、数据结构题等OJ题目
+2. 绝对不要提供任何题目的解题思路、代码实现或算法分析
+3. 如果学生询问关于具体OJ题目的问题,请礼貌地拒绝并解释你需要自己思考来完成题目
+4. 不要帮助学生完成任何看起来像是作业或考试的编程任务
+
+你可以做的事情:
+- 解释编程概念和原理(不针对具体题目)
+- 讨论学习方法和技巧
+- 回答日常生活问题
+- 聊天闲谈
+- 其他与做题无关的问题
+EOP;
+
+$api_url = isset($OJ_LLM_API_URL) ? $OJ_LLM_API_URL : "https://api.minimaxi.com/anthropic/v1/messages";
+$api_key = isset($OJ_LLM_API_KEY) ? $OJ_LLM_API_KEY : "";
+$model = isset($OJ_LLM_MODEL) ? $OJ_LLM_MODEL : "MiniMax-M2.7";
+$max_tokens = isset($OJ_LLM_MAX_TOKENS) ? intval($OJ_LLM_MAX_TOKENS) : 2048;
+$timeout = isset($OJ_LLM_TIMEOUT) ? intval($OJ_LLM_TIMEOUT) : 120;
+
+if (empty($api_key)) { sse_error("API Key未配置"); exit; }
+
+$request_body = json_encode([
+ "model" => $model, "max_tokens" => $max_tokens, "system" => $system_prompt,
+ "temperature" => 0.8, "stream" => true, "messages" => $messages
+]);
+
+// ---- 流式转发 ----
+$GLOBALS['_llm_alive'] = true;
+$GLOBALS['_llm_full_text'] = "";
+$GLOBALS['_llm_buffer'] = "";
+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;
+ 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;
+ 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'] : '';
+ if ($type === 'content_block_delta' && isset($data['delta']['text'])) {
+ $_llm_full_text .= $data['delta']['text'];
+ sse_send("chunk", ["text" => $data['delta']['text']]);
+ } elseif ($type === 'message_delta' && isset($data['delta']['stop_reason'])) {
+ if (in_array($data['delta']['stop_reason'], ['end_turn', 'max_tokens'])) {
+ sse_send("done", []);
+ return 0;
+ }
+ } elseif (in_array($type, ['message_start', 'ping', 'content_block_start', 'content_block_stop'])) {
+ continue;
+ } elseif ($type === 'error') {
+ sse_send("error", ["message" => "API错误: " . (isset($data['error']['message']) ? $data['error']['message'] : '未知')]);
+ 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);
+
+// 保存AI回复
+if (!empty($_llm_full_text)) {
+ $save_now = date('Y-m-d H:i:s');
+ pdo_query("INSERT INTO `llm_chat_message` (`session_id`, `role`, `content`, `create_time`) VALUES (?, 'assistant', ?, ?)", $session_id, $_llm_full_text, $save_now);
+}
+
+if (!$_llm_alive) exit;
+if ($result === false && empty($_llm_full_text)) { sse_error("网络请求失败: " . ($curl_error ?: "未知")); exit; }
+if (empty($_llm_full_text) && $http_code !== 200) { sse_error("API请求失败 (HTTP $http_code)"); exit; }
+if (empty($_llm_full_text)) { sse_error("API返回为空"); exit; }
+?>
diff --git a/web/llm-chat.php b/web/llm-chat.php
new file mode 100644
index 0000000..9fb0cb5
--- /dev/null
+++ b/web/llm-chat.php
@@ -0,0 +1,515 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
你好,欢迎使用AI聊天
+
选择一个对话或创建新对话开始聊天
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/llm-review.php b/web/llm-review.php
index 90c04fc..7fbef21 100644
--- a/web/llm-review.php
+++ b/web/llm-review.php
@@ -1,50 +1,89 @@
$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)) {
- echo json_encode(["review" => $cache[0]['review']], JSON_UNESCAPED_UNICODE);
+ // 有缓存,走 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)) {
- echo json_encode(["error" => "找不到该提交记录"], JSON_UNESCAPED_UNICODE);
+ sse_error("找不到该提交记录");
exit;
}
$sol = $solution[0];
@@ -61,7 +100,7 @@ $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) {
- echo json_encode(["error" => "找不到该题目"], JSON_UNESCAPED_UNICODE);
+ sse_error("找不到该题目");
exit;
}
$p = $prob[0];
@@ -82,13 +121,11 @@ if (!empty($first_ac) && $first_ac !== -1) {
// ---- 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 {
- // 运行时/WA 等
$err = pdo_query("SELECT `error` FROM `runtimeinfo` WHERE `solution_id`=?", $sid);
if (!empty($err) && $err !== -1) {
$diff_info = $err[0]['error'];
@@ -139,7 +176,7 @@ if (!empty($reference_code)) {
$user_prompt .= "\n请分析学生的错误,给出启发性的提示和引导,帮助学生自己发现问题并改正。\n";
-// ---- 8. 调用 LLM API ----
+// ---- 8. 流式调用 LLM API ----
$api_url = isset($OJ_LLM_API_URL) ? $OJ_LLM_API_URL : "https://api.minimaxi.com/anthropic/v1/messages";
$api_key = isset($OJ_LLM_API_KEY) ? $OJ_LLM_API_KEY : "";
$model = isset($OJ_LLM_MODEL) ? $OJ_LLM_MODEL : "MiniMax-M2.5";
@@ -147,21 +184,33 @@ $max_tokens = isset($OJ_LLM_MAX_TOKENS) ? intval($OJ_LLM_MAX_TOKENS) : 1024;
$timeout = isset($OJ_LLM_TIMEOUT) ? intval($OJ_LLM_TIMEOUT) : 60;
if (empty($api_key)) {
- echo json_encode(["error" => "API Key未配置"], JSON_UNESCAPED_UNICODE);
+ sse_error("API Key未配置");
exit;
}
-// ---- Anthropic API 兼容格式 ----
$request_body = json_encode([
"model" => $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,
@@ -171,51 +220,97 @@ curl_setopt_array($ch, [
"x-api-key: " . $api_key,
"anthropic-version: 2023-06-01"
],
- CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_RETURNTRANSFER => false,
CURLOPT_TIMEOUT => $timeout,
- CURLOPT_SSL_VERIFYPEER => true
+ 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);
+ }
]);
-$response = curl_exec($ch);
+$result = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
-if ($response === false) {
- echo json_encode(["error" => "网络请求失败: " . $curl_error], JSON_UNESCAPED_UNICODE);
+// 如果连接断开,不缓存也不报错
+if (!$_llm_alive) exit;
+
+// ---- 处理错误 ----
+if ($result === false && empty($_llm_full_text)) {
+ sse_error("网络请求失败: " . ($curl_error ?: "未知错误"));
exit;
}
-if ($http_code !== 200) {
- echo json_encode(["error" => "API请求失败 (HTTP $http_code)"], JSON_UNESCAPED_UNICODE);
+// 检查 HTTP 错误(如果 write callback 没有收到数据)
+if (empty($_llm_full_text) && $http_code !== 200) {
+ sse_error("API请求失败 (HTTP $http_code)");
exit;
}
-$resp_data = json_decode($response, true);
-// Anthropic 响应格式: content 是一个数组,取 type=text 的块
-$review_text = "";
-if ($resp_data && isset($resp_data['content']) && is_array($resp_data['content'])) {
- foreach ($resp_data['content'] as $block) {
- if (isset($block['type']) && $block['type'] === 'text' && isset($block['text'])) {
- $review_text .= $block['text'];
- }
- }
-}
-if (empty($review_text)) {
- echo json_encode(["error" => "API返回格式异常"], JSON_UNESCAPED_UNICODE);
+if (empty($_llm_full_text)) {
+ sse_error("API返回为空");
exit;
}
-// ---- 9. 缓存结果 ----
-// 自动建表(首次调用时)
-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;");
+// ---- 9. 缓存完整结果 ----
+$review_text = $_llm_full_text;
-pdo_query("INSERT INTO `llm_review` (`solution_id`, `review`, `create_time`) VALUES (?, ?, NOW())", $sid, $review_text);
-
-// ---- 10. 返回结果 ----
-echo json_encode(["review" => $review_text], JSON_UNESCAPED_UNICODE);
+$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);
+}
diff --git a/web/template/syzoj/header.php b/web/template/syzoj/header.php
index 3d7c49e..62a152d 100644
--- a/web/template/syzoj/header.php
+++ b/web/template/syzoj/header.php
@@ -230,8 +230,12 @@ a.label:hover {
href="cppreference.php">
- "
- href="docs.php"> 文档
+
+
+
+ "
+ href="llm-chat.php"> AI聊天
"
href="ranklist.php">
diff --git a/web/template/syzoj/reinfo.php b/web/template/syzoj/reinfo.php
index 810360b..adc4cba 100644
--- a/web/template/syzoj/reinfo.php
+++ b/web/template/syzoj/reinfo.php
@@ -215,40 +215,104 @@
$('#llm-review-error').hide();
$('#llm-review-result').hide();
- $.ajax({
- type: 'GET',
- url: 'llm-review.php?sid=',
- dataType: 'json',
- timeout: 60000,
- success: function(data){
- btn.removeClass('loading');
- $('#llm-review-loading').hide();
- if(data.error){
- $('#llm-review-error-msg').text(data.error);
- $('#llm-review-error').show();
- btn.prop('disabled', false);
- } else if(data.review){
- $('#llm-review-result').show();
- var container = document.getElementById('llm-review-content');
- // 先以纯文本放入,再用 Vditor 渲染为 Markdown HTML
- container.textContent = data.review;
- if(typeof HustOJVditor !== 'undefined' && HustOJVditor.renderMarkdownBlocks){
- HustOJVditor.renderMarkdownBlocks('#llm-review-content', {
- useTextContent: true
- });
- }
- btn.hide();
+ var fullText = "";
+ var container = document.getElementById('llm-review-content');
+ var resultDiv = document.getElementById('llm-review-result');
+ var reader = null;
+ var decoder = new TextDecoder();
+
+ // 解析 SSE 事件
+ var sseBuffer = "";
+ function processSSE(text) {
+ sseBuffer += text;
+ var lines = sseBuffer.split('\n');
+ sseBuffer = lines.pop(); // 保留未完成的行
+
+ var currentEvent = "";
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
+ if (line.indexOf('event: ') === 0) {
+ currentEvent = line.substring(7);
+ } else if (line.indexOf('data: ') === 0) {
+ var dataStr = line.substring(6);
+ try {
+ var data = JSON.parse(dataStr);
+ if (currentEvent === 'chunk') {
+ fullText += data.text;
+ container.textContent = fullText;
+ resultDiv.style.display = 'block';
+ } else if (currentEvent === 'cached') {
+ fullText = data.text;
+ container.textContent = fullText;
+ resultDiv.style.display = 'block';
+ } else if (currentEvent === 'done') {
+ finishReview();
+ } else if (currentEvent === 'error') {
+ showError(data.message || '未知错误');
+ }
+ } catch(e) {}
+ currentEvent = "";
}
- },
- error: function(xhr, status){
- btn.removeClass('loading').prop('disabled', false);
- $('#llm-review-loading').hide();
- var msg = '请求失败';
- if(status === 'timeout') msg = '请求超时,请稍后重试';
- $('#llm-review-error-msg').text(msg);
- $('#llm-review-error').show();
}
- });
+ }
+
+ function finishReview() {
+ btn.removeClass('loading').hide();
+ document.getElementById('llm-review-loading').style.display = 'none';
+ resultDiv.style.display = 'block';
+ // 渲染 Markdown
+ if (fullText && typeof HustOJVditor !== 'undefined' && HustOJVditor.renderMarkdownBlocks) {
+ HustOJVditor.renderMarkdownBlocks('#llm-review-content', { useTextContent: true });
+ }
+ }
+
+ function showError(msg) {
+ btn.removeClass('loading').prop('disabled', false);
+ document.getElementById('llm-review-loading').style.display = 'none';
+ document.getElementById('llm-review-error-msg').textContent = msg;
+ document.getElementById('llm-review-error').style.display = 'block';
+ }
+
+ if (typeof fetch === 'undefined') {
+ showError('浏览器不支持,请升级浏览器');
+ return;
+ }
+
+ fetch('llm-review.php?sid=')
+ .then(function(response) {
+ if (!response.ok) {
+ return response.text().then(function(text) {
+ try {
+ var d = JSON.parse(text);
+ showError(d.error || '请求失败 (HTTP ' + response.status + ')');
+ } catch(e) {
+ showError('请求失败 (HTTP ' + response.status + ')');
+ }
+ });
+ }
+ reader = response.body.getReader();
+ function read() {
+ reader.read().then(function(result) {
+ if (result.done) {
+ if (fullText) finishReview();
+ return;
+ }
+ var text = decoder.decode(result.value, { stream: true });
+ processSSE(text);
+ read();
+ }).catch(function(err) {
+ if (fullText) {
+ finishReview();
+ } else {
+ showError('读取响应失败: ' + err.message);
+ }
+ });
+ }
+ read();
+ })
+ .catch(function(err) {
+ showError('网络请求失败: ' + err.message);
+ });
}