Compare commits

..

26 commits

Author SHA1 Message Date
Jethro Lin
15ad765e2c add new api doc v2 2024-12-09 14:42:15 +08:00
Jethro Lin
c351250e6f token fix 2024-12-09 14:17:58 +08:00
Jethro Lin
de2c68aaf6 clinet fix 2024-12-09 14:17:29 +08:00
Jethro Lin
e900ebc957 bug fix for login and llm providers 2024-12-05 14:35:21 +08:00
Jethro Lin
24daddff7b fix route 2024-12-05 13:49:11 +08:00
Jethro Lin
d91477263f init sql 2024-12-05 11:38:28 +08:00
Jethro Lin
8122ab2b26 modify sql 2024-12-04 17:27:55 +08:00
Jethro Lin
e0e55719d0 bug fix 2024-12-04 17:03:46 +08:00
Jethro Lin
1ab5da5576 remove Traits 2024-12-04 16:41:43 +08:00
Jethro Lin
27cad57159 fix response 2024-12-04 13:53:58 +08:00
Jethro Lin
e1119a9426 client response 2024-12-04 13:51:51 +08:00
Jethro Lin
74790f6180 bug fix 2024-12-04 13:48:22 +08:00
Jethro Lin
383e73c2cd clinet fix 2024-12-04 13:34:46 +08:00
Jethro Lin
4b1ed6ade3 token system fix 2024-12-04 13:32:14 +08:00
Jethro Lin
33dde9dc2c token system change 2024-12-04 13:28:22 +08:00
Jethro Lin
ae5a7662ca add command to clean token 2024-12-04 12:55:07 +08:00
Jethro Lin
ae4ef60215 fix token issue 2024-12-04 12:54:46 +08:00
Jethro Lin
fdaccc918f fix premission error 2024-12-04 12:47:30 +08:00
Jethro Lin
0af7e10998 bug fix 2024-12-04 12:43:04 +08:00
Jethro Lin
9df4178800 bug fix 2024-12-04 12:27:33 +08:00
Jethro Lin
c5258233a8 5. [请求与响应格式](#5-请求与响应格式)
- [5.1 通用请求头](#51-通用请求头)
   - [5.2 响应格式](#52-响应格式)
6. [错误代码](#6-错误代码)
7. [安全性考虑](#7-安全性考虑)
2024-12-04 12:14:43 +08:00
Jethro Lin
31b69e318a - [4.3.2 客户用户管理](#432-客户用户管理)
- [4.3.2.1 新增客户用户](#4321-新增客户用户)
       - [4.3.2.2 修改客户用户](#4322-修改客户用户)
       - [4.3.2.3 删除客户用户](#4323-删除客户用户)
       - [4.3.2.4 获取客户用户列表](#4324-获取客户用户列表)
     - [4.3.3 生成认证令牌](#433-生成认证令牌)
2024-12-04 12:10:15 +08:00
Jethro Lin
c44c25d86f 4. [API 详细说明](#4-api-详细说明)
- [4.1 认证 API](#41-认证-api)
     - [4.1.1 获取访问令牌](#411-获取访问令牌)
   - [4.2 客户用户 API](#42-客户用户-api)
     - [4.2.1 发送提示词请求](#421-发送提示词请求)
   - [4.3 管理员 API](#43-管理员-api)
     - [4.3.1 LLM 提供商管理](#431-llm-提供商管理)
       - [4.3.1.1 新增 LLM 提供商](#4311-新增-llm-提供商)
       - [4.3.1.2 修改 LLM 提供商](#4312-修改-llm-提供商)
       - [4.3.1.3 删除 LLM 提供商](#4313-删除-llm-提供商)
       - [4.3.1.4 获取 LLM 提供商列表](#4314-获取-llm-提供商列表)
2024-12-04 12:01:56 +08:00
Jethro Lin
9cebfec5b0 3. [API 概览](#3-api-概览)
- [3.1 客户用户 API](#31-客户用户-api)
   - [3.2 管理员 API](#32-管理员-api)
2024-12-04 11:45:27 +08:00
Jethro Lin
1ab2e796a9 Created missing models:
Admin: For managing administrators with role-based access
LlmProvider: For managing LLM service providers
Created SQL schema:
All tables as per the database design
Proper foreign key constraints and indexes
Timestamps for auditing
Appropriate character sets and collations
Added operation logging:
Created LogService for centralized logging
Integrated logging into TokenService
Logs all token-related operations with user info and IP address
Enhanced token management:
Added comprehensive logging for all token operations
Improved error handling and validation
Added proper cleanup for revoked tokens
2024-12-04 11:31:10 +08:00
Jethro Lin
dca86354ba agnet step 1 add auth 2024-12-04 11:24:47 +08:00
47 changed files with 4987 additions and 11 deletions

27
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// 移除不必要的清理任務
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Constants;
class ErrorCode
{
// Authentication & Authorization
public const VALIDATION_ERROR = 'validation_error';
public const SERVER_ERROR = 'server_error';
public const NOT_FOUND = 'not_found';
public const UNAUTHORIZED = 'unauthorized';
public const FORBIDDEN = 'forbidden';
public const TOKEN_INVALID = 'token_invalid';
public const TOKEN_EXPIRED = 'token_expired';
public const INVALID_CREDENTIALS = 'invalid_credentials';
// Resource Related
public const RESOURCE_NOT_FOUND = 'resource_not_found';
public const RESOURCE_IN_USE = 'resource_in_use';
// Request Related
public const INVALID_REQUEST_FORMAT = 'invalid_request_format';
public const METHOD_NOT_ALLOWED = 'method_not_allowed';
public const TOO_MANY_REQUESTS = 'too_many_requests';
// Client Related
public const CLIENT_INACTIVE = 'client_inactive';
public const CLIENT_NOT_FOUND = 'client_not_found';
public const CLIENT_HAS_ACTIVE_TOKENS = 'client_has_active_tokens';
// Provider Related
public const PROVIDER_ERROR = 'provider_error';
public const LLM_PROVIDER_NOT_FOUND = 'llm_provider_not_found';
public const LLM_REQUEST_FAILED = 'llm_request_failed';
private static array $messages = [
// Authentication & Authorization
self::VALIDATION_ERROR => '請求參數驗證失敗。',
self::SERVER_ERROR => '服務器內部錯誤。',
self::NOT_FOUND => '請求的資源不存在。',
self::UNAUTHORIZED => '未授權,請先登錄。',
self::FORBIDDEN => '禁止訪問,權限不足。',
self::TOKEN_INVALID => '令牌無效。',
self::TOKEN_EXPIRED => '令牌已過期。',
self::INVALID_CREDENTIALS => '認證憑據無效。',
// Resource Related
self::RESOURCE_NOT_FOUND => '請求的資源不存在。',
self::RESOURCE_IN_USE => '資源正在使用中,無法執行操作。',
// Request Related
self::INVALID_REQUEST_FORMAT => '請求格式無效。',
self::METHOD_NOT_ALLOWED => '請求方法不允許。',
self::TOO_MANY_REQUESTS => '請求過於頻繁,請稍後再試。',
// Client Related
self::CLIENT_INACTIVE => '客戶帳戶未啟用。',
self::CLIENT_NOT_FOUND => '客戶不存在。',
self::CLIENT_HAS_ACTIVE_TOKENS => '客戶有活躍的認證令牌。',
// Provider Related
self::PROVIDER_ERROR => '提供商服務錯誤。',
self::LLM_PROVIDER_NOT_FOUND => 'LLM 提供商不存在。',
self::LLM_REQUEST_FAILED => 'LLM 請求失敗。',
];
public static function getMessage(string $code): string
{
return self::$messages[$code] ?? '未知錯誤。';
}
}

181
app/Exceptions/Handler.php Normal file
View file

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use App\Constants\ErrorCode;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* API 路由前缀
*/
protected const API_PREFIX = 'api';
/**
* 不需要报告的异常类型
*
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
AuthenticationException::class,
AuthorizationException::class,
ValidationException::class,
ModelNotFoundException::class,
];
/**
* 注册异常处理回调
*/
public function register(): void
{
$this->renderable(function (Throwable $e, Request $request) {
if ($this->isApiRequest($request)) {
return $this->handleApiException($e);
}
});
}
/**
* 处理 API 异常
*
* @param Throwable $e
* @return JsonResponse
*/
protected function handleApiException(Throwable $e): JsonResponse
{
if ($e instanceof ValidationException) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors(),
Response::HTTP_UNPROCESSABLE_ENTITY
);
}
if ($e instanceof AuthenticationException) {
return $this->error(
ErrorCode::UNAUTHORIZED,
ErrorCode::getMessage(ErrorCode::UNAUTHORIZED),
null,
Response::HTTP_UNAUTHORIZED
);
}
if ($e instanceof AuthorizationException) {
return $this->error(
ErrorCode::FORBIDDEN,
ErrorCode::getMessage(ErrorCode::FORBIDDEN),
null,
Response::HTTP_FORBIDDEN
);
}
if ($e instanceof ModelNotFoundException) {
return $this->error(
ErrorCode::RESOURCE_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND),
null,
Response::HTTP_NOT_FOUND
);
}
if ($e instanceof NotFoundHttpException) {
return $this->error(
ErrorCode::RESOURCE_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND),
null,
Response::HTTP_NOT_FOUND
);
}
if ($e instanceof MethodNotAllowedHttpException) {
return $this->error(
ErrorCode::METHOD_NOT_ALLOWED,
ErrorCode::getMessage(ErrorCode::METHOD_NOT_ALLOWED),
null,
Response::HTTP_METHOD_NOT_ALLOWED
);
}
if ($e instanceof TooManyRequestsHttpException) {
return $this->error(
ErrorCode::TOO_MANY_REQUESTS,
ErrorCode::getMessage(ErrorCode::TOO_MANY_REQUESTS),
null,
Response::HTTP_TOO_MANY_REQUESTS
);
}
if ($e instanceof HttpException) {
return $this->error(
ErrorCode::SERVER_ERROR,
$e->getMessage(),
null,
$e->getStatusCode()
);
}
// 记录未处理的异常
$this->report($e);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR),
config('app.debug') ? [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
] : null,
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
/**
* 判断是否为 API 请求
*
* @param Request $request
* @return bool
*/
protected function isApiRequest(Request $request): bool
{
return $request->is(static::API_PREFIX . '/*') || $request->expectsJson();
}
/**
* 返回错误响应
*
* @param string $error 错误代码
* @param string $message 错误消息
* @param mixed $errors 详细错误信息
* @param int $code HTTP状态码
* @return JsonResponse
*/
protected function error(string $error, string $message, mixed $errors = null, int $code = Response::HTTP_BAD_REQUEST): JsonResponse
{
$response = [
'success' => false,
'error' => $error,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
}

View file

@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use App\Services\LogService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* @var LogService
*/
private readonly LogService $logService;
public function __construct(LogService $logService)
{
$this->logService = $logService;
}
/**
* 管理员登录
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function login(Request $request): JsonResponse
{
try {
/** @var array{email: string, password: string} $validated */
$validated = $request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
// 添加請求日誌
Log::info('Login attempt details', [
'email' => $validated['email'],
'request_data' => $request->all()
]);
/** @var Admin|null $admin */
$admin = Admin::where('email', $validated['email'])->first();
// 添加用戶查詢日誌
Log::info('Admin query result', [
'admin_found' => $admin ? 'yes' : 'no',
'admin_data' => $admin ? [
'id' => $admin->id,
'email' => $admin->email,
'role' => $admin->role
] : null
]);
if (!$admin || !Hash::check($validated['password'], $admin->password)) {
// 添加密碼驗證日誌
Log::info('Password verification failed', [
'has_admin' => $admin ? 'yes' : 'no',
'password_check' => $admin ? Hash::check($validated['password'], $admin->password) : 'admin not found'
]);
return $this->error(
ErrorCode::INVALID_CREDENTIALS,
ErrorCode::getMessage(ErrorCode::INVALID_CREDENTIALS)
);
}
/** @var string $token */
$token = $admin->createToken('admin-token')->plainTextToken;
$this->logService->logOperation(
'admin',
$admin->id,
'Admin logged in'
);
return $this->success([
'token' => $token,
'admin' => [
'id' => $admin->id,
'email' => $admin->email,
],
]);
} catch (ValidationException $e) {
Log::error('Validation error during login', [
'errors' => $e->errors(),
]);
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (\Exception $e) {
Log::error('Error during admin login', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 管理员登出
*
* @param Request $request
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse
{
try {
/** @var Admin|null $admin */
$admin = $request->user();
if (!$admin) {
return $this->error(
ErrorCode::UNAUTHORIZED,
'未登錄或會話已過期。'
);
}
$admin->currentAccessToken()->delete();
$this->logService->logOperation(
'admin',
$admin->id,
'Admin logged out'
);
return $this->success(null, '已成功登出。');
} catch (\Exception $e) {
Log::error('Error during admin logout', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'admin_id' => $request->user()?->id,
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 修改管理员密码
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function changePassword(Request $request): JsonResponse
{
try {
/** @var Admin|null $admin */
$admin = $request->user();
if (!$admin) {
return $this->error(
ErrorCode::UNAUTHORIZED,
'未登錄或會話已過期。'
);
}
/** @var array{current_password: string, new_password: string} $validated */
$validated = $request->validate([
'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed',
]);
if (!Hash::check($validated['current_password'], $admin->password)) {
return $this->error(
ErrorCode::INVALID_CREDENTIALS,
'当前密码错误。'
);
}
$admin->password = Hash::make($validated['new_password']);
$admin->save();
$this->logService->logOperation(
'admin',
$admin->id,
'Admin changed password'
);
return $this->success(null, '密码修改成功。');
} catch (ValidationException $e) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (\Exception $e) {
Log::error('Error changing admin password', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'admin_id' => $request->user()?->id,
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
}

View file

@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use App\Models\Client;
use App\Services\Auth\TokenService;
use App\Services\LogService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\ValidationException;
class ClientController extends Controller
{
/**
* @var Admin
*/
private Admin $admin;
public function __construct(
private readonly TokenService $tokenService,
private readonly LogService $logService,
Request $request
) {
$admin = $request->admin;
if (!$admin) {
throw new \RuntimeException('管理員信息未找到。');
}
$this->admin = $admin;
}
/**
* 獲取客戶列表
*/
public function index(): JsonResponse
{
try {
$query = Client::select([
'id',
'name',
'llm_provider_id',
'created_at',
]);
// 如果不是超級管理員,只能看到自己管理的客戶
if (!$this->admin->isSuperAdmin()) {
$query->whereHas('admins', function ($query) {
$query->where('admin_id', $this->admin->id);
});
}
$clients = $query->get();
return $this->success([
'items' => $clients->map(fn($client) => [
'id' => $client->id,
'name' => $client->name,
'llm_provider_id' => $client->llm_provider_id,
'created_at' => $client->created_at->toIso8601String(),
])
]);
} catch (\Exception $e) {
Log::error('Error fetching clients', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 新增客戶
*/
public function store(Request $request): JsonResponse
{
try {
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
'unique:clients',
],
'llm_provider_id' => [
'required',
'integer',
'exists:llm_providers,id',
],
]);
// 檢查是否有權限管理該提供商
if (!$this->admin->canManageLlmProvider($validated['llm_provider_id'])) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權使用該 LLM 提供商。'
);
}
$validated['status'] = Client::STATUS_ACTIVE;
$validated['rate_limit'] = config('llm.default_rate_limit', 60);
$validated['timeout'] = config('llm.default_timeout', 30);
$client = Client::create($validated);
// 如果不是超級管理員,需要建立關聯
if (!$this->admin->isSuperAdmin()) {
$client->admins()->attach($this->admin->id);
}
$this->logService->logOperation(
'admin',
$this->admin->id,
"Created client: {$client->name}"
);
return $this->created([
'id' => $client->id,
'name' => $client->name,
'llm_provider_id' => $client->llm_provider_id,
'created_at' => $client->created_at->toIso8601String(),
]);
} catch (ValidationException $e) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (\Exception $e) {
Log::error('Error creating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_data' => $request->all(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 修改客戶
*/
public function update(Request $request, int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
"unique:clients,name,{$id}",
],
'llm_provider_id' => [
'required',
'integer',
'exists:llm_providers,id',
],
]);
$client->update($validated);
$this->logService->logOperation(
'admin',
$this->admin->id,
"Updated client: {$client->name}"
);
return $this->success([
'id' => $client->id,
'name' => $client->name,
'llm_provider_id' => $client->llm_provider_id,
'updated_at' => $client->updated_at->toIso8601String(),
]);
} catch (ValidationException $e) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error updating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'request_data' => $request->all(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 刪除客戶
*/
public function destroy(int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法刪除。'
);
}
$client->delete();
$this->logService->logOperation(
'admin',
$this->admin->id,
"Deleted client: {$client->name}"
);
return $this->success(null, '客戶已刪除。');
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error deleting client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 為客戶生成認證令牌
*/
public function generateAuthToken(int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
if (!$client->isActive()) {
return $this->error(
ErrorCode::CLIENT_INACTIVE,
'客戶未啟用,無法生成令牌。'
);
}
$result = $this->tokenService->generateAuthToken($client);
return $this->success([
'client_id' => $client->id,
'auth_token' => $result['auth_token'],
'created_at' => now()->toIso8601String(),
]);
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error generating auth token', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'client_exists' => Client::find($id) ? 'yes' : 'no',
'redis_status' => Redis::ping() ? 'connected' : 'disconnected',
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 檢查客戶是否有活躍的令牌
*/
private function hasActiveTokens(Client $client): bool
{
$pattern = 'auth_token:*';
$keys = Redis::keys($pattern);
foreach ($keys as $key) {
$data = Redis::get($key);
if ($data) {
$tokenData = json_decode($data, true);
if ($tokenData &&
$tokenData['client_id'] === $client->id &&
now()->isBefore($tokenData['expires_at'])) {
return true;
}
}
}
return false;
}
}

View file

@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use App\Models\LlmProvider;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class LlmProviderController extends Controller
{
/**
* @var Admin
*/
private readonly Admin $admin;
public function __construct(Request $request)
{
$admin = $request->admin;
if (!$admin instanceof Admin) {
throw new \RuntimeException('管理員信息未找到。');
}
$this->admin = $admin;
}
/**
* 獲取 LLM 提供商列表
*
* @return JsonResponse
*/
public function index(): JsonResponse
{
try {
/** @var \Illuminate\Database\Eloquent\Builder $providers */
$providers = LlmProvider::select([
'id',
'name',
'service_name',
'api_url',
'status',
'created_at',
]);
// 如果不是超級管理員,只能看到自己管理的提供商
if (!$this->admin->isSuperAdmin()) {
$providers->whereHas('clients', function ($query) {
$query->whereHas('admins', function ($query) {
$query->where('admin_id', $this->admin->id);
});
});
}
/** @var \Illuminate\Database\Eloquent\Collection $providerList */
$providerList = $providers->get();
return response()->json([
'items' => $providerList->map(fn($provider) => [
'id' => $provider->id,
'name' => $provider->name,
'service_name' => $provider->service_name,
'api_url' => $provider->api_url,
'status' => $provider->status,
'created_at' => $provider->created_at->toIso8601String(),
])
]);
} catch (\Exception $e) {
Log::error('Error fetching LLM providers', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'error' => ErrorCode::SERVER_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
]);
}
}
/**
* 新增 LLM 提供商
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function store(Request $request): JsonResponse
{
try {
// 只有超級管理員可以新增提供商
if (!$this->admin->isSuperAdmin()) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '只有超級管理員可以新增 LLM 提供商。'
]);
}
/** @var array{
* name: string,
* service_name: string,
* api_url: string,
* api_token: string
* } $validated */
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
'unique:llm_providers',
],
'service_name' => [
'required',
'string',
'max:100',
],
'api_url' => [
'required',
'string',
'url',
'max:255',
],
'api_token' => [
'required',
'string',
'max:255',
],
]);
$validated['status'] = LlmProvider::STATUS_ACTIVE;
/** @var LlmProvider $provider */
$provider = LlmProvider::create($validated);
return response()->json([
'id' => $provider->id,
'name' => $provider->name,
'service_name' => $provider->service_name,
'api_url' => $provider->api_url,
'api_token' => $provider->api_token,
'created_at' => $provider->created_at->toIso8601String(),
]);
} catch (ValidationException $e) {
return response()->json([
'error' => ErrorCode::VALIDATION_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors()
]);
} catch (\Exception $e) {
Log::error('Error creating LLM provider', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_data' => $request->except(['api_token']),
]);
return response()->json([
'error' => ErrorCode::SERVER_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
]);
}
}
/**
* 修改 LLM 提供商
*/
public function update(Request $request, int $id): JsonResponse
{
try {
$provider = LlmProvider::findOrFail($id);
if (!$this->admin->canManageLlmProvider($provider->id)) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '您無權管理該 LLM 提供商。'
]);
}
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
"unique:llm_providers,name,{$id}",
],
'service_name' => [
'required',
'string',
'max:100',
],
'api_url' => [
'required',
'string',
'url',
'max:255',
],
'api_token' => [
'required',
'string',
'max:255',
],
'status' => [
'required',
'string',
'in:active,inactive',
],
]);
// 如果要停用提供商,檢查是否有客戶在使用
if ($validated['status'] === LlmProvider::STATUS_INACTIVE &&
$provider->status === LlmProvider::STATUS_ACTIVE) {
if ($provider->clients()->exists()) {
return response()->json([
'error' => ErrorCode::RESOURCE_IN_USE,
'message' => '該提供商正在被客戶使用,無法停用。'
]);
}
}
$provider->update($validated);
return response()->json([
'id' => $provider->id,
'name' => $provider->name,
'service_name' => $provider->service_name,
'api_url' => $provider->api_url,
'api_token' => $provider->api_token,
'updated_at' => $provider->updated_at->toIso8601String(),
]);
} catch (ValidationException $e) {
return response()->json([
'error' => ErrorCode::VALIDATION_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors()
]);
} catch (\Exception $e) {
Log::error('Error updating LLM provider', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'provider_id' => $id,
'request_data' => $request->except(['api_token']),
]);
return response()->json([
'error' => ErrorCode::SERVER_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
]);
}
}
/**
* 刪除 LLM 提供商
*/
public function destroy(int $id): JsonResponse
{
try {
$provider = LlmProvider::findOrFail($id);
if (!$this->admin->canManageLlmProvider($provider->id)) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '您無權管理該 LLM 提供商。'
]);
}
// 檢查是否有客戶在使用
if ($provider->clients()->exists()) {
return response()->json([
'error' => ErrorCode::RESOURCE_IN_USE,
'message' => '該提供商正在被客戶使用,無法刪除。'
]);
}
$providerName = $provider->name;
$provider->delete();
return response()->json([
'message' => 'LLM 提供商已刪除。'
]);
} catch (\Exception $e) {
Log::error('Error deleting LLM provider', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'provider_id' => $id,
]);
return response()->json([
'error' => ErrorCode::SERVER_ERROR,
'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
]);
}
}
}

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Services\Auth\TokenService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* @var TokenService
*/
private readonly TokenService $tokenService;
public function __construct(TokenService $tokenService)
{
$this->tokenService = $tokenService;
}
/**
* 獲取訪問令牌
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function getAccessToken(Request $request): JsonResponse
{
try {
/** @var array{auth_token: string} $validated */
$validated = $request->validate([
'auth_token' => 'required|string|size:64',
]);
/** @var array{client_id: int, expires_at: string}|null $authTokenData */
$authTokenData = $this->tokenService->validateAuthToken($validated['auth_token']);
if (!$authTokenData || !isset($authTokenData['expires_at'])) {
return $this->error(
ErrorCode::TOKEN_INVALID,
'認證令牌無效。'
);
}
if (now()->isAfter($authTokenData['expires_at'])) {
return $this->error(
ErrorCode::TOKEN_EXPIRED,
'認證令牌已過期,請重新獲取。'
);
}
/** @var array{access_token: string, expires_in: int} $result */
$result = $this->tokenService->generateAccessToken($authTokenData);
if (!isset($result['access_token']) || !isset($result['expires_in'])) {
throw new \RuntimeException('生成訪問令牌失敗。');
}
return $this->success([
'access_token' => $result['access_token'],
'expires_in' => $result['expires_in'],
]);
} catch (ValidationException $e) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (\Exception $e) {
Log::error('Error generating access token', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Services\LlmService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class LlmController extends Controller
{
/**
* @var LlmService
*/
private readonly LlmService $llmService;
public function __construct(LlmService $llmService)
{
$this->llmService = $llmService;
}
/**
* 發送 LLM 請求
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException|\RuntimeException
*/
public function request(Request $request): JsonResponse
{
try {
if (!$request->client) {
throw new \RuntimeException('客戶信息未找到。');
}
/** @var array{
* prompt: string,
* max_tokens?: int,
* temperature?: float,
* top_p?: float,
* frequency_penalty?: float,
* presence_penalty?: float
* } $validated */
$validated = $request->validate([
'prompt' => 'required|string|max:4000',
'max_tokens' => 'nullable|integer|min:1|max:4000',
'temperature' => 'nullable|numeric|min:0|max:2',
'top_p' => 'nullable|numeric|min:0|max:1',
'frequency_penalty' => 'nullable|numeric|min:-2|max:2',
'presence_penalty' => 'nullable|numeric|min:-2|max:2',
]);
/** @var array{response: string} $result */
$result = $this->llmService->sendRequest(
$request->client,
$validated['prompt'],
array_filter($validated, fn($key) => $key !== 'prompt', ARRAY_FILTER_USE_KEY)
);
if (!isset($result['response'])) {
throw new \RuntimeException('LLM 提供商返回的響應格式無效。');
}
return $this->success([
'response' => $result['response'],
]);
} catch (ValidationException $e) {
return $this->error(
ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors()
);
} catch (\InvalidArgumentException $e) {
return $this->error(
ErrorCode::INVALID_REQUEST_FORMAT,
$e->getMessage()
);
} catch (\RuntimeException $e) {
return $this->error(
ErrorCode::PROVIDER_ERROR,
$e->getMessage()
);
} catch (\Exception $e) {
Log::error('Error processing LLM request', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $request->client->id,
'request_data' => $request->except(['prompt']),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
}

View file

@ -1,8 +1,103 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
abstract class Controller abstract class Controller
{ {
// /**
* 成功响应
*
* @param mixed $data 响应数据
* @param string|null $message 成功消息
* @param int $code HTTP状态码
*/
protected function success(mixed $data = null, ?string $message = null, int $code = Response::HTTP_OK): JsonResponse
{
$response = [
'success' => true,
];
if ($data !== null) {
$response['data'] = $data;
}
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response, $code);
}
/**
* 错误响应
*
* @param string $error 错误代码
* @param string $message 错误消息
* @param mixed $errors 详细错误信息
* @param int $code HTTP状态码
*/
protected function error(
string $error,
string $message,
mixed $errors = null,
int $code = Response::HTTP_BAD_REQUEST
): JsonResponse {
$response = [
'success' => false,
'error' => $error,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
/**
* 分页响应
*
* @param array $items 分页数据
* @param array $meta 分页元数据
* @param string|null $message 成功消息
*/
protected function paginate(array $items, array $meta, ?string $message = null): JsonResponse
{
$response = [
'success' => true,
'data' => $items,
'meta' => $meta,
];
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response);
}
/**
* 创建成功响应
*
* @param mixed $data 创建的资源数据
* @param string|null $message 成功消息
*/
protected function created(mixed $data, ?string $message = null): JsonResponse
{
return $this->success($data, $message, Response::HTTP_CREATED);
}
/**
* 无内容响应
*/
protected function noContent(): JsonResponse
{
return response()->json(null, Response::HTTP_NO_CONTENT);
}
} }

69
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\App\Http\Middleware\ValidateHeaders::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.access_token' => \App\Http\Middleware\ValidateAccessToken::class,
'admin' => \App\Http\Middleware\AdminAuthenticate::class,
];
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Constants\ErrorCode;
use App\Models\Admin;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class AdminAuthenticate
{
public function handle(Request $request, Closure $next): Response
{
// 检查是否已登录
if (!Auth::guard('admin')->check()) {
return response()->json([
'error' => ErrorCode::UNAUTHORIZED,
'message' => '未授权,请先登录。',
], Response::HTTP_UNAUTHORIZED);
}
/** @var Admin $admin */
$admin = Auth::guard('admin')->user();
// 检查是否是有效的管理员
if (!$admin->isValidAdmin()) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '无权访问管理员资源。',
], Response::HTTP_FORBIDDEN);
}
// 检查资源访问权限
if (!$this->checkResourcePermission($request, $admin)) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '无权访问该资源。',
], Response::HTTP_FORBIDDEN);
}
// Add admin information to the request
$request->merge(['admin' => $admin]);
return $next($request);
}
/**
* 检查管理员是否有权限访问请求的资源
*/
private function checkResourcePermission(Request $request, Admin $admin): bool
{
// 超级管理员可以访问所有资源
if ($admin->isSuperAdmin()) {
return true;
}
// 获取路由参数
$clientId = $request->route('client') ?? $request->route('id');
$providerId = $request->route('llm_provider') ?? $request->route('id');
// 检查客户管理权限
if ($clientId && str_contains($request->path(), 'clients')) {
return $admin->canManageClient((int)$clientId);
}
// 检查LLM提供商管理权限
if ($providerId && str_contains($request->path(), 'llm-providers')) {
return $admin->canManageLlmProvider((int)$providerId);
}
// 默认允许访问其他资源
return true;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @return mixed
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Constants\ErrorCode;
use App\Models\Client;
use App\Services\Auth\TokenService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ValidateAccessToken
{
public function __construct(
private readonly TokenService $tokenService
) {}
public function handle(Request $request, Closure $next): Response
{
// 禁止訪問管理員路由
if (str_starts_with($request->path(), 'api/admin')) {
return response()->json([
'success' => false,
'error' => ErrorCode::FORBIDDEN,
'message' => '客戶用戶無權訪問管理員資源。',
], Response::HTTP_FORBIDDEN);
}
$bearerToken = $request->bearerToken();
if (!$bearerToken) {
return response()->json([
'success' => false,
'error' => ErrorCode::UNAUTHORIZED,
'message' => '未授權,令牌無效或未提供。',
], Response::HTTP_UNAUTHORIZED);
}
$tokenData = $this->tokenService->validateAccessToken($bearerToken);
if (!$tokenData) {
return response()->json([
'success' => false,
'error' => ErrorCode::UNAUTHORIZED,
'message' => '訪問令牌無效或已過期。',
], Response::HTTP_UNAUTHORIZED);
}
// 檢查客戶狀態
$client = Client::find($tokenData['client_id']);
if (!$client || !$client->isActive()) {
return response()->json([
'success' => false,
'error' => ErrorCode::CLIENT_INACTIVE,
'message' => '客戶帳戶未啟用。',
], Response::HTTP_FORBIDDEN);
}
// 檢查 LLM 提供商狀態
if (!$client->canSendLlmRequest()) {
return response()->json([
'success' => false,
'error' => ErrorCode::PROVIDER_ERROR,
'message' => 'LLM 提供商服務暫時不可用。',
], Response::HTTP_SERVICE_UNAVAILABLE);
}
// 將客戶信息添加到請求中
$request->merge(['client' => $client]);
return $next($request);
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Constants\ErrorCode;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ValidateHeaders
{
/**
* 处理请求
*
* @param Request $request
* @param Closure $next
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
// 检查 Content-Type
if ($request->isMethod('POST') || $request->isMethod('PUT') || $request->isMethod('PATCH')) {
if (!$request->hasHeader('Content-Type') || !str_contains($request->header('Content-Type'), 'application/json')) {
return response()->json([
'success' => false,
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
'message' => '请求头必须包含 Content-Type: application/json',
], Response::HTTP_BAD_REQUEST);
}
}
// 检查 Accept
if (!$request->hasHeader('Accept') || !str_contains($request->header('Accept'), 'application/json')) {
return response()->json([
'success' => false,
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
'message' => '请求头必须包含 Accept: application/json',
], Response::HTTP_BAD_REQUEST);
}
// 检查 API 版本
if (!$request->hasHeader('X-API-Version')) {
return response()->json([
'success' => false,
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
'message' => '请求头必须包含 X-API-Version',
], Response::HTTP_BAD_REQUEST);
}
// 检查客户端标识
if (!$request->hasHeader('X-Client-ID')) {
return response()->json([
'success' => false,
'error' => ErrorCode::INVALID_REQUEST_FORMAT,
'message' => '请求头必须包含 X-Client-ID',
], Response::HTTP_BAD_REQUEST);
}
return $next($request);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'api/*',
];
}

111
app/Models/Admin.php Normal file
View file

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class Admin extends Authenticatable
{
use HasApiTokens;
/**
* 管理员角色常量
*/
public const ROLE_SUPER = 'super';
public const ROLE_ADMIN = 'admin';
/**
* 有效的角色列表
*/
public const VALID_ROLES = [
self::ROLE_SUPER,
self::ROLE_ADMIN,
];
protected $table = 'admins';
protected $fillable = [
'username',
'email',
'password',
'role',
];
protected $hidden = [
'password',
];
protected $casts = [
'role' => 'string',
];
/**
* @return BelongsToMany<Client>
*/
public function clients(): BelongsToMany
{
return $this->belongsToMany(Client::class, 'admin_client')
->withTimestamps();
}
/**
* 检查是否是超级管理员
*/
public function isSuperAdmin(): bool
{
return $this->role === self::ROLE_SUPER;
}
/**
* 检查是否是普通管理员
*/
public function isAdmin(): bool
{
return $this->role === self::ROLE_ADMIN;
}
/**
* 检查是否有效的管理员(包括超级管理员和普通管理员)
*/
public function isValidAdmin(): bool
{
return in_array($this->role, self::VALID_ROLES, true);
}
/**
* 检查是否可以管理指定的客户
*
* @param int $clientId
*/
public function canManageClient(int $clientId): bool
{
if ($this->isSuperAdmin()) {
return true;
}
return $this->clients()->where('client_id', $clientId)->exists();
}
/**
* 检查是否可以管理指定的LLM提供商
*
* @param int $providerId
*/
public function canManageLlmProvider(int $providerId): bool
{
if ($this->isSuperAdmin()) {
return true;
}
// 普通管理员只能管理与其关联客户绑定的LLM提供商
return $this->clients()
->whereHas('llmProvider', function ($query) use ($providerId) {
$query->where('id', $providerId);
})
->exists();
}
}

38
app/Models/AuthToken.php Normal file
View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AuthToken extends Model
{
protected $table = 'auth_tokens';
protected $fillable = [
'client_id',
'token',
'expires_at',
];
protected $casts = [
'expires_at' => 'datetime',
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function isValid(): bool
{
return $this->expires_at && $this->expires_at->isFuture();
}
public function isExpired(): bool
{
return !$this->isValid();
}
}

77
app/Models/Client.php Normal file
View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Client extends Model
{
/**
* 客户状态常量
*/
public const STATUS_ACTIVE = 'active';
public const STATUS_INACTIVE = 'inactive';
/**
* 有效的状态列表
*/
public const VALID_STATUSES = [
self::STATUS_ACTIVE,
self::STATUS_INACTIVE,
];
protected $table = 'clients';
protected $fillable = [
'name',
'llm_provider_id',
'rate_limit',
'timeout',
'status',
];
protected $casts = [
'rate_limit' => 'integer',
'timeout' => 'integer',
'status' => 'string',
];
/**
* @return BelongsTo<LlmProvider>
*/
public function llmProvider(): BelongsTo
{
return $this->belongsTo(LlmProvider::class);
}
/**
* @return BelongsToMany<Admin>
*/
public function admins(): BelongsToMany
{
return $this->belongsToMany(Admin::class, 'admin_client')
->withTimestamps();
}
/**
* 检查客户是否处于活跃状态
*/
public function isActive(): bool
{
return $this->status === self::STATUS_ACTIVE;
}
/**
* 检查是否可以发送LLM请求
*/
public function canSendLlmRequest(): bool
{
/** @var LlmProvider|null $provider */
$provider = $this->llmProvider;
return $this->isActive() && $provider && $provider->isActive();
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class LlmProvider extends Model
{
/**
* 提供商状态常量
*/
public const STATUS_ACTIVE = 'active';
public const STATUS_INACTIVE = 'inactive';
/**
* 有效的状态列表
*/
public const VALID_STATUSES = [
self::STATUS_ACTIVE,
self::STATUS_INACTIVE,
];
protected $table = 'llm_providers';
protected $fillable = [
'name',
'service_name',
'api_url',
'api_token',
'status',
];
protected $hidden = [
'api_token',
];
protected $casts = [
'status' => 'string',
];
/**
* @return HasMany<Client>
*/
public function clients(): HasMany
{
return $this->hasMany(Client::class);
}
/**
* 检查提供商是否处于活跃状态
*/
public function isActive(): bool
{
return $this->status === self::STATUS_ACTIVE;
}
}

View file

@ -2,7 +2,12 @@
namespace App\Providers; namespace App\Providers;
use App\Http\Middleware\AdminAuthenticate;
use App\Http\Middleware\ValidateAccessToken;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -11,7 +16,8 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
// Route::aliasMiddleware('auth.access_token', ValidateAccessToken::class);
Route::aliasMiddleware('auth.admin', AdminAuthenticate::class);
} }
/** /**
@ -19,6 +25,17 @@ public function register(): void
*/ */
public function boot(): void public function boot(): void
{ {
// Route::aliasMiddleware('auth.access_token', ValidateAccessToken::class);
Route::aliasMiddleware('auth.admin', AdminAuthenticate::class);
// Redis 連接檢查
try {
Redis::ping();
} catch (\Exception $e) {
Log::error('Redis connection failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
} }
} }

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
logger('Loading API routes...'); // 測試代碼
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\RateLimiter;
use Closure;
class ThrottleAuthToken implements Rule
{
private const MAX_ATTEMPTS = 5; // 最大尝试次数
private const DECAY_MINUTES = 1; // 重置时间(分钟)
public function passes($attribute, $value): bool
{
$key = 'auth_token_' . $value;
if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) {
return false;
}
RateLimiter::hit($key, self::DECAY_MINUTES * 60);
return true;
}
public function message(): string
{
return '请求过于频繁,请稍后重试。';
}
}

View file

@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace App\Services\Auth;
use App\Models\Client;
use App\Services\LogService;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
class TokenService
{
private const AUTH_TOKEN_PREFIX = 'auth_token:';
private const ACCESS_TOKEN_PREFIX = 'access_token:';
private const BLACKLIST_PREFIX = 'token_blacklist:';
private const MAX_AUTH_TOKEN_HOURS = 8; // 最大 8 小時有效期
private const ACCESS_TOKEN_TTL = 3600; // 1 小時有效期
public function __construct(
private readonly LogService $logService
) {}
/**
* 生成認證令牌
*
* @return array{auth_token: string, expires_at: string, client_id: int}
*/
public function generateAuthToken(Client $client, ?int $expiresInHours = null): array
{
// 確保不超過最大有效期
$effectiveHours = min($expiresInHours ?? self::MAX_AUTH_TOKEN_HOURS, self::MAX_AUTH_TOKEN_HOURS);
$effectiveSeconds = $effectiveHours * 3600;
// 生成新令牌
$token = Str::random(64);
$expiresAt = now()->addHours($effectiveHours);
// 存儲到 Redis
Redis::setex(
self::AUTH_TOKEN_PREFIX . $token,
$effectiveSeconds,
json_encode([
'client_id' => $client->id,
'token' => $token,
'created_at' => now()->toIso8601String(),
'expires_at' => $expiresAt->toIso8601String(),
])
);
// 記錄操作日誌
$this->logService->logOperation(
'client',
$client->id,
'Generated new auth token'
);
return [
'auth_token' => $token,
'expires_at' => $expiresAt->toIso8601String(),
'client_id' => $client->id,
];
}
/**
* 驗證認證令牌
*
* @param string $token
* @return array{client_id: int, token: string, created_at: string, expires_at: string}|null
*/
public function validateAuthToken(string $token): ?array
{
// 檢查黑名單
/** @var bool $isBlacklisted */
$isBlacklisted = Redis::exists(self::BLACKLIST_PREFIX . $token);
if ($isBlacklisted) {
return null;
}
// 檢查令牌是否存在且有效
/** @var string|null $tokenData */
$tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token);
if (!$tokenData) {
return null;
}
/** @var array{client_id: int, token: string, created_at: string, expires_at: string}|null $data */
$data = json_decode($tokenData, true);
if (!$data || now()->isAfter($data['expires_at'])) {
Redis::del(self::AUTH_TOKEN_PREFIX . $token);
return null;
}
return $data;
}
/**
* 生成訪問令牌
*
* @return array{access_token: string, expires_in: int}
*/
public function generateAccessToken(array $authTokenData): array
{
$accessToken = Str::random(64);
// 存儲到 Redis包含更多信息以便追蹤
Redis::setex(
self::ACCESS_TOKEN_PREFIX . $accessToken,
self::ACCESS_TOKEN_TTL,
json_encode([
'client_id' => $authTokenData['client_id'],
'auth_token' => $authTokenData['token'],
'created_at' => now()->toIso8601String(),
])
);
// 記錄操作日誌
$this->logService->logOperation(
'client',
$authTokenData['client_id'],
'Generated new access token'
);
return [
'access_token' => $accessToken,
'expires_in' => self::ACCESS_TOKEN_TTL,
];
}
/**
* 驗證訪問令牌
*
* @param string $token
* @return array{client_id: int, auth_token: string, created_at: string}|null
*/
public function validateAccessToken(string $token): ?array
{
/** @var string|null $tokenData */
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
if (!$tokenData) {
return null;
}
/** @var array{client_id: int, auth_token: string, created_at: string}|null $data */
$data = json_decode($tokenData, true);
return $data;
}
/**
* 撤銷訪問令牌
*/
public function revokeAccessToken(string $token): void
{
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
if ($tokenData) {
$data = json_decode($tokenData, true);
if ($data) {
// 記錄操作日誌
$this->logService->logOperation(
'client',
$data['client_id'],
'Access token revoked'
);
}
// 從 Redis 中刪除
Redis::del(self::ACCESS_TOKEN_PREFIX . $token);
}
}
/**
* 撤銷認證令牌
*/
public function revokeAuthToken(string $token): void
{
$tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token);
if ($tokenData) {
$data = json_decode($tokenData, true);
if ($data) {
// 計算剩餘有效期
$expiresAt = now()->parse($data['expires_at']);
$remainingSeconds = max(1, $expiresAt->diffInSeconds(now()));
// 加入黑名單
Redis::setex(
self::BLACKLIST_PREFIX . $token,
$remainingSeconds,
'revoked'
);
// 記錄操作日誌
$this->logService->logOperation(
'client',
$data['client_id'],
'Auth token revoked'
);
}
// 從 Redis 中刪除
Redis::del(self::AUTH_TOKEN_PREFIX . $token);
}
}
}

View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Client;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
class LlmService
{
private const REQUEST_TIMEOUT = 30; // 请求超时时间(秒)
/**
* 发送 LLM 请求
*
* @param Client $client 客户
* @param string $prompt 提示词
* @param array $options 可选参数
* @return array{response: string, usage: array}
* @throws \RuntimeException
*/
public function sendRequest(Client $client, string $prompt, array $options = []): array
{
// 检查请求频率限制
$key = 'llm_request_' . $client->id;
if (RateLimiter::tooManyAttempts($key, $client->rate_limit)) {
$seconds = RateLimiter::availableIn($key);
throw new \RuntimeException("请求过于频繁,请在 {$seconds} 秒后重试。");
}
try {
/** @var \App\Models\LlmProvider $provider */
$provider = $client->llmProvider;
if ($provider->status !== 'active') {
throw new \RuntimeException('LLM 提供商当前不可用。');
}
// 发送请求到 LLM 提供商
$response = Http::timeout($client->timeout)
->withToken($provider->api_token)
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post($provider->api_url, array_merge([
'prompt' => $prompt,
], $options));
if (!$response->successful()) {
Log::error('LLM provider request failed', [
'status' => $response->status(),
'body' => $response->body(),
'provider' => $provider->name,
'client_id' => $client->id,
]);
throw new \RuntimeException('LLM 提供商服务异常,请稍后重试。');
}
// 记录成功的请求
RateLimiter::hit($key, 60);
$result = $response->json();
return [
'response' => $result['choices'][0]['text'] ?? $result['response'] ?? '',
'usage' => $result['usage'] ?? [],
];
} catch (\Illuminate\Http\Client\ConnectionException $e) {
Log::error('LLM provider connection timeout', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'provider' => $provider->name ?? null,
'client_id' => $client->id,
]);
throw new \RuntimeException('LLM 提供商响应超时,请稍后重试。');
}
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Request;
class LogService
{
public function logOperation(string $userType, int $userId, string $operation): void
{
DB::table('operation_logs')->insert([
'user_type' => $userType,
'user_id' => $userId,
'operation' => $operation,
'ip_address' => Request::ip(),
'created_at' => now(),
]);
}
}

View file

@ -7,6 +7,7 @@
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )

46
clear_and_cache.sh Normal file
View file

@ -0,0 +1,46 @@
#!/bin/bash
# 清除 Laravel 快取和重新快取設定
echo "Clearing Laravel caches..."
php artisan config:clear
if [ $? -eq 0 ]; then
echo "Config cache cleared successfully."
else
echo "Failed to clear config cache."
exit 1
fi
php artisan cache:clear
if [ $? -eq 0 ]; then
echo "Application cache cleared successfully."
else
echo "Failed to clear application cache."
exit 1
fi
php artisan route:clear
if [ $? -eq 0 ]; then
echo "Route cache cleared successfully."
else
echo "Failed to clear route cache."
exit 1
fi
php artisan view:clear
if [ $? -eq 0 ]; then
echo "View cache cleared successfully."
else
echo "Failed to clear view cache."
exit 1
fi
php artisan config:cache
if [ $? -eq 0 ]; then
echo "Config cache regenerated successfully."
else
echo "Failed to regenerate config cache."
exit 1
fi
echo "All Laravel cache operations completed successfully."

View file

@ -8,7 +8,9 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"laravel/framework": "^11.31", "laravel/framework": "^11.31",
"laravel/tinker": "^2.9" "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.9",
"predis/predis": "^2.3"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",

127
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "626b9e7ddd47fb7eff9aaa53cce0c9ad", "content-hash": "e7dd48d806ba5d560c773b49c46386e5",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1326,6 +1326,70 @@
}, },
"time": "2024-11-12T14:59:47+00:00" "time": "2024-11-12T14:59:47+00:00"
}, },
{
"name": "laravel/sanctum",
"version": "v4.0.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fe361b9a63407a228f884eb78d7217f680b50140"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fe361b9a63407a228f884eb78d7217f680b50140",
"reference": "fe361b9a63407a228f884eb78d7217f680b50140",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0",
"illuminate/contracts": "^11.0",
"illuminate/database": "^11.0",
"illuminate/support": "^11.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2024-11-26T14:36:23+00:00"
},
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v2.0.0", "version": "v2.0.0",
@ -2406,6 +2470,67 @@
], ],
"time": "2024-07-20T21:41:07+00:00" "time": "2024-07-20T21:41:07+00:00"
}, },
{
"name": "predis/predis",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9",
"reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^8.0 || ^9.4"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"type": "library",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
}
],
"description": "A flexible and feature-complete Redis client for PHP.",
"homepage": "http://github.com/predis/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v2.3.0"
},
"funding": [
{
"url": "https://github.com/sponsors/tillkruss",
"type": "github"
}
],
"time": "2024-11-21T20:00:02+00:00"
},
{ {
"name": "psr/clock", "name": "psr/clock",
"version": "1.0.0", "version": "1.0.0",

View file

@ -40,6 +40,10 @@
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'admin' => [
'driver' => 'sanctum',
'provider' => 'admins',
],
], ],
/* /*
@ -62,13 +66,12 @@
'providers' => [ 'providers' => [
'users' => [ 'users' => [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class), 'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
], ],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
], ],
/* /*

83
config/sanctum.php Normal file
View file

@ -0,0 +1,83 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

30
database/sql/init.sql Normal file
View file

@ -0,0 +1,30 @@
-- 插入超級管理員帳號
-- 密碼 'abc123' 使用 bcrypt 加密
INSERT INTO `admins` (`username`, `email`, `password`, `role`, `created_at`, `updated_at`)
VALUES (
'admin',
'admin@cv6.me',
'$2y$12$co71F.UxUP.TGvI/fMD4JuYS.meR7yoKfPQjQ43hOF.NXIBDn5dRm', -- 'abc123' 的新 hash
'super',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 插入預設的 LLM Provider
INSERT INTO `llm_providers` (
`name`,
`service_name`,
`api_url`,
`api_token`,
`status`,
`created_at`,
`updated_at`
) VALUES (
'OpenAI',
'openai',
'https://api.openai.com/v1',
'sk-default-token',
'active',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);

123
database/sql/schema.sql Normal file
View file

@ -0,0 +1,123 @@
-- 禁用外鍵檢查以避免在刪除表格時出現外鍵依賴問題
SET FOREIGN_KEY_CHECKS = 0;
-- ===========================
-- 1. 刪除外鍵約束(如果存在)
-- ===========================
-- 刪除 `clients` 表的外鍵約束
ALTER TABLE `clients` DROP FOREIGN KEY IF EXISTS `clients_llm_provider_id_foreign`;
-- 刪除 `auth_tokens` 表的外鍵約束
ALTER TABLE `auth_tokens` DROP FOREIGN KEY IF EXISTS `auth_tokens_client_id_foreign`;
-- 刪除 `admin_client` 表的外鍵約束
ALTER TABLE `admin_client` DROP FOREIGN KEY IF EXISTS `admin_client_admin_id_foreign`;
ALTER TABLE `admin_client` DROP FOREIGN KEY IF EXISTS `admin_client_client_id_foreign`;
-- ===========================
-- 2. 刪除表格(如果存在)
-- ===========================
-- 按照依賴順序刪除表格
DROP TABLE IF EXISTS `admin_client`;
DROP TABLE IF EXISTS `auth_tokens`;
DROP TABLE IF EXISTS `clients`;
DROP TABLE IF EXISTS `llm_providers`;
DROP TABLE IF EXISTS `admins`;
DROP TABLE IF EXISTS `operation_logs`;
-- ===========================
-- 3. 創建表格
-- ===========================
-- 創建 `admins` 表
CREATE TABLE `admins` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(100) NULL DEFAULT NULL,
`role` ENUM('super', 'admin') NOT NULL DEFAULT 'admin',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `admins_username_unique` (`username`),
KEY `admins_email_index` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 創建 `llm_providers` 表
CREATE TABLE `llm_providers` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`service_name` VARCHAR(100) NOT NULL,
`api_url` VARCHAR(255) NOT NULL,
`api_token` VARCHAR(255) NOT NULL,
`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `llm_providers_name_unique` (`name`),
KEY `llm_providers_service_name_index` (`service_name`),
KEY `llm_providers_status_index` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 創建 `clients` 表
CREATE TABLE `clients` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`llm_provider_id` BIGINT UNSIGNED NOT NULL,
`rate_limit` INT NOT NULL DEFAULT 60,
`timeout` INT NOT NULL DEFAULT 30,
`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `clients_llm_provider_id_foreign` (`llm_provider_id`),
KEY `clients_status_index` (`status`),
CONSTRAINT `clients_llm_provider_id_foreign` FOREIGN KEY (`llm_provider_id`)
REFERENCES `llm_providers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 創建 `auth_tokens` 表
CREATE TABLE `auth_tokens` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`client_id` BIGINT UNSIGNED NOT NULL,
`token` CHAR(64) NOT NULL,
`expires_at` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `auth_tokens_token_unique` (`token`),
KEY `auth_tokens_client_id_index` (`client_id`),
CONSTRAINT `auth_tokens_client_id_foreign` FOREIGN KEY (`client_id`)
REFERENCES `clients` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 創建 `admin_client` 表(聯接表)
CREATE TABLE `admin_client` (
`admin_id` BIGINT UNSIGNED NOT NULL,
`client_id` BIGINT UNSIGNED NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`admin_id`, `client_id`),
KEY `admin_client_client_id_foreign` (`client_id`),
CONSTRAINT `admin_client_admin_id_foreign` FOREIGN KEY (`admin_id`)
REFERENCES `admins` (`id`) ON DELETE CASCADE,
CONSTRAINT `admin_client_client_id_foreign` FOREIGN KEY (`client_id`)
REFERENCES `clients` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 創建 `operation_logs` 表
CREATE TABLE `operation_logs` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_type` ENUM('admin', 'client') NOT NULL,
`user_id` BIGINT UNSIGNED NOT NULL,
`operation` VARCHAR(255) NOT NULL,
`ip_address` VARCHAR(45) NULL DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `operation_logs_user_type_user_id_index` (`user_type`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 啟用外鍵檢查
SET FOREIGN_KEY_CHECKS = 1;

268
doc/db.md Normal file
View file

@ -0,0 +1,268 @@
# LLM API 转发平台数据库规格书
---
## 目录
1. [简介](#1-简介)
2. [数据库概览](#2-数据库概览)
3. [MySQL 数据库设计](#3-mysql-数据库设计)
- [3.1 管理员表admins](#31-管理员表admins)
- [3.2 客户用户表clients](#32-客户用户表clients)
- [3.3 LLM 提供商表llm_providers](#33-llm-提供商表llm_providers)
- [3.4 认证令牌表auth_tokens](#34-认证令牌表auth_tokens)
- [3.5 管理员与客户用户关联表admin_client](#35-管理员与客户用户关联表admin_client)
- [3.6 操作日志表operation_logs](#36-操作日志表operation_logs)
4. [Redis 缓存设计](#4-redis-缓存设计)
- [4.1 访问令牌存储](#41-访问令牌存储)
- [4.2 令牌注销机制](#42-令牌注销机制)
5. [数据库关系图](#5-数据库关系图)
6. [总结](#6-总结)
---
## 1. 简介
本规格书旨在基于之前的系统设计,详细设计 LLM API 转发平台的数据库结构。数据库采用 **MySQL** 作为主要的数据存储,**Redis** 用于缓存短期有效的访问令牌和支持动态注销功能。
---
## 2. 数据库概览
- **MySQL**持久化存储管理员、客户用户、LLM 提供商、认证令牌等信息。
- **Redis**:缓存短期访问令牌,实现高效验证和支持令牌的动态注销。
---
## 3. MySQL 数据库设计
### 3.1 管理员表(`admins`
**描述**:存储超级管理员和管理员的信息。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|-----------------|-----------------|----------|--------|----------------------|
| `id` | BIGINT UNSIGNED | 否 | 自增 | 主键 |
| `username` | VARCHAR(50) | 否 | | 用户名 |
| `password` | VARCHAR(255) | 否 | | 加密后的密码 |
| `email` | VARCHAR(100) | 是 | NULL | 邮箱 |
| `role` | ENUM('super', 'admin') | 否 | 'admin' | 角色,'super'为超级管理员,'admin'为管理员 |
| `created_at` | TIMESTAMP | 否 | 当前时间 | 创建时间 |
| `updated_at` | TIMESTAMP | 否 | 当前时间 | 更新时间 |
**索引**
- `UNIQUE INDEX``username`
- `INDEX``email`
### 3.2 客户用户表(`clients`
**描述**:存储客户用户的信息。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|-----------------------|-----------------|----------|--------|------------------------------|
| `id` | BIGINT UNSIGNED | 否 | 自增 | 主键 |
| `name` | VARCHAR(100) | 否 | | 客户用户名称 |
| `llm_provider_id` | BIGINT UNSIGNED | 否 | | 外键,关联到 `llm_providers.id` |
| `created_at` | TIMESTAMP | 否 | 当前时间 | 创建时间 |
| `updated_at` | TIMESTAMP | 否 | 当前时间 | 更新时间 |
**索引**
- `INDEX``llm_provider_id`
**外键约束**
- `llm_provider_id` 引用 `llm_providers.id`,级联更新和删除。
### 3.3 LLM 提供商表(`llm_providers`
**描述**:存储 LLM 提供商的信息。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|------------------|-----------------|----------|--------|------------------------------|
| `id` | BIGINT UNSIGNED | 否 | 自增 | 主键 |
| `name` | VARCHAR(100) | 否 | | 提供商名称 |
| `service_name` | VARCHAR(100) | 否 | | 服务名称,用于后端调用逻辑 |
| `api_url` | VARCHAR(255) | 否 | | 提供商的 API 接口地址 |
| `api_token` | VARCHAR(255) | 否 | | 调用提供商 API 所需的令牌 |
| `created_at` | TIMESTAMP | 否 | 当前时间 | 创建时间 |
| `updated_at` | TIMESTAMP | 否 | 当前时间 | 更新时间 |
**索引**
- `UNIQUE INDEX``name`
- `INDEX``service_name`
### 3.4 认证令牌表(`auth_tokens`
**描述**:存储客户用户的长期认证令牌。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|-----------------|-----------------|----------|--------|--------------------------------|
| `id` | BIGINT UNSIGNED | 否 | 自增 | 主键 |
| `client_id` | BIGINT UNSIGNED | 否 | | 外键,关联到 `clients.id` |
| `token` | CHAR(64) | 否 | | 认证令牌随机生成的64位字符串 |
| `expires_at` | TIMESTAMP | 是 | NULL | 过期时间NULL表示永久有效 |
| `created_at` | TIMESTAMP | 否 | 当前时间 | 创建时间 |
| `updated_at` | TIMESTAMP | 否 | 当前时间 | 更新时间 |
**索引**
- `UNIQUE INDEX``token`
- `INDEX``client_id`
**外键约束**
- `client_id` 引用 `clients.id`,级联删除。
### 3.5 管理员与客户用户关联表(`admin_client`
**描述**:存储管理员与客户用户的多对多关系。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|--------------|-----------------|----------|--------|-----------------------------|
| `admin_id` | BIGINT UNSIGNED | 否 | | 外键,关联到 `admins.id` |
| `client_id` | BIGINT UNSIGNED | 否 | | 外键,关联到 `clients.id` |
| `assigned_at`| TIMESTAMP | 否 | 当前时间 | 分配时间 |
**索引**
- `PRIMARY KEY`(`admin_id`, `client_id`)
- `INDEX``client_id`
**外键约束**
- `admin_id` 引用 `admins.id`,级联删除。
- `client_id` 引用 `clients.id`,级联删除。
### 3.6 操作日志表(`operation_logs`
**描述**:记录管理员和客户用户的操作日志。
**字段**
| 字段名 | 数据类型 | 允许为空 | 默认值 | 描述 |
|-----------------|-----------------|----------|--------|------------------------------|
| `id` | BIGINT UNSIGNED | 否 | 自增 | 主键 |
| `user_type` | ENUM('admin', 'client') | 否 | | 用户类型 |
| `user_id` | BIGINT UNSIGNED | 否 | | 用户ID关联管理员或客户用户 |
| `operation` | VARCHAR(255) | 否 | | 操作描述 |
| `ip_address` | VARCHAR(45) | 是 | NULL | IP地址支持IPv6 |
| `created_at` | TIMESTAMP | 否 | 当前时间 | 操作时间 |
**索引**
- `INDEX``user_type`, `user_id`
---
## 4. Redis 缓存设计
### 4.1 访问令牌存储
**描述**:存储客户用户的短期访问令牌,实现高效的令牌验证和自动过期。
- **Key 格式**`access_token:{token}`
- **Value 内容**:序列化的令牌信息,包括`client_id`、`expires_at`等。
**示例**
- **Key**`access_token:a1b2c3d4e5f6...`
- **Value**
```json
{
"client_id": 12345,
"expires_at": "2023-10-01T12:00:00Z"
}
```
- **过期策略**:设置 `TTL`,令牌将在 `expires_at` 到达时自动过期。
### 4.2 令牌注销机制
**描述**:支持访问令牌的即时注销,防止被滥用。
- **方案**:当需要注销令牌时,直接从 Redis 中删除对应的 `access_token:{token}` 键。
- **实现步骤**
1. 调用 Redis 的 `DEL` 命令删除指定的令牌键。
2. 后续的令牌验证中,如令牌不存在或已过期,则拒绝访问。
---
## 5. 数据库关系图
```
+----------------+ +----------------+ +----------------+
| admins | | clients | | llm_providers |
+----------------+ +----------------+ +----------------+
| id |<----->| id | | id |
| username | | name | | name |
| password | | llm_provider_id|<----->| service_name |
| role | | | | api_url |
+----------------+ +----------------+ | api_token |
^ ^ +----------------+
| |
| +---------------+
| |
+-----------------+
| admin_client |
+-----------------+
| admin_id |
| client_id |
+-----------------+
+----------------+
| auth_tokens |
+----------------+
| id |
| client_id |
| token |
+----------------+
```
**说明**
- `admins``clients` 之间是多对多关系,通过 `admin_client` 关联。
- `clients``llm_providers` 是多对一关系,`clients.llm_provider_id` 外键引用 `llm_providers.id`
- `clients``auth_tokens` 是一对多关系,一个客户用户可以有多个认证令牌。
---
## 6. 总结
以上是基于系统设计的数据库规格书,详细描述了 MySQL 中的表结构和 Redis 中的缓存设计。
- **MySQL**
- **管理员表**:存储超级管理员和管理员的信息。
- **客户用户表**:存储客户用户的信息,并绑定 LLM 提供商。
- **LLM 提供商表**:存储 LLM 提供商的配置和调用信息。
- **认证令牌表**:存储客户用户的长期认证令牌。
- **管理员与客户用户关联表**:实现管理员和客户用户的多对多关系。
- **操作日志表**:记录系统的操作日志,便于审计和追踪。
- **Redis**
- **访问令牌存储**:高效存储和验证短期访问令牌,支持自动过期和动态注销。
**注意事项**
- 所有涉及敏感信息的字段如密码、令牌、API Token在存储时应进行加密或哈希处理确保数据安全。
- 数据库的外键约束和索引设计应充分考虑性能和数据完整性。
- Redis 中的令牌管理应注意内存占用和过期策略,防止缓存过期造成的认证问题。
---
如有任何疑问或需要进一步的修改和完善,请随时联系数据库设计团队。

623
doc/llm_apiv1.txt Normal file
View file

@ -0,0 +1,623 @@
# LLM API 转发平台 API 开发文档
---
## 目录
1. [简介](#1-简介)
2. [认证与授权](#2-认证与授权)
- [2.1 认证令牌Auth Token](#21-认证令牌auth-token)
- [2.2 访问令牌Access Token](#22-访问令牌access-token)
3. [API 概览](#3-api-概览)
- [3.1 客户用户 API](#31-客户用户-api)
- [3.2 管理员 API](#32-管理员-api)
4. [API 详细说明](#4-api-详细说明)
- [4.1 认证 API](#41-认证-api)
- [4.1.1 获取访问令牌](#411-获取访问令牌)
- [4.2 客户用户 API](#42-客户用户-api)
- [4.2.1 发送提示词请求](#421-发送提示词请求)
- [4.3 管理员 API](#43-管理员-api)
- [4.3.1 LLM 提供商管理](#431-llm-提供商管理)
- [4.3.1.1 新增 LLM 提供商](#4311-新增-llm-提供商)
- [4.3.1.2 修改 LLM 提供商](#4312-修改-llm-提供商)
- [4.3.1.3 删除 LLM 提供商](#4313-删除-llm-提供商)
- [4.3.1.4 获取 LLM 提供商列表](#4314-获取-llm-提供商列表)
- [4.3.2 客户用户管理](#432-客户用户管理)
- [4.3.2.1 新增客户用户](#4321-新增客户用户)
- [4.3.2.2 修改客户用户](#4322-修改客户用户)
- [4.3.2.3 删除客户用户](#4323-删除客户用户)
- [4.3.2.4 获取客户用户列表](#4324-获取客户用户列表)
- [4.3.3 生成认证令牌](#433-生成认证令牌)
5. [请求与响应格式](#5-请求与响应格式)
- [5.1 通用请求头](#51-通用请求头)
- [5.2 响应格式](#52-响应格式)
6. [错误代码](#6-错误代码)
7. [安全性考虑](#7-安全性考虑)
8. [示例](#8-示例)
- [8.1 获取访问令牌示例](#81-获取访问令牌示例)
- [8.2 发送提示词请求示例](#82-发送提示词请求示例)
9. [结论](#9-结论)
---
## 1. 简介
本文档详细描述了 LLM API 转发平台的 API 接口规范,旨在指导开发者基于 Laravel、MySQL 和 Redis 技术栈,实现平台的 API 功能。平台提供了基于令牌的 LLM 调用服务,支持多层级管理员管理和 LLM 提供商的动态配置。
---
## 2. 认证与授权
平台采用基于令牌的认证机制,包括长期有效的**认证令牌Auth Token**和短期有效的**访问令牌Access Token**。
### 2.1 认证令牌Auth Token
- **用途**:客户用户使用认证令牌获取访问令牌。
- **特点**
- 由管理员生成并分发给客户用户。
- 长期有效,或由管理员设定过期时间。
### 2.2 访问令牌Access Token
- **用途**:客户用户使用访问令牌访问受保护的 API 接口。
- **特点**
- 由客户用户使用认证令牌获取。
- 短期有效(如 1 小时),存储在 Redis 中,支持自动过期和动态注销。
---
## 3. API 概览
### 3.1 客户用户 API
- **获取访问令牌**
- `POST /api/auth/token`
- **发送提示词请求**
- `POST /api/llm/request`
### 3.2 管理员 API
- **LLM 提供商管理**
- 新增 LLM 提供商:`POST /api/admin/llm-providers`
- 修改 LLM 提供商:`PUT /api/admin/llm-providers/{id}`
- 删除 LLM 提供商:`DELETE /api/admin/llm-providers/{id}`
- 获取 LLM 提供商列表:`GET /api/admin/llm-providers`
- **客户用户管理**
- 新增客户用户:`POST /api/admin/clients`
- 修改客户用户:`PUT /api/admin/clients/{id}`
- 删除客户用户:`DELETE /api/admin/clients/{id}`
- 获取客户用户列表:`GET /api/admin/clients`
- **生成认证令牌**
- `POST /api/admin/clients/{id}/auth-token`
---
## 4. API 详细说明
### 4.1 认证 API
#### 4.1.1 获取访问令牌
- **URL**`/api/auth/token`
- **方法**`POST`
- **描述**:使用认证令牌获取短期访问令牌。
**请求参数**
- **Header**
- `Content-Type: application/json`
- **Body**
```json
{
"auth_token": "string"
}
```
- `auth_token`(必填):管理员分发的认证令牌。
**响应示例**
- **成功**
```json
{
"access_token": "string",
"expires_in": 3600
}
```
- `access_token`:短期访问令牌。
- `expires_in`:令牌有效期,单位为秒。
- **失败**
```json
{
"error": "invalid_auth_token",
"message": "认证令牌无效。"
}
```
### 4.2 客户用户 API
#### 4.2.1 发送提示词请求
- **URL**`/api/llm/request`
- **方法**`POST`
- **描述**:使用访问令牌向绑定的 LLM 提供商发送提示词请求。
**请求参数**
- **Header**
- `Content-Type: application/json`
- `Authorization: Bearer {access_token}`
- **Body**
```json
{
"prompt": "string"
}
```
- `prompt`(必填):提示词内容。
**响应示例**
- **成功**
```json
{
"response": "LLM 提供商返回的响应内容"
}
```
- **失败**
```json
{
"error": "invalid_access_token",
"message": "访问令牌无效或已过期。"
}
```
### 4.3 管理员 API
> **注意**:管理员 API 需要使用管理员的身份认证(如 JWT Token 或 Session具体认证方式可根据系统需求设计。
#### 4.3.1 LLM 提供商管理
##### 4.3.1.1 新增 LLM 提供商
- **URL**`/api/admin/llm-providers`
- **方法**`POST`
- **描述**:新增一个 LLM 提供商。
**请求参数**
- **Header**
- `Content-Type: application/json`
- **Body**
```json
{
"name": "string",
"service_name": "string",
"api_url": "string",
"api_token": "string"
}
```
- `name`(必填):提供商名称。
- `service_name`(必填):对应的服务逻辑名称。
- `api_url`(必填):提供商的 API 接口地址。
- `api_token`(必填):调用提供商 API 所需的令牌。
**响应示例**
- **成功**
```json
{
"id": 1,
"name": "OpenAI",
"service_name": "OpenAIService",
"api_url": "https://api.openai.com/v1/engines/davinci/completions",
"api_token": "sk-xxxxx",
"created_at": "2023-10-01T12:00:00Z"
}
```
- **失败**
```json
{
"error": "validation_error",
"message": "提供商名称已存在。"
}
```
##### 4.3.1.2 修改 LLM 提供商
- **URL**`/api/admin/llm-providers/{id}`
- **方法**`PUT`
- **描述**:修改指定的 LLM 提供商信息。
**请求参数**
- **路径参数**
- `id`必填LLM 提供商的 ID。
- **Header**
- `Content-Type: application/json`
- **Body**
```json
{
"name": "string",
"service_name": "string",
"api_url": "string",
"api_token": "string"
}
```
- 参数同新增接口,可选填。
**响应示例**
- **成功**
```json
{
"id": 1,
"name": "OpenAI",
"service_name": "OpenAIService",
"api_url": "https://api.openai.com/v1/engines/davinci/completions",
"api_token": "sk-xxxxx",
"updated_at": "2023-10-02T12:00:00Z"
}
```
- **失败**
```json
{
"error": "not_found",
"message": "LLM 提供商不存在。"
}
```
##### 4.3.1.3 删除 LLM 提供商
- **URL**`/api/admin/llm-providers/{id}`
- **方法**`DELETE`
- **描述**:删除指定的 LLM 提供商。
**请求参数**
- **路径参数**
- `id`必填LLM 提供商的 ID。
**响应示例**
- **成功**
```json
{
"message": "LLM 提供商已删除。"
}
```
- **失败**
```json
{
"error": "not_found",
"message": "LLM 提供商不存在。"
}
```
##### 4.3.1.4 获取 LLM 提供商列表
- **URL**`/api/admin/llm-providers`
- **方法**`GET`
- **描述**:获取所有 LLM 提供商的列表。
**响应示例**
- **成功**
```json
[
{
"id": 1,
"name": "OpenAI",
"service_name": "OpenAIService",
"api_url": "https://api.openai.com/v1/engines/davinci/completions",
"created_at": "2023-10-01T12:00:00Z"
},
{
"id": 2,
"name": "Anthropic",
"service_name": "AnthropicService",
"api_url": "https://api.anthropic.com/v1/complete",
"created_at": "2023-10-01T13:00:00Z"
}
]
```
#### 4.3.2 客户用户管理
##### 4.3.2.1 新增客户用户
- **URL**`/api/admin/clients`
- **方法**`POST`
- **描述**:新增一个客户用户,并绑定 LLM 提供商。
**请求参数**
- **Header**
- `Content-Type: application/json`
- **Body**
```json
{
"name": "string",
"llm_provider_id": 1
}
```
- `name`(必填):客户用户名称。
- `llm_provider_id`(必填):绑定的 LLM 提供商 ID。
**响应示例**
- **成功**
```json
{
"id": 1001,
"name": "Client A",
"llm_provider_id": 1,
"created_at": "2023-10-01T14:00:00Z"
}
```
- **失败**
```json
{
"error": "validation_error",
"message": "客户用户名称已存在。"
}
```
##### 4.3.2.2 修改客户用户
- **URL**`/api/admin/clients/{id}`
- **方法**`PUT`
- **描述**:修改指定的客户用户信息。
**请求参数**
- **路径参数**
- `id`(必填):客户用户的 ID。
- **Header**
- `Content-Type: application/json`
- **Body**
```json
{
"name": "string",
"llm_provider_id": 2
}
```
- 参数可选填。
**响应示例**
- **成功**
```json
{
"id": 1001,
"name": "Client A Updated",
"llm_provider_id": 2,
"updated_at": "2023-10-02T14:00:00Z"
}
```
- **失败**
```json
{
"error": "not_found",
"message": "客户用户不存在。"
}
```
##### 4.3.2.3 删除客户用户
- **URL**`/api/admin/clients/{id}`
- **方法**`DELETE`
- **描述**:删除指定的客户用户。
**请求参数**
- **路径参数**
- `id`(必填):客户用户的 ID。
**响应示例**
- **成功**
```json
{
"message": "客户用户已删除。"
}
```
- **失败**
```json
{
"error": "not_found",
"message": "客户用户不存在。"
}
```
##### 4.3.2.4 获取客户用户列表
- **URL**`/api/admin/clients`
- **方法**`GET`
- **描述**:获取所有客户用户的列表。
**响应示例**
- **成功**
```json
[
{
"id": 1001,
"name": "Client A",
"llm_provider_id": 1,
"created_at": "2023-10-01T14:00:00Z"
},
{
"id": 1002,
"name": "Client B",
"llm_provider_id": 2,
"created_at": "2023-10-01T15:00:00Z"
}
]
```
#### 4.3.3 生成认证令牌
- **URL**`/api/admin/clients/{id}/auth-token`
- **方法**`POST`
- **描述**:为指定的客户用户生成一个新的认证令牌。
**请求参数**
- **路径参数**
- `id`(必填):客户用户的 ID。
**响应示例**
- **成功**
```json
{
"client_id": 1001,
"auth_token": "abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef9012",
"created_at": "2023-10-01T16:00:00Z"
}
```
- **失败**
```json
{
"error": "not_found",
"message": "客户用户不存在。"
}
```
---
## 5. 请求与响应格式
### 5.1 通用请求头
- `Content-Type: application/json`
- 对于需要认证的接口,使用以下方式传递令牌:
- **客户用户访问令牌**`Authorization: Bearer {access_token}`
- **管理员认证**:根据系统设计,可采用 `Authorization: Bearer {admin_token}`,或使用 Session/Cookie。
### 5.2 响应格式
- 所有响应均为 JSON 格式。
- 成功响应:
- HTTP 状态码为 `200` 或 `201`,响应体为请求的数据。
- 失败响应:
- HTTP 状态码为 `4xx` 或 `5xx`,响应体包含 `error` 和 `message` 字段。
**错误响应示例**
```json
{
"error": "invalid_request",
"message": "请求参数缺失。"
}
```
---
## 6. 错误代码
| 错误代码 | HTTP 状态码 | 描述 |
|------------------------|-------------|------------------------------|
| `invalid_request` | 400 | 请求无效,参数缺失或格式错误。 |
| `unauthorized` | 401 | 未授权,令牌无效或未提供。 |
| `forbidden` | 403 | 禁止访问,没有权限。 |
| `not_found` | 404 | 资源不存在。 |
| `validation_error` | 422 | 请求参数验证失败。 |
| `server_error` | 500 | 服务器内部错误。 |
---
## 7. 安全性考虑
- **HTTPS 加密**:所有 API 请求必须通过 HTTPS 协议,确保数据传输的安全性。
- **令牌安全**
- 认证令牌和访问令牌应保密存储,避免泄露。
- 令牌在传输过程中只能通过 `Authorization` 头部发送,避免在 URL 或请求体中传递。
- **权限控制**
- 后端需要验证令牌的有效性和权限,防止未授权的访问。
- 管理员和客户用户的权限应严格区分,防止越权操作。
- **输入验证**
- 对所有输入参数进行严格的格式和类型验证,防止 SQL 注入和 XSS 攻击。
- **错误信息**
- 返回的错误信息应简洁明了,避免泄露服务器内部信息。
---
## 8. 示例
### 8.1 获取访问令牌示例
**请求**
```http
POST /api/auth/token HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
{
"auth_token": "abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef9012"
}
```
**响应**
```json
{
"access_token": "wxyz1234abcd5678efgh9012ijkl3456mnop7890qrst1234uvwx5678yzab9012",
"expires_in": 3600
}
```
### 8.2 发送提示词请求示例
**请求**
```http
POST /api/llm/request HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
Authorization: Bearer wxyz1234abcd5678efgh9012ijkl3456mnop7890qrst1234uvwx5678yzab9012
{
"prompt": "Hello, how are you?"
}
```
**响应**
```json
{
"response": "I'm doing well, thank you for asking!"
}
```
---
## 9. 结论
本 API 开发文档详细描述了 LLM API 转发平台的各项接口、请求与响应格式、错误代码和安全性考虑。开发者应根据本规范进行接口的开发与测试,确保系统的稳定性和安全性。
---
**注意**:在实际开发中,可能需要根据具体的业务需求和技术选型,进一步完善和调整本 API 文档。
---
# 附录
## 技术栈说明
- **Laravel**PHP 的优秀框架,提供了丰富的功能和高效的开发体验。
- **MySQL**:关系型数据库,存储平台的核心数据。
- **Redis**:高性能的内存缓存数据库,用于存储短期令牌和支持动态注销。
## 开发建议
- **代码规范**:遵循 Laravel 和 PHP 的代码规范,使用 PSR 标准。
- **日志记录**:重要的操作和错误需要记录日志,便于追踪和排查问题。
- **单元测试**:编写单元测试和集成测试,确保接口的稳定性和可靠性。
- **性能优化**:关注接口的性能,使用缓存、异步等方式优化响应速度。
---
**感谢阅读本 API 开发文档,如有任何疑问或建议,请与项目组联系。**

339
doc/llmapiv2.html Normal file
View file

@ -0,0 +1,339 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM API Documentation v2</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
pre {
background-color: #f6f8fa;
border-radius: 6px;
padding: 16px;
overflow: auto;
}
code {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
font-size: 85%;
background-color: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 3px;
}
h1, h2, h3, h4 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
h1 { font-size: 2em; border-bottom: 1px solid #eaecef; }
h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; }
h3 { font-size: 1.25em; }
h4 { font-size: 1em; }
.endpoint {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 6px;
}
.method {
font-weight: bold;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
margin-right: 8px;
}
.get { background-color: #61affe; }
.post { background-color: #49cc90; }
.put { background-color: #fca130; }
.delete { background-color: #f93e3e; }
</style>
</head>
<body>
<h1>LLM API Documentation v2</h1>
<h2>Overview</h2>
<p>This document describes the API endpoints for the LLM service.</p>
<h2>Base URL</h2>
<pre><code>https://llmbackend.local:7890</code></pre>
<h2>Authentication</h2>
<ul>
<li>Admin endpoints require Bearer token authentication obtained from <code>/api/admin/login</code></li>
<li>Client endpoints require access token obtained from <code>/api/auth/token</code></li>
</ul>
<h2>Required Headers</h2>
<p>All API requests must include:</p>
<pre><code>Content-Type: application/json
Accept: application/json
X-API-Version: 1.0
X-Client-ID: your-client-id</code></pre>
<h2>Endpoints</h2>
<h3>Public Routes</h3>
<div class="endpoint">
<h4><span class="method get">GET</span> /</h4>
<p>Root endpoint that returns the API status.</p>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"status": "ok",
"version": "1.0"
}
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method get">GET</span> /api/test</h4>
<p>Simple test endpoint to verify API connectivity.</p>
</div>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/auth/token</h4>
<p>Exchange auth token for an access token.</p>
<h5>Request Body:</h5>
<pre><code>{
"auth_token": "64-character-auth-token"
}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"access_token": "generated-access-token",
"expires_in": 3600
}
}</code></pre>
</div>
<h3>Protected Client Routes</h3>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/llm/request</h4>
<p>Make a request to the LLM service.</p>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {access_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"prompt": "Your prompt text here",
"max_tokens": 100,
"temperature": 0.7,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0
}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"response": "LLM generated response"
}
}</code></pre>
</div>
<h3>Admin Authentication</h3>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/admin/login</h4>
<h5>Request Body:</h5>
<pre><code>{
"email": "your-email",
"password": "your-password"
}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"token": "admin-bearer-token",
"admin": {
"id": 1,
"email": "your-email"
}
}
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/admin/logout</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method put">PUT</span> /api/admin/change-password</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"current_password": "current-password",
"new_password": "new-password",
"new_password_confirmation": "new-password"
}</code></pre>
</div>
<h3>Client Management</h3>
<div class="endpoint">
<h4><span class="method get">GET</span> /api/admin/clients</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"items": [
{
"id": 1,
"name": "Client Name",
"llm_provider_id": 1,
"created_at": "2024-12-05T00:00:00Z"
}
]
}
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/admin/clients</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"name": "New Client Name",
"llm_provider_id": 1
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method get">GET</span> /api/admin/clients/{client_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method put">PUT</span> /api/admin/clients/{client_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"name": "Updated Client Name",
"llm_provider_id": 1
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method delete">DELETE</span> /api/admin/clients/{client_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/admin/clients/{client_id}/auth-token</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"client_id": 1,
"auth_token": "generated-auth-token",
"created_at": "2024-12-05T00:00:00Z"
}
}</code></pre>
</div>
<h3>LLM Provider Management</h3>
<div class="endpoint">
<h4><span class="method get">GET</span> /api/admin/llm-providers</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Response:</h5>
<pre><code>{
"success": true,
"data": {
"items": [
{
"id": 1,
"name": "OpenAI",
"service_name": "openai",
"api_url": "https://api.openai.com/v1",
"status": "active",
"created_at": "2024-12-05T00:00:00Z"
}
]
}
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method post">POST</span> /api/admin/llm-providers</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"name": "OpenAI",
"service_name": "openai",
"api_url": "https://api.openai.com/v1",
"api_token": "your-api-token",
"status": "active"
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method get">GET</span> /api/admin/llm-providers/{provider_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method put">PUT</span> /api/admin/llm-providers/{provider_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
<h5>Request Body:</h5>
<pre><code>{
"name": "Updated OpenAI",
"service_name": "openai",
"api_url": "https://api.openai.com/v1",
"api_token": "your-api-token",
"status": "active"
}</code></pre>
</div>
<div class="endpoint">
<h4><span class="method delete">DELETE</span> /api/admin/llm-providers/{provider_id}</h4>
<h5>Headers:</h5>
<pre><code>Authorization: Bearer {admin_token}</code></pre>
</div>
<h2>Error Responses</h2>
<p>All endpoints return standardized error responses:</p>
<pre><code>{
"success": false,
"error": "error_code",
"message": "Error message",
"errors": {
"field": ["Error details"]
}
}</code></pre>
<h2>Success Responses</h2>
<p>All successful responses follow the format:</p>
<pre><code>{
"success": true,
"data": {
// Response data
}
}</code></pre>
</body>
</html>

View file

@ -0,0 +1,227 @@
# Requirements and Rules Document for LLM API Forwarding Platform
## 1. Introduction
This document outlines the requirements and rules for an LLM (Large Language Model) API forwarding platform. The system enables clients to send prompts to various LLMs and receive responses, managing access through a token-based authentication system. The platform includes separate components for administrators and clients, each with distinct functionalities and access levels.
## 2. System Overview
The platform consists of two main components:
- **Frontend API**: Receives prompts from clients and forwards them to the appropriate LLM based on the client's token.
- **Admin Backend**: Enables administrators to manage client users and other administrators via a separate frontend interface.
The system employs a token-based authentication mechanism, using authentication tokens and access tokens to control access and usage. Client users do not need to log in; they interact with the system using tokens provided by administrators.
## 3. Components
### 3.1 Frontend API
- **Type**: Pure API endpoint (no user interface).
- **Functionality**:
- Receives prompts from clients.
- Determines which LLM to route the prompt to based on the client's token.
- Forwards the prompt to the designated LLM.
- Returns the LLM's response to the client in JSON format.
- **Token Handling**:
- Validates the client's access token before processing the request.
- Access tokens are short-term and obtained using authentication tokens.
### 3.2 Admin Backend
- **Type**: Backend API endpoint with a separate frontend interface for administrators.
- **Functionality**:
- Administrators log in via the frontend interface using a username and password.
- Provides APIs for administrative tasks.
- Administrators can:
- Change their passwords.
- Add new client users.
- Manage authentication tokens for clients.
- Add new administrators (if they have Super Administrator privileges).
- **Client User Management**:
- When adding a new client user, the system:
- Generates one or more authentication tokens for the client.
- Displays the authentication tokens to the administrator.
- Administrators provide these tokens to the client users securely.
- **User Roles**:
- **Super Administrator**: Has full control, including adding other administrators.
- **Administrator**: Can manage client users and their tokens but can only add administrators if they have the necessary privileges.
## 4. User Roles
- **Super Administrator**
- Highest level of access.
- Can add and manage administrators and client users.
- Issues authentication tokens to clients.
- **Administrator**
- Can manage client users.
- Issues authentication tokens.
- Can change their own password.
- **Client User**
- Does not have a username or password.
- Uses tokens provided by administrators to interact with the system.
- Can have multiple authentication tokens.
- Obtains access tokens using their authentication tokens to use the LLM services.
## 5. Authentication and Authorization
### 5.1 Tokens
- **Authentication Tokens**
- Long-term tokens provided by administrators.
- Clients can have multiple authentication tokens.
- Used to obtain short-term access tokens.
- **Access Tokens**
- Short-term tokens (valid for one hour).
- Used to authenticate requests to the Frontend API.
### 5.2 Token Workflow
1. **Issuance**
- Administrators generate authentication tokens for client users.
- Administrators can generate multiple authentication tokens per client for different purposes.
- The system displays the authentication tokens to the administrator upon creation.
- Administrators securely provide these tokens to the client users.
2. **Access Token Retrieval**
- Clients use their authentication tokens to request access tokens.
3. **Service Access**
- Clients use the access tokens to interact with the Frontend API and send prompts to the LLM.
### 5.3 Access Control
- Access to different LLMs is determined by the client's tokens.
- The system ensures that clients can only access LLMs they are authorized to use.
- Authentication tokens can have specific permissions or access levels assigned by administrators.
## 6. Site-Wide Rules
- **Frontend and Backend Separation**
- The system architecture separates frontend interfaces from backend services.
- Communication occurs through APIs.
- **Frontends**
- **Admin Frontend**: For administrators to perform management tasks.
- **No Open Registration**
- New client users are added exclusively by administrators.
- There is no public registration API or interface.
- **User Levels**
- Users are categorized into three levels: Super Administrator, Administrator, and Client User.
- **Token Types**
- Clients use two types of tokens: authentication tokens and access tokens.
- Access tokens are required for using LLM services.
## 7. Additional Requirements and Considerations
### 7.1 Client User Management
- **Multiple Authentication Tokens**
- Clients can have multiple authentication tokens.
- Each authentication token is unique and can be managed independently.
- **Token Management Features**
- **Token List**: Administrators can view and manage all authentication tokens for a client.
- **Token Revocation**: Administrators can deactivate or revoke authentication tokens if necessary.
- **Token Naming**: Administrators can assign names or descriptions to tokens for easier management.
- **Permissions and Access Levels**: Authentication tokens can have different access levels or permissions assigned.
### 7.2 Security
- **Encryption**
- All data transmissions should use HTTPS to secure communications.
- **Token Security**
- Tokens should be securely stored and transmitted.
- Implement measures to prevent token theft and misuse.
- **Administrator Password Policies**
- Enforce strong password requirements for administrators.
- **Vulnerability Protection**
- Protect against common security threats like SQL injection, XSS, CSRF, etc.
- **Token Rotation**
- Encourage regular rotation of authentication tokens for enhanced security.
### 7.3 Error Handling
- Provide clear and descriptive error messages in JSON format.
- Handle token expiration and invalidation gracefully, prompting clients to obtain a new access token if necessary.
### 7.4 Logging and Monitoring
- **Activity Logging**
- Log all authentication attempts, token issuances, user creations, token revocations, and API interactions.
- **Monitoring**
- Implement monitoring tools to track system performance and detect anomalies.
### 7.5 Scalability
- Design the system to handle increasing numbers of clients and requests.
- Consider load balancing and resource optimization strategies.
### 7.6 Rate Limiting
- Implement rate limiting to prevent abuse and ensure fair usage.
- Define rate limits per client or per token as necessary.
### 7.7 Documentation
- Provide comprehensive documentation for administrators and clients.
- Include guidelines on token management, API usage, and best practices.
### 7.8 Internationalization and Localization
- Consider supporting multiple languages for the admin frontend interface.
- Ensure date, time, and numerical formats are adaptable to different locales.
### 7.9 Compliance
- Ensure the system complies with relevant laws and regulations, such as data protection and privacy laws (e.g., GDPR).
- Implement data retention and deletion policies as required.
### 7.10 Backup and Recovery
- Establish regular backup procedures for critical data.
- Develop a disaster recovery plan to restore services in case of system failure.
### 7.11 Future Enhancements
- **Self-Service Features**
- Allow clients to request additional services or features through secure channels.
- **Analytics**
- Provide analytics and reporting tools for administrators to monitor usage patterns.
- **Third-Party Integrations**
- Explore integration with external services or additional LLM providers.
## 8. Assumptions and Constraints
- **No Code or API Specifications Provided**
- This document focuses on functional requirements and rules, not implementation details.
- **Closed System**
- The platform is intended for use by authorized clients and administrators only.
- **Token Validity Periods**
- Authentication tokens have long-term validity, while access tokens expire after one hour.
- **System Availability**
- The platform should aim for high availability, minimizing downtime and maintenance windows.
## 9. Summary of Key Points
- **Clients Use Tokens Exclusively**
- Clients interact with the system using tokens provided by administrators.
- Clients do not have usernames and passwords.
- **Administrator Responsibilities**
- Administrators manage client users and tokens.
- When adding client users, administrators generate and provide authentication tokens.
- **Multiple Authentication Tokens per Client**
- Clients can have multiple authentication tokens for different purposes.
- This allows for greater flexibility and security in managing access.
- **Access Control via Tokens**
- Permissions and access to specific LLMs can be controlled via tokens.
- Tokens can have specific permissions or access levels assigned.
---
This comprehensive requirements document integrates all previous adjustments and considerations, including:
- The removal of client usernames and passwords.
- The use of tokens exclusively for client authentication.
- The ability for clients to have multiple authentication tokens.
- The inclusion of token management features for administrators.
- The display of authentication tokens to administrators when creating client users.
I hope this document meets your needs and accurately reflects all the requirements and adjustments discussed.

239
doc/systemDesign.md Normal file
View file

@ -0,0 +1,239 @@
# LLM API 转发平台系统设计书
---
## 1. 系统概览
### 1.1 系统目标
- **基于令牌的 LLM API 转发平台**:建立一个平台,允许客户用户通过认证和访问令牌,向绑定的 LLM 提供商发送提示词并获取响应。
- **多层级管理员功能**
- **超级管理员**:管理所有管理员、客户用户和 LLM 提供商。
- **管理员**:管理分配给他们的客户用户和对应的 LLM 提供商。
- **LLM 提供商管理功能**:支持管理员通过后台管理不同的 LLM 提供商及其对应的服务处理逻辑。
- **高性能与安全性**:利用 Redis 提升短期令牌的验证性能,支持动态注销,确保系统安全。
### 1.2 技术选型
- **后端框架Laravel**
- 支持服务容器绑定和多种中间件扩展,便于实现动态服务逻辑。
- **数据库MySQL**
- 用于存储管理员、客户用户、令牌、LLM 提供商等数据。
- **缓存系统Redis**
- 存储短期访问令牌,实现高效验证和动态注销。
- **安全措施**
- 强制使用 HTTPS 加密通信。
- 敏感数据如令牌和 LLM 提供商配置加密存储。
---
## 2. 系统架构
### 2.1 核心组件
#### 1. **Frontend API**
- **功能**
- 面向客户用户的 API 服务。
- 根据客户用户的 LLM 配置,路由请求至对应的 LLM 提供商。
- 动态调用不同的响应处理逻辑,适配各 LLM 提供商。
#### 2. **Admin Backend**
- **功能**
- 面向管理员的后台管理模块。
- 支持管理员对客户用户、令牌和 LLM 提供商的管理。
- 提供 LLM 提供商管理功能,允许新增、修改和删除 LLM 提供商。
#### 3. **数据库层MySQL**
- **功能**
- 持久化存储管理员、客户用户、令牌、LLM 提供商等信息。
- 支持复杂的关系管理和查询。
#### 4. **缓存层Redis**
- **功能**
- 存储短期访问令牌,实现高效验证和动态注销。
- 支持令牌的自动过期和主动注销。
---
### 2.2 模块划分
#### 1. **身份验证模块**
- **功能**
- 管理认证令牌和访问令牌的生成、验证与注销。
- 使用 Redis 提升验证性能,支持动态注销。
#### 2. **客户用户管理模块**
- **功能**
- 管理员可以添加、修改、删除客户用户。
- 客户用户绑定特定的 LLM 提供商。
#### 3. **LLM 提供商管理模块**
- **功能**
- 管理员可以管理 LLM 提供商列表。
- 每个提供商需配置名称、服务名称对应的服务逻辑、URL 和 Token。
#### 4. **日志与监控模块**
- **功能**
- 记录系统操作日志,便于审计和故障排查。
- 提供系统性能监控接口,支持集成第三方监控工具。
#### 5. **安全模块**
- **功能**
- 实现 HTTPS 加密通信。
- 防御常见的安全攻击,如 SQL 注入、XSS、CSRF 等。
- 管理令牌的安全性,支持即时注销和过期机制。
---
## 3. 数据流程
### 3.1 客户用户使用流程
1. **认证令牌分发**
- 管理员通过后台为客户用户生成认证令牌,并分发给客户用户。
2. **生成访问令牌**
- 客户用户使用认证令牌,通过 API 获取短期访问令牌。
3. **提示词请求**
- 客户用户使用访问令牌,向 Frontend API 发送提示词请求。
- 系统根据客户用户绑定的 LLM 提供商,调用对应的服务逻辑,发送请求并获取响应。
- 返回处理后的 LLM 响应给客户用户。
---
### 3.2 管理员操作流程
1. **管理员登录**
- 超级管理员和管理员使用用户名和密码登录管理后台。
2. **客户用户管理**
- 超级管理员可以管理所有客户用户。
- 管理员只能管理分配给他们的客户用户。
3. **LLM 提供商管理**
- 管理员可以新增、修改、删除 LLM 提供商。
- 配置提供商的名称、服务名称、URL、Token 等信息。
4. **令牌管理**
- 管理员为客户用户生成和管理认证令牌。
- 可以查看、修改或注销令牌。
---
## 4. 关键技术实现
### 4.1 令牌机制
- **认证令牌**
- 长期有效,由管理员生成,客户用户使用其获取访问令牌。
- **访问令牌**
- 短期有效(如 1 小时),存储在 Redis 中,支持自动过期和动态注销。
### 4.2 权限控制
- **超级管理员**
- 具有全局权限,管理所有资源,包括管理员、客户用户和 LLM 提供商。
- **管理员**
- 管理分配给他们的客户用户和 LLM 提供商。
- 无法访问其他管理员的资源。
### 4.3 LLM 提供商管理
#### 功能描述
- **提供商管理**
- 管理员可通过后台管理 LLM 提供商列表。
- 每个提供商需配置:
- 提供商名称(如 OpenAI、Anthropic 等)。
- 服务名称(对应 Laravel 中的服务类,用于处理逻辑)。
- API URL 和访问 Token。
- **客户用户绑定**
- 客户用户在创建或修改时,需要绑定一个 LLM 提供商。
#### 业务规则
- **权限限制**
- 管理员只能管理自己添加的 LLM 提供商。
- 超级管理员可以管理所有 LLM 提供商。
- **动态调用**
- 系统根据客户用户绑定的 LLM 提供商,动态调用对应的服务逻辑,适配不同的请求和响应格式。
---
## 5. 安全设计
### 5.1 加密通信
- **HTTPS 强制加密**:所有通信均通过 HTTPS确保数据传输安全。
### 5.2 权限隔离
- **数据隔离**
- 管理员和客户用户的资源严格隔离,防止未授权访问。
- **令牌安全**
- 令牌在存储和传输过程中均加密,防止泄露。
### 5.3 令牌注销
- **即时注销**
- 支持管理员主动注销令牌,令牌立即失效。
- **动态验证**
- 使用 Redis 存储令牌状态,支持高效的验证和注销。
---
## 6. 部署架构
### 6.1 服务分层
1. **应用层**
- 部署 Laravel 应用,分别处理 Frontend API 和 Admin Backend。
2. **缓存层**
- 部署 Redis用于存储和管理短期访问令牌。
3. **数据库层**
- 部署 MySQL存储持久化数据。
4. **日志与监控层**
- 部署 ELK 或 Prometheus用于日志收集和系统监控。
### 6.2 部署方式
- **容器化部署**
- 使用 Docker 对各服务进行容器化,便于部署和扩展。
- **负载均衡**
- 使用 Nginx 或其他负载均衡器,实现流量分发和高可用性。
- **高可用性**
- 数据库和 Redis 配置主从或集群模式,支持高并发和容错。
---
## 7. 总结
- **灵活性**
- 系统支持管理员动态管理 LLM 提供商,便于扩展和维护。
- **安全性**
- 采用多重安全措施,确保系统和数据的安全。
- **可扩展性**
- 系统架构设计支持高并发和横向扩展,满足业务增长需求。
如有任何进一步的需求或需要调整的地方,请随时告知!

217
doc/userflow.md Normal file
View file

@ -0,0 +1,217 @@
# LLM API 转发平台使用者操作流程说明书
---
## 目录
1. [简介](#1-简介)
2. [角色与权限](#2-角色与权限)
3. [操作流程概览](#3-操作流程概览)
4. [管理员操作指南](#4-管理员操作指南)
- [4.1 登录后台管理系统](#41-登录后台管理系统)
- [4.2 管理LLM提供商](#42-管理llm提供商)
- [4.2.1 新增LLM提供商](#421-新增llm提供商)
- [4.2.2 修改LLM提供商](#422-修改llm提供商)
- [4.2.3 删除LLM提供商](#423-删除llm提供商)
- [4.3 管理客户用户](#43-管理客户用户)
- [4.3.1 新增客户用户](#431-新增客户用户)
- [4.3.2 修改客户用户](#432-修改客户用户)
- [4.3.3 删除客户用户](#433-删除客户用户)
- [4.4 生成和管理认证令牌](#44-生成和管理认证令牌)
5. [客户用户操作指南](#5-客户用户操作指南)
- [5.1 获取访问令牌](#51-获取访问令牌)
- [5.2 发送提示词请求](#52-发送提示词请求)
6. [常见问题与解答](#6-常见问题与解答)
7. [技术支持](#7-技术支持)
---
## 1. 简介
本说明书旨在指导**管理员**和**客户用户**如何使用LLM API转发平台。平台提供了基于令牌的LLM大型语言模型调用服务支持多层级管理员管理和LLM提供商的动态配置。
---
## 2. 角色与权限
- **超级管理员**
- 拥有系统最高权限。
- 可以管理所有管理员、客户用户和LLM提供商。
- **管理员**
- 可以管理分配给他们的客户用户和LLM提供商。
- 无法访问其他管理员的资源。
- **客户用户**
- 通过认证令牌和访问令牌与系统交互。
- 只能使用绑定的LLM提供商进行提示词请求。
---
## 3. 操作流程概览
1. **管理员登录后台管理系统**
2. **管理员管理LLM提供商**
- 新增、修改或删除LLM提供商。
3. **管理员管理客户用户**
- 新增客户用户并绑定LLM提供商。
- 为客户用户生成认证令牌。
4. **客户用户使用认证令牌获取访问令牌**
5. **客户用户使用访问令牌发送提示词请求**
---
## 4. 管理员操作指南
### 4.1 登录后台管理系统
1. **打开浏览器**访问后台管理系统URL。
2. 在登录页面输入**用户名**和**密码**。
3. 点击“**登录**”按钮进入后台管理界面。
### 4.2 管理LLM提供商
管理员可以通过后台管理界面管理LLM提供商。
#### 4.2.1 新增LLM提供商
1. 在左侧导航栏选择“**LLM提供商管理**”。
2. 点击“**新增提供商**”按钮。
3. 填写提供商信息:
- **提供商名称**如OpenAI、Anthropic等。
- **服务名称**:对应后端服务逻辑的名称。
- **API URL**提供商的API接口地址。
- **访问Token**调用提供商API所需的令牌。
4. 确认信息无误后,点击“**保存**”按钮。
#### 4.2.2 修改LLM提供商
1. 在“**LLM提供商管理**”列表中,找到需要修改的提供商。
2. 点击对应行的“**编辑**”按钮。
3. 修改需要更新的提供商信息。
4. 点击“**保存**”按钮。
#### 4.2.3 删除LLM提供商
1. 在“**LLM提供商管理**”列表中,找到需要删除的提供商。
2. 点击对应行的“**删除**”按钮。
3. 系统会弹出确认提示,点击“**确认**”进行删除。
> **注意**删除LLM提供商会影响绑定了该提供商的客户用户请谨慎操作。
### 4.3 管理客户用户
#### 4.3.1 新增客户用户
1. 在左侧导航栏选择“**客户用户管理**”。
2. 点击“**新增客户用户**”按钮。
3. 填写客户用户信息:
- **用户名称**:客户用户的名称或标识。
- **绑定LLM提供商**从下拉列表中选择一个LLM提供商。
4. 点击“**保存**”按钮。
#### 4.3.2 修改客户用户
1. 在“**客户用户管理**”列表中,找到需要修改的客户用户。
2. 点击对应行的“**编辑**”按钮。
3. 修改客户用户的信息或更换绑定的LLM提供商。
4. 点击“**保存**”按钮。
#### 4.3.3 删除客户用户
1. 在“**客户用户管理**”列表中,找到需要删除的客户用户。
2. 点击对应行的“**删除**”按钮。
3. 系统会弹出确认提示,点击“**确认**”进行删除。
### 4.4 生成和管理认证令牌
1. 在“**客户用户管理**”列表中,找到需要生成令牌的客户用户。
2. 点击对应行的“**生成认证令牌**”按钮。
3. 系统将生成一个新的认证令牌,并显示在页面上。
4. 将认证令牌安全地发送给客户用户。
> **提示**:认证令牌是客户用户获取访问令牌的凭证,请确保安全传输。
---
## 5. 客户用户操作指南
### 5.1 获取访问令牌
客户用户需要使用认证令牌获取短期有效的访问令牌。
1. **发送请求**
- URL`https://api.yourdomain.com/auth/token`
- 方法:`POST`
- 请求头:
- `Content-Type: application/json`
- 请求体:
```json
{
"auth_token": "您的认证令牌"
}
```
2. **接收响应**
- 成功时,返回:
```json
{
"access_token": "您的访问令牌",
"expires_in": 3600
}
```
- `access_token`短期访问令牌有效期一般为1小时。
- `expires_in`:令牌有效期,单位为秒。
### 5.2 发送提示词请求
使用获取的访问令牌,向平台发送提示词请求。
1. **发送请求**
- URL`https://api.yourdomain.com/llm/request`
- 方法:`POST`
- 请求头:
- `Content-Type: application/json`
- `Authorization: Bearer 您的访问令牌`
- 请求体:
```json
{
"prompt": "您的提示词内容"
}
```
2. **接收响应**
- 成功时返回LLM提供商处理后的响应内容。
- 失败时,返回错误信息,如令牌失效、权限不足等。
> **注意**:请确保在访问令牌有效期内发送请求,过期后需要重新获取访问令牌。
---
## 6. 常见问题与解答
**Q1**:访问令牌过期了怎么办?
- **A**访问令牌有效期一般为1小时过期后需要使用认证令牌重新获取新的访问令牌。
**Q2**:提示“认证令牌无效”如何处理?
- **A**:请确认您使用的认证令牌是否正确,若仍有问题,请联系管理员重新生成认证令牌。
**Q3**无法获取LLM响应或响应异常
- **A**可能是绑定的LLM提供商配置有误或LLM服务暂时不可用请联系管理员检查LLM提供商设置。
**Q4**:如何保证令牌的安全性?
- **A**请妥善保管您的认证令牌和访问令牌避免泄露。令牌仅应在HTTPS加密的环境下传输。
---
## 7. 技术支持
如在使用过程中遇到任何问题,请联系技术支持:
- **邮箱**support@yourdomain.com
- **电话**+86-123-4567-890
- **工作时间**:周一至周五 9:00 - 18:00
---
感谢您使用LLM API转发平台您的满意是我们最大的动力

38
routes/api.php Normal file
View file

@ -0,0 +1,38 @@
<?php
use App\Http\Controllers\Api\Admin\AuthController as AdminAuthController;
use App\Http\Controllers\Api\Admin\ClientController;
use App\Http\Controllers\Api\Admin\LlmProviderController;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\LlmController;
use Illuminate\Support\Facades\Route;
Route::get('/test', function () {
return 'Test Route';
});
// Public routes
Route::post('/auth/token', [AuthController::class, 'getAccessToken']);
// Protected routes (require access token)
Route::middleware('auth.access_token')->group(function () {
Route::post('/llm/request', [LlmController::class, 'request']);
});
// Admin routes
Route::prefix('admin')->group(function () {
// Admin auth routes (public)
Route::post('login', [AdminAuthController::class, 'login']);
// Protected admin routes
Route::middleware(['auth:sanctum', 'auth.admin'])->group(function () {
Route::post('logout', [AdminAuthController::class, 'logout']);
Route::put('change-password', [AdminAuthController::class, 'changePassword']);
// LLM Provider management
Route::apiResource('llm-providers', LlmProviderController::class);
// Client management
Route::apiResource('clients', ClientController::class);
Route::post('clients/{id}/auth-token', [ClientController::class, 'generateAuthToken']);
});
});