Files
CyberGril/web/src/adapters/demo.ts
T
2026-10-22 00:56:18 +08:00

34 lines
2.0 KiB
TypeScript

import type { ServerEvent } from '../domain/model';
export interface ChatAdapter {
send(text: string, turnId: string, epoch: number, signal: AbortSignal, emit: (event: ServerEvent) => void, fail?: boolean): Promise<void>;
}
export function delay(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => { if (signal.aborted) {
reject(new DOMException('Cancelled', 'AbortError'));
return;
} const stop = () => { clearTimeout(timer); reject(new DOMException('Cancelled', 'AbortError')); }; const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(); }, ms); signal.addEventListener('abort', stop, { once: true }); });
}
export class DemoAdapter implements ChatAdapter {
async send(_text: string, turnId: string, epoch: number, signal: AbortSignal, emit: (e: ServerEvent) => void, fail = false) {
await delay(500, signal);
if (fail)
throw new Error('没发出去,你写的话还在。');
const reply = '我听见了。我们可以慢慢聊,不用急着把话说完。';
for (const chunk of reply.match(/.{1,5}/gu) || []) {
await delay(90, signal);
emit({ t: 'chat.delta', turn_id: turnId, epoch, delta: chunk });
}
emit({ t: 'chat.final', turn_id: turnId, epoch, utterance: { text: reply, speech_tags: [] } });
}
}
// Transport-independent gate: also rejects deliberately late events from non-cooperative adapters.
export class TurnGate {
private controller: AbortController | null = null;
private current = '';
epoch = 1;
begin(id: string) { this.cancel(); this.current = id; this.controller = new AbortController(); return this.controller.signal; }
accepts(event: ServerEvent) { return event.epoch === this.epoch && event.turn_id === this.current && !this.controller?.signal.aborted; }
cancel() { this.controller?.abort(); this.current = ''; }
resync() { this.cancel(); this.epoch++; }
}