From 5875ffe671b7fef19961aa31e71327a0d62df055 Mon Sep 17 00:00:00 2001 From: lq1405 <2769838458@qq.com> Date: Sun, 23 Nov 2025 12:04:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9C=BA=E5=99=A8=E7=A0=81?= =?UTF-8?q?=E7=9A=84=E9=AA=8C=E8=AF=81=EF=BC=8C=E6=8A=BD=E8=B1=A1=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=96=B9=E6=B3=95=20=E6=B7=BB=E5=8A=A0=20OpenAI=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E7=9A=84=20=E6=B5=81=E5=BC=8F=E8=BD=AC?= =?UTF-8?q?=E5=8F=91=E6=8E=A5=E5=8F=A3=20=E4=BF=9D=E6=8C=81=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=9A=84=E6=95=B0=E6=8D=AE=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LMS.service/Controllers/ForwardController.cs | 71 +++++++++++- LMS.service/Service/ForwardWordService.cs | 101 ++++++++++++++++-- LMS.service/Service/MachineService.cs | 17 ++- .../Other/MachineAuthorizationService.cs | 1 + 4 files changed, 178 insertions(+), 12 deletions(-) diff --git a/LMS.service/Controllers/ForwardController.cs b/LMS.service/Controllers/ForwardController.cs index 2c6d42f..c317c90 100644 --- a/LMS.service/Controllers/ForwardController.cs +++ b/LMS.service/Controllers/ForwardController.cs @@ -38,7 +38,7 @@ public class ForwardController(ForwardWordService forwardWordService, ILogger /// 流式转发 /// @@ -83,6 +83,75 @@ public class ForwardController(ForwardWordService forwardWordService, ILogger ForwardStreamStruct([FromBody] ForwardModel req) + { + HttpResponseMessage? upstreamResponse; + try + { + upstreamResponse = await _forwardWordService.ForwardWordStreamRaw(req); + } + catch (Exception e) + { + return BadRequest(e.Message); + } + + // 处理上游响应与转发 + try + { + // A. 上游 OpenAI 报错 (如 401 key 错误, 429 限流, 500 等) + // 我们需要透传这个错误给前端,而不是报 400 + if (!upstreamResponse.IsSuccessStatusCode) + { + Response.StatusCode = (int)upstreamResponse.StatusCode; + + if (upstreamResponse.Content.Headers.ContentType != null) + { + Response.ContentType = upstreamResponse.Content.Headers.ContentType.ToString(); + } + + // 直接读取错误内容并写入 + var errorContent = await upstreamResponse.Content.ReadAsStringAsync(); + await Response.WriteAsync(errorContent); + + // 显式返回空结果,告诉框架处理结束 + return new EmptyResult(); + } + + // B. 上游成功 (200 OK) -> 建立 SSE 管道 + HttpContext.Response.ContentType = "text/event-stream"; + HttpContext.Response.Headers.Add("Cache-Control", "no-cache"); + HttpContext.Response.Headers.Add("Connection", "keep-alive"); + + // 获取上游流 + await using var stream = await upstreamResponse.Content.ReadAsStreamAsync(); + + // 开始流式复制 + // CopyToAsync 会自动处理缓冲,直到上游流结束 + await stream.CopyToAsync(Response.Body); + await Response.Body.FlushAsync(); + } + catch (Exception) + { + if (!Response.HasStarted) + { + return StatusCode(502, "Upstream connection failed."); + } + } + finally + { + upstreamResponse?.Dispose(); + } + + // 兜底返回,确保所有路径都有返回值 + return new EmptyResult(); + } + + #endregion + #region Post 直接转发接口 diff --git a/LMS.service/Service/ForwardWordService.cs b/LMS.service/Service/ForwardWordService.cs index 99d1f56..2bcdcea 100644 --- a/LMS.service/Service/ForwardWordService.cs +++ b/LMS.service/Service/ForwardWordService.cs @@ -1,24 +1,30 @@ -using LMS.DAO; +using Betalgo.Ranul.OpenAI; +using Betalgo.Ranul.OpenAI.Managers; +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using LMS.Common.Extensions; +using LMS.DAO; using LMS.Repository.DB; using LMS.Repository.Forward; +using LMS.Repository.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using static LMS.Common.Enums.ResponseCodeEnum; using System.Net; +using System.Net.Http.Headers; using System.Text; -using LMS.Repository.Model; -using Betalgo.Ranul.OpenAI.Managers; -using Betalgo.Ranul.OpenAI; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using LMS.Common.Extensions; +using static LMS.Common.Enums.ResponseCodeEnum; +using static LMS.service.Controllers.ForwardController; namespace LMS.service.Service; -public class ForwardWordService(ApplicationDbContext context) +public class ForwardWordService(ApplicationDbContext context, IHttpClientFactory httpClientFactory, MachineService machineService) { private readonly ApplicationDbContext _context = context; + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + + private readonly MachineService _machineService = machineService; + #region 非流转发接口,需要系统数据 /// /// 转发OpenAi格式的请求 非流 @@ -228,7 +234,84 @@ public class ForwardWordService(ApplicationDbContext context) } } - #endregion + + #endregion + #region OpenAI 格式流式返回接口(保留接口),需要系统数据 + + public async Task ForwardWordStreamRaw(ForwardModel request) + { + // --- 1. 基础校验 (保持原有逻辑) --- + if (string.IsNullOrEmpty(request.Word)) throw new Exception("参数错误"); + 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", + "https://zhiluoai.net" + }; + + // 简单的校验逻辑优化 + if (!allowedUrls.Any(url => request.GptUrl.StartsWith(url))) + { + throw new Exception("请求的url不合法"); + } + + // 校验机器码 + Machine? machine = await _machineService.GetActiveMachineByMachineId(request.MachineId); + if (machine == null) + { + throw new Exception("机器码不存在或已过期"); + } + + // --- 2. 获取提示词预设 (保持原有逻辑) --- + Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId); + if (prompt == null) + { + throw new Exception("FindPromptStringFail"); // 建议使用具体错误码 + } + + // --- 3. 手动构建 OpenAI 格式的请求体 --- + // SDK 帮你做了这一步,现在我们要自己做,以便获得原始流 + var payload = new + { + model = request.Model, + messages = new List + { + new { role = "system", content = prompt.PromptString }, + new { role = "user", content = request.Word } + }, + stream = true, // 必须开启流式 + // max_tokens = 2000, // 如果有需要可以加其他参数 + // temperature = 0.7 + }; + + var jsonContent = JsonConvert.SerializeObject(payload, new JsonSerializerSettings { ContractResolver = new LowercaseContractResolver() }); + var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + // --- 4. 发起 HTTP 请求 --- + var client = _httpClientFactory.CreateClient(); // 或者直接 new HttpClient(); + + // 拼接完整的 API 地址,通常 OpenAI 兼容接口的路径是 /v1/chat/completions + // 注意处理 request.GptUrl 结尾是否有 / 的情况 + var baseUrl = request.GptUrl.TrimEnd('/'); + var targetUrl = $"{baseUrl}/v1/chat/completions"; + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, targetUrl); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", request.ApiKey); + requestMessage.Content = httpContent; + + // *** 关键点:使用 HttpCompletionOption.ResponseHeadersRead *** + // 这表示一旦读到响应头就返回,不要等待整个 Body 下载完成,这样才能实现流式转发 + var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); + + return response; + } + + #endregion + #region Get直接转发接口 internal async Task>> GetTransfer(GetTransferModel getTransferModel) diff --git a/LMS.service/Service/MachineService.cs b/LMS.service/Service/MachineService.cs index c91ee7d..7924805 100644 --- a/LMS.service/Service/MachineService.cs +++ b/LMS.service/Service/MachineService.cs @@ -216,6 +216,19 @@ namespace LMS.service.Service #endregion + #region 查询当前的机器码状态,返回机器码 + + public async Task GetActiveMachineByMachineId(string machineId) + { + Machine? machine = await _context.Machine.FirstOrDefaultAsync( + x => x.MachineId == machineId + && x.Status == MachineStatus.Active + && (x.DeactivationTime == null || x.DeactivationTime > BeijingTimeExtension.GetBeijingTime())); + return machine; + } + + #endregion + #region 获取机器码状态 /// @@ -224,12 +237,12 @@ namespace LMS.service.Service /// /// /// - internal async Task>> GetMachineStatus(string machineId) + public async Task>> GetMachineStatus(string machineId) { try { // 获取对应的machine - Machine? machine = await _context.Machine.FirstOrDefaultAsync(x => x.MachineId == machineId && x.Status == MachineStatus.Active && x.DeactivationTime > BeijingTimeExtension.GetBeijingTime()); + Machine? machine = await GetActiveMachineByMachineId(machineId); if (machine == null) { return APIResponseModel.CreateErrorResponseModel(ResponseCode.MachineNotFound); diff --git a/LMS.service/Service/Other/MachineAuthorizationService.cs b/LMS.service/Service/Other/MachineAuthorizationService.cs index 3a796b0..35b0c02 100644 --- a/LMS.service/Service/Other/MachineAuthorizationService.cs +++ b/LMS.service/Service/Other/MachineAuthorizationService.cs @@ -412,6 +412,7 @@ namespace LMS.service.Service.Other #endregion + #region 验证对应的程序和机器码是不是有效 /// /// 验证对应的程序和机器码是不是有效