修改OpenAI原格式的转发逻辑和参数
This commit is contained in:
parent
5875ffe671
commit
8019b24b61
@ -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;
|
||||
}
|
||||
|
||||
@ -87,11 +87,12 @@ public class ForwardController(ForwardWordService forwardWordService, ILogger<Fo
|
||||
|
||||
[HttpPost]
|
||||
[Route("/lms/Forward/forward-stream-struct")]
|
||||
public async Task<IActionResult> ForwardStreamStruct([FromBody] ForwardModel req)
|
||||
public async Task<IActionResult> 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<Fo
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
|
||||
// 处理上游响应与转发
|
||||
try
|
||||
{
|
||||
// A. 上游 OpenAI 报错 (如 401 key 错误, 429 限流, 500 等)
|
||||
// 我们需要透传这个错误给前端,而不是报 400
|
||||
// 2. 处理错误情况
|
||||
if (!upstreamResponse.IsSuccessStatusCode)
|
||||
{
|
||||
Response.StatusCode = (int)upstreamResponse.StatusCode;
|
||||
|
||||
// 复制 Content-Type,防止前端解析乱码
|
||||
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");
|
||||
// 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<IHttpResponseBodyFeature>();
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
@ -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<HttpResponseMessage> ForwardWordStreamRaw(ForwardModel request)
|
||||
public async Task<HttpResponseMessage> 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<object>
|
||||
// 没有数据 默认为 "{{SYSTEM}}"
|
||||
var content = systemMsg["content"]?.ToString() ?? "{{SYSTEM}}";
|
||||
// 检查是否包含占位符 "{{SYSTEM}}"
|
||||
if (content.Contains("{{SYSTEM}}"))
|
||||
{
|
||||
new { role = "system", content = prompt.PromptString },
|
||||
new { role = "user", content = request.Word }
|
||||
},
|
||||
stream = true, // 必须开启流式
|
||||
// max_tokens = 2000, // 如果有需要可以加其他参数
|
||||
// temperature = 0.7
|
||||
};
|
||||
// 替换占位符为真实的 promptString
|
||||
systemMsg["content"] = content.Replace("{{SYSTEM}}", prompt.PromptString);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user