34 lines
2.0 KiB
TypeScript
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++; }
|
|
}
|