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($"- {orderedMatch.Groups[2].Value}
");
+ 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();