Files
nl-admin-api/app/Service/common/ai/AiRuntimeConfigService.php

287 lines
10 KiB
PHP
Raw Normal View History

2026-08-10 15:51:00 +08:00
<?php
namespace App\Service\common\ai;
use App\BaseApp\BaseService;
use App\Models\ai\AiApiKeyModel;
use App\Models\ai\AiModelModel;
use App\Models\ai\AiPlatformModel;
use App\Service\common\FieldEncryptService;
use App\Service\SystemConfigService;
/**
* AI 运行时配置:从系统配置 + 平台/模型/密钥表解析当前要用的供应商与明文密钥
* 优先数据库配置;未配置时回落 envconfig/ai.php
*/
class AiRuntimeConfigService extends BaseService
{
public const CFG_PROVIDER = 'ai_active_provider';
public const CFG_API_KEY_ID = 'ai_active_api_key_id';
public const CFG_MODEL = 'ai_active_model';
public function __construct()
{
// 工厂解析不依赖登录态
$this->isAuth = false;
parent::__construct();
}
/**
* 解析指定供应商的调用配置
*
* @return array{provider:string,api_url:string,api_key:string,model:string,timeout:float,platform_id:int,api_key_id:int}
*/
public function resolve(?string $providerOverride = null): array
{
$sys = SystemConfigService::getInstance();
$provider = strtolower(trim((string) (
$providerOverride
?: $sys->getValue(self::CFG_PROVIDER, '')
?: config('ai.default_provider', 'deepseek')
)));
$modelOverride = trim((string) $sys->getValue(self::CFG_MODEL, ''));
$keyId = (int) $sys->getValue(self::CFG_API_KEY_ID, '0');
$platform = AiPlatformModel::where('code', $provider)
->where('deleted_at', 0)
->where('status', 1)
->first();
$apiUrl = '';
$model = '';
$apiKey = '';
$platformId = 0;
$timeout = 60.0;
if ($platform) {
$platformId = (int) $platform->id;
$apiUrl = trim((string) $platform->api_url);
// 模型:系统配置覆盖 → 模型表默认 → 模型表首个启用
if ($modelOverride !== '') {
$model = $modelOverride;
} else {
$defaultModel = AiModelModel::where('platform_id', $platformId)
->where('deleted_at', 0)
->where('status', 1)
->where('is_default', 1)
->value('code');
if ($defaultModel) {
$model = trim((string) $defaultModel);
} else {
$firstModel = AiModelModel::where('platform_id', $platformId)
->where('deleted_at', 0)
->where('status', 1)
->orderByDesc('sort')
->orderBy('id')
->value('code');
$model = trim((string) ($firstModel ?: ''));
}
}
// 指定密钥优先;否则默认密钥;再否则取该平台启用中的第一条
$keyQuery = AiApiKeyModel::where('platform_id', $platformId)
->where('deleted_at', 0)
->where('status', 1);
if ($keyId > 0) {
$keyRow = (clone $keyQuery)->where('id', $keyId)->first();
} else {
$keyRow = null;
}
if (!$keyRow) {
$keyRow = (clone $keyQuery)->where('is_default', 1)->first()
?: $keyQuery->orderByDesc('sort')->orderBy('id')->first();
}
if ($keyRow) {
$apiKey = $this->decryptApiKey((string) $keyRow->api_key);
$keyId = (int) $keyRow->id;
}
}
// 回落 env保证未录入密钥时仍可用旧配置
$envCfg = config('ai.' . $provider, []);
if ($apiUrl === '') {
$apiUrl = (string) ($envCfg['api_url'] ?? '');
}
if ($model === '') {
$model = (string) ($envCfg['model'] ?? '');
}
if ($apiKey === '') {
if ($provider === 'spark') {
$apiKey = (string) ($envCfg['password'] ?? '');
} else {
$apiKey = (string) ($envCfg['api_key'] ?? '');
}
}
$timeout = (float) ($envCfg['timeout'] ?? 60);
return [
'provider' => $provider,
'api_url' => $apiUrl,
'api_key' => $apiKey,
'model' => $model,
'timeout' => $timeout,
'platform_id' => $platformId,
'api_key_id' => $keyId,
];
}
/**
* 库内 AES 解密 api_key非加密串原样返回兼容过渡
* 委托 FieldEncryptService nl_ai_api_key 密文格式一致
*/
public function decryptApiKey(string $cipher): string
{
return FieldEncryptService::getInstance()->decryptFromStorage($cipher, true);
}
/**
* 明文入库前 AES 加密
* 委托 FieldEncryptService写出 nl_ase_256_ 前缀密文
*/
public function encryptApiKey(string $plain): string
{
return FieldEncryptService::getInstance()->encryptForStorage($plain);
}
/**
* 系统配置页:启用中的平台卡片 + 模型 + 密钥选项
* 为什么组装成卡片结构:前端 C 端风格按平台展示,减少二次请求
*/
public function optionsForConfig(): array
{
$platforms = AiPlatformModel::where('deleted_at', 0)
->where('status', 1)
->orderByDesc('sort')
->orderBy('id')
->get()
->toArray();
$models = AiModelModel::where('deleted_at', 0)
->where('status', 1)
->orderByDesc('is_default')
->orderByDesc('sort')
->orderBy('id')
->get(['id', 'platform_id', 'code', 'name', 'description', 'is_default'])
->toArray();
$keys = AiApiKeyModel::where('deleted_at', 0)
->where('status', 1)
->orderByDesc('is_default')
->orderByDesc('sort')
->orderBy('id')
->get(['id', 'platform_id', 'name', 'is_default'])
->toArray();
$modelMap = [];
foreach ($models as $m) {
$pid = (int) $m['platform_id'];
$modelMap[$pid][] = [
'id' => (int) $m['id'],
'label' => $m['name'] . '' . $m['code'] . '',
'value' => (string) $m['code'],
'code' => (string) $m['code'],
'name' => (string) $m['name'],
'description' => (string) ($m['description'] ?? ''),
'is_default' => (int) ($m['is_default'] ?? 0),
];
}
$keyMap = [];
foreach ($keys as $k) {
$pid = (int) $k['platform_id'];
$name = trim((string) ($k['name'] ?? ''));
if ($name === '') {
$name = '密钥#' . (int) $k['id'];
}
$keyMap[$pid][] = [
'id' => (int) $k['id'],
'key_name' => $name,
'name' => $name,
'label' => $name,
'value' => (int) $k['id'],
'is_default' => (int) ($k['is_default'] ?? 0),
];
}
$cards = [];
foreach ($platforms as $p) {
$id = (int) $p['id'];
$platModels = $modelMap[$id] ?? [];
$defaultModel = '';
foreach ($platModels as $pm) {
if ((int) $pm['is_default'] === 1) {
$defaultModel = $pm['code'];
break;
}
}
if ($defaultModel === '' && !empty($platModels[0]['code'])) {
$defaultModel = (string) $platModels[0]['code'];
}
$cards[] = [
'id' => $id,
'code' => $p['code'],
'name' => $p['name'],
'logo' => $p['logo'],
'description' => $p['description'],
'default_model' => $defaultModel,
'api_url' => $p['api_url'],
'models' => $platModels,
'keys' => $keyMap[$id] ?? [],
];
}
$sys = SystemConfigService::getInstance();
return [
'platforms' => $cards,
'active' => [
'provider' => (string) $sys->getValue(self::CFG_PROVIDER, config('ai.default_provider', 'deepseek')),
'api_key_id' => (int) $sys->getValue(self::CFG_API_KEY_ID, '0'),
'model' => (string) $sys->getValue(self::CFG_MODEL, ''),
],
];
}
/**
* 保存默认运行时组合(平台 / 模型 / 密钥)
* 为什么校验平台存在:避免写入无效编码导致代码生成 AI 调用失败
*/
public function saveActiveConfig(string $provider, string $model, int $apiKeyId): array
{
$provider = strtolower(trim($provider));
$model = trim($model);
if ($provider === '') {
$this->utils->errorThrow('请选择 AI 平台');
}
$platform = AiPlatformModel::where('code', $provider)
->where('deleted_at', 0)
->where('status', 1)
->first();
if (!$platform) {
$this->utils->errorThrow('AI 平台不存在或已禁用');
}
if ($model === '') {
$this->utils->errorThrow('请选择模型');
}
$modelExists = AiModelModel::where('platform_id', (int) $platform->id)
->where('code', $model)
->where('deleted_at', 0)
->where('status', 1)
->exists();
if (!$modelExists) {
$this->utils->errorThrow('模型不属于当前平台或已禁用');
}
if ($apiKeyId > 0) {
$keyExists = AiApiKeyModel::where('id', $apiKeyId)
->where('platform_id', (int) $platform->id)
->where('deleted_at', 0)
->where('status', 1)
->exists();
if (!$keyExists) {
$this->utils->errorThrow('密钥不属于当前平台或已禁用');
}
}
$sys = SystemConfigService::getInstance();
$sys->setValue(self::CFG_PROVIDER, $provider, '当前启用的 AI 平台编码');
$sys->setValue(self::CFG_MODEL, $model, '当前启用的模型编码');
$sys->setValue(self::CFG_API_KEY_ID, (string) $apiKeyId, '当前启用的 API 密钥 ID');
return [
'provider' => $provider,
'model' => $model,
'api_key_id' => $apiKeyId,
];
}
}