diff --git a/pyproject.toml b/pyproject.toml index 27adf61..527888b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "codex-register-v2" -version = "0.1.0" -description = "OpenAI/Codex CLI 自动注册系统" +version = "1.0,4" +description = "OpenAI 自动注册系统 v2" requires-python = ">=3.10" dependencies = [ "curl-cffi>=0.14.0", diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index ecab653..41de039 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -393,6 +393,82 @@ async def export_accounts_csv(request: BatchExportRequest): ) +@router.post("/export/sub2api") +async def export_accounts_sub2api(request: BatchExportRequest): + """导出账号为 Sub2Api 格式(每个账号单独一个 JSON 文件,多个打包为 ZIP)""" + import io + import zipfile + + def make_sub2api_json(acc) -> dict: + expires_at = int(acc.expires_at.timestamp()) if acc.expires_at else 0 + return { + "proxies": [], + "accounts": [ + { + "name": acc.email, + "platform": "openai", + "type": "oauth", + "credentials": { + "access_token": acc.access_token or "", + "chatgpt_account_id": acc.account_id or "", + "chatgpt_user_id": "", + "client_id": acc.client_id or "", + "expires_at": expires_at, + "expires_in": 863999, + "model_mapping": { + "gpt-5.1": "gpt-5.1", + "gpt-5.1-codex": "gpt-5.1-codex", + "gpt-5.1-codex-max": "gpt-5.1-codex-max", + "gpt-5.1-codex-mini": "gpt-5.1-codex-mini", + "gpt-5.2": "gpt-5.2", + "gpt-5.2-codex": "gpt-5.2-codex" + }, + "organization_id": acc.workspace_id or "", + "refresh_token": acc.refresh_token or "" + }, + "extra": {}, + "concurrency": 10, + "priority": 1, + "rate_multiplier": 1, + "auto_pause_on_expired": True + } + ] + } + + with get_db() as db: + ids = resolve_account_ids( + db, request.ids, request.select_all, + request.status_filter, request.email_service_filter, request.search_filter + ) + accounts = db.query(Account).filter(Account.id.in_(ids)).all() + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + if len(accounts) == 1: + acc = accounts[0] + content = json.dumps(make_sub2api_json(acc), ensure_ascii=False, indent=2) + filename = f"{acc.email}_sub2api.json" + return StreamingResponse( + iter([content]), + media_type="application/json", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: + for acc in accounts: + content = json.dumps(make_sub2api_json(acc), ensure_ascii=False, indent=2) + zf.writestr(f"{acc.email}_sub2api.json", content) + + zip_buffer.seek(0) + zip_filename = f"sub2api_tokens_{timestamp}.zip" + return StreamingResponse( + zip_buffer, + media_type="application/zip", + headers={"Content-Disposition": f"attachment; filename={zip_filename}"} + ) + + @router.post("/export/cpa") async def export_accounts_cpa(request: BatchExportRequest): """导出账号为 CPA Token JSON 格式(每个账号单独一个 JSON 文件,打包为 ZIP)""" diff --git a/static/js/accounts.js b/static/js/accounts.js index 3bf89bc..ba2baac 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -696,7 +696,7 @@ async function exportAccounts(format) { // 从 Content-Disposition 获取文件名 const disposition = response.headers.get('Content-Disposition'); - let filename = `accounts_${Date.now()}.${format === 'cpa' ? 'json' : format}`; + let filename = `accounts_${Date.now()}.${(format === 'cpa' || format === 'sub2api') ? 'json' : format}`; if (disposition) { const match = disposition.match(/filename=(.+)/); if (match) { diff --git a/templates/accounts.html b/templates/accounts.html index 25da3bb..35ffe03 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -144,6 +144,7 @@ 导出 JSON 导出 CSV 导出 CPA 格式 + 导出 Sub2Api 格式