feat: 完成ai-hub第一期功能

This commit is contained in:
chenchun
2025-06-25 17:12:09 +08:00
parent 4f71d874bd
commit 695aaedfba
18 changed files with 360 additions and 103 deletions

View File

@@ -5,7 +5,7 @@ public class ModelGetListOutput
/// <summary>
/// 模型ID
/// </summary>
public long Id { get; set; }
public Guid Id { get; set; }
/// <summary>
/// 模型分类
@@ -20,7 +20,7 @@ public class ModelGetListOutput
/// <summary>
/// 模型描述
/// </summary>
public string ModelDescribe { get; set; }
public string? ModelDescribe { get; set; }
/// <summary>
/// 模型价格
@@ -55,5 +55,5 @@ public class ModelGetListOutput
/// <summary>
/// 备注信息
/// </summary>
public string Remark { get; set; }
public string? Remark { get; set; }
}

View File

@@ -1,17 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Options;
public class AiGateWayOptions
{
public AiChatOptionDic Chats { get; set; }
}
public class AiChatOptionDic : Dictionary<string, AiChatModelOptions>
{
}
public class AiChatModelOptions
{
public List<string> ModelIds { get; set; }
public string Endpoint { get; set; }
public string ApiKey { get; set; }
}

View File

@@ -5,17 +5,17 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OpenAI.Chat;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Options;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
@@ -24,16 +24,17 @@ namespace Yi.Framework.AiHub.Application.Services;
/// </summary>
public class AiChatService : ApplicationService
{
private readonly AiGateWayOptions _options;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AiMessageManager _aiMessageManager;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly AiBlacklistManager _aiBlacklistManager;
public AiChatService(IOptions<AiGateWayOptions> options, IHttpContextAccessor httpContextAccessor,
AiMessageManager aiMessageManager)
public AiChatService(IHttpContextAccessor httpContextAccessor,
AiMessageManager aiMessageManager, AiBlacklistManager aiBlacklistManager)
{
_options = options.Value;
this._httpContextAccessor = httpContextAccessor;
_aiMessageManager = aiMessageManager;
_aiBlacklistManager = aiBlacklistManager;
}
@@ -56,18 +57,20 @@ public class AiChatService : ApplicationService
/// <returns></returns>
public async Task<List<ModelGetListOutput>> GetModelAsync()
{
var output = _options.Chats.SelectMany(x => x.Value.ModelIds)
.Select(x => new ModelGetListOutput()
{
Id = 001,
Category = "chat",
ModelName = x,
ModelDescribe = "这是一个直连模型",
ModelPrice = 4,
ModelType = "1",
ModelShow = "0",
Remark = "直连模型"
}).ToList();
var output = await _aiModelRepository._DbQueryable.Select(x => new ModelGetListOutput
{
Id = x.Id,
Category = "chat",
ModelName = x.Name,
ModelDescribe = x.Description,
ModelPrice = 0,
ModelType = "1",
ModelShow = "0",
SystemPrompt = null,
ApiHost = null,
ApiKey = null,
Remark = x.Description
}).ToListAsync();
return output;
}
@@ -79,6 +82,25 @@ public class AiChatService : ApplicationService
/// <param name="cancellationToken"></param>
public async Task PostSendAsync(SendMessageInput input, CancellationToken cancellationToken)
{
//除了免费模型,其他的模型都要校验
if (input.Model != "DeepSeek-R1-0528")
{
//有token需要黑名单校验
if (CurrentUser.IsAuthenticated)
{
await _aiBlacklistManager.VerifiyAiBlacklist(CurrentUser.GetId());
if (!CurrentUser.Roles.Contains("YiXinAi-Vip"))
{
throw new UserFriendlyException("该模型需要VIP用户才能使用请购买VIP后重新登录重试");
}
}
else
{
throw new UserFriendlyException("未登录用户只能使用未加速的DeepSeek-R1请登录后重试");
}
}
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var response = httpContext.Response;
// 设置响应头,声明是 SSE 流
@@ -116,7 +138,7 @@ public class AiChatService : ApplicationService
// 启动一个后台任务来消费队列
var outputTask = Task.Run(async () =>
{
while (!(isComplete&&messageQueue.IsEmpty))
while (!(isComplete && messageQueue.IsEmpty))
{
if (messageQueue.TryDequeue(out var message))
{
@@ -153,16 +175,15 @@ public class AiChatService : ApplicationService
await outputTask;
if (CurrentUser.IsAuthenticated && input.SessionId.HasValue)
{
// 等待接入token
// await _aiMessageManager.CreateMessageAsync(CurrentUser.GetId(), input.SessionId.Value, new MessageInputDto
// {
// Content = null,
// Role = null,
// DeductCost = 0,
// TotalTokens = 0,
// ModelId = null,
// Remark = null
// });
await _aiMessageManager.CreateMessageAsync(CurrentUser.GetId(), input.SessionId.Value, new MessageInputDto
{
Content = input.Messages.LastOrDefault().Content,
Role = input.Messages.LastOrDefault().Role,
DeductCost = 0,
TotalTokens = 0,
ModelId = input.Model,
Remark = null
});
}
}

View File

@@ -0,0 +1,49 @@
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
public class AiModelDescribe
{
/// <summary>
/// 应用id
/// </summary>
public Guid AppId { get; set; }
/// <summary>
/// 应用名称
/// </summary>
public string AppName { get; set; }
/// <summary>
/// 应用终结点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// 应用key
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// 处理名
/// </summary>
public string HandlerName { get; set; }
/// <summary>
/// 模型id
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// 模型描述
/// </summary>
public string? Description { get; set; }
}

View File

@@ -7,7 +7,6 @@
<ItemGroup>
<Folder Include="Consts\" />
<Folder Include="Dtos\" />
<Folder Include="Enums\" />
<Folder Include="Etos\" />
</ItemGroup>

View File

@@ -1,8 +1,10 @@
using OpenAI.Chat;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiChat;
public interface IChatService
{
public IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,CancellationToken cancellationToken);
public IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
CancellationToken cancellationToken);
}

View File

@@ -1,28 +1,24 @@
using System.Runtime.CompilerServices;
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Options;
using OpenAI.Chat;
using Yi.Framework.AiHub.Application.Contracts.Options;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiChat.Impl;
public class AzureChatService : IChatService
{
private readonly AiChatModelOptions _options;
public AzureChatService(IOptions<AiGateWayOptions> options)
public AzureChatService()
{
this._options = options.Value.Chats[nameof(AzureChatService)];
}
public async IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,
public async IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var endpoint = new Uri(_options.Endpoint);
var endpoint = new Uri(aiModelDescribe.Endpoint);
var deploymentName = modelId;
var apiKey = _options.ApiKey;
var deploymentName = aiModelDescribe.ModelId;
var apiKey = aiModelDescribe.ApiKey;
AzureOpenAIClient azureClient = new(
endpoint,

View File

@@ -1,28 +1,24 @@
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenAI.Chat;
using Yi.Framework.AiHub.Application.Contracts.Options;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiChat.Impl;
public class AzureRestChatService : IChatService
{
private readonly AiChatModelOptions _options;
public AzureRestChatService(IOptions<AiGateWayOptions> options)
public AzureRestChatService()
{
this._options = options.Value.Chats[nameof(AzureRestChatService)];
}
public async IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,
public async IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// 设置API URL
var apiUrl = $"{_options.Endpoint}models/chat/completions";
var apiUrl = $"{aiModelDescribe.Endpoint}models/chat/completions";
var ss = messages.Select(x => new
@@ -45,7 +41,7 @@ public class AzureRestChatService : IChatService
top_p = 0.1,
presence_penalty = 0,
frequency_penalty = 0,
model = modelId
model = aiModelDescribe.ModelId
};
// 序列化请求内容为JSON
@@ -53,24 +49,25 @@ public class AzureRestChatService : IChatService
using var httpClient = new HttpClient();
// 设置请求头
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_options.ApiKey}");
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {aiModelDescribe.ApiKey}");
// 其他头信息如Content-Type在StringContent中设置
// 构造 POST 请求
var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
// 设置请求内容(示例)
request.Content =new StringContent(jsonBody, Encoding.UTF8, "application/json");
request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
// 发送POST请求
HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
HttpResponseMessage response =
await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
// 确认响应成功
response.EnsureSuccessStatusCode();
// 读取响应内容
var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
// 从流中读取数据并输出到控制台
using var streamReader = new System.IO.StreamReader(responseStream);
using var streamReader = new StreamReader(responseStream);
string line;
while ((line = await streamReader.ReadLineAsync(cancellationToken)) != null)
{
@@ -117,7 +114,5 @@ public class AzureRestChatService : IChatService
// 解析失败
return null;
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// ai黑名单
/// </summary>
[SugarTable("Ai_Blacklist")]
public class AiBlacklistAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 用户
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 有效开始时间
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// 有效结束时间
/// </summary>
public DateTime EndTime { get; set; }
}

View File

@@ -0,0 +1,38 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.Core.Data;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// ai应用
/// </summary>
[SugarTable("Ai_App")]
public class AiAppAggregateRoot : FullAuditedAggregateRoot<Guid>, IOrderNum
{
/// <summary>
/// 应用名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 应用终结点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// 应用key
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// ai模型
/// </summary>
[Navigate(NavigateType.OneToMany, nameof(AiModelEntity.AiAppId))]
public List<AiModelEntity> AiModels { get; set; }
}

View File

@@ -0,0 +1,48 @@
using SqlSugar;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.Core.Data;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// ai模型定义
/// </summary>
[SugarTable("Ai_Model")]
public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
{
/// <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>
/// 软删除
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// ai应用id
/// </summary>
public Guid AiAppId { get; set; }
}

View File

@@ -0,0 +1,31 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 用量统计
/// </summary>
[SugarTable("Ai_UsageStatistics")]
public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 用户id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 哪个模型
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 总token使用
/// </summary>
public decimal TotalTokens { get; set; }
/// <summary>
/// 对话次数
/// </summary>
public int Number { get; set; }
}

View File

@@ -0,0 +1,31 @@
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class AiBlacklistManager : DomainService
{
private readonly ISqlSugarRepository<AiBlacklistAggregateRoot> _aiBlacklistRepository;
public AiBlacklistManager(ISqlSugarRepository<AiBlacklistAggregateRoot> aiBlacklistRepository)
{
_aiBlacklistRepository = aiBlacklistRepository;
}
/// <summary>
/// 校验黑名单
/// </summary>
/// <param name="userId"></param>
/// <exception cref="UserFriendlyException"></exception>
public async Task VerifiyAiBlacklist(Guid userId)
{
var now = DateTime.Now;
if (await _aiBlacklistRepository._DbQueryable
.Where(x => now >= x.StartTime && now <= x.EndTime)
.AnyAsync(x => x.UserId == userId))
{
throw new UserFriendlyException("当前用户已被加入黑名单,请联系管理员处理");
}
}
}

View File

@@ -1,35 +1,70 @@
using Azure;
using Azure.AI.OpenAI;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenAI.Chat;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Application.Contracts.Options;
using Yi.Framework.AiHub.Domain.AiChat;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class AiGateWayManager : DomainService
{
private readonly AiGateWayOptions _options;
private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository;
public AiGateWayManager(IOptions<AiGateWayOptions> options)
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository)
{
this._options = options.Value;
_aiAppRepository = aiAppRepository;
}
public IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,
CancellationToken cancellationToken)
/// <summary>
/// 获取模型
/// </summary>
/// <param name="modelId"></param>
/// <returns></returns>
private async Task<AiModelDescribe> GetModelAsync(string modelId)
{
foreach (var chat in _options.Chats)
var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync();
foreach (var app in allApp)
{
if (chat.Value.ModelIds.Contains(modelId))
var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId);
if (model is not null)
{
var chatService = LazyServiceProvider.GetRequiredKeyedService<IChatService>(chat.Key);
return chatService.CompleteChatAsync(modelId, messages, cancellationToken);
return new AiModelDescribe
{
AppId = app.Id,
AppName = app.Name,
Endpoint = app.Endpoint,
ApiKey = app.ApiKey,
OrderNum = model.OrderNum,
HandlerName = model.HandlerName,
ModelId = model.ModelId,
ModelName = model.Name,
Description = model.Description
};
}
}
throw new UserFriendlyException($"当前暂不支持该模型-【{modelId}】");
throw new UserFriendlyException($"{modelId}模型当前版本不支持");
}
/// <summary>
/// 聊天完成
/// </summary>
/// <param name="modelId"></param>
/// <param name="messages"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var modelDescribe = await GetModelAsync(modelId);
var chatService = LazyServiceProvider.GetRequiredKeyedService<IChatService>(modelDescribe.HandlerName);
await foreach (var result in chatService.CompleteChatAsync(modelDescribe, messages, cancellationToken))
{
yield return result;
}
}
}

View File

@@ -1,11 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using OpenAI.Chat;
using Volo.Abp.Caching;
using Volo.Abp.Domain;
using Yi.Framework.AiHub.Application.Contracts.Options;
using Yi.Framework.AiHub.Domain.AiChat;
using Yi.Framework.AiHub.Domain.AiChat.Impl;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared;
using Yi.Framework.Mapster;
@@ -23,11 +17,10 @@ namespace Yi.Framework.AiHub.Domain
var configuration = context.Services.GetConfiguration();
var services = context.Services;
Configure<AiGateWayOptions>(configuration.GetSection("AiGateWay"));
services.AddKeyedTransient<IChatService, AzureChatService>(nameof(AzureChatService));
services.AddKeyedTransient<IChatService, AzureRestChatService>(nameof(AzureRestChatService));
// Configure<AiGateWayOptions>(configuration.GetSection("AiGateWay"));
//
// services.AddKeyedTransient<IChatService, AzureChatService>(nameof(AzureChatService));
// services.AddKeyedTransient<IChatService, AzureRestChatService>(nameof(AzureRestChatService));
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)