fix(tts): surface provider response diagnostics (#555)

This commit is contained in:
matevip 2026-08-10 22:36:56 -04:00
parent b196dc73fd
commit 7563d2dd19
4 changed files with 97 additions and 3 deletions

View File

@ -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) + "...";
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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"));
}
}