diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs index 0147b34a..502924de 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Text; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -8,10 +9,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; 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.OpenAiDto; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.AiHub.Domain.Shared.Dtos; +using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; @@ -19,18 +23,34 @@ public class OpenApiService : ApplicationService { private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; + private readonly TokenManager _tokenManager; + private readonly AiMessageManager _aiMessageManager; + private readonly UsageStatisticsManager _usageStatisticsManager; - public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger logger) + public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger logger, + TokenManager tokenManager, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager) { _httpContextAccessor = httpContextAccessor; _logger = logger; + _tokenManager = tokenManager; + _aiMessageManager = aiMessageManager; + _usageStatisticsManager = usageStatisticsManager; } + /// + /// 对话 + /// + /// + /// [HttpPost("/v1/chat/completions")] public async Task ChatCompletionsAsync(ChatCompletionsInput input, CancellationToken cancellationToken) { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; + + var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + + var response = httpContext.Response; // 设置响应头,声明是 SSE 流 response.ContentType = "text/event-stream"; @@ -125,6 +145,26 @@ public class OpenApiService : ApplicationService isComplete = true; await outputTask; + + await _aiMessageManager.CreateUserMessageAsync(userId, null, + new MessageInputDto + { + Content = input.Messages.LastOrDefault() + .Content, + ModelId = input.Model, + TokenUsage = tokenUsage, + }); + + await _aiMessageManager.CreateSystemMessageAsync(userId, null, + new MessageInputDto + { + Content = backupSystemContent.ToString(), + ModelId = input.Model, + TokenUsage = tokenUsage + }); + + await _usageStatisticsManager.SetUsageAsync(userId, input.Model, tokenUsage.InputTokenCount, + tokenUsage.OutputTokenCount); } private SendMessageOutputDto MapToMessage(string modelId, string content) @@ -204,4 +244,18 @@ public class OpenApiService : ApplicationService return output; } + + private string? GetTokenByHttpContext(HttpContext httpContext) + { + // 获取Authorization头 + string authHeader = httpContext.Request.Headers["Authorization"]; + + // 检查是否有Bearer token + if (authHeader != null && authHeader.StartsWith("Bearer ")) + { + return authHeader.Substring("Bearer ".Length).Trim(); + } + + return null; + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/TokenService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/TokenService.cs new file mode 100644 index 00000000..de376e39 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/TokenService.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Services; +using Volo.Abp.Users; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; +using Yi.Framework.AiHub.Domain.Managers; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.AiHub.Application.Services; + +public class TokenService : ApplicationService +{ + private readonly ISqlSugarRepository _tokenRepository; + private readonly TokenManager _tokenManager; + + /// + /// 构造函数 + /// + /// + /// + public TokenService(ISqlSugarRepository tokenRepository, TokenManager tokenManager) + { + _tokenRepository = tokenRepository; + _tokenManager = tokenManager; + } + + /// + /// 获取token + /// + /// + [Authorize] + public async Task GetAsync() + { + return await _tokenManager.GetAsync(CurrentUser.GetId()); + } + + /// + /// 创建token + /// + /// + [Authorize] + public async Task CreateAsync() + { + if (!CurrentUser.Roles.Contains("YiXinAi-Vip") && CurrentUser.UserName != "cc") + { + throw new UserFriendlyException("充值成为Vip,畅想第三方token服务"); + } + + await _tokenManager.CreateAsync(CurrentUser.GetId()); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/MessageTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/MessageTypeEnum.cs new file mode 100644 index 00000000..74ed998f --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/MessageTypeEnum.cs @@ -0,0 +1,7 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Enums; + +public enum MessageTypeEnum +{ + Web = 1, + Api = 2 +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Yi.Framework.AiHub.Domain.Shared.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Yi.Framework.AiHub.Domain.Shared.csproj index 248e1dc2..dac23eef 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Yi.Framework.AiHub.Domain.Shared.csproj +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Yi.Framework.AiHub.Domain.Shared.csproj @@ -7,7 +7,6 @@ - diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs index 47070083..e01d0f05 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs @@ -3,6 +3,7 @@ using SqlSugar; using Volo.Abp.Domain.Entities.Auditing; using Yi.Framework.AiHub.Domain.Entities.ValueObjects; using Yi.Framework.AiHub.Domain.Shared.Dtos; +using Yi.Framework.AiHub.Domain.Shared.Enums; namespace Yi.Framework.AiHub.Domain.Entities.Chat; @@ -17,7 +18,7 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot { } - public MessageAggregateRoot(Guid userId, Guid sessionId, string content, string role, string modelId, + public MessageAggregateRoot(Guid userId, Guid? sessionId, string content, string role, string modelId, TokenUsage? tokenUsage) { UserId = userId; @@ -27,13 +28,14 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot ModelId = modelId; if (tokenUsage is not null) { - this.TokenUsage = tokenUsage.Adapt(); + this.TokenUsage = tokenUsage.Adapt(); } + this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web; } public Guid UserId { get; set; } - public Guid SessionId { get; set; } + public Guid? SessionId { get; set; } [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] public string Content { get; set; } @@ -42,7 +44,7 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot public string ModelId { get; set; } public string? Remark { get; set; } - [SugarColumn(IsOwnsOne = true)] - public TokenUsageValueObject TokenUsage { get; set; }= new TokenUsageValueObject(); - + [SugarColumn(IsOwnsOne = true)] public TokenUsageValueObject TokenUsage { get; set; } = new TokenUsageValueObject(); + + public MessageTypeEnum MessageType { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs new file mode 100644 index 00000000..96377eb7 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs @@ -0,0 +1,52 @@ +using System.Text; +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities.OpenApi; + +[SugarTable("Ai_Token")] +public class TokenAggregateRoot : FullAuditedAggregateRoot +{ + public TokenAggregateRoot() + { + } + + public TokenAggregateRoot(Guid userId) + { + this.UserId = userId; + this.Token = GenerateToken(); + } + + public string Token { get; set; } + public Guid UserId { get; set; } + + /// + /// 重置token + /// + public void ResetToken() + { + this.Token = GenerateToken(); + } + + private string GenerateToken(int length = 36) + { + // 定义可能的字符集:大写字母、小写字母和数字 + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + // 创建随机数生成器 + Random random = new Random(); + + // 使用StringBuilder高效构建字符串 + StringBuilder sb = new StringBuilder(length); + + // 生成指定长度的随机字符串 + for (int i = 0; i < length; i++) + { + // 从字符集中随机选择一个字符 + int index = random.Next(chars.Length); + sb.Append(chars[index]); + } + + return "yi-" + sb.ToString(); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs index c41c1073..38c14605 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs @@ -23,7 +23,7 @@ public class AiMessageManager : DomainService /// /// /// - public async Task CreateSystemMessageAsync(Guid userId, Guid sessionId, MessageInputDto input) + public async Task CreateSystemMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input) { input.Role = "system"; var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); @@ -37,7 +37,7 @@ public class AiMessageManager : DomainService /// /// /// - public async Task CreateUserMessageAsync(Guid userId, Guid sessionId, MessageInputDto input) + public async Task CreateUserMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input) { input.Role = "user"; var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs new file mode 100644 index 00000000..1a0ead20 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs @@ -0,0 +1,64 @@ +using Volo.Abp.Domain.Services; +using Volo.Abp.Users; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.AiHub.Domain.Managers; + +public class TokenManager : DomainService +{ + private readonly ISqlSugarRepository _tokenRepository; + + public TokenManager(ISqlSugarRepository tokenRepository) + { + _tokenRepository = tokenRepository; + } + + public async Task GetAsync(Guid userId) + { + var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); + if (entity is not null) + { + return entity.Token; + } + else + { + return null; + } + } + + public async Task CreateAsync(Guid userId) + { + var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); + if (entity is not null) + { + entity.ResetToken(); + await _tokenRepository.UpdateAsync(entity); + } + else + { + var token = new TokenAggregateRoot(userId); + await _tokenRepository.InsertAsync(token); + } + } + + public async Task GetUserIdAsync(string? token) + { + if (token is null) + { + throw new UserFriendlyException("当前请求未包含token", "401"); + } + + if (token.StartsWith("yi-")) + { + var entity = await _tokenRepository._DbQueryable.Where(x => x.Token == token).FirstAsync(); + if (entity is null) + { + throw new UserFriendlyException("当前请求token无效", "401"); + } + + return entity.UserId; + } + throw new UserFriendlyException("当前请求token非法", "401"); + } +} \ No newline at end of file