Fix #55: LLM-Verbindungstest mit Diagnoseausgabe
- LlmService::testConnection() gibt cURL-Fehler, HTTP-Code, Antwortzeit und verfügbare Modelle zurück - probeOllama() schreibt cURL-Fehler ins PHP-Fehlerlog - Einstellungsseite: Button "Verbindung testen" mit Inline-Diagnose und Firewall-Hinweis - Route POST /settings/llm-test
This commit is contained in:
parent
f9f89b933f
commit
54f7d9bd38
5 changed files with 123 additions and 5 deletions
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-06-08 – Fix: KI-Verbindungstest mit Diagnoseinformationen (Issue #55)
|
||||
|
||||
- **`LlmService`:** Neue Methode `testConnection()` gibt detaillierte Diagnose zurück (cURL-Fehlernummer/-meldung, HTTP-Code, Antwortzeit, verfügbare Modelle)
|
||||
- **`LlmService`:** `probeOllama()` schreibt bei cURL-Fehlern jetzt einen Eintrag ins PHP-Fehlerlog (`docker compose logs web`)
|
||||
- **Einstellungsseite:** Neuer Button „Verbindung testen" im KI-Abschnitt; zeigt bei Erfolg verfügbare Modelle, bei Fehler cURL-Fehler, URL und Firewall-Hinweis an
|
||||
- **Route:** `POST /settings/llm-test` → `SettingsController::llmTest()`
|
||||
|
||||
## 2026-06-01 – Fix: EN16931-Validierungsfehler in ZUGFeRD-Rechnungen
|
||||
|
||||
- **[BR-FX-EN-04]** behoben: `ZugferdService` setzt jetzt BT-72 (Liefer-/Leistungsdatum) via `setDocumentSupplyChainEvent()`; das Datum wird aus dem Feld `leistungszeitraum` geparst (Format `DD.MM.YYYY` → direkt; Format `MM.YYYY` → letzter Tag des Monats; sonst Fallback auf Rechnungsdatum)
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ $router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailT
|
|||
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
|
||||
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
|
||||
$router->post('/settings/generate-app-key', fn($p) => (new SettingsController())->generateAppKey($p));
|
||||
$router->post('/settings/llm-test', fn($p) => (new SettingsController())->llmTest($p));
|
||||
$router->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p));
|
||||
$router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p));
|
||||
$router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($p));
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ class SettingsController extends BaseController
|
|||
echo json_encode(['key' => bin2hex(random_bytes(32))]);
|
||||
}
|
||||
|
||||
public function llmTest(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
$result = (new \Steuerkram\Services\LlmService())->testConnection();
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
public function bankImport(array $params): void
|
||||
{
|
||||
$formats = BankSource::all('csv');
|
||||
|
|
|
|||
|
|
@ -134,9 +134,54 @@ class LlmService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert Diagnose-Informationen für den Verbindungstest auf der Einstellungsseite.
|
||||
* Gibt ein Array zurück: ['ok' => bool, 'url' => string, 'elapsed' => int, 'error'? => string, 'models'? => string[]]
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function testConnection(): array
|
||||
{
|
||||
if ($this->mode === null) {
|
||||
return ['ok' => false, 'error' => 'Kein LLM-Provider konfiguriert.'];
|
||||
}
|
||||
if ($this->mode === 'openai') {
|
||||
return ['ok' => true, 'url' => $this->url, 'elapsed' => 0, 'info' => 'OpenAI-Modus – kein Verbindungstest möglich.'];
|
||||
}
|
||||
|
||||
$url = $this->url . '/api/tags';
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
]);
|
||||
$start = microtime(true);
|
||||
$response = curl_exec($ch);
|
||||
$elapsed = (int)round((microtime(true) - $start) * 1000);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$errno = curl_errno($ch);
|
||||
$errstr = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errno !== 0) {
|
||||
error_log("[Steuerkram] LlmService::testConnection() cURL-Fehler #{$errno} auf {$url}: {$errstr}");
|
||||
return ['ok' => false, 'url' => $url, 'elapsed' => $elapsed, 'error' => "cURL #{$errno}: {$errstr}"];
|
||||
}
|
||||
if ($code < 200 || $code >= 300) {
|
||||
return ['ok' => false, 'url' => $url, 'elapsed' => $elapsed, 'error' => "HTTP {$code}"];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$models = array_column($data['models'] ?? [], 'name');
|
||||
|
||||
return ['ok' => true, 'url' => $url, 'elapsed' => $elapsed, 'models' => $models];
|
||||
}
|
||||
|
||||
private function probeOllama(string $baseUrl): bool
|
||||
{
|
||||
$ch = curl_init($baseUrl . '/api/tags');
|
||||
$url = $baseUrl . '/api/tags';
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 2,
|
||||
|
|
@ -145,9 +190,15 @@ class LlmService
|
|||
]);
|
||||
curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_errno($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return $err === 0 && $code >= 200 && $code < 300;
|
||||
|
||||
if ($errno !== 0) {
|
||||
error_log("[Steuerkram] LlmService::probeOllama() cURL-Fehler #{$errno} auf {$url}: {$err}");
|
||||
}
|
||||
|
||||
return $errno === 0 && $code >= 200 && $code < 300;
|
||||
}
|
||||
|
||||
private function callOllama(string $prompt): string
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@
|
|||
$reachable = $providerLabel !== null ? $llm->isReachable() : false;
|
||||
$ollamaUrlEnv = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? '');
|
||||
?>
|
||||
<p class="text-sm" style="margin:0 0 0 1.75rem;color:<?= $reachable ? 'var(--color-success,#16a34a)' : '#dc2626' ?>">
|
||||
<p class="text-sm" style="margin:0 0 .5rem 1.75rem;color:<?= $reachable ? 'var(--color-success,#16a34a)' : '#dc2626' ?>">
|
||||
<?php if ($reachable): ?>
|
||||
✓ Provider erreichbar: <?= $h($providerLabel) ?>
|
||||
<?php elseif ($providerLabel): ?>
|
||||
|
|
@ -269,6 +269,11 @@
|
|||
— Kein LLM-Provider konfiguriert (Auto-Discovery localhost:11434 fehlgeschlagen)
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<div style="margin-left:1.75rem;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="btn-llm-test">Verbindung testen</button>
|
||||
<span id="llm-test-result" style="font-size:.8rem"></span>
|
||||
</div>
|
||||
<div id="llm-test-detail" style="margin:.5rem 0 0 1.75rem;font-size:.8rem;display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -477,6 +482,53 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('btn-llm-test')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var result = document.getElementById('llm-test-result');
|
||||
var detail = document.getElementById('llm-test-detail');
|
||||
btn.disabled = true;
|
||||
result.textContent = 'Teste…';
|
||||
result.style.color = '';
|
||||
detail.style.display = 'none';
|
||||
fetch('/settings/llm-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.ok) {
|
||||
result.textContent = '✓ Verbindung OK (' + (data.elapsed || 0) + ' ms)';
|
||||
result.style.color = 'green';
|
||||
if (data.models && data.models.length > 0) {
|
||||
detail.textContent = 'Modelle: ' + data.models.join(', ');
|
||||
detail.style.display = 'block';
|
||||
detail.style.color = '#374151';
|
||||
} else if (data.info) {
|
||||
detail.textContent = data.info;
|
||||
detail.style.display = 'block';
|
||||
detail.style.color = '#374151';
|
||||
}
|
||||
} else {
|
||||
result.textContent = '✗ Verbindung fehlgeschlagen';
|
||||
result.style.color = 'red';
|
||||
if (data.error) {
|
||||
var lines = ['Fehler: ' + data.error];
|
||||
if (data.url) lines.push('URL: ' + data.url);
|
||||
if (data.elapsed) lines.push('Nach ' + data.elapsed + ' ms');
|
||||
lines.push('Tipp: sudo ufw allow from 172.16.0.0/12 to any port 11434 (oder firewall prüfen)');
|
||||
lines.push('Logs: docker compose logs web');
|
||||
detail.innerHTML = lines.map(function (l) { return '<div>' + l + '</div>'; }).join('');
|
||||
detail.style.display = 'block';
|
||||
detail.style.color = '#dc2626';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
result.textContent = '✗ Netzwerkfehler';
|
||||
result.style.color = 'red';
|
||||
})
|
||||
.finally(function () { btn.disabled = false; });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue