From 7563d2dd1975b4c1dbf487afcab0c3274e59dd51 Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 10 Aug 2026 22:36:56 -0400 Subject: [PATCH] fix(tts): surface provider response diagnostics (#555) --- .../vip/mate/tts/TtsResponseDiagnostics.java | 31 ++++++++++ .../tts/provider/DashScopeTtsProvider.java | 7 ++- .../mate/tts/provider/OpenAiTtsProvider.java | 4 +- .../mate/tts/TtsResponseDiagnosticsTest.java | 58 +++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/tts/TtsResponseDiagnostics.java create mode 100644 mateclaw-server/src/test/java/vip/mate/tts/TtsResponseDiagnosticsTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/tts/TtsResponseDiagnostics.java b/mateclaw-server/src/main/java/vip/mate/tts/TtsResponseDiagnostics.java new file mode 100644 index 00000000..b0209a59 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/tts/TtsResponseDiagnostics.java @@ -0,0 +1,31 @@ +package vip.mate.tts; + +/** + * Compact diagnostics for HTTP-based TTS provider failures. + */ +public final class TtsResponseDiagnostics { + + static final int MAX_SNIPPET_CHARS = 200; + + private TtsResponseDiagnostics() { + } + + public static String failureMessage(String provider, String endpoint, int status, String body) { + return provider + " 失败: endpoint=" + endpoint + + ", status=" + status + + ", body=" + snippet(body); + } + + public static String snippet(String body) { + if (body == null || body.isBlank()) { + return "(空响应体)"; + } + String collapsed = body.trim() + .replaceAll("(?i)Bearer\\s+[A-Za-z0-9._~+/=-]+", "Bearer [REDACTED]") + .replaceAll("\\s+", " "); + if (collapsed.length() <= MAX_SNIPPET_CHARS) { + return collapsed; + } + return collapsed.substring(0, MAX_SNIPPET_CHARS) + "..."; + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/tts/provider/DashScopeTtsProvider.java b/mateclaw-server/src/main/java/vip/mate/tts/provider/DashScopeTtsProvider.java index 5bfa1a34..0519a336 100644 --- a/mateclaw-server/src/main/java/vip/mate/tts/provider/DashScopeTtsProvider.java +++ b/mateclaw-server/src/main/java/vip/mate/tts/provider/DashScopeTtsProvider.java @@ -11,6 +11,7 @@ import vip.mate.llm.service.ModelProviderService; import vip.mate.system.model.SystemSettingsDTO; import vip.mate.tts.TtsProvider; import vip.mate.tts.TtsRequest; +import vip.mate.tts.TtsResponseDiagnostics; import vip.mate.tts.TtsResult; import java.util.List; @@ -101,7 +102,8 @@ public class DashScopeTtsProvider implements TtsProvider { body.put("speed", request.getSpeed()); } - HttpResponse response = HttpRequest.post(BASE_URL + "/audio/speech") + String endpoint = BASE_URL + "/audio/speech"; + HttpResponse response = HttpRequest.post(endpoint) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .body(body.toString()) @@ -115,7 +117,8 @@ public class DashScopeTtsProvider implements TtsProvider { } else { String errBody = response.body(); log.warn("[DashScope TTS] Failed: HTTP {} - {}", response.getStatus(), errBody); - return TtsResult.failure("DashScope TTS 失败: HTTP " + response.getStatus()); + return TtsResult.failure(TtsResponseDiagnostics.failureMessage( + "DashScope TTS", endpoint, response.getStatus(), errBody)); } } catch (Exception e) { log.error("[DashScope TTS] Error: {}", e.getMessage(), e); diff --git a/mateclaw-server/src/main/java/vip/mate/tts/provider/OpenAiTtsProvider.java b/mateclaw-server/src/main/java/vip/mate/tts/provider/OpenAiTtsProvider.java index 60a6984f..fb6ead6a 100644 --- a/mateclaw-server/src/main/java/vip/mate/tts/provider/OpenAiTtsProvider.java +++ b/mateclaw-server/src/main/java/vip/mate/tts/provider/OpenAiTtsProvider.java @@ -11,6 +11,7 @@ import vip.mate.llm.service.ModelProviderService; import vip.mate.system.model.SystemSettingsDTO; import vip.mate.tts.TtsProvider; import vip.mate.tts.TtsRequest; +import vip.mate.tts.TtsResponseDiagnostics; import vip.mate.tts.TtsResult; import java.util.List; @@ -112,7 +113,8 @@ public class OpenAiTtsProvider implements TtsProvider { } else { String errBody = response.body(); log.warn("[OpenAI TTS] Failed: HTTP {} - {}", response.getStatus(), errBody); - return TtsResult.failure("OpenAI TTS 失败: HTTP " + response.getStatus()); + return TtsResult.failure(TtsResponseDiagnostics.failureMessage( + "OpenAI TTS", url, response.getStatus(), errBody)); } } catch (Exception e) { log.error("[OpenAI TTS] Error: {}", e.getMessage(), e); diff --git a/mateclaw-server/src/test/java/vip/mate/tts/TtsResponseDiagnosticsTest.java b/mateclaw-server/src/test/java/vip/mate/tts/TtsResponseDiagnosticsTest.java new file mode 100644 index 00000000..485627cd --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/tts/TtsResponseDiagnosticsTest.java @@ -0,0 +1,58 @@ +package vip.mate.tts; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TtsResponseDiagnosticsTest { + + @Test + @DisplayName("failureMessage includes provider, endpoint, status and sanitized body snippet") + void failureMessageIncludesActionableDiagnostics() { + String message = TtsResponseDiagnostics.failureMessage( + "DashScope TTS", + "https://dashscope.aliyuncs.com/compatible-mode/v1/audio/speech", + 400, + """ + {"code":"InvalidParameter","message":"voice does not exist"} + """); + + assertTrue(message.contains("DashScope TTS 失败")); + assertTrue(message.contains("endpoint=https://dashscope.aliyuncs.com/compatible-mode/v1/audio/speech")); + assertTrue(message.contains("status=400")); + assertTrue(message.contains("body={\"code\":\"InvalidParameter\",\"message\":\"voice does not exist\"}")); + } + + @Test + @DisplayName("failureMessage truncates long response bodies") + void failureMessageTruncatesLongBodies() { + String body = "x".repeat(800); + + String message = TtsResponseDiagnostics.failureMessage( + "OpenAI TTS", + "https://api.openai.com/v1/audio/speech", + 500, + body); + + assertTrue(message.contains("OpenAI TTS 失败")); + assertTrue(message.contains("status=500")); + assertTrue(message.contains("body=")); + assertTrue(message.endsWith("...")); + assertTrue(message.length() < 420); + } + + @Test + @DisplayName("snippet redacts bearer tokens and collapses whitespace") + void snippetRedactsSecretsAndCollapsesWhitespace() { + String snippet = TtsResponseDiagnostics.snippet(""" + Authorization: Bearer sk-abcdef123456 + gateway failed + """); + + assertEquals("Authorization: Bearer [REDACTED] gateway failed", snippet); + assertFalse(snippet.contains("sk-abcdef")); + } +}