mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30: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:
@@ -65,6 +65,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
const {
|
||||
data: usage,
|
||||
isFetching: loading,
|
||||
isError,
|
||||
lastQueriedAt,
|
||||
refetch,
|
||||
} = useUsageQuery(providerId, appId, {
|
||||
@@ -86,11 +87,13 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
return () => clearInterval(interval);
|
||||
}, [lastQueriedAt]);
|
||||
|
||||
// 只在启用用量查询且有数据时显示
|
||||
if (!usageEnabled || !usage) return null;
|
||||
// 只在启用用量查询且有数据时显示。后端把瞬时传输失败转成了 reject:有缓存
|
||||
// 成功值时 react-query 保留 data 照常展示;首次查询就失败则 data 为空——
|
||||
// 此时(isError)仍要渲染失败态给出重试入口,否则 footer 整体消失、无从重查。
|
||||
if (!usageEnabled || (!usage && !isError)) return null;
|
||||
|
||||
// 错误状态
|
||||
if (!usage.success) {
|
||||
// 错误状态(业务失败,或无缓存成功值的 reject)
|
||||
if (!usage || !usage.success) {
|
||||
if (inline) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2 text-xs rounded-lg border border-border-default bg-card px-3 py-2 shadow-sm">
|
||||
@@ -115,7 +118,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2 text-red-500 dark:text-red-400">
|
||||
<AlertCircle size={14} />
|
||||
<span>{usage.error || t("usage.queryFailed")}</span>
|
||||
<span>{usage?.error || t("usage.queryFailed")}</span>
|
||||
</div>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { usageApi, settingsApi, type AppId } from "@/lib/api";
|
||||
import { copilotGetUsage, copilotGetUsageForAccount } from "@/lib/api/copilot";
|
||||
import { useSettingsQuery } from "@/lib/query";
|
||||
import { resolveManagedAccountId } from "@/lib/authBinding";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { useDarkMode } from "@/hooks/useDarkMode";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
@@ -663,8 +664,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 后端命令 Err(String) 时 invoke 以裸字符串 reject(如瞬时网络失败),
|
||||
// 直接读 .message 会得到 undefined。
|
||||
toast.error(
|
||||
`${t("usageScript.testFailed")}: ${error?.message || t("common.unknown")}`,
|
||||
`${t("usageScript.testFailed")}: ${extractErrorMessage(error) || t("common.unknown")}`,
|
||||
{
|
||||
duration: 5000,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user