初始化仓库及v1.0.0提交
This commit is contained in:
0
app/Controllers/.gitkeep
Normal file
0
app/Controllers/.gitkeep
Normal file
69
app/Controllers/AuthController.php
Normal file
69
app/Controllers/AuthController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Config\AppConfig;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public static function login(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$username = $input['username'] ?? '';
|
||||
$password = $input['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '用户名和密码不能为空']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::findByUsername($username);
|
||||
if (!$user || !User::verifyPassword($username, $password)) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => '用户名或密码错误']);
|
||||
return;
|
||||
}
|
||||
|
||||
$jwtSecret = AppConfig::get('jwtSecret');
|
||||
$jwtExpiry = AppConfig::get('jwtExpiry', 86400);
|
||||
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'iat' => time(),
|
||||
'exp' => time() + $jwtExpiry
|
||||
];
|
||||
|
||||
$token = JWT::encode($payload, $jwtSecret, 'HS256');
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public static function me(): void
|
||||
{
|
||||
$user = $GLOBALS['auth_user'];
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'id' => $user['userId'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
117
app/Controllers/ChatController.php
Normal file
117
app/Controllers/ChatController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Config;
|
||||
use App\Services\AIService;
|
||||
|
||||
class ChatController
|
||||
{
|
||||
public static function completions(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$provider = $input['provider'] ?? '';
|
||||
$model = $input['model'] ?? '';
|
||||
$messages = $input['messages'] ?? [];
|
||||
$stream = $input['stream'] ?? true;
|
||||
$systemPrompt = $input['systemPrompt'] ?? '';
|
||||
$thinkingMode = $input['thinkingMode'] ?? false;
|
||||
|
||||
if (!$provider || !$model || !$messages) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '缺少必要参数(provider、model、messages)']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($messages) || empty($messages)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'messages 必须是非空数组']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取供应商配置
|
||||
$configRow = Config::getByKey('providers');
|
||||
if (!$configRow) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '未找到供应商配置']);
|
||||
return;
|
||||
}
|
||||
|
||||
$providers = json_decode($configRow['config_value'], true);
|
||||
if (!is_array($providers)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '供应商配置格式错误']);
|
||||
return;
|
||||
}
|
||||
|
||||
$providerKey = $provider;
|
||||
if (!isset($providers[$providerKey])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '供应商不存在: ' . $providerKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
$providerConfig = $providers[$providerKey];
|
||||
if (empty($providerConfig['enabled'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '供应商已禁用: ' . $providerKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
$models = $providerConfig['models'] ?? [];
|
||||
if (!empty($models) && !in_array($model, $models)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '模型不在供应商支持列表中: ' . $model]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 非流式模式暂不支持
|
||||
if (!$stream) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '当前仅支持流式响应']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 SSE 响应头
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// 关闭输出缓冲
|
||||
while (ob_get_level()) {
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
$options = [
|
||||
'provider' => $providerConfig,
|
||||
'systemPrompt' => $systemPrompt,
|
||||
'thinkingMode' => (bool) $thinkingMode
|
||||
];
|
||||
|
||||
try {
|
||||
AIService::streamChat($providerKey, $model, $messages, $options, function ($chunk, $type = 'content') {
|
||||
if ($type === 'thinking') {
|
||||
self::sendSSE(['thinking' => $chunk]);
|
||||
} else {
|
||||
self::sendSSE(['content' => $chunk]);
|
||||
}
|
||||
});
|
||||
|
||||
self::sendSSE('[DONE]');
|
||||
} catch (\Throwable $e) {
|
||||
self::sendSSE(['type' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function sendSSE($data): void
|
||||
{
|
||||
if (is_string($data)) {
|
||||
echo "data: " . $data . "\n\n";
|
||||
} else {
|
||||
echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
}
|
||||
flush();
|
||||
}
|
||||
}
|
||||
106
app/Controllers/ConfigController.php
Normal file
106
app/Controllers/ConfigController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Config;
|
||||
use App\Models\Personality;
|
||||
|
||||
class ConfigController
|
||||
{
|
||||
public static function getConfig(): void
|
||||
{
|
||||
$configRow = Config::getByKey('providers');
|
||||
$providers = $configRow ? json_decode($configRow['config_value'], true) : [];
|
||||
echo json_encode(['success' => true, 'data' => ['providers' => $providers]]);
|
||||
}
|
||||
|
||||
public static function updateConfig(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$providers = $input['providers'] ?? [];
|
||||
|
||||
if (!is_array($providers)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'providers 必须是数组']);
|
||||
return;
|
||||
}
|
||||
|
||||
Config::setByKey('providers', json_encode($providers, JSON_UNESCAPED_UNICODE));
|
||||
echo json_encode(['success' => true, 'message' => '配置更新成功']);
|
||||
}
|
||||
|
||||
public static function listPersonalities(): void
|
||||
{
|
||||
$personalities = Personality::findAll();
|
||||
echo json_encode(['success' => true, 'data' => $personalities]);
|
||||
}
|
||||
|
||||
public static function createPersonality(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$name = $input['name'] ?? '';
|
||||
$prompt = $input['prompt'] ?? '';
|
||||
|
||||
if (!$name || !$prompt) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '名称和提示词不能为空']);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'prompt' => $prompt,
|
||||
'description' => $input['description'] ?? null,
|
||||
'icon' => $input['icon'] ?? null,
|
||||
'is_preset' => 0,
|
||||
'created_by' => $GLOBALS['auth_user']['userId']
|
||||
];
|
||||
|
||||
$personality = Personality::create($data);
|
||||
echo json_encode(['success' => true, 'data' => $personality]);
|
||||
}
|
||||
|
||||
public static function updatePersonality($id): void
|
||||
{
|
||||
$personality = Personality::findById($id);
|
||||
if (!$personality) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '人格不存在']);
|
||||
return;
|
||||
}
|
||||
if ($personality['is_preset']) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '预设人格不可编辑']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$data = array_filter([
|
||||
'name' => $input['name'] ?? null,
|
||||
'prompt' => $input['prompt'] ?? null,
|
||||
'description' => $input['description'] ?? null,
|
||||
'icon' => $input['icon'] ?? null,
|
||||
], fn($v) => $v !== null);
|
||||
|
||||
$updated = Personality::update($id, $data);
|
||||
echo json_encode(['success' => true, 'data' => $updated]);
|
||||
}
|
||||
|
||||
public static function deletePersonality($id): void
|
||||
{
|
||||
$personality = Personality::findById($id);
|
||||
if (!$personality) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '人格不存在']);
|
||||
return;
|
||||
}
|
||||
if ($personality['is_preset']) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '预设人格不可删除']);
|
||||
return;
|
||||
}
|
||||
|
||||
Personality::delete($id);
|
||||
echo json_encode(['success' => true, 'message' => '删除成功']);
|
||||
}
|
||||
}
|
||||
118
app/Controllers/InstallController.php
Normal file
118
app/Controllers/InstallController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Services\Installer;
|
||||
use App\Config\AppConfig;
|
||||
use App\Config\Database;
|
||||
use App\Models\User;
|
||||
use App\Models\Config;
|
||||
|
||||
class InstallController
|
||||
{
|
||||
public static function status(): void
|
||||
{
|
||||
$installed = Installer::isInstalled();
|
||||
echo json_encode(['success' => true, 'data' => ['installed' => $installed]]);
|
||||
}
|
||||
|
||||
public static function testDb(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$host = $input['host'] ?? '';
|
||||
$port = $input['port'] ?? 3306;
|
||||
$user = $input['user'] ?? '';
|
||||
$password = $input['password'] ?? '';
|
||||
$database = $input['database'] ?? '';
|
||||
|
||||
if (!$host || !$user || !$database) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '缺少必要参数']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4";
|
||||
$pdo = new \PDO($dsn, $user, $password, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
echo json_encode(['success' => true, 'message' => '数据库连接成功']);
|
||||
} catch (\PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '数据库连接失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function setup(): void
|
||||
{
|
||||
if (Installer::isInstalled()) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '系统已安装,不能重复安装']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$username = $input['username'] ?? '';
|
||||
$password = $input['password'] ?? '';
|
||||
$dbConfig = $input['dbConfig'] ?? [];
|
||||
$providers = $input['providers'] ?? [];
|
||||
|
||||
if (!$username || !$password) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '用户名和密码不能为空']);
|
||||
return;
|
||||
}
|
||||
if (strlen($password) < 6) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '密码长度不能少于6位']);
|
||||
return;
|
||||
}
|
||||
if (!$dbConfig || !$dbConfig['host'] || !$dbConfig['user'] || !$dbConfig['database']) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '请填写数据库配置']);
|
||||
return;
|
||||
}
|
||||
if (!is_array($providers) || count($providers) === 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '请至少配置一个AI服务提供商']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host={$dbConfig['host']};port=" . ($dbConfig['port'] ?? 3306) . ";dbname={$dbConfig['database']};charset=utf8mb4";
|
||||
$pdo = new \PDO($dsn, $dbConfig['user'], $dbConfig['password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
|
||||
} catch (\PDOException $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '数据库连接失败: ' . $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$configDir = __DIR__ . '/../../config';
|
||||
file_put_contents($configDir . '/db-config.json', json_encode($dbConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
try {
|
||||
Installer::runMigration();
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '数据库迁移失败: ' . $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
Installer::seedDefaults();
|
||||
|
||||
$admin = User::create(['username' => $username, 'password' => $password, 'role' => 'admin']);
|
||||
|
||||
$jwtSecret = bin2hex(random_bytes(32));
|
||||
AppConfig::set('jwtSecret', $jwtSecret);
|
||||
AppConfig::set('jwtExpiry', 86400);
|
||||
AppConfig::set('corsOrigin', '');
|
||||
|
||||
Config::setByKey('providers', json_encode($providers));
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '安装成功',
|
||||
'data' => ['user' => $admin]
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
app/Controllers/MessageController.php
Normal file
57
app/Controllers/MessageController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Session;
|
||||
use App\Models\Message;
|
||||
|
||||
class MessageController
|
||||
{
|
||||
public static function index(int $sessionId): void
|
||||
{
|
||||
$user = $GLOBALS['auth_user'];
|
||||
$session = Session::findById($sessionId);
|
||||
if (!$session || $session['user_id'] != $user['userId']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
$messages = Message::findBySessionId($sessionId);
|
||||
echo json_encode(['success' => true, 'data' => $messages]);
|
||||
}
|
||||
|
||||
public static function create(int $sessionId): void
|
||||
{
|
||||
$user = $GLOBALS['auth_user'];
|
||||
$session = Session::findById($sessionId);
|
||||
if (!$session || $session['user_id'] != $user['userId']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['role']) || !isset($input['content']) || $input['content'] === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'role 和 content 为必填字段']);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session_id' => $sessionId,
|
||||
'role' => $input['role'],
|
||||
'content' => $input['content'],
|
||||
];
|
||||
|
||||
if (isset($input['file_info'])) {
|
||||
$data['file_info'] = $input['file_info'];
|
||||
}
|
||||
if (isset($input['thinking_content'])) {
|
||||
$data['thinking_content'] = $input['thinking_content'];
|
||||
}
|
||||
|
||||
$message = Message::create($data);
|
||||
echo json_encode(['success' => true, 'data' => $message]);
|
||||
}
|
||||
}
|
||||
88
app/Controllers/SessionController.php
Normal file
88
app/Controllers/SessionController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Session;
|
||||
|
||||
class SessionController
|
||||
{
|
||||
public static function index(): void
|
||||
{
|
||||
$user = $GLOBALS['auth_user'];
|
||||
$sessions = Session::findByUserId($user['userId']);
|
||||
echo json_encode(['success' => true, 'data' => $sessions]);
|
||||
}
|
||||
|
||||
public static function create(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$user = $GLOBALS['auth_user'];
|
||||
|
||||
$data = [
|
||||
'user_id' => $user['userId'],
|
||||
'name' => $input['name'] ?? '新会话',
|
||||
'provider' => $input['provider'] ?? 'newapi',
|
||||
'model' => $input['model'] ?? 'gpt-3.5-turbo',
|
||||
'system_prompt' => $input['system_prompt'] ?? '',
|
||||
'thinking_mode' => $input['thinking_mode'] ?? false,
|
||||
];
|
||||
|
||||
if (isset($input['personality_id'])) {
|
||||
$data['personality_id'] = $input['personality_id'];
|
||||
}
|
||||
|
||||
$session = Session::create($data);
|
||||
echo json_encode(['success' => true, 'data' => $session]);
|
||||
}
|
||||
|
||||
public static function update(int $id): void
|
||||
{
|
||||
$session = Session::findById($id);
|
||||
if (!$session) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $GLOBALS['auth_user'];
|
||||
if ($session['user_id'] != $user['userId']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$allowedFields = ['name', 'provider', 'model', 'system_prompt', 'personality_id', 'thinking_mode'];
|
||||
$data = [];
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$data[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
Session::update($id, $data);
|
||||
$updatedSession = Session::findById($id);
|
||||
echo json_encode(['success' => true, 'data' => $updatedSession]);
|
||||
}
|
||||
|
||||
public static function delete(int $id): void
|
||||
{
|
||||
$session = Session::findById($id);
|
||||
if (!$session) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $GLOBALS['auth_user'];
|
||||
if ($session['user_id'] != $user['userId']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => '会话不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
Session::delete($id);
|
||||
echo json_encode(['success' => true, 'message' => '删除成功']);
|
||||
}
|
||||
}
|
||||
56
app/Controllers/UploadController.php
Normal file
56
app/Controllers/UploadController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class UploadController
|
||||
{
|
||||
public static function upload(): void
|
||||
{
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '请选择要上传的文件']);
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
$allowedTypes = [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
||||
'js', 'ts', 'py', 'java', 'cpp', 'c', 'html', 'css', 'json', 'xml',
|
||||
'txt', 'md', 'go', 'rs', 'php', 'rb', 'sql', 'yaml', 'yml', 'sh', 'bat'
|
||||
];
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($ext, $allowedTypes)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '不支持的文件类型: .' . $ext]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($file['size'] > 10 * 1024 * 1024) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '文件大小不能超过10MB']);
|
||||
return;
|
||||
}
|
||||
|
||||
$filename = uniqid() . '.' . $ext;
|
||||
$uploadDir = __DIR__ . '/../../uploads/';
|
||||
$filepath = $uploadDir . $filename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $filepath)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '文件上传失败']);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'url' => '/uploads/' . $filename,
|
||||
'name' => $file['name'],
|
||||
'size' => $file['size'],
|
||||
'type' => $ext
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user