Compare commits

..

252 Commits

Author SHA1 Message Date
chenchun
53e56134d4 Merge branch 'abp' into codex 2025-12-11 17:45:04 +08:00
chenchun
0d2f2cb826 fix: 仅从 Query 获取 access_token/refresh_token,简化 OnMessageReceived 逻辑
- 修改文件:Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
- 将 JwtBearerEvents.OnMessageReceived 的上下文参数名改为 messageContext,统一变量名。
- 简化 Token 获取逻辑:只从 request.Query 中读取 access_token 与 refresh_token,移除从 Cookies(Token)和请求头(refresh_token)读取的分支。
2025-12-11 17:41:38 +08:00
chenchun
f90105ebb4 feat: 全站优化 2025-12-11 17:33:12 +08:00
chenchun
67ed1ac1e3 fix: 聊天模型列表仅返回 OpenAi 类型
在 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs 中,为查询添加了 .Where(x => x.ModelApiType == ModelApiTypeEnum.OpenAi) 过滤,确保只返回 ModelType 为 Chat 且 ModelApiType 为 OpenAi 的模型,避免将非 OpenAi 的模型纳入聊天模型列表。
2025-12-11 17:17:35 +08:00
chenchun
69b84f6613 feat: 完成openai响应接口 2025-12-11 17:16:21 +08:00
ccnetcore
433d616b9b feat: 支持codex 2025-12-11 01:17:31 +08:00
chenchun
53aa575ad4 Merge branch 'abp' into ai-hub 2025-12-10 15:54:50 +08:00
chenchun
571df74c43 chore: 在 common.props 添加 SatelliteResourceLanguages=en;zh-CN
在 Yi.Abp.Net8/common.props 中新增 SatelliteResourceLanguages 属性,指定生成卫星资源语言为 en 和 zh-CN,以便打包对应的本地化资源。
2025-12-10 15:53:18 +08:00
chenchun
b7847c7e7d feat: 发布2.6版本 2025-12-10 15:14:45 +08:00
chenchun
94eb41996e Merge branch 'abp' into ai-hub 2025-12-10 15:11:44 +08:00
chenchun
cefde6848d perf: 去除35MB又臭又大的腾讯云sdk 2025-12-10 15:10:54 +08:00
chenchun
381b712b25 feat: 完成模型库功能模块 2025-12-10 15:08:16 +08:00
Gsh
c319b0b4e4 fix: 模型库优化 2025-12-10 01:34:40 +08:00
ccnetcore
1a32fa9e20 feat: 支持多选模型库条件 2025-12-10 00:31:14 +08:00
Gsh
909406238c fix: 模型库前端布局优化 2025-12-09 23:38:11 +08:00
chenchun
54a1d2a66f feat: 完成模型库 2025-12-09 19:11:30 +08:00
chenchun
8dcbfcad33 feat: 同步商品价格 2025-12-08 14:08:01 +08:00
ccnetcore
f64fd43951 Merge branch 'abp' into ai-hub 2025-12-07 18:50:37 +08:00
ccnetcore
551597765c perf: 优化sqlsguar分页查询 2025-12-07 18:50:02 +08:00
Gsh
bfda33280a fix: 图标显示优化 2025-12-05 23:32:59 +08:00
chenchun
8d0411f1f4 feat: 完成codefirst 2025-12-04 16:38:37 +08:00
chenchun
3995d4acab Merge branch 'token' into ai-hub 2025-12-04 16:35:17 +08:00
chenchun
6ff5727156 feat: 发布新版 2025-12-04 16:34:58 +08:00
chenchun
f654386dfe feat: 发布新版 2025-12-04 16:33:17 +08:00
chenchun
c03ef82643 feat:完成多token分发 2025-12-04 16:32:30 +08:00
Gsh
525545329b fix: 多api密钥增加分页 2025-11-30 00:04:33 +08:00
Gsh
755cb6f509 feat: 优化token用量查看 2025-11-29 23:44:38 +08:00
Gsh
55469708f0 feat: 新增多token用量查看 2025-11-29 23:29:54 +08:00
ccnetcore
94c52c62fe style: 修改token描述 2025-11-29 18:33:39 +08:00
ccnetcore
37b4709d76 feat: 新增token默认分组 2025-11-29 18:28:42 +08:00
ccnetcore
86555af6ce feat: 完成token下拉框 2025-11-29 18:25:43 +08:00
Gsh
ddb00879f4 feat: 新增多token功能 2025-11-29 17:35:17 +08:00
chenchun
2d0ca08314 feat: 新增功能 启动时初始化 AiHub 的 Message、Token、UsageStatistics 聚合根表并添加相应命名空间 2025-11-27 19:23:44 +08:00
chenchun
b78ecf27d5 feat: 完成token功能 2025-11-27 19:01:16 +08:00
Gsh
02a5f69958 feat: 前端2.4版本 2025-11-26 21:20:14 +08:00
Gsh
cf5bf746ef feat: 模型尊享标识优化 2025-11-25 22:14:48 +08:00
chenchun
0a5e40ee25 feat: 新增 PremiumPackageConst 模型 gpt-5.1-codex-max
在 Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs 的 premiumModels 列表中添加 "gpt-5.1-codex-max"(并补上末尾换行)。
2025-11-25 14:18:06 +08:00
chenchun
51a266ef58 feat: 在 PremiumPackageConst 中新增 claude-opus-4-5-20251101
文件:Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs
说明:向 premium package 列表中添加新模型标识 claude-opus-4-5-20251101,以支持该付费包。
2025-11-25 12:42:44 +08:00
chenchun
1f0901c90c feat: 新增功能
- 更新 PremiumPackageConst.ModeIds,新增支持的模型 ID:
  - claude-haiku-4-5-20251001
  - gemini-3-pro-preview
- 文件:Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs
- 目的:扩展可识别的 premium 模型列表,便于后续对新模型的支持与路由处理

注意:修改后需重新编译并在相关使用处确认新模型 ID 的兼容性。
2025-11-25 10:57:08 +08:00
chenchun
a725c06396 fix: 移除对 Usage.TotalTokens 的空检查,始终按 multiplier 四舍五入并赋值
移除 TotalTokens 的 null 判断,避免保留 null 值,统一将其按 multiplier 四舍五入后赋为整数,防止后续使用出现空值异常。
2025-11-25 10:19:11 +08:00
chenchun
54547f0d7c fix: 缩放 ThorChatCompletionsResponse.Usage.TotalTokens 按 multiplier
当 Usage.TotalTokens 不为 null 时,按 multiplier 进行四舍五入缩放;与 PromptTokens/CompletionTokens 的缩放逻辑保持一致,修复 TotalTokens 未被缩放的问题。
2025-11-25 10:18:45 +08:00
chenchun
afe9c8bcae feat: 新增模型列表 IsPremiumPackage 字段并在 AiChatService 中设置
- 在 Yi.Framework.AiHub.Application.Contracts.Dtos.ModelGetListOutput 中新增 bool 属性 IsPremiumPackage。
- 在 Yi.Framework.AiHub.Application.Services.Chat.AiChatService 的模型映射中设置该属性,判断逻辑为 PremiumPackageConst.ModeIds.Contains(x.ModelId)。
- 便于前端区分并展示“尊享包”模型。
2025-11-25 09:59:31 +08:00
chenchun
688d93e5c1 feat: 完成倍率的配置化 2025-11-25 09:54:13 +08:00
chenchun
4c65b2398d fix: 将默认 max_tokens 从 100000 调整为 64000
将 Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/ClaudiaChatCompletionsService.cs 中对外请求的默认 max_tokens 值由 100000 降为 64000。

原因:避免超出模型/服务允许的 token 限制或引发资源/性能异常;仍然允许通过 input.MaxTokens 显式覆盖该默认值。已在本地构建并用简单请求验证变更生效。
2025-11-24 17:42:18 +08:00
chenchun
41435f1aa3 feat: 兼容maxtoken问题 2025-11-24 09:42:40 +08:00
chenchun
20206bbc44 fix: 调整 ThorClaude 聊天默认 max_tokens 从 2048 到 100000
修改文件:
Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/ClaudiaChatCompletionsService.cs

说明:
- 将默认 max_tokens 由 2048 提高到 100000,避免长回复被截断,提升对大输出场景的支持。
- 修改可能影响请求的响应长度与资源消耗,请确认后端/模型能够支持该上限并监控性能与计费变化。
2025-11-20 10:20:19 +08:00
chenchun
f2dc0d1825 fix: 仅对 gpt-5.1-chat 设置 MaxCompletionTokens,gpt-5-mini 单独处理 Temperature/TopP
将原先同时匹配 gpt-5.1-chat 与 gpt-5-mini 的处理拆分为两段:
- gpt-5.1-chat:仍将 MaxTokens 映射到 MaxCompletionTokens,并清空 Temperature/TopP。
- gpt-5-mini:只清空 Temperature/TopP,不再修改 MaxTokens/MaxCompletionTokens。

修复了为 gpt-5-mini 不当设置 MaxCompletionTokens 的问题。
2025-11-18 14:35:58 +08:00
chenchun
51b4d1b072 fix: 请求处理中同时重置 MaxTokens 避免与模型不兼容
在 YiFrameworkAiHubDomainModule 的请求处理器中,当清除 Temperature 与 TopP 时一并将 request.MaxTokens 设为 null,防止在不支持该参数的模型上出现错误或参数冲突。文件:Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs。
2025-11-18 14:33:58 +08:00
chenchun
9180799e4e feat: 为 gpt-5-mini 与 databricks-claude-sonnet-4 添加请求特殊处理 2025-11-18 11:36:18 +08:00
chenchun
9788b9182b fix: 区分 gpt-5.1-chat 与 o1 的请求参数清理逻辑
将原先在同一处理器中对 gpt-5.1-chat 与 o1 一并清除 Temperature/TopP 的逻辑拆分为两个处理器:
- gpt-5.1-chat:清除 Temperature 与 TopP
- o1:仅清除 Temperature

文件:Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs

目的:恢复/调整对不同模型的期望处理,避免对 o1 不必要地清除 TopP。
2025-11-18 11:26:05 +08:00
chenchun
260b9a4795 feat: 支持 gpt-5.1-chat 模型的特殊处理
- 将模型判断从仅 "o1" 扩展为 "gpt-5.1-chat" 或 "o1",对这些模型将 Temperature 置为 null。
- 微调了 User-Agent 字符串的空格并做了小范围的格式清理(增加空行以提升可读性)。
2025-11-18 10:39:34 +08:00
chenchun
9380e3daa8 Merge branch 'card-flip' into ai-hub 2025-11-18 10:27:53 +08:00
chenchun
bb894e14a4 Merge remote-tracking branch 'origin/card-flip' into card-flip
# Conflicts:
#	Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs
2025-11-17 11:22:44 +08:00
chenchun
b492d82442 Merge branch 'abp' into card-flip
# Conflicts:
#	Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs
#	Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs
#	Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs
#	Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
2025-11-17 11:21:14 +08:00
chenchun
5eaffe2ec2 feat: 新增更新并发乐观锁配置与支持
- 在 DbConnOptions 新增 EnabledConcurrencyException(bool,默认 false) 配置项。
- 在 SqlSugarRepository 引入 IAbpLazyServiceProvider,通过 IOptions<DbConnOptions> 延迟获取配置。
- UpdateAsync 改为仅当 EnabledConcurrencyException 为 true 且实体实现 IHasConcurrencyStamp 时,使用 ExecuteCommandWithOptLockAsync 并捕获 VersionExceptions 抛出 AbpDbConcurrencyException;否则回退到原有的 UpdateAsync 实现。
- 清理/调整部分 using 引用,新增 Microsoft.Extensions.Options 与 Volo.Abp.DependencyInjection 引用。

注意:默认值为 false,需在配置中显式开启 EnabledConcurrencyException 才会启用乐观并发校验,开启后会改变之前对带版本实体自动使用乐观锁的行为。
2025-11-17 11:19:15 +08:00
Gsh
d7bcad9da7 feat: 前端打包报错处理 2025-11-17 02:03:10 +08:00
Gsh
04e11d15e2 feat: 新手引导优化 2025-11-17 01:39:13 +08:00
Gsh
97e3dc5eed feat: 公告弹窗优化 2025-11-17 01:20:20 +08:00
Gsh
695bd56a27 feat: 增加新手引导 2025-11-17 01:05:57 +08:00
ccnetcore
7919383be3 feat: 完成v2.3.0发布 2025-11-17 00:43:41 +08:00
ccnetcore
d6cc3c5d96 Merge branch 'abp' into card-flip
# Conflicts:
#	Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs
#	Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs
2025-11-17 00:43:27 +08:00
Gsh
19f0d05a69 fix: 尊享明细表格优化 2025-11-16 23:00:19 +08:00
Gsh
3c5e575e9b fix: 公告优化 2025-11-16 22:39:42 +08:00
Gsh
f875617de1 fix: 版本号2.3 2025-11-16 22:05:44 +08:00
Gsh
9976e8a6e2 fix: 翻牌机制优化 2025-11-16 22:05:01 +08:00
Gsh
38e8cbc5ca fix: 优化邀请 2025-11-16 21:50:54 +08:00
Gsh
d95c14c903 fix: 优化尊享包记录 2025-11-16 21:39:08 +08:00
ccnetcore
ffb2f2fb4c fix: 修复尊享包查询条件并新增时间范围筛选 2025-11-16 21:32:41 +08:00
ccnetcore
4bd2fc357d refactor: 邀请码逻辑调整,支持双方填写/使用邀请码统计,并移除已被邀请状态字段 2025-11-14 23:53:29 +08:00
chenchun
da23d17af8 feat: 为尊享包 Token 列表新增按是否免费过滤并添加请求 DTO
- 新增 PremiumTokenUsageGetListInput,包含 IsFree 过滤项并继承分页 DTO
- 修改 UsageStatisticsService.GetPremiumTokenUsageListAsync 签名为使用新的输入 DTO,并根据 IsFree 添加 WhereIF 过滤
- 微调 DTO 导入与格式化(PremiumTokenUsageGetListOutput)
2025-11-14 18:00:49 +08:00
chenchun
c1a6046107 feat: 完成公告、尊享记录功能 2025-11-14 17:54:40 +08:00
chenchun
2ec7b5f4fd fix: 修复软删除时空引用异常
在 SqlSugarRepository.cs 中对 ISoftDelete 分支增加 GetByIdAsync 返回 null 的判断,避免在实体为 null 时继续反射赋值导致 NullReferenceException。若实体不存在,直接返回 false。
2025-11-13 16:49:44 +08:00
Gsh
d21f61646a fix: 系统公告与尊享额度明细 2025-11-12 23:08:52 +08:00
chenchun
eecdf442fb feat: 新增公告跳转链接字段 Url
- 在 AnnouncementAggregateRoot、AnnouncementLogDto、AnnouncementCacheDto 中新增 string? Url 属性,用于存储公告的跳转链接。
- 如果需要持久化到数据库,请同步添加对应的迁移/映射配置。
2025-11-12 21:49:31 +08:00
chenchun
8e8338743d fix: 修正 YiXinVip 枚举值及属性(8个月改为7个月,更新价格与显示名) 2025-11-10 17:03:54 +08:00
chenchun
1b4c3cbb8d feat: 支持尊享包用量统计 2025-11-10 15:18:05 +08:00
chenchun
b7756e2112 feat: 新增功能
- 概要
  - 重构并扩展公告相关模型、DTO、服务,新增公告类型、图片与时间字段,调整缓存与查询处理。
  - 新增枚举 AnnouncementTypeEnum。

- 主要改动(简要)
  - Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs
    - 新增 ImageUrl、StartTime、EndTime、Type 字段,移除 Date 字段,Title 不再默认空串。
  - Yi.Framework.AiHub.Domain/Entities
    - 重命名 AnnouncementLogAggregateRoot -> AnnouncementAggregateRoot
    - 表名由 Ai_AnnouncementLog 改为 Ai_Announcement(SugarTable 标注)
    - 新增 ImageUrl、StartTime、EndTime、Type、Remark 字段(Remark 已存在,保持)
  - Yi.Framework.AiHub.Domain.Shared/Enums/AnnouncementTypeEnum.cs
    - 新增枚举文件(Activity=1, System=2)
  - Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs
    - GetAsync 返回类型由 AnnouncementOutput 改为 List<AnnouncementLogDto>
  - Yi.Framework.AiHub.Application/Services/AnnouncementService.cs
    - 使用 Mapster 进行 DTO 映射
    - 查询按 StartTime 降序,返回 List<AnnouncementLogDto>,缓存结构简化
  - Yi.Abp.Web/YiAbpWebModule.cs
    - 改为初始化 AnnouncementAggregateRoot 的表(Ai_Announcement)
  - Yi.Ai.Vue3/types/import_meta.d.ts
    - 移除 VITE_BUILD_COMPRESS 环境变量声明

- 重要注意/兼容性提示
  - 接口变更:IAnnouncementService.GetAsync 返回类型已改变,调用方需同步更新(之前返回 AnnouncementOutput 的代码需调整)。
  - 数据库表变更:表名从 Ai_AnnouncementLog -> Ai_Announcement,若需保留历史数据,请在部署前做好数据迁移(重命名表或迁移数据到新表结构),或使用 CodeFirst 初始化新表(当前代码在启动时会 InitTables<AnnouncementAggregateRoot>())。
  - 新增 Mapster 适配(确保项目有 Mapster 依赖)。
  - 前端类型声明移除环境变量后,前端构建/运行脚本若依赖 VITE_BUILD_COMPRESS 需同步调整。
  - 若有缓存结构(AnnouncementCacheDto)或序列化相关约定变更,确认兼容性。

- 建议操作
  - 更新所有使用 IAnnouncementService 的代码(API 层/前端适配返回结构)。
  - 在非生产环境先执行数据迁移验证(保留旧表数据或写迁移脚本)。
  - 确认 Mapster 包已安装并编译通过。
  - 前端项目检查并同步 import_meta.d.ts 变更。
2025-11-10 15:03:02 +08:00
chenchun
cb49059e84 feat: 翻牌与邀请码逻辑重构,新增中奖记录与前7次中奖概率
- CardFlipTaskAggregateRoot.cs
  - 用 WinRecords(Dictionary<int,long>) 替代原先第8/9/10次的各自字段,且以 JSON 存库(SugarColumn IsJson)。
  - 构造函数初始化 WinRecords。
  - 新增 SetWinReward(int flipCount, long amount) 统一记录中奖。

- CardFlipService.cs
  - 读取并展示 WinRecords,按翻牌顺序映射中奖信息(不再依赖单独的第8/9/10字段)。

- CardFlipManager.cs
  - 重构中奖逻辑:
    - 前7次翻牌改为 50% 概率可中奖,奖励范围 1w–3w(新增配置常量 FREE_*)。
    - 统一通过 SetWinReward 记录任意次的中奖金额。
    - GenerateRandomReward 根据最小值自动选单位(1w 或 100w)。
  - 邀请类型翻牌校验由“仅统计被填写次数”改为“统计本周作为邀请人或被邀请人的邀请记录数量”(双方都计入),并据此判断是否可解锁邀请翻牌次数。

- InviteCodeManager.cs
  - 使用邀请码时:
    - 验证规则调整:一个账号只能填写别人的邀请码一次(hasUsedOthersCode 检查)。
    - 邀请记录的语义变化:当使用邀请码时,邀请记录同时代表邀请人和被邀请人各增加一次翻牌机会。
    - 不再将邀请码标记为单次已用;改为增加 UsedCount(一个邀请码可以被多人使用)。
    - 优化日志与提示信息。

- InviteCodeAggregateRoot.cs
  - 移除 IsUsed、UsedTime、UsedByUserId、MarkAsUsed 等单次使用相关字段/方法。
  - 保留 IsUserInvited(被邀请后不能再作为被邀请者使用)和 UsedCount(统计多人使用次数)。

注意事项
- 这是数据结构与业务逻辑的变更,数据库表结构发生变化(新增 WinRecords JSON 字段,移除若干字段)。上线前请准备相应的迁移脚本或数据迁移方案,确保历史中奖数据平滑迁移到 WinRecords。
- 变更会影响相关单元/集成测试、前端展示字段,需同步更新对应测试与界面展示逻辑。
2025-11-07 21:31:18 +08:00
chenchun
690cabfd96 feat: 新增公告功能 2025-11-06 16:59:29 +08:00
chenchun
4521212a90 feat: 新增文件缓存功能
- 在 Yi.Framework.Rbac.Application.Services.FileService 中注入 IMemoryCache,用于缓存文件元数据,减少对仓储的重复读取。
  - 在 Get 方法中通过 key "File:{code}" 缓存 FileCacheItem,设置绝对过期时间为 1 天。
  - 缓存项使用 Mapster 适配为 FileCacheItem,再适配回 FileAggregateRoot(保留现有逻辑判断和路径获取)。
- 新增缓存模型 Yi.Framework.Rbac.Domain.Shared.Caches.FileCacheItem(包含 Id、FileSize、FileName、FilePath、创建/修改信息等)。
- 增加并调整相关 using 引用(Microsoft.Extensions.Caching.Memory、Volo.Abp.Caching、Domain.Shared.Caches)。
- 同时修复了保存多文件时的缩进/空格格式(不影响功能)。
2025-11-06 11:29:21 +08:00
chenchun
771ecd9d81 Merge branch 'abp' into ai-hub
# Conflicts:
#	Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/FileService.cs
2025-11-06 11:23:46 +08:00
chenchun
94834f45c3 perf: 使用 FileStreamResult 流式返回文件,避免一次性读取到内存
改为 FileStream 并返回 FileStreamResult,减小内存占用并支持大型文件;修正变量名拼写并添加 null-forgiving 标记。
2025-11-06 11:13:50 +08:00
chenchun
a9b2979a21 Merge branch 'abp' into ai-hub 2025-11-06 11:00:04 +08:00
chenchun
22ac150acd fix: 修正 FileAggregateRoot.FilePath 的赋值,保存目录路径而非包含文件名的完整路径 2025-11-06 10:58:33 +08:00
Gsh
17337b8d78 fix: 系统公告弹窗前端 2025-11-05 23:12:23 +08:00
chenchun
09fb43ee14 refactor: 修改 YiCrudAppService.DeleteAsync 的参数名 ids -> id
在 Yi.Abp.Net8/framework/Yi.Framework.Ddd.Application/YiCrudAppService.cs 中,将 DeleteAsync 方法的参数名由 ids 改为 id,更新了对应的 XML 注释与对 Repository.DeleteManyAsync 的调用参数。仅为参数重命名,无功能变更。
2025-11-05 16:28:52 +08:00
ccnetcore
477c0e3f2c Merge branch 'invitation' into ai-hub 2025-11-02 13:00:36 +08:00
Gsh
2e4f520dac fix: 更改版本号 2025-11-02 01:27:03 +08:00
ccnetcore
067b25b9af Merge remote-tracking branch 'origin/invitation' into invitation 2025-11-02 01:21:33 +08:00
ccnetcore
36370c215d fix: 修复周邀请次数统计时使用错误的用户ID字段 2025-11-02 01:21:28 +08:00
Gsh
e24731acfe fix: 分享词修改 2025-11-02 01:17:22 +08:00
Gsh
927e9df7de fix: 分享地址固定为海外地址 2025-11-02 00:55:47 +08:00
Gsh
114b41144e fix: 增加邀请链接逻辑 2025-11-02 00:51:14 +08:00
ccnetcore
5019a36138 fix: 优化邀请码不足提示文案 2025-11-02 00:32:04 +08:00
Gsh
e15eb6149b fix: 翻牌样式优化,动画效果完善 2025-11-01 18:48:17 +08:00
Gsh
9d401a9c93 fix: 翻牌样式优化,动画效果完善 2025-11-01 17:56:19 +08:00
Gsh
eacf86e118 fix: 翻牌样式优化,动画效果待完善 2025-10-31 00:41:21 +08:00
chenchun
c4b631c815 feat: 新增翻牌幸运值悬浮球及相关逻辑
- .claude/settings.local.json:新增 Read 权限路径(Read(//e/code/github/Yi/Yi.Ai.Vue3/**))
- Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue:
  - 新增 luckyValue 响应式状态与 updateLuckyValue() 方法,并在获取任务状态后更新幸运值
  - 新增悬浮球 UI(SVG 进度环、图标、百分比文本)及样式和动画
  - 调整了 v-loading 为 false,并注释了部分错误提示(可能为调试遗留)
- 说明:样式使用嵌套写法(scss/sass 风格),请确认构建流程支持;建议确认 v-loading 与错误提示变更是否为预期并视情况修正。
2025-10-30 21:16:19 +08:00
chenchun
fb25e75a3a feat: 完成邀请机制 2025-10-30 20:17:14 +08:00
chenchun
e9099bbe04 feat: 增加基于本周填写邀请码数量的邀请翻牌校验
- 注入 ISqlSugarRepository<InvitationRecordAggregateRoot> 到 CardFlipManager 并更新构造函数。
- 在邀请类型(FlipType.Invite)翻牌时,改为校验用户本周已填写的邀请码数量是否满足本次翻牌所需(根据 InviteFlipsUsed 计算所需数量),不足则抛出友好异常提示。
- 保持原有错误处理与日志逻辑不变。
2025-10-30 20:13:49 +08:00
chenchun
f02fb91175 feat: 增加邀请码每周使用上限并调整翻牌规则(扩展免费次数、移除赠送翻牌与翻倍提示) 2025-10-30 19:51:56 +08:00
chenchun
5beef22269 fix: 修复权限判断逻辑(应为 &&,避免始终抛出权限异常)
修正 AiAccountService.GetProfitStatisticsAsync 中的条件判断,原先使用 || 导致即使为 Guo 或 cc 仍被拒绝访问。
2025-10-30 14:48:53 +08:00
chenchun
933cbb91d8 feat: 新增尊享包利润统计接口及 ElCollapseTransition 类型声明
- 在 AiAccountService 中新增 GetProfitStatisticsAsync 接口(GET account/profit-statistics),注入 PremiumPackage 仓储并统计尊享包已消耗/剩余、总成本、总收益、利润率及按200售价的成本估算。接口受授权控制。
- 注入 ISqlSugarRepository<PremiumPackageAggregateRoot> 并在构造函数中赋值。
- 在 types/components.d.ts 中新增 ElCollapseTransition 类型声明,补充前端组件类型提示。
- 注意:接口中对用户权限的判断使用了 "CurrentUser.UserName != \"Guo\" || CurrentUser.UserName != \"cc\"",该逻辑可能有误(应为 &&),建议确认并修正权限校验。
2025-10-30 14:38:58 +08:00
chenchun
efd917d184 style: 全部样式更新2.0 2025-10-30 11:21:11 +08:00
chenchun
e906208f4a feat: 新增邀请翻牌验证及相关文案与界面调整
- CardFlipManager:注入 InviteCodeManager,新增对 Invite 类型翻牌的邀请校验(未使用邀请码则抛出异常),防止未被邀请的用户使用邀请类型翻牌。
- CardFlipService:调整提示文案,统一使用“本周”前缀,并在邀请解锁提示中强调必定中奖且每次中奖最大额度翻倍。
- 前端:
  - CardFlipActivity.vue:注释掉翻牌失败的全局提示,调整统计文案为“本周已翻/本周剩余/本周邀请”,并在邀请弹窗文案中说明必定中奖且奖励翻倍。
  - Avatar.vue:更新菜单项标签为“每日任务(限时)”和“每周邀请(限时)”。
2025-10-30 11:19:22 +08:00
chenchun
cf137f6307 fix: 兼容客户端空值,Contents 为空时返回 "_" 并修正 Content 判空逻辑
修复 AnthropicMessageInput 中对 Content/Contents 的判空处理:
- 当 Contents 为 null 或 Count==0 时返回 "_",以兼容客户端对空值的特殊处理。
- 修正对 Content 的判空逻辑,使用 !string.IsNullOrEmpty(...) 确保非空字符串优先返回,避免将空字符串当作有效内容。
2025-10-29 22:23:09 +08:00
chenchun
e6b991fe86 feat: 调整翻牌与邀请码逻辑,增加第8次奖励及前端骨架屏 2025-10-29 21:55:17 +08:00
chenchun
3e75792e43 fix: 修复bug - 在可用性检查中支持忽略剩余令牌校验,避免负数用量包被错误过滤
- 将 PremiumPackageAggregateRoot.IsAvailable 增加参数 isVerifyRemainingToken=true,保持默认行为不变,允许按需跳过对 RemainingTokens 的校验。
- 在 UsageStatisticsService 中计算可用包时改为使用 p.IsAvailable(false),仅过滤过期或禁用的包,但不再因 RemainingTokens 为负而将包排除,从而保证统计(如 TotalTokens/RemainingTokens 汇总)包含负数用量的包,修正统计错误。

修改文件:
- Yi.Framework.AiHub.Domain/Entities/PremiumPackageAggregateRoot.cs
- Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs
2025-10-29 16:34:53 +08:00
Gsh
dd3f6325bb fix: 个人中心优化 2025-10-29 00:17:36 +08:00
chenchun
108ba348f6 feat: 扣减尊享包用量并调整日常任务奖励
- 在 AiGateWayManager 中新增:当请求使用尊享包模型时,按实际使用的 totalTokens 调用 PremiumPackageManager.TryConsumeTokensAsync 扣减用户尊享包用量(仅在 totalTokens > 0 时)。
- 调整 DailyTaskService 中两项日常任务的奖励配置:1000w 消耗奖励由 200w -> 100w,3000w 消耗奖励由 400w -> 200w。
- 兼顾少量格式化优化(if 条件空格调整)。
2025-10-28 17:43:23 +08:00
chenchun
bcdcec40e0 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-28 16:13:13 +08:00
chenchun
2ce8baea42 fix: 优化对话异常提示信息
将抛出异常的消息从 "OpenAI对话异常{StatusCode}" 修改为更详细的中文提示,包含 StatusCode 与 Response 内容,便于排查。未改变逻辑,仅调整异常文本。
2025-10-28 16:12:52 +08:00
chenchun
c6425ca206 fix: 优化对话异常提示信息
将抛出异常的消息从 "OpenAI对话异常{StatusCode}" 修改为更详细的中文提示,包含 StatusCode 与 Response 内容,便于排查。未改变逻辑,仅调整异常文本。
2025-10-28 16:02:01 +08:00
chenchun
acb359ec33 style: 删除多余的 SqlSugar InitTables 注释并调整注释格式
在 Yi.Abp.Web/YiAbpWebModule.cs 中移除两行多余的注释,调整剩余注释的空格格式,清理代码注释,不影响程序逻辑。
2025-10-27 22:01:57 +08:00
chenchun
a1395d9a33 feat: 新增翻牌顺序追踪并重构翻牌/邀请码逻辑到 Manager,更新前端
- 在 CardFlipStatusOutput 与前端 types 添加 FlipOrderIndex 字段以记录牌在翻牌顺序中的位置
- 在域实体 CardFlipTaskAggregateRoot 增加 FlippedOrder(Json 列)以保存用户实际翻牌顺序
- 将 CardFlipService 重构为调用 CardFlipManager 与 InviteCodeManager,移除大量内聚的业务实现与常量(职责下沉到 Manager)
- 调整翻牌、使用邀请码和查询相关流程为 Manager 驱动,更新返回结构与提示文本
- 更新前端 CardFlipActivity 组件与 types,允许任意未翻的卡片被点击并显示翻牌顺序位置
- 若干文案、格式与日志细节修正
2025-10-27 21:57:26 +08:00
ccnetcore
609de29e71 feat: AnthropicMessageContent 新增 Signature 字段 2025-10-26 14:51:48 +08:00
ccnetcore
2efed4f4a5 feat: AnthropicThinkingInput 新增 signature、thinking、data、text 字段 2025-10-26 10:38:01 +08:00
chenchun
aec90ec9d6 feat: 新增翻牌活动入口与全局组件声明
- 在 Header Avatar 菜单新增翻牌活动(cardFlip)入口,并添加对应插槽 <card-flip-activity/>
- 在 types/components.d.ts 中添加 CardFlipActivity 与 ElCollapseTransition 类型声明
- 在 .eslintrc-auto-import.json 中新增 ElMessage 与 ElMessageBox 自动导入
- 从 import_meta.d.ts 中移除 VITE_BUILD_COMPRESS 环境声明
- 在 YiAbpWebModule.cs 中添加相关 using 并保留数据库建表初始化的注释(CodeFirst.InitTables)
2025-10-23 21:58:47 +08:00
chenchun
1aaff2942d fix: 调整 Anthropic DTO 属性为可空类型以避免反序列化错误 2025-10-21 16:55:05 +08:00
chenchun
cdbfc5383d feat: 为充值记录新增订单类型字段并区分VIP与套餐逻辑 2025-10-20 10:18:24 +08:00
ccnetcore
f302555e0c feat: 完善描述 2025-10-18 17:40:46 +08:00
ccnetcore
86c5890476 feat: 用户中心新增每日任务组件并在头像菜单中集成 2025-10-18 17:34:46 +08:00
ccnetcore
a13ee395c7 feat: 支持 x-api-key 认证并扩展 Anthropic 响应字段,优化工具调用处理 2025-10-18 13:23:54 +08:00
Gsh
9abcd72aca fix: 增加教程导航 2025-10-16 22:50:10 +08:00
ccnetcore
4ddea6d468 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-16 22:12:36 +08:00
ccnetcore
867a2dc861 fix: 修正Claude聊天响应的Token统计逻辑并优化AiGateWayManager使用条件,同时移除前端无用环境变量定义 2025-10-16 22:11:09 +08:00
chenchun
4a72e3fa0d Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-16 09:35:56 +08:00
chenchun
8b4371aabb feat: 尊享包购买流程新增充值记录保存功能 2025-10-16 09:35:25 +08:00
Gsh
799dd08ec0 feat: 模型提示词、剩余额度、对话状态优化 2025-10-16 01:20:11 +08:00
Gsh
c5c22224cf feat: 2.0发布 2025-10-15 23:44:18 +08:00
ccnetcore
2dae47e85c feat: 修复价格 2025-10-15 23:18:26 +08:00
ccnetcore
375dd4f797 fix: 修复支付3位数问题 2025-10-15 23:04:09 +08:00
ccnetcore
acb2db8397 fix: 商品类型返回值 2025-10-15 23:00:42 +08:00
ccnetcore
b7a3e76d0b Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-15 19:51:28 +08:00
ccnetcore
48150b712a refactor: 会话ID为空时不存储消息内容,并移除无用注释 2025-10-15 19:49:33 +08:00
chenchun
6db9dfc308 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-15 11:54:06 +08:00
chenchun
2d6c1f3c46 fix: 验证交易状态仅在成功时执行充值逻辑 2025-10-15 11:53:54 +08:00
Gsh
161e10d2d1 feat: 产品样式调整 2025-10-15 00:16:57 +08:00
Gsh
a9a2a91183 feat: 产品样式调整 2025-10-15 00:05:10 +08:00
Gsh
1c9a6f108e feat: 产品样式调整 2025-10-15 00:05:10 +08:00
ccnetcore
d6adf9b736 feat: 增加 Claude 模型 Token 使用量倍数调整功能 2025-10-14 23:41:26 +08:00
ccnetcore
959eb3f782 fix: 优化服务号与支付逻辑,增加AccessToken为空校验及优惠描述完善 2025-10-14 23:02:44 +08:00
ccnetcore
7a53e0c90c refactor: 简化尊享包Token扣减逻辑,移除多包分配与校验流程 2025-10-14 22:34:05 +08:00
ccnetcore
533b87fc5b fix: 修复统计近7天token消耗时角色过滤条件错误 2025-10-14 22:22:35 +08:00
ccnetcore
15713cf7fe feat: 支持Claude模型API类型及尊享包校验与扣减逻辑 2025-10-14 22:17:21 +08:00
Gsh
31dc756868 feat: 尊享模型效果 2025-10-14 21:29:20 +08:00
ccnetcore
52f6b6130f feat: 为 HttpClient 添加默认 User-Agent 请求头 2025-10-13 23:08:15 +08:00
ccnetcore
16945b3d5b fix: 修复剩余令牌统计逻辑,增加过期时间判断 2025-10-13 22:09:47 +08:00
Gsh
bdc664fc44 feat: 增加尊享token包产品 2025-10-13 01:14:40 +08:00
Gsh
9555ef10e0 feat: 增加尊享token包产品 2025-10-13 01:03:41 +08:00
Gsh
49e6cb26fc feat: 增加尊享产品 2025-10-12 23:00:08 +08:00
ccnetcore
3ace29e692 fix: 修复无会话时仍存储消息内容的问题 2025-10-12 22:46:20 +08:00
ccnetcore
aa9dd0129b refactor: 将尊享包Token统计逻辑从AiAccountService迁移至UsageStatisticsService,并移除AiUserRoleMenuDto相关字段 2025-10-12 21:51:51 +08:00
ccnetcore
1464271fbd Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-12 21:12:37 +08:00
ccnetcore
754f145559 fix: 允许尊享包扣减到负数并优化Token统计逻辑 2025-10-12 21:12:21 +08:00
Gsh
6afd0cb955 feat: 个人中心优化 2025-10-12 21:08:23 +08:00
ccnetcore
d32906702a feat: 商品枚举与支付服务优化,支持中文名称、参考价格及类别筛选 2025-10-12 21:04:08 +08:00
ccnetcore
9bcdaf6bd8 fix: 更新尊享包折扣规则为每10元减2.5元,最多减50元,并同步修改提示文案 2025-10-12 20:14:07 +08:00
ccnetcore
db82a8cf08 Merge branch 'premium' into ai-hub 2025-10-12 20:10:23 +08:00
ccnetcore
a9e8b2b01f feat: 增加尊享包商品及折扣逻辑,完善VIP与尊享包相关接口和数据返回
- 新增尊享包商品类型,支持 5000W 和 10000W Tokens
- 增加尊享包折扣计算与折扣后价格获取方法
- PayService 新增获取商品列表接口,支持尊享包折扣展示
- PayManager 支持尊享包订单金额按折扣计算,并新增获取用户累计充值金额方法
- OpenApiService Anthropic接口增加VIP与尊享包用量校验
- AiGateWayManager 增加尊享包Token扣减逻辑
- AiAccountService 返回用户VIP状态、到期时间及尊享包Token统计信息
2025-10-12 20:07:58 +08:00
Gsh
85bd1ce8d6 feat: 个人中心新增尊享服务、模型列表区分 2025-10-12 18:30:34 +08:00
ccnetcore
4d09243efd feat: 完成尊享服务 2025-10-12 16:42:26 +08:00
ccnetcore
5934056fe6 fix: 修复Anthropic接口TokenUsage序列化及HttpClient创建方式问题 2025-10-12 14:38:26 +08:00
chenchun
2a81062fa3 perf: 优化 HttpClient 配置,增加 10 分钟超时设置 2025-10-12 00:02:34 +08:00
chenchun
fdc868323f fix: 修正 TokenUsage 计算逻辑,使用 CacheReadInputTokens 替代重复的 CacheCreationInputTokens 2025-10-11 23:36:26 +08:00
chenchun
593b3a4cdd fix: 修正消息与Anthropic返回的Token统计逻辑,避免零值覆盖并支持缓存Token计算 2025-10-11 23:27:46 +08:00
chenchun
2b12e18e6c fix: 为AnthropicHandles添加MaxTokens有效性校验 2025-10-11 20:32:41 +08:00
chenchun
345ed80ec8 feat: 新增claude接口转换支持 2025-10-11 15:25:43 +08:00
chenchun
29dc1ae250 fix: 限制 Azure OpenAI 请求最大 tokens 并优化响应处理空行格式 2025-10-10 15:16:16 +08:00
ccnetcore
9fdd41b134 fix: 更新会员套餐为8个月并调整价格信息 2025-10-08 21:24:29 +08:00
ccnetcore
31ee5e8ffb feat: 新增 YiXinVip 8 个月商品类型并移除 9 个月配置 2025-10-08 21:18:42 +08:00
ccnetcore
d7922bb71d fix: 修复 tokenUsage 为空时的空引用问题 2025-09-27 17:40:31 +08:00
ccnetcore
1cc5f2a14f refactor: 注释掉 Furion 统一结果 API 注册,保留 ABP 默认处理方式 2025-09-27 17:26:13 +08:00
橙子
d9997eeb28 !100 update Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs.
Merge pull request !100 from Gary/N/A
2025-09-22 07:14:02 +00:00
Gary
06e77aa8fd update Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs.
判断是否为模型验证错误,如果是,将errors传回,并打印日志

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

Signed-off-by: Gary <1511313969@qq.com>
2025-09-22 02:58:59 +00:00
chenchun
fa3ac91ba4 fix: 修正文件整理大师Vip数量限制提示错误 2025-09-18 16:17:10 +08:00
chenchun
f90d3871fa feat: 启用 Furion 统一返回结果并优化过滤器配置
- 在 `YiAbpWebModule` 中启用 `AddFurionUnifyResultApi` 以支持 Furion 风格的统一 API 返回格式
- 调整 `UnifyResultExtensions`,移除 `AbpExceptionFilter` 和 `AbpNoContentActionFilter`,确保统一结果过滤器优先执行
2025-09-16 11:48:36 +08:00
ccnetcore
e7c152e955 fix: 修复文件整理大师文件数量限制及模型名称错误 2025-09-13 21:19:54 +08:00
ccnetcore
0223b5c104 fix: 更新客服联系方式和产品价格信息
- 统一修改客服支持提示信息为"备注ai获取专属客服支持"
- 更新会员套餐价格和描述信息
- 替换模型排行榜iframe为openrouter链接
- 调整内容截断长度从2000到10000字符
2025-09-07 18:49:58 +08:00
ccnetcore
9e41a7c446 fix: 调整YiXinVip商品价格及有效期配置 2025-09-07 18:29:41 +08:00
ccnetcore
2ba0ffc1b7 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-09-07 17:16:13 +08:00
ccnetcore
7d038e1266 style: 优化样式1.3 2025-09-07 17:16:07 +08:00
Gsh
b98285f314 fix: 处理二维码接口过多调用问题 2025-09-07 13:30:03 +08:00
ccnetcore
73438da666 fix: 将 VerifyNext 接口由 GET 改为 POST 请求 2025-09-07 01:34:52 +08:00
ccnetcore
85f2e1b579 feat: 新增文件整理大师服务及校验与对话接口 2025-09-07 01:34:25 +08:00
ccnetcore
ece89ebad0 refactor: 移除自定义 HttpClientFactory,改用依赖注入的 IHttpClientFactory 2025-09-04 22:26:06 +08:00
Gsh
6e2dd39246 fix: 未登录支付按钮改登录 2025-09-03 11:43:40 +08:00
Gsh
a61286e534 fix: 非会员模型选择跳转修改 2025-09-03 10:56:44 +08:00
Gsh
4f944a5466 fix: 优化二维码加载错误显示 2025-08-31 23:56:42 +08:00
ccnetcore
d29aac088a feat: 全面优化ai-hub前端细节 2025-08-31 01:37:36 +08:00
Gsh
8abd122773 feat: 重新登录逻辑更改 2025-08-30 23:58:57 +08:00
Gsh
08084aa0bc feat: 默认展示二维码登录 2025-08-30 23:13:46 +08:00
Gsh
e69cd5a73c feat: 用户头像路径固定 2025-08-30 23:05:14 +08:00
Gsh
76aa3bdc64 feat: 用户头像路径固定 2025-08-30 22:39:27 +08:00
Gsh
93251104af Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-08-30 22:29:21 +08:00
Gsh
3cae477f3e feat: 增加扫码登录功能 2025-08-30 22:28:38 +08:00
ccnetcore
25c736dc0a fix: 修复扫码回调在非等待状态下仍被处理的问题 2025-08-30 22:07:09 +08:00
ccnetcore
96a09d8980 fix: 修复绑定用户时返回值未包含用户ID的问题 2025-08-30 21:58:43 +08:00
ccnetcore
72387235a0 refactor: 移除分布式锁获取时的超时参数 2025-08-30 21:17:25 +08:00
ccnetcore
1b00e505b7 fix: 修复绑定微信二维码生成时未登录用户的异常提示 2025-08-30 18:02:46 +08:00
ccnetcore
1c54e47b9e feat: 新增AI账户服务及扩展用户信息获取功能,支持通过userId查询用户信息 2025-08-30 17:55:13 +08:00
ccnetcore
ba07e2c905 feat: 新增服务号注册授权页面并优化注册提示信息 2025-08-30 00:02:27 +08:00
ccnetcore
bde4611a50 fix: 修复服务号注册缓存时间及锁定范围问题 2025-08-29 23:32:40 +08:00
ccnetcore
e7326fea7b Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-08-29 22:21:11 +08:00
ccnetcore
d13b23ad2e fix: 修复服务号扫码场景缓存未保存用户ID且场景结果错误的问题 2025-08-29 22:20:52 +08:00
Gsh
8b1830a711 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-08-29 22:02:14 +08:00
Gsh
b70c530754 fix: 增加扫码 2025-08-29 22:01:44 +08:00
ccnetcore
d90e24f9ed fix: 修复服务号回调处理返回用户ID及缓存更新逻辑 2025-08-29 21:55:07 +08:00
chenchun
1fbd521d1a fix: 注册接口加分布式锁防止并发重复注册 2025-08-29 14:33:35 +08:00
chenchun
2ae6183e7f fix: 防止重复注册意社区账号 2025-08-29 14:31:04 +08:00
chenchun
7905911624 feat: 注册用户时支持传入头像参数 2025-08-29 14:11:50 +08:00
chenchun
c5b6b33d8e feat: 注册公众号用户时保存额外用户信息到数据库 2025-08-29 13:54:02 +08:00
chenchun
5d29fd6d3b feat: 为微信服务号用户信息响应模型添加JsonPropertyName映射并移除无用字段 2025-08-29 13:41:50 +08:00
chenchun
ad8f48f36b feat: 服务号用户信息获取增加日志记录 2025-08-29 11:53:36 +08:00
chenchun
f9843c13d4 fix: 修复场景缓存为空时的处理逻辑并调整注册成功缓存写入方式 2025-08-29 11:34:57 +08:00
chenchun
6bd561b094 feat: 新增微信公众号扫码注册功能及幂等处理
- 新增 `FuwuhaoConst` 常量类,统一缓存 Key 前缀管理
- `FuwuhaoOptions` 增加 FromUser、RedirectUri、PicUrl 配置项
- `FuwuhaoManager` 新增 `BuildRegisterMessage` 方法,构建注册引导图文消息
- `FuwuhaoService`
  - 增加 OpenId 与 Scene 绑定缓存,支持扫码注册有效期管理
  - 回调处理支持注册场景,返回图文消息引导用户注册
  - 新增注册接口 `RegisterByCodeAsync`,根据微信授权信息自动注册账号并更新场景状态
- `AccountManager` 注册方法增加分布式锁,防止重复注册,并校验用户名唯一性
2025-08-29 11:01:09 +08:00
chenchun
d2c6238df1 feat: 启用AI股票生成与新闻生成任务并切换至OpenAI接口配置 2025-08-28 15:40:59 +08:00
chenchun
1d108983e8 feat: 增加服务号回调签名校验及扫码回调幂等处理
- `FuwuhaoManager` 新增 `ValidateCallback` 方法,用于校验微信回调签名
- `FuwuhaoOptions` 增加 `CallbackToken` 配置项
- `QrCodeResponse` 属性添加 `JsonPropertyName` 标注,支持 JSON 序列化映射
- `FuwuhaoService` 在回调接口中增加签名校验,并通过分布式锁实现幂等处理
- 调整场景值解析逻辑,过滤非扫码/关注事件
- 优化缓存过期时间设置
2025-08-28 15:20:15 +08:00
ccnetcore
b768bca638 feat: 完成支持微信扫码功能 2025-08-27 23:42:46 +08:00
chenchun
28fcd6c9ce feat: 新增服务号回调处理服务及数据模型 2025-08-27 17:46:39 +08:00
HW-July
6005b9329d 岗位状态修改 2025-08-25 17:12:18 +08:00
ccnetcore
10559a925c style: 优化样式1.2 2025-08-25 00:39:16 +08:00
ccnetcore
942e218a9e feat: 新增 FileMaster 发送消息接口 2025-08-23 13:15:28 +08:00
ccnetcore
f6af9edc38 feat: 删除文件 2025-08-23 12:23:17 +08:00
ccnetcore
e0f6331ec3 feat:构建 2025-08-23 12:21:16 +08:00
ccnetcore
06f0c6caa7 style: 整体调整 2025-08-23 12:14:09 +08:00
ccnetcore
56ec260e3a perf: 优化已完成状态下的输出等待时间,加快响应速度 2025-08-21 21:50:06 +08:00
ccnetcore
176cf84369 fix: 修复 AiGateWayManager 中可能的空引用异常 2025-08-21 01:16:57 +08:00
Gsh
6de3b722ed fix: 新增文件助手目录 2025-08-18 23:07:34 +08:00
Gsh
2cf6326764 fix: 更新组件库 2025-08-18 21:02:55 +08:00
Gsh
ec27ee58b4 fix: 充值成功与记录页面增加联系客服,apikey教程更改 2025-08-17 22:08:03 +08:00
ccnetcore
4e42e2202e refactor: 移除代码补全兼容逻辑并优化 DTO 可空类型处理 2025-08-16 19:20:58 +08:00
ccnetcore
e60d8eceb7 feat: 新增 YiXinVip 商品价格配置及前端构建压缩环境变量定义 2025-08-16 00:08:03 +08:00
Gsh
08fb939b38 fix: 动态支付响应页面 2025-08-15 23:49:18 +08:00
ccnetcore
482dd73afd feat: 支持创建订单时自定义支付宝回调地址 2025-08-15 23:41:01 +08:00
Gsh
f09a9fee75 fix: 产品相关页面样式优化 2025-08-15 23:28:09 +08:00
chenchun
9a31d14b41 feat: 优化CORS配置支持通配符和代码格式化
- 支持CORS配置使用通配符"*"允许所有来源访问
- 优化CORS策略配置逻辑,区分通配符和具体域名处理
- 格式化代码,移除多余空行和统一代码风格
- 修复Hangfire配置中的变量赋值格式
- 更新默认CORS配置为通配符模式
2025-08-14 17:04:27 +08:00
chenchun
2fd7f88f04 feat: 新增海外站点流量限制和CORS配置优化
- 新增yxai.chat域名到CORS白名单
- 为海外站点yxai.chat添加大流量接口访问限制
- 修复Azure OpenAI图像生成服务默认尺寸设置
2025-08-14 15:14:30 +08:00
chenchun
9d4cc802e9 fix: 添加CodeGeeX跨域支持
在CORS配置中新增http://codegeex域名,支持CodeGeeX工具的跨域访问请求
2025-08-14 14:24:03 +08:00
Gsh
ee6b4827fa feat: 增加支付宝在线支付、套餐订购弹窗、会员权益、支持模型展示等 2025-08-14 00:26:39 +08:00
ccnetcore
48d8c528f6 fix: 调整YiXinVip各档价格为0.01方便测试 2025-08-13 22:21:03 +08:00
ccnetcore
40c0a5ac64 feat: 支付完成后自动为用户充值VIP并支持按商品类型计算有效期 2025-08-13 22:19:31 +08:00
橙子
9d4b3e7d0c update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-05 07:50:10 +00:00
chenchun
72795382a1 style: 调整样式 2025-07-02 15:03:16 +08:00
橙子
35cdff2afa update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:04:18 +00:00
橙子
dcf547f513 style: 新增赞助
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:03:28 +00:00
橙子
8660d45f36 update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-07-02 06:01:51 +00:00
橙子
63dd55e7a4 !95 修正分页导致部门结构显示异常。取消后台分页功能,同菜单结构无需分页。
Merge pull request !95 from Po/N/A
2025-07-02 03:49:56 +00:00
Po
40cd89f90c 修正分页导致部门结构显示异常。取消后台分页功能,同菜单结构无需分页。
Signed-off-by: Po <448443959@qq.com>
2025-07-02 02:50:06 +00:00
243 changed files with 25409 additions and 1549 deletions

3
.gitignore vendored
View File

@@ -265,6 +265,7 @@ src/Acme.BookStore.Blazor.Server.Tiered/Logs/*
**/wwwroot/libs/*
public
dist
dist - 副本
.vscode
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Production.json
@@ -277,3 +278,5 @@ database_backup
/Yi.Abp.Net8/src/Yi.Abp.Web/yi-abp-dev.db
package-lock.json
.claude

View File

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

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)",
"Read(//e/code/github/Yi/Yi.Ai.Vue3/**)"
],
"deny": [],
"ask": []
}
}

View File

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

View File

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

View File

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

View File

@@ -199,11 +199,11 @@ namespace Yi.Framework.Ddd.Application
/// <summary>
/// 批量删除实体
/// </summary>
/// <param name="ids">实体ID集合</param>
/// <param name="id">实体ID集合</param>
[RemoteService(isEnabled: true)]
public virtual async Task DeleteAsync(IEnumerable<TKey> ids)
public virtual async Task DeleteAsync(IEnumerable<TKey> id)
{
await Repository.DeleteManyAsync(ids);
await Repository.DeleteManyAsync(id);
}
/// <summary>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
using Yi.Framework.Rbac.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
public class AiUserRoleMenuDto:UserRoleMenuDto
{
/// <summary>
/// 是否绑定服务号
/// </summary>
public bool IsBindFuwuhao { get; set; }
/// <summary>
/// 是否为VIP用户
/// </summary>
public bool IsVip { get; set; }
/// <summary>
/// VIP到期时间
/// </summary>
public DateTime? VipExpireTime { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 公告缓存 DTO
/// </summary>
public class AnnouncementCacheDto
{
/// <summary>
/// 版本号
/// </summary>
public string Version { get; set; } = string.Empty;
/// <summary>
/// 公告日志列表
/// </summary>
public List<AnnouncementLogDto> Logs { get; set; } = new List<AnnouncementLogDto>();
/// <summary>
/// 跳转链接
/// </summary>
public string? Url { get; set; }
}

View File

@@ -0,0 +1,44 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 公告日志 DTO
/// </summary>
public class AnnouncementLogDto
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 内容列表
/// </summary>
public List<string> Content { get; set; } = new List<string>();
/// <summary>
/// 图片url
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 开始时间(系统公告时间、活动开始时间)
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// 活动结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 公告类型(系统、活动)
/// </summary>
public AnnouncementTypeEnum Type{ get; set; }
/// <summary>
/// 跳转链接
/// </summary>
public string? Url { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
/// <summary>
/// 公告输出 DTO
/// </summary>
public class AnnouncementOutput
{
/// <summary>
/// 版本号
/// </summary>
public string Version { get; set; } = string.Empty;
/// <summary>
/// 公告日志列表
/// </summary>
public List<AnnouncementLogDto> Logs { get; set; } = new List<AnnouncementLogDto>();
}

View File

@@ -0,0 +1,93 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌任务状态输出
/// </summary>
public class CardFlipStatusOutput
{
/// <summary>
/// 本周总翻牌次数
/// </summary>
public int TotalFlips { get; set; }
/// <summary>
/// 剩余免费次数
/// </summary>
public int RemainingFreeFlips { get; set; }
/// <summary>
/// 剩余赠送次数
/// </summary>
public int RemainingBonusFlips { get; set; }
/// <summary>
/// 剩余邀请解锁次数
/// </summary>
public int RemainingInviteFlips { get; set; }
/// <summary>
/// 是否可以翻牌
/// </summary>
public bool CanFlip { get; set; }
/// <summary>
/// 用户的邀请码
/// </summary>
public string? MyInviteCode { get; set; }
/// <summary>
/// 本周邀请人数
/// </summary>
public int InvitedCount { get; set; }
/// <summary>
/// 翻牌记录
/// </summary>
public List<CardFlipRecord> FlipRecords { get; set; } = new();
/// <summary>
/// 下次可翻牌提示
/// </summary>
public string? NextFlipTip { get; set; }
/// <summary>
/// 当前用户是否已经填写过邀请码
/// </summary>
public bool IsFilledInviteCode { get; set; }
}
/// <summary>
/// 翻牌记录
/// </summary>
public class CardFlipRecord
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
/// <summary>
/// 是否已翻
/// </summary>
public bool IsFlipped { get; set; }
/// <summary>
/// 是否中奖
/// </summary>
public bool IsWin { get; set; }
/// <summary>
/// 奖励金额token数
/// </summary>
public long? RewardAmount { get; set; }
/// <summary>
/// 翻牌类型描述
/// </summary>
public string? FlipTypeDesc { get; set; }
/// <summary>
/// 在翻牌顺序中的位置1-10表示第几个翻
/// </summary>
public int FlipOrderIndex { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌输入
/// </summary>
public class FlipCardInput
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
}

View File

@@ -0,0 +1,32 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌输出
/// </summary>
public class FlipCardOutput
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
/// <summary>
/// 是否中奖
/// </summary>
public bool IsWin { get; set; }
/// <summary>
/// 奖励金额token数
/// </summary>
public long? RewardAmount { get; set; }
/// <summary>
/// 奖励描述
/// </summary>
public string? RewardDesc { get; set; }
/// <summary>
/// 剩余可翻次数
/// </summary>
public int RemainingFlips { get; set; }
}

View File

@@ -0,0 +1,48 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 邀请码信息输出
/// </summary>
public class InviteCodeOutput
{
/// <summary>
/// 我的邀请码
/// </summary>
public string? MyInviteCode { get; set; }
/// <summary>
/// 本周邀请人数
/// </summary>
public int InvitedCount { get; set; }
/// <summary>
/// 是否已被邀请
/// </summary>
public bool IsInvited { get; set; }
/// <summary>
/// 邀请历史记录
/// </summary>
public List<InvitationHistoryItem> InvitationHistory { get; set; } = new();
}
/// <summary>
/// 邀请历史记录项
/// </summary>
public class InvitationHistoryItem
{
/// <summary>
/// 被邀请人昵称(脱敏)
/// </summary>
public string InvitedUserName { get; set; } = string.Empty;
/// <summary>
/// 邀请时间
/// </summary>
public DateTime InvitationTime { get; set; }
/// <summary>
/// 本周所在
/// </summary>
public string WeekDescription { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 使用邀请码输入
/// </summary>
public class UseInviteCodeInput
{
/// <summary>
/// 邀请码
/// </summary>
public string InviteCode { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
/// <summary>
/// 领取任务奖励输入
/// </summary>
public class ClaimTaskRewardInput
{
/// <summary>
/// 任务等级1=1000w任务2=3000w任务
/// </summary>
public int TaskLevel { get; set; }
}

View File

@@ -0,0 +1,58 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
/// <summary>
/// 每日任务状态输出
/// </summary>
public class DailyTaskStatusOutput
{
/// <summary>
/// 今日消耗的尊享包Token数
/// </summary>
public long TodayConsumedTokens { get; set; }
/// <summary>
/// 任务列表
/// </summary>
public List<DailyTaskItem> Tasks { get; set; } = new();
}
/// <summary>
/// 每日任务项
/// </summary>
public class DailyTaskItem
{
/// <summary>
/// 任务等级1=1000w任务2=3000w任务
/// </summary>
public int Level { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 任务描述
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 任务要求的Token消耗量
/// </summary>
public long RequiredTokens { get; set; }
/// <summary>
/// 奖励的Token数量
/// </summary>
public long RewardTokens { get; set; }
/// <summary>
/// 任务状态0=未完成1=可领取2=已领取
/// </summary>
public int Status { get; set; }
/// <summary>
/// 任务进度百分比0-100
/// </summary>
public decimal Progress { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.FileMaster;
public class VerifyNextInput
{
/// <summary>
/// 文件数
/// </summary>
public int FileCount { get; set; }
/// <summary>
/// 文件夹数
/// </summary>
public int DirectoryCount { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
public class QrCodeOutput
{
/// <summary>
/// Qrcode url
/// </summary>
public string QrCodeUrl { get; set; }
/// <summary>
/// 场景值
/// </summary>
public string Scene { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
public class QrCodeResultOutput
{
/// <summary>
/// 返回状态
/// </summary>
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
/// <summary>
/// 如果是已登录返回token
/// </summary>
public string? Token { get; set; }
/// <summary>
/// 刷新token
/// </summary>
public string? RefreshToken { get; set; }
}

View File

@@ -0,0 +1,16 @@
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
public class SceneCacheDto
{
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
public SceneTypeEnum SceneType { get; set; }
/// <summary>
/// 如果是绑定类型需要用户id
/// </summary>
public Guid? UserId { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
/// <summary>
/// API类型选项
/// </summary>
public class ModelApiTypeOption
{
/// <summary>
/// 显示名称
/// </summary>
public string Label { get; set; }
/// <summary>
/// 枚举值
/// </summary>
public int Value { get; set; }
}

View File

@@ -0,0 +1,64 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
/// <summary>
/// 模型库展示数据
/// </summary>
public class ModelLibraryDto
{
/// <summary>
/// 模型ID
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 模型描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型类型名称
/// </summary>
public string ModelTypeName { get; set; }
/// <summary>
/// 模型API类型
/// </summary>
public ModelApiTypeEnum ModelApiType { get; set; }
/// <summary>
/// 模型API类型名称
/// </summary>
public string ModelApiTypeName { get; set; }
/// <summary>
/// 模型显示倍率
/// </summary>
public decimal MultiplierShow { get; set; }
/// <summary>
/// 供应商分组名称
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// 模型图标URL
/// </summary>
public string? IconUrl { get; set; }
/// <summary>
/// 是否为尊享模型PremiumChat类型
/// </summary>
public bool IsPremium { get; set; }
}

View File

@@ -0,0 +1,35 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
/// <summary>
/// 获取模型库列表查询参数
/// </summary>
public class ModelLibraryGetListInput : PagedAllResultRequestDto
{
/// <summary>
/// 搜索关键词搜索模型名称、模型ID
/// </summary>
public string? SearchKey { get; set; }
/// <summary>
/// 供应商名称筛选
/// </summary>
public List<string>? ProviderNames { get; set; }
/// <summary>
/// 模型类型筛选
/// </summary>
public List<ModelTypeEnum>? ModelTypes { get; set; }
/// <summary>
/// API类型筛选
/// </summary>
public List<ModelApiTypeEnum>? ModelApiTypes { get; set; }
/// <summary>
/// 是否只显示尊享模型
/// </summary>
public bool? IsPremiumOnly { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
/// <summary>
/// 模型类型选项
/// </summary>
public class ModelTypeOption
{
/// <summary>
/// 显示名称
/// </summary>
public string Label { get; set; }
/// <summary>
/// 枚举值
/// </summary>
public int Value { get; set; }
}

View File

@@ -62,4 +62,9 @@ public class ModelGetListOutput
/// 备注信息
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 是否为尊享包
/// </summary>
public bool IsPremiumPackage { get; set; }
}

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
@@ -10,5 +11,8 @@ public class CreateOrderInput
/// <summary>
/// 商品类型
/// </summary>
[Required]
public GoodsTypeEnum GoodsType { get; set; }
public string? ReturnUrl{ get; set; }
}

View File

@@ -0,0 +1,16 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 获取商品列表输入DTO
/// </summary>
public class GetGoodsListInput
{
/// <summary>
/// 商品类别(可选)
/// 如果不传,则返回所有商品
/// 如果传了则只返回指定类别的商品VIP服务或尊享包
/// </summary>
public GoodsCategoryType? GoodsCategoryType { get; set; }
}

View File

@@ -0,0 +1,55 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 商品列表输出DTO
/// </summary>
public class GoodsListOutput
{
/// <summary>
/// 商品名称
/// </summary>
public string GoodsName { get; set; }
/// <summary>
/// 商品原价
/// </summary>
public decimal OriginalPrice { get; set; }
/// <summary>
/// 商品参考价格
/// </summary>
public decimal ReferencePrice { get; set; }
/// <summary>
/// 商品实际价格(折扣后的价格)
/// </summary>
public decimal GoodsPrice { get; set; }
/// <summary>
/// 折扣金额(仅尊享包)
/// </summary>
public decimal? DiscountAmount { get; set; }
/// <summary>
/// 商品类别
/// </summary>
public string GoodsCategory { get; set; }
/// <summary>
/// 商品备注
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 折扣说明(仅尊享包)
/// </summary>
public string? DiscountDescription { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
}

View File

@@ -25,9 +25,10 @@ public class RechargeCreateInput
public string Content { get; set; } = string.Empty;
/// <summary>
/// 到期时间为空表示永久VIP
/// VIP月数(为空或0表示永久VIP
/// </summary>
public DateTime? ExpireDateTime { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "月数必须大于等于0")]
public int? Months { get; set; }
/// <summary>
/// 备注

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
/// <summary>
/// 创建Token输入
/// </summary>
public class TokenCreateInput
{
/// <summary>
/// 名称(同一用户不能重复)
/// </summary>
[Required(ErrorMessage = "名称不能为空")]
[StringLength(100, ErrorMessage = "名称长度不能超过100个字符")]
public string Name { get; set; }
/// <summary>
/// 过期时间(空为永不过期)
/// </summary>
public DateTime? ExpireTime { get; set; }
/// <summary>
/// 尊享包额度限制(空为不限制)
/// </summary>
public long? PremiumQuotaLimit { get; set; }
}

View File

@@ -0,0 +1,47 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
/// <summary>
/// Token列表输出
/// </summary>
public class TokenGetListOutputDto
{
/// <summary>
/// Token Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// Token密钥
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// 过期时间(空为永不过期)
/// </summary>
public DateTime? ExpireTime { get; set; }
/// <summary>
/// 尊享包额度限制(空为不限制)
/// </summary>
public long? PremiumQuotaLimit { get; set; }
/// <summary>
/// 尊享包已使用额度
/// </summary>
public long PremiumUsedQuota { get; set; }
/// <summary>
/// 是否禁用
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
public class TokenSelectListOutputDto
{
public Guid TokenId { get; set; }
public string Name { get; set; }
public bool IsDisabled { get; set; }
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
/// <summary>
/// 编辑Token输入
/// </summary>
public class TokenUpdateInput
{
/// <summary>
/// Token Id
/// </summary>
[Required(ErrorMessage = "Id不能为空")]
public Guid Id { get; set; }
/// <summary>
/// 名称(同一用户不能重复)
/// </summary>
[Required(ErrorMessage = "名称不能为空")]
[StringLength(100, ErrorMessage = "名称长度不能超过100个字符")]
public string Name { get; set; }
/// <summary>
/// 过期时间(空为永不过期)
/// </summary>
public DateTime? ExpireTime { get; set; }
/// <summary>
/// 尊享包额度限制(空为不限制)
/// </summary>
public long? PremiumQuotaLimit { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 尊享服务Token用量统计DTO
/// </summary>
public class PremiumTokenUsageDto
{
/// <summary>
/// 总Token数
/// </summary>
public long PremiumTotalTokens { get; set; }
/// <summary>
/// 已使用Token数
/// </summary>
public long PremiumUsedTokens { get; set; }
/// <summary>
/// 剩余Token数
/// </summary>
public long PremiumRemainingTokens { get; set; }
}

View File

@@ -0,0 +1,12 @@
using Volo.Abp.Application.Dtos;
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
public class PremiumTokenUsageGetListInput : PagedAllResultRequestDto
{
/// <summary>
/// 是否免费
/// </summary>
public bool? IsFree { get; set; }
}

View File

@@ -0,0 +1,57 @@
using Volo.Abp.Application.Dtos;
using Yi.Framework.Ddd.Application.Contracts;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
public class PremiumTokenUsageGetListOutput : CreationAuditedEntityDto
{
/// <summary>
/// id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 包名称
/// </summary>
public string PackageName { get; set; }
/// <summary>
/// 总用量总token数
/// </summary>
public long TotalTokens { get; set; }
/// <summary>
/// 剩余用量剩余token数
/// </summary>
public long RemainingTokens { get; set; }
/// <summary>
/// 已使用token数
/// </summary>
public long UsedTokens { get; set; }
/// <summary>
/// 到期时间
/// </summary>
public DateTime? ExpireDateTime { get; set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// 购买金额
/// </summary>
public decimal PurchaseAmount { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 尊享包不同Token用量占比DTO饼图
/// </summary>
public class TokenPremiumUsageDto
{
/// <summary>
/// Token Id
/// </summary>
public Guid TokenId { get; set; }
/// <summary>
/// Token名称
/// </summary>
public string TokenName { get; set; }
/// <summary>
/// Token消耗量
/// </summary>
public long Tokens { get; set; }
/// <summary>
/// 占比(百分比)
/// </summary>
public decimal Percentage { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
public class UsageStatisticsGetInput
{
/// <summary>
/// tokenId
/// </summary>
public Guid? TokenId { get; set; }
}

View File

@@ -0,0 +1,15 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 公告服务接口
/// </summary>
public interface IAnnouncementService
{
/// <summary>
/// 获取公告信息
/// </summary>
/// <returns>公告信息</returns>
Task<List<AnnouncementLogDto>> GetAsync();
}

View File

@@ -0,0 +1,41 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 翻牌服务接口
/// </summary>
public interface ICardFlipService
{
/// <summary>
/// 获取本周翻牌任务状态
/// </summary>
/// <returns></returns>
Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync();
/// <summary>
/// 翻牌
/// </summary>
/// <param name="input">翻牌输入</param>
/// <returns></returns>
Task<FlipCardOutput> FlipCardAsync(FlipCardInput input);
/// <summary>
/// 使用邀请码解锁翻牌次数
/// </summary>
/// <param name="input">邀请码输入</param>
/// <returns></returns>
Task UseInviteCodeAsync(UseInviteCodeInput input);
/// <summary>
/// 获取我的邀请码信息
/// </summary>
/// <returns></returns>
Task<InviteCodeOutput> GetMyInviteCodeAsync();
/// <summary>
/// 生成我的邀请码(如果没有)
/// </summary>
/// <returns></returns>
Task<string> GenerateMyInviteCodeAsync();
}

View File

@@ -0,0 +1,35 @@
using Volo.Abp.Application.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 模型服务接口
/// </summary>
public interface IModelService
{
/// <summary>
/// 获取模型库列表(公开接口,无需登录)
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>分页模型列表</returns>
Task<PagedResultDto<ModelLibraryDto>> GetListAsync(ModelLibraryGetListInput input);
/// <summary>
/// 获取供应商列表(公开接口,无需登录)
/// </summary>
/// <returns>供应商列表</returns>
Task<List<string>> GetProviderListAsync();
/// <summary>
/// 获取模型类型选项列表(公开接口,无需登录)
/// </summary>
/// <returns>模型类型选项</returns>
Task<List<ModelTypeOption>> GetModelTypeOptionsAsync();
/// <summary>
/// 获取API类型选项列表公开接口无需登录
/// </summary>
/// <returns>API类型选项</returns>
Task<List<ModelApiTypeOption>> GetApiTypeOptionsAsync();
}

View File

@@ -30,4 +30,11 @@ public interface IPayService : IApplicationService
/// <param name="input">查询订单状态输入</param>
/// <returns>订单状态信息</returns>
Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input);
/// <summary>
/// 获取商品列表
/// </summary>
/// <param name="input">获取商品列表输入</param>
/// <returns>商品列表</returns>
Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input);
}

View File

@@ -1,4 +1,6 @@
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
public interface IRechargeService
{
@@ -6,4 +8,11 @@ public interface IRechargeService
/// 移除用户vip及角色
/// </summary>
Task RemoveVipRoleByExpireAsync();
/// <summary>
/// 给用户充值VIP
/// </summary>
/// <param name="input">充值输入参数</param>
/// <returns></returns>
Task RechargeVipAsync(RechargeCreateInput input);
}

View File

@@ -11,11 +11,17 @@ public interface IUsageStatisticsService
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync();
Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync(UsageStatisticsGetInput input);
/// <summary>
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync(UsageStatisticsGetInput input);
/// <summary>
/// 获取当前用户尊享服务Token用量统计
/// </summary>
/// <returns>尊享服务Token用量统计</returns>
Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync();
}

View File

@@ -0,0 +1,275 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 翻牌服务 - 应用层组合服务
/// </summary>
[Authorize]
public class CardFlipService : ApplicationService, ICardFlipService
{
private readonly CardFlipManager _cardFlipManager;
private readonly InviteCodeManager _inviteCodeManager;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ILogger<CardFlipService> _logger;
public CardFlipService(
CardFlipManager cardFlipManager,
InviteCodeManager inviteCodeManager,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ILogger<CardFlipService> logger)
{
_cardFlipManager = cardFlipManager;
_inviteCodeManager = inviteCodeManager;
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
}
/// <summary>
/// 获取本周翻牌任务状态
/// </summary>
public async Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync()
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取本周任务
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: false);
// 获取邀请码信息
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
// 统计本周邀请人数
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
//当前用户是否已填写过邀请码
var isFilledInviteCode = await _inviteCodeManager.IsFilledInviteCodeAsync(userId);
var output = new CardFlipStatusOutput
{
TotalFlips = task?.TotalFlips ?? 0,
RemainingFreeFlips = CardFlipManager.MAX_FREE_FLIPS - (task?.FreeFlipsUsed ?? 0),
RemainingBonusFlips = 0, // 已废弃
RemainingInviteFlips = CardFlipManager.MAX_INVITE_FLIPS - (task?.InviteFlipsUsed ?? 0),
CanFlip = _cardFlipManager.CanFlipCard(task),
MyInviteCode = inviteCode?.Code,
InvitedCount = invitedCount,
FlipRecords = BuildFlipRecords(task),
IsFilledInviteCode = isFilledInviteCode
};
// 生成提示信息
output.NextFlipTip = GenerateNextFlipTip(output);
return output;
}
/// <summary>
/// 翻牌
/// </summary>
public async Task<FlipCardOutput> FlipCardAsync(FlipCardInput input)
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 执行翻牌逻辑(由Manager处理验证和翻牌)
var result = await _cardFlipManager.ExecuteFlipAsync(userId, input.FlipNumber, weekStart);
// 如果中奖,发放奖励
if (result.IsWin)
{
await GrantRewardAsync(userId, result.RewardAmount, $"翻牌活动-序号{input.FlipNumber}中奖");
}
// 构建输出
var output = new FlipCardOutput
{
FlipNumber = result.FlipNumber,
IsWin = result.IsWin,
RewardAmount = result.RewardAmount,
RewardDesc = result.RewardDesc,
RemainingFlips = CardFlipManager.TOTAL_MAX_FLIPS - input.FlipNumber
};
return output;
}
/// <summary>
/// 使用邀请码解锁翻牌次数
/// </summary>
public async Task UseInviteCodeAsync(UseInviteCodeInput input)
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取本周任务
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true);
// 验证是否已经使用了所有邀请解锁次数
if (task.InviteFlipsUsed >= CardFlipManager.MAX_INVITE_FLIPS)
{
throw new UserFriendlyException("本周邀请解锁次数已用完");
}
// 使用邀请码(由Manager处理验证和邀请逻辑)
await _inviteCodeManager.UseInviteCodeAsync(userId, input.InviteCode);
_logger.LogInformation($"用户 {userId} 使用邀请码 {input.InviteCode} 解锁翻牌次数成功");
}
/// <summary>
/// 获取我的邀请码信息
/// </summary>
public async Task<InviteCodeOutput> GetMyInviteCodeAsync()
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取我的邀请码
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
// 统计本周邀请人数
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
// 获取邀请历史
var invitationHistory = await _inviteCodeManager.GetInvitationHistoryAsync(userId, 10);
return new InviteCodeOutput
{
MyInviteCode = inviteCode?.Code,
InvitedCount = invitedCount,
InvitationHistory = invitationHistory.Select(x => new InvitationHistoryItem
{
InvitedUserName = x.InvitedUserName,
InvitationTime = x.InvitationTime,
WeekDescription = GetWeekDescription(x.InvitationTime)
}).ToList()
};
}
/// <summary>
/// 生成我的邀请码
/// </summary>
public async Task<string> GenerateMyInviteCodeAsync()
{
var userId = CurrentUser.GetId();
// 生成邀请码(由Manager处理)
var code = await _inviteCodeManager.GenerateInviteCodeForUserAsync(userId);
return code;
}
#region
/// <summary>
/// 构建翻牌记录列表
/// </summary>
private List<CardFlipRecord> BuildFlipRecords(CardFlipTaskAggregateRoot? task)
{
var records = new List<CardFlipRecord>();
// 获取已翻牌的顺序
var flippedOrder = task != null ? _cardFlipManager.GetFlippedOrder(task) : new List<int>();
var flippedNumbers = new HashSet<int>(flippedOrder);
// 获取中奖记录
var winRecords = task?.WinRecords ?? new Dictionary<int, long>();
// 构建记录按照原始序号1-10排列
for (int i = 1; i <= CardFlipManager.TOTAL_MAX_FLIPS; i++)
{
var record = new CardFlipRecord
{
FlipNumber = i,
IsFlipped = flippedNumbers.Contains(i),
IsWin = false,
FlipTypeDesc = CardFlipManager.GetFlipTypeDesc(i),
// 设置在翻牌顺序中的位置0表示未翻>0表示第几个翻的
FlipOrderIndex = flippedOrder.IndexOf(i) >= 0 ? flippedOrder.IndexOf(i) + 1 : 0
};
// 设置中奖信息
// 判断这张卡是第几次翻的
if (task != null && flippedNumbers.Contains(i))
{
var flipOrderIndex = flippedOrder.IndexOf(i) + 1; // 第几次翻的1-based
// 检查这次翻牌是否中奖
if (winRecords.TryGetValue(flipOrderIndex, out var rewardAmount))
{
record.IsWin = true;
record.RewardAmount = rewardAmount;
}
}
records.Add(record);
}
return records;
}
/// <summary>
/// 生成下次翻牌提示
/// </summary>
private string GenerateNextFlipTip(CardFlipStatusOutput status)
{
if (status.TotalFlips >= CardFlipManager.TOTAL_MAX_FLIPS)
{
return "本周翻牌次数已用完,请下周再来!";
}
if (status.RemainingFreeFlips > 0)
{
return $"本周您还有{status.RemainingFreeFlips}次免费翻牌机会";
}
else if (status.RemainingInviteFlips > 0)
{
if (status.TotalFlips >= 7)
{
return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,且必中大奖!每次中奖最大额度将翻倍!";
}
return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,必中大奖!每次中奖最大额度将翻倍!";
}
return "继续加油!";
}
/// <summary>
/// 发放奖励
/// </summary>
private async Task GrantRewardAsync(Guid userId, long amount, string description)
{
var premiumPackage = new PremiumPackageAggregateRoot(userId, amount, description)
{
PurchaseAmount = 0, // 奖励不需要付费
Remark = $"翻牌活动奖励:{amount / 10000}w tokens"
};
await _premiumPackageRepository.InsertAsync(premiumPackage);
_logger.LogInformation($"用户 {userId} 获得翻牌奖励 {amount / 10000}w tokens");
}
/// <summary>
/// 获取周描述
/// </summary>
private string GetWeekDescription(DateTime date)
{
var weekStart = CardFlipManager.GetWeekStartDate(date);
var weekEnd = weekStart.AddDays(6);
return $"{weekStart:MM-dd} 至 {weekEnd:MM-dd}";
}
#endregion
}

View File

@@ -0,0 +1,185 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using SqlSugar;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 每日任务服务
/// </summary>
[Authorize]
public class DailyTaskService : ApplicationService
{
private readonly ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> _dailyTaskRepository;
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ILogger<DailyTaskService> _logger;
// 任务配置
private readonly Dictionary<int, (long RequiredTokens, long RewardTokens, string Name, string Description)>
_taskConfigs = new()
{
{ 1, (10000000, 1000000, "尊享包1000w token任务", "累积使用尊享包 1000w token") }, // 1000w消耗 -> 100w奖励
{ 2, (30000000, 2000000, "尊享包3000w token任务", "累积使用尊享包 3000w token") } // 3000w消耗 -> 200w奖励
};
public DailyTaskService(
ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> dailyTaskRepository,
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ILogger<DailyTaskService> logger)
{
_dailyTaskRepository = dailyTaskRepository;
_messageRepository = messageRepository;
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
}
/// <summary>
/// 获取今日任务状态
/// </summary>
/// <returns></returns>
public async Task<DailyTaskStatusOutput> GetTodayTaskStatusAsync()
{
var userId = CurrentUser.GetId();
var today = DateTime.Today;
// 1. 统计今日尊享包Token消耗量
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
// 2. 查询今日已领取的任务
var claimedTasks = await _dailyTaskRepository._DbQueryable
.Where(x => x.UserId == userId && x.TaskDate == today)
.Select(x => new { x.TaskLevel, x.IsRewarded })
.ToListAsync();
// 3. 构建任务列表
var tasks = new List<DailyTaskItem>();
foreach (var (level, config) in _taskConfigs)
{
var claimed = claimedTasks.FirstOrDefault(x => x.TaskLevel == level);
int status;
if (claimed != null && claimed.IsRewarded)
{
status = 2; // 已领取
}
else if (todayConsumed >= config.RequiredTokens)
{
status = 1; // 可领取
}
else
{
status = 0; // 未完成
}
var progress = todayConsumed >= config.RequiredTokens
? 100
: Math.Round((decimal)todayConsumed / config.RequiredTokens * 100, 2);
tasks.Add(new DailyTaskItem
{
Level = level,
Name = config.Name,
Description = config.Description,
RequiredTokens = config.RequiredTokens,
RewardTokens = config.RewardTokens,
Status = status,
Progress = progress
});
}
return new DailyTaskStatusOutput
{
TodayConsumedTokens = todayConsumed,
Tasks = tasks
};
}
/// <summary>
/// 领取任务奖励
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task ClaimTaskRewardAsync(ClaimTaskRewardInput input)
{
var userId = CurrentUser.GetId();
var today = DateTime.Today;
// 1. 验证任务等级
if (!_taskConfigs.TryGetValue(input.TaskLevel, out var taskConfig))
{
throw new UserFriendlyException($"无效的任务等级: {input.TaskLevel}");
}
// 2. 检查是否已领取
var existingRecord = await _dailyTaskRepository._DbQueryable
.Where(x => x.UserId == userId && x.TaskDate == today && x.TaskLevel == input.TaskLevel)
.FirstAsync();
if (existingRecord != null)
{
throw new UserFriendlyException("今日该任务奖励已领取,请明天再来!");
}
// 3. 验证今日Token消耗是否达标
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
if (todayConsumed < taskConfig.RequiredTokens)
{
throw new UserFriendlyException(
$"Token消耗未达标需要 {taskConfig.RequiredTokens / 10000}w当前 {todayConsumed / 10000}w");
}
// 4. 创建奖励包(使用 PremiumPackageManager
var premiumPackage =
new PremiumPackageAggregateRoot(userId, taskConfig.RewardTokens, $"每日任务:{taskConfig.Name}")
{
PurchaseAmount = 0, // 奖励不需要付费
Remark = $"{today:yyyy-MM-dd} 每日任务奖励"
};
await _premiumPackageRepository.InsertAsync(premiumPackage);
// 5. 记录领取记录
var record = new DailyTaskRewardRecordAggregateRoot(userId, input.TaskLevel, today, taskConfig.RewardTokens)
{
Remark = $"完成任务{input.TaskLevel},名称:{taskConfig.Name},消耗 {todayConsumed / 10000}w token"
};
await _dailyTaskRepository.InsertAsync(record);
_logger.LogInformation(
$"用户 {userId} 领取每日任务 {input.TaskLevel} 奖励成功,获得 {taskConfig.RewardTokens / 10000}w tokens");
}
/// <summary>
/// 获取今日尊享包Token消耗量
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="today">今日日期</param>
/// <returns>消耗的Token总数</returns>
private async Task<long> GetTodayPremiumTokenConsumptionAsync(Guid userId, DateTime today)
{
var tomorrow = today.AddDays(1);
// 查询今日所有使用尊享包模型的消息role=system 表示消耗)
var totalTokens = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.Role == "system") // system角色表示实际消耗
.Where(x => PremiumPackageConst.ModeIds.Contains(x.ModelId)) // 尊享包模型
.Where(x => x.CreationTime >= today && x.CreationTime < tomorrow)
.SumAsync(x => x.TokenUsage.TotalTokenCount);
return totalTokens;
}
}

View File

@@ -0,0 +1,151 @@
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;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.AiHub.Domain.Extensions;
namespace Yi.Framework.AiHub.Application.Services;
public class AiAccountService : ApplicationService
{
private IAccountService _accountService;
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
private ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
private ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
public AiAccountService(
IAccountService accountService,
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
{
_accountService = accountService;
_userRepository = userRepository;
_rechargeRepository = rechargeRepository;
_premiumPackageRepository = premiumPackageRepository;
}
/// <summary>
/// 获取ai用户信息
/// </summary>
/// <returns></returns>
[Authorize]
[HttpGet("account/ai")]
public async Task<AiUserRoleMenuDto> GetAsync()
{
var userId = CurrentUser.GetId();
var userAccount = await _accountService.GetAsync(null, null, userId: CurrentUser.GetId());
var output = userAccount.Adapt<AiUserRoleMenuDto>();
// 是否绑定服务号
output.IsBindFuwuhao = await _userRepository.IsAnyAsync(x => userId == x.UserId);
// 是否为VIP用户
output.IsVip = CurrentUser.IsAiVip();
// 获取VIP到期时间
if (output.IsVip)
{
var recharges = await _rechargeRepository._DbQueryable
.Where(x => x.UserId == userId)
.ToListAsync();
if (recharges.Any())
{
// 如果有任何一个充值记录的过期时间为null说明是永久VIP
if (recharges.Any(x => !x.ExpireDateTime.HasValue))
{
output.VipExpireTime = null; // 永久VIP
}
else
{
// 取最大的过期时间
output.VipExpireTime = recharges
.Where(x => x.ExpireDateTime.HasValue)
.Max(x => x.ExpireDateTime);
}
}
}
return output;
}
/// <summary>
/// 获取利润统计数据
/// </summary>
/// <param name="currentCost">当前成本(RMB)</param>
/// <returns></returns>
[Authorize]
[HttpGet("account/profit-statistics")]
public async Task<string> GetProfitStatisticsAsync([FromQuery] decimal currentCost)
{
if (CurrentUser.UserName != "Guo" && CurrentUser.UserName != "cc")
{
throw new UserFriendlyException("您暂无权限访问");
}
// 1. 获取尊享包总消耗和剩余库存
var premiumPackages = await _premiumPackageRepository._DbQueryable.ToListAsync();
long totalUsedTokens = premiumPackages.Sum(p => p.UsedTokens);
long totalRemainingTokens = premiumPackages.Sum(p => p.RemainingTokens);
// 2. 计算1亿Token成本
decimal costPerHundredMillion = totalUsedTokens > 0
? currentCost / (totalUsedTokens / 100000000m)
: 0;
// 3. 计算总成本(剩余+已使用的总成本)
long totalTokens = totalUsedTokens + totalRemainingTokens;
decimal totalCost = totalTokens > 0
? (totalTokens / 100000000m) * costPerHundredMillion
: 0;
// 4. 获取总收益(RechargeType=PremiumPackage的充值金额总和)
decimal totalRevenue = await _rechargeRepository._DbQueryable
.Where(x => x.RechargeType == Domain.Shared.Enums.RechargeTypeEnum.PremiumPackage)
.SumAsync(x => x.RechargeAmount);
// 5. 计算利润率
decimal profitRate = totalCost > 0
? (totalRevenue / totalCost - 1) * 100
: 0;
// 6. 按200售价计算成本
decimal costAt200Price = totalRevenue > 0
? (totalCost / totalRevenue) * 200
: 0;
// 7. 格式化输出
var today = DateTime.Now;
string dayOfWeek = today.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
string weekDay = dayOfWeek switch
{
"星期一" => "周1",
"星期二" => "周2",
"星期三" => "周3",
"星期四" => "周4",
"星期五" => "周5",
"星期六" => "周6",
"星期日" => "周日",
_ => dayOfWeek
};
var result = $@"{today:M月d日} {weekDay}
尊享包已消耗({totalUsedTokens / 100000000m:F2}亿){totalUsedTokens}
尊享包剩余库存({totalRemainingTokens / 100000000m:F2}亿){totalRemainingTokens}
当前成本:{currentCost:F2}RMB
1亿Token成本:{costPerHundredMillion:F2} RMB=1亿 Token
总成本:{totalCost:F2} RMB
总收益:{totalRevenue:F2}RMB
利润率: {profitRate:F1}%
按200售价来算,成本在{costAt200Price:F2}";
return result;
}
}

View File

@@ -0,0 +1,68 @@
using Mapster;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 公告服务
/// </summary>
public class AnnouncementService : ApplicationService, IAnnouncementService
{
private readonly ISqlSugarRepository<AnnouncementAggregateRoot> _announcementRepository;
private readonly IConfiguration _configuration;
private readonly IDistributedCache<AnnouncementCacheDto> _announcementCache;
private const string AnnouncementCacheKey = "AiHub:Announcement";
public AnnouncementService(
ISqlSugarRepository<AnnouncementAggregateRoot> announcementRepository,
IConfiguration configuration,
IDistributedCache<AnnouncementCacheDto> announcementCache)
{
_announcementRepository = announcementRepository;
_configuration = configuration;
_announcementCache = announcementCache;
}
/// <summary>
/// 获取公告信息
/// </summary>
public async Task<List<AnnouncementLogDto>> GetAsync()
{
// 使用 GetOrAddAsync 从缓存获取或添加数据缓存1小时
var cacheData = await _announcementCache.GetOrAddAsync(
AnnouncementCacheKey,
async () => await LoadAnnouncementDataAsync(),
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
}
);
return cacheData?.Logs ?? new List<AnnouncementLogDto>();
}
/// <summary>
/// 从数据库加载公告数据
/// </summary>
private async Task<AnnouncementCacheDto> LoadAnnouncementDataAsync()
{
// 查询所有公告日志,按日期降序排列
var logs = await _announcementRepository._DbQueryable
.OrderByDescending(x => x.StartTime)
.ToListAsync();
// 转换为 DTO
var logDtos = logs.Adapt<List<AnnouncementLogDto>>();
return new AnnouncementCacheDto
{
Logs = logDtos
};
}
}

View File

@@ -16,6 +16,7 @@ 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.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
@@ -35,17 +36,19 @@ public class AiChatService : ApplicationService
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly ILogger<AiChatService> _logger;
private readonly AiGateWayManager _aiGateWayManager;
private readonly PremiumPackageManager _premiumPackageManager;
public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager)
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager)
{
_httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager;
_aiModelRepository = aiModelRepository;
_logger = logger;
_aiGateWayManager = aiGateWayManager;
_premiumPackageManager = premiumPackageManager;
}
@@ -70,6 +73,7 @@ public class AiChatService : ApplicationService
{
var output = await _aiModelRepository._DbQueryable
.Where(x => x.ModelType == ModelTypeEnum.Chat)
.Where(x=>x.ModelApiType==ModelApiTypeEnum.OpenAi)
.OrderByDescending(x => x.OrderNum)
.Select(x => new ModelGetListOutput
{
@@ -84,7 +88,8 @@ public class AiChatService : ApplicationService
SystemPrompt = null,
ApiHost = null,
ApiKey = null,
Remark = x.Description
Remark = x.Description,
IsPremiumPackage = PremiumPackageConst.ModeIds.Contains(x.ModelId)
}).ToListAsync();
return output;
}
@@ -118,8 +123,56 @@ public class AiChatService : ApplicationService
}
}
//如果是尊享包服务,需要校验是是否尊享包足够
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, cancellationToken);
CurrentUser.Id, sessionId, null, cancellationToken);
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("ai-chat/FileMaster/send")]
public async Task PostFileMasterSendAsync([FromBody] ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
if (!string.IsNullOrWhiteSpace(input.Model))
{
throw new BusinessException("当前接口不支持第三方使用");
}
if (CurrentUser.IsAuthenticated)
{
await _aiBlacklistManager.VerifiyAiBlacklist(CurrentUser.GetId());
if (CurrentUser.IsAiVip())
{
input.Model = "gpt-5-chat";
}
else
{
input.Model = "gpt-4.1-mini";
}
}
else
{
input.Model = "DeepSeek-R1-0528";
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, null, null, cancellationToken);
}
}

View File

@@ -0,0 +1,117 @@
using Mapster;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.AiHub.Domain.Shared.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services.Chat;
/// <summary>
/// 模型服务
/// </summary>
public class ModelService : ApplicationService, IModelService
{
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
public ModelService(ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
{
_modelRepository = modelRepository;
}
/// <summary>
/// 获取模型库列表(公开接口,无需登录)
/// </summary>
public async Task<PagedResultDto<ModelLibraryDto>> GetListAsync(ModelLibraryGetListInput input)
{
RefAsync<int> total = 0;
// 查询所有未删除的模型使用WhereIF动态添加筛选条件
var models = await _modelRepository._DbQueryable
.Where(x => !x.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), x =>
x.Name.Contains(input.SearchKey) || x.ModelId.Contains(input.SearchKey))
.WhereIF(input.ProviderNames is not null, x =>
input.ProviderNames.Contains(x.ProviderName))
.WhereIF(input.ModelTypes is not null, x =>
input.ModelTypes.Contains(x.ModelType))
.WhereIF(input.ModelApiTypes is not null, x =>
input.ModelApiTypes.Contains(x.ModelApiType))
.WhereIF(input.IsPremiumOnly == true, x =>
PremiumPackageConst.ModeIds.Contains(x.ModelId))
.OrderBy(x => x.OrderNum)
.OrderBy(x => x.Name)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
// 转换为DTO
var result = models.Select(model => new ModelLibraryDto
{
ModelId = model.ModelId,
Name = model.Name,
Description = model.Description,
ModelType = model.ModelType,
ModelTypeName = model.ModelType.GetDescription(),
ModelApiType = model.ModelApiType,
ModelApiTypeName = model.ModelApiType.GetDescription(),
MultiplierShow = model.MultiplierShow,
ProviderName = model.ProviderName,
IconUrl = model.IconUrl,
IsPremium = PremiumPackageConst.ModeIds.Contains(model.ModelId)
}).ToList();
return new PagedResultDto<ModelLibraryDto>(total, result);
}
/// <summary>
/// 获取供应商列表(公开接口,无需登录)
/// </summary>
public async Task<List<string>> GetProviderListAsync()
{
var providers = await _modelRepository._DbQueryable
.Where(x => !x.IsDeleted)
.Where(x => !string.IsNullOrEmpty(x.ProviderName))
.GroupBy(x => x.ProviderName)
.OrderBy(x => x.ProviderName)
.Select(x => x.ProviderName)
.ToListAsync();
return providers;
}
/// <summary>
/// 获取模型类型选项列表(公开接口,无需登录)
/// </summary>
public Task<List<ModelTypeOption>> GetModelTypeOptionsAsync()
{
var options = Enum.GetValues<ModelTypeEnum>()
.Select(e => new ModelTypeOption
{
Label = e.GetDescription(),
Value = (int)e
})
.ToList();
return Task.FromResult(options);
}
/// <summary>
/// 获取API类型选项列表公开接口无需登录
/// </summary>
public Task<List<ModelApiTypeOption>> GetApiTypeOptionsAsync()
{
var options = Enum.GetValues<ModelApiTypeEnum>()
.Select(e => new ModelApiTypeOption
{
Label = e.GetDescription(),
Value = (int)e
})
.ToList();
return Task.FromResult(options);
}
}

View File

@@ -0,0 +1,245 @@
using Dm.util;
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.Token;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.Ddd.Application.Contracts;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// Token服务
/// </summary>
[Authorize]
public class TokenService : ApplicationService
{
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
public TokenService(
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
{
_tokenRepository = tokenRepository;
_usageStatisticsRepository = usageStatisticsRepository;
}
/// <summary>
/// 获取当前用户的Token列表
/// </summary>
[HttpGet("token/list")]
public async Task<PagedResultDto<TokenGetListOutputDto>> GetListAsync([FromQuery] PagedAllResultRequestDto input)
{
RefAsync<int> total = 0;
var userId = CurrentUser.GetId();
var tokens = await _tokenRepository._DbQueryable
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.CreationTime)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
if (!tokens.Any())
{
return new PagedResultDto<TokenGetListOutputDto>();
}
// 获取尊享包模型ID列表
var premiumModelIds = PremiumPackageConst.ModeIds;
// 批量查询所有Token的尊享包已使用额度
var tokenIds = tokens.Select(t => t.Id).ToList();
var usageStats = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId && tokenIds.Contains(x.TokenId) && premiumModelIds.Contains(x.ModelId))
.GroupBy(x => x.TokenId)
.Select(g => new
{
TokenId = g.TokenId,
UsedQuota = SqlFunc.AggregateSum(g.TotalTokenCount)
})
.ToListAsync();
var result = tokens.Select(t =>
{
var usedQuota = usageStats.FirstOrDefault(u => u.TokenId == t.Id)?.UsedQuota ?? 0;
return new TokenGetListOutputDto
{
Id = t.Id,
Name = t.Name,
ApiKey = t.Token,
ExpireTime = t.ExpireTime,
PremiumQuotaLimit = t.PremiumQuotaLimit,
PremiumUsedQuota = usedQuota,
IsDisabled = t.IsDisabled,
CreationTime = t.CreationTime
};
}).ToList();
return new PagedResultDto<TokenGetListOutputDto>(total, result);
}
[HttpGet("token/select-list")]
public async Task<List<TokenSelectListOutputDto>> GetSelectListAsync()
{
var userId = CurrentUser.GetId();
var tokens = await _tokenRepository._DbQueryable
.Where(x => x.UserId == userId)
.OrderBy(x => x.IsDisabled)
.OrderByDescending(x => x.CreationTime)
.Select(x => new TokenSelectListOutputDto
{
TokenId = x.Id,
Name = x.Name,
IsDisabled = x.IsDisabled
}).ToListAsync();
tokens.Insert(0,new TokenSelectListOutputDto
{
TokenId = Guid.Empty,
Name = "默认",
IsDisabled = false
});
return tokens;
}
/// <summary>
/// 创建Token
/// </summary>
[HttpPost("token")]
public async Task<TokenGetListOutputDto> CreateAsync([FromBody] TokenCreateInput input)
{
var userId = CurrentUser.GetId();
// 检查用户是否为VIP
if (!CurrentUser.IsAiVip())
{
throw new UserFriendlyException("充值成为Vip畅享第三方token服务");
}
// 检查名称是否重复
var exists = await _tokenRepository._DbQueryable
.AnyAsync(x => x.UserId == userId && x.Name == input.Name);
if (exists)
{
throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称");
}
var token = new TokenAggregateRoot(userId, input.Name)
{
ExpireTime = input.ExpireTime,
PremiumQuotaLimit = input.PremiumQuotaLimit
};
await _tokenRepository.InsertAsync(token);
return new TokenGetListOutputDto
{
Id = token.Id,
Name = token.Name,
ApiKey = token.Token,
ExpireTime = token.ExpireTime,
PremiumQuotaLimit = token.PremiumQuotaLimit,
PremiumUsedQuota = 0,
IsDisabled = token.IsDisabled,
CreationTime = token.CreationTime
};
}
/// <summary>
/// 编辑Token
/// </summary>
[HttpPut("token")]
public async Task UpdateAsync([FromBody] TokenUpdateInput input)
{
var userId = CurrentUser.GetId();
var token = await _tokenRepository._DbQueryable
.FirstAsync(x => x.Id == input.Id && x.UserId == userId);
if (token is null)
{
throw new UserFriendlyException("Token不存在或无权限操作");
}
// 检查名称是否重复(排除自己)
var exists = await _tokenRepository._DbQueryable
.AnyAsync(x => x.UserId == userId && x.Name == input.Name && x.Id != input.Id);
if (exists)
{
throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称");
}
token.Name = input.Name;
token.ExpireTime = input.ExpireTime;
token.PremiumQuotaLimit = input.PremiumQuotaLimit;
await _tokenRepository.UpdateAsync(token);
}
/// <summary>
/// 删除Token
/// </summary>
[HttpDelete("token/{id}")]
public async Task DeleteAsync(Guid id)
{
var userId = CurrentUser.GetId();
var token = await _tokenRepository._DbQueryable
.FirstAsync(x => x.Id == id && x.UserId == userId);
if (token is null)
{
throw new UserFriendlyException("Token不存在或无权限操作");
}
await _tokenRepository.DeleteAsync(token);
}
/// <summary>
/// 启用Token
/// </summary>
[HttpPost("token/{id}/enable")]
public async Task EnableAsync(Guid id)
{
var userId = CurrentUser.GetId();
var token = await _tokenRepository._DbQueryable
.FirstAsync(x => x.Id == id && x.UserId == userId);
if (token is null)
{
throw new UserFriendlyException("Token不存在或无权限操作");
}
token.Enable();
await _tokenRepository.UpdateAsync(token);
}
/// <summary>
/// 禁用Token
/// </summary>
[HttpPost("token/{id}/disable")]
public async Task DisableAsync(Guid id)
{
var userId = CurrentUser.GetId();
var token = await _tokenRepository._DbQueryable
.FirstAsync(x => x.Id == id && x.UserId == userId);
if (token is null)
{
throw new UserFriendlyException("Token不存在或无权限操作");
}
token.Disable();
await _tokenRepository.UpdateAsync(token);
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.FileMaster;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Application.Services.FileMaster;
public class FileMasterService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AiGateWayManager _aiGateWayManager;
private readonly AiBlacklistManager _aiBlacklistManager;
public FileMasterService(IHttpContextAccessor httpContextAccessor, AiGateWayManager aiGateWayManager,
AiBlacklistManager aiBlacklistManager)
{
_httpContextAccessor = httpContextAccessor;
_aiGateWayManager = aiGateWayManager;
_aiBlacklistManager = aiBlacklistManager;
}
/// <summary>
/// 校验下一步
/// </summary>
/// <returns></returns>
[HttpPost("FileMaster/VerifyNext")]
public Task<string> VerifyNextAsync(VerifyNextInput input)
{
if (!CurrentUser.IsAuthenticated)
{
if (input.DirectoryCount + input.FileCount >= 20)
{
throw new UserFriendlyException("未登录用户文件夹与文件数量不能大于20个请登录后解锁全部功能");
}
}
else
{
if (input.DirectoryCount + input.FileCount >= 100)
{
throw new UserFriendlyException("为防止无限制暴力使用当前文件整理大师Vip最多支持100文件与文件夹数量");
}
}
return Task.FromResult("success");
}
/// <summary>
/// 对话
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("FileMaster/chat/completions")]
public async Task ChatCompletionsAsync([FromBody] ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
if (CurrentUser.IsAuthenticated)
{
input.Model = "gpt-5-chat";
}
else
{
input.Model = "gpt-5-chat";
}
Guid? userId = CurrentUser.IsAuthenticated ? CurrentUser.GetId() : null;
if (userId is not null)
{
await _aiBlacklistManager.VerifiyAiBlacklist(userId.Value);
}
//ai网关代理httpcontext
if (input.Stream == true)
{
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId, null, null, cancellationToken);
}
else
{
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null, null,
cancellationToken);
}
}
}

View File

@@ -0,0 +1,308 @@
using System.Text;
using System.Xml.Serialization;
using Medallion.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services.Fuwuhao;
/// <summary>
/// 服务号服务
/// </summary>
public class FuwuhaoService : ApplicationService
{
private readonly ILogger<FuwuhaoService> _logger;
private readonly IHttpContextAccessor _accessor;
private readonly FuwuhaoManager _fuwuhaoManager;
private IDistributedCache<SceneCacheDto> _sceneCache;
private IDistributedCache<string> _openIdToSceneCache;
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
private IAccountService _accountService;
private IFileService _fileService;
public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService,
IDistributedCache<string> openIdToSceneCache, ISqlSugarRepository<AiUserExtraInfoEntity> userRepositroy)
{
_logger = logger;
_accessor = accessor;
_fuwuhaoManager = fuwuhaoManager;
_sceneCache = sceneCache;
_accountService = accountService;
_fileService = fileService;
_openIdToSceneCache = openIdToSceneCache;
_userRepository = userRepositroy;
}
/// <summary>
/// 服务器号测试回调
/// </summary>
/// <returns></returns>
[HttpGet("fuwuhao/callback")]
public async Task<string> GetCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
[FromQuery] string nonce, [FromQuery] string echostr)
{
return echostr;
}
/// <summary>
/// 服务号关注回调
/// </summary>
/// <param name="signature"></param>
/// <param name="timestamp"></param>
/// <param name="nonce"></param>
/// <returns></returns>
[HttpPost("fuwuhao/callback")]
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
[FromQuery] string nonce)
{
_fuwuhaoManager.ValidateCallback(signature, timestamp, nonce);
var request = _accessor.HttpContext.Request;
// 1. 读取原始 XML 内容
using var reader = new StreamReader(request.Body, Encoding.UTF8);
var xmlString = await reader.ReadToEndAsync();
var serializer = new XmlSerializer(typeof(FuwuhaoCallModel));
using var stringReader = new StringReader(xmlString);
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
//获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可
var scene = body.EventKey.Replace("qrscene_", "");
if (!(body.Event is "SCAN" or "subscribe"))
{
throw new UserFriendlyException("当前回调只处理扫码 与 关注");
}
if (scene is null)
{
throw new UserFriendlyException("服务号返回无场景值");
}
//制作幂等
await using (var handle =
await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}"))
{
if (handle == null)
{
return "success"; // 跳过直接返回成功
}
var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}");
if (cache == null)
{
return "success"; // 跳过直接返回成功
}
if (cache.SceneResult != SceneResultEnum.Wait)
{
return "success"; // 跳过直接返回成功
}
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去相当于用户10s扫完app后轮询要在10秒内完成
var scenResult =
await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
cache.SceneResult = scenResult.SceneResult;
cache.UserId = scenResult.UserId;
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", cache,
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
//如果是注册将OpenId与Scene进行绑定代表用户有30分钟进行注册
if (scenResult.SceneResult == SceneResultEnum.Register)
{
await _openIdToSceneCache.SetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{body.FromUserName}", scene,
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
var replyMessage =
_fuwuhaoManager.BuildRegisterMessage(body.FromUserName);
return replyMessage;
}
}
return "success";
}
/// <summary>
/// 创建带参数的二维码
/// </summary>
/// <returns>二维码URL</returns>
[HttpPost("fuwuhao/qrcode")]
public async Task<QrCodeOutput> GetQrCodeAsync([FromQuery] SceneTypeEnum sceneType)
{
if (sceneType == SceneTypeEnum.Bind && CurrentUser.Id is null)
{
throw new UserFriendlyException("绑定微信,需登录用户,请重新登录后重试");
}
//生成一个随机场景值
var scene = Guid.NewGuid().ToString("N");
var qrCodeUrl = await _fuwuhaoManager.CreateQrCodeAsync(scene);
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto()
{
UserId = CurrentUser.IsAuthenticated ? CurrentUser.GetId() : null,
SceneType = sceneType
}, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
return new QrCodeOutput()
{
QrCodeUrl = qrCodeUrl,
Scene = scene
};
}
/// <summary>
/// 扫码登录/注册/绑定,轮询接口
/// </summary>
/// <param name="scene"></param>
/// <returns></returns>
[HttpGet("fuwuhao/qrcode/result")]
public async Task<QrCodeResultOutput> GetQrCodeResultAsync([FromQuery] string scene)
{
var output = new QrCodeResultOutput();
var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}");
if (cache is null)
{
output.SceneResult = SceneResultEnum.Expired;
return output;
}
output.SceneResult = cache.SceneResult;
switch (output.SceneResult)
{
case SceneResultEnum.Login:
if (cache.UserId is null)
{
throw new ApplicationException("获取用户id异常请重试");
}
var loginInfo = await _accountService.PostLoginAsync(cache.UserId!.Value);
output.Token = loginInfo.Token;
output.RefreshToken = loginInfo.RefreshToken;
break;
}
return output;
}
/// <summary>
/// 注册账号需要微信code
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
[HttpGet("fuwuhao/register")]
public async Task<IActionResult> RegisterByCodeAsync([FromQuery] string code)
{
var message = await RegisterByCodeForMessageAsync(code);
//var message = "恭喜注册";
var filePath = Path.Combine("wwwroot", "aihub", "auth.html");
var html = await File.ReadAllTextAsync(filePath);
var result = new ContentResult
{
Content = html.Replace("{{message}}", message),
ContentType = "text/html",
StatusCode = 200
};
return result;
}
private async Task<string> RegisterByCodeForMessageAsync(string code)
{
//根据code获取到openid、微信用户昵称、头像
var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code);
if (userInfo is null)
{
return "当前注册已经失效,请重新扫码注册!";
}
var scene = await _openIdToSceneCache.GetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{userInfo.OpenId}");
if (scene is null)
{
return "当前注册已经过期,请重新扫码注册!";
}
var files = new FormFileCollection();
// 下载头像并添加到系统文件中
if (!string.IsNullOrEmpty(userInfo.HeadImgUrl))
{
using var httpClient = new HttpClient();
var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl);
var imageStream = new MemoryStream(imageBytes);
// 从URL中提取文件扩展名默认为png
var fileName = $"avatar_{userInfo.OpenId}.png";
var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/png"
};
files.Add(formFile);
}
var result = await _fileService.Post(files);
//由于存在查询/编辑在同一个事务操作,上锁防止并发
await using (await DistributedLock.AcquireLockAsync($"fuwuhao:RegisterLock:{userInfo.OpenId}",
TimeSpan.FromMinutes(1)))
{
if (await _userRepository.IsAnyAsync(x => x.FuwuhaoOpenId == userInfo.OpenId))
{
return "你已注册过意社区账号!";
}
Guid userId;
try
{
userId = await _accountService.PostSystemRegisterAsync(new RegisterDto
{
UserName = $"wx{Random.Shared.Next(100000, 999999)}",
Password = Guid.NewGuid().ToString("N"),
Phone = null,
Email = null,
Nick = userInfo.Nickname,
Icon = result.FirstOrDefault()?.Id.ToString()
});
}
catch (UserFriendlyException e)
{
return e.Message;
}
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(userId, userInfo.OpenId));
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto
{
UserId = userId,
SceneResult = SceneResultEnum.Login
},
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
}
return "恭喜你成功注册意社区账号!";
}
}
[XmlRoot("xml")]
public class FuwuhaoCallModel
{
[XmlElement("ToUserName")] public string ToUserName { get; set; }
[XmlElement("FromUserName")] public string FromUserName { get; set; }
[XmlElement("CreateTime")] public string CreateTime { get; set; }
[XmlElement("MsgType")] public string MsgType { get; set; }
[XmlElement("Event")] public string Event { get; set; }
[XmlElement("EventKey")] public string EventKey { get; set; }
[XmlElement("Ticket")] public string Ticket { get; set; }
}

View File

@@ -2,13 +2,19 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
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.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
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.Dtos.OpenAi.Responses;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
@@ -21,9 +27,13 @@ public class OpenApiService : ApplicationService
private readonly AiGateWayManager _aiGateWayManager;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly IAccountService _accountService;
private readonly PremiumPackageManager _premiumPackageManager;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager)
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager,
IAccountService accountService, PremiumPackageManager premiumPackageManager)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
@@ -31,6 +41,8 @@ public class OpenApiService : ApplicationService
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
_aiBlacklistManager = aiBlacklistManager;
_accountService = accountService;
_premiumPackageManager = premiumPackageManager;
}
/// <summary>
@@ -44,22 +56,37 @@ public class OpenApiService : ApplicationService
{
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model);
var userId = tokenValidation.UserId;
var tokenId = tokenValidation.TokenId;
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext
if (input.Stream == true)
{
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId, null, cancellationToken);
userId, null, tokenId, cancellationToken);
}
else
{
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null,
null, tokenId,
cancellationToken);
}
}
/// <summary>
/// 图片生成
/// </summary>
@@ -69,11 +96,15 @@ public class OpenApiService : ApplicationService
public async Task ImagesGenerationsAsync([FromBody] ImageCreateRequest input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
Intercept(httpContext);
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model);
var userId = tokenValidation.UserId;
var tokenId = tokenValidation.TokenId;
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input);
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input, tokenId);
}
/// <summary>
/// 向量生成
/// </summary>
@@ -83,9 +114,12 @@ public class OpenApiService : ApplicationService
public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
Intercept(httpContext);
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model);
var userId = tokenValidation.UserId;
var tokenId = tokenValidation.TokenId;
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input);
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input, tokenId);
}
@@ -114,17 +148,141 @@ public class OpenApiService : ApplicationService
};
}
/// <summary>
/// Anthropic对话尊享服务专用
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/messages")]
public async Task MessagesAsync([FromBody] AnthropicInput input,
CancellationToken cancellationToken)
{
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model);
var userId = tokenValidation.UserId;
var tokenId = tokenValidation.TokenId;
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
// 验证用户是否为VIP
var userInfo = await _accountService.GetAsync(null, null, userId);
if (userInfo == null)
{
throw new UserFriendlyException("用户信息不存在");
}
// 检查是否为VIP使用RoleCodes判断
if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc")
{
throw new UserFriendlyException("该接口为尊享服务专用需要VIP权限才能使用");
}
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
//ai网关代理httpcontext
if (input.Stream)
{
await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
}
else
{
await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId,
null, tokenId,
cancellationToken);
}
}
/// <summary>
/// 响应-Openai新规范 (尊享服务专用)
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/responses")]
public async Task ResponsesAsync([FromBody] OpenAiResponsesInput input, CancellationToken cancellationToken)
{
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model);
var userId = tokenValidation.UserId;
var tokenId = tokenValidation.TokenId;
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
// 验证用户是否为VIP
var userInfo = await _accountService.GetAsync(null, null, userId);
if (userInfo == null)
{
throw new UserFriendlyException("用户信息不存在");
}
// 检查是否为VIP使用RoleCodes判断
if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc")
{
throw new UserFriendlyException("该接口为尊享服务专用需要VIP权限才能使用");
}
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
//ai网关代理httpcontext
if (input.Stream == true)
{
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
}
else
{
await _aiGateWayManager.OpenAiResponsesAsyncForStatisticsAsync(_httpContextAccessor.HttpContext, input,
userId,
null, tokenId,
cancellationToken);
}
}
#region
private string? GetTokenByHttpContext(HttpContext httpContext)
{
// 获取Authorization头
string authHeader = httpContext.Request.Headers["Authorization"];
// 优先从 x-api-key 获取
string apiKeyHeader = httpContext.Request.Headers["x-api-key"];
if (!string.IsNullOrWhiteSpace(apiKeyHeader))
{
return apiKeyHeader.Trim();
}
// 检查是否有Bearer token
if (authHeader != null && authHeader.StartsWith("Bearer "))
// 检查 Authorization 头
string authHeader = httpContext.Request.Headers["Authorization"];
if (!string.IsNullOrWhiteSpace(authHeader) &&
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return authHeader.Substring("Bearer ".Length).Trim();
}
return null;
}
private void Intercept(HttpContext httpContext)
{
if (httpContext.Request.Host.Value == "yxai.chat")
{
throw new UserFriendlyException("当前海外站点不支持大流量接口请使用转发站点https://ai.ccnetcore.com");
}
}
#endregion
}

View File

@@ -14,6 +14,8 @@ using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.SqlSugarCore.Abstractions;
using System.ComponentModel;
using System.Reflection;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
namespace Yi.Framework.AiHub.Application.Services;
@@ -26,16 +28,23 @@ public class PayService : ApplicationService, IPayService
private readonly PayManager _payManager;
private readonly ILogger<PayService> _logger;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
private readonly IRechargeService _rechargeService;
private readonly PremiumPackageManager _premiumPackageManager;
public PayService(
AlipayManager alipayManager,
PayManager payManager,
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
ILogger<PayService> logger,
ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
IRechargeService rechargeService,
PremiumPackageManager premiumPackageManager)
{
_alipayManager = alipayManager;
_payManager = payManager;
_logger = logger;
_payOrderRepository = payOrderRepository;
_rechargeService = rechargeService;
_premiumPackageManager = premiumPackageManager;
}
/// <summary>
@@ -47,14 +56,15 @@ public class PayService : ApplicationService, IPayService
[HttpPost("pay/Order")]
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
{
// 1. 通过PayManager创建订单
// 1. 通过PayManager创建订单内部会验证VIP资格
var order = await _payManager.CreateOrderAsync(input.GoodsType);
// 2. 通过AlipayManager发起页面支付
var paymentPageHtml = await _alipayManager.PaymentPageAsync(
order.GoodsName,
order.OutTradeNo,
order.TotalAmount);
order.TotalAmount,
input.ReturnUrl);
// 3. 返回结果
return new CreateOrderOutput
@@ -87,9 +97,8 @@ public class PayService : ApplicationService, IPayService
// 2. 验证签名
await _alipayManager.VerifyNotifyAsync(notifyData);
// 3. 记录支付通知
await _payManager.RecordPayNoticeAsync(notifyData,signStr);
await _payManager.RecordPayNoticeAsync(notifyData, signStr);
// 4. 更新订单状态
var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty);
@@ -99,9 +108,51 @@ public class PayService : ApplicationService, IPayService
if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus))
{
var status = ParseTradeStatus(tradeStatus);
await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
var order = await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
// 验证交易状态,只有交易成功才执行充值逻辑
if (status != TradeStatusEnum.TRADE_SUCCESS)
{
_logger.LogError($"订单 {outTradeNo} 状态为 {tradeStatus},不执行充值逻辑");
return "success";
}
// 5. 根据商品类型进行不同的处理
if (order.GoodsType.IsPremiumPackage())
{
// 处理尊享包商品:创建尊享包记录
await _premiumPackageManager.CreatePremiumPackageAsync(
order.UserId,
order.GoodsType,
order.TotalAmount,
expireMonths: null // 尊享包不设置过期时间,或者可以根据需求设置
);
_logger.LogInformation(
$"用户 {order.UserId} 购买尊享包成功,订单号:{outTradeNo},商品:{order.GoodsName}");
}
else if (order.GoodsType.IsVipService())
{
// 处理VIP服务商品充值VIP
await _rechargeService.RechargeVipAsync(new RechargeCreateInput
{
UserId = order.UserId,
RechargeAmount = order.TotalAmount,
Content = order.GoodsName,
Months = order.GoodsType.GetValidMonths(),
Remark = "自助充值",
ContactInfo = null
});
_logger.LogInformation(
$"用户 {order.UserId} 充值VIP成功订单号{outTradeNo},月数:{order.GoodsType.GetValidMonths()}");
}
else
{
_logger.LogWarning($"未知的商品类型:{order.GoodsType},订单号:{outTradeNo}");
}
}
else
{
@@ -142,6 +193,85 @@ public class PayService : ApplicationService, IPayService
};
}
/// <summary>
/// 获取商品列表
/// </summary>
/// <param name="input">获取商品列表输入</param>
/// <returns>商品列表</returns>
[HttpGet("pay/GoodsList")]
public async Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input)
{
var goodsList = new List<GoodsListOutput>();
// 获取当前用户的累加充值金额(仅已登录用户)
decimal totalRechargeAmount = 0m;
if (CurrentUser.IsAuthenticated)
{
totalRechargeAmount = await _payManager.GetUserTotalRechargeAmountAsync(CurrentUser.GetId());
}
// 遍历所有商品枚举
foreach (GoodsTypeEnum goodsType in Enum.GetValues(typeof(GoodsTypeEnum)))
{
// 如果指定了商品类别,则过滤
if (input.GoodsCategoryType.HasValue)
{
var goodsCategory = goodsType.GetGoodsCategory();
if (goodsCategory != input.GoodsCategoryType.Value)
{
continue; // 跳过不匹配的商品
}
}
var originalPrice = goodsType.GetTotalAmount();
decimal actualPrice = originalPrice;
decimal? discountAmount = null;
string? discountDescription = null;
// 如果是尊享包商品,计算折扣
if (goodsType.IsPremiumPackage())
{
if (CurrentUser.IsAuthenticated)
{
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
if (discountAmount > 0)
{
discountDescription = $"根据累积充值已优惠 ¥{discountAmount:F2}";
}
else
{
discountDescription = $"累积充值过低,暂无优惠";
}
}
else
{
discountDescription = $"登录后查看优惠";
}
}
var goodsItem = new GoodsListOutput
{
GoodsName = goodsType.GetChineseName(),
OriginalPrice = originalPrice,
ReferencePrice = goodsType.GetReferencePrice(),
GoodsPrice = actualPrice,
GoodsCategory = goodsType.GetGoodsCategory().ToString(),
Remark = goodsType.GetRemark(),
DiscountAmount = discountAmount,
DiscountDescription = discountDescription,
GoodsType = goodsType
};
goodsList.Add(goodsItem);
}
return goodsList;
}
/// <summary>
/// 获取交易状态描述
/// </summary>
@@ -165,6 +295,7 @@ public class PayService : ApplicationService, IPayService
{
return result;
}
return TradeStatusEnum.WAIT_TRADE;
}
}

View File

@@ -8,12 +8,13 @@ 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.AiHub.Domain.Shared.Enums;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services
{
public class RechargeService : ApplicationService,IRechargeService
public class RechargeService : ApplicationService, IRechargeService
{
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
private readonly ICurrentUser _currentUser;
@@ -57,15 +58,37 @@ namespace Yi.Framework.AiHub.Application.Services
[HttpPost("recharge/vip")]
public async Task RechargeVipAsync(RechargeCreateInput input)
{
DateTime? expireDateTime = null;
// 如果传入了月数,计算过期时间
if (input.Months.HasValue && input.Months.Value > 0)
{
// 直接查询该用户最大的过期时间
var maxExpireTime = await _repository._DbQueryable
.Where(x => x.RechargeType == RechargeTypeEnum.Vip)
.Where(x => x.UserId == input.UserId && x.ExpireDateTime.HasValue)
.MaxAsync(x => x.ExpireDateTime);
// 如果最大过期时间大于现在时间,从最大过期时间开始计算
// 否则从当天开始计算
DateTime baseDateTime = maxExpireTime.HasValue && maxExpireTime.Value > DateTime.Now
? maxExpireTime.Value
: DateTime.Now;
// 计算新的过期时间
expireDateTime = baseDateTime.AddMonths(input.Months.Value);
}
// 如果月数为空或0表示永久VIPExpireDateTime保持为null
// 创建充值记录
var rechargeRecord = new AiRechargeAggregateRoot
{
UserId = input.UserId,
RechargeAmount = input.RechargeAmount,
Content = input.Content,
ExpireDateTime = input.ExpireDateTime,
ExpireDateTime = expireDateTime,
Remark = input.Remark,
ContactInfo = input.ContactInfo
ContactInfo = input.ContactInfo,
RechargeType = RechargeTypeEnum.Vip
};
// 保存充值记录到数据库

View File

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

View File

@@ -1,11 +1,18 @@
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.UsageStatistics;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.Ddd.Application.Contracts;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
@@ -18,20 +25,26 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
{
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
{
_messageRepository = messageRepository;
_usageStatisticsRepository = usageStatisticsRepository;
_premiumPackageRepository = premiumPackageRepository;
_tokenRepository = tokenRepository;
}
/// <summary>
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync()
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync([FromQuery]UsageStatisticsGetInput input)
{
var userId = CurrentUser.GetId();
var endDate = DateTime.Today;
@@ -40,7 +53,9 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
// 从Message表统计近7天的token消耗
var dailyUsage = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.Role == "assistant" || x.Role == "system")
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
.WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId)
.GroupBy(x => x.CreationTime.Date)
.Select(g => new
{
@@ -70,17 +85,19 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync()
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync([FromQuery]UsageStatisticsGetInput input)
{
var userId = CurrentUser.GetId();
// 从UsageStatistics表获取各模型的token消耗统计
// 从UsageStatistics表获取各模型的token消耗统计按ModelId聚合因为同一模型可能有多个TokenId的记录
var modelUsages = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId)
.WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId)
.GroupBy(x => x.ModelId)
.Select(x => new
{
x.ModelId,
x.TotalTokenCount
ModelId = x.ModelId,
TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount)
})
.ToListAsync();
@@ -102,4 +119,107 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
return result;
}
/// <summary>
/// 获取当前用户尊享服务Token用量统计
/// </summary>
/// <returns>尊享服务Token用量统计</returns>
public async Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync()
{
var userId = CurrentUser.GetId();
// 获取尊享包Token信息
var premiumPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive)
.ToListAsync();
var result = new PremiumTokenUsageDto();
if (premiumPackages.Any())
{
// 过滤掉已过期、禁用的包,不过滤用量负数的包
var validPackages = premiumPackages
.Where(p => p.IsAvailable(false))
.ToList();
result.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
result.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
result.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
}
return result;
}
/// <summary>
/// 获取当前用户尊享服务token用量统计列表
/// </summary>
/// <returns></returns>
[HttpGet("usage-statistics/premium-token-usage/list")]
public async Task<PagedResultDto<PremiumTokenUsageGetListOutput>> GetPremiumTokenUsageListAsync(
PremiumTokenUsageGetListInput input)
{
var userId = CurrentUser.GetId();
RefAsync<int> total = 0;
// 获取尊享包Token信息
var entities = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId)
.WhereIF(input.IsFree == false, x => x.PurchaseAmount > 0)
.WhereIF(input.IsFree == true, x => x.PurchaseAmount == 0)
.WhereIF(input.StartTime is not null && input.EndTime is not null,
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
.OrderByDescending(x => x.CreationTime)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
return new PagedResultDto<PremiumTokenUsageGetListOutput>(total,
entities.Adapt<List<PremiumTokenUsageGetListOutput>>());
}
/// <summary>
/// 获取当前用户尊享包不同Token用量占比饼图
/// </summary>
/// <returns>各Token的尊享模型用量及占比</returns>
[HttpGet("usage-statistics/premium-token-usage/by-token")]
public async Task<List<TokenPremiumUsageDto>> GetPremiumTokenUsageByTokenAsync()
{
var userId = CurrentUser.GetId();
var premiumModelIds = PremiumPackageConst.ModeIds;
// 从UsageStatistics表获取尊享模型的token消耗统计按TokenId聚合
var tokenUsages = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId && premiumModelIds.Contains(x.ModelId))
.GroupBy(x => x.TokenId)
.Select(x => new
{
TokenId = x.TokenId,
TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount)
})
.ToListAsync();
if (!tokenUsages.Any())
{
return new List<TokenPremiumUsageDto>();
}
// 获取用户的所有Token信息用于名称映射
var tokenIds = tokenUsages.Select(x => x.TokenId).ToList();
var tokens = await _tokenRepository._DbQueryable
.Where(x => x.UserId == userId && tokenIds.Contains(x.Id))
.Select(x => new { x.Id, x.Name })
.ToListAsync();
var tokenNameDict = tokens.ToDictionary(x => x.Id, x => x.Name);
// 计算总token数
var totalTokens = tokenUsages.Sum(x => x.TotalTokenCount);
// 计算各Token占比
var result = tokenUsages.Select(x => new TokenPremiumUsageDto
{
TokenId = x.TokenId,
TokenName = x.TokenId == Guid.Empty ? "默认" : (tokenNameDict.TryGetValue(x.TokenId, out var name) ? name : "其他"),
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,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class FuwuhaoConst
{
public const string SceneCacheKey = "fuwuhao:scene:";
public const string OpenIdToSceneCacheKey = "fuwuhao:OpenIdToScene:";
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class PremiumPackageConst
{
public static List<string> ModeIds =
[
"claude-sonnet-4-5-20250929",
"claude-haiku-4-5-20251001",
"claude-opus-4-5-20251101",
"gemini-3-pro-preview",
"gpt-5.1-codex-max"
];
}

View File

@@ -56,4 +56,9 @@ public class AiModelDescribe
/// 模型额外信息
/// </summary>
public string? ModelExtraInfo { get; set; }
/// <summary>
/// 模型倍率
/// </summary>
public decimal Multiplier { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public sealed class AnthropicCacheControl
{
[JsonPropertyName("type")]
public string Type { get; set; }
}

View File

@@ -0,0 +1,190 @@
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public class AnthropicStreamDto
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("index")] public int? Index { get; set; }
[JsonPropertyName("content_block")] public AnthropicChatCompletionDtoContentBlock? ContentBlock { get; set; }
[JsonPropertyName("delta")] public AnthropicChatCompletionDtoDelta? Delta { get; set; }
[JsonPropertyName("message")] public AnthropicChatCompletionDto? Message { get; set; }
[JsonPropertyName("usage")] public AnthropicCompletionDtoUsage? Usage { get; set; }
[JsonPropertyName("error")] public AnthropicStreamErrorDto? Error { get; set; }
[JsonIgnore]
public ThorUsageResponse TokenUsage => new ThorUsageResponse
{
PromptTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
InputTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
OutputTokens = Usage?.OutputTokens,
InputTokensDetails = null,
CompletionTokens = Usage?.OutputTokens,
TotalTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens +
Usage?.OutputTokens,
PromptTokensDetails = null,
CompletionTokensDetails = null
};
public void SupplementalMultiplier(decimal multiplier)
{
if (this.Usage is not null)
{
this.Usage.CacheCreationInputTokens =
(int)Math.Round((this.Usage.CacheCreationInputTokens ?? 0) * multiplier);
this.Usage.CacheReadInputTokens =
(int)Math.Round((this.Usage.CacheReadInputTokens ?? 0) * multiplier);
this.Usage.InputTokens =
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
}
}
}
public class AnthropicStreamErrorDto
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("message")] public string? Message { get; set; }
}
public class AnthropicChatCompletionDtoDelta
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
[JsonPropertyName("partial_json")] public string? PartialJson { get; set; }
[JsonPropertyName("stop_reason")] public string? StopReason { get; set; }
}
public class AnthropicChatCompletionDtoContentBlock
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
[JsonPropertyName("signature")] public string? Signature { get; set; }
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("input")] public object? Input { get; set; }
[JsonPropertyName("server_name")] public string? ServerName { get; set; }
[JsonPropertyName("is_error")] public bool? IsError { get; set; }
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
[JsonPropertyName("content")] public object? Content { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
}
public class AnthropicChatCompletionDto
{
public string id { get; set; }
public string type { get; set; }
public string role { get; set; }
public AnthropicChatCompletionDtoContent[] content { get; set; }
public string model { get; set; }
public string stop_reason { get; set; }
public object stop_sequence { get; set; }
public AnthropicCompletionDtoUsage? Usage { get; set; }
[JsonIgnore]
public ThorUsageResponse TokenUsage => new ThorUsageResponse
{
PromptTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
InputTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
OutputTokens = Usage?.OutputTokens,
InputTokensDetails = null,
CompletionTokens = Usage?.OutputTokens,
TotalTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens +
Usage?.OutputTokens,
PromptTokensDetails = null,
CompletionTokensDetails = null
};
public void SupplementalMultiplier(decimal multiplier)
{
if (this.Usage is not null)
{
this.Usage.CacheCreationInputTokens =
(int)Math.Round((this.Usage?.CacheCreationInputTokens ?? 0) * multiplier);
this.Usage.CacheReadInputTokens =
(int)Math.Round((this.Usage?.CacheReadInputTokens ?? 0) * multiplier);
this.Usage.InputTokens =
(int)Math.Round((this.Usage?.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage?.OutputTokens ?? 0) * multiplier);
}
}
}
public class AnthropicChatCompletionDtoContent
{
public string type { get; set; }
public string? text { get; set; }
public string? id { get; set; }
public string? name { get; set; }
public object? input { get; set; }
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
[JsonPropertyName("partial_json")] public string? PartialJson { get; set; }
public string? signature { get; set; }
}
public class AnthropicCompletionDtoUsage
{
[JsonPropertyName("input_tokens")] public int? InputTokens { get; set; }
[JsonPropertyName("cache_creation_input_tokens")]
public int? CacheCreationInputTokens { get; set; }
[JsonPropertyName("cache_read_input_tokens")]
public int? CacheReadInputTokens { get; set; }
[JsonPropertyName("output_tokens")] public int? OutputTokens { get; set; }
[JsonPropertyName("server_tool_use")] public AnthropicServerToolUse? ServerToolUse { get; set; }
}
public class AnthropicServerToolUse
{
[JsonPropertyName("web_search_requests")]
public int? WebSearchRequests { get; set; }
}

View File

@@ -0,0 +1,160 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public sealed class AnthropicInput
{
[JsonPropertyName("stream")] public bool Stream { get; set; }
[JsonPropertyName("model")] public string Model { get; set; }
[JsonPropertyName("max_tokens")] public int? MaxTokens { get; set; }
[JsonPropertyName("messages")] public JsonElement? Messages { get; set; }
[JsonPropertyName("tools")] public IList<AnthropicMessageTool>? Tools { get; set; }
[JsonPropertyName("tool_choice")]
public object? ToolChoiceCalculated
{
get
{
if (string.IsNullOrEmpty(ToolChoiceString))
{
return ToolChoiceString;
}
if (ToolChoice?.Type == "function")
{
return ToolChoice;
}
return ToolChoice?.Type;
}
set
{
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String)
{
ToolChoiceString = jsonElement.GetString();
}
else if (jsonElement.ValueKind == JsonValueKind.Object)
{
ToolChoice = jsonElement.Deserialize<AnthropicTooChoiceInput>(ThorJsonSerializer.DefaultOptions);
}
}
else
{
ToolChoice = (AnthropicTooChoiceInput)value;
}
}
}
[JsonIgnore] public string? ToolChoiceString { get; set; }
[JsonIgnore] public AnthropicTooChoiceInput? ToolChoice { get; set; }
[JsonIgnore] public IList<AnthropicMessageContent>? Systems { get; set; }
[JsonIgnore] public string? System { get; set; }
[JsonPropertyName("system")]
public object? SystemCalculated
{
get
{
if (System is not null && Systems is not null)
{
throw new ValidationException("System 和 Systems 字段不能同时有值");
}
if (System is not null)
{
return System;
}
return Systems!;
}
set
{
if (value is JsonElement str)
{
if (str.ValueKind == JsonValueKind.String)
{
System = value?.ToString();
}
else if (str.ValueKind == JsonValueKind.Array)
{
Systems = JsonSerializer.Deserialize<IList<AnthropicMessageContent>>(value?.ToString(),
ThorJsonSerializer.DefaultOptions);
}
}
else
{
System = value?.ToString();
}
}
}
[JsonPropertyName("thinking")] public AnthropicThinkingInput? Thinking { get; set; }
[JsonPropertyName("temperature")] public double? Temperature { get; set; }
[JsonPropertyName("metadata")] public Dictionary<string, object>? Metadata { get; set; }
}
public class AnthropicThinkingInput
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("budget_tokens")] public int? BudgetTokens { get; set; }
[JsonPropertyName("signature")] public string? Signature { get; set; }
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
[JsonPropertyName("data")] public string? Data { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
}
public class AnthropicTooChoiceInput
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
}
public class AnthropicMessageTool
{
[JsonPropertyName("name")] public string? name { get; set; }
[JsonPropertyName("description")] public string? Description { get; set; }
[JsonPropertyName("input_schema")] public Input_schema? InputSchema { get; set; }
}
public class Input_schema
{
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("properties")] public Dictionary<string, InputSchemaValue>? Properties { get; set; }
[JsonPropertyName("required")] public string[]? Required { get; set; }
}
public class InputSchemaValue
{
public string? type { get; set; }
public string? description { get; set; }
public InputSchemaValueItems? items { get; set; }
}
public class InputSchemaValueItems
{
public string? type { get; set; }
}

View File

@@ -0,0 +1,78 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public class AnthropicMessageContent
{
[JsonPropertyName("cache_control")] public AnthropicCacheControl? CacheControl { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
[JsonPropertyName("signature")] public string? Signature { get; set; }
[JsonPropertyName("input")] public object? Input { get; set; }
[JsonPropertyName("content")]
public object? Content
{
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<List<AnthropicMessageContent>>(value?.ToString());
}
}
else
{
_content = value?.ToString();
}
}
}
private string? _content;
private List<AnthropicMessageContent>? _contents;
public class AnthropicMessageContentSource
{
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("media_type")] public string? MediaType { get; set; }
[JsonPropertyName("data")] public string? Data { get; set; }
}
[JsonPropertyName("source")] public AnthropicMessageContentSource? Source { get; set; }
}

View File

@@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public class AnthropicMessageInput
{
[JsonPropertyName("role")]
public string Role { get; set; }
[JsonIgnore]
public string? Content;
[JsonPropertyName("content")]
public object? ContentCalculated
{
get
{
if (Content is not null && Contents is not null)
{
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
}
if (!string.IsNullOrEmpty(Content))
{
return Content;
}
// 如果 Contents 为空或 null返回空字符串而不是 null
if (Contents == null || Contents.Count == 0)
{
return "_"; // 兼容客户端空值问题
}
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<AnthropicMessageContent>>(value?.ToString(),ThorJsonSerializer.DefaultOptions);
}
}
else
{
Content = value?.ToString();
}
}
}
[JsonIgnore]
public IList<AnthropicMessageContent>? Contents;
}

View File

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

View File

@@ -0,0 +1,47 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses;
public class OpenAiResponsesInput
{
[JsonPropertyName("stream")] public bool? Stream { get; set; }
[JsonPropertyName("model")] public string Model { get; set; }
[JsonPropertyName("input")] public JsonElement Input { get; set; }
[JsonPropertyName("max_output_tokens")]
public int? MaxOutputTokens { get; set; }
[JsonPropertyName("max_tool_calls")] public JsonElement? MaxToolCalls { get; set; }
[JsonPropertyName("instructions")] public string? Instructions { get; set; }
[JsonPropertyName("metadata")] public JsonElement? Metadata { get; set; }
[JsonPropertyName("parallel_tool_calls")]
public bool? ParallelToolCalls { get; set; }
[JsonPropertyName("previous_response_id")]
public string? PreviousResponseId { get; set; }
[JsonPropertyName("prompt")] public JsonElement? Prompt { get; set; }
[JsonPropertyName("prompt_cache_key")] public string? PromptCacheKey { get; set; }
[JsonPropertyName("prompt_cache_retention")]
public string? PromptCacheRetention { get; set; }
[JsonPropertyName("reasoning")] public JsonElement? Reasoning { get; set; }
[JsonPropertyName("safety_identifier")]
public string? SafetyIdentifier { get; set; }
[JsonPropertyName("service_tier")] public string? ServiceTier { get; set; }
[JsonPropertyName("store")] public bool? Store { get; set; }
[JsonPropertyName("stream_options")] public JsonElement? StreamOptions { get; set; }
[JsonPropertyName("temperature")] public decimal? Temperature { get; set; }
[JsonPropertyName("text")] public JsonElement? Text { get; set; }
[JsonPropertyName("tool_choice")] public JsonElement? ToolChoice { get; set; }
[JsonPropertyName("tools")] public JsonElement? Tools { get; set; }
[JsonPropertyName("top_logprobs")] public int? TopLogprobs { get; set; }
[JsonPropertyName("top_p")] public decimal? TopP { get; set; }
[JsonPropertyName("truncation")] public string? Truncation { get; set; }
}

View File

@@ -0,0 +1,91 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses;
public class OpenAiResponsesOutput
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("object")]
public string? Object { get; set; }
[JsonPropertyName("created_at")]
public long CreatedAt { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
[JsonPropertyName("error")]
public dynamic? Error { get; set; }
[JsonPropertyName("incomplete_details")]
public dynamic? IncompleteDetails { get; set; }
[JsonPropertyName("instructions")]
public dynamic? Instructions { get; set; }
[JsonPropertyName("max_output_tokens")]
public dynamic? MaxOutputTokens { get; set; }
[JsonPropertyName("model")]
public string? Model { get; set; }
// output 是复杂对象
[JsonPropertyName("output")]
public List<dynamic>? Output { get; set; }
[JsonPropertyName("parallel_tool_calls")]
public bool ParallelToolCalls { get; set; }
[JsonPropertyName("previous_response_id")]
public dynamic? PreviousResponseId { get; set; }
[JsonPropertyName("reasoning")]
public dynamic? Reasoning { get; set; }
[JsonPropertyName("store")]
public bool Store { get; set; }
[JsonPropertyName("temperature")]
public double Temperature { get; set; }
[JsonPropertyName("text")]
public dynamic? Text { get; set; }
[JsonPropertyName("tool_choice")]
public string? ToolChoice { get; set; }
[JsonPropertyName("tools")]
public List<dynamic>? Tools { get; set; }
[JsonPropertyName("top_p")]
public double TopP { get; set; }
[JsonPropertyName("truncation")]
public string? Truncation { get; set; }
// usage 为唯一强类型
[JsonPropertyName("usage")]
public OpenAiResponsesUsageOutput? Usage { get; set; }
[JsonPropertyName("user")]
public dynamic? User { get; set; }
[JsonPropertyName("metadata")]
public dynamic? Metadata { get; set; }
public void SupplementalMultiplier(decimal multiplier)
{
if (this.Usage is not null)
{
this.Usage.InputTokens =
(int)Math.Round((this.Usage?.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage?.OutputTokens ?? 0) * multiplier);
}
}
}
public class OpenAiResponsesUsageOutput
{
[JsonPropertyName("input_tokens")]
public int InputTokens { get; set; }
[JsonPropertyName("input_tokens_details")]
public OpenAiResponsesInputTokensDetails? InputTokensDetails { get; set; }
[JsonPropertyName("output_tokens")]
public int OutputTokens { get; set; }
[JsonPropertyName("output_tokens_details")]
public OpenAiResponsesOutputTokensDetails? OutputTokensDetails { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}
public class OpenAiResponsesInputTokensDetails
{
[JsonPropertyName("cached_tokens")]
public int CachedTokens { get; set; }
}
public class OpenAiResponsesOutputTokensDetails
{
[JsonPropertyName("reasoning_tokens")]
public int ReasoningTokens { get; set; }
}

View File

@@ -28,64 +28,7 @@ public class ThorChatCompletionsRequest
/// </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>
@@ -336,13 +279,10 @@ public class ThorChatCompletionsRequest
[JsonPropertyName("thinking")] public ThorChatClaudeThinking? Thinking { get; set; }
/// <summary>
/// 参数验证
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public IEnumerable<ValidationResult> Validate()
{
throw new NotImplementedException();
}
[JsonPropertyName("enable_thinking")] public bool? EnableThinking { get; set; }
[JsonPropertyName("web_search_options")]
public ThorChatWebSearchOptions? WebSearchOptions { get; set; } = null;
[JsonPropertyName("reasoning_effort")] public string? ReasoningEffort { get; set; }
}

View File

@@ -60,4 +60,22 @@ public record ThorChatCompletionsResponse
/// </summary>
[JsonPropertyName("error")]
public ThorError? Error { get; set; }
public void SupplementalMultiplier(decimal multiplier)
{
if (this.Usage is not null)
{
this.Usage.InputTokens =
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
this.Usage.CompletionTokens =
(int)Math.Round((this.Usage.CompletionTokens ?? 0) * multiplier);
this.Usage.PromptTokens =
(int)Math.Round((this.Usage.PromptTokens ?? 0) * multiplier);
this.Usage.TotalTokens =
(int)Math.Round((this.Usage.TotalTokens ?? 0) * multiplier);
}
}
}

View File

@@ -51,16 +51,17 @@ public class ThorChatMessage
/// <summary>
/// 发出的消息内容计算用于json序列号和反序列化Content 和 Contents 不能同时赋值,只能二选一
/// 如果是工具调用,还真可能为空
/// </summary>
[JsonPropertyName("content")]
public object ContentCalculated
public object? ContentCalculated
{
get
{
if (Content is not null && Contents is not null)
{
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
}
// if (Content is not null && Contents is not null)
// {
// throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
// }
if (Content is not null)
{

View File

@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public class ThorChatWebSearchOptions
{
[JsonPropertyName("search_context_size")]
public string? SearchContextSize { get; set; }
[JsonPropertyName("user_location")]
public ThorUserLocation? UserLocation { get; set; }
}
public sealed class ThorUserLocation
{
[JsonPropertyName("type")] public required string Type { get; set; }
[JsonPropertyName("approximate")]
public ThorUserLocationApproximate? Approximate { get; set; }
}
public sealed class ThorUserLocationApproximate
{
[JsonPropertyName("city")]
public string? City { get; set; }
[JsonPropertyName("country")]
public string? Country { get; set; }
[JsonPropertyName("region")]
public string? Region { get; set; }
[JsonPropertyName("timezone")]
public string? Timezone { get; set; }
}

View File

@@ -30,5 +30,5 @@ public class ThorToolFunctionDefinition
/// documentation about the format.
/// </summary>
[JsonPropertyName("parameters")]
public ThorToolFunctionPropertyDefinition Parameters { get; set; }
public ThorToolFunctionPropertyDefinition? Parameters { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
@@ -25,37 +26,77 @@ public class ThorToolFunctionPropertyDefinition
/// 表示字符串类型的函数对象
/// </summary>
String,
/// <summary>
/// 表示整数类型的函数对象
/// </summary>
Integer,
/// <summary>
/// 表示数字(包括浮点数等)类型的函数对象
/// </summary>
Number,
/// <summary>
/// 表示对象类型的函数对象
/// </summary>
Object,
/// <summary>
/// 表示数组类型的函数对象
/// </summary>
Array,
/// <summary>
/// 表示布尔类型的函数对象
/// </summary>
Boolean,
/// <summary>
/// 表示空值类型的函数对象
/// </summary>
Null
}
public string typeStr = "object";
public string[] Types;
/// <summary>
/// 必填的。函数参数对象类型。默认值为“object”。
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = "object";
public object Type
{
get
{
if (Types is { Length: > 0 })
{
return Types;
}
return typeStr;
}
set
{
if (value is JsonElement str)
{
switch (str.ValueKind)
{
case JsonValueKind.String:
typeStr = value?.ToString();
break;
case JsonValueKind.Array:
Types = JsonSerializer.Deserialize<string[]>(value?.ToString());
break;
}
}
else
{
typeStr = value?.ToString();
}
}
}
/// <summary>
/// 可选。“函数参数”列表,作为从参数名称映射的字典
@@ -68,13 +109,13 @@ public class ThorToolFunctionPropertyDefinition
/// 可选。列出必需的“function arguments”列表。
/// </summary>
[JsonPropertyName("required")]
public List<string>? Required { get; set; }
public string[]? Required { get; set; }
/// <summary>
/// 可选。是否允许附加属性。默认值为true。
/// </summary>
[JsonPropertyName("additionalProperties")]
public bool? AdditionalProperties { get; set; }
public object? AdditionalProperties { get; set; }
/// <summary>
/// 可选。参数描述。
@@ -219,11 +260,12 @@ public class ThorToolFunctionPropertyDefinition
/// <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)
public static ThorToolFunctionPropertyDefinition DefineObject(
IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
string[]? required,
object? additionalProperties,
string? description,
List<string>? @enum)
{
return new ThorToolFunctionPropertyDefinition
{
@@ -242,7 +284,6 @@ public class ThorToolFunctionPropertyDefinition
/// </summary>
/// <param name="type">要转换的类型</param>
/// <returns>给定类型的字符串表示形式</returns>
public static string ConvertTypeToString(FunctionObjectTypes type)
{
return type switch

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum AnnouncementTypeEnum
{
Activity=1,
System=2
}

View File

@@ -0,0 +1,29 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
public enum SceneResultEnum
{
/// <summary>
/// 等待,用户未扫码
/// </summary>
Wait = 0,
/// <summary>
/// 已扫码完成登录
/// </summary>
Login = 1,
/// <summary>
/// 已扫码完成注册
/// </summary>
Register = 2,
/// <summary>
/// 已扫码完成绑定
/// </summary>
Bind = 3,
/// <summary>
/// 已过期
/// </summary>
Expired = 10
}

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
public enum SceneTypeEnum
{
LoginOrRegister,
Bind
}

View File

@@ -10,10 +10,16 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public class PriceAttribute : Attribute
{
public decimal Price { get; }
public PriceAttribute(double price)
public decimal ReferencePrice { get; }
public int ValidMonths { get; }
public PriceAttribute(double price, int validMonths, double referencePrice)
{
Price = (decimal)price;
ValidMonths = validMonths;
ReferencePrice = (decimal)referencePrice;
}
}
@@ -24,10 +30,60 @@ public class PriceAttribute : Attribute
public class DisplayNameAttribute : Attribute
{
public string DisplayName { get; }
public DisplayNameAttribute(string displayName)
public string ChineseName { get; }
public string Remark { get; }
public DisplayNameAttribute(string displayName, string chineseName = "", string remark = "")
{
DisplayName = displayName;
ChineseName = chineseName;
Remark = remark;
}
}
/// <summary>
/// 商品类型特性
/// 用于标识商品是VIP服务还是尊享包服务
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class GoodsCategoryAttribute : Attribute
{
public GoodsCategoryType Category { get; }
public GoodsCategoryAttribute(GoodsCategoryType category)
{
Category = category;
}
}
/// <summary>
/// 商品类别类型
/// </summary>
public enum GoodsCategoryType
{
/// <summary>
/// VIP服务
/// </summary>
Vip = 1,
/// <summary>
/// 尊享包服务
/// </summary>
PremiumPackage = 2
}
/// <summary>
/// Token数量特性
/// 用于标识尊享包的token数量
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class TokenAmountAttribute : Attribute
{
public long TokenAmount { get; }
public TokenAmountAttribute(long tokenAmount)
{
TokenAmount = tokenAmount;
}
}
@@ -36,25 +92,28 @@ public class DisplayNameAttribute : Attribute
/// </summary>
public enum GoodsTypeEnum
{
[Price(0.01)]
[DisplayName("YiXinVip Test")]
YiXinVipTest = 0,
[Price(29.9)]
[DisplayName("YiXinVip 1 month")]
// VIP服务
[Price(29.9, 1, 29.9)] [DisplayName("YiXinVip 1 month", "1个月", "灵活选择")] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip1 = 1,
[Price(80.7)]
[DisplayName("YiXinVip 3 month")]
[Price(83.7, 3, 27.9)] [DisplayName("YiXinVip 3 month", "3个月", "短期体验")] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip3 = 3,
[Price(143.9)]
[DisplayName("YiXinVip 6 month")]
[Price(155.4, 6, 25.9)] [DisplayName("YiXinVip 6 month", "6个月", "年度热销")] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip6 = 6,
[Price(199.9)]
[DisplayName("YiXinVip 10 month")]
YiXinVip10 = 10
// 尊享包服务 - 需要VIP资格才能购买
[Price(188.9, 0, 1750)]
[DisplayName("YiXinPremiumPackage 5000W Tokens", "5000万Tokens", "简单尝试")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(50000000)]
PremiumPackage5000W = 101,
[Price(248.9, 0, 3500)]
[DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens推荐", "极致性价比")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(100000000)]
PremiumPackage10000W = 102,
}
public static class GoodsTypeEnumExtensions
@@ -70,7 +129,7 @@ public static class GoodsTypeEnumExtensions
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.Price ?? 0m;
}
/// <summary>
/// 获取商品价格描述
/// </summary>
@@ -81,7 +140,7 @@ public static class GoodsTypeEnumExtensions
var price = goodsType.GetTotalAmount();
return $"¥{price:F1}";
}
/// <summary>
/// 获取商品名称
/// </summary>
@@ -93,4 +152,146 @@ public static class GoodsTypeEnumExtensions
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.DisplayName ?? goodsType.ToString();
}
}
/// <summary>
/// 获取商品有效月份
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>有效月份</returns>
public static int GetValidMonths(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.ValidMonths ?? 1;
}
/// <summary>
/// 获取商品月均价格
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>月均价格</returns>
public static decimal GetMonthlyPrice(this GoodsTypeEnum goodsType)
{
var totalPrice = goodsType.GetTotalAmount();
var validMonths = goodsType.GetValidMonths();
return validMonths > 0 ? totalPrice / validMonths : 0m;
}
/// <summary>
/// 获取商品参考价格
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>参考价格</returns>
public static decimal GetReferencePrice(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.ReferencePrice ?? 0m;
}
/// <summary>
/// 获取商品类别
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>商品类别</returns>
public static GoodsCategoryType GetGoodsCategory(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>();
return categoryAttribute?.Category ?? GoodsCategoryType.Vip;
}
/// <summary>
/// 是否为尊享包商品
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>是否为尊享包</returns>
public static bool IsPremiumPackage(this GoodsTypeEnum goodsType)
{
return goodsType.GetGoodsCategory() == GoodsCategoryType.PremiumPackage;
}
/// <summary>
/// 是否为VIP服务商品
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>是否为VIP服务</returns>
public static bool IsVipService(this GoodsTypeEnum goodsType)
{
return goodsType.GetGoodsCategory() == GoodsCategoryType.Vip;
}
/// <summary>
/// 获取尊享包Token数量
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>Token数量</returns>
public static long GetTokenAmount(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var tokenAttribute = fieldInfo?.GetCustomAttribute<TokenAmountAttribute>();
return tokenAttribute?.TokenAmount ?? 0;
}
/// <summary>
/// 获取商品中文名称
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>中文名称</returns>
public static string GetChineseName(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.ChineseName ?? goodsType.ToString();
}
/// <summary>
/// 获取商品备注
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>备注信息</returns>
public static string GetRemark(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.Remark ?? string.Empty;
}
/// <summary>
/// 计算折扣金额(仅用于尊享包)
/// 规则每累加充值10元减少2.5元最多减少50元
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <param name="totalRechargeAmount">用户累加充值金额</param>
/// <returns>折扣金额</returns>
public static decimal CalculateDiscount(this GoodsTypeEnum goodsType, decimal totalRechargeAmount)
{
// 只有尊享包才有折扣
if (!goodsType.IsPremiumPackage())
{
return 0m;
}
// 每10元减2.5元
var discountAmount = Math.Floor(totalRechargeAmount / 2.5m);
// 最多减少50元
return Math.Min(discountAmount, 50m);
}
/// <summary>
/// 获取折扣后的价格(仅用于尊享包)
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <param name="totalRechargeAmount">用户累加充值金额</param>
/// <returns>折扣后的价格</returns>
public static decimal GetDiscountedPrice(this GoodsTypeEnum goodsType, decimal totalRechargeAmount)
{
var originalPrice = goodsType.GetTotalAmount();
var discount = goodsType.CalculateDiscount(totalRechargeAmount);
var discountedPrice = originalPrice - discount;
// 确保价格不为负数至少为0.01元
return Math.Round(discountedPrice, 2);
}
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelApiTypeEnum
{
[Description("OpenAI")]
OpenAi,
[Description("Claude")]
Claude,
[Description("Response")]
Response
}

View File

@@ -1,8 +1,15 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
using System.ComponentModel;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelTypeEnum
{
[Description("聊天")]
Chat = 0,
[Description("图片")]
Image = 1,
[Description("嵌入")]
Embedding = 2
}

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum RechargeTypeEnum
{
Vip = 10,
PremiumPackage = 20
}

View File

@@ -0,0 +1,27 @@
using System.ComponentModel;
using System.Reflection;
namespace Yi.Framework.AiHub.Domain.Shared.Extensions;
/// <summary>
/// 枚举扩展方法
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// 获取枚举的Description特性值
/// </summary>
/// <param name="value">枚举值</param>
/// <returns>Description特性值如果没有则返回枚举名称</returns>
public static string GetDescription(this Enum value)
{
var field = value.GetType().GetField(value.ToString());
if (field == null)
{
return value.ToString();
}
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
return attribute?.Description ?? value.ToString();
}
}

View File

@@ -0,0 +1,279 @@
using System.Text.Json;
namespace Yi.Framework.AiHub.Domain.Shared.Extensions;
public static class JsonElementExtensions
{
#region 访
/// <summary>
/// 链式获取深层属性,支持对象属性和数组索引
/// </summary>
/// <example>
/// root.GetPath("user", "addresses", 0, "city")
/// </example>
public static JsonElement? GetPath(this JsonElement element, params object[] path)
{
JsonElement current = element;
foreach (var key in path)
{
switch (key)
{
case string propertyName:
if (current.ValueKind != JsonValueKind.Object ||
!current.TryGetProperty(propertyName, out current))
return null;
break;
case int index:
if (current.ValueKind != JsonValueKind.Array ||
index < 0 || index >= current.GetArrayLength())
return null;
current = current[index];
break;
default:
return null;
}
}
return current;
}
/// <summary>
/// 安全获取对象属性
/// </summary>
public static JsonElement? Get(this JsonElement element, string propertyName)
{
if (element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(propertyName, out var value))
return value;
return null;
}
/// <summary>
/// 安全获取数组元素
/// </summary>
public static JsonElement? Get(this JsonElement element, int index)
{
if (element.ValueKind == JsonValueKind.Array &&
index >= 0 && index < element.GetArrayLength())
return element[index];
return null;
}
/// <summary>
/// 链式安全获取对象属性
/// </summary>
public static JsonElement? Get(this JsonElement? element, string propertyName)
=> element?.Get(propertyName);
/// <summary>
/// 链式安全获取数组元素
/// </summary>
public static JsonElement? Get(this JsonElement? element, int index)
=> element?.Get(index);
#endregion
#region
public static string? GetString(this JsonElement? element, string? defaultValue = null)
=> element?.ValueKind == JsonValueKind.String ? element.Value.GetString() : defaultValue;
public static int GetInt(this JsonElement? element, int defaultValue = 0)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt32() : defaultValue;
public static long GetLong(this JsonElement? element, long defaultValue = 0)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt64() : defaultValue;
public static double GetDouble(this JsonElement? element, double defaultValue = 0)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetDouble() : defaultValue;
public static decimal GetDecimal(this JsonElement? element, decimal defaultValue = 0)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetDecimal() : defaultValue;
public static bool GetBool(this JsonElement? element, bool defaultValue = false)
=> element?.ValueKind is JsonValueKind.True or JsonValueKind.False
? element.Value.GetBoolean()
: defaultValue;
public static DateTime GetDateTime(this JsonElement? element, DateTime defaultValue = default)
=> element?.ValueKind == JsonValueKind.String && element.Value.TryGetDateTime(out var dt)
? dt
: defaultValue;
public static Guid GetGuid(this JsonElement? element, Guid defaultValue = default)
=> element?.ValueKind == JsonValueKind.String && element.Value.TryGetGuid(out var guid)
? guid
: defaultValue;
#endregion
#region
public static int? GetIntOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt32() : null;
public static long? GetLongOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt64() : null;
public static double? GetDoubleOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetDouble() : null;
public static decimal? GetDecimalOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Number ? element.Value.GetDecimal() : null;
public static bool? GetBoolOrNull(this JsonElement? element)
=> element?.ValueKind is JsonValueKind.True or JsonValueKind.False
? element.Value.GetBoolean()
: null;
public static DateTime? GetDateTimeOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.String && element.Value.TryGetDateTime(out var dt)
? dt
: null;
public static Guid? GetGuidOrNull(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.String && element.Value.TryGetGuid(out var guid)
? guid
: null;
#endregion
#region
/// <summary>
/// 安全获取数组,不存在返回空数组
/// </summary>
public static IEnumerable<JsonElement> GetArray(this JsonElement? element)
{
if (element?.ValueKind == JsonValueKind.Array)
{
foreach (var item in element.Value.EnumerateArray())
yield return item;
}
}
/// <summary>
/// 获取数组长度
/// </summary>
public static int GetArrayLength(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Array ? element.Value.GetArrayLength() : 0;
/// <summary>
/// 数组转 List
/// </summary>
public static List<string?> ToStringList(this JsonElement? element)
=> element.GetArray().Select(e => e.GetString()).ToList();
public static List<int> ToIntList(this JsonElement? element)
=> element.GetArray()
.Where(e => e.ValueKind == JsonValueKind.Number)
.Select(e => e.GetInt32())
.ToList();
#endregion
#region
/// <summary>
/// 安全枚举对象属性
/// </summary>
public static IEnumerable<JsonProperty> GetProperties(this JsonElement? element)
{
if (element?.ValueKind == JsonValueKind.Object)
{
foreach (var prop in element.Value.EnumerateObject())
yield return prop;
}
}
/// <summary>
/// 获取所有属性名
/// </summary>
public static IEnumerable<string> GetPropertyNames(this JsonElement? element)
=> element.GetProperties().Select(p => p.Name);
/// <summary>
/// 判断是否包含某属性
/// </summary>
public static bool HasProperty(this JsonElement? element, string propertyName)
=> element?.ValueKind == JsonValueKind.Object &&
element.Value.TryGetProperty(propertyName, out _);
#endregion
#region
public static bool IsNull(this JsonElement? element)
=> element == null || element.Value.ValueKind == JsonValueKind.Null;
public static bool IsNullOrUndefined(this JsonElement? element)
=> element == null || element.Value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined;
public static bool IsObject(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Object;
public static bool IsArray(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Array;
public static bool IsString(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.String;
public static bool IsNumber(this JsonElement? element)
=> element?.ValueKind == JsonValueKind.Number;
public static bool IsBool(this JsonElement? element)
=> element?.ValueKind is JsonValueKind.True or JsonValueKind.False;
public static bool Exists(this JsonElement? element)
=> element != null && element.Value.ValueKind != JsonValueKind.Undefined;
#endregion
#region
/// <summary>
/// 反序列化为指定类型
/// </summary>
public static T? Deserialize<T>(this JsonElement? element, JsonSerializerOptions? options = null)
=> element.HasValue ? element.Value.Deserialize<T>(options) : default;
/// <summary>
/// 反序列化为指定类型,带默认值
/// </summary>
public static T Deserialize<T>(this JsonElement? element, T defaultValue, JsonSerializerOptions? options = null)
=> element.HasValue ? element.Value.Deserialize<T>(options) ?? defaultValue : defaultValue;
#endregion
#region /
/// <summary>
/// 转换为 Dictionary
/// </summary>
public static Dictionary<string, JsonElement>? ToDictionary(this JsonElement? element)
{
if (element?.ValueKind != JsonValueKind.Object)
return null;
var dict = new Dictionary<string, JsonElement>();
foreach (var prop in element.Value.EnumerateObject())
dict[prop.Name] = prop.Value;
return dict;
}
#endregion
#region
/// <summary>
/// 获取原始 JSON 字符串
/// </summary>
public static string? GetRawText(this JsonElement? element)
=> element?.GetRawText();
#endregion
}

View File

@@ -39,6 +39,12 @@ public static class HttpClientFactory
private static readonly ConcurrentDictionary<string, Lazy<List<HttpClient>>> HttpClientPool = new();
/// <summary>
/// 高并发下有问题
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
[Obsolete]
public static HttpClient GetHttpClient(string key)
{
return HttpClientPool.GetOrAdd(key, k => new Lazy<List<HttpClient>>(() =>
@@ -70,4 +76,4 @@ public static class HttpClientFactory
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.Anthropic;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IAnthropicChatCompletionService
{
/// <summary>
/// 非流式对话补全
/// </summary>
/// <param name="request">对话补全请求参数对象</param>
/// <param name="aiModelDescribe">平台参数对象</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns></returns>
Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe aiModelDescribe,
AnthropicInput request,
CancellationToken cancellationToken = default);
/// <summary>
/// 流式对话补全
/// </summary>
/// <param name="request">对话补全请求参数对象</param>
/// <param name="aiModelDescribe">平台参数对象</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns></returns>
IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe aiModelDescribe,
AnthropicInput request,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IOpenAiResponseService
{
/// <summary>
/// 响应-流式
/// </summary>
/// <param name="aiModelDescribe"></param>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public IAsyncEnumerable<(string, JsonElement?)> ResponsesStreamAsync(AiModelDescribe aiModelDescribe,
OpenAiResponsesInput input,
CancellationToken cancellationToken);
/// <summary>
/// 响应-非流式
/// </summary>
/// <param name="aiModelDescribe"></param>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<OpenAiResponsesOutput> ResponsesAsync(AiModelDescribe aiModelDescribe,
OpenAiResponsesInput input,
CancellationToken cancellationToken);
}

View File

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

View File

@@ -10,7 +10,7 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCompletionsService> logger)
public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
: IChatCompletionService
{
private string GetAddress(AiModelDescribe? options, string model)
@@ -31,7 +31,7 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
chatCompletionCreate.StreamOptions = null;
var response = await HttpClientFactory.GetHttpClient(address).HttpRequestRaw(
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
address,
chatCompletionCreate, options.ApiKey);
@@ -149,7 +149,7 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
var response = await HttpClientFactory.GetHttpClient(address).PostJsonAsync(
var response = await httpClientFactory.CreateClient().PostJsonAsync(
address,
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);

View File

@@ -10,7 +10,7 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChatCompletionCompletionsService> logger)
public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChatCompletionCompletionsService> logger,IHttpClientFactory httpClientFactory)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
@@ -21,12 +21,12 @@ public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChat
Activity.Current?.Source.StartActivity("Azure OpenAI 对话流式补全");
var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model);
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(url,
var response = await httpClientFactory.CreateClient().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();
@@ -86,7 +86,7 @@ public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChat
var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model);
var response =
await HttpClientFactory.GetHttpClient(options.Endpoint)
await httpClientFactory.CreateClient()
.PostJsonAsync(url, chatCompletionCreate, options.ApiKey, "Api-Key");
openai?.SetTag("Model", chatCompletionCreate.Model);

View File

@@ -5,7 +5,7 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
public class AzureOpenAIServiceImageService : IImageService
public class AzureOpenAIServiceImageService(IHttpClientFactory httpClientFactory) : IImageService
{
public async Task<ImageCreateResponse> CreateImage(ImageCreateRequest imageCreate, AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
@@ -13,7 +13,7 @@ public class AzureOpenAIServiceImageService : IImageService
var createClient = AzureOpenAIFactory.CreateClient(options);
var client = createClient.GetImageClient(imageCreate.Model);
imageCreate.Size??="1024x1024";
// 将size字符串拆分为宽度和高度
var size = imageCreate.Size.Split('x');
if (size.Length != 2)
@@ -98,7 +98,7 @@ public class AzureOpenAIServiceImageService : IImageService
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Image), "image",
imageEditCreateRequest.ImageName);
return await HttpClientFactory.GetHttpClient(url).PostFileAndReadAsAsync<ImageCreateResponse>(
return await httpClientFactory.CreateClient().PostFileAndReadAsAsync<ImageCreateResponse>(
url,
multipartContent, cancellationToken);
}

View File

@@ -0,0 +1,173 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
public class AnthropicChatCompletionsService(
IHttpClientFactory httpClientFactory,
ILogger<AnthropicChatCompletionsService> logger)
: IAnthropicChatCompletionService
{
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
CancellationToken cancellationToken = default)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "authorization", "Bearer " + options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
bool isThink = input.Model.EndsWith("-thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (isThink && input.Thinking is null)
{
input.Thinking = new AnthropicThinkingInput()
{
Type = "enabled",
BudgetTokens = 4000
};
}
if (input.Thinking is not null && input.Thinking.BudgetTokens > 0 && input.MaxTokens != null)
{
if (input.Thinking.BudgetTokens > input.MaxTokens)
{
input.Thinking.BudgetTokens = input.MaxTokens.Value - 1;
if (input.Thinking.BudgetTokens > 63999)
{
input.Thinking.BudgetTokens = 63999;
}
}
}
var response =
await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", input, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于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 Exception( $"恭喜你运气爆棚遇到了错误尊享包对话异常StatusCode【{response.StatusCode}】Response【{error}】");
}
var value =
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken);
return value;
}
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options,
AnthropicInput input,
CancellationToken cancellationToken = default)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "authorization", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
var isThinking = input.Model.EndsWith("thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", input, string.Empty,
headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于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 Exception("OpenAI对话异常" + response.StatusCode);
}
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
string? line = string.Empty;
string? data = null;
string eventType = string.Empty;
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 Exception("OpenAI对话异常" + line);
}
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (line.StartsWith("event:"))
{
eventType = line;
continue;
}
if (!line.StartsWith(OpenAIConstant.Data)) continue;
data = line[OpenAIConstant.Data.Length..].Trim();
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(data,
ThorJsonSerializer.DefaultOptions);
yield return (eventType, result);
}
}
}

View File

@@ -0,0 +1,878 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
public sealed class ClaudiaChatCompletionsService(
IHttpClientFactory httpClientFactory,
ILogger<ClaudiaChatCompletionsService> logger)
: IChatCompletionService
{
public List<ThorChatChoiceResponse> CreateResponse(AnthropicChatCompletionDto completionDto)
{
var response = new ThorChatChoiceResponse();
var chatMessage = new ThorChatMessage();
if (completionDto == null)
{
return new List<ThorChatChoiceResponse>();
}
if (completionDto.content.Any(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)))
{
// 将推理字段合并到返回对象去
chatMessage.ReasoningContent = completionDto.content
.First(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)).Thinking;
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.First(x => x.type.Equals("text", StringComparison.OrdinalIgnoreCase)).text;
}
else
{
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.FirstOrDefault()?.text;
}
response.Delta = chatMessage;
response.Message = chatMessage;
if (completionDto.content.Any(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase)))
{
var toolUse = completionDto.content
.First(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase));
chatMessage.ToolCalls =
[
new()
{
Id = toolUse.id,
Function = new ThorChatMessageFunction()
{
Name = toolUse.name,
Arguments = JsonSerializer.Serialize(toolUse.input,
ThorJsonSerializer.DefaultOptions),
},
Index = 0,
}
];
return
[
response
];
}
return new List<ThorChatChoiceResponse> { response };
}
private object CreateMessage(List<ThorChatMessage> messages, AiModelDescribe options)
{
var list = new List<object>();
foreach (var message in messages)
{
// 如果是图片
if (message.ContentCalculated is IList<ThorChatMessageContent> contentCalculated)
{
list.Add(new
{
role = message.Role,
content = (List<object>)contentCalculated.Select<ThorChatMessageContent, object>(x =>
{
if (x.Type == "text")
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "text",
text = x.Text,
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "text",
text = x.Text
};
}
var isBase64 = x.ImageUrl?.Url.StartsWith("http") == true;
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
},
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
}
};
})
});
}
else
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content,
cache_control = new
{
type = "ephemeral"
}
});
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
else
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content
});
}
else if (message.Role == "tool")
{
list.Add(new
{
role = "user",
content = new List<object>
{
new
{
type = "tool_result",
tool_use_id = message.ToolCallId,
content = message.Content
}
}
});
}
else if (message.Role == "assistant")
{
// {
// "role": "assistant",
// "content": [
// {
// "type": "text",
// "text": "<thinking>I need to use get_weather, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// },
// {
// "type": "tool_use",
// "id": "toolu_01A09q90qw90lq917835lq9",
// "name": "get_weather",
// "input": {
// "location": "San Francisco, CA",
// "unit": "celsius"
// }
// }
// ]
// },
if (message.ToolCalls?.Count > 0)
{
var content = new List<object>();
if (!string.IsNullOrEmpty(message.Content))
{
content.Add(new
{
type = "text",
text = message.Content
});
}
foreach (var toolCall in message.ToolCalls)
{
content.Add(new
{
type = "tool_use",
id = toolCall.Id,
name = toolCall.Function?.Name,
input = JsonSerializer.Deserialize<Dictionary<string, object>>(
toolCall.Function?.Arguments, ThorJsonSerializer.DefaultOptions)
});
}
list.Add(new
{
role = "assistant",
content
});
}
else
{
list.Add(new
{
role = "assistant",
content = new List<object>
{
new
{
type = "text",
text = message.Content
}
}
});
}
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
}
}
return list;
}
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
var isThinking = input.Model.EndsWith("thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 64000,
stream = true,
tool_choice,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThinking ? null : input.TopP,
thinking = isThinking
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThinking ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于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 Exception("OpenAI对话异常" + response.StatusCode);
}
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;
string? toolId = null;
string? toolName = null;
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 Exception("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;
}
if (line.StartsWith("event: ", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(line,
ThorJsonSerializer.DefaultOptions);
if (result?.Type == "content_block_delta")
{
if (result.Delta.Type is "text" or "text_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new()
{
Message = new ThorChatMessage()
{
Content = result.Delta.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else if (result.Delta.Type == "input_json_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName,
Arguments = result.Delta.PartialJson
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else
{
yield return new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
{
new()
{
Message = new ThorChatMessage()
{
ReasoningContent = result.Delta.Thinking,
Role = "assistant",
}
}
},
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens
}
};
}
continue;
}
if (result?.Type == "content_block_start")
{
if (result?.ContentBlock?.Id is not null)
{
toolId = result.ContentBlock.Id;
}
if (result?.ContentBlock?.Name is not null)
{
toolName = result.ContentBlock.Name;
}
if (toolId is null)
{
continue;
}
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
if (result.Type == "content_block_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCallId = result?.ContentBlock?.Id,
FunctionCall = new ThorChatMessageFunction()
{
Name = result?.ContentBlock?.Name,
Arguments = result?.Delta?.PartialJson
},
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens
}
};
continue;
}
if (result.Type == "message_start")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result?.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
continue;
}
if (result.Type == "message_delta")
{
var deltaOutput = new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse
{
InputTokens = result.Usage?.InputTokens + result.Usage?.CacheCreationInputTokens +
result.Usage?.CacheReadInputTokens,
OutputTokens = result.Usage?.OutputTokens,
}
};
deltaOutput.Usage.PromptTokens = deltaOutput.Usage.InputTokens;
deltaOutput.Usage.CompletionTokens = deltaOutput.Usage.OutputTokens;
deltaOutput.Usage.TotalTokens = deltaOutput.Usage.InputTokens + deltaOutput.Usage.OutputTokens;
yield return deltaOutput;
continue;
}
if (result.Message == null)
{
continue;
}
var chat = CreateResponse(result.Message);
var content = chat?.FirstOrDefault()?.Delta;
if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
{
continue;
}
if (first && content.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink)
{
// 需要将content的内容转换到其他字段
foreach (var choice in chat)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
var output = new ThorChatCompletionsResponse()
{
Choices = chat,
Model = input.Model,
Id = result.Message.id,
Usage = new ThorUsageResponse()
{
InputTokens = result.Message.Usage?.InputTokens + result.Message.Usage?.CacheCreationInputTokens +
result.Message.Usage?.CacheReadInputTokens,
OutputTokens = result.Message.Usage?.OutputTokens,
}
};
output.Usage.PromptTokens = output.Usage.InputTokens;
output.Usage.CompletionTokens = output.Usage.OutputTokens;
output.Usage.TotalTokens = output.Usage.InputTokens + output.Usage.OutputTokens;
yield return output;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
bool isThink = input.Model.EndsWith("-thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
var response = await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 2000,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThink ? null : input.TopP,
tool_choice,
thinking = isThink
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThink ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于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 Exception("OpenAI对话异常" + response.StatusCode.ToString());
}
var value =
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken);
var thor = new ThorChatCompletionsResponse()
{
Choices = CreateResponse(value),
Model = input.Model,
Id = value.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = value.Usage.OutputTokens,
PromptTokens = value.Usage.InputTokens
}
};
if (value.Usage.CacheReadInputTokens != null)
{
thor.Usage.PromptTokensDetails ??= new ThorUsageResponsePromptTokensDetails()
{
CachedTokens = value.Usage.CacheReadInputTokens.Value,
};
if (value.Usage.InputTokens > 0)
{
thor.Usage.InputTokens = value.Usage.InputTokens;
}
if (value.Usage.OutputTokens > 0)
{
thor.Usage.CompletionTokens = value.Usage.OutputTokens;
thor.Usage.OutputTokens = value.Usage.OutputTokens;
}
}
thor.Usage.TotalTokens = thor.Usage.InputTokens + thor.Usage.OutputTokens;
return thor;
}
}

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