diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs
new file mode 100644
index 00000000..3bb5cee2
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentResultOutput.cs
@@ -0,0 +1,59 @@
+using System.Reflection;
+using System.Text.Json.Serialization;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+
+public class AgentResultOutput
+{
+ ///
+ /// 类型
+ ///
+ [JsonIgnore]
+ public AgentResultTypeEnum TypeEnum { get; set; }
+
+ ///
+ /// 类型
+ ///
+ public string Type => TypeEnum.GetJsonName();
+
+ ///
+ /// 内容载体
+ ///
+ public object Content { get; set; }
+}
+
+public enum AgentResultTypeEnum
+{
+ ///
+ /// 文本内容
+ ///
+ [JsonPropertyName("text")]
+ Text,
+ ///
+ /// 工具调用中
+ ///
+ [JsonPropertyName("toolCalling")]
+ ToolCalling,
+
+ ///
+ /// 工具调用完成
+ ///
+ [JsonPropertyName("toolCalled")]
+ ToolCalled,
+
+ ///
+ /// 用量
+ ///
+ [JsonPropertyName("usage")]
+ Usage
+}
+
+public static class AgentResultTypeEnumExtensions
+{
+ public static string GetJsonName(this AgentResultTypeEnum value)
+ {
+ var member = typeof(AgentResultTypeEnum).GetMember(value.ToString()).FirstOrDefault();
+ var attr = member?.GetCustomAttribute();
+ return attr?.Name ?? value.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs
new file mode 100644
index 00000000..e2eed240
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/AgentSendInput.cs
@@ -0,0 +1,29 @@
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+
+public class AgentSendInput
+{
+ ///
+ /// 会话id
+ ///
+ public Guid SessionId { get; set; }
+
+ ///
+ /// 用户内容
+ ///
+ public string Content { get; set; }
+
+ ///
+ /// api密钥Id
+ ///
+ public Guid TokenId { get; set; }
+
+ ///
+ /// 模型id
+ ///
+ public string ModelId { get; set; }
+
+ ///
+ /// 已选择工具
+ ///
+ public List Tools { get; set; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs
index e6695aad..4e6d6789 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs
@@ -17,6 +17,7 @@ using OpenAI.Chat;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
+using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
using Yi.Framework.AiHub.Domain;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Model;
@@ -145,16 +146,26 @@ public class AiChatService : ApplicationService
}
}
- // 使用 ChatManager
+ //ai网关代理httpcontext
+ await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
+ CurrentUser.Id, sessionId, null, cancellationToken);
}
-
+
///
/// Agent 发送消息
///
[HttpPost("ai-chat/agent/send")]
- public async Task PostAgentSendAsync()
+ [Authorize]
+ public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken)
{
- await _chatManager.CompleteChatStreamAsync();
+ await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext,
+ input.SessionId,
+ input.Content,
+ input.TokenId,
+ input.ModelId,
+ CurrentUser.GetId(),
+ input.Tools,
+ cancellationToken);
}
///
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs
new file mode 100644
index 00000000..efc3a501
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/AgentStoreAggregateRoot.cs
@@ -0,0 +1,45 @@
+using SqlSugar;
+using Volo.Abp.Auditing;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Domain.Entities.Auditing;
+
+namespace Yi.Framework.AiHub.Domain.Entities.Chat;
+
+[SugarTable("Ai_AgentStore")]
+[SugarIndex($"index_{{table}}_{nameof(SessionId)}",
+ $"{nameof(SessionId)}", OrderByType.Desc
+)]
+public class AgentStoreAggregateRoot : FullAuditedAggregateRoot
+{
+ public AgentStoreAggregateRoot()
+ {
+ }
+
+ ///
+ /// 构建
+ ///
+ ///
+ public AgentStoreAggregateRoot(Guid sessionId)
+ {
+ SessionId = sessionId;
+ }
+
+ ///
+ /// 会话id
+ ///
+ public Guid SessionId { get; set; }
+
+ ///
+ /// 存储
+ ///
+ [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
+ public string? Store { get; set; }
+
+ ///
+ /// 设置存储
+ ///
+ public void SetStore()
+ {
+ this.Store = Store;
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
index 3f6942ae..17325f5c 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
@@ -234,7 +234,7 @@ public class AiGateWayManager : DomainService
tokenUsage = data.Usage;
}
- var message = System.Text.Json.JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions);
+ var message = JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions);
backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content);
// 将消息加入队列而不是直接写入
messageQueue.Enqueue($"data: {message}\n\n");
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs
index c3fdb3a7..0c3e156e 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ChatManager.cs
@@ -2,9 +2,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection;
+using System.Text;
using System.Text.Json;
using Dm.util;
using Microsoft.Agents.AI;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -12,7 +14,14 @@ using ModelContextProtocol.Server;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Responses;
+using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
+using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+using Yi.Framework.AiHub.Domain.AiGateWay;
+using Yi.Framework.AiHub.Domain.Entities.Chat;
+using Yi.Framework.AiHub.Domain.Entities.OpenApi;
+using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
+using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -20,58 +29,136 @@ public class ChatManager : DomainService
{
private readonly AiGateWayManager _aiGateWayManager;
private readonly ILoggerFactory _loggerFactory;
+ private readonly ISqlSugarRepository _messageRepository;
+ private readonly ISqlSugarRepository _agentStoreRepository;
+ private readonly ISqlSugarRepository _tokenRepository;
- public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory)
+ public ChatManager(AiGateWayManager aiGateWayManager, ILoggerFactory loggerFactory,
+ ISqlSugarRepository messageRepository,
+ ISqlSugarRepository agentStoreRepository,
+ ISqlSugarRepository tokenRepository)
{
_aiGateWayManager = aiGateWayManager;
_loggerFactory = loggerFactory;
+ _messageRepository = messageRepository;
+ _agentStoreRepository = agentStoreRepository;
+ _tokenRepository = tokenRepository;
}
- public async Task CompleteChatStreamAsync()
+ public async Task AgentCompleteChatStreamAsync(HttpContext httpContext,
+ Guid sessionId,
+ string content,
+ Guid tokenId,
+ string modelId,
+ Guid userId,
+ List tools
+ , CancellationToken cancellationToken)
{
- //token可以用户传进来
- // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888");
- var modelId = "gpt-5.2";
- var client = new OpenAIClient(new ApiKeyCredential("xxx"),
+
+ // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888");
+ var response = httpContext.Response;
+ // 设置响应头,声明是 SSE 流
+ response.ContentType = "text/event-stream;charset=utf-8;";
+ response.Headers.TryAdd("Cache-Control", "no-cache");
+ response.Headers.TryAdd("Connection", "keep-alive");
+
+ //token状态检查,在应用层统一处理
+ var token = await _tokenRepository.GetFirstAsync(x => x.Id == tokenId);
+ var client = new OpenAIClient(new ApiKeyCredential(token.Token),
new OpenAIClientOptions
{
Endpoint = new Uri("https://yxai.chat/v1"),
});
+
var agent = client.GetChatClient(modelId)
- .CreateAIAgent("你是一个专业的网页ai助手");
+ .CreateAIAgent("你是一个专业的网页ai助手,擅长解答用户问题");
+
+ //线程根据sessionId数据库中获取
+ var agentStore =
+ await _agentStoreRepository.GetFirstAsync(x => x.SessionId == sessionId);
+ if (agentStore is null)
+ {
+ agentStore = new AgentStoreAggregateRoot(sessionId);
+ }
+
+ //获取当前线程
+ AgentThread currentThread;
+ if (!string.IsNullOrWhiteSpace(agentStore.Store))
+ {
+ //获取当前存储
+ JsonElement reloaded = JsonSerializer.Deserialize(agentStore.Store, JsonSerializerOptions.Web);
+ currentThread = agent.DeserializeThread(reloaded, JsonSerializerOptions.Web);
+ }
+ else
+ {
+ currentThread = agent.GetNewThread();
+ }
- var thread = agent.GetNewThread();
-
- var tools = GetTools();
+ var toolContents = GetTools();
var chatOptions = new ChatOptions()
{
- Tools = tools.Select(x => (AITool)x).ToList(),
+ Tools = toolContents.Select(x => (AITool)x).ToList(),
ToolMode = ChatToolMode.Auto
};
- await foreach (var update in agent.RunStreamingAsync("联网搜索一下,奥德赛第一中学学生会会长是谁", thread,
- new ChatClientAgentRunOptions(chatOptions)))
+ await foreach (var update in agent.RunStreamingAsync(content, currentThread, new ChatClientAgentRunOptions(chatOptions), cancellationToken))
{
// 检查每个更新中的内容
- foreach (var content in update.Contents)
+ foreach (var updateContent in update.Contents)
{
- switch (content)
+ switch (updateContent)
{
+ //工具调用中
case FunctionCallContent functionCall:
- Console.WriteLine();
- Console.WriteLine(
- $"🔧 工具调用开始: {functionCall.CallId},{functionCall.Name},{functionCall.Arguments}");
+ await SendHttpStreamMessageAsync(httpContext,
+ new AgentResultOutput
+ {
+ TypeEnum = AgentResultTypeEnum.ToolCalling,
+ Content = functionCall.Name
+ },
+ isDone: false, cancellationToken);
break;
+
+ //工具调用完成
case FunctionResultContent functionResult:
- Console.WriteLine();
- Console.WriteLine($"✅ 工具调用完成: {functionResult.CallId},{functionResult.Result}");
+ await SendHttpStreamMessageAsync(httpContext,
+ new AgentResultOutput
+ {
+ TypeEnum = AgentResultTypeEnum.ToolCalled,
+ Content = functionResult.Result
+ },
+ isDone: false, cancellationToken);
break;
+
+ //内容输出
case TextContent textContent:
- Console.Write($"{textContent.Text}");
+ //发送消息给前端
+ await SendHttpStreamMessageAsync(httpContext,
+ new AgentResultOutput
+ {
+ TypeEnum = AgentResultTypeEnum.Text,
+ Content = textContent.Text
+ },
+ isDone: false, cancellationToken);
break;
+
+ //用量统计
case UsageContent usageContent:
+ //存储message 为了token算费
+ await SendHttpStreamMessageAsync(httpContext,
+ new AgentResultOutput
+ {
+ TypeEnum = AgentResultTypeEnum.Usage,
+ Content = new ThorUsageResponse
+ {
+ InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0),
+ OutputTokens = Convert.ToInt32(usageContent.Details.OutputTokenCount ?? 0),
+ TotalTokens = usageContent.Details.TotalTokenCount ?? 0,
+ }
+ },
+ isDone: false, cancellationToken);
Console.WriteLine();
Console.WriteLine($"✅ 用量统计: {usageContent.Details.TotalTokenCount}");
break;
@@ -79,10 +166,15 @@ public class ChatManager : DomainService
}
}
+ //断开连接
+ await SendHttpStreamMessageAsync(httpContext, null, isDone: true, cancellationToken);
- string serializedJson = thread.Serialize(JsonSerializerOptions.Web).GetRawText();
- JsonElement reloaded = JsonSerializer.Deserialize(serializedJson, JsonSerializerOptions.Web);
- var newThread = agent.DeserializeThread(reloaded, JsonSerializerOptions.Web);
+ //将线程持久化到数据库
+ string serializedJson = currentThread.Serialize(JsonSerializerOptions.Web).GetRawText();
+ agentStore.Store = serializedJson;
+
+ //插入或者更新
+ await _agentStoreRepository.InsertOrUpdateAsync(agentStore);
}
@@ -106,4 +198,32 @@ public class ChatManager : DomainService
return mcpTools;
}
+
+ ///
+ /// 发送消息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task SendHttpStreamMessageAsync(HttpContext httpContext,
+ AgentResultOutput? content,
+ bool isDone = false,
+ CancellationToken cancellationToken = default)
+ {
+ var response = httpContext.Response;
+ string output;
+ if (isDone)
+ {
+ output = "[DONE]";
+ }
+ else
+ {
+ output = JsonSerializer.Serialize(content,ThorJsonSerializer.DefaultOptions);
+ }
+
+ await response.WriteAsync($"data: {output}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false);
+ await response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj
index 8f017820..b059f58b 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj
@@ -12,6 +12,7 @@
+
diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
index 8de0dab9..62cf6531 100644
--- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
+++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
@@ -358,7 +358,7 @@ namespace Yi.Abp.Web
var app = context.GetApplicationBuilder();
app.UseRouting();
- // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables();
+ //app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables();
// app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables();
// app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables();