diff --git a/openless-all/app/src-tauri/src/asr/bailian.rs b/openless-all/app/src-tauri/src/asr/bailian.rs index 2aac2fa9..ce225581 100644 --- a/openless-all/app/src-tauri/src/asr/bailian.rs +++ b/openless-all/app/src-tauri/src/asr/bailian.rs @@ -62,6 +62,16 @@ impl BailianCredentials { } } +/// Bailian 实时 ASR 走 DashScope WebSocket 网关,接口地址只接受 ws:// 或 +/// wss://。用户容易把百炼控制台的 `https://` 兼容模式 / 专属域名地址粘进来 +/// ——那是另一套 HTTP 协议,WebSocket 握手必然失败且底层报错 +/// ("URL scheme not supported")对用户不可读。在验证入口先拦下, +/// 前端据 `bailianEndpointSchemeInvalid` 错误码给出可操作的提示。 +pub fn endpoint_scheme_is_websocket(endpoint: &str) -> bool { + let lower = endpoint.trim().to_ascii_lowercase(); + lower.starts_with("wss://") || lower.starts_with("ws://") +} + #[derive(Debug, thiserror::Error)] pub enum BailianASRError { #[error("credentials missing")] @@ -880,6 +890,23 @@ mod tests { // ---- existing tests kept ---- + #[test] + fn endpoint_scheme_accepts_websocket_only() { + assert!(endpoint_scheme_is_websocket(DEFAULT_ENDPOINT)); + assert!(endpoint_scheme_is_websocket("ws://localhost:9000/api-ws")); + assert!(endpoint_scheme_is_websocket( + " WSS://dashscope.aliyuncs.com/api-ws/v1/inference/ " + )); + // 百炼控制台的 https 兼容模式 / 专属域名地址不是 WebSocket 网关 + assert!(!endpoint_scheme_is_websocket( + "https://llm-xxx.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" + )); + assert!(!endpoint_scheme_is_websocket( + "http://dashscope.aliyuncs.com/api-ws/v1/inference/" + )); + assert!(!endpoint_scheme_is_websocket("")); + } + #[test] fn credentials_apply_default_endpoint_and_model() { let creds = BailianCredentials { diff --git a/openless-all/app/src-tauri/src/commands/providers.rs b/openless-all/app/src-tauri/src/commands/providers.rs index d6e29f37..9a376ffd 100644 --- a/openless-all/app/src-tauri/src/commands/providers.rs +++ b/openless-all/app/src-tauri/src/commands/providers.rs @@ -28,6 +28,11 @@ pub async fn validate_provider_credentials(kind: String) -> Result Result { if kind == "asr" && CredentialsVault::get_active_asr() == crate::asr::bailian::PROVIDER_ID { + // Bailian 实时 ASR 没有模型列表 HTTP 接口,列表只能是静态的。但直接返回 + // 硬编码列表会让「拉取模型」在 Key / endpoint 全错时也显示成功,给用户 + // "已连通"的错觉(其他厂商这颗按钮是真实带 Key GET /models)。先跑一次 + // 与「验证」相同的 WebSocket 连通性检查,语义对齐后再返回静态列表。 + validate_bailian_asr_provider().await?; return Ok(ProviderModelsResult { models: vec![crate::asr::bailian::DEFAULT_MODEL.to_string()], }); @@ -229,6 +234,12 @@ async fn validate_bailian_asr_provider() -> Result<(), String> { .map_err(|e| e.to_string())? .filter(|s| !s.trim().is_empty()) .unwrap_or_else(|| crate::asr::bailian::DEFAULT_ENDPOINT.to_string()); + // 协议头先行校验:填成 https://(百炼兼容模式 / 专属域名地址)时,WebSocket + // 握手报的 "URL scheme not supported" 会被前端兜底成笼统的「操作失败」, + // 用户无从定位。这里拦下并返回专用错误码,前端映射成可操作的提示。 + if !crate::asr::bailian::endpoint_scheme_is_websocket(&endpoint) { + return Err("bailianEndpointSchemeInvalid".to_string()); + } let model = CredentialsVault::get(CredentialAccount::AsrModel) .map_err(|e| e.to_string())? .filter(|s| !s.trim().is_empty()) @@ -245,9 +256,12 @@ async fn validate_bailian_asr_provider() -> Result<(), String> { }, )); asr.open_session().await.map_err(|e| e.to_string())?; + // 验证音频必须 ≥200ms:只发 1 个 100ms 静音块时 DashScope 必然返回 + // task-failed: EmptyAudio,导致有效凭据也永远验证失败(2026-07 实测边界: + // 100ms 拒、200ms 起收)。取 500ms 留余量,与 Mimo 验证的 250ms 同量级。 crate::asr::AudioConsumer::consume_pcm_chunk( &*asr, - &vec![0u8; crate::asr::bailian::TARGET_AUDIO_CHUNK_BYTES], + &vec![0u8; crate::asr::bailian::TARGET_AUDIO_CHUNK_BYTES * 5], ); asr.send_last_frame().await.map_err(|e| e.to_string())?; asr.await_final_result() diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index af5ae6da..200bd48f 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -837,6 +837,7 @@ export const en: typeof zhCN = { providerHttpStatus: 'Provider returned HTTP {{status}}. Check the API key permissions or endpoint.', endpointMustUseHttps: 'HTTP endpoints are allowed, but API keys and audio content may leak in transit.', endpointInvalid: 'Endpoint format is invalid.', + bailianEndpointSchemeInvalid: 'Bailian realtime ASR uses the DashScope WebSocket gateway: the endpoint must start with wss:// (default: wss://dashscope.aliyuncs.com/api-ws/v1/inference/). An https:// compatible-mode URL will not work here.', responseTooLarge: 'Provider response is too large to validate safely.', asrInvalidJson: 'ASR response is not valid JSON.', asrMissingTextField: 'ASR response is missing the text field.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 63a64688..77142167 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -839,6 +839,7 @@ export const ja: typeof zhCN = { providerHttpStatus: 'サプライヤーが {{status}} を返しました。API Key 権限またはエンドポイントを確認してください。', endpointMustUseHttps: 'HTTP Endpoint も使用できますが、API Key と音声内容が通信中に漏えいする可能性があります。', endpointInvalid: 'Endpoint の形式が無効です。', + bailianEndpointSchemeInvalid: 'Bailian リアルタイム ASR は DashScope の WebSocket ゲートウェイを使用します。エンドポイントは wss:// で始まる必要があります(既定: wss://dashscope.aliyuncs.com/api-ws/v1/inference/)。https:// の互換モード URL はここでは使用できません。', responseTooLarge: 'サプライヤーの応答が大きすぎるため、安全のため検証を停止しました。', asrInvalidJson: 'ASR の応答が有効な JSON ではありません。', asrMissingTextField: 'ASR の応答に text フィールドがありません。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 2b33b2ab..699966d4 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -839,6 +839,7 @@ export const ko: typeof zhCN = { providerHttpStatus: '공급자가 {{status}} 를 반환했습니다. API Key 권한 또는 Endpoint 를 확인해 주세요.', endpointMustUseHttps: 'HTTP Endpoint 를 사용할 수 있지만, API Key 와 음성 내용이 전송 중 유출될 수 있습니다.', endpointInvalid: 'Endpoint 형식이 올바르지 않습니다.', + bailianEndpointSchemeInvalid: 'Bailian 실시간 ASR은 DashScope WebSocket 게이트웨이를 사용합니다. 엔드포인트는 wss://로 시작해야 합니다(기본값: wss://dashscope.aliyuncs.com/api-ws/v1/inference/). https:// 호환 모드 주소는 여기서 사용할 수 없습니다.', responseTooLarge: '공급자 응답이 너무 커서 안전을 위해 검증을 중단했습니다.', asrInvalidJson: 'ASR 응답이 유효한 JSON 이 아닙니다.', asrMissingTextField: 'ASR 응답에 text 필드가 없습니다.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 1207c7ac..04b5a8df 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -835,6 +835,7 @@ export const zhCN = { providerHttpStatus: '供应商接口返回 {{status}},请检查 API Key 权限或 Endpoint。', endpointMustUseHttps: '允许使用 HTTP Endpoint,但请注意:API Key 和音频内容可能在传输中泄漏。', endpointInvalid: 'Endpoint 格式不合法。', + bailianEndpointSchemeInvalid: '百炼实时 ASR 走 DashScope WebSocket 网关,接口地址必须以 wss:// 开头(默认 wss://dashscope.aliyuncs.com/api-ws/v1/inference/);https:// 的兼容模式地址在此不可用。', responseTooLarge: '供应商响应过大,已停止验证以保证安全。', asrInvalidJson: 'ASR 响应不是有效 JSON。', asrMissingTextField: 'ASR 响应缺少 text 字段。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index dc6b6da0..0e316d29 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -837,6 +837,7 @@ export const zhTW: typeof zhCN = { providerHttpStatus: '供應商接口返回 {{status}},請檢查 API Key 權限或 Endpoint。', endpointMustUseHttps: '允許使用 HTTP Endpoint,但請注意:API Key 和音訊內容可能在傳輸中外洩。', endpointInvalid: 'Endpoint 格式不合法。', + bailianEndpointSchemeInvalid: '百煉即時 ASR 走 DashScope WebSocket 閘道,接口地址必須以 wss:// 開頭(預設 wss://dashscope.aliyuncs.com/api-ws/v1/inference/);https:// 的相容模式地址在此不可用。', responseTooLarge: '供應商響應過大,已停止驗證以保證安全。', asrInvalidJson: 'ASR 響應不是有效 JSON。', asrMissingTextField: 'ASR 響應缺少 text 字段。', diff --git a/openless-all/app/src/pages/settings/ProvidersSection.tsx b/openless-all/app/src/pages/settings/ProvidersSection.tsx index 3a2cff4e..63012cce 100644 --- a/openless-all/app/src/pages/settings/ProvidersSection.tsx +++ b/openless-all/app/src/pages/settings/ProvidersSection.tsx @@ -621,6 +621,7 @@ function providerErrorMessage(error: unknown, t: ReturnType