Compare commits

..

77 Commits

Author SHA1 Message Date
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
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
chenchun
fa3ac91ba4 fix: 修正文件整理大师Vip数量限制提示错误 2025-09-18 16:17:10 +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
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
112 changed files with 7747 additions and 515 deletions

View File

@@ -0,0 +1,36 @@
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; }
/// <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,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

@@ -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,44 @@
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 GoodsPrice { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 商品备注
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 折扣金额(仅尊享包)
/// </summary>
public decimal? DiscountAmount { get; set; }
/// <summary>
/// 折扣说明(仅尊享包)
/// </summary>
public string? DiscountDescription { 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

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

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

@@ -0,0 +1,96 @@
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;
using Yi.Framework.AiHub.Domain.Shared.Consts;
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);
}
}
}
// 获取尊享包Token信息
var premiumPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive)
.ToListAsync();
if (premiumPackages.Any())
{
// 过滤掉已过期的包
var validPackages = premiumPackages
.Where(p => p.IsAvailable())
.ToList();
output.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
output.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
output.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
}
return output;
}
}

View File

@@ -122,4 +122,41 @@ public class AiChatService : ApplicationService
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, 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, cancellationToken);
}
}

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, cancellationToken);
}
else
{
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null,
cancellationToken);
}
}
}

View File

@@ -0,0 +1,309 @@
using System.Text;
using System.Text.Json;
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;
/// <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,18 @@
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.Enums;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
@@ -21,9 +26,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 +40,8 @@ public class OpenApiService : ApplicationService
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
_aiBlacklistManager = aiBlacklistManager;
_accountService = accountService;
_premiumPackageManager = premiumPackageManager;
}
/// <summary>
@@ -69,11 +80,12 @@ public class OpenApiService : ApplicationService
public async Task ImagesGenerationsAsync([FromBody] ImageCreateRequest input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
Intercept(httpContext);
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input);
}
/// <summary>
/// 向量生成
/// </summary>
@@ -83,6 +95,7 @@ public class OpenApiService : ApplicationService
public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
Intercept(httpContext);
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input);
@@ -114,6 +127,58 @@ 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 userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
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, cancellationToken);
}
else
{
await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
null,
cancellationToken);
}
}
#region
private string? GetTokenByHttpContext(HttpContext httpContext)
{
// 获取Authorization头
@@ -127,4 +192,15 @@ public class OpenApiService : ApplicationService
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,44 @@ 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);
// 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 +186,85 @@ public class PayService : ApplicationService, IPayService
};
}
/// <summary>
/// 获取商品列表
/// </summary>
/// <returns>商品列表</returns>
[HttpGet("pay/GoodsList")]
public async Task<List<GoodsListOutput>> GetGoodsListAsync()
{
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)))
{
var originalPrice = goodsType.GetTotalAmount();
decimal actualPrice = originalPrice;
decimal? discountAmount = null;
string? discountDescription = null;
// 如果是尊享包商品,计算折扣
if (goodsType.IsPremiumPackage() && CurrentUser.IsAuthenticated)
{
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
if (discountAmount > 0)
{
discountDescription = $"已优惠 ¥{discountAmount:F2}累计充值每10元减1元最多减20元";
}
else
{
discountDescription = "累计充值每10元可减1元最多减20元";
}
}
var goodsItem = new GoodsListOutput
{
GoodsName = goodsType.GetDisplayName(),
OriginalPrice = originalPrice,
GoodsPrice = actualPrice,
GoodsType = goodsType,
Remark = GetGoodsRemark(goodsType),
DiscountAmount = discountAmount,
DiscountDescription = discountDescription
};
goodsList.Add(goodsItem);
}
return goodsList;
}
/// <summary>
/// 获取商品备注信息
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>商品备注</returns>
private string GetGoodsRemark(GoodsTypeEnum goodsType)
{
if (goodsType.IsPremiumPackage())
{
var tokenAmount = goodsType.GetTokenAmount();
return $"尊享包服务,提供 {tokenAmount:N0} Tokens需要VIP资格";
}
else if (goodsType.IsVipService())
{
var validMonths = goodsType.GetValidMonths();
var monthlyPrice = goodsType.GetMonthlyPrice();
return $"VIP服务有效期 {validMonths} 个月,月均价 ¥{monthlyPrice:F2}";
}
return "未知商品类型";
}
/// <summary>
/// 获取交易状态描述
/// </summary>
@@ -167,4 +290,4 @@ public class PayService : ApplicationService, IPayService
}
return TradeStatusEnum.WAIT_TRADE;
}
}
}

View File

@@ -13,7 +13,7 @@ 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,13 +57,33 @@ 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.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
};

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,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,151 @@
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 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; }
}
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 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,153 @@
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 IList<AnthropicMessageInput> 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; }
}
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,76 @@
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("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,54 @@
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 (Content is not null)
{
return Content;
}
return Contents!;
}
set
{
if (value is JsonElement str)
{
if (str.ValueKind == JsonValueKind.String)
{
Content = value?.ToString();
}
else if (str.ValueKind == JsonValueKind.Array)
{
Contents = JsonSerializer.Deserialize<IList<AnthropicMessageContent>>(value?.ToString(),ThorJsonSerializer.DefaultOptions);
}
}
else
{
Content = value?.ToString();
}
}
}
[JsonIgnore]
public IList<AnthropicMessageContent>? Contents;
}

View File

@@ -0,0 +1,648 @@
using System.Text.Json;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
public static class AnthropicToOpenAi
{
/// <summary>
/// 将AnthropicInput转换为ThorChatCompletionsRequest
/// </summary>
public static ThorChatCompletionsRequest ConvertAnthropicToOpenAi(AnthropicInput anthropicInput)
{
var openAiRequest = new ThorChatCompletionsRequest
{
Model = anthropicInput.Model,
MaxTokens = anthropicInput.MaxTokens,
Stream = anthropicInput.Stream,
Messages = new List<ThorChatMessage>(anthropicInput.Messages.Count)
};
// high medium minimal low
if (openAiRequest.Model.EndsWith("-high") ||
openAiRequest.Model.EndsWith("-medium") ||
openAiRequest.Model.EndsWith("-minimal") ||
openAiRequest.Model.EndsWith("-low"))
{
openAiRequest.ReasoningEffort = openAiRequest.Model switch
{
var model when model.EndsWith("-high") => "high",
var model when model.EndsWith("-medium") => "medium",
var model when model.EndsWith("-minimal") => "minimal",
var model when model.EndsWith("-low") => "low",
_ => "medium"
};
openAiRequest.Model = openAiRequest.Model.Replace("-high", "")
.Replace("-medium", "")
.Replace("-minimal", "")
.Replace("-low", "");
}
if (anthropicInput.Thinking != null &&
anthropicInput.Thinking.Type.Equals("enabled", StringComparison.OrdinalIgnoreCase))
{
openAiRequest.Thinking = new ThorChatClaudeThinking()
{
BudgetToken = anthropicInput.Thinking.BudgetTokens,
Type = "enabled",
};
openAiRequest.EnableThinking = true;
}
if (openAiRequest.Model.EndsWith("-thinking"))
{
openAiRequest.EnableThinking = true;
openAiRequest.Model = openAiRequest.Model.Replace("-thinking", "");
}
if (openAiRequest.Stream == true)
{
openAiRequest.StreamOptions = new ThorStreamOptions()
{
IncludeUsage = true,
};
}
if (!string.IsNullOrEmpty(anthropicInput.System))
{
openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(anthropicInput.System));
}
if (anthropicInput.Systems?.Count > 0)
{
foreach (var systemContent in anthropicInput.Systems)
{
openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(systemContent.Text ?? string.Empty));
}
}
// 处理messages
if (anthropicInput.Messages != null)
{
foreach (var message in anthropicInput.Messages)
{
var thorMessages = ConvertAnthropicMessageToThor(message);
// 需要过滤 空消息
if (thorMessages.Count == 0)
{
continue;
}
openAiRequest.Messages.AddRange(thorMessages);
}
openAiRequest.Messages = openAiRequest.Messages
.Where(m => !string.IsNullOrEmpty(m.Content) || m.Contents?.Count > 0 || m.ToolCalls?.Count > 0 ||
!string.IsNullOrEmpty(m.ToolCallId))
.ToList();
}
// 处理tools
if (anthropicInput.Tools is { Count: > 0 })
{
openAiRequest.Tools = anthropicInput.Tools.Where(x => x.name != "web_search")
.Select(ConvertAnthropicToolToThor).ToList();
}
// 判断是否存在web_search
if (anthropicInput.Tools?.Any(x => x.name == "web_search") == true)
{
openAiRequest.WebSearchOptions = new ThorChatWebSearchOptions()
{
};
}
// 处理tool_choice
if (anthropicInput.ToolChoice != null)
{
openAiRequest.ToolChoice = ConvertAnthropicToolChoiceToThor(anthropicInput.ToolChoice);
}
return openAiRequest;
}
/// <summary>
/// 根据最后的内容块类型和OpenAI的完成原因确定Claude的停止原因
/// </summary>
public static string GetStopReasonByLastContentType(string? openAiFinishReason, string lastContentBlockType)
{
// 如果最后一个内容块是工具调用优先返回tool_use
if (lastContentBlockType == "tool_use")
{
return "tool_use";
}
// 否则使用标准的转换逻辑
return GetClaudeStopReason(openAiFinishReason);
}
/// <summary>
/// 创建message_start事件
/// </summary>
public static AnthropicStreamDto CreateMessageStartEvent(string messageId, string model)
{
return new AnthropicStreamDto
{
Type = "message_start",
Message = new AnthropicChatCompletionDto
{
id = messageId,
type = "message",
role = "assistant",
model = model,
content = new AnthropicChatCompletionDtoContent[0],
Usage = new AnthropicCompletionDtoUsage
{
InputTokens = 0,
OutputTokens = 0,
CacheCreationInputTokens = 0,
CacheReadInputTokens = 0
}
}
};
}
/// <summary>
/// 创建content_block_start事件
/// </summary>
public static AnthropicStreamDto CreateContentBlockStartEvent()
{
return new AnthropicStreamDto
{
Type = "content_block_start",
Index = 0,
ContentBlock = new AnthropicChatCompletionDtoContentBlock
{
Type = "text",
Id = null,
Name = null
}
};
}
/// <summary>
/// 创建thinking block start事件
/// </summary>
public static AnthropicStreamDto CreateThinkingBlockStartEvent()
{
return new AnthropicStreamDto
{
Type = "content_block_start",
Index = 0,
ContentBlock = new AnthropicChatCompletionDtoContentBlock
{
Type = "thinking",
Id = null,
Name = null
}
};
}
/// <summary>
/// 创建content_block_delta事件
/// </summary>
public static AnthropicStreamDto CreateContentBlockDeltaEvent(string text)
{
return new AnthropicStreamDto
{
Type = "content_block_delta",
Index = 0,
Delta = new AnthropicChatCompletionDtoDelta
{
Type = "text_delta",
Text = text
}
};
}
/// <summary>
/// 创建thinking delta事件
/// </summary>
public static AnthropicStreamDto CreateThinkingBlockDeltaEvent(string thinking)
{
return new AnthropicStreamDto
{
Type = "content_block_delta",
Index = 0,
Delta = new AnthropicChatCompletionDtoDelta
{
Type = "thinking",
Thinking = thinking
}
};
}
/// <summary>
/// 创建content_block_stop事件
/// </summary>
public static AnthropicStreamDto CreateContentBlockStopEvent()
{
return new AnthropicStreamDto
{
Type = "content_block_stop",
Index = 0
};
}
/// <summary>
/// 创建message_delta事件
/// </summary>
public static AnthropicStreamDto CreateMessageDeltaEvent(string finishReason, AnthropicCompletionDtoUsage usage)
{
return new AnthropicStreamDto
{
Type = "message_delta",
Usage = usage,
Delta = new AnthropicChatCompletionDtoDelta
{
StopReason = finishReason
}
};
}
/// <summary>
/// 创建message_stop事件
/// </summary>
public static AnthropicStreamDto CreateMessageStopEvent()
{
return new AnthropicStreamDto
{
Type = "message_stop"
};
}
/// <summary>
/// 创建tool block start事件
/// </summary>
public static AnthropicStreamDto CreateToolBlockStartEvent(string? toolId, string? toolName)
{
return new AnthropicStreamDto
{
Type = "content_block_start",
Index = 0,
ContentBlock = new AnthropicChatCompletionDtoContentBlock
{
Type = "tool_use",
Id = toolId,
Name = toolName
}
};
}
/// <summary>
/// 创建tool delta事件
/// </summary>
public static AnthropicStreamDto CreateToolBlockDeltaEvent(string partialJson)
{
return new AnthropicStreamDto
{
Type = "content_block_delta",
Index = 0,
Delta = new AnthropicChatCompletionDtoDelta
{
Type = "input_json_delta",
PartialJson = partialJson
}
};
}
/// <summary>
/// 转换Anthropic消息为Thor消息列表
/// </summary>
public static List<ThorChatMessage> ConvertAnthropicMessageToThor(AnthropicMessageInput anthropicMessage)
{
var results = new List<ThorChatMessage>();
// 处理简单的字符串内容
if (anthropicMessage.Content != null)
{
var thorMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
Content = anthropicMessage.Content
};
results.Add(thorMessage);
return results;
}
// 处理多模态内容
if (anthropicMessage.Contents is { Count: > 0 })
{
var currentContents = new List<ThorChatMessageContent>();
var currentToolCalls = new List<ThorToolCall>();
foreach (var content in anthropicMessage.Contents)
{
switch (content.Type)
{
case "text":
currentContents.Add(ThorChatMessageContent.CreateTextContent(content.Text ?? string.Empty));
break;
case "thinking" when !string.IsNullOrEmpty(content.Thinking):
results.Add(new ThorChatMessage()
{
ReasoningContent = content.Thinking
});
break;
case "image":
{
if (content.Source != null)
{
var imageUrl = content.Source.Type == "base64"
? $"data:{content.Source.MediaType};base64,{content.Source.Data}"
: content.Source.Data;
currentContents.Add(ThorChatMessageContent.CreateImageUrlContent(imageUrl ?? string.Empty));
}
break;
}
case "tool_use":
{
// 如果有普通内容,先创建内容消息
if (currentContents.Count > 0)
{
if (currentContents.Count == 1 && currentContents.Any(x => x.Type == "text"))
{
var contentMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
ContentCalculated = currentContents.FirstOrDefault()?.Text ?? string.Empty
};
results.Add(contentMessage);
}
else
{
var contentMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
Contents = currentContents
};
results.Add(contentMessage);
}
currentContents = new List<ThorChatMessageContent>();
}
// 收集工具调用
var toolCall = new ThorToolCall
{
Id = content.Id,
Type = "function",
Function = new ThorChatMessageFunction
{
Name = content.Name,
Arguments = JsonSerializer.Serialize(content.Input)
}
};
currentToolCalls.Add(toolCall);
break;
}
case "tool_result":
{
// 如果有普通内容,先创建内容消息
if (currentContents.Count > 0)
{
var contentMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
Contents = currentContents
};
results.Add(contentMessage);
currentContents = [];
}
// 如果有工具调用,先创建工具调用消息
if (currentToolCalls.Count > 0)
{
var toolCallMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
ToolCalls = currentToolCalls
};
results.Add(toolCallMessage);
currentToolCalls = new List<ThorToolCall>();
}
// 创建工具结果消息
var toolMessage = new ThorChatMessage
{
Role = "tool",
ToolCallId = content.ToolUseId,
Content = content.Content?.ToString() ?? string.Empty
};
results.Add(toolMessage);
break;
}
}
}
// 处理剩余的内容
if (currentContents.Count > 0)
{
var contentMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
Contents = currentContents
};
results.Add(contentMessage);
}
// 处理剩余的工具调用
if (currentToolCalls.Count > 0)
{
var toolCallMessage = new ThorChatMessage
{
Role = anthropicMessage.Role,
ToolCalls = currentToolCalls
};
results.Add(toolCallMessage);
}
}
// 如果没有任何内容,返回一个空的消息
if (results.Count == 0)
{
results.Add(new ThorChatMessage
{
Role = anthropicMessage.Role,
Content = string.Empty
});
}
// 如果只有一个text则使用content字段
if (results is [{ Contents.Count: 1 }] &&
results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Type == "text" &&
!string.IsNullOrEmpty(results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text))
{
return
[
new ThorChatMessage
{
Role = results[0].Role,
Content = results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text ?? string.Empty
}
];
}
return results;
}
/// <summary>
/// 转换Anthropic工具为Thor工具
/// </summary>
public static ThorToolDefinition ConvertAnthropicToolToThor(AnthropicMessageTool anthropicTool)
{
IDictionary<string, ThorToolFunctionPropertyDefinition> values =
new Dictionary<string, ThorToolFunctionPropertyDefinition>();
if (anthropicTool.InputSchema?.Properties != null)
{
foreach (var property in anthropicTool.InputSchema.Properties)
{
if (property.Value?.description != null)
{
var definitionType = new ThorToolFunctionPropertyDefinition()
{
Description = property.Value.description,
Type = property.Value.type
};
if (property.Value?.items?.type != null)
{
definitionType.Items = new ThorToolFunctionPropertyDefinition()
{
Type = property.Value.items.type
};
}
values.Add(property.Key, definitionType);
}
}
}
return new ThorToolDefinition
{
Type = "function",
Function = new ThorToolFunctionDefinition
{
Name = anthropicTool.name,
Description = anthropicTool.Description,
Parameters = new ThorToolFunctionPropertyDefinition
{
Type = anthropicTool.InputSchema?.Type ?? "object",
Properties = values,
Required = anthropicTool.InputSchema?.Required
}
}
};
}
/// <summary>
/// 将OpenAI的完成原因转换为Claude的停止原因
/// </summary>
public static string GetClaudeStopReason(string? openAIFinishReason)
{
return openAIFinishReason switch
{
"stop" => "end_turn",
"length" => "max_tokens",
"tool_calls" => "tool_use",
"content_filter" => "stop_sequence",
_ => "end_turn"
};
}
/// <summary>
/// 将OpenAI响应转换为Claude响应格式
/// </summary>
public static AnthropicChatCompletionDto ConvertOpenAIToClaude(ThorChatCompletionsResponse openAIResponse,
AnthropicInput originalRequest)
{
var claudeResponse = new AnthropicChatCompletionDto
{
id = openAIResponse.Id,
type = "message",
role = "assistant",
model = openAIResponse.Model ?? originalRequest.Model,
stop_reason = GetClaudeStopReason(openAIResponse.Choices?.FirstOrDefault()?.FinishReason),
stop_sequence = "",
content = []
};
if (openAIResponse.Choices is { Count: > 0 })
{
var choice = openAIResponse.Choices.First();
var contents = new List<AnthropicChatCompletionDtoContent>();
if (!string.IsNullOrEmpty(choice.Message.Content) && !string.IsNullOrEmpty(choice.Message.ReasoningContent))
{
contents.Add(new AnthropicChatCompletionDtoContent
{
type = "thinking",
Thinking = choice.Message.ReasoningContent
});
contents.Add(new AnthropicChatCompletionDtoContent
{
type = "text",
text = choice.Message.Content
});
}
else
{
// 处理思维内容
if (!string.IsNullOrEmpty(choice.Message.ReasoningContent))
contents.Add(new AnthropicChatCompletionDtoContent
{
type = "thinking",
Thinking = choice.Message.ReasoningContent
});
// 处理文本内容
if (!string.IsNullOrEmpty(choice.Message.Content))
contents.Add(new AnthropicChatCompletionDtoContent
{
type = "text",
text = choice.Message.Content
});
}
// 处理工具调用
if (choice.Message.ToolCalls is { Count: > 0 })
contents.AddRange(choice.Message.ToolCalls.Select(toolCall => new AnthropicChatCompletionDtoContent
{
type = "tool_use", id = toolCall.Id, name = toolCall.Function?.Name,
input = JsonSerializer.Deserialize<object>(toolCall.Function?.Arguments ?? "{}")
}));
claudeResponse.content = contents.ToArray();
}
// 处理使用情况统计 - 确保始终提供Usage信息
claudeResponse.Usage = new AnthropicCompletionDtoUsage
{
InputTokens = openAIResponse.Usage?.PromptTokens ?? 0,
OutputTokens = (int?)(openAIResponse.Usage?.CompletionTokens ?? 0),
CacheCreationInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0,
CacheReadInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0
};
return claudeResponse;
}
/// <summary>
/// 转换Anthropic工具选择为Thor工具选择
/// </summary>
public static ThorToolChoice ConvertAnthropicToolChoiceToThor(AnthropicTooChoiceInput anthropicToolChoice)
{
return new ThorToolChoice
{
Type = anthropicToolChoice.Type ?? "auto",
Function = anthropicToolChoice.Name != null
? new ThorToolChoiceFunctionTool { Name = anthropicToolChoice.Name }
: null
};
}
}

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

@@ -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

@@ -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,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,12 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public class PriceAttribute : Attribute
{
public decimal Price { get; }
public PriceAttribute(double price)
public int ValidMonths { get; }
public PriceAttribute(double price, int validMonths)
{
Price = (decimal)price;
ValidMonths = validMonths;
}
}
@@ -24,37 +26,98 @@ public class PriceAttribute : Attribute
public class DisplayNameAttribute : Attribute
{
public string DisplayName { get; }
public DisplayNameAttribute(string displayName)
{
DisplayName = displayName;
}
}
/// <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>
VipService = 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;
}
}
/// <summary>
/// 商品枚举
/// </summary>
public enum GoodsTypeEnum
{
[Price(0.01)]
[DisplayName("YiXinVip Test")]
YiXinVipTest = 0,
[Price(29.9)]
// VIP服务
[Price(29.9, 1)]
[DisplayName("YiXinVip 1 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip1 = 1,
[Price(80.7)]
[Price(83.7, 3)]
[DisplayName("YiXinVip 3 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip3 = 3,
[Price(143.9)]
[Price(155.4, 6)]
[DisplayName("YiXinVip 6 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip6 = 6,
[Price(199.9)]
[DisplayName("YiXinVip 10 month")]
YiXinVip10 = 10
[Price(183.2, 8)]
[DisplayName("YiXinVip 8 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip8 = 8,
// 尊享包服务 - 需要VIP资格才能购买
[Price(188.9, 0)]
[DisplayName("Premium Package 5000W Tokens")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(5000)]
PremiumPackage5000W = 101,
[Price(248.9, 0)]
[DisplayName("Premium Package 10000W Tokens")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(10000)]
PremiumPackage10000W = 102,
}
public static class GoodsTypeEnumExtensions
@@ -70,7 +133,7 @@ public static class GoodsTypeEnumExtensions
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.Price ?? 0m;
}
/// <summary>
/// 获取商品价格描述
/// </summary>
@@ -81,7 +144,7 @@ public static class GoodsTypeEnumExtensions
var price = goodsType.GetTotalAmount();
return $"¥{price:F1}";
}
/// <summary>
/// 获取商品名称
/// </summary>
@@ -93,4 +156,110 @@ 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 GoodsCategoryType GetGoodsCategory(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>();
return categoryAttribute?.Category ?? GoodsCategoryType.VipService;
}
/// <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.VipService;
}
/// <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>
/// 计算折扣金额(仅用于尊享包)
/// 规则每累加充值10元减少1元最多减少20元
/// </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元减1元
var discountAmount = Math.Floor(totalRechargeAmount / 10m);
// 最多减少20元
return Math.Min(discountAmount, 20m);
}
/// <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.Max(discountedPrice, 0.01m);
}
}

View File

@@ -4,5 +4,6 @@ public enum ModelTypeEnum
{
Chat = 0,
Image = 1,
Embedding = 2
Embedding = 2,
PremiumChat = 3
}

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

@@ -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,167 @@
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("OpenAI对话异常" + response.StatusCode.ToString());
}
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,313 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
/// <summary>
/// OpenAI到Claude适配器服务
/// 将Claude格式的请求转换为OpenAI格式然后将OpenAI的响应转换为Claude格式
/// </summary>
public class CustomOpenAIAnthropicChatCompletionsService(
IAbpLazyServiceProvider serviceProvider,
ILogger<CustomOpenAIAnthropicChatCompletionsService> logger)
: IAnthropicChatCompletionService
{
private IChatCompletionService GetChatCompletionService()
{
return serviceProvider.GetRequiredKeyedService<IChatCompletionService>(nameof(OpenAiChatCompletionsService));
}
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe aiModelDescribe,
AnthropicInput request,
CancellationToken cancellationToken = default)
{
// 转换请求格式Claude -> OpenAI
var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request);
if (openAIRequest.Model.StartsWith("gpt-5"))
{
openAIRequest.MaxCompletionTokens = request.MaxTokens;
openAIRequest.MaxTokens = null;
}
else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini"))
{
openAIRequest.MaxCompletionTokens = request.MaxTokens;
openAIRequest.MaxTokens = null;
openAIRequest.Temperature = null;
}
// 调用OpenAI服务
var openAIResponse =
await GetChatCompletionService().CompleteChatAsync(aiModelDescribe,openAIRequest, cancellationToken);
// 转换响应格式OpenAI -> Claude
var claudeResponse = AnthropicToOpenAi.ConvertOpenAIToClaude(openAIResponse, request);
return claudeResponse;
}
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe aiModelDescribe,
AnthropicInput request,
CancellationToken cancellationToken = default)
{
var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request);
openAIRequest.Stream = true;
if (openAIRequest.Model.StartsWith("gpt-5"))
{
openAIRequest.MaxCompletionTokens = request.MaxTokens;
openAIRequest.MaxTokens = null;
}
else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini"))
{
openAIRequest.MaxCompletionTokens = request.MaxTokens;
openAIRequest.MaxTokens = null;
openAIRequest.Temperature = null;
}
var messageId = Guid.NewGuid().ToString();
var hasStarted = false;
var hasTextContentBlockStarted = false;
var hasThinkingContentBlockStarted = false;
var toolBlocksStarted = new Dictionary<int, bool>(); // 使用索引而不是ID
var toolCallIds = new Dictionary<int, string>(); // 存储每个索引对应的ID
var toolCallIndexToBlockIndex = new Dictionary<int, int>(); // 工具调用索引到块索引的映射
var accumulatedUsage = new AnthropicCompletionDtoUsage();
var isFinished = false;
var currentContentBlockType = ""; // 跟踪当前内容块类型
var currentBlockIndex = 0; // 跟踪当前块索引
var lastContentBlockType = ""; // 跟踪最后一个内容块类型,用于确定停止原因
await foreach (var openAIResponse in GetChatCompletionService().CompleteChatStreamAsync(aiModelDescribe,openAIRequest,
cancellationToken))
{
// 发送message_start事件
if (!hasStarted && openAIResponse.Choices?.Count > 0 &&
openAIResponse.Choices.Any(x => x.Delta.ToolCalls?.Count > 0) == false)
{
hasStarted = true;
var messageStartEvent = AnthropicToOpenAi.CreateMessageStartEvent(messageId, request.Model);
yield return ("message_start", messageStartEvent);
}
// 更新使用情况统计
if (openAIResponse.Usage != null)
{
// 使用最新的token计数OpenAI通常在最后的响应中提供完整的统计
if (openAIResponse.Usage.PromptTokens.HasValue)
{
accumulatedUsage.InputTokens = openAIResponse.Usage.PromptTokens.Value;
}
if (openAIResponse.Usage.CompletionTokens.HasValue)
{
accumulatedUsage.OutputTokens = (int)openAIResponse.Usage.CompletionTokens.Value;
}
if (openAIResponse.Usage.PromptTokensDetails?.CachedTokens.HasValue == true)
{
accumulatedUsage.CacheReadInputTokens =
openAIResponse.Usage.PromptTokensDetails.CachedTokens.Value;
}
// 记录调试信息
logger.LogDebug("OpenAI Usage更新: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}",
accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens,
accumulatedUsage.CacheReadInputTokens);
}
if (openAIResponse.Choices is { Count: > 0 })
{
var choice = openAIResponse.Choices.First();
// 处理内容
if (!string.IsNullOrEmpty(choice.Delta?.Content))
{
// 如果当前有其他类型的内容块在运行,先结束它们
if (currentContentBlockType != "text" && !string.IsNullOrEmpty(currentContentBlockType))
{
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
stopEvent.Index = currentBlockIndex;
yield return ("content_block_stop", stopEvent);
currentBlockIndex++; // 切换内容块时增加索引
currentContentBlockType = "";
}
// 发送content_block_start事件仅第一次
if (!hasTextContentBlockStarted || currentContentBlockType != "text")
{
hasTextContentBlockStarted = true;
currentContentBlockType = "text";
lastContentBlockType = "text";
var contentBlockStartEvent = AnthropicToOpenAi.CreateContentBlockStartEvent();
contentBlockStartEvent.Index = currentBlockIndex;
yield return ("content_block_start",
contentBlockStartEvent);
}
// 发送content_block_delta事件
var contentDeltaEvent = AnthropicToOpenAi.CreateContentBlockDeltaEvent(choice.Delta.Content);
contentDeltaEvent.Index = currentBlockIndex;
yield return ("content_block_delta",
contentDeltaEvent);
}
// 处理工具调用
if (choice.Delta?.ToolCalls is { Count: > 0 })
{
foreach (var toolCall in choice.Delta.ToolCalls)
{
var toolCallIndex = toolCall.Index; // 使用索引来标识工具调用
// 发送tool_use content_block_start事件
if (toolBlocksStarted.TryAdd(toolCallIndex, true))
{
// 如果当前有文本或thinking内容块在运行先结束它们
if (currentContentBlockType == "text" || currentContentBlockType == "thinking")
{
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
stopEvent.Index = currentBlockIndex;
yield return ("content_block_stop", stopEvent);
currentBlockIndex++; // 增加块索引
}
// 如果当前有其他工具调用在运行,也需要结束它们
else if (currentContentBlockType == "tool_use")
{
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
stopEvent.Index = currentBlockIndex;
yield return ("content_block_stop", stopEvent);
currentBlockIndex++; // 增加块索引
}
currentContentBlockType = "tool_use";
lastContentBlockType = "tool_use";
// 为此工具调用分配一个新的块索引
toolCallIndexToBlockIndex[toolCallIndex] = currentBlockIndex;
// 保存工具调用的ID如果有的话
if (!string.IsNullOrEmpty(toolCall.Id))
{
toolCallIds[toolCallIndex] = toolCall.Id;
}
else if (!toolCallIds.ContainsKey(toolCallIndex))
{
// 如果没有ID且之前也没有保存过生成一个新的ID
toolCallIds[toolCallIndex] = Guid.NewGuid().ToString();
}
var toolBlockStartEvent = AnthropicToOpenAi.CreateToolBlockStartEvent(
toolCallIds[toolCallIndex],
toolCall.Function?.Name);
toolBlockStartEvent.Index = currentBlockIndex;
yield return ("content_block_start",
toolBlockStartEvent);
}
// 如果有增量的参数发送content_block_delta事件
if (!string.IsNullOrEmpty(toolCall.Function?.Arguments))
{
var toolDeltaEvent =
AnthropicToOpenAi.CreateToolBlockDeltaEvent(toolCall.Function.Arguments);
// 使用该工具调用对应的块索引
toolDeltaEvent.Index = toolCallIndexToBlockIndex[toolCallIndex];
yield return ("content_block_delta",
toolDeltaEvent);
}
}
}
// 处理推理内容
if (!string.IsNullOrEmpty(choice.Delta?.ReasoningContent))
{
// 如果当前有其他类型的内容块在运行,先结束它们
if (currentContentBlockType != "thinking" && !string.IsNullOrEmpty(currentContentBlockType))
{
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
stopEvent.Index = currentBlockIndex;
yield return ("content_block_stop", stopEvent);
currentBlockIndex++; // 增加块索引
currentContentBlockType = "";
}
// 对于推理内容,也需要发送对应的事件
if (!hasThinkingContentBlockStarted || currentContentBlockType != "thinking")
{
hasThinkingContentBlockStarted = true;
currentContentBlockType = "thinking";
lastContentBlockType = "thinking";
var thinkingBlockStartEvent = AnthropicToOpenAi.CreateThinkingBlockStartEvent();
thinkingBlockStartEvent.Index = currentBlockIndex;
yield return ("content_block_start",
thinkingBlockStartEvent);
}
var thinkingDeltaEvent =
AnthropicToOpenAi.CreateThinkingBlockDeltaEvent(choice.Delta.ReasoningContent);
thinkingDeltaEvent.Index = currentBlockIndex;
yield return ("content_block_delta",
thinkingDeltaEvent);
}
// 处理结束
if (!string.IsNullOrEmpty(choice.FinishReason) && !isFinished)
{
isFinished = true;
// 发送content_block_stop事件如果有活跃的内容块
if (!string.IsNullOrEmpty(currentContentBlockType))
{
var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
contentBlockStopEvent.Index = currentBlockIndex;
yield return ("content_block_stop",
contentBlockStopEvent);
}
// 发送message_delta事件
var messageDeltaEvent = AnthropicToOpenAi.CreateMessageDeltaEvent(
AnthropicToOpenAi.GetStopReasonByLastContentType(choice.FinishReason, lastContentBlockType),
accumulatedUsage);
// 记录最终Usage统计
logger.LogDebug(
"流式响应结束最终Usage: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}",
accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens,
accumulatedUsage.CacheReadInputTokens);
yield return ("message_delta",
messageDeltaEvent);
// 发送message_stop事件
var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent();
yield return ("message_stop",
messageStopEvent);
}
}
}
// 确保流正确结束
if (!isFinished)
{
if (!string.IsNullOrEmpty(currentContentBlockType))
{
var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
contentBlockStopEvent.Index = currentBlockIndex;
yield return ("content_block_stop",
contentBlockStopEvent);
}
var messageDeltaEvent =
AnthropicToOpenAi.CreateMessageDeltaEvent(
AnthropicToOpenAi.GetStopReasonByLastContentType("end_turn", lastContentBlockType),
accumulatedUsage);
yield return ("message_delta", messageDeltaEvent);
var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent();
yield return ("message_stop",
messageStopEvent);
}
}
}

View File

@@ -0,0 +1,167 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey);
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorizedAccessException();
}
// 如果限流则抛出限流异常
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new ThorRateLimitException();
}
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync();
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
error);
throw new BusinessException("OpenAI对话异常" + error, response.StatusCode.ToString());
}
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
string? line = string.Empty;
var first = true;
var isThink = false;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
line);
throw new BusinessException("OpenAI对话异常", line);
}
if (line.StartsWith(OpenAIConstant.Data))
line = line[OpenAIConstant.Data.Length..];
line = line.Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
if (line == OpenAIConstant.Done)
{
break;
}
if (line.StartsWith(':'))
{
continue;
}
var result = JsonSerializer.Deserialize<ThorChatCompletionsResponse>(line,
ThorJsonSerializer.DefaultOptions);
if (result == null)
{
continue;
}
var content = result?.Choices?.FirstOrDefault()?.Delta;
if (first && content?.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink && result?.Choices != null)
{
// 需要将content的内容转换到其他字段
foreach (var choice in result.Choices)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
yield return result;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new BusinessException("渠道未登录,请联系管理人员", "401");
}
// 如果限流则抛出限流异常
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new ThorRateLimitException();
}
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
response.StatusCode, error);
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
}
var result =
await response.Content.ReadFromJsonAsync<ThorChatCompletionsResponse>(
cancellationToken: cancellationToken).ConfigureAwait(false);
return result;
}
}

View File

@@ -9,7 +9,7 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletionsService> logger)
public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
@@ -24,7 +24,7 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey);
@@ -142,7 +142,7 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
options.Endpoint = "https://api.deepseek.com/v1";
}
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);

View File

@@ -4,7 +4,7 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
public sealed class SiliconFlowTextEmbeddingService
public sealed class SiliconFlowTextEmbeddingService(IHttpClientFactory httpClientFactory)
: ITextEmbeddingService
{
public async Task<EmbeddingCreateResponse> EmbeddingAsync(
@@ -12,7 +12,7 @@ public sealed class SiliconFlowTextEmbeddingService
AiModelDescribe? options = null,
CancellationToken cancellationToken = default)
{
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/v1/embeddings",
createEmbeddingModel, options!.ApiKey);

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
@@ -20,4 +21,12 @@ public class SpecialCompatible : ISpecialCompatible,ISingletonDependency
handle(request);
}
}
public void AnthropicCompatible(AnthropicInput request)
{
foreach (var handle in _options.Value.AnthropicHandles)
{
handle(request);
}
}
}

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 class SpecialCompatibleOptions
{
public List<Action<ThorChatCompletionsRequest>> Handles { get; set; } = new();
public List<Action<AnthropicInput>> AnthropicHandles { get; set; } = new();
}

View File

@@ -1,6 +1,7 @@
using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel.Util;
using Alipay.EasySDK.Payment.Page.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services;
@@ -21,13 +22,13 @@ public class AlipayManager : DomainService
/// <returns></returns>
/// <exception cref="AlipayException"></exception>
public Task<AlipayTradePagePayResponse> PaymentPageAsync(string productName, string orderNumber,
decimal totalAmount)
decimal totalAmount, string? returnUrl)
{
try
{
// 2. 发起API调用以创建当面付收款二维码为例
var response = Factory.Payment.Page()
.Pay(productName, orderNumber, totalAmount.ToString(), "https://ccnetcore.com/pay/sucess");
.Pay(productName, orderNumber, totalAmount.ToString(), returnUrl ?? string.Empty);
// 3. 处理响应或异常
if (ResponseChecker.Success(response))
{

View File

@@ -0,0 +1,40 @@
using SqlSugar;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// ai用户表
/// </summary>
[SugarTable("Ai_UserExtraInfo")]
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)]
public class AiUserExtraInfoEntity : Entity<Guid>, IHasCreationTime, ISoftDelete
{
public AiUserExtraInfoEntity()
{
}
public AiUserExtraInfoEntity(Guid userId, string fuwuhaoOpenId)
{
this.UserId = userId;
this.FuwuhaoOpenId = fuwuhaoOpenId;
}
/// <summary>
/// 用户id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 服务号openid
/// </summary>
public string FuwuhaoOpenId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
public bool IsDeleted { get; set; }
}

View File

@@ -29,13 +29,20 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
ModelId = modelId;
if (tokenUsage is not null)
{
long inputTokenCount = tokenUsage.PromptTokens
?? tokenUsage.InputTokens
?? 0;
long inputTokenCount =
(tokenUsage.PromptTokens.HasValue && tokenUsage.PromptTokens.Value != 0)
? tokenUsage.PromptTokens.Value
: (tokenUsage.InputTokens.HasValue && tokenUsage.InputTokens.Value != 0)
? tokenUsage.InputTokens.Value
: 0;
long outputTokenCount =
(tokenUsage.CompletionTokens.HasValue && tokenUsage.CompletionTokens.Value != 0)
? tokenUsage.CompletionTokens.Value
: (tokenUsage.OutputTokens.HasValue && tokenUsage.OutputTokens.Value != 0)
? tokenUsage.OutputTokens.Value
: 0;
long outputTokenCount = tokenUsage.CompletionTokens
?? tokenUsage.OutputTokens
?? 0;
this.TokenUsage = new TokenUsageValueObject
{

View File

@@ -0,0 +1,147 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 尊享包聚合根
/// 用于给VIP扩展额外购买尊享token包
/// </summary>
[SugarTable("Ai_PremiumPackage")]
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)]
public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public PremiumPackageAggregateRoot()
{
}
public PremiumPackageAggregateRoot(Guid userId, long totalTokens, string packageName)
{
UserId = userId;
TotalTokens = totalTokens;
RemainingTokens = totalTokens;
PackageName = packageName;
IsActive = true;
}
/// <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; }
/// <summary>
/// 消耗token
/// </summary>
/// <param name="tokenCount">消耗的token数量</param>
/// <returns>是否消耗成功</returns>
public bool ConsumeTokens(long tokenCount)
{
if (RemainingTokens < tokenCount)
{
return false;
}
if (!IsActive)
{
return false;
}
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
{
return false;
}
RemainingTokens -= tokenCount;
UsedTokens += tokenCount;
return true;
}
/// <summary>
/// 检查是否可用
/// </summary>
/// <returns>是否可用</returns>
public bool IsAvailable()
{
if (!IsActive)
{
return false;
}
if (RemainingTokens <= 0)
{
return false;
}
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
{
return false;
}
return true;
}
/// <summary>
/// 停用尊享包
/// </summary>
public void Deactivate()
{
IsActive = false;
}
/// <summary>
/// 激活尊享包
/// </summary>
public void Activate()
{
IsActive = true;
}
/// <summary>
/// 设置到期时间
/// </summary>
/// <param name="expireDateTime">到期时间</param>
public void SetExpireDateTime(DateTime expireDateTime)
{
ExpireDateTime = expireDateTime;
}
}

View File

@@ -13,11 +13,14 @@ using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
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.Core.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions;
using JsonSerializer = System.Text.Json.JsonSerializer;
using ThorJsonSerializer = Yi.Framework.AiHub.Domain.AiGateWay.ThorJsonSerializer;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -28,6 +31,7 @@ public class AiGateWayManager : DomainService
private readonly AiMessageManager _aiMessageManager;
private readonly UsageStatisticsManager _usageStatisticsManager;
private readonly ISpecialCompatible _specialCompatible;
private PremiumPackageManager? _premiumPackageManager;
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
@@ -40,6 +44,9 @@ public class AiGateWayManager : DomainService
_specialCompatible = specialCompatible;
}
private PremiumPackageManager PremiumPackageManager =>
_premiumPackageManager ??= LazyServiceProvider.LazyGetRequiredService<PremiumPackageManager>();
/// <summary>
/// 获取模型
/// </summary>
@@ -132,7 +139,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = data.Choices.FirstOrDefault()?.Delta.Content,
Content = data.Choices?.FirstOrDefault()?.Delta.Content,
ModelId = request.Model,
TokenUsage = data.Usage
});
@@ -195,6 +202,11 @@ public class AiGateWayManager : DomainService
// 如果没有完成,才等待,已完成,全部输出
await Task.Delay(outputInterval, cancellationToken).ConfigureAwait(false);
}
else
{
//已经完成了,也等待,但是速度可以放快
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
}
}
}, cancellationToken);
@@ -389,7 +401,7 @@ public class AiGateWayManager : DomainService
var usage = new ThorUsageResponse()
{
PromptTokens = stream.Usage?.PromptTokens??0,
PromptTokens = stream.Usage?.PromptTokens ?? 0,
InputTokens = stream.Usage?.InputTokens ?? 0,
CompletionTokens = 0,
TotalTokens = stream.Usage?.InputTokens ?? 0
@@ -436,4 +448,240 @@ public class AiGateWayManager : DomainService
throw new UserFriendlyException(errorContent);
}
}
/// <summary>
/// Anthropic聊天完成-流式
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> AnthropicCompleteChatStreamAsync(
AnthropicInput request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
_specialCompatible.AnthropicCompatible(request);
var modelDescribe = await GetModelAsync(request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
await foreach (var result in chatService.StreamChatCompletionsAsync(modelDescribe, request, cancellationToken))
{
yield return result;
}
}
/// <summary>
/// Anthropic聊天完成-非流式
/// </summary>
/// <param name="httpContext"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task AnthropicCompleteChatForStatisticsAsync(HttpContext httpContext,
AnthropicInput request,
Guid? userId = null,
Guid? sessionId = null,
CancellationToken cancellationToken = default)
{
_specialCompatible.AnthropicCompatible(request);
var response = httpContext.Response;
// 设置响应头,声明是 json
//response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken);
if (userId is not null)
{
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = request.Messages?.FirstOrDefault()?.Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = data.TokenUsage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = data.content?.FirstOrDefault()?.text,
ModelId = request.Model,
TokenUsage = data.TokenUsage
});
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
// 扣减尊享token包用量
var totalTokens = (data.TokenUsage?.InputTokens ?? 0) + (data.TokenUsage?.OutputTokens ?? 0);
if (totalTokens > 0)
{
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
if (!consumeSuccess)
{
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败消耗token数: {totalTokens}");
}
}
}
await response.WriteAsJsonAsync(data, cancellationToken);
}
/// <summary>
/// Anthropic聊天完成-缓存处理
/// </summary>
/// <param name="httpContext"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task AnthropicCompleteChatStreamForStatisticsAsync(
HttpContext httpContext,
AnthropicInput request,
Guid? userId = null,
Guid? sessionId = null,
CancellationToken cancellationToken = default)
{
var response = httpContext.Response;
// 设置响应头,声明是 SSE 流
response.ContentType = "text/event-stream;charset=utf-8;";
response.Headers.TryAdd("Cache-Control", "no-cache");
response.Headers.TryAdd("Connection", "keep-alive");
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
var completeChatResponse = gateWay.AnthropicCompleteChatStreamAsync(request, cancellationToken);
ThorUsageResponse? tokenUsage = null;
StringBuilder backupSystemContent = new StringBuilder();
try
{
await foreach (var responseResult in completeChatResponse)
{
//message_start是为了保底机制
if (responseResult.Item1.Contains("message_delta")||responseResult.Item1.Contains("message_start"))
{
tokenUsage = responseResult.Item2?.TokenUsage;
}
backupSystemContent.Append(responseResult.Item2?.Delta?.Text);
await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2,
cancellationToken);
}
}
catch (Exception e)
{
_logger.LogError(e, $"Ai对话异常");
var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
throw new UserFriendlyException(errorContent);
// var model = new AnthropicStreamDto
// {
// Message = new AnthropicChatCompletionDto
// {
// content =
// [
// new AnthropicChatCompletionDtoContent
// {
// text = errorContent,
// }
// ],
// },
// Error = new AnthropicStreamErrorDto
// {
// Type = null,
// Message = errorContent
// }
// };
// var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
// {
// ContractResolver = new CamelCasePropertyNamesContractResolver()
// });
// await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
}
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = tokenUsage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = backupSystemContent.ToString(),
ModelId = request.Model,
TokenUsage = tokenUsage
});
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
// 扣减尊享token包用量
if (userId.HasValue && tokenUsage is not null)
{
var totalTokens = tokenUsage.TotalTokens??0;
if (totalTokens > 0)
{
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
if (!consumeSuccess)
{
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败消耗token数: {totalTokens}");
}
}
}
}
#region Anthropic格式Http响应
private static readonly byte[] EventPrefix = "event: "u8.ToArray();
private static readonly byte[] DataPrefix = "data: "u8.ToArray();
private static readonly byte[] NewLine = "\n"u8.ToArray();
private static readonly byte[] DoubleNewLine = "\n\n"u8.ToArray();
/// <summary>
/// 使用 JsonSerializer.SerializeAsync 直接序列化到响应流
/// </summary>
private static async ValueTask WriteAsEventStreamDataAsync<T>(
HttpContext context,
string @event,
T value,
CancellationToken cancellationToken = default)
where T : class
{
var response = context.Response;
var bodyStream = response.Body;
// 确保 SSE Header 已经设置好
// e.g. Content-Type: text/event-stream; charset=utf-8
await response.StartAsync(cancellationToken).ConfigureAwait(false);
// 写事件类型
await bodyStream.WriteAsync(EventPrefix, cancellationToken).ConfigureAwait(false);
await WriteUtf8StringAsync(bodyStream, @event.Trim(), cancellationToken).ConfigureAwait(false);
await bodyStream.WriteAsync(NewLine, cancellationToken).ConfigureAwait(false);
// 写 data: + JSON
await bodyStream.WriteAsync(DataPrefix, cancellationToken).ConfigureAwait(false);
await JsonSerializer.SerializeAsync(
bodyStream,
value,
ThorJsonSerializer.DefaultOptions,
cancellationToken
).ConfigureAwait(false);
// 事件结束 \n\n
await bodyStream.WriteAsync(DoubleNewLine, cancellationToken).ConfigureAwait(false);
// 及时把数据发送给客户端
await bodyStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
private static async ValueTask WriteUtf8StringAsync(Stream stream, string value, CancellationToken token)
{
if (string.IsNullOrEmpty(value))
return;
var buffer = Encoding.UTF8.GetBytes(value);
await stream.WriteAsync(buffer, token).ConfigureAwait(false);
}
#endregion
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
/// <summary>
/// AccessToken响应对象
/// </summary>
public class AccessTokenResponse
{
/// <summary>
/// 访问令牌
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// 过期时间(秒)
/// </summary>
public int ExpiresIn { get; set; }
}

View File

@@ -0,0 +1,249 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Caching;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
public class FuwuhaoManager : DomainService
{
private readonly FuwuhaoOptions _options;
private readonly IHttpClientFactory _httpClientFactory;
private IDistributedCache<AccessTokenResponse> _accessTokenCache;
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
private readonly ILogger<FuwuhaoManager> _logger;
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
IDistributedCache<AccessTokenResponse> accessTokenCache, ILogger<FuwuhaoManager> logger)
{
_options = options.Value;
_httpClientFactory = httpClientFactory;
_userRepository = userRepository;
_accessTokenCache = accessTokenCache;
_logger = logger;
}
/// <summary>
/// 获取微信公众号AccessToken
/// </summary>
/// <returns>AccessToken响应对象</returns>
private async Task<string> GetAccessTokenAsync()
{
var output = await _accessTokenCache.GetOrAddAsync("Fuwuhao", async () =>
{
var url =
$"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={_options.AppId}&secret={_options.Secret}";
var response = await _httpClientFactory.CreateClient().GetAsync(url);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<AccessTokenResponse>(jsonContent, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
return result;
}, () => new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3600)
});
return output.AccessToken;
}
/// <summary>
/// 创建带参数的二维码
/// </summary>
/// <param name="scene">场景值</param>
/// <returns>二维码URL</returns>
public async Task<string> CreateQrCodeAsync(string scene)
{
var accessToken = await GetAccessTokenAsync();
var url = $"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={accessToken}";
var requestBody = new
{
action_name = "QR_STR_SCENE",
expire_seconds = 600,
action_info = new
{
scene = new
{
scene_str = scene
}
}
};
var jsonContent = JsonSerializer.Serialize(requestBody);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClientFactory.CreateClient().PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<QrCodeResponse>(responseContent);
return $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={result.Ticket}";
}
/// <summary>
/// 通过code获取用户基础信息接口为了获取用户access_token和openid
/// </summary>
/// <param name="code">授权码</param>
/// <returns>用户基础信息响应对象</returns>
private async Task<UserBaseInfoResponse> GetBaseUserInfoByCodeAsync(string code)
{
var url =
$"https://api.weixin.qq.com/sns/oauth2/access_token?appid={_options.AppId}&secret={_options.Secret}&code={code}&grant_type=authorization_code";
var response = await _httpClientFactory.CreateClient().GetAsync(url);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"服务号code获取用户基础信息{jsonContent}");
var result = JsonSerializer.Deserialize<UserBaseInfoResponse>(jsonContent);
return result;
}
/// <summary>
/// 通过code获取用户信息接口
/// </summary>
/// <param name="code">授权码</param>
/// <returns>用户信息响应对象</returns>
public async Task<UserInfoResponse?> GetUserInfoByCodeAsync(string code)
{
var baseUserInfo = await GetBaseUserInfoByCodeAsync(code);
var url =
$"https://api.weixin.qq.com/sns/userinfo?access_token={baseUserInfo.AccessToken}&openid={baseUserInfo.OpenId}&lang=zh_CN";
var response = await _httpClientFactory.CreateClient().GetAsync(url);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"服务号code获取用户详细信息{jsonContent}");
var result = JsonSerializer.Deserialize<UserInfoResponse>(jsonContent);
return result;
}
/// <summary>
/// 校验微信服务器回调参数是否正确
/// </summary>
/// <param name="signature">微信加密签名</param>
/// <param name="timestamp">时间戳</param>
/// <param name="nonce">随机数</param>
/// <returns>true表示验证通过false表示验证失败</returns>
public void ValidateCallback(string signature, string timestamp, string nonce)
{
var token = _options.CallbackToken; // 您设置的token
// 将token、timestamp、nonce三个参数进行字典序排序
var parameters = new[] { token, timestamp, nonce };
Array.Sort(parameters);
// 将三个参数字符串拼接成一个字符串
var concatenated = string.Join("", parameters);
// 进行SHA1计算签名
using (var sha1 = SHA1.Create())
{
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(concatenated));
var calculatedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
// 与URL链接中的signature参数进行对比
var result = calculatedSignature.Equals(signature, StringComparison.OrdinalIgnoreCase);
if (!result)
{
throw new UserFriendlyException("服务号回调签名异常");
}
}
}
/// <summary>
/// 构建引导注册图文消息体
/// </summary>
/// <param name="toUser">接收用户的OpenID</param>
/// <param name="title">图文消息标题</param>
/// <param name="description">图文消息描述</param>
/// <returns>XML格式的图文消息体</returns>
public string BuildRegisterMessage(string toUser, string title="意社区点击一键注册账号", string description="来自意社区SSO统一注册安全中心")
{
var createTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var fromUser = _options.FromUser;
var url =
$"https://open.weixin.qq.com/connect/oauth2/authorize?appid={_options.AppId}&redirect_uri={_options.RedirectUri}&response_type=code&scope=snsapi_userinfo&state={createTime}#wechat_redirect";
var xml = $@"<xml>
<ToUserName><![CDATA[{toUser}]]></ToUserName>
<FromUserName><![CDATA[{fromUser}]]></FromUserName>
<CreateTime>{createTime}</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[{title}]]></Title>
<Description><![CDATA[{description}]]></Description>
<PicUrl><![CDATA[{_options.PicUrl}]]></PicUrl>
<Url><![CDATA[{url}]]></Url>
</item>
</Articles>
</xml>";
return xml;
}
/// <summary>
/// 处理回调逻辑
/// </summary>
/// <param name="sceneType"></param>
/// <param name="openId"></param>
/// <param name="bindUserId"></param>
/// <returns></returns>
public async Task<(SceneResultEnum SceneResult,Guid? UserId)> CallBackHandlerAsync(SceneTypeEnum sceneType, string openId, Guid? bindUserId)
{
var aiUserInfo = await _userRepository._DbQueryable.Where(x => x.FuwuhaoOpenId == openId).FirstAsync();
switch (sceneType)
{
case SceneTypeEnum.LoginOrRegister:
//有openid说明登录成功
if (aiUserInfo is not null)
{
return (SceneResultEnum.Login,aiUserInfo.UserId);
}
//无openid说明需要进行注册
else
{
return (SceneResultEnum.Register,null);
}
break;
case SceneTypeEnum.Bind:
//说明已经有微信号,直接换绑
if (aiUserInfo is not null)
{
await _userRepository.DeleteByIdAsync(aiUserInfo.Id);
}
if (bindUserId is null)
{
throw new UserFriendlyException("绑定用户需要传入绑定的用户id");
}
//说明没有绑定过,直接绑定
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(bindUserId.Value, openId));
return (SceneResultEnum.Bind,bindUserId);
break;
default:
throw new ArgumentOutOfRangeException(nameof(sceneType), sceneType, null);
}
}
}

View File

@@ -0,0 +1,34 @@
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
public class FuwuhaoOptions
{
/// <summary>
/// 微信公众号AppId
/// </summary>
public string AppId { get; set; }
/// <summary>
/// 微信公众号AppSecret
/// </summary>
public string Secret { get; set; }
/// <summary>
/// 回调token
/// </summary>
public string CallbackToken { get; set; }
/// <summary>
/// 微信公众号原始ID用于FromUser
/// </summary>
public string FromUser { get; set; }
/// <summary>
/// 微信网页授权跳转地址
/// </summary>
public string RedirectUri { get; set; }
/// <summary>
/// 图片地址
/// </summary>
public string PicUrl { get; set; }
}

View File

@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
/// <summary>
/// 二维码响应对象
/// </summary>
public class QrCodeResponse
{
/// <summary>
/// 二维码票据
/// </summary>
[JsonPropertyName("ticket")]
public string Ticket { get; set; }
/// <summary>
/// 过期时间(秒)
/// </summary>
[JsonPropertyName("expire_seconds")]
public int ExpireSeconds { get; set; }
/// <summary>
/// 二维码URL
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
/// <summary>
/// 用户基础信息响应对象
/// </summary>
public class UserBaseInfoResponse
{
/// <summary>
/// 访问令牌
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// 过期时间(秒)
/// </summary>
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
/// <summary>
/// 刷新令牌
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// 用户OpenID
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 授权作用域
/// </summary>
[JsonPropertyName("scope")]
public string Scope { get; set; }
}

View File

@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
/// <summary>
/// 用户信息响应对象
/// </summary>
public class UserInfoResponse
{
/// <summary>
/// 用户OpenID
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
[JsonPropertyName("nickname")]
public string Nickname { get; set; }
/// <summary>
/// 用户性别值为1时是男性值为2时是女性值为0时是未知
/// </summary>
[JsonPropertyName("sex")]
public int Sex { get; set; }
/// <summary>
/// 用户个人资料填写的省份
/// </summary>
[JsonPropertyName("province")]
public string Province { get; set; }
/// <summary>
/// 普通用户个人资料填写的城市
/// </summary>
[JsonPropertyName("city")]
public string City { get; set; }
/// <summary>
/// 国家如中国为CN
/// </summary>
[JsonPropertyName("country")]
public string Country { get; set; }
/// <summary>
/// 用户头像最后一个数值代表正方形头像大小有0、46、64、96、132数值可选0代表640*640正方形头像
/// </summary>
[JsonPropertyName("headimgurl")]
public string HeadImgUrl { get; set; }
}

View File

@@ -1,9 +1,11 @@
using System.Text.Json;
using Volo.Abp.Domain.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.AiHub.Domain.Extensions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -15,14 +17,18 @@ public class PayManager : DomainService
private readonly ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> _payNoticeRepository;
private readonly ICurrentUser _currentUser;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
private readonly ISqlSugarRepository<AiRechargeAggregateRoot, Guid> _rechargeRepository;
public PayManager(
ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> payNoticeRepository,
ICurrentUser currentUser, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
ICurrentUser currentUser,
ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
ISqlSugarRepository<AiRechargeAggregateRoot, Guid> rechargeRepository)
{
_payNoticeRepository = payNoticeRepository;
_currentUser = currentUser;
_payOrderRepository = payOrderRepository;
_rechargeRepository = rechargeRepository;
}
/// <summary>
@@ -38,18 +44,43 @@ public class PayManager : DomainService
throw new UserFriendlyException("用户未登录");
}
var userId = _currentUser.GetId();
// 如果是尊享包商品需要验证用户是否为VIP
if (goodsType.IsPremiumPackage())
{
if (!_currentUser.IsAiVip())
{
throw new UserFriendlyException("购买尊享包需要VIP资格请先开通VIP");
}
}
// 生成订单号
var outTradeNo = GenerateOutTradeNo();
// 获取商品信息
var goodsName = goodsType.GetDisplayName();
var totalAmount = goodsType.GetTotalAmount();
// 计算订单金额尊享包使用折扣价格VIP服务使用原价
decimal totalAmount;
if (goodsType.IsPremiumPackage())
{
// 获取用户累加充值金额
var totalRechargeAmount = await GetUserTotalRechargeAmountAsync(userId);
// 使用折扣后的价格
totalAmount = goodsType.GetDiscountedPrice(totalRechargeAmount);
}
else
{
// VIP服务使用原价
totalAmount = goodsType.GetTotalAmount();
}
// 创建订单实体
var payOrder = new PayOrderAggregateRoot
{
OutTradeNo = outTradeNo,
UserId = _currentUser.GetId(),
UserId = userId,
UserName = _currentUser.UserName ?? string.Empty,
TotalAmount = totalAmount,
GoodsName = goodsName,
@@ -69,7 +100,7 @@ public class PayManager : DomainService
/// <param name="tradeStatus">交易状态</param>
/// <param name="tradeNo">支付宝交易号</param>
/// <returns></returns>
public async Task UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null)
public async Task<PayOrderAggregateRoot> UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null)
{
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == outTradeNo);
if (order == null)
@@ -84,6 +115,7 @@ public class PayManager : DomainService
}
await _payOrderRepository.UpdateAsync(order);
return order;
}
@@ -128,13 +160,25 @@ public class PayManager : DomainService
/// <returns></returns>
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
{
return tradeStatus switch
if (Enum.TryParse<TradeStatusEnum>(tradeStatus, out var result))
{
"WAIT_BUYER_PAY" => TradeStatusEnum.WAIT_BUYER_PAY,
"TRADE_SUCCESS" => TradeStatusEnum.TRADE_SUCCESS,
"TRADE_FINISHED" => TradeStatusEnum.TRADE_FINISHED,
"TRADE_CLOSED" => TradeStatusEnum.TRADE_CLOSED,
_ => TradeStatusEnum.WAIT_TRADE
};
return result;
}
return TradeStatusEnum.WAIT_TRADE;
}
}
/// <summary>
/// 获取用户累加充值金额
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>累加充值金额</returns>
public async Task<decimal> GetUserTotalRechargeAmountAsync(Guid userId)
{
var totalAmount = await _rechargeRepository
._DbQueryable
.Where(x => x.UserId == userId )
.SumAsync(x => x.RechargeAmount);
return totalAmount;
}
}

View File

@@ -0,0 +1,184 @@
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
/// <summary>
/// 尊享包管理器
/// </summary>
public class PremiumPackageManager : DomainService
{
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> _premiumPackageRepository;
private readonly ILogger<PremiumPackageManager> _logger;
public PremiumPackageManager(
ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> premiumPackageRepository,
ILogger<PremiumPackageManager> logger)
{
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
}
/// <summary>
/// 为用户创建尊享包
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="goodsType">商品类型</param>
/// <param name="totalAmount">支付金额</param>
/// <param name="expireMonths">过期月数0或null表示永久</param>
/// <returns></returns>
public async Task<PremiumPackageAggregateRoot> CreatePremiumPackageAsync(
Guid userId,
GoodsTypeEnum goodsType,
decimal totalAmount,
int? expireMonths = null)
{
if (!goodsType.IsPremiumPackage())
{
throw new UserFriendlyException($"商品类型 {goodsType} 不是尊享包商品");
}
var tokenAmount = goodsType.GetTokenAmount();
var packageName = goodsType.GetDisplayName();
var premiumPackage = new PremiumPackageAggregateRoot(userId, tokenAmount, packageName)
{
PurchaseAmount = totalAmount
};
// 设置到期时间
if (expireMonths.HasValue && expireMonths.Value > 0)
{
premiumPackage.SetExpireDateTime(DateTime.Now.AddMonths(expireMonths.Value));
}
await _premiumPackageRepository.InsertAsync(premiumPackage);
_logger.LogInformation(
$"用户 {userId} 购买尊享包成功: {packageName}, Token数量: {tokenAmount}, 金额: {totalAmount}");
return premiumPackage;
}
/// <summary>
/// 消耗用户尊享包的Token
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="tokenCount">需要消耗的Token数量</param>
/// <returns>是否消耗成功</returns>
public async Task<bool> ConsumeTokensAsync(Guid userId, long tokenCount)
{
// 获取用户所有可用的尊享包按剩余token升序排列优先消耗快用完的
var availablePackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
.OrderBy(x => x.RemainingTokens)
.ToListAsync();
if (!availablePackages.Any())
{
_logger.LogWarning($"用户 {userId} 没有可用的尊享包");
return false;
}
// 过滤掉已过期的包
var validPackages = availablePackages
.Where(p => p.IsAvailable())
.ToList();
if (!validPackages.Any())
{
_logger.LogWarning($"用户 {userId} 的尊享包已全部过期");
return false;
}
// 计算总可用Token
var totalAvailableTokens = validPackages.Sum(p => p.RemainingTokens);
if (totalAvailableTokens < tokenCount)
{
_logger.LogWarning(
$"用户 {userId} 尊享包Token不足需要: {tokenCount}, 可用: {totalAvailableTokens}");
return false;
}
// 从可用的包中逐个扣除Token
var remainingToConsume = tokenCount;
foreach (var package in validPackages)
{
if (remainingToConsume <= 0)
break;
var toConsume = Math.Min(remainingToConsume, package.RemainingTokens);
if (package.ConsumeTokens(toConsume))
{
await _premiumPackageRepository.UpdateAsync(package);
remainingToConsume -= toConsume;
_logger.LogInformation(
$"用户 {userId} 从尊享包 {package.Id} 消耗 {toConsume} tokens, 剩余: {package.RemainingTokens}");
}
}
return remainingToConsume == 0;
}
/// <summary>
/// 获取用户可用的尊享包总Token数
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>可用Token总数</returns>
public async Task<long> GetAvailableTokensAsync(Guid userId)
{
var packages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
.ToListAsync();
return packages
.Where(p => p.IsAvailable())
.Sum(p => p.RemainingTokens);
}
/// <summary>
/// 获取用户的所有尊享包
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>尊享包列表</returns>
public async Task<List<PremiumPackageAggregateRoot>> GetUserPremiumPackagesAsync(Guid userId)
{
return await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.CreationTime)
.ToListAsync();
}
/// <summary>
/// 停用过期的尊享包
/// </summary>
/// <returns>停用的包数量</returns>
public async Task<int> DeactivateExpiredPackagesAsync()
{
_logger.LogInformation("开始执行尊享包过期自动停用任务");
var now = DateTime.Now;
var expiredPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.IsActive && x.ExpireDateTime.HasValue && x.ExpireDateTime.Value < now)
.ToListAsync();
if (!expiredPackages.Any())
{
_logger.LogInformation("没有找到过期的尊享包");
return 0;
}
foreach (var package in expiredPackages)
{
package.Deactivate();
await _premiumPackageRepository.UpdateAsync(package);
}
_logger.LogInformation($"成功停用 {expiredPackages.Count} 个过期的尊享包");
return expiredPackages.Count;
}
}

View File

@@ -21,11 +21,11 @@ public class UsageStatisticsManager : DomainService
public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage)
{
long inputTokenCount = tokenUsage?.PromptTokens
?? tokenUsage.InputTokens
?? tokenUsage?.InputTokens
?? 0;
long outputTokenCount = tokenUsage?.CompletionTokens
?? tokenUsage.OutputTokens
?? tokenUsage?.OutputTokens
?? 0;
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}"))

View File

@@ -1,7 +1,5 @@
using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel;
using Alipay.EasySDK.Kernel.Util;
using Alipay.EasySDK.Payment.FaceToFace.Models;
using Dm.util;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -10,11 +8,15 @@ using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
using Yi.Framework.AiHub.Domain.Shared;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.Mapster;
using Microsoft.Extensions.DependencyInjection;
namespace Yi.Framework.AiHub.Domain
{
@@ -30,26 +32,46 @@ namespace Yi.Framework.AiHub.Domain
var configuration = context.Services.GetConfiguration();
var services = context.Services;
// Configure<AiGateWayOptions>(configuration.GetSection("AiGateWay"));
#region OpenAi ChatCompletion
services.AddKeyedTransient<IChatCompletionService, AzureOpenAiChatCompletionCompletionsService>(
nameof(AzureOpenAiChatCompletionCompletionsService));
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
nameof(AzureDatabricksChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, DeepSeekChatCompletionsService>(
nameof(DeepSeekChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>(
nameof(OpenAiChatCompletionsService));
#endregion
#region Anthropic ChatCompletion
services.AddKeyedTransient<IAnthropicChatCompletionService, CustomOpenAIAnthropicChatCompletionsService>(
nameof(CustomOpenAIAnthropicChatCompletionsService));
services.AddKeyedTransient<IAnthropicChatCompletionService, AnthropicChatCompletionsService>(
nameof(AnthropicChatCompletionsService));
#endregion
#region Image
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
nameof(AzureOpenAIServiceImageService));
#endregion
#region Embedding
services.AddKeyedTransient<ITextEmbeddingService, SiliconFlowTextEmbeddingService>(
nameof(SiliconFlowTextEmbeddingService));
#endregion
//ai模型特殊性兼容处理
Configure<SpecialCompatibleOptions>(options =>
{
options.Handles.add(request => { request.CompatibleCodeCompletion(); });
options.Handles.Add(request =>
{
if (request.Model == "o1")
@@ -76,15 +98,38 @@ namespace Yi.Framework.AiHub.Domain
};
}
});
options.Handles.Add(request =>
{
if (request.MaxTokens >= 16384)
{
request.MaxTokens = 16384;
}
});
options.AnthropicHandles.add(request =>
{
if (request.MaxTokens is null || request.MaxTokens <= 0)
{
throw new UserFriendlyException("MaxTokens must be greater than or equal to 0");
}
});
});
//配置支付宝支付
var config = configuration.GetSection("Alipay").Get<Config>();
Factory.SetOptions(config);
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
//配置服务号
Configure<FuwuhaoOptions>(configuration.GetSection("Fuwuhao"));
services.AddHttpClient()
.ConfigureHttpClientDefaults(builder =>
{
builder.ConfigureHttpClient(client =>
{
client.Timeout = TimeSpan.FromMinutes(10);
});
});
}
}
}

View File

@@ -49,7 +49,7 @@ public class SemanticKernelClient : ITransientDependency
// MaxTokens =1000
};
var chatCompletionService = this.Kernel.GetRequiredService<IChatCompletionService>();
var chatCompletionService = this.Kernel.GetRequiredService<IChatCompletionService>("gpt-5-mini");
var results = await chatCompletionService.GetChatMessageContentsAsync(
question,

View File

@@ -32,18 +32,18 @@ namespace Yi.Framework.Stock.Domain
foreach (var optionsModelId in options.ModelIds)
{
services.AddKernel()
.AddAzureOpenAIChatCompletion(
deploymentName: optionsModelId,
endpoint: options.Endpoint,
apiKey: options.ApiKey,
serviceId: optionsModelId,
modelId: optionsModelId);
// .AddAzureOpenAIChatCompletion(
// deploymentName: optionsModelId,
// endpoint: options.Endpoint,
// apiKey: options.ApiKey,
// serviceId: optionsModelId,
// modelId: optionsModelId);
// .AddOpenAIChatCompletion(
// serviceId: optionsModelId,
// modelId: optionsModelId,
// endpoint: new Uri(options.Endpoint),
// apiKey: options.ApiKey);
.AddOpenAIChatCompletion(
serviceId: optionsModelId,
modelId: optionsModelId,
endpoint: new Uri(options.Endpoint),
apiKey: options.ApiKey);
}
// 添加插件
services.AddSingleton<KernelPlugin>(sp =>

View File

@@ -170,7 +170,7 @@ public class WeChatMiniProgramAccountService : ApplicationService
//走普通注册流程
//同时再加一个小程序绑定即可
var userName = GenerateRandomString(6);
await _accountService.PostTempRegisterAsync(new RegisterDto
await _accountService.PostSystemRegisterAsync(new RegisterDto
{
UserName = $"ls_{userName}",
Password = GenerateRandomString(20),

View File

@@ -36,6 +36,11 @@
/// 昵称
/// </summary>
public string? Nick{ get; set; }
/// <summary>
/// 头像
/// </summary>
public string? Icon { get; set; }
}
}

View File

@@ -24,8 +24,9 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
/// </summary>
/// <param name="userName"></param>
/// <param name="phone"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<UserRoleMenuDto?> GetAsync(string? userName, long? phone);
Task<UserRoleMenuDto?> GetAsync(string? userName, long? phone,Guid? userId = null);
/// <summary>
/// 校验电话验证码,需要与电话号码绑定
@@ -38,6 +39,6 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
/// </summary>
/// <param name="input"></param>
Task PostTempRegisterAsync(RegisterDto input);
Task<Guid> PostSystemRegisterAsync(RegisterDto input);
}
}

View File

@@ -246,20 +246,21 @@ namespace Yi.Framework.Rbac.Application.Services
//注册之后免再次登录直接给前端token
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
input.Nick);
input.Nick, null);
return await this.PostLoginAsync(userId);
}
/// <summary>
/// 临时注册
/// 系统直接注册用户
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
/// </summary>
/// <param name="input"></param>
[RemoteService(isEnabled: false)]
public async Task PostTempRegisterAsync(RegisterDto input)
public async Task<Guid> PostSystemRegisterAsync(RegisterDto input)
{
//注册领域逻辑
await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email, input.Nick);
return await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
input.Nick, input.Icon);
}
/// <summary>
@@ -283,11 +284,12 @@ namespace Yi.Framework.Rbac.Application.Services
}
[RemoteService(isEnabled: false)]
public async Task<UserRoleMenuDto?> GetAsync(string? userName, long? phone)
public async Task<UserRoleMenuDto?> GetAsync(string? userName, long? phone = null, Guid? userId = null)
{
var user = await _userRepository._DbQueryable
.WhereIF(userName is not null, x => x.UserName == userName)
.WhereIF(phone is not null, x => x.Phone == phone)
.WhereIF(userId is not null, x => x.Id == userId)
.Where(x => x.State == true)
.FirstAsync();

View File

@@ -19,12 +19,14 @@ namespace Yi.Framework.Rbac.Domain.Entities
{
}
public UserAggregateRoot(string userName, string password, long? phone, string? email, string? nick = null)
public UserAggregateRoot(string userName, string password, long? phone, string? email, string? nick = null,
string? icon = null)
{
UserName = userName;
EncryPassword.Password = password;
Phone = phone;
Email = email;
Icon = icon;
Nick = string.IsNullOrWhiteSpace(nick) ? "萌新-" + userName : nick.Trim();
BuildPassword();
}

View File

@@ -2,6 +2,7 @@
using System.Security.Claims;
using System.Text;
using Mapster;
using Medallion.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -37,6 +38,16 @@ namespace Yi.Framework.Rbac.Domain.Managers
private ISqlSugarRepository<RoleAggregateRoot> _roleRepository;
private RefreshJwtOptions _refreshJwtOptions;
/// <summary>
/// 缓存前缀
/// </summary>
private string CacheKeyPrefix => LazyServiceProvider
.LazyGetRequiredService<IOptions<AbpDistributedCacheOptions>>()
.Value.KeyPrefix;
public IDistributedLockProvider DistributedLock =>
LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
public AccountManager(IUserRepository repository
, IOptions<JwtOptions> jwtOptions
, ILocalEventBus localEventBus
@@ -285,19 +296,37 @@ namespace Yi.Framework.Rbac.Domain.Managers
/// <param name="phone"></param>
/// <param name="email"></param>
/// <param name="nick"></param>
/// <param name="icon"></param>
/// <returns></returns>
public async Task<Guid> RegisterAsync(string userName, string password, long? phone, string? email,
string? nick)
string? nick, string? icon)
{
if (phone is null && string.IsNullOrWhiteSpace(email))
if (userName is null)
{
throw new UserFriendlyException("注册时,电话与邮箱不能同时为空");
throw new UserFriendlyException("注册时,用户名不能为空");
}
var user = new UserAggregateRoot(userName, password, phone, email, nick);
var userId = await _userManager.CreateAsync(user);
await _userManager.SetDefautRoleAsync(user.Id);
return userId;
//制作幂等
await using (var handle =
await DistributedLock.TryAcquireLockAsync($"{CacheKeyPrefix}Register:Lock:{userName}"))
{
if (handle is null)
{
throw new UserFriendlyException($"{userName}用户正在注册中,请稍等。。。");
}
var userUpName = userName.ToUpper();
if (await _userManager._repository._DbQueryable.Where(x => x.UserName.ToUpper() == userUpName)
.AnyAsync())
{
throw new UserFriendlyException($"{userName}用户已注册");
}
var user = new UserAggregateRoot(userName, password, phone, email, nick, icon);
var userId = await _userManager.CreateAsync(user);
await _userManager.SetDefautRoleAsync(user.Id);
return userId;
}
}
}
}

View File

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

View File

@@ -9,11 +9,11 @@ namespace Yi.Abp.Web.Jobs.ai_stock
public class GenerateNewsJob : HangfireBackgroundWorkerBase
{
private NewsManager _newsManager;
public GenerateNewsJob(NewsManager newsManager)
{
_newsManager = newsManager;
RecurringJobId = "AI股票新闻生成";
//每个小时整点执行一次
CronExpression = "0 0 * * * ?";
@@ -24,10 +24,10 @@ namespace Yi.Abp.Web.Jobs.ai_stock
// 每次触发只有2/24的概率执行生成新闻
var random = new Random();
var probability = random.Next(0, 24);
if (probability < 2)
{
// await _newsManager.GenerateNewsAsync();
await _newsManager.GenerateNewsAsync();
}
}
}

View File

@@ -8,11 +8,11 @@ namespace Yi.Abp.Web.Jobs.ai_stock
public class GenerateStockPricesJob : HangfireBackgroundWorkerBase
{
private readonly StockMarketManager _stockMarketManager;
public GenerateStockPricesJob(StockMarketManager stockMarketManager)
{
_stockMarketManager = stockMarketManager;
RecurringJobId = "AI股票价格生成";
//每天凌晨1点执行一次
CronExpression = "0 0 1 * * ?";
@@ -20,7 +20,7 @@ namespace Yi.Abp.Web.Jobs.ai_stock
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
{
// await _stockMarketManager.GenerateStocksAsync();
await _stockMarketManager.GenerateStocksAsync();
}
}
}
}

View File

@@ -57,6 +57,9 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Remove="logs\**" />
<Content Update="wwwroot\aihub\auth.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View File

@@ -56,14 +56,12 @@ namespace Yi.Abp.Web
typeof(YiAbpApplicationModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpSwashbuckleModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpAuditingModule),
typeof(AbpAspNetCoreAuthenticationJwtBearerModule),
typeof(YiFrameworkAspNetCoreModule),
typeof(YiFrameworkAspNetCoreAuthenticationOAuthModule),
typeof(YiFrameworkBackgroundWorkersHangfireModule),
typeof(AbpAutofacModule)
)]
@@ -108,12 +106,9 @@ namespace Yi.Abp.Web
//本地开发环境,可以禁用作业执行
if (host.IsDevelopment())
{
Configure<AbpBackgroundWorkerOptions> (options =>
{
options.IsEnabled = false;
});
Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
}
//请求日志
Configure<AbpAuditingOptions>(options =>
{
@@ -172,18 +167,27 @@ namespace Yi.Abp.Web
{
options.AddPolicy(DefaultCorsPolicyName, builder =>
{
var corsOrigins = configuration["App:CorsOrigins"]!;
builder
.WithOrigins(
configuration["App:CorsOrigins"]!
.WithAbpExposedHeaders()
.AllowAnyHeader()
.AllowAnyMethod();
if (corsOrigins == "*")
{
builder.AllowAnyOrigin();
}
else
{
builder
.WithOrigins(corsOrigins
.Split(";", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
.ToArray())
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowCredentials();
}
});
});
@@ -200,17 +204,17 @@ namespace Yi.Abp.Web
//配置Hangfire定时任务存储开启redis后优先使用redis
var redisConfiguration = configuration["Redis:Configuration"];
context.Services.AddHangfire(config=>
context.Services.AddHangfire(config =>
{
var redisEnabled=configuration.GetSection("Redis").GetValue<bool>("IsEnabled");
var redisEnabled = configuration.GetSection("Redis").GetValue<bool>("IsEnabled");
if (redisEnabled)
{
var jobDb=configuration.GetSection("Redis").GetValue<int>("JobDb");
var jobDb = configuration.GetSection("Redis").GetValue<int>("JobDb");
config.UseRedisStorage(
ConnectionMultiplexer.Connect(redisConfiguration),
new RedisStorageOptions()
{
Db =jobDb,
Db = jobDb,
InvisibilityTimeout = TimeSpan.FromHours(1), //JOB允许执行1小时
Prefix = "Yi:HangfireJob:"
}).WithJobExpirationTimeout(TimeSpan.FromHours(1));

View File

@@ -21,7 +21,7 @@
//应用启动
"App": {
"SelfUrl": "http://*:19001",
"CorsOrigins": "http://localhost:19001;http://localhost:18000;vscode-file://vscode-app;https://web.chatboxai.app;capacitor://localhost"
"CorsOrigins": "*"
},
//配置
"Settings": {

View File

@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>意社区授权页</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, rgb(10, 10, 10) 0%, rgb(13, 21, 32) 30%, rgb(10, 10, 10) 70%, rgb(15, 21, 32) 100%),
linear-gradient(135deg, rgba(0, 255, 136, 0.03) 0%, rgba(0, 0, 0, 0.8) 50%, rgba(0, 255, 136, 0.02) 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 页头 */
.header {
background: rgba(10, 10, 10, 0.9);
padding: 20px;
text-align: center;
border-bottom: 1px solid rgba(0, 255, 136, 0.2);
}
.header h1 {
color: #ffffff;
font-size: 28px;
line-height: 30px;
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.auth-container {
max-width: 600px;
width: 100%;
text-align: center;
}
/* 大标题 */
.main-title {
color: #ffffff;
font-size: 68px;
line-height: 74px;
margin-bottom: 30px;
font-weight: bold;
}
/* 授权结果卡片 */
.auth-result-card {
padding: 20px;
background-color: #04080B;
border-radius: 0px;
box-shadow: none;
display: flex;
flex-direction: column;
align-self: stretch;
border: 1px solid rgba(0, 255, 136, 0.2);
margin-bottom: 40px;
transition: border-color 0.3s ease;
}
.auth-result-card:hover {
border-color: rgba(0, 255, 136, 0.4);
}
.success-icon {
font-size: 60px;
color: #00ff88;
margin-bottom: 20px;
}
.auth-message {
color: #ffffff;
font-size: 28px;
line-height: 30px;
margin-bottom: 15px;
}
.auth-description {
color: #a0a0a0;
line-height: 22px;
font-size: 16px;
font-weight: 400;
margin-bottom: 30px;
}
.highlight-text {
color: #00ff88;
}
/* 按钮样式 */
.btn {
font-weight: 600;
border: none;
padding: 14px 28px;
font-size: 1rem;
line-height: 1.5rem;
border-radius: 0px;
cursor: pointer;
text-decoration: none;
display: inline-block;
margin: 10px;
transition: all 0.3s ease;
}
.btn-primary {
color: #000;
background: #00D36E;
animation: pulse 2s infinite;
}
.btn-primary:hover {
box-shadow: 0 10px 25px rgba(0, 255, 136, 0.3);
}
.btn-secondary {
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
background: transparent;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
/* 脉冲动画 */
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* 链接样式 */
.link {
color: #00ff88;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: all 0.3s ease;
}
.link:hover {
color: #ffffff;
border-bottom-color: #00ff88;
}
/* 备注内容 */
.note {
color: #666;
font-size: 12px;
line-height: 18px;
margin-top: 20px;
}
/* 页脚 */
.footer {
background: rgba(10, 10, 10, 0.9);
padding: 20px;
text-align: center;
border-top: 1px solid rgba(0, 255, 136, 0.2);
}
.footer p {
color: #a0a0a0;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-title {
font-size: 48px;
line-height: 54px;
}
.auth-message {
font-size: 24px;
line-height: 28px;
}
.main-content {
padding: 20px 15px;
}
.auth-result-card {
padding: 15px;
}
.btn {
padding: 12px 24px;
font-size: 0.9rem;
margin: 8px;
}
}
@media (max-width: 480px) {
.main-title {
font-size: 36px;
line-height: 42px;
}
.auth-message {
font-size: 20px;
line-height: 24px;
}
.success-icon {
font-size: 40px;
}
.btn {
width: 100%;
margin: 5px 0;
}
}
</style>
</head>
<body>
<!-- 页头 -->
<header class="header">
<h1>SharpDance-意社区</h1>
</header>
<!-- 主要内容 -->
<main class="main-content">
<div class="auth-container">
<h1 class="main-title">授权页</h1>
<div class="auth-result-card">
<div class="success-icon"></div>
<h2 class="auth-message"><span class="highlight-text">{{message}}</span></h2>
<p class="auth-description">
您已经完成我们的授权全部流程<br/>现在可返回扫码页面继续操作
</p>
<div class="button-group">
<a href="https://ccnetcore.com" class="btn btn-primary">进入意社区</a>
<a href="https://sharpdance.cn" class="btn btn-secondary">了解SharpDance</a>
</div>
<div class="note">
<p>如有任何问题,请联系 <a href="#" class="link">wx:chengzilaoge520</a></p>
</div>
</div>
<div class="additional-info">
<p class="auth-description">
您的账号已受意社区账号体系全面保护。现在您可以享受SharpDance产品矩阵提供的所有功能和服务。
</p>
<!-- <p class="auth-description">
感谢您加入意社区!构建智能未来的开发者社区。
</p> -->
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<p>&copy; 2025 意社区. 保留所有权利.</p>
<p><a href="#" class="link">隐私政策</a> | <a href="#" class="link">服务条款</a> | <a href="#" class="link">帮助中心</a></p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,7 @@
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-lVcGFMOQfXYtZWp1rD4SaBNDNpM270UX2wDqWh",
"ANTHROPIC_BASE_URL": "https://api.token-ai.cn",
"ANTHROPIC_MODEL": "gpt-4o-mini"
}
}

View File

@@ -37,6 +37,7 @@
"@jsonlee_12138/enum": "^1.0.4",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"date-fns": "^2.30.0",
"driver.js": "^1.3.6",
"echarts": "^6.0.0",
"element-plus": "^2.10.4",
@@ -50,7 +51,7 @@
"radash": "^12.1.1",
"reset-css": "^5.0.2",
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.0",
"vue-element-plus-x": "1.3.7",
"vue-router": "4"
},
"devDependencies": {

View File

@@ -32,6 +32,9 @@ importers:
'@vueuse/integrations':
specifier: ^13.5.0
version: 13.5.0(async-validator@4.2.5)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.17(typescript@5.8.3))
date-fns:
specifier: ^2.30.0
version: 2.30.0
driver.js:
specifier: ^1.3.6
version: 1.3.6
@@ -72,8 +75,8 @@ importers:
specifier: ^3.5.17
version: 3.5.17(typescript@5.8.3)
vue-element-plus-x:
specifier: 1.3.0
version: 1.3.0(@element-plus/icons-vue@2.3.1(vue@3.5.17(typescript@5.8.3)))(element-plus@2.10.4(vue@3.5.17(typescript@5.8.3)))(typescript-api-pro@0.0.7)(typescript@5.8.3)
specifier: 1.3.7
version: 1.3.7(rollup@4.41.1)(vue@3.5.17(typescript@5.8.3))
vue-router:
specifier: '4'
version: 4.5.1(vue@3.5.17(typescript@5.8.3))
@@ -863,12 +866,30 @@ packages:
cpu: [x64]
os: [win32]
'@shikijs/core@3.12.0':
resolution: {integrity: sha512-rPfCBd6gHIKBPpf2hKKWn2ISPSrmRKAFi+bYDjvZHpzs3zlksWvEwaF3Z4jnvW+xHxSRef7qDooIJkY0RpA9EA==}
'@shikijs/core@3.7.0':
resolution: {integrity: sha512-yilc0S9HvTPyahHpcum8eonYrQtmGTU0lbtwxhA6jHv4Bm1cAdlPFRCJX4AHebkCm75aKTjjRAW+DezqD1b/cg==}
'@shikijs/engine-javascript@3.12.0':
resolution: {integrity: sha512-Ni3nm4lnKxyKaDoXQQJYEayX052BL7D0ikU5laHp+ynxPpIF1WIwyhzrMU6WDN7AoAfggVR4Xqx3WN+JTS+BvA==}
'@shikijs/engine-oniguruma@3.12.0':
resolution: {integrity: sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg==}
'@shikijs/langs@3.12.0':
resolution: {integrity: sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ==}
'@shikijs/themes@3.12.0':
resolution: {integrity: sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==}
'@shikijs/transformers@3.7.0':
resolution: {integrity: sha512-VplaqIMRNsNOorCXJHkbF5S0pT6xm8Z/s7w7OPZLohf8tR93XH0krvUafpNy/ozEylrWuShJF0+ftEB+wFRwGA==}
'@shikijs/types@3.12.0':
resolution: {integrity: sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==}
'@shikijs/types@3.7.0':
resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==}
@@ -913,24 +934,15 @@ packages:
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.17':
resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -1562,8 +1574,8 @@ packages:
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
chatarea@5.5.3:
resolution: {integrity: sha512-uJqlS5ecwITPOGBKetovgOyUufumJP/JweVkgp/tEtBM9O6+WMVRGYYm5I8I1eyhkJb70HU8S+BH9esCtyk5MA==}
chatarea@5.9.3:
resolution: {integrity: sha512-rBOASntx4o5tzvylbgu1TAipA2U0G/OixxQpfYdQGsywpSYufExpzHPrq3+k16fEG9pGdm3AoBMmXU1zzs9Yng==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
@@ -1774,6 +1786,10 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -3025,9 +3041,6 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lint-staged@16.1.2:
resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==}
engines: {node: '>=20.17'}
@@ -3118,13 +3131,6 @@ packages:
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
engines: {node: '>=0.10.0'}
markdown-it-async@2.2.0:
resolution: {integrity: sha512-sITME+kf799vMeO/ww/CjH6q+c05f6TLpn6VOmmWCGNqPJzSh+uFgZoMB9s0plNtW6afy63qglNAC3MhrhP/gg==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@@ -3189,9 +3195,6 @@ packages:
mdn-data@2.21.0:
resolution: {integrity: sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@@ -3439,6 +3442,12 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
oniguruma-parser@0.12.1:
resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
@@ -3727,10 +3736,6 @@ packages:
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -3793,6 +3798,15 @@ packages:
resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
engines: {node: '>=0.10.0'}
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
regex-utilities@2.3.0:
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
regex@6.0.1:
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
regexp-ast-analysis@0.7.1:
resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -4073,6 +4087,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shiki@3.12.0:
resolution: {integrity: sha512-E+ke51tciraTHpaXYXfqnPZFSViKHhSQ3fiugThlfs/om/EonlQ0hSldcqgzOWWqX6PcjkKKzFgrjIaiPAXoaA==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -4456,6 +4473,10 @@ packages:
peerDependencies:
typescript: '>=4.0.0'
ts-md5@2.0.1:
resolution: {integrity: sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==}
engines: {node: '>=18'}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
@@ -4486,6 +4507,9 @@ packages:
resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==}
engines: {node: '>= 0.4'}
typescript-api-pro@0.0.6:
resolution: {integrity: sha512-wRA64AFESZkwyqukGgQJAbzF1E77CCYbydy74TwtQ5QopcXql6DsY00E1aCsCSkdH+NyEwOJsb8L5UoaRvndgg==}
typescript-api-pro@0.0.7:
resolution: {integrity: sha512-lCdArKa/rbJptU+ea+Ry+oLz+JgQucYAefO3GXNQuZPIUsW9iAC6OpC3bfQ/8bEmwO2HK6AWj98LoiDMtd6AoA==}
@@ -4494,9 +4518,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@@ -4724,12 +4745,10 @@ packages:
'@vue/composition-api':
optional: true
vue-element-plus-x@1.3.0:
resolution: {integrity: sha512-LTR1QoPXBOcF1iEqMjXt//fA88RZkaQT/pr04NvfAbwRsNP+sb/I9dBN7bTtBC7v0O7NLzqguS1gxeBwyPYC9w==}
vue-element-plus-x@1.3.7:
resolution: {integrity: sha512-Di3i1thbtn4YAg2nZCIJwaDHAq8VQA/TspXCJ8GPzBYdD9lioaYp/iy8sgEjLzKm4hViOmao/QS0WWH4snXaog==}
peerDependencies:
'@element-plus/icons-vue': ^2.3.1
element-plus: ^2.9.7
typescript-api-pro: ^0.0.6
vue: ^3.5.17
vue-eslint-parser@10.2.0:
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
@@ -5613,6 +5632,13 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.41.1':
optional: true
'@shikijs/core@3.12.0':
dependencies:
'@shikijs/types': 3.12.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/core@3.7.0':
dependencies:
'@shikijs/types': 3.7.0
@@ -5620,11 +5646,35 @@ snapshots:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@3.12.0':
dependencies:
'@shikijs/types': 3.12.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.3
'@shikijs/engine-oniguruma@3.12.0':
dependencies:
'@shikijs/types': 3.12.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@3.12.0':
dependencies:
'@shikijs/types': 3.12.0
'@shikijs/themes@3.12.0':
dependencies:
'@shikijs/types': 3.12.0
'@shikijs/transformers@3.7.0':
dependencies:
'@shikijs/core': 3.7.0
'@shikijs/types': 3.7.0
'@shikijs/types@3.12.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/types@3.7.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
@@ -5670,25 +5720,16 @@ snapshots:
'@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.17
'@types/lodash@4.17.17': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/mdurl@2.0.0': {}
'@types/ms@2.1.0': {}
'@types/node@12.20.55': {}
@@ -6443,7 +6484,7 @@ snapshots:
chardet@0.7.0: {}
chatarea@5.5.3: {}
chatarea@5.9.3: {}
chokidar@3.6.0:
dependencies:
@@ -6659,6 +6700,10 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.27.6
dayjs@1.11.13: {}
de-indent@1.0.2: {}
@@ -8094,10 +8139,6 @@ snapshots:
lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
lint-staged@16.1.2:
dependencies:
chalk: 5.4.1
@@ -8196,20 +8237,6 @@ snapshots:
dependencies:
object-visit: 1.0.1
markdown-it-async@2.2.0:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-table@3.0.4: {}
math-intrinsics@1.1.0: {}
@@ -8364,8 +8391,6 @@ snapshots:
mdn-data@2.21.0: {}
mdurl@2.0.0: {}
memoize-one@6.0.0: {}
meow@12.1.1: {}
@@ -8738,6 +8763,14 @@ snapshots:
dependencies:
mimic-function: 5.0.1
oniguruma-parser@0.12.1: {}
oniguruma-to-es@4.3.3:
dependencies:
oniguruma-parser: 0.12.1
regex: 6.0.1
regex-recursion: 6.0.2
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
@@ -8991,8 +9024,6 @@ snapshots:
property-information@7.1.0: {}
punycode.js@2.3.1: {}
punycode@2.3.1: {}
qrcode@1.5.4:
@@ -9064,6 +9095,16 @@ snapshots:
extend-shallow: 3.0.2
safe-regex: 1.1.0
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
regex-utilities@2.3.0: {}
regex@6.0.1:
dependencies:
regex-utilities: 2.3.0
regexp-ast-analysis@0.7.1:
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -9378,6 +9419,17 @@ snapshots:
shebang-regex@3.0.0: {}
shiki@3.12.0:
dependencies:
'@shikijs/core': 3.12.0
'@shikijs/engine-javascript': 3.12.0
'@shikijs/engine-oniguruma': 3.12.0
'@shikijs/langs': 3.12.0
'@shikijs/themes': 3.12.0
'@shikijs/types': 3.12.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -9836,6 +9888,8 @@ snapshots:
picomatch: 4.0.2
typescript: 5.8.3
ts-md5@2.0.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
@@ -9888,12 +9942,12 @@ snapshots:
typed-array-buffer: 1.0.3
typed-array-byte-offset: 1.0.4
typescript-api-pro@0.0.6: {}
typescript-api-pro@0.0.7: {}
typescript@5.8.3: {}
uc.micro@2.1.0: {}
ufo@1.6.1: {}
uglify-js@3.19.3:
@@ -10180,18 +10234,16 @@ snapshots:
dependencies:
vue: 3.5.17(typescript@5.8.3)
vue-element-plus-x@1.3.0(@element-plus/icons-vue@2.3.1(vue@3.5.17(typescript@5.8.3)))(element-plus@2.10.4(vue@3.5.17(typescript@5.8.3)))(typescript-api-pro@0.0.7)(typescript@5.8.3):
vue-element-plus-x@1.3.7(rollup@4.41.1)(vue@3.5.17(typescript@5.8.3)):
dependencies:
'@element-plus/icons-vue': 2.3.1(vue@3.5.17(typescript@5.8.3))
'@shikijs/transformers': 3.7.0
'@vueuse/core': 13.5.0(vue@3.5.17(typescript@5.8.3))
chatarea: 5.5.3
chatarea: 5.9.3
deepmerge: 4.3.1
dompurify: 3.2.6
element-plus: 2.10.4(vue@3.5.17(typescript@5.8.3))
github-markdown-css: 5.8.1
highlight.js: 11.11.1
markdown-it-async: 2.2.0
prismjs: 1.30.0
property-information: 7.1.0
radash: 12.1.1
@@ -10203,13 +10255,17 @@ snapshots:
remark-math: 6.0.0
remark-parse: 11.0.0
remark-rehype: 11.1.2
rollup-plugin-visualizer: 6.0.3(rollup@4.41.1)
shiki: 3.12.0
swrv: 1.1.0(vue@3.5.17(typescript@5.8.3))
typescript-api-pro: 0.0.7
ts-md5: 2.0.1
typescript-api-pro: 0.0.6
unified: 11.0.5
vue: 3.5.17(typescript@5.8.3)
transitivePeerDependencies:
- rolldown
- rollup
- supports-color
- typescript
vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2)):
dependencies:

View File

@@ -1,5 +1,6 @@
export * from './auth';
export * from './chat';
export * from './model';
export * from './pay';
export * from './session';
export * from './user';

View File

@@ -0,0 +1,11 @@
import { get, post } from '@/utils/request.ts';
// 创建订单并发起支付
export function createOrder(params: any) {
return post<any>(`/pay/Order`, params).json();
}
// 查询订单状态
export function getOrderStatus(OutTradeNo: any) {
return get<any>(`/pay/OrderStatus?OutTradeNo=${OutTradeNo}`).json();
}

View File

@@ -1,6 +1,21 @@
import { get } from '@/utils/request';
import { get, post } from '@/utils/request';
// 获取用户信息
export function getUserInfo() {
return get<any>('/ai-chat/account').json();
return get<any>('/account/ai').json();
}
// 获取二维码 LoginOrRegister 登录注册, Bind 绑定
export function getQrCode(data: any) {
return post<any>(`/fuwuhao/qrcode?sceneType=${data.sceneType}`, data).json();
}
// 扫码轮询
// 0=Wait, 1=Login, 2=Register, 3=Bind, 10=Expired
export function getQrCodeResult(data: any) {
return get<any>('/fuwuhao/qrcode/result', data).json();
}
// 注册微信授权
export function getWechatAuth(data: any) {
return post<any>('/fuwuhao/register', data).json();
}

View File

@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="837px" height="275px" viewBox="0 0 837 275" enable-background="new 0 0 837 275" xml:space="preserve"> <image id="image0" width="837" height="275" x="0" y="0"
xlink:href="data:image/png;base64,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" />
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,146 +1,305 @@
<script lang="ts" setup>
import { Check, Picture as IconPicture, Refresh } from '@element-plus/icons-vue';
import { useCountdown } from '@vueuse/core';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { useRouter } from 'vue-router';
import { getQrCode, getQrCodeResult, getUserInfo } from '@/api';
import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session.ts';
import { WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
const props = defineProps({
type: {
type: String,
default: WECHAT_QRCODE_TYPE.LoginOrRegister,
},
});
const emit = defineEmits(['bind-wechat']);
const QrCodeType = props.type || WECHAT_QRCODE_TYPE.LoginOrRegister;
// 响应式状态
const urlText = shallowRef('');
const qrCodeUrl = useQRCode(urlText);
// const urlText = shallowRef('');
const qrCodeUrl = ref('');
const isExpired = ref(false);
const isScanned = ref(false); // 新增:是否已扫码
const isConfirming = ref(false); // 新增:是否进入确认登录阶段
const confirmCountdownSeconds = shallowRef(180); // 确认登录倒计时3分钟
const isScanned = ref(false);
const isConfirming = ref(false);
const isAuthorization = ref(false);
const confirmCountdownSeconds = shallowRef(180);
const sceneStr = ref(''); // 场景值,用于标识二维码
const userStore = useUserStore();
const router = useRouter();
const sessionStore = useSessionStore();
const isQrCodeError = ref(false);
// 二维码倒计时实例
const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(60), {
const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), {
interval: 1000,
onComplete: () => {
isExpired.value = true;
stopPolling(); // 二维码过期时停止轮询
stopPolling();
},
});
// 计算
const isLoading = computed(() => {
return qrCodeUrl.value ? false : !isQrCodeError.value;
});
// 确认登录倒计时实例
const { start: confirmStart, stop: confirmStop } = useCountdown(confirmCountdownSeconds, {
interval: 1000,
onComplete: () => {
isExpired.value = true;
isConfirming.value = false;
stopPolling(); // 确认倒计时结束时停止轮询
stopPolling();
},
});
// 轮询相关
let scanPolling: number | null = null;
let confirmPolling: number | null = null;
let statusPolling: number | null = null;
// 模拟后端接口 这里返回新的二维码地址
async function fetchNewQRCode() {
await new Promise(resolve => setTimeout(resolve, 500));
return `https://login-api.com/qr/${Date.now()}`;
}
// 模拟后端接口 这里返回是否已扫码
async function checkScanStatus() {
// 模拟扫码状态接口(实际应调用后端接口)
await new Promise(resolve => setTimeout(resolve, 300));
return Math.random() > 0.3; // 30%概率未扫码70%概率已扫码
// 获取登录二维码图片和二维码标识
async function fetchQRCodeInfo() {
try {
isQrCodeError.value = false;
const param = {
sceneType: QrCodeType,
};
const response = await getQrCode(param);
const urlObj = new URL(response.data.qrCodeUrl);
const params = new URLSearchParams(urlObj.search);
const ticketValue = params.get('ticket');
if (response && response.data.qrCodeUrl && response.data.scene && ticketValue) {
qrCodeUrl.value = response.data.qrCodeUrl;
sceneStr.value = response.data.scene;
return true;
}
return false;
}
catch (err: any) {
return false;
}
}
// 模拟后端接口 这里返回扫码后是否已确认
async function checkConfirmStatus() {
// 模拟确认登录接口(实际应调用后端接口)
await new Promise(resolve => setTimeout(resolve, 200));
return Math.random() > 0.5; // 50%概率已确认
// 轮询二维码状态
async function checkQRCodeStatus() {
if (!sceneStr.value)
return;
try {
const param = {
scene: sceneStr.value,
};
const response = await getQrCodeResult(param);
switch (response.data.sceneResult) {
case 'Wait': // Wait
// 继续等待
break;
case 'Login': // Login
// 登录成功
await handleLoginSuccess(response.data.token, response.data.refreshToken);
break;
case 'Register': // Register
// 需要注册
handleRegister();
break;
case 'Bind': // Bind
// 绑定成功
handleBind();
break;
case 'Expired': // Expired
// 二维码过期
isExpired.value = true;
stopPolling();
break;
default:
console.warn('未知状态:', response.data.sceneResult);
}
// 更新UI状态
updateUIStatus(response.data.sceneResult);
}
catch (error) {
console.error('检查二维码状态失败:', error);
}
}
// 模拟登录逻辑 如果在客户端已确认,则会调用这个方法进行登录
async function mockLogin() {
// 模拟登录成功逻辑
await new Promise(resolve => setTimeout(resolve, 500));
// 处理登录成功
async function handleLoginSuccess(token: string, refreshToken: string) {
// 停止轮询
stopPolling();
userStore.setToken(token, refreshToken);
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
// 提示用户
ElMessage.success('登录成功');
await router.replace('/');
await sessionStore.requestSessionList(1, true);
userStore.closeLoginDialog();
}
// 处理注册授权
function handleRegister() {
ElMessage.info('请在微信授权');
}
// 处理绑定
async function handleBind() {
// 停止轮询
stopPolling();
// 处理账号绑定逻辑
ElMessage.success('微信绑定成功');
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
// 调用父组件方法
emit('bind-wechat');
}
// 更新UI状态
function updateUIStatus(status: string) {
switch (status) {
case 'Wait': // Wait
isScanned.value = false;
isConfirming.value = false;
break;
case 'Login': // Login - 已扫码并确认
case 'Bind': // Bind - 已扫码并确认
isScanned.value = true;
isConfirming.value = false;
break;
case 'Register': // Register - 已扫码并确认
isScanned.value = true;
isAuthorization.value = true;
break;
case 'Expired': // Expired
isExpired.value = true;
isScanned.value = false;
isConfirming.value = false;
break;
default:
// 其他状态认为是已扫码但未确认
isScanned.value = true;
isConfirming.value = true;
}
}
/** 停止所有轮询 */
function stopPolling() {
if (scanPolling)
clearInterval(scanPolling);
if (confirmPolling)
clearInterval(confirmPolling);
scanPolling = null;
confirmPolling = null;
if (statusPolling) {
clearInterval(statusPolling);
statusPolling = null;
}
}
/** 刷新二维码 */
// async function handleRefresh() {
// isExpired.value = false;
// isQrCodeError.value = false;
// isScanned.value = false;
// isConfirming.value = false;
// stopPolling();
//
// const success = await fetchQRCodeInfo();
// if (success) {
// qrStart(shallowRef(600));
// startStatusPolling();
// }
// else {
// isQrCodeError.value = true;
// stopPolling();
// ElMessage.error('刷新二维码失败');
// }
// }
/** 刷新二维码按钮 */
async function handleRefresh() {
isExpired.value = false;
isScanned.value = false;
isConfirming.value = false;
stopPolling();
qrStart(shallowRef(60));
const newUrl = await fetchNewQRCode();
urlText.value = newUrl;
await initQRCode();
}
/** 启动扫码状态轮询 */
function startScanPolling() {
scanPolling = setInterval(async () => {
if (!isExpired.value && !isScanned.value) {
const scanned = await checkScanStatus();
if (scanned) {
isScanned.value = true;
isConfirming.value = true;
confirmStart(confirmCountdownSeconds); // 启动确认倒计时
startConfirmPolling(); // 开始确认登录轮询
stopPolling(); // 停止扫码轮询
}
}
}, 2000); // 每2秒轮询一次
}
/** 启动状态轮询 */
function startStatusPolling() {
stopPolling(); // 先停止之前的轮询
/** 启动确认登录轮询 */
function startConfirmPolling() {
confirmPolling = setInterval(async () => {
if (isConfirming.value && !isExpired.value) {
const confirmed = await checkConfirmStatus();
if (confirmed) {
stopPolling();
confirmStop();
await mockLogin();
handleRefresh(); // 登录成功后刷新二维码
}
statusPolling = setInterval(async () => {
if (!isExpired.value) {
await checkQRCodeStatus();
}
}, 2000); // 每2秒轮询一次
}
/** 组件初始化 */
onMounted(async () => {
const initialUrl = await fetchNewQRCode();
urlText.value = initialUrl;
qrStart();
startScanPolling(); // 初始启动扫码轮询
await initQRCode();
// const success = await fetchQRCodeInfo();
// if (success) {
// qrStart();
// startStatusPolling();
// }
// else {
// isQrCodeError.value = true;
// stopPolling();
// ElMessage.error('初始化二维码失败');
// }
});
/** 初始化或刷新二维码 */
async function initQRCode() {
// 清理旧的定时器和轮询
qrStop();
confirmStop();
stopPolling();
const success = await fetchQRCodeInfo();
if (success) {
isExpired.value = false;
isQrCodeError.value = false;
isScanned.value = false;
isConfirming.value = false;
qrStart(); // 重新开始倒计时
startStatusPolling(); // 开始轮询
}
else {
isQrCodeError.value = true;
ElMessage.error('获取二维码失败');
}
}
/** 组件卸载清理 */
onBeforeUnmount(() => {
qrStop();
confirmStop();
stopPolling();
});
onMounted(() => {
});
</script>
<template>
<div class="qr-wrapper">
<div class="tip">
请使用手机扫码登录
{{ QrCodeType === WECHAT_QRCODE_TYPE.Bind ? '请使用手机微信扫码绑定' : '请使用手机微信扫码登录/注册' }}
</div>
<div class="qr-img-wrapper">
<el-image v-loading="!qrCodeUrl" :src="qrCodeUrl" alt="登录二维码" class="qr-img">
<el-image v-loading="isLoading" :src="qrCodeUrl" alt="登录二维码" class="qr-img">
<template #error>
<el-icon><IconPicture /></el-icon>
</template>
</el-image>
<!-- 失败覆盖层 -->
<div v-if="isQrCodeError" class="expired-overlay" @click.stop="handleRefresh">
<div class="expired-content">
<p class="expired-text">
二维码获取失败
</p>
<el-button class="refresh-btn" link>
<el-icon><Refresh /></el-icon>
点击刷新
</el-button>
</div>
</div>
<!-- 过期覆盖层 -->
<div v-if="isExpired" class="expired-overlay" @click.stop="handleRefresh">
<div class="expired-content">
@@ -164,16 +323,54 @@ onBeforeUnmount(() => {
已扫码
</p>
<p class="scanned-text">
<p v-if="isConfirming" class="scanned-text">
请在手机端确认登录
</p>
<p v-if="isAuthorization" class="scanned-text">
请在手机端微信继续操作<br>
请关注微信服务号并授权
</p>
<p v-else class="scanned-text">
处理中...
</p>
</div>
</div>
</div>
<div class="help-text">
{{ QrCodeType === WECHAT_QRCODE_TYPE.Bind ? '扫码后请在微信服务号中授权' : '扫码后请在微信服务号中授权' }}
</div>
<div v-if="QrCodeType === WECHAT_QRCODE_TYPE.LoginOrRegister" class="tip-old-user">
<p>提示</p>
<p>意社区老用户可用原方式登录</p>
<p>登录后前往个人中心绑定微信</p>
</div>
<div v-if="QrCodeType === WECHAT_QRCODE_TYPE.Bind" class="tip-old-user-bind">
提示<br>
若扫码的微信已注册过意社区账号<br>
将直接解绑到当前登录账号
</div>
</div>
</template>
<style scoped lang="scss">
.tip-old-user{
line-height: 1.5;
margin: 5px 0;
color: #FF4D4F;
display: flex;
flex-direction: column;
text-align: center;
}
.tip-old-user-bind{
line-height: 1.5;
margin: 5px 0;
color: #FF4D4F;
display: flex;
align-items: center;
}
.qr-wrapper {
display: flex;
flex-direction: column;
@@ -183,6 +380,7 @@ onBeforeUnmount(() => {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 10px;
}
.qr-img-wrapper {
position: relative;
@@ -256,5 +454,9 @@ onBeforeUnmount(() => {
}
}
}
.help-text {
font-size: 12px;
color: #909399;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api';
@@ -34,7 +34,7 @@ watch(
(newVal) => {
if (newVal) {
// 恢复默认
isQrMode.value = false;
isQrMode.value = true;
// 显示时立即展示遮罩
showMask.value = true;
}
@@ -56,10 +56,25 @@ function handleMaskClick() {
// 过渡动画结束回调
function onAfterLeave() {
if (!visible.value) {
isQrMode.value = false;
showMask.value = false; // 动画结束后隐藏遮罩
}
}
// 重新登录
async function onReLogin() {
// 在这里执行退出方法
await userStore.logout();
// 清空回话列表并回到默认页
await sessionStore.requestSessionList(1, true);
await sessionStore.createSessionBtn();
ElMessage({
type: 'success',
message: '退出成功',
});
await router.replace('/');
}
function handleThirdPartyLogin(type: any) {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
console.log('cccc', type);
@@ -142,11 +157,12 @@ function handleLoginAgainYi() {
userStore.setToken(token, refreshToken);
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
ElMessage.success('登录成功');
// 关闭弹窗
if (popup && !popup.closed)
popup.close();
// 后续逻辑
ElMessage.success('登录成功');
userStore.closeLoginDialog();
await sessionStore.requestSessionList(1, true);
await router.replace('/');
@@ -208,7 +224,7 @@ function openContact() {
</h4>
<img
src="${wxSrc.value}"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
class="w-50 h-50 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wechat-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
@@ -218,7 +234,7 @@ function openContact() {
</h4>
<img
src="${wxGroupQD}"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
class="w-50 h-50 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wx-group-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
@@ -283,13 +299,15 @@ function openContact() {
</div>
</div>
<div class="right-section">
<!-- 隐藏二维码登录 -->
<div v-if="false" class="mode-toggle" @click.stop="toggleLoginMode">
<div class="mode-toggle" @click.stop="toggleLoginMode">
<SvgIcon v-if="!isQrMode" name="erweimadenglu" />
<SvgIcon v-else name="zhanghaodenglu" />
</div>
<div class="content-wrapper">
<div v-if="!isQrMode" class="form-box">
<div v-if="isQrMode" class="qr-container">
<QrCodeLogin />
</div>
<div v-else class="form-box">
<!-- 表单容器父组件可以自定定义表单插槽 -->
<slot name="form">
<!-- 父组件不用插槽则显示默认表单 默认使用 AccountPassword 组件 -->
@@ -318,16 +336,16 @@ function openContact() {
</div>
<el-divider content-position="center">
<p class="w-max">
开通Vip后点击下方重新登录意社区
开通Vip后点击下方重新登录以生效
</p>
</el-divider>
<el-button
class="w-full"
type="primary"
size="large"
@click="handleThirdPartyLogin(SSO_CLIENT_LOGIN_AGAIN)"
@click="onReLogin()"
>
意社区重新登录
重新登录
</el-button>
<el-divider class="w-max">
<p class="w-max">
@@ -356,9 +374,6 @@ function openContact() {
</div>
</slot>
</div>
<div v-else class="qr-container">
<QrCodeLogin />
</div>
</div>
</div>
</div>
@@ -390,7 +405,7 @@ function openContact() {
position: fixed;
top: 0;
left: 0;
z-index: 2000;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -8,6 +8,7 @@ import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores';
import { useModelStore } from '@/stores/modules/model';
import { showProductPackage } from '@/utils/product-package.ts';
import { isUserVip } from '@/utils/user';
const router = useRouter();
@@ -56,6 +57,7 @@ async function showPopover() {
// 点击
// 处理模型点击
function handleModelClick(item: GetSessionListVO) {
console.log('modelStore.modelList', modelStore.modelList);
if (!isModelAvailable(item)) {
ElMessageBox.confirm(
`
@@ -71,13 +73,13 @@ function handleModelClick(item: GetSessionListVO) {
${
isUserVip()
? '<p class="text-sm text-gray-500">您可随时访问产品页面查看更多特权内容。</p>'
: '<p class="text-sm text-gray-500">点击下方按钮,立即升级为 VIP 会员</p>'
: '<p class="text-sm text-gray-500">点击右上角登录按钮,登录后进行购买</p>'
}
</div>
`,
isUserVip() ? '会员状态' : '会员尊享',
{
confirmButtonText: '前往产品页面',
confirmButtonText: '产品查看',
cancelButtonText: '关闭',
dangerouslyUseHTMLString: true,
type: 'info',
@@ -86,10 +88,12 @@ function handleModelClick(item: GetSessionListVO) {
},
)
.then(() => {
router.push({
name: 'products', // 使用命名路由
query: { from: isUserVip() ? 'vip' : 'user' }, // 可选:添加来源标识
});
showProductPackage();
// router.push({
// name: 'products', // 使用命名路由
// query: { from: isUserVip() ? 'vip' : 'user' }, // 可选:添加来源标识
// });
})
.catch(() => {
// 点击右上角关闭或“关闭”按钮,不执行任何操作

View File

@@ -0,0 +1,997 @@
<script setup lang="ts">
import { CircleCheck } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { createOrder, getOrderStatus } from '@/api';
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
import ProductPage from '@/pages/products/index.vue';
import { useUserStore } from '@/stores';
const emit = defineEmits(['close']);
const packagesData = {
member: [
{ id: 1, name: '8个月推荐', desc: '', price: 183.2, perMonth: 22.9, tag: '超高性价比', discount: '限时活动', key: 8 },
{ id: 2, name: '6个月', desc: '', price: 155.4, perMonth: 25.9, tag: '年度热销', key: 6 },
{ id: 3, name: '3个月', desc: '', price: 83.7, perMonth: 27.9, tag: '短期体验', discount: '', key: 3 },
{ id: 4, name: '1个月', desc: '', price: 29.9, originalPrice: 49.9, tag: '灵活选择', discount: '', key: 1 },
// { id: 5, name: '测试', desc: '', price: 0.01, originalPrice: 9.9, tag: '测试使用', discount: '', key: 0 },
],
token: [
{ id: 6, name: '10M 输入Token', desc: '', price: 49.9, tag: '轻量用户', discount: '' },
{ id: 7, name: '20M 输入Token', desc: '', price: 79.9, tag: '中等使用', discount: '' },
{ id: 8, name: '30M 输入Token', desc: '', price: 99.9, tag: '量大管饱', discount: '' },
{ id: 9, name: '联系站长', desc: '', price: 0, tag: '企业级需求', discount: '' },
],
};
const userStore = useUserStore();
const visible = ref(true);
const activeTab = ref('member');
const selectedId = ref<number | null>(packagesData.member[3].id);
const selectedPrice = ref(packagesData.member[3].price);
const selectPackageObject = ref<any>(packagesData.member[3]);
const showDetails = ref(false);
const isMobile = ref(false);
const isLoading = ref(false);
const paymentWindow = ref<Window | null>(null);
const pollInterval = ref<NodeJS.Timeout | null>(null);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', checkMobile);
cleanupPayment();
});
function cleanupPayment() {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
// if (paymentWindow.value && !paymentWindow.value.closed) {
// paymentWindow.value.close();
// }
// 清除轮询定时器
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
// 关闭支付窗口(如果还在)
// if (paymentWindow.value && !paymentWindow.value.closed) {
// paymentWindow.value.close();
// paymentWindow.value = null;
// }
retryCount = 0; // 重置重试计数器
}
const tabs = [
{ key: 'member', label: '会员套餐' },
// { key: 'token', label: 'Token 套餐' },
];
const benefitsData = {
member: [
{ name: '站点全模型解锁使用', value: '' },
{ name: '全站所有Ai日常“无限制”使用', value: '' },
{ name: 'Ai专线超级加速', value: '' },
{ name: '专属Api接口提供', value: '' },
{ name: '支持文件/图片/知识库功能', value: '' },
{ name: '支持各类第三方工具集成IDE/翻译/Utools等', value: '' },
{ name: '支持Mcp/FunctionCall开发', value: '' },
{ name: '支持安卓/ios/web/客户端使用', value: '' },
{ name: '支持售后群服务一起畅玩前沿Ai', value: '' },
],
token: [
{ name: 'Token 用途', value: '用于调用 API 或模型生成内容' },
{ name: '灵活计费', value: '按调用量扣费,更加自由' },
{ name: '支持多模型', value: '适配多种模型调用需求' },
],
};
const fullBenefitsData = {
member: [
{ category: 'YiXinAI AI Pro', free: '1项', vip: '5项', value: '价值68/月' },
{ category: '基础对话', free: '3条/天', vip: '不限条款', value: 'DeepSeek-R1 32b蒸馏版、AI-4o mini' },
{ category: '高级对话', free: '-', vip: '400条/月', value: 'DeepSeek-R1 671b满血版、4o、Cd3.5s、Code、文档对话' },
{ category: 'AI基础绘画', free: '-', vip: '500条/月', value: '' },
{ category: 'AI高级绘画', free: '-', vip: '50条/月', value: '' },
{ category: 'AI-4.0联网搜索', free: '-', vip: '支持', value: '' },
{ category: 'AI PPT', free: '1项', vip: '12项', value: '价值99/月' },
{ category: 'AI 思维导图', free: '1项', vip: '8项', value: '价值99/月' },
{ category: '文档对话', free: '0项', vip: '6项', value: '价值99/月' },
{ category: 'AI 绘图', free: '0项', vip: '5项', value: '价值99/月' },
{ category: 'AI 写作', free: '1项', vip: '6项', value: '价值99/月' },
],
};
const currentPackages = computed(() => packagesData[activeTab.value]);
const currentBenefits = computed(() => benefitsData[activeTab.value]);
function selectPackage(pkg: any) {
selectedId.value = pkg.id;
selectedPrice.value = pkg.price;
selectPackageObject.value = pkg;
}
function handleClickLogin() {
userStore.openLoginDialog();
}
async function pay() {
if (!selectedId.value) {
ElMessage.warning('请选择一个套餐');
return;
}
isLoading.value = true;
try {
// const returnUrl = `https://ai.ccnetcore.com/pay-result`;
const returnUrl = `${window.location.origin}/pay-result`;
const params = {
goodsType: selectPackageObject.value?.key,
ReturnUrl: returnUrl,
};
const response = await createOrder(params);
console.log('订单创建成功:', response);
if (response.data.paymentPageHtml) {
handlePaymentPage(response.data.paymentPageHtml, response.data.orderId, response.data.outTradeNo);
}
else {
throw new Error('未获取到支付页面');
}
}
catch (error: any) {
console.error('支付失败:', error);
ElMessage.error(`支付失败: ${error.message || '未知错误'}`);
}
finally {
isLoading.value = false;
}
}
function handlePaymentPage(html: string, orderId: string, outTradeNo: string) {
// 关闭当前弹窗(如果有)
close();
// 创建支付窗口
paymentWindow.value = window.open('', '_blank');
if (!paymentWindow.value) {
ElMessage.error('无法打开支付窗口,请检查浏览器弹窗设置或允许弹窗');
return;
}
// 写入支付页面HTML并自动提交
paymentWindow.value.document.open();
paymentWindow.value.document.write(html);
// paymentWindow.value.document.close();
// 3秒后开始轮询支付状态给支付页面加载时间
setTimeout(() => {
startPolling(outTradeNo);
}, 3000);
}
function startPolling(outTradeNo: string) {
// 先清理之前的轮询任务
cleanupPayment();
// 立即检查一次状态(避免等待第一个间隔)
checkPaymentStatus(outTradeNo);
// 设置轮询任务每3秒检查一次
pollInterval.value = setInterval(() => {
checkPaymentStatus(outTradeNo);
}, 3000);
}
let retryCount = 0; // 错误重试计数器
async function checkPaymentStatus(outTradeNo: string) {
try {
const result = await getOrderStatus(outTradeNo);
console.log('订单状态检查结果:', result);
if (result.data.tradeStatus === 'TRADE_SUCCESS') {
// 支付成功处理
cleanupPayment();
ElMessage.success('支付成功!');
close(); // 关闭弹窗
// 可以在这里添加跳转到成功页面的逻辑
// window.location.href = '/pay/success?order=' + outTradeNo;
}
else if (result.data.tradeStatus === 'TRADE_CLOSED'
|| result.data.tradeStatus === 'TRADE_FAILED') {
// 支付失败处理
cleanupPayment();
ElMessage.warning(`支付失败: ${result.data.tradeStatusDesc || '未知原因'}`);
}
// 其他状态(如待支付)不做处理,继续轮询
}
catch (error) {
console.error('检查订单状态失败:', error);
// 网络错误等情况,可以重试几次后停止
if (retryCount > 3) {
cleanupPayment();
ElMessage.error('检查支付状态失败,请手动刷新页面确认');
}
retryCount++;
}
}
const router = useRouter();
function toggleDetails() {
showDetails.value = !showDetails.value;
}
function close() {
visible.value = false;
emit('close');
}
function onClose() {
emit('close');
}
</script>
<template>
<el-dialog
v-model="visible" :width="isMobile ? '90%' : '980px'" :fullscreen="isMobile && showDetails"
:show-close="false" destroy-on-close class="product-package-dialog" @close="onClose"
>
<!-- 详情页 -->
<div v-if="showDetails" class="details-view">
<!-- 顶部标题和返回按钮 -->
<div class="flex items-center mb-6 sticky top-0 bg-white z-10 pt-2 pb-4">
<el-button text circle size="small" class="mr-2" @click="toggleDetails">
</el-button>
<div class="text-xl font-bold">
YiXinAI会员详细权益
</div>
</div>
<ProductPage />
<!-- 权益详情表格 -->
<div v-if="false" class="benefits-table">
<div class="table-header">
<div class="table-cell">
服务项
</div>
<div class="table-cell">
免费用户
</div>
<div class="table-cell">
AI大会员
</div>
</div>
<div v-for="(item, index) in fullBenefitsData.member" :key="index" class="table-row">
<div class="table-cell font-medium">
{{ item.category }}
</div>
<div class="table-cell">
{{ item.free }}
</div>
<div class="table-cell">
<div>{{ item.vip }}</div>
<div v-if="item.value" class="text-gray-500 text-xs">
{{ item.value }}
</div>
</div>
</div>
</div>
</div>
<!-- 主页面 -->
<div v-else>
<!-- 顶部标题和关闭按钮 -->
<div class="flex justify-between items-center mb-4">
<div class="text-xl font-bold">
购买套餐
</div>
<el-button circle size="small" @click="close">
</el-button>
</div>
<!-- Tab 切换 -->
<div class="flex border-b mb-6 overflow-x-auto">
<div
v-for="tab in tabs" :key="tab.key"
class="cursor-pointer px-5 py-2 -mb-px border-b-2 transition whitespace-nowrap" :class="activeTab === tab.key
? 'border-orange-500 text-orange-500 font-semibold'
: 'border-transparent text-gray-500 hover:text-orange-500'" @click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-layout">
<!-- 套餐卡片列表 -->
<div class="package-list">
<div
v-for="pkg in currentPackages" :key="pkg.id" class="package-card"
:class="{ selected: pkg.id === selectedId }" @click="selectPackage(pkg)"
>
<!-- 标签 -->
<div v-if="pkg.discount" class="discount-tag">
{{ pkg.discount }}
</div>
<div v-if="pkg.tag" class="tag">
{{ pkg.tag }}
</div>
<!-- 套餐信息 -->
<div class="package-info">
<div class="package-name">
{{ pkg.name }}
</div>
<div class="package-desc">
{{ pkg.desc }}
</div>
</div>
<!-- 价格 -->
<div class="package-price">
<span class="price">¥{{ pkg.price }}</span>
<span v-if="pkg.perMonth" class="per-month">
¥{{ pkg.perMonth }}/
</span>
<div v-if="pkg.originalPrice" class="original-price">
原价¥{{ pkg.originalPrice }}
</div>
</div>
</div>
</div>
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
<div>
<p>充值后加客服微信回复账号名可专享vip售后服务</p>
<p style="margin-top: 10px;">
客服微信号chengzilaoge520 或扫描右侧二维码
</p>
</div>
<div>
<img style="height: 80px;width: 80px;" src="/src/assets/images/wx.png" alt="">
</div>
</div>
<!-- 权益预览 -->
<div class="benefits-preview max-h-200px overflow-y-auto">
<div class="section-title">
专属权益
</div>
<ul class="benefits-list">
<li v-for="(b, index) in currentBenefits" :key="index" class="benefit-item">
<span class="dot">
<el-icon>
<CircleCheck />
</el-icon>
</span>
<span>
<span class="benefit-name">{{ b.name }}</span>
<span v-if="b.value">{{ b.value }}</span>
</span>
</li>
</ul>
<SupportModelList />
</div>
<!-- 支付区域 -->
<div class="payment-area">
<div v-if="false" class="agreement-text">
登录和注册都代表同意YiXinAI的会员协议
</div>
<div class="payment-info">
<div class="actual-payment">
<span>实际支付</span>
<span class="price">¥{{ selectedPrice || 0 }}</span>
</div>
<el-button text type="primary" class="view-details-btn" @click="toggleDetails">
了解更多
</el-button>
<el-button
v-if="userStore.userInfo"
type="primary" :disabled="!selectedId || isLoading" :loading="isLoading" class="pay-button"
@click="pay"
>
立即支付
</el-button>
<el-button
v-else
type="primary" class="pay-button"
@click="handleClickLogin"
>
立即登录/注册
</el-button>
</div>
<div class="note-text">
可叠加购买次数过期时间以最后订单为准<br>
最终解释权归YiXinAI所有
</div>
</div>
</div>
<!-- 桌面端布局 -->
<div v-else class="flex gap-6 desktop-layout">
<!-- 左栏 套餐卡片 + 支付 -->
<div class="w-[60%] flex flex-col justify-between">
<!-- 套餐卡片列表 -->
<div class="flex flex-wrap gap-4">
<div
v-for="pkg in currentPackages" :key="pkg.id" class="package-card"
:class="{ selected: pkg.id === selectedId }" @click="selectPackage(pkg)"
>
<!-- 标签 -->
<div v-if="pkg.discount" class="discount-tag">
{{ pkg.discount }}
</div>
<div v-if="pkg.tag" class="tag">
{{ pkg.tag }}
</div>
<!-- 套餐信息 -->
<div class="package-info">
<div class="package-name">
{{ pkg.name }}
</div>
<div class="package-desc">
{{ pkg.desc }}
</div>
</div>
<!-- 价格 -->
<div class="package-price">
<span class="price">¥{{ pkg.price }}</span>
<span v-if="pkg.perMonth" class="per-month">
¥{{ pkg.perMonth }}/
</span>
<div v-if="pkg.originalPrice" class="original-price">
原价¥{{ pkg.originalPrice }}
</div>
</div>
</div>
</div>
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
<div>
<p>充值后加客服微信回复账号名可专享vip售后服务</p>
<p style="margin-top: 10px;">
客服微信号chengzilaoge520 或扫描右侧二维码
</p>
</div>
<div>
<img style="height: 80px;width: 80px;" src="/src/assets/images/wx.png" alt="">
</div>
</div>
<!-- 支付按钮 -->
<div class="payment-section">
<div v-if="false" class="agreement-text">
登录和注册都代表同意YiXinAI的会员协议
</div>
<div class="payment-action">
<div class="actual-payment">
<span>实际支付</span>
<span class="price">¥{{ selectedPrice || 0 }}</span>
</div>
<div>
<el-button class="pay-button" text type="primary" @click="toggleDetails">
了解更多
</el-button>
<el-button
v-if="userStore.userInfo"
type="primary" size="large" :disabled="!selectedId || isLoading" :loading="isLoading"
class="pay-button" @click="pay"
>
立即支付
</el-button>
<el-button
v-else
type="primary" size="large"
class="pay-button" @click="handleClickLogin"
>
立即登录/注册
</el-button>
</div>
</div>
<div class="note-text">
可叠加购买次数过期时间以最后订单为准<br>
最终解释权归YiXinAI所有
</div>
</div>
</div>
<!-- 右栏 套餐权益 + 详情 -->
<div class="w-[40%] flex flex-col justify-between right-panel max-h-400px overflow-y-auto">
<div>
<div class="section-title">
会员权益
</div>
<ul class="benefits-list ">
<li v-for="(b, index) in currentBenefits" :key="index" class="benefit-item ">
<span class="dot">
<el-icon>
<CircleCheck />
</el-icon>
</span>
<span>
<span class="benefit-name">{{ b.name }}</span>
<span v-if="b.value">{{ b.value }}</span>
</span>
</li>
</ul>
<!-- 额外描述 -->
<div v-if="activeTab === 'member'" class="extra-description ">
<SupportModelList />
<!-- <div class="description-card"> -->
<!-- <div class="title"> -->
<!-- 前沿模型AI对话 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- DP-RI深度思考精准解答 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- AI写作文档对话AI思维导图等赋能职场 -->
<!-- </div> -->
<!-- </div> -->
<!-- <div class="description-card"> -->
<!-- <div class="title"> -->
<!-- AI绘图与设计能力 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- 视觉吸睛赋能 -->
<!-- </div> -->
<!-- <div class="subtext"> -->
<!-- "AI+办公!"解锁300+工具箱会员权益 -->
<!-- </div> -->
<!-- </div> -->
</div>
</div>
<!-- 查看详情 -->
<div class="view-details">
<!-- <el-button text type="primary" @click="toggleDetails"> -->
<!-- 查看详情 -->
<!-- </el-button> -->
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<style scoped lang="scss">
.product-package-dialog {
.el-dialog__header {
display: none;
}
.details-view {
height: 600px;
overflow-y: auto;
padding-right: 8px;
.benefits-table {
display: table;
width: 100%;
border-collapse: collapse;
.table-header,
.table-row {
display: table-row;
}
.table-cell {
display: table-cell;
padding: 12px 16px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
.table-header {
font-weight: bold;
background-color: #f8f8f8;
.table-cell {
border-bottom: 2px solid #ddd;
}
}
}
}
/* 移动端样式 */
.mobile-layout {
display: flex;
flex-direction: column;
gap: 24px;
.package-list {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.package-card {
position: relative;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
cursor: pointer;
&.selected {
border: 3px solid #fde19d;
background-image: url('@/assets/images/product_background.svg');
background-repeat: no-repeat;
/* 按需设置是否重复 */
background-size: cover;
/* 按需设置背景图尺寸适配方式,比如 cover、contain 等 */
background-position: bottom;
/* 按需设置背景图位置 */
box-shadow: 0 4px 6px -1px #fff4e3;
}
.discount-tag {
position: absolute;
top: -6px;
left: 8px;
background-color: #ef4444;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.tag {
position: absolute;
top: -6px;
right: 8px;
background-color: #f97316;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.package-info {
margin-bottom: 12px;
.package-name {
font-size: 16px;
font-weight: 600;
}
.package-desc {
font-size: 12px;
color: #6b7280;
}
}
.package-price {
.price {
font-size: 20px;
font-weight: 700;
color: #f97316;
}
.per-month {
font-size: 12px;
color: #6b7280;
margin-left: 4px;
}
.original-price {
font-size: 12px;
color: #9ca3af;
text-decoration: line-through;
}
}
}
.benefits-preview {
background-color: #f9fafb;
border-radius: 8px;
padding: 16px;
.section-title {
font-weight: 600;
margin-bottom: 12px;
}
.benefits-list {
list-style: none;
padding: 0;
margin: 0;
.benefit-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 14px;
color: #4b5563;
.dot {
color: #f97316;
margin-right: 8px;
}
.benefit-name {
font-weight: 500;
}
}
}
.view-details-btn {
width: 100%;
margin-top: 16px;
justify-content: flex-end;
}
}
.payment-area {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
.agreement-text {
font-size: 12px;
color: #6b7280;
margin-bottom: 12px;
}
.payment-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.actual-payment {
.price {
font-size: 18px;
font-weight: 700;
color: #f97316;
}
}
.pay-button {
width: 120px;
}
}
.note-text {
font-size: 12px;
color: #9ca3af;
}
}
}
/* 桌面端样式 */
.desktop-layout {
.package-card {
cursor: pointer;
position: relative;
width: calc(50% - 0.5rem);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
display: flex;
flex-direction: column;
justify-content: space-between;
//background-color: rgba(255, 245, 238, 0.38);
background: linear-gradient(90deg, #f1f2ebb5, white);
&.selected {
border: 3px solid #fde19d;
background-image: url('@/assets/images/product_background.svg');
background-repeat: no-repeat;
/* 按需设置是否重复 */
background-size: cover;
/* 按需设置背景图尺寸适配方式,比如 cover、contain 等 */
background-position: bottom;
/* 按需设置背景图位置 */
box-shadow: 0 4px 6px -1px #fff4e3;
}
.discount-tag {
position: absolute;
top: -6px;
left: 8px;
background-color: #ef4444;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.tag {
position: absolute;
top: -6px;
right: 8px;
background-color: #f97316;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.package-info {
margin-bottom: 12px;
.package-name {
font-size: 16px;
font-weight: 600;
}
.package-desc {
font-size: 12px;
color: #6b7280;
}
}
.package-price {
.price {
font-size: 20px;
font-weight: 700;
color: #f97316;
}
.per-month {
font-size: 12px;
color: #6b7280;
margin-left: 4px;
}
.original-price {
font-size: 12px;
color: #9ca3af;
text-decoration: line-through;
}
}
}
.payment-section {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
margin-top: 16px;
.agreement-text {
font-size: 12px;
color: #6b7280;
margin-bottom: 12px;
}
.payment-action {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.actual-payment {
.price {
font-size: 18px;
font-weight: 700;
color: #f97316;
}
}
}
.note-text {
font-size: 12px;
color: #9ca3af;
}
}
.right-panel {
.section-title {
font-weight: 600;
margin-bottom: 12px;
}
.benefits-list {
list-style: none;
padding: 0;
margin: 0;
.benefit-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 14px;
color: #4b5563;
.dot {
color: #f97316;
margin-right: 8px;
}
.benefit-name {
font-weight: 500;
}
}
}
.extra-description {
margin-top: 24px;
.description-card {
background-color: #f9fafb;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.title {
font-weight: 600;
margin-bottom: 4px;
}
.subtext {
font-size: 12px;
color: #6b7280;
line-height: 1.4;
}
}
}
.view-details {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
margin-top: 16px;
}
}
}
/* 响应式调整 */
@media (max-width: 768px) {
.el-dialog {
margin-top: 20px !important;
margin-bottom: 20px !important;
}
.details-view {
height: auto;
max-height: 80vh;
.benefits-table {
display: block;
overflow-x: auto;
white-space: nowrap;
.table-header,
.table-row {
display: table;
width: 100%;
table-layout: fixed;
}
}
}
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 3px;
}
}
</style>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
const models = [
{ name: 'DeepSeek-R1', price: '2', desc: '国产开源,深度思索模式,不过幻读问题比较大,同时具备思考响应链,在开源模型中永远的神!' },
{ name: 'DeepSeek-chat', price: '1', desc: '国产开源,简单聊天模式,对于中文文章语义体验较好,但响应速度一般' },
@@ -36,10 +38,13 @@ const models = [
意心AI 为您集成市面上热门模型订阅即享多模型使用权限无需额外购买真正做到一步到位
</p>
<h2 class="text-2xl font-semibold mb-4 text-center">
<!-- 网格布局默认2列 -->
<SupportModelList layout="grid" />
<h2 v-if="false" class="text-2xl font-semibold mb-4 text-center">
热门大模型价格排行榜
</h2>
<div class="bg-white shadow rounded-2xl overflow-hidden">
<div v-if="false" class="bg-white shadow rounded-2xl overflow-hidden">
<table class="w-full table-auto border-collapse">
<thead class="bg-gray-100">
<tr>
@@ -72,25 +77,18 @@ const models = [
<div class="mt-16">
<h2 class="text-2xl font-semibold mb-4 text-center">
查看更多大模型价格实时排行榜
热门大模型价格实时排行榜
</h2>
<div class="rounded-2xl shadow-lg overflow-hidden border border-gray-200">
<iframe
src="https://easyllm.site/static/models.html"
width="100%"
height="700"
class="w-full"
loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups"
/>
<div class="rounded-2xl shadow-lg overflow-hidden border border-gray-200 flex justify-center items-center p-4">
<a href="https://openrouter.ai/models">https://openrouter.ai/models</a>
</div>
<p class="text-sm text-center text-gray-500 mt-2">
来源LMSYS Chatbot Arena 排行
来源openrouter 模型
</p>
</div>
<div class="mt-8 text-center">
<h2 class="text-2xl font-semibold mb-2">
一口价订阅仅需 <span class="text-red-500 text-3xl font-bold">49.9/</span>
一口价订阅最低仅需 <span class="text-red-500 text-3xl font-bold">21.9/</span>
</h2>
<p class="text-gray-600 mb-4">
即解锁以上全部模型随时切换无需单独付费

View File

@@ -7,7 +7,7 @@ import { useUserStore } from '@/stores';
const greeting = useTimeGreeting();
const userStore = useUserStore();
const username = computed(() => userStore.userInfo?.username ?? '意心Ai一心只为打造更良心的AI平台');
const username = computed(() => userStore.userInfo?.username ?? '意心Ai一心只为打造更良心的Ai平台');
</script>
<template>

View File

@@ -72,7 +72,7 @@ function openContact() {
<div class="mt-8 text-center">
<h2 class="text-2xl font-semibold mb-2">
一口价订阅仅需 <span class="text-red-500 text-3xl font-bold">9.9/</span>
一口价订阅最低仅需 <span class="text-red-500 text-3xl font-bold">9.9/</span>
</h2>
<p class="text-gray-600 mb-4">
即解锁以上全部模型随时切换无需单独付费

View File

@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup xmlns="http://www.w3.org/1999/html">
import { CircleCheck } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { onMounted, ref } from 'vue';
@@ -239,14 +239,44 @@ onMounted(async () => {
<!-- 使用说明 -->
<div v-if="apiKey" class="usage-guide">
<el-divider />
<h3>使用说明</h3>
<div class="guide-content">
<div class="max-w-4xl mx-auto p-4 space-y-4">
<!-- 标题链接 -->
<h1 class="text-2xl font-bold text-gray-800 hover:text-blue-600 transition-colors">
<a
href="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
target="_blank"
class="flex items-center gap-2 group"
style="color: #E6A23C;"
title="点击跳转玩法指南专栏"
>
意心AiAI工具玩法指南
<span class="text-sm text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity">
点击查看完整指南
</span>
</a>
</h1>
<!-- iframe 容器关键修复部分 -->
<div class="relative w-full overflow-auto rounded-xl shadow-lg border border-gray-200 bg-gray-50">
<!-- 自适应缩放 iframe -->
<iframe
src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
class="min-w-full h-[700px] scale-100 duration-300"
loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups"
@load="document.querySelector('.iframe-loading')?.remove()"
/>
</div>
</div>
<div v-if="false" class="guide-content">
<p><strong>API地址</strong>https://ai.ccnetcore.com</p>
<p><strong>密钥</strong>上面申请的token</p>
<p><strong>模型</strong>聊天界面显示的模型名称</p>
</div>
<div class="guide-images">
<div v-if="false" class="guide-images">
<el-image
style="max-width: 100%; margin: 10px 0;"
src="/images/api_usage_instructions.png"

View File

@@ -1,7 +1,9 @@
<script lang="ts" setup>
import { List, Refresh, Search } from '@element-plus/icons-vue';
import { ChatLineRound, List, Refresh, Search } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { getRechargeLog } from '@/api/model/index.ts';
import { isUserVip } from '@/utils/user.ts';
interface RechargeLog {
id: string;
@@ -15,11 +17,48 @@ interface RechargeLog {
}
const loading = ref(false);
const innerVisibleContact = ref(false);
const logData = ref<RechargeLog[]>([]);
const searchText = ref('');
const currentSort = ref({ prop: '', order: '' });
const currentPage = ref(1);
const pageSize = ref(10);
const showWechatFullscreen = ref(false);
const showWxGroupFullscreen = ref(false);
const wxSrc = computed(
() => `/src/assets/images/wx.png`,
);
const wxGroupQD = computed(
() => `/src/assets/images/wx.png`,
);
// 复制微信号
function copyWechatId() {
navigator.clipboard.writeText('chengzilaoge520').then(() => {
ElMessage({
message: '微信号已复制到剪贴板',
type: 'success',
duration: 2000,
});
});
}
// 显示微信二维码全屏
function showWechatFullscreenImage() {
showWechatFullscreen.value = true;
}
// 显示微信群二维码全屏
function showWxGroupFullscreenImage() {
showWxGroupFullscreen.value = true;
}
// 关闭全屏图片
function closeFullscreenImage() {
showWechatFullscreen.value = false;
showWxGroupFullscreen.value = false;
}
// 模拟数据获取
async function fetchRechargeLog() {
@@ -60,6 +99,11 @@ function refreshLog() {
currentSort.value = { prop: '', order: '' };
}
// 联系售后弹窗
function contactCustomerService() {
innerVisibleContact.value = !innerVisibleContact.value;
}
// 过滤和排序后的数据
const filteredData = computed(() => {
let data = [...logData.value];
@@ -90,18 +134,11 @@ const filteredData = computed(() => {
});
}
// 分页处理
// if (showPagination.value) {
// const start = (currentPage.value - 1) * pageSize.value;
// return data.slice(start, start + pageSize.value);
// }
return data;
});
// 是否显示分页 暂时不需要分页功能
const showPagination = computed(() => {
// return logData.value.length > 10;
return false;
});
@@ -111,6 +148,74 @@ onMounted(() => {
</script>
<template>
<el-dialog
v-model="innerVisibleContact"
width="500"
title="售后支持"
append-to-body
>
<h3 class="text-lg font-bold mb-3">
请扫码加入微信交流群<br>
备注ai获取专属客服支持
</h3>
<div class="mb-4 flex items-center justify-center space-x-2">
<span class="font-semibold">站长微信账号</span>
<span id="wechat-id" class="text-blue-600 font-mono select-text">chengzilaoge520</span>
<span
class="cursor-pointer"
title="点击复制"
@click="copyWechatId"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 opacity-70 hover:opacity-100" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v16h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 18H8V7h11v16z" />
</svg>
</span>
</div>
<div class="flex justify-center mb-4">
<div class="px-4">
<h4>站长微信(备注AI以便通过)</h4>
<img
:src="wxSrc"
class="w-50 py-5 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
alt="微信二维码"
@click="showWechatFullscreenImage"
>
</div>
<div class="px-4">
<h4>微信交流群</h4>
<img
:src="wxGroupQD"
class="w-50 py-5 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
alt="微信二维码"
@click="showWxGroupFullscreenImage"
>
</div>
</div>
<!-- 全屏放大二维码 -->
<div
v-if="showWechatFullscreen"
class="fullscreen-image-overlay"
@click="closeFullscreenImage"
>
<img
:src="wxSrc"
class="fullscreen-image"
>
</div>
<div
v-if="showWxGroupFullscreen"
class="fullscreen-image-overlay"
@click="closeFullscreenImage"
>
<img
:src="wxGroupQD"
class="fullscreen-image"
>
</div>
</el-dialog>
<div class="recharge-log-container">
<div class="log-header">
<h2 class="log-title">
@@ -118,6 +223,13 @@ onMounted(() => {
充值记录
</h2>
<div class="header-actions">
<el-tooltip v-if="isUserVip()" content="联系售后" placement="top">
<el-button circle :loading="loading" @click="contactCustomerService">
<el-icon color="#07c160">
<ChatLineRound />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="刷新数据" placement="top">
<el-button circle :loading="loading" @click="refreshLog">
<el-icon><Refresh /></el-icon>
@@ -125,7 +237,7 @@ onMounted(() => {
</el-tooltip>
<el-input
v-model="searchText"
placeholder="搜索联系方式/备注"
placeholder="搜索备注"
clearable
style="width: 200px; margin-left: 10px;"
@clear="handleSearchClear"
@@ -141,6 +253,7 @@ onMounted(() => {
</div>
<el-table
v-loading="loading"
:data="filteredData"
style="width: 100%"
@@ -152,14 +265,15 @@ onMounted(() => {
<el-table-column
prop="content"
label="套餐类型"
width="120"
width="150"
sortable="custom"
show-overflow-tooltip
/>
<el-table-column
show-overflow-tooltip
prop="rechargeAmount"
label="金额(元)"
width="120"
align="right"
width="110"
sortable="custom"
>
<template #default="{ row }">
@@ -169,24 +283,26 @@ onMounted(() => {
<el-table-column
prop="creationTime"
label="充值时间"
width="180"
width="160"
sortable="custom"
show-overflow-tooltip
/>
<el-table-column
prop="expireDateTime"
label="到期时间"
width="180"
width="160"
sortable="custom"
show-overflow-tooltip
/>
<el-table-column prop="contactInfo" width="100" label="联系方式">
<!-- <el-table-column show-overflow-tooltip prop="contactInfo" width="100" label="联系方式">
<template #default="{ row }">
<el-tooltip v-if="row.contactInfo && row.contactInfo.length > 8" :content="row.contactInfo" placement="top">
<span class="ellipsis-text">{{ row.contactInfo }}</span>
</el-tooltip>
<span v-else>{{ row.contactInfo || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注">
</el-table-column> -->
<el-table-column show-overflow-tooltip prop="remark" label="备注" width="160">
<template #default="{ row }">
<el-tooltip v-if="row.remark && row.remark.length > 10" :content="row.remark" placement="top">
<span class="ellipsis-text">{{ row.remark }}</span>
@@ -214,6 +330,25 @@ onMounted(() => {
</template>
<style scoped>
/* 新增全屏图片样式 */
.fullscreen-image-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.fullscreen-image {
max-width: 90%;
max-height: 90%;
border: 8px solid white;
border-radius: 16px;
box-shadow: 0 0 40px rgba(255, 255, 255, 0.2);
}
.recharge-log-container {
padding: 20px;
background: #fff;

View File

@@ -0,0 +1,183 @@
<SupportModelList :layout="'grid'" />
<script lang="ts" setup>
import { useModelStore } from '@/stores/modules/model';
interface Props {
layout?: 'list' | 'grid'; // 布局方式list(单列)或grid(网格)
columns?: number; // 网格布局时的列数默认为2
}
const props = withDefaults(defineProps<Props>(), {
layout: 'list',
columns: 2,
});
// 从store获取模型列表
const modelStore = useModelStore();
const modelList = computed(() => modelStore.modelList);
console.log('modelList---', modelList);
// 计算网格布局的列数
const gridTemplateColumns = computed(() => {
if (props.layout === 'list')
return '1fr';
return `repeat(${props.columns}, minmax(300px, 1fr))`;
});
</script>
<template>
<div class="model-container">
<div class="model-header">
支持的模型
</div>
<div
class="model-grid"
:style="{ 'grid-template-columns': gridTemplateColumns }"
>
<div v-for="model in modelList" :key="model.id" class="model-card">
<div class="model-card-header">
<h3 class="model-name">
{{ model.modelName }}
</h3>
<div class="model-price">
<template v-if="model.modelPrice === 0">
<span
class="free-tag"
:class="model.modelId === 'DeepSeek-R1-0528' ? 'free' : 'vip'"
>
{{ model.modelId === 'DeepSeek-R1-0528' ? '免费' : 'Vip专享' }}
</span>
</template>
<template v-else>
<span class="price">{{ model.modelPrice }}/</span>
<span class="per-token">{{ model.modelPrice * 100 }}/百万Token</span>
</template>
</div>
</div>
<div class="model-description">
{{ model.modelDescribe }}
</div>
<div class="model-footer">
<span class="model-id">{{ model.modelId }}</span>
<!-- <el-tag v-if="model.category === 'chat'" size="small" type="success"> -->
<!-- 对话 -->
<!-- </el-tag> -->
<!-- <el-tag v-else size="small" type="info"> -->
<!-- 其他 -->
<!-- </el-tag> -->
</div>
</div>
</div>
</div>
</template>
<style scoped>
.model-container {
margin: 10px 0;
}
.model-header {
font-size: 14px;
margin-bottom: 24px;
color: #333;
font-weight: 600;
}
.model-grid {
display: grid;
gap: 20px;
}
.model-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 10px;
transition: all 0.3s ease;
border: 1px solid #ebeef5;
}
.model-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.1);
}
.model-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.model-name {
font-size: 14px;
font-weight: 600;
margin: 0;
color: #333;
}
.model-price {
text-align: right;
}
.free-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.free-tag.free {
background: #f0f9eb;
color: #67c23a;
}
.free-tag.vip {
background: #fff7e6;
color: #d48806;
}
.price {
display: block;
font-size: 13px;
font-weight: 600;
color: #f56c6c;
}
.per-token {
font-size: 12px;
color: #909399;
}
.model-description {
color: #606266;
line-height: 1.6;
margin-bottom: 16px;
min-height: 60px;
}
.model-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.model-id {
font-size: 12px;
color: #909399;
}
@media (max-width: 768px) {
.model-grid {
grid-template-columns: 1fr !important;
}
.model-card {
padding: 16px;
}
}
</style>

View File

@@ -364,7 +364,7 @@ onBeforeUnmount(() => {
<div class="card-header">
<span>近七天每日Token消耗量</span>
<el-tag type="primary">
总计: {{ totalTokens }} tokens
近七日总计: {{ totalTokens }} tokens
</el-tag>
</div>
</template>
@@ -387,7 +387,7 @@ onBeforeUnmount(() => {
<el-card v-loading="loading" class="chart-card">
<template #header>
<div class="card-header">
<span>各模型Token消耗量</span>
<span>各模型Token消耗量</span>
</div>
</template>
<div class="chart-container">

View File

@@ -1,24 +1,343 @@
<script lang="ts" setup>
interface User {
name: string;
email: string;
role: string;
import { Camera, Edit, SuccessFilled } from '@element-plus/icons-vue';
import { format } from 'date-fns';
import { computed, onMounted, ref } from 'vue';
import { getUserInfo } from '@/api';
import QrCodeLogin from '@/components/LoginDialog/components/QrCodeLogin/index.vue';
import { useUserStore } from '@/stores';
import { getUserProfilePicture, WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
const userStore = useUserStore();
onMounted(async () => {
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
});
const user = computed(() => userStore.userInfo.user || {});
const wechatDialogVisible = ref(false);
// 计算属性
const userIcon = computed(() => {
return getUserProfilePicture() || `https://your-cdn.com/${user.value.icon}`;
});
const userNick = computed(() => {
return user.value.nick || user.value.userName || '未知用户';
});
// 是否绑定了微信
const isWechatBound = computed(() => {
return userStore.userInfo.isBindFuwuhao || false;
});
// 格式化日期
function formatDate(dateString: string | null) {
if (!dateString)
return '-';
try {
return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss');
}
catch {
return dateString;
}
}
const users: User[] = [
{ name: '张三', email: 'zhangsan@example.com', role: '管理员' },
{ name: '李四', email: 'lisi@example.com', role: '编辑' },
{ name: '王五', email: 'wangwu@example.com', role: '查看者' },
];
// 性别显示
function getSexText(sex: string | null) {
const sexMap: Record<string, string> = {
Male: '男',
Female: '女',
Unknown: '未知',
};
return sexMap[sex || 'Unknown'] || '未知';
}
function getSexTagType(sex: string | null) {
const typeMap: Record<string, string> = {
Male: 'primary',
Female: 'danger',
Unknown: 'info',
};
return typeMap[sex || 'Unknown'] || 'info';
}
// 敏感信息脱敏
function maskEmail(email: string) {
if (!email)
return '';
const [name, domain] = email.split('@');
if (name.length <= 2)
return email;
return `${name.substring(0, 2)}****@${domain}`;
}
function maskPhone(phone: number) {
if (!phone)
return '';
return phone.toString().replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 操作处理
function handleEdit() {
ElMessage.info('编辑功能开发中');
}
function changeAvatar() {
ElMessage.info('更换头像功能开发中');
}
function handleWechatBind() {
wechatDialogVisible.value = true;
}
// 微信绑定成功
function bindWechat() {
wechatDialogVisible.value = false;
}
</script>
<template>
<div class="user-management">
<h3>用户管理</h3>
<el-table :data="users" style="width: 100%">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="role" label="角色" />
</el-table>
<div class="user-profile">
<el-card class="profile-card">
<template #header>
<div class="card-header">
<h3>个人信息</h3>
<el-button v-if="false" type="primary" size="small" @click="handleEdit">
<el-icon><Edit /></el-icon>
编辑信息
</el-button>
</div>
</template>
<div class="profile-content">
<!-- 头像区域 -->
<div class="avatar-section">
<el-avatar :size="100" :src="userIcon" class="user-avatar">
{{ userNick.charAt(0) }}
</el-avatar>
<div v-if="false" class="avatar-actions">
<el-button size="small" @click="changeAvatar">
<el-icon><Camera /></el-icon>
更换头像
</el-button>
</div>
</div>
<!-- 基本信息 -->
<div class="info-section">
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">
{{ user.userName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="昵称">
{{ userNick }}
</el-descriptions-item>
<el-descriptions-item label="性别">
<el-tag :type="getSexTagType(user.sex)">
{{ getSexText(user.sex) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="注册时间">
{{ formatDate(user.creationTime) }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
<span v-if="user.email">
{{ maskEmail(user.email) }}
<el-tooltip content="已验证" placement="top">
<el-icon color="#67C23A"><SuccessFilled /></el-icon>
</el-tooltip>
</span>
<span v-else class="unset-text">未设置</span>
</el-descriptions-item>
<el-descriptions-item label="手机号">
<span v-if="user.phone">
{{ maskPhone(user.phone) }}
</span>
<span v-else class="unset-text">未设置</span>
</el-descriptions-item>
<el-descriptions-item label="微信绑定">
<div class="wechat-binding">
<span v-if="isWechatBound">
<el-icon color="#07C160"><SuccessFilled /></el-icon>
已绑定
<!-- <span class="wechat-id">({{ maskWechat(wechatInfo) }})</span> -->
</span>
<span v-else class="unset-text">
未绑定
</span>
<el-button
v-if="!isWechatBound"
class="bind-btn"
type="primary"
@click="handleWechatBind"
>
绑定
</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item label="个人简介">
<span v-if="user.introduction">
{{ user.introduction }}
</span>
<span v-else class="unset-text">暂无简介</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
<!-- 微信绑定对话框 -->
<el-dialog
v-model="wechatDialogVisible"
title="微信绑定"
width="400px"
>
<div class="wechat-dialog">
<QrCodeLogin :type="WECHAT_QRCODE_TYPE.Bind" @bind-wechat="bindWechat()" />
</div>
<template #footer>
<el-button @click="wechatDialogVisible = false">
取消
</el-button>
<el-button
type="primary"
@click="wechatDialogVisible = false"
>
关闭
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.user-profile {
padding: 20px;
}
.profile-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h3 {
margin: 0;
color: #333;
}
.profile-content {
display: flex;
gap: 40px;
align-items: flex-start;
}
.avatar-section {
text-align: center;
flex-shrink: 0;
}
.user-avatar {
margin-bottom: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-size: 40px;
font-weight: bold;
}
.avatar-actions {
margin-top: 10px;
}
.info-section {
flex: 1;
}
:deep(.el-descriptions__body) {
background-color: #fafafa;
}
:deep(.el-descriptions__label) {
font-weight: 600;
width: 100px;
}
.unset-text {
color: #999;
font-style: italic;
}
.wechat-binding {
display: flex;
align-items: center;
justify-content: space-between;
}
.wechat-id {
color: #666;
font-size: 12px;
margin-left: 5px;
}
.bind-btn {
margin-left: 10px;
}
.wechat-dialog {
text-align: center;
}
.qrcode-section {
margin: 20px 0;
}
.qrcode-placeholder {
width: 200px;
height: 200px;
margin: 0 auto;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
}
.qrcode-placeholder .el-icon {
font-size: 48px;
margin-bottom: 10px;
}
.wechat-tip {
color: #666;
margin-top: 10px;
}
.wechat-info {
color: #07C160;
font-weight: 500;
}
@media (max-width: 768px) {
.profile-content {
flex-direction: column;
gap: 20px;
}
.avatar-section {
align-self: center;
}
}
</style>

View File

@@ -212,7 +212,7 @@ function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo
position: absolute;
top: 0;
left: 0;
z-index: 11;
// z-index: 11;
width: var(--sidebar-default-width);
height: 100%;
pointer-events: auto;
@@ -315,7 +315,7 @@ function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo
.aside-container-collapse {
position: absolute;
top: 54px;
z-index: 22;
// z-index: 22;
height: auto;
max-height: calc(100% - 110px);
padding-bottom: 12px;
@@ -381,5 +381,9 @@ function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo
padding-left: 12px !important;
background-color: var(--sidebar-background-color) !important;
}
.conversation-group .active-sticky
{
z-index: 0 ;
}
}
</style>

View File

@@ -5,6 +5,7 @@ import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
import { showProductPackage } from '@/utils/product-package';
import { getUserProfilePicture, isUserVip } from '@/utils/user';
const router = useRouter();
@@ -57,12 +58,14 @@ const popoverList = ref([
const dialogVisible = ref(false);
const navItems = [
// { name: 'user', label: '用户管理', icon: 'User' },
{ name: 'user', label: '用户信息', icon: 'User' },
// { name: 'role', label: '角色管理', icon: 'Avatar' },
// { name: 'permission', label: '权限管理', icon: 'Key' },
// { name: 'userInfo', label: '用户信息', icon: 'User' },
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
];
function openDialog() {
dialogVisible.value = true;
@@ -163,12 +166,22 @@ function openVipGuide() {
}
/* 弹出面板 结束 */
function onProductPackage() {
showProductPackage();
}
// 直接调用
</script>
<template>
<div class="flex items-center gap-2">
<el-button
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
@click="onProductPackage"
>
<span>立即购买</span>
</el-button>
<!-- 用户信息区域 -->
<div class=" cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openVipGuide">
<div class=" cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="onProductPackage">
<div class="text-sm font-semibold text-gray-800">
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
</div>
@@ -186,7 +199,7 @@ function openVipGuide() {
v-else
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
>
普通用户 · 开通 VIP
普通用户
</span>
</div>
</div>
@@ -225,7 +238,7 @@ function openVipGuide() {
v-else
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
>
普通用户 · 开通 VIP
普通用户
</span>
</div>
</div>
@@ -264,6 +277,10 @@ function openVipGuide() {
<template #usageStatistics>
<usage-statistics />
</template>
<!-- 用量统计 -->
<!-- <template #usageStatistics2> -->
<!-- <usage-statistics2 /> -->
<!-- </template> -->
<!-- 角色管理内容 -->
<template #role>
@@ -297,4 +314,44 @@ function openVipGuide() {
border-radius: 8px;
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
}
.buy-btn {
background: linear-gradient(90deg, #FFD700, #FFC107);
color: #fff;
border: none;
border-radius: 9999px;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
background: linear-gradient(90deg, #FFC107, #FFD700);
}
.icon-rocket {
color: #fff;
}
.animate-bounce {
animation: bounce 1.2s infinite;
}
}
//移动端屏幕小于756px
@media screen and (max-width: 756px) {
.buy-btn {
background: linear-gradient(90deg, #FFD700, #FFC107);
color: #fff;
border: none;
border-radius: 9999px;
transition: transform 0.2s, box-shadow 0.2s;
font-size: 12px;
max-width: 60px;
}
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
</style>

View File

@@ -23,7 +23,7 @@ const sessionId = computed(() => route.params?.id);
flex-direction: column;
align-items: center;
justify-content: center;
width: calc(100% - 32px);
// width: calc(100% - 32px);
height: 100%;
padding: 0 16px;
overflow-anchor: none;

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