diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs new file mode 100644 index 00000000..ca6866ea --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// 创建Token输入 +/// +public class TokenCreateInput +{ + /// + /// 名称(同一用户不能重复) + /// + [Required(ErrorMessage = "名称不能为空")] + [StringLength(100, ErrorMessage = "名称长度不能超过100个字符")] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs new file mode 100644 index 00000000..6358985b --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs @@ -0,0 +1,47 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// Token列表输出 +/// +public class TokenGetListOutputDto +{ + /// + /// Token Id + /// + public Guid Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// Token密钥 + /// + public string ApiKey { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } + + /// + /// 尊享包已使用额度 + /// + public long PremiumUsedQuota { get; set; } + + /// + /// 是否禁用 + /// + public bool IsDisabled { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs new file mode 100644 index 00000000..445ef6f0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs @@ -0,0 +1,9 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +public class TokenSelectListOutputDto +{ + public Guid TokenId { get; set; } + public string Name { get; set; } + + public bool IsDisabled { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs new file mode 100644 index 00000000..e149d6d3 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// 编辑Token输入 +/// +public class TokenUpdateInput +{ + /// + /// Token Id + /// + [Required(ErrorMessage = "Id不能为空")] + public Guid Id { get; set; } + + /// + /// 名称(同一用户不能重复) + /// + [Required(ErrorMessage = "名称不能为空")] + [StringLength(100, ErrorMessage = "名称长度不能超过100个字符")] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs new file mode 100644 index 00000000..77bbbf18 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs @@ -0,0 +1,27 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +/// +/// 尊享包不同Token用量占比DTO(饼图) +/// +public class TokenPremiumUsageDto +{ + /// + /// Token Id + /// + public Guid TokenId { get; set; } + + /// + /// Token名称 + /// + public string TokenName { get; set; } + + /// + /// Token消耗量 + /// + public long Tokens { get; set; } + + /// + /// 占比(百分比) + /// + public decimal Percentage { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs new file mode 100644 index 00000000..98ed1fd4 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs @@ -0,0 +1,9 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +public class UsageStatisticsGetInput +{ + /// + /// tokenId + /// + public Guid? TokenId { get; set; } +} \ No newline at end of file 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 add4d348..99be00a6 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 @@ -11,13 +11,13 @@ public interface IUsageStatisticsService /// 获取当前用户近7天的Token消耗统计 /// /// 每日Token使用量列表 - Task> GetLast7DaysTokenUsageAsync(); + Task> GetLast7DaysTokenUsageAsync(UsageStatisticsGetInput input); /// /// 获取当前用户各个模型的Token消耗量及占比 /// /// 模型Token使用量列表 - Task> GetModelTokenUsageAsync(); + Task> GetModelTokenUsageAsync(UsageStatisticsGetInput input); /// /// 获取当前用户尊享服务Token用量统计 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs index f6d266e2..be069631 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs @@ -135,7 +135,7 @@ public class AiChatService : ApplicationService //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - CurrentUser.Id, sessionId, cancellationToken); + CurrentUser.Id, sessionId, null, cancellationToken); } @@ -172,6 +172,6 @@ public class AiChatService : ApplicationService //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - CurrentUser.Id, null, cancellationToken); + CurrentUser.Id, null, null, cancellationToken); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs index 65d30166..23e7e666 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs @@ -1,55 +1,245 @@ -using Microsoft.AspNetCore.Authorization; +using Dm.util; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos.Token; +using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Extensions; -using Yi.Framework.AiHub.Domain.Managers; +using Yi.Framework.AiHub.Domain.Shared.Consts; +using Yi.Framework.Ddd.Application.Contracts; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; +/// +/// Token服务 +/// +[Authorize] public class TokenService : ApplicationService { private readonly ISqlSugarRepository _tokenRepository; - private readonly TokenManager _tokenManager; + private readonly ISqlSugarRepository _usageStatisticsRepository; - /// - /// 构造函数 - /// - /// - /// - public TokenService(ISqlSugarRepository tokenRepository, TokenManager tokenManager) + public TokenService( + ISqlSugarRepository tokenRepository, + ISqlSugarRepository usageStatisticsRepository) { _tokenRepository = tokenRepository; - _tokenManager = tokenManager; + _usageStatisticsRepository = usageStatisticsRepository; } /// - /// 获取token + /// 获取当前用户的Token列表 /// - /// - [Authorize] - public async Task GetAsync() + [HttpGet("token/list")] + public async Task> GetListAsync([FromQuery] PagedAllResultRequestDto input) { - return new TokenOutput + RefAsync total = 0; + var userId = CurrentUser.GetId(); + + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.CreationTime) + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + + if (!tokens.Any()) { - ApiKey = await _tokenManager.GetAsync(CurrentUser.GetId()) - }; + return new PagedResultDto(); + } + + // 获取尊享包模型ID列表 + var premiumModelIds = PremiumPackageConst.ModeIds; + + // 批量查询所有Token的尊享包已使用额度 + var tokenIds = tokens.Select(t => t.Id).ToList(); + var usageStats = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && tokenIds.Contains(x.TokenId) && premiumModelIds.Contains(x.ModelId)) + .GroupBy(x => x.TokenId) + .Select(g => new + { + TokenId = g.TokenId, + UsedQuota = SqlFunc.AggregateSum(g.TotalTokenCount) + }) + .ToListAsync(); + + var result = tokens.Select(t => + { + var usedQuota = usageStats.FirstOrDefault(u => u.TokenId == t.Id)?.UsedQuota ?? 0; + return new TokenGetListOutputDto + { + Id = t.Id, + Name = t.Name, + ApiKey = t.Token, + ExpireTime = t.ExpireTime, + PremiumQuotaLimit = t.PremiumQuotaLimit, + PremiumUsedQuota = usedQuota, + IsDisabled = t.IsDisabled, + CreationTime = t.CreationTime + }; + }).ToList(); + + return new PagedResultDto(total, result); } - /// - /// 创建token - /// - /// - [Authorize] - public async Task CreateAsync() + [HttpGet("token/select-list")] + public async Task> GetSelectListAsync() { + var userId = CurrentUser.GetId(); + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId) + .OrderBy(x => x.IsDisabled) + .OrderByDescending(x => x.CreationTime) + .Select(x => new TokenSelectListOutputDto + { + TokenId = x.Id, + Name = x.Name, + IsDisabled = x.IsDisabled + }).ToListAsync(); + + tokens.Insert(0,new TokenSelectListOutputDto + { + TokenId = Guid.Empty, + Name = "默认", + IsDisabled = false + }); + return tokens; + } + + + /// + /// 创建Token + /// + [HttpPost("token")] + public async Task CreateAsync([FromBody] TokenCreateInput input) + { + var userId = CurrentUser.GetId(); + + // 检查用户是否为VIP if (!CurrentUser.IsAiVip()) { throw new UserFriendlyException("充值成为Vip,畅享第三方token服务"); } - await _tokenManager.CreateAsync(CurrentUser.GetId()); + // 检查名称是否重复 + var exists = await _tokenRepository._DbQueryable + .AnyAsync(x => x.UserId == userId && x.Name == input.Name); + if (exists) + { + throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称"); + } + + var token = new TokenAggregateRoot(userId, input.Name) + { + ExpireTime = input.ExpireTime, + PremiumQuotaLimit = input.PremiumQuotaLimit + }; + + await _tokenRepository.InsertAsync(token); + + return new TokenGetListOutputDto + { + Id = token.Id, + Name = token.Name, + ApiKey = token.Token, + ExpireTime = token.ExpireTime, + PremiumQuotaLimit = token.PremiumQuotaLimit, + PremiumUsedQuota = 0, + IsDisabled = token.IsDisabled, + CreationTime = token.CreationTime + }; + } + + /// + /// 编辑Token + /// + [HttpPut("token")] + public async Task UpdateAsync([FromBody] TokenUpdateInput input) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == input.Id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + // 检查名称是否重复(排除自己) + var exists = await _tokenRepository._DbQueryable + .AnyAsync(x => x.UserId == userId && x.Name == input.Name && x.Id != input.Id); + if (exists) + { + throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称"); + } + + token.Name = input.Name; + token.ExpireTime = input.ExpireTime; + token.PremiumQuotaLimit = input.PremiumQuotaLimit; + + await _tokenRepository.UpdateAsync(token); + } + + /// + /// 删除Token + /// + [HttpDelete("token/{id}")] + public async Task DeleteAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + await _tokenRepository.DeleteAsync(token); + } + + /// + /// 启用Token + /// + [HttpPost("token/{id}/enable")] + public async Task EnableAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + token.Enable(); + await _tokenRepository.UpdateAsync(token); + } + + /// + /// 禁用Token + /// + [HttpPost("token/{id}/disable")] + public async Task DisableAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + token.Disable(); + await _tokenRepository.UpdateAsync(token); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs index 64c71e10..f04ecf45 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs @@ -76,12 +76,12 @@ public class FileMasterService : ApplicationService if (input.Stream == true) { await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, null, cancellationToken); } else { await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, null, cancellationToken); } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs index 23cf473c..60145a29 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs @@ -55,7 +55,9 @@ public class OpenApiService : ApplicationService { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); //如果是尊享包服务,需要校验是是否尊享包足够 @@ -68,17 +70,17 @@ public class OpenApiService : ApplicationService throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); } } - + //ai网关代理httpcontext if (input.Stream == true) { await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, tokenId, cancellationToken); } else { await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, tokenId, cancellationToken); } } @@ -93,9 +95,11 @@ public class OpenApiService : ApplicationService { var httpContext = this._httpContextAccessor.HttpContext; Intercept(httpContext); - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); - await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input); + await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input, tokenId); } /// @@ -108,9 +112,11 @@ public class OpenApiService : ApplicationService { var httpContext = this._httpContextAccessor.HttpContext; Intercept(httpContext); - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); - await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input); + await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input, tokenId); } @@ -151,7 +157,9 @@ public class OpenApiService : ApplicationService { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); // 验证用户是否为VIP @@ -178,12 +186,12 @@ public class OpenApiService : ApplicationService if (input.Stream) { await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, tokenId, cancellationToken); } else { await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, tokenId, cancellationToken); } } 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 feedf690..bffce1e6 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 @@ -9,7 +9,9 @@ using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; using Yi.Framework.AiHub.Application.Contracts.IServices; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Extensions; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.Ddd.Application.Contracts; using Yi.Framework.SqlSugarCore.Abstractions; @@ -24,22 +26,25 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic private readonly ISqlSugarRepository _messageRepository; private readonly ISqlSugarRepository _usageStatisticsRepository; private readonly ISqlSugarRepository _premiumPackageRepository; + private readonly ISqlSugarRepository _tokenRepository; public UsageStatisticsService( ISqlSugarRepository messageRepository, ISqlSugarRepository usageStatisticsRepository, - ISqlSugarRepository premiumPackageRepository) + ISqlSugarRepository premiumPackageRepository, + ISqlSugarRepository tokenRepository) { _messageRepository = messageRepository; _usageStatisticsRepository = usageStatisticsRepository; _premiumPackageRepository = premiumPackageRepository; + _tokenRepository = tokenRepository; } /// /// 获取当前用户近7天的Token消耗统计 /// /// 每日Token使用量列表 - public async Task> GetLast7DaysTokenUsageAsync() + public async Task> GetLast7DaysTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); var endDate = DateTime.Today; @@ -50,6 +55,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic .Where(x => x.UserId == userId) .Where(x => x.Role == "assistant" || 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) .Select(g => new { @@ -79,17 +85,19 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户各个模型的Token消耗量及占比 /// /// 模型Token使用量列表 - public async Task> GetModelTokenUsageAsync() + public async Task> GetModelTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); - // 从UsageStatistics表获取各模型的token消耗统计 + // 从UsageStatistics表获取各模型的token消耗统计(按ModelId聚合,因为同一模型可能有多个TokenId的记录) var modelUsages = await _usageStatisticsRepository._DbQueryable .Where(x => x.UserId == userId) + .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) + .GroupBy(x => x.ModelId) .Select(x => new { - x.ModelId, - x.TotalTokenCount + ModelId = x.ModelId, + TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount) }) .ToListAsync(); @@ -164,4 +172,54 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic return new PagedResultDto(total, entities.Adapt>()); } + + /// + /// 获取当前用户尊享包不同Token用量占比(饼图) + /// + /// 各Token的尊享模型用量及占比 + [HttpGet("usage-statistics/premium-token-usage/by-token")] + public async Task> GetPremiumTokenUsageByTokenAsync() + { + var userId = CurrentUser.GetId(); + var premiumModelIds = PremiumPackageConst.ModeIds; + + // 从UsageStatistics表获取尊享模型的token消耗统计(按TokenId聚合) + var tokenUsages = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && premiumModelIds.Contains(x.ModelId)) + .GroupBy(x => x.TokenId) + .Select(x => new + { + TokenId = x.TokenId, + TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount) + }) + .ToListAsync(); + + if (!tokenUsages.Any()) + { + return new List(); + } + + // 获取用户的所有Token信息用于名称映射 + var tokenIds = tokenUsages.Select(x => x.TokenId).ToList(); + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId && tokenIds.Contains(x.Id)) + .Select(x => new { x.Id, x.Name }) + .ToListAsync(); + + var tokenNameDict = tokens.ToDictionary(x => x.Id, x => x.Name); + + // 计算总token数 + var totalTokens = tokenUsages.Sum(x => x.TotalTokenCount); + + // 计算各Token占比 + var result = tokenUsages.Select(x => new TokenPremiumUsageDto + { + TokenId = x.TokenId, + 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(); + + return result; + } } \ 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 bb1b97f7..69a43156 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 @@ -20,10 +20,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot } public MessageAggregateRoot(Guid? userId, Guid? sessionId, string content, string role, string modelId, - ThorUsageResponse? tokenUsage) + ThorUsageResponse? tokenUsage, Guid? tokenId = null) { UserId = userId; SessionId = sessionId; + TokenId = tokenId ?? Guid.Empty; //如果没有会话,不存储对话内容 Content = sessionId is null ? null : content; Role = role; @@ -59,6 +60,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot public Guid? UserId { get; set; } public Guid? SessionId { get; set; } + /// + /// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty) + /// + public Guid TokenId { get; set; } + [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] public string? Content { get; set; } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs index 96377eb7..d5217ca9 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs @@ -5,27 +5,84 @@ using Volo.Abp.Domain.Entities.Auditing; namespace Yi.Framework.AiHub.Domain.Entities.OpenApi; [SugarTable("Ai_Token")] +[SugarIndex($"index_{{table}}_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)] public class TokenAggregateRoot : FullAuditedAggregateRoot { public TokenAggregateRoot() { } - public TokenAggregateRoot(Guid userId) + public TokenAggregateRoot(Guid userId, string name) { - this.UserId = userId; - this.Token = GenerateToken(); + UserId = userId; + Name = name; + Token = GenerateToken(); + IsDisabled = false; } + /// + /// Token密钥 + /// public string Token { get; set; } + + /// + /// 用户Id + /// public Guid UserId { get; set; } /// - /// 重置token + /// 名称 /// - public void ResetToken() + [SugarColumn(Length = 100)] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } + + /// + /// 是否禁用 + /// + public bool IsDisabled { get; set; } + + /// + /// 检查Token是否可用 + /// + public bool IsAvailable() { - this.Token = GenerateToken(); + if (IsDisabled) + { + return false; + } + + if (ExpireTime.HasValue && ExpireTime.Value < DateTime.Now) + { + return false; + } + + return true; + } + + /// + /// 禁用Token + /// + public void Disable() + { + IsDisabled = true; + } + + /// + /// 启用Token + /// + public void Enable() + { + IsDisabled = false; } private string GenerateToken(int length = 36) 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 6f9e6aa3..6008f343 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 @@ -7,16 +7,22 @@ namespace Yi.Framework.AiHub.Domain.Entities; /// 用量统计 /// [SugarTable("Ai_UsageStatistics")] +[SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(ModelId)}_{nameof(TokenId)}", + nameof(UserId), OrderByType.Asc, + nameof(ModelId), OrderByType.Asc, + nameof(TokenId), OrderByType.Asc +)] public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot { public UsageStatisticsAggregateRoot() { } - public UsageStatisticsAggregateRoot(Guid? userId, string modelId) + public UsageStatisticsAggregateRoot(Guid? userId, string modelId, Guid tokenId) { UserId = userId; ModelId = modelId; + TokenId = tokenId; } /// @@ -29,6 +35,11 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot /// public string ModelId { get; set; } + /// + /// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty) + /// + public Guid TokenId { get; set; } + /// /// 对话次数 /// 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 ffb70822..4e2d3645 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 @@ -120,12 +120,14 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CompleteChatForStatisticsAsync(HttpContext httpContext, ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { _specialCompatible.Compatible(request); @@ -145,7 +147,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault().Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.Usage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto @@ -154,9 +156,9 @@ public class AiGateWayManager : DomainService sessionId is null ? "不予存储" : data.Choices?.FirstOrDefault()?.Delta.Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.Usage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage, tokenId); // 扣减尊享token包用量 if (PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -179,6 +181,7 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CompleteChatStreamForStatisticsAsync( @@ -186,6 +189,7 @@ public class AiGateWayManager : DomainService ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { var response = httpContext.Response; @@ -288,7 +292,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = tokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -296,9 +300,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(), ModelId = request.Model, TokenUsage = tokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId); // 扣减尊享token包用量 if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -319,10 +323,11 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CreateImageForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, - ImageCreateRequest request) + ImageCreateRequest request, Guid? tokenId = null) { try { @@ -350,7 +355,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Prompt, ModelId = model, TokenUsage = response.Usage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -358,9 +363,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : response.Results?.FirstOrDefault()?.Url, ModelId = model, TokenUsage = response.Usage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage); + await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage, tokenId); // 扣减尊享token包用量 if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -384,13 +389,14 @@ public class AiGateWayManager : DomainService /// 向量生成 /// /// + /// /// /// - /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task EmbeddingForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, - ThorEmbeddingInput input) + ThorEmbeddingInput input, Guid? tokenId = null) { try { @@ -474,7 +480,7 @@ public class AiGateWayManager : DomainService // TokenUsage = usage // }); - await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage); + await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage, tokenId); } catch (ThorRateLimitException) { @@ -522,12 +528,14 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task AnthropicCompleteChatForStatisticsAsync(HttpContext httpContext, AnthropicInput request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { _specialCompatible.AnthropicCompatible(request); @@ -549,7 +557,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.TokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto @@ -557,9 +565,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text, ModelId = request.Model, TokenUsage = data.TokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage, tokenId); // 扣减尊享token包用量 var totalTokens = data.TokenUsage.TotalTokens ?? 0; @@ -579,6 +587,7 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task AnthropicCompleteChatStreamForStatisticsAsync( @@ -586,6 +595,7 @@ public class AiGateWayManager : DomainService AnthropicInput request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { var response = httpContext.Response; @@ -627,7 +637,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = tokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -635,9 +645,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(), ModelId = request.Model, TokenUsage = tokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId); // 扣减尊享token包用量 if (userId.HasValue && tokenUsage is not null) 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 2620c2a2..95730424 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 @@ -19,28 +19,30 @@ public class AiMessageManager : DomainService /// /// 创建系统消息 /// - /// - /// - /// + /// 用户Id + /// 会话Id + /// 消息输入 + /// Token Id(Web端传Guid.Empty) /// - public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input) + public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input, Guid? tokenId = null) { input.Role = "system"; - var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); + var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId, input.TokenUsage, tokenId); await _repository.InsertAsync(message); } - + /// - /// 创建系统消息 + /// 创建用户消息 /// - /// - /// - /// + /// 用户Id + /// 会话Id + /// 消息输入 + /// Token Id(Web端传Guid.Empty) /// - public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input) + public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input, Guid? tokenId = null) { input.Role = "user"; - var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); + var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId, input.TokenUsage, tokenId); await _repository.InsertAsync(message); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs index 1a0ead20..dd2ae774 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs @@ -1,64 +1,134 @@ -using Volo.Abp.Domain.Services; -using Volo.Abp.Users; +using SqlSugar; +using Volo.Abp.Domain.Services; +using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.OpenApi; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Domain.Managers; +/// +/// Token验证结果 +/// +public class TokenValidationResult +{ + /// + /// 用户Id + /// + public Guid UserId { get; set; } + + /// + /// Token Id + /// + public Guid TokenId { get; set; } +} + public class TokenManager : DomainService { private readonly ISqlSugarRepository _tokenRepository; + private readonly ISqlSugarRepository _usageStatisticsRepository; - public TokenManager(ISqlSugarRepository tokenRepository) + public TokenManager( + ISqlSugarRepository tokenRepository, + ISqlSugarRepository usageStatisticsRepository) { _tokenRepository = tokenRepository; + _usageStatisticsRepository = usageStatisticsRepository; } - public async Task GetAsync(Guid userId) - { - var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); - if (entity is not null) - { - return entity.Token; - } - else - { - return null; - } - } - - public async Task CreateAsync(Guid userId) - { - var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); - if (entity is not null) - { - entity.ResetToken(); - await _tokenRepository.UpdateAsync(entity); - } - else - { - var token = new TokenAggregateRoot(userId); - await _tokenRepository.InsertAsync(token); - } - } - - public async Task GetUserIdAsync(string? token) + /// + /// 验证Token并返回用户Id和TokenId + /// + /// Token密钥 + /// 模型Id(用于判断是否是尊享模型需要检查额度) + /// Token验证结果 + public async Task ValidateTokenAsync(string? token, string? modelId = null) { if (token is null) { throw new UserFriendlyException("当前请求未包含token", "401"); } - if (token.StartsWith("yi-")) + if (!token.StartsWith("yi-")) { - var entity = await _tokenRepository._DbQueryable.Where(x => x.Token == token).FirstAsync(); - if (entity is null) - { - throw new UserFriendlyException("当前请求token无效", "401"); - } - - return entity.UserId; + throw new UserFriendlyException("当前请求token非法", "401"); } - throw new UserFriendlyException("当前请求token非法", "401"); + + var entity = await _tokenRepository._DbQueryable + .Where(x => x.Token == token) + .FirstAsync(); + + if (entity is null) + { + throw new UserFriendlyException("当前请求token无效", "401"); + } + + // 检查Token是否被禁用 + if (entity.IsDisabled) + { + throw new UserFriendlyException("当前Token已被禁用,请启用后再使用", "403"); + } + + // 检查Token是否过期 + if (entity.ExpireTime.HasValue && entity.ExpireTime.Value < DateTime.Now) + { + throw new UserFriendlyException("当前Token已过期,请更新过期时间或创建新的Token", "403"); + } + + // 如果是尊享模型且Token设置了额度限制,检查是否超限 + if (!string.IsNullOrEmpty(modelId) && + PremiumPackageConst.ModeIds.Contains(modelId) && + entity.PremiumQuotaLimit.HasValue) + { + var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id); + if (usedQuota >= entity.PremiumQuotaLimit.Value) + { + throw new UserFriendlyException($"当前Token的尊享包额度已用完(已使用:{usedQuota},限制:{entity.PremiumQuotaLimit.Value}),请调整额度限制或使用其他Token", "403"); + } + } + + return new TokenValidationResult + { + UserId = entity.UserId, + TokenId = entity.Id + }; } -} \ No newline at end of file + + /// + /// 获取Token的尊享包已使用额度 + /// + private async Task GetTokenPremiumUsedQuotaAsync(Guid userId, Guid tokenId) + { + var premiumModelIds = PremiumPackageConst.ModeIds; + + var usedQuota = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && x.TokenId == tokenId && premiumModelIds.Contains(x.ModelId)) + .SumAsync(x => x.TotalTokenCount); + + return usedQuota; + } + + /// + /// 获取用户的Token(兼容旧接口,返回第一个可用的Token) + /// + [Obsolete("请使用 ValidateTokenAsync 方法")] + public async Task GetAsync(Guid userId) + { + var entity = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId && !x.IsDisabled) + .OrderBy(x => x.CreationTime) + .FirstAsync(); + + return entity?.Token; + } + + /// + /// 获取用户Id(兼容旧接口) + /// + [Obsolete("请使用 ValidateTokenAsync 方法")] + public async Task GetUserIdAsync(string? token) + { + var result = await ValidateTokenAsync(token); + return result.UserId; + } +} 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 index 682c4b96..a04d1ff5 100644 --- 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 @@ -18,8 +18,10 @@ public class UsageStatisticsManager : DomainService private IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetRequiredService(); - public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage) + public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage, Guid? tokenId = null) { + var actualTokenId = tokenId ?? Guid.Empty; + long inputTokenCount = tokenUsage?.PromptTokens ?? tokenUsage?.InputTokens ?? 0; @@ -28,10 +30,10 @@ public class UsageStatisticsManager : DomainService ?? tokenUsage?.OutputTokens ?? 0; - await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}")) + await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}:{actualTokenId}:{modelId}")) { - var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId); - //存在数据,更细 + var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId && x.TokenId == actualTokenId); + //存在数据,更新 if (entity is not null) { entity.AddOnceChat(inputTokenCount, outputTokenCount); @@ -40,7 +42,7 @@ public class UsageStatisticsManager : DomainService //不存在插入 else { - var usage = new UsageStatisticsAggregateRoot(userId, modelId); + var usage = new UsageStatisticsAggregateRoot(userId, modelId, actualTokenId); usage.AddOnceChat(inputTokenCount, outputTokenCount); await _repository.InsertAsync(usage); } diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index a7d136b7..6b76a323 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -31,6 +31,8 @@ using Yi.Abp.SqlsugarCore; using Yi.Framework.AiHub.Application; using Yi.Framework.AiHub.Application.Services; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AspNetCore; using Yi.Framework.AspNetCore.Authentication.OAuth; using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee; @@ -355,10 +357,9 @@ namespace Yi.Abp.Web var app = context.GetApplicationBuilder(); app.UseRouting(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); //跨域 app.UseCors(DefaultCorsPolicyName); diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index e6da3f77..91748029 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -1,9 +1,8 @@ import type { GetSessionListVO } from './types'; -import { get, post } from '@/utils/request'; +import { del, get, post, put } from '@/utils/request'; // 获取当前用户的模型列表 export function getModelList() { - // return get('/system/model/modelList'); return get('/ai-chat/model').json(); } // 申请ApiKey @@ -21,10 +20,99 @@ export function getRechargeLog() { } // 查询用户近7天token消耗 -export function getLast7DaysTokenUsage() { - return get('/usage-statistics/last7Days-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getLast7DaysTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/last7Days-token-usage?tokenId=${tokenId}` + : '/usage-statistics/last7Days-token-usage'; + return get(url).json(); } // 查询用户token消耗各模型占比 -export function getModelTokenUsage() { - return get('/usage-statistics/model-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getModelTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/model-token-usage?tokenId=${tokenId}` + : '/usage-statistics/model-token-usage'; + return get(url).json(); } + +// 获取当前用户得token列表 +export function getTokenList(params?: { + skipCount?: number; + maxResultCount?: number; + orderByColumn?: string; + isAsc?: string; +}) { + // 构建查询参数 + const queryParams = new URLSearchParams(); + if (params?.skipCount !== undefined) { + queryParams.append('SkipCount', params.skipCount.toString()); + } + if (params?.maxResultCount !== undefined) { + queryParams.append('MaxResultCount', params.maxResultCount.toString()); + } + if (params?.orderByColumn) { + queryParams.append('OrderByColumn', params.orderByColumn); + } + if (params?.isAsc) { + queryParams.append('IsAsc', params.isAsc); + } + + const queryString = queryParams.toString(); + const url = queryString ? `/token/list?${queryString}` : '/token/list'; + + return get(url).json(); +} + +// 创建token +export function createToken(data: any) { + return post('/token', data).json(); +} + +// 编辑token +export function editToken(data: any) { + return put('/token', data).json(); +} + +// 删除token +export function deleteToken(id: string) { + return del(`/token/${id}`).json(); +} + +// 启用token +export function enableToken(id: string) { + return post(`/token/${id}/enable`).json(); +} + +// 禁用token +export function disableToken(id: string) { + return post(`/token/${id}/disable`).json(); +} + +// 新增接口2 +// 获取可选择的token信息 +export function getSelectableTokenInfo() { + return get('/token/select-list').json(); +} +/* +返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "string", + "isDisabled": true + } + ] */ +// 获取当前用户尊享包不同token用量占比(饼图) +export function getPremiumPackageTokenUsage() { + return get('/usage-statistics/premium-token-usage/by-token').json(); +} +/* 返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "tokenName": "string", + "tokens": 0, + "percentage": 0 + } + ] */ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue index d229c51d..e1025d90 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue @@ -1,46 +1,102 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + API密钥管理中心 + 管理您的 API 访问密钥,每个 API密钥 拥有独立的配额和使用统计 - + + + 共 {{ totalCount }} 个 + + + + + + + + + + 新增 API密钥 + + + + + 刷新列表 + + + + + + + + + + + + + + + + {{ row.name }} + + + + + + + + + + {{ row.showKey ? row.apiKey : '•••••••••••••••••••••' }} + + + + + {{ row.showKey ? '隐藏' : '查看' }} + + + 复制 + + + + + + + + + + + {{ formatQuota(row.premiumUsedQuota) }} + / + + {{ (row.premiumQuotaLimit == null || row.premiumQuotaLimit === 0) ? '∞ 无限' : formatQuota(row.premiumQuotaLimit) }} + + + + + + + + + + + + + + {{ formatDateTime(row.expireTime) }} + + + + 永不过期 + + + + + + + + + + + + {{ row.isDisabled ? '已禁用' : '已启用' }} + + + + + + + + {{ formatDateTime(row.creationTime) }} + + + + + + + + + 编辑 + + + {{ row.isDisabled ? '启用' : '禁用' }} + + + 删除 + + + + + + + + + - - 🎁 尊享会员专属福利 🎁 - - YiXinAi重磅推出专属Api接入服务 - - YiXinAI-Vip 限时免费领取 - 点击礼盒领取您的专属API密钥 - - - - - 🎉 恭喜您已获得专属API密钥 🎉 - - - - - - - - - - - - - - - 重置密钥 + + + + + 创建第一个 API密钥 - - 重置后原密钥将立即失效 - - + - - - - + - - - + + + + + + + + + + 使用说明 + + + + + 【意心Ai】AI工具玩法指南 - - → 点击查看完整指南 - + - - - + + - - API地址:https://ai.ccnetcore.com - 密钥:上面申请的token - 模型:聊天界面显示的模型名称 - - - - - - - - - - - - - - - 恭喜您成功领取API密钥! - - - 请妥善保管您的密钥 - - - - - 确定 - - - + + - diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/PremiumPackageInfo.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/PremiumPackageInfo.vue index 5dff94eb..460088bb 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/PremiumPackageInfo.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/PremiumPackageInfo.vue @@ -1,7 +1,36 @@ @@ -231,6 +444,98 @@ function onProductPackage() { + + + + + + + + + + + 各API密钥用量占比 + Premium APIKEY Usage Distribution + + + + + + + + + + + + + + + + #{{ index + 1 }} + + + + {{ item.tokenName }} + + + + + 用量: + {{ item.tokens.toLocaleString() }} + tokens + + + + + + + + + + + + + + + + + + + + + + + + + + 暂无Token使用数据 + + + 当您开始使用Token后,这里将展示各Token的用量占比统计 + + + + 创建并使用Token后即可查看详细的用量分析 + + + + + + @@ -495,6 +800,270 @@ function onProductPackage() { font-size: 13px; } +/* Token用量占比卡片 */ +.token-usage-card { + margin-top: 24px; +} + +.token-icon { + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + color: #0284c7; +} + +.token-usage-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.chart-container-wrapper { + width: 100%; + background: linear-gradient(135deg, #fafbfc 0%, #f5f6f8 100%); + border-radius: 12px; + padding: 20px; +} + +.token-pie-chart { + width: 100%; + height: 400px; +} + +/* Token统计列表 */ +.token-stats-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.token-stat-item { + padding: 16px; + background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%); + border-radius: 12px; + border: 1px solid #f0f2f5; + transition: all 0.3s; + + &:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); + transform: translateY(-2px); + } +} + +.token-stat-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.token-rank { + flex-shrink: 0; +} + +.rank-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + font-size: 14px; + font-weight: 700; + color: white; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + + &.rank-1 { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + } + + &.rank-2 { + background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%); + } + + &.rank-3 { + background: linear-gradient(135deg, #fb923c 0%, #ea580c 100%); + } +} + +.token-name { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + + .el-icon { + color: #667eea; + font-size: 18px; + } +} + +.token-stat-data { + display: flex; + flex-direction: column; + gap: 12px; +} + +.stat-tokens { + display: flex; + align-items: baseline; + gap: 6px; + font-size: 14px; + + .label { + color: #909399; + } + + .value { + font-size: 20px; + font-weight: 700; + color: #667eea; + } + + .unit { + color: #909399; + font-size: 12px; + } +} + +.stat-percentage { + :deep(.el-progress__text) { + font-size: 14px !important; + font-weight: 700; + } +} + +/* 空状态 */ +.token-empty-state { + padding: 60px 20px; + + :deep(.el-empty__image) { + width: auto; + } +} + +.custom-empty-image { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 140px; + height: 140px; + margin: 0 auto; +} + +.empty-main-icon { + font-size: 80px; + color: #667eea; + position: relative; + z-index: 2; + animation: float 3s ease-in-out infinite; +} + +.empty-decoration { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.decoration-circle { + position: absolute; + border-radius: 50%; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); + + &:nth-child(1) { + width: 60px; + height: 60px; + top: 10%; + left: 10%; + animation: pulse 2s ease-in-out infinite; + } + + &:nth-child(2) { + width: 40px; + height: 40px; + bottom: 20%; + right: 15%; + animation: pulse 2s ease-in-out infinite 0.5s; + } + + &:nth-child(3) { + width: 30px; + height: 30px; + top: 60%; + right: 5%; + animation: pulse 2s ease-in-out infinite 1s; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 0.6; + } + 50% { + transform: scale(1.2); + opacity: 0.3; + } +} + +.empty-description { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-top: 20px; +} + +.empty-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #303133; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.empty-text { + margin: 0; + font-size: 14px; + color: #606266; + line-height: 1.6; + max-width: 360px; +} + +.empty-tips { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%); + border-radius: 8px; + font-size: 13px; + color: #667eea; + margin-top: 8px; + + .el-icon { + font-size: 16px; + flex-shrink: 0; + } +} + /* 警告卡片 */ .warning-card { border-radius: 12px; diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue new file mode 100644 index 00000000..74798b29 --- /dev/null +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + API密钥将在过期时间后自动失效 + + + + + + + + + + + + + + + + 超出配额后API密钥将无法继续使用 + + + + + + + + + + + diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue index d402143d..8be7425f 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue @@ -11,7 +11,7 @@ import { } from 'echarts/components'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { getLast7DaysTokenUsage, getModelTokenUsage } from '@/api'; +import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo } from '@/api'; // 注册必要的组件 echarts.use([ @@ -48,16 +48,54 @@ const totalTokens = ref(0); const usageData = ref([]); const modelUsageData = ref([]); +// Token选择相关 +const selectedTokenId = ref(''); // 空字符串表示查询全部 +const tokenOptions = ref([]); +const tokenOptionsLoading = ref(false); + // 计算属性:是否有模型数据 const hasModelData = computed(() => modelUsageData.value.length > 0); +// 计算属性:当前选择的token名称 +const selectedTokenName = computed(() => { + if (!selectedTokenId.value) + return '全部API密钥'; + const token = tokenOptions.value.find(t => t.tokenId === selectedTokenId.value); + return token?.name || '未知API密钥'; +}); + +// 获取可选择的Token列表 +async function fetchTokenOptions() { + try { + tokenOptionsLoading.value = true; + const res = await getSelectableTokenInfo(); + if (res.data) { + // 不再过滤禁用的token,全部显示 + tokenOptions.value = res.data; + } + } + catch (error) { + console.error('获取API密钥列表失败:', error); + ElMessage.error('获取TAPI密钥列表失败'); + } + finally { + tokenOptionsLoading.value = false; + } +} + +// Token选择变化 +function handleTokenChange() { + fetchUsageData(); +} + // 获取用量数据 async function fetchUsageData() { loading.value = true; try { + const tokenId = selectedTokenId.value || undefined; const [res, res2] = await Promise.all([ - getLast7DaysTokenUsage(), - getModelTokenUsage(), + getLast7DaysTokenUsage(tokenId), + getModelTokenUsage(tokenId), ]); usageData.value = res.data || []; @@ -235,49 +273,47 @@ function updatePieChart() { formatter: '{a} {b}: {c} tokens ({d}%)', }, legend: { - orient: isManyItems ? 'vertical' : 'horizontal', - right: isManyItems ? 10 : 'auto', - bottom: isManyItems ? 0 : 10, - type: isManyItems ? 'scroll' : 'plain', - pageIconColor: '#3a4de9', - pageIconInactiveColor: '#ccc', - pageTextStyle: { color: '#333' }, - itemGap: isSmallContainer ? 5 : 10, - itemWidth: isSmallContainer ? 15 : 25, - itemHeight: isSmallContainer ? 10 : 14, - textStyle: { - fontSize: isSmallContainer ? 10 : 12, - }, - formatter(name: string) { - return name.length > 15 ? `${name.substring(0, 12)}...` : name; - }, - data: data.map(item => item.name), + show: false, // 隐藏图例,使用标签线代替 }, series: [ { name: '模型用量', type: 'pie', radius: ['50%', '70%'], - center: isManyItems ? ['40%', '50%'] : ['50%', '50%'], - avoidLabelOverlap: false, + center: ['50%', '50%'], + avoidLabelOverlap: true, itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2, }, label: { - show: false, - position: 'center', + show: true, + position: 'outside', + formatter: '{b}: {d}%', + fontSize: 13, + fontWeight: 600, + color: '#333', }, emphasis: { label: { show: true, - fontSize: '18', + fontSize: 16, fontWeight: 'bold', }, + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.3)', + }, }, labelLine: { - show: false, + show: true, + length: 15, + length2: 10, + lineStyle: { + width: 1.5, + }, }, data, }, @@ -453,6 +489,7 @@ watch([pieContainerSize.width, barContainerSize.width], () => { onMounted(() => { initCharts(); + fetchTokenOptions(); fetchUsageData(); }); @@ -475,19 +512,56 @@ onBeforeUnmount(() => { Token用量统计 - + + + + + + + + 全部Token + + + + + + + + {{ token.name }} + + 已禁用 + + + + + + - 📊 近七天每日Token消耗量 + 📊 近七天每日Token消耗量{{ selectedTokenId ? ` (${selectedTokenName})` : '' }} 近七日总计: {{ totalTokens }} tokens @@ -501,7 +575,7 @@ onBeforeUnmount(() => { - 🥧 各模型Token消耗占比 + 🥧 各模型Token消耗占比{{ selectedTokenId ? ` (${selectedTokenName})` : '' }} @@ -512,7 +586,7 @@ onBeforeUnmount(() => { - 📈 各模型总Token消耗量 + 📈 各模型总Token消耗量{{ selectedTokenId ? ` (${selectedTokenName})` : '' }} @@ -560,6 +634,62 @@ onBeforeUnmount(() => { border-bottom: 2px solid #e9ecef; } +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.token-selector { + width: 240px; +} + +.token-option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + + &.disabled-token { + opacity: 0.6; + + .option-label { + text-decoration: line-through; + color: #909399; + } + } +} + +.option-icon { + color: #667eea; + font-size: 16px; + flex-shrink: 0; + + &.all-icon { + color: #409eff; + } + + &.disabled-icon { + color: #c0c4cc; + } +} + +.option-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.disabled-tag { + margin-left: auto; + flex-shrink: 0; + font-size: 11px; + padding: 0 6px; + height: 18px; + line-height: 18px; +} + .header h2 { display: flex; align-items: center; diff --git a/Yi.Ai.Vue3/src/utils/user.ts b/Yi.Ai.Vue3/src/utils/user.ts index 5e1f8adc..7142a410 100644 --- a/Yi.Ai.Vue3/src/utils/user.ts +++ b/Yi.Ai.Vue3/src/utils/user.ts @@ -1,11 +1,9 @@ -import { useUserStore } from '@/stores/index.js'; +import {useUserStore} from '@/stores/index.js'; // 判断是否是 VIP 用户 export function isUserVip(): boolean { const userStore = useUserStore(); - const userRoles = userStore.userInfo?.roles ?? []; - const isVip = userRoles.some((role: any) => role.roleCode === 'YiXinAi-Vip'); - return isVip; + return userStore.userInfo.isVip; } // 用户头像 diff --git a/Yi.Ai.Vue3/types/components.d.ts b/Yi.Ai.Vue3/types/components.d.ts index a7089848..fd719b56 100644 --- a/Yi.Ai.Vue3/types/components.d.ts +++ b/Yi.Ai.Vue3/types/components.d.ts @@ -69,6 +69,7 @@ declare module 'vue' { SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default'] SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default'] SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default'] + TokenFormDialog: typeof import('./../src/components/userPersonalCenter/components/TokenFormDialog.vue')['default'] UsageStatistics: typeof import('./../src/components/userPersonalCenter/components/UsageStatistics.vue')['default'] UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default'] VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default'] diff --git a/Yi.Ai.Vue3/types/import_meta.d.ts b/Yi.Ai.Vue3/types/import_meta.d.ts index d8a60d41..b3e9d275 100644 --- a/Yi.Ai.Vue3/types/import_meta.d.ts +++ b/Yi.Ai.Vue3/types/import_meta.d.ts @@ -6,7 +6,6 @@ interface ImportMetaEnv { readonly VITE_WEB_ENV: string; readonly VITE_WEB_BASE_API: string; readonly VITE_API_URL: string; - readonly VITE_BUILD_COMPRESS: string; readonly VITE_SSO_SEVER_URL: string; readonly VITE_APP_VERSION: string; }
点击礼盒领取您的专属API密钥
- 重置后原密钥将立即失效 -
API地址:https://ai.ccnetcore.com
密钥:上面申请的token
模型:聊天界面显示的模型名称
- 恭喜您成功领取API密钥! -
- 请妥善保管您的密钥 -
+ 当您开始使用Token后,这里将展示各Token的用量占比统计 +