287 lines
10 KiB
PHP
287 lines
10 KiB
PHP
<?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 运行时配置:从系统配置 + 平台/模型/密钥表解析当前要用的供应商与明文密钥
|
||
* 优先数据库配置;未配置时回落 env(config/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,
|
||
];
|
||
}
|
||
}
|