From caa90cc2277b062149166db0850719fd480e8132 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Fri, 23 Jan 2026 22:13:51 +0800 Subject: [PATCH 1/2] =?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 2/2] =?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 {