399 lines
16 KiB
C#
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
|
|
}
|