This commit is contained in:
team 1
2026-05-09 11:10:29 +02:00
parent 71fc5a2501
commit 424aef2575
8 changed files with 437 additions and 30 deletions

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Agent\AgentRunner;
use App\Config\ChatMessagesConfig;
use App\Context\ContextService;
use App\Http\ClientIdResolver;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -27,6 +28,7 @@ final readonly class AskSseController
public function __construct(
private AgentRunner $agentRunner,
private ChatMessagesConfig $chatMessages,
private ClientIdResolver $clientIdResolver,
private ContextService $contextService,
private string $projectDir,
@@ -40,7 +42,7 @@ final readonly class AskSseController
$prompt = trim((string) ($data['prompt'] ?? ''));
if ($prompt === '') {
return new JsonResponse(['error' => 'Empty prompt'], Response::HTTP_BAD_REQUEST);
return new JsonResponse(['error' => $this->chatMessages->getSseEmptyPrompt()], Response::HTTP_BAD_REQUEST);
}
/**
@@ -73,7 +75,7 @@ final readonly class AskSseController
]);
} catch (Throwable $e) {
return new JsonResponse(
['error' => 'Stream job could not be created: ' . $this->formatThrowableForClient($e)],
['error' => $this->chatMessages->getSseJobCreateFailed($this->formatThrowableForClient($e))],
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
@@ -95,7 +97,7 @@ final readonly class AskSseController
if (!is_array($job)) {
return new JsonResponse([
'status' => 'missing',
'message' => 'Der Antwort-Job wurde nicht gefunden.',
'message' => $this->chatMessages->getSseJobMissing(),
'lastEventId' => 0,
], Response::HTTP_NOT_FOUND);
}
@@ -213,8 +215,8 @@ final readonly class AskSseController
$this->sendComment('stream-open');
if ($prompt === '') {
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, 'Empty prompt');
$this->sendEvent('error', 'Empty prompt');
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $this->chatMessages->getSseEmptyPrompt());
$this->sendEvent('error', $this->chatMessages->getSseEmptyPrompt());
$this->sendDoneEvent();
return;
}
@@ -225,7 +227,7 @@ final readonly class AskSseController
$this->markJobStatus(
$jobId,
self::JOB_STATUS_INTERRUPTED,
'Die Verbindung zum Antwort-Stream wurde unterbrochen.'
$this->chatMessages->getSseStreamInterrupted()
);
return;
}
@@ -235,11 +237,11 @@ final readonly class AskSseController
$this->sendData($chunk, $eventId);
}
} catch (Throwable $e) {
$message = 'Stream abgebrochen: ' . $this->formatThrowableForClient($e);
$message = $this->chatMessages->getSseStreamAborted($this->formatThrowableForClient($e));
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message);
$this->sendEvent(
'error',
'❌ ' . $message
$this->chatMessages->getSseStreamAbortedEvent($message)
);
if ($prompt !== '' && $clientId !== '') {
@@ -260,13 +262,13 @@ final readonly class AskSseController
$message = trim(preg_replace('/\s+/u', ' ', $message) ?? $message);
if ($message === '') {
$message = 'Unbekannter Streamfehler.';
$message = $this->chatMessages->getSseUnknownStreamError();
}
$this->contextService->appendHistory(
$clientId,
$prompt,
'Systemhinweis: Antwort konnte nicht abgeschlossen werden. Ursache: ' . $message
$this->chatMessages->getSseHistoryFailure($message)
);
} catch (Throwable) {
// History persistence must never break the SSE error response.
@@ -288,11 +290,10 @@ final readonly class AskSseController
return;
}
$message = sprintf(
'❌ Fataler Serverfehler: %s in %s:%s',
(string) ($error['message'] ?? 'unknown error'),
(string) ($error['file'] ?? 'unknown file'),
(string) ($error['line'] ?? '?')
$message = $this->chatMessages->getSseFatalServerError(
(string) ($error['message'] ?? $this->chatMessages->getSseFatalUnknownError()),
(string) ($error['file'] ?? $this->chatMessages->getSseFatalUnknownFile()),
(string) ($error['line'] ?? $this->chatMessages->getSseFatalUnknownLine())
);
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message);
@@ -420,13 +421,13 @@ final readonly class AskSseController
$directory = $this->jobDirectory();
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new \RuntimeException('Stream job directory could not be created.');
throw new \RuntimeException($this->chatMessages->getSseStorageDirectoryCreateFailed());
}
$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (file_put_contents($this->jobPath($jobId), $json, LOCK_EX) === false) {
throw new \RuntimeException('Stream job could not be written.');
throw new \RuntimeException($this->chatMessages->getSseStorageWriteFailed());
}
}
@@ -664,8 +665,8 @@ final readonly class AskSseController
$this->sendEvent(
'error',
$message !== ''
? 'Der Antwort-Stream ist fehlgeschlagen: ' . $message
: 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.'
? $this->chatMessages->getSseStreamFailedWithMessage($message)
: $this->chatMessages->getSseStreamFailedRetry()
);
$this->sendDoneEvent();
return;
@@ -676,7 +677,7 @@ final readonly class AskSseController
'error',
$message !== ''
? $message
: 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.'
: $this->chatMessages->getSseStreamInterruptedRetry()
);
$this->sendDoneEvent();
return;
@@ -857,7 +858,7 @@ final readonly class AskSseController
return $job;
}
$message = 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet, damit die Oberfläche nicht hängen bleibt.';
$message = $this->chatMessages->getSseJobStale();
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message);
$freshJob = $this->readJob($jobId);
@@ -875,39 +876,40 @@ final readonly class AskSseController
$message = is_string($claim['message'] ?? null) ? trim((string) $claim['message']) : '';
if ($reason === 'expired') {
return 'Der Antwort-Job ist abgelaufen. Bitte sende die Anfrage erneut.';
return $this->chatMessages->getSseClaimExpired();
}
if ($reason === 'invalid') {
return 'Der Antwort-Job ist ungültig. Bitte sende die Anfrage erneut.';
return $this->chatMessages->getSseClaimInvalid();
}
if ($reason === 'lock_failed') {
return 'Der Antwort-Job ist gerade gesperrt. Bitte sende die Anfrage erneut, falls keine Antwort erscheint.';
return $this->chatMessages->getSseClaimLockFailed();
}
if ($reason === 'not_pending') {
if ($status === self::JOB_STATUS_RUNNING) {
return 'Der Antwort-Stream läuft bereits oder wurde nach einem Verbindungsabbruch erneut geöffnet. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
return $this->chatMessages->getSseClaimRunning();
}
if ($status === self::JOB_STATUS_INTERRUPTED) {
return 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
return $this->chatMessages->getSseClaimInterrupted();
}
if ($status === self::JOB_STATUS_COMPLETED) {
return 'Der Antwort-Stream wurde bereits abgeschlossen. Bitte sende eine neue Anfrage, wenn du eine weitere Antwort brauchst.';
return $this->chatMessages->getSseClaimCompleted();
}
if ($status === self::JOB_STATUS_FAILED) {
return $message !== ''
? 'Der Antwort-Stream ist fehlgeschlagen: ' . $message
: 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.';
? $this->chatMessages->getSseStreamFailedWithMessage($message)
: $this->chatMessages->getSseStreamFailedRetry();
}
}
return 'Der Antwort-Job wurde nicht gefunden. Falls deine Verbindung kurz unterbrochen war, sende die Anfrage bitte erneut.';
return $this->chatMessages->getSseClaimMissing();
}
private function cleanupExpiredJobs(): void
{
$directory = $this->jobDirectory();