初始化仓库及v1.0.0提交

This commit is contained in:
2026-05-05 03:21:58 +08:00
commit 813bb02672
67 changed files with 5263 additions and 0 deletions

0
app/Controllers/.gitkeep Normal file
View File

View 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']
]
]);
}
}

View 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();
}
}

View 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' => '删除成功']);
}
}

View 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]
]);
}
}

View 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]);
}
}

View 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' => '删除成功']);
}
}

View 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
]
]);
}
}