mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 16:56:16 +08:00
6a34253934
Adds a new managed OAuth provider that lets Claude Code route requests through a user's ChatGPT Plus/Pro subscription via the chatgpt.com backend-api/codex endpoint. - CodexOAuthManager: OpenAI Device Code flow with multi-account support, JWT-based account identification, and automatic access_token refresh. - Reuses the generic managed-auth command surface (auth_start_login, auth_poll_for_account, etc.) via provider dispatch in commands/auth.rs. - ClaudeAdapter detects codex_oauth providers, forces the base URL to the ChatGPT backend, pins api_format to openai_responses, and emits Authorization + originator headers; the forwarder injects the dynamic access_token and ChatGPT-Account-Id per request. - transform_responses gains an is_codex_oauth path that aligns the body with OpenAI's codex-rs ResponsesApiRequest contract: sets store:false, appends reasoning.encrypted_content to include, strips max_output_tokens / temperature / top_p, injects default instructions/tools/parallel_tool_calls, and forces stream:true. Covered by 9 new unit tests plus regression guards for the non-Codex path. - Stream check reuses the same transform flag so detection matches the production request shape. - Frontend adds CodexOAuthSection + useCodexOauth hook, integrates it into ClaudeFormFields / ProviderForm / AuthCenterPanel, ships a new "Codex (ChatGPT Plus/Pro)" preset, and adds zh/en/ja i18n strings.
299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
use tauri::State;
|
|
|
|
use crate::commands::codex_oauth::CodexOAuthState;
|
|
use crate::commands::copilot::CopilotAuthState;
|
|
use crate::proxy::providers::codex_oauth_auth::CodexOAuthError;
|
|
use crate::proxy::providers::copilot_auth::{
|
|
CopilotAuthError, GitHubAccount, GitHubDeviceCodeResponse,
|
|
};
|
|
|
|
const AUTH_PROVIDER_GITHUB_COPILOT: &str = "github_copilot";
|
|
const AUTH_PROVIDER_CODEX_OAUTH: &str = "codex_oauth";
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct ManagedAuthAccount {
|
|
pub id: String,
|
|
pub provider: String,
|
|
pub login: String,
|
|
pub avatar_url: Option<String>,
|
|
pub authenticated_at: i64,
|
|
pub is_default: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct ManagedAuthStatus {
|
|
pub provider: String,
|
|
pub authenticated: bool,
|
|
pub default_account_id: Option<String>,
|
|
pub migration_error: Option<String>,
|
|
pub accounts: Vec<ManagedAuthAccount>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct ManagedAuthDeviceCodeResponse {
|
|
pub provider: String,
|
|
pub device_code: String,
|
|
pub user_code: String,
|
|
pub verification_uri: String,
|
|
pub expires_in: u64,
|
|
pub interval: u64,
|
|
}
|
|
|
|
fn ensure_auth_provider(auth_provider: &str) -> Result<&'static str, String> {
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => Ok(AUTH_PROVIDER_GITHUB_COPILOT),
|
|
AUTH_PROVIDER_CODEX_OAUTH => Ok(AUTH_PROVIDER_CODEX_OAUTH),
|
|
_ => Err(format!("Unsupported auth provider: {auth_provider}")),
|
|
}
|
|
}
|
|
|
|
fn map_account(
|
|
provider: &str,
|
|
account: GitHubAccount,
|
|
default_account_id: Option<&str>,
|
|
) -> ManagedAuthAccount {
|
|
ManagedAuthAccount {
|
|
is_default: default_account_id == Some(account.id.as_str()),
|
|
id: account.id,
|
|
provider: provider.to_string(),
|
|
login: account.login,
|
|
avatar_url: account.avatar_url,
|
|
authenticated_at: account.authenticated_at,
|
|
}
|
|
}
|
|
|
|
fn map_device_code_response(
|
|
provider: &str,
|
|
response: GitHubDeviceCodeResponse,
|
|
) -> ManagedAuthDeviceCodeResponse {
|
|
ManagedAuthDeviceCodeResponse {
|
|
provider: provider.to_string(),
|
|
device_code: response.device_code,
|
|
user_code: response.user_code,
|
|
verification_uri: response.verification_uri,
|
|
expires_in: response.expires_in,
|
|
interval: response.interval,
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_start_login(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<ManagedAuthDeviceCodeResponse, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let response = auth_manager
|
|
.start_device_flow()
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(map_device_code_response(auth_provider, response))
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let response = auth_manager
|
|
.start_device_flow()
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(map_device_code_response(auth_provider, response))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_poll_for_account(
|
|
auth_provider: String,
|
|
device_code: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<Option<ManagedAuthAccount>, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
match auth_manager.poll_for_token(&device_code).await {
|
|
Ok(account) => {
|
|
let default_account_id = auth_manager.get_status().await.default_account_id;
|
|
Ok(account.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
}))
|
|
}
|
|
Err(CopilotAuthError::AuthorizationPending) => Ok(None),
|
|
Err(e) => Err(e.to_string()),
|
|
}
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
match auth_manager.poll_for_token(&device_code).await {
|
|
Ok(account) => {
|
|
let default_account_id = auth_manager.get_status().await.default_account_id;
|
|
Ok(account.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
}))
|
|
}
|
|
Err(CodexOAuthError::AuthorizationPending) => Ok(None),
|
|
Err(e) => Err(e.to_string()),
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_list_accounts(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<Vec<ManagedAuthAccount>, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let status = auth_manager.get_status().await;
|
|
let default_account_id = status.default_account_id.clone();
|
|
Ok(status
|
|
.accounts
|
|
.into_iter()
|
|
.map(|account| map_account(auth_provider, account, default_account_id.as_deref()))
|
|
.collect())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let status = auth_manager.get_status().await;
|
|
let default_account_id = status.default_account_id.clone();
|
|
Ok(status
|
|
.accounts
|
|
.into_iter()
|
|
.map(|account| map_account(auth_provider, account, default_account_id.as_deref()))
|
|
.collect())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_get_status(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<ManagedAuthStatus, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let status = auth_manager.get_status().await;
|
|
let default_account_id = status.default_account_id.clone();
|
|
Ok(ManagedAuthStatus {
|
|
provider: auth_provider.to_string(),
|
|
authenticated: status.authenticated,
|
|
default_account_id: default_account_id.clone(),
|
|
migration_error: status.migration_error,
|
|
accounts: status
|
|
.accounts
|
|
.into_iter()
|
|
.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let status = auth_manager.get_status().await;
|
|
let default_account_id = status.default_account_id.clone();
|
|
Ok(ManagedAuthStatus {
|
|
provider: auth_provider.to_string(),
|
|
authenticated: status.authenticated,
|
|
default_account_id: default_account_id.clone(),
|
|
migration_error: None,
|
|
accounts: status
|
|
.accounts
|
|
.into_iter()
|
|
.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_remove_account(
|
|
auth_provider: String,
|
|
account_id: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager
|
|
.remove_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager
|
|
.remove_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_set_default_account(
|
|
auth_provider: String,
|
|
account_id: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager
|
|
.set_default_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager
|
|
.set_default_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_logout(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager.clear_auth().await.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager.clear_auth().await.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|