Compare commits

..

28 Commits

Author SHA1 Message Date
橙子
a50c45f7a3 !102 修正删除部门无效问题
Merge pull request !102 from Po/cherry-pick-1765609320
2025-12-14 04:37:59 +00:00
Po
2bc8837155 修正删除部门无效问题 2025-12-13 07:02:19 +00:00
ccnetcore
8a6e5abf48 fix: 修复token鉴权 2025-12-11 23:32:57 +08:00
ccnetcore
8b191330b8 Revert "fix: 仅从 Query 获取 access_token/refresh_token,简化 OnMessageReceived 逻辑"
This reverts commit 0d2f2cb826.
2025-12-11 23:31:29 +08:00
chenchun
0d2f2cb826 fix: 仅从 Query 获取 access_token/refresh_token,简化 OnMessageReceived 逻辑
- 修改文件:Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
- 将 JwtBearerEvents.OnMessageReceived 的上下文参数名改为 messageContext,统一变量名。
- 简化 Token 获取逻辑:只从 request.Query 中读取 access_token 与 refresh_token,移除从 Cookies(Token)和请求头(refresh_token)读取的分支。
2025-12-11 17:41:38 +08:00
chenchun
571df74c43 chore: 在 common.props 添加 SatelliteResourceLanguages=en;zh-CN
在 Yi.Abp.Net8/common.props 中新增 SatelliteResourceLanguages 属性,指定生成卫星资源语言为 en 和 zh-CN,以便打包对应的本地化资源。
2025-12-10 15:53:18 +08:00
chenchun
cefde6848d perf: 去除35MB又臭又大的腾讯云sdk 2025-12-10 15:10:54 +08:00
ccnetcore
551597765c perf: 优化sqlsguar分页查询 2025-12-07 18:50:02 +08:00
chenchun
5eaffe2ec2 feat: 新增更新并发乐观锁配置与支持
- 在 DbConnOptions 新增 EnabledConcurrencyException(bool,默认 false) 配置项。
- 在 SqlSugarRepository 引入 IAbpLazyServiceProvider,通过 IOptions<DbConnOptions> 延迟获取配置。
- UpdateAsync 改为仅当 EnabledConcurrencyException 为 true 且实体实现 IHasConcurrencyStamp 时,使用 ExecuteCommandWithOptLockAsync 并捕获 VersionExceptions 抛出 AbpDbConcurrencyException;否则回退到原有的 UpdateAsync 实现。
- 清理/调整部分 using 引用,新增 Microsoft.Extensions.Options 与 Volo.Abp.DependencyInjection 引用。

注意:默认值为 false,需在配置中显式开启 EnabledConcurrencyException 才会启用乐观并发校验,开启后会改变之前对带版本实体自动使用乐观锁的行为。
2025-11-17 11:19:15 +08:00
chenchun
2ec7b5f4fd fix: 修复软删除时空引用异常
在 SqlSugarRepository.cs 中对 ISoftDelete 分支增加 GetByIdAsync 返回 null 的判断,避免在实体为 null 时继续反射赋值导致 NullReferenceException。若实体不存在,直接返回 false。
2025-11-13 16:49:44 +08:00
chenchun
4521212a90 feat: 新增文件缓存功能
- 在 Yi.Framework.Rbac.Application.Services.FileService 中注入 IMemoryCache,用于缓存文件元数据,减少对仓储的重复读取。
  - 在 Get 方法中通过 key "File:{code}" 缓存 FileCacheItem,设置绝对过期时间为 1 天。
  - 缓存项使用 Mapster 适配为 FileCacheItem,再适配回 FileAggregateRoot(保留现有逻辑判断和路径获取)。
- 新增缓存模型 Yi.Framework.Rbac.Domain.Shared.Caches.FileCacheItem(包含 Id、FileSize、FileName、FilePath、创建/修改信息等)。
- 增加并调整相关 using 引用(Microsoft.Extensions.Caching.Memory、Volo.Abp.Caching、Domain.Shared.Caches)。
- 同时修复了保存多文件时的缩进/空格格式(不影响功能)。
2025-11-06 11:29:21 +08:00
chenchun
94834f45c3 perf: 使用 FileStreamResult 流式返回文件,避免一次性读取到内存
改为 FileStream 并返回 FileStreamResult,减小内存占用并支持大型文件;修正变量名拼写并添加 null-forgiving 标记。
2025-11-06 11:13:50 +08:00
chenchun
22ac150acd fix: 修正 FileAggregateRoot.FilePath 的赋值,保存目录路径而非包含文件名的完整路径 2025-11-06 10:58:33 +08:00
ccnetcore
1cc5f2a14f refactor: 注释掉 Furion 统一结果 API 注册,保留 ABP 默认处理方式 2025-09-27 17:26:13 +08:00
橙子
d9997eeb28 !100 update Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs.
Merge pull request !100 from Gary/N/A
2025-09-22 07:14:02 +00:00
Gary
06e77aa8fd update Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs.
判断是否为模型验证错误,如果是,将errors传回,并打印日志

Signed-off-by: Gary <1511313969@qq.com>
2025-09-22 07:13:46 +00:00
橙子
e9e2228f6e !97 岗位状态修改
Merge pull request !97 from 嗳摸嫫/jun
2025-09-22 07:13:45 +00:00
橙子
d516a381d0 !98 update Yi.Abp.Net8/framework/Yi.Framework.Mapster/YiFrameworkMapsterModule.cs.
Merge pull request !98 from Gary/N/A
2025-09-22 07:13:26 +00:00
Gary
4e792ba976 update Yi.Abp.Net8/framework/Yi.Framework.Mapster/YiFrameworkMapsterModule.cs.
自动扫描所有继承IRegister 的Mapster 配置

Signed-off-by: Gary <1511313969@qq.com>
2025-09-22 02:58:59 +00:00
chenchun
f90d3871fa feat: 启用 Furion 统一返回结果并优化过滤器配置
- 在 `YiAbpWebModule` 中启用 `AddFurionUnifyResultApi` 以支持 Furion 风格的统一 API 返回格式
- 调整 `UnifyResultExtensions`,移除 `AbpExceptionFilter` 和 `AbpNoContentActionFilter`,确保统一结果过滤器优先执行
2025-09-16 11:48:36 +08:00
HW-July
6005b9329d 岗位状态修改 2025-08-25 17:12:18 +08:00
橙子
9d4b3e7d0c update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-05 07:50:10 +00:00
chenchun
72795382a1 style: 调整样式 2025-07-02 15:03:16 +08:00
橙子
35cdff2afa update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:04:18 +00:00
橙子
dcf547f513 style: 新增赞助
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:03:28 +00:00
橙子
8660d45f36 update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:01:51 +00:00
橙子
63dd55e7a4 !95 修正分页导致部门结构显示异常。取消后台分页功能,同菜单结构无需分页。
Merge pull request !95 from Po/N/A
2025-07-02 03:49:56 +00:00
Po
40cd89f90c 修正分页导致部门结构显示异常。取消后台分页功能,同菜单结构无需分页。
Signed-off-by: Po <448443959@qq.com>
2025-07-02 02:50:06 +00:00
1427 changed files with 965 additions and 32806 deletions

View File

@@ -7,6 +7,12 @@
[![fork](https://gitee.com/ccnetcore/yi/badge/fork.svg?theme=dark)](https://gitee.com/ccnetcore/Yi)
[![license](https://img.shields.io/badge/license-MIT-yellow)](https://gitee.com/ccnetcore/Yi)
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
<img src="readme/edgeone.png"/>
[English](README-en.md) | 简体中文
****
## 🍍 简介:
@@ -60,9 +66,9 @@ bbs前端`docker run -d --name yi.bbs -p 18001:18001 -v /home/Yi/Yi.Bbs.Vue3/
Yi社区官网网址Bbs社区正式[ccnetcore.com](https://ccnetcore.com) (已上线,欢迎加入)
Rbac后台演示地址https://ccnetcore.com:1000 用户cc、密码123456
Rbac后台演示地址https://data.ccnetcore.com:1000 用户cc、密码123456
Pure后台演示地址https://ccnetcore.com:1001 用户cc、密码123456
Pure后台演示地址https://data.ccnetcore.com:1001 用户cc、密码123456
## 🍏 支持:

View File

@@ -186,18 +186,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.Stock.Domain.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.Stock.SqlSugarCore", "module\ai-stock\Yi.Framework.Stock.SqlSugarCore\Yi.Framework.Stock.SqlSugarCore.csproj", "{5F49318F-E6C7-4194-BAE0-83D4FB8D1983}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ai-hub", "ai-hub", "{7AD5DBAE-44F9-474B-8F7B-837EDE908934}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.AiHub.Application", "module\ai-hub\Yi.Framework.AiHub.Application\Yi.Framework.AiHub.Application.csproj", "{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.AiHub.Application.Contracts", "module\ai-hub\Yi.Framework.AiHub.Application.Contracts\Yi.Framework.AiHub.Application.Contracts.csproj", "{123D1C81-D667-4060-8E85-FFE7FB4584AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.AiHub.Domain", "module\ai-hub\Yi.Framework.AiHub.Domain\Yi.Framework.AiHub.Domain.csproj", "{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.AiHub.Domain.Shared", "module\ai-hub\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj", "{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.AiHub.SqlSugarCore", "module\ai-hub\Yi.Framework.AiHub.SqlSugarCore\Yi.Framework.AiHub.SqlSugarCore.csproj", "{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -480,26 +468,6 @@ Global
{5F49318F-E6C7-4194-BAE0-83D4FB8D1983}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F49318F-E6C7-4194-BAE0-83D4FB8D1983}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F49318F-E6C7-4194-BAE0-83D4FB8D1983}.Release|Any CPU.Build.0 = Release|Any CPU
{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895}.Release|Any CPU.Build.0 = Release|Any CPU
{123D1C81-D667-4060-8E85-FFE7FB4584AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{123D1C81-D667-4060-8E85-FFE7FB4584AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{123D1C81-D667-4060-8E85-FFE7FB4584AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{123D1C81-D667-4060-8E85-FFE7FB4584AD}.Release|Any CPU.Build.0 = Release|Any CPU
{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D}.Release|Any CPU.Build.0 = Release|Any CPU
{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48}.Release|Any CPU.Build.0 = Release|Any CPU
{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -583,12 +551,6 @@ Global
{162821E4-8FE0-4A68-B3C0-49BD6596446F} = {DB46873F-981A-43D8-91B0-D464CCB65943}
{10273544-715D-4BB3-893C-6F010D947BDD} = {DB46873F-981A-43D8-91B0-D464CCB65943}
{5F49318F-E6C7-4194-BAE0-83D4FB8D1983} = {DB46873F-981A-43D8-91B0-D464CCB65943}
{7AD5DBAE-44F9-474B-8F7B-837EDE908934} = {2317227D-7796-4E7B-BEDB-7CD1CAE7B853}
{1AD10DD2-535E-4EAB-A8A4-EC3FCA206895} = {7AD5DBAE-44F9-474B-8F7B-837EDE908934}
{123D1C81-D667-4060-8E85-FFE7FB4584AD} = {7AD5DBAE-44F9-474B-8F7B-837EDE908934}
{8EB4C8BB-6B21-4811-9FAB-B98FA5CA754D} = {7AD5DBAE-44F9-474B-8F7B-837EDE908934}
{5FC6CA90-D5B4-433E-9B2C-94330FFB4C48} = {7AD5DBAE-44F9-474B-8F7B-837EDE908934}
{8698C812-4DDC-4E80-BCD6-24C5D56AEDB1} = {7AD5DBAE-44F9-474B-8F7B-837EDE908934}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23D6FBC9-C970-4641-BC1E-2AEA59F51C18}

View File

@@ -12,6 +12,7 @@
</PropertyGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en;zh-CN</SatelliteResourceLanguages>
<LangVersion>latest</LangVersion>
<Version>1.0.0</Version>
<NoWarn>$(NoWarn);CS1591;CS8618;CS1998;CS8604;CS8620;CS8600;CS8602</NoWarn>

View File

@@ -1,64 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Yi.Framework.Core.Authentication;
namespace Yi.Framework.AspNetCore.Microsoft.AspNetCore.Authentication;
/// <summary>
/// 可刷新的鉴权提供者
/// </summary>
public class RefreshAuthenticationHandlerProvider : IRefreshAuthenticationHandlerProvider
{
private Dictionary<string, IAuthenticationHandler> _handlerMap =
new Dictionary<string, IAuthenticationHandler>((IEqualityComparer<string>)StringComparer.Ordinal);
/// <summary>Constructor.</summary>
/// <param name="schemes">The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" />.</param>
public RefreshAuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
this.Schemes = schemes;
}
/// <summary>
/// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" />.
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; }
/// <summary>Returns the handler instance that will be used.</summary>
/// <param name="context">The context.</param>
/// <param name="authenticationScheme">The name of the authentication scheme being handled.</param>
/// <returns>The handler instance.</returns>
public async Task<IAuthenticationHandler?> GetHandlerAsync(
HttpContext context,
string authenticationScheme)
{
IAuthenticationHandler handlerAsync;
if (this._handlerMap.TryGetValue(authenticationScheme, out handlerAsync))
return handlerAsync;
AuthenticationScheme schemeAsync = await this.Schemes.GetSchemeAsync(authenticationScheme);
if (schemeAsync == null)
return (IAuthenticationHandler)null;
if ((context.RequestServices.GetService(schemeAsync.HandlerType) ??
ActivatorUtilities.CreateInstance(context.RequestServices, schemeAsync.HandlerType)) is
IAuthenticationHandler handler)
{
handlerAsync = handler;
await handler.InitializeAsync(schemeAsync, context);
this._handlerMap[authenticationScheme] = handler;
}
return handlerAsync;
}
/// <summary>
/// 刷新鉴权
/// </summary>
public void RefreshAuthentication()
{
_handlerMap = new Dictionary<string, IAuthenticationHandler>((IEqualityComparer<string>)StringComparer.Ordinal);
}
}

View File

@@ -1,12 +1,17 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.Xml.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Options;
namespace Yi.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection
{

View File

@@ -1,4 +1,4 @@
// MIT 许可证
// MIT 许可证
//
// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者
//
@@ -17,25 +17,25 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Validation;
using Yi.Framework.Core.Extensions;
namespace Yi.Framework.AspNetCore.UnifyResult.Fiters;
/// <summary>
/// 友好异常拦截器
/// 友好异常拦截器
/// </summary>
public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
{
/// <summary>
/// 异常拦截
/// 异常拦截
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnExceptionAsync(ExceptionContext context)
{
// 排除 WebSocket 请求处理
if (context.HttpContext.IsWebSocketRequest()) return;
@@ -44,20 +44,23 @@ public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
// 解析异常信息
var exceptionMetadata = GetExceptionMetadata(context);
IUnifyResultProvider unifyResult = context.GetRequiredService<IUnifyResultProvider>();
var unifyResult = context.GetRequiredService<IUnifyResultProvider>();
// 执行规范化异常处理
context.Result = unifyResult.OnException(context, exceptionMetadata);
// 创建日志记录器
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<FriendlyExceptionFilter>>();
var errorMsg = "";
if (exceptionMetadata.Errors != null) errorMsg = "\n" + JsonConvert.SerializeObject(exceptionMetadata.Errors);
// 记录拦截日常
logger.LogError(context.Exception, context.Exception.Message);
logger.LogError(context.Exception, context.Exception.Message + errorMsg);
}
/// <summary>
/// 获取异常元数据
/// 获取异常元数据
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
@@ -74,22 +77,25 @@ public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
// 判断是否是 ExceptionContext 或者 ActionExecutedContext
var exception = context is ExceptionContext exContext
? exContext.Exception
: (
context is ActionExecutedContext edContext
? edContext.Exception
: default
);
: context is ActionExecutedContext edContext
? edContext.Exception
: default;
if (exception is AbpValidationException validationException)
{
errors = validationException.ValidationErrors;
isValidationException = true;
}
// 判断是否是友好异常
if (exception is UserFriendlyException friendlyException)
{
int statusCode2 = 500;
var statusCode2 = 500;
int.TryParse(friendlyException.Code, out statusCode2);
isFriendlyException = true;
errorCode = friendlyException.Code;
originErrorCode = friendlyException.Code;
statusCode = statusCode2==0?403:statusCode2;
isValidationException = false;
statusCode = statusCode2 == 0 ? 403 : statusCode2;
errors = friendlyException.Message;
data = friendlyException.Data;
}

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Response;
using Yi.Framework.AspNetCore.UnifyResult.Fiters;
namespace Yi.Framework.AspNetCore.UnifyResult;
@@ -20,9 +21,10 @@ public static class UnifyResultExtensions
services.AddTransient<FriendlyExceptionFilter>();
services.AddMvc(options =>
{
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpExceptionFilter));
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpNoContentActionFilter));
options.Filters.AddService<SucceededUnifyResultFilter>(99);
options.Filters.AddService<FriendlyExceptionFilter>(100);
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpExceptionFilter));
});
return services;
}

View File

@@ -1,10 +1,21 @@
using Microsoft.AspNetCore.Authentication;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Linq;
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.WebClientInfo;
using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Authentication;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Modularity;
using Yi.Framework.AspNetCore.Mvc;
using Yi.Framework.Core;
using Yi.Framework.Core.Authentication;
namespace Yi.Framework.AspNetCore
{
@@ -24,14 +35,8 @@ namespace Yi.Framework.AspNetCore
// 替换默认的WebClientInfoProvider为支持代理的实现
services.Replace(new ServiceDescriptor(
typeof(IWebClientInfoProvider),
typeof(RealIpHttpContextWebClientInfoProvider),
typeof(RealIpHttpContextWebClientInfoProvider),
ServiceLifetime.Transient));
// 替换默认的AuthenticationHandlerProvider为支持刷新鉴权
services.Replace(new ServiceDescriptor(
typeof(IAuthenticationHandlerProvider),
typeof(RefreshAuthenticationHandlerProvider),
ServiceLifetime.Scoped));
}
}
}

View File

@@ -2,7 +2,6 @@
using Hangfire;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundJobs.Hangfire;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.BackgroundWorkers.Hangfire;
@@ -33,19 +32,13 @@ public sealed class YiFrameworkBackgroundWorkersHangfireModule : AbpModule
/// <param name="context">应用程序初始化上下文</param>
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
if (!context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundWorkerOptions>>().Value.IsEnabled)
{
return;
}
// 获取后台任务管理器和所有 Hangfire 后台任务
var backgroundWorkerManager = context.ServiceProvider.GetRequiredService<IBackgroundWorkerManager>();
var workers = context.ServiceProvider.GetServices<IHangfireBackgroundWorker>();
// 获取配置
var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
// 检查是否启用 Redis
var isRedisEnabled = configuration.GetValue<bool>("Redis:IsEnabled");
@@ -63,11 +56,11 @@ public sealed class YiFrameworkBackgroundWorkersHangfireModule : AbpModule
{
// 内存模式:直接使用 Hangfire
var unProxyWorker = ProxyHelper.UnProxy(worker);
// 添加或更新循环任务
RecurringJob.AddOrUpdate(
worker.RecurringJobId,
(Expression<Func<Task>>)(() =>
(Expression<Func<Task>>)(() =>
((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(default)),
worker.CronExpression,
new RecurringJobOptions

View File

@@ -1,18 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Yi.Framework.Core.Authentication;
public static class AuthenticationExtensions
{
public static void RefreshAuthentication(this HttpContext context)
{
var currentAuthenticationHandler =
context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
if (currentAuthenticationHandler is IRefreshAuthenticationHandlerProvider refreshAuthenticationHandler)
{
refreshAuthenticationHandler.RefreshAuthentication();
}
}
}

View File

@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authentication;
namespace Yi.Framework.Core.Authentication;
public interface IRefreshAuthenticationHandlerProvider: IAuthenticationHandlerProvider
{
/// <summary>
/// 刷新鉴权
/// </summary>
void RefreshAuthentication();
}

View File

@@ -1,7 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectMapping;
using Yi.Framework.Core;
using Mapster;
namespace Yi.Framework.Mapster
{
@@ -22,7 +23,8 @@ namespace Yi.Framework.Mapster
public override void ConfigureServices(ServiceConfigurationContext context)
{
var services = context.Services;
// 扫描并注册所有映射配置
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
// 注册Mapster相关服务
services.AddTransient<IAutoObjectMappingProvider, MapsterAutoObjectMappingProvider>();
services.AddTransient<IObjectMapper, MapsterObjectMapper>();

View File

@@ -1,11 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<ItemGroup>
<ProjectReference Include="..\Yi.Framework.Core\Yi.Framework.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" Version="1.57.0" />
</ItemGroup>
</Project>

View File

@@ -1,15 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Yi.Framework.Core.Options;
namespace Yi.Framework.SemanticKernel;
public class YiFrameworkSemanticKernelModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var services = context.Services;
}
}

View File

@@ -60,8 +60,8 @@ namespace Yi.Framework.SqlSugarCore.Abstractions
public bool EnabledSaasMultiTenancy { get; set; } = false;
/// <summary>
/// 并发乐观锁异常,否则不处理
/// 是否开启更新并发乐观锁
/// </summary>
public bool EnabledConcurrencyException { get; set; } = true;
public bool EnabledConcurrencyException { get;set; } = false;
}
}

View File

@@ -1,12 +1,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Extensions.Logging;
using System.Linq.Expressions;
using Microsoft.Extensions.Options;
using Nito.AsyncEx;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
@@ -17,18 +12,19 @@ using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.SqlSugarCore.Repositories
{
public class SqlSugarRepository<TEntity> : ISqlSugarRepository<TEntity>, IRepository<TEntity>
where TEntity : class, IEntity, new()
public class SqlSugarRepository<TEntity> : ISqlSugarRepository<TEntity>, IRepository<TEntity> where TEntity : class, IEntity, new()
{
[Obsolete("使用GetDbContextAsync()")]
public ISqlSugarClient _Db => AsyncContext.Run(async () => await GetDbContextAsync());
[Obsolete("使用AsQueryable()")]
public ISugarQueryable<TEntity> _DbQueryable => _Db.Queryable<TEntity>();
private readonly ISugarDbContextProvider<ISqlSugarDbContext> _dbContextProvider;
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
protected DbConnOptions? Options => LazyServiceProvider?.LazyGetService<IOptions<DbConnOptions>>().Value;
/// <summary>
/// 异步查询执行器
/// </summary>
@@ -64,26 +60,22 @@ namespace Yi.Framework.SqlSugarCore.Repositories
#region Abp模块
public virtual async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true, CancellationToken cancellationToken = default)
public virtual async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await GetFirstAsync(predicate);
}
public virtual async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true, CancellationToken cancellationToken = default)
public virtual async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await GetFirstAsync(predicate);
}
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
await this.DeleteAsync(predicate);
}
public virtual async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate,
CancellationToken cancellationToken = default)
public virtual async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{
await this.DeleteAsync(predicate);
}
@@ -113,71 +105,60 @@ namespace Yi.Framework.SqlSugarCore.Repositories
throw new NotImplementedException();
}
public virtual async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate,
bool includeDetails = false, CancellationToken cancellationToken = default)
public virtual async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await GetListAsync(predicate);
}
public virtual async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
return await InsertReturnEntityAsync(entity);
}
public virtual async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
await InsertRangeAsync(entities.ToList());
}
public virtual async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
await UpdateAsync(entity);
return entity;
}
public virtual async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
await UpdateRangeAsync(entities.ToList());
}
public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
await DeleteAsync(entity);
}
public virtual async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
await DeleteAsync(entities.ToList());
}
public virtual async Task<List<TEntity>> GetListAsync(bool includeDetails = false,
CancellationToken cancellationToken = default)
public virtual async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await GetListAsync();
}
public virtual async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await this.CountAsync(_ => true);
return await this.CountAsync(_=>true);
}
public virtual async Task<List<TEntity>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting,
bool includeDetails = false, CancellationToken cancellationToken = default)
public virtual async Task<List<TEntity>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await GetPageListAsync(_ => true, skipCount, maxResultCount);
}
#endregion
#region DB快捷操作
public virtual async Task<IDeleteable<TEntity>> AsDeleteable()
{
return (await GetDbSimpleClientAsync()).AsDeleteable();
@@ -192,7 +173,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return (await GetDbSimpleClientAsync()).AsInsertable(insertObj);
}
public virtual async Task<IInsertable<TEntity>> AsInsertable(TEntity[] insertObjs)
{
return (await GetDbSimpleClientAsync()).AsInsertable(insertObjs);
@@ -232,11 +213,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return (await GetDbSimpleClientAsync()).AsUpdateable(updateObjs);
}
#endregion
#region SimpleClient模块
public virtual async Task<int> CountAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await (await GetDbSimpleClientAsync()).CountAsync(whereExpression);
@@ -253,6 +232,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObj);
}
}
public virtual async Task<bool> DeleteAsync(List<TEntity> deleteObjs)
@@ -272,13 +252,13 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
return await (await GetDbSimpleClientAsync()).AsUpdateable()
.SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0;
return await (await GetDbSimpleClientAsync()).AsUpdateable().SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0;
}
else
{
return await (await GetDbSimpleClientAsync()).DeleteAsync(whereExpression);
}
}
public virtual async Task<bool> DeleteByIdAsync(dynamic id)
@@ -286,11 +266,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
var entity = await GetByIdAsync(id);
if (entity is null)
{
return false;
}
if (entity == null) return false;
//反射赋值
ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, entity);
return await UpdateAsync(entity);
@@ -311,7 +287,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return false;
}
//反射赋值
entities.ForEach(e => ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e));
return await UpdateRangeAsync(entities);
@@ -320,6 +295,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(ids);
}
}
public virtual async Task<TEntity> GetByIdAsync(dynamic id)
@@ -328,6 +304,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
}
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await (await GetDbSimpleClientAsync()).GetFirstAsync(whereExpression);
@@ -343,19 +320,14 @@ namespace Yi.Framework.SqlSugarCore.Repositories
return await (await GetDbSimpleClientAsync()).GetListAsync(whereExpression);
}
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression,
int pageNum, int pageSize)
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression, int pageNum, int pageSize)
{
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
new PageModel() { PageIndex = pageNum, PageSize = pageSize });
return await (await AsQueryable()).Where(whereExpression).ToPageListAsync(pageNum, pageSize);
}
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression,
int pageNum, int pageSize, Expression<Func<TEntity, object>>? orderByExpression = null,
OrderByType orderByType = OrderByType.Asc)
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression, int pageNum, int pageSize, Expression<Func<TEntity, object>>? orderByExpression = null, OrderByType orderByType = OrderByType.Asc)
{
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType);
return await (await AsQueryable()).Where(whereExpression) .OrderBy( orderByExpression,orderByType).ToPageListAsync(pageNum, pageSize);
}
public virtual async Task<TEntity> GetSingleAsync(Expression<Func<TEntity, bool>> whereExpression)
@@ -410,9 +382,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories
public virtual async Task<bool> UpdateAsync(TEntity updateObj)
{
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>()) //带版本号乐观锁更新
if (Options is not null && Options.EnabledConcurrencyException)
{
if (Options is not null && Options.EnabledConcurrencyException)
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>()) //带版本号乐观锁更新
{
try
{
@@ -426,24 +398,18 @@ namespace Yi.Framework.SqlSugarCore.Repositories
$"{ex.Message}[更新失败ConcurrencyStamp不是最新版本],entityInfo{updateObj}", ex);
}
}
else
{
int num = await (await GetDbSimpleClientAsync())
.Context.Updateable(updateObj).ExecuteCommandAsync();
return num > 0;
}
}
return await (await GetDbSimpleClientAsync()).UpdateAsync(updateObj);
}
public virtual async Task<bool> UpdateAsync(Expression<Func<TEntity, TEntity>> columns,
Expression<Func<TEntity, bool>> whereExpression)
public virtual async Task<bool> UpdateAsync(Expression<Func<TEntity, TEntity>> columns, Expression<Func<TEntity, bool>> whereExpression)
{
return await (await GetDbSimpleClientAsync()).UpdateAsync(columns, whereExpression);
}
public virtual async Task<bool> UpdateRangeAsync(List<TEntity> updateObjs)
{
return await (await GetDbSimpleClientAsync()).UpdateRangeAsync(updateObjs);
@@ -452,36 +418,30 @@ namespace Yi.Framework.SqlSugarCore.Repositories
#endregion
}
public class SqlSugarRepository<TEntity, TKey> : SqlSugarRepository<TEntity>, ISqlSugarRepository<TEntity, TKey>,
IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>, new()
public class SqlSugarRepository<TEntity, TKey> : SqlSugarRepository<TEntity>, ISqlSugarRepository<TEntity, TKey>, IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>, new()
{
public SqlSugarRepository(ISugarDbContextProvider<ISqlSugarDbContext> dbContextProvider) : base(
dbContextProvider)
public SqlSugarRepository(ISugarDbContextProvider<ISqlSugarDbContext> sugarDbContextProvider) : base(sugarDbContextProvider)
{
}
public virtual async Task DeleteAsync(TKey id, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
{
await DeleteByIdAsync(id);
}
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false,
CancellationToken cancellationToken = default)
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
await DeleteByIdsAsync(ids.Select(x => (object)x).ToArray());
}
public virtual async Task<TEntity?> FindAsync(TKey id, bool includeDetails = true,
CancellationToken cancellationToken = default)
public virtual async Task<TEntity?> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await GetByIdAsync(id);
}
public virtual async Task<TEntity> GetAsync(TKey id, bool includeDetails = true,
CancellationToken cancellationToken = default)
public virtual async Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await GetByIdAsync(id);
}
}
}
}

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
using SqlSugar;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class MessageInputDto
{
public string? Content { get; set; }
public string Role { get; set; }
public string ModelId { get; set; }
public string? Remark { get; set; }
public ThorUsageResponse? TokenUsage { get; set; }
}

View File

@@ -1,65 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class ModelGetListOutput
{
/// <summary>
/// 模型ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 模型分类
/// </summary>
public string Category { get; set; }
/// <summary>
/// 模型id
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// 模型描述
/// </summary>
public string? ModelDescribe { get; set; }
/// <summary>
/// 模型价格
/// </summary>
public double ModelPrice { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public string ModelType { get; set; }
/// <summary>
/// 模型展示状态
/// </summary>
public string ModelShow { get; set; }
/// <summary>
/// 系统提示
/// </summary>
public string SystemPrompt { get; set; }
/// <summary>
/// API 主机地址
/// </summary>
public string ApiHost { get; set; }
/// <summary>
/// API 密钥
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// 备注信息
/// </summary>
public string? Remark { get; set; }
}

View File

@@ -1,28 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
public class ModelsListDto
{
[JsonPropertyName("object")] public string @object { get; set; }
[JsonPropertyName("data")] public List<ModelsDataDto> Data { get; set; }
public ModelsListDto()
{
Data = new();
}
}
public class ModelsDataDto
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("object")] public string @object { get; set; }
[JsonPropertyName("created")] public long Created { get; set; }
[JsonPropertyName("owned_by")] public string OwnedBy { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
}

View File

@@ -1,28 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// OpenAI常量
/// </summary>
public static class OpenAIConstant
{
/// <summary>
/// 字符串utf-8编码
/// </summary>
/// <returns></returns>
public const string Done = "[DONE]";
/// <summary>
/// Data: 协议头
/// </summary>
public const string Data = "data:";
/// <summary>
/// think: 协议头
/// </summary>
public const string ThinkStart = "<think>";
/// <summary>
/// think: 协议尾
/// </summary>
public const string ThinkEnd = "</think>";
}

View File

@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
public sealed class ThorChatAudioRequest
{
[JsonPropertyName("voice")]
public string? Voice { get; set; }
[JsonPropertyName("format")]
public string? Format { get; set; }
}

View File

@@ -1,66 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 聊天完成选项列
/// </summary>
public record ThorChatChoiceResponse
{
/// <summary>
/// 模型生成的聊天完成消息。【流式】模型响应生成的聊天完成增量存储在此属性。<br/>
/// 在当前模型中无论流式还是非流式Message 和 Delta存储相同的值
/// </summary>
[JsonPropertyName("delta")]
public ThorChatMessage Delta
{
get => Message;
set => Message = value;
}
/// <summary>
/// 模型生成的聊天完成消息。【非流式】返回的消息存储在此属性。<br/>
/// 在当前模型中无论流式还是非流式Message 和 Delta存储相同的值
/// </summary>
[JsonPropertyName("message")]
public ThorChatMessage Message { get; set; }
/// <summary>
/// 选项列表中选项的索引。
/// </summary>
[JsonPropertyName("index")]
public int? Index { get; set; }
/// <summary>
/// 用于处理请求的服务层。仅当在请求中指定了 service_tier 参数时,才包含此字段。
/// </summary>
[JsonPropertyName("service_tier")]
public string? ServiceTier { get; set; }
/// <summary>
/// 模型停止生成令牌的原因。
/// stop 如果模型达到自然停止点或提供的停止序列,
/// length 如果达到请求中指定的最大标记数,
/// content_filter 如果由于内容过滤器中的标志而省略了内容,
/// tool_calls 如果模型调用了工具,或者 function_call (已弃用)
/// 如果模型调用了函数,则会出现这种情况。
/// </summary>
[JsonPropertyName("finish_reason")]
public string? FinishReason { get; set; }
/// <summary>
/// 此指纹表示模型运行时使用的后端配置。
/// 可以与 seed 请求参数结合使用,以了解何时进行了可能影响确定性的后端更改。
/// </summary>
[JsonPropertyName("finish_details")]
public FinishDetailsResponse? FinishDetails { get; set; }
/// <summary>
///
/// </summary>
public class FinishDetailsResponse
{
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("stop")] public string Stop { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
public class ThorChatClaudeThinking
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("budget_tokens")]
public int? BudgetToken { get; set; }
}

View File

@@ -1,289 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 对话补全请求参数对象
/// </summary>
public class ThorChatCompletionsRequest
{
public ThorChatCompletionsRequest()
{
Messages = new List<ThorChatMessage>();
}
[JsonPropertyName("store")]
public bool? Store { get; set; }
/// <summary>
/// 表示对话中支持的模态类型数组。可以为 null。
/// </summary>
[JsonPropertyName("modalities")]
public string[]? Modalities { get; set; }
/// <summary>
/// 表示对话中的音频请求参数。可以为 null。
/// </summary>
[JsonPropertyName("audio")] public ThorChatAudioRequest? Audio { get; set; }
/// <summary>
/// 包含迄今为止对话的消息列表
/// </summary>
[JsonPropertyName("messages")]
public List<ThorChatMessage> Messages { get; set; }
/// <summary>
/// 模型唯一编码值,如 gpt-4gpt-3.5-turbo,moonshot-v1-8k看底层具体平台定义
/// </summary>
[JsonPropertyName("model")]
public string Model { get; set; }
/// <summary>
/// 温度采样的替代方法称为核采样,介于 0 和 1 之间,其中模型考虑具有 top_p 概率质量的标记的结果。
/// 因此 0.1 意味着仅考虑包含前 10% 概率质量的标记。
/// 我们通常建议更改此项或 temperature ,但不要同时更改两者。
/// 默认 1
/// </summary>
[JsonPropertyName("top_p")]
public float? TopP { get; set; }
/// <summary>
/// 使用什么采样温度,介于 0 和 2 之间。
/// 较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性。
/// 我们通常建议更改此项或 top_p ,但不要同时更改两者。
/// 默认 1
/// </summary>
[JsonPropertyName("temperature")]
public float? Temperature { get; set; }
/// <summary>
/// 为每条输入消息生成多少个结果
/// <para>
/// 默认为 1不得大于 5。特别的当 temperature 非常小靠近 0 的时候,
/// 我们只能返回 1 个结果,如果这个时候 n 已经设置并且 > 1
/// 我们的服务会返回不合法的输入参数(invalid_request_error)
/// </para>
/// </summary>
[JsonPropertyName("n")]
public int? N { get; set; }
/// <summary>
/// 如果设置,将发送部分消息增量,就像在 ChatGPT 中一样。
/// 令牌可用时将作为仅数据服务器发送事件发送,流由 data: [DONE] 消息终止。
/// </summary>
[JsonPropertyName("stream")]
public bool? Stream { get; set; }
/// <summary>
/// 流响应选项。仅当您设置 stream: true 时才设置此项。
/// </summary>
[JsonPropertyName("stream_options")]
public ThorStreamOptions? StreamOptions { get; set; }
/// <summary>
/// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。
/// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节,
/// 默认 null
/// </summary>
[JsonIgnore]
public string? Stop { get; set; }
/// <summary>
/// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。
/// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节,
/// 默认 null
/// </summary>
[JsonIgnore]
public IList<string>? StopAsList { get; set; }
/// <summary>
/// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。
/// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节,
/// 默认 null
/// </summary>
[JsonPropertyName("stop")]
public IList<string>? StopCalculated
{
get
{
if (Stop is not null && StopAsList is not null)
{
throw new ValidationException(
"Stop 和 StopAsList 不能同时有值,其中一个应该为 null");
}
if (Stop is not null)
{
return new List<string> { Stop };
}
return StopAsList;
}
}
/// <summary>
/// 生成的答案允许的最大令牌数。默认情况下模型可以返回的令牌数量为4096个提示令牌
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/completions/create#completions/create-max_tokens" />
[JsonPropertyName("max_tokens")]
public int? MaxTokens { get; set; }
/// <summary>
/// 可为补全生成的令牌数量的上限,包括可见输出令牌和推理令牌。
/// </summary>
[JsonPropertyName("max_completion_tokens")]
public int? MaxCompletionTokens { get; set; }
/// <summary>
/// 存在惩罚,介于 -2.0 到 2.0 之间的数字。
/// 正值会根据新生成的词汇是否出现在文本中来进行惩罚,增加模型讨论新话题的可能性,
/// 默认为 0
/// </summary>
/// <seealso href="https://platform.openai.com/docs/api-reference/parameter-details" />
[JsonPropertyName("presence_penalty")]
public float? PresencePenalty { get; set; }
/// <summary>
/// 频率惩罚,介于-2.0到2.0之间的数字。
/// 正值会根据新生成的词汇在文本中现有的频率来进行惩罚,减少模型一字不差重复同样话语的可能性.
/// 默认为 0
/// </summary>
/// <seealso href="https://platform.openai.com/docs/api-reference/parameter-details" />
[JsonPropertyName("frequency_penalty")]
public float? FrequencyPenalty { get; set; }
/// <summary>
/// 接受一个 JSON 对象,该对象将标记(由标记生成器中的标记 ID 指定)映射到从 -100 到 100 的关联偏差值。
/// 从数学上讲,偏差会在采样之前添加到模型生成的 logits 中。
/// 每个模型的确切效果会有所不同,但 -1 和 1 之间的值应该会降低或增加选择的可能性;
/// 像 -100 或 100 这样的值应该会导致相关令牌的禁止或独占选择。
/// </summary>
/// <seealso href="https://platform.openai.com/tokenizer?view=bpe" />
[JsonPropertyName("logit_bias")]
public object? LogitBias { get; set; }
/// <summary>
/// 是否返回输出标记的对数概率。如果为 true则返回 message 的 content 中返回的每个输出标记的对数概率。
/// </summary>
[JsonPropertyName("logprobs")]
public bool? Logprobs { get; set; }
/// <summary>
/// 0 到 20 之间的整数,指定每个标记位置最有可能返回的标记数量,每个标记都有关联的对数概率。
/// 如果使用此参数, logprobs 必须设置为 true 。
/// </summary>
[JsonPropertyName("top_logprobs")]
public int? TopLogprobs { get; set; }
/// <summary>
/// 指定用于处理请求的延迟层。此参数与订阅规模层服务的客户相关:
/// 如果设置为“auto”系统将使用规模等级积分直至用完。
/// 如果设置为“default”则将使用具有较低正常运行时间 SLA 且无延迟保证的默认服务层来处理请求。
/// 默认null
/// </summary>
[JsonPropertyName("service_tier")]
public string? ServiceTier { get; set; }
/// <summary>
/// 模型可能调用的工具列表。目前,仅支持函数作为工具。使用它来提供模型可以为其生成 JSON 输入的函数列表。最多支持 128 个功能。
/// </summary>
[JsonPropertyName("tools")]
public List<ThorToolDefinition>? Tools { get; set; }
/// <summary>
/// 控制模型调用哪个(如果有)工具。
/// none 表示模型不会调用任何工具,而是生成一条消息。
/// auto 表示模型可以在生成消息或调用一个或多个工具之间进行选择。
/// required 表示模型必须调用一个或多个工具。
/// 通过 {"type": "function", "function": {"name": "my_function"}} 指定特定工具会强制模型调用该工具。
/// 当不存在任何工具时, none 是默认值。如果存在工具,则 auto 是默认值。
/// </summary>
[JsonIgnore]
public ThorToolChoice? ToolChoice { get; set; }
[JsonPropertyName("tool_choice")]
public object? ToolChoiceCalculated
{
get
{
if (ToolChoice != null &&
ToolChoice.Type != ThorToolChoiceTypeConst.Function &&
ToolChoice.Function != null)
{
throw new ValidationException(
"当 type 为 \"function\" 时,属性 Function 不可为null。");
}
if (ToolChoice?.Type == ThorToolChoiceTypeConst.Function)
{
return ToolChoice;
}
return ToolChoice?.Type;
}
set
{
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String)
{
ToolChoice = new ThorToolChoice
{
Type = jsonElement.GetString()
};
}
else if (jsonElement.ValueKind == JsonValueKind.Object)
{
ToolChoice = jsonElement.Deserialize<ThorToolChoice>();
}
}
else
{
ToolChoice = (ThorToolChoice)value;
}
}
}
/// <summary>
/// 设置为 {"type": "json_object"} 可启用 JSON 模式,从而保证模型生成的信息是有效的 JSON。
/// 当你将 response_format 设置为 {"type": "json_object"} 时,
/// 你需要在 prompt 中明确地引导模型输出 JSON 格式的内容,
/// 并告知模型该 JSON 的具体格式,否则将可能导致不符合预期的结果。
/// 默认为 {"type": "text"}
/// </summary>
[JsonPropertyName("response_format")]
public ThorResponseFormat? ResponseFormat { get; set; }
[JsonPropertyName("metadata")] public Dictionary<string, string>? Metadata { get; set; }
/// <summary>
/// 此功能处于测试阶段。
/// 如果指定,我们的系统将尽最大努力进行确定性采样,
/// 以便具有相同 seed 和参数的重复请求应返回相同的结果。
/// 不保证确定性,您应该参考 system_fingerprint 响应参数来监控后端的变化。
/// </summary>
[JsonPropertyName("seed")]
public int? Seed { get; set; }
/// <summary>
/// 代表您的最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。
/// </summary>
[JsonPropertyName("user")]
public string? User { get; set; }
[JsonPropertyName("thinking")] public ThorChatClaudeThinking? Thinking { get; set; }
/// <summary>
/// 参数验证
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public IEnumerable<ValidationResult> Validate()
{
throw new NotImplementedException();
}
}

View File

@@ -1,63 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 对话补全服务返回结果
/// </summary>
public record ThorChatCompletionsResponse
{
/// <summary>
/// 对话补全的唯一标识符。<br/>
/// 聊天完成的唯一标识符。如果是流式对话,每个区块都具有相同的 ID。
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }
/// <summary>
/// 用于对话补全的模型。
/// </summary>
[JsonPropertyName("model")]
public string? Model { get; set; }
/// <summary>
/// 对象类型<br/>
/// 非流式对话补全始终为 chat.completion<br/>
/// 流式对话补全始终为 chat.completion.chunk<br/>
/// </summary>
[JsonPropertyName("object")]
public string? ObjectTypeName { get; set; }
/// <summary>
/// 对话补全选项列表。如果 n 大于 1则可以是多个。
/// </summary>
[JsonPropertyName("choices")]
public List<ThorChatChoiceResponse>? Choices { get; set; }
/// <summary>
/// 完成请求的使用情况统计信息。
/// 仅在您 stream_options: {"include_usage": true} 设置请求时才会显示。
/// 如果存在,则它包含一个 null 值,但最后一个块除外,该块包含整个请求的令牌使用情况统计信息。
/// </summary>
[JsonPropertyName("usage")]
public ThorUsageResponse? Usage { get; set; }
/// <summary>
/// 创建对话补全时的 Unix 时间戳(以秒为单位)。
/// </summary>
[JsonPropertyName("created")]
public int Created { get; set; }
/// <summary>
/// 此指纹表示模型运行时使用的后端配置。
/// 可以与 seed 请求参数结合使用,以了解何时进行了可能影响确定性的后端更改。
/// </summary>
[JsonPropertyName("system_fingerprint")]
public string SystemFingerPrint { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("error")]
public ThorError? Error { get; set; }
}

View File

@@ -1,193 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 聊天消息体建议使用CreeateXXX系列方法构建内容
/// </summary>
public class ThorChatMessage
{
/// <summary>
///
/// </summary>
public ThorChatMessage()
{
}
/// <summary>
/// 【必填】发出消息的角色,请使用<see cref="ThorChatMessageRoleConst.User"/>赋值,如ThorChatMessageRoleConst.User
/// </summary>
[JsonPropertyName("role")]
public string Role { get; set; }
/// <summary>
/// 发出的消息内容,如:你好
/// </summary>
[JsonIgnore]
public string? Content { get; set; }
/// <summary>
/// 发出的消息内容,仅当使用 gpt-4o 模型时才支持图像输入。
/// </summary>
/// <example>
/// 示例数据:
/// "content": [
/// {
/// "type": "text",
/// "text": "What'\''s in this image?"
/// },
/// {
/// "type": "image_url",
/// "image_url": {
/// "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
/// }
/// }
/// ]
/// </example>
[JsonIgnore]
public IList<ThorChatMessageContent>? Contents { get; set; }
/// <summary>
/// 发出的消息内容计算用于json序列号和反序列化Content 和 Contents 不能同时赋值,只能二选一
/// </summary>
[JsonPropertyName("content")]
public object ContentCalculated
{
get
{
if (Content is not null && Contents is not null)
{
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
}
if (Content is not null)
{
return Content;
}
return Contents!;
}
set
{
if (value is JsonElement str)
{
if (str.ValueKind == JsonValueKind.String)
{
Content = value?.ToString();
}
else if (str.ValueKind == JsonValueKind.Array)
{
Contents = JsonSerializer.Deserialize<IList<ThorChatMessageContent>>(value?.ToString());
}
}
else
{
Content = value?.ToString();
}
}
}
/// <summary>
/// 【可选】参与者的可选名称。提供模型信息以区分相同角色的参与者。
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
/// <summary>
/// 工具调用 ID,此消息正在响应的工具调用。
/// </summary>
[JsonPropertyName("tool_call_id")]
public string? ToolCallId { get; set; }
/// <summary>
/// 函数调用,已过期,不要使用,请使用 ToolCalls
/// </summary>
[JsonPropertyName("function_call")]
public ThorChatMessageFunction? FunctionCall { get; set; }
/// <summary>
/// 【可选】推理内容
/// </summary>
[JsonPropertyName("reasoning_content")]
public string? ReasoningContent { get; set; }
[JsonPropertyName("id")]
public string? Id { get; set; }
/// <summary>
/// 工具调用列表,模型生成的工具调用,例如函数调用。<br/>
/// 此属性存储在客户端进行tool use 第一次调用模型返回的使用的函数名和传入的参数
/// </summary>
[JsonPropertyName("tool_calls")]
public List<ThorToolCall>? ToolCalls { get; set; }
/// <summary>
/// 创建系统消息
/// </summary>
/// <param name="content">系统消息内容</param>
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
/// <returns></returns>
public static ThorChatMessage CreateSystemMessage(string content, string? name = null)
{
return new()
{
Role = ThorChatMessageRoleConst.System,
Content = content,
Name = name
};
}
/// <summary>
/// 创建用户消息
/// </summary>
/// <param name="content">系统消息内容</param>
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
/// <returns></returns>
public static ThorChatMessage CreateUserMessage(string content, string? name = null)
{
return new()
{
Role = ThorChatMessageRoleConst.User,
Content = content,
Name = name
};
}
/// <summary>
/// 创建助手消息
/// </summary>
/// <param name="content">系统消息内容</param>
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
/// <param name="toolCalls">工具调用参数列表</param>
/// <returns></returns>
public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, List<ThorToolCall> toolCalls = null)
{
return new()
{
Role = ThorChatMessageRoleConst.Assistant,
Content = content,
Name = name,
ToolCalls=toolCalls,
};
}
/// <summary>
/// 创建工具消息
/// </summary>
/// <param name="content">系统消息内容</param>
/// <param name="toolCallId">工具调用 ID,此消息正在响应的工具调用。</param>
/// <returns></returns>
public static ThorChatMessage CreateToolMessage(string content, string toolCallId = null)
{
return new()
{
Role = ThorChatMessageRoleConst.Tool,
Content = content,
ToolCallId= toolCallId
};
}
}

View File

@@ -1,11 +0,0 @@
using System.Text.Json.Serialization;
public sealed class ThorChatMessageAudioContent
{
[JsonPropertyName("data")]
public string? Data { get; set; }
[JsonPropertyName("format")]
public string? Format { get; set; }
}

View File

@@ -1,98 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 发出的消息内容包含图文一般是一文一图一文多图两种情况请使用CreeateXXX系列方法构建内容
/// </summary>
public class ThorChatMessageContent
{
public ThorChatMessageContent()
{
}
/// <summary>
/// 消息内容类型,只能使用<see cref="ThorMessageContentTypeConst"/> 定义的值赋值ThorMessageContentTypeConst.Text
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
/// <summary>
/// 消息内容类型为 text 时候的赋值,如:图片上描述了什么
/// </summary>
[JsonPropertyName("text")]
public string? Text { get; set; }
/// <summary>
/// 消息内容类型为 image_url 时候的赋值
/// </summary>
[JsonPropertyName("image_url")]
public ThorVisionImageUrl? ImageUrl { get; set; }
/// <summary>
/// 音频消息内容,包含音频数据和格式信息。
/// </summary>
[JsonPropertyName("input_audio")]
public ThorChatMessageAudioContent? InputAudio { get; set; }
/// <summary>
/// 创建文本类消息
/// <param name="text">文本内容</param>
/// </summary>
public static ThorChatMessageContent CreateTextContent(string text)
{
return new()
{
Type = ThorMessageContentTypeConst.Text,
Text = text
};
}
/// <summary>
/// 创建图片类消息图片url形式
/// <param name="imageUrl">图片 url</param>
/// <param name="detail">指定图像的详细程度。通过控制 detail 参数(该参数具有三个选项: low 、 high 或 auto ),您
/// 可以控制模型的处理方式图像并生成其文本理解。默认情况下,模型将使用 auto 设置,
/// 该设置将查看图像输入大小并决定是否应使用 low 或 high 设置。</param>
/// </summary>
public static ThorChatMessageContent CreateImageUrlContent(string imageUrl, string? detail = "auto")
{
return new()
{
Type = ThorMessageContentTypeConst.ImageUrl,
ImageUrl = new()
{
Url = imageUrl,
Detail = detail
}
};
}
/// <summary>
/// 创建图片类消息,字节流转base64字符串形式
/// <param name="binaryImage">The image binary data as byte array</param>
/// <param name="imageType">图片类型,如 png,jpg</param>
/// <param name="detail">指定图像的详细程度。</param>
/// </summary>
public static ThorChatMessageContent CreateImageBinaryContent(
byte[] binaryImage,
string imageType,
string? detail = "auto"
)
{
return new()
{
Type = ThorMessageContentTypeConst.ImageUrl,
ImageUrl = new()
{
Url = string.Format(
"data:image/{0};base64,{1}",
imageType,
Convert.ToBase64String(binaryImage)
),
Detail = detail
}
};
}
}

View File

@@ -1,36 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
/// <summary>
/// 模型调用的函数。
/// </summary>
public class ThorChatMessageFunction
{
/// <summary>
/// 功能名,如get_current_weather
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
/// <summary>
/// 调用函数所用的参数,由模型以 JSON 格式生成。请注意,该模型并不总是生成有效的 JSON
/// 并且可能会产生未由函数架构定义的参数。
/// 在调用函数之前验证代码中的参数。
/// 如:"{\"location\": \"San Francisco, USA\", \"format\": \"celsius\"}"
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; set; }
/// <summary>
/// 转换参数为字典
/// </summary>
/// <returns></returns>
public Dictionary<string, object> ParseArguments()
{
var result = string.IsNullOrWhiteSpace(Arguments) == false ? JsonSerializer.Deserialize<Dictionary<string, object>>(Arguments) : new Dictionary<string, object>();
return result;
}
}
}

View File

@@ -1,45 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
/// <summary>
/// 对话消息角色定义
/// </summary>
public class ThorChatMessageRoleConst
{
/// <summary>
/// 系统角色
/// <para>
/// 用于为聊天助手分配特定的行为或上下文,以影响对话的模型行为。
/// 例如,可以将系统角色设定为“您是足球专家”,
/// 那么 ChatGPT 在对话中会表现出特定的个性或专业知识。
/// </para>
/// </summary>
public static string System => "system";
/// <summary>
/// 用户角色
/// <para>
/// 代表实际的最终用户,向 ChatGPT 发送提示或消息,
/// 用于指示消息/提示来自最终用户或人类。
/// </para>
/// </summary>
public static string User => "user";
/// <summary>
/// 助手角色
/// <para>
/// 表示对最终用户提示的响应实体,用于保持对话的连贯性。
/// 它是由模型自动生成并回复的,用于设置模型的先前响应,以继续对话流程。
/// </para>
/// </summary>
public static string Assistant => "assistant";
/// <summary>
/// 工具角色
/// <para>
/// 表示对最终用户提示的响应实体,用于保持对话的连贯性。
/// 它是由模型自动生成并回复的,用于设置模型的先前响应,以继续对话流程。
/// </para>
/// </summary>
public static string Tool => "tool";
}
}

View File

@@ -1,70 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
public class ThorError
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; set; }
/// <summary>
/// 参数
/// </summary>
[JsonPropertyName("param")]
public string? Param { get; set; }
/// <summary>
/// 类型
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonIgnore]
public string? Message { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonIgnore]
public List<string?> Messages { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("message")]
public object MessageObject
{
set
{
switch (value)
{
case string s:
Message = s;
Messages = new() { s };
break;
case List<object> list when list.All(i => i is JsonElement):
Messages = list.Cast<JsonElement>().Select(e => e.GetString()).ToList();
Message = string.Join(Environment.NewLine, Messages);
break;
}
}
get
{
if (Messages?.Count > 1)
{
return Messages;
}
return Message;
}
}
}
}

View File

@@ -1,23 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
/// <summary>
/// 支持图片识别的消息体内容类型
/// </summary>
public class ThorMessageContentTypeConst
{
/// <summary>
/// 文本内容
/// </summary>
public static string Text => "text";
/// <summary>
/// 图片 Url 类型
/// </summary>
public static string ImageUrl => "image_url";
/// <summary>
/// 图片 Url 类型
/// </summary>
public static string Image => "image";
}
}

View File

@@ -1,21 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 指定模型必须输出的格式的对象。用于启用JSON模式。
/// </summary>
public class ThorResponseFormat
{
/// <summary>
/// 设置为json_object启用json模式。
/// 这保证了模型生成的消息是有效的JSON。
/// 注意如果finish_reason=“length”则消息内容可能是部分的
/// 这表示生成超过了max_tokens或对话超过了最大上下文长度。
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("json_schema")]
public ThorResponseJsonSchema JsonSchema { get; set; }
}

View File

@@ -1,18 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
public class ThorResponseJsonSchema
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("strict")]
public bool? Strict { get; set; }
[JsonPropertyName("schema")]
public object Schema { get; set; }
}

View File

@@ -1,18 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
/// <summary>
/// 流响应选项。仅当您设置 stream: true 时才设置此项。
/// </summary>
public class ThorStreamOptions
{
/// <summary>
/// 如果设置,则会在 data: [DONE] 消息之前传输附加块。
/// 该块上的 usage 字段显示整个请求的令牌使用统计信息,
/// choices 字段将始终为空数组。所有其他块也将包含一个 usage 字段,但具有空值。
/// </summary>
[JsonPropertyName("include_usage")]
public bool? IncludeUsage { get; set; }
}
}

View File

@@ -1,38 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 工具调用对象定义
/// </summary>
public class ThorToolCall
{
public ThorToolCall()
{
Id = Guid.NewGuid().ToString("N");
}
/// <summary>
/// 工具调用序号值
/// </summary>
[JsonPropertyName("index")]
public int Index { get; set; }
/// <summary>
/// 工具调用的 ID
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }
/// <summary>
/// 工具的类型。目前仅支持 function
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; set; } = "function";
/// <summary>
/// 模型调用的函数。
/// </summary>
[JsonPropertyName("function")]
public ThorChatMessageFunction? Function { get; set; }
}

View File

@@ -1,55 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 工具
/// </summary>
public class ThorToolChoice
{
/// <summary>
/// 表示模型不会调用任何工具
/// </summary>
public static ThorToolChoice GetNone() => new() { Type = ThorToolChoiceTypeConst.None };
/// <summary>
/// 表示模型可以在生成消息或调用一个或多个工具之间进行选择
/// </summary>
public static ThorToolChoice GetAuto() => new() { Type = ThorToolChoiceTypeConst.Auto };
/// <summary>
/// 表示模型必须调用一个或多个工具
/// </summary>
public static ThorToolChoice GetRequired() => new() { Type = ThorToolChoiceTypeConst.Required };
/// <summary>
/// 指定特定工具会强制模型调用该工具
/// </summary>
/// <param name="functionName">函数名</param>
/// <returns></returns>
public static ThorToolChoice GetFunction(string functionName) => new()
{
Type = ThorToolChoiceTypeConst.Function,
Function = new ThorToolChoiceFunctionTool()
{
Name = functionName
}
};
/// <summary>
/// "none" 表示模型不会调用任何工具<br />
/// "auto" 表示模型可以在生成消息或调用一个或多个工具之间进行选择 <br />
/// "required" 表示模型必须调用一个或多个工具 <br />
/// "function" 指定特定工具会强制模型调用该工具<br />
/// 使用<see cref="ThorToolChoiceTypeConst"/> 赋值
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
/// <summary>
/// 调用的函数定义
/// </summary>
[JsonPropertyName("function")]
public ThorToolChoiceFunctionTool? Function { get; set; }
}

View File

@@ -1,10 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
public class ThorToolChoiceFunctionTool
{
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@@ -1,25 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
public class ThorToolChoiceTypeConst
{
/// <summary>
/// 指定特定工具会强制模型调用该工具
/// </summary>
public static string Function => "function";
/// <summary>
/// 表示模型可以在生成消息或调用一个或多个工具之间进行选择
/// </summary>
public static string Auto => "auto";
/// <summary>
/// 表示模型不会调用任何工具
/// </summary>
public static string None => "none";
/// <summary>
/// 表示模型必须调用一个或多个工具
/// </summary>
public static string Required => "required ";
}
}

View File

@@ -1,32 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 有效工具的定义。
/// </summary>
public class ThorToolDefinition
{
/// <summary>
/// 必修的。工具的类型。目前仅支持 function 。
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = ThorToolTypeConst.Function;
/// <summary>
/// 函数对象
/// </summary>
[JsonPropertyName("function")]
public ThorToolFunctionDefinition? Function { get; set; }
/// <summary>
/// 创建函数工具
/// </summary>
/// <param name="function"></param>
/// <returns></returns>
public static ThorToolDefinition CreateFunctionTool(ThorToolFunctionDefinition function) => new()
{
Type = ThorToolTypeConst.Function,
Function = function
};
}

View File

@@ -1,34 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 有效函数调用的定义。
/// </summary>
public class ThorToolFunctionDefinition
{
[JsonPropertyName("type")]
public string? Type { get; set; }
/// <summary>
/// 要调用的函数的名称。必须是 a-z、A-Z、0-9 或包含下划线和破折号,最大长度为 64。
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// 函数功能的描述,模型使用它来选择何时以及如何调用函数。
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }
/// <summary>
/// 函数接受的参数,描述为 JSON 架构对象。有关示例,请参阅指南,有关格式的文档,请参阅 JSON 架构参考。
/// 省略 parameters 定义一个参数列表为空的函数。
/// See the <a href="https://platform.openai.com/docs/guides/gpt/function-calling">guide</a> for examples,
/// and the <a href="https://json-schema.org/understanding-json-schema/">JSON Schema reference</a> for
/// documentation about the format.
/// </summary>
[JsonPropertyName("parameters")]
public ThorToolFunctionPropertyDefinition Parameters { get; set; }
}

View File

@@ -1,260 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 函数参数是JSON格式对象
/// https://json-schema.org/understanding-json-schema/reference/object.html
/// </summary>
/// <example>
/// 定义属性示例:
/// [JsonPropertyName("location")]
/// public ThorToolFunctionPropertyDefinition Location = ThorToolFunctionPropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA");
///
/// [JsonPropertyName("unit")]
/// public ThorToolFunctionPropertyDefinition Unit = ThorToolFunctionPropertyDefinition.DefineEnum(["celsius", "fahrenheit"]);
/// </example>
public class ThorToolFunctionPropertyDefinition
{
/// <summary>
/// 定义了函数对象的类型枚举
/// </summary>
public enum FunctionObjectTypes
{
/// <summary>
/// 表示字符串类型的函数对象
/// </summary>
String,
/// <summary>
/// 表示整数类型的函数对象
/// </summary>
Integer,
/// <summary>
/// 表示数字(包括浮点数等)类型的函数对象
/// </summary>
Number,
/// <summary>
/// 表示对象类型的函数对象
/// </summary>
Object,
/// <summary>
/// 表示数组类型的函数对象
/// </summary>
Array,
/// <summary>
/// 表示布尔类型的函数对象
/// </summary>
Boolean,
/// <summary>
/// 表示空值类型的函数对象
/// </summary>
Null
}
/// <summary>
/// 必填的。函数参数对象类型。默认值为“object”。
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = "object";
/// <summary>
/// 可选。“函数参数”列表,作为从参数名称映射的字典
/// 对于描述类型的对象,可能还有可能的枚举值等等。
/// </summary>
[JsonPropertyName("properties")]
public IDictionary<string, ThorToolFunctionPropertyDefinition>? Properties { get; set; }
/// <summary>
/// 可选。列出必需的“function arguments”列表。
/// </summary>
[JsonPropertyName("required")]
public List<string>? Required { get; set; }
/// <summary>
/// 可选。是否允许附加属性。默认值为true。
/// </summary>
[JsonPropertyName("additionalProperties")]
public bool? AdditionalProperties { get; set; }
/// <summary>
/// 可选。参数描述。
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }
/// <summary>
/// 可选。此参数的允许值列表。
/// </summary>
[JsonPropertyName("enum")]
public List<string>? Enum { get; set; }
/// <summary>
/// 可以使用minProperties和maxProperties关键字限制对象上的属性数量。每一个
/// 这些必须是非负整数。
/// </summary>
[JsonPropertyName("minProperties")]
public int? MinProperties { get; set; }
/// <summary>
/// 可以使用minProperties和maxProperties关键字限制对象上的属性数量。每一个
/// 这些必须是非负整数。
/// </summary>
[JsonPropertyName("maxProperties")]
public int? MaxProperties { get; set; }
/// <summary>
/// 如果type为“array”则指定数组中所有项目的元素类型。
/// 如果类型不是“array”则应为null。
/// 有关更多详细信息,请参阅 https://json-schema.org/understanding-json-schema/reference/array.html
/// </summary>
[JsonPropertyName("items")]
public ThorToolFunctionPropertyDefinition? Items { get; set; }
/// <summary>
/// 定义数组
/// </summary>
/// <param name="arrayItems"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineArray(ThorToolFunctionPropertyDefinition? arrayItems = null)
{
return new ThorToolFunctionPropertyDefinition
{
Items = arrayItems,
Type = ConvertTypeToString(FunctionObjectTypes.Array)
};
}
/// <summary>
/// 定义枚举
/// </summary>
/// <param name="enumList"></param>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineEnum(List<string> enumList, string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Enum = enumList,
Type = ConvertTypeToString(FunctionObjectTypes.String)
};
}
/// <summary>
/// 定义整型
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineInteger(string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Type = ConvertTypeToString(FunctionObjectTypes.Integer)
};
}
/// <summary>
/// 定义数字
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineNumber(string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Type = ConvertTypeToString(FunctionObjectTypes.Number)
};
}
/// <summary>
/// 定义字符串
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineString(string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Type = ConvertTypeToString(FunctionObjectTypes.String)
};
}
/// <summary>
/// 定义布尔值
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineBoolean(string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Type = ConvertTypeToString(FunctionObjectTypes.Boolean)
};
}
/// <summary>
/// 定义null
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineNull(string? description = null)
{
return new ThorToolFunctionPropertyDefinition
{
Description = description,
Type = ConvertTypeToString(FunctionObjectTypes.Null)
};
}
/// <summary>
/// 定义对象
/// </summary>
/// <param name="properties"></param>
/// <param name="required"></param>
/// <param name="additionalProperties"></param>
/// <param name="description"></param>
/// <param name="enum"></param>
/// <returns></returns>
public static ThorToolFunctionPropertyDefinition DefineObject(IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
List<string>? required,
bool? additionalProperties,
string? description,
List<string>? @enum)
{
return new ThorToolFunctionPropertyDefinition
{
Properties = properties,
Required = required,
AdditionalProperties = additionalProperties,
Description = description,
Enum = @enum,
Type = ConvertTypeToString(FunctionObjectTypes.Object)
};
}
/// <summary>
/// 将 `FunctionObjectTypes` 枚举值转换为其对应的字符串表示形式。
/// </summary>
/// <param name="type">要转换的类型</param>
/// <returns>给定类型的字符串表示形式</returns>
public static string ConvertTypeToString(FunctionObjectTypes type)
{
return type switch
{
FunctionObjectTypes.String => "string",
FunctionObjectTypes.Integer => "integer",
FunctionObjectTypes.Number => "number",
FunctionObjectTypes.Object => "object",
FunctionObjectTypes.Array => "array",
FunctionObjectTypes.Boolean => "boolean",
FunctionObjectTypes.Null => "null",
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
};
}
}

View File

@@ -1,13 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
{
/// <summary>
/// 工具类型定义
/// </summary>
public class ThorToolTypeConst
{
/// <summary>
/// 函数
/// </summary>
public static string Function => "function";
}
}

View File

@@ -1,102 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 统计信息模型
/// </summary>
public record ThorUsageResponse
{
/// <summary>
/// 提示中的令牌数。
/// </summary>
[JsonPropertyName("prompt_tokens")]
public int? PromptTokens { get; set; }
[JsonPropertyName("input_tokens")]
public int? InputTokens { get; set; }
[JsonPropertyName("output_tokens")]
public int? OutputTokens { get; set; }
[JsonPropertyName("input_tokens_details")]
public ThorUsageResponseInputTokensDetails? InputTokensDetails { get; set; }
/// <summary>
/// 生成的完成中的令牌数。
/// </summary>
[JsonPropertyName("completion_tokens")]
public long? CompletionTokens { get; set; }
/// <summary>
/// 请求中使用的令牌总数(提示 + 完成)。
/// </summary>
[JsonPropertyName("total_tokens")]
public long? TotalTokens { get; set; }
/// <summary>
/// ThorUsageResponsePromptTokensDetails
/// </summary>
[JsonPropertyName("prompt_tokens_details")]
public ThorUsageResponsePromptTokensDetails? PromptTokensDetails { get; set; }
/// <summary>
/// ThorUsageResponseCompletionTokensDetails
/// </summary>
[JsonPropertyName("completion_tokens_details")]
public ThorUsageResponseCompletionTokensDetails? CompletionTokensDetails { get; set; }
}
public class ThorUsageResponseInputTokensDetails
{
[JsonPropertyName("image_tokens")]
public int? ImageTokens { get; set; }
[JsonPropertyName("text_tokens")]
public int? TextTokens { get; set; }
}
public record ThorUsageResponsePromptTokensDetails
{
/// <summary>
/// 缓存的令牌数。
/// </summary>
[JsonPropertyName("cached_tokens")]
public int? CachedTokens { get; set; }
/// <summary>
/// audio_tokens
/// </summary>
[JsonPropertyName("audio_tokens")]
public int? AudioTokens { get; set; }
}
/// <summary>
/// completion_tokens_details
/// </summary>
public record ThorUsageResponseCompletionTokensDetails
{
/// <summary>
/// 使用 Predicted Outputs 时, Prediction 的 Final。
/// </summary>
[JsonPropertyName("accepted_prediction_tokens")]
public int? AcceptedPredictionTokens { get; set; }
/// <summary>
/// 模型生成的音频输入令牌。
/// </summary>
[JsonPropertyName("audio_tokens")]
public int? AudioTokens { get; set; }
/// <summary>
/// 模型生成的用于推理的 Token。
/// </summary>
[JsonPropertyName("reasoning_tokens")]
public int? ReasoningTokens { get; set; }
/// <summary>
/// 使用 Predicted Outputs 时, 预测,但未出现在 completion 中。但是,与 reasoning 令牌,这些令牌仍然计入总数 用于 Billing、Output 和 Context Window 的完成令牌 限制。
/// </summary>
[JsonPropertyName("rejected_prediction_tokens")]
public int? RejectedPredictionTokens { get; set; }
}

View File

@@ -1,29 +0,0 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
/// <summary>
/// 图片消息内容对象
/// </summary>
public class ThorVisionImageUrl
{
/// <summary>
/// 图片的url地址https://localhost/logo.jpg ,一般只支持 .png , .jpg .webp .gif
/// 也可以是base64字符串,如data:image/jpeg;base64,{base64_image}
/// 要看底层平台具体要求
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }
/// <summary>
/// 指定图像的细节级别。在愿景指南中了解更多信息。https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding
/// <para>
/// 指定图像的详细程度。通过控制 detail 参数(该参数具有三个选项: low 、 high 或 auto ),您
/// 可以控制模型的处理方式图像并生成其文本理解。默认情况下,模型将使用 auto 设置,
/// 该设置将查看图像输入大小并决定是否应使用 low 或 high 设置。
/// </para>
/// </summary>
[JsonPropertyName("detail")]
public string? Detail { get; set; } = "auto";
}

View File

@@ -1,45 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
public class RechargeGetListOutput
{
/// <summary>
/// 充值金额
/// </summary>
public decimal RechargeAmount { get; set; }
/// <summary>
/// ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 用户
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 充值内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 到期时间
/// </summary>
public DateTime? ExpireDateTime { get; set; }
/// <summary>
/// 联系方式
/// </summary>
public string? ContactInfo { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SendMessageInput
{
public List<Message> Messages { get; set; }
public string Model { get; set; }
public Guid? SessionId{ get; set; }
}
public class Message
{
public string Role { get; set; }
public string Content { get; set; }
}

View File

@@ -1,164 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SendMessageStreamOutputDto
{
/// <summary>
/// 唯一标识符
/// </summary>
public string Id { get; set; }
/// <summary>
/// 对象类型
/// </summary>
public string Object { get; set; }
/// <summary>
/// 创建时间Unix时间戳格式
/// </summary>
public long Created { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string Model { get; set; }
/// <summary>
/// 选择项列表
/// </summary>
public List<Choice> Choices { get; set; }
/// <summary>
/// 系统指纹(可能为空)
/// </summary>
public string SystemFingerprint { get; set; }
/// <summary>
/// 使用情况信息
/// </summary>
public Usage Usage { get; set; }
}
/// <summary>
/// 选择项类,表示模型返回的一个选择
/// </summary>
public class Choice
{
/// <summary>
/// 选择索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// 变化内容,包括内容字符串和角色
/// </summary>
public Delta Delta { get; set; }
/// <summary>
/// 结束原因,可能为空
/// </summary>
public string? FinishReason { get; set; }
/// <summary>
/// 内容过滤结果
/// </summary>
public ContentFilterResults ContentFilterResults { get; set; }
}
/// <summary>
/// 变化内容
/// </summary>
public class Delta
{
/// <summary>
/// 内容文本
/// </summary>
public string Content { get; set; }
/// <summary>
/// 角色,例如"assistant"
/// </summary>
public string Role { get; set; }
}
/// <summary>
/// 内容过滤结果
/// </summary>
public class ContentFilterResults
{
public FilterStatus Hate { get; set; }
public FilterStatus SelfHarm { get; set; }
public FilterStatus Sexual { get; set; }
public FilterStatus Violence { get; set; }
public FilterStatus Jailbreak { get; set; }
public FilterStatus Profanity { get; set; }
}
/// <summary>
/// 过滤状态,表示是否经过过滤以及检测是否命中
/// </summary>
public class FilterStatus
{
/// <summary>
/// 是否被过滤
/// </summary>
public bool Filtered { get; set; }
/// <summary>
/// 是否检测到该类型(例如 Jailbreak 中存在此字段)
/// </summary>
public bool? Detected { get; set; }
}
/// <summary>
/// 使用情况,记录 token 数量等信息
/// </summary>
public class Usage
{
/// <summary>
/// 提示词数量
/// </summary>
public int PromptTokens { get; set; }
/// <summary>
/// 补全词数量
/// </summary>
public int CompletionTokens { get; set; }
/// <summary>
/// 总的 Token 数量
/// </summary>
public int TotalTokens { get; set; }
/// <summary>
/// 提示词详细信息
/// </summary>
public PromptTokensDetails PromptTokensDetails { get; set; }
/// <summary>
/// 补全文字详细信息
/// </summary>
public CompletionTokensDetails CompletionTokensDetails { get; set; }
}
/// <summary>
/// 提示词相关 token 详细信息
/// </summary>
public class PromptTokensDetails
{
public int AudioTokens { get; set; }
public int CachedTokens { get; set; }
}
/// <summary>
/// 补全相关 token 详细信息
/// </summary>
public class CompletionTokensDetails
{
public int AudioTokens { get; set; }
public int ReasoningTokens { get; set; }
public int AcceptedPredictionTokens { get; set; }
public int RejectedPredictionTokens { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SessionCreateAndUpdateInput
{
public string SessionTitle { get; set; }
public string SessionContent { get; set; }
public string? Remark { get; set; }
}

View File

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

View File

@@ -1,8 +0,0 @@
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class SessionGetListInput:PagedAllResultRequestDto
{
public string? SessionTitle { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
public class TokenOutput
{
public string? ApiKey { get; set; }
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<ItemGroup>
<ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application.Contracts\Yi.Framework.Ddd.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" />
</ItemGroup>
<ItemGroup>
<Folder Include="IServices\" />
</ItemGroup>
</Project>

View File

@@ -1,21 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.AiHub.Domain.Shared;
using Yi.Framework.Ddd.Application.Contracts;
using Yi.Framework.Rbac.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts
{
[DependsOn(
typeof(YiFrameworkAiHubDomainSharedModule),
typeof(YiFrameworkDddApplicationContractsModule),
typeof(YiFrameworkRbacApplicationContractsModule)
)]
public class YiFrameworkAiHubApplicationContractsModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var build = context.Services.GetConfiguration();
}
}
}

View File

@@ -1,121 +0,0 @@
using System.Collections.Concurrent;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OpenAI.Chat;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// ai服务
/// </summary>
public class AiChatService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly ILogger<AiChatService> _logger;
private readonly AiGateWayManager _aiGateWayManager;
public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager)
{
_httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager;
_aiModelRepository = aiModelRepository;
_logger = logger;
_aiGateWayManager = aiGateWayManager;
}
/// <summary>
/// 查询已登录的账户信息
/// </summary>
/// <returns></returns>
[Route("ai-chat/account")]
[Authorize]
public async Task<UserRoleMenuDto> GetAsync()
{
var accountService = LazyServiceProvider.GetRequiredService<IAccountService>();
var output = await accountService.GetAsync();
return output;
}
/// <summary>
/// 获取模型列表
/// </summary>
/// <returns></returns>
public async Task<List<ModelGetListOutput>> GetModelAsync()
{
var output = await _aiModelRepository._DbQueryable
.OrderByDescending(x => x.OrderNum)
.Select(x => new ModelGetListOutput
{
Id = x.Id,
Category = "chat",
ModelId = x.ModelId,
ModelName = x.Name,
ModelDescribe = x.Description,
ModelPrice = 0,
ModelType = "1",
ModelShow = "0",
SystemPrompt = null,
ApiHost = null,
ApiKey = null,
Remark = x.Description
}).ToListAsync();
return output;
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="input"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromRoute] Guid sessionId,
CancellationToken cancellationToken)
{
//除了免费模型,其他的模型都要校验
if (!input.Model.Contains("DeepSeek-R1"))
{
//有token需要黑名单校验
if (CurrentUser.IsAuthenticated)
{
await _aiBlacklistManager.VerifiyAiBlacklist(CurrentUser.GetId());
if (!CurrentUser.IsAiVip())
{
throw new UserFriendlyException("该模型需要VIP用户才能使用请购买VIP后重新登录重试");
}
}
else
{
throw new UserFriendlyException("未登录用户只能使用未加速的DeepSeek-R1请登录后重试");
}
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, cancellationToken);
}
}

View File

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

View File

@@ -1,95 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
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.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
public class OpenApiService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<OpenApiService> _logger;
private readonly TokenManager _tokenManager;
private readonly AiGateWayManager _aiGateWayManager;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_tokenManager = tokenManager;
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
}
/// <summary>
/// 对话
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/chat/completions")]
public async Task ChatCompletionsAsync([FromBody] ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
//ai网关代理httpcontext
if (input.Stream == true)
{
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId, null, cancellationToken);
}
else
{
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null,
cancellationToken);
}
}
/// <summary>
/// 获取模型列表
/// </summary>
/// <returns></returns>
[HttpGet("openApi/v1/models")]
public async Task<ModelsListDto> ModelsAsync()
{
var data = await _aiModelRepository._DbQueryable
.OrderByDescending(x => x.OrderNum)
.Select(x => new ModelsDataDto
{
Id = x.ModelId,
@object = "model",
Created = DateTime.Now.ToUnixTimeSeconds(),
OwnedBy = "organization-owner",
Type = x.ModelId
}).ToListAsync();
return new ModelsListDto()
{
Data = data
};
}
private string? GetTokenByHttpContext(HttpContext httpContext)
{
// 获取Authorization头
string authHeader = httpContext.Request.Headers["Authorization"];
// 检查是否有Bearer token
if (authHeader != null && authHeader.StartsWith("Bearer "))
{
return authHeader.Substring("Bearer ".Length).Trim();
}
return null;
}
}

View File

@@ -1,41 +0,0 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// ai 充值表
/// </summary>
public class RechargeService : ApplicationService
{
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
public RechargeService(ISqlSugarRepository<AiRechargeAggregateRoot> repository)
{
_repository = repository;
}
/// <summary>
/// 查询已登录的账户充值记录
/// </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;
}
}

View File

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

View File

@@ -1,55 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
public class TokenService : ApplicationService
{
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly TokenManager _tokenManager;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="tokenRepository"></param>
/// <param name="tokenManager"></param>
public TokenService(ISqlSugarRepository<TokenAggregateRoot> tokenRepository, TokenManager tokenManager)
{
_tokenRepository = tokenRepository;
_tokenManager = tokenManager;
}
/// <summary>
/// 获取token
/// </summary>
/// <returns></returns>
[Authorize]
public async Task<TokenOutput> GetAsync()
{
return new TokenOutput
{
ApiKey = await _tokenManager.GetAsync(CurrentUser.GetId())
};
}
/// <summary>
/// 创建token
/// </summary>
/// <exception cref="UserFriendlyException"></exception>
[Authorize]
public async Task CreateAsync()
{
if (!CurrentUser.IsAiVip())
{
throw new UserFriendlyException("充值成为Vip畅想第三方token服务");
}
await _tokenManager.CreateAsync(CurrentUser.GetId());
}
}

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<ItemGroup>
<ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application\Yi.Framework.Ddd.Application.csproj" />
<ProjectReference Include="..\Yi.Framework.AiHub.Application.Contracts\Yi.Framework.AiHub.Application.Contracts.csproj" />
<ProjectReference Include="..\Yi.Framework.AiHub.Domain\Yi.Framework.AiHub.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,15 +0,0 @@
using Yi.Framework.AiHub.Application.Contracts;
using Yi.Framework.AiHub.Domain;
using Yi.Framework.Ddd.Application;
namespace Yi.Framework.AiHub.Application
{
[DependsOn(
typeof(YiFrameworkAiHubApplicationContractsModule),
typeof(YiFrameworkAiHubDomainModule),
typeof(YiFrameworkDddApplicationModule)
)]
public class YiFrameworkAiHubApplicationModule : AbpModule
{
}
}

View File

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

View File

@@ -1,10 +0,0 @@
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
public class TokenUsage
{
public int OutputTokenCount { get; set; }
public int InputTokenCount { get; set; }
public int TotalTokenCount { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum MessageTypeEnum
{
Web = 1,
Api = 2
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Domain.Shared" Version="$(AbpVersion)" />
</ItemGroup>
<ItemGroup>
<Folder Include="Consts\" />
<Folder Include="Etos\" />
</ItemGroup>
</Project>

View File

@@ -1,11 +0,0 @@
using Volo.Abp.Domain;
namespace Yi.Framework.AiHub.Domain.Shared
{
[DependsOn(
typeof(AbpDddDomainSharedModule))]
public class YiFrameworkAiHubDomainSharedModule : AbpModule
{
}
}

View File

@@ -1,5 +0,0 @@
namespace Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
public sealed class PaymentRequiredException() : Exception()
{
}

View File

@@ -1,12 +0,0 @@
namespace Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
public class ThorRateLimitException : Exception
{
public ThorRateLimitException()
{
}
public ThorRateLimitException(string message) : base(message)
{
}
}

View File

@@ -1,274 +0,0 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public static class HttpClientExtensions
{
public static async Task<HttpResponseMessage> HttpRequestRaw(this HttpClient httpClient, string url,
object? postData,
string token)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add("Authorization", $"Bearer {token}");
}
var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
return response;
}
public static async Task<HttpResponseMessage> HttpRequestRaw(this HttpClient httpClient, string url,
object? postData,
string token, string tokenKey)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add(tokenKey, token);
}
var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
return response;
}
public static async Task<HttpResponseMessage> HttpRequestRaw(this HttpClient httpClient, string url,
object? postData,
string token, Dictionary<string, string> headers)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add("Authorization", $"Bearer {token}");
}
foreach (var header in headers)
{
req.Headers.Add(header.Key, header.Value);
}
var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
return response;
}
public static async Task<HttpResponseMessage> HttpRequestRaw(this HttpClient httpClient, HttpRequestMessage req,
object? postData)
{
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
return response;
}
public static async Task<HttpResponseMessage> PostJsonAsync(this HttpClient httpClient, string url,
object? postData,
string token)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
var stringContent =
new StringContent(JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions),
Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add("Authorization", $"Bearer {token}");
}
return await httpClient.SendAsync(req);
}
public static async Task<HttpResponseMessage> PostJsonAsync(this HttpClient httpClient, string url,
object? postData,
string token, Dictionary<string, string> headers)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add("Authorization", $"Bearer {token}");
}
foreach (var header in headers)
{
req.Headers.Add(header.Key, header.Value);
}
return await httpClient.SendAsync(req);
}
public static Task<HttpResponseMessage> PostJsonAsync(this HttpClient httpClient, string url, object? postData,
string token, string tokenKey)
{
HttpRequestMessage req = new(HttpMethod.Post, url);
if (postData != null)
{
if (postData is HttpContent data)
{
req.Content = data;
}
else
{
string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions);
var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
req.Content = stringContent;
}
}
if (!string.IsNullOrEmpty(token))
{
req.Headers.Add(tokenKey, token);
}
return httpClient.SendAsync(req);
}
public static async Task<TResponse> PostAndReadAsAsync<TResponse>(this HttpClient client, string uri,
object? requestModel, CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new()
{
var response = await client.PostAsJsonAsync(uri, requestModel, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
}, cancellationToken);
return await HandleResponseContent<TResponse>(response, cancellationToken);
}
public static async Task<TResponse> PostFileAndReadAsAsync<TResponse>(this HttpClient client, string uri,
HttpContent content, CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new()
{
var response = await client.PostAsync(uri, content, cancellationToken);
return await HandleResponseContent<TResponse>(response, cancellationToken);
}
public static async Task<string> PostFileAndReadAsStringAsync(this HttpClient client, string uri,
HttpContent content, CancellationToken cancellationToken = default)
{
var response = await client.PostAsync(uri, content, cancellationToken);
return await response.Content.ReadAsStringAsync(cancellationToken) ?? throw new InvalidOperationException();
}
public static async Task<TResponse> DeleteAndReadAsAsync<TResponse>(this HttpClient client, string uri,
CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new()
{
var response = await client.DeleteAsync(uri, cancellationToken);
return await HandleResponseContent<TResponse>(response, cancellationToken);
}
private static async Task<TResponse> HandleResponseContent<TResponse>(this HttpResponseMessage response,
CancellationToken cancellationToken) where TResponse : ThorBaseResponse, new()
{
TResponse result;
if (!response.Content.Headers.ContentType?.MediaType?.Equals("application/json",
StringComparison.OrdinalIgnoreCase) ?? true)
{
result = new()
{
Error = new()
{
MessageObject = await response.Content.ReadAsStringAsync(cancellationToken)
}
};
}
else
{
result = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: cancellationToken) ??
throw new InvalidOperationException();
}
return result;
}
}

View File

@@ -1,73 +0,0 @@
using System.Collections.Concurrent;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public static class HttpClientFactory
{
/// <summary>
/// HttpClient池总数
/// </summary>
/// <returns></returns>
private static int _poolSize;
private static int PoolSize
{
get
{
if (_poolSize == 0)
{
// 获取环境变量
var poolSize = Environment.GetEnvironmentVariable("HttpClientPoolSize");
if (!string.IsNullOrEmpty(poolSize) && int.TryParse(poolSize, out var size))
{
_poolSize = size;
}
else
{
_poolSize = Environment.ProcessorCount;
}
if (_poolSize < 1)
{
_poolSize = 2;
}
}
return _poolSize;
}
}
private static readonly ConcurrentDictionary<string, Lazy<List<HttpClient>>> HttpClientPool = new();
public static HttpClient GetHttpClient(string key)
{
return HttpClientPool.GetOrAdd(key, k => new Lazy<List<HttpClient>>(() =>
{
var clients = new List<HttpClient>(PoolSize);
for (var i = 0; i < PoolSize; i++)
{
clients.Add(new HttpClient(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(30),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(30),
EnableMultipleHttp2Connections = true,
// 连接超时5分钟
ConnectTimeout = TimeSpan.FromMinutes(5),
MaxAutomaticRedirections = 3,
AllowAutoRedirect = true,
Expect100ContinueTimeout = TimeSpan.FromMinutes(30),
})
{
Timeout = TimeSpan.FromMinutes(30),
DefaultRequestHeaders =
{
{ "User-Agent", "yxai" },
}
});
}
return clients;
})).Value[new Random().Next(0, PoolSize)];
}
}

View File

@@ -1,29 +0,0 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IChatCompletionService
{
/// <summary>
/// 聊天完成-流式
/// </summary>
/// <param name="aiModelDescribe"></param>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe aiModelDescribe,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken);
/// <summary>
/// 聊天完成-非流式
/// </summary>
/// <param name="aiModelDescribe"></param>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken);
}

View File

@@ -1,8 +0,0 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface ISpecialCompatible
{
public void Compatible(ThorChatCompletionsRequest request);
}

View File

@@ -1,187 +0,0 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCompletionsService> logger)
: IChatCompletionService
{
private string GetAddress(AiModelDescribe? options, string model)
{
// This method should return the appropriate URL for the Azure Databricks API
// based on the provided options and model.
// For now, we will return a placeholder URL.
return $"{options?.Endpoint.TrimEnd('/')}/serving-endpoints/{model}/invocations";
}
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var address = GetAddress(options, chatCompletionCreate.Model);
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
chatCompletionCreate.StreamOptions = null;
var response = await HttpClientFactory.GetHttpClient(address).HttpRequestRaw(
address,
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)
{
var error = await response.Content.ReadAsStringAsync();
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
error);
throw new BusinessException(response.StatusCode.ToString(), "OpenAI对话异常" + error);
}
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(cancellationToken).ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
line);
throw new BusinessException("500", "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);
if (result == null)
{
continue;
}
var content = result?.Choices?.FirstOrDefault()?.Delta;
if (first && content?.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink && result?.Choices != null)
{
// 需要将content的内容转换到其他字段
foreach (var choice in result.Choices)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
yield return result;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
var address = GetAddress(options, chatCompletionCreate.Model);
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
var response = await HttpClientFactory.GetHttpClient(address).PostJsonAsync(
address,
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对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new BusinessException(response.StatusCode.ToString(), "OpenAI对话异常");
}
var result =
await response.Content.ReadFromJsonAsync<ThorChatCompletionsResponse>(
cancellationToken: cancellationToken).ConfigureAwait(false);
return result;
}
}

View File

@@ -1,71 +0,0 @@
using System.ClientModel;
using System.Collections.Concurrent;
using Azure.AI.OpenAI;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI;
public static class AzureOpenAIFactory
{
private const string AddressTemplate = "{0}/openai/deployments/{1}/chat/completions?api-version={2}";
private const string EditImageAddressTemplate = "{0}/openai/deployments/{1}/images/edits?api-version={2}";
private const string AudioSpeechTemplate = "{0}/openai/deployments/{1}/audio/speech?api-version={2}";
private const string AudioTranscriptions =
"{0}/openai/deployments/{1}/audio/transcriptions?api-version={2}";
private static readonly ConcurrentDictionary<string, AzureOpenAIClient> Clients = new();
public static string GetAudioTranscriptionsAddress(AiModelDescribe options, string model)
{
if (string.IsNullOrEmpty(options.AppExtraUrl))
{
options.AppExtraUrl = "2025-03-01-preview";
}
return string.Format(AudioTranscriptions, options.Endpoint.TrimEnd('/'), model, options.AppExtraUrl);
}
public static string GetAudioSpeechAddress(AiModelDescribe options, string model)
{
if (string.IsNullOrEmpty(options.AppExtraUrl))
{
options.AppExtraUrl = "2025-03-01-preview";
}
return string.Format(AudioSpeechTemplate, options.Endpoint.TrimEnd('/'), model, options.AppExtraUrl);
}
public static string GetAddress(AiModelDescribe options, string model)
{
if (string.IsNullOrEmpty(options.AppExtraUrl))
{
options.AppExtraUrl = "2025-03-01-preview";
}
return string.Format(AddressTemplate, options.Endpoint.TrimEnd('/'), model, options.AppExtraUrl);
}
public static string GetEditImageAddress(AiModelDescribe options, string model)
{
if (string.IsNullOrEmpty(options.AppExtraUrl))
{
options.AppExtraUrl = "2025-03-01-preview";
}
return string.Format(EditImageAddressTemplate, options.Endpoint.TrimEnd('/'), model, options.AppExtraUrl);
}
public static AzureOpenAIClient CreateClient(AiModelDescribe options)
{
return Clients.GetOrAdd($"{options.ApiKey}_{options.Endpoint}_{options.AppExtraUrl}", (_) =>
{
const AzureOpenAIClientOptions.ServiceVersion version = AzureOpenAIClientOptions.ServiceVersion.V2024_06_01;
var client = new AzureOpenAIClient(new Uri(options.Endpoint), new ApiKeyCredential(options.ApiKey),
new AzureOpenAIClientOptions(version));
return client;
});
}
}

View File

@@ -1,113 +0,0 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChatCompletionCompletionsService> logger)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Azure OpenAI 对话流式补全");
var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model);
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(url,
chatCompletionCreate, options.ApiKey, "Api-Key");
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync();
logger.LogError("Azure对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
error);
throw new BusinessException(response.StatusCode.ToString(), "AzureOpenAI对话异常" + error);
}
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
string? line = string.Empty;
var first = true;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("AzureOpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}",
response.StatusCode,
line);
throw new BusinessException("500", "AzureOpenAI对话异常", 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);
yield return result;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Azure OpenAI 对话补全");
var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model);
var response =
await HttpClientFactory.GetHttpClient(options.Endpoint)
.PostJsonAsync(url, chatCompletionCreate, options.ApiKey, "Api-Key");
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 如果限流则抛出限流异常
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new ThorRateLimitException();
}
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
logger.LogError("Azure对话异常 , StatusCode: {StatusCode} Response: {Response} Url:{Url}", response.StatusCode,
await response.Content.ReadAsStringAsync(cancellationToken), url);
}
var result = await response.Content
.ReadFromJsonAsync<ThorChatCompletionsResponse>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
return result;
}
}

View File

@@ -1,23 +0,0 @@
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public class SpecialCompatible : ISpecialCompatible,ISingletonDependency
{
private readonly IOptions<SpecialCompatibleOptions> _options;
public SpecialCompatible(IOptions<SpecialCompatibleOptions> options)
{
_options = options;
}
public void Compatible(ThorChatCompletionsRequest request)
{
foreach (var handle in _options.Value.Handles)
{
handle(request);
}
}
}

View File

@@ -1,8 +0,0 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public class SpecialCompatibleOptions
{
public List<Action<ThorChatCompletionsRequest>> Handles { get; set; } = new();
}

View File

@@ -1,26 +0,0 @@
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public record ThorBaseResponse
{
/// <summary>
/// 对象类型
/// </summary>
[JsonPropertyName("object")]
public string? ObjectTypeName { get; set; }
/// <summary>
///
/// </summary>
public bool Successful => Error == null;
/// <summary>
///
/// </summary>
[JsonPropertyName("error")]
public ThorError? Error { get; set; }
}

View File

@@ -1,14 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public static class ThorJsonSerializer
{
public static JsonSerializerOptions DefaultOptions => new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
}

View File

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

View File

@@ -1,41 +0,0 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 充值表
/// </summary>
[SugarTable("Ai_Recharge")]
public class AiRechargeAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 充值金额
/// </summary>
public decimal RechargeAmount { get; set; }
/// <summary>
/// 用户
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 充值内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 到期时间
/// </summary>
public DateTime? ExpireDateTime { get; set; }
/// <summary>
/// 联系方式
/// </summary>
public string? ContactInfo { get; set; }
}

View File

@@ -1,56 +0,0 @@
using Mapster;
using SqlSugar;
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.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
[SugarTable("Ai_Message")]
[SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(SessionId)}",
$"{nameof(UserId)}", OrderByType.Desc,
$"{nameof(SessionId)}", OrderByType.Desc
)]
public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public MessageAggregateRoot()
{
}
public MessageAggregateRoot(Guid userId, Guid? sessionId, string content, string role, string modelId,
ThorUsageResponse? tokenUsage)
{
UserId = userId;
SessionId = sessionId;
Content = content;
Role = role;
ModelId = modelId;
if (tokenUsage is not null)
{
this.TokenUsage = new TokenUsageValueObject
{
OutputTokenCount = tokenUsage.OutputTokens ?? 0,
InputTokenCount = tokenUsage.InputTokens ?? 0,
TotalTokenCount = tokenUsage.TotalTokens ?? 0
};
}
this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web;
}
public Guid UserId { get; set; }
public Guid? SessionId { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string Content { get; set; }
public string Role { get; set; }
public string ModelId { get; set; }
public string? Remark { get; set; }
[SugarColumn(IsOwnsOne = true)] public TokenUsageValueObject TokenUsage { get; set; } = new TokenUsageValueObject();
public MessageTypeEnum MessageType { get; set; }
}

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
using SqlSugar;
using Volo.Abp.Domain.Entities;
using Yi.Framework.Core.Data;
namespace Yi.Framework.AiHub.Domain.Entities.Model;
/// <summary>
/// ai模型定义
/// </summary>
[SugarTable("Ai_Model")]
public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
{
/// <summary>
/// 处理名
/// </summary>
public string HandlerName { get; set; }
/// <summary>
/// 模型id
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 模型描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNum { get; set; }
/// <summary>
/// 软删除
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// ai应用id
/// </summary>
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
public string? ExtraInfo { get; set; }
}

View File

@@ -1,52 +0,0 @@
using System.Text;
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities.OpenApi;
[SugarTable("Ai_Token")]
public class TokenAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public TokenAggregateRoot()
{
}
public TokenAggregateRoot(Guid userId)
{
this.UserId = userId;
this.Token = GenerateToken();
}
public string Token { get; set; }
public Guid UserId { get; set; }
/// <summary>
/// 重置token
/// </summary>
public void ResetToken()
{
this.Token = GenerateToken();
}
private string GenerateToken(int length = 36)
{
// 定义可能的字符集:大写字母、小写字母和数字
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 创建随机数生成器
Random random = new Random();
// 使用StringBuilder高效构建字符串
StringBuilder sb = new StringBuilder(length);
// 生成指定长度的随机字符串
for (int i = 0; i < length; i++)
{
// 从字符集中随机选择一个字符
int index = random.Next(chars.Length);
sb.Append(chars[index]);
}
return "yi-" + sb.ToString();
}
}

View File

@@ -1,62 +0,0 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 用量统计
/// </summary>
[SugarTable("Ai_UsageStatistics")]
public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public UsageStatisticsAggregateRoot()
{
}
public UsageStatisticsAggregateRoot(Guid userId, string modelId)
{
UserId = userId;
ModelId = modelId;
}
/// <summary>
/// 用户id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 哪个模型
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 对话次数
/// </summary>
public int UsageTotalNumber { get; set; }
/// <summary>
/// 使用输出token总数
/// </summary>
public int UsageOutputTokenCount { get; set; }
/// <summary>
/// 使用输入总数
/// </summary>
public int UsageInputTokenCount { get; set; }
/// <summary>
/// 总token使用数量
/// </summary>
public int TotalTokenCount { get; set; }
/// <summary>
/// 新增一次聊天统计
/// </summary>
public void AddOnceChat(int inputTokenCount, int outputTokenCount)
{
UsageTotalNumber += 1;
UsageOutputTokenCount += outputTokenCount;
UsageInputTokenCount += inputTokenCount;
TotalTokenCount += (outputTokenCount + inputTokenCount);
}
}

View File

@@ -1,10 +0,0 @@
namespace Yi.Framework.AiHub.Domain.Entities.ValueObjects;
public class TokenUsageValueObject
{
public int OutputTokenCount { get; set; }
public int InputTokenCount { get; set; }
public long TotalTokenCount { get; set; }
}

View File

@@ -1,21 +0,0 @@
using System.Reflection;
using OpenAI.Chat;
namespace Yi.Framework.AiHub.Domain.Extensions;
public static class ChatMessageExtensions
{
public static string GetRoleAsString(this ChatMessage message)
{
var type = message.GetType();
var propertyInfo = type.GetProperty("Role", BindingFlags.NonPublic | BindingFlags.Instance);
if (propertyInfo != null)
{
var value = propertyInfo.GetValue(message) as ChatMessageRole?;
return value.ToString().ToLower();
}
return string.Empty;
}
}

View File

@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Http;
using Volo.Abp.Users;
namespace Yi.Framework.AiHub.Domain.Extensions;
public static class CurrentExtensions
{
public static bool IsAiVip(this ICurrentUser currentUser)
{
return currentUser.Roles.Contains("YiXinAi-Vip") || currentUser.UserName == "cc";
}
}

View File

@@ -1,19 +0,0 @@
namespace Yi.Framework.AiHub.Domain.Extensions;
public static class TimeExtensions
{
public static long ToUnixTimeSeconds(this DateTime dateTime)
{
return new DateTimeOffset(dateTime).ToUnixTimeSeconds();
}
public static long ToUnixTimeMilliseconds(this DateTime dateTime)
{
return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
}
public static DateTime FromUnixTimeSeconds(this long seconds)
{
return DateTimeOffset.FromUnixTimeSeconds(seconds).DateTime;
}
}

View File

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

View File

@@ -1,278 +0,0 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
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.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class AiGateWayManager : DomainService
{
private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository;
private readonly ILogger<AiGateWayManager> _logger;
private readonly AiMessageManager _aiMessageManager;
private readonly UsageStatisticsManager _usageStatisticsManager;
private readonly ISpecialCompatible _specialCompatible;
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
ISpecialCompatible specialCompatible)
{
_aiAppRepository = aiAppRepository;
_logger = logger;
_aiMessageManager = aiMessageManager;
_usageStatisticsManager = usageStatisticsManager;
_specialCompatible = specialCompatible;
}
/// <summary>
/// 获取模型
/// </summary>
/// <param name="modelId"></param>
/// <returns></returns>
private async Task<AiModelDescribe> GetModelAsync(string modelId)
{
var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync();
foreach (var app in allApp)
{
var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId);
if (model is not null)
{
return new AiModelDescribe
{
AppId = app.Id,
AppName = app.Name,
Endpoint = app.Endpoint,
ApiKey = app.ApiKey,
OrderNum = model.OrderNum,
HandlerName = model.HandlerName,
ModelId = model.ModelId,
ModelName = model.Name,
Description = model.Description,
AppExtraUrl = app.ExtraUrl,
ModelExtraInfo = model.ExtraInfo
};
}
}
throw new UserFriendlyException($"{modelId}模型当前版本不支持");
}
/// <summary>
/// 聊天完成-流式
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(
ThorChatCompletionsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
_specialCompatible.Compatible(request);
var modelDescribe = await GetModelAsync(request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken))
{
yield return result;
}
}
/// <summary>
/// 聊天完成-非流式
/// </summary>
/// <param name="httpContext"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task CompleteChatForStatisticsAsync(HttpContext httpContext,
ThorChatCompletionsRequest request,
Guid? userId = null,
Guid? sessionId = null,
CancellationToken cancellationToken = default)
{
_specialCompatible.Compatible(request);
var response = httpContext.Response;
// 设置响应头,声明是 json
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 chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken);
if (userId is not null)
{
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = request.Messages.LastOrDefault().Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = data.Usage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = data.Choices.FirstOrDefault()?.Delta.Content,
ModelId = request.Model,
TokenUsage = data.Usage
});
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage.InputTokens ?? 0,
data.Usage.OutputTokens ?? 0);
}
var body = JsonConvert.SerializeObject(data, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
await writer.WriteLineAsync(body);
await writer.FlushAsync(cancellationToken);
}
/// <summary>
/// 聊天完成-缓存处理
/// </summary>
/// <param name="httpContext"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task CompleteChatStreamForStatisticsAsync(
HttpContext httpContext,
ThorChatCompletionsRequest request,
Guid? userId = null,
Guid? sessionId = null,
CancellationToken cancellationToken = default)
{
var response = httpContext.Response;
// 设置响应头,声明是 SSE 流
response.ContentType = "text/event-stream";
response.Headers.Append("Cache-Control", "no-cache");
response.Headers.Append("Connection", "keep-alive");
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken);
var tokenUsage = new ThorUsageResponse();
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
//缓存队列算法
// 创建一个队列来缓存消息
var messageQueue = new ConcurrentQueue<string>();
StringBuilder backupSystemContent = new StringBuilder();
// 设置输出速率例如每50毫秒输出一次
var outputInterval = TimeSpan.FromMilliseconds(75);
// 标记是否完成接收
var isComplete = false;
// 启动一个后台任务来消费队列
var outputTask = Task.Run(async () =>
{
while (!(isComplete && messageQueue.IsEmpty))
{
if (messageQueue.TryDequeue(out var message))
{
await writer.WriteLineAsync(message);
await writer.FlushAsync(cancellationToken);
}
if (!isComplete)
{
// 如果没有完成,才等待,已完成,全部输出
await Task.Delay(outputInterval, cancellationToken);
}
}
}, cancellationToken);
//IAsyncEnumerable 只能在最外层捕获异常(如果你有其他办法的话...
try
{
await foreach (var data in completeChatResponse)
{
if (data.Usage is not null && data.Usage.TotalTokens is not null)
{
tokenUsage = data.Usage;
}
var message = JsonConvert.SerializeObject(data, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content);
// 将消息加入队列而不是直接写入
messageQueue.Enqueue($"data: {message}\n");
}
}
catch (Exception e)
{
_logger.LogError(e, $"Ai对话异常");
var errorContent = $"Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}";
var model = new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
{
new ThorChatChoiceResponse()
{
Delta = new ThorChatMessage()
{
Content = errorContent
}
}
}
};
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
backupSystemContent.Append(errorContent);
messageQueue.Enqueue($"data: {message}\n");
}
//断开连接
messageQueue.Enqueue("data: [DONE]\n");
// 标记完成并发送结束标记
isComplete = true;
await outputTask;
if (userId is not null)
{
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = request.Messages.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = tokenUsage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = backupSystemContent.ToString(),
ModelId = request.Model,
TokenUsage = tokenUsage
});
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, tokenUsage.InputTokens ?? 0,
tokenUsage.OutputTokens ?? 0);
}
}
}

View File

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

View File

@@ -1,64 +0,0 @@
using Volo.Abp.Domain.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class TokenManager : DomainService
{
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
public TokenManager(ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
{
_tokenRepository = tokenRepository;
}
public async Task<string?> GetAsync(Guid userId)
{
var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId);
if (entity is not null)
{
return entity.Token;
}
else
{
return null;
}
}
public async Task CreateAsync(Guid userId)
{
var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId);
if (entity is not null)
{
entity.ResetToken();
await _tokenRepository.UpdateAsync(entity);
}
else
{
var token = new TokenAggregateRoot(userId);
await _tokenRepository.InsertAsync(token);
}
}
public async Task<Guid> GetUserIdAsync(string? token)
{
if (token is null)
{
throw new UserFriendlyException("当前请求未包含token", "401");
}
if (token.StartsWith("yi-"))
{
var entity = await _tokenRepository._DbQueryable.Where(x => x.Token == token).FirstAsync();
if (entity is null)
{
throw new UserFriendlyException("当前请求token无效", "401");
}
return entity.UserId;
}
throw new UserFriendlyException("当前请求token非法", "401");
}
}

View File

@@ -1,44 +0,0 @@
using Medallion.Threading;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class UsageStatisticsManager : DomainService
{
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _repository;
public UsageStatisticsManager(ISqlSugarRepository<UsageStatisticsAggregateRoot> repository)
{
_repository = repository;
}
private IDistributedLockProvider DistributedLock =>
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
public async Task SetUsageAsync(Guid userId, string modelId, int inputTokenCount, int outputTokenCount)
{
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId.ToString()}"))
{
var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId);
//存在数据,更细
if (entity is not null)
{
entity.AddOnceChat(inputTokenCount, outputTokenCount);
await _repository.UpdateAsync(entity);
}
//不存在插入
else
{
var usage = new UsageStatisticsAggregateRoot(userId, modelId);
usage.AddOnceChat(inputTokenCount, outputTokenCount);
await _repository.InsertAsync(usage);
}
}
}
}
internal class LazyServiceProvider
{
}

View File

@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.DistributedLocking" Version="$(AbpVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\framework\Yi.Framework.Mapster\Yi.Framework.Mapster.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" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More