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 格式