feat: 完成ai message、session搭建

This commit is contained in:
ccnetcore
2025-06-21 13:02:38 +08:00
parent 29985e2118
commit ac04e846fa
18 changed files with 353 additions and 58 deletions

View File

@@ -0,0 +1,15 @@
using Volo.Abp.Application.Dtos;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class MessageDto : FullAuditedEntityDto<Guid>
{
public Guid UserId { get; set; }
public Guid SessionId { get; set; }
public string Content { get; set; }
public string Role { get; set; }
public decimal DeductCost { get; set; }
public decimal TotalTokens { get; set; }
public string ModelId { get; set; }
public string Remark { get; set; }
}

View File

@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class MessageGetListInput:PagedAllResultRequestDto
{
[Required]
public Guid SessionId { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class MessageInputDto
{
public string Content { get; set; }
public string Role { get; set; }
public decimal DeductCost { get; set; }
public decimal TotalTokens { get; set; }
public string ModelId { get; set; }
public string Remark { get; set; }
}

View File

@@ -4,6 +4,8 @@ public class SendMessageInput
{
public List<Message> Messages { get; set; }
public string Model { get; set; }
public Guid? SessionId{ get; set; }
}
public class Message

View File

@@ -0,0 +1,10 @@
using Volo.Abp.Application.Dtos;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SessionDto : FullAuditedEntityDto<Guid>
{
public string SessionTitle { get; set; }
public string SessionContent { get; set; }
public string Remark { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SessionGetListInput
{
}

View File

@@ -0,0 +1,9 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SessionInputDto
{
public Guid UserId { get; set; }
public string SessionTitle { get; set; }
public string SessionContent { get; set; }
public string Remark { get; set; }
}

View File

@@ -6,21 +6,28 @@ 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.Managers;
namespace Yi.Framework.AiHub.Application.Services;
public class AiService : ApplicationService
/// <summary>
/// ai服务
/// </summary>
public class AiChatService : ApplicationService
{
private readonly AiGateWayOptions _options;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AiMessageManager _aiMessageManager;
public AiService(IOptions<AiGateWayOptions> options, IHttpContextAccessor httpContextAccessor)
public AiChatService(IOptions<AiGateWayOptions> options, IHttpContextAccessor httpContextAccessor,
AiMessageManager aiMessageManager)
{
_options = options.Value;
this._httpContextAccessor = httpContextAccessor;
_aiMessageManager = aiMessageManager;
}
@@ -92,6 +99,20 @@ public class AiService : ApplicationService
//断开连接
await writer.WriteLineAsync($"data: done\n");
await writer.FlushAsync(cancellationToken); // 确保立即推送数据
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
// });
}
}

View File

@@ -0,0 +1,40 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
public class MessageService : ApplicationService
{
private readonly ISqlSugarRepository<MessageAggregateRoot> _repository;
public MessageService(ISqlSugarRepository<MessageAggregateRoot> repository)
{
_repository = repository;
}
/// <summary>
/// 查询消息
/// 需要会话id
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Authorize]
public async Task<PagedResultDto<MessageDto>> GetListAsync(MessageGetListInput input)
{
RefAsync<int> total = 0;
var userId = CurrentUser.GetId();
var entities = await _repository._DbQueryable
.Where(x => x.SessionId == input.SessionId)
.Where(x=>x.UserId == userId)
.OrderByDescending(x => x.Id)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
return new PagedResultDto<MessageDto>(total, entities.Adapt<List<MessageDto>>());
}
}

View File

@@ -0,0 +1,83 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
public class SessionService : CrudAppService<SessionAggregateRoot, SessionDto, Guid, SessionGetListInput>
{
private readonly ISqlSugarRepository<SessionAggregateRoot, Guid> _repository;
public readonly ISqlSugarRepository<MessageAggregateRoot, Guid> _messageRepository;
public SessionService(ISqlSugarRepository<SessionAggregateRoot, Guid> repository, ISqlSugarRepository<MessageAggregateRoot, Guid> messageRepository) : base(repository)
{
_repository = repository;
_messageRepository = messageRepository;
}
/// <summary>
/// 创建会话
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Authorize]
public override async Task<SessionDto> CreateAsync(SessionDto input)
{
var entity = await MapToEntityAsync(input);
entity.UserId = CurrentUser.GetId();
await _repository.InsertAsync(entity);
return entity.Adapt<SessionDto>();
}
/// <summary>
/// 详情会话
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Authorize]
public override Task<SessionDto> GetAsync(Guid id)
{
return base.GetAsync(id);
}
/// <summary>
/// 编辑会话
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
[Authorize]
public override Task<SessionDto> UpdateAsync(Guid id, SessionDto input)
{
return base.UpdateAsync(id, input);
}
/// <summary>
/// 删除会话
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Authorize]
public override async Task DeleteAsync(Guid id)
{
await base.DeleteAsync(id);
//对应的消息一起删除
await _messageRepository.DeleteAsync(x => x.SessionId == id);
}
/// <summary>
/// 查询会话
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Authorize]
public override Task<PagedResultDto<SessionDto>> GetListAsync(SessionGetListInput input)
{
return base.GetListAsync(input);
}
}

View File

@@ -0,0 +1,31 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
[SugarTable("Ai_Message")]
[SugarIndex($"index_{{table}}_{nameof(UserId)}", $"{nameof(UserId)}", OrderByType.Asc)]
public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public MessageAggregateRoot()
{
}
public MessageAggregateRoot(Guid userId, Guid sessionId, string content, string role, string modelId)
{
UserId = userId;
SessionId = sessionId;
Content = content;
Role = role;
ModelId = modelId;
}
public Guid UserId { get; set; }
public Guid SessionId { get; set; }
public string Content { get; set; }
public string Role { get; set; }
public decimal DeductCost { get; set; }
public decimal TotalTokens { get; set; }
public string ModelId { get; set; }
public string Remark { get; set; }
}

View File

@@ -0,0 +1,14 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
[SugarTable("Ai_Session")]
[SugarIndex($"index_{{table}}_{nameof(UserId)}",$"{nameof(UserId)}", OrderByType.Asc)]
public class SessionAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public Guid UserId { get; set; }
public string SessionTitle { get; set; }
public string SessionContent { get; set; }
public string Remark { get; set; }
}

View File

@@ -0,0 +1,30 @@
using Volo.Abp.Domain.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class AiMessageManager : DomainService
{
private readonly ISqlSugarRepository<MessageAggregateRoot> _repository;
public AiMessageManager(ISqlSugarRepository<MessageAggregateRoot> repository)
{
_repository = repository;
}
/// <summary>
/// 创建消息
/// </summary>
/// <param name="sessionId"></param>
/// <param name="userId"></param>
/// <param name="input"></param>
/// <returns></returns>
public async Task CreateMessageAsync(Guid userId, Guid sessionId, MessageInputDto input)
{
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId);
await _repository.InsertAsync(message);
}
}

View File

@@ -10,7 +10,7 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
Task<UserRoleMenuDto> GetAsync();
Task<CaptchaImageDto> GetCaptchaImageAsync();
Task<LoginOutputDto> PostLoginAsync(LoginInputVo input);
Task PostRegisterAsync(RegisterDto input);
Task<LoginOutputDto> PostRegisterAsync(RegisterDto input);
Task<bool> RestPasswordAsync(Guid userId, RestPasswordDto input);
/// <summary>

View File

@@ -5,11 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.Caching;
@@ -82,7 +78,7 @@ namespace Yi.Framework.Rbac.Application.Services
/// 校验图片登录验证码,无需和账号绑定
/// </summary>
[RemoteService(isEnabled: false)]
public void ValidationImageCaptcha(string? uuid,string? code )
private void ValidationImageCaptcha(string? uuid, string? code)
{
if (_rbacOptions.EnableCaptcha)
{
@@ -174,7 +170,7 @@ namespace Yi.Framework.Rbac.Application.Services
/// <summary>
/// 验证电话号码
/// </summary>
/// <param name="str_handset"></param>
/// <param name="phone"></param>
private async Task ValidationPhone(string phone)
{
var res = Regex.IsMatch(phone, @"^\d{11}$");
@@ -310,7 +306,7 @@ namespace Yi.Framework.Rbac.Application.Services
/// <returns></returns>
[AllowAnonymous]
[UnitOfWork]
public async Task PostRegisterAsync(RegisterDto input)
public async Task<LoginOutputDto> PostRegisterAsync(RegisterDto input)
{
if (_rbacOptions.EnableRegister == false)
{
@@ -321,6 +317,7 @@ namespace Yi.Framework.Rbac.Application.Services
{
throw new UserFriendlyException("手机号不能为空");
}
//临时账号
if (input.UserName.StartsWith("ls_"))
{
@@ -333,8 +330,9 @@ namespace Yi.Framework.Rbac.Application.Services
await ValidationPhoneCaptchaAsync(ValidationPhoneTypeEnum.Register, input.Phone.Value, input.Code);
}
//注册领域逻辑
await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Nick);
//注册之后免再次登录直接给前端token
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Nick);
return await this.PostLoginAsync(userId);
}
/// <summary>
@@ -423,13 +421,17 @@ namespace Yi.Framework.Rbac.Application.Services
{
//将后端菜单转换成前端路由,组件级别需要过滤
output =
ObjectMapper.Map<List<MenuDto>, List<MenuAggregateRoot>>(menus.Where(x=>x.MenuSource==MenuSourceEnum.Ruoyi).ToList()).Vue3RuoYiRouterBuild();
ObjectMapper
.Map<List<MenuDto>, List<MenuAggregateRoot>>(menus
.Where(x => x.MenuSource == MenuSourceEnum.Ruoyi).ToList()).Vue3RuoYiRouterBuild();
}
else if (routerType == "pure")
{
//将后端菜单转换成前端路由,组件级别需要过滤
output =
ObjectMapper.Map<List<MenuDto>, List<MenuAggregateRoot>>(menus.Where(x=>x.MenuSource==MenuSourceEnum.Pure).ToList()).Vue3PureRouterBuild();
ObjectMapper
.Map<List<MenuDto>, List<MenuAggregateRoot>>(menus
.Where(x => x.MenuSource == MenuSourceEnum.Pure).ToList()).Vue3PureRouterBuild();
}
return output;

View File

@@ -272,11 +272,12 @@ namespace Yi.Framework.Rbac.Domain.Managers
/// <param name="password"></param>
/// <param name="phone"></param>
/// <returns></returns>
public async Task RegisterAsync(string userName, string password, long? phone,string? nick)
public async Task<Guid> RegisterAsync(string userName, string password, long? phone,string? nick)
{
var user = new UserAggregateRoot(userName, password, phone,nick);
await _userManager.CreateAsync(user);
var userId=await _userManager.CreateAsync(user);
await _userManager.SetDefautRoleAsync(user.Id);
return userId;
}
}

View File

@@ -14,7 +14,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
string CreateRefreshToken(Guid userId);
Task<string> GetTokenByUserIdAsync(Guid userId,Action<UserRoleMenuDto>? getUserInfo=null);
Task LoginValidationAsync(string userName, string password, Action<UserAggregateRoot>? userAction = null);
Task RegisterAsync(string userName, string password, long? phone,string? nick);
Task<Guid> RegisterAsync(string userName, string password, long? phone,string? nick);
Task<bool> RestPasswordAsync(Guid userId, string password);
Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword);
}

View File

@@ -29,9 +29,16 @@ namespace Yi.Framework.Rbac.Domain.Managers
private readonly IGuidGenerator _guidGenerator;
private IUserRepository _userRepository;
private ILocalEventBus _localEventBus;
public UserManager(ISqlSugarRepository<UserAggregateRoot> repository, ISqlSugarRepository<UserRoleEntity> repositoryUserRole, ISqlSugarRepository<UserPostEntity> repositoryUserPost, IGuidGenerator guidGenerator, IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache, IUserRepository userRepository, ILocalEventBus localEventBus, ISqlSugarRepository<RoleAggregateRoot> roleRepository) =>
(_repository, _repositoryUserRole, _repositoryUserPost, _guidGenerator, _userCache, _userRepository, _localEventBus, _roleRepository) =
(repository, repositoryUserRole, repositoryUserPost, guidGenerator, userCache, userRepository, localEventBus, roleRepository);
public UserManager(ISqlSugarRepository<UserAggregateRoot> repository,
ISqlSugarRepository<UserRoleEntity> repositoryUserRole,
ISqlSugarRepository<UserPostEntity> repositoryUserPost, IGuidGenerator guidGenerator,
IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache, IUserRepository userRepository,
ILocalEventBus localEventBus, ISqlSugarRepository<RoleAggregateRoot> roleRepository) =>
(_repository, _repositoryUserRole, _repositoryUserPost, _guidGenerator, _userCache, _userRepository,
_localEventBus, _roleRepository) =
(repository, repositoryUserRole, repositoryUserPost, guidGenerator, userCache, userRepository,
localEventBus, roleRepository);
/// <summary>
/// 给用户设置角色
@@ -56,6 +63,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
userRoleEntities.Add(new UserRoleEntity() { UserId = userId, RoleId = roleId });
}
//一次性批量添加
await _repositoryUserRole.InsertRangeAsync(userRoleEntities);
}
@@ -88,7 +96,6 @@ namespace Yi.Framework.Rbac.Domain.Managers
//一次性批量添加
await _repositoryUserPost.InsertRangeAsync(userPostEntities);
}
}
}
@@ -96,7 +103,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
/// 创建用户
/// </summary>
/// <returns></returns>
public async Task CreateAsync(UserAggregateRoot userEntity)
public async Task<Guid> CreateAsync(UserAggregateRoot userEntity)
{
//校验用户名
ValidateUserName(userEntity);
@@ -111,7 +118,6 @@ namespace Yi.Framework.Rbac.Domain.Managers
if (await _repository.IsAnyAsync(x => x.Phone == userEntity.Phone))
{
throw new UserFriendlyException(UserConst.Phone_Repeat);
}
}
@@ -123,10 +129,8 @@ namespace Yi.Framework.Rbac.Domain.Managers
var entity = await _repository.InsertReturnEntityAsync(userEntity);
userEntity = entity;
await _localEventBus.PublishAsync(new UserCreateEventArgs(entity.Id));
return entity.Id;
}
@@ -174,17 +178,20 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
throw new AbpAuthorizationException();
}
//data.Menus.Clear();
// output = data;
return data;
// var output = await GetInfoByCacheAsync(userId);
// return output;
}
private async Task<UserRoleMenuDto> GetInfoByCacheAsync(Guid userId)
{
//此处优先从缓存中获取
UserRoleMenuDto output = null;
var tokenExpiresMinuteTime = LazyServiceProvider.GetRequiredService<IOptions<JwtOptions>>().Value.ExpiresMinuteTime;
var tokenExpiresMinuteTime =
LazyServiceProvider.GetRequiredService<IOptions<JwtOptions>>().Value.ExpiresMinuteTime;
var cacheData = await _userCache.GetOrAddAsync(new UserInfoCacheKey(userId),
async () =>
{
@@ -195,16 +202,19 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
throw new AbpAuthorizationException();
}
//data.Menus.Clear();
output = data;
return new UserInfoCacheItem(data);
},
() => new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(tokenExpiresMinuteTime) });
() => new DistributedCacheEntryOptions
{ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(tokenExpiresMinuteTime) });
if (cacheData is not null)
{
output = cacheData.Info;
}
return output!;
}
@@ -221,14 +231,13 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
output.Add(await GetInfoByCacheAsync(userId));
}
return output;
}
private UserRoleMenuDto EntityMapToDto(UserAggregateRoot user)
{
var userRoleMenu = new UserRoleMenuDto();
//首先获取到该用户全部信息,导航到角色、菜单,(菜单需要去重,完全交给Set来处理即可)
if (user is null)
@@ -236,6 +245,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
//为了解决token前端缓存后端数据库重新dbseed
throw new UserFriendlyException($"数据错误,查询用户不存在,请重新登录");
}
user.EncryPassword.Password = string.Empty;
user.EncryPassword.Salt = string.Empty;
@@ -264,6 +274,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
userRoleMenu.PermissionCodes.Add(menu.PermissionCode);
}
userRoleMenu.Menus.Add(menu.Adapt<MenuDto>());
}
}
@@ -279,5 +290,4 @@ namespace Yi.Framework.Rbac.Domain.Managers
return userRoleMenu;
}
}
}