mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-26 13:35:49 +08:00
fix(voice): separate client and server backends
User-selected backend URLs must remain usable without letting clients control server requests. Call custom providers from the browser while keeping the server proxy bound to its configured host. This restores voice controls for frontend settings without reopening the SSRF path.
This commit is contained in:
@@ -1,31 +1,26 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { VOICE_CONFIG_SYNC_EVENT, voiceConfigHeaders } from '../../../hooks/useVoiceConfig';
|
||||
import { readVoiceConfig, VOICE_CONFIG_SYNC_EVENT } from '../../../hooks/useVoiceConfig';
|
||||
|
||||
// Voice UI is gated on the `voiceEnabled` UI preference (toggled in Quick Settings /
|
||||
// the Settings modal) and a configured voice backend.
|
||||
const STORAGE_KEY = 'uiPreferences';
|
||||
const SYNC_EVENT = 'ui-preferences:sync';
|
||||
const healthRequests = new Map<string, Promise<boolean>>();
|
||||
let healthRequest: Promise<boolean> | null = null;
|
||||
|
||||
function checkVoiceHealth(): Promise<boolean> {
|
||||
const baseUrl = voiceConfigHeaders()['x-voice-base-url'];
|
||||
const signature = baseUrl || '';
|
||||
const pending = healthRequests.get(signature);
|
||||
if (pending) return pending;
|
||||
const request = authenticatedFetch('/api/voice/health', {
|
||||
headers: baseUrl ? { 'x-voice-base-url': baseUrl } : {},
|
||||
})
|
||||
if (healthRequest) return healthRequest;
|
||||
const request = authenticatedFetch('/api/voice/health')
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error(`Voice health check failed (${response.status})`);
|
||||
const data = await response.json();
|
||||
return data?.configured === true;
|
||||
})
|
||||
.finally(() => {
|
||||
healthRequests.delete(signature);
|
||||
healthRequest = null;
|
||||
});
|
||||
healthRequests.set(signature, request);
|
||||
healthRequest = request;
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -65,6 +60,10 @@ export function useVoiceAvailable(): boolean {
|
||||
setAvailable(false);
|
||||
return;
|
||||
}
|
||||
if (readVoiceConfig().baseUrl.trim()) {
|
||||
setAvailable(true);
|
||||
return;
|
||||
}
|
||||
const id = ++requestId;
|
||||
try {
|
||||
const result = await checkVoiceHealth();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { voiceConfigHeaders } from '../../../hooks/useVoiceConfig';
|
||||
import { transcribeVoice } from '../../../lib/voiceApi';
|
||||
|
||||
// Mobile-safe recording: iOS Safari 18.4+ supports webm/opus; older iOS needs mp4.
|
||||
const MIME_CANDIDATES = [
|
||||
@@ -97,13 +96,7 @@ export function useVoiceInput(
|
||||
setState('transcribing');
|
||||
try {
|
||||
const ext = type.includes('mp4') ? 'm4a' : type.includes('ogg') ? 'ogg' : 'webm';
|
||||
const fd = new FormData();
|
||||
fd.append('audio', blob, `recording.${ext}`);
|
||||
const res = await authenticatedFetch('/api/voice/transcribe', {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: voiceConfigHeaders(),
|
||||
});
|
||||
const res = await transcribeVoice(blob, `recording.${ext}`);
|
||||
if (!res.ok) throw new Error(`transcribe ${res.status}`);
|
||||
const data = await res.json();
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
Reference in New Issue
Block a user