fix: ASR 切换强制覆盖 endpoint/model + 重试只限连接层失败
处理 PR-Agent 第 8 轮: - ProvidersSection.onAsrProviderChange: asr.endpoint / asr.model 是所有 ASR 厂商 共用的凭据槽,原「仅槽空时填」逻辑使跨厂商切换不生效(dropdown 切了、实际仍 打旧厂商地址)。改为强制覆盖预设默认值,与 onLlmProviderChange 一致。 - net.rs send_with_retry: 重试范围从 is_connect() || is_request() 收窄到只剩 is_connect()。is_request() 多为确定性失败(endpoint 配置错误等),重试 10 次 只是徒增数秒延迟才暴露错误;连接层失败才是真正瞬时、值得重试的一类。
This commit is contained in:
1 parent
00b2bd0a8d
commit
99f22a6aee
2 files changed
+19
-23
No files matched your search
@@ -7,11 +7,12 @@
|
||||
//! 这里提供两件东西:
|
||||
//! - `http()`:进程级共享客户端。一次握手成功后的连接进连接池,后续命令直接复用,
|
||||
//! 不再付握手成本。
|
||||
//! - `send_with_retry`:对「连接失败 / 请求未送出」这两类传输层失败做指数退避
|
||||
//! 重试 —— 它们都发生在请求送达服务端之前,重试幂等安全。**不重试超时**:
|
||||
//! reqwest 的 `is_timeout()` 也涵盖「请求已发出、等响应时超时」,此时服务端可能
|
||||
//! 已收到并在处理,重试 POST / DELETE 会重复执行。HTTP 4xx/5xx 同样不重试 ——
|
||||
//! 服务端已应答,状态码交给调用方判断。
|
||||
//! - `send_with_retry`:只对**连接层失败**(`is_connect()` —— 握手重置 / 连接被拒
|
||||
//! 等)做指数退避重试。这类失败发生在请求送达服务端之前、且通常是瞬时的(代理
|
||||
//! 分流抖动等),重试既幂等安全又有意义。**不重试超时与其他请求层错误**:超时
|
||||
//! 可能发生在服务端已收到之后(重试 POST / DELETE 会重复执行);`is_request()`
|
||||
//! 类错误多为确定性失败(如 endpoint 配置错误),重试只是徒增数秒延迟。HTTP
|
||||
//! 4xx/5xx 同样不重试 —— 服务端已应答,状态码交给调用方判断。
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -38,12 +39,13 @@ pub fn http() -> &'static reqwest::Client {
|
||||
/// 单次请求最多尝试的次数。失败本身很快(握手重置 ~0.5s),10 次总耗时仍可控。
|
||||
const MAX_ATTEMPTS: u32 = 10;
|
||||
|
||||
/// 发送请求,对连接层失败(连接重置 / 请求未送出)做指数退避重试。
|
||||
/// 发送请求,只对连接层失败(`is_connect()`:握手重置 / 连接被拒等)做指数退避重试。
|
||||
///
|
||||
/// `make` 每次尝试都重新构造 `RequestBuilder`(`send()` 会消耗它)。只重试
|
||||
/// `is_connect()` / `is_request()` —— 这两类失败时请求尚未送达服务端,重试幂等
|
||||
/// 安全。**超时不重试**:服务端可能已收到并在处理,重试非幂等请求(POST / DELETE)
|
||||
/// 会重复执行。拿到任意 HTTP 响应(含 4xx/5xx)即返回,状态码由调用方自行判断。
|
||||
/// `is_connect()` —— 连接尚未建立、请求未送达服务端,且这类失败通常是瞬时的,
|
||||
/// 重试幂等安全且有价值。超时(可能服务端已在处理)与其他 `is_request()` 类错误
|
||||
/// (多为 endpoint 配置错误等确定性失败)都不重试。拿到任意 HTTP 响应(含
|
||||
/// 4xx/5xx)即返回,状态码由调用方自行判断。
|
||||
pub async fn send_with_retry<F>(make: F) -> reqwest::Result<reqwest::Response>
|
||||
where
|
||||
F: Fn() -> reqwest::RequestBuilder,
|
||||
@@ -54,7 +56,7 @@ where
|
||||
match make().send().await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(err) => {
|
||||
let retryable = err.is_connect() || err.is_request();
|
||||
let retryable = err.is_connect();
|
||||
if !retryable || attempt >= MAX_ATTEMPTS {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
@@ -270,25 +270,19 @@ export function ProvidersSection() {
|
||||
await updatePrefs(next);
|
||||
if (seq !== asrSwitchSeqRef.current) return;
|
||||
}
|
||||
// OpenAI 兼容厂商首次切换时预填 baseUrl / model 默认值,省得用户必踩
|
||||
// 「跨厂商 model 名根本不一样」的坑;但用户已自定义后就不再覆盖。
|
||||
// volcengine 走另一套凭据,跳过。
|
||||
// asr.endpoint / asr.model 是所有 ASR 厂商共用的一对凭据槽(persistence.rs
|
||||
// 未做 per-provider 隔离)。若只在槽空时填默认值,老用户从 A 厂商切到 B 厂商
|
||||
// 时槽里仍是 A 的 endpoint/model —— dropdown 切了、实际还打 A 的地址。改成切到
|
||||
// 有默认值的预设就强制覆盖,让切换真切到位。volcengine 走另一套凭据、本地引擎
|
||||
// 无 baseUrl,都被 if 守卫天然跳过。与 onLlmProviderChange 同款修法。
|
||||
const preset = ASR_PRESETS.find(p => p.id === id);
|
||||
if (preset && preset.baseUrl) {
|
||||
const existing = await readCredential('asr.endpoint');
|
||||
await setCredential('asr.endpoint', preset.baseUrl);
|
||||
if (seq !== asrSwitchSeqRef.current) return;
|
||||
if (!existing) {
|
||||
await setCredential('asr.endpoint', preset.baseUrl);
|
||||
if (seq !== asrSwitchSeqRef.current) return;
|
||||
}
|
||||
}
|
||||
if (preset && preset.model) {
|
||||
const existing = await readCredential('asr.model');
|
||||
await setCredential('asr.model', preset.model);
|
||||
if (seq !== asrSwitchSeqRef.current) return;
|
||||
if (!existing) {
|
||||
await setCredential('asr.model', preset.model);
|
||||
if (seq !== asrSwitchSeqRef.current) return;
|
||||
}
|
||||
}
|
||||
setCommittedAsrProvider(id);
|
||||
emitSaved('saved', t('common.saved'));
|
||||
|
||||
Reference in new issue
Block a user