get(); $chatbotEnabled = Sysparam::getValue('chatbot_enabled', '0') === '1'; return view('admin.ai-providers.index', compact('providers', 'chatbotEnabled')); } /** * 新增 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' ? '开启' : '关闭'; return response()->json([ 'status' => 'success', 'message' => "聊天机器人已{$status}", 'enabled' => $newValue === '1', ]); } /** * 测试指定 AI 厂商的接口连通性 * * 发送一条简短的测试消息,返回响应结果和耗时,用于验证配置是否正确。 * * @param int $id 厂商配置 ID * @return JsonResponse 测试结果 */ public function testConnection(int $id): JsonResponse { $provider = AiProviderConfig::findOrFail($id); $apiKey = $provider->getDecryptedApiKey(); $base = rtrim($provider->api_endpoint, '/'); $endpoint = str_ends_with($base, '/v1') ? $base.'/chat/completions' : $base.'/v1/chat/completions'; $startTime = microtime(true); try { $response = \Illuminate\Support\Facades\Http::withToken($apiKey) ->timeout(60) // Ollama 本地模型冷启动较慢,给 60s ->post($endpoint, [ 'model' => $provider->model, 'messages' => [ ['role' => 'user', 'content' => '请用一句话介绍你自己。'], ], 'max_tokens' => 64, ]); $ms = (int) ((microtime(true) - $startTime) * 1000); $data = $response->json(); if (! $response->successful()) { return response()->json([ 'ok' => false, 'message' => "HTTP {$response->status()}:{$response->body()}", 'ms' => $ms, ]); } $reply = $data['choices'][0]['message']['content'] ?? '(无回复内容)'; return response()->json([ 'ok' => true, 'message' => trim($reply), 'ms' => $ms, 'model' => $data['model'] ?? $provider->model, ]); } 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}!"); } }