Files
hustoj/web/llm-chat-api.php

276 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* LLM Chat API - AI聊天接口带对话管理
* 支持:创建/删除/切换对话,消息历史持久化
*/
require_once('./include/db_info.inc.php');
require_once('./include/setlang.php');
require_once('./include/const.inc.php');
// ---- SSE 辅助函数 ----
function sse_send($event, $data) {
echo "event: $event\ndata: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
@ob_flush();
@flush();
}
function sse_error($msg) {
sse_send("error", ["message" => $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; }
?>