From 411d6061ae74cef7cc255def6b36903f22dacc27 Mon Sep 17 00:00:00 2001 From: chengyongru <61816729+chengyongru@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:17:57 +0800 Subject: [PATCH] fix(webui): explain HTTPS requirement for voice input (#5304) --- docs/webui.md | 11 +++++ .../src/components/thread/ThreadComposer.tsx | 2 +- webui/src/hooks/useVoiceRecorder.ts | 5 +++ webui/src/i18n/locales/en/common.json | 1 + webui/src/i18n/locales/es/common.json | 1 + webui/src/i18n/locales/fr/common.json | 1 + webui/src/i18n/locales/id/common.json | 1 + webui/src/i18n/locales/ja/common.json | 1 + webui/src/i18n/locales/ko/common.json | 1 + webui/src/i18n/locales/pt-BR/common.json | 1 + webui/src/i18n/locales/vi/common.json | 1 + webui/src/i18n/locales/zh-CN/common.json | 1 + webui/src/i18n/locales/zh-TW/common.json | 1 + webui/src/tests/thread-composer.test.tsx | 43 +++++++++++++++++++ 14 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/webui.md b/docs/webui.md index 13889e034..930062485 100644 --- a/docs/webui.md +++ b/docs/webui.md @@ -288,6 +288,13 @@ The gateway refuses to start with `host` set to `"0.0.0.0"` unless `token` or `http://:8765` from the other device and enter the secret in the login form. +Plain HTTP is enough for basic WebUI access, but browsers expose microphone +capture only in secure contexts. Voice input works on same-machine localhost; +from another device, serve the WebUI over HTTPS with a certificate that device +trusts. Configure [`sslCertfile` and `sslKeyfile`](./websocket.md#tlsssl) on the +WebSocket channel and open `https://:8765`, or terminate HTTPS at a +reverse proxy and use that proxy's HTTPS URL. + Remote WebUI clients with a valid token can view and use Apps. Actions that install missing nanobot support packages, such as adding a channel dependency, are blocked by default. To let trusted remote administrators change the Python @@ -322,6 +329,10 @@ If the page does not open, check these in order: 4. You are opening port `8765`, not the gateway health port. 5. LAN access uses `host: "0.0.0.0"` and a token or token issue secret. +If voice input asks for a secure connection, use HTTPS with a certificate the +device trusts. Browsers do not expose microphone capture to +`http://` origins. + For detailed diagnostics, see [`troubleshooting.md#webui-problems`](./troubleshooting.md#webui-problems). For frontend development, see [`../webui/README.md`](../webui/README.md). diff --git a/webui/src/components/thread/ThreadComposer.tsx b/webui/src/components/thread/ThreadComposer.tsx index c51a8b607..43b318c6e 100644 --- a/webui/src/components/thread/ThreadComposer.tsx +++ b/webui/src/components/thread/ThreadComposer.tsx @@ -2383,7 +2383,7 @@ export function ThreadComposer({
{ if (!onTranscribeAudio || state !== "idle" || startPendingRef.current) return; onClearError(); + if (window.isSecureContext === false) { + onError("insecureContext"); + return; + } const mediaDevices = navigator.mediaDevices; const MediaRecorderCtor = mediaRecorderConstructor(); if (!mediaDevices?.getUserMedia || !MediaRecorderCtor) { diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index 72c8229df..76beb1401 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -1147,6 +1147,7 @@ "recordingStatus": "Recording {{time}}" }, "voiceErrors": { + "insecureContext": "Chrome and other browsers block microphone access on remote HTTP pages for security. Open this WebUI over HTTPS to use voice input.", "unsupported": "Voice input is not supported in this browser.", "permission": "Allow microphone access in the address bar, then retry.", "notConfigured": "Configure a transcription provider first.", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 6cb97d0cf..4c430665f 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -1134,6 +1134,7 @@ "recordingStatus": "Grabando {{time}}" }, "voiceErrors": { + "insecureContext": "Por seguridad, Chrome y otros navegadores bloquean el acceso al micrófono en páginas HTTP remotas. Abre esta WebUI mediante HTTPS para usar la entrada de voz.", "unsupported": "Este navegador no admite entrada de voz.", "permission": "Permite el micrófono en la barra de direcciones y vuelve a intentarlo.", "notConfigured": "Configura primero un proveedor de transcripción.", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 266ff019a..c9393d403 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "Enregistrement {{time}}" }, "voiceErrors": { + "insecureContext": "Pour des raisons de sécurité, Chrome et d’autres navigateurs bloquent l’accès au microphone sur les pages HTTP distantes. Ouvrez cette WebUI en HTTPS pour utiliser la saisie vocale.", "unsupported": "La saisie vocale n'est pas prise en charge par ce navigateur.", "permission": "Autorisez le microphone dans la barre d'adresse, puis réessayez.", "notConfigured": "Configurez d'abord un fournisseur de transcription.", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 975b5b8ed..405c75193 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "Merekam {{time}}" }, "voiceErrors": { + "insecureContext": "Demi keamanan, Chrome dan browser lain memblokir akses mikrofon pada halaman HTTP jarak jauh. Buka WebUI ini melalui HTTPS untuk menggunakan input suara.", "unsupported": "Input suara tidak didukung di browser ini.", "permission": "Izinkan mikrofon di bilah alamat, lalu coba lagi.", "notConfigured": "Konfigurasikan penyedia transkripsi terlebih dahulu.", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index 968ac0993..31b9bda7a 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "録音中 {{time}}" }, "voiceErrors": { + "insecureContext": "セキュリティ上、Chrome などのブラウザーはリモートの HTTP ページからのマイクアクセスをブロックします。音声入力を使用するには、この WebUI を HTTPS で開いてください。", "unsupported": "このブラウザーは音声入力に対応していません。", "permission": "アドレスバーでマイクを許可してから再試行してください。", "notConfigured": "先に文字起こしプロバイダーを設定してください。", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index 8b6ec667f..270624cbb 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "녹음 중 {{time}}" }, "voiceErrors": { + "insecureContext": "보안을 위해 Chrome 및 기타 브라우저는 원격 HTTP 페이지의 마이크 접근을 차단합니다. 음성 입력을 사용하려면 HTTPS로 이 WebUI를 여세요.", "unsupported": "이 브라우저는 음성 입력을 지원하지 않습니다.", "permission": "주소 표시줄에서 마이크를 허용한 후 다시 시도하세요.", "notConfigured": "먼저 음성 변환 제공업체를 설정하세요.", diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index 9fbe9cbb8..ee4deab9b 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -1147,6 +1147,7 @@ "recordingStatus": "Gravando {{time}}" }, "voiceErrors": { + "insecureContext": "Por segurança, o Chrome e outros navegadores bloqueiam o acesso ao microfone em páginas HTTP remotas. Abra esta WebUI por HTTPS para usar a entrada de voz.", "unsupported": "A entrada de voz não é compatível com este navegador.", "permission": "Permita o microfone na barra de endereço e tente novamente.", "notConfigured": "Configure primeiro um provedor de transcrição.", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index e9a79be0d..e53bd9d16 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "Đang ghi {{time}}" }, "voiceErrors": { + "insecureContext": "Vì lý do bảo mật, Chrome và các trình duyệt khác chặn quyền truy cập micrô trên các trang HTTP từ xa. Mở WebUI này qua HTTPS để dùng tính năng nhập bằng giọng nói.", "unsupported": "Trình duyệt này không hỗ trợ nhập bằng giọng nói.", "permission": "Cho phép micrô trong thanh địa chỉ rồi thử lại.", "notConfigured": "Hãy cấu hình nhà cung cấp chép lời trước.", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 224c07ba5..b5a90c996 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -1146,6 +1146,7 @@ "recordingStatus": "正在录音 {{time}}" }, "voiceErrors": { + "insecureContext": "出于安全考虑,Chrome 等浏览器会阻止远程 HTTP 页面访问麦克风。请通过 HTTPS 打开此 WebUI 以使用语音输入。", "unsupported": "当前浏览器不支持语音输入。", "permission": "请在地址栏允许麦克风后重试。", "notConfigured": "请先配置转写提供商。", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index a46be8969..694349bd0 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -1133,6 +1133,7 @@ "recordingStatus": "正在錄音 {{time}}" }, "voiceErrors": { + "insecureContext": "基於安全考量,Chrome 等瀏覽器會阻止遠端 HTTP 頁面存取麥克風。請透過 HTTPS 開啟此 WebUI 以使用語音輸入。", "unsupported": "目前瀏覽器不支援語音輸入。", "permission": "請在網址列允許麥克風後重試。", "notConfigured": "請先設定轉寫供應商。", diff --git a/webui/src/tests/thread-composer.test.tsx b/webui/src/tests/thread-composer.test.tsx index 6efaad45d..95745579d 100644 --- a/webui/src/tests/thread-composer.test.tsx +++ b/webui/src/tests/thread-composer.test.tsx @@ -651,6 +651,49 @@ describe("ThreadComposer", () => { expect(onSend).not.toHaveBeenCalled(); }); + it("explains the HTTPS requirement for voice input on an insecure origin", async () => { + const { getUserMedia } = mockVoiceRecorder(); + vi.stubGlobal("isSecureContext", false); + render( + "unused")} + placeholder="Type your message..." + />, + ); + + fireEvent.click(screen.getByRole("button", { name: "Voice input" })); + + expect( + await screen.findByText( + "Chrome and other browsers block microphone access on remote HTTP pages for security. " + + "Open this WebUI over HTTPS to use voice input.", + ), + ).toBeInTheDocument(); + expect(screen.getByRole("alert")).toHaveClass("max-h-24"); + expect(getUserMedia).not.toHaveBeenCalled(); + }); + + it("keeps the unsupported-browser error for secure origins without recording support", async () => { + const { getUserMedia } = mockVoiceRecorder(); + vi.stubGlobal("isSecureContext", true); + vi.stubGlobal("MediaRecorder", undefined); + render( + "unused")} + placeholder="Type your message..." + />, + ); + + fireEvent.click(screen.getByRole("button", { name: "Voice input" })); + + expect( + await screen.findByText("Voice input is not supported in this browser."), + ).toBeInTheDocument(); + expect(getUserMedia).not.toHaveBeenCalled(); + }); + it("converts voice recordings to wav for Xiaomi MiMo transcription", async () => { mockVoiceRecorder(new Blob([new Uint8Array([1, 2, 3, 4])], { type: "audio/webm" })); const { decodeAudioData } = mockVoiceAudioInput(