overrides['base_url'] ?? blog_setting('llm_base_url', config('services.llm.base_url', env('LLM_BASE_URL')))), '/'); } public function apiKey(): string { return (string) ($this->overrides['api_key'] ?? blog_setting('llm_api_key', config('services.llm.api_key', env('LLM_API_KEY')))); } public function model(): string { return (string) ($this->overrides['model'] ?? blog_setting('llm_model', config('services.llm.model', env('LLM_MODEL', 'gpt-4o-mini')))); } public function isConfigured(): bool { return $this->baseUrl() !== '' && $this->apiKey() !== ''; } /** * 已配置的额外 AI 服务商列表(后台 AI 设置维护)。 * * @return list */ public static function profiles(): array { $profiles = json_decode((string) blog_setting('llm_profiles', '[]'), true); return is_array($profiles) ? $profiles : []; } /** * 切换到指定名称的服务商配置;名称为空或不存在时返回默认配置。 */ public function forProfile(?string $name): static { if (! $name) { return $this; } foreach (self::profiles() as $profile) { if (($profile['name'] ?? '') === $name) { $clone = clone $this; $clone->overrides = array_filter([ 'base_url' => $profile['base_url'] ?? null, 'api_key' => $profile['api_key'] ?? null, 'model' => $profile['model'] ?? null, ], fn ($value) => filled($value)); return $clone; } } return $this; } /** * 简单的聊天补全。 * * @param array $messages */ public function chat(array $messages, array $options = []): string { if (! $this->isConfigured()) { throw new \RuntimeException('LLM 未配置:请设置 LLM_BASE_URL 与 LLM_API_KEY'); } $response = Http::timeout(60) ->withToken($this->apiKey()) ->post($this->baseUrl().'/chat/completions', [ 'model' => $options['model'] ?? $this->model(), 'messages' => $messages, 'temperature' => $options['temperature'] ?? 0.3, 'max_tokens' => $options['max_tokens'] ?? 1024, ]); if ($response->failed()) { Log::error('LLM 请求失败', ['status' => $response->status(), 'body' => $response->body()]); throw new \RuntimeException('LLM 请求失败:HTTP '.$response->status()); } $content = $response->json('choices.0.message.content', ''); return is_array($content) ? json_encode($content, JSON_UNESCAPED_UNICODE) : (string) $content; } }