Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions openless-all/app/src-tauri/src/asr/bailian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 15 additions & 1 deletion openless-all/app/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub async fn validate_provider_credentials(kind: String) -> Result<ProviderCheck
#[tauri::command]
pub async fn list_provider_models(kind: String) -> Result<ProviderModelsResult, String> {
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()],
});
Expand Down Expand Up @@ -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())
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 フィールドがありません。',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 필드가 없습니다.',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 字段。',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 字段。',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/pages/settings/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ function providerErrorMessage(error: unknown, t: ReturnType<typeof useTranslatio
}
if (message === 'endpointMustUseHttps') return t('settings.providers.endpointMustUseHttps');
if (message === 'endpointInvalid') return t('settings.providers.endpointInvalid');
if (message === 'bailianEndpointSchemeInvalid') return t('settings.providers.bailianEndpointSchemeInvalid');
if (message === 'providerResponseTooLarge') return t('settings.providers.responseTooLarge');
if (message === 'asrInvalidJson') return t('settings.providers.asrInvalidJson');
if (message === 'asrMissingTextField') return t('settings.providers.asrMissingTextField');
Expand Down
Loading