Merge branch 'ai-agent-backend' into ai-agent

# Conflicts:
#	Yi.Ai.Vue3/src/pages/console/index.vue
#	Yi.Ai.Vue3/src/routers/modules/staticRouter.ts
This commit is contained in:
ccnetcore
2026-01-02 19:45:55 +08:00
34 changed files with 1860 additions and 137 deletions

View File

@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 创建AI应用输入
/// </summary>
public class AiAppCreateInput
{
/// <summary>
/// 应用名称
/// </summary>
[Required(ErrorMessage = "应用名称不能为空")]
[StringLength(100, ErrorMessage = "应用名称不能超过100个字符")]
public string Name { get; set; }
/// <summary>
/// 应用终结点
/// </summary>
[Required(ErrorMessage = "应用终结点不能为空")]
[StringLength(500, ErrorMessage = "应用终结点不能超过500个字符")]
public string Endpoint { get; set; }
/// <summary>
/// 额外URL
/// </summary>
[StringLength(500, ErrorMessage = "额外URL不能超过500个字符")]
public string? ExtraUrl { get; set; }
/// <summary>
/// 应用Key
/// </summary>
[Required(ErrorMessage = "应用Key不能为空")]
[StringLength(500, ErrorMessage = "应用Key不能超过500个字符")]
public string ApiKey { get; set; }
/// <summary>
/// 排序
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
public int OrderNum { get; set; }
}

View File

@@ -0,0 +1,42 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// AI应用DTO
/// </summary>
public class AiAppDto
{
/// <summary>
/// 应用ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 应用名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 应用终结点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// 额外URL
/// </summary>
public string? ExtraUrl { get; set; }
/// <summary>
/// 应用Key
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
}

View File

@@ -0,0 +1,14 @@
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 获取AI应用列表输入
/// </summary>
public class AiAppGetListInput : PagedAllResultRequestDto
{
/// <summary>
/// 搜索关键词(搜索应用名称)
/// </summary>
public string? SearchKey { get; set; }
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 更新AI应用输入
/// </summary>
public class AiAppUpdateInput
{
/// <summary>
/// 应用ID
/// </summary>
[Required(ErrorMessage = "应用ID不能为空")]
public Guid Id { get; set; }
/// <summary>
/// 应用名称
/// </summary>
[Required(ErrorMessage = "应用名称不能为空")]
[StringLength(100, ErrorMessage = "应用名称不能超过100个字符")]
public string Name { get; set; }
/// <summary>
/// 应用终结点
/// </summary>
[Required(ErrorMessage = "应用终结点不能为空")]
[StringLength(500, ErrorMessage = "应用终结点不能超过500个字符")]
public string Endpoint { get; set; }
/// <summary>
/// 额外URL
/// </summary>
[StringLength(500, ErrorMessage = "额外URL不能超过500个字符")]
public string? ExtraUrl { get; set; }
/// <summary>
/// 应用Key
/// </summary>
[Required(ErrorMessage = "应用Key不能为空")]
[StringLength(500, ErrorMessage = "应用Key不能超过500个字符")]
public string ApiKey { get; set; }
/// <summary>
/// 排序
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
public int OrderNum { get; set; }
}

View File

@@ -0,0 +1,96 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 创建AI模型输入
/// </summary>
public class AiModelCreateInput
{
/// <summary>
/// 处理名
/// </summary>
[Required(ErrorMessage = "处理名不能为空")]
[StringLength(100, ErrorMessage = "处理名不能超过100个字符")]
public string HandlerName { get; set; }
/// <summary>
/// 模型ID
/// </summary>
[Required(ErrorMessage = "模型ID不能为空")]
[StringLength(200, ErrorMessage = "模型ID不能超过200个字符")]
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
[Required(ErrorMessage = "模型名称不能为空")]
[StringLength(200, ErrorMessage = "模型名称不能超过200个字符")]
public string Name { get; set; }
/// <summary>
/// 模型描述
/// </summary>
[StringLength(1000, ErrorMessage = "模型描述不能超过1000个字符")]
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
public int OrderNum { get; set; }
/// <summary>
/// AI应用ID
/// </summary>
[Required(ErrorMessage = "AI应用ID不能为空")]
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
[StringLength(2000, ErrorMessage = "额外信息不能超过2000个字符")]
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// </summary>
[Required(ErrorMessage = "模型类型不能为空")]
public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型API类型
/// </summary>
[Required(ErrorMessage = "模型API类型不能为空")]
public ModelApiTypeEnum ModelApiType { get; set; }
/// <summary>
/// 模型倍率
/// </summary>
[Range(0.01, double.MaxValue, ErrorMessage = "模型倍率必须大于0")]
public decimal Multiplier { get; set; } = 1;
/// <summary>
/// 模型显示倍率
/// </summary>
[Range(0.01, double.MaxValue, ErrorMessage = "模型显示倍率必须大于0")]
public decimal MultiplierShow { get; set; } = 1;
/// <summary>
/// 供应商分组名称
/// </summary>
[StringLength(100, ErrorMessage = "供应商分组名称不能超过100个字符")]
public string? ProviderName { get; set; }
/// <summary>
/// 模型图标URL
/// </summary>
[StringLength(500, ErrorMessage = "模型图标URL不能超过500个字符")]
public string? IconUrl { get; set; }
/// <summary>
/// 是否为尊享模型
/// </summary>
public bool IsPremium { get; set; }
}

View File

@@ -0,0 +1,84 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// AI模型DTO
/// </summary>
public class AiModelDto
{
/// <summary>
/// 模型ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 处理名
/// </summary>
public string HandlerName { get; set; }
/// <summary>
/// 模型ID
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 模型描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// AI应用ID
/// </summary>
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型API类型
/// </summary>
public ModelApiTypeEnum ModelApiType { get; set; }
/// <summary>
/// 模型倍率
/// </summary>
public decimal Multiplier { get; set; }
/// <summary>
/// 模型显示倍率
/// </summary>
public decimal MultiplierShow { get; set; }
/// <summary>
/// 供应商分组名称
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// 模型图标URL
/// </summary>
public string? IconUrl { get; set; }
/// <summary>
/// 是否为尊享模型
/// </summary>
public bool IsPremium { get; set; }
}

View File

@@ -0,0 +1,24 @@
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 获取AI模型列表输入
/// </summary>
public class AiModelGetListInput : PagedAllResultRequestDto
{
/// <summary>
/// 搜索关键词(搜索模型名称、模型ID)
/// </summary>
public string? SearchKey { get; set; }
/// <summary>
/// AI应用ID筛选
/// </summary>
public Guid? AiAppId { get; set; }
/// <summary>
/// 是否只显示尊享模型
/// </summary>
public bool? IsPremiumOnly { get; set; }
}

View File

@@ -0,0 +1,102 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
/// <summary>
/// 更新AI模型输入
/// </summary>
public class AiModelUpdateInput
{
/// <summary>
/// 模型ID
/// </summary>
[Required(ErrorMessage = "模型ID不能为空")]
public Guid Id { get; set; }
/// <summary>
/// 处理名
/// </summary>
[Required(ErrorMessage = "处理名不能为空")]
[StringLength(100, ErrorMessage = "处理名不能超过100个字符")]
public string HandlerName { get; set; }
/// <summary>
/// 模型ID
/// </summary>
[Required(ErrorMessage = "模型ID不能为空")]
[StringLength(200, ErrorMessage = "模型ID不能超过200个字符")]
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
[Required(ErrorMessage = "模型名称不能为空")]
[StringLength(200, ErrorMessage = "模型名称不能超过200个字符")]
public string Name { get; set; }
/// <summary>
/// 模型描述
/// </summary>
[StringLength(1000, ErrorMessage = "模型描述不能超过1000个字符")]
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
public int OrderNum { get; set; }
/// <summary>
/// AI应用ID
/// </summary>
[Required(ErrorMessage = "AI应用ID不能为空")]
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
[StringLength(2000, ErrorMessage = "额外信息不能超过2000个字符")]
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// </summary>
[Required(ErrorMessage = "模型类型不能为空")]
public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型API类型
/// </summary>
[Required(ErrorMessage = "模型API类型不能为空")]
public ModelApiTypeEnum ModelApiType { get; set; }
/// <summary>
/// 模型倍率
/// </summary>
[Range(0.01, double.MaxValue, ErrorMessage = "模型倍率必须大于0")]
public decimal Multiplier { get; set; }
/// <summary>
/// 模型显示倍率
/// </summary>
[Range(0.01, double.MaxValue, ErrorMessage = "模型显示倍率必须大于0")]
public decimal MultiplierShow { get; set; }
/// <summary>
/// 供应商分组名称
/// </summary>
[StringLength(100, ErrorMessage = "供应商分组名称不能超过100个字符")]
public string? ProviderName { get; set; }
/// <summary>
/// 模型图标URL
/// </summary>
[StringLength(500, ErrorMessage = "模型图标URL不能超过500个字符")]
public string? IconUrl { get; set; }
/// <summary>
/// 是否为尊享模型
/// </summary>
public bool IsPremium { get; set; }
}

View File

@@ -0,0 +1,86 @@
using Volo.Abp.Application.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 渠道商管理服务接口
/// </summary>
public interface IChannelService
{
#region AI应用管理
/// <summary>
/// 获取AI应用列表
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>分页应用列表</returns>
Task<PagedResultDto<AiAppDto>> GetAppListAsync(AiAppGetListInput input);
/// <summary>
/// 根据ID获取AI应用
/// </summary>
/// <param name="id">应用ID</param>
/// <returns>应用详情</returns>
Task<AiAppDto> GetAppByIdAsync(Guid id);
/// <summary>
/// 创建AI应用
/// </summary>
/// <param name="input">创建输入</param>
/// <returns>创建的应用</returns>
Task<AiAppDto> CreateAppAsync(AiAppCreateInput input);
/// <summary>
/// 更新AI应用
/// </summary>
/// <param name="input">更新输入</param>
/// <returns>更新后的应用</returns>
Task<AiAppDto> UpdateAppAsync(AiAppUpdateInput input);
/// <summary>
/// 删除AI应用
/// </summary>
/// <param name="id">应用ID</param>
Task DeleteAppAsync(Guid id);
#endregion
#region AI模型管理
/// <summary>
/// 获取AI模型列表
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>分页模型列表</returns>
Task<PagedResultDto<AiModelDto>> GetModelListAsync(AiModelGetListInput input);
/// <summary>
/// 根据ID获取AI模型
/// </summary>
/// <param name="id">模型ID</param>
/// <returns>模型详情</returns>
Task<AiModelDto> GetModelByIdAsync(Guid id);
/// <summary>
/// 创建AI模型
/// </summary>
/// <param name="input">创建输入</param>
/// <returns>创建的模型</returns>
Task<AiModelDto> CreateModelAsync(AiModelCreateInput input);
/// <summary>
/// 更新AI模型
/// </summary>
/// <param name="input">更新输入</param>
/// <returns>更新后的模型</returns>
Task<AiModelDto> UpdateModelAsync(AiModelUpdateInput input);
/// <summary>
/// 删除AI模型(软删除)
/// </summary>
/// <param name="id">模型ID</param>
Task DeleteModelAsync(Guid id);
#endregion
}

View File

@@ -7,6 +7,7 @@ using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
@@ -25,6 +26,7 @@ public class DailyTaskService : ApplicationService
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ILogger<DailyTaskService> _logger;
private IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
// 任务配置
private readonly Dictionary<int, (long RequiredTokens, long RewardTokens, string Name, string Description)>
_taskConfigs = new()
@@ -37,12 +39,13 @@ public class DailyTaskService : ApplicationService
ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> dailyTaskRepository,
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ILogger<DailyTaskService> logger)
ILogger<DailyTaskService> logger, ISqlSugarRepository<AiModelEntity> aiModelRepository)
{
_dailyTaskRepository = dailyTaskRepository;
_messageRepository = messageRepository;
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
_aiModelRepository = aiModelRepository;
}
/// <summary>
@@ -179,10 +182,16 @@ public class DailyTaskService : ApplicationService
var tomorrow = today.AddDays(1);
// 查询今日所有使用尊享包模型的消息role=system 表示消耗)
// 先获取所有尊享模型的ModelId列表
var premiumModelIds = await _aiModelRepository._DbQueryable
.Where(x => x.IsPremium)
.Select(x => x.ModelId)
.ToListAsync();
var totalTokens = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.Role == "system") // system角色表示实际消耗
.Where(x => PremiumPackageConst.ModeIds.Contains(x.ModelId)) // 尊享包模型
.Where(x => premiumModelIds.Contains(x.ModelId)) // 尊享包模型
.Where(x => x.CreationTime >= today && x.CreationTime < tomorrow)
.SumAsync(x => x.TokenUsage.TotalTokenCount);

View File

@@ -0,0 +1,240 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 渠道商管理服务实现
/// </summary>
[Authorize]
public class ChannelService : ApplicationService, IChannelService
{
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
public ChannelService(
ISqlSugarRepository<AiAppAggregateRoot, Guid> appRepository,
ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
{
_appRepository = appRepository;
_modelRepository = modelRepository;
}
#region AI应用管理
/// <summary>
/// 获取AI应用列表
/// </summary>
[HttpGet("channel/app")]
public async Task<PagedResultDto<AiAppDto>> GetAppListAsync(AiAppGetListInput input)
{
RefAsync<int> total = 0;
var entities = await _appRepository._DbQueryable
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), x => x.Name.Contains(input.SearchKey))
.OrderByDescending(x => x.OrderNum)
.OrderByDescending(x => x.CreationTime)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
var output = entities.Adapt<List<AiAppDto>>();
return new PagedResultDto<AiAppDto>(total, output);
}
/// <summary>
/// 根据ID获取AI应用
/// </summary>
[HttpGet("channel/app/{id}")]
public async Task<AiAppDto> GetAppByIdAsync([FromRoute]Guid id)
{
var entity = await _appRepository.GetByIdAsync(id);
return entity.Adapt<AiAppDto>();
}
/// <summary>
/// 创建AI应用
/// </summary>
public async Task<AiAppDto> CreateAppAsync(AiAppCreateInput input)
{
var entity = new AiAppAggregateRoot
{
Name = input.Name,
Endpoint = input.Endpoint,
ExtraUrl = input.ExtraUrl,
ApiKey = input.ApiKey,
OrderNum = input.OrderNum
};
await _appRepository.InsertAsync(entity);
return entity.Adapt<AiAppDto>();
}
/// <summary>
/// 更新AI应用
/// </summary>
public async Task<AiAppDto> UpdateAppAsync(AiAppUpdateInput input)
{
var entity = await _appRepository.GetByIdAsync(input.Id);
entity.Name = input.Name;
entity.Endpoint = input.Endpoint;
entity.ExtraUrl = input.ExtraUrl;
entity.ApiKey = input.ApiKey;
entity.OrderNum = input.OrderNum;
await _appRepository.UpdateAsync(entity);
return entity.Adapt<AiAppDto>();
}
/// <summary>
/// 删除AI应用
/// </summary>
[HttpDelete("channel/app/{id}")]
public async Task DeleteAppAsync([FromRoute]Guid id)
{
// 检查是否有关联的模型
var hasModels = await _modelRepository._DbQueryable
.Where(x => x.AiAppId == id && !x.IsDeleted)
.AnyAsync();
if (hasModels)
{
throw new Volo.Abp.UserFriendlyException("该应用下存在模型,无法删除");
}
await _appRepository.DeleteAsync(id);
}
#endregion
#region AI模型管理
/// <summary>
/// 获取AI模型列表
/// </summary>
[HttpGet("channel/model")]
public async Task<PagedResultDto<AiModelDto>> GetModelListAsync(AiModelGetListInput input)
{
RefAsync<int> total = 0;
var query = _modelRepository._DbQueryable
.Where(x => !x.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), x =>
x.Name.Contains(input.SearchKey) || x.ModelId.Contains(input.SearchKey))
.WhereIF(input.AiAppId.HasValue, x => x.AiAppId == input.AiAppId.Value)
.WhereIF(input.IsPremiumOnly == true, x => x.IsPremium);
var entities = await query
.OrderBy(x => x.OrderNum)
.OrderByDescending(x => x.Id)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
var output = entities.Adapt<List<AiModelDto>>();
return new PagedResultDto<AiModelDto>(total, output);
}
/// <summary>
/// 根据ID获取AI模型
/// </summary>
[HttpGet("channel/model/{id}")]
public async Task<AiModelDto> GetModelByIdAsync([FromRoute]Guid id)
{
var entity = await _modelRepository.GetByIdAsync(id);
return entity.Adapt<AiModelDto>();
}
/// <summary>
/// 创建AI模型
/// </summary>
public async Task<AiModelDto> CreateModelAsync(AiModelCreateInput input)
{
// 验证应用是否存在
var appExists = await _appRepository._DbQueryable
.Where(x => x.Id == input.AiAppId)
.AnyAsync();
if (!appExists)
{
throw new Volo.Abp.UserFriendlyException("指定的AI应用不存在");
}
var entity = new AiModelEntity
{
HandlerName = input.HandlerName,
ModelId = input.ModelId,
Name = input.Name,
Description = input.Description,
OrderNum = input.OrderNum,
AiAppId = input.AiAppId,
ExtraInfo = input.ExtraInfo,
ModelType = input.ModelType,
ModelApiType = input.ModelApiType,
Multiplier = input.Multiplier,
MultiplierShow = input.MultiplierShow,
ProviderName = input.ProviderName,
IconUrl = input.IconUrl,
IsPremium = input.IsPremium,
IsDeleted = false
};
await _modelRepository.InsertAsync(entity);
return entity.Adapt<AiModelDto>();
}
/// <summary>
/// 更新AI模型
/// </summary>
public async Task<AiModelDto> UpdateModelAsync(AiModelUpdateInput input)
{
var entity = await _modelRepository.GetByIdAsync(input.Id);
// 验证应用是否存在
if (entity.AiAppId != input.AiAppId)
{
var appExists = await _appRepository._DbQueryable
.Where(x => x.Id == input.AiAppId)
.AnyAsync();
if (!appExists)
{
throw new Volo.Abp.UserFriendlyException("指定的AI应用不存在");
}
}
entity.HandlerName = input.HandlerName;
entity.ModelId = input.ModelId;
entity.Name = input.Name;
entity.Description = input.Description;
entity.OrderNum = input.OrderNum;
entity.AiAppId = input.AiAppId;
entity.ExtraInfo = input.ExtraInfo;
entity.ModelType = input.ModelType;
entity.ModelApiType = input.ModelApiType;
entity.Multiplier = input.Multiplier;
entity.MultiplierShow = input.MultiplierShow;
entity.ProviderName = input.ProviderName;
entity.IconUrl = input.IconUrl;
entity.IsPremium = input.IsPremium;
await _modelRepository.UpdateAsync(entity);
return entity.Adapt<AiModelDto>();
}
/// <summary>
/// 删除AI模型(软删除)
/// </summary>
[HttpDelete("channel/model/{id}")]
public async Task DeleteModelAsync(Guid id)
{
await _modelRepository.DeleteByIdAsync(id);
}
#endregion
}

View File

@@ -40,35 +40,37 @@ namespace Yi.Framework.AiHub.Application.Services;
public class AiChatService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly ILogger<AiChatService> _logger;
private readonly AiGateWayManager _aiGateWayManager;
private readonly ModelManager _modelManager;
private readonly PremiumPackageManager _premiumPackageManager;
private readonly ChatManager _chatManager;
private readonly TokenManager _tokenManager;
private readonly IAccountService _accountService;
private readonly ISqlSugarRepository<AgentStoreAggregateRoot> _agentStoreRepository;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger,
AiGateWayManager aiGateWayManager,
ModelManager modelManager,
PremiumPackageManager premiumPackageManager,
ChatManager chatManager, TokenManager tokenManager, IAccountService accountService,
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository)
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository)
{
_httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager;
_aiModelRepository = aiModelRepository;
_logger = logger;
_aiGateWayManager = aiGateWayManager;
_modelManager = modelManager;
_premiumPackageManager = premiumPackageManager;
_chatManager = chatManager;
_tokenManager = tokenManager;
_accountService = accountService;
_agentStoreRepository = agentStoreRepository;
_aiModelRepository = aiModelRepository;
}
@@ -102,7 +104,7 @@ public class AiChatService : ApplicationService
ModelName = x.Name,
ModelDescribe = x.Description,
Remark = x.Description,
IsPremiumPackage = PremiumPackageConst.ModeIds.Contains(x.ModelId)
IsPremiumPackage = x.IsPremium
}).ToListAsync();
return output;
}
@@ -137,19 +139,24 @@ public class AiChatService : ApplicationService
}
//如果是尊享包服务,需要校验是是否尊享包足够
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
if (CurrentUser.IsAuthenticated)
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
if (availableTokens <= 0)
var isPremium = await _modelManager.IsPremiumModelAsync(input.Model);
if (isPremium)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, null, cancellationToken);
CurrentUser.Id, sessionId, null, CancellationToken.None);
}
/// <summary>
@@ -185,7 +192,7 @@ public class AiChatService : ApplicationService
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, null, null, cancellationToken);
CurrentUser.Id, null, null, CancellationToken.None);
}
@@ -212,7 +219,9 @@ public class AiChatService : ApplicationService
}
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
var isPremium = await _modelManager.IsPremiumModelAsync(input.ModelId);
if (isPremium)
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(tokenValidation.UserId);
@@ -230,7 +239,7 @@ public class AiChatService : ApplicationService
input.ModelId,
tokenValidation.UserId,
input.Tools,
cancellationToken);
CancellationToken.None);
}
/// <summary>

View File

@@ -29,6 +29,7 @@ public class AiImageService : ApplicationService
private readonly IBackgroundJobManager _backgroundJobManager;
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly PremiumPackageManager _premiumPackageManager;
private readonly ModelManager _modelManager;
private readonly IGuidGenerator _guidGenerator;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly TokenManager _tokenManager;
@@ -37,6 +38,7 @@ public class AiImageService : ApplicationService
IBackgroundJobManager backgroundJobManager,
AiBlacklistManager aiBlacklistManager,
PremiumPackageManager premiumPackageManager,
ModelManager modelManager,
IGuidGenerator guidGenerator,
IWebHostEnvironment webHostEnvironment, TokenManager tokenManager)
{
@@ -44,6 +46,7 @@ public class AiImageService : ApplicationService
_backgroundJobManager = backgroundJobManager;
_aiBlacklistManager = aiBlacklistManager;
_premiumPackageManager = premiumPackageManager;
_modelManager = modelManager;
_guidGenerator = guidGenerator;
_webHostEnvironment = webHostEnvironment;
_tokenManager = tokenManager;
@@ -76,8 +79,9 @@ public class AiImageService : ApplicationService
throw new UserFriendlyException("图片生成功能需要VIP用户才能使用请购买VIP后重新登录重试");
}
// 尊享包校验
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
// 尊享包校验 - 使用ModelManager统一判断
var isPremium = await _modelManager.IsPremiumModelAsync(input.ModelId);
if (isPremium)
{
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)

View File

@@ -1,10 +1,12 @@
using Mapster;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.AiHub.Domain.Shared.Extensions;
@@ -18,10 +20,12 @@ namespace Yi.Framework.AiHub.Application.Services.Chat;
public class ModelService : ApplicationService, IModelService
{
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
private readonly ModelManager _modelManager;
public ModelService(ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
public ModelService(ISqlSugarRepository<AiModelEntity, Guid> modelRepository, ModelManager modelManager)
{
_modelRepository = modelRepository;
_modelManager = modelManager;
}
/// <summary>
@@ -41,8 +45,7 @@ public class ModelService : ApplicationService, IModelService
input.ModelTypes.Contains(x.ModelType))
.WhereIF(input.ModelApiTypes is not null, x =>
input.ModelApiTypes.Contains(x.ModelApiType))
.WhereIF(input.IsPremiumOnly == true, x =>
PremiumPackageConst.ModeIds.Contains(x.ModelId))
.WhereIF(input.IsPremiumOnly == true, x => x.IsPremium)
.GroupBy(x => x.ModelId)
.Select(x => x.ModelId)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total));
@@ -61,7 +64,7 @@ public class ModelService : ApplicationService, IModelService
MultiplierShow = x.First().MultiplierShow,
ProviderName = x.First().ProviderName,
IconUrl = x.First().IconUrl,
IsPremium = PremiumPackageConst.ModeIds.Contains(x.First().ModelId),
IsPremium = x.First().IsPremium,
OrderNum = x.First().OrderNum
}).ToList();
@@ -77,11 +80,11 @@ public class ModelService : ApplicationService, IModelService
.Where(x => !x.IsDeleted)
.Where(x => !string.IsNullOrEmpty(x.ProviderName))
.GroupBy(x => x.ProviderName)
.OrderBy(x => x.ProviderName)
.OrderBy(x => x.OrderNum)
.Select(x => x.ProviderName)
.ToListAsync();
return providers;
return providers!;
}
/// <summary>
@@ -115,4 +118,13 @@ public class ModelService : ApplicationService, IModelService
return Task.FromResult(options);
}
/// <summary>
/// 清除尊享模型ID缓存
/// </summary>
[HttpPost("model/clear-premium-cache")]
public async Task ClearPremiumModelCacheAsync()
{
await _modelManager.ClearPremiumModelIdsCacheAsync();
}
}

View File

@@ -7,8 +7,10 @@ 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.Model;
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;
@@ -23,13 +25,15 @@ public class TokenService : ApplicationService
{
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
private readonly ModelManager _modelManager;
public TokenService(
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
ModelManager modelManager)
{
_tokenRepository = tokenRepository;
_usageStatisticsRepository = usageStatisticsRepository;
_modelManager = modelManager;
}
/// <summary>
@@ -51,8 +55,8 @@ public class TokenService : ApplicationService
return new PagedResultDto<TokenGetListOutputDto>();
}
// 获取尊享包模型ID列表
var premiumModelIds = PremiumPackageConst.ModeIds;
// 通过ModelManager获取尊享包模型ID列表
var premiumModelIds = await _modelManager.GetPremiumModelIdsAsync();
// 批量查询所有Token的尊享包已使用额度
var tokenIds = tokens.Select(t => t.Id).ToList();

View File

@@ -27,25 +27,27 @@ public class OpenApiService : ApplicationService
private readonly ILogger<OpenApiService> _logger;
private readonly TokenManager _tokenManager;
private readonly AiGateWayManager _aiGateWayManager;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly ModelManager _modelManager;
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly IAccountService _accountService;
private readonly PremiumPackageManager _premiumPackageManager;
private readonly ISqlSugarRepository<ImageStoreTaskAggregateRoot> _imageStoreRepository;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager,
IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository)
ModelManager modelManager, AiBlacklistManager aiBlacklistManager,
IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_tokenManager = tokenManager;
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
_modelManager = modelManager;
_aiBlacklistManager = aiBlacklistManager;
_accountService = accountService;
_premiumPackageManager = premiumPackageManager;
_imageStoreRepository = imageStoreRepository;
_aiModelRepository = aiModelRepository;
}
/// <summary>
@@ -65,7 +67,9 @@ public class OpenApiService : ApplicationService
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.Model))
var isPremium = await _modelManager.IsPremiumModelAsync(input.Model);
if (isPremium)
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
@@ -79,13 +83,13 @@ public class OpenApiService : ApplicationService
if (input.Stream == true)
{
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId, null, tokenId, cancellationToken);
userId, null, tokenId,CancellationToken.None);
}
else
{
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null, tokenId,
cancellationToken);
CancellationToken.None);
}
}
@@ -193,14 +197,14 @@ public class OpenApiService : ApplicationService
{
await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
userId, null, tokenId, CancellationToken.None);
}
else
{
await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId,
null, tokenId,
cancellationToken);
CancellationToken.None);
}
}
@@ -245,14 +249,14 @@ public class OpenApiService : ApplicationService
{
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
userId, null, tokenId, CancellationToken.None);
}
else
{
await _aiGateWayManager.OpenAiResponsesAsyncForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId,
null, tokenId,
cancellationToken);
CancellationToken.None);
}
}
@@ -304,7 +308,7 @@ public class OpenApiService : ApplicationService
modelId, input,
userId,
null, tokenId,
cancellationToken);
CancellationToken.None);
}
else
{
@@ -312,7 +316,7 @@ public class OpenApiService : ApplicationService
modelId, input,
userId,
null, tokenId,
cancellationToken);
CancellationToken.None);
}
}

View File

@@ -9,8 +9,10 @@ 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.Model;
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;
@@ -27,17 +29,19 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ModelManager _modelManager;
public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
ModelManager modelManager)
{
_messageRepository = messageRepository;
_usageStatisticsRepository = usageStatisticsRepository;
_premiumPackageRepository = premiumPackageRepository;
_tokenRepository = tokenRepository;
_modelManager = modelManager;
}
/// <summary>
@@ -181,7 +185,9 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
public async Task<List<TokenPremiumUsageDto>> GetPremiumTokenUsageByTokenAsync()
{
var userId = CurrentUser.GetId();
var premiumModelIds = PremiumPackageConst.ModeIds;
// 通过ModelManager获取所有尊享模型的ModelId列表
var premiumModelIds = await _modelManager.GetPremiumModelIdsAsync();
// 从UsageStatistics表获取尊享模型的token消耗统计按TokenId聚合
var tokenUsages = await _usageStatisticsRepository._DbQueryable

View File

@@ -107,35 +107,6 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
{
continue;
}
// var content = result?.Choices?.FirstOrDefault()?.Delta;
//
// if (first && content?.Content == OpenAIConstant.ThinkStart)
// {
// isThink = true;
// continue;
// // 需要将content的内容转换到其他字段
// }
//
// if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
// {
// isThink = false;
// // 需要将content的内容转换到其他字段
// continue;
// }
//
// if (isThink && result?.Choices != null)
// {
// // 需要将content的内容转换到其他字段
// foreach (var choice in result.Choices)
// {
// choice.Delta.ReasoningContent = choice.Delta.Content;
// choice.Delta.Content = string.Empty;
// }
// }
//
// first = false;
yield return result;
}
}

View File

@@ -9,7 +9,9 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
public sealed class OpenAiChatCompletionsService(
ILogger<OpenAiChatCompletionsService> logger,
IHttpClientFactory httpClientFactory)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
@@ -19,8 +21,18 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/chat/completions";
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
requestUri,
chatCompletionCreate, options.ApiKey);
openai?.SetTag("Model", chatCompletionCreate.Model);
@@ -130,8 +142,16 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/chat/completions";
var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
requestUri,
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
openai?.SetTag("Model", chatCompletionCreate.Model);
@@ -152,7 +172,8 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());

View File

@@ -22,7 +22,16 @@ public class OpenAiResponseService(ILogger<OpenAiResponseService> logger,IHttpCl
var client = httpClientFactory.CreateClient();
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/responses", input, options.ApiKey);
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/responses";
var response = await client.HttpRequestRaw(requestUri, input, options.ApiKey);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
@@ -86,8 +95,17 @@ public class OpenAiResponseService(ILogger<OpenAiResponseService> logger,IHttpCl
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 响应");
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/responses";
var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/responses",
requestUri,
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
openai?.SetTag("Model", chatCompletionCreate.Model);

View File

@@ -23,9 +23,17 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/chat/completions";
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
requestUri,
chatCompletionCreate, options.ApiKey);
openai?.SetTag("Model", chatCompletionCreate.Model);
@@ -92,40 +100,6 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
var result = JsonSerializer.Deserialize<ThorChatCompletionsResponse>(line,
ThorJsonSerializer.DefaultOptions);
// var content = result?.Choices?.FirstOrDefault()?.Delta;
//
// // if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
// // {
// // continue;
// // }
//
// if (first && content.Content == OpenAIConstant.ThinkStart)
// {
// isThink = true;
// //continue;
// // 需要将content的内容转换到其他字段
// }
//
// if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
// {
// isThink = false;
// // 需要将content的内容转换到其他字段
// //continue;
// }
//
// if (isThink)
// {
// // 需要将content的内容转换到其他字段
// foreach (var choice in result.Choices)
// {
// //choice.Delta.ReasoningContent = choice.Delta.Content;
// //choice.Delta.Content = string.Empty;
// }
// }
// first = false;
yield return result;
}
}
@@ -142,8 +116,16 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
options.Endpoint = "https://api.deepseek.com/v1";
}
var endpoint = options?.Endpoint.TrimEnd('/');
//兼容 v1结尾
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
}
var requestUri = endpoint + "/v1/chat/completions";
var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
requestUri,
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
openai?.SetTag("Model", chatCompletionCreate.Model);

View File

@@ -80,4 +80,9 @@ public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
/// 模型图标URL
/// </summary>
public string? IconUrl { get; set; }
/// <summary>
/// 是否为尊享模型
/// </summary>
public bool IsPremium { get; set; }
}

View File

@@ -150,7 +150,12 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage, tokenId);
// 扣减尊享token包用量
if (PremiumPackageConst.ModeIds.Contains(request.Model))
var isPremium = await _aiModelRepository._DbQueryable
.Where(x => x.ModelId == request.Model)
.Select(x => x.IsPremium)
.FirstAsync();
if (isPremium)
{
var totalTokens = data.Usage?.TotalTokens ?? 0;
if (totalTokens > 0)
@@ -300,12 +305,20 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
if (userId is not null)
{
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
var isPremium = await _aiModelRepository._DbQueryable
.Where(x => x.ModelId == request.Model)
.Select(x => x.IsPremium)
.FirstAsync();
if (isPremium)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
}
}
}
}
@@ -363,12 +376,20 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage, tokenId);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
if (userId is not null)
{
var totalTokens = response.Usage.TotalTokens ?? 0;
if (totalTokens > 0)
var isPremium = await _aiModelRepository._DbQueryable
.Where(x => x.ModelId == request.Model)
.Select(x => x.IsPremium)
.FirstAsync();
if (isPremium)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
var totalTokens = response.Usage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
}
}
}
}

View File

@@ -15,6 +15,7 @@ using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Shared.Attributes;
using Yi.Framework.AiHub.Domain.Shared.Consts;
@@ -34,12 +35,13 @@ public class ChatManager : DomainService
private readonly UsageStatisticsManager _usageStatisticsManager;
private readonly PremiumPackageManager _premiumPackageManager;
private readonly AiGateWayManager _aiGateWayManager;
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
public ChatManager(ILoggerFactory loggerFactory,
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, AiMessageManager aiMessageManager,
UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager,
AiGateWayManager aiGateWayManager)
AiGateWayManager aiGateWayManager, ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository)
{
_loggerFactory = loggerFactory;
_messageRepository = messageRepository;
@@ -48,6 +50,7 @@ public class ChatManager : DomainService
_usageStatisticsManager = usageStatisticsManager;
_premiumPackageManager = premiumPackageManager;
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
}
/// <summary>
@@ -207,7 +210,12 @@ public class ChatManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId, modelId, usage, tokenId);
//扣减尊享token包用量
if (PremiumPackageConst.ModeIds.Contains(modelId))
var isPremium = await _aiModelRepository._DbQueryable
.Where(x => x.ModelId == modelId)
.Select(x => x.IsPremium)
.FirstAsync();
if (isPremium)
{
var totalTokens = usage?.TotalTokens ?? 0;
if (totalTokens > 0)

View File

@@ -0,0 +1,78 @@
using Microsoft.Extensions.Logging;
using Volo.Abp.Caching;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
/// <summary>
/// 模型管理器
/// </summary>
public class ModelManager : DomainService
{
private 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";
public ModelManager(
ISqlSugarRepository<AiModelEntity> aiModelRepository,
IDistributedCache<List<string>, string> distributedCache,
ILogger<ModelManager> logger)
{
_aiModelRepository = aiModelRepository;
_distributedCache = distributedCache;
_logger = logger;
}
/// <summary>
/// 获取所有尊享模型ID列表(使用分布式缓存,10分钟过期)
/// </summary>
/// <returns>尊享模型ID列表</returns>
public async Task<List<string>> GetPremiumModelIdsAsync()
{
var output = await _distributedCache.GetOrAddAsync(
PREMIUM_MODEL_IDS_CACHE_KEY,
async () =>
{
// 从数据库查询
var premiumModelIds = await _aiModelRepository._DbQueryable
.Where(x => x.IsPremium)
.Select(x => x.ModelId)
.ToListAsync();
return premiumModelIds;
},
() => new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
}
);
return output ?? new List<string>();
}
/// <summary>
/// 判断指定模型是否为尊享模型
/// </summary>
/// <param name="modelId">模型ID</param>
/// <returns>是否为尊享模型</returns>
public async Task<bool> IsPremiumModelAsync(string modelId)
{
if (string.IsNullOrWhiteSpace(modelId))
{
return false;
}
var premiumModelIds = await GetPremiumModelIdsAsync();
return premiumModelIds.Contains(modelId);
}
/// <summary>
/// 清除尊享模型ID缓存
/// </summary>
public async Task ClearPremiumModelIdsCacheAsync()
{
await _distributedCache.RemoveAsync(PREMIUM_MODEL_IDS_CACHE_KEY);
_logger.LogInformation("已清除尊享模型ID分布式缓存");
}
}

View File

@@ -1,6 +1,7 @@
using SqlSugar;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;
@@ -32,13 +33,16 @@ public class TokenManager : DomainService
{
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
public TokenManager(
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository)
{
_tokenRepository = tokenRepository;
_usageStatisticsRepository = usageStatisticsRepository;
_aiModelRepository = aiModelRepository;
}
/// <summary>
@@ -92,14 +96,20 @@ public class TokenManager : DomainService
}
// 如果是尊享模型且Token设置了额度限制检查是否超限
if (!string.IsNullOrEmpty(modelId) &&
PremiumPackageConst.ModeIds.Contains(modelId) &&
entity.PremiumQuotaLimit.HasValue)
if (!string.IsNullOrEmpty(modelId) && entity.PremiumQuotaLimit.HasValue)
{
var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id);
if (usedQuota >= entity.PremiumQuotaLimit.Value)
var isPremium = await _aiModelRepository._DbQueryable
.Where(x => x.ModelId == modelId)
.Select(x => x.IsPremium)
.FirstAsync();
if (isPremium)
{
throw new UserFriendlyException($"当前Token的尊享包额度已用完已使用{usedQuota},限制:{entity.PremiumQuotaLimit.Value}请调整额度限制或使用其他Token", "403");
var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id);
if (usedQuota >= entity.PremiumQuotaLimit.Value)
{
throw new UserFriendlyException($"当前Token的尊享包额度已用完已使用{usedQuota},限制:{entity.PremiumQuotaLimit.Value}请调整额度限制或使用其他Token", "403");
}
}
}
@@ -116,7 +126,11 @@ public class TokenManager : DomainService
/// </summary>
private async Task<long> GetTokenPremiumUsedQuotaAsync(Guid userId, Guid tokenId)
{
var premiumModelIds = PremiumPackageConst.ModeIds;
// 先获取所有尊享模型的ModelId列表
var premiumModelIds = await _aiModelRepository._DbQueryable
.Where(x => x.IsPremium)
.Select(x => x.ModelId)
.ToListAsync();
var usedQuota = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId && x.TokenId == tokenId && premiumModelIds.Contains(x.ModelId))

View File

@@ -358,7 +358,7 @@ namespace Yi.Abp.Web
var app = context.GetApplicationBuilder();
app.UseRouting();
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AgentStoreAggregateRoot>();
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AiModelEntity>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();