Compare commits

...

200 Commits

Author SHA1 Message Date
chenchun
3a60bcc174 refactor: 优化交易状态枚举处理方式
- 为TradeStatusEnum枚举添加Description特性标注
- 重构GetTradeStatusDescription方法,使用反射获取Description特性值
- 简化ParseTradeStatus方法,使用Enum.TryParse替代switch表达式
- 提高代码可维护性,避免硬编码状态描述
2025-08-13 18:30:56 +08:00
chenchun
2b3fad16fd feat: 优化支付宝回调通知记录功能
- 新增SignStr字段记录支付宝回调的原始签名字符串
- 修改日志记录格式,使用键值对形式记录回调通知数据
- 更新PayManager.RecordPayNoticeAsync方法支持记录原始签名字符串
- 移除AlipayManager中冗余的注释说明
2025-08-13 18:21:05 +08:00
chenchun
f0cf6bf5c8 fix: 修复支付宝支付功能相关问题
- 修复支付接口参数顺序错误,调整商品名称和订单号参数位置
- 修复支付页面HTML返回格式,直接返回Body内容而非序列化字符串
- 添加支付相关接口的权限控制,支付回调接口允许匿名访问
- 优化支付宝回调验签逻辑,保持原始参数顺序避免验签失败
- 增加回调格式错误的异常处理
- 修复商品类型枚举显示名称为英文,新增测试商品类型
- 修正Token服务提示文案中的错别字
- 移除订单更新时不必要的时间字段设置
2025-08-13 17:42:13 +08:00
chenchun
0ba4e3240b feat: 完成支付宝接入 2025-08-13 12:07:35 +08:00
ccnetcore
9332b17fc1 feat: 集成支付宝支付SDK并添加当面付测试调用,更新CORS配置支持capacitor 2025-08-13 08:26:45 +08:00
ccnetcore
4ec4023f40 feat: 增加EmbeddingResponse的object字段并完善AiGateWayManager的Usage统计,更新CORS配置 2025-08-11 20:24:48 +08:00
chenchun
d9971541f2 feat: 支持字符串类型的embedding输入参数
在AiGateWayManager中新增对JsonElement字符串类型的处理,确保embedding请求能够正确处理单个字符串输入参数。
2025-08-11 18:10:11 +08:00
chenchun
7b0e4fcc73 fix: 修复Embedding输入处理逻辑和字段可空性
- 优化Embedding输入类型判断逻辑,支持string和JsonElement数组类型
- 将EncodingFormat字段设置为可空类型,提高兼容性
- 注释知识库场景下的消息统计功能,避免不必要的数据记录
2025-08-11 18:05:33 +08:00
chenchun
cfde73d13a fix: 修复输出为空问题 2025-08-11 16:53:33 +08:00
chenchun
c17c9000a8 refactor: 移除AiHub Domain层对Application.Contracts的循环依赖
移除Yi.Framework.AiHub.Domain项目中对Yi.Framework.AiHub.Application.Contracts的项目引用,解决领域层和应用层之间的循环依赖问题,符合DDD架构分层原则。
2025-08-11 15:51:59 +08:00
chenchun
42d537a68b style: 调整架构引用 2025-08-11 15:31:11 +08:00
chenchun
25eebec8f7 feat: 新增向量嵌入服务支持
新增SiliconFlow向量嵌入服务实现,支持文本向量化功能:
- 新增ITextEmbeddingService接口和SiliconFlowTextEmbeddingService实现
- 新增EmbeddingCreateRequest/Response等向量相关DTO
- 在AiGateWayManager中新增EmbeddingForStatisticsAsync方法
- 在OpenApiService中新增向量生成API接口
- 扩展ModelTypeEnum枚举支持Embedding类型
- 优化ThorChatMessage的Content属性处理逻辑
2025-08-11 15:29:24 +08:00
Gsh
bbe5b01872 fix: 优化token图表,增加全屏显示 2025-08-10 15:34:53 +08:00
ccnetcore
6b31536de5 fix: 修复用户过期判断逻辑,按日期比较避免当天误判 2025-08-10 12:07:09 +08:00
ccnetcore
2e5db5500f Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-08-10 11:53:52 +08:00
ccnetcore
7038d31c53 feat: 新增VIP充值接口并支持通过角色代码为用户分配角色 2025-08-10 11:53:28 +08:00
Gsh
3eb27c3d35 fix: 增加对话token显示,token消耗统计 2025-08-10 00:56:44 +08:00
ccnetcore
a9c3a1bcec fix: 修复统计中 Token 数量计算错误,将计数改为求和 2025-08-09 23:38:56 +08:00
ccnetcore
384926e73a feat: 新增用户数据导出功能 2025-08-09 22:55:26 +08:00
ccnetcore
4335c12659 chore: 注释掉生成新闻和股票价格的异步调用 2025-08-09 13:58:26 +08:00
ccnetcore
e6e4829164 feat: 新增VIP过期自动卸载功能
- 新增`AiRechargeManager`类,实现VIP过期用户的自动卸载逻辑。
- 新增`AiHubConst`常量类,统一管理角色名称。
- 在`IRoleService`中添加`RemoveUserRoleByRoleCodeAsync`方法,用于移除指定用户的角色。
- 在`RoleManager`中实现`RemoveUserRoleByRoleCodeAsync`方法。
- 优化`CurrentExtensions`中VIP角色判断逻辑,使用常量替代硬编码。
- 调整`YiAbpWebModule`中部分代码格式,提升可读性。
2025-08-09 13:14:15 +08:00
ccnetcore
f3c67cf598 fix: 修复统计数量偶发问题 2025-08-09 12:20:28 +08:00
ccnetcore
4681d468ce style: 优化验证码样式 2025-08-05 22:41:20 +08:00
chenchun
63e7d3d5f5 style: 更新主题2.2 2025-08-05 18:23:33 +08:00
chenchun
f47d8c8ce3 style: 优化2.1样式 2025-08-05 17:19:03 +08:00
chenchun
6f69f45ddc Merge branch 'bbs-sharpdance' into ai-hub 2025-08-05 14:11:16 +08:00
chenchun
e73678c788 style: 全部样式更新2.0 2025-08-05 14:09:39 +08:00
ccnetcore
09a2f91cbf style: 优化样式1.1 2025-08-04 23:55:48 +08:00
ccnetcore
29da7499a4 Merge branch 'bbs-sharpdance' into ai-hub 2025-08-04 23:37:11 +08:00
ccnetcore
5b024e9443 style: 重写ele 2025-08-04 23:34:13 +08:00
ccnetcore
225932eff1 style: 上线全局样式 2025-08-04 23:29:25 +08:00
Gsh
65d5f5ae86 fix: 加载优化、vip状态优化、apikey优化 2025-08-04 23:11:42 +08:00
ccnetcore
3e647ef14d style: 全局修改样式主题 2025-08-04 22:35:45 +08:00
chenchun
7cb3aea2e6 style: 调整样式 2025-08-04 18:27:18 +08:00
chenchun
7f4b8f1c8a feat: 添加暗色主题支持
- 在HTML根元素添加dark类名以启用暗色模式
- 引入Element Plus暗色主题CSS变量文件
- 格式化代码缩进和结构,提升代码可读性
2025-08-04 17:07:01 +08:00
ccnetcore
0a2710b865 feat: 支持图片生成 2025-08-04 01:03:47 +08:00
ccnetcore
2a301c4983 feat: 支持图片生成 2025-08-03 23:23:32 +08:00
Gsh
faa8131a1b fix: 未登录对话id逻辑玩优化 2025-08-03 21:56:51 +08:00
ccnetcore
71bd885bd0 fix: 支持router参数 2025-08-03 21:47:22 +08:00
ccnetcore
691a1e50f0 feat: 支持未登录用户统计 2025-08-03 21:32:54 +08:00
ccnetcore
ef6e9fd16d style: 优化提示词 2025-08-02 22:04:22 +08:00
chenchun
17f9ac6d54 style: 优化防抖样式 2025-08-01 17:58:07 +08:00
chenchun
3f8e6e48c0 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 14:39:09 +08:00
chenchun
bda4fdf69d feat: 兼容代码补全功能 2025-07-28 14:39:02 +08:00
Gsh
5c85ed13fd fix: 加载进度优化与登录弹窗优化 2025-07-28 13:43:46 +08:00
chenchun
1986901031 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 13:15:49 +08:00
chenchun
e1d3ec21e5 feat: 支持错误处理 2025-07-28 13:15:42 +08:00
Gsh
f45283dade Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 12:59:42 +08:00
Gsh
31c44d8df7 fix: 登录弹窗超时功能取消 2025-07-28 12:59:07 +08:00
chenchun
bf443963c8 fix: 修复ThorChatCompletionsRequest中Messages属性的可空类型问题 2025-07-28 12:50:48 +08:00
chenchun
a0eb234539 feat: 兼容了用量使用显示 2025-07-22 10:40:23 +08:00
ccnetcore
b6d670c240 perf: 兼容deepseek格式 2025-07-21 22:03:55 +08:00
ccnetcore
b5fb2c42c6 feat: 兼容deepseek协议 2025-07-21 21:57:14 +08:00
ccnetcore
d72cc529ba perf: 优化流式输出 2025-07-21 21:15:02 +08:00
Gsh
660bd00cae fix: apikey加载状态 2025-07-20 22:12:48 +08:00
Gsh
b5489711ec fix: 加载优化 2025-07-20 21:01:41 +08:00
Gsh
76717c4f8a Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-20 17:23:33 +08:00
ccnetcore
3d53d0bcd6 style: 完成进度条加载 2025-07-20 17:14:05 +08:00
ccnetcore
c7c9428b68 style: 完成进度条加载 2025-07-20 17:01:17 +08:00
ccnetcore
991a970d6a style: 完成进度条加载 2025-07-20 16:40:54 +08:00
ccnetcore
cbe93b9f7e style: 完成进度条加载 2025-07-20 15:15:05 +08:00
ccnetcore
5d7217b775 feat: 完成支持functioncall功能 2025-07-18 23:12:20 +08:00
ccnetcore
d6836b8bcf feat: 提交cicd产物 2025-07-18 21:08:51 +08:00
ccnetcore
c367651c78 chorm: 构建 2025-07-18 21:00:23 +08:00
ccnetcore
77123fd971 cicd: 提交流水线 2025-07-18 21:00:06 +08:00
ccnetcore
9d73a6837b Merge branch 'ai-hub' into ai-hub-dev 2025-07-18 20:46:36 +08:00
ccnetcore
651f0157dc feat: 完成兼容处理 2025-07-18 20:46:30 +08:00
Gsh
90e8dbe449 fix: 对话参数修改 2025-07-18 20:33:51 +08:00
ccnetcore
ccba2667bc Merge branch 'ai-hub' into ai-hub-dev 2025-07-18 19:51:15 +08:00
Gsh
3ce9fc9790 fix: 对话参数修改 2025-07-18 19:49:11 +08:00
ccnetcore
2f24dd77bf feat: 完成对接 2025-07-18 00:27:59 +08:00
ccnetcore
2bc07cb3df feat: 完成错误信息展示 2025-07-18 00:14:19 +08:00
ccnetcore
30678dbbb4 feat: 完成功能 2025-07-17 23:52:00 +08:00
ccnetcore
c5b0f69b51 feat: 重构完成 2025-07-17 23:16:16 +08:00
ccnetcore
e593f2cba4 feat: Thor搭建 2025-07-17 23:10:26 +08:00
ccnetcore
10f7499066 feat: 完成cicd搭建 2025-07-16 23:34:52 +08:00
Gsh
36b7e495f7 update: md渲染优化与依赖更新(0715 02:07) 2025-07-16 00:12:00 +08:00
Gsh
94b96e3c19 fix: 更新md 2025-07-15 16:42:47 +08:00
Gsh
0d1ee18da0 fix: 增加重新登录意社区 2025-07-15 00:54:34 +08:00
Gsh
cab0b61ee0 fix: 对话md渲染优化 2025-07-14 22:21:24 +08:00
Gsh
8e6611d76d Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-14 22:20:55 +08:00
Gsh
1ef82e5f93 fix: 对话md渲染优化 2025-07-14 22:20:32 +08:00
ccnetcore
43dc962606 feat: 支持邮箱注册功能 2025-07-13 21:26:46 +08:00
ccnetcore
020d674ca2 style: 调整header 2025-07-12 18:42:26 +08:00
ccnetcore
bb0e1081cc style: 调整header 2025-07-12 18:41:26 +08:00
Gsh
5162f9ce3b fix: 对话创建防抖 2025-07-12 00:36:11 +08:00
ccnetcore
57fae7fe4b fix: 知识库访问 2025-07-09 23:26:32 +08:00
ccnetcore
17412d7de7 feat: 新增支持Prompt 2025-07-09 23:11:57 +08:00
ccnetcore
d59f40dfba Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-09 22:44:36 +08:00
ccnetcore
5953be63cb feat: 兼容cline 2025-07-09 22:44:24 +08:00
Gsh
58a4311947 fix: 更新消息暂停 2025-07-09 22:31:52 +08:00
ccnetcore
c5a9b9a15f feat: 支持非流式传输 2025-07-09 21:52:00 +08:00
chenchun
716c344780 feat: 支持非流式传输功能 2025-07-09 19:12:53 +08:00
Gsh
9af8c4897b fix: 增加消息复制、消息时间 2025-07-09 00:08:30 +08:00
Gsh
ca72024a68 feat: 增加用户充值记录查询 2025-07-08 22:59:24 +08:00
chenchun
0d2bc585a9 feat: 提高模型输出速度 2025-07-08 18:24:21 +08:00
chenchun
4e3edefb35 feat: 整体调节 2025-07-08 15:47:51 +08:00
Gsh
9408242726 fix: 禁止移动端缩放、对话头像更改 2025-07-08 00:29:41 +08:00
Gsh
4710208e81 fix: 隐藏文件上传按钮,去除不必要的log打印 2025-07-07 23:29:39 +08:00
Gsh
4fc6a1e818 fix: 隐藏文件上传按钮,去除不必要的log打印 2025-07-07 23:27:55 +08:00
ccnetcore
c9b79a074b style: 增加markdown样式优化 2025-07-07 22:34:19 +08:00
Gsh
2e79eb346f fix: 添加微信群二维码 2025-07-07 21:47:21 +08:00
Gsh
3f88bd4158 fix:增加md样式重写文件 2025-07-07 21:21:55 +08:00
Gsh
f58e079741 fix:增加md样式文件 2025-07-07 21:12:35 +08:00
Gsh
6f1eb1f4b9 feat: 新增markdown渲染 2025-07-07 21:01:59 +08:00
ccnetcore
826d529997 fix: 修复接口名称 2025-07-05 17:47:44 +08:00
ccnetcore
43e60eab4a feat: 完成充值记录 2025-07-05 17:43:48 +08:00
Gsh
6c33024790 fix:百度seo添加与对话错误处理 2025-07-05 17:25:14 +08:00
Gsh
d27e625fde fix:前端模型主键换位modelId 2025-07-05 15:59:22 +08:00
Gsh
23cecb9360 fix:401、403错误提示,对话角色更改assistant,模型选择持久化 2025-07-05 15:49:29 +08:00
ccnetcore
7e4c835ced fix: 修复nugetapi 2025-07-05 15:31:18 +08:00
ccnetcore
aff460f555 feat: 升级yi.abp.tool 2025-07-05 15:23:08 +08:00
ccnetcore
52961b459e feat: 优化整体aihub架构 2025-07-05 15:11:56 +08:00
ccnetcore
0af2f867fc feat: 提交构建 2025-07-05 01:06:19 +08:00
chenchun
85e291e0b8 chorm: 修改构建域名 2025-07-04 19:17:45 +08:00
chenchun
6d8a859b20 feat: 关闭前端动画 2025-07-04 19:13:21 +08:00
ccnetcore
a70dfb0769 feat: 完成跨域处理 2025-07-04 00:16:58 +08:00
Gsh
c637d412e6 fix:增加用户中心,完成Apikey功能页,增加角色工具方法 2025-07-04 00:12:26 +08:00
ccnetcore
e996bc2d7f feat: 完成token模块 2025-07-03 22:44:52 +08:00
ccnetcore
15be047371 feat: 完成openapi改造 2025-07-03 22:31:39 +08:00
ccnetcore
0a0e0bca10 feat: 完成上下文功能 2025-07-03 21:28:40 +08:00
Gsh
9a8f3bd161 fix:增加seo优化 2025-07-03 17:13:21 +08:00
ccnetcore
7e2c035692 feat: 完成api接口搭建 2025-07-02 23:30:29 +08:00
ccnetcore
44b2ade9bc feat: 完成错误信息输出 2025-07-02 00:28:44 +08:00
Gsh
1200d02fbf fix:对话时只提供最近6条记录 2025-07-02 00:11:43 +08:00
chenchun
b020f48325 style: 调整配置jwt文件 2025-07-01 16:41:58 +08:00
chenchun
917857f1ff feat: 修改超时,改成10分钟 2025-07-01 16:11:41 +08:00
ccnetcore
69d8ff1034 fix: 修改deepseek校验 2025-06-30 22:23:37 +08:00
ccnetcore
9a334101ca fix: 修复ai模型问题 2025-06-30 21:58:34 +08:00
ccnetcore
ee53b3d9c4 feat: 完成细节调整 2025-06-30 21:08:32 +08:00
Gsh
01a5ad5302 fix:模型选择限制 2025-06-30 17:53:59 +08:00
Gsh
f12f0e1f84 fix:双token更新 2025-06-30 16:59:20 +08:00
Gsh
6aefcdbed8 fix:登录判断优化 2025-06-30 16:02:39 +08:00
ccnetcore
3d22a2ef65 feat: 完成支持鉴权刷新功能 2025-06-29 19:34:09 +08:00
Gsh
a33c6dbf1a fix: 增加用户角色标识与优化产品页 2025-06-29 17:47:07 +08:00
ccnetcore
2b7c779e14 style: 新增样式 2025-06-29 16:36:34 +08:00
ccnetcore
0e36f7c0b3 feat: 完成登录校验拦截 2025-06-29 15:41:49 +08:00
ccnetcore
228a309545 Revert "fix: 修复token样式"
This reverts commit b15ad8eb5e.
2025-06-29 15:22:57 +08:00
ccnetcore
b15ad8eb5e fix: 修复token样式 2025-06-29 15:22:04 +08:00
ccnetcore
a525735b0b Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-06-29 15:18:45 +08:00
ccnetcore
6a58af8dfb feat: 完成双token刷新 2025-06-29 15:18:30 +08:00
Gsh
0089e63832 feat: 产品订阅页面优化 2025-06-29 14:42:10 +08:00
ccnetcore
d4f00eb89f style: 修改Ai_Session表类型 2025-06-29 12:21:28 +08:00
Gsh
d15e6e395b fix: 修复拦截器报错 2025-06-29 12:09:34 +08:00
Gsh
39eb4bef07 fix: bbs与ai存储refreshToken 2025-06-29 00:57:57 +08:00
ccnetcore
03de576d8c fix: 修复值对象报错问题 2025-06-28 23:39:15 +08:00
ccnetcore
216b57a4c7 feat: 更新hook fetch 库 2025-06-28 23:07:32 +08:00
Gsh
5383d2d40e fix: 前端请求头增加浏览器指纹 2025-06-28 18:44:10 +08:00
Gsh
1d7a2013e3 fix: 单点登录优化与环境变量完善 2025-06-28 18:14:12 +08:00
Gsh
24d2908cca update: 修复样式规则报错 2025-06-28 14:33:13 +08:00
ccnetcore
330845a387 style: 调整下拉框样式 2025-06-27 22:50:51 +08:00
ccnetcore
bbedd01a72 fix: 兼容claude ai 2025-06-27 22:49:08 +08:00
ccnetcore
2b07061c18 feat: 完成个个模型ai统计 2025-06-27 22:21:44 +08:00
ccnetcore
01a3c81359 feat: 完成用量统计功能模块 2025-06-27 22:13:26 +08:00
Gsh
96e275efa6 fix: 单点登录优化 2025-06-27 14:23:06 +08:00
chenchun
12eb6c73c3 feat: 完成接入claude 2025-06-26 17:54:52 +08:00
ccnetcore
4166eddd28 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-06-26 00:38:53 +08:00
ccnetcore
6ea1592c19 style: 调整标题样式 2025-06-26 00:38:36 +08:00
Gsh
f8799a073c Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-06-26 00:37:04 +08:00
Gsh
0eb83fc930 fix: 产品页面完善,增加空白布局与布局切换 2025-06-26 00:35:13 +08:00
ccnetcore
a5dd3946f8 fix: 修复模型接口错误 2025-06-25 23:15:31 +08:00
ccnetcore
2732df24af fix: 修复模型接口错误 2025-06-25 23:05:20 +08:00
ccnetcore
f0ae27a50b fix: 修复模型接口错误 2025-06-25 22:45:57 +08:00
ccnetcore
c5037ea397 feat: 完成ai网关改造 2025-06-25 22:41:32 +08:00
chenchun
695aaedfba feat: 完成ai-hub第一期功能 2025-06-25 17:12:09 +08:00
ccnetcore
4f71d874bd style: 调整速度 2025-06-25 00:35:25 +08:00
ccnetcore
c69729fadd feat: 提交队列 2025-06-25 00:30:01 +08:00
ccnetcore
64d04996af perf: 优化sse流式传输 2025-06-25 00:23:00 +08:00
ccnetcore
8eea510583 feat: ai完成接入deepseek 2025-06-25 00:05:00 +08:00
Gsh
04c2b246f6 fix: 放开产品页面 2025-06-23 23:36:59 +08:00
ccnetcore
a46eb176d7 feat: 还原 2025-06-23 23:04:22 +08:00
ccnetcore
2bea88f1a3 feat: 新增pro打包 2025-06-23 23:03:54 +08:00
ccnetcore
06617de984 feat: 完成对接接口 2025-06-22 19:09:13 +08:00
Gsh
6459d7c024 fix: ai-hub接口替换 2025-06-21 22:12:21 +08:00
Gsh
bd4af8039f fix: 用户信息接口替换 2025-06-21 21:57:07 +08:00
Gsh
8aaa22cea3 feat: ai-hub与bbs单点登录联通 2025-06-21 21:52:44 +08:00
ccnetcore
7d902682f8 feat: 完成账户信息转发 2025-06-21 21:40:51 +08:00
ccnetcore
a81be99100 style: 调整前端样式 2025-06-21 13:34:56 +08:00
ccnetcore
b6dfe93d2c Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-06-21 13:30:21 +08:00
ccnetcore
35aa022984 fix: 优化用户更新,超管问题 2025-06-21 13:30:12 +08:00
ccnetcore
dfe2d4cc37 fix: 优化用户更新,超管问题 2025-06-21 13:29:41 +08:00
ccnetcore
1d16502d32 feat: 完成dto搭建 2025-06-21 13:20:13 +08:00
ccnetcore
25c88187a3 feat: 改造接口 2025-06-21 13:15:14 +08:00
ccnetcore
ac04e846fa feat: 完成ai message、session搭建 2025-06-21 13:02:38 +08:00
ccnetcore
29985e2118 feat: 完成ai网关搭建 2025-06-21 01:41:05 +08:00
ccnetcore
3b74dfd49a feat:完成ai网关搭建 2025-06-21 01:08:14 +08:00
cc
6abcc49ed4 feat: 提交 2025-06-20 18:06:33 +08:00
Gsh
f16e1cd7a6 fix: 关闭打包检查 2025-06-20 01:19:15 +08:00
ccnetcore
4341b8a24b style: 设置前端logo样式 2025-06-20 00:06:10 +08:00
Gsh
a89e11d132 feat: 前端接口代理 2025-06-19 23:45:22 +08:00
ccnetcore
bc91a8cff2 feat: 新增取消功能 2025-06-19 22:24:21 +08:00
ccnetcore
8040010b98 feat: 完成ai接口 2025-06-19 21:24:13 +08:00
ccnetcore
b39f15c798 feat: 新增文件夹 2025-06-19 19:13:43 +08:00
ccnetcore
c3cf49c63e feat: 完成节点改造 2025-06-19 14:17:24 +08:00
ccnetcore
899bd7e316 feat: 完成cicd流水线 2025-06-19 01:02:08 +08:00
Gsh
890727d495 feat: 产品页面搭建 2025-06-18 23:28:27 +08:00
ccnetcore
8a8e69596a feat: 完成ai改造 2025-06-17 23:38:20 +08:00
ccnetcore
58fcc92e4d feat: 完成AzureOpenAI改造 2025-06-17 23:25:55 +08:00
Gsh
0cd795f57a feat: 前端搭建 2025-06-17 22:37:37 +08:00
ccnetcore
4830be6388 feat: 搭建ai 2025-06-16 22:39:09 +08:00
1502 changed files with 36381 additions and 1185 deletions

View File

@@ -186,6 +186,18 @@ 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
@@ -468,6 +480,26 @@ 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
@@ -551,6 +583,12 @@ 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

@@ -0,0 +1,64 @@
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,17 +1,12 @@
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,21 +1,10 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Authentication;
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 Volo.Abp.DependencyInjection;
using Volo.Abp.Modularity;
using Yi.Framework.AspNetCore.Mvc;
using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Authentication;
using Yi.Framework.Core;
using Yi.Framework.Core.Authentication;
namespace Yi.Framework.AspNetCore
{
@@ -35,8 +24,14 @@ 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,6 +2,7 @@
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;
@@ -32,13 +33,19 @@ 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");
@@ -56,11 +63,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

@@ -0,0 +1,18 @@
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

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

View File

@@ -0,0 +1,11 @@
<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

@@ -0,0 +1,15 @@
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

@@ -58,5 +58,10 @@ namespace Yi.Framework.SqlSugarCore.Abstractions
/// 是否启用SaaS多租户
/// </summary>
public bool EnabledSaasMultiTenancy { get; set; } = false;
/// <summary>
/// 并发乐观锁异常,否则不处理
/// </summary>
public bool EnabledConcurrencyException { get; set; } = true;
}
}

View File

@@ -2,11 +2,13 @@
using System.Linq.Expressions;
using System.Text;
using Microsoft.Extensions.Logging;
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;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
@@ -15,14 +17,18 @@ 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()
{
public ISqlSugarClient _Db => AsyncContext.Run(async () => await GetDbContextAsync());
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>
@@ -58,22 +64,26 @@ 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);
}
@@ -103,60 +113,71 @@ 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();
@@ -171,7 +192,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);
@@ -211,9 +232,11 @@ 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);
@@ -230,7 +253,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObj);
}
}
public virtual async Task<bool> DeleteAsync(List<TEntity> deleteObjs)
@@ -250,13 +272,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)
@@ -264,6 +286,11 @@ namespace Yi.Framework.SqlSugarCore.Repositories
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
var entity = await GetByIdAsync(id);
if (entity is null)
{
return false;
}
//反射赋值
ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, entity);
return await UpdateAsync(entity);
@@ -284,6 +311,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return false;
}
//反射赋值
entities.ForEach(e => ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e));
return await UpdateRangeAsync(entities);
@@ -292,7 +320,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories
{
return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(ids);
}
}
public virtual async Task<TEntity> GetByIdAsync(dynamic id)
@@ -301,7 +328,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories
}
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await (await GetDbSimpleClientAsync()).GetFirstAsync(whereExpression);
@@ -317,14 +343,19 @@ 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 GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
new PageModel() { PageIndex = pageNum, PageSize = 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 GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType);
}
public virtual async Task<TEntity> GetSingleAsync(Expression<Func<TEntity, bool>> whereExpression)
@@ -379,30 +410,40 @@ namespace Yi.Framework.SqlSugarCore.Repositories
public virtual async Task<bool> UpdateAsync(TEntity updateObj)
{
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>())//带版本号乐观锁更新
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>()) //带版本号乐观锁更新
{
try
if (Options is not null && Options.EnabledConcurrencyException)
{
int num = await (await GetDbSimpleClientAsync())
.Context.Updateable(updateObj).ExecuteCommandWithOptLockAsync(true);
return num>0;
try
{
int num = await (await GetDbSimpleClientAsync())
.Context.Updateable(updateObj).ExecuteCommandWithOptLockAsync(true);
return num > 0;
}
catch (VersionExceptions ex)
{
throw new AbpDbConcurrencyException(
$"{ex.Message}[更新失败ConcurrencyStamp不是最新版本],entityInfo{updateObj}", ex);
}
}
catch (VersionExceptions ex)
else
{
throw new AbpDbConcurrencyException($"{ex.Message}[更新失败ConcurrencyStamp不是最新版本],entityInfo{updateObj}", ex);
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);
@@ -411,30 +452,36 @@ 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> sugarDbContextProvider) : base(sugarDbContextProvider)
public SqlSugarRepository(ISugarDbContextProvider<ISqlSugarDbContext> dbContextProvider) : base(
dbContextProvider)
{
}
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

@@ -0,0 +1,13 @@
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

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

View File

@@ -0,0 +1,65 @@
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

@@ -0,0 +1,14 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 创建订单输入DTO
/// </summary>
public class CreateOrderInput
{
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 创建订单输出DTO
/// </summary>
public class CreateOrderOutput
{
/// <summary>
/// 订单ID
/// </summary>
public Guid OrderId { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 支付页面HTML内容
/// </summary>
public object PaymentPageHtml { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 查询订单状态输入DTO
/// </summary>
public class QueryOrderStatusInput
{
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
}

View File

@@ -0,0 +1,59 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 查询订单状态输出DTO
/// </summary>
public class QueryOrderStatusOutput
{
/// <summary>
/// 订单ID
/// </summary>
public Guid OrderId { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 支付宝交易号
/// </summary>
public string? TradeNo { get; set; }
/// <summary>
/// 交易状态
/// </summary>
public TradeStatusEnum TradeStatus { get; set; }
/// <summary>
/// 交易状态描述
/// </summary>
public string TradeStatusDescription { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// 商品名称
/// </summary>
public string GoodsName { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
/// <summary>
/// 最后修改时间
/// </summary>
public DateTime? LastModificationTime { get; set; }
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
public class RechargeCreateInput
{
/// <summary>
/// 用户ID
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// 充值金额
/// </summary>
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "充值金额必须大于0")]
public decimal RechargeAmount { get; set; }
/// <summary>
/// 充值内容
/// </summary>
[Required]
[StringLength(500, ErrorMessage = "充值内容不能超过500个字符")]
public string Content { get; set; } = string.Empty;
/// <summary>
/// 到期时间为空表示永久VIP
/// </summary>
public DateTime? ExpireDateTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[StringLength(1000, ErrorMessage = "备注不能超过1000个字符")]
public string? Remark { get; set; }
/// <summary>
/// 联系方式
/// </summary>
[StringLength(200, ErrorMessage = "联系方式不能超过200个字符")]
public string? ContactInfo { get; set; }
}

View File

@@ -0,0 +1,45 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,164 @@
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

@@ -0,0 +1,8 @@
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

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 每日Token使用量统计DTO
/// </summary>
public class DailyTokenUsageDto
{
/// <summary>
/// 日期
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// Token消耗量
/// </summary>
public long Tokens { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 模型Token使用量统计DTO
/// </summary>
public class ModelTokenUsageDto
{
/// <summary>
/// 模型ID
/// </summary>
public string Model { get; set; }
/// <summary>
/// 总消耗量
/// </summary>
public long Tokens { get; set; }
/// <summary>
/// 占比(百分比)
/// </summary>
public decimal Percentage { get; set; }
}

View File

@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 支付服务接口
/// </summary>
public interface IPayService : IApplicationService
{
/// <summary>
/// 创建订单并发起支付
/// </summary>
/// <param name="input">创建订单输入</param>
/// <returns>订单创建结果</returns>
Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input);
/// <summary>
/// 支付宝异步通知处理
/// </summary>
/// <param name="form">表单数据</param>
/// <returns></returns>
Task<string> AlipayNotifyAsync([FromForm] IFormCollection form);
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="input">查询订单状态输入</param>
/// <returns>订单状态信息</returns>
Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input);
}

View File

@@ -0,0 +1,9 @@
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
public interface IRechargeService
{
/// <summary>
/// 移除用户vip及角色
/// </summary>
Task RemoveVipRoleByExpireAsync();
}

View File

@@ -0,0 +1,21 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 使用量统计服务接口
/// </summary>
public interface IUsageStatisticsService
{
/// <summary>
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync();
/// <summary>
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
}

View File

@@ -0,0 +1,10 @@
<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>
</Project>

View File

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,125 @@
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.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.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
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
.Where(x => x.ModelType == ModelTypeEnum.Chat)
.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>
[HttpPost("ai-chat/send")]
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromQuery] 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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,130 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
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.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
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;
private readonly AiBlacklistManager _aiBlacklistManager;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_tokenManager = tokenManager;
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
_aiBlacklistManager = aiBlacklistManager;
}
/// <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));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//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>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/images/generations")]
public async Task ImagesGenerationsAsync([FromBody] ImageCreateRequest input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input);
}
/// <summary>
/// 向量生成
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/embeddings")]
public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input);
}
/// <summary>
/// 获取模型列表
/// </summary>
/// <returns></returns>
[HttpGet("openApi/v1/models")]
public async Task<ModelsListDto> ModelsAsync()
{
var data = await _aiModelRepository._DbQueryable
.Where(x => x.ModelType == ModelTypeEnum.Chat)
.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

@@ -0,0 +1,170 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Domain.Alipay;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Volo.Abp;
using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.SqlSugarCore.Abstractions;
using System.ComponentModel;
using System.Reflection;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 支付服务
/// </summary>
public class PayService : ApplicationService, IPayService
{
private readonly AlipayManager _alipayManager;
private readonly PayManager _payManager;
private readonly ILogger<PayService> _logger;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
public PayService(
AlipayManager alipayManager,
PayManager payManager,
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
{
_alipayManager = alipayManager;
_payManager = payManager;
_logger = logger;
_payOrderRepository = payOrderRepository;
}
/// <summary>
/// 创建订单并发起支付
/// </summary>
/// <param name="input">创建订单输入</param>
/// <returns>订单创建结果</returns>
[Authorize]
[HttpPost("pay/Order")]
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
{
// 1. 通过PayManager创建订单
var order = await _payManager.CreateOrderAsync(input.GoodsType);
// 2. 通过AlipayManager发起页面支付
var paymentPageHtml = await _alipayManager.PaymentPageAsync(
order.GoodsName,
order.OutTradeNo,
order.TotalAmount);
// 3. 返回结果
return new CreateOrderOutput
{
OrderId = order.Id,
OutTradeNo = order.OutTradeNo,
PaymentPageHtml = paymentPageHtml.Body
};
}
/// <summary>
/// 支付宝异步通知处理
/// </summary>
/// <param name="form">表单数据</param>
/// <returns></returns>
[HttpPost("pay/AlipayNotify")]
[AllowAnonymous]
public async Task<string> AlipayNotifyAsync([FromForm] IFormCollection form)
{
// 1. 将表单数据转换为字典,保持原始顺序
var notifyData = new Dictionary<string, string>();
foreach (var item in form)
{
notifyData[item.Key] = item.Value.ToString();
}
var signStr = string.Join("&", notifyData.Select(kv => $"{kv.Key}={kv.Value}"));
_logger.LogInformation($"收到支付宝回调通知:{signStr}");
// 2. 验证签名
await _alipayManager.VerifyNotifyAsync(notifyData);
// 3. 记录支付通知
await _payManager.RecordPayNoticeAsync(notifyData,signStr);
// 4. 更新订单状态
var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty);
var tradeStatus = notifyData.GetValueOrDefault("trade_status", string.Empty);
var tradeNo = notifyData.GetValueOrDefault("trade_no", string.Empty);
if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus))
{
var status = ParseTradeStatus(tradeStatus);
await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
}
else
{
throw new AlipayException($"回调格式错误");
}
return "success";
}
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="input">查询订单状态输入</param>
/// <returns>订单状态信息</returns>
[HttpGet("pay/OrderStatus")]
[Authorize]
public async Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input)
{
// 通过PayManager查询订单
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == input.OutTradeNo);
if (order == null)
{
throw new UserFriendlyException($"订单不存在:{input.OutTradeNo}");
}
return new QueryOrderStatusOutput
{
OrderId = order.Id,
OutTradeNo = order.OutTradeNo,
TradeNo = order.TradeNo,
TradeStatus = order.TradeStatus,
TradeStatusDescription = GetTradeStatusDescription(order.TradeStatus),
TotalAmount = order.TotalAmount,
GoodsName = order.GoodsName,
GoodsType = order.GoodsType,
CreationTime = order.CreationTime,
LastModificationTime = order.LastModificationTime
};
}
/// <summary>
/// 获取交易状态描述
/// </summary>
/// <param name="tradeStatus">交易状态</param>
/// <returns>状态描述</returns>
private string GetTradeStatusDescription(TradeStatusEnum tradeStatus)
{
var fieldInfo = tradeStatus.GetType().GetField(tradeStatus.ToString());
var descriptionAttribute = fieldInfo?.GetCustomAttribute<DescriptionAttribute>();
return descriptionAttribute?.Description ?? "未知状态";
}
/// <summary>
/// 解析交易状态
/// </summary>
/// <param name="tradeStatus">状态字符串</param>
/// <returns></returns>
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
{
if (Enum.TryParse<TradeStatusEnum>(tradeStatus, out var result))
{
return result;
}
return TradeStatusEnum.WAIT_TRADE;
}
}

View File

@@ -0,0 +1,92 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services
{
public class RechargeService : ApplicationService,IRechargeService
{
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
private readonly ICurrentUser _currentUser;
private readonly IUserService _userService;
private readonly IRoleService _roleService;
private readonly AiRechargeManager _aiMessageManager;
public RechargeService(
ISqlSugarRepository<AiRechargeAggregateRoot> repository,
ICurrentUser currentUser,
IUserService userService, IRoleService roleService, AiRechargeManager aiMessageManager)
{
_repository = repository;
_currentUser = currentUser;
_userService = userService;
_roleService = roleService;
_aiMessageManager = aiMessageManager;
}
/// <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;
}
/// <summary>
/// 给用户充值VIP
/// </summary>
/// <param name="input">充值输入参数</param>
/// <returns></returns>
[HttpPost("recharge/vip")]
public async Task RechargeVipAsync(RechargeCreateInput input)
{
// 创建充值记录
var rechargeRecord = new AiRechargeAggregateRoot
{
UserId = input.UserId,
RechargeAmount = input.RechargeAmount,
Content = input.Content,
ExpireDateTime = input.ExpireDateTime,
Remark = input.Remark,
ContactInfo = input.ContactInfo
};
// 保存充值记录到数据库
await _repository.InsertAsync(rechargeRecord);
// 使用UserService给用户添加VIP角色
await _userService.AddUserRoleByRoleCodeAsync(input.UserId,
new List<string>() { AiHubConst.VipRole, "default" });
}
/// <summary>
/// 移除用户vip及角色
/// </summary>
[RemoteService(isEnabled: false)]
public async Task RemoveVipRoleByExpireAsync()
{
var expiredUserIds = await _aiMessageManager.RemoveVipByExpireAsync();
if (expiredUserIds is not null)
{
await _roleService.RemoveUserRoleByRoleCodeAsync(expiredUserIds, AiHubConst.VipRole);
}
}
}
}

View File

@@ -0,0 +1,92 @@
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

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Authorization;
using SqlSugar;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 使用量统计服务
/// </summary>
[Authorize]
public class UsageStatisticsService : ApplicationService, IUsageStatisticsService
{
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
{
_messageRepository = messageRepository;
_usageStatisticsRepository = usageStatisticsRepository;
}
/// <summary>
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync()
{
var userId = CurrentUser.GetId();
var endDate = DateTime.Today;
var startDate = endDate.AddDays(-6); // 近7天
// 从Message表统计近7天的token消耗
var dailyUsage = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
.GroupBy(x => x.CreationTime.Date)
.Select(g => new
{
Date = g.CreationTime.Date,
Tokens = SqlFunc.AggregateSum(g.TokenUsage.TotalTokenCount)
})
.ToListAsync();
// 生成完整的7天数据包括没有使用记录的日期
var result = new List<DailyTokenUsageDto>();
for (int i = 0; i < 7; i++)
{
var date = startDate.AddDays(i);
var usage = dailyUsage.FirstOrDefault(x => x.Date == date);
result.Add(new DailyTokenUsageDto
{
Date = date,
Tokens = usage?.Tokens ?? 0
});
}
return result.OrderBy(x => x.Date).ToList();
}
/// <summary>
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync()
{
var userId = CurrentUser.GetId();
// 从UsageStatistics表获取各模型的token消耗统计
var modelUsages = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId)
.Select(x => new
{
x.ModelId,
x.TotalTokenCount
})
.ToListAsync();
if (!modelUsages.Any())
{
return new List<ModelTokenUsageDto>();
}
// 计算总token数
var totalTokens = modelUsages.Sum(x => x.TotalTokenCount);
// 计算各模型占比
var result = modelUsages.Select(x => new ModelTokenUsageDto
{
Model = x.ModelId,
Tokens = x.TotalTokenCount,
Percentage = totalTokens > 0 ? Math.Round((decimal)x.TotalTokenCount / totalTokens * 100, 2) : 0
}).OrderByDescending(x => x.Tokens).ToList();
return result;
}
}

View File

@@ -0,0 +1,12 @@
<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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,6 @@
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class AiHubConst
{
public const string VipRole = "YiXinAi-Vip";
}

View File

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,13 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,79 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
//TODO add model validation
//TODO check what is string or array for prompt,..
public record EmbeddingCreateRequest
{
/// <summary>
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
/// length.
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
/// observed inferior results when newlines are present.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
[JsonIgnore]
public List<string>? InputAsList { get; set; }
/// <summary>
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
/// length.
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
/// observed inferior results when newlines are present.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
[JsonIgnore]
public string? Input { get; set; }
[JsonPropertyName("input")]
public IList<string>? InputCalculated
{
get
{
if (Input != null && InputAsList != null)
{
throw new ValidationException(
"Input and InputAsList can not be assigned at the same time. One of them is should be null.");
}
if (Input != null)
{
return new List<string> { Input };
}
return InputAsList;
}
}
/// <summary>
/// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your
/// available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-model" />
[JsonPropertyName("model")]
public string? Model { get; set; }
/// <summary>
/// The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings-create-dimensions" />
[JsonPropertyName("dimensions")]
public int? Dimensions { get; set; }
/// <summary>
/// The format to return the embeddings in. Can be either float or base64.
/// </summary>
/// <returns></returns>
[JsonPropertyName("encoding_format")]
public string? EncodingFormat { get; set; }
public IEnumerable<ValidationResult> Validate()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,113 @@
using System.Buffers;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
public record EmbeddingCreateResponse : ThorBaseResponse
{
[JsonPropertyName("model")] public string Model { get; set; }
[JsonPropertyName("data")] public List<EmbeddingResponse> Data { get; set; } = [];
/// <summary>
/// 类型转换如果类型是base64,则将float[]转换为base64,如果是空或是float和原始类型一样则不转换
/// </summary>
public void ConvertEmbeddingData(string? encodingFormat)
{
if (Data.Count == 0)
{
return;
}
switch (encodingFormat)
{
// 判断第一个是否是float[],如果是则不转换
case null or "float" when Data[0].Embedding is float[]:
return;
// 否则转换成float[]
case null or "float":
{
foreach (var embeddingResponse in Data)
{
if (embeddingResponse.Embedding is string base64)
{
embeddingResponse.Embedding = Convert.FromBase64String(base64);
}
}
return;
}
// 判断第一个是否是string如果是则不转换
case "base64" when Data[0].Embedding is string:
return;
// 否则转换成base64
case "base64":
{
foreach (var embeddingResponse in Data)
{
if (embeddingResponse.Embedding is JsonElement str)
{
if (str.ValueKind == JsonValueKind.Array)
{
var floats = str.EnumerateArray().Select(element => element.GetSingle()).ToArray();
embeddingResponse.Embedding = ConvertFloatArrayToBase64(floats);
}
}
else if (embeddingResponse.Embedding is IList<double> doubles)
{
embeddingResponse.Embedding = ConvertFloatArrayToBase64(doubles.ToArray());
}
}
break;
}
}
}
public static string ConvertFloatArrayToBase64(double[] floatArray)
{
// 将 float[] 转换成 byte[]
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
try
{
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
// 将 byte[] 转换成 base64 字符串
return Convert.ToBase64String(byteArray);
}
finally
{
ArrayPool<byte>.Shared.Return(byteArray);
}
}
public static string ConvertFloatArrayToBase64(float[] floatArray)
{
// 将 float[] 转换成 byte[]
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
try
{
Buffer.BlockCopy(floatArray, 0, byteArray, 0, floatArray.Length);
// 将 byte[] 转换成 base64 字符串
return Convert.ToBase64String(byteArray);
}
finally
{
ArrayPool<byte>.Shared.Return(byteArray);
}
}
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; }
}
public record EmbeddingResponse
{
[JsonPropertyName("object")] public string Object { get; set; } = "embedding";
[JsonPropertyName("index")] public int? Index { get; set; }
[JsonPropertyName("embedding")] public object Embedding { get; set; }
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
public sealed class ThorEmbeddingInput
{
[JsonPropertyName("model")]
public string Model { get; set; }
[JsonPropertyName("input")]
public object Input { get; set; }
[JsonPropertyName("encoding_format")]
public string? EncodingFormat { get; set; }
[JsonPropertyName("dimensions")]
public int? Dimensions { get; set; }
[JsonPropertyName("user")]
public string? User { get; set; }
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
/// <summary>
/// Image Create Request Model
/// </summary>
public record ImageCreateRequest : SharedImageRequestBaseModel
{
public ImageCreateRequest()
{
}
public ImageCreateRequest(string prompt)
{
Prompt = prompt;
}
/// <summary>
/// A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3
/// </summary>
[JsonPropertyName("prompt")]
public string Prompt { get; set; }
/// <summary>
/// The quality of the image that will be generated. Possible values are 'standard' or 'hd' (default is 'standard').
/// Hd creates images with finer details and greater consistency across the image.
/// This param is only supported for dall-e-3 model.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Quality"/> for possible values
/// </summary>
[JsonPropertyName("quality")]
public string? Quality { get; set; }
/// <summary>
/// The style of the generated images. Must be one of vivid or natural.
/// Vivid causes the model to lean towards generating hyper-real and dramatic images.
/// Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Style"/> for possible values
/// </summary>
[JsonPropertyName("style")]
public string? Style { get; set; }
[JsonPropertyName("background")]
public string? Background { get; set; }
[JsonPropertyName("moderation")]
public string? Moderation { get; set; }
[JsonPropertyName("output_compression")]
public string? OutputCompression { get; set; }
[JsonPropertyName("output_format")]
public string? OutputFormat { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageCreateResponse : ThorBaseResponse
{
[JsonPropertyName("data")] public List<ImageDataResult> Results { get; set; }
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; } = new();
public record ImageDataResult
{
[JsonPropertyName("url")] public string Url { get; set; }
[JsonPropertyName("b64_json")] public string B64 { get; set; }
[JsonPropertyName("revised_prompt")] public string RevisedPrompt { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageEditCreateRequest : SharedImageRequestBaseModel
{
/// <summary>
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
/// </summary>
public byte[]? Image { get; set; }
/// <summary>
/// Image file name
/// </summary>
public string ImageName { get; set; }
/// <summary>
/// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited.
/// Must be a valid PNG file, less than 4MB, and have the same dimensions as image.
/// </summary>
public byte[]? Mask { get; set; }
/// <summary>
/// Mask file name
/// </summary>
public string? MaskName { get; set; }
[JsonPropertyName("quality")]
public string Quality { get; set; }
/// <summary>
/// A text description of the desired image(s). The maximum length is 1000 characters.
/// </summary>
[JsonPropertyName("prompt")]
public string Prompt { get; set; }
[JsonPropertyName("background")]
public string? Background { get; set; }
[JsonPropertyName("moderation")]
public string? Moderation { get; set; }
[JsonPropertyName("output_compression")]
public string? OutputCompression { get; set; }
[JsonPropertyName("output_format")]
public string? OutputFormat { get; set; }
[JsonPropertyName("style")]
public string? Style { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageVariationCreateRequest : SharedImageRequestBaseModel
{
/// <summary>
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
/// </summary>
public byte[] Image { get; set; }
/// <summary>
/// Image file name
/// </summary>
public string ImageName { get; set; }
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record SharedImageRequestBaseModel
{
/// <summary>
/// The number of images to generate. Must be between 1 and 10.
/// For dall-e-3 model, only n=1 is supported.
/// </summary>
[JsonPropertyName("n")]
public int? N { get; set; }
/// <summary>
/// The size of the generated images.
/// Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2.
/// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Size"/> for possible values
/// </summary>
[JsonPropertyName("size")]
public string? Size { get; set; }
/// <summary>
/// The format in which the generated images are returned. Must be one of url or b64_json
/// </summary>
[JsonPropertyName("response_format")]
public string? ResponseFormat { get; set; }
/// <summary>
/// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
/// <a href="https://platform.openai.com/docs/usage-policies/end-user-ids">Learn more</a>.
/// </summary>
[JsonPropertyName("user")]
public string? User { get; set; }
/// <summary>
/// The model to use for image generation. Must be one of dall-e-2 or dall-e-3
/// For ImageEditCreateRequest and for ImageVariationCreateRequest only dall-e-2 modell is supported at this time.
/// </summary>
[JsonPropertyName("model")]
public string? Model { get; set; }
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,28 @@
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
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

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

View File

@@ -0,0 +1,66 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

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

View File

@@ -0,0 +1,348 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 对话补全请求参数对象
/// </summary>
public class ThorChatCompletionsRequest
{
[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>
/// 兼容-代码补全
/// </summary>
[JsonPropertyName("suffix")]
public string? Suffix { get; set; }
/// <summary>
/// 兼容-代码补全
/// </summary>
[JsonPropertyName("prompt")]
public string? Prompt { get; set; }
private const string CodeCompletionPrompt = """
You are a code modification assistant. Your task is to modify the provided code based on the user's instructions.
Rules:
1. Return only the modified code, with no additional text or explanations.
2. The first character of your response must be the first character of the code.
3. The last character of your response must be the last character of the code.
4. NEVER use triple backticks (```) or any other markdown formatting in your response.
5. Do not use any code block indicators, syntax highlighting markers, or any other formatting characters.
6. Present the code exactly as it would appear in a plain text editor, preserving all whitespace, indentation, and line breaks.
7. Maintain the original code structure and only make changes as specified by the user's instructions.
8. Ensure that the modified code is syntactically and semantically correct for the given programming language.
9. Use consistent indentation and follow language-specific style guidelines.
10. If the user's request cannot be translated into code changes, respond only with the word NULL (without quotes or any formatting).
11. Do not include any comments or explanations within the code unless specifically requested.
12. Assume that any necessary dependencies or libraries are already imported or available.
IMPORTANT: Your response must NEVER begin or end with triple backticks, single backticks, or any other formatting characters.
The relevant context before the current editing content is: {0}.
After the current editing content is: {1}.
""";
/// <summary>
/// 兼容代码补全
/// </summary>
public void CompatibleCodeCompletion()
{
if (Messages is null || !Messages.Any())
{
//兼容代码补全模式Prompt为当前代码前内容Suffix为当前代码后内容
Messages = new List<ThorChatMessage>()
{
new ThorChatMessage
{
Role = "system",
Content = string.Format(CodeCompletionPrompt, Prompt, Suffix)
}
};
}
Suffix = null;
Prompt = null;
}
/// <summary>
/// 模型唯一编码值,如 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()
// };
// }
if (jsonElement.ValueKind == JsonValueKind.Object)
{
ToolChoice = jsonElement.Deserialize<ThorToolChoice>();
}
}
else if (value is string text)
{
ToolChoice = new ThorToolChoice
{
Type = text
};
}
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

@@ -0,0 +1,63 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,191 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public sealed class ThorChatMessageAudioContent
{
[JsonPropertyName("data")]
public string? Data { get; set; }
[JsonPropertyName("format")]
public string? Format { get; set; }
}

View File

@@ -0,0 +1,98 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,36 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,45 @@
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,23 @@
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,55 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

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

View File

@@ -0,0 +1,25 @@
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,260 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

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

View File

@@ -0,0 +1,102 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,96 @@
using System;
using System.Reflection;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
/// <summary>
/// 价格特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class PriceAttribute : Attribute
{
public decimal Price { get; }
public PriceAttribute(double price)
{
Price = (decimal)price;
}
}
/// <summary>
/// 显示名称特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class DisplayNameAttribute : Attribute
{
public string DisplayName { get; }
public DisplayNameAttribute(string displayName)
{
DisplayName = displayName;
}
}
/// <summary>
/// 商品枚举
/// </summary>
public enum GoodsTypeEnum
{
[Price(0.01)]
[DisplayName("YiXinVip Test")]
YiXinVipTest = 0,
[Price(29.9)]
[DisplayName("YiXinVip 1 month")]
YiXinVip1 = 1,
[Price(80.7)]
[DisplayName("YiXinVip 3 month")]
YiXinVip3 = 3,
[Price(143.9)]
[DisplayName("YiXinVip 6 month")]
YiXinVip6 = 6,
[Price(199.9)]
[DisplayName("YiXinVip 10 month")]
YiXinVip10 = 10
}
public static class GoodsTypeEnumExtensions
{
/// <summary>
/// 获取商品总金额
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>总金额</returns>
public static decimal GetTotalAmount(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.Price ?? 0m;
}
/// <summary>
/// 获取商品价格描述
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>价格描述</returns>
public static string GetPriceDescription(this GoodsTypeEnum goodsType)
{
var price = goodsType.GetTotalAmount();
return $"¥{price:F1}";
}
/// <summary>
/// 获取商品名称
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>商品名称</returns>
public static string GetDisplayName(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.DisplayName ?? goodsType.ToString();
}
}

View File

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

View File

@@ -0,0 +1,8 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelTypeEnum
{
Chat = 0,
Image = 1,
Embedding = 2
}

View File

@@ -0,0 +1,36 @@
using System.ComponentModel;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum TradeStatusEnum
{
/// <summary>
/// 准备发起
/// </summary>
[Description("准备发起")]
WAIT_TRADE = 0,
/// <summary>
/// 交易创建
/// </summary>
[Description("等待买家付款")]
WAIT_BUYER_PAY = 10,
/// <summary>
/// 交易关闭
/// </summary>
[Description("交易关闭")]
TRADE_CLOSED = 20,
/// <summary>
/// 交易成功
/// </summary>
[Description("交易成功")]
TRADE_SUCCESS = 100,
/// <summary>
/// 交易结束
/// </summary>
[Description("交易结束")]
TRADE_FINISHED = -10
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,275 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
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

@@ -0,0 +1,73 @@
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

@@ -0,0 +1,29 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
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

@@ -0,0 +1,39 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IImageService
{
/// <summary>Creates an image given a prompt.</summary>
/// <param name="imageCreate"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImage(
ImageCreateRequest imageCreate,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates an edited or extended image given an original image and a prompt.
/// </summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageEdit(
ImageEditCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>Creates a variation of a given image.</summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageVariation(
ImageVariationCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
}

View File

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

View File

@@ -0,0 +1,19 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface ITextEmbeddingService
{
/// <summary>
///
/// </summary>
/// <param name="createEmbeddingModel"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<EmbeddingCreateResponse> EmbeddingAsync(
EmbeddingCreateRequest createEmbeddingModel,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,187 @@
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.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.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

@@ -0,0 +1,71 @@
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

@@ -0,0 +1,114 @@
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.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.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;
}
}

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