Skip to content

Commit 337e49f

Browse files
committed
Release v1.0.8
1 parent 2ec8c56 commit 337e49f

48 files changed

Lines changed: 2174 additions & 325 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 1.0.8 - 2026-07-21
4+
5+
+ [新增] OpenAI 与 Grok 账号导出统一支持 CPA ZIP 和 Sub2API JSON,可分别导出选中账号或全部账号,并保留对应 OAuth/SSO 元数据。
6+
+ [新增] Grok 账号页增加 OAuth 未授权、正常、限流、过期和失效筛选,支持单个或批量加入 OAuth 授权队列,并展示请求额度、Token 额度、探测与恢复状态。
7+
+ [优化] Grok 账号列表改为先安全加载持久化快照、再后台刷新运行时数据;概览统计按来源设置超时和降级状态,运行时繁忙或暂时不可用时页面仍可正常打开。
8+
+ [优化] Grok OAuth 日志从注册日志中独立分流,成功、失败、权限待生效和上传结果统一脱敏;后台恢复队列按本地 Solver 容量使用 2 个 worker,并只重试明确的瞬时错误。
9+
+ [修复] 兼容 xAI `createSession` 新旧嵌套响应、相对 Cookie Setter URL 和直接写入 Cookie 的流程,修复 `missing-host/`;同时解析 RPC 登录错误,账号密码错误不再重复消耗验证码。
10+
+ [修复] Outlook 邮箱池显示本次任务失败邮箱,可勾选指定记录并只释放、重试所选邮箱;“释放全部占用/失败”和“删除未使用邮箱”恢复为各自准确语义。
11+
+ [修复] SQLite 本地账号后端增加连接串行化、WAL 回退和 busy timeout,降低并发读取时的锁冲突;账号导出、运行池同步和恢复时间展示补齐异常兼容。
12+
313
## 1.0.7 - 2026-07-20
414

515
+ [新增] Grok SSO 与 OAuth 后台探测:服务启动后按周期分批验证运行池账号和真实 `grok-4.5` 能力,持久化有效、限流、失效、未知、请求额度和 Token 额度。

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ GPTGrok2API 是一个自托管的 GPT 与 Grok 统一网关,将已接入的订
2727
| 本地 Captcha Solver | 源码内置于 `captcha-solver/`,使用 Docker/Xvfb 中的有头 CloakBrowser/Chromium 处理 Grok Turnstile,支持动态并发、代理透传和浏览器资源回收,不需要额外克隆第二个仓库。 |
2828
| Checkout 提链 | 注册 Checkout 仅保留 UPI 最终支付链接提取;IN Checkout、Provider、Approve 共享同一 sticky 出口,VN Promotion 使用独立代理持续轮换重试。 |
2929
| 代理与稳定出口 | 支持全局代理、账号代理、代理配置、代理组、节点并发限制、故障反馈、备用出口、WARP、Privoxy、FlareSolverr 和 Clearance 刷新。 |
30-
| 外部系统接入 | 支持从 Sub2API、远程 CPA、本地 CPA 和 Access Token 导入账号;Grok 注册成功后立即进入 3-worker OAuth 优先级队列,并按配置投递 NovaApi 与 CPA;服务器部署提供 `gptgrok2api` Docker 网络别名。 |
30+
| 外部系统接入 | 支持从 Sub2API、远程 CPA、本地 CPA 和 Access Token 导入账号;Grok 注册成功后立即进入 2-worker OAuth 优先级队列,并按配置投递 NovaApi 与 CPA;服务器部署提供 `gptgrok2api` Docker 网络别名。 |
3131
| 管理控制台 | 提供概览、GPT/Grok 账号与运行池管理、iCloud 邮箱、注册任务、Checkout 任务、代理管理、日志、实时监控、图片管理、调试中心和系统设置。 |
3232
| 存储与备份 | 账号数据支持 JSON、SQLite、PostgreSQL 和 Git 后端;图片支持本地与 WebDAV;备份支持本地归档和 R2。 |
3333
| 运维与发布 | 支持 Docker Compose、WARP 编排、内置 sidecar、Nginx HTTPS、运行日志、指标趋势、健康检查和 GitHub Releases 更新源。 |

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.7
1+
1.0.8

‎api/accounts.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from services.cpa_service import cpa_config, cpa_import_service, list_remote_files
3131
from services.oauth_login_service import OAuthLoginError, oauth_login_service
3232
from services.sub2api_service import (
33+
build_openai_oauth_export_account,
3334
list_remote_accounts as sub2api_list_remote_accounts,
3435
list_remote_groups as sub2api_list_remote_groups,
3536
sub2api_config,
@@ -70,7 +71,7 @@ class AccountRefreshRequest(BaseModel):
7071

7172
class AccountExportRequest(BaseModel):
7273
access_tokens: list[str] = Field(default_factory=list)
73-
format: Literal["json", "zip"] = "json"
74+
format: Literal["json", "zip", "cpa", "sub2api"] = "json"
7475

7576

7677
class AccountUpdateRequest(BaseModel):
@@ -755,12 +756,24 @@ async def export_accounts(body: AccountExportRequest, authorization: str | None
755756
)
756757

757758
timestamp = _download_timestamp()
758-
if body.format == "zip":
759+
if body.format in {"zip", "cpa"}:
759760
content = _account_zip_bytes(items)
760761
return Response(
761762
content,
762763
media_type="application/zip",
763-
headers={"Content-Disposition": f'attachment; filename="codex-accounts-{timestamp}.zip"'},
764+
headers={"Content-Disposition": f'attachment; filename="codex-accounts-cpa-{timestamp}.zip"'},
765+
)
766+
767+
if body.format == "sub2api":
768+
payload = {
769+
"exported_at": datetime.now(timezone.utc).isoformat(),
770+
"proxies": [],
771+
"accounts": [build_openai_oauth_export_account(item) for item in items],
772+
}
773+
return Response(
774+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
775+
media_type="application/json",
776+
headers={"Content-Disposition": f'attachment; filename="openai-accounts-sub2api-{timestamp}.json"'},
764777
)
765778

766779
payload: dict[str, str] | list[dict[str, str]] = items[0] if len(items) == 1 else items

‎api/accounts_test.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from __future__ import annotations
22

3+
import io
4+
import json
35
import unittest
6+
import zipfile
7+
from unittest.mock import patch
48

9+
from fastapi import FastAPI
10+
from fastapi.testclient import TestClient
11+
12+
import api.accounts as accounts_api
513
from api.accounts import _account_for_api, _status_matches_filter
614

715

@@ -36,5 +44,62 @@ def test_valid_checkout_filter_requires_ready_final_url_and_future_expiry(self)
3644
self.assertFalse(_status_matches_filter({**base, "checkout_qr_expires_at": ""}, "valid_checkout"))
3745

3846

47+
class AccountExportApiTest(unittest.TestCase):
48+
def setUp(self) -> None:
49+
app = FastAPI()
50+
app.include_router(accounts_api.create_router())
51+
self.client = TestClient(app)
52+
self.item = {
53+
"type": "codex",
54+
"email": "person@example.test",
55+
"account_id": "chatgpt-account",
56+
"access_token": "access-token",
57+
"refresh_token": "refresh-token",
58+
"id_token": "id-token",
59+
"expired": "2030-01-01T00:00:00+00:00",
60+
"last_refresh": "2029-01-01T00:00:00+00:00",
61+
}
62+
63+
def test_sub2api_export_uses_standard_account_envelope(self) -> None:
64+
with patch.object(accounts_api, "require_admin"), patch.object(
65+
accounts_api.account_service,
66+
"build_export_items",
67+
return_value=[self.item],
68+
):
69+
response = self.client.post(
70+
"/api/accounts/export",
71+
json={"access_tokens": ["access-token"], "format": "sub2api"},
72+
)
73+
74+
self.assertEqual(response.status_code, 200)
75+
payload = response.json()
76+
self.assertEqual(payload["proxies"], [])
77+
self.assertEqual(len(payload["accounts"]), 1)
78+
account = payload["accounts"][0]
79+
self.assertEqual(account["platform"], "openai")
80+
self.assertEqual(account["type"], "oauth")
81+
self.assertEqual(account["credentials"]["chatgpt_account_id"], "chatgpt-account")
82+
self.assertEqual(account["credentials"]["refresh_token"], "refresh-token")
83+
84+
def test_cpa_export_is_zip_of_codex_auth_files(self) -> None:
85+
with patch.object(accounts_api, "require_admin"), patch.object(
86+
accounts_api.account_service,
87+
"build_export_items",
88+
return_value=[self.item],
89+
):
90+
response = self.client.post(
91+
"/api/accounts/export",
92+
json={"access_tokens": ["access-token"], "format": "cpa"},
93+
)
94+
95+
self.assertEqual(response.status_code, 200)
96+
self.assertEqual(response.headers["content-type"], "application/zip")
97+
with zipfile.ZipFile(io.BytesIO(response.content)) as archive:
98+
self.assertEqual(archive.namelist(), ["person-example.test.json"])
99+
exported = json.loads(archive.read(archive.namelist()[0]))
100+
self.assertEqual(exported["type"], "codex")
101+
self.assertEqual(exported["access_token"], "access-token")
102+
103+
39104
if __name__ == "__main__":
40105
unittest.main()

‎api/register.py‎

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class OutlookPoolResetRequest(BaseModel):
3333
scope: str | None = None
3434

3535

36+
class OutlookPoolRetryRequest(BaseModel):
37+
provider_id: str = Field(min_length=1, max_length=128)
38+
mailbox_ids: list[str] = Field(min_length=1)
39+
40+
3641
class GptMailStatusRequest(BaseModel):
3742
provider: dict | None = None
3843
force: bool | None = None
@@ -55,6 +60,11 @@ class GrokAccountChatTestRequest(BaseModel):
5560
model: str | None = Field(default=None, max_length=128)
5661

5762

63+
class GrokAccountExportRequest(BaseModel):
64+
ids: list[str] = Field(default_factory=list)
65+
format: Literal["sub2api", "cpa"] = "sub2api"
66+
67+
5868
def _grok_account_ids(values: list[str]) -> list[str]:
5969
return list(dict.fromkeys(str(value or "").strip() for value in values if str(value or "").strip()))
6070

@@ -102,6 +112,22 @@ async def reset_outlook_pool(body: OutlookPoolResetRequest, authorization: str |
102112
require_admin(authorization)
103113
return {"register": register_service.reset_outlook_pool(body.scope or "all")}
104114

115+
@router.post("/api/register/outlook-pool/retry-selected")
116+
async def retry_selected_outlook_mailboxes(
117+
body: OutlookPoolRetryRequest,
118+
authorization: str | None = Header(default=None),
119+
):
120+
require_admin(authorization)
121+
try:
122+
return {
123+
"register": register_service.retry_outlook_failed(
124+
body.provider_id,
125+
body.mailbox_ids,
126+
)
127+
}
128+
except ValueError as exc:
129+
raise HTTPException(status_code=400, detail=str(exc)) from exc
130+
105131
@router.post("/api/register/gptmail/status")
106132
async def get_gptmail_status(body: GptMailStatusRequest, authorization: str | None = Header(default=None)):
107133
require_admin(authorization)
@@ -147,6 +173,11 @@ async def list_grok_accounts(
147173
"runtime_error": str(view.get("runtime_error") or ""),
148174
}
149175

176+
@router.post("/api/register/grok/accounts/runtime/snapshot")
177+
async def refresh_grok_runtime_snapshot(authorization: str | None = Header(default=None)):
178+
require_admin(authorization)
179+
return await run_in_threadpool(register_service.refresh_grok_runtime_snapshot)
180+
150181
@router.get("/api/register/grok/accounts/{account_id}/credentials")
151182
async def get_grok_account_login_credentials(
152183
account_id: str,
@@ -174,6 +205,17 @@ async def sync_grok_accounts(
174205
raise HTTPException(status_code=400, detail={"error": "ids is required"})
175206
return await run_in_threadpool(register_service.sync_grok_accounts, ids)
176207

208+
@router.post("/api/register/grok/accounts/oauth/authorize")
209+
async def authorize_grok_accounts_oauth(
210+
body: GrokAccountIdsRequest,
211+
authorization: str | None = Header(default=None),
212+
):
213+
require_admin(authorization)
214+
ids = _grok_account_ids(body.ids)
215+
if not ids:
216+
raise HTTPException(status_code=400, detail={"error": "ids is required"})
217+
return await run_in_threadpool(register_service.authorize_grok_accounts_oauth, ids)
218+
177219
@router.post("/api/register/grok/accounts/runtime/refresh")
178220
async def refresh_grok_accounts_runtime(
179221
body: GrokAccountIdsRequest,
@@ -244,26 +286,60 @@ async def delete_grok_accounts(
244286

245287
@router.get("/api/register/grok/accounts/export")
246288
async def export_grok_accounts(
247-
format: Literal["json", "txt"] = "json",
289+
format: Literal["sub2api", "cpa"] = "sub2api",
248290
authorization: str | None = Header(default=None),
249291
):
250292
require_admin(authorization)
251293
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
252-
if format == "txt":
253-
return Response(
254-
register_service.export_grok_accounts_text(),
255-
media_type="text/plain; charset=utf-8",
256-
headers={
257-
"Cache-Control": "no-store",
258-
"Content-Disposition": f'attachment; filename="grok-accounts-{timestamp}.txt"',
259-
},
260-
)
294+
try:
295+
if format == "cpa":
296+
return Response(
297+
register_service.export_grok_accounts_cpa(),
298+
media_type="application/zip",
299+
headers={
300+
"Cache-Control": "no-store",
301+
"Content-Disposition": f'attachment; filename="grok-accounts-cpa-{timestamp}.zip"',
302+
},
303+
)
304+
payload = register_service.export_grok_accounts_sub2api()
305+
except ValueError as exc:
306+
raise HTTPException(status_code=400, detail=str(exc)) from exc
307+
return Response(
308+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
309+
media_type="application/json",
310+
headers={
311+
"Cache-Control": "no-store",
312+
"Content-Disposition": f'attachment; filename="grok-accounts-sub2api-{timestamp}.json"',
313+
},
314+
)
315+
316+
@router.post("/api/register/grok/accounts/export")
317+
async def export_selected_grok_accounts(
318+
body: GrokAccountExportRequest,
319+
authorization: str | None = Header(default=None),
320+
):
321+
require_admin(authorization)
322+
ids = _grok_account_ids(body.ids)
323+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
324+
try:
325+
if body.format == "cpa":
326+
return Response(
327+
register_service.export_grok_accounts_cpa(ids or None),
328+
media_type="application/zip",
329+
headers={
330+
"Cache-Control": "no-store",
331+
"Content-Disposition": f'attachment; filename="grok-accounts-cpa-{timestamp}.zip"',
332+
},
333+
)
334+
payload = register_service.export_grok_accounts_sub2api(ids or None)
335+
except ValueError as exc:
336+
raise HTTPException(status_code=400, detail=str(exc)) from exc
261337
return Response(
262-
json.dumps(register_service.export_grok_accounts(), ensure_ascii=False, indent=2) + "\n",
338+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
263339
media_type="application/json",
264340
headers={
265341
"Cache-Control": "no-store",
266-
"Content-Disposition": f'attachment; filename="grok-accounts-{timestamp}.json"',
342+
"Content-Disposition": f'attachment; filename="grok-accounts-sub2api-{timestamp}.json"',
267343
},
268344
)
269345

0 commit comments

Comments
 (0)