mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
2df2212ceb
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).
92 lines
3.3 KiB
Rust
92 lines
3.3 KiB
Rust
//! Codex OAuth Tauri Commands
|
|
//!
|
|
//! 提供 OpenAI ChatGPT Plus/Pro OAuth 认证相关的 Tauri 命令。
|
|
//!
|
|
//! 大部分认证命令通过通用 `auth_*` 命令(参见 `commands::auth`)暴露给前端,
|
|
//! 此处定义 State wrapper 以及 Codex OAuth 专属的订阅额度和模型列表查询命令。
|
|
|
|
use crate::proxy::providers::codex_oauth_auth::CodexOAuthManager;
|
|
use crate::services::model_fetch::FetchedModel;
|
|
use crate::services::subscription::{query_codex_quota, CredentialStatus, SubscriptionQuota};
|
|
use std::sync::Arc;
|
|
use tauri::State;
|
|
use tokio::sync::RwLock;
|
|
|
|
/// Codex OAuth 认证状态
|
|
pub struct CodexOAuthState(pub Arc<RwLock<CodexOAuthManager>>);
|
|
|
|
/// 查询 Codex OAuth (ChatGPT Plus/Pro) 订阅额度
|
|
///
|
|
/// - `account_id` 未指定时回退到 `CodexOAuthManager` 的默认账号
|
|
/// - 没有任何账号时返回 `not_found`,前端 `SubscriptionQuotaView` 会静默不渲染
|
|
/// - 复用 `services::subscription::query_codex_quota`,因此 wham/usage 端点协议
|
|
/// 与 Codex CLI 路径完全一致
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn get_codex_oauth_quota(
|
|
account_id: Option<String>,
|
|
state: State<'_, CodexOAuthState>,
|
|
) -> Result<SubscriptionQuota, String> {
|
|
let manager = state.0.read().await;
|
|
|
|
// 解析最终使用的账号 ID:显式 > 默认账号 > 无账号 (not_found)
|
|
let resolved = match account_id {
|
|
Some(id) => Some(id),
|
|
None => manager.default_account_id().await,
|
|
};
|
|
let Some(id) = resolved else {
|
|
return Ok(SubscriptionQuota::not_found("codex_oauth"));
|
|
};
|
|
|
|
// 获取(必要时自动刷新)access_token
|
|
let token = match manager.get_valid_token_for_account(&id).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
return Ok(SubscriptionQuota::error(
|
|
"codex_oauth",
|
|
CredentialStatus::Expired,
|
|
format!("Codex OAuth token unavailable: {e}"),
|
|
));
|
|
}
|
|
};
|
|
|
|
// 瞬时传输失败以 Err 传播(前端 reject → retry + 保留上次成功值)。
|
|
query_codex_quota(
|
|
&token,
|
|
Some(&id),
|
|
"codex_oauth",
|
|
"Codex OAuth access token expired or rejected. Please re-login via cc-switch.",
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// 获取 Codex OAuth (ChatGPT Plus/Pro) 可用模型列表
|
|
///
|
|
/// ChatGPT Codex 反代使用 `chatgpt.com/backend-api/codex/*`,不是 OpenAI 兼容
|
|
/// `/v1/models`。这里复用托管 OAuth 账号的 access_token,直接读取 Codex 后端
|
|
/// 暴露的模型列表端点。
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn get_codex_oauth_models(
|
|
account_id: Option<String>,
|
|
state: State<'_, CodexOAuthState>,
|
|
) -> Result<Vec<FetchedModel>, String> {
|
|
let manager = state.0.read().await;
|
|
let resolved = match account_id
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|id| !id.is_empty())
|
|
{
|
|
Some(id) => Some(id.to_string()),
|
|
None => manager.default_account_id().await,
|
|
};
|
|
let Some(id) = resolved else {
|
|
return Err("No ChatGPT account available".to_string());
|
|
};
|
|
|
|
let token = manager
|
|
.get_valid_token_for_account(&id)
|
|
.await
|
|
.map_err(|e| format!("Codex OAuth token unavailable: {e}"))?;
|
|
|
|
crate::services::codex_oauth_models::fetch_models_with_token(&token, &id).await
|
|
}
|