using System.Net.Http.Headers; namespace Service.AI; public interface IOpenAiService { Task ChatAsync(string systemPrompt, string userPrompt); Task ChatAsync(string prompt); IAsyncEnumerable ChatStreamAsync(string systemPrompt, string userPrompt); IAsyncEnumerable ChatStreamAsync(string prompt); } public class OpenAiService( IOptions aiSettings, ILogger logger ) : IOpenAiService { public async Task ChatAsync(string systemPrompt, string userPrompt) { var cfg = aiSettings.Value; if (string.IsNullOrWhiteSpace(cfg.Endpoint) || string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); return null; } using var http = new HttpClient(); http.Timeout = TimeSpan.FromSeconds(15); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key); var payload = new { model = cfg.Model, temperature = 0, messages = new object[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } } }; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); try { using var resp = await http.PostAsync(url, content); if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}"); } var respText = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(respText); var root = doc.RootElement; var contentText = root.GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString(); return contentText; } catch (Exception ex) { logger.LogError(ex, "AI 调用失败"); throw; } } public async Task ChatAsync(string prompt) { var cfg = aiSettings.Value; if (string.IsNullOrWhiteSpace(cfg.Endpoint) || string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); return null; } using var http = new HttpClient(); http.Timeout = TimeSpan.FromSeconds(60 * 5); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key); var payload = new { model = cfg.Model, temperature = 0, messages = new object[] { new { role = "user", content = prompt } } }; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); try { using var resp = await http.PostAsync(url, content); if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}"); } var respText = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(respText); var root = doc.RootElement; var contentText = root.GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString(); return contentText; } catch (Exception ex) { logger.LogError(ex, "AI 调用失败"); throw; } } public async IAsyncEnumerable ChatStreamAsync(string prompt) { var cfg = aiSettings.Value; if (string.IsNullOrWhiteSpace(cfg.Endpoint) || string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); yield break; } using var http = new HttpClient(); http.Timeout = TimeSpan.FromMinutes(5); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key); var payload = new { model = cfg.Model, temperature = 0, stream = true, messages = new object[] { new { role = "user", content = prompt } } }; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = content; using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}"); } using var stream = await resp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync()) != null) { if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; var data = line.Substring(6).Trim(); if (data == "[DONE]") break; using var doc = JsonDocument.Parse(data); var root = doc.RootElement; if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) { var delta = choices[0].GetProperty("delta"); if (delta.TryGetProperty("content", out var contentProp)) { var contentText = contentProp.GetString(); if (!string.IsNullOrEmpty(contentText)) { yield return contentText; } } } } } public async IAsyncEnumerable ChatStreamAsync(string systemPrompt, string userPrompt) { var cfg = aiSettings.Value; if (string.IsNullOrWhiteSpace(cfg.Endpoint) || string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); yield break; } using var http = new HttpClient(); http.Timeout = TimeSpan.FromMinutes(5); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key); var payload = new { model = cfg.Model, temperature = 0, stream = true, messages = new object[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } } }; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); // 使用 SendAsync 来支持 HttpCompletionOption using var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = content; using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}"); } using var stream = await resp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync()) != null) { if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; var data = line.Substring(6).Trim(); if (data == "[DONE]") break; // 解析JSON时不使用try-catch,因为在async iterator中不能使用 using var doc = JsonDocument.Parse(data); var root = doc.RootElement; if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) { var delta = choices[0].GetProperty("delta"); if (delta.TryGetProperty("content", out var contentProp)) { var contentText = contentProp.GetString(); if (!string.IsNullOrEmpty(contentText)) { yield return contentText; } } } } } }