Skip to content
Merged
2 changes: 1 addition & 1 deletion src-tauri/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ use crate::prompt_files::prompt_file_path;
use crate::provider::ProviderManager;

/// 应用类型
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AppType {
Claude,
Expand Down
44 changes: 40 additions & 4 deletions src-tauri/src/commands/provider.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use indexmap::IndexMap;
use tauri::State;
use tauri::{Emitter, State};

use crate::app_config::AppType;
use crate::commands::copilot::CopilotAuthState;
Expand Down Expand Up @@ -153,19 +153,55 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<
#[allow(non_snake_case)]
#[tauri::command]
pub async fn queryProviderUsage(
app_handle: tauri::AppHandle,
state: State<'_, AppState>,
copilot_state: State<'_, CopilotAuthState>,
#[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端
app: String,
) -> 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 回调,不吞错误。
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}");
}
state.usage_cache.put_script(app_type, providerId, snapshot);
crate::tray::schedule_tray_refresh(&app_handle);
inner
}

async fn query_provider_usage_inner(
state: &AppState,
copilot_state: &CopilotAuthState,
app_type: AppType,
provider_id: &str,
) -> Result<crate::provider::UsageResult, String> {
// 从数据库读取供应商信息,检查特殊模板类型
let providers = state
.db
.get_all_providers(app_type.as_str())
.map_err(|e| format!("Failed to get providers: {e}"))?;
let provider = providers.get(&providerId);
let provider = providers.get(provider_id);
let usage_script = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.usage_script.as_ref());
Expand Down Expand Up @@ -294,7 +330,7 @@ pub async fn queryProviderUsage(
}

// ── 通用 JS 脚本路径 ──
ProviderService::query_usage(state.inner(), app_type, &providerId)
ProviderService::query_usage(state, app_type, provider_id)
.await
.map_err(|e| e.to_string())
}
Expand Down Expand Up @@ -406,7 +442,7 @@ pub fn update_providers_sort_order(

use crate::provider::UniversalProvider;
use std::collections::HashMap;
use tauri::{AppHandle, Emitter};
use tauri::AppHandle;

#[derive(Clone, serde::Serialize)]
pub struct UniversalProviderSyncedEvent {
Expand Down
39 changes: 35 additions & 4 deletions src-tauri/src/commands/subscription.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
use crate::services::subscription::SubscriptionQuota;
use std::str::FromStr;
use tauri::{Emitter, State};

use crate::app_config::AppType;
use crate::services::subscription::{CredentialStatus, SubscriptionQuota};
use crate::store::AppState;

/// 查询官方订阅额度
///
/// 读取 CLI 工具已有的 OAuth 凭据并调用官方 API 获取使用额度。
/// 不需要 AppState(不访问数据库),直接读文件 + 发 HTTP。
/// 结果(无论业务失败还是 transport 层 Err)都会写入 `UsageCache`、通知托盘
/// 刷新,并 emit `usage-cache-updated`,让前端 React Query 与托盘共享同一份
/// 最新数据。失败快照写入后 `format_subscription_summary` 会通过 `success=false`
/// 守卫返回 `None`,托盘 suffix 自然消失,避免长期滞留旧配额数字。
/// Err 原样向前端返回,React Query 的 onError 不会被吞掉。
#[tauri::command]
pub async fn get_subscription_quota(tool: String) -> Result<SubscriptionQuota, String> {
crate::services::subscription::get_subscription_quota(&tool).await
pub async fn get_subscription_quota(
app: tauri::AppHandle,
state: State<'_, AppState>,
tool: String,
) -> Result<SubscriptionQuota, String> {
let inner = crate::services::subscription::get_subscription_quota(&tool).await;
let snapshot = match &inner {
Ok(q) => q.clone(),
// transport 层 Err —— 凭据状态不明,用 Valid 表达"凭据没问题,是通信/parse 出错"。
Err(err_msg) => SubscriptionQuota::error(&tool, CredentialStatus::Valid, err_msg.clone()),
};
if let Ok(app_type) = AppType::from_str(&tool) {
let payload = serde_json::json!({
"kind": "subscription",
"appType": app_type.as_str(),
"data": &snapshot,
});
if let Err(e) = app.emit("usage-cache-updated", payload) {
log::error!("emit usage-cache-updated (subscription) 失败: {e}");
}
state.usage_cache.put_subscription(app_type, snapshot);
crate::tray::schedule_tray_refresh(&app);
}
inner
}
13 changes: 10 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,9 +748,16 @@ pub fn run() {

// 构建托盘
let mut tray_builder = TrayIconBuilder::with_id(tray::TRAY_ID)
.on_tray_icon_event(|_tray, event| match event {
// 左键点击已通过 show_menu_on_left_click(true) 打开菜单,这里不再额外处理
TrayIconEvent::Click { .. } => {}
.on_tray_icon_event(|tray, event| match event {
// 鼠标悬停/点击到托盘图标时,后台异步刷新用量缓存,
// 让用户下一次(或快速打开菜单的那一刻)看到较新的数字。
// refresh_all_usage_in_tray 内部有 10 秒防抖。
TrayIconEvent::Enter { .. } | TrayIconEvent::Click { .. } => {
let app = tray.app_handle().clone();
tauri::async_runtime::spawn(async move {
crate::tray::refresh_all_usage_in_tray(&app).await;
});
}
_ => log::debug!("unhandled event {event:?}"),
})
.menu(&menu)
Expand Down
19 changes: 19 additions & 0 deletions src-tauri/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ impl Provider {
in_failover_queue: false,
}
}

pub fn is_codex_oauth(&self) -> bool {
self.meta.as_ref().and_then(|m| m.provider_type.as_deref()) == Some("codex_oauth")
}

pub fn codex_fast_mode_enabled(&self) -> bool {
self.meta
.as_ref()
.map(|m| m.codex_fast_mode_enabled())
.unwrap_or(false)
}

pub fn has_usage_script_enabled(&self) -> bool {
self.meta
.as_ref()
.and_then(|m| m.usage_script.as_ref())
.map(|s| s.enabled)
.unwrap_or(false)
}
}

/// 供应商管理器
Expand Down
12 changes: 2 additions & 10 deletions src-tauri/src/proxy/providers/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,7 @@ pub fn transform_claude_request_for_api_format(
session_id: Option<&str>,
shadow_store: Option<&super::gemini_shadow::GeminiShadowStore>,
) -> Result<serde_json::Value, ProxyError> {
let is_codex_oauth = provider
.meta
.as_ref()
.and_then(|m| m.provider_type.as_deref())
== Some("codex_oauth");
let is_codex_oauth = provider.is_codex_oauth();

// Copilot 场景:优先从 metadata.user_id 提取 session ID 作为 cache key
// 格式: "uuid_sessionId" → 提取 "_" 后面的部分作为 session 标识
Expand Down Expand Up @@ -151,11 +147,7 @@ pub fn transform_claude_request_for_api_format(
"openai_responses" => {
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
let codex_fast_mode = provider
.meta
.as_ref()
.map(|m| m.codex_fast_mode_enabled())
.unwrap_or(false);
let codex_fast_mode = provider.codex_fast_mode_enabled();
super::transform_responses::anthropic_to_responses(
body,
Some(cache_key),
Expand Down
Loading
Loading