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

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