Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 44 additions & 36 deletions src-tauri/src/cli/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,37 +354,41 @@ fn reset_quota(
confirm: bool,
json: bool,
) -> Result<(), AppError> {
// Resolve the default exactly once: inspection and consumption must target the same account.
let account_id = match account_id {
Some(id) => normalize_account_id(&id)?.to_string(),
None => runtime
.block_on(AuthService::get_status(AUTH_PROVIDER_CODEX_OAUTH))
.map_err(AppError::Message)?
.default_account_id
.ok_or_else(|| {
AppError::localized(
"quota.reset.no_account",
"没有可用的 ChatGPT 账号",
"No ChatGPT account available",
)
})?,
};
let quota = runtime
.block_on(AuthService::get_quota(
AUTH_PROVIDER_CODEX_OAUTH,
account_id.as_deref(),
Some(&account_id),
))
.map_err(AppError::Message)?;

let summary = quota.reset_credits.as_ref();
let available_count = summary.map(|s| s.available_count).unwrap_or(0);
if available_count == 0 {
return Err(AppError::Message(
"No rate limit reset credits are available for this account.".to_string(),
));
}

let credits = summary.map(|s| &s.credits[..]).unwrap_or(&[]);
let selected_credit = match &credit_id {
Some(cid) => credits.iter().find(|c| &c.id == cid).cloned().ok_or_else(|| {
AppError::Message(format!(
"Specified credit ID '{cid}' was not found in available credits."
))
})?,
None => credits.first().cloned().ok_or_else(|| {
AppError::Message("No available reset credit to consume.".to_string())
})?,
};
let selected_credit = super::quota_reset::select_credit(&quota, credit_id.as_deref())?;
let available_count = quota
.reset_credits
.as_ref()
.map(|s| s.available_count)
.unwrap_or(0);
let command = super::quota_reset::auth_confirmation(&account_id, &selected_credit.id)?;

if !confirm {
if json {
let dry_run = serde_json::json!({
"dryRun": true,
"accountId": account_id,
"confirmationCommand": command,
"targetCredit": selected_credit,
"availableCount": available_count,
"message": "To redeem this credit and reset rate limits, run again with --confirm.",
Expand All @@ -397,6 +401,15 @@ fn reset_quota(
}

println!("{}", highlight("=== Rate Limit Reset Credit (Dry Run) ==="));
println!(
"{}: {}",
if crate::cli::i18n::is_chinese() {
"账号 ID"
} else {
"Account ID"
},
account_id
);
println!("Credit ID: {}", selected_credit.id);
if let Some(title) = &selected_credit.title {
println!("Title: {}", title);
Expand All @@ -412,18 +425,22 @@ fn reset_quota(
);
println!(
"{}",
info(
"To consume this credit, re-run with --confirm:
cc-switch auth reset-quota --confirm"
)
info(&format!(
"{}\n {command}",
if crate::cli::i18n::is_chinese() {
"确认兑换此卡,请执行:"
} else {
"To consume this credit, run:"
}
))
);
return Ok(());
}

let result = runtime
.block_on(AuthService::reset_quota(
AUTH_PROVIDER_CODEX_OAUTH,
account_id.as_deref(),
Some(&account_id),
Some(&selected_credit.id),
))
.map_err(AppError::Message)?;
Expand All @@ -446,15 +463,6 @@ fn reset_quota(
if let Some(win) = result.windows_reset {
println!("Windows Reset: {}", win);
}
if let Some(new_q) = &result.new_quota {
println!();
println!("{}", highlight("Updated Quota:"));
for tier in &new_q.tiers {
println!(" {}: {:.1}%", tier.name, tier.utilization);
}
if let Some(rc) = &new_q.reset_credits {
println!(" Remaining Credits: {}", rc.available_count);
}
}
super::quota_reset::print_updated_quota(result.new_quota.as_ref());
Ok(())
}
1 change: 1 addition & 0 deletions src-tauri/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod provider_input;
mod provider_inspect;
pub mod provider_usage_query;
pub mod proxy;
mod quota_reset;
pub mod sessions;
pub mod settings;
pub mod skills;
Expand Down
117 changes: 79 additions & 38 deletions src-tauri/src/cli/commands/provider_inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ pub(crate) fn quota_provider(
let target = quota_target_for_provider(&app_type, id, provider);
let queried_at = chrono::Utc::now().timestamp_millis();

let output = if let Some(target) = target {
let output = if let Some(mut target) = target {
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| AppError::Message(format!("Failed to create async runtime: {}", e)))?;

Expand All @@ -451,45 +451,68 @@ pub(crate) fn quota_provider(
|| matches!(&target.kind, QuotaTargetKind::SubscriptionTool { tool } if tool == "codex");
if !is_codex {
return Err(AppError::Message(
"Rate limit reset credits are only supported for Codex / OpenAI accounts.".to_string(),
"Rate limit reset credits are only supported for Codex / OpenAI accounts."
.to_string(),
));
}

if let QuotaTargetKind::CodexOAuth { account_id } = &mut target.kind {
if account_id.is_none() {
*account_id = Some(
runtime
.block_on(CodexOAuthService::get_status())
.default_account_id
.ok_or_else(|| {
AppError::localized(
"quota.reset.no_account",
"没有可用的 ChatGPT 账号",
"No ChatGPT account available",
)
})?,
);
}
}

let current_quota = match runtime.block_on(query_quota(&target)) {
Ok(ProviderUsageQuota::Subscription(q)) => q,
Ok(_) => {
return Err(AppError::Message("Unexpected quota response format".to_string()));
return Err(AppError::Message(
"Unexpected quota response format".to_string(),
));
}
Err(e) => {
return Err(AppError::Message(format!("Failed to query current quota: {e}")));
return Err(AppError::Message(format!(
"Failed to query current quota: {e}"
)));
}
};

let summary = current_quota.reset_credits.as_ref();
let available_count = summary.map(|s| s.available_count).unwrap_or(0);
if available_count == 0 {
return Err(AppError::Message(
"No rate limit reset credits are available for this account.".to_string(),
));
}

let credits = summary.map(|s| &s.credits[..]).unwrap_or(&[]);
let selected_credit = match &credit_id {
Some(cid) => credits.iter().find(|c| &c.id == cid).cloned().ok_or_else(|| {
AppError::Message(format!(
"Specified credit ID '{cid}' was not found in available credits."
))
})?,
None => credits.first().cloned().ok_or_else(|| {
AppError::Message("No available reset credit to consume.".to_string())
})?,
let selected_credit =
super::quota_reset::select_credit(&current_quota, credit_id.as_deref())?;
let available_count = current_quota
.reset_credits
.as_ref()
.map(|s| s.available_count)
.unwrap_or(0);
let account_id = match &target.kind {
QuotaTargetKind::CodexOAuth { account_id } => account_id.as_deref(),
_ => None,
};
let command = super::quota_reset::provider_confirmation(
app_type.as_str(),
id,
&selected_credit.id,
account_id,
)?;

if !confirm {
if json {
let dry_run = serde_json::json!({
"dryRun": true,
"providerId": id,
"app": app_type,
"accountId": account_id,
"confirmationCommand": command,
"targetCredit": selected_credit,
"availableCount": available_count,
"message": "To redeem this credit and reset rate limits, run again with --confirm.",
Expand All @@ -503,6 +526,17 @@ pub(crate) fn quota_provider(

println!("{}", highlight("=== Rate Limit Reset Credit (Dry Run) ==="));
println!("Provider: {} ({})", provider_name, id);
if let Some(account_id) = account_id {
println!(
"{}: {}",
if crate::cli::i18n::is_chinese() {
"账号 ID"
} else {
"Account ID"
},
account_id
);
}
println!("Credit ID: {}", selected_credit.id);
if let Some(title) = &selected_credit.title {
println!("Title: {}", title);
Expand All @@ -519,9 +553,12 @@ pub(crate) fn quota_provider(
println!(
"{}",
info(&format!(
"To consume this credit, re-run with --confirm:
cc-switch provider quota {} --reset --confirm",
id
"{}\n {command}",
if crate::cli::i18n::is_chinese() {
"确认兑换此卡,请执行:"
} else {
"To consume this credit, run:"
}
))
);
return Ok(());
Expand Down Expand Up @@ -549,16 +586,7 @@ pub(crate) fn quota_provider(
if let Some(win) = result.windows_reset {
println!("Windows Reset: {}", win);
}
if let Some(new_q) = &result.new_quota {
println!();
println!("{}", highlight("Updated Quota:"));
for tier in &new_q.tiers {
println!(" {}: {:.1}%", tier.name, tier.utilization);
}
if let Some(rc) = &new_q.reset_credits {
println!(" Remaining Credits: {}", rc.available_count);
}
}
super::quota_reset::print_updated_quota(result.new_quota.as_ref());
return Ok(());
}

Expand Down Expand Up @@ -586,7 +614,8 @@ pub(crate) fn quota_provider(
} else {
if reset {
return Err(AppError::Message(
"Rate limit reset credits are only supported for Codex / OpenAI accounts.".to_string(),
"Rate limit reset credits are only supported for Codex / OpenAI accounts."
.to_string(),
));
}
ProviderQuotaOutput {
Expand Down Expand Up @@ -830,15 +859,27 @@ fn push_subscription_quota_lines(
lines.push(line);
}
if let Some(reset_credits) = &quota.reset_credits {
if reset_credits.available_count > 0 {
if reset_credits.inspection_error.is_some() {
lines.push(
if crate::cli::i18n::is_chinese() {
"重置卡查询失败,请重试查询。"
} else {
"Reset credit inspection failed; retry the query."
}
.to_string(),
);
} else if reset_credits.available_count > 0 {
lines.push(format!(
"Reset Credits: {} available",
reset_credits.available_count
));
for credit in &reset_credits.credits {
let expires = credit.expires_at.as_deref().unwrap_or("never");
let title = credit.title.as_deref().unwrap_or("Rate Limit Reset");
lines.push(format!(" - {} (expires: {}) [{}]", title, expires, credit.id));
lines.push(format!(
" - {} (expires: {}) [{}]",
title, expires, credit.id
));
}
}
}
Expand Down
Loading
Loading