This commit is contained in:
Jethro Lin 2024-12-04 13:48:22 +08:00
parent 383e73c2cd
commit 74790f6180
5 changed files with 284 additions and 132 deletions

View file

@ -33,32 +33,121 @@ public function __construct(
}
/**
* 檢查客戶是否有活躍的令牌
* 獲取客戶列表
*/
private function hasActiveTokens(Client $client): bool
public function index(): JsonResponse
{
$pattern = 'auth_token:*';
$keys = Redis::keys($pattern);
try {
$clients = Client::with('llmProvider:id,name,service_name')
->select([
'id',
'name',
'llm_provider_id',
'rate_limit',
'timeout',
'status',
'created_at',
])
->get();
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 $this->success($clients);
return false;
} 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 deactivate(int $id): JsonResponse
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',
],
'rate_limit' => [
'required',
'integer',
'min:1',
'max:1000',
],
'timeout' => [
'required',
'integer',
'min:1',
'max:300',
],
]);
$validated['status'] = Client::STATUS_ACTIVE;
$client = Client::create($validated);
// 加載 LLM 提供商信息
$client->load('llmProvider:id,name,service_name');
$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,
'llm_provider' => [
'id' => $client->llmProvider->id,
'name' => $client->llmProvider->name,
'service_name' => $client->llmProvider->service_name,
],
'rate_limit' => $client->rate_limit,
'timeout' => $client->timeout,
'status' => $client->status,
'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);
@ -70,32 +159,91 @@ public function deactivate(int $id): JsonResponse
);
}
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
"unique:clients,name,{$id}",
],
'llm_provider_id' => [
'required',
'integer',
'exists:llm_providers,id',
],
'rate_limit' => [
'required',
'integer',
'min:1',
'max:1000',
],
'timeout' => [
'required',
'integer',
'min:1',
'max:300',
],
'status' => [
'required',
'string',
'in:active,inactive',
],
]);
// 如果要停用客戶,檢查是否有活躍的令牌
if ($validated['status'] === Client::STATUS_INACTIVE &&
$client->status === Client::STATUS_ACTIVE) {
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法停用。'
);
}
}
$client->update(['status' => Client::STATUS_INACTIVE]);
$client->update($validated);
// 加載 LLM 提供商信息
$client->load('llmProvider:id,name,service_name');
$this->logService->logOperation(
'admin',
$this->admin->id,
"Deactivated client: {$client->name}"
"Updated client: {$client->name}"
);
return $this->success();
return $this->success([
'id' => $client->id,
'name' => $client->name,
'llm_provider_id' => $client->llm_provider_id,
'llm_provider' => [
'id' => $client->llmProvider->id,
'name' => $client->llmProvider->name,
'service_name' => $client->llmProvider->service_name,
],
'rate_limit' => $client->rate_limit,
'timeout' => $client->timeout,
'status' => $client->status,
'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 deactivating client', [
Log::error('Error updating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'request_data' => $request->all(),
]);
return $this->error(
@ -135,7 +283,7 @@ public function destroy(int $id): JsonResponse
"Deleted client: {$client->name}"
);
return $this->success();
return $this->success(null, '客戶已刪除。');
} catch (ModelNotFoundException $e) {
return $this->error(
@ -146,6 +294,7 @@ public function destroy(int $id): JsonResponse
Log::error('Error deleting client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return $this->error(
@ -177,7 +326,7 @@ public function generateAuthToken(int $id): JsonResponse
);
}
$token = $this->tokenService->generateAuthToken($client);
$result = $this->tokenService->generateAuthToken($client);
$this->logService->logOperation(
'admin',
@ -185,15 +334,7 @@ public function generateAuthToken(int $id): JsonResponse
"Generated auth token for client: {$client->name}"
);
return $this->success([
'auth_token' => $token['token'],
'expires_at' => $token['expires_at'],
'client' => [
'id' => $client->id,
'name' => $client->name,
'status' => $client->status,
],
]);
return $this->success($result);
} catch (ModelNotFoundException $e) {
return $this->error(
@ -204,6 +345,7 @@ public function generateAuthToken(int $id): JsonResponse
Log::error('Error generating auth token', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return $this->error(
@ -212,4 +354,27 @@ public function generateAuthToken(int $id): JsonResponse
);
}
}
/**
* 檢查客戶是否有活躍的令牌
*/
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

@ -6,6 +6,7 @@
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use App\Models\LlmProvider;
use App\Services\LogService;
use App\Traits\ApiResponse;
@ -19,48 +20,31 @@ class LlmProviderController extends Controller
{
use ApiResponse;
private Admin $admin;
public function __construct(
private readonly LogService $logService,
) {}
Request $request
) {
$this->admin = $request->admin;
}
/**
* 获取 LLM 提供商列表
*
* @param Request $request
* @return JsonResponse
* 獲取 LLM 提供商列表
*/
public function index(Request $request): JsonResponse
public function index(): JsonResponse
{
try {
$perPage = $request->input('per_page', 15);
$perPage = min(max($perPage, 1), 100);
$search = $request->input('search');
$status = $request->input('status');
$providers = LlmProvider::select([
'id',
'name',
'service_name',
'api_url',
'status',
'created_at',
])->get();
$query = LlmProvider::query()->orderBy('created_at', 'desc');
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('service_name', 'like', "%{$search}%");
});
}
if ($status && in_array($status, ['active', 'inactive'])) {
$query->where('status', $status);
}
$providers = $query->paginate($perPage);
return $this->paginate(
$providers->items(),
[
'current_page' => $providers->currentPage(),
'per_page' => $providers->perPage(),
'total' => $providers->total(),
'total_pages' => $providers->lastPage(),
]
);
return $this->success($providers);
} catch (\Exception $e) {
Log::error('Error fetching LLM providers', [
@ -76,10 +60,7 @@ public function index(Request $request): JsonResponse
}
/**
* 创建 LLM 提供商
*
* @param Request $request
* @return JsonResponse
* 新增 LLM 提供商
*/
public function store(Request $request): JsonResponse
{
@ -90,38 +71,42 @@ public function store(Request $request): JsonResponse
'string',
'max:100',
'unique:llm_providers',
'regex:/^[\w\-\s]+$/u',
],
'service_name' => [
'required',
'string',
'max:100',
'regex:/^[\w\-]+$/u',
],
'api_url' => [
'required',
'string',
'max:255',
'url',
'starts_with:https',
'max:255',
],
'api_token' => [
'required',
'string',
'max:255',
],
'api_token' => 'required|string|max:255',
], [
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。',
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
]);
$validated['status'] = 'active';
$validated['status'] = LlmProvider::STATUS_ACTIVE;
$provider = LlmProvider::create($validated);
$this->logService->logOperation(
'admin',
$request->admin->id,
$this->admin->id,
"Created LLM provider: {$provider->name}"
);
return $this->created($provider);
return $this->created([
'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 $this->error(
@ -144,57 +129,57 @@ public function store(Request $request): JsonResponse
}
/**
* 更新 LLM 提供商
*
* @param Request $request
* @param int $id
* @return JsonResponse
* 修改 LLM 提供商
*/
public function update(Request $request, int $id): JsonResponse
{
try {
$provider = LlmProvider::findOrFail($id);
if (!$this->admin->canManageLlmProvider($provider->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該 LLM 提供商。'
);
}
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
"unique:llm_providers,name,{$id}",
'regex:/^[\w\-\s]+$/u',
],
'service_name' => [
'required',
'string',
'max:100',
'regex:/^[\w\-]+$/u',
],
'api_url' => [
'required',
'string',
'max:255',
'url',
'starts_with:https',
'max:255',
],
'api_token' => [
'required',
'string',
'max:255',
],
'api_token' => 'required|string|max:255',
'status' => [
'required',
'string',
'in:active,inactive',
],
], [
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。',
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
'status.in' => '状态只能是 active 或 inactive。',
]);
// 如果状态改为 inactive检查是否有客户正在使用
if ($validated['status'] === 'inactive' && $provider->status === 'active') {
// 如果要停用提供商,檢查是否有客戶在使用
if ($validated['status'] === LlmProvider::STATUS_INACTIVE &&
$provider->status === LlmProvider::STATUS_ACTIVE) {
if ($provider->clients()->exists()) {
return $this->error(
ErrorCode::RESOURCE_IN_USE,
'该提供商正在被客户使用,无法停用。'
'該提供商正在被客戶使用,無法停用。'
);
}
}
@ -203,11 +188,18 @@ public function update(Request $request, int $id): JsonResponse
$this->logService->logOperation(
'admin',
$request->admin->id,
$this->admin->id,
"Updated LLM provider: {$provider->name}"
);
return $this->success($provider);
return $this->success([
'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 $this->error(
@ -236,22 +228,25 @@ public function update(Request $request, int $id): JsonResponse
}
/**
* 删除 LLM 提供商
*
* @param Request $request
* @param int $id
* @return JsonResponse
* 刪除 LLM 提供商
*/
public function destroy(Request $request, int $id): JsonResponse
public function destroy(int $id): JsonResponse
{
try {
$provider = LlmProvider::findOrFail($id);
// 检查是否有客户正在使用
if (!$this->admin->canManageLlmProvider($provider->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該 LLM 提供商。'
);
}
// 檢查是否有客戶在使用
if ($provider->clients()->exists()) {
return $this->error(
ErrorCode::RESOURCE_IN_USE,
'该提供商正在被客户使用,无法删除。'
'該提供商正在被客戶使用,無法刪除。'
);
}
@ -260,11 +255,11 @@ public function destroy(Request $request, int $id): JsonResponse
$this->logService->logOperation(
'admin',
$request->admin->id,
$this->admin->id,
"Deleted LLM provider: {$providerName}"
);
return $this->success(null, '提供商已删除。');
return $this->success(null, 'LLM 提供商已刪除。');
} catch (ModelNotFoundException $e) {
return $this->error(

View file

@ -49,11 +49,7 @@ public function getAccessToken(Request $request): JsonResponse
$result = $this->tokenService->generateAccessToken($authTokenData);
return $this->success([
'access_token' => $result['access_token'],
'expires_in' => $result['expires_in'],
'token_type' => $result['token_type'],
]);
return $this->success($result);
} catch (ValidationException $e) {
return $this->error(

View file

@ -22,10 +22,7 @@ public function __construct(
) {}
/**
* 发送 LLM 请求
*
* @param Request $request
* @return JsonResponse
* 發送 LLM 請求
*/
public function request(Request $request): JsonResponse
{
@ -47,7 +44,6 @@ public function request(Request $request): JsonResponse
return $this->success([
'response' => $result['response'],
'usage' => $result['usage'],
]);
} catch (ValidationException $e) {

View file

@ -24,7 +24,7 @@ public function __construct(
/**
* 生成認證令牌
*
* @return array{token: string, expires_at: string}
* @return array{auth_token: string, expires_at: string, client_id: int}
*/
public function generateAuthToken(Client $client, ?int $expiresInHours = null): array
{
@ -56,8 +56,9 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null):
);
return [
'token' => $token,
'auth_token' => $token,
'expires_at' => $expiresAt->toIso8601String(),
'client_id' => $client->id,
];
}
@ -91,7 +92,7 @@ public function validateAuthToken(string $token): ?array
/**
* 生成訪問令牌
*
* @return array{access_token: string, expires_in: int, token_type: string}
* @return array{access_token: string, expires_in: int}
*/
public function generateAccessToken(array $authTokenData): array
{
@ -118,7 +119,6 @@ public function generateAccessToken(array $authTokenData): array
return [
'access_token' => $accessToken,
'expires_in' => self::ACCESS_TOKEN_TTL,
'token_type' => 'Bearer',
];
}