65 lines
1.8 KiB
PHP
Executable File
65 lines
1.8 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service;
|
|
|
|
/**
|
|
* 诊单 AI 助手 SSE 事件状态机:start -> (progress|delta)* -> done|error。
|
|
*/
|
|
final class AssistantSseProtocol
|
|
{
|
|
private const HEARTBEAT_INTERVAL_NS = 10_000_000_000;
|
|
|
|
private int $seq = 0;
|
|
|
|
private int $lastOutputAtNs = 0;
|
|
|
|
private bool $started = false;
|
|
|
|
private bool $terminal = false;
|
|
|
|
/** @param array<string,mixed> $payload */
|
|
public function encode(string $event, array $payload): ?string
|
|
{
|
|
if ($this->terminal || !in_array($event, ['start', 'progress', 'delta', 'done', 'error'], true)) {
|
|
return null;
|
|
}
|
|
if ((!$this->started && $event !== 'start') || ($this->started && $event === 'start')) {
|
|
return null;
|
|
}
|
|
|
|
$nextSeq = $this->seq + 1;
|
|
$encoded = json_encode(
|
|
['seq' => $nextSeq] + $payload,
|
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
|
|
);
|
|
if (!is_string($encoded)) {
|
|
return null;
|
|
}
|
|
|
|
$this->seq = $nextSeq;
|
|
$this->started = true;
|
|
$this->lastOutputAtNs = hrtime(true);
|
|
if (in_array($event, ['done', 'error'], true)) {
|
|
$this->terminal = true;
|
|
}
|
|
return 'event: ' . $event . "\n" . 'data: ' . $encoded . "\n\n";
|
|
}
|
|
|
|
/** Keep an idle authenticated stream open without inventing application events. */
|
|
public function heartbeat(): ?string
|
|
{
|
|
if (!$this->started || $this->terminal) return null;
|
|
$now = hrtime(true);
|
|
if ($now - $this->lastOutputAtNs < self::HEARTBEAT_INTERVAL_NS) return null;
|
|
$this->lastOutputAtNs = $now;
|
|
return ": keep-alive\n\n";
|
|
}
|
|
|
|
public function isTerminal(): bool
|
|
{
|
|
return $this->terminal;
|
|
}
|
|
}
|