398 lines
13 KiB
PHP
398 lines
13 KiB
PHP
<?php
|
||
|
||
/**
|
||
* 文件功能:AI 厂商配置管理控制器(管理后台)
|
||
*
|
||
* 提供完整的 AI 厂商 CRUD 管理功能,包括:
|
||
* - 列表/新增/编辑/删除 AI 厂商配置
|
||
* - 切换启用/禁用状态
|
||
* - 设置默认厂商(互斥,同时只有一个默认)
|
||
* - 全局开关控制机器人是否启用
|
||
*
|
||
* @author ChatRoom Laravel
|
||
*
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\AiProviderConfig;
|
||
use App\Models\Sysparam;
|
||
use App\Services\ChatStateService;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Crypt;
|
||
use Illuminate\View\View;
|
||
|
||
class AiProviderController extends Controller
|
||
{
|
||
/**
|
||
* 构造函数
|
||
*/
|
||
public function __construct(
|
||
private readonly ChatStateService $chatState,
|
||
) {}
|
||
|
||
/**
|
||
* 显示 AI 厂商配置列表页面
|
||
*
|
||
* 列出所有已配置的 AI 厂商,并显示全局开关状态。
|
||
*
|
||
* @return View AI 厂商管理页面
|
||
*/
|
||
public function index(): View
|
||
{
|
||
$providers = AiProviderConfig::orderBy('sort_order')->get();
|
||
$chatbotEnabled = Sysparam::getValue('chatbot_enabled', '0') === '1';
|
||
$chatbotMaxGold = Sysparam::getValue('chatbot_max_gold', '5000');
|
||
$chatbotMaxDailyRewards = Sysparam::getValue('chatbot_max_daily_rewards', '1');
|
||
|
||
return view('admin.ai-providers.index', compact('providers', 'chatbotEnabled', 'chatbotMaxGold', 'chatbotMaxDailyRewards'));
|
||
}
|
||
|
||
/**
|
||
* 保存全局设置
|
||
*/
|
||
public function updateSettings(Request $request): RedirectResponse
|
||
{
|
||
$data = $request->validate([
|
||
'chatbot_max_gold' => 'required|integer|min:1',
|
||
'chatbot_max_daily_rewards' => 'required|integer|min:1',
|
||
]);
|
||
Sysparam::updateOrCreate(
|
||
['alias' => 'chatbot_max_gold'],
|
||
[
|
||
'body' => (string) $data['chatbot_max_gold'],
|
||
'guidetxt' => '单次最高发放金币金额',
|
||
]
|
||
);
|
||
Sysparam::clearCache('chatbot_max_gold');
|
||
|
||
Sysparam::updateOrCreate(
|
||
['alias' => 'chatbot_max_daily_rewards'],
|
||
[
|
||
'body' => (string) $data['chatbot_max_daily_rewards'],
|
||
'guidetxt' => '每个用户单日最多获得金币次数',
|
||
]
|
||
);
|
||
Sysparam::clearCache('chatbot_max_daily_rewards');
|
||
|
||
return back()->with('success', '全局设置保存成功!');
|
||
}
|
||
|
||
/**
|
||
* 新增 AI 厂商配置
|
||
*
|
||
* @param Request $request 请求对象
|
||
* @return RedirectResponse 重定向到列表页
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$data = $request->validate([
|
||
'provider' => 'required|string|max:50',
|
||
'name' => 'required|string|max:100',
|
||
'api_key' => 'required|string',
|
||
'api_endpoint' => 'required|url|max:255',
|
||
'model' => 'required|string|max:100',
|
||
'temperature' => 'nullable|numeric|min:0|max:2',
|
||
'max_tokens' => 'nullable|integer|min:100|max:32000',
|
||
'sort_order' => 'nullable|integer|min:0',
|
||
]);
|
||
|
||
// 加密 API Key
|
||
$data['api_key'] = Crypt::encryptString($data['api_key']);
|
||
$data['temperature'] = $data['temperature'] ?? 0.3;
|
||
$data['max_tokens'] = $data['max_tokens'] ?? 2048;
|
||
$data['sort_order'] = $data['sort_order'] ?? 0;
|
||
$data['is_enabled'] = true;
|
||
$data['is_default'] = false;
|
||
|
||
AiProviderConfig::create($data);
|
||
|
||
return redirect()->route('admin.ai-providers.index')
|
||
->with('success', '已成功添加 AI 厂商配置!');
|
||
}
|
||
|
||
/**
|
||
* 更新 AI 厂商配置
|
||
*
|
||
* @param Request $request 请求对象
|
||
* @param int $id 厂商配置 ID
|
||
* @return RedirectResponse 重定向到列表页
|
||
*/
|
||
public function update(Request $request, int $id): RedirectResponse
|
||
{
|
||
$provider = AiProviderConfig::findOrFail($id);
|
||
|
||
$data = $request->validate([
|
||
'provider' => 'required|string|max:50',
|
||
'name' => 'required|string|max:100',
|
||
'api_key' => 'nullable|string',
|
||
'api_endpoint' => 'required|url|max:255',
|
||
'model' => 'required|string|max:100',
|
||
'temperature' => 'nullable|numeric|min:0|max:2',
|
||
'max_tokens' => 'nullable|integer|min:100|max:32000',
|
||
'sort_order' => 'nullable|integer|min:0',
|
||
]);
|
||
|
||
// 只在用户提供了新 API Key 时才更新(空值表示不修改)
|
||
if (! empty($data['api_key'])) {
|
||
$data['api_key'] = Crypt::encryptString($data['api_key']);
|
||
} else {
|
||
unset($data['api_key']);
|
||
}
|
||
|
||
$provider->update($data);
|
||
|
||
return redirect()->route('admin.ai-providers.index')
|
||
->with('success', "已更新 {$provider->name} 的配置!");
|
||
}
|
||
|
||
/**
|
||
* 切换 AI 厂商的启用/禁用状态
|
||
*
|
||
* @param int $id 厂商配置 ID
|
||
* @return JsonResponse 操作结果
|
||
*/
|
||
public function toggleEnabled(int $id): JsonResponse
|
||
{
|
||
$provider = AiProviderConfig::findOrFail($id);
|
||
$provider->is_enabled = ! $provider->is_enabled;
|
||
$provider->save();
|
||
|
||
$status = $provider->is_enabled ? '启用' : '禁用';
|
||
|
||
return response()->json([
|
||
'status' => 'success',
|
||
'message' => "{$provider->name} 已{$status}",
|
||
'is_enabled' => $provider->is_enabled,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 设置指定厂商为默认使用
|
||
*
|
||
* 互斥操作:将其他厂商的 is_default 全部置为 false。
|
||
*
|
||
* @param int $id 厂商配置 ID
|
||
* @return JsonResponse 操作结果
|
||
*/
|
||
public function setDefault(int $id): JsonResponse
|
||
{
|
||
$provider = AiProviderConfig::findOrFail($id);
|
||
|
||
// 先将所有厂商的默认标记清除
|
||
AiProviderConfig::where('is_default', true)->update(['is_default' => false]);
|
||
|
||
// 设置当前厂商为默认
|
||
$provider->is_default = true;
|
||
$provider->is_enabled = true; // 默认的必须是启用状态
|
||
$provider->save();
|
||
|
||
return response()->json([
|
||
'status' => 'success',
|
||
'message' => "{$provider->name} 已设为默认 AI 厂商",
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 切换聊天机器人全局开关
|
||
*
|
||
* 通过 sysparam 的 chatbot_enabled 参数控制是否在聊天室中显示 AI 机器人。
|
||
*
|
||
* @param Request $request 请求对象
|
||
* @return JsonResponse 操作结果
|
||
*/
|
||
public function toggleChatBot(Request $request): JsonResponse
|
||
{
|
||
$current = Sysparam::getValue('chatbot_enabled', '0');
|
||
$newValue = $current === '1' ? '0' : '1';
|
||
|
||
// 更新 sysparam
|
||
Sysparam::updateOrCreate(
|
||
['alias' => 'chatbot_enabled'],
|
||
[
|
||
'body' => $newValue,
|
||
'guidetxt' => 'AI聊天机器人开关(1=开启,0=关闭)',
|
||
]
|
||
);
|
||
|
||
// 刷新缓存
|
||
$this->chatState->setSysParam('chatbot_enabled', $newValue);
|
||
Sysparam::clearCache('chatbot_enabled');
|
||
|
||
$status = $newValue === '1' ? '开启' : '关闭';
|
||
$isEnabled = $newValue === '1';
|
||
|
||
// 确保 AI 实体账号存在
|
||
$user = \App\Models\User::firstOrCreate(
|
||
['username' => 'AI小班长'],
|
||
[
|
||
'password' => \Illuminate\Support\Facades\Hash::make(\Illuminate\Support\Str::random(16)),
|
||
'user_level' => 10,
|
||
'sex' => 0, // 女性
|
||
'usersf' => 'storage/avatars/ai_bot_cn_girl.png',
|
||
'jjb' => 1000000,
|
||
'sign' => '本群首席智慧小管家',
|
||
]
|
||
);
|
||
|
||
// 防止后期头像变动,强制更新到最新女生头像
|
||
if (! str_contains($user->usersf ?? '', 'ai_bot_cn_girl.png')) {
|
||
$user->update([
|
||
'usersf' => 'storage/avatars/ai_bot_cn_girl.png',
|
||
'sex' => 0,
|
||
]);
|
||
}
|
||
|
||
$userData = [
|
||
'user_id' => $user->id,
|
||
'username' => $user->username,
|
||
'level' => $user->user_level,
|
||
'sex' => $user->sex,
|
||
'headface' => $user->headface,
|
||
'vip_icon' => $user->vipIcon(),
|
||
'vip_name' => $user->vipName(),
|
||
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
|
||
'is_admin' => false,
|
||
'position_icon' => '',
|
||
'position_name' => '',
|
||
];
|
||
|
||
// 广播机器人进出事件(供前端名单增删)
|
||
broadcast(new \App\Events\ChatBotToggled($userData, $isEnabled));
|
||
|
||
// 像真实的玩家一样,对全网活跃房间进行高调进出场播报
|
||
$activeRoomIds = $this->chatState->getAllActiveRoomIds();
|
||
if (empty($activeRoomIds)) {
|
||
$activeRoomIds = [1]; // 兜底
|
||
}
|
||
|
||
// 把 AI 实体挂名到一个主房间,即可被 app/Console/Commands/AutoSaveExp.php 扫描发经验
|
||
$mainRoomId = $activeRoomIds[0];
|
||
if ($isEnabled) {
|
||
$this->chatState->userJoin($mainRoomId, $user->username, $userData);
|
||
} else {
|
||
// 清理可能存在的所有房间的残留挂名
|
||
foreach ($activeRoomIds as $rId) {
|
||
$this->chatState->userLeave($rId, $user->username);
|
||
}
|
||
}
|
||
|
||
foreach ($activeRoomIds as $roomId) {
|
||
$content = $isEnabled
|
||
? '<span style="color: #9333ea; font-weight: bold;">🤖 【AI小班长】 迈着整齐的步伐进入了房间,随时为您服务!</span>'
|
||
: '<span style="color: #9ca3af; font-weight: bold;">🤖 【AI小班长】 去休息啦,大家聊得开心!</span>';
|
||
|
||
$botMsg = [
|
||
'id' => $this->chatState->nextMessageId($roomId),
|
||
'room_id' => $roomId,
|
||
'from_user' => '进出播报',
|
||
'to_user' => '大家',
|
||
'content' => $content,
|
||
'is_secret' => false,
|
||
'font_color' => '#9333ea',
|
||
'action' => 'system_welcome',
|
||
'welcome_user' => $user->username,
|
||
'sent_at' => now()->toDateTimeString(),
|
||
];
|
||
|
||
$this->chatState->pushMessage($roomId, $botMsg);
|
||
broadcast(new \App\Events\MessageSent($roomId, $botMsg));
|
||
\App\Jobs\SaveMessageJob::dispatch($botMsg);
|
||
}
|
||
|
||
return response()->json([
|
||
'status' => 'success',
|
||
'message' => "聊天机器人已{$status}",
|
||
'enabled' => $isEnabled,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 测试指定 AI 厂商的接口连通性
|
||
*
|
||
* 通过 GET /v1/models 检查端点可达性与 API Key 有效性,毫秒级响应,
|
||
* 不触发模型推理,避免经 Cloudflare 代理时因推理耗时导致 524 超时。
|
||
*
|
||
* @param int $id 厂商配置 ID
|
||
* @return JsonResponse 测试结果(含可用模型列表)
|
||
*/
|
||
public function testConnection(int $id): JsonResponse
|
||
{
|
||
$provider = AiProviderConfig::findOrFail($id);
|
||
|
||
$apiKey = $provider->getDecryptedApiKey();
|
||
$base = rtrim($provider->api_endpoint, '/');
|
||
|
||
// 拼接 /v1/models 端点(检查连通性,不触发推理)
|
||
$modelsUrl = str_ends_with($base, '/v1')
|
||
? $base.'/models'
|
||
: $base.'/v1/models';
|
||
|
||
$startTime = microtime(true);
|
||
|
||
try {
|
||
$response = \Illuminate\Support\Facades\Http::withToken($apiKey)
|
||
->timeout(10)
|
||
->get($modelsUrl);
|
||
|
||
$ms = (int) ((microtime(true) - $startTime) * 1000);
|
||
|
||
if (! $response->successful()) {
|
||
return response()->json([
|
||
'ok' => false,
|
||
'message' => "HTTP {$response->status()}:{$response->body()}",
|
||
'ms' => $ms,
|
||
]);
|
||
}
|
||
|
||
$data = $response->json();
|
||
|
||
// 提取可用模型列表(兼容 Ollama 和 OpenAI 格式)
|
||
$models = collect($data['models'] ?? $data['data'] ?? [])
|
||
->pluck('id')
|
||
->filter()
|
||
->values()
|
||
->toArray();
|
||
|
||
$modelList = count($models) > 0
|
||
? implode('、', array_slice($models, 0, 5)).(count($models) > 5 ? ' 等' : '')
|
||
: $provider->model;
|
||
|
||
return response()->json([
|
||
'ok' => true,
|
||
'message' => "接口连通正常,可用模型:{$modelList}",
|
||
'ms' => $ms,
|
||
'models' => $models,
|
||
]);
|
||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||
$ms = (int) ((microtime(true) - $startTime) * 1000);
|
||
|
||
return response()->json([
|
||
'ok' => false,
|
||
'message' => '连接失败:'.$e->getMessage(),
|
||
'ms' => $ms,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除 AI 厂商配置
|
||
*
|
||
* @param int $id 厂商配置 ID
|
||
* @return RedirectResponse 重定向到列表页
|
||
*/
|
||
public function destroy(int $id): RedirectResponse
|
||
{
|
||
$provider = AiProviderConfig::findOrFail($id);
|
||
$name = $provider->name;
|
||
$provider->delete();
|
||
|
||
return redirect()->route('admin.ai-providers.index')
|
||
->with('success', "已删除 {$name}!");
|
||
}
|
||
}
|