From 8019b24b61c985c36602eba937e12f4c757ef4e3 Mon Sep 17 00:00:00 2001 From: lq1405 <2769838458@qq.com> Date: Sun, 23 Nov 2025 18:17:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9OpenAI=E5=8E=9F=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E8=BD=AC=E5=8F=91=E9=80=BB=E8=BE=91=E5=92=8C?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LMS.Repository/Forward/ForwardModel.cs | 6 ++ LMS.service/Controllers/ForwardController.cs | 59 +++++++++-------- LMS.service/Service/ForwardWordService.cs | 68 ++++++++++++++------ 3 files changed, 89 insertions(+), 44 deletions(-) diff --git a/LMS.Repository/Forward/ForwardModel.cs b/LMS.Repository/Forward/ForwardModel.cs index 17a225f..631405f 100644 --- a/LMS.Repository/Forward/ForwardModel.cs +++ b/LMS.Repository/Forward/ForwardModel.cs @@ -46,3 +46,9 @@ public class ForwardModel [Required] public string Word { get; set; } = string.Empty; } + +public class ForwardModelOpenAI : ForwardModel +{ + [Required] + public string OpenAIBodyString { get; set; } = string.Empty; +} diff --git a/LMS.service/Controllers/ForwardController.cs b/LMS.service/Controllers/ForwardController.cs index c317c90..f05c528 100644 --- a/LMS.service/Controllers/ForwardController.cs +++ b/LMS.service/Controllers/ForwardController.cs @@ -87,11 +87,12 @@ public class ForwardController(ForwardWordService forwardWordService, ILogger ForwardStreamStruct([FromBody] ForwardModel req) + public async Task ForwardStreamStruct([FromBody] ForwardModelOpenAI req) { - HttpResponseMessage? upstreamResponse; + HttpResponseMessage? upstreamResponse = null; try { + // 1. Service 层请求 (保持不变,这里已经是 HeadersRead 模式了) upstreamResponse = await _forwardWordService.ForwardWordStreamRaw(req); } catch (Exception e) @@ -99,54 +100,60 @@ public class ForwardController(ForwardWordService forwardWordService, ILogger 建立 SSE 管道 - HttpContext.Response.ContentType = "text/event-stream"; - HttpContext.Response.Headers.Add("Cache-Control", "no-cache"); - HttpContext.Response.Headers.Add("Connection", "keep-alive"); + // 3. 成功连接,设置 SSE 响应头 + Response.ContentType = "text/event-stream"; + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + // 禁用缓冲 (对于某些服务器环境很重要) + // var responseFeature = HttpContext.Features.Get(); + // responseFeature?.DisableBuffering(); - // 获取上游流 - await using var stream = await upstreamResponse.Content.ReadAsStreamAsync(); + // 4. 【核心修改】手动流式转发循环 + await using var upstreamStream = await upstreamResponse.Content.ReadAsStreamAsync(); - // 开始流式复制 - // CopyToAsync 会自动处理缓冲,直到上游流结束 - await stream.CopyToAsync(Response.Body); - await Response.Body.FlushAsync(); + // 定义一个较小的缓冲区,比如 1024 甚至更小,其实 buffer 大小不影响实时性,因为 ReadAsync 会在收到任何数据时立即返回 + var buffer = new byte[4096]; + int bytesRead; + + // 使用 HttpContext.RequestAborted,这样前端断开时后端也会停止读取 + while ((bytesRead = await upstreamStream.ReadAsync(buffer, HttpContext.RequestAborted)) != 0) + { + // 收到多少发多少 + await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), HttpContext.RequestAborted); + + // 【关键】立刻刷新缓冲区,将数据强制推送到网络 + await Response.Body.FlushAsync(HttpContext.RequestAborted); + } + } + catch (OperationCanceledException) + { + // 客户端(前端)主动断开连接,这是正常现象,不做处理 } catch (Exception) { - if (!Response.HasStarted) - { - return StatusCode(502, "Upstream connection failed."); - } + // 网络异常处理 + if (!Response.HasStarted) return StatusCode(502); } finally { upstreamResponse?.Dispose(); } - // 兜底返回,确保所有路径都有返回值 return new EmptyResult(); } diff --git a/LMS.service/Service/ForwardWordService.cs b/LMS.service/Service/ForwardWordService.cs index 2bcdcea..f8a20d4 100644 --- a/LMS.service/Service/ForwardWordService.cs +++ b/LMS.service/Service/ForwardWordService.cs @@ -9,6 +9,7 @@ using LMS.Repository.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System.Net; using System.Net.Http.Headers; using System.Text; @@ -238,15 +239,16 @@ public class ForwardWordService(ApplicationDbContext context, IHttpClientFactory #endregion #region OpenAI 格式流式返回接口(保留接口),需要系统数据 - public async Task ForwardWordStreamRaw(ForwardModel request) + public async Task ForwardWordStreamRaw(ForwardModelOpenAI request) { // --- 1. 基础校验 (保持原有逻辑) --- - if (string.IsNullOrEmpty(request.Word)) throw new Exception("参数错误"); + if (string.IsNullOrWhiteSpace(request.OpenAIBodyString)) + { + throw new Exception("OpenAIBodyString 不能为空"); + } if (string.IsNullOrEmpty(request.GptUrl)) throw new Exception("请求的url为空"); var allowedUrls = new[] { - "https://ark.cn-beijing.volces.com", - "https://api.moonshot.cn", "https://laitool.net", "https://api.laitool.cc", "https://laitool.cc", @@ -266,6 +268,24 @@ public class ForwardWordService(ApplicationDbContext context, IHttpClientFactory throw new Exception("机器码不存在或已过期"); } + // ================= 2. JSON 结构解析与校验 ================= + JObject jsonBody; + try + { + jsonBody = JObject.Parse(request.OpenAIBodyString); + } + catch (JsonReaderException) + { + throw new Exception("OpenAIBodyString 不是有效的 JSON 格式"); + } + + // 检查 messages 是否存在且为数组 + var messages = jsonBody["messages"] as JArray; + if (messages == null || messages.Count == 0) + { + throw new Exception("请求体结构错误: 缺少 'messages' 数组"); + } + // --- 2. 获取提示词预设 (保持原有逻辑) --- Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId); if (prompt == null) @@ -273,23 +293,35 @@ public class ForwardWordService(ApplicationDbContext context, IHttpClientFactory throw new Exception("FindPromptStringFail"); // 建议使用具体错误码 } - // --- 3. 手动构建 OpenAI 格式的请求体 --- - // SDK 帮你做了这一步,现在我们要自己做,以便获得原始流 - var payload = new + // ================= 4. 替换逻辑 (System) ================= + // 遍历寻找 role 为 system 的消息 + var systemMsg = messages.FirstOrDefault(m => m["role"]?.ToString() == "system"); + if (systemMsg != null) { - model = request.Model, - messages = new List + // 没有数据 默认为 "{{SYSTEM}}" + var content = systemMsg["content"]?.ToString() ?? "{{SYSTEM}}"; + // 检查是否包含占位符 "{{SYSTEM}}" + if (content.Contains("{{SYSTEM}}")) + { + // 替换占位符为真实的 promptString + systemMsg["content"] = content.Replace("{{SYSTEM}}", prompt.PromptString); + } + } + else { - new { role = "system", content = prompt.PromptString }, - new { role = "user", content = request.Word } - }, - stream = true, // 必须开启流式 - // max_tokens = 2000, // 如果有需要可以加其他参数 - // temperature = 0.7 - }; + throw new Exception("缺少 system 消息节点"); + } - var jsonContent = JsonConvert.SerializeObject(payload, new JsonSerializerSettings { ContractResolver = new LowercaseContractResolver() }); - var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + // ================= 5. 强制修正关键字段 ================= + // 强制使用 C# 模型类中指定的 Model,或者校验两者是否一致 (这里选择覆盖,以 C# 参数为准) + jsonBody["model"] = request.Model; + + // 强制开启流式 + jsonBody["stream"] = true; + var finalJsonContent = jsonBody.ToString(Formatting.None); + + //var jsonContent = JsonConvert.SerializeObject(finalJsonContent, new JsonSerializerSettings { ContractResolver = new LowercaseContractResolver() }); + var httpContent = new StringContent(finalJsonContent, Encoding.UTF8, "application/json"); // --- 4. 发起 HTTP 请求 --- var client = _httpClientFactory.CreateClient(); // 或者直接 new HttpClient();