Merge remote-tracking branch 'origin/ai-hub' into ai-hub

This commit is contained in:
Gsh
2026-01-24 15:48:30 +08:00
6 changed files with 180 additions and 16 deletions

View File

@@ -19,4 +19,9 @@ public class ModelTodayUsageDto
/// 今日消耗总Token数
/// </summary>
public long TotalTokens { get; set; }
/// <summary>
/// 模型图标URL
/// </summary>
public string? IconUrl { get; set; }
}

View File

@@ -30,6 +30,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ModelManager _modelManager;
public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
@@ -48,7 +49,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync([FromQuery]UsageStatisticsGetInput input)
public async Task<List<DailyTokenUsageDto>> 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消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync([FromQuery]UsageStatisticsGetInput input)
public async Task<List<ModelTokenUsageDto>> 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消耗统计柱状图
/// </summary>
/// <returns>每小时Token使用量列表包含各模型堆叠数据</returns>
public async Task<List<HourlyTokenUsageDto>> GetLast24HoursTokenUsageAsync([FromQuery]UsageStatisticsGetInput input)
public async Task<List<HourlyTokenUsageDto>> 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
/// 获取当前用户今日各模型使用量统计(卡片列表)
/// </summary>
/// <returns>模型今日使用量列表包含使用次数和总Token</returns>
public async Task<List<ModelTodayUsageDto>> GetTodayModelUsageAsync([FromQuery]UsageStatisticsGetInput input)
public async Task<List<ModelTodayUsageDto>> 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<string>(x => x.ModelId, y => y.IconUrl);
modelStats.ForEach(x =>
{
if (modelDic.TryGetValue(x.ModelId, out var icon))
{
x.IconUrl = icon;
}
});
}
return modelStats;
}
}

View File

@@ -100,9 +100,11 @@ public class ChatManager : DomainService
ChatOptions = new()
{
Instructions = """
Ai
Ai
Ai平台YxaiKnowledgeDirectory和YxaiKnowledge查找意心Ai知识库内容
"""
},
Name = "橙子小弟",

View File

@@ -11,7 +11,7 @@ namespace Yi.Framework.AiHub.Domain.Managers;
/// </summary>
public class ModelManager : DomainService
{
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
public readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly IDistributedCache<List<string>, string> _distributedCache;
private readonly ILogger<ModelManager> _logger;
private const string PREMIUM_MODEL_IDS_CACHE_KEY = "PremiumModelIds";

View File

@@ -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<YxaiKnowledgeTool> _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<YxaiKnowledgeTool> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
[YiAgentTool("意心Ai平台知识库目录"), DisplayName("YxaiKnowledgeDirectory"),
Description("获取意心AI相关内容的知识库目录列表")]
public async Task<List<YxaiKnowledgeDirectoryItem>> YxaiKnowledgeDirectory()
{
try
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync(DirectoryUrl);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("意心知识库目录接口调用失败: {StatusCode}", response.StatusCode);
return new List<YxaiKnowledgeDirectoryItem>();
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize(json, YxaiKnowledgeJsonContext.Default.ListYxaiKnowledgeDirectoryItem);
return result ?? new List<YxaiKnowledgeDirectoryItem>();
}
catch (Exception ex)
{
_logger.LogError(ex, "获取意心知识库目录发生异常");
return new List<YxaiKnowledgeDirectoryItem>();
}
}
[YiAgentTool("意心Ai平台知识库内容"), DisplayName("YxaiKnowledge"),
Description("根据目录ID获取意心AI知识库的具体内容")]
public async Task<string> 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<YxaiKnowledgeDirectoryItem>))]
[JsonSerializable(typeof(YxaiKnowledgeContentResponse))]
internal partial class YxaiKnowledgeJsonContext : JsonSerializerContext
{
}
#endregion

View File

@@ -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 {