GUI-Bugfixes: Defaults beim Start, kopierbare Meldungen mit Zeitstempel, Befundauflistung, Modell-ComboBox links, effektiver API-Key für Modellabruf

- Blank-Startzustand zeigt jetzt die Standardvorlage (wie nach "Neu"), neue Factory createEmptyStartState für Tests
- Meldungsbereich ist per Kontextmenü bzw. Strg+C kopierbar
- Jede Meldung trägt ein führendes [HH:mm:ss]-Präfix
- Validieren- und Tests-Aktionen akkumulieren Meldungen, automatische Validierung ersetzt still ihre Einträge
- Validieren-Meldung listet alle konkreten Befunde einzeln auf
- Modell-ComboBox und manuelles Modellfeld sind linksbündig
- ApiKeyResolutionPort liefert jetzt den effektiven API-Schlüsselwert (Default + Env-Adapter-Override), so dass der Modellliste-Test in den technischen Tests nicht mehr "API-Schlüssel fehlt" meldet, obwohl er gesetzt ist
This commit is contained in:
2026-04-21 16:04:15 +02:00
parent 6babdd226e
commit ada7e203e3
18 changed files with 471 additions and 204 deletions
@@ -1,5 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.validation.editor;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
@@ -22,6 +24,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
* <p>
* Implementierungen dieses Ports liegen im Adapter-Out-Modul.
*/
@FunctionalInterface
public interface ApiKeyResolutionPort {
/**
@@ -37,4 +40,32 @@ public interface ApiKeyResolutionPort {
* @return der Descriptor für die effektive Schlüsselherkunft; nie {@code null}
*/
EffectiveApiKeyDescriptor resolve(AiProviderFamily family, String propertyValue);
/**
* Liefert den effektiven API-Schlüssel-Rohwert anhand derselben Vorrangregel wie {@link #resolve}.
* <p>
* Dient technischen Tests wie dem Modellkatalogabruf, die den tatsächlichen Schlüssel im
* HTTP-Header benötigen. Die Herkunft selbst wird nicht mit zurückgegeben dafür ist
* {@link #resolve} zuständig.
* <p>
* Diese Default-Implementierung deckt den Fall ab, in dem ein Adapter ausschließlich die
* Property-Datei kennt: liefert {@link #resolve} {@code ABSENT}, wird ein leerer Optional
* zurückgegeben; andernfalls wird der nicht-leere Property-Wert geliefert. Adapter, die
* Umgebungsvariablen lesen, müssen diese Methode überschreiben, damit der ENV-Wert auch
* tatsächlich an HTTP-Aufrufer durchgereicht wird.
*
* @param family die Provider-Familie; darf nicht {@code null} sein
* @param propertyValue aktueller Property-Wert aus dem Editor (kann leer sein); darf nicht {@code null} sein
* @return der effektive Schlüssel-Rohwert, falls eine der Quellen einen Wert liefert; sonst leer
*/
default Optional<String> resolveEffectiveApiKeyValue(AiProviderFamily family, String propertyValue) {
EffectiveApiKeyDescriptor descriptor = resolve(family, propertyValue);
if (descriptor.isAbsent()) {
return Optional.empty();
}
if (propertyValue != null && !propertyValue.isBlank()) {
return Optional.of(propertyValue);
}
return Optional.empty();
}
}
@@ -28,10 +28,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
* @param claudeModel Rohtextwert des Claude-Modellnamens
* @param claudeTimeoutSeconds Rohtextwert des Claude-Timeouts
* @param claudeApiKeyDescriptor API-Key-Herkunft für den Claude-Provider; nie {@code null}
* @param claudeApiKeyPropertyValue Rohwert des Claude-API-Schlüssels aus der Properties-Datei
* @param openaiBaseUrl Rohtextwert der OpenAI-kompatiblen Basis-URL
* @param openaiModel Rohtextwert des OpenAI-kompatiblen Modellnamens
* @param openaiTimeoutSeconds Rohtextwert des OpenAI-kompatiblen Timeouts
* @param openaiApiKeyDescriptor API-Key-Herkunft für den OpenAI-kompatiblen Provider; nie {@code null}
* @param openaiApiKeyPropertyValue Rohwert des OpenAI-API-Schlüssels aus der Properties-Datei
*/
public record EditorValidationInput(
String activeProviderIdentifier,
@@ -46,10 +48,12 @@ public record EditorValidationInput(
String claudeModel,
String claudeTimeoutSeconds,
EffectiveApiKeyDescriptor claudeApiKeyDescriptor,
String claudeApiKeyPropertyValue,
String openaiBaseUrl,
String openaiModel,
String openaiTimeoutSeconds,
EffectiveApiKeyDescriptor openaiApiKeyDescriptor) {
EffectiveApiKeyDescriptor openaiApiKeyDescriptor,
String openaiApiKeyPropertyValue) {
/**
* Erstellt eine neue Eingabe für den Validator.
@@ -66,10 +70,12 @@ public record EditorValidationInput(
* @param claudeModel Claude-Modellname; {@code null} wird zu leerem String
* @param claudeTimeoutSeconds Claude-Timeout; {@code null} wird zu leerem String
* @param claudeApiKeyDescriptor Claude-API-Key-Herkunft; darf nicht {@code null} sein
* @param claudeApiKeyPropertyValue Claude-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
* @param openaiBaseUrl OpenAI-Basis-URL; {@code null} wird zu leerem String
* @param openaiModel OpenAI-Modellname; {@code null} wird zu leerem String
* @param openaiTimeoutSeconds OpenAI-Timeout; {@code null} wird zu leerem String
* @param openaiApiKeyDescriptor OpenAI-API-Key-Herkunft; darf nicht {@code null} sein
* @param openaiApiKeyPropertyValue OpenAI-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
* @throws NullPointerException wenn {@code claudeApiKeyDescriptor} oder {@code openaiApiKeyDescriptor} {@code null} sind
*/
public EditorValidationInput {
@@ -86,11 +92,13 @@ public record EditorValidationInput(
claudeTimeoutSeconds = normalizeText(claudeTimeoutSeconds);
claudeApiKeyDescriptor = Objects.requireNonNull(claudeApiKeyDescriptor,
"claudeApiKeyDescriptor must not be null");
claudeApiKeyPropertyValue = normalizeText(claudeApiKeyPropertyValue);
openaiBaseUrl = normalizeText(openaiBaseUrl);
openaiModel = normalizeText(openaiModel);
openaiTimeoutSeconds = normalizeText(openaiTimeoutSeconds);
openaiApiKeyDescriptor = Objects.requireNonNull(openaiApiKeyDescriptor,
"openaiApiKeyDescriptor must not be null");
openaiApiKeyPropertyValue = normalizeText(openaiApiKeyPropertyValue);
}
private static String normalizeText(String value) {
@@ -79,6 +79,8 @@ public class ProviderTechnicalTestService {
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
private final AiModelCatalogPort modelCatalogPort;
private final ApiKeyResolutionPort apiKeyResolutionPort;
/**
* Erstellt einen neuen Service mit den erforderlichen Ports.
*
@@ -89,7 +91,7 @@ public class ProviderTechnicalTestService {
public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
ApiKeyResolutionPort apiKeyResolutionPort) {
this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
Objects.requireNonNull(apiKeyResolutionPort,
this.apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
"apiKeyResolutionPort must not be null");
}
@@ -377,13 +379,10 @@ public class ProviderTechnicalTestService {
/**
* Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
* <p>
* Da {@link EditorValidationInput} keinen direkten API-Key-String enthält, sondern
* nur einen bereits aufgelösten {@link EffectiveApiKeyDescriptor}, wird der Descriptor
* aus dem Editorzustand direkt verwendet. Der Adapter-Out-Seitige Dispatcher erwartet
* den Key entweder als ENV-Variable (die er selbst liest) oder als optionalen Wert
* im Request. Da die Auflösung beim Service bereits über {@link ApiKeyResolutionPort}
* erfolgt ist, wird für den Catalog-Request ein leerer Optional-Wert geliefert
* der Adapter verwendet dann intern seine eigene ENV-Variable-Auflösung.
* Der effektive API-Key-Rohwert wird über den {@link ApiKeyResolutionPort} ermittelt
* (Vorrangregel: providerspezifische ENV → Legacy-ENV → Property-Wert) und in den
* Request übernommen. Dadurch ist der Schlüssel bereits beim Adapter verfügbar und
* spiegelt exakt die Quelle wider, die zuvor im Deskriptor ausgewiesen wurde.
*
* @param input aktueller Editorzustand
* @param family aktive Provider-Familie
@@ -393,12 +392,9 @@ public class ProviderTechnicalTestService {
private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
AiProviderFamily family,
EffectiveApiKeyDescriptor apiKeyDesc) {
// EditorValidationInput enthält keinen direkten API-Key-String-Wert, nur den Descriptor.
// Für den ModelCatalogRequest übergeben wir einen leeren Optional für den apiKey,
// sodass der Adapter seine eigene ENV-Variable-Auflösung durchführt.
// Der Adapter liefert dann IncompleteConfiguration, wenn auch er keinen Key findet
// was aber nicht passiert, da wir oben bereits geprüft haben, dass apiKeyDesc nicht ABSENT ist.
Optional<String> apiKeyForRequest = Optional.empty();
String propertyValue = resolveApiKeyPropertyValue(input, family);
Optional<String> apiKeyForRequest = apiKeyResolutionPort
.resolveEffectiveApiKeyValue(family, propertyValue);
String rawBaseUrl = resolveBaseUrlValue(input, family);
Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);
@@ -412,6 +408,21 @@ public class ProviderTechnicalTestService {
timeout);
}
/**
* Liest den Roh-Property-Wert des API-Schlüssels für die angegebene Provider-Familie
* aus dem Editorzustand.
*
* @param input aktueller Editorzustand
* @param family aktive Provider-Familie
* @return Property-Wert; nie {@code null}, leer wenn nicht gesetzt
*/
private String resolveApiKeyPropertyValue(EditorValidationInput input, AiProviderFamily family) {
return switch (family) {
case CLAUDE -> input.claudeApiKeyPropertyValue();
case OPENAI_COMPATIBLE -> input.openaiApiKeyPropertyValue();
};
}
/**
* Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
* direkt aus dem {@link EditorValidationInput}.