Skip to content

Commit 5a2d970

Browse files
author
李杰
committed
Codex调整
1 parent 585e3e3 commit 5a2d970

5 files changed

Lines changed: 148 additions & 12 deletions

File tree

src-tauri/src/models/codex.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ pub struct CodexAccount {
5858
pub api_provider_name: Option<String>,
5959
pub user_id: Option<String>,
6060
pub plan_type: Option<String>,
61+
#[serde(default, skip_serializing_if = "Option::is_none")]
62+
pub auth_file_plan_type: Option<String>,
6163
pub account_id: Option<String>,
6264
pub organization_id: Option<String>,
6365
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -224,6 +226,7 @@ impl CodexAccount {
224226
api_provider_name: None,
225227
user_id: None,
226228
plan_type: None,
229+
auth_file_plan_type: None,
227230
account_id: None,
228231
organization_id: None,
229232
account_name: None,

src-tauri/src/modules/codex_account.rs

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,7 +2295,13 @@ fn import_account_struct(account: CodexAccount) -> Result<CodexAccount, String>
22952295
);
22962296
}
22972297

2298-
upsert_account(account.tokens)
2298+
let imported_auth_file_plan_type =
2299+
normalize_auth_file_plan_type(account.auth_file_plan_type.as_deref());
2300+
let mut imported = upsert_account(account.tokens)?;
2301+
if apply_auth_file_plan_type(&mut imported, imported_auth_file_plan_type) {
2302+
save_account(&imported)?;
2303+
}
2304+
Ok(imported)
22992305
}
23002306

23012307
/// 从 JSON 字符串导入账号
@@ -2468,6 +2474,53 @@ pub struct CodexFileImportFailure {
24682474
pub error: String,
24692475
}
24702476

2477+
fn normalize_auth_file_plan_type(value: Option<&str>) -> Option<String> {
2478+
let normalized = normalize_optional_ref(value)?
2479+
.to_ascii_lowercase()
2480+
.replace('_', "-")
2481+
.replace(' ', "-");
2482+
2483+
match normalized.as_str() {
2484+
"prolite" | "pro-lite" => Some("prolite".to_string()),
2485+
"promax" | "pro-max" => Some("promax".to_string()),
2486+
_ => None,
2487+
}
2488+
}
2489+
2490+
fn detect_auth_file_plan_type_from_path(path: &std::path::Path) -> Option<String> {
2491+
let stem = path.file_stem()?.to_str()?;
2492+
let normalized = stem
2493+
.trim()
2494+
.to_ascii_lowercase()
2495+
.replace('_', "-")
2496+
.replace(' ', "-");
2497+
2498+
if normalized.ends_with("-prolite") || normalized.ends_with("-pro-lite") {
2499+
return Some("prolite".to_string());
2500+
}
2501+
if normalized.ends_with("-promax") || normalized.ends_with("-pro-max") {
2502+
return Some("promax".to_string());
2503+
}
2504+
2505+
None
2506+
}
2507+
2508+
fn apply_auth_file_plan_type(
2509+
account: &mut CodexAccount,
2510+
auth_file_plan_type: Option<String>,
2511+
) -> bool {
2512+
let Some(normalized) = normalize_auth_file_plan_type(auth_file_plan_type.as_deref()) else {
2513+
return false;
2514+
};
2515+
2516+
if account.auth_file_plan_type.as_deref() == Some(normalized.as_str()) {
2517+
return false;
2518+
}
2519+
2520+
account.auth_file_plan_type = Some(normalized);
2521+
true
2522+
}
2523+
24712524
/// 从单个 JSON 值中提取 CodexTokens
24722525
fn extract_codex_tokens_from_value(
24732526
obj: &serde_json::Value,
@@ -2529,9 +2582,10 @@ fn extract_codex_tokens_from_value(
25292582
#[cfg(test)]
25302583
mod tests {
25312584
use super::{
2532-
build_account_storage_id, extract_codex_tokens_from_value, get_accounts_dir,
2533-
get_accounts_storage_path, get_current_account, list_accounts_checked, load_account,
2534-
load_account_index, read_api_provider_from_config_toml, read_quick_config_from_config_toml,
2585+
build_account_storage_id, detect_auth_file_plan_type_from_path,
2586+
extract_codex_tokens_from_value, get_accounts_dir, get_accounts_storage_path,
2587+
get_current_account, list_accounts_checked, load_account, load_account_index,
2588+
read_api_provider_from_config_toml, read_quick_config_from_config_toml,
25352589
resolve_api_provider_config, save_account, save_account_index, sync_account_from_auth_dir,
25362590
validate_api_key_credentials, write_api_provider_to_config_toml,
25372591
write_quick_config_to_config_toml, ApiProviderConfig, CodexAccountIndex,
@@ -2737,6 +2791,22 @@ mod tests {
27372791
assert_eq!(account_id_hint.as_deref(), Some("acc_2"));
27382792
}
27392793

2794+
#[test]
2795+
fn detect_auth_file_plan_type_from_filename() {
2796+
let prolite = detect_auth_file_plan_type_from_path(std::path::Path::new(
2797+
"/tmp/codex-demo@example.com-prolite.json",
2798+
));
2799+
let promax = detect_auth_file_plan_type_from_path(std::path::Path::new(
2800+
"/tmp/codex-demo@example.com-pro-max.json",
2801+
));
2802+
let team =
2803+
detect_auth_file_plan_type_from_path(std::path::Path::new("/tmp/codex-demo-team.json"));
2804+
2805+
assert_eq!(prolite.as_deref(), Some("prolite"));
2806+
assert_eq!(promax.as_deref(), Some("promax"));
2807+
assert_eq!(team, None);
2808+
}
2809+
27402810
#[test]
27412811
fn current_account_syncs_latest_tokens_from_local_auth_json() {
27422812
let _lock = TEST_ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner());
@@ -3057,8 +3127,8 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
30573127
file_paths.len()
30583128
));
30593129

3060-
// 收集所有候选: (CodexTokens, account_id_hint, label)
3061-
let mut candidates: Vec<(CodexTokens, Option<String>, String)> = Vec::new();
3130+
// 收集所有候选: (CodexTokens, account_id_hint, label, auth_file_plan_type)
3131+
let mut candidates: Vec<(CodexTokens, Option<String>, String, Option<String>)> = Vec::new();
30623132

30633133
for file_path in &file_paths {
30643134
let path = Path::new(file_path);
@@ -3076,6 +3146,7 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
30763146
.and_then(|s| s.to_str())
30773147
.unwrap_or("unknown")
30783148
.to_string();
3149+
let auth_file_plan_type = detect_auth_file_plan_type_from_path(path);
30793150

30803151
let parsed: serde_json::Value = match serde_json::from_str(&content) {
30813152
Ok(v) => v,
@@ -3088,7 +3159,7 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
30883159
match &parsed {
30893160
serde_json::Value::Object(_) => {
30903161
if let Some((tokens, hint)) = extract_codex_tokens_from_value(&parsed) {
3091-
candidates.push((tokens, hint, filename_label));
3162+
candidates.push((tokens, hint, filename_label, auth_file_plan_type.clone()));
30923163
} else {
30933164
logger::log_error(&format!("未找到有效 Token {:?}", file_path));
30943165
}
@@ -3101,7 +3172,7 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
31013172
.and_then(|v| v.as_str())
31023173
.unwrap_or(&filename_label)
31033174
.to_string();
3104-
candidates.push((tokens, hint, label));
3175+
candidates.push((tokens, hint, label, auth_file_plan_type.clone()));
31053176
}
31063177
}
31073178
}
@@ -3124,7 +3195,9 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
31243195
let mut failed: Vec<CodexFileImportFailure> = Vec::new();
31253196
let total = candidates.len();
31263197

3127-
for (index, (tokens, account_id_hint, label)) in candidates.into_iter().enumerate() {
3198+
for (index, (tokens, account_id_hint, label, auth_file_plan_type)) in
3199+
candidates.into_iter().enumerate()
3200+
{
31283201
// 发送进度事件
31293202
if let Some(app_handle) = crate::get_app_handle() {
31303203
use tauri::Emitter;
@@ -3139,7 +3212,10 @@ pub fn import_from_files(file_paths: Vec<String>) -> Result<CodexFileImportResul
31393212
}
31403213

31413214
match upsert_account_with_hints(tokens, account_id_hint, None) {
3142-
Ok(account) => {
3215+
Ok(mut account) => {
3216+
if apply_auth_file_plan_type(&mut account, auth_file_plan_type) {
3217+
save_account(&account)?;
3218+
}
31433219
logger::log_info(&format!("Codex 导入成功: {}", account.email));
31443220
imported.push(account);
31453221
}

src/presentation/platformAccountPresentation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
import {
3838
formatCodexResetTime,
3939
getCodexCodeReviewQuotaMetric,
40+
getCodexPlanBadgeLabel,
4041
getCodexPlanDisplayName,
4142
getCodexQuotaClass,
4243
getCodexQuotaWindows,
@@ -500,7 +501,6 @@ export function buildCodexAccountPresentation(
500501
t: Translate,
501502
): UnifiedAccountPresentation {
502503
const normalizedPlan = getCodexPlanDisplayName(account.plan_type);
503-
const rawPlan = account.plan_type?.trim();
504504
const apiKeyDisplayName = account.account_name?.trim();
505505
const displayName =
506506
isCodexApiKeyAccount(account) && apiKeyDisplayName ? apiKeyDisplayName : account.email;
@@ -529,7 +529,7 @@ export function buildCodexAccountPresentation(
529529
return {
530530
id: account.id,
531531
displayName,
532-
planLabel: rawPlan || normalizedPlan,
532+
planLabel: getCodexPlanBadgeLabel(account),
533533
planClass: normalizedPlan.toLowerCase(),
534534
quotaItems,
535535
};

src/types/codex.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface CodexAccount {
1919
api_provider_name?: string;
2020
user_id?: string;
2121
plan_type?: string;
22+
auth_file_plan_type?: string;
2223
account_id?: string;
2324
organization_id?: string;
2425
account_name?: string;
@@ -350,6 +351,46 @@ export function getCodexPlanDisplayName(planType?: string): string {
350351
return upper;
351352
}
352353

354+
function normalizeCodexPlanKey(planType?: string): string {
355+
const normalized = (planType || '').trim().toLowerCase();
356+
if (!normalized) return 'free';
357+
if (normalized.includes('api')) return 'api_key';
358+
if (normalized.includes('enterprise')) return 'enterprise';
359+
if (normalized.includes('business')) return 'business';
360+
if (normalized.includes('team')) return 'team';
361+
if (normalized.includes('edu')) return 'edu';
362+
if (normalized.includes('go')) return 'go';
363+
if (normalized.includes('plus')) return 'plus';
364+
if (normalized.includes('pro')) return 'pro';
365+
if (normalized.includes('free')) return 'free';
366+
return normalized;
367+
}
368+
369+
function normalizeCodexAuthFilePlanType(value?: string): 'prolite' | 'promax' | undefined {
370+
const normalized = (value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
371+
if (normalized === 'prolite' || normalized === 'pro-lite') return 'prolite';
372+
if (normalized === 'promax' || normalized === 'pro-max') return 'promax';
373+
return undefined;
374+
}
375+
376+
export function getCodexPlanBadgeLabel(account: CodexAccount): string {
377+
const baseLabel = getCodexPlanDisplayName(account.plan_type);
378+
if (normalizeCodexPlanKey(account.plan_type) !== 'pro') {
379+
return baseLabel;
380+
}
381+
382+
const authFilePlanType =
383+
normalizeCodexAuthFilePlanType(account.auth_file_plan_type) ??
384+
normalizeCodexAuthFilePlanType(account.plan_type);
385+
if (authFilePlanType === 'prolite') {
386+
return `${baseLabel} 5x`;
387+
}
388+
if (authFilePlanType === 'promax') {
389+
return `${baseLabel} 20x`;
390+
}
391+
return baseLabel;
392+
}
393+
353394
export function isCodexTeamLikePlan(planType?: string): boolean {
354395
if (!planType) return false;
355396
const upper = planType.toUpperCase();

src/utils/codexExportFormats.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ export type CodexExportFormat = 'cockpit_tools' | 'sub2api' | 'cpa';
55
type JsonRecord = Record<string, unknown>;
66

77
interface Sub2apiBatchCreatePayload {
8+
exported_at: string;
9+
proxies: [];
810
accounts: Sub2apiCreateAccountItem[];
11+
type: 'sub2api-data';
12+
version: 1;
913
}
1014

1115
interface Sub2apiCreateAccountItem {
1216
name: string;
1317
platform: 'openai';
1418
type: 'oauth';
1519
credentials: JsonRecord;
20+
concurrency: number;
21+
priority: number;
1622
}
1723

1824
interface CpaCodexTokenStorage {
@@ -114,6 +120,10 @@ function normalizeTimestampToIso(value: unknown): string | undefined {
114120
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
115121
}
116122

123+
function formatSub2apiExportedAt(): string {
124+
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
125+
}
126+
117127
function resolveSubscriptionExpiresAt(account: CodexAccount): string | undefined {
118128
const authPayload = resolveAuthPayload(account);
119129
return normalizeTimestampToIso(authPayload?.chatgpt_subscription_active_until);
@@ -184,6 +194,8 @@ function toSub2apiAccount(account: CodexAccount): Sub2apiCreateAccountItem {
184194
platform: 'openai',
185195
type: 'oauth',
186196
credentials: buildSub2apiCredentials(account),
197+
concurrency: 0,
198+
priority: 0,
187199
};
188200
}
189201

@@ -222,7 +234,11 @@ export function transformCodexExportJson(
222234
const accounts = parseCockpitToolsCodexExport(rawJson);
223235
if (format === 'sub2api') {
224236
const payload: Sub2apiBatchCreatePayload = {
237+
exported_at: formatSub2apiExportedAt(),
238+
proxies: [],
225239
accounts: accounts.map(toSub2apiAccount),
240+
type: 'sub2api-data',
241+
version: 1,
226242
};
227243
return JSON.stringify(payload, null, 2);
228244
}

0 commit comments

Comments
 (0)