From 01a3c81359a36dbe7b9db7e6e41f1924244a6b08 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Fri, 27 Jun 2025 22:13:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dtos/MessageInputDto.cs | 11 ++- .../Services/AiChatService.cs | 86 ++++++++++++------- .../Services/MessageService.cs | 3 +- .../Services/SessionService.cs | 1 + .../Dtos/CompleteChatResponse.cs | 8 ++ .../Dtos/TokenUsage.cs | 10 +++ .../AiChat/IChatService.cs | 2 +- .../AiChat/Impl/AzureChatService.cs | 24 +++++- .../AiChat/Impl/AzureRestChatService.cs | 79 ++++++++++------- .../Entities/Chat/MessageAggregateRoot.cs | 22 ++++- .../Entities/Chat/SessionAggregateRoot.cs | 2 +- .../Entities/Model/AiAppAggregateRoot.cs | 2 +- .../Entities/Model/AiModelEntity.cs | 3 +- .../Entities/UsageStatisticsAggregateRoot.cs | 48 ++++++++--- .../ValueObjects/TokenUsageValueObject.cs | 10 +++ .../Managers/AiGateWayManager.cs | 3 +- .../Managers/AiMessageManager.cs | 22 ++++- .../Managers/UsageStatisticsManager.cs | 37 ++++++++ .../Yi.Framework.AiHub.Domain.csproj | 2 +- Yi.Ai.Vue3/.env.development | 4 +- 20 files changed, 281 insertions(+), 98 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/TokenUsage.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs index e0ec143a..8c01b62c 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs @@ -1,11 +1,14 @@ -namespace Yi.Framework.AiHub.Application.Contracts.Dtos; +using SqlSugar; +using Yi.Framework.AiHub.Domain.Shared.Dtos; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos; public class MessageInputDto { public string Content { get; set; } public string Role { get; set; } - public decimal DeductCost { get; set; } - public decimal TotalTokens { get; set; } public string ModelId { get; set; } - public string Remark { get; set; } + public string? Remark { get; set; } + + public TokenUsage? TokenUsage { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs index 1fefc569..69410d1a 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs @@ -12,7 +12,9 @@ using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Managers; +using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Domain.Shared.Dtos; using Yi.Framework.SqlSugarCore.Abstractions; @@ -28,15 +30,17 @@ public class AiChatService : ApplicationService private readonly AiMessageManager _aiMessageManager; private readonly ISqlSugarRepository _aiModelRepository; private readonly AiBlacklistManager _aiBlacklistManager; + private readonly UsageStatisticsManager _usageStatisticsManager; public AiChatService(IHttpContextAccessor httpContextAccessor, AiMessageManager aiMessageManager, AiBlacklistManager aiBlacklistManager, - ISqlSugarRepository aiModelRepository) + ISqlSugarRepository aiModelRepository, UsageStatisticsManager usageStatisticsManager) { this._httpContextAccessor = httpContextAccessor; _aiMessageManager = aiMessageManager; _aiBlacklistManager = aiBlacklistManager; _aiModelRepository = aiModelRepository; + _usageStatisticsManager = usageStatisticsManager; } @@ -60,21 +64,21 @@ public class AiChatService : ApplicationService public async Task> GetModelAsync() { var output = await _aiModelRepository._DbQueryable - .OrderByDescending(x=>x.OrderNum) + .OrderByDescending(x => x.OrderNum) .Select(x => new ModelGetListOutput - { - Id = x.Id, - Category = "chat", - ModelName = x.Name, - ModelDescribe = x.Description, - ModelPrice = 0, - ModelType = "1", - ModelShow = "0", - SystemPrompt = null, - ApiHost = null, - ApiKey = null, - Remark = x.Description - }).ToListAsync(); + { + Id = x.Id, + Category = "chat", + ModelName = x.Name, + ModelDescribe = x.Description, + ModelPrice = 0, + ModelType = "1", + ModelShow = "0", + SystemPrompt = null, + ApiHost = null, + ApiKey = null, + Remark = x.Description + }).ToListAsync(); return output; } @@ -128,13 +132,15 @@ public class AiChatService : ApplicationService var gateWay = LazyServiceProvider.GetRequiredService(); var completeChatResponse = gateWay.CompleteChatAsync(input.Model, history, cancellationToken); + var tokenUsage = new TokenUsage(); await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true); //缓存队列算法 - // 创建一个队列来缓存消息 var messageQueue = new ConcurrentQueue(); + + StringBuilder backupSystemContent = new StringBuilder(); // 设置输出速率(例如每50毫秒输出一次) var outputInterval = TimeSpan.FromMilliseconds(100); // 标记是否完成接收 @@ -161,33 +167,49 @@ public class AiChatService : ApplicationService await foreach (var data in completeChatResponse) { - var model = MapToMessage(input.Model, data); + if (data.IsFinish) + { + tokenUsage = data.TokenUsage; + } + + var model = MapToMessage(input.Model, data.Content); var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - + backupSystemContent.Append(data.Content); // 将消息加入队列而不是直接写入 messageQueue.Enqueue($"data: {message}\n"); } - // 标记完成并发送结束标记 - isComplete = true; + //断开连接 messageQueue.Enqueue("data: done\n"); + // 标记完成并发送结束标记 + isComplete = true; await outputTask; if (CurrentUser.IsAuthenticated && input.SessionId.HasValue) { - await _aiMessageManager.CreateMessageAsync(CurrentUser.GetId(), input.SessionId.Value, new MessageInputDto - { - Content = input.Messages.LastOrDefault().Content, - Role = input.Messages.LastOrDefault().Role, - DeductCost = 0, - TotalTokens = 0, - ModelId = input.Model, - Remark = null - }); + await _aiMessageManager.CreateUserMessageAsync(CurrentUser.GetId(), input.SessionId.Value, + new MessageInputDto + { + Content = input.Messages.LastOrDefault() + .Content, + ModelId = input.Model, + TokenUsage = tokenUsage, + }); + + await _aiMessageManager.CreateSystemMessageAsync(CurrentUser.GetId(), input.SessionId.Value, + new MessageInputDto + { + Content = backupSystemContent.ToString(), + ModelId = input.Model, + TokenUsage = tokenUsage + }); + + await _usageStatisticsManager.SetUsageAsync(CurrentUser.GetId(), input.Model, tokenUsage.InputTokenCount, + tokenUsage.OutputTokenCount); } } @@ -249,9 +271,9 @@ public class AiChatService : ApplicationService SystemFingerprint = "", Usage = new Usage { - PromptTokens = 75, - CompletionTokens = 25, - TotalTokens = 100, + PromptTokens = 0, + CompletionTokens = 0, + TotalTokens = 0, PromptTokensDetails = new() { AudioTokens = 0, diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs index af6211ff..6084c4dd 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs @@ -7,6 +7,7 @@ using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; @@ -34,7 +35,7 @@ public class MessageService : ApplicationService var entities = await _repository._DbQueryable .Where(x => x.SessionId == input.SessionId) .Where(x=>x.UserId == userId) - .OrderByDescending(x => x.Id) + .OrderBy(x => x.Id) .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); return new PagedResultDto(total, entities.Adapt>()); } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs index 2e423c06..1a7cb4f0 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs @@ -8,6 +8,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs new file mode 100644 index 00000000..b0ceb1cd --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs @@ -0,0 +1,8 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Dtos; + +public class CompleteChatResponse +{ + public TokenUsage? TokenUsage { get; set; } + public bool IsFinish { get; set; } + public string? Content { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/TokenUsage.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/TokenUsage.cs new file mode 100644 index 00000000..75c7b12a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/TokenUsage.cs @@ -0,0 +1,10 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Dtos; + +public class TokenUsage +{ + public int OutputTokenCount { get; set; } + + public int InputTokenCount { get; set; } + + public int TotalTokenCount { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs index fdc56878..6bd0b422 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs @@ -5,6 +5,6 @@ namespace Yi.Framework.AiHub.Domain.AiChat; public interface IChatService { - public IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, List messages, + public IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, List messages, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs index a45f2182..6496a341 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs @@ -12,7 +12,8 @@ public class AzureChatService : IChatService { } - public async IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, List messages, + public async IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, + List messages, [EnumeratorCancellation] CancellationToken cancellationToken) { var endpoint = new Uri(aiModelDescribe.Endpoint); @@ -32,9 +33,28 @@ public class AzureChatService : IChatService await foreach (StreamingChatCompletionUpdate update in response) { + var result = new CompleteChatResponse(); + var isFinish = update.Usage?.OutputTokenCount is not null; + if (isFinish) + { + result.IsFinish = true; + result.TokenUsage = new TokenUsage + { + OutputTokenCount = update.Usage.OutputTokenCount, + InputTokenCount = update.Usage.InputTokenCount, + TotalTokenCount = update.Usage.TotalTokenCount + }; + } + foreach (ChatMessageContentPart updatePart in update.ContentUpdate) { - yield return updatePart.Text; + result.Content = updatePart.Text; + yield return result; + } + + if (isFinish) + { + yield return result; } } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs index 5a7f9c70..1d2d6357 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs @@ -14,7 +14,8 @@ public class AzureRestChatService : IChatService { } - public async IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, List messages, + public async IAsyncEnumerable CompleteChatAsync(AiModelDescribe aiModelDescribe, + List messages, [EnumeratorCancellation] CancellationToken cancellationToken) { // 设置API URL @@ -61,51 +62,65 @@ public class AzureRestChatService : IChatService var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); // 从流中读取数据并输出到控制台 using var streamReader = new StreamReader(responseStream); - string line; - while ((line = await streamReader.ReadLineAsync(cancellationToken)) != null) + while (await streamReader.ReadLineAsync(cancellationToken) is { } line) { - var result = GetContent(line); - if (result is not null) + var result = new CompleteChatResponse(); + try { - yield return result; + var jsonObj = MapToJObject(line); + var content = GetContent(jsonObj); + var tokenUsage = GetTokenUsage(jsonObj); + result= new CompleteChatResponse + { + TokenUsage = tokenUsage, + IsFinish = tokenUsage is not null, + Content = content + }; } + catch (Exception e) + { + Console.WriteLine("解析失败"); + } + + yield return result; } } - private string? GetContent(string line) + private JObject? MapToJObject(string line) { if (string.IsNullOrWhiteSpace(line)) return null; string prefix = "data: "; line = line.Substring(prefix.Length); + return JObject.Parse(line); + } + private string? GetContent(JObject? jsonObj) + { + var contentToken = jsonObj.SelectToken("choices[0].delta.content"); + if (contentToken != null && contentToken.Type != JTokenType.Null) + { + return contentToken.ToString(); + } - try + return null; + } + + private TokenUsage? GetTokenUsage(JObject? jsonObj) + { + var usage = jsonObj.SelectToken("usage"); + if (usage is not null && usage.Type != JTokenType.Null) { - // 解析为JObject - var jsonObj = JObject.Parse(line); - var content = jsonObj["choices"][0]["delta"]["content"].ToString(); - return content; - // // 判断choices是否存在且是数组,并且有元素 - // if (jsonObj.TryGetValue("choices", out var choicesToken) && choicesToken is JArray choicesArray && - // choicesArray.Count > 0) - // { - // var firstChoice = choicesArray[0] as JObject; - // // 判断delta字段是否存在 - // if (firstChoice.TryGetValue("delta", out var deltaToken)) - // { - // // 获取content字段 - // if (deltaToken.Type == JTokenType.Object && ((JObject)deltaToken).TryGetValue("content", out var contentToken)) - // { - // return contentToken.ToString(); - // } - // } - // } - } - catch (Exception) - { - // 解析失败 - return null; + var result = new TokenUsage() + { + OutputTokenCount = usage["completion_tokens"].ToObject(), + InputTokenCount = usage["prompt_tokens"].ToObject(), + TotalTokenCount = usage["total_tokens"].ToObject() + }; + + return result; } + + return null; } } \ No newline at end of file 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 0250266f..d9d5fb58 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 @@ -1,7 +1,10 @@ -using SqlSugar; +using Mapster; +using SqlSugar; using Volo.Abp.Domain.Entities.Auditing; +using Yi.Framework.AiHub.Domain.Entities.ValueObjects; +using Yi.Framework.AiHub.Domain.Shared.Dtos; -namespace Yi.Framework.AiHub.Domain.Entities; +namespace Yi.Framework.AiHub.Domain.Entities.Chat; [SugarTable("Ai_Message")] [SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(SessionId)}", @@ -14,21 +17,32 @@ 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; SessionId = sessionId; Content = content; Role = role; ModelId = modelId; + if (tokenUsage is not null) + { + this.TokenUsage = tokenUsage.Adapt(); + } + } public Guid UserId { get; set; } public Guid SessionId { get; set; } + + [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] public string Content { get; set; } + public string Role { get; set; } public decimal DeductCost { get; set; } public decimal TotalTokens { get; set; } public string ModelId { get; set; } - public string Remark { get; set; } + public string? Remark { get; set; } + + [SugarColumn(IsOwnsOne = true)] public TokenUsageValueObject TokenUsage { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/SessionAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/SessionAggregateRoot.cs index 6d290364..b7fabf4e 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/SessionAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/SessionAggregateRoot.cs @@ -1,7 +1,7 @@ using SqlSugar; using Volo.Abp.Domain.Entities.Auditing; -namespace Yi.Framework.AiHub.Domain.Entities; +namespace Yi.Framework.AiHub.Domain.Entities.Chat; [SugarTable("Ai_Session")] [SugarIndex($"index_{{table}}_{nameof(UserId)}",$"{nameof(UserId)}", OrderByType.Asc)] diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppAggregateRoot.cs index cd7eced2..6a698f4f 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppAggregateRoot.cs @@ -2,7 +2,7 @@ using Volo.Abp.Domain.Entities.Auditing; using Yi.Framework.Core.Data; -namespace Yi.Framework.AiHub.Domain.Entities; +namespace Yi.Framework.AiHub.Domain.Entities.Model; /// /// ai应用 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs index d221d8eb..d9dcd656 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs @@ -1,9 +1,8 @@ using SqlSugar; using Volo.Abp.Domain.Entities; -using Volo.Abp.Domain.Entities.Auditing; using Yi.Framework.Core.Data; -namespace Yi.Framework.AiHub.Domain.Entities; +namespace Yi.Framework.AiHub.Domain.Entities.Model; /// /// ai模型定义 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs index 8c9b8b07..62170a8a 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs @@ -9,6 +9,16 @@ namespace Yi.Framework.AiHub.Domain.Entities; [SugarTable("Ai_UsageStatistics")] public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot { + public UsageStatisticsAggregateRoot() + { + } + + public UsageStatisticsAggregateRoot(Guid userId, string modelId) + { + UserId = userId; + ModelId = modelId; + } + /// /// 用户id /// @@ -19,18 +29,34 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot /// public string ModelId { get; set; } - /// - /// 输入使用token使用 - /// - public decimal InputTokens { get; set; } - - /// - /// 输出使用token使用 - /// - public decimal OutputTokens { get; set; } - /// /// 对话次数 /// - public int Number { get; set; } + public int UsageTotalNumber { get; set; } + + /// + /// 使用输出token总数 + /// + public int UsageOutputTokenCount { get; set; } + + /// + /// 使用输入总数 + /// + public int UsageInputTokenCount { get; set; } + + /// + /// 总token使用数量 + /// + public int TotalTokenCount { get; set; } + + /// + /// 新增一次聊天统计 + /// + public void AddOnceChat(int inputTokenCount, int outputTokenCount) + { + UsageTotalNumber += 1; + UsageOutputTokenCount += outputTokenCount; + UsageInputTokenCount += inputTokenCount; + TotalTokenCount += (outputTokenCount + inputTokenCount); + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs new file mode 100644 index 00000000..f3a5dcd8 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs @@ -0,0 +1,10 @@ +namespace Yi.Framework.AiHub.Domain.Entities.ValueObjects; + +public class TokenUsageValueObject +{ + public int OutputTokenCount { get; set; } + + public int InputTokenCount { get; set; } + + public int TotalTokenCount { get; set; } +} \ 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 6f4a5a57..8bba851a 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 @@ -4,6 +4,7 @@ using OpenAI.Chat; using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Domain.AiChat; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.SqlSugarCore.Abstractions; @@ -57,7 +58,7 @@ public class AiGateWayManager : DomainService /// /// /// - public async IAsyncEnumerable CompleteChatAsync(string modelId, List messages, + public async IAsyncEnumerable CompleteChatAsync(string modelId, List messages, [EnumeratorCancellation] CancellationToken cancellationToken) { var modelDescribe = await GetModelAsync(modelId); 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 67333e44..c41c1073 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 @@ -2,6 +2,7 @@ using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Domain.Managers; @@ -16,15 +17,30 @@ public class AiMessageManager : DomainService } /// - /// 创建消息 + /// 创建系统消息 /// /// /// /// /// - public async Task CreateMessageAsync(Guid userId, Guid sessionId, MessageInputDto input) + public async Task CreateSystemMessageAsync(Guid userId, Guid sessionId, MessageInputDto input) { - var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId); + input.Role = "system"; + var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); + await _repository.InsertAsync(message); + } + + /// + /// 创建系统消息 + /// + /// + /// + /// + /// + 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); await _repository.InsertAsync(message); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs new file mode 100644 index 00000000..40077531 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs @@ -0,0 +1,37 @@ +using Medallion.Threading; +using Volo.Abp.Domain.Services; +using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.SqlSugarCore.Abstractions; + +public class UsageStatisticsManager : DomainService +{ + private readonly ISqlSugarRepository _repository; + + private IDistributedLockProvider DistributedLock => + LazyServiceProvider.LazyGetRequiredService(); + + public async Task SetUsageAsync(Guid userId, string modelId, int inputTokenCount, int outputTokenCount) + { + await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId.ToString()}")) + { + var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId); + //存在数据,更细 + if (entity is not null) + { + entity.AddOnceChat(inputTokenCount, outputTokenCount); + await _repository.UpdateAsync(entity); + } + //不存在插入 + else + { + var usage = new UsageStatisticsAggregateRoot(userId, modelId); + usage.AddOnceChat(inputTokenCount, outputTokenCount); + await _repository.InsertAsync(usage); + } + } + } +} + +internal class LazyServiceProvider +{ +} \ 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 dcca5457..17f70e7b 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 @@ -3,7 +3,7 @@ - + diff --git a/Yi.Ai.Vue3/.env.development b/Yi.Ai.Vue3/.env.development index ce807789..60773b7c 100644 --- a/Yi.Ai.Vue3/.env.development +++ b/Yi.Ai.Vue3/.env.development @@ -11,8 +11,8 @@ VITE_WEB_ENV = 'development' VITE_WEB_BASE_API = '/dev-api' # 本地接口 -# VITE_API_URL = http://localhost:19001/api/app -VITE_API_URL=http://ccnetcore.com:19001/api/app + VITE_API_URL = http://localhost:19001/api/app +#VITE_API_URL=http://ccnetcore.com:19001/api/app # SSO单点登录url # SSO_SEVER_URL='http://localhost:18001'