Files
LMS.service/LMS.service/Service/ForwardWordService.cs
T

399 lines
16 KiB
C#

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 Newtonsoft.Json.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using static LMS.Common.Enums.ResponseCodeEnum;
using static LMS.service.Controllers.ForwardController;
namespace LMS.service.Service;
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 非流转发接口,需要系统数据
/// <summary>
/// 转发OpenAi格式的请求 非流
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<ActionResult<APIResponseModel<object>>> ForwardWord(ForwardModel request)
{
try
{
// 要校验机器码,但是目前不需要
if (request.Word == null || request.Word == "")
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ParameterError);
}
// 获取提示词预设
Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId);
if (prompt == null)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.FindPromptStringFail);
}
// 开始拼接请求体
using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + request.ApiKey);
string json = JsonConvert.SerializeObject(new
{
model = request.Model,
temperature = 0.3,
messages = new List<OpenAI.RequestMessage>
{
new OpenAI.RequestMessage
{
role = "system",
content = prompt.PromptString
},
new OpenAI.RequestMessage
{
role = "user",
content = request.Word
}
}
});
// 判断请求的url是不是满足条件
if (string.IsNullOrEmpty(request.GptUrl))
{
throw new Exception("请求的url为空");
}
if (!request.GptUrl.StartsWith("https://ark.cn-beijing.volces.com") && !request.GptUrl.StartsWith("https://api.moonshot.cn") && !request.GptUrl.StartsWith("https://laitool.net") && !request.GptUrl.StartsWith("https://api.laitool.cc") && !request.GptUrl.StartsWith("https://laitool.cc"))
{
throw new Exception("请求的url不合法");
}
client.Timeout = Timeout.InfiniteTimeSpan;
var response = await client.PostAsync(request.GptUrl, new StringContent(json, Encoding.UTF8, "application/json"));
// 判断返回的状态码
if (response.StatusCode != HttpStatusCode.OK)
{
// 读取响应体
string responseContent = await response.Content.ReadAsStringAsync();
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ForwardWordFail, responseContent, "请求失败");
}
var content = await response.Content.ReadAsStringAsync();
// 序列化一下
return APIResponseModel<object>.CreateSuccessResponseModel(content);
}
catch (Exception e)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
#region 流式转发接口,需要系统数据
/// <summary>
/// 流式转发
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async IAsyncEnumerable<string> ForwardWordStream(ForwardModel request)
{
// 要校验机器码,但是目前不需要
if (request.Word == null || request.Word == "")
{
throw new Exception("参数错误");
}
// 判断请求的url是不是满足条件
if (string.IsNullOrEmpty(request.GptUrl))
{
throw new Exception("请求的url为空");
}
if (!request.GptUrl.StartsWith("https://ark.cn-beijing.volces.com") && !request.GptUrl.StartsWith("https://api.moonshot.cn") && !request.GptUrl.StartsWith("https://laitool.net") && !request.GptUrl.StartsWith("https://api.laitool.cc") && !request.GptUrl.StartsWith("https://laitool.cc"))
{
throw new Exception("请求的url不合法");
}
// 获取提示词预设
Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId);
if (prompt == null)
{
throw new Exception(ResponseCode.FindPromptStringFail.GetResult());
}
var openAiService = new OpenAIService(new OpenAIOptions()
{
ApiKey = request.ApiKey,
BaseDomain = request.GptUrl,
});
var completionResult = openAiService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest
{
Messages = new List<ChatMessage>
{
ChatMessage.FromSystem(prompt.PromptString),
ChatMessage.FromUser(request.Word)
},
Model = request.Model,
Stream = true
});
await foreach (var completion in completionResult)
{
if (completion.Successful)
{ // 这边只返回数据,不返回全部的数据结构了
yield return completion.Choices.First().Message.Content ?? "";
}
else
{
if (completion.Error == null)
{
throw new Exception("Unknown Error");
}
throw new Exception($"{completion.Error.Code}: {completion.Error.Message}");
}
}
}
#endregion
#region Post 转发接口
/// <summary>
/// 简单的转发接口
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<ActionResult<APIResponseModel<object>>> SimpleTransfer(SimpleTransferModel request)
{
try
{
// 开始拼接请求体
using HttpClient client = new();
if (request.url.StartsWith("https://api.laitool.net"))
{
//client.DefaultRequestHeaders.Add("Authorization", "Bearer " + request.APIKey);
client.DefaultRequestHeaders.Add("mj-api-secret", request.APIKey);
}
else
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + request.APIKey);
}
// 判断请求的url是不是满足条件
if (string.IsNullOrEmpty(request.url))
{
throw new Exception("请求的url为空");
}
if (!request.url.StartsWith("https://ark.cn-beijing.volces.com")
&& !request.url.StartsWith("https://api.moonshot.cn")
&& !request.url.StartsWith("https://laitool.net")
&& !request.url.StartsWith("https://api.laitool.net")
&& !request.url.StartsWith("https://api.laitool.cc")
&& !request.url.StartsWith("https://laitool.cc"))
{
throw new Exception("请求的url不支持转发");
}
client.Timeout = Timeout.InfiniteTimeSpan;
var response = await client.PostAsync(request.url, new StringContent(request.dataString, Encoding.UTF8, "application/json"));
// 判断返回的状态码
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ForwardWordFail, "responseCode: 401", "请求失败");
}
// 读取响应体
string responseContent = await response.Content.ReadAsStringAsync();
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ForwardWordFail, responseContent, "请求失败");
}
var content = await response.Content.ReadAsStringAsync();
return APIResponseModel<object>.CreateSuccessResponseModel(content);
}
catch (Exception e)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
#region OpenAI 格式流式返回接口(保留接口),需要系统数据
public async Task<HttpResponseMessage> ForwardWordStreamRaw(ForwardModelOpenAI request)
{
// --- 1. 基础校验 (保持原有逻辑) ---
if (string.IsNullOrWhiteSpace(request.OpenAIBodyString))
{
throw new Exception("OpenAIBodyString 不能为空");
}
if (string.IsNullOrEmpty(request.GptUrl)) throw new Exception("请求的url为空");
var allowedUrls = new[] {
"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. 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)
{
throw new Exception("FindPromptStringFail"); // 建议使用具体错误码
}
// ================= 4. 替换逻辑 (System) =================
// 遍历寻找 role 为 system 的消息
var systemMsg = messages.FirstOrDefault(m => m["role"]?.ToString() == "system");
if (systemMsg != null)
{
// 没有数据 默认为 "{{SYSTEM}}"
var content = systemMsg["content"]?.ToString() ?? "{{SYSTEM}}";
// 检查是否包含占位符 "{{SYSTEM}}"
if (content.Contains("{{SYSTEM}}"))
{
// 替换占位符为真实的 promptString
systemMsg["content"] = content.Replace("{{SYSTEM}}", prompt.PromptString);
}
}
else
{
throw new Exception("缺少 system 消息节点");
}
// ================= 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();
// 拼接完整的 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<ActionResult<APIResponseModel<object>>> GetTransfer(GetTransferModel getTransferModel)
{
try
{
// 开始拼接请求体
using HttpClient client = new();
if (getTransferModel.url.StartsWith("https://api.laitool.net"))
{
client.DefaultRequestHeaders.Add("mj-api-secret", getTransferModel.APIKey);
}
else
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + getTransferModel.APIKey);
}
// 判断请求的url是不是满足条件
if (string.IsNullOrEmpty(getTransferModel.url))
{
throw new Exception("请求的url为空");
}
if (!getTransferModel.url.StartsWith("https://ark.cn-beijing.volces.com")
&& !getTransferModel.url.StartsWith("https://api.moonshot.cn")
&& !getTransferModel.url.StartsWith("https://laitool.net")
&& !getTransferModel.url.StartsWith("https://api.laitool.net")
&& !getTransferModel.url.StartsWith("https://api.laitool.cc")
&& !getTransferModel.url.StartsWith("https://laitool.cc"))
{
throw new Exception("请求的url不支持转发");
}
client.Timeout = Timeout.InfiniteTimeSpan;
var response = await client.GetAsync(getTransferModel.url);
// 判断返回的状态码
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
// 读取响应体
string responseContent = await response.Content.ReadAsStringAsync();
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ForwardWordFail, responseContent, "请求失败");
}
var content = await response.Content.ReadAsStringAsync();
// 序列化一下
return APIResponseModel<object>.CreateSuccessResponseModel(content);
}
catch (Exception e)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.SystemError, e.Message);
}
}
#endregion
}