diff --git a/README.md b/README.md index d484c6c..577b131 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,54 @@ Codex Switcher

-

Codex Switcher

+

Codex 多账号切换

- A Desktop Application for Managing Multiple OpenAI Codex CLI Accounts
- Easily switch between accounts, monitor usage limits, and stay in control of your quota + 一款用于管理多个 OpenAI Codex CLI 账号的桌面应用
+ 可快速切换账号、查看配额使用情况,并更方便地管理你的额度

-## Features +## 功能特性 -- **Multi-Account Management** – Add and manage multiple Codex accounts in one place -- **Quick Switching** – Switch between accounts with a single click -- **Usage Monitoring** – View real-time usage for both 5-hour and weekly limits -- **Dual Login Mode** – OAuth authentication or import existing `auth.json` files +- **多账号管理**:在一个界面中添加并管理多个 Codex 账号 +- **快速切换**:一键切换当前使用的账号 +- **配额监控**:实时查看 5 小时配额和周配额的使用情况 +- **双登录模式**:支持 OAuth 登录,也支持导入现有的 `auth.json` 文件 -## Installation +## 安装 -### Prerequisites +### 环境要求 - [Node.js](https://nodejs.org/) (v18+) - [pnpm](https://pnpm.io/) - [Rust](https://rustup.rs/) -### Build from Source +### 从源码构建 ```bash -# Clone the repository +# 克隆仓库 git clone https://github.com/Lampese/codex-switcher.git cd codex-switcher -# Install dependencies +# 安装依赖 pnpm install -# Run in development mode +# 开发模式运行 pnpm tauri dev -# Build for production +# 构建生产版本 pnpm tauri build ``` -The built application will be in `src-tauri/target/release/bundle/`. +构建产物位于 `src-tauri/target/release/bundle/`。 -## Disclaimer +## 免责声明 -This tool is designed **exclusively for individuals who personally own multiple OpenAI/ChatGPT accounts**. It is intended to help users manage their own accounts more conveniently. +本工具**仅适用于本人合法持有多个 OpenAI/ChatGPT 账号的个人用户**,目的是帮助用户更方便地管理自己的账号。 -**This tool is NOT intended for:** -- Sharing accounts between multiple users -- Circumventing OpenAI's terms of service -- Any form of account pooling or credential sharing +**本工具不适用于以下用途:** +- 多人之间共享账号 +- 规避 OpenAI 的服务条款 +- 任何形式的账号池化或凭据共享 -By using this software, you agree that you are the rightful owner of all accounts you add to the application. The authors are not responsible for any misuse or violations of OpenAI's terms of service. +使用本软件即表示你确认自己是添加到应用中的所有账号的合法持有人。作者不对任何滥用行为或违反 OpenAI 服务条款的行为承担责任。 diff --git a/index.html b/index.html index 90be5fb..6010460 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - Codex Switcher + Codex 多账号切换 diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 6743d7f..37cf766 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -3,7 +3,7 @@ use crate::auth::{ add_account, create_chatgpt_account_from_refresh_token, get_active_account, import_from_auth_json, load_accounts, remove_account, save_accounts, set_active_account, - switch_to_account, touch_account, + switch_to_account, touch_account, get_codex_auth_file, }; use crate::types::{AccountInfo, AccountsStore, AuthData, ImportAccountsSummary, StoredAccount}; @@ -109,6 +109,15 @@ pub async fn add_account_from_file(path: String, name: String) -> Result Result { + let path = get_codex_auth_file().map_err(|e| e.to_string())?; + path.to_str() + .map(str::to_owned) + .ok_or_else(|| "Failed to resolve auth.json path".to_string()) +} + /// Switch to a different account #[tauri::command] pub async fn switch_account(account_id: String) -> Result<(), String> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 075cb3a..9e02702 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,9 +8,9 @@ pub mod types; use commands::{ add_account_from_file, cancel_login, check_codex_processes, complete_login, delete_account, export_accounts_full_encrypted_file, export_accounts_slim_text, get_active_account_info, - get_usage, import_accounts_full_encrypted_file, import_accounts_slim_text, list_accounts, - refresh_all_accounts_usage, rename_account, start_login, switch_account, warmup_account, - warmup_all_accounts, + get_default_auth_json_path, get_usage, import_accounts_full_encrypted_file, + import_accounts_slim_text, list_accounts, refresh_all_accounts_usage, rename_account, + start_login, switch_account, warmup_account, warmup_all_accounts, }; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -23,6 +23,7 @@ pub fn run() { list_accounts, get_active_account_info, add_account_from_file, + get_default_auth_json_path, switch_account, delete_account, rename_account, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d775937..36742de 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "Codex Switcher", + "productName": "Codex 多账号切换", "version": "0.1.1", "identifier": "com.lampese.codex-switcher", "build": { @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "Codex Switcher", + "title": "Codex 多账号切换", "width": 900, "height": 700, "minWidth": 600, diff --git a/src/App.tsx b/src/App.tsx index 5ac8f87..1a00e9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -163,13 +163,13 @@ function App() { }; const formatWarmupError = (err: unknown) => { - if (!err) return "Unknown error"; + if (!err) return "未知错误"; if (err instanceof Error && err.message) return err.message; if (typeof err === "string") return err; try { return JSON.stringify(err); } catch { - return "Unknown error"; + return "未知错误"; } }; @@ -177,11 +177,11 @@ function App() { try { setWarmingUpId(accountId); await warmupAccount(accountId); - showWarmupToast(`Warm-up sent for ${accountName}`); + showWarmupToast(`已向 ${accountName} 发送保活请求`); } catch (err) { console.error("Failed to warm up account:", err); showWarmupToast( - `Warm-up failed for ${accountName}: ${formatWarmupError(err)}`, + `${accountName} 保活失败:${formatWarmupError(err)}`, true ); } finally { @@ -194,25 +194,21 @@ function App() { setIsWarmingAll(true); const summary = await warmupAllAccounts(); if (summary.total_accounts === 0) { - showWarmupToast("No accounts available for warm-up", true); + showWarmupToast("当前没有可保活的账号", true); return; } if (summary.failed_account_ids.length === 0) { - showWarmupToast( - `Warm-up sent for all ${summary.warmed_accounts} account${ - summary.warmed_accounts === 1 ? "" : "s" - }` - ); + showWarmupToast(`已向全部 ${summary.warmed_accounts} 个账号发送保活请求`); } else { showWarmupToast( - `Warmed ${summary.warmed_accounts}/${summary.total_accounts}. Failed: ${summary.failed_account_ids.length}`, + `保活完成 ${summary.warmed_accounts}/${summary.total_accounts},失败 ${summary.failed_account_ids.length} 个`, true ); } } catch (err) { console.error("Failed to warm up all accounts:", err); - showWarmupToast(`Warm-up all failed: ${formatWarmupError(err)}`, true); + showWarmupToast(`全部保活失败:${formatWarmupError(err)}`, true); } finally { setIsWarmingAll(false); } @@ -229,12 +225,12 @@ function App() { setIsExportingSlim(true); const payload = await exportAccountsSlimText(); setConfigPayload(payload); - showWarmupToast(`Slim text exported (${accounts.length} accounts).`); + showWarmupToast(`已导出精简配置(共 ${accounts.length} 个账号)`); } catch (err) { console.error("Failed to export slim text:", err); const message = err instanceof Error ? err.message : String(err); setConfigModalError(message); - showWarmupToast("Slim export failed", true); + showWarmupToast("精简配置导出失败", true); } finally { setIsExportingSlim(false); } @@ -250,7 +246,7 @@ function App() { const handleImportSlimText = async () => { if (!configPayload.trim()) { - setConfigModalError("Please paste the slim text string first."); + setConfigModalError("请先粘贴精简配置。"); return; } @@ -261,13 +257,13 @@ function App() { setMaskedAccounts(new Set()); setIsConfigModalOpen(false); showWarmupToast( - `Imported ${summary.imported_count}, skipped ${summary.skipped_count} (total ${summary.total_in_payload})` + `已导入 ${summary.imported_count} 个,跳过 ${summary.skipped_count} 个(共 ${summary.total_in_payload} 个)` ); } catch (err) { console.error("Failed to import slim text:", err); const message = err instanceof Error ? err.message : String(err); setConfigModalError(message); - showWarmupToast("Slim import failed", true); + showWarmupToast("精简配置导入失败", true); } finally { setIsImportingSlim(false); } @@ -277,11 +273,11 @@ function App() { try { setIsExportingFull(true); const selected = await save({ - title: "Export Full Encrypted Account Config", + title: "导出完整加密备份", defaultPath: "codex-switcher-full.cswf", filters: [ { - name: "Codex Switcher Full Backup", + name: "Codex 完整加密备份", extensions: ["cswf"], }, ], @@ -290,10 +286,10 @@ function App() { if (!selected) return; await exportAccountsFullEncryptedFile(selected); - showWarmupToast("Full encrypted file exported."); + showWarmupToast("完整加密备份已导出。"); } catch (err) { console.error("Failed to export full encrypted file:", err); - showWarmupToast("Full export failed", true); + showWarmupToast("完整加密备份导出失败", true); } finally { setIsExportingFull(false); } @@ -304,10 +300,10 @@ function App() { setIsImportingFull(true); const selected = await open({ multiple: false, - title: "Import Full Encrypted Account Config", + title: "导入完整加密备份", filters: [ { - name: "Codex Switcher Full Backup", + name: "Codex 完整加密备份", extensions: ["cswf"], }, ], @@ -318,11 +314,11 @@ function App() { const summary = await importAccountsFullEncryptedFile(selected); setMaskedAccounts(new Set()); showWarmupToast( - `Imported ${summary.imported_count}, skipped ${summary.skipped_count} (total ${summary.total_in_payload})` + `已导入 ${summary.imported_count} 个,跳过 ${summary.skipped_count} 个(共 ${summary.total_in_payload} 个)` ); } catch (err) { console.error("Failed to import full encrypted file:", err); - showWarmupToast("Full import failed", true); + showWarmupToast("完整加密备份导入失败", true); } finally { setIsImportingFull(false); } @@ -388,7 +384,7 @@ function App() {

- Codex Switcher + Codex 多账号切换

{processInfo && ( {hasRunningProcesses - ? `${processInfo.count} Codex running` - : "0 Codex running"} + ? `检测到 ${processInfo.count} 个 Codex 进程` + : "未检测到 Codex 进程"} )}

- Multi-account manager for Codex CLI + Codex CLI 多账号与配额管理

@@ -419,7 +415,7 @@ function App() { @@ -469,7 +465,7 @@ function App() { onClick={() => setIsActionsMenuOpen((prev) => !prev)} className="h-10 px-4 py-2 text-sm font-medium rounded-lg bg-gray-900 hover:bg-gray-800 text-white transition-colors shrink-0 whitespace-nowrap" > - Account ▾ + 账号操作 ▾ {isActionsMenuOpen && (
@@ -480,7 +476,7 @@ function App() { }} className="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-gray-100 text-gray-700" > - + Add Account + + 添加账号
)} @@ -535,11 +531,11 @@ function App() { {loading && accounts.length === 0 ? (
-

Loading accounts...

+

正在加载账号列表...

) : error ? (
-
Failed to load accounts
+
加载账号失败

{error}

) : accounts.length === 0 ? ( @@ -548,16 +544,16 @@ function App() { 👤

- No accounts yet + 还没有账号

- Add your first Codex account to get started + 添加第一个 Codex 账号后即可开始使用

) : ( @@ -566,7 +562,7 @@ function App() { {activeAccount && (

- Active Account + 当前账号

- Other Accounts ({otherAccounts.length}) + 其他账号({otherAccounts.length})

@@ -662,7 +658,7 @@ function App() { {/* Refresh Success Toast */} {refreshSuccess && (
- Usage refreshed successfully + 配额已刷新
)} @@ -682,7 +678,7 @@ function App() { {/* Delete Confirmation Toast */} {deleteConfirmId && (
- Click delete again to confirm removal + 再次点击删除以确认
)} @@ -702,7 +698,7 @@ function App() {

- {configModalMode === "slim_export" ? "Export Slim Text" : "Import Slim Text"} + {configModalMode === "slim_export" ? "导出精简配置" : "导入精简配置"}