mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
fix(usage): reject transient transport failures so retry and keep-last-good work
Usage/quota queries frequently showed spurious "query failed" states that manual refresh could not clear (#3820). Root cause: all transport-level failures were folded into Ok(success:false), so react-query's retry never fired and the failure body poisoned the cache as regular data. Backend: - balance/coding_plan/subscription services now return Err for send failures and body-read failures (read body via bytes() before serde_json::from_slice; reqwest's json() wraps read errors as Decode, making error-kind checks on it dead code). Auth/4xx/parse errors stay Ok(success:false) and surface immediately. - Script path maps transient AppError keys (request_failed / read_response_failed) to Err; Volcengine adds a Transient call variant. - Command layer skips snapshot persistence, usage-cache-updated emit and tray refresh on Err so the cache bridge cannot overwrite retained data. - Expired-credential retry propagates transient errors instead of rewriting them as "OAuth token has expired". Frontend: - resolveDisplayUsage generalized to subscription quotas; 5th param is now an options object with a `rejected` flag: stale success data retained by react-query across rejections is re-anchored to dataUpdatedAt and expires through the same 10-minute keep-last-good window instead of being shown indefinitely (a total outage used to mask longer than a single 5xx). - useSubscriptionQuota / useCodexOauthQuota gain keep-last-good with per-scope snapshot reset; rejected queries with no displayable value synthesize a failure placeholder carrying the real error message so footers keep rendering a retry entry point. - HTTP 429 is classified transient (retry-later) alongside 5xx. - UsageScriptModal surfaces string rejections via extractErrorMessage. Tests: 6 backend behavior tests drive real HTTP against a local listener to pin the Err/Ok channel semantics; frontend suite extends keepLastGoodUsage coverage (405 passing).
This commit is contained in:
@@ -381,32 +381,30 @@ pub async fn queryProviderUsage(
|
||||
) -> Result<crate::provider::UsageResult, String> {
|
||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||
// inner 可能以两种形式失败:
|
||||
// 1) 返回 Ok(UsageResult { success: false, .. }) —— 业务失败(401、脚本报错等)
|
||||
// 2) 返回 Err(String) —— RPC/DB/Copilot fetch_usage 等 transport 层失败
|
||||
// 两种都要把"失败"写进 UsageCache 并刷新托盘,让 format_script_summary 的
|
||||
// success 守卫生效、suffix 自然消失,避免旧 success 快照长期滞留。
|
||||
// 同时保持原始 Err 返回给前端 React Query 的 onError 回调,不吞错误。
|
||||
// 1) 返回 Ok(UsageResult { success: false, .. }) —— 确定性失败(401、脚本
|
||||
// 报错、未知供应商等)。写进 UsageCache 并刷新托盘,让
|
||||
// format_script_summary 的 success 守卫生效、suffix 自然消失。
|
||||
// 2) 返回 Err(String) —— 瞬时传输失败(网络/超时)及 DB/Copilot fetch 等。
|
||||
// 不写失败快照、不 emit:保留上一份托盘快照,与前端 react-query reject
|
||||
// 保留上次 data 的语义一致;否则失败快照会经 useUsageCacheBridge 盲写
|
||||
// 回 query 缓存,抹掉 reject 本该保留的旧值。
|
||||
let inner =
|
||||
query_provider_usage_inner(&state, &copilot_state, app_type.clone(), &providerId).await;
|
||||
let snapshot = match &inner {
|
||||
Ok(r) => r.clone(),
|
||||
Err(err_msg) => crate::provider::UsageResult {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(err_msg.clone()),
|
||||
},
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"kind": "script",
|
||||
"appType": app_type.as_str(),
|
||||
"providerId": &providerId,
|
||||
"data": &snapshot,
|
||||
});
|
||||
if let Err(e) = app_handle.emit("usage-cache-updated", payload) {
|
||||
log::error!("emit usage-cache-updated (script) 失败: {e}");
|
||||
if let Ok(snapshot) = &inner {
|
||||
let payload = serde_json::json!({
|
||||
"kind": "script",
|
||||
"appType": app_type.as_str(),
|
||||
"providerId": &providerId,
|
||||
"data": snapshot,
|
||||
});
|
||||
if let Err(e) = app_handle.emit("usage-cache-updated", payload) {
|
||||
log::error!("emit usage-cache-updated (script) 失败: {e}");
|
||||
}
|
||||
state
|
||||
.usage_cache
|
||||
.put_script(app_type, providerId, snapshot.clone());
|
||||
crate::tray::schedule_tray_refresh(&app_handle);
|
||||
}
|
||||
state.usage_cache.put_script(app_type, providerId, snapshot);
|
||||
crate::tray::schedule_tray_refresh(&app_handle);
|
||||
inner
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user