From 2e5ba91b95d565638b644cf82c41b92952c6c8ef Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 29 May 2026 11:28:31 +0800 Subject: [PATCH] fix(asr): allow HTTP endpoints with risk warning --- openless-all/app/src-tauri/src/commands.rs | 9 +- openless-all/app/src/i18n/en.ts | 2 +- openless-all/app/src/i18n/ja.ts | 2 +- openless-all/app/src/i18n/ko.ts | 2 +- openless-all/app/src/i18n/zh-CN.ts | 2 +- openless-all/app/src/i18n/zh-TW.ts | 2 +- .../src/pages/settings/ProvidersSection.tsx | 93 ++++++++++--------- 7 files changed, 60 insertions(+), 52 deletions(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index fc750d4c..524ee5c8 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -1150,11 +1150,6 @@ async fn validate_asr_transcription(config: &ProviderConfig, model: &str) -> Res fn asr_transcriptions_url(base_url: &str) -> Result { let parsed = reqwest::Url::parse(base_url.trim()).map_err(|_| "endpointInvalid".to_string())?; - let host = parsed.host_str().unwrap_or_default(); - let localhost = host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1"; - if parsed.scheme() != "https" && !localhost { - return Err("endpointMustUseHttps".to_string()); - } // Work on the URL path only so we don't corrupt query parameters. let mut url = parsed.clone(); @@ -3756,6 +3751,10 @@ mod tests { asr_transcriptions_url("https://api.openai.com/v1?api-version=2024-12-01").unwrap(), "https://api.openai.com/v1/audio/transcriptions?api-version=2024-12-01" ); + assert_eq!( + asr_transcriptions_url("http://192.168.1.10:8000/v1").unwrap(), + "http://192.168.1.10:8000/v1/audio/transcriptions" + ); } #[test] diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index fa189d50..c55faaaf 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -663,7 +663,7 @@ export const en: typeof zhCN = { validateSuccess: 'Connection check passed.', validateFailed: 'Connection check failed.', providerHttpStatus: 'Provider returned HTTP {{status}}. Check the API key permissions or endpoint.', - endpointMustUseHttps: 'Endpoint must use HTTPS (localhost/127.0.0.1 are allowed for local testing).', + endpointMustUseHttps: 'HTTP endpoints are allowed, but API keys and audio content may leak in transit.', endpointInvalid: 'Endpoint format is invalid.', responseTooLarge: 'Provider response is too large to validate safely.', asrInvalidJson: 'ASR response is not valid JSON.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index fde07e6e..fd5abcd3 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -665,7 +665,7 @@ export const ja: typeof zhCN = { validateSuccess: '接続チェックに合格しました。', validateFailed: '接続チェックに失敗しました。', providerHttpStatus: 'サプライヤーが {{status}} を返しました。API Key 権限またはエンドポイントを確認してください。', - endpointMustUseHttps: 'Endpoint は HTTPS を使用する必要があります(localhost/127.0.0.1 を除く)。', + endpointMustUseHttps: 'HTTP Endpoint も使用できますが、API Key と音声内容が通信中に漏えいする可能性があります。', endpointInvalid: 'Endpoint の形式が無効です。', responseTooLarge: 'サプライヤーの応答が大きすぎるため、安全のため検証を停止しました。', asrInvalidJson: 'ASR の応答が有効な JSON ではありません。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 96106ccc..6377e17d 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -665,7 +665,7 @@ export const ko: typeof zhCN = { validateSuccess: '연결 확인을 통과했습니다.', validateFailed: '연결 확인에 실패했습니다.', providerHttpStatus: '공급자가 {{status}} 를 반환했습니다. API Key 권한 또는 Endpoint 를 확인해 주세요.', - endpointMustUseHttps: 'Endpoint 는 HTTPS 를 사용해야 합니다(localhost/127.0.0.1 제외).', + endpointMustUseHttps: 'HTTP Endpoint 를 사용할 수 있지만, API Key 와 음성 내용이 전송 중 유출될 수 있습니다.', endpointInvalid: 'Endpoint 형식이 올바르지 않습니다.', responseTooLarge: '공급자 응답이 너무 커서 안전을 위해 검증을 중단했습니다.', asrInvalidJson: 'ASR 응답이 유효한 JSON 이 아닙니다.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 7a1d8cd5..6dec518d 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -661,7 +661,7 @@ export const zhCN = { validateSuccess: '连接检查通过。', validateFailed: '连接检查未通过。', providerHttpStatus: '供应商接口返回 {{status}},请检查 API Key 权限或 Endpoint。', - endpointMustUseHttps: 'Endpoint 必须使用 HTTPS(本地 localhost/127.0.0.1 测试除外)。', + endpointMustUseHttps: '允许使用 HTTP Endpoint,但请注意:API Key 和音频内容可能在传输中泄漏。', endpointInvalid: 'Endpoint 格式不合法。', responseTooLarge: '供应商响应过大,已停止验证以保证安全。', asrInvalidJson: 'ASR 响应不是有效 JSON。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index e987706a..5c7e689b 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -663,7 +663,7 @@ export const zhTW: typeof zhCN = { validateSuccess: '連接檢查通過。', validateFailed: '連接檢查未通過。', providerHttpStatus: '供應商接口返回 {{status}},請檢查 API Key 權限或 Endpoint。', - endpointMustUseHttps: 'Endpoint 必須使用 HTTPS(本地 localhost/127.0.0.1 測試除外)。', + endpointMustUseHttps: '允許使用 HTTP Endpoint,但請注意:API Key 和音訊內容可能在傳輸中外洩。', endpointInvalid: 'Endpoint 格式不合法。', responseTooLarge: '供應商響應過大,已停止驗證以保證安全。', asrInvalidJson: 'ASR 響應不是有效 JSON。', diff --git a/openless-all/app/src/pages/settings/ProvidersSection.tsx b/openless-all/app/src/pages/settings/ProvidersSection.tsx index e9cda5d0..5b87a6a4 100644 --- a/openless-all/app/src/pages/settings/ProvidersSection.tsx +++ b/openless-all/app/src/pages/settings/ProvidersSection.tsx @@ -714,55 +714,64 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue const inputType = mask && !revealed ? 'password' : 'text'; const disabled = !loaded; + const showInsecureAsrEndpointWarning = account === 'asr.endpoint' + && value.trim().toLowerCase().startsWith('http://'); return ( -
- - {defaultValue && !value && loaded && ( - - )} - {trailing} - {mask && ( +
+
+ + {defaultValue && !value && loaded && ( + + )} + {trailing} + {mask && ( + + )} - )} - - {/* readError 是字段无法读取的持续错误,留在原位提示用户该字段不可用; - 其它瞬态状态(saving / saved / saveError / copied / copyError)都通过 - emitSaved 发到右上角统一 toast,不再内联占位。 */} - {status === 'readError' && ( - - {t('settings.providers.readFailed')} + {/* readError 是字段无法读取的持续错误,留在原位提示用户该字段不可用; + 其它瞬态状态(saving / saved / saveError / copied / copyError)都通过 + emitSaved 发到右上角统一 toast,不再内联占位。 */} + {status === 'readError' && ( + + {t('settings.providers.readFailed')} + + )} +
+ {showInsecureAsrEndpointWarning && ( + + {t('settings.providers.endpointMustUseHttps')} )}