Skip to content

Commit 95519b3

Browse files
committed
Release v1.1.0
1 parent fd5c741 commit 95519b3

56 files changed

Lines changed: 3051 additions & 457 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## 1.1.0 - 2026-07-23
4+
5+
+ [新增] OpenAI 账号存活追踪、跨进程 scheduler lease 和手动探测接口;网络或 Token 异常仅记录当次失败,不直接判定账号停用。
6+
+ [新增] Outlook Token 邮箱池改用 SQLite lease 原子预占,支持多进程注册与旧 JSON 状态自动迁移,避免并发 worker 重复领取或覆盖新状态。
7+
+ [新增] Codex Agent Identity 独立加密归档,支持单账号 `auth.json` 和批量 ZIP 导出,私钥不写入普通账号列表。
8+
+ [修复] Grok 默认切换到已验证的 xconsole 注册链路,对齐无 Castle 发码、`ValidatePassword``tos=$undefined` 和创建阶段空 Castle;真实 OAuth 探测可获得 100 万 Token 初始额度,同时保留旧链路回退。
9+
+ [优化] Grok Device Code OAuth 优先复用已有 SSO,失效时再回退邮箱密码;增加可选 Authorization Code + PKCE 对照授权和 402 权益延迟复检。
10+
+ [优化] 本地 Turnstile 不再受 solver 内部固定 2 线程隐藏阀限制;注册页“注册解题并发”成为唯一控制项,默认为 2,设为 0 可取消共享并发上限。
11+
312
## 1.0.9 - 2026-07-22
413

514
+ [新增] GPT 对接 Sub2API 支持 Codex Agent Identity 新模式;兼容新版 JWT claims、Cloudflare WARP 重试,并在上游不支持 Agent Identity 时自动降级为 Codex Session 导入。

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ docker compose -f docker-compose.warp.yml --profile local-icloud up -d
5050

5151
定时创建按账号执行:新接口每个账号每小时最多 `20` 个,旧接口每个账号每小时最多 `5` 个;两种登录态都保存后每小时最多 `25` 个。每个账号累计达到 `750` 个后自动停止该账号,所有账号达到目标后定时器自动结束。控制台支持按选中账号启动定时创建,默认每 `60` 分钟执行一轮;邮箱卡片可直接查看或复制邮箱地址、单邮箱 API 及 `邮箱----API` 组合。
5252

53+
## OpenAI 注册归档与存活追踪
54+
55+
Outlook Token 邮箱池使用 `data/outlook_token_used.db` 原子预占邮箱,支持多进程注册;旧版 `data/outlook_token_used.json` 会在首次访问时自动迁移。每次领取都有独立 lease,超时 worker 不会覆盖邮箱后来产生的新状态。
56+
57+
OpenAI 注册默认把 Agent Identity 独立加密归档到 `data/openai_agent_identities.db`,密钥保存在权限为 `0600``data/openai_agent_identities.key`,私钥不会写入普通账号列表。账号页可以导出单账号 `auth.json` 或多账号 ZIP;列表元数据接口为 `GET /api/accounts/agent-identities`
58+
59+
OpenAI 存活追踪默认每 `60` 分钟运行,并通过跨进程 scheduler lease 避免重复探测。配置、状态和手动执行接口分别为 `POST /api/register/openai/survival``GET /api/register/openai/survival``POST /api/register/openai/survival/run`;Token 失效或网络错误只记为本次探测失败,不会直接把账号判定为停用。
60+
5361
## Grok 后台探测与自动恢复
5462

5563
Grok 账号探测随服务启动自动在后台运行,无需在账号管理页手动启用或点击执行。默认每 `60` 分钟运行一轮,每批处理 `50` 个账号;首次启动且没有历史完成记录时会立即执行。

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.9
1+
1.1.0

api/accounts.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
from services.oauth_login_service import OAuthLoginError, oauth_login_service
3232
from services.sub2api_service import (
3333
build_openai_oauth_export_account,
34+
ensure_openai_agent_identity,
3435
list_remote_accounts as sub2api_list_remote_accounts,
3536
list_remote_groups as sub2api_list_remote_groups,
3637
sub2api_config,
3738
sub2api_import_service,
3839
)
40+
from services.openai_agent_identity_store import openai_agent_identity_store
3941

4042

4143

@@ -71,7 +73,7 @@ class AccountRefreshRequest(BaseModel):
7173

7274
class AccountExportRequest(BaseModel):
7375
access_tokens: list[str] = Field(default_factory=list)
74-
format: Literal["json", "zip", "cpa", "sub2api"] = "json"
76+
format: Literal["json", "zip", "cpa", "sub2api", "agent_identity"] = "json"
7577

7678

7779
class AccountUpdateRequest(BaseModel):
@@ -748,14 +750,71 @@ async def get_refresh_progress(progress_id: str, authorization: str | None = Hea
748750
async def export_accounts(body: AccountExportRequest, authorization: str | None = Header(default=None)):
749751
require_admin(authorization)
750752
access_tokens = _unique_tokens(body.access_tokens)
753+
timestamp = _download_timestamp()
754+
755+
if body.format == "agent_identity":
756+
selected = set(access_tokens)
757+
accounts = [
758+
account
759+
for account in account_service.list_accounts()
760+
if not selected or _account_payload_token(account) in selected
761+
]
762+
auth_items: list[dict[str, Any]] = []
763+
errors: list[str] = []
764+
for account in accounts:
765+
try:
766+
auth_items.append(ensure_openai_agent_identity(account))
767+
except Exception as exc:
768+
label = _clean_text(account.get("email")) or "unknown account"
769+
errors.append(f"{label}: {str(exc)[:200]}")
770+
if not auth_items:
771+
detail = ";".join(errors[:3]) or "没有可导出的 Agent Identity"
772+
raise HTTPException(status_code=400, detail={"error": detail})
773+
if len(auth_items) == 1:
774+
return Response(
775+
json.dumps(auth_items[0], ensure_ascii=False, indent=2) + "\n",
776+
media_type="application/json",
777+
headers={
778+
"Cache-Control": "no-store",
779+
"Content-Disposition": 'attachment; filename="auth.json"',
780+
},
781+
)
782+
archive = io.BytesIO()
783+
used_names: set[str] = set()
784+
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as bundle:
785+
for index, auth_json in enumerate(auth_items, start=1):
786+
identity = auth_json.get("agent_identity") if isinstance(auth_json, dict) else {}
787+
identity = identity if isinstance(identity, dict) else {}
788+
base_name = _safe_export_name(
789+
_clean_text(identity.get("email")),
790+
f"account-{index}",
791+
)
792+
name = base_name
793+
suffix = 2
794+
while name in used_names:
795+
name = f"{base_name}-{suffix}"
796+
suffix += 1
797+
used_names.add(name)
798+
bundle.writestr(
799+
f"{name}/auth.json",
800+
json.dumps(auth_json, ensure_ascii=False, indent=2) + "\n",
801+
)
802+
return Response(
803+
archive.getvalue(),
804+
media_type="application/zip",
805+
headers={
806+
"Cache-Control": "no-store",
807+
"Content-Disposition": f'attachment; filename="openai-agent-identities-{timestamp}.zip"',
808+
},
809+
)
810+
751811
items = account_service.build_export_items(access_tokens)
752812
if not items:
753813
raise HTTPException(
754814
status_code=400,
755815
detail={"error": "没有可导出的完整账号,需要同时有 access_token、refresh_token 和 id_token"},
756816
)
757817

758-
timestamp = _download_timestamp()
759818
if body.format in {"zip", "cpa"}:
760819
content = _account_zip_bytes(items)
761820
return Response(
@@ -783,6 +842,12 @@ async def export_accounts(body: AccountExportRequest, authorization: str | None
783842
headers={"Content-Disposition": f'attachment; filename="codex-accounts-{timestamp}.json"'},
784843
)
785844

845+
@router.get("/api/accounts/agent-identities")
846+
async def list_agent_identities(authorization: str | None = Header(default=None)):
847+
require_admin(authorization)
848+
items = openai_agent_identity_store.summary()
849+
return {"total": len(items), "items": items}
850+
786851
@router.post("/api/accounts/update")
787852
async def update_account(body: AccountUpdateRequest, authorization: str | None = Header(default=None)):
788853
require_admin(authorization)

api/accounts_test.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ def setUp(self) -> None:
6060
"last_refresh": "2029-01-01T00:00:00+00:00",
6161
}
6262

63+
@staticmethod
64+
def _agent_identity(account_id: str, email: str) -> dict:
65+
return {
66+
"auth_mode": "agent_identity",
67+
"agent_identity": {
68+
"agent_runtime_id": f"runtime-{account_id}",
69+
"agent_private_key": f"private-{account_id}",
70+
"account_id": account_id,
71+
"chatgpt_user_id": f"user-{account_id}",
72+
"email": email,
73+
"plan_type": "free",
74+
"chatgpt_account_is_fedramp": False,
75+
},
76+
}
77+
6378
def test_sub2api_export_uses_standard_account_envelope(self) -> None:
6479
with patch.object(accounts_api, "require_admin"), patch.object(
6580
accounts_api.account_service,
@@ -100,6 +115,78 @@ def test_cpa_export_is_zip_of_codex_auth_files(self) -> None:
100115
self.assertEqual(exported["type"], "codex")
101116
self.assertEqual(exported["access_token"], "access-token")
102117

118+
def test_single_agent_identity_export_is_auth_json(self) -> None:
119+
auth_json = self._agent_identity("one", "one@example.test")
120+
with patch.object(accounts_api, "require_admin"), patch.object(
121+
accounts_api.account_service,
122+
"list_accounts",
123+
return_value=[self.item],
124+
), patch.object(
125+
accounts_api,
126+
"ensure_openai_agent_identity",
127+
return_value=auth_json,
128+
):
129+
response = self.client.post(
130+
"/api/accounts/export",
131+
json={"access_tokens": ["access-token"], "format": "agent_identity"},
132+
)
133+
134+
self.assertEqual(response.status_code, 200)
135+
self.assertEqual(response.headers["content-type"], "application/json")
136+
self.assertEqual(response.headers["content-disposition"], 'attachment; filename="auth.json"')
137+
self.assertEqual(response.json(), auth_json)
138+
139+
def test_multiple_agent_identity_export_is_zip(self) -> None:
140+
accounts = [
141+
{**self.item, "access_token": "token-one", "email": "one@example.test"},
142+
{**self.item, "access_token": "token-two", "email": "two@example.test"},
143+
]
144+
identities = [
145+
self._agent_identity("one", "one@example.test"),
146+
self._agent_identity("two", "two@example.test"),
147+
]
148+
with patch.object(accounts_api, "require_admin"), patch.object(
149+
accounts_api.account_service,
150+
"list_accounts",
151+
return_value=accounts,
152+
), patch.object(
153+
accounts_api,
154+
"ensure_openai_agent_identity",
155+
side_effect=identities,
156+
):
157+
response = self.client.post(
158+
"/api/accounts/export",
159+
json={"access_tokens": [], "format": "agent_identity"},
160+
)
161+
162+
self.assertEqual(response.status_code, 200)
163+
self.assertEqual(response.headers["content-type"], "application/zip")
164+
with zipfile.ZipFile(io.BytesIO(response.content)) as archive:
165+
self.assertEqual(archive.namelist(), [
166+
"one-example.test/auth.json",
167+
"two-example.test/auth.json",
168+
])
169+
exported = [json.loads(archive.read(name)) for name in archive.namelist()]
170+
self.assertEqual(exported, identities)
171+
172+
def test_agent_identity_list_does_not_include_private_keys(self) -> None:
173+
summaries = [{
174+
"account_id": "one",
175+
"email": "one@example.test",
176+
"agent_runtime_id": "runtime-one",
177+
"updated_at": "2030-01-01T00:00:00+00:00",
178+
}]
179+
with patch.object(accounts_api, "require_admin"), patch.object(
180+
accounts_api.openai_agent_identity_store,
181+
"summary",
182+
return_value=summaries,
183+
):
184+
response = self.client.get("/api/accounts/agent-identities")
185+
186+
self.assertEqual(response.status_code, 200)
187+
self.assertEqual(response.json(), {"total": 1, "items": summaries})
188+
self.assertNotIn("agent_private_key", response.text)
189+
103190

104191
if __name__ == "__main__":
105192
unittest.main()

api/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from services.log_service import cleanup_old_logs, start_log_cleanup_scheduler
3737
from services.grok_runtime import grok_runtime
3838
from services.register_service import register_service
39+
from services.openai_survival_service import openai_survival_service
3940
from services.realtime_monitor_service import realtime_monitor_service
4041
from utils.log import logger
4142

@@ -77,6 +78,7 @@ async def lifespan(host_app: FastAPI):
7778
cleanup_thread = start_image_cleanup_scheduler(stop_event)
7879
log_cleanup_thread = start_log_cleanup_scheduler(stop_event)
7980
register_service.start_grok_probe_scheduler(stop_event)
81+
openai_survival_thread = openai_survival_service.start_scheduler(stop_event)
8082
backup_service.start()
8183
config.cleanup_old_images()
8284
cleanup_old_logs()
@@ -88,6 +90,8 @@ async def lifespan(host_app: FastAPI):
8890
cleanup_thread.join(timeout=1)
8991
log_cleanup_thread.join(timeout=1)
9092
register_service.stop_grok_probe_scheduler()
93+
openai_survival_service.stop_scheduler()
94+
openai_survival_thread.join(timeout=1)
9195
try:
9296
dashboard_metrics_service.flush()
9397
except Exception as exc:

api/register.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from api.support import require_admin
1414
from services.register_service import GrokAccountChatTestError, register_service
15+
from services.openai_survival_service import openai_survival_service
1516

1617

1718
class RegisterConfigRequest(BaseModel):
@@ -20,6 +21,7 @@ class RegisterConfigRequest(BaseModel):
2021
mail: dict | None = None
2122
checkout: dict | None = None
2223
sub2api_sync: dict | None = None
24+
agent_identity_archive: dict | None = None
2325
proxy: str | None = None
2426
total: int | None = None
2527
threads: int | None = None
@@ -43,6 +45,13 @@ class GptMailStatusRequest(BaseModel):
4345
force: bool | None = None
4446

4547

48+
class OpenAISurvivalConfigRequest(BaseModel):
49+
enabled: bool | None = None
50+
interval_minutes: int | None = Field(default=None, ge=15, le=1440)
51+
concurrency: int | None = Field(default=None, ge=1, le=8)
52+
refresh_codex_rt: bool | None = None
53+
54+
4655
class GrokAccountIdsRequest(BaseModel):
4756
ids: list[str] = Field(default_factory=list)
4857

@@ -55,6 +64,10 @@ class GrokAccountDisabledRequest(GrokAccountIdsRequest):
5564
disabled: bool
5665

5766

67+
class GrokProbePollingRequest(BaseModel):
68+
enabled: bool
69+
70+
5871
class GrokAccountChatTestRequest(BaseModel):
5972
prompt: str = Field(min_length=1, max_length=1_200)
6073
model: str | None = Field(default=None, max_length=128)
@@ -144,6 +157,31 @@ async def refresh_gptmail_public_key(body: GptMailStatusRequest, authorization:
144157
except Exception as exc:
145158
raise HTTPException(status_code=400, detail=str(exc)) from exc
146159

160+
@router.get("/api/register/openai/survival")
161+
async def get_openai_survival(authorization: str | None = Header(default=None)):
162+
require_admin(authorization)
163+
return {"survival": openai_survival_service.status()}
164+
165+
@router.post("/api/register/openai/survival")
166+
async def update_openai_survival(
167+
body: OpenAISurvivalConfigRequest,
168+
authorization: str | None = Header(default=None),
169+
):
170+
require_admin(authorization)
171+
try:
172+
status = openai_survival_service.update_config(body.model_dump(exclude_none=True))
173+
except ValueError as exc:
174+
raise HTTPException(status_code=400, detail=str(exc)) from exc
175+
return {"survival": status}
176+
177+
@router.post("/api/register/openai/survival/run")
178+
async def run_openai_survival(authorization: str | None = Header(default=None)):
179+
require_admin(authorization)
180+
return {
181+
"started": openai_survival_service.run_now(),
182+
"survival": openai_survival_service.status(),
183+
}
184+
147185
@router.get("/api/register/grok/accounts")
148186
async def list_grok_accounts(
149187
page: int = Query(default=1, ge=1),
@@ -171,8 +209,17 @@ async def list_grok_accounts(
171209
"summary": view.get("summary") if isinstance(view.get("summary"), dict) else {},
172210
"runtime_available": bool(view.get("runtime_available")),
173211
"runtime_error": str(view.get("runtime_error") or ""),
212+
"probe_scheduler": register_service.grok_probe_scheduler_status(),
174213
}
175214

215+
@router.post("/api/register/grok/probe-polling")
216+
async def set_grok_probe_polling(
217+
body: GrokProbePollingRequest,
218+
authorization: str | None = Header(default=None),
219+
):
220+
require_admin(authorization)
221+
return {"probe_scheduler": register_service.set_grok_probe_scheduler_enabled(body.enabled)}
222+
176223
@router.post("/api/register/grok/accounts/runtime/snapshot")
177224
async def refresh_grok_runtime_snapshot(authorization: str | None = Header(default=None)):
178225
require_admin(authorization)

api/register_test.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def test_list_grok_accounts_supports_filters_and_pagination(self) -> None:
3838
register_api.register_service,
3939
"grok_accounts_view",
4040
return_value=view,
41-
) as list_accounts:
41+
) as list_accounts, patch.object(
42+
register_api.register_service,
43+
"grok_probe_scheduler_status",
44+
return_value={"enabled": False, "running": False, "next_run_at": ""},
45+
):
4246
response = self.client.get(
4347
"/api/register/grok/accounts",
4448
params={
@@ -62,10 +66,24 @@ def test_list_grok_accounts_supports_filters_and_pagination(self) -> None:
6266
"summary": {"total": 8, "synced": 3},
6367
"runtime_available": True,
6468
"runtime_error": "",
69+
"probe_scheduler": {"enabled": False, "running": False, "next_run_at": ""},
6570
},
6671
)
6772
list_accounts.assert_called_once_with(keyword="example", status="active")
6873

74+
def test_grok_probe_polling_toggle_uses_persistent_service_setting(self) -> None:
75+
status = {"enabled": True, "running": False, "next_run_at": "2030-01-01T00:00:00+00:00"}
76+
with patch.object(register_api, "require_admin"), patch.object(
77+
register_api.register_service,
78+
"set_grok_probe_scheduler_enabled",
79+
return_value=status,
80+
) as toggle:
81+
response = self.client.post("/api/register/grok/probe-polling", json={"enabled": True})
82+
83+
self.assertEqual(response.status_code, 200)
84+
self.assertEqual(response.json(), {"probe_scheduler": status})
85+
toggle.assert_called_once_with(True)
86+
6987
def test_stop_checkout_retries_uses_dedicated_service_action(self) -> None:
7088
response_payload = {"enabled": False, "checkout_tasks": [{"status": "cancelled"}]}
7189
with patch.object(register_api, "require_admin"), patch.object(

0 commit comments

Comments
 (0)