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(); if (!string.IsNullOrEmpty(request.SystemPrompt)) { messages.Add(new { role = "system", content = request.SystemPrompt }); } foreach (var msg in request.Messages) { messages.Add(new { role = msg.Role, content = msg.Content }); } return new { model = request.Model, messages = messages, max_tokens = request.MaxTokens, temperature = request.Temperature, stream = stream }; } /// /// 解析聊天响应 /// private ChatResponse ParseChatResponse(string json) { using var document = JsonDocument.Parse(json); var root = document.RootElement; var response = new ChatResponse { RawJson = json, IsSuccess = true }; if (root.TryGetProperty("id", out var id)) response.Id = id.GetString(); if (root.TryGetProperty("model", out var model)) response.Model = model.GetString(); if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) { var choice = choices[0]; if (choice.TryGetProperty("message", out var message)) { if (message.TryGetProperty("content", out var content)) response.Content = content.GetString(); } if (choice.TryGetProperty("finish_reason", out var finishReason)) { // 检查是否被内容安全过滤 if (finishReason.GetString() == "content_filter") { response.IsSuccess = false; response.ErrorMessage = "内容被安全策略过滤"; } } } if (root.TryGetProperty("usage", out var usage)) { response.Usage = new TokenUsage { PromptTokens = usage.TryGetProperty("prompt_tokens", out var pt) ? pt.GetInt32() : 0, CompletionTokens = usage.TryGetProperty("completion_tokens", out var ct) ? ct.GetInt32() : 0, TotalTokens = usage.TryGetProperty("total_tokens", out var tt) ? tt.GetInt32() : 0 }; } return response; } /// /// 解析流式数据块 /// private StreamChunk? ParseStreamChunk(string data) { using var document = JsonDocument.Parse(data); var root = document.RootElement; var chunk = new StreamChunk(); if (root.TryGetProperty("model", out var model)) chunk.Model = model.GetString(); if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) { var choice = choices[0]; if (choice.TryGetProperty("delta", out var delta)) { if (delta.TryGetProperty("content", out var content)) { chunk.Content = content.GetString() ?? string.Empty; } } // 检查是否结束 if (choice.TryGetProperty("finish_reason", out var finishReason)) { var reason = finishReason.GetString(); chunk.IsDone = reason == "stop" || reason == "content_filter"; if (reason == "content_filter") { chunk.Content = "[内容被安全策略过滤]"; } } } return chunk; } /// /// 从错误响应创建异常 /// private TokenHubException CreateExceptionFromResponse(HttpStatusCode statusCode, string responseJson) { var statusCodeInt = (int)statusCode; var errorDetail = ParseErrorResponse(responseJson, statusCodeInt); var message = errorDetail?.Message ?? $"HTTP {(int)statusCode}: 请求失败"; _logger.LogWarning("TokenHub API 返回错误: StatusCode={StatusCode}, Code={Code}, Type={Type}, RequestId={RequestId}", statusCodeInt, errorDetail?.Code, errorDetail?.Type, errorDetail?.RequestId); return new TokenHubException(message, statusCodeInt, errorDetail); } /// /// 解析错误响应 /// private TokenHubError? ParseErrorResponse(string responseJson, int statusCode) { try { using var document = JsonDocument.Parse(responseJson); var root = document.RootElement; // 判断是 OpenAI 协议还是 Anthropic 协议 if (root.TryGetProperty("error", out var errorElement)) { // OpenAI 协议 if (errorElement.ValueKind == JsonValueKind.Object) { return ParseOpenAiError(errorElement); } } // Anthropic 协议 if (root.TryGetProperty("type", out var typeProp) && typeProp.GetString() == "error") { if (root.TryGetProperty("error", out var anthropicError)) { return ParseAnthropicError(anthropicError); } } // 简化格式(链路短路等) if (root.TryGetProperty("error", out var simpleError) && simpleError.ValueKind == JsonValueKind.Object) { return new TokenHubError { Type = simpleError.TryGetProperty("type", out var t) ? t.GetString() : null, Code = simpleError.TryGetProperty("code", out var c) ? c.GetString() : null, Message = simpleError.TryGetProperty("message", out var m) ? m.GetString() : null, Message_ZH = simpleError.TryGetProperty("message_zh", out var m_zh) ? m_zh.GetString() : null, RequestId = simpleError.TryGetProperty("request_id", out var r) ? r.GetString() : null }; } return null; } catch (System.Text.Json.JsonException ex) { _logger.LogWarning(ex, "解析错误响应失败: {ResponseJson}", responseJson); return null; } } /// /// 解析 OpenAI 协议错误 /// private TokenHubError ParseOpenAiError(JsonElement errorElement) { var error = new TokenHubError(); if (errorElement.TryGetProperty("type", out var type)) error.Type = type.GetString(); if (errorElement.TryGetProperty("code", out var code)) error.Code = code.GetString(); if (errorElement.TryGetProperty("message", out var message)) error.Message = message.GetString(); if (errorElement.TryGetProperty("message_zh", out var message_zh)) error.Message_ZH = message_zh.GetString(); if (errorElement.TryGetProperty("source", out var source)) error.Source = source.GetString(); if (errorElement.TryGetProperty("upstream_code", out var upstreamCode)) error.UpstreamCode = upstreamCode.GetString(); if (errorElement.TryGetProperty("upstream_status", out var upstreamStatus)) error.UpstreamStatus = upstreamStatus.GetInt32(); if (errorElement.TryGetProperty("request_id", out var requestId)) error.RequestId = requestId.GetString(); return error; } /// /// 解析 Anthropic 协议错误 /// private TokenHubError ParseAnthropicError(JsonElement errorElement) { var error = new TokenHubError { Source = "upstream" // Anthropic 协议默认视为上游 }; if (errorElement.TryGetProperty("type", out var type)) error.Type = type.GetString(); if (errorElement.TryGetProperty("message", out var message)) error.Message = message.GetString(); if (errorElement.TryGetProperty("message_zh", out var message_zh)) error.Message_ZH = message_zh.GetString(); if (errorElement.TryGetProperty("reqid", out var reqid)) error.RequestId = reqid.GetString(); // Anthropic 协议没有业务码,根据类型映射 error.Code = MapAnthropicTypeToCode(error.Type); return error; } /// /// 将 Anthropic 错误类型映射到业务码 /// private string? MapAnthropicTypeToCode(string? type) { return type switch { "invalid_request_error" => "400002", "authentication_error" => "401002", "permission_error" => "403001", "not_found_error" => "400004", "rate_limit_error" => "429001", "overloaded_error" => "502001", "api_error" => "500001", _ => null }; } /// /// 从响应中提取 Request ID /// private string? ExtractRequestId(string responseJson) { try { using var document = JsonDocument.Parse(responseJson); var root = document.RootElement; if (root.TryGetProperty("error", out var error)) { if (error.TryGetProperty("request_id", out var requestId)) return requestId.GetString(); } if (root.TryGetProperty("id", out var id)) return id.GetString(); } catch { // 忽略解析错误 } return null; } #endregion /// /// 释放资源 /// public void Dispose() { _httpClient?.Dispose(); GC.SuppressFinalize(this); } }