mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-26 13:35:49 +08:00
fix(voice): validate config and request boundaries
Malformed stored settings could break voice requests instead of using safe defaults. Health results could outlive auth changes. URL checks also did not guard the fetch sink. Remove constant recorder branches so lifecycle cancellation stays clear.
This commit is contained in:
@@ -7,13 +7,11 @@ import { VOICE_CONFIG_SYNC_EVENT, voiceConfigHeaders } from '../../../hooks/useV
|
||||
// the Settings modal) and a configured voice backend.
|
||||
const STORAGE_KEY = 'uiPreferences';
|
||||
const SYNC_EVENT = 'ui-preferences:sync';
|
||||
const healthCache = new Map<string, boolean>();
|
||||
const healthRequests = new Map<string, Promise<boolean>>();
|
||||
|
||||
function checkVoiceHealth(): Promise<boolean> {
|
||||
const baseUrl = voiceConfigHeaders()['x-voice-base-url'];
|
||||
const signature = baseUrl || '';
|
||||
if (healthCache.has(signature)) return Promise.resolve(healthCache.get(signature) ?? false);
|
||||
const pending = healthRequests.get(signature);
|
||||
if (pending) return pending;
|
||||
const request = authenticatedFetch('/api/voice/health', {
|
||||
@@ -24,10 +22,6 @@ function checkVoiceHealth(): Promise<boolean> {
|
||||
const data = await response.json();
|
||||
return data?.configured === true;
|
||||
})
|
||||
.then((available) => {
|
||||
healthCache.set(signature, available);
|
||||
return available;
|
||||
})
|
||||
.finally(() => {
|
||||
healthRequests.delete(signature);
|
||||
});
|
||||
|
||||
@@ -63,7 +63,6 @@ export function useVoiceInput(
|
||||
const start = useCallback(async () => {
|
||||
if (startingRef.current || (recorderRef.current && recorderRef.current.state !== 'inactive')) return;
|
||||
startingRef.current = true;
|
||||
let recordingCancelled = false;
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { echoCancellation: true, noiseSuppression: true },
|
||||
@@ -84,7 +83,7 @@ export function useVoiceInput(
|
||||
|
||||
rec.onstop = async () => {
|
||||
stopTracks();
|
||||
if (recordingCancelled || cancelledRef.current) return;
|
||||
if (cancelledRef.current) return;
|
||||
// Capture and clear the send intent for this stop before any async work.
|
||||
const shouldSend = sendRef.current;
|
||||
sendRef.current = false;
|
||||
@@ -107,23 +106,22 @@ export function useVoiceInput(
|
||||
});
|
||||
if (!res.ok) throw new Error(`transcribe ${res.status}`);
|
||||
const data = await res.json();
|
||||
if (recordingCancelled || cancelledRef.current) return;
|
||||
if (cancelledRef.current) return;
|
||||
const text = String(data?.text || '').trim();
|
||||
if (text) onTranscript(text, shouldSend);
|
||||
else onError?.('No speech detected');
|
||||
} catch (e) {
|
||||
if (!recordingCancelled && !cancelledRef.current) {
|
||||
if (!cancelledRef.current) {
|
||||
onError?.(`Transcription failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} finally {
|
||||
if (!recordingCancelled && !cancelledRef.current) setState('idle');
|
||||
if (!cancelledRef.current) setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
rec.start();
|
||||
setState('recording');
|
||||
} catch (e) {
|
||||
recordingCancelled = true;
|
||||
recorderRef.current = null;
|
||||
stopTracks();
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
Reference in New Issue
Block a user