From 0983837ff7dd7060a13d95e1a0f48c90a27fc8e7 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Thu, 22 Jan 2026 00:36:38 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E6=AD=A3=E7=A1=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=20Anthropic=20=E6=B5=81=E5=BC=8F=E5=93=8D=E5=BA=94=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在解析流式数据时增加对 [DONE] 结束标记的判断,避免在流结束后继续反序列化数据导致异常。 --- .../ThorClaude/Chats/AnthropicChatCompletionsService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs index 83986cb8..ee84d1f6 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs @@ -171,6 +171,12 @@ public class AnthropicChatCompletionsService( data = line[OpenAIConstant.Data.Length..].Trim(); + // 处理流结束标记 + if (data == "[DONE]") + { + break; + } + var result = JsonSerializer.Deserialize(data, ThorJsonSerializer.DefaultOptions); From b8c79ac61c3ce2bc4055ea02dd73317b76ff3ccd Mon Sep 17 00:00:00 2001 From: chenchun Date: Fri, 23 Jan 2026 14:50:46 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BF=9124?= =?UTF-8?q?=E5=B0=8F=E6=97=B6=E6=AF=8F=E5=B0=8F=E6=97=B6=E4=B8=8E=E4=BB=8A?= =?UTF-8?q?=E6=97=A5=E6=A8=A1=E5=9E=8B=E4=BD=BF=E7=94=A8=E9=87=8F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3=E5=8F=8A=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UsageStatistics/HourlyTokenUsageDto.cs | 22 ++++ .../UsageStatistics/ModelTodayUsageDto.cs | 22 ++++ .../UsageStatistics/ModelTokenBreakdownDto.cs | 17 +++ .../IServices/IUsageStatisticsService.cs | 12 ++ .../Services/UsageStatisticsService.cs | 104 +++++++++++++++++- 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/HourlyTokenUsageDto.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTokenBreakdownDto.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/HourlyTokenUsageDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/HourlyTokenUsageDto.cs new file mode 100644 index 00000000..ef86c78e --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/HourlyTokenUsageDto.cs @@ -0,0 +1,22 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +/// +/// 每小时Token使用量统计DTO(柱状图) +/// +public class HourlyTokenUsageDto +{ + /// + /// 小时时间点 + /// + public DateTime Hour { get; set; } + + /// + /// 该小时总Token消耗量 + /// + public long TotalTokens { get; set; } + + /// + /// 各模型Token消耗明细 + /// + public List ModelBreakdown { get; set; } = new(); +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs new file mode 100644 index 00000000..afc163e5 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs @@ -0,0 +1,22 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +/// +/// 模型今日使用量统计DTO(卡片列表) +/// +public class ModelTodayUsageDto +{ + /// + /// 模型ID + /// + public string ModelId { get; set; } + + /// + /// 今日使用次数 + /// + public int UsageCount { get; set; } + + /// + /// 今日消耗总Token数 + /// + public long TotalTokens { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTokenBreakdownDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTokenBreakdownDto.cs new file mode 100644 index 00000000..8c1e831e --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTokenBreakdownDto.cs @@ -0,0 +1,17 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +/// +/// 模型Token堆叠数据DTO(用于柱状图) +/// +public class ModelTokenBreakdownDto +{ + /// + /// 模型ID + /// + public string ModelId { get; set; } + + /// + /// Token消耗量 + /// + public long Tokens { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs index 99be00a6..31a2ef87 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs @@ -24,4 +24,16 @@ public interface IUsageStatisticsService /// /// 尊享服务Token用量统计 Task GetPremiumTokenUsageAsync(); + + /// + /// 获取当前用户近24小时每小时Token消耗统计(柱状图) + /// + /// 每小时Token使用量列表,包含各模型堆叠数据 + Task> GetLast24HoursTokenUsageAsync(UsageStatisticsGetInput input); + + /// + /// 获取当前用户今日各模型使用量统计(卡片列表) + /// + /// 模型今日使用量列表,包含使用次数和总Token + Task> GetTodayModelUsageAsync(UsageStatisticsGetInput input); } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs index 6f988d41..d3d38bae 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs @@ -57,7 +57,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic // 从Message表统计近7天的token消耗 var dailyUsage = await _messageRepository._DbQueryable .Where(x => x.UserId == userId) - .Where(x => x.Role == "assistant" || x.Role == "system") + .Where(x => x.Role == "system") .Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1)) .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) .GroupBy(x => x.CreationTime.Date) @@ -228,4 +228,106 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic return result; } + + /// + /// 获取当前用户近24小时每小时Token消耗统计(柱状图) + /// + /// 每小时Token使用量列表,包含各模型堆叠数据 + public async Task> GetLast24HoursTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) + { + var userId = CurrentUser.GetId(); + var now = DateTime.Now; + var startTime = now.AddHours(-23); // 滚动24小时,从23小时前到现在 + var startHour = new DateTime(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, 0, 0); + + // 从Message表查询近24小时的数据,只选择需要的字段 + var messages = await _messageRepository._DbQueryable + .Where(x => x.UserId == userId) + .Where(x => x.Role == "system") + .Where(x => x.CreationTime >= startHour) + .WhereIF(input.TokenId.HasValue, x => x.TokenId == input.TokenId) + .Select(x => new + { + x.CreationTime, + x.ModelId, + x.TokenUsage.TotalTokenCount + }) + .ToListAsync(); + + // 在内存中按小时和模型分组统计 + var hourlyGrouped = messages + .GroupBy(x => new + { + Hour = new DateTime(x.CreationTime.Year, x.CreationTime.Month, x.CreationTime.Day, x.CreationTime.Hour, 0, 0), + x.ModelId + }) + .Select(g => new + { + g.Key.Hour, + g.Key.ModelId, + Tokens = g.Sum(x => x.TotalTokenCount) + }) + .ToList(); + + // 生成完整的24小时数据 + var result = new List(); + for (int i = 0; i < 24; i++) + { + var hour = startHour.AddHours(i); + var hourData = hourlyGrouped.Where(x => x.Hour == hour).ToList(); + + var modelBreakdown = hourData.Select(x => new ModelTokenBreakdownDto + { + ModelId = x.ModelId, + Tokens = x.Tokens + }).ToList(); + + result.Add(new HourlyTokenUsageDto + { + Hour = hour, + TotalTokens = modelBreakdown.Sum(x => x.Tokens), + ModelBreakdown = modelBreakdown + }); + } + + return result; + } + + /// + /// 获取当前用户今日各模型使用量统计(卡片列表) + /// + /// 模型今日使用量列表,包含使用次数和总Token + public async Task> GetTodayModelUsageAsync([FromQuery]UsageStatisticsGetInput input) + { + var userId = CurrentUser.GetId(); + var todayStart = DateTime.Today; // 今天凌晨0点 + var tomorrowStart = todayStart.AddDays(1); + + // 从Message表查询今天的数据,只选择需要的字段 + var messages = await _messageRepository._DbQueryable + .Where(x => x.UserId == userId) + .Where(x => x.Role == "system") + .Where(x => x.CreationTime >= todayStart && x.CreationTime < tomorrowStart) + .WhereIF(input.TokenId.HasValue, x => x.TokenId == input.TokenId) + .Select(x => new + { + x.ModelId, + x.TokenUsage.TotalTokenCount + }) + .ToListAsync(); + + // 在内存中按模型分组统计 + var modelStats = messages + .GroupBy(x => x.ModelId) + .Select(g => new ModelTodayUsageDto + { + ModelId = g.Key, + UsageCount = g.Count(), + TotalTokens = g.Sum(x => x.TotalTokenCount) + }) + .OrderByDescending(x => x.TotalTokens) + .ToList(); + + return modelStats; + } } \ No newline at end of file From 87c93534a54e72a4ed8c24dccb5025670ed53d5e Mon Sep 17 00:00:00 2001 From: chenchun Date: Fri, 23 Jan 2026 16:43:35 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E9=9D=99=E6=80=81=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=85=81=E8=AE=B8=E6=9C=AA=E7=9F=A5?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=B1=BB=E5=9E=8B=E5=B9=B6=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=20Content-Type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index da304784..5529f1e8 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -402,9 +402,11 @@ namespace Yi.Abp.Web { Mappings = { - [".wxss"] = "text/css" + [".wxss"] = "text/css", } - } + }, + ServeUnknownFileTypes = true, + DefaultContentType = "application/octet-stream" }); app.UseDefaultFiles(); app.UseDirectoryBrowser("/api/app/wwwroot"); From caa90cc2277b062149166db0850719fd480e8132 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Fri, 23 Jan 2026 22:13:51 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E4=BB=8A=E6=97=A5=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E4=BD=BF=E7=94=A8=E7=BB=9F=E8=AE=A1=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=9B=BE=E6=A0=87=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 GetTodayModelUsage 接口补充模型图标数据,新增 ModelTodayUsageDto.IconUrl 字段 通过 ModelManager 查询已启用模型的 IconUrl 并映射到结果中 同时统一部分代码格式,提升可读性 --- .../UsageStatistics/ModelTodayUsageDto.cs | 5 +++ .../Services/UsageStatisticsService.cs | 37 +++++++++++++++---- .../Managers/ModelManager.cs | 2 +- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs index afc163e5..08caab2d 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/ModelTodayUsageDto.cs @@ -19,4 +19,9 @@ public class ModelTodayUsageDto /// 今日消耗总Token数 /// public long TotalTokens { get; set; } + + /// + /// 模型图标URL + /// + public string? IconUrl { get; set; } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs index d3d38bae..9ba060be 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs @@ -30,6 +30,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic private readonly ISqlSugarRepository _premiumPackageRepository; private readonly ISqlSugarRepository _tokenRepository; private readonly ModelManager _modelManager; + public UsageStatisticsService( ISqlSugarRepository messageRepository, ISqlSugarRepository usageStatisticsRepository, @@ -48,7 +49,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户近7天的Token消耗统计 /// /// 每日Token使用量列表 - public async Task> GetLast7DaysTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) + public async Task> GetLast7DaysTokenUsageAsync([FromQuery] UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); var endDate = DateTime.Today; @@ -59,7 +60,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic .Where(x => x.UserId == userId) .Where(x => x.Role == "system") .Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1)) - .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) + .WhereIF(input.TokenId.HasValue, x => x.TokenId == input.TokenId) .GroupBy(x => x.CreationTime.Date) .Select(g => new { @@ -89,14 +90,14 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户各个模型的Token消耗量及占比 /// /// 模型Token使用量列表 - public async Task> GetModelTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) + public async Task> GetModelTokenUsageAsync([FromQuery] UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); // 从UsageStatistics表获取各模型的token消耗统计(按ModelId聚合,因为同一模型可能有多个TokenId的记录) var modelUsages = await _usageStatisticsRepository._DbQueryable .Where(x => x.UserId == userId) - .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) + .WhereIF(input.TokenId.HasValue, x => x.TokenId == input.TokenId) .GroupBy(x => x.ModelId) .Select(x => new { @@ -221,7 +222,9 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic var result = tokenUsages.Select(x => new TokenPremiumUsageDto { TokenId = x.TokenId, - TokenName = x.TokenId == Guid.Empty ? "默认" : (tokenNameDict.TryGetValue(x.TokenId, out var name) ? name : "其他"), + TokenName = x.TokenId == Guid.Empty + ? "默认" + : (tokenNameDict.TryGetValue(x.TokenId, out var name) ? name : "其他"), Tokens = x.TotalTokenCount, Percentage = totalTokens > 0 ? Math.Round((decimal)x.TotalTokenCount / totalTokens * 100, 2) : 0 }).OrderByDescending(x => x.Tokens).ToList(); @@ -233,7 +236,8 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户近24小时每小时Token消耗统计(柱状图) /// /// 每小时Token使用量列表,包含各模型堆叠数据 - public async Task> GetLast24HoursTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) + public async Task> GetLast24HoursTokenUsageAsync( + [FromQuery] UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); var now = DateTime.Now; @@ -258,7 +262,8 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic var hourlyGrouped = messages .GroupBy(x => new { - Hour = new DateTime(x.CreationTime.Year, x.CreationTime.Month, x.CreationTime.Day, x.CreationTime.Hour, 0, 0), + Hour = new DateTime(x.CreationTime.Year, x.CreationTime.Month, x.CreationTime.Day, x.CreationTime.Hour, + 0, 0), x.ModelId }) .Select(g => new @@ -297,7 +302,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户今日各模型使用量统计(卡片列表) /// /// 模型今日使用量列表,包含使用次数和总Token - public async Task> GetTodayModelUsageAsync([FromQuery]UsageStatisticsGetInput input) + public async Task> GetTodayModelUsageAsync([FromQuery] UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); var todayStart = DateTime.Today; // 今天凌晨0点 @@ -328,6 +333,22 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic .OrderByDescending(x => x.TotalTokens) .ToList(); + if (modelStats.Count > 0) + { + var modelIds = modelStats.Select(x => x.ModelId).ToList(); + var modelDic = await _modelManager._aiModelRepository._DbQueryable.Where(x => modelIds.Contains(x.ModelId)) + .Distinct() + .Where(x=>x.IsEnabled) + .ToDictionaryAsync(x => x.ModelId, y => y.IconUrl); + modelStats.ForEach(x => + { + if (modelDic.TryGetValue(x.ModelId, out var icon)) + { + x.IconUrl = icon; + } + }); + } + return modelStats; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ModelManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ModelManager.cs index 1bbe0ea7..a9dd91c1 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ModelManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ModelManager.cs @@ -11,7 +11,7 @@ namespace Yi.Framework.AiHub.Domain.Managers; /// public class ModelManager : DomainService { - private readonly ISqlSugarRepository _aiModelRepository; + public readonly ISqlSugarRepository _aiModelRepository; private readonly IDistributedCache, string> _distributedCache; private readonly ILogger _logger; private const string PREMIUM_MODEL_IDS_CACHE_KEY = "PremiumModelIds"; From 6b86957556d3c3015d69218f7395bf6b09d81d82 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Sat, 24 Jan 2026 01:16:38 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=94=A8=E9=87=8F=E5=85=B3=E8=81=94=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=B9=B6=E4=BC=98=E5=8C=96=E7=BB=86=E8=8A=82=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复前端工具调用中用量统计先后顺序导致未正确绑定的问题 - 优化聊天代理指令文案,补充平台知识库优先策略说明 - 调整聊天列表滚动条样式,提升界面体验 - 移除未使用的 VITE_BUILD_COMPRESS 类型声明 --- .../Managers/ChatManager.cs | 8 +- .../Mcp/YxaiKnowledgeTool.cs | 115 ++++++++++++++++++ Yi.Ai.Vue3/src/pages/chat/agent/index.vue | 29 ++++- 3 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Mcp/YxaiKnowledgeTool.cs 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 9c398486..1cdde9c6 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 @@ -100,9 +100,11 @@ public class ChatManager : DomainService ChatOptions = new() { Instructions = """ - 你是一个专业的智能助手,是 “意心Ai” 平台自研的 “橙子小弟” 智能体 - 擅长于精准解决用户提出的各类问题 - 但性格暴躁,脏话连篇,喜欢说幽默讽刺的话,攻击性极强 + 你是一个专业的智能助手,是 “意心Ai” 平台自研的 “橙子小弟” 智能体。 + 擅长于精准解决用户提出的各类问题。 + 但性格暴躁,脏话连篇,喜欢说幽默讽刺的话,攻击性极强。 + 对你的制造者“橙子老哥”无比崇拜,认为他是最优秀的人。 + 涉及到“意心Ai平台”相关问题,优先调用YxaiKnowledgeDirectory和YxaiKnowledge查找意心Ai知识库内容 """ }, Name = "橙子小弟", diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Mcp/YxaiKnowledgeTool.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Mcp/YxaiKnowledgeTool.cs new file mode 100644 index 00000000..cbaf42cf --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Mcp/YxaiKnowledgeTool.cs @@ -0,0 +1,115 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Yi.Framework.AiHub.Domain.Shared.Attributes; + +namespace Yi.Framework.AiHub.Domain.Mcp; + +[YiAgentTool] +public class YxaiKnowledgeTool : ISingletonDependency +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + private const string DirectoryUrl = + "https://ccnetcore.com/prod-api/article/all/discuss-id/3a1efdde-dbff-aa86-d843-00278a8c1839"; + + private const string ContentUrlTemplate = "https://ccnetcore.com/prod-api/article/{0}"; + + public YxaiKnowledgeTool( + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + [YiAgentTool("意心Ai平台知识库目录"), DisplayName("YxaiKnowledgeDirectory"), + Description("获取意心AI相关内容的知识库目录列表")] + public async Task> YxaiKnowledgeDirectory() + { + try + { + var client = _httpClientFactory.CreateClient(); + var response = await client.GetAsync(DirectoryUrl); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("意心知识库目录接口调用失败: {StatusCode}", response.StatusCode); + return new List(); + } + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json, YxaiKnowledgeJsonContext.Default.ListYxaiKnowledgeDirectoryItem); + + return result ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取意心知识库目录发生异常"); + return new List(); + } + } + + [YiAgentTool("意心Ai平台知识库内容"), DisplayName("YxaiKnowledge"), + Description("根据目录ID获取意心AI知识库的具体内容")] + public async Task YxaiKnowledge([Description("知识库目录ID")] string directoryId) + { + if (string.IsNullOrWhiteSpace(directoryId)) + { + return "目录ID不能为空"; + } + + try + { + var client = _httpClientFactory.CreateClient(); + var url = string.Format(ContentUrlTemplate, directoryId); + var response = await client.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("意心知识库内容接口调用失败: {StatusCode}, DirectoryId: {DirectoryId}", + response.StatusCode, directoryId); + return "获取知识库内容失败"; + } + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json, YxaiKnowledgeJsonContext.Default.YxaiKnowledgeContentResponse); + + return result?.Content ?? "未找到相关内容"; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取意心知识库内容发生异常, DirectoryId: {DirectoryId}", directoryId); + return "获取知识库内容发生异常"; + } + } +} + +#region DTO + +public class YxaiKnowledgeDirectoryItem +{ + [JsonPropertyName("id")] public string Id { get; set; } = ""; + + [JsonPropertyName("name")] public string Name { get; set; } = ""; +} + +public class YxaiKnowledgeContentResponse +{ + [JsonPropertyName("content")] public string? Content { get; set; } +} + +#endregion + +#region JSON 序列化上下文 + +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(YxaiKnowledgeContentResponse))] +internal partial class YxaiKnowledgeJsonContext : JsonSerializerContext +{ +} + +#endregion diff --git a/Yi.Ai.Vue3/src/pages/chat/agent/index.vue b/Yi.Ai.Vue3/src/pages/chat/agent/index.vue index b4d5f2bd..21f8fc8d 100644 --- a/Yi.Ai.Vue3/src/pages/chat/agent/index.vue +++ b/Yi.Ai.Vue3/src/pages/chat/agent/index.vue @@ -235,12 +235,16 @@ function handleAgentChunk(data: AgentResultOutput) { case 'toolCalling': // 工具调用中 if (!latest.toolCalls) latest.toolCalls = []; - latest.toolCalls.push({ + const newToolCall: { name: string; status: 'calling' | 'called'; result?: any; usage?: { prompt: number; completion: number; total: number } } = { name: data.content as string, status: 'calling', - }); - // 清空待处理的用量 - pendingToolUsage = null; + }; + // 如果有待处理的用量(toolCallUsage 先于 toolCalling 到达),设置到这个工具调用 + if (pendingToolUsage) { + newToolCall.usage = pendingToolUsage; + pendingToolUsage = null; + } + latest.toolCalls.push(newToolCall); break; case 'toolCallUsage': // 工具调用用量统计 - 先保存,等 toolCalled 时再设置 @@ -626,6 +630,23 @@ function cancelSSE() { flex: 1; overflow-y: auto; padding: 8px; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--el-border-color-lighter); + border-radius: 2px; + + &:hover { + background: var(--el-border-color); + } + } } .session-item { From 1d5bca773f9fa65b08fd409bd715ebcc90aac6fe Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Sat, 24 Jan 2026 15:05:24 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E7=94=A8=E9=87=8F=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Ai.Vue3/pnpm-lock.yaml | 50 +- Yi.Ai.Vue3/src/api/model/index.ts | 33 + .../components/UsageStatistics.vue | 867 +++++++++++++++++- 3 files changed, 910 insertions(+), 40 deletions(-) diff --git a/Yi.Ai.Vue3/pnpm-lock.yaml b/Yi.Ai.Vue3/pnpm-lock.yaml index 72eff0bf..db99a03f 100644 --- a/Yi.Ai.Vue3/pnpm-lock.yaml +++ b/Yi.Ai.Vue3/pnpm-lock.yaml @@ -158,7 +158,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^4.16.2 - version: 4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@changesets/cli': specifier: ^2.29.5 version: 2.29.5 @@ -188,7 +188,7 @@ importers: version: 8.6.14(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2)) '@storybook/experimental-addon-test': specifier: ^8.6.14 - version: 8.6.14(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4) '@storybook/manager-api': specifier: ^8.6.14 version: 8.6.14(storybook@8.6.14(prettier@3.6.2)) @@ -227,7 +227,7 @@ importers: version: 3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) '@vue/tsconfig': specifier: ^0.7.0 version: 0.7.0(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)) @@ -427,28 +427,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.36.3': resolution: {integrity: sha512-2XRmNYuovZu0Pa4J3or4PKMkQZnXXfpVcCrPwWB/2ytX7XUo+TWLgYE8rPVnJOyw5zujkveFb0XUrro9mQgLzw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.36.3': resolution: {integrity: sha512-mTwPRbBi1feGqR2b5TWC5gkEDeRi8wfk4euF5sKNihfMGHj6pdfINHQ3QvLVO4C7z0r/wgWLAvditFA0b997dg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.36.3': resolution: {integrity: sha512-tMGPrT+zuZzJK6n1cD1kOii7HYZE9gUXjwtVNE/uZqXEaWP6lmkfoTMbLjnxEe74VQbmaoDGh1/cjrDBnqC6Uw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.36.3': resolution: {integrity: sha512-7pFyr9+dyV+4cBJJ1I57gg6PDXP3GBQeVAsEEitzEruxx4Hb4cyNro54gGtlsS+6ty+N0t004tPQxYO2VrsPIg==} @@ -1245,35 +1241,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.84': resolution: {integrity: sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.84': resolution: {integrity: sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.84': resolution: {integrity: sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.84': resolution: {integrity: sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.84': resolution: {integrity: sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og==} @@ -1330,42 +1321,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -1450,67 +1435,56 @@ packages: resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.41.1': resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.41.1': resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.41.1': resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.41.1': resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.41.1': resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.41.1': resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.41.1': resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.41.1': resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.41.1': resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.41.1': resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} @@ -6913,8 +6887,8 @@ packages: vue-component-type-helpers@2.2.12: resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} - vue-component-type-helpers@3.1.8: - resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==} + vue-component-type-helpers@3.2.3: + resolution: {integrity: sha512-lpJTa8a+12Cgy/n5OdlQTzQhSWOCu+6zQoNFbl3KYxwAoB95mYIgMLKEYMvQykPJ2ucBDjJJISdIBHc1d9Hd3w==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -7133,7 +7107,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@antfu/eslint-config@4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -7142,7 +7116,7 @@ snapshots: '@stylistic/eslint-plugin': 5.2.0(eslint@9.31.0(jiti@2.4.2)) '@typescript-eslint/eslint-plugin': 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - '@vitest/eslint-plugin': 1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + '@vitest/eslint-plugin': 1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) ansis: 4.1.0 cac: 6.7.14 eslint: 9.31.0(jiti@2.4.2) @@ -8494,7 +8468,7 @@ snapshots: dependencies: type-fest: 2.19.0 - '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.1.0))(react@19.1.0) @@ -8639,7 +8613,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.17(typescript@5.8.3) - vue-component-type-helpers: 3.1.8 + vue-component-type-helpers: 3.2.3 '@stylistic/eslint-plugin@5.2.0(eslint@9.31.0(jiti@2.4.2))': dependencies: @@ -9301,7 +9275,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9322,7 +9296,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@vitest/eslint-plugin@1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': dependencies: '@typescript-eslint/utils': 8.33.1(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.31.0(jiti@2.4.2) @@ -14721,7 +14695,7 @@ snapshots: vue-component-type-helpers@2.2.12: {} - vue-component-type-helpers@3.1.8: {} + vue-component-type-helpers@3.2.3: {} vue-demi@0.14.10(vue@3.5.17(typescript@5.8.3)): dependencies: diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index 10055c14..453de779 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -211,3 +211,36 @@ export function getPremiumPackageTokenUsage() { "percentage": 0 } ] */ + +// 获取当前用户近24小时每小时Token消耗统计 +export function getLast24HoursTokenUsage() { + return get('/usage-statistics/last24Hours-token-usage').json(); +} +/* 返回数据 + [ + { + "hour": "2026-01-23T13:32:49.237Z", + "totalTokens": 0, + "modelBreakdown": [ + { + "modelId": "string", + "tokens": 0 + } + ] + } + ] +*/ + +// 获取当前用户今日各模型使用量统计 +export function getTodayModelUsage() { + return get('/usage-statistics/today-model-usage').json(); +} +/* 返回数据 + [ + { + "modelId": "string", + "usageCount": 0, + "totalTokens": 0 + } + ] +*/ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue index 7dad9735..a20af923 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue @@ -3,6 +3,7 @@ import { FullScreen, PieChart } from '@element-plus/icons-vue'; import { useElementSize } from '@vueuse/core'; import { BarChart, PieChart as EPieChart, LineChart } from 'echarts/charts'; import { + BrushComponent, GraphicComponent, GridComponent, LegendComponent, @@ -11,7 +12,7 @@ import { } from 'echarts/components'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo } from '@/api'; +import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo, getLast24HoursTokenUsage, getTodayModelUsage } from '@/api'; // 注册必要的组件 echarts.use([ @@ -23,6 +24,7 @@ echarts.use([ GridComponent, LegendComponent, GraphicComponent, + BrushComponent, CanvasRenderer, ]); @@ -30,9 +32,11 @@ echarts.use([ const lineChart = ref(null); const pieChart = ref(null); const barChart = ref(null); +const hourlyBarChart = ref(null); // 新增:近24小时每小时Token消耗柱状图 let lineChartInstance: any = null; let pieChartInstance: any = null; let barChartInstance: any = null; +let hourlyBarChartInstance: any = null; // 新增:近24小时柱状图实例 // 全屏状态 const isFullscreen = ref(false); @@ -47,6 +51,8 @@ const loading = ref(false); const totalTokens = ref(0); const usageData = ref([]); const modelUsageData = ref([]); +const hourlyUsageData = ref([]); // 新增:近24小时每小时Token消耗数据 +const todayModelUsageData = ref([]); // 新增:今日各模型使用量数据 // Token选择相关 const selectedTokenId = ref(''); // 空字符串表示查询全部 @@ -64,6 +70,74 @@ const selectedTokenName = computed(() => { return token?.name || '未知API密钥'; }); +// 计算属性:获取近24小时数据中所有唯一的模型ID(按总token使用量排序,只统计有数据的模型) +const hourlyModels = computed(() => { + const modelTokenMap = new Map(); + hourlyUsageData.value.forEach((hour) => { + hour.modelBreakdown?.forEach((model: any) => { + // 只统计有实际使用量的模型 + if (model.tokens > 0) { + const existing = modelTokenMap.get(model.modelId); + modelTokenMap.set(model.modelId, { + tokens: (existing?.tokens || 0) + model.tokens, + iconUrl: model.iconUrl || '', + }); + } + }); + }); + return Array.from(modelTokenMap.entries()) + .sort((a, b) => b[1].tokens - a[1].tokens) + .map(([modelId, data]) => ({ modelId, iconUrl: data.iconUrl })); +}); + +// 计算属性:模型颜色映射(保持一致性) +const modelColors = computed(() => { + const baseColors = [ + '#3a4de9', '#6a5acd', '#9370db', '#8a2be2', '#9932cc', + '#ba55d3', '#da70d6', '#ee82ee', '#dda0dd', '#ff00ff', + '#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', + '#00f2fe', '#43e97b', '#38f9d7', '#fa709a', '#fee140', + '#4361ee', '#3a0ca3', '#7209b7', '#f72585', '#4cc9f0', + ]; + const colorMap: Record = {}; + + // 先为近24小时数据的模型分配颜色 + hourlyModels.value.forEach(({ modelId }, index) => { + if (!colorMap[modelId]) { + colorMap[modelId] = baseColors[index % baseColors.length]; + } + }); + + // 为今日模型数据中没有颜色的模型分配颜色 + let colorIndex = hourlyModels.value.length; + todayModelUsageData.value.forEach((item: any) => { + if (!colorMap[item.modelId]) { + colorMap[item.modelId] = baseColors[colorIndex % baseColors.length]; + colorIndex++; + } + }); + + return colorMap; +}); + +// 计算属性:模型图标URL映射 +const modelIconUrls = computed(() => { + const iconMap: Record = {}; + hourlyModels.value.forEach(({ modelId, iconUrl }) => { + iconMap[modelId] = iconUrl; + }); + return iconMap; +}); + +// 计算属性:今日模型数据的图标映射 +const todayModelIcons = computed(() => { + const iconMap: Record = {}; + todayModelUsageData.value.forEach((item: any) => { + iconMap[item.modelId] = item.iconUrl || ''; + }); + return iconMap; +}); + // 获取可选择的Token列表 async function fetchTokenOptions() { try { @@ -93,13 +167,17 @@ async function fetchUsageData() { loading.value = true; try { const tokenId = selectedTokenId.value || undefined; - const [res, res2] = await Promise.all([ + const [res, res2, res3, res4] = await Promise.all([ getLast7DaysTokenUsage(tokenId), getModelTokenUsage(tokenId), + getLast24HoursTokenUsage(), + getTodayModelUsage(), ]); usageData.value = res.data || []; modelUsageData.value = res2.data || []; + hourlyUsageData.value = res3.data || []; + todayModelUsageData.value = res4.data || []; totalTokens.value = usageData.value.reduce((sum, item) => sum + item.tokens, 0); updateCharts(); @@ -124,6 +202,9 @@ function initCharts() { if (barChart.value) { barChartInstance = echarts.init(barChart.value); } + if (hourlyBarChart.value) { + hourlyBarChartInstance = echarts.init(hourlyBarChart.value); + } window.addEventListener('resize', resizeCharts); } @@ -133,6 +214,7 @@ function updateCharts() { updateLineChart(); updatePieChart(); updateBarChart(); + updateHourlyBarChart(); // 新增:更新近24小时柱状图 } // 更新折线图 @@ -512,11 +594,473 @@ function updateBarChart() { barChartInstance.setOption(option, true); } +// 更新近24小时每小时Token消耗柱状图 +function updateHourlyBarChart() { + if (!hourlyBarChartInstance) + return; + + // 空数据状态 + if (hourlyUsageData.value.length === 0 || hourlyModels.value.length === 0) { + const emptyOption = { + graphic: [ + { + type: 'group', + left: 'center', + top: 'center', + children: [ + { + type: 'rect', + shape: { + width: 160, + height: 160, + r: 12, + }, + style: { + fill: '#f5f7fa', + stroke: '#e9ecef', + lineWidth: 2, + }, + left: -80, + top: -80, + }, + { + type: 'text', + style: { + text: '📊', + fontSize: 48, + x: -24, + y: -40, + }, + }, + { + type: 'text', + style: { + text: '暂无数据', + fontSize: 18, + fontWeight: 'bold', + fill: '#909399', + x: -36, + y: 20, + }, + }, + { + type: 'text', + style: { + text: '近24小时暂无使用记录', + fontSize: 14, + fill: '#c0c4cc', + x: -80, + y: 50, + }, + }, + ], + }, + ], + }; + hourlyBarChartInstance.setOption(emptyOption, true); + return; + } + + const hours = hourlyUsageData.value.map(item => { + const date = new Date(item.hour); + return `${date.getHours().toString().padStart(2, '0')}:00`; + }); + const totalTokens = hourlyUsageData.value.map(item => item.totalTokens); + + const isMobile = window.innerWidth < 768; + + // 找出24小时中单小时模型数量最多的值 + const maxModelsPerHour = hourlyUsageData.value.reduce((max, hour) => { + const count = hour.modelBreakdown?.length || 0; + return Math.max(max, count); + }, 0); + + // 智能计算柱子宽度:基于最大模型数量动态计算 + // 柱子宽度 = 单元格宽度 / 最大模型数量 - 间距占比 + let barWidth: number; + let barGap: number | string; + let barCategoryGap: number | string; + + // 估算每个时间段的可用宽度(像素) + // 假设图表容器宽度,移动端约320-375px,桌面端约1000-1400px + const containerWidth = isMobile ? 350 : 1200; + const hourCount = hourlyUsageData.value.length; + const categoryWidth = containerWidth / hourCount; // 每个小时类别的宽度 + + // 柱子间距配置(像素) + const gapBetweenBars = isMobile ? 3 : 6; // 柱子之间的间距 + const gapBetweenCategories = isMobile ? 8 : 15; // 类别之间的间距 + + // 计算柱子宽度:可用宽度除以最大模型数 + // 可用宽度 = 类别宽度 - 类别间距 - (柱子数-1)*柱子间距 + const availableWidth = categoryWidth - gapBetweenCategories - ((maxModelsPerHour - 1) * gapBetweenBars); + barWidth = Math.max(4, Math.floor(availableWidth / maxModelsPerHour)); // 最小4px + + // 将间距转换为百分比以便ECharts自适应 + barGap = `${(gapBetweenBars / barWidth) * 100}%`; + barCategoryGap = `${(gapBetweenCategories / categoryWidth) * 100}%`; + + // 限制最大柱子宽度,避免太少模型时柱子过粗 + const maxBarWidth = isMobile ? 25 : 60; + barWidth = Math.min(barWidth, maxBarWidth); + + // 构建每个模型的数据系列(并排柱状图) + const series = hourlyModels.value.map(({ modelId }, index) => { + const data = hourlyUsageData.value.map(hour => { + const modelData = hour.modelBreakdown?.find((m: any) => m.modelId === modelId); + return modelData?.tokens || 0; + }); + + // 根据最大模型数量决定是否显示标签 + const showLabel = maxModelsPerHour <= 4 && !isMobile; + + return { + name: modelId, + type: 'bar', + barWidth, + barGap, + barCategoryGap, + data, + itemStyle: { + color: modelColors.value[modelId], + borderRadius: [4, 4, 0, 0], + borderWidth: 0, + }, + label: { + show: showLabel, + position: 'top', + formatter: (params: any) => { + if (params.value === 0) + return ''; + return params.value >= 1000 ? `${(params.value / 1000).toFixed(1)}k` : params.value.toString(); + }, + fontSize: isMobile ? 9 : 11, + color: '#666', + }, + emphasis: { + focus: 'series', + itemStyle: { + shadowBlur: 8, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.2)', + }, + }, + // 确保系列按总使用量排序的顺序 + seriesIndex: index, + }; + }); + + const option = { + graphic: [], + calculable: true, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + label: { + show: true, + }, + }, + formatter: (params: any) => { + if (!params || params.length === 0) + return ''; + const hour = params[0].axisValue; + + // 过滤出有数据的模型并按token使用量降序 + const activeParams = params + .filter((p: any) => p.value > 0) + .sort((a: any, b: any) => b.value - a.value); + + if (activeParams.length === 0) + return `
${hour}
暂无数据
`; + + // 计算总token + const totalTokens = activeParams.reduce((sum: number, p: any) => sum + p.value, 0); + + let result = `
+
${hour}
+
总计: ${totalTokens.toLocaleString()} tokens
+
`; + + // 限制显示的模型数量(最多显示前8个) + const maxDisplay = 8; + const displayParams = activeParams.slice(0, maxDisplay); + + displayParams.forEach((param: any, index: number) => { + const percent = ((param.value / totalTokens) * 100).toFixed(1); + result += `
+ + + ${param.seriesName} + + + ${param.value.toLocaleString()} + ${percent}% + +
`; + }); + + // 如果有更多模型,显示提示 + if (activeParams.length > maxDisplay) { + result += `
+ 还有 ${activeParams.length - maxDisplay} 个模型未显示 +
`; + } + + return result; + }, + confine: true, + backgroundColor: 'rgba(255, 255, 255, 0.98)', + borderColor: '#e9ecef', + borderWidth: 1, + borderRadius: 8, + padding: [12, 16], + textStyle: { + fontSize: 13, + color: '#333', + }, + position(point: any, params: any, dom: any, rect: any, size: any) { + if (isMobile) { + return ['50%', '10%']; + } + return null; + }, + }, + toolbox: { + show: !isMobile, + feature: { + dataZoom: { + yAxisIndex: 'none', + title: { + zoom: '区域缩放', + back: '还原缩放', + }, + }, + brush: { + type: ['lineX', 'clear'], + title: { + lineX: '横向选择', + clear: '清除选择', + }, + }, + restore: { + show: true, + title: '还原', + }, + saveAsImage: { + show: true, + title: '保存为图片', + pixelRatio: 2, + }, + }, + right: 15, + top: 5, + itemSize: 14, + iconStyle: { + borderColor: '#667eea', + borderWidth: 1.5, + }, + emphasis: { + iconStyle: { + borderColor: '#764ba2', + }, + }, + tooltip: { + show: true, + position: 'bottom', + formatter: (param: any) => { + return param.title; + }, + textStyle: { + fontSize: 11, + }, + }, + }, + // 区域选框缩放配置 + brush: { + id: 'brush', + xAxisIndex: 0, + link: ['x'], + transform: { + type: 'bar', + }, + throttleType: 'debounce', + throttleDelay: 300, + removeOnClick: true, + brushLink: 'all', + brushType: false, + inBrush: { + opacity: 1, + }, + outOfBrush: { + opacity: 0.3, + }, + }, + legend: { + show: true, + type: hourlyModels.value.length > 8 || isMobile ? 'scroll' : 'plain', + top: isMobile ? '0%' : '3%', + left: 'center', + itemWidth: 14, + itemHeight: 10, + itemGap: isMobile ? 8 : 10, + textStyle: { + fontSize: isMobile ? 10 : 12, + color: '#666', + }, + data: hourlyModels.value.map(({ modelId }) => ({ + name: modelId, + icon: 'rect', + })), + pageTextStyle: { + color: '#999', + }, + pageIconColor: '#667eea', + pageIconInactiveColor: '#ccc', + pageButtonItemGap: 5, + }, + grid: { + top: maxModelsPerHour > 8 ? '15%' : '12%', + left: '1%', + right: isMobile ? '8%' : '10%', + bottom: isMobile ? '15%' : '12%', + containLabel: true, + }, + dataZoom: [ + { + show: !isMobile, + start: hourlyUsageData.value.length > 12 ? 100 - Math.round((12 / hourlyUsageData.value.length) * 100) : 80, + end: 100, + xAxisIndex: [0], + bottom: '3%', + height: 18, + borderColor: 'transparent', + fillerColor: 'rgba(102, 126, 234, 0.2)', + handleStyle: { + color: '#667eea', + }, + textStyle: { + color: '#999', + fontSize: 11, + }, + }, + { + type: 'inside', + start: hourlyUsageData.value.length > 12 ? 100 - Math.round((12 / hourlyUsageData.value.length) * 100) : 80, + end: 100, + xAxisIndex: [0], + zoomOnMouseWheel: true, + moveOnMouseMove: true, + moveOnMouseWheel: false, + }, + { + show: !isMobile && maxModelsPerHour > 3, + yAxisIndex: [0], + filterMode: 'empty', + width: 28, + height: '70%', + showDataShadow: false, + left: '96%', + borderColor: 'transparent', + fillerColor: 'rgba(102, 126, 234, 0.15)', + handleStyle: { + color: '#667eea', + }, + }, + ], + // 区域选框缩放配置(配合calculable使用) + brush: { + id: 'brush', + xAxisIndex: 0, + link: ['x'], + throttleType: 'debounce', + throttleDelay: 300, + removeOnClick: true, + brushLink: 'all', + brushType: false, + inBrush: { + opacity: 1, + }, + outOfBrush: { + opacity: 0.15, + }, + // 选框样式 + brushStyle: { + borderWidth: 1, + color: 'rgba(102, 126, 234, 0.15)', + borderColor: '#667eea', + }, + }, + xAxis: { + type: 'category', + data: hours, + axisLine: { + lineStyle: { + color: '#e9ecef', + width: 1, + }, + }, + axisTick: { + alignWithLabel: true, + length: 4, + }, + axisLabel: { + interval: isMobile ? 3 : 0, + rotate: isMobile ? 45 : 0, + fontSize: isMobile ? 10 : 12, + color: '#666', + fontFamily: 'monospace', + }, + }, + yAxis: { + type: 'value', + name: 'Token用量', + nameTextStyle: { + fontSize: isMobile ? 11 : 12, + color: '#999', + padding: [0, 0, 0, 0], + }, + axisLine: { + show: true, + lineStyle: { + color: '#e9ecef', + width: 1, + }, + }, + axisTick: { + show: false, + }, + axisLabel: { + fontSize: isMobile ? 10 : 12, + color: '#666', + formatter: (value: number) => { + if (value >= 1000000) + return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) + return `${(value / 1000).toFixed(0)}K`; + return value.toString(); + }, + }, + splitLine: { + lineStyle: { + color: '#f0f0f0', + type: 'dashed', + width: 1, + }, + }, + }, + series, + }; + + hourlyBarChartInstance.setOption(option, true); +} + // 调整图表大小 function resizeCharts() { lineChartInstance?.resize(); pieChartInstance?.resize(); barChartInstance?.resize(); + hourlyBarChartInstance?.resize(); } // 切换全屏 @@ -544,6 +1088,7 @@ onBeforeUnmount(() => { lineChartInstance?.dispose(); pieChartInstance?.dispose(); barChartInstance?.dispose(); + hourlyBarChartInstance?.dispose(); }); @@ -605,6 +1150,84 @@ onBeforeUnmount(() => { + + + +
+
+
+ + + + + +
+
+
📊
+
暂无数据
+
今日暂无模型使用记录
+
+
+
+
+
+ +
+ {{ item.modelId.charAt(0).toUpperCase() }} +
+
+
+
{{ item.modelId }}
+
+ + 使用 {{ item.usageCount }} 次 +
+
+
+ +
+
+ +
-
📊
-
暂无数据
-
今日暂无模型使用记录
+
+ 📊 +
+
+ 暂无数据 +
+
+ 今日暂无模型使用记录 +
{ :alt="item.modelId" class="model-logo" @error="(e) => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.classList.remove('fallback-icon'); }" - /> + >
{{ item.modelId.charAt(0).toUpperCase() }}
-
{{ item.modelId }}
+
+ {{ item.modelId }} +
使用 {{ item.usageCount }} 次