fix(asr): allow HTTP endpoints with risk warning

This commit is contained in:
H-Chris233 committed 2026-05-29 11:28:31 +08:00
1 parent e4acd1d9b4
commit 2e5ba91b95
7 files changed
+61 -53

No files matched your search

+4 -5
View File
@@ -1150,11 +1150,6 @@ async fn validate_asr_transcription(config: &ProviderConfig, model: &str) -> Res
fn asr_transcriptions_url(base_url: &str) -> Result<String, String> {
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]
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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 ではありません。',
+1 -1
View File
@@ -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 이 아닙니다.',
+1 -1
View File
@@ -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。',
+1 -1
View File
@@ -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。',
@@ -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 (
<SettingRow label={label}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', width: '100%', maxWidth: 420 }}>
<input
type={inputType}
value={value}
placeholder={loaded ? placeholder : t('common.loading')}
onChange={handleChange}
onBlur={onBlur}
disabled={disabled}
style={{ ...inputStyle, fontFamily: mono ? 'var(--ol-font-mono)' : 'inherit' }}
/>
{defaultValue && !value && loaded && (
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle} disabled={!loaded}>
<Icon name="check" size={13} />
</button>
)}
{trailing}
{mask && (
<button
onClick={() => setRevealed(r => !r)}
title={revealed ? t('common.hide') : t('common.show')}
style={iconBtnStyle}
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, width: '100%', maxWidth: 420 }}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', width: '100%' }}>
<input
type={inputType}
value={value}
placeholder={loaded ? placeholder : t('common.loading')}
onChange={handleChange}
onBlur={onBlur}
disabled={disabled}
style={{ ...inputStyle, fontFamily: mono ? 'var(--ol-font-mono)' : 'inherit' }}
/>
{defaultValue && !value && loaded && (
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle} disabled={!loaded}>
<Icon name="check" size={13} />
</button>
)}
{trailing}
{mask && (
<button
onClick={() => setRevealed(r => !r)}
title={revealed ? t('common.hide') : t('common.show')}
style={iconBtnStyle}
disabled={disabled}
>
<Icon name="eye" size={14} />
</button>
)}
<button
onClick={onCopy}
title={t('common.copy')}
style={iconBtnStyle}
disabled={!value || disabled}
>
<Icon name="eye" size={14} />
<Icon name="copy" size={14} />
</button>
)}
<button
onClick={onCopy}
title={t('common.copy')}
style={iconBtnStyle}
disabled={!value || disabled}
>
<Icon name="copy" size={14} />
</button>
{/* readError 是字段无法读取的持续错误,留在原位提示用户该字段不可用;
其它瞬态状态(saving / saved / saveError / copied / copyError)都通过
emitSaved 发到右上角统一 toast,不再内联占位。 */}
{status === 'readError' && (
<span
style={{
fontSize: 11,
color: 'var(--ol-warn)',
whiteSpace: 'nowrap',
}}
>
{t('settings.providers.readFailed')}
{/* readError 是字段无法读取的持续错误,留在原位提示用户该字段不可用;
其它瞬态状态(saving / saved / saveError / copied / copyError)都通过
emitSaved 发到右上角统一 toast,不再内联占位。 */}
{status === 'readError' && (
<span
style={{
fontSize: 11,
color: 'var(--ol-warn)',
whiteSpace: 'nowrap',
}}
>
{t('settings.providers.readFailed')}
</span>
)}
</div>
{showInsecureAsrEndpointWarning && (
<span style={{ fontSize: 11, color: 'var(--ol-warn)', lineHeight: 1.45 }}>
{t('settings.providers.endpointMustUseHttps')}
</span>
)}
</div>