Fix #43: Benutzerfreundliche Fehlermeldungen bei FAILED-Einträgen im Detailbereich
Jeder Fehlertyp erhält jetzt eine präzise deutsche Meldung: - Kein lesbarer Text (NO_USABLE_TEXT) → OCR-Hinweis - Titel zu lang → Titeltext + tatsächliche Länge + Limit - Defekte/nicht extrahierbare PDF → Beschädigungshinweis - Verbindungsfehler/Timeout → Verbindungs- und Konfigurationshinweis - Unbekannter Fehler → neutraler Fallback ohne Log-Verweis Der Verweis auf "Details im Anwendungslog" wurde vollständig entfernt. Das "Fehler:"-Präfix in buildDetailText() entfällt; bei vorhandener Fehlermeldung wird NO_REASONING_TEXT unterdrückt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+73
-10
@@ -1,13 +1,13 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Übersetzt technische KI-Fehlermeldungen in benutzerfreundliche deutsche Texte.
|
* Übersetzt strukturierte Fehlermeldungen aus der Anwendungsschicht in
|
||||||
|
* benutzerfreundliche deutsche Texte für den Detailbereich des Verarbeitungslauf-Tabs.
|
||||||
* <p>
|
* <p>
|
||||||
* Die Klasse wertet die rohe, englischsprachige Fehlermeldung aus dem
|
* Die Klasse wertet die englischsprachige Fehlermeldung aus dem Verarbeitungsversuch
|
||||||
* Verarbeitungsversuch aus und liefert eine für den Endbenutzer lesbare
|
* musterbasiert aus und liefert eine für den Endbenutzer lesbare Beschreibung des
|
||||||
* Beschreibung des Fehlergrunds. Das ursprüngliche Datenmodell bleibt
|
* Fehlergrunds. Das ursprüngliche Datenmodell bleibt unverändert; die Übersetzung
|
||||||
* unverändert; die Übersetzung findet ausschließlich in der Darstellungsschicht
|
* findet ausschließlich in der Darstellungsschicht statt.
|
||||||
* statt.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Die Mustererkennung erfolgt ohne Berücksichtigung der Groß-/Kleinschreibung
|
* Die Mustererkennung erfolgt ohne Berücksichtigung der Groß-/Kleinschreibung
|
||||||
* und prüft die definierten Schlüsselbegriffe in festgelegter Reihenfolge,
|
* und prüft die definierten Schlüsselbegriffe in festgelegter Reihenfolge,
|
||||||
@@ -26,14 +26,35 @@ final class AiFailureMessageTranslator {
|
|||||||
* Fallback-Text zurückgegeben.
|
* Fallback-Text zurückgegeben.
|
||||||
*
|
*
|
||||||
* @param technicalMessage die rohe technische Fehlermeldung; darf {@code null} sein
|
* @param technicalMessage die rohe technische Fehlermeldung; darf {@code null} sein
|
||||||
* @return eine nicht-leere deutsche Benutzerfehlermeldung
|
* @return eine nicht-leere deutsche Benutzerfehlermeldung ohne führendes Warnsymbol
|
||||||
*/
|
*/
|
||||||
static String translate(String technicalMessage) {
|
static String translate(String technicalMessage) {
|
||||||
if (technicalMessage == null || technicalMessage.isBlank()) {
|
if (technicalMessage == null || technicalMessage.isBlank()) {
|
||||||
return "KI-Aufruf fehlgeschlagen. Details im Anwendungslog.";
|
return "Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.";
|
||||||
}
|
}
|
||||||
String lower = technicalMessage.toLowerCase(java.util.Locale.ROOT);
|
String lower = technicalMessage.toLowerCase(java.util.Locale.ROOT);
|
||||||
|
|
||||||
|
// Pre-Check-Fehler: kein lesbarer Text im PDF
|
||||||
|
if (lower.contains("no usable text")) {
|
||||||
|
return "PDF enthält keinen lesbaren Text. Möglicherweise handelt es sich um einen Scan"
|
||||||
|
+ " ohne Texterkennung (OCR). Eine automatische Benennung ist nicht möglich.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// KI-Validierungsfehler: Titel überschreitet die konfigurierte Maximallänge
|
||||||
|
if (lower.contains("title exceeds")) {
|
||||||
|
return buildTitleExceedsMessage(technicalMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defekte oder strukturell nicht lesbare PDF-Datei
|
||||||
|
if (lower.contains("content not extractable")
|
||||||
|
|| lower.contains("ioexception")
|
||||||
|
|| lower.contains("end of file")
|
||||||
|
|| lower.contains("endoffileexception")
|
||||||
|
|| lower.contains("eof")) {
|
||||||
|
return "Die PDF-Datei ist ungültig oder beschädigt und kann nicht verarbeitet werden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP-Authentifizierungsfehler
|
||||||
if (lower.contains("http_401")) {
|
if (lower.contains("http_401")) {
|
||||||
return "KI-Dienst: Ungültiger API-Schlüssel. Bitte in den Einstellungen prüfen.";
|
return "KI-Dienst: Ungültiger API-Schlüssel. Bitte in den Einstellungen prüfen.";
|
||||||
}
|
}
|
||||||
@@ -46,9 +67,51 @@ final class AiFailureMessageTranslator {
|
|||||||
if (lower.contains("http_5")) {
|
if (lower.contains("http_5")) {
|
||||||
return "KI-Dienst vorübergehend nicht erreichbar. Bitte später erneut versuchen.";
|
return "KI-Dienst vorübergehend nicht erreichbar. Bitte später erneut versuchen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Netzwerk- und Verbindungsfehler
|
||||||
if (lower.contains("connection") || lower.contains("timeout") || lower.contains("refused")) {
|
if (lower.contains("connection") || lower.contains("timeout") || lower.contains("refused")) {
|
||||||
return "KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.";
|
return "KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.";
|
||||||
}
|
}
|
||||||
return "KI-Aufruf fehlgeschlagen. Details im Anwendungslog.";
|
|
||||||
|
return "Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut aus einer „Title exceeds"-Fehlermeldung einen benutzerfreundlichen Text,
|
||||||
|
* der Titel, tatsächliche Länge und konfiguriertes Limit nennt.
|
||||||
|
* <p>
|
||||||
|
* Erwartet wird das Format:
|
||||||
|
* {@code … Title exceeds N characters (base title): 'Titel' …}
|
||||||
|
* <p>
|
||||||
|
* Kann das Format nicht geparst werden, wird ein generischer Hinweis zurückgegeben.
|
||||||
|
*
|
||||||
|
* @param technicalMessage die vollständige technische Fehlermeldung
|
||||||
|
* @return benutzerfreundlicher Hinweis auf den zu langen Titel
|
||||||
|
*/
|
||||||
|
private static String buildTitleExceedsMessage(String technicalMessage) {
|
||||||
|
try {
|
||||||
|
int exceedsIdx = technicalMessage.indexOf("Title exceeds ");
|
||||||
|
if (exceedsIdx >= 0) {
|
||||||
|
String afterExceeds = technicalMessage.substring(exceedsIdx + "Title exceeds ".length());
|
||||||
|
int charIdx = afterExceeds.indexOf(" characters");
|
||||||
|
if (charIdx > 0) {
|
||||||
|
int limit = Integer.parseInt(afterExceeds.substring(0, charIdx).trim());
|
||||||
|
int colonQuote = technicalMessage.indexOf(": '", exceedsIdx);
|
||||||
|
if (colonQuote >= 0) {
|
||||||
|
String afterQuote = technicalMessage.substring(colonQuote + 3);
|
||||||
|
int closingQuote = afterQuote.lastIndexOf("'");
|
||||||
|
if (closingQuote > 0) {
|
||||||
|
String title = afterQuote.substring(0, closingQuote);
|
||||||
|
return "KI-Vorschlag abgelehnt: '" + title + "' ist zu lang ("
|
||||||
|
+ title.length() + " Zeichen, Limit: " + limit
|
||||||
|
+ "). Bitte Dateinamen manuell kürzen.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException | StringIndexOutOfBoundsException ignored) {
|
||||||
|
// Fallback unten
|
||||||
|
}
|
||||||
|
return "KI-Vorschlag abgelehnt: Titel überschreitet die maximale Länge. Bitte Dateinamen manuell kürzen.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -1277,13 +1277,9 @@ public final class GuiBatchRunTab {
|
|||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
row.aiReasoning().ifPresentOrElse(
|
row.aiReasoning().ifPresentOrElse(
|
||||||
reasoning -> builder.append(reasoning),
|
reasoning -> builder.append(reasoning),
|
||||||
() -> {
|
() -> row.aiFailureMessage().ifPresentOrElse(
|
||||||
row.aiFailureMessage().ifPresent(msg ->
|
msg -> builder.append("\u26A0 ").append(AiFailureMessageTranslator.translate(msg)),
|
||||||
builder.append("\u26A0 Fehler: ")
|
() -> builder.append(NO_REASONING_TEXT)));
|
||||||
.append(AiFailureMessageTranslator.translate(msg))
|
|
||||||
.append("\n\n"));
|
|
||||||
builder.append(NO_REASONING_TEXT);
|
|
||||||
});
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+112
-15
@@ -1,6 +1,7 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@@ -10,10 +11,91 @@ import org.junit.jupiter.api.Test;
|
|||||||
* Unit-Tests für {@link AiFailureMessageTranslator}.
|
* Unit-Tests für {@link AiFailureMessageTranslator}.
|
||||||
* <p>
|
* <p>
|
||||||
* Prüft alle definierten Mapping-Fälle sowie Randbedingungen wie
|
* Prüft alle definierten Mapping-Fälle sowie Randbedingungen wie
|
||||||
* {@code null}-Eingaben und unbekannte Fehlertexte.
|
* {@code null}-Eingaben und unbekannte Fehlertexte. Die zurückgegebenen Meldungen
|
||||||
|
* dürfen keinen Verweis auf interne Logdateien enthalten.
|
||||||
*/
|
*/
|
||||||
class AiFailureMessageTranslatorTest {
|
class AiFailureMessageTranslatorTest {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pre-Check-Fehler: kein lesbarer Text
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noUsableText_liefertLesbarkeitsMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Processing failed (retryable). Reason: No usable text in extracted PDF content");
|
||||||
|
assertTrue(result.contains("keinen lesbaren Text"), result);
|
||||||
|
assertTrue(result.contains("OCR"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noUsableText_grossKleinschreibung_wirdErkannt() {
|
||||||
|
String result = AiFailureMessageTranslator.translate("NO USABLE TEXT found");
|
||||||
|
assertTrue(result.contains("keinen lesbaren Text"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// KI-Validierungsfehler: Titel zu lang
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleExceeds_liefertTitelZuLangMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Processing failed finally (not retryable). AI functional error: "
|
||||||
|
+ "Title exceeds 60 characters (base title): 'Sehr langer Titel der das Limit ueberschreitet'");
|
||||||
|
assertTrue(result.contains("ist zu lang"), result);
|
||||||
|
assertTrue(result.contains("Sehr langer Titel der das Limit ueberschreitet"), result);
|
||||||
|
assertTrue(result.contains("Limit: 60"), result);
|
||||||
|
assertTrue(result.contains("manuell kürzen"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleExceeds_mitTitellaengeInMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Title exceeds 60 characters (base title): 'Ein langer Dokumenttitel'");
|
||||||
|
assertTrue(result.contains("Ein langer Dokumenttitel"), result);
|
||||||
|
assertTrue(result.contains("Limit: 60"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleExceeds_ohneParsebaresFormat_liefertGenerischenHinweis() {
|
||||||
|
String result = AiFailureMessageTranslator.translate("title exceeds some limit");
|
||||||
|
assertTrue(result.contains("Titel"), result);
|
||||||
|
assertTrue(result.contains("manuell kürzen"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Defekte oder strukturell nicht lesbare PDF-Datei
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contentNotExtractable_liefertDefektePdfMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Processing failed (retryable). Reason: PDF content not extractable");
|
||||||
|
assertTrue(result.contains("ungültig oder beschädigt"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ioException_liefertDefektePdfMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Processing failed (retryable). Technical: IOException reading file: test.pdf");
|
||||||
|
assertTrue(result.contains("ungültig oder beschädigt"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endOfFile_liefertDefektePdfMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Processing failed (retryable). Technical: Unexpected end of file");
|
||||||
|
assertTrue(result.contains("ungültig oder beschädigt"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void eof_liefertDefektePdfMeldung() {
|
||||||
|
String result = AiFailureMessageTranslator.translate(
|
||||||
|
"Technical error: EOF while reading PDF structure");
|
||||||
|
assertTrue(result.contains("ungültig oder beschädigt"), result);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// HTTP-Statuscodes
|
// HTTP-Statuscodes
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -56,59 +138,74 @@ class AiFailureMessageTranslatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Verbindungsfehler
|
// Netzwerk- und Verbindungsfehler
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void connectionFehler_liefertNetzwerkMeldung() {
|
void connectionFehler_liefertVerbindungsMeldung() {
|
||||||
String result = AiFailureMessageTranslator.translate("Connection refused to https://api.example.com");
|
String result = AiFailureMessageTranslator.translate("Connection refused to https://api.example.com");
|
||||||
assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result);
|
assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void timeoutFehler_liefertNetzwerkMeldung() {
|
void timeoutFehler_liefertVerbindungsMeldung() {
|
||||||
String result = AiFailureMessageTranslator.translate("AI technical error: timeout after 30s");
|
String result = AiFailureMessageTranslator.translate("AI technical error: timeout after 30s");
|
||||||
assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result);
|
assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void refusedFehler_liefertNetzwerkMeldung() {
|
void refusedFehler_liefertVerbindungsMeldung() {
|
||||||
String result = AiFailureMessageTranslator.translate("connect refused");
|
String result = AiFailureMessageTranslator.translate("connect refused");
|
||||||
assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result);
|
assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Fallback
|
// Unbekannter Fehler (Fallback)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void unbekannterFehler_liefertFallback() {
|
void unbekannterFehler_liefertFallback() {
|
||||||
String result = AiFailureMessageTranslator.translate("some completely unknown error text");
|
String result = AiFailureMessageTranslator.translate("some completely unknown error text");
|
||||||
assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result);
|
assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void null_liefertFallback() {
|
void null_liefertFallback() {
|
||||||
String result = AiFailureMessageTranslator.translate(null);
|
String result = AiFailureMessageTranslator.translate(null);
|
||||||
assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result);
|
assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void leerstring_liefertFallback() {
|
void leerstring_liefertFallback() {
|
||||||
String result = AiFailureMessageTranslator.translate("");
|
String result = AiFailureMessageTranslator.translate("");
|
||||||
assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result);
|
assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void nurLeerzeichen_liefertFallback() {
|
void nurLeerzeichen_liefertFallback() {
|
||||||
String result = AiFailureMessageTranslator.translate(" ");
|
String result = AiFailureMessageTranslator.translate(" ");
|
||||||
assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result);
|
assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Allgemeine Qualität der Rückgabe
|
// Qualitätsregeln: kein Verweis auf interne Logs
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keineMeldungEnthältAnwendungslogVerweis() {
|
||||||
|
String[] eingaben = {
|
||||||
|
null, "", " ", "HTTP_401", "HTTP_403", "HTTP_429", "HTTP_500",
|
||||||
|
"connection error", "timeout", "refused", "unbekannt",
|
||||||
|
"no usable text", "content not extractable", "IOException",
|
||||||
|
"Title exceeds 60 characters (base title): 'Titel'"
|
||||||
|
};
|
||||||
|
for (String eingabe : eingaben) {
|
||||||
|
String result = AiFailureMessageTranslator.translate(eingabe);
|
||||||
|
assertFalse(result.toLowerCase().contains("anwendungslog"),
|
||||||
|
"Meldung darf keinen Anwendungslog-Verweis enthalten für: " + eingabe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void alleEingaben_liefernNichtLeereTexte() {
|
void alleEingaben_liefernNichtLeereTexte() {
|
||||||
String[] eingaben = {
|
String[] eingaben = {
|
||||||
@@ -118,7 +215,7 @@ class AiFailureMessageTranslatorTest {
|
|||||||
for (String eingabe : eingaben) {
|
for (String eingabe : eingaben) {
|
||||||
String result = AiFailureMessageTranslator.translate(eingabe);
|
String result = AiFailureMessageTranslator.translate(eingabe);
|
||||||
assertNotNull(result, "Ergebnis darf nicht null sein für: " + eingabe);
|
assertNotNull(result, "Ergebnis darf nicht null sein für: " + eingabe);
|
||||||
assertTrue(!result.isBlank(), "Ergebnis darf nicht leer sein für: " + eingabe);
|
assertFalse(result.isBlank(), "Ergebnis darf nicht leer sein für: " + eingabe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user