From 9feb9e09d9c27f0b3c2c4de1a2088bd4d745906b Mon Sep 17 00:00:00 2001 From: wxd <123@qq.com> Date: Mon, 17 Nov 2025 16:36:37 +0800 Subject: [PATCH] ai --- .../AIMessages/DeepModel.cs | 67 ++++++ .../AIMessages/AIMessageAppService.cs | 109 +-------- src/Shentun.Peis.Domain/TextFormatter.cs | 210 ++++++++++++++++++ .../Controllers/AiMessageWsController.cs | 188 ++++++++++++---- .../PeisHttpApiHostModule.cs | 12 +- 5 files changed, 437 insertions(+), 149 deletions(-) create mode 100644 src/Shentun.Peis.Application.Contracts/AIMessages/DeepModel.cs create mode 100644 src/Shentun.Peis.Domain/TextFormatter.cs diff --git a/src/Shentun.Peis.Application.Contracts/AIMessages/DeepModel.cs b/src/Shentun.Peis.Application.Contracts/AIMessages/DeepModel.cs new file mode 100644 index 0000000..e3ad196 --- /dev/null +++ b/src/Shentun.Peis.Application.Contracts/AIMessages/DeepModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Shentun.Peis.AIMessages +{ + //如果好用,请收藏地址,帮忙分享。 + public class Delta + { + /// + /// 保持 + /// + public string content { get; set; } + /// + /// + /// + public string reasoning_content { get; set; } + } + + public class ChoicesItem + { + /// + /// + /// + public int index { get; set; } + /// + /// + /// + public Delta delta { get; set; } + /// + /// + /// + public string logprobs { get; set; } + /// + /// + /// + public string finish_reason { get; set; } + } + + public class Root + { + /// + /// + /// + public string id { get; set; } + /// + /// + /// + public string @object { get; set; } + /// + /// + /// + public int created { get; set; } + /// + /// + /// + public string model { get; set; } + /// + /// + /// + public string system_fingerprint { get; set; } + /// + /// + /// + public List choices { get; set; } + } +} diff --git a/src/Shentun.Peis.Application/AIMessages/AIMessageAppService.cs b/src/Shentun.Peis.Application/AIMessages/AIMessageAppService.cs index 17b0d36..ac5ea36 100644 --- a/src/Shentun.Peis.Application/AIMessages/AIMessageAppService.cs +++ b/src/Shentun.Peis.Application/AIMessages/AIMessageAppService.cs @@ -1,6 +1,9 @@ -using Microsoft.AspNetCore.Authorization; +using Azure; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; using Shentun.Peis.Enums; using Shentun.Peis.Models; using System; @@ -134,108 +137,6 @@ namespace Shentun.Peis.AIMessages return messageDto; } - - - ///// - ///// 获取Ai回复内容 流式返回 - ///// - ///// - ///// - ///// - //[HttpPost("api/app/AIMessage/GetAIMessageResultStream")] - //public async Task GetAIMessageResultStreamAsync(GetAIMessageResultInputDto input) - //{ - // var messageDto = new GetAIMessageResultDto(); - - // if (string.IsNullOrWhiteSpace(input.Message)) - // { - // throw new UserFriendlyException("请求内容不能为空"); - // } - - // var thirdInterface = await _thirdInterfaceRepository.FirstOrDefaultAsync(f => f.ThirdInterfaceType == ThirdInterfaceTypeFlag.WebAI); - - // if (thirdInterface == null) - // { - // throw new UserFriendlyException("未配置第三方AI接口"); - // } - - // if (thirdInterface.IsActive != 'Y') - // { - // throw new UserFriendlyException("该接口已禁用"); - // } - - // var parmValue = thirdInterface.ParmValue; - // var configurationBuilder = new ConfigurationBuilder() - // .AddJsonStream(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(parmValue))); - // var config = configurationBuilder.Build(); - // var apiBaseAddress = config.GetSection("Interface").GetSection("BaseAddress").Value; - // var apiKey = config.GetSection("Interface").GetSection("ApiKey").Value; - // var aiType = config.GetSection("Interface").GetSection("AIType").Value; - // var modelValue = config.GetSection("Interface").GetSection("ModelValue").Value; - - // if (aiType == AITypeFlag.DeepSeek) - // { - // using (HttpClient client = new HttpClient()) - // { - // client.BaseAddress = new Uri(apiBaseAddress); - // // 设置API密钥或其他认证信息(如果有的话) - // client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); - // //client.DefaultRequestHeaders.Add("Accept", "text/html"); - // try - // { - // var requestBody = new - // { - // model = modelValue, - // messages = new[] { new { role = "user", content = input.Message } } - // //response_format = "html" - // }; - - // var response = await client.PostAsJsonAsync("chat/completions", requestBody); - // var result = await response.Content.ReadFromJsonAsync(); - // string data = result.Choices.First().Message.Content; - - // //string dataHtml = data.Replace("### ", "").Replace("---", "").Replace("-", ""); - // //var dataHtmlList = dataHtml.Split("**", StringSplitOptions.RemoveEmptyEntries).ToList(); - // //StringBuilder stringBuilder = new StringBuilder(); - // //foreach (var item in dataHtmlList) - // //{ - // // var sindex = dataHtmlList.IndexOf(item) + 1; - // // if (sindex > 1) - // // { - // // if (sindex % 2 == 0) - // // { - - // // stringBuilder.Append("" + item); - // // } - // // else - // // { - - // // stringBuilder.Append("" + item); - // // } - // // } - // // else - // // { - // // stringBuilder.Append(item); - // // } - // //} - // //messageDto.Result = stringBuilder.ToString(); - - // string dataHtml = data.Replace("### ", "").Replace("---", "").Replace("-", "").Replace("**", ""); - - // messageDto.Result = dataHtml; - - // } - // catch (HttpRequestException e) - // { - // throw new UserFriendlyException($"获取异常:{e.Message}"); - // } - // } - // } - // else - // { - // throw new UserFriendlyException("AI接口类型不正确"); - // } - // return messageDto; - //} + } } diff --git a/src/Shentun.Peis.Domain/TextFormatter.cs b/src/Shentun.Peis.Domain/TextFormatter.cs new file mode 100644 index 0000000..8d36381 --- /dev/null +++ b/src/Shentun.Peis.Domain/TextFormatter.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shentun.Peis +{ + public class TextFormatter + { + public static string FormatStreamingText(string text, int consoleWidth = 80) + { + if (string.IsNullOrEmpty(text)) + return text; + + // 1. 处理Markdown风格的格式 + text = ProcessMarkdown(text); + + // 2. 自动换行 + text = WordWrap(text, consoleWidth); + + // 3. 处理代码块 + text = ProcessCodeBlocks(text); + + // 4. 处理列表 + text = ProcessLists(text); + + //text = text.Replace("\n\n","").Replace("\n",""); + + return text; + } + + private static string ProcessMarkdown(string text) + { + // 处理粗体 - Markdown: **text** 或 __text__ → HTML: text + text = System.Text.RegularExpressions.Regex.Replace( + text, @"\*\*(.*?)\*\*", "$1"); + + text = System.Text.RegularExpressions.Regex.Replace( + text, @"__(.*?)__", "$1"); + + // 处理斜体 - Markdown: *text* 或 _text_ → HTML: text + text = System.Text.RegularExpressions.Regex.Replace( + text, @"\*(.*?)\*", "$1"); + + text = System.Text.RegularExpressions.Regex.Replace( + text, @"_(.*?)_", "$1"); + + // 处理标题 + text = System.Text.RegularExpressions.Regex.Replace( + text, @"^### (.*)$", "

$1

", + System.Text.RegularExpressions.RegexOptions.Multiline); + + text = System.Text.RegularExpressions.Regex.Replace( + text, @"^## (.*)$", "

$1

", + System.Text.RegularExpressions.RegexOptions.Multiline); + + text = System.Text.RegularExpressions.Regex.Replace( + text, @"^# (.*)$", "

$1

", + System.Text.RegularExpressions.RegexOptions.Multiline); + + return text; + } + + private static string WordWrap(string text, int maxWidth) + { + var lines = text.Split("\n", StringSplitOptions.None); + var result = new List(); + + foreach (var line in lines) + { + if (line.Length <= maxWidth) + { + result.Add(line); + continue; + } + + var words = line.Split(' '); + var currentLine = new StringBuilder(); + + foreach (var word in words) + { + if (currentLine.Length + word.Length + 1 > maxWidth) + { + result.Add(currentLine.ToString()); + currentLine.Clear(); + } + + if (currentLine.Length > 0) + currentLine.Append(' '); + + currentLine.Append(word); + } + + if (currentLine.Length > 0) + result.Add(currentLine.ToString()); + } + + return string.Join("
", result); + } + + private static string ProcessCodeBlocks(string text) + { + // 简单的代码块处理 + var lines = text.Split('\n'); + var result = new List(); + bool inCodeBlock = false; + + foreach (var line in lines) + { + if (line.Trim().StartsWith("```")) + { + inCodeBlock = !inCodeBlock; + result.Add(new string('=', 60)); + continue; + } + + if (inCodeBlock) + { + result.Add($" {line}"); + } + else + { + result.Add(line); + } + } + + return string.Join("\n", result); + } + + private static string ProcessLists(string text) + { + var lines = text.Split('\n'); + var result = new List(); + + bool inUnorderedList = false; + bool inOrderedList = false; + var orderedListStart = 0; + + foreach (var line in lines) + { + // 处理无序列表项 (*, -, +) + var unorderedMatch = System.Text.RegularExpressions.Regex.Match(line, @"^[\*\-+]\s+(.+)"); + if (unorderedMatch.Success) + { + if (!inUnorderedList) + { + // 开始无序列表 + if (inOrderedList) + { + result.Add(""); + inOrderedList = false; + } + result.Add("
    "); + inUnorderedList = true; + } + result.Add($"
  • {unorderedMatch.Groups[1].Value}
  • "); + continue; + } + + // 处理有序列表项 (1., 2., 等) + var orderedMatch = System.Text.RegularExpressions.Regex.Match(line, @"^(\d+)\.\s+(.+)"); + if (orderedMatch.Success) + { + if (!inOrderedList) + { + // 开始有序列表 + if (inUnorderedList) + { + result.Add("
"); + inUnorderedList = false; + } + result.Add("
    "); + inOrderedList = true; + orderedListStart = int.Parse(orderedMatch.Groups[1].Value); + } + result.Add($"
  1. {orderedMatch.Groups[2].Value}
  2. "); + continue; + } + + // 如果不是列表项,关闭之前的列表 + if (inUnorderedList) + { + result.Add(""); + inUnorderedList = false; + } + if (inOrderedList) + { + result.Add("
"); + inOrderedList = false; + } + + // 添加普通行 + result.Add(line); + } + + // 处理末尾未关闭的列表 + if (inUnorderedList) + { + result.Add(""); + } + if (inOrderedList) + { + result.Add(""); + } + + return string.Join("\n", result); + } + } +} diff --git a/src/Shentun.Peis.HttpApi.Host/Controllers/AiMessageWsController.cs b/src/Shentun.Peis.HttpApi.Host/Controllers/AiMessageWsController.cs index ee258f9..ceb2e13 100644 --- a/src/Shentun.Peis.HttpApi.Host/Controllers/AiMessageWsController.cs +++ b/src/Shentun.Peis.HttpApi.Host/Controllers/AiMessageWsController.cs @@ -6,64 +6,164 @@ using System.Threading; using System; using System.Threading.Tasks; using System.Collections.Generic; +using Shentun.Peis.Models; +using Volo.Abp.Domain.Repositories; +using Microsoft.Extensions.Configuration; +using Shentun.Peis.AIMessages; +using Shentun.Peis.Enums; +using System.IO; +using System.Net.Http; +using Volo.Abp; +using Newtonsoft.Json; +using System.Linq; namespace Shentun.Peis.Controllers { - [Route("api/[controller]")] + [ApiController] public class AiMessageWsController : ControllerBase { - - [HttpGet] - public IAsyncEnumerable GetData() + private readonly IRepository _thirdInterfaceRepository; + public AiMessageWsController( + IRepository thirdInterfaceRepository + ) { - return GenerateDataAsync(); + _thirdInterfaceRepository = thirdInterfaceRepository; + + // 配置TLS1.2加密协议 + System.Net.ServicePointManager.SecurityProtocol = + System.Net.SecurityProtocolType.Tls12; } - private async IAsyncEnumerable GenerateDataAsync() + + + + /// + /// 获取Ai回复内容 + /// + /// + /// + /// + [HttpPost("api/app/AiMessageWs/GetAIMessageResult")] + public async Task GetAIMessageResultAsync(GetAIMessageResultInputDto input) { - for (int i = 0; i < 100; i++) + + Response.ContentType = "text/event-stream"; + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + + if (string.IsNullOrWhiteSpace(input.Message)) + { + throw new UserFriendlyException("请求内容不能为空"); + } + + var thirdInterface = await _thirdInterfaceRepository.FirstOrDefaultAsync(f => f.ThirdInterfaceType == ThirdInterfaceTypeFlag.WebAI); + + if (thirdInterface == null) + { + throw new UserFriendlyException("未配置第三方AI接口"); + } + + if (thirdInterface.IsActive != 'Y') + { + throw new UserFriendlyException("该接口已禁用"); + } + + var parmValue = thirdInterface.ParmValue; + var configurationBuilder = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(parmValue))); + var config = configurationBuilder.Build(); + var apiBaseAddress = config.GetSection("Interface").GetSection("BaseAddress").Value; + var apiKey = config.GetSection("Interface").GetSection("ApiKey").Value; + var aiType = config.GetSection("Interface").GetSection("AIType").Value; + var modelValue = config.GetSection("Interface").GetSection("ModelValue").Value; + + if (aiType == AITypeFlag.DeepSeek) { - await Task.Delay(100); // 模拟延迟 - yield return $"Data {i}"; + using (HttpClient client = new HttpClient()) + { + client.BaseAddress = new Uri(apiBaseAddress); + // 设置API密钥或其他认证信息(如果有的话) + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + //client.DefaultRequestHeaders.Add("Accept", "text/html"); + try + { + var requestBody = new + { + model = "deepseek-reasoner", + messages = new[] { new { role = "user", content = input.Message } }, + stream = true + }; + + var content = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"); + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.deepseek.com/v1/chat/completions") + { + Content = content + }; + + + using var response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead); + + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new System.IO.StreamReader(stream); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (line?.StartsWith("data: ") == true) + { + var s1 = line[6..]; + try + { + var streamResponse = JsonConvert.DeserializeObject(s1); + + if (streamResponse?.choices?[0]?.delta?.content != null) + { + var contentPiece = streamResponse.choices[0].delta.content; + + // 实时输出并格式化 + contentPiece = FormatAndDisplay(contentPiece); + + await Response.WriteAsync($"data: {contentPiece}\n\n"); + await Response.Body.FlushAsync(); + } + } + catch (Exception ex) { } + + if (HttpContext.RequestAborted.IsCancellationRequested) + break; + } + } + + await Response.WriteAsync("data: [DONE]\n\n"); + await Response.Body.FlushAsync(); + + } + catch (HttpRequestException e) + { + throw new UserFriendlyException($"获取异常:{e.Message}"); + } + } + } + else + { + throw new UserFriendlyException("AI接口类型不正确"); } + } - //public async Task HandleWebSocket() - //{ - // if (HttpContext.WebSockets.IsWebSocketRequest) - // { - // using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - // await ProcessDeepSeekStream(webSocket); - // } - // else - // { - // HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - // } - //} - - - - //private async Task ProcessDeepSeekStream(WebSocket webSocket) - //{ - // // 调用deepseek API - // var stream = await GetDeepSeekStream(); - - // foreach (var chunk in stream) - // { - // var buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chunk)); - // await webSocket.SendAsync( - // new ArraySegment(buffer), - // WebSocketMessageType.Text, - // true, - // CancellationToken.None); - // } - - // await webSocket.CloseAsync( - // WebSocketCloseStatus.NormalClosure, - // "Stream completed", - // CancellationToken.None); - //} + private string FormatAndDisplay(string fullText) + { + // 应用格式化规则 + var formattedText = TextFormatter.FormatStreamingText(fullText); + + return formattedText; + } } } diff --git a/src/Shentun.Peis.HttpApi.Host/PeisHttpApiHostModule.cs b/src/Shentun.Peis.HttpApi.Host/PeisHttpApiHostModule.cs index 7f429b5..2990ea7 100644 --- a/src/Shentun.Peis.HttpApi.Host/PeisHttpApiHostModule.cs +++ b/src/Shentun.Peis.HttpApi.Host/PeisHttpApiHostModule.cs @@ -463,6 +463,16 @@ public class PeisHttpApiHostModule : AbpModule .AllowAnyMethod() .AllowCredentials(); }); + + //options.AddDefaultPolicy(builder => + // { + // builder + // .AllowAnyOrigin() + // .AllowAnyHeader() + // .AllowAnyMethod(); + // }); + + }); @@ -606,7 +616,7 @@ public class PeisHttpApiHostModule : AbpModule RequestPath = configuration["PacsVirtualPath:RequestPath"] }); - + app.UseRouting(); app.UseCors();