fix: 修复用量统计线程问题并完善搜索与Token计算逻辑
- OnlineSearch 增加 daysAgo 非法值保护,避免无效时间范围 - 修复 UsageStatistics 中 Prompt/Completion Token 为 0 时的统计异常 - 引入独立 UnitOfWork,解决流式处理下的并发与事务问题 - 确保用量统计、系统消息与尊享包扣减的原子性 - 补充前端 Element Plus 组件类型声明 - 统一并优化部分代码格式,不影响业务逻辑
This commit is contained in:
@@ -8,17 +8,15 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
using OpenAI.Chat;
|
using OpenAI.Chat;
|
||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Volo.Abp.Uow;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay;
|
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Attributes;
|
using Yi.Framework.AiHub.Domain.Shared.Attributes;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
@@ -36,12 +34,14 @@ public class ChatManager : DomainService
|
|||||||
private readonly PremiumPackageManager _premiumPackageManager;
|
private readonly PremiumPackageManager _premiumPackageManager;
|
||||||
private readonly AiGateWayManager _aiGateWayManager;
|
private readonly AiGateWayManager _aiGateWayManager;
|
||||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
|
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
|
||||||
|
private readonly IUnitOfWorkManager _unitOfWorkManager;
|
||||||
|
|
||||||
public ChatManager(ILoggerFactory loggerFactory,
|
public ChatManager(ILoggerFactory loggerFactory,
|
||||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||||
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, AiMessageManager aiMessageManager,
|
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, AiMessageManager aiMessageManager,
|
||||||
UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager,
|
UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager,
|
||||||
AiGateWayManager aiGateWayManager, ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository)
|
AiGateWayManager aiGateWayManager, ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository,
|
||||||
|
IUnitOfWorkManager unitOfWorkManager)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_messageRepository = messageRepository;
|
_messageRepository = messageRepository;
|
||||||
@@ -51,6 +51,7 @@ public class ChatManager : DomainService
|
|||||||
_premiumPackageManager = premiumPackageManager;
|
_premiumPackageManager = premiumPackageManager;
|
||||||
_aiGateWayManager = aiGateWayManager;
|
_aiGateWayManager = aiGateWayManager;
|
||||||
_aiModelRepository = aiModelRepository;
|
_aiModelRepository = aiModelRepository;
|
||||||
|
_unitOfWorkManager = unitOfWorkManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -189,6 +190,9 @@ public class ChatManager : DomainService
|
|||||||
|
|
||||||
//用量统计
|
//用量统计
|
||||||
case UsageContent usageContent:
|
case UsageContent usageContent:
|
||||||
|
//由于MAF线程问题
|
||||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
|
||||||
|
{
|
||||||
var usage = new ThorUsageResponse
|
var usage = new ThorUsageResponse
|
||||||
{
|
{
|
||||||
InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0),
|
InputTokens = Convert.ToInt32(usageContent.Details.InputTokenCount ?? 0),
|
||||||
@@ -224,6 +228,8 @@ public class ChatManager : DomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await uow.CompleteAsync();
|
||||||
|
|
||||||
await SendHttpStreamMessageAsync(httpContext,
|
await SendHttpStreamMessageAsync(httpContext,
|
||||||
new AgentResultOutput
|
new AgentResultOutput
|
||||||
{
|
{
|
||||||
@@ -239,6 +245,7 @@ public class ChatManager : DomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//断开连接
|
//断开连接
|
||||||
await SendHttpStreamMessageAsync(httpContext, null, isDone: true, cancellationToken);
|
await SendHttpStreamMessageAsync(httpContext, null, isDone: true, cancellationToken);
|
||||||
@@ -247,8 +254,13 @@ public class ChatManager : DomainService
|
|||||||
string serializedJson = currentThread.Serialize(JsonSerializerOptions.Web).GetRawText();
|
string serializedJson = currentThread.Serialize(JsonSerializerOptions.Web).GetRawText();
|
||||||
agentStore.Store = serializedJson;
|
agentStore.Store = serializedJson;
|
||||||
|
|
||||||
|
//由于MAF线程问题
|
||||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
|
||||||
|
{
|
||||||
//插入或者更新
|
//插入或者更新
|
||||||
await _agentStoreRepository.InsertOrUpdateAsync(agentStore);
|
await _agentStoreRepository.InsertOrUpdateAsync(agentStore);
|
||||||
|
await uow.CompleteAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ public class UsageStatisticsManager : DomainService
|
|||||||
{
|
{
|
||||||
var actualTokenId = tokenId ?? Guid.Empty;
|
var actualTokenId = tokenId ?? Guid.Empty;
|
||||||
|
|
||||||
long inputTokenCount = tokenUsage?.PromptTokens
|
long inputTokenCount = tokenUsage?.PromptTokens > 0
|
||||||
?? tokenUsage?.InputTokens
|
? tokenUsage.PromptTokens.Value
|
||||||
?? 0;
|
: tokenUsage?.InputTokens ?? 0;
|
||||||
|
|
||||||
long outputTokenCount = tokenUsage?.CompletionTokens
|
long outputTokenCount = tokenUsage?.CompletionTokens > 0
|
||||||
?? tokenUsage?.OutputTokens
|
? tokenUsage.CompletionTokens.Value
|
||||||
?? 0;
|
: tokenUsage?.OutputTokens ?? 0;
|
||||||
|
|
||||||
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}:{actualTokenId}:{modelId}"))
|
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}:{actualTokenId}:{modelId}"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,8 +29,14 @@ public class OnlineSearchTool : ISingletonDependency
|
|||||||
}
|
}
|
||||||
|
|
||||||
[YiAgentTool("联网搜索"), DisplayName("OnlineSearch"), Description("进行在线搜索,获取最新的网络信息,近期信息是7天,实时信息是1天")]
|
[YiAgentTool("联网搜索"), DisplayName("OnlineSearch"), Description("进行在线搜索,获取最新的网络信息,近期信息是7天,实时信息是1天")]
|
||||||
public async Task<string> OnlineSearch([Description("搜索关键字")]string keyword, [Description("距离现在多久天")]int? daysAgo = null)
|
public async Task<string> OnlineSearch([Description("搜索关键字")] string keyword,
|
||||||
|
[Description("距离现在多久天")] int? daysAgo = null)
|
||||||
{
|
{
|
||||||
|
if (daysAgo <= 0)
|
||||||
|
{
|
||||||
|
daysAgo = 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(keyword))
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
{
|
{
|
||||||
return "搜索关键词不能为空";
|
return "搜索关键词不能为空";
|
||||||
@@ -149,8 +155,7 @@ public class OnlineSearchTool : ISingletonDependency
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchRequest
|
public class BaiduSearchRequest
|
||||||
{
|
{
|
||||||
[JsonPropertyName("messages")]
|
[JsonPropertyName("messages")] public List<BaiduSearchMessage> Messages { get; set; } = new();
|
||||||
public List<BaiduSearchMessage> Messages { get; set; } = new();
|
|
||||||
|
|
||||||
[JsonPropertyName("search_filter")]
|
[JsonPropertyName("search_filter")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
@@ -162,8 +167,7 @@ public class BaiduSearchRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchFilter
|
public class BaiduSearchFilter
|
||||||
{
|
{
|
||||||
[JsonPropertyName("range")]
|
[JsonPropertyName("range")] public BaiduSearchRange? Range { get; set; }
|
||||||
public BaiduSearchRange? Range { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -171,8 +175,7 @@ public class BaiduSearchFilter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchRange
|
public class BaiduSearchRange
|
||||||
{
|
{
|
||||||
[JsonPropertyName("page_time")]
|
[JsonPropertyName("page_time")] public BaiduSearchPageTime? PageTime { get; set; }
|
||||||
public BaiduSearchPageTime? PageTime { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -180,11 +183,9 @@ public class BaiduSearchRange
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchPageTime
|
public class BaiduSearchPageTime
|
||||||
{
|
{
|
||||||
[JsonPropertyName("gte")]
|
[JsonPropertyName("gte")] public string? Gte { get; set; }
|
||||||
public string? Gte { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("lte")]
|
[JsonPropertyName("lte")] public string? Lte { get; set; }
|
||||||
public string? Lte { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -192,11 +193,9 @@ public class BaiduSearchPageTime
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchMessage
|
public class BaiduSearchMessage
|
||||||
{
|
{
|
||||||
[JsonPropertyName("role")]
|
[JsonPropertyName("role")] public string Role { get; set; } = "user";
|
||||||
public string Role { get; set; } = "user";
|
|
||||||
|
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")] public string Content { get; set; } = "";
|
||||||
public string Content { get; set; } = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -204,11 +203,9 @@ public class BaiduSearchMessage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchResponse
|
public class BaiduSearchResponse
|
||||||
{
|
{
|
||||||
[JsonPropertyName("request_id")]
|
[JsonPropertyName("request_id")] public string? RequestId { get; set; }
|
||||||
public string? RequestId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("references")]
|
[JsonPropertyName("references")] public List<BaiduSearchReference>? References { get; set; }
|
||||||
public List<BaiduSearchReference>? References { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -216,23 +213,17 @@ public class BaiduSearchResponse
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaiduSearchReference
|
public class BaiduSearchReference
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")] public int Id { get; set; }
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("url")]
|
[JsonPropertyName("url")] public string? Url { get; set; }
|
||||||
public string? Url { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("title")]
|
[JsonPropertyName("title")] public string? Title { get; set; }
|
||||||
public string? Title { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("date")]
|
[JsonPropertyName("date")] public string? Date { get; set; }
|
||||||
public string? Date { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("snippet")]
|
[JsonPropertyName("snippet")] public string? Snippet { get; set; }
|
||||||
public string? Snippet { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("website")]
|
[JsonPropertyName("website")] public string? Website { get; set; }
|
||||||
public string? Website { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
3
Yi.Ai.Vue3/types/components.d.ts
vendored
3
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -20,6 +20,7 @@ declare module 'vue' {
|
|||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
|
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
@@ -34,8 +35,10 @@ declare module 'vue' {
|
|||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
|||||||
Reference in New Issue
Block a user