using DramaLing.Api.Models.Configuration; using Microsoft.Extensions.Options; using System.Text.Json; using System.Text; namespace DramaLing.Api.Services.AI.Gemini; /// /// Gemini API HTTP 客戶端實作 /// public class GeminiClient : IGeminiClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly GeminiOptions _options; public GeminiClient( HttpClient httpClient, IOptions options, ILogger logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options.Value ?? throw new ArgumentNullException(nameof(options)); _logger.LogInformation("GeminiClient initialized with model: {Model}, timeout: {Timeout}s", _options.Model, _options.TimeoutSeconds); _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); } public async Task CallGeminiAPIAsync(string prompt) { try { var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = _options.Temperature, topK = 40, topP = 0.95, maxOutputTokens = _options.MaxOutputTokens } }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync( $"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", content); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); _logger.LogInformation("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length))); return ExtractTextFromResponse(responseJson); } catch (Exception ex) { _logger.LogError(ex, "Gemini API call failed"); throw; } } public async Task TestConnectionAsync() { try { await CallGeminiAPIAsync("Test connection"); return true; } catch { return false; } } private string ExtractTextFromResponse(string responseJson) { using var document = JsonDocument.Parse(responseJson); var root = document.RootElement; string aiText = string.Empty; if (root.TryGetProperty("candidates", out var candidatesElement) && candidatesElement.ValueKind == JsonValueKind.Array) { _logger.LogInformation("Found candidates array with {Count} items", candidatesElement.GetArrayLength()); var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault(); if (firstCandidate.ValueKind != JsonValueKind.Undefined) { if (firstCandidate.TryGetProperty("content", out var contentElement)) { if (contentElement.TryGetProperty("parts", out var partsElement) && partsElement.ValueKind == JsonValueKind.Array) { var firstPart = partsElement.EnumerateArray().FirstOrDefault(); if (firstPart.TryGetProperty("text", out var textElement)) { aiText = textElement.GetString() ?? string.Empty; _logger.LogInformation("Successfully extracted text: {Length} characters", aiText.Length); } } } } } // 檢查是否有安全過濾 if (root.TryGetProperty("promptFeedback", out var feedbackElement)) { _logger.LogWarning("Gemini prompt feedback received: {Feedback}", feedbackElement.ToString()); } _logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length); if (!string.IsNullOrEmpty(aiText)) { _logger.LogInformation("AI text preview: {Preview}", aiText.Substring(0, Math.Min(200, aiText.Length))); } // 如果沒有獲取到文本且有安全過濾回饋,返回友好訊息 if (string.IsNullOrWhiteSpace(aiText) && root.TryGetProperty("promptFeedback", out _)) { return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content."; } return aiText; } }