using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging.Abstractions;
using OASystem.API.OAMethodLib.TokenHubAI.Models;
using System.Net;
using System.Text.Json;
namespace OASystem.API.OAMethodLib.TokenHubAI;
///
/// TokenHub AI 服务实现
///
public class TokenHubService : ITokenHubService, IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
private readonly string _baseUrl;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger _logger;
// 重试配置
private int _maxRetries = 3;
private TimeSpan _initialRetryDelay = TimeSpan.FromSeconds(1);
private TimeSpan _maxRetryDelay = TimeSpan.FromSeconds(30);
public TokenHubService(
string apiKey,
string? baseUrl = null,
ILogger? logger = null)
{
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentException("API Key 不能为空", nameof(apiKey));
_apiKey = apiKey;
_baseUrl = baseUrl ?? "https://tokenhub.tencentmaas.com/v1/chat/completions";
_logger = logger ?? NullLogger.Instance;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
_httpClient.Timeout = TimeSpan.FromSeconds(120);
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
}
///
/// 发送聊天请求(非流式)
///
public async Task ChatAsync(ChatRequest request, CancellationToken cancellationToken = default)
{
try
{
return await SendChatRequestAsync(request, cancellationToken);
}
catch (TokenHubException ex)
{
_logger.LogWarning(ex, ex.ErrorDetail?.Message ?? "-");
var message = ex.ErrorDetail?.Message_ZH
?? TokenHubErrorExtensions.GetMessage(ex.ErrorCode);
throw new TokenHubException(message, 400, ex.ErrorDetail);
}
catch (TaskCanceledException ex) when (ex.CancellationToken == cancellationToken)
{
_logger.LogWarning(ex, "请求被取消");
throw new TokenHubException("请求已被取消", 499, new TokenHubError
{
Type = "server_error",
Code = "499001",
Message = "请求已被客户端取消,请检查客户端连接状态"
});
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, "请求超时");
throw new TokenHubException("请求超时", 504, new TokenHubError
{
Type = "timeout_error",
Code = "504001",
Message = "服务响应超时,请稍后重试"
});
}
catch (Exception ex) when (ex is not TokenHubException)
{
_logger.LogError(ex, "发送请求时发生未预期错误");
throw new TokenHubException($"请求失败: {ex.Message}", 500, new TokenHubError
{
Type = "server_error",
Code = "500001",
Message = "服务内部发生错误,请稍后重试或联系技术支持"
});
}
}
///
/// 发送聊天请求(流式)
///
public async Task StreamChatAsync(ChatRequest request, Action onChunk, CancellationToken cancellationToken = default)
{
if (onChunk == null)
throw new ArgumentNullException(nameof(onChunk));
_httpClient.DefaultRequestHeaders.Remove("Accept");
_httpClient.DefaultRequestHeaders.Add("Accept", "text/event-stream");
try
{
await SendStreamRequestAsync(request, onChunk, cancellationToken);
}
catch (TokenHubException ex)
{
var message = ex.ErrorDetail?.Message_ZH
?? TokenHubErrorExtensions.GetMessage(ex.ErrorCode);
throw new TokenHubException(message, 400, ex.ErrorDetail);
}
catch (TaskCanceledException ex) when (ex.CancellationToken == cancellationToken)
{
_logger.LogWarning(ex, "流式请求被取消");
throw new TokenHubException("请求已被取消", 499, new TokenHubError
{
Type = "server_error",
Code = "499001",
Message = "request was canceled"
});
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, "流式请求超时");
throw new TokenHubException("请求超时", 504, new TokenHubError
{
Type = "timeout_error",
Code = "504001",
Message = "gateway timeout"
});
}
catch (Exception ex) when (ex is not TokenHubException)
{
_logger.LogError(ex, "发送流式请求时发生未预期错误");
throw new TokenHubException($"请求失败: {ex.Message}", 500, new TokenHubError
{
Type = "server_error",
Code = "500001",
Message = ex.Message
});
}
finally
{
_httpClient.DefaultRequestHeaders.Remove("Accept");
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
}
///
/// 带重试的聊天请求
///
public async Task ChatWithRetryAsync(ChatRequest request, int maxRetries = 3, CancellationToken cancellationToken = default)
{
var retryCount = 0;
var delay = _initialRetryDelay;
while (true)
{
try
{
return await ChatAsync(request, cancellationToken);
}
catch (TokenHubException ex) when (ex.IsRetryable && retryCount < maxRetries)
{
retryCount++;
_logger.LogWarning(ex,
"请求失败 (尝试 {RetryCount}/{MaxRetries}),将在 {DelayMs}ms 后重试",
retryCount, maxRetries, delay.TotalMilliseconds);
await Task.Delay(delay, cancellationToken);
// 指数退避
delay = TimeSpan.FromSeconds(Math.Min(
delay.TotalSeconds * 2,
_maxRetryDelay.TotalSeconds
));
}
catch (TokenHubException ex) when (ex.ErrorCode == "429001" || ex.ErrorCode == "429002")
{
// 限流错误,使用更长的退避时间
retryCount++;
var retryAfter = TimeSpan.FromSeconds(Math.Min(
delay.TotalSeconds * 3,
_maxRetryDelay.TotalSeconds
));
_logger.LogWarning(ex,
"触发限流 (尝试 {RetryCount}/{MaxRetries}),将在 {DelayMs}ms 后重试",
retryCount, maxRetries, retryAfter.TotalMilliseconds);
await Task.Delay(retryAfter, cancellationToken);
delay = retryAfter;
}
catch (TokenHubException)
{
// 不可重试的错误直接抛出
throw;
}
}
}
///
/// 获取可用模型列表
///
public async Task> GetAvailableModelsAsync(CancellationToken cancellationToken = default)
{
// TokenHub 当前没有公开的模型列表 API
return await Task.FromResult(new List
{
"hunyuan-2.0-instruct-20251111"
});
}
///
/// 设置超时时间
///
public void SetTimeout(TimeSpan timeout)
{
_httpClient.Timeout = timeout;
}
///
/// 设置重试策略
///
public void SetRetryPolicy(int maxRetries, TimeSpan? initialDelay = null, TimeSpan? maxDelay = null)
{
_maxRetries = maxRetries;
if (initialDelay.HasValue)
_initialRetryDelay = initialDelay.Value;
if (maxDelay.HasValue)
_maxRetryDelay = maxDelay.Value;
}
#region 私有方法
///
/// 发送非流式请求
///
private async Task SendChatRequestAsync(ChatRequest request, CancellationToken cancellationToken)
{
var requestBody = BuildRequestBody(request, stream: false);
var json = System.Text.Json.JsonSerializer.Serialize(requestBody, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_logger.LogDebug("发送请求到 TokenHub: Model={Model}, Messages={MessageCount}",
request.Model, request.Messages.Count);
var httpResponse = await _httpClient.PostAsync(_baseUrl, content, cancellationToken);
var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
_logger.LogDebug("收到响应: StatusCode={StatusCode}, RequestId={RequestId}",
httpResponse.StatusCode,
ExtractRequestId(responseJson));
if (!httpResponse.IsSuccessStatusCode)
{
throw CreateExceptionFromResponse(httpResponse.StatusCode, responseJson);
}
return ParseChatResponse(responseJson);
}
///
/// 发送流式请求
///
private async Task SendStreamRequestAsync(
ChatRequest request,
Action onChunk,
CancellationToken cancellationToken)
{
var requestBody = BuildRequestBody(request, stream: true);
var json = System.Text.Json.JsonSerializer.Serialize(requestBody, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
_logger.LogDebug("发送流式请求到 TokenHub: Model={Model}", request.Model);
var httpResponse = await _httpClient.PostAsync(_baseUrl, content, cancellationToken);
if (!httpResponse.IsSuccessStatusCode)
{
var errorJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
throw CreateExceptionFromResponse(httpResponse.StatusCode, errorJson);
}
using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: "))
continue;
var data = line.Substring(6);
if (data == "[DONE]")
{
onChunk(new StreamChunk { IsDone = true });
break;
}
try
{
var chunk = ParseStreamChunk(data);
if (chunk != null)
{
onChunk(chunk);
if (chunk.IsDone)
break;
}
}
catch (System.Text.Json.JsonException ex)
{
_logger.LogWarning(ex, "解析流式数据失败: {Data}", data);
// 跳过无法解析的数据
}
}
}
///
/// 构建请求体
///
private object BuildRequestBody(ChatRequest request, bool stream)
{
var messages = new List