mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-14 21:46:38 +08:00
Compare commits
63 Commits
ai-hub-dev
...
alipay
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a60bcc174 | ||
|
|
2b3fad16fd | ||
|
|
f0cf6bf5c8 | ||
|
|
0ba4e3240b | ||
|
|
9332b17fc1 | ||
|
|
4ec4023f40 | ||
|
|
d9971541f2 | ||
|
|
7b0e4fcc73 | ||
|
|
cfde73d13a | ||
|
|
c17c9000a8 | ||
|
|
42d537a68b | ||
|
|
25eebec8f7 | ||
|
|
bbe5b01872 | ||
|
|
6b31536de5 | ||
|
|
2e5db5500f | ||
|
|
7038d31c53 | ||
|
|
3eb27c3d35 | ||
|
|
a9c3a1bcec | ||
|
|
384926e73a | ||
|
|
4335c12659 | ||
|
|
e6e4829164 | ||
|
|
f3c67cf598 | ||
|
|
4681d468ce | ||
|
|
63e7d3d5f5 | ||
|
|
f47d8c8ce3 | ||
|
|
6f69f45ddc | ||
|
|
e73678c788 | ||
|
|
09a2f91cbf | ||
|
|
29da7499a4 | ||
|
|
5b024e9443 | ||
|
|
225932eff1 | ||
|
|
65d5f5ae86 | ||
|
|
3e647ef14d | ||
|
|
7cb3aea2e6 | ||
|
|
7f4b8f1c8a | ||
|
|
0a2710b865 | ||
|
|
2a301c4983 | ||
|
|
faa8131a1b | ||
|
|
71bd885bd0 | ||
|
|
691a1e50f0 | ||
|
|
ef6e9fd16d | ||
|
|
17f9ac6d54 | ||
|
|
3f8e6e48c0 | ||
|
|
bda4fdf69d | ||
|
|
5c85ed13fd | ||
|
|
1986901031 | ||
|
|
e1d3ec21e5 | ||
|
|
f45283dade | ||
|
|
31c44d8df7 | ||
|
|
bf443963c8 | ||
|
|
a0eb234539 | ||
|
|
b6d670c240 | ||
|
|
b5fb2c42c6 | ||
|
|
d72cc529ba | ||
|
|
660bd00cae | ||
|
|
b5489711ec | ||
|
|
76717c4f8a | ||
|
|
3d53d0bcd6 | ||
|
|
c7c9428b68 | ||
|
|
991a970d6a | ||
|
|
cbe93b9f7e | ||
|
|
5d7217b775 | ||
|
|
d6836b8bcf |
@@ -0,0 +1,14 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单输入DTO
|
||||||
|
/// </summary>
|
||||||
|
public class CreateOrderInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品类型
|
||||||
|
/// </summary>
|
||||||
|
public GoodsTypeEnum GoodsType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单输出DTO
|
||||||
|
/// </summary>
|
||||||
|
public class CreateOrderOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商家订单号
|
||||||
|
/// </summary>
|
||||||
|
public string OutTradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付页面HTML内容
|
||||||
|
/// </summary>
|
||||||
|
public object PaymentPageHtml { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询订单状态输入DTO
|
||||||
|
/// </summary>
|
||||||
|
public class QueryOrderStatusInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商家订单号
|
||||||
|
/// </summary>
|
||||||
|
public string OutTradeNo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询订单状态输出DTO
|
||||||
|
/// </summary>
|
||||||
|
public class QueryOrderStatusOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商家订单号
|
||||||
|
/// </summary>
|
||||||
|
public string OutTradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝交易号
|
||||||
|
/// </summary>
|
||||||
|
public string? TradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易状态
|
||||||
|
/// </summary>
|
||||||
|
public TradeStatusEnum TradeStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易状态描述
|
||||||
|
/// </summary>
|
||||||
|
public string TradeStatusDescription { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名称
|
||||||
|
/// </summary>
|
||||||
|
public string GoodsName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品类型
|
||||||
|
/// </summary>
|
||||||
|
public GoodsTypeEnum GoodsType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后修改时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastModificationTime { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
|
||||||
|
|
||||||
|
public class RechargeCreateInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户ID
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Range(0.01, double.MaxValue, ErrorMessage = "充值金额必须大于0")]
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值内容
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[StringLength(500, ErrorMessage = "充值内容不能超过500个字符")]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到期时间(为空表示永久VIP)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ExpireDateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(1000, ErrorMessage = "备注不能超过1000个字符")]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系方式
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(200, ErrorMessage = "联系方式不能超过200个字符")]
|
||||||
|
public string? ContactInfo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日Token使用量统计DTO
|
||||||
|
/// </summary>
|
||||||
|
public class DailyTokenUsageDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日期
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token消耗量
|
||||||
|
/// </summary>
|
||||||
|
public long Tokens { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模型Token使用量统计DTO
|
||||||
|
/// </summary>
|
||||||
|
public class ModelTokenUsageDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模型ID
|
||||||
|
/// </summary>
|
||||||
|
public string Model { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总消耗量
|
||||||
|
/// </summary>
|
||||||
|
public long Tokens { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 占比(百分比)
|
||||||
|
/// </summary>
|
||||||
|
public decimal Percentage { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IPayService : IApplicationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单并发起支付
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">创建订单输入</param>
|
||||||
|
/// <returns>订单创建结果</returns>
|
||||||
|
Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝异步通知处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="form">表单数据</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<string> AlipayNotifyAsync([FromForm] IFormCollection form);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询订单状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询订单状态输入</param>
|
||||||
|
/// <returns>订单状态信息</returns>
|
||||||
|
Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
|
|
||||||
|
public interface IRechargeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 移除用户vip及角色
|
||||||
|
/// </summary>
|
||||||
|
Task RemoveVipRoleByExpireAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用量统计服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IUsageStatisticsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户近7天的Token消耗统计
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>每日Token使用量列表</returns>
|
||||||
|
Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户各个模型的Token消耗量及占比
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>模型Token使用量列表</returns>
|
||||||
|
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
|
||||||
|
}
|
||||||
@@ -6,8 +6,5 @@
|
|||||||
<ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" />
|
<ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
|
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="IServices\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ using OpenAI.Chat;
|
|||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Extensions;
|
using Yi.Framework.AiHub.Domain.Extensions;
|
||||||
using Yi.Framework.AiHub.Domain.Managers;
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||||
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
@@ -68,6 +69,7 @@ public class AiChatService : ApplicationService
|
|||||||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||||||
{
|
{
|
||||||
var output = await _aiModelRepository._DbQueryable
|
var output = await _aiModelRepository._DbQueryable
|
||||||
|
.Where(x => x.ModelType == ModelTypeEnum.Chat)
|
||||||
.OrderByDescending(x => x.OrderNum)
|
.OrderByDescending(x => x.OrderNum)
|
||||||
.Select(x => new ModelGetListOutput
|
.Select(x => new ModelGetListOutput
|
||||||
{
|
{
|
||||||
@@ -94,7 +96,8 @@ public class AiChatService : ApplicationService
|
|||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <param name="sessionId"></param>
|
/// <param name="sessionId"></param>
|
||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromRoute] Guid sessionId,
|
[HttpPost("ai-chat/send")]
|
||||||
|
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromQuery] Guid? sessionId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
//除了免费模型,其他的模型都要校验
|
//除了免费模型,其他的模型都要校验
|
||||||
@@ -114,6 +117,7 @@ public class AiChatService : ApplicationService
|
|||||||
throw new UserFriendlyException("未登录用户,只能使用未加速的DeepSeek-R1,请登录后重试");
|
throw new UserFriendlyException("未登录用户,只能使用未加速的DeepSeek-R1,请登录后重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//ai网关代理httpcontext
|
//ai网关代理httpcontext
|
||||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||||
CurrentUser.Id, sessionId, cancellationToken);
|
CurrentUser.Id, sessionId, cancellationToken);
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Extensions;
|
using Yi.Framework.AiHub.Domain.Extensions;
|
||||||
using Yi.Framework.AiHub.Domain.Managers;
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Services;
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
@@ -17,16 +20,17 @@ public class OpenApiService : ApplicationService
|
|||||||
private readonly TokenManager _tokenManager;
|
private readonly TokenManager _tokenManager;
|
||||||
private readonly AiGateWayManager _aiGateWayManager;
|
private readonly AiGateWayManager _aiGateWayManager;
|
||||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||||
|
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||||
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
|
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
|
||||||
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
|
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
|
||||||
ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager)
|
||||||
{
|
{
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_tokenManager = tokenManager;
|
_tokenManager = tokenManager;
|
||||||
_aiGateWayManager = aiGateWayManager;
|
_aiGateWayManager = aiGateWayManager;
|
||||||
_aiModelRepository = aiModelRepository;
|
_aiModelRepository = aiModelRepository;
|
||||||
|
_aiBlacklistManager = aiBlacklistManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,6 +45,7 @@ public class OpenApiService : ApplicationService
|
|||||||
//前面都是校验,后面才是真正的调用
|
//前面都是校验,后面才是真正的调用
|
||||||
var httpContext = this._httpContextAccessor.HttpContext;
|
var httpContext = this._httpContextAccessor.HttpContext;
|
||||||
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||||
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
//ai网关代理httpcontext
|
//ai网关代理httpcontext
|
||||||
if (input.Stream == true)
|
if (input.Stream == true)
|
||||||
{
|
{
|
||||||
@@ -55,6 +60,35 @@ public class OpenApiService : ApplicationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片生成
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
[HttpPost("openApi/v1/images/generations")]
|
||||||
|
public async Task ImagesGenerationsAsync([FromBody] ImageCreateRequest input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpContext = this._httpContextAccessor.HttpContext;
|
||||||
|
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||||
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
|
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向量生成
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
[HttpPost("openApi/v1/embeddings")]
|
||||||
|
public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpContext = this._httpContextAccessor.HttpContext;
|
||||||
|
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||||
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
|
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取模型列表
|
/// 获取模型列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -63,6 +97,7 @@ public class OpenApiService : ApplicationService
|
|||||||
public async Task<ModelsListDto> ModelsAsync()
|
public async Task<ModelsListDto> ModelsAsync()
|
||||||
{
|
{
|
||||||
var data = await _aiModelRepository._DbQueryable
|
var data = await _aiModelRepository._DbQueryable
|
||||||
|
.Where(x => x.ModelType == ModelTypeEnum.Chat)
|
||||||
.OrderByDescending(x => x.OrderNum)
|
.OrderByDescending(x => x.OrderNum)
|
||||||
.Select(x => new ModelsDataDto
|
.Select(x => new ModelsDataDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
using Yi.Framework.AiHub.Domain.Alipay;
|
||||||
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付服务
|
||||||
|
/// </summary>
|
||||||
|
public class PayService : ApplicationService, IPayService
|
||||||
|
{
|
||||||
|
private readonly AlipayManager _alipayManager;
|
||||||
|
private readonly PayManager _payManager;
|
||||||
|
private readonly ILogger<PayService> _logger;
|
||||||
|
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
|
||||||
|
|
||||||
|
public PayService(
|
||||||
|
AlipayManager alipayManager,
|
||||||
|
PayManager payManager,
|
||||||
|
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
|
||||||
|
{
|
||||||
|
_alipayManager = alipayManager;
|
||||||
|
_payManager = payManager;
|
||||||
|
_logger = logger;
|
||||||
|
_payOrderRepository = payOrderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单并发起支付
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">创建订单输入</param>
|
||||||
|
/// <returns>订单创建结果</returns>
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("pay/Order")]
|
||||||
|
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
|
||||||
|
{
|
||||||
|
// 1. 通过PayManager创建订单
|
||||||
|
var order = await _payManager.CreateOrderAsync(input.GoodsType);
|
||||||
|
|
||||||
|
// 2. 通过AlipayManager发起页面支付
|
||||||
|
var paymentPageHtml = await _alipayManager.PaymentPageAsync(
|
||||||
|
order.GoodsName,
|
||||||
|
order.OutTradeNo,
|
||||||
|
order.TotalAmount);
|
||||||
|
|
||||||
|
// 3. 返回结果
|
||||||
|
return new CreateOrderOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
OutTradeNo = order.OutTradeNo,
|
||||||
|
PaymentPageHtml = paymentPageHtml.Body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝异步通知处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="form">表单数据</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("pay/AlipayNotify")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<string> AlipayNotifyAsync([FromForm] IFormCollection form)
|
||||||
|
{
|
||||||
|
// 1. 将表单数据转换为字典,保持原始顺序
|
||||||
|
var notifyData = new Dictionary<string, string>();
|
||||||
|
foreach (var item in form)
|
||||||
|
{
|
||||||
|
notifyData[item.Key] = item.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var signStr = string.Join("&", notifyData.Select(kv => $"{kv.Key}={kv.Value}"));
|
||||||
|
_logger.LogInformation($"收到支付宝回调通知:{signStr}");
|
||||||
|
|
||||||
|
// 2. 验证签名
|
||||||
|
await _alipayManager.VerifyNotifyAsync(notifyData);
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 记录支付通知
|
||||||
|
await _payManager.RecordPayNoticeAsync(notifyData,signStr);
|
||||||
|
|
||||||
|
// 4. 更新订单状态
|
||||||
|
var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty);
|
||||||
|
var tradeStatus = notifyData.GetValueOrDefault("trade_status", string.Empty);
|
||||||
|
var tradeNo = notifyData.GetValueOrDefault("trade_no", string.Empty);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus))
|
||||||
|
{
|
||||||
|
var status = ParseTradeStatus(tradeStatus);
|
||||||
|
await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
|
||||||
|
|
||||||
|
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AlipayException($"回调格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询订单状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询订单状态输入</param>
|
||||||
|
/// <returns>订单状态信息</returns>
|
||||||
|
[HttpGet("pay/OrderStatus")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input)
|
||||||
|
{
|
||||||
|
// 通过PayManager查询订单
|
||||||
|
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == input.OutTradeNo);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException($"订单不存在:{input.OutTradeNo}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryOrderStatusOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
OutTradeNo = order.OutTradeNo,
|
||||||
|
TradeNo = order.TradeNo,
|
||||||
|
TradeStatus = order.TradeStatus,
|
||||||
|
TradeStatusDescription = GetTradeStatusDescription(order.TradeStatus),
|
||||||
|
TotalAmount = order.TotalAmount,
|
||||||
|
GoodsName = order.GoodsName,
|
||||||
|
GoodsType = order.GoodsType,
|
||||||
|
CreationTime = order.CreationTime,
|
||||||
|
LastModificationTime = order.LastModificationTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取交易状态描述
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tradeStatus">交易状态</param>
|
||||||
|
/// <returns>状态描述</returns>
|
||||||
|
private string GetTradeStatusDescription(TradeStatusEnum tradeStatus)
|
||||||
|
{
|
||||||
|
var fieldInfo = tradeStatus.GetType().GetField(tradeStatus.ToString());
|
||||||
|
var descriptionAttribute = fieldInfo?.GetCustomAttribute<DescriptionAttribute>();
|
||||||
|
return descriptionAttribute?.Description ?? "未知状态";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析交易状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tradeStatus">状态字符串</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<TradeStatusEnum>(tradeStatus, out var result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return TradeStatusEnum.WAIT_TRADE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,92 @@
|
|||||||
using Mapster;
|
using Mapster;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||||
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Services;
|
namespace Yi.Framework.AiHub.Application.Services
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ai 充值表
|
|
||||||
/// </summary>
|
|
||||||
public class RechargeService : ApplicationService
|
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
|
public class RechargeService : ApplicationService,IRechargeService
|
||||||
|
|
||||||
public RechargeService(ISqlSugarRepository<AiRechargeAggregateRoot> repository)
|
|
||||||
{
|
{
|
||||||
_repository = repository;
|
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
|
||||||
}
|
private readonly ICurrentUser _currentUser;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IRoleService _roleService;
|
||||||
|
private readonly AiRechargeManager _aiMessageManager;
|
||||||
|
|
||||||
/// <summary>
|
public RechargeService(
|
||||||
/// 查询已登录的账户充值记录
|
ISqlSugarRepository<AiRechargeAggregateRoot> repository,
|
||||||
/// </summary>
|
ICurrentUser currentUser,
|
||||||
/// <returns></returns>
|
IUserService userService, IRoleService roleService, AiRechargeManager aiMessageManager)
|
||||||
[Route("recharge/account")]
|
{
|
||||||
[Authorize]
|
_repository = repository;
|
||||||
public async Task<List<RechargeGetListOutput>> GetListByAccountAsync()
|
_currentUser = currentUser;
|
||||||
{
|
_userService = userService;
|
||||||
var userId = CurrentUser.Id;
|
_roleService = roleService;
|
||||||
var entities = await _repository._DbQueryable.Where(x => x.UserId == userId)
|
_aiMessageManager = aiMessageManager;
|
||||||
.OrderByDescending(x => x.CreationTime)
|
}
|
||||||
.ToListAsync();
|
|
||||||
var output = entities.Adapt<List<RechargeGetListOutput>>();
|
/// <summary>
|
||||||
return output;
|
/// 查询已登录的账户充值记录
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Route("recharge/account")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<List<RechargeGetListOutput>> GetListByAccountAsync()
|
||||||
|
{
|
||||||
|
var userId = CurrentUser.Id;
|
||||||
|
var entities = await _repository._DbQueryable.Where(x => x.UserId == userId)
|
||||||
|
.OrderByDescending(x => x.CreationTime)
|
||||||
|
.ToListAsync();
|
||||||
|
var output = entities.Adapt<List<RechargeGetListOutput>>();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 给用户充值VIP
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">充值输入参数</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("recharge/vip")]
|
||||||
|
public async Task RechargeVipAsync(RechargeCreateInput input)
|
||||||
|
{
|
||||||
|
// 创建充值记录
|
||||||
|
var rechargeRecord = new AiRechargeAggregateRoot
|
||||||
|
{
|
||||||
|
UserId = input.UserId,
|
||||||
|
RechargeAmount = input.RechargeAmount,
|
||||||
|
Content = input.Content,
|
||||||
|
ExpireDateTime = input.ExpireDateTime,
|
||||||
|
Remark = input.Remark,
|
||||||
|
ContactInfo = input.ContactInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存充值记录到数据库
|
||||||
|
await _repository.InsertAsync(rechargeRecord);
|
||||||
|
|
||||||
|
// 使用UserService给用户添加VIP角色
|
||||||
|
await _userService.AddUserRoleByRoleCodeAsync(input.UserId,
|
||||||
|
new List<string>() { AiHubConst.VipRole, "default" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除用户vip及角色
|
||||||
|
/// </summary>
|
||||||
|
[RemoteService(isEnabled: false)]
|
||||||
|
public async Task RemoveVipRoleByExpireAsync()
|
||||||
|
{
|
||||||
|
var expiredUserIds = await _aiMessageManager.RemoveVipByExpireAsync();
|
||||||
|
if (expiredUserIds is not null)
|
||||||
|
{
|
||||||
|
await _roleService.RemoveUserRoleByRoleCodeAsync(expiredUserIds, AiHubConst.VipRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ public class TokenService : ApplicationService
|
|||||||
{
|
{
|
||||||
if (!CurrentUser.IsAiVip())
|
if (!CurrentUser.IsAiVip())
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException("充值成为Vip,畅想第三方token服务");
|
throw new UserFriendlyException("充值成为Vip,畅享第三方token服务");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _tokenManager.CreateAsync(CurrentUser.GetId());
|
await _tokenManager.CreateAsync(CurrentUser.GetId());
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
using Volo.Abp.Users;
|
||||||
|
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.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用量统计服务
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
public class UsageStatisticsService : ApplicationService, IUsageStatisticsService
|
||||||
|
{
|
||||||
|
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
|
||||||
|
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||||
|
|
||||||
|
public UsageStatisticsService(
|
||||||
|
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||||
|
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||||
|
{
|
||||||
|
_messageRepository = messageRepository;
|
||||||
|
_usageStatisticsRepository = usageStatisticsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户近7天的Token消耗统计
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>每日Token使用量列表</returns>
|
||||||
|
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync()
|
||||||
|
{
|
||||||
|
var userId = CurrentUser.GetId();
|
||||||
|
var endDate = DateTime.Today;
|
||||||
|
var startDate = endDate.AddDays(-6); // 近7天
|
||||||
|
|
||||||
|
// 从Message表统计近7天的token消耗
|
||||||
|
var dailyUsage = await _messageRepository._DbQueryable
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
|
||||||
|
.GroupBy(x => x.CreationTime.Date)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
Date = g.CreationTime.Date,
|
||||||
|
Tokens = SqlFunc.AggregateSum(g.TokenUsage.TotalTokenCount)
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 生成完整的7天数据,包括没有使用记录的日期
|
||||||
|
var result = new List<DailyTokenUsageDto>();
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
var date = startDate.AddDays(i);
|
||||||
|
var usage = dailyUsage.FirstOrDefault(x => x.Date == date);
|
||||||
|
|
||||||
|
result.Add(new DailyTokenUsageDto
|
||||||
|
{
|
||||||
|
Date = date,
|
||||||
|
Tokens = usage?.Tokens ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.OrderBy(x => x.Date).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户各个模型的Token消耗量及占比
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>模型Token使用量列表</returns>
|
||||||
|
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync()
|
||||||
|
{
|
||||||
|
var userId = CurrentUser.GetId();
|
||||||
|
|
||||||
|
// 从UsageStatistics表获取各模型的token消耗统计
|
||||||
|
var modelUsages = await _usageStatisticsRepository._DbQueryable
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.ModelId,
|
||||||
|
x.TotalTokenCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!modelUsages.Any())
|
||||||
|
{
|
||||||
|
return new List<ModelTokenUsageDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总token数
|
||||||
|
var totalTokens = modelUsages.Sum(x => x.TotalTokenCount);
|
||||||
|
|
||||||
|
// 计算各模型占比
|
||||||
|
var result = modelUsages.Select(x => new ModelTokenUsageDto
|
||||||
|
{
|
||||||
|
Model = x.ModelId,
|
||||||
|
Tokens = x.TotalTokenCount,
|
||||||
|
Percentage = totalTokens > 0 ? Math.Round((decimal)x.TotalTokenCount / totalTokens * 100, 2) : 0
|
||||||
|
}).OrderByDescending(x => x.Tokens).ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||||
|
|
||||||
|
public class AiHubConst
|
||||||
|
{
|
||||||
|
public const string VipRole = "YiXinAi-Vip";
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using SqlSugar;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
|
||||||
public class MessageInputDto
|
public class MessageInputDto
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
|
||||||
|
//TODO add model validation
|
||||||
|
//TODO check what is string or array for prompt,..
|
||||||
|
public record EmbeddingCreateRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
|
||||||
|
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
|
||||||
|
/// length.
|
||||||
|
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
|
||||||
|
/// observed inferior results when newlines are present.
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<string>? InputAsList { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
|
||||||
|
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
|
||||||
|
/// length.
|
||||||
|
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
|
||||||
|
/// observed inferior results when newlines are present.
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? Input { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[JsonPropertyName("input")]
|
||||||
|
public IList<string>? InputCalculated
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Input != null && InputAsList != null)
|
||||||
|
{
|
||||||
|
throw new ValidationException(
|
||||||
|
"Input and InputAsList can not be assigned at the same time. One of them is should be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input != null)
|
||||||
|
{
|
||||||
|
return new List<string> { Input };
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputAsList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your
|
||||||
|
/// available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-model" />
|
||||||
|
[JsonPropertyName("model")]
|
||||||
|
public string? Model { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models.
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings-create-dimensions" />
|
||||||
|
[JsonPropertyName("dimensions")]
|
||||||
|
public int? Dimensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The format to return the embeddings in. Can be either float or base64.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[JsonPropertyName("encoding_format")]
|
||||||
|
public string? EncodingFormat { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
|
||||||
|
public record EmbeddingCreateResponse : ThorBaseResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("model")] public string Model { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")] public List<EmbeddingResponse> Data { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 类型转换,如果类型是base64,则将float[]转换为base64,如果是空或是float和原始类型一样,则不转换
|
||||||
|
/// </summary>
|
||||||
|
public void ConvertEmbeddingData(string? encodingFormat)
|
||||||
|
{
|
||||||
|
if (Data.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (encodingFormat)
|
||||||
|
{
|
||||||
|
// 判断第一个是否是float[],如果是则不转换
|
||||||
|
case null or "float" when Data[0].Embedding is float[]:
|
||||||
|
return;
|
||||||
|
// 否则转换成float[]
|
||||||
|
case null or "float":
|
||||||
|
{
|
||||||
|
foreach (var embeddingResponse in Data)
|
||||||
|
{
|
||||||
|
if (embeddingResponse.Embedding is string base64)
|
||||||
|
{
|
||||||
|
embeddingResponse.Embedding = Convert.FromBase64String(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 判断第一个是否是string,如果是则不转换
|
||||||
|
case "base64" when Data[0].Embedding is string:
|
||||||
|
return;
|
||||||
|
// 否则转换成base64
|
||||||
|
case "base64":
|
||||||
|
{
|
||||||
|
foreach (var embeddingResponse in Data)
|
||||||
|
{
|
||||||
|
if (embeddingResponse.Embedding is JsonElement str)
|
||||||
|
{
|
||||||
|
if (str.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var floats = str.EnumerateArray().Select(element => element.GetSingle()).ToArray();
|
||||||
|
|
||||||
|
embeddingResponse.Embedding = ConvertFloatArrayToBase64(floats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (embeddingResponse.Embedding is IList<double> doubles)
|
||||||
|
{
|
||||||
|
embeddingResponse.Embedding = ConvertFloatArrayToBase64(doubles.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConvertFloatArrayToBase64(double[] floatArray)
|
||||||
|
{
|
||||||
|
// 将 float[] 转换成 byte[]
|
||||||
|
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
|
||||||
|
|
||||||
|
// 将 byte[] 转换成 base64 字符串
|
||||||
|
return Convert.ToBase64String(byteArray);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(byteArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConvertFloatArrayToBase64(float[] floatArray)
|
||||||
|
{
|
||||||
|
// 将 float[] 转换成 byte[]
|
||||||
|
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(floatArray, 0, byteArray, 0, floatArray.Length);
|
||||||
|
|
||||||
|
// 将 byte[] 转换成 base64 字符串
|
||||||
|
return Convert.ToBase64String(byteArray);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(byteArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EmbeddingResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("object")] public string Object { get; set; } = "embedding";
|
||||||
|
|
||||||
|
[JsonPropertyName("index")] public int? Index { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("embedding")] public object Embedding { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
|
||||||
|
public sealed class ThorEmbeddingInput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("model")]
|
||||||
|
public string Model { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("input")]
|
||||||
|
public object Input { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("encoding_format")]
|
||||||
|
public string? EncodingFormat { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("dimensions")]
|
||||||
|
public int? Dimensions { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
public string? User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Image Create Request Model
|
||||||
|
/// </summary>
|
||||||
|
public record ImageCreateRequest : SharedImageRequestBaseModel
|
||||||
|
{
|
||||||
|
public ImageCreateRequest()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageCreateRequest(string prompt)
|
||||||
|
{
|
||||||
|
Prompt = prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("prompt")]
|
||||||
|
public string Prompt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The quality of the image that will be generated. Possible values are 'standard' or 'hd' (default is 'standard').
|
||||||
|
/// Hd creates images with finer details and greater consistency across the image.
|
||||||
|
/// This param is only supported for dall-e-3 model.
|
||||||
|
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Quality"/> for possible values
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("quality")]
|
||||||
|
public string? Quality { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The style of the generated images. Must be one of vivid or natural.
|
||||||
|
/// Vivid causes the model to lean towards generating hyper-real and dramatic images.
|
||||||
|
/// Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3.
|
||||||
|
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Style"/> for possible values
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("style")]
|
||||||
|
public string? Style { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("background")]
|
||||||
|
public string? Background { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("moderation")]
|
||||||
|
public string? Moderation { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("output_compression")]
|
||||||
|
public string? OutputCompression { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("output_format")]
|
||||||
|
public string? OutputFormat { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
public record ImageCreateResponse : ThorBaseResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")] public List<ImageDataResult> Results { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; } = new();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public record ImageDataResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("url")] public string Url { get; set; }
|
||||||
|
[JsonPropertyName("b64_json")] public string B64 { get; set; }
|
||||||
|
[JsonPropertyName("revised_prompt")] public string RevisedPrompt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
public record ImageEditCreateRequest : SharedImageRequestBaseModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? Image { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Image file name
|
||||||
|
/// </summary>
|
||||||
|
public string ImageName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited.
|
||||||
|
/// Must be a valid PNG file, less than 4MB, and have the same dimensions as image.
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? Mask { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mask file name
|
||||||
|
/// </summary>
|
||||||
|
public string? MaskName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("quality")]
|
||||||
|
public string Quality { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A text description of the desired image(s). The maximum length is 1000 characters.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("prompt")]
|
||||||
|
public string Prompt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("background")]
|
||||||
|
public string? Background { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("moderation")]
|
||||||
|
public string? Moderation { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("output_compression")]
|
||||||
|
public string? OutputCompression { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("output_format")]
|
||||||
|
public string? OutputFormat { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("style")]
|
||||||
|
public string? Style { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
public record ImageVariationCreateRequest : SharedImageRequestBaseModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Image { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Image file name
|
||||||
|
/// </summary>
|
||||||
|
public string ImageName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
public record SharedImageRequestBaseModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of images to generate. Must be between 1 and 10.
|
||||||
|
/// For dall-e-3 model, only n=1 is supported.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("n")]
|
||||||
|
public int? N { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size of the generated images.
|
||||||
|
/// Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2.
|
||||||
|
/// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.
|
||||||
|
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Size"/> for possible values
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("size")]
|
||||||
|
public string? Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The format in which the generated images are returned. Must be one of url or b64_json
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("response_format")]
|
||||||
|
public string? ResponseFormat { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
|
||||||
|
/// <a href="https://platform.openai.com/docs/usage-policies/end-user-ids">Learn more</a>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
public string? User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The model to use for image generation. Must be one of dall-e-2 or dall-e-3
|
||||||
|
/// For ImageEditCreateRequest and for ImageVariationCreateRequest only dall-e-2 modell is supported at this time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("model")]
|
||||||
|
public string? Model { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public class ModelsListDto
|
public class ModelsListDto
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OpenAI常量
|
/// OpenAI常量
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public record ThorBaseResponse
|
public record ThorBaseResponse
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public sealed class ThorChatAudioRequest
|
public sealed class ThorChatAudioRequest
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 聊天完成选项列
|
/// 聊天完成选项列
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public class ThorChatClaudeThinking
|
public class ThorChatClaudeThinking
|
||||||
{
|
{
|
||||||
@@ -2,20 +2,14 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对话补全请求参数对象
|
/// 对话补全请求参数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ThorChatCompletionsRequest
|
public class ThorChatCompletionsRequest
|
||||||
{
|
{
|
||||||
public ThorChatCompletionsRequest()
|
[JsonPropertyName("store")] public bool? Store { get; set; }
|
||||||
{
|
|
||||||
Messages = new List<ThorChatMessage>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("store")]
|
|
||||||
public bool? Store { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示对话中支持的模态类型数组。可以为 null。
|
/// 表示对话中支持的模态类型数组。可以为 null。
|
||||||
@@ -26,14 +20,72 @@ public class ThorChatCompletionsRequest
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示对话中的音频请求参数。可以为 null。
|
/// 表示对话中的音频请求参数。可以为 null。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("audio")] public ThorChatAudioRequest? Audio { get; set; }
|
[JsonPropertyName("audio")]
|
||||||
|
public ThorChatAudioRequest? Audio { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 包含迄今为止对话的消息列表
|
/// 包含迄今为止对话的消息列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("messages")]
|
[JsonPropertyName("messages")]
|
||||||
public List<ThorChatMessage> Messages { get; set; }
|
public List<ThorChatMessage>? Messages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼容-代码补全
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("suffix")]
|
||||||
|
public string? Suffix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼容-代码补全
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("prompt")]
|
||||||
|
public string? Prompt { get; set; }
|
||||||
|
|
||||||
|
private const string CodeCompletionPrompt = """
|
||||||
|
You are a code modification assistant. Your task is to modify the provided code based on the user's instructions.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Return only the modified code, with no additional text or explanations.
|
||||||
|
2. The first character of your response must be the first character of the code.
|
||||||
|
3. The last character of your response must be the last character of the code.
|
||||||
|
4. NEVER use triple backticks (```) or any other markdown formatting in your response.
|
||||||
|
5. Do not use any code block indicators, syntax highlighting markers, or any other formatting characters.
|
||||||
|
6. Present the code exactly as it would appear in a plain text editor, preserving all whitespace, indentation, and line breaks.
|
||||||
|
7. Maintain the original code structure and only make changes as specified by the user's instructions.
|
||||||
|
8. Ensure that the modified code is syntactically and semantically correct for the given programming language.
|
||||||
|
9. Use consistent indentation and follow language-specific style guidelines.
|
||||||
|
10. If the user's request cannot be translated into code changes, respond only with the word NULL (without quotes or any formatting).
|
||||||
|
11. Do not include any comments or explanations within the code unless specifically requested.
|
||||||
|
12. Assume that any necessary dependencies or libraries are already imported or available.
|
||||||
|
|
||||||
|
IMPORTANT: Your response must NEVER begin or end with triple backticks, single backticks, or any other formatting characters.
|
||||||
|
|
||||||
|
The relevant context before the current editing content is: {0}.
|
||||||
|
After the current editing content is: {1}.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼容代码补全
|
||||||
|
/// </summary>
|
||||||
|
public void CompatibleCodeCompletion()
|
||||||
|
{
|
||||||
|
if (Messages is null || !Messages.Any())
|
||||||
|
{
|
||||||
|
//兼容代码补全模式,Prompt为当前代码前内容,Suffix为当前代码后内容
|
||||||
|
Messages = new List<ThorChatMessage>()
|
||||||
|
{
|
||||||
|
new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = "system",
|
||||||
|
Content = string.Format(CodeCompletionPrompt, Prompt, Suffix)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Suffix = null;
|
||||||
|
Prompt = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模型唯一编码值,如 gpt-4,gpt-3.5-turbo,moonshot-v1-8k,看底层具体平台定义
|
/// 模型唯一编码值,如 gpt-4,gpt-3.5-turbo,moonshot-v1-8k,看底层具体平台定义
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -229,18 +281,25 @@ public class ThorChatCompletionsRequest
|
|||||||
{
|
{
|
||||||
if (value is JsonElement jsonElement)
|
if (value is JsonElement jsonElement)
|
||||||
{
|
{
|
||||||
if (jsonElement.ValueKind == JsonValueKind.String)
|
// if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
{
|
// {
|
||||||
ToolChoice = new ThorToolChoice
|
// ToolChoice = new ThorToolChoice
|
||||||
{
|
// {
|
||||||
Type = jsonElement.GetString()
|
// Type = jsonElement.GetString()
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
else if (jsonElement.ValueKind == JsonValueKind.Object)
|
if (jsonElement.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
ToolChoice = jsonElement.Deserialize<ThorToolChoice>();
|
ToolChoice = jsonElement.Deserialize<ThorToolChoice>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (value is string text)
|
||||||
|
{
|
||||||
|
ToolChoice = new ThorToolChoice
|
||||||
|
{
|
||||||
|
Type = text
|
||||||
|
};
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ToolChoice = (ThorToolChoice)value;
|
ToolChoice = (ThorToolChoice)value;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对话补全服务返回结果
|
/// 对话补全服务返回结果
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 聊天消息体,建议使用CreeateXXX系列方法构建内容
|
/// 聊天消息体,建议使用CreeateXXX系列方法构建内容
|
||||||
@@ -14,7 +14,6 @@ public class ThorChatMessage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ThorChatMessage()
|
public ThorChatMessage()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -87,7 +86,6 @@ public class ThorChatMessage
|
|||||||
{
|
{
|
||||||
Content = value?.ToString();
|
Content = value?.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,15 +106,14 @@ public class ThorChatMessage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("function_call")]
|
[JsonPropertyName("function_call")]
|
||||||
public ThorChatMessageFunction? FunctionCall { get; set; }
|
public ThorChatMessageFunction? FunctionCall { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【可选】推理内容
|
/// 【可选】推理内容
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("reasoning_content")]
|
[JsonPropertyName("reasoning_content")]
|
||||||
public string? ReasoningContent { get; set; }
|
public string? ReasoningContent { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
public string? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 工具调用列表,模型生成的工具调用,例如函数调用。<br/>
|
/// 工具调用列表,模型生成的工具调用,例如函数调用。<br/>
|
||||||
@@ -164,14 +161,15 @@ public class ThorChatMessage
|
|||||||
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
|
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
|
||||||
/// <param name="toolCalls">工具调用参数列表</param>
|
/// <param name="toolCalls">工具调用参数列表</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, List<ThorToolCall> toolCalls = null)
|
public static ThorChatMessage CreateAssistantMessage(string content, string? name = null,
|
||||||
|
List<ThorToolCall> toolCalls = null)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Role = ThorChatMessageRoleConst.Assistant,
|
Role = ThorChatMessageRoleConst.Assistant,
|
||||||
Content = content,
|
Content = content,
|
||||||
Name = name,
|
Name = name,
|
||||||
ToolCalls=toolCalls,
|
ToolCalls = toolCalls,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +185,7 @@ public class ThorChatMessage
|
|||||||
{
|
{
|
||||||
Role = ThorChatMessageRoleConst.Tool,
|
Role = ThorChatMessageRoleConst.Tool,
|
||||||
Content = content,
|
Content = content,
|
||||||
ToolCallId= toolCallId
|
ToolCallId = toolCallId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public sealed class ThorChatMessageAudioContent
|
public sealed class ThorChatMessageAudioContent
|
||||||
{
|
{
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发出的消息内容,包含图文,一般是一文一图,一文多图两种情况,请使用CreeateXXX系列方法构建内容
|
/// 发出的消息内容,包含图文,一般是一文一图,一文多图两种情况,请使用CreeateXXX系列方法构建内容
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模型调用的函数。
|
/// 模型调用的函数。
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对话消息角色定义
|
/// 对话消息角色定义
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
public class ThorError
|
public class ThorError
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支持图片识别的消息体内容类型
|
/// 支持图片识别的消息体内容类型
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 指定模型必须输出的格式的对象。用于启用JSON模式。
|
/// 指定模型必须输出的格式的对象。用于启用JSON模式。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
public class ThorResponseJsonSchema
|
public class ThorResponseJsonSchema
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 流响应选项。仅当您设置 stream: true 时才设置此项。
|
/// 流响应选项。仅当您设置 stream: true 时才设置此项。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 工具调用对象定义
|
/// 工具调用对象定义
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 工具
|
/// 工具
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
public class ThorToolChoiceFunctionTool
|
public class ThorToolChoiceFunctionTool
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
public class ThorToolChoiceTypeConst
|
public class ThorToolChoiceTypeConst
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 有效工具的定义。
|
/// 有效工具的定义。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 有效函数调用的定义。
|
/// 有效函数调用的定义。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 函数参数是JSON格式对象
|
/// 函数参数是JSON格式对象
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 工具类型定义
|
/// 工具类型定义
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 统计信息模型
|
/// 统计信息模型
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图片消息内容对象
|
/// 图片消息内容对象
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 价格特性
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class PriceAttribute : Attribute
|
||||||
|
{
|
||||||
|
public decimal Price { get; }
|
||||||
|
|
||||||
|
public PriceAttribute(double price)
|
||||||
|
{
|
||||||
|
Price = (decimal)price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示名称特性
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class DisplayNameAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public DisplayNameAttribute(string displayName)
|
||||||
|
{
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品枚举
|
||||||
|
/// </summary>
|
||||||
|
public enum GoodsTypeEnum
|
||||||
|
{
|
||||||
|
[Price(0.01)]
|
||||||
|
[DisplayName("YiXinVip Test")]
|
||||||
|
YiXinVipTest = 0,
|
||||||
|
|
||||||
|
[Price(29.9)]
|
||||||
|
[DisplayName("YiXinVip 1 month")]
|
||||||
|
YiXinVip1 = 1,
|
||||||
|
|
||||||
|
[Price(80.7)]
|
||||||
|
[DisplayName("YiXinVip 3 month")]
|
||||||
|
YiXinVip3 = 3,
|
||||||
|
|
||||||
|
[Price(143.9)]
|
||||||
|
[DisplayName("YiXinVip 6 month")]
|
||||||
|
YiXinVip6 = 6,
|
||||||
|
|
||||||
|
[Price(199.9)]
|
||||||
|
[DisplayName("YiXinVip 10 month")]
|
||||||
|
YiXinVip10 = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GoodsTypeEnumExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品总金额
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="goodsType">商品类型</param>
|
||||||
|
/// <returns>总金额</returns>
|
||||||
|
public static decimal GetTotalAmount(this GoodsTypeEnum goodsType)
|
||||||
|
{
|
||||||
|
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||||
|
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
|
||||||
|
return priceAttribute?.Price ?? 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品价格描述
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="goodsType">商品类型</param>
|
||||||
|
/// <returns>价格描述</returns>
|
||||||
|
public static string GetPriceDescription(this GoodsTypeEnum goodsType)
|
||||||
|
{
|
||||||
|
var price = goodsType.GetTotalAmount();
|
||||||
|
return $"¥{price:F1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品名称
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="goodsType">商品类型</param>
|
||||||
|
/// <returns>商品名称</returns>
|
||||||
|
public static string GetDisplayName(this GoodsTypeEnum goodsType)
|
||||||
|
{
|
||||||
|
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||||
|
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
|
||||||
|
return displayNameAttribute?.DisplayName ?? goodsType.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
public enum ModelTypeEnum
|
||||||
|
{
|
||||||
|
Chat = 0,
|
||||||
|
Image = 1,
|
||||||
|
Embedding = 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
public enum TradeStatusEnum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 准备发起
|
||||||
|
/// </summary>
|
||||||
|
[Description("准备发起")]
|
||||||
|
WAIT_TRADE = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易创建
|
||||||
|
/// </summary>
|
||||||
|
[Description("等待买家付款")]
|
||||||
|
WAIT_BUYER_PAY = 10,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易关闭
|
||||||
|
/// </summary>
|
||||||
|
[Description("交易关闭")]
|
||||||
|
TRADE_CLOSED = 20,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易成功
|
||||||
|
/// </summary>
|
||||||
|
[Description("交易成功")]
|
||||||
|
TRADE_SUCCESS = 100,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易结束
|
||||||
|
/// </summary>
|
||||||
|
[Description("交易结束")]
|
||||||
|
TRADE_FINISHED = -10
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Consts\" />
|
|
||||||
<Folder Include="Etos\" />
|
<Folder Include="Etos\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
public interface IImageService
|
||||||
|
{
|
||||||
|
/// <summary>Creates an image given a prompt.</summary>
|
||||||
|
/// <param name="imageCreate"></param>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ImageCreateResponse> CreateImage(
|
||||||
|
ImageCreateRequest imageCreate,
|
||||||
|
AiModelDescribe? aiModelDescribe = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an edited or extended image given an original image and a prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="imageEditCreateRequest"></param>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ImageCreateResponse> CreateImageEdit(
|
||||||
|
ImageEditCreateRequest imageEditCreateRequest,
|
||||||
|
AiModelDescribe? aiModelDescribe = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>Creates a variation of a given image.</summary>
|
||||||
|
/// <param name="imageEditCreateRequest"></param>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ImageCreateResponse> CreateImageVariation(
|
||||||
|
ImageVariationCreateRequest imageEditCreateRequest,
|
||||||
|
AiModelDescribe? aiModelDescribe = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
public interface ITextEmbeddingService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="createEmbeddingModel"></param>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<EmbeddingCreateResponse> EmbeddingAsync(
|
||||||
|
EmbeddingCreateRequest createEmbeddingModel,
|
||||||
|
AiModelDescribe? aiModelDescribe = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ using System.Net.Http.Json;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
||||||
|
|
||||||
@@ -108,33 +108,33 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = result?.Choices?.FirstOrDefault()?.Delta;
|
// var content = result?.Choices?.FirstOrDefault()?.Delta;
|
||||||
|
//
|
||||||
if (first && content?.Content == OpenAIConstant.ThinkStart)
|
// if (first && content?.Content == OpenAIConstant.ThinkStart)
|
||||||
{
|
// {
|
||||||
isThink = true;
|
// isThink = true;
|
||||||
continue;
|
// continue;
|
||||||
// 需要将content的内容转换到其他字段
|
// // 需要将content的内容转换到其他字段
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
|
// if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
|
||||||
{
|
// {
|
||||||
isThink = false;
|
// isThink = false;
|
||||||
// 需要将content的内容转换到其他字段
|
// // 需要将content的内容转换到其他字段
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (isThink && result?.Choices != null)
|
// if (isThink && result?.Choices != null)
|
||||||
{
|
// {
|
||||||
// 需要将content的内容转换到其他字段
|
// // 需要将content的内容转换到其他字段
|
||||||
foreach (var choice in result.Choices)
|
// foreach (var choice in result.Choices)
|
||||||
{
|
// {
|
||||||
choice.Delta.ReasoningContent = choice.Delta.Content;
|
// choice.Delta.ReasoningContent = choice.Delta.Content;
|
||||||
choice.Delta.Content = string.Empty;
|
// choice.Delta.Content = string.Empty;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
first = false;
|
// first = false;
|
||||||
|
|
||||||
yield return result;
|
yield return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ using System.Net.Http.Json;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChat
|
|||||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||||
{
|
{
|
||||||
var error = await response.Content.ReadAsStringAsync();
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
logger.LogError("Azure对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
|
logger.LogError("Azure对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
|
||||||
error);
|
error);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using OpenAI.Images;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
||||||
|
|
||||||
|
public class AzureOpenAIServiceImageService : IImageService
|
||||||
|
{
|
||||||
|
public async Task<ImageCreateResponse> CreateImage(ImageCreateRequest imageCreate, AiModelDescribe? options = null,
|
||||||
|
CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var createClient = AzureOpenAIFactory.CreateClient(options);
|
||||||
|
|
||||||
|
var client = createClient.GetImageClient(imageCreate.Model);
|
||||||
|
|
||||||
|
// 将size字符串拆分为宽度和高度
|
||||||
|
var size = imageCreate.Size.Split('x');
|
||||||
|
if (size.Length != 2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Size must be in the format of 'width x height'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var response = await client.GenerateImageAsync(imageCreate.Prompt, new ImageGenerationOptions()
|
||||||
|
{
|
||||||
|
Quality = imageCreate.Quality == "standard" ? GeneratedImageQuality.Standard : GeneratedImageQuality.High,
|
||||||
|
Size = new GeneratedImageSize(Convert.ToInt32(size[0]), Convert.ToInt32(size[1])),
|
||||||
|
Style = imageCreate.Style == "vivid" ? GeneratedImageStyle.Vivid : GeneratedImageStyle.Natural,
|
||||||
|
ResponseFormat =
|
||||||
|
imageCreate.ResponseFormat == "url" ? GeneratedImageFormat.Uri : GeneratedImageFormat.Bytes,
|
||||||
|
// User = imageCreate.User
|
||||||
|
EndUserId = imageCreate.User
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
var ret = new ImageCreateResponse()
|
||||||
|
{
|
||||||
|
Results = new List<ImageCreateResponse.ImageDataResult>()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.Value.ImageUri != null)
|
||||||
|
{
|
||||||
|
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
|
||||||
|
{
|
||||||
|
Url = response.Value.ImageUri.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
|
||||||
|
{
|
||||||
|
B64 = Convert.ToBase64String(response.Value.ImageBytes.ToArray())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImageCreateResponse> CreateImageEdit(ImageEditCreateRequest imageEditCreateRequest,
|
||||||
|
AiModelDescribe? options = null,
|
||||||
|
CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var url = AzureOpenAIFactory.GetEditImageAddress(options, imageEditCreateRequest.Model);
|
||||||
|
|
||||||
|
var multipartContent = new MultipartFormDataContent();
|
||||||
|
if (imageEditCreateRequest.User != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.User), "user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageEditCreateRequest.ResponseFormat != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.ResponseFormat), "response_format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageEditCreateRequest.Size != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.Size), "size");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageEditCreateRequest.N != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.N.ToString()!), "n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageEditCreateRequest.Model != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.Model!), "model");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageEditCreateRequest.Mask != null)
|
||||||
|
{
|
||||||
|
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Mask), "mask",
|
||||||
|
imageEditCreateRequest.MaskName);
|
||||||
|
}
|
||||||
|
|
||||||
|
multipartContent.Add(new StringContent(imageEditCreateRequest.Prompt), "prompt");
|
||||||
|
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Image), "image",
|
||||||
|
imageEditCreateRequest.ImageName);
|
||||||
|
|
||||||
|
return await HttpClientFactory.GetHttpClient(url).PostFileAndReadAsAsync<ImageCreateResponse>(
|
||||||
|
url,
|
||||||
|
multipartContent, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ImageCreateResponse> CreateImageVariation(ImageVariationCreateRequest imageEditCreateRequest,
|
||||||
|
AiModelDescribe? options = null,
|
||||||
|
CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
||||||
|
|
||||||
|
public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletionsService> logger)
|
||||||
|
: IChatCompletionService
|
||||||
|
{
|
||||||
|
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
|
||||||
|
ThorChatCompletionsRequest chatCompletionCreate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Endpoint))
|
||||||
|
{
|
||||||
|
options.Endpoint = "https://api.deepseek.com/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
using var openai =
|
||||||
|
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
|
||||||
|
|
||||||
|
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(
|
||||||
|
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||||
|
chatCompletionCreate, options.ApiKey);
|
||||||
|
|
||||||
|
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||||
|
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.PaymentRequired)
|
||||||
|
{
|
||||||
|
throw new PaymentRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果限流则抛出限流异常
|
||||||
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
throw new ThorRateLimitException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大于等于400的状态码都认为是异常
|
||||||
|
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} ", response.StatusCode);
|
||||||
|
|
||||||
|
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
|
||||||
|
|
||||||
|
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
|
||||||
|
string? line = string.Empty;
|
||||||
|
var first = true;
|
||||||
|
var isThink = false;
|
||||||
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||||
|
{
|
||||||
|
line += Environment.NewLine;
|
||||||
|
|
||||||
|
if (line.StartsWith('{'))
|
||||||
|
{
|
||||||
|
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
|
||||||
|
line);
|
||||||
|
|
||||||
|
throw new BusinessException("OpenAI对话异常", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(OpenAIConstant.Data))
|
||||||
|
line = line[OpenAIConstant.Data.Length..];
|
||||||
|
|
||||||
|
line = line.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||||
|
|
||||||
|
if (line == OpenAIConstant.Done)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(':'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
|
||||||
|
ThorChatCompletionsRequest chatCompletionCreate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var openai =
|
||||||
|
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Endpoint))
|
||||||
|
{
|
||||||
|
options.Endpoint = "https://api.deepseek.com/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
|
||||||
|
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||||
|
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
|
||||||
|
|
||||||
|
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||||
|
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new BusinessException("渠道未登录,请联系管理人员", "401");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果限流则抛出限流异常
|
||||||
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
throw new ThorRateLimitException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大于等于400的状态码都认为是异常
|
||||||
|
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode, error);
|
||||||
|
|
||||||
|
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await response.Content.ReadFromJsonAsync<ThorChatCompletionsResponse>(
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
||||||
|
|
||||||
|
public sealed class SiliconFlowTextEmbeddingService
|
||||||
|
: ITextEmbeddingService
|
||||||
|
{
|
||||||
|
public async Task<EmbeddingCreateResponse> EmbeddingAsync(
|
||||||
|
EmbeddingCreateRequest createEmbeddingModel,
|
||||||
|
AiModelDescribe? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
|
||||||
|
options?.Endpoint.TrimEnd('/') + "/v1/embeddings",
|
||||||
|
createEmbeddingModel, options!.ApiKey);
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await response.Content.ReadFromJsonAsync<EmbeddingCreateResponse>(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Alipay;
|
||||||
|
|
||||||
|
public class AlipayException:UserFriendlyException
|
||||||
|
{
|
||||||
|
public AlipayException(string message, string? code = null, string? details = null, Exception? innerException = null, LogLevel logLevel = LogLevel.Warning) : base(message, code, details, innerException, logLevel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using Alipay.EasySDK.Factory;
|
||||||
|
using Alipay.EasySDK.Kernel.Util;
|
||||||
|
using Alipay.EasySDK.Payment.Page.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Alipay;
|
||||||
|
|
||||||
|
public class AlipayManager : DomainService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AlipayManager> _logger;
|
||||||
|
|
||||||
|
public AlipayManager(ILogger<AlipayManager> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一Page支付
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="AlipayException"></exception>
|
||||||
|
public Task<AlipayTradePagePayResponse> PaymentPageAsync(string productName, string orderNumber,
|
||||||
|
decimal totalAmount)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 发起API调用(以创建当面付收款二维码为例)
|
||||||
|
var response = Factory.Payment.Page()
|
||||||
|
.Pay(productName, orderNumber, totalAmount.ToString(), "https://ccnetcore.com/pay/sucess");
|
||||||
|
// 3. 处理响应或异常
|
||||||
|
if (ResponseChecker.Success(response))
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"支付宝:PaymentPage发起调用成功,返回内容:{response.Body}");
|
||||||
|
//插入数据库
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AlipayException($"支付宝:PaymentPage发起调用失败,原因:{response.Body}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new AlipayException($"支付宝:PaymentPage发起调用错误,原因:{ex.Message}", innerException: ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知验签
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="form"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="AlipayException"></exception>
|
||||||
|
public Task VerifyNotifyAsync(Dictionary<string, string> form)
|
||||||
|
{
|
||||||
|
// 支付宝的验签需要保持原始参数顺序,排序会导致验签失败
|
||||||
|
var result = Factory.Payment.Common().VerifyNotify(form);
|
||||||
|
if (result == false)
|
||||||
|
{
|
||||||
|
_logger.LogError($"支付宝支付验签失败,回调参数:{System.Text.Json.JsonSerializer.Serialize(form)}");
|
||||||
|
throw new AlipayException($"支付宝支付,验签失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("支付宝回调验签成功");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Mapster;
|
using Mapster;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using Volo.Abp.Domain.Entities.Auditing;
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.Entities.ValueObjects;
|
using Yi.Framework.AiHub.Domain.Entities.ValueObjects;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
|
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
@@ -19,7 +19,7 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageAggregateRoot(Guid userId, Guid? sessionId, string content, string role, string modelId,
|
public MessageAggregateRoot(Guid? userId, Guid? sessionId, string content, string role, string modelId,
|
||||||
ThorUsageResponse? tokenUsage)
|
ThorUsageResponse? tokenUsage)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
@@ -29,10 +29,18 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
ModelId = modelId;
|
ModelId = modelId;
|
||||||
if (tokenUsage is not null)
|
if (tokenUsage is not null)
|
||||||
{
|
{
|
||||||
|
long inputTokenCount = tokenUsage.PromptTokens
|
||||||
|
?? tokenUsage.InputTokens
|
||||||
|
?? 0;
|
||||||
|
|
||||||
|
long outputTokenCount = tokenUsage.CompletionTokens
|
||||||
|
?? tokenUsage.OutputTokens
|
||||||
|
?? 0;
|
||||||
|
|
||||||
this.TokenUsage = new TokenUsageValueObject
|
this.TokenUsage = new TokenUsageValueObject
|
||||||
{
|
{
|
||||||
OutputTokenCount = tokenUsage.OutputTokens ?? 0,
|
OutputTokenCount = outputTokenCount,
|
||||||
InputTokenCount = tokenUsage.InputTokens ?? 0,
|
InputTokenCount = inputTokenCount,
|
||||||
TotalTokenCount = tokenUsage.TotalTokens ?? 0
|
TotalTokenCount = tokenUsage.TotalTokens ?? 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -40,11 +48,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web;
|
this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
public Guid? SessionId { get; set; }
|
public Guid? SessionId { get; set; }
|
||||||
|
|
||||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||||
public string Content { get; set; }
|
public string? Content { get; set; }
|
||||||
|
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
public string ModelId { get; set; }
|
public string ModelId { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
using Yi.Framework.Core.Data;
|
using Yi.Framework.Core.Data;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
@@ -8,7 +9,7 @@ namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
|||||||
/// ai模型定义
|
/// ai模型定义
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarTable("Ai_Model")]
|
[SugarTable("Ai_Model")]
|
||||||
public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
|
public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 处理名
|
/// 处理名
|
||||||
@@ -44,9 +45,14 @@ public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
|
|||||||
/// ai应用id
|
/// ai应用id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid AiAppId { get; set; }
|
public Guid AiAppId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 额外信息
|
/// 额外信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ExtraInfo { get; set; }
|
public string? ExtraInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模型类型
|
||||||
|
/// </summary>
|
||||||
|
public ModelTypeEnum ModelType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付通知记录
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("Ai_PayNoticeRecord")]
|
||||||
|
public class PayNoticeRecordAggregateRoot: FullAuditedAggregateRoot<Guid>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通知时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime NotifyTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝交易号
|
||||||
|
/// </summary>
|
||||||
|
public string TradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商家订单号
|
||||||
|
/// </summary>
|
||||||
|
public string OutTradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 买家openId
|
||||||
|
/// </summary>
|
||||||
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态
|
||||||
|
/// </summary>
|
||||||
|
public TradeStatusEnum TradeStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实收金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal ReceiptAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户支付金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal BuyerPayAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知原始数据
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||||
|
public string NotifyData { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始signstr
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||||
|
public string SignStr { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付订单
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("Ai_PayOrder")]
|
||||||
|
public class PayOrderAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商家订单号
|
||||||
|
/// </summary>
|
||||||
|
public string OutTradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单用户
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单用户名称
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态
|
||||||
|
/// </summary>
|
||||||
|
public TradeStatusEnum TradeStatus { get; set; } = TradeStatusEnum.WAIT_TRADE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品类型
|
||||||
|
/// </summary>
|
||||||
|
public GoodsTypeEnum GoodsType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名称
|
||||||
|
/// </summary>
|
||||||
|
public string GoodsName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝交易号
|
||||||
|
/// </summary>
|
||||||
|
public string? TradeNo { get; set; }
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public UsageStatisticsAggregateRoot(Guid userId, string modelId)
|
public UsageStatisticsAggregateRoot(Guid? userId, string modelId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
ModelId = modelId;
|
ModelId = modelId;
|
||||||
@@ -22,7 +22,7 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户id
|
/// 用户id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 哪个模型
|
/// 哪个模型
|
||||||
@@ -37,22 +37,22 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用输出token总数
|
/// 使用输出token总数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UsageOutputTokenCount { get; set; }
|
public long UsageOutputTokenCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用输入总数
|
/// 使用输入总数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UsageInputTokenCount { get; set; }
|
public long UsageInputTokenCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 总token使用数量
|
/// 总token使用数量
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalTokenCount { get; set; }
|
public long TotalTokenCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新增一次聊天统计
|
/// 新增一次聊天统计
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddOnceChat(int inputTokenCount, int outputTokenCount)
|
public void AddOnceChat(long inputTokenCount, long outputTokenCount)
|
||||||
{
|
{
|
||||||
UsageTotalNumber += 1;
|
UsageTotalNumber += 1;
|
||||||
UsageOutputTokenCount += outputTokenCount;
|
UsageOutputTokenCount += outputTokenCount;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
public class TokenUsageValueObject
|
public class TokenUsageValueObject
|
||||||
{
|
{
|
||||||
public int OutputTokenCount { get; set; }
|
public long OutputTokenCount { get; set; }
|
||||||
|
|
||||||
public int InputTokenCount { get; set; }
|
public long InputTokenCount { get; set; }
|
||||||
|
|
||||||
public long TotalTokenCount { get; set; }
|
public long TotalTokenCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Extensions;
|
namespace Yi.Framework.AiHub.Domain.Extensions;
|
||||||
|
|
||||||
@@ -7,6 +8,6 @@ public static class CurrentExtensions
|
|||||||
{
|
{
|
||||||
public static bool IsAiVip(this ICurrentUser currentUser)
|
public static bool IsAiVip(this ICurrentUser currentUser)
|
||||||
{
|
{
|
||||||
return currentUser.Roles.Contains("YiXinAi-Vip") || currentUser.UserName == "cc";
|
return currentUser.Roles.Contains(AiHubConst.VipRole) || currentUser.UserName == "cc";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
|
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay;
|
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
|
using Yi.Framework.Core.Extensions;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
@@ -83,7 +88,7 @@ public class AiGateWayManager : DomainService
|
|||||||
var modelDescribe = await GetModelAsync(request.Model);
|
var modelDescribe = await GetModelAsync(request.Model);
|
||||||
var chatService =
|
var chatService =
|
||||||
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
||||||
|
|
||||||
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken))
|
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken))
|
||||||
{
|
{
|
||||||
yield return result;
|
yield return result;
|
||||||
@@ -109,8 +114,7 @@ public class AiGateWayManager : DomainService
|
|||||||
_specialCompatible.Compatible(request);
|
_specialCompatible.Compatible(request);
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
// 设置响应头,声明是 json
|
// 设置响应头,声明是 json
|
||||||
response.ContentType = "application/json; charset=UTF-8";
|
//response.ContentType = "application/json; charset=UTF-8";
|
||||||
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
|
|
||||||
var modelDescribe = await GetModelAsync(request.Model);
|
var modelDescribe = await GetModelAsync(request.Model);
|
||||||
var chatService =
|
var chatService =
|
||||||
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
||||||
@@ -120,7 +124,7 @@ public class AiGateWayManager : DomainService
|
|||||||
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||||
new MessageInputDto
|
new MessageInputDto
|
||||||
{
|
{
|
||||||
Content = request.Messages.LastOrDefault().Content ?? string.Empty,
|
Content = request.Messages?.LastOrDefault().Content ?? string.Empty,
|
||||||
ModelId = request.Model,
|
ModelId = request.Model,
|
||||||
TokenUsage = data.Usage,
|
TokenUsage = data.Usage,
|
||||||
});
|
});
|
||||||
@@ -133,16 +137,10 @@ public class AiGateWayManager : DomainService
|
|||||||
TokenUsage = data.Usage
|
TokenUsage = data.Usage
|
||||||
});
|
});
|
||||||
|
|
||||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage.InputTokens ?? 0,
|
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage);
|
||||||
data.Usage.OutputTokens ?? 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
await response.WriteAsJsonAsync(data, cancellationToken);
|
||||||
{
|
|
||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
|
||||||
});
|
|
||||||
await writer.WriteLineAsync(body);
|
|
||||||
await writer.FlushAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -163,15 +161,14 @@ public class AiGateWayManager : DomainService
|
|||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
// 设置响应头,声明是 SSE 流
|
// 设置响应头,声明是 SSE 流
|
||||||
response.ContentType = "text/event-stream";
|
response.ContentType = "text/event-stream;charset=utf-8;";
|
||||||
response.Headers.Append("Cache-Control", "no-cache");
|
response.Headers.TryAdd("Cache-Control", "no-cache");
|
||||||
response.Headers.Append("Connection", "keep-alive");
|
response.Headers.TryAdd("Connection", "keep-alive");
|
||||||
|
|
||||||
|
|
||||||
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
|
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
|
||||||
var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken);
|
var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken);
|
||||||
var tokenUsage = new ThorUsageResponse();
|
var tokenUsage = new ThorUsageResponse();
|
||||||
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
|
|
||||||
|
|
||||||
//缓存队列算法
|
//缓存队列算法
|
||||||
// 创建一个队列来缓存消息
|
// 创建一个队列来缓存消息
|
||||||
@@ -189,14 +186,14 @@ public class AiGateWayManager : DomainService
|
|||||||
{
|
{
|
||||||
if (messageQueue.TryDequeue(out var message))
|
if (messageQueue.TryDequeue(out var message))
|
||||||
{
|
{
|
||||||
await writer.WriteLineAsync(message);
|
await response.WriteAsync(message, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
|
||||||
await writer.FlushAsync(cancellationToken);
|
await response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isComplete)
|
if (!isComplete)
|
||||||
{
|
{
|
||||||
// 如果没有完成,才等待,已完成,全部输出
|
// 如果没有完成,才等待,已完成,全部输出
|
||||||
await Task.Delay(outputInterval, cancellationToken);
|
await Task.Delay(outputInterval, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
@@ -207,24 +204,21 @@ public class AiGateWayManager : DomainService
|
|||||||
{
|
{
|
||||||
await foreach (var data in completeChatResponse)
|
await foreach (var data in completeChatResponse)
|
||||||
{
|
{
|
||||||
if (data.Usage is not null && data.Usage.TotalTokens is not null)
|
if (data.Usage is not null)
|
||||||
{
|
{
|
||||||
tokenUsage = data.Usage;
|
tokenUsage = data.Usage;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
var message = System.Text.Json.JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions);
|
||||||
{
|
|
||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
|
||||||
});
|
|
||||||
backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content);
|
backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content);
|
||||||
// 将消息加入队列而不是直接写入
|
// 将消息加入队列而不是直接写入
|
||||||
messageQueue.Enqueue($"data: {message}\n");
|
messageQueue.Enqueue($"data: {message}\n\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Ai对话异常");
|
_logger.LogError(e, $"Ai对话异常");
|
||||||
var errorContent = $"Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}";
|
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
var model = new ThorChatCompletionsResponse()
|
var model = new ThorChatCompletionsResponse()
|
||||||
{
|
{
|
||||||
Choices = new List<ThorChatChoiceResponse>()
|
Choices = new List<ThorChatChoiceResponse>()
|
||||||
@@ -243,36 +237,203 @@ public class AiGateWayManager : DomainService
|
|||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
});
|
});
|
||||||
backupSystemContent.Append(errorContent);
|
backupSystemContent.Append(errorContent);
|
||||||
messageQueue.Enqueue($"data: {message}\n");
|
messageQueue.Enqueue($"data: {message}\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
//断开连接
|
//断开连接
|
||||||
messageQueue.Enqueue("data: [DONE]\n");
|
messageQueue.Enqueue("data: [DONE]\n\n");
|
||||||
// 标记完成并发送结束标记
|
// 标记完成并发送结束标记
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
|
|
||||||
await outputTask;
|
await outputTask;
|
||||||
|
|
||||||
if (userId is not null)
|
|
||||||
|
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = tokenUsage,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = backupSystemContent.ToString(),
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = tokenUsage
|
||||||
|
});
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片生成
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <exception cref="BusinessException"></exception>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task CreateImageForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId,
|
||||||
|
ImageCreateRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
var model = request.Model;
|
||||||
|
if (string.IsNullOrEmpty(model)) model = "dall-e-2";
|
||||||
|
|
||||||
|
var modelDescribe = await GetModelAsync(model);
|
||||||
|
|
||||||
|
// 获取渠道指定的实现类型的服务
|
||||||
|
var imageService =
|
||||||
|
LazyServiceProvider.GetRequiredKeyedService<IImageService>(modelDescribe.HandlerName);
|
||||||
|
|
||||||
|
var response = await imageService.CreateImage(request, modelDescribe);
|
||||||
|
|
||||||
|
if (response.Error != null || response.Results.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(response.Error?.Message ?? "图片生成失败", response.Error?.Code?.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsJsonAsync(response);
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||||
new MessageInputDto
|
new MessageInputDto
|
||||||
{
|
{
|
||||||
Content = request.Messages.LastOrDefault()?.Content ?? string.Empty,
|
Content = request.Prompt,
|
||||||
ModelId = request.Model,
|
ModelId = model,
|
||||||
TokenUsage = tokenUsage,
|
TokenUsage = response.Usage,
|
||||||
});
|
});
|
||||||
|
|
||||||
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||||
new MessageInputDto
|
new MessageInputDto
|
||||||
{
|
{
|
||||||
Content = backupSystemContent.ToString(),
|
Content = response.Results?.FirstOrDefault()?.Url,
|
||||||
ModelId = request.Model,
|
ModelId = model,
|
||||||
TokenUsage = tokenUsage
|
TokenUsage = response.Usage
|
||||||
});
|
});
|
||||||
|
|
||||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, tokenUsage.InputTokens ?? 0,
|
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
|
||||||
tokenUsage.OutputTokens ?? 0);
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var errorContent = $"图片生成Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
|
throw new UserFriendlyException(errorContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向量生成
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
/// <exception cref="BusinessException"></exception>
|
||||||
|
public async Task EmbeddingForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId,
|
||||||
|
ThorEmbeddingInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (input == null) throw new Exception("模型校验异常");
|
||||||
|
|
||||||
|
using var embedding =
|
||||||
|
Activity.Current?.Source.StartActivity("向量模型调用");
|
||||||
|
|
||||||
|
var modelDescribe = await GetModelAsync(input.Model);
|
||||||
|
|
||||||
|
// 获取渠道指定的实现类型的服务
|
||||||
|
var embeddingService =
|
||||||
|
LazyServiceProvider.GetRequiredKeyedService<ITextEmbeddingService>(modelDescribe.HandlerName);
|
||||||
|
|
||||||
|
var embeddingCreateRequest = new EmbeddingCreateRequest
|
||||||
|
{
|
||||||
|
Model = input.Model,
|
||||||
|
EncodingFormat = input.EncodingFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
//dto进行转换,支持多种格式
|
||||||
|
if (input.Input is JsonElement str)
|
||||||
|
{
|
||||||
|
if (str.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
embeddingCreateRequest.Input = str.ToString();
|
||||||
|
}
|
||||||
|
else if (str.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var inputString = str.EnumerateArray().Select(x => x.ToString()).ToArray();
|
||||||
|
embeddingCreateRequest.InputAsList = inputString.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Input,输入格式错误,非string或Array类型");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (input.Input is string strInput)
|
||||||
|
{
|
||||||
|
embeddingCreateRequest.Input = strInput;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Input,输入格式错误,未找到类型");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var stream =
|
||||||
|
await embeddingService.EmbeddingAsync(embeddingCreateRequest, modelDescribe, context.RequestAborted);
|
||||||
|
|
||||||
|
var usage = new ThorUsageResponse()
|
||||||
|
{
|
||||||
|
PromptTokens = stream.Usage?.PromptTokens??0,
|
||||||
|
InputTokens = stream.Usage?.InputTokens ?? 0,
|
||||||
|
CompletionTokens = 0,
|
||||||
|
TotalTokens = stream.Usage?.InputTokens ?? 0
|
||||||
|
};
|
||||||
|
await context.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
input.Model,
|
||||||
|
stream.Data,
|
||||||
|
stream.Error,
|
||||||
|
Object = stream.ObjectTypeName,
|
||||||
|
Usage = usage
|
||||||
|
});
|
||||||
|
|
||||||
|
//知识库暂不使用message统计
|
||||||
|
// await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||||
|
// new MessageInputDto
|
||||||
|
// {
|
||||||
|
// Content = string.Empty,
|
||||||
|
// ModelId = input.Model,
|
||||||
|
// TokenUsage = usage,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||||
|
// new MessageInputDto
|
||||||
|
// {
|
||||||
|
// Content = string.Empty,
|
||||||
|
// ModelId = input.Model,
|
||||||
|
// TokenUsage = usage
|
||||||
|
// });
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage);
|
||||||
|
}
|
||||||
|
catch (ThorRateLimitException)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 429;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException e)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var errorContent = $"嵌入Ai异常,异常信息:\n当前Ai模型:{input.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
@@ -23,7 +23,7 @@ public class AiMessageManager : DomainService
|
|||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task CreateSystemMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input)
|
public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input)
|
||||||
{
|
{
|
||||||
input.Role = "system";
|
input.Role = "system";
|
||||||
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
||||||
@@ -37,7 +37,7 @@ public class AiMessageManager : DomainService
|
|||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task CreateUserMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input)
|
public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input)
|
||||||
{
|
{
|
||||||
input.Role = "user";
|
input.Role = "user";
|
||||||
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
|
||||||
|
public class AiRechargeManager : DomainService
|
||||||
|
{
|
||||||
|
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
|
||||||
|
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||||
|
private readonly ILogger<AiRechargeManager> _logger;
|
||||||
|
|
||||||
|
public AiRechargeManager(ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository,
|
||||||
|
ISqlSugarRepository<TokenAggregateRoot> tokenRepository, ILogger<AiRechargeManager> logger)
|
||||||
|
{
|
||||||
|
_rechargeRepository = rechargeRepository;
|
||||||
|
_tokenRepository = tokenRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Guid>?> RemoveVipByExpireAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("开始执行VIP过期自动卸载任务");
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
var currentTime = DateTime.Now;
|
||||||
|
|
||||||
|
// 查找所有充值记录,按用户分组
|
||||||
|
var allRecharges = await _rechargeRepository._DbQueryable
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!allRecharges.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("没有找到任何充值记录");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按用户分组,找出真正过期的用户
|
||||||
|
var expiredUserIds = allRecharges
|
||||||
|
.GroupBy(x => x.UserId)
|
||||||
|
.Where(group =>
|
||||||
|
{
|
||||||
|
// 如果用户有任何一个过期时间为空的记录,说明是永久VIP,不过期
|
||||||
|
if (group.Any(x => !x.ExpireDateTime.HasValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 找到用户最大的过期时间
|
||||||
|
var maxExpireTime = group.Max(x => x.ExpireDateTime);
|
||||||
|
|
||||||
|
// 如果最大过期时间小于当前时间,说明用户已过期(比较日期,满足用户最后一天)
|
||||||
|
return maxExpireTime.HasValue && maxExpireTime.Value.Date < currentTime.Date;
|
||||||
|
})
|
||||||
|
.Select(group => group.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!expiredUserIds.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("没有找到过期的VIP用户");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"找到 {expiredUserIds.Count} 个过期的VIP用户");
|
||||||
|
|
||||||
|
|
||||||
|
// 删除过期用户的Token密钥
|
||||||
|
var removedTokenCount = await _tokenRepository.DeleteAsync(x => expiredUserIds.Contains(x.UserId));
|
||||||
|
|
||||||
|
_logger.LogInformation($"成功删除 {removedTokenCount} 个用户的Token密钥");
|
||||||
|
_logger.LogInformation($"VIP过期自动卸载任务执行完成,共处理 {expiredUserIds.Count} 个过期用户");
|
||||||
|
|
||||||
|
return expiredUserIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Volo.Abp.Users;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付管理器
|
||||||
|
/// </summary>
|
||||||
|
public class PayManager : DomainService
|
||||||
|
{
|
||||||
|
private readonly ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> _payNoticeRepository;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
|
||||||
|
|
||||||
|
public PayManager(
|
||||||
|
ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> payNoticeRepository,
|
||||||
|
ICurrentUser currentUser, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
|
||||||
|
{
|
||||||
|
_payNoticeRepository = payNoticeRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_payOrderRepository = payOrderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="goodsType">商品类型</param>
|
||||||
|
/// <returns>订单信息</returns>
|
||||||
|
public async Task<PayOrderAggregateRoot> CreateOrderAsync(GoodsTypeEnum goodsType)
|
||||||
|
{
|
||||||
|
// 验证用户是否登录
|
||||||
|
if (!_currentUser.IsAuthenticated)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成订单号
|
||||||
|
var outTradeNo = GenerateOutTradeNo();
|
||||||
|
|
||||||
|
// 获取商品信息
|
||||||
|
var goodsName = goodsType.GetDisplayName();
|
||||||
|
var totalAmount = goodsType.GetTotalAmount();
|
||||||
|
|
||||||
|
// 创建订单实体
|
||||||
|
var payOrder = new PayOrderAggregateRoot
|
||||||
|
{
|
||||||
|
OutTradeNo = outTradeNo,
|
||||||
|
UserId = _currentUser.GetId(),
|
||||||
|
UserName = _currentUser.UserName ?? string.Empty,
|
||||||
|
TotalAmount = totalAmount,
|
||||||
|
GoodsName = goodsName,
|
||||||
|
GoodsType = goodsType
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存订单
|
||||||
|
await _payOrderRepository.InsertAsync(payOrder);
|
||||||
|
|
||||||
|
return payOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新订单状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outTradeNo">商户订单号</param>
|
||||||
|
/// <param name="tradeStatus">交易状态</param>
|
||||||
|
/// <param name="tradeNo">支付宝交易号</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null)
|
||||||
|
{
|
||||||
|
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == outTradeNo);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException($"订单不存在:{outTradeNo}");
|
||||||
|
}
|
||||||
|
|
||||||
|
order.TradeStatus = tradeStatus;
|
||||||
|
if (!string.IsNullOrEmpty(tradeNo))
|
||||||
|
{
|
||||||
|
order.TradeNo = tradeNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _payOrderRepository.UpdateAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录支付通知
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notifyData">通知数据</param>
|
||||||
|
/// <param name="signStr"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task RecordPayNoticeAsync(Dictionary<string, string> notifyData, string signStr)
|
||||||
|
{
|
||||||
|
var payNotice = new PayNoticeRecordAggregateRoot
|
||||||
|
{
|
||||||
|
NotifyTime = DateTime.Parse(notifyData.GetValueOrDefault("notify_time", string.Empty)),
|
||||||
|
TradeNo = notifyData.GetValueOrDefault("trade_no", string.Empty),
|
||||||
|
OutTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty),
|
||||||
|
BuyerId = notifyData.GetValueOrDefault("buyer_id", string.Empty),
|
||||||
|
TradeStatus = ParseTradeStatus(notifyData.GetValueOrDefault("trade_status", string.Empty)),
|
||||||
|
TotalAmount = decimal.TryParse(notifyData.GetValueOrDefault("total_amount", "-1"), out var amount)
|
||||||
|
? amount
|
||||||
|
: 0,
|
||||||
|
NotifyData = JsonSerializer.Serialize(notifyData),
|
||||||
|
SignStr = signStr
|
||||||
|
};
|
||||||
|
|
||||||
|
await _payNoticeRepository.InsertAsync(payNotice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成商户订单号
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string GenerateOutTradeNo()
|
||||||
|
{
|
||||||
|
return $"YI_{DateTime.Now:yyyyMMddHHmmss}_{Random.Shared.Next(1000, 9999)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析交易状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tradeStatus">状态字符串</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
|
||||||
|
{
|
||||||
|
return tradeStatus switch
|
||||||
|
{
|
||||||
|
"WAIT_BUYER_PAY" => TradeStatusEnum.WAIT_BUYER_PAY,
|
||||||
|
"TRADE_SUCCESS" => TradeStatusEnum.TRADE_SUCCESS,
|
||||||
|
"TRADE_FINISHED" => TradeStatusEnum.TRADE_FINISHED,
|
||||||
|
"TRADE_CLOSED" => TradeStatusEnum.TRADE_CLOSED,
|
||||||
|
_ => TradeStatusEnum.WAIT_TRADE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Medallion.Threading;
|
using Medallion.Threading;
|
||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
@@ -17,9 +18,17 @@ public class UsageStatisticsManager : DomainService
|
|||||||
private IDistributedLockProvider DistributedLock =>
|
private IDistributedLockProvider DistributedLock =>
|
||||||
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
|
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
|
||||||
|
|
||||||
public async Task SetUsageAsync(Guid userId, string modelId, int inputTokenCount, int outputTokenCount)
|
public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage)
|
||||||
{
|
{
|
||||||
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId.ToString()}"))
|
long inputTokenCount = tokenUsage?.PromptTokens
|
||||||
|
?? tokenUsage.InputTokens
|
||||||
|
?? 0;
|
||||||
|
|
||||||
|
long outputTokenCount = tokenUsage?.CompletionTokens
|
||||||
|
?? tokenUsage.OutputTokens
|
||||||
|
?? 0;
|
||||||
|
|
||||||
|
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}"))
|
||||||
{
|
{
|
||||||
var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId);
|
var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId);
|
||||||
//存在数据,更细
|
//存在数据,更细
|
||||||
@@ -37,8 +46,4 @@ public class UsageStatisticsManager : DomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal class LazyServiceProvider
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\..\..\common.props" />
|
<Import Project="..\..\..\common.props" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AlipayEasySDK" Version="2.1.3" />
|
||||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
|
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
|
||||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||||
<PackageReference Include="Volo.Abp.DistributedLocking" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.DistributedLocking" Version="$(AbpVersion)" />
|
||||||
@@ -9,8 +10,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\framework\Yi.Framework.Mapster\Yi.Framework.Mapster.csproj" />
|
<ProjectReference Include="..\..\..\framework\Yi.Framework.Mapster\Yi.Framework.Mapster.csproj" />
|
||||||
<ProjectReference Include="..\..\..\framework\Yi.Framework.SqlSugarCore.Abstractions\Yi.Framework.SqlSugarCore.Abstractions.csproj" />
|
<ProjectReference Include="..\..\..\framework\Yi.Framework.SqlSugarCore.Abstractions\Yi.Framework.SqlSugarCore.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\Yi.Framework.AiHub.Application.Contracts\Yi.Framework.AiHub.Application.Contracts.csproj" />
|
|
||||||
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
|
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="AiGateWay\Impl\ThorSiliconFlow\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
using Alipay.EasySDK.Factory;
|
||||||
|
using Alipay.EasySDK.Kernel;
|
||||||
|
using Alipay.EasySDK.Kernel.Util;
|
||||||
|
using Alipay.EasySDK.Payment.FaceToFace.Models;
|
||||||
|
using Dm.util;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Volo.Abp.Domain;
|
using Volo.Abp.Domain;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay;
|
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
||||||
using Yi.Framework.AiHub.Domain.Shared;
|
using Yi.Framework.AiHub.Domain.Shared;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.Mapster;
|
using Yi.Framework.Mapster;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain
|
namespace Yi.Framework.AiHub.Domain
|
||||||
@@ -25,10 +35,21 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
nameof(AzureOpenAiChatCompletionCompletionsService));
|
nameof(AzureOpenAiChatCompletionCompletionsService));
|
||||||
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
|
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
|
||||||
nameof(AzureDatabricksChatCompletionsService));
|
nameof(AzureDatabricksChatCompletionsService));
|
||||||
|
services.AddKeyedTransient<IChatCompletionService, DeepSeekChatCompletionsService>(
|
||||||
|
nameof(DeepSeekChatCompletionsService));
|
||||||
|
|
||||||
|
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
|
||||||
|
nameof(AzureOpenAIServiceImageService));
|
||||||
|
|
||||||
|
|
||||||
|
services.AddKeyedTransient<ITextEmbeddingService, SiliconFlowTextEmbeddingService>(
|
||||||
|
nameof(SiliconFlowTextEmbeddingService));
|
||||||
|
|
||||||
//ai模型特殊性兼容处理
|
//ai模型特殊性兼容处理
|
||||||
Configure<SpecialCompatibleOptions>(options =>
|
Configure<SpecialCompatibleOptions>(options =>
|
||||||
{
|
{
|
||||||
|
options.Handles.add(request => { request.CompatibleCodeCompletion(); });
|
||||||
|
|
||||||
options.Handles.Add(request =>
|
options.Handles.Add(request =>
|
||||||
{
|
{
|
||||||
if (request.Model == "o1")
|
if (request.Model == "o1")
|
||||||
@@ -36,12 +57,34 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
request.Temperature = null;
|
request.Temperature = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
options.Handles.Add(request =>
|
||||||
|
{
|
||||||
|
if (request.Model.StartsWith("o3-mini") || request.Model.StartsWith("o4-mini"))
|
||||||
|
{
|
||||||
|
request.MaxCompletionTokens = request.MaxTokens;
|
||||||
|
request.MaxTokens = null;
|
||||||
|
request.Temperature = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
options.Handles.Add(request =>
|
||||||
|
{
|
||||||
|
if (request.Stream == true)
|
||||||
|
{
|
||||||
|
request.StreamOptions = new ThorStreamOptions()
|
||||||
|
{
|
||||||
|
IncludeUsage = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//配置支付宝支付
|
||||||
|
var config = configuration.GetSection("Alipay").Get<Config>();
|
||||||
|
Factory.SetOptions(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
||||||
{
|
{
|
||||||
var service = context.ServiceProvider;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,12 +23,13 @@ public class AccessLogMiddleware : IMiddleware, ITransientDependency
|
|||||||
{
|
{
|
||||||
_accessLogNumber = 0;
|
_accessLogNumber = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static int GetAccessLogNumber()
|
internal static int GetAccessLogNumber()
|
||||||
{
|
{
|
||||||
return _accessLogNumber;
|
return _accessLogNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
@@ -64,28 +65,25 @@ public class AccessLogResetEventHandler : ILocalEventHandler<AccessLogResetArgs>
|
|||||||
return redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled);
|
return redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//该事件由job定时10秒触发
|
//该事件由job定时10秒触发
|
||||||
public async Task HandleEventAsync(AccessLogResetArgs eventData)
|
public async Task HandleEventAsync(AccessLogResetArgs eventData)
|
||||||
{
|
{
|
||||||
if (EnableRedisCache)
|
if (EnableRedisCache)
|
||||||
{
|
{
|
||||||
//分布式锁
|
//分布式锁
|
||||||
if (await RedisClient.SetNxAsync("AccessLogLock",true,TimeSpan.FromSeconds(5)))
|
if (await RedisClient.SetNxAsync("AccessLogLock", true, TimeSpan.FromSeconds(5)))
|
||||||
{
|
{
|
||||||
//自增长数
|
//自增长数
|
||||||
var incrNumber= AccessLogMiddleware.GetAccessLogNumber();
|
var incrNumber = AccessLogMiddleware.GetAccessLogNumber();
|
||||||
//立即重置,开始计算,方式丢失
|
//立即重置,开始计算,方式丢失
|
||||||
AccessLogMiddleware.ResetAccessLogNumber();
|
AccessLogMiddleware.ResetAccessLogNumber();
|
||||||
if (incrNumber>0)
|
if (incrNumber > 0)
|
||||||
{
|
{
|
||||||
await RedisClient.IncrByAsync(
|
await RedisClient.IncrByAsync(
|
||||||
$"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date:yyyyMMdd}", incrNumber);
|
$"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date:yyyyMMdd}", incrNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,12 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRoleService : IYiCrudAppService<RoleGetOutputDto, RoleGetListOutputDto, Guid, RoleGetListInputVo, RoleCreateInputVo, RoleUpdateInputVo>
|
public interface IRoleService : IYiCrudAppService<RoleGetOutputDto, RoleGetListOutputDto, Guid, RoleGetListInputVo, RoleCreateInputVo, RoleUpdateInputVo>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据角色名称移除指定用户的角色
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userIds"></param>
|
||||||
|
/// <param name="roleName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,12 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUserService : IYiCrudAppService<UserGetOutputDto, UserGetListOutputDto, Guid, UserGetListInputVo, UserCreateInputVo, UserUpdateInputVo>
|
public interface IUserService : IYiCrudAppService<UserGetOutputDto, UserGetListOutputDto, Guid, UserGetListInputVo, UserCreateInputVo, UserUpdateInputVo>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通过角色代码给用户添加角色
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户ID</param>
|
||||||
|
/// <param name="roleCodes">角色代码</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task AddUserRoleByRoleCodeAsync(Guid userId, List<string> roleCodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ using Mapster;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
using Volo.Abp.Uow;
|
|
||||||
using Yi.Framework.Ddd.Application;
|
using Yi.Framework.Ddd.Application;
|
||||||
using Yi.Framework.Rbac.Application.Contracts.Dtos.Role;
|
using Yi.Framework.Rbac.Application.Contracts.Dtos.Role;
|
||||||
using Yi.Framework.Rbac.Application.Contracts.Dtos.User;
|
using Yi.Framework.Rbac.Application.Contracts.Dtos.User;
|
||||||
@@ -98,7 +96,8 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
|||||||
{
|
{
|
||||||
var entity = await _repository.GetByIdAsync(id);
|
var entity = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
var isExist = await _repository._DbQueryable.Where(x => x.Id != entity.Id).AnyAsync(x => x.RoleCode == input.RoleCode || x.RoleName == input.RoleName);
|
var isExist = await _repository._DbQueryable.Where(x => x.Id != entity.Id)
|
||||||
|
.AnyAsync(x => x.RoleCode == input.RoleCode || x.RoleName == input.RoleName);
|
||||||
if (isExist)
|
if (isExist)
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException(RoleConst.Exist);
|
throw new UserFriendlyException(RoleConst.Exist);
|
||||||
@@ -213,7 +212,18 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
|||||||
await _userRoleRepository._Db.Deleteable<UserRoleEntity>().Where(x => x.RoleId == input.RoleId)
|
await _userRoleRepository._Db.Deleteable<UserRoleEntity>().Where(x => x.RoleId == input.RoleId)
|
||||||
.Where(x => input.UserIds.Contains(x.UserId))
|
.Where(x => input.UserIds.Contains(x.UserId))
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据角色名称移除指定用户的角色
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userIds"></param>
|
||||||
|
/// <param name="roleCode"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[RemoteService(isEnabled: false)]
|
||||||
|
public Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleCode)
|
||||||
|
{
|
||||||
|
return _roleManager.RemoveUserRoleByRoleCodeAsync(userIds, roleCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,5 +241,29 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
|||||||
{
|
{
|
||||||
return base.PostImportExcelAsync(input);
|
return base.PostImportExcelAsync(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过角色代码给用户添加角色
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户ID</param>
|
||||||
|
/// <param name="roleCodes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task AddUserRoleByRoleCodeAsync(Guid userId, List<string> roleCodes)
|
||||||
|
{
|
||||||
|
// 根据角色代码查找角色ID
|
||||||
|
var roleRepository = LazyServiceProvider.LazyGetRequiredService<ISqlSugarRepository<RoleAggregateRoot>>();
|
||||||
|
var roleIds = await roleRepository._DbQueryable
|
||||||
|
.Where(r => roleCodes.Contains(r.RoleCode) && r.State == true)
|
||||||
|
.Select(r=>r.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!roleIds.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用UserManager给用户设置角色
|
||||||
|
await _userManager.GiveUserSetRoleAsync(new List<Guid> { userId }, roleIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" />
|
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.2" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
||||||
<PackageReference Include="Volo.Abp.BackgroundJobs.Hangfire" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.BackgroundJobs.Hangfire" Version="$(AbpVersion)" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using Lazy.Captcha.Core.Generator;
|
using Lazy.Captcha.Core;
|
||||||
|
using Lazy.Captcha.Core.Generator;
|
||||||
|
using Lazy.Captcha.Core.Generator.Image.Option;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SkiaSharp;
|
||||||
using Yi.Framework.Ddd.Application;
|
using Yi.Framework.Ddd.Application;
|
||||||
using Yi.Framework.Rbac.Application.Contracts;
|
using Yi.Framework.Rbac.Application.Contracts;
|
||||||
using Yi.Framework.Rbac.Domain;
|
using Yi.Framework.Rbac.Domain;
|
||||||
@@ -21,7 +24,9 @@ namespace Yi.Framework.Rbac.Application
|
|||||||
|
|
||||||
service.AddCaptcha(options =>
|
service.AddCaptcha(options =>
|
||||||
{
|
{
|
||||||
options.CaptchaType = CaptchaType.ARITHMETIC;
|
options.CaptchaType = CaptchaType.NUMBER;
|
||||||
|
options.ImageOption.BackgroundColor = SkiaSharp.SKColors.Transparent;
|
||||||
|
options.ImageOption.FontFamily = DefaultFontFamilies.Prefix;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,15 @@ public class FileManager : DomainService, IFileManager
|
|||||||
/// <exception cref="ArgumentException"></exception>
|
/// <exception cref="ArgumentException"></exception>
|
||||||
public async Task<List<FileAggregateRoot>> CreateAsync(IEnumerable<IFormFile> files)
|
public async Task<List<FileAggregateRoot>> CreateAsync(IEnumerable<IFormFile> files)
|
||||||
{
|
{
|
||||||
if (files.Count() == 0)
|
var formFiles = files as IFormFile[] ?? files.ToArray();
|
||||||
|
if (!formFiles.Any())
|
||||||
{
|
{
|
||||||
throw new ArgumentException("文件上传为空!");
|
throw new ArgumentException("文件上传为空!");
|
||||||
}
|
}
|
||||||
|
|
||||||
//批量插入
|
//批量插入
|
||||||
List<FileAggregateRoot> entities = new();
|
List<FileAggregateRoot> entities = new();
|
||||||
foreach (var file in files)
|
foreach (var file in formFiles)
|
||||||
{
|
{
|
||||||
FileAggregateRoot data = new(_guidGenerator.Create(), file.FileName, (decimal)file.Length / 1024);
|
FileAggregateRoot data = new(_guidGenerator.Create(), file.FileName, (decimal)file.Length / 1024);
|
||||||
data.CheckDirectoryOrCreate();
|
data.CheckDirectoryOrCreate();
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
{
|
{
|
||||||
private ISqlSugarRepository<RoleAggregateRoot> _repository;
|
private ISqlSugarRepository<RoleAggregateRoot> _repository;
|
||||||
private ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository;
|
private ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository;
|
||||||
public RoleManager(ISqlSugarRepository<RoleAggregateRoot> repository, ISqlSugarRepository<RoleMenuEntity> roleMenuRepository)
|
private ISqlSugarRepository<UserRoleEntity> _userRoleRepository;
|
||||||
|
public RoleManager(ISqlSugarRepository<RoleAggregateRoot> repository, ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, ISqlSugarRepository<UserRoleEntity> userRoleRepository)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_roleMenuRepository = roleMenuRepository;
|
_roleMenuRepository = roleMenuRepository;
|
||||||
|
_userRoleRepository = userRoleRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -24,19 +26,45 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
{
|
{
|
||||||
//这个是需要事务的,在service中进行工作单元
|
//这个是需要事务的,在service中进行工作单元
|
||||||
await _roleMenuRepository.DeleteAsync(u => roleIds.Contains(u.RoleId));
|
await _roleMenuRepository.DeleteAsync(u => roleIds.Contains(u.RoleId));
|
||||||
|
//添加新的关系
|
||||||
|
List<RoleMenuEntity> roleMenuEntity = new();
|
||||||
//遍历用户
|
//遍历用户
|
||||||
foreach (var roleId in roleIds)
|
foreach (var roleId in roleIds)
|
||||||
{
|
{
|
||||||
//添加新的关系
|
|
||||||
List<RoleMenuEntity> roleMenuEntity = new();
|
|
||||||
foreach (var menu in menuIds)
|
foreach (var menu in menuIds)
|
||||||
{
|
{
|
||||||
roleMenuEntity.Add(new RoleMenuEntity() { RoleId = roleId, MenuId = menu });
|
roleMenuEntity.Add(new RoleMenuEntity() { RoleId = roleId, MenuId = menu });
|
||||||
}
|
}
|
||||||
//一次性批量添加
|
|
||||||
await _roleMenuRepository.InsertRangeAsync(roleMenuEntity);
|
}
|
||||||
|
//一次性批量添加
|
||||||
|
await _roleMenuRepository.InsertRangeAsync(roleMenuEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据角色名称移除指定用户的角色
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userIds">用户ID列表</param>
|
||||||
|
/// <param name="roleName">角色名称</param>
|
||||||
|
/// <returns>移除的角色关系数量</returns>
|
||||||
|
public async Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleName)
|
||||||
|
{
|
||||||
|
// 获取角色ID
|
||||||
|
var role = await _repository._DbQueryable
|
||||||
|
.Where(x => x.RoleCode == roleName)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (role == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除用户角色关系
|
||||||
|
var removedCount = await _userRoleRepository._Db.Deleteable<UserRoleEntity>()
|
||||||
|
.Where(x => userIds.Contains(x.UserId) && x.RoleId == role.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
return removedCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,27 +46,25 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
/// <param name="userIds"></param>
|
/// <param name="userIds"></param>
|
||||||
/// <param name="roleIds"></param>
|
/// <param name="roleIds"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task GiveUserSetRoleAsync(List<Guid> userIds, List<Guid> roleIds)
|
public async Task GiveUserSetRoleAsync(List<Guid> userIds, List<Guid>? roleIds)
|
||||||
{
|
{
|
||||||
//删除用户之前所有的用户角色关系(物理删除,没有恢复的必要)
|
//删除用户之前所有的用户角色关系
|
||||||
await _repositoryUserRole.DeleteAsync(u => userIds.Contains(u.UserId));
|
await _repositoryUserRole.DeleteAsync(u => userIds.Contains(u.UserId));
|
||||||
|
|
||||||
if (roleIds is not null)
|
if (roleIds is not null)
|
||||||
{
|
{
|
||||||
|
//添加新的关系
|
||||||
|
List<UserRoleEntity> userRoleEntities = new();
|
||||||
//遍历用户
|
//遍历用户
|
||||||
foreach (var userId in userIds)
|
foreach (var userId in userIds)
|
||||||
{
|
{
|
||||||
//添加新的关系
|
|
||||||
List<UserRoleEntity> userRoleEntities = new();
|
|
||||||
|
|
||||||
foreach (var roleId in roleIds)
|
foreach (var roleId in roleIds)
|
||||||
{
|
{
|
||||||
userRoleEntities.Add(new UserRoleEntity() { UserId = userId, RoleId = roleId });
|
userRoleEntities.Add(new UserRoleEntity() { UserId = userId, RoleId = roleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
//一次性批量添加
|
|
||||||
await _repositoryUserRole.InsertRangeAsync(userRoleEntities);
|
|
||||||
}
|
}
|
||||||
|
//一次性批量添加
|
||||||
|
await _repositoryUserRole.InsertRangeAsync(userRoleEntities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,25 +75,24 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
/// <param name="userIds"></param>
|
/// <param name="userIds"></param>
|
||||||
/// <param name="postIds"></param>
|
/// <param name="postIds"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task GiveUserSetPostAsync(List<Guid> userIds, List<Guid> postIds)
|
public async Task GiveUserSetPostAsync(List<Guid> userIds, List<Guid>? postIds)
|
||||||
{
|
{
|
||||||
//删除用户之前所有的用户角色关系(物理删除,没有恢复的必要)
|
//删除用户之前所有的用户角色关系(物理删除,没有恢复的必要)
|
||||||
await _repositoryUserPost.DeleteAsync(u => userIds.Contains(u.UserId));
|
await _repositoryUserPost.DeleteAsync(u => userIds.Contains(u.UserId));
|
||||||
if (postIds is not null)
|
if (postIds is not null)
|
||||||
{
|
{
|
||||||
|
//添加新的关系
|
||||||
|
List<UserPostEntity> userPostEntities = new();
|
||||||
//遍历用户
|
//遍历用户
|
||||||
foreach (var userId in userIds)
|
foreach (var userId in userIds)
|
||||||
{
|
{
|
||||||
//添加新的关系
|
|
||||||
List<UserPostEntity> userPostEntities = new();
|
|
||||||
foreach (var post in postIds)
|
foreach (var post in postIds)
|
||||||
{
|
{
|
||||||
userPostEntities.Add(new UserPostEntity() { UserId = userId, PostId = post });
|
userPostEntities.Add(new UserPostEntity() { UserId = userId, PostId = post });
|
||||||
}
|
}
|
||||||
|
|
||||||
//一次性批量添加
|
|
||||||
await _repositoryUserPost.InsertRangeAsync(userPostEntities);
|
|
||||||
}
|
}
|
||||||
|
//一次性批量添加
|
||||||
|
await _repositoryUserPost.InsertRangeAsync(userPostEntities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +134,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
public async Task SetDefautRoleAsync(Guid userId)
|
public async Task SetDefautRoleAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var role = await _roleRepository.GetFirstAsync(x => x.RoleCode == UserConst.DefaultRoleCode);
|
var role = await _roleRepository.GetFirstAsync(x => x.RoleCode == UserConst.DefaultRoleCode);
|
||||||
if (role is not null)
|
await GiveUserSetRoleAsync(new List<Guid> { userId }, new List<Guid> { role.Id });
|
||||||
{
|
|
||||||
await GiveUserSetRoleAsync(new List<Guid> { userId }, new List<Guid> { role.Id });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateUserName(UserAggregateRoot input)
|
private void ValidateUserName(UserAggregateRoot input)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@echo on
|
@echo on
|
||||||
|
|
||||||
set SERVER_USER=ccnetcore
|
set SERVER_USER=root
|
||||||
set SERVER_IP=yxai.chat
|
set SERVER_IP=yxai.chat
|
||||||
set FILE_PATH=publish_02.zip
|
set FILE_PATH=publish_02.zip
|
||||||
set REMOTE_PATH=/home/yi/build/publish_02.zip
|
set REMOTE_PATH=/home/yi/build/publish_02.zip
|
||||||
|
|||||||
32
Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-hub/VipExpireJob.cs
Normal file
32
Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-hub/VipExpireJob.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.BackgroundWorkers.Hangfire;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||||
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.Rbac.Domain.Entities;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
namespace Yi.Abp.Web.Jobs.ai_hub;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VIP过期自动卸载任务
|
||||||
|
/// </summary>
|
||||||
|
public class VipExpireJob : HangfireBackgroundWorkerBase
|
||||||
|
{
|
||||||
|
private readonly IRechargeService _rechargeService;
|
||||||
|
|
||||||
|
public VipExpireJob(IRechargeService rechargeService)
|
||||||
|
{
|
||||||
|
_rechargeService = rechargeService;
|
||||||
|
RecurringJobId = "VIP过期自动卸载";
|
||||||
|
// 每天凌晨0点执行一次
|
||||||
|
CronExpression = "0 0 0 * * ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
|
{
|
||||||
|
await _rechargeService.RemoveVipRoleByExpireAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ namespace Yi.Abp.Web.Jobs.ai_stock
|
|||||||
|
|
||||||
if (probability < 2)
|
if (probability < 2)
|
||||||
{
|
{
|
||||||
await _newsManager.GenerateNewsAsync();
|
// await _newsManager.GenerateNewsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user