feat: Chemikalien-Recherche über lokale RiSU-Stoffliste (kein KI-Aufruf mehr)
Ersetzt den ursprünglichen KI-Websuche-Ansatz (GESTIS scheiterte sowohl über Websuche als auch über direkten API-Zugriff an fehlenden schulspezifischen Tätigkeitsbeschränkungen) durch einen reinen lokalen Datenbank-Lookup gegen die offizielle RiSU-Stoffliste (1764 Stoffe inkl. Tätigkeitsbeschränkungs-Codes, Legende zur Abfragezeit übersetzt statt in die Datenbank gebacken). Keine KI-Kosten, keine Drittanbieter-Abhängigkeit mehr für diese Funktion. HazardSubstance.AgeRestriction zu ActivityRestriction umbenannt, da die Tätigkeitsbeschränkung mehr abdeckt als reine Altersgrenzen. Details und verworfene Zwischenstände in TODO.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,29 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
Converters = { new GermanDateOnlyJsonConverter(), new GermanTimeOnlyJsonConverter() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Baut eine nutzergerichtete Fehlermeldung für eine fehlgeschlagene Backend-Antwort, die
|
||||
/// weder 401 noch 402 war (die haben eigene, feste Meldungen). Liest den vom Backend
|
||||
/// gesendeten Klartext-Grund (<c>ai_backend_fail</c> liefert immer <c>{"error": "..."}</c>)
|
||||
/// mit, statt nur eine pauschale Meldung zu zeigen — ohne das war z.B. beim Fehlschlagen der
|
||||
/// Websuche in substance.php (falscher Tool-Bezeichner, fehlender Beta-Header o.ä.) der
|
||||
/// eigentliche Grund nirgends sichtbar, nur "Die Anfrage ist fehlgeschlagen".
|
||||
/// </summary>
|
||||
private static async Task<AiBackendException> BuildRequestFailedExceptionAsync(HttpResponseMessage resp)
|
||||
{
|
||||
string? backendReason = null;
|
||||
try
|
||||
{
|
||||
var body = await resp.Content.ReadFromJsonAsync<BackendErrorResult>(JsonOptions);
|
||||
backendReason = string.IsNullOrWhiteSpace(body?.Error) ? null : body!.Error;
|
||||
}
|
||||
catch { /* Antwortkörper war kein valides {"error": "..."}-JSON - Fallback unten greift. */ }
|
||||
|
||||
return new AiBackendException(backendReason is null
|
||||
? "Die Anfrage an den KI-Dienst ist fehlgeschlagen."
|
||||
: $"Die Anfrage an den KI-Dienst ist fehlgeschlagen: {backendReason}");
|
||||
}
|
||||
|
||||
public async Task<string> LoginAsync(string username, string password)
|
||||
{
|
||||
HttpResponseMessage resp;
|
||||
@@ -327,7 +350,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
if (resp.StatusCode == (HttpStatusCode)402)
|
||||
throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen.");
|
||||
throw await BuildRequestFailedExceptionAsync(resp);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -368,7 +391,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
if (resp.StatusCode == (HttpStatusCode)402)
|
||||
throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen.");
|
||||
throw await BuildRequestFailedExceptionAsync(resp);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -409,7 +432,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
if (resp.StatusCode == (HttpStatusCode)402)
|
||||
throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen.");
|
||||
throw await BuildRequestFailedExceptionAsync(resp);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -422,6 +445,46 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fragt sicherheitsrelevante Daten zu einer Chemikalie ab (Nutzerwunsch neben 4.2): eigener
|
||||
/// Endpunkt <c>ai-backend/substance.php</c>, serverseitig dateibasiert gecacht (Name/CAS,
|
||||
/// Ablauffrist je nach Einstufung) — wiederholte Anfragen für denselben Stoff verursachen
|
||||
/// nach dem ersten Mal keine weiteren Kosten mehr. Kein Unit-/Guppen-Kontext nötig, nur der
|
||||
/// eingetippte Stoffname.
|
||||
/// </summary>
|
||||
public async Task<AiSubstanceResearchResponse> RequestSubstanceResearchAsync(string substanceName, string token)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, "substance.php")
|
||||
{
|
||||
Content = JsonContent.Create(new { name = substanceName }, options: JsonOptions),
|
||||
};
|
||||
req.Headers.Authorization = new("Bearer", token);
|
||||
|
||||
HttpResponseMessage resp;
|
||||
try { resp = await http.SendAsync(req); }
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw new AiBackendException("Der KI-Dienst ist nicht erreichbar. Bitte Internetverbindung prüfen.");
|
||||
}
|
||||
|
||||
if (resp.StatusCode == HttpStatusCode.Unauthorized)
|
||||
throw new AiBackendException("Anmeldung abgelaufen. Bitte in den Einstellungen erneut anmelden.");
|
||||
if (resp.StatusCode == (HttpStatusCode)402)
|
||||
throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw await BuildRequestFailedExceptionAsync(resp);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await resp.Content.ReadFromJsonAsync<AiSubstanceResearchResponse>(JsonOptions);
|
||||
return result ?? throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not AiBackendException)
|
||||
{
|
||||
throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden. Bitte erneut versuchen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rein (nur Repository-Lesezugriff für den Alternativpfad-Katalog, kein Schreiben) — testbar
|
||||
/// mit Fakes. Gibt die zu speichernden Lesson-Objekte zurück; der Aufrufer ruft
|
||||
@@ -478,4 +541,5 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
|
||||
private class LoginResult { public string Token { get; set; } = ""; }
|
||||
private class BalanceResult { public decimal BalanceUsd { get; set; } }
|
||||
private class BackendErrorResult { public string? Error { get; set; } }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user