mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-03 00:00:58 +08:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e8b2b01f | ||
|
|
4d09243efd | ||
|
|
5934056fe6 | ||
|
|
2a81062fa3 | ||
|
|
fdc868323f | ||
|
|
593b3a4cdd | ||
|
|
2b12e18e6c | ||
|
|
345ed80ec8 | ||
|
|
29dc1ae250 | ||
|
|
9fdd41b134 | ||
|
|
31ee5e8ffb | ||
|
|
d7922bb71d | ||
|
|
fa3ac91ba4 | ||
|
|
e7c152e955 | ||
|
|
0223b5c104 | ||
|
|
9e41a7c446 | ||
|
|
2ba0ffc1b7 | ||
|
|
7d038e1266 | ||
|
|
b98285f314 | ||
|
|
73438da666 | ||
|
|
85f2e1b579 | ||
|
|
ece89ebad0 | ||
|
|
6e2dd39246 | ||
|
|
a61286e534 | ||
|
|
4f944a5466 | ||
|
|
d29aac088a | ||
|
|
8abd122773 | ||
|
|
08084aa0bc | ||
|
|
e69cd5a73c | ||
|
|
76aa3bdc64 | ||
|
|
93251104af | ||
|
|
3cae477f3e | ||
|
|
25c736dc0a | ||
|
|
96a09d8980 | ||
|
|
72387235a0 | ||
|
|
1b00e505b7 | ||
|
|
1c54e47b9e | ||
|
|
ba07e2c905 | ||
|
|
bde4611a50 | ||
|
|
e7326fea7b | ||
|
|
d13b23ad2e | ||
|
|
8b1830a711 | ||
|
|
b70c530754 | ||
|
|
d90e24f9ed | ||
|
|
1fbd521d1a | ||
|
|
2ae6183e7f | ||
|
|
7905911624 | ||
|
|
c5b6b33d8e | ||
|
|
5d29fd6d3b | ||
|
|
ad8f48f36b | ||
|
|
f9843c13d4 | ||
|
|
6bd561b094 | ||
|
|
d2c6238df1 | ||
|
|
1d108983e8 | ||
|
|
b768bca638 | ||
|
|
28fcd6c9ce | ||
|
|
10559a925c | ||
|
|
942e218a9e | ||
|
|
f6af9edc38 | ||
|
|
e0f6331ec3 | ||
|
|
06f0c6caa7 | ||
|
|
56ec260e3a | ||
|
|
176cf84369 | ||
|
|
6de3b722ed | ||
|
|
2cf6326764 | ||
|
|
ec27ee58b4 | ||
|
|
4e42e2202e | ||
|
|
e60d8eceb7 | ||
|
|
08fb939b38 | ||
|
|
482dd73afd | ||
|
|
f09a9fee75 | ||
|
|
9a31d14b41 | ||
|
|
2fd7f88f04 | ||
|
|
9d4cc802e9 | ||
|
|
ee6b4827fa | ||
|
|
48d8c528f6 | ||
|
|
40c0a5ac64 |
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
/// 备注
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,表示永久VIP,ExpireDateTime保持为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
|
||||
};
|
||||
|
||||
@@ -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:";
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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-4,gpt-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; }
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -30,5 +30,5 @@ public class ThorToolFunctionDefinition
|
||||
/// documentation about the format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parameters")]
|
||||
public ThorToolFunctionPropertyDefinition Parameters { get; set; }
|
||||
public ThorToolFunctionPropertyDefinition? Parameters { get; set; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||
|
||||
public enum SceneTypeEnum
|
||||
{
|
||||
LoginOrRegister,
|
||||
Bind
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum ModelTypeEnum
|
||||
{
|
||||
Chat = 0,
|
||||
Image = 1,
|
||||
Embedding = 2
|
||||
Embedding = 2,
|
||||
PremiumChat = 3
|
||||
}
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()}"))
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
public string? Nick{ get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 头像
|
||||
/// </summary>
|
||||
public string? Icon { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Remove="logs\**" />
|
||||
<Content Update="wwwroot\aihub\auth.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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": {
|
||||
|
||||
282
Yi.Abp.Net8/src/Yi.Abp.Web/wwwroot/aihub/auth.html
Normal file
282
Yi.Abp.Net8/src/Yi.Abp.Web/wwwroot/aihub/auth.html
Normal 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>© 2025 意社区. 保留所有权利.</p>
|
||||
<p><a href="#" class="link">隐私政策</a> | <a href="#" class="link">服务条款</a> | <a href="#" class="link">帮助中心</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
7
Yi.Ai.Vue3/.claude/settings.json
Normal file
7
Yi.Ai.Vue3/.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-lVcGFMOQfXYtZWp1rD4SaBNDNpM270UX2wDqWh",
|
||||
"ANTHROPIC_BASE_URL": "https://api.token-ai.cn",
|
||||
"ANTHROPIC_MODEL": "gpt-4o-mini"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
212
Yi.Ai.Vue3/pnpm-lock.yaml
generated
212
Yi.Ai.Vue3/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './auth';
|
||||
export * from './chat';
|
||||
export * from './model';
|
||||
export * from './pay';
|
||||
export * from './session';
|
||||
export * from './user';
|
||||
|
||||
11
Yi.Ai.Vue3/src/api/pay/index.ts
Normal file
11
Yi.Ai.Vue3/src/api/pay/index.ts
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
346
Yi.Ai.Vue3/src/assets/images/product_background.svg
Normal file
346
Yi.Ai.Vue3/src/assets/images/product_background.svg
Normal 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 |
BIN
Yi.Ai.Vue3/src/assets/images/wx.png
Normal file
BIN
Yi.Ai.Vue3/src/assets/images/wx.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
|
||||
997
Yi.Ai.Vue3/src/components/ProductPackage/index.vue
Normal file
997
Yi.Ai.Vue3/src/components/ProductPackage/index.vue
Normal 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>
|
||||
@@ -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">
|
||||
即解锁以上全部模型,随时切换,无需单独付费。
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
即解锁以上全部模型,随时切换,无需单独付费。
|
||||
|
||||
@@ -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="点击跳转玩法指南专栏"
|
||||
>
|
||||
【意心Ai】AI工具玩法指南
|
||||
<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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user