Files
nl-admin-api/app/Service/common/ai/AiRuntimeConfigService.php
2026-08-10 15:51:00 +08:00

287 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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,
];
}
}