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
|
## 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)
|
- **[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', fn($p) => (new SettingsController())->uploadLogo($p));
|
||||||
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($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/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->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p));
|
||||||
$router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p));
|
$router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p));
|
||||||
$router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($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))]);
|
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
|
public function bankImport(array $params): void
|
||||||
{
|
{
|
||||||
$formats = BankSource::all('csv');
|
$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
|
private function probeOllama(string $baseUrl): bool
|
||||||
{
|
{
|
||||||
$ch = curl_init($baseUrl . '/api/tags');
|
$url = $baseUrl . '/api/tags';
|
||||||
|
$ch = curl_init($url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 2,
|
CURLOPT_TIMEOUT => 2,
|
||||||
|
|
@ -145,9 +190,15 @@ class LlmService
|
||||||
]);
|
]);
|
||||||
curl_exec($ch);
|
curl_exec($ch);
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$err = curl_errno($ch);
|
$errno = curl_errno($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
curl_close($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
|
private function callOllama(string $prompt): string
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
$reachable = $providerLabel !== null ? $llm->isReachable() : false;
|
$reachable = $providerLabel !== null ? $llm->isReachable() : false;
|
||||||
$ollamaUrlEnv = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? '');
|
$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): ?>
|
<?php if ($reachable): ?>
|
||||||
✓ Provider erreichbar: <?= $h($providerLabel) ?>
|
✓ Provider erreichbar: <?= $h($providerLabel) ?>
|
||||||
<?php elseif ($providerLabel): ?>
|
<?php elseif ($providerLabel): ?>
|
||||||
|
|
@ -269,6 +269,11 @@
|
||||||
— Kein LLM-Provider konfiguriert (Auto-Discovery localhost:11434 fehlgeschlagen)
|
— Kein LLM-Provider konfiguriert (Auto-Discovery localhost:11434 fehlgeschlagen)
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -477,6 +482,53 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||||
var btn = this;
|
var btn = this;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue