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 ? '🤖 【AI小班长】 迈着整齐的步伐进入了房间,随时为您服务!' : '🤖 【AI小班长】 去休息啦,大家聊得开心!'; $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}!"); } }