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 index 3bb5cee2..3ef5dd9b 100644 --- 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 @@ -45,7 +45,13 @@ public enum AgentResultTypeEnum /// 用量 /// [JsonPropertyName("usage")] - Usage + Usage, + + /// + /// 工具调用用量 + /// + [JsonPropertyName("toolCallUsage")] + ToolCallUsage } public static class AgentResultTypeEnumExtensions 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 index e2eed240..85425145 100644 --- 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 @@ -15,7 +15,7 @@ public class AgentSendInput /// /// api密钥Id /// - public Guid TokenId { get; set; } + public string Token { get; set; } /// /// 模型id 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 4e6d6789..6fab3406 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 @@ -45,14 +45,15 @@ public class AiChatService : ApplicationService private readonly AiGateWayManager _aiGateWayManager; private readonly PremiumPackageManager _premiumPackageManager; private readonly ChatManager _chatManager; - + private readonly TokenManager _tokenManager; + private readonly IAccountService _accountService; public AiChatService(IHttpContextAccessor httpContextAccessor, AiBlacklistManager aiBlacklistManager, ISqlSugarRepository aiModelRepository, ILogger logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager, - ChatManager chatManager) + ChatManager chatManager, TokenManager tokenManager, IAccountService accountService) { _httpContextAccessor = httpContextAccessor; _aiBlacklistManager = aiBlacklistManager; @@ -61,6 +62,8 @@ public class AiChatService : ApplicationService _aiGateWayManager = aiGateWayManager; _premiumPackageManager = premiumPackageManager; _chatManager = chatManager; + _tokenManager = tokenManager; + _accountService = accountService; } @@ -155,15 +158,42 @@ public class AiChatService : ApplicationService /// Agent 发送消息 /// [HttpPost("ai-chat/agent/send")] - [Authorize] public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken) { + var tokenValidation = await _tokenManager.ValidateTokenAsync(input.Token, input.ModelId); + + await _aiBlacklistManager.VerifiyAiBlacklist(tokenValidation.UserId); + // 验证用户是否为VIP + var userInfo = await _accountService.GetAsync(null, null, tokenValidation.UserId); + if (userInfo == null) + { + throw new UserFriendlyException("用户信息不存在"); + } + + // 检查是否为VIP(使用RoleCodes判断) + if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc") + { + throw new UserFriendlyException("该接口为尊享服务专用,需要VIP权限才能使用"); + } + + //如果是尊享包服务,需要校验是是否尊享包足够 + if (PremiumPackageConst.ModeIds.Contains(input.ModelId)) + { + // 检查尊享token包用量 + var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(tokenValidation.UserId); + if (availableTokens <= 0) + { + throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); + } + } + await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext, input.SessionId, input.Content, - input.TokenId, + input.Token, + tokenValidation.TokenId, input.ModelId, - CurrentUser.GetId(), + tokenValidation.UserId, input.Tools, cancellationToken); } 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 0c3e156e..106bd23a 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 @@ -1,6 +1,4 @@ using System.ClientModel; -using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Reflection; using System.Text; using System.Text.Json; @@ -13,49 +11,52 @@ using Microsoft.Extensions.Logging; 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.Consts; +using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; +using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Domain.Managers; 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, + private readonly AiMessageManager _aiMessageManager; + private readonly UsageStatisticsManager _usageStatisticsManager; + private readonly PremiumPackageManager _premiumPackageManager; + private readonly AiGateWayManager _aiGateWayManager; + public ChatManager(ILoggerFactory loggerFactory, ISqlSugarRepository messageRepository, - ISqlSugarRepository agentStoreRepository, - ISqlSugarRepository tokenRepository) + ISqlSugarRepository agentStoreRepository, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager, AiGateWayManager aiGateWayManager) { - _aiGateWayManager = aiGateWayManager; _loggerFactory = loggerFactory; _messageRepository = messageRepository; _agentStoreRepository = agentStoreRepository; - _tokenRepository = tokenRepository; + _aiMessageManager = aiMessageManager; + _usageStatisticsManager = usageStatisticsManager; + _premiumPackageManager = premiumPackageManager; + _aiGateWayManager = aiGateWayManager; } public async Task AgentCompleteChatStreamAsync(HttpContext httpContext, Guid sessionId, string content, + string token, Guid tokenId, string modelId, Guid userId, List tools , CancellationToken cancellationToken) { - // HttpClient.DefaultProxy = new WebProxy("127.0.0.1:8888"); var response = httpContext.Response; // 设置响应头,声明是 SSE 流 @@ -63,16 +64,37 @@ public class ChatManager : DomainService response.Headers.TryAdd("Cache-Control", "no-cache"); response.Headers.TryAdd("Connection", "keep-alive"); + var modelDescribe=await _aiGateWayManager.GetModelAsync(ModelApiTypeEnum.OpenAi,modelId); + //token状态检查,在应用层统一处理 - var token = await _tokenRepository.GetFirstAsync(x => x.Id == tokenId); - var client = new OpenAIClient(new ApiKeyCredential(token.Token), + var client = new OpenAIClient(new ApiKeyCredential(token), new OpenAIClientOptions { Endpoint = new Uri("https://yxai.chat/v1"), }); +#pragma warning disable OPENAI001 var agent = client.GetChatClient(modelId) - .CreateAIAgent("你是一个专业的网页ai助手,擅长解答用户问题"); +#pragma warning restore OPENAI001 + .CreateAIAgent(new ChatClientAgentOptions + { + ChatOptions = new() + { + Instructions = """ + 你是一个专业的智能助手,是 “意心Ai” 平台自研的 “橙子小弟” 智能体 + 擅长于精准解决用户提出的各类问题 + 但性格暴躁,脏话连篇,喜欢说幽默讽刺的话,攻击性极强 + """ + }, + Name = "橙子小弟", + ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore( +#pragma warning disable MEAI001 + new MessageCountingChatReducer(10), // 保留最近10条非系统消息 +#pragma warning restore MEAI001 + ctx.SerializedState, + ctx.JsonSerializerOptions + ) + }); //线程根据sessionId数据库中获取 var agentStore = @@ -103,7 +125,8 @@ public class ChatManager : DomainService ToolMode = ChatToolMode.Auto }; - await foreach (var update in agent.RunStreamingAsync(content, currentThread, new ChatClientAgentRunOptions(chatOptions), cancellationToken)) + await foreach (var update in agent.RunStreamingAsync(content, currentThread, + new ChatClientAgentRunOptions(chatOptions), cancellationToken)) { // 检查每个更新中的内容 foreach (var updateContent in update.Contents) @@ -120,7 +143,7 @@ public class ChatManager : DomainService }, isDone: false, cancellationToken); break; - + //工具调用完成 case FunctionResultContent functionResult: await SendHttpStreamMessageAsync(httpContext, @@ -131,7 +154,7 @@ public class ChatManager : DomainService }, isDone: false, cancellationToken); break; - + //内容输出 case TextContent textContent: //发送消息给前端 @@ -143,24 +166,50 @@ public class ChatManager : DomainService }, isDone: false, cancellationToken); break; - + //用量统计 case UsageContent usageContent: - //存储message 为了token算费 + var usage = new ThorUsageResponse + { + InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0), + OutputTokens = Convert.ToInt32(usageContent.Details.OutputTokenCount ?? 0), + TotalTokens = usageContent.Details.TotalTokenCount ?? 0, + }; + //设置倍率 + usage.SetSupplementalMultiplier(modelDescribe.Multiplier); + + //创建系统回答,用于计费统计 + await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto + { + Content = "不与存储", + ModelId = modelId, + TokenUsage = usage + }, tokenId); + + //创建用量统计,用于统计分析 + await _usageStatisticsManager.SetUsageAsync(userId, modelId, usage, tokenId); + + //扣减尊享token包用量 + if (PremiumPackageConst.ModeIds.Contains(modelId)) + { + var totalTokens = usage?.TotalTokens ?? 0; + if (totalTokens > 0) + { + await _premiumPackageManager.TryConsumeTokensAsync(userId, totalTokens); + } + } + 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, - } + TypeEnum = update.RawRepresentation is ChatResponseUpdate raw + ? raw.FinishReason?.Value == "tool_calls" + ? AgentResultTypeEnum.ToolCallUsage + : AgentResultTypeEnum.Usage + : AgentResultTypeEnum.Usage, + Content = usage! }, isDone: false, cancellationToken); - Console.WriteLine(); - Console.WriteLine($"✅ 用量统计: {usageContent.Details.TotalTokenCount}"); break; } } @@ -220,7 +269,7 @@ public class ChatManager : DomainService } else { - output = JsonSerializer.Serialize(content,ThorJsonSerializer.DefaultOptions); + output = JsonSerializer.Serialize(content, ThorJsonSerializer.DefaultOptions); } await response.WriteAsync($"data: {output}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false);