HouYunFei Codex commited on
Commit
2331be7
ยท
1 Parent(s): 6de60d1

Add registration runner UI

Browse files

Co-Authored-By: Codex <noreply@openai.com>

api/accounts.py CHANGED
@@ -327,4 +327,3 @@ def create_router() -> APIRouter:
327
  return {"import_job": server.get("import_job")}
328
 
329
  return router
330
-
 
327
  return {"import_job": server.get("import_job")}
328
 
329
  return router
 
api/app.py CHANGED
@@ -8,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.responses import FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
- from api import accounts, ai, system
12
  from api.support import resolve_web_asset, start_limited_account_watcher
13
  from services.config import config
14
 
@@ -37,6 +37,7 @@ def create_app() -> FastAPI:
37
  )
38
  app.include_router(ai.create_router())
39
  app.include_router(accounts.create_router())
 
40
  app.include_router(system.create_router(app_version))
41
  if config.images_dir.exists():
42
  app.mount("/images", StaticFiles(directory=str(config.images_dir)), name="images")
 
8
  from fastapi.responses import FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
+ from api import accounts, ai, register, system
12
  from api.support import resolve_web_asset, start_limited_account_watcher
13
  from services.config import config
14
 
 
37
  )
38
  app.include_router(ai.create_router())
39
  app.include_router(accounts.create_router())
40
+ app.include_router(register.create_router())
41
  app.include_router(system.create_router(app_version))
42
  if config.images_dir.exists():
43
  app.mount("/images", StaticFiles(directory=str(config.images_dir)), name="images")
api/register.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+
6
+ from fastapi import APIRouter, Header
7
+ from fastapi.responses import StreamingResponse
8
+ from pydantic import BaseModel
9
+
10
+ from api.support import require_admin
11
+ from services.register_service import register_service
12
+
13
+
14
+ class RegisterConfigRequest(BaseModel):
15
+ mail: dict | None = None
16
+ proxy: str | None = None
17
+ total: int | None = None
18
+ threads: int | None = None
19
+ mode: str | None = None
20
+ target_quota: int | None = None
21
+ target_available: int | None = None
22
+ check_interval: int | None = None
23
+
24
+
25
+ def create_router() -> APIRouter:
26
+ router = APIRouter()
27
+
28
+ @router.get("/api/register")
29
+ async def get_register_config(authorization: str | None = Header(default=None)):
30
+ require_admin(authorization)
31
+ return {"register": register_service.get()}
32
+
33
+ @router.post("/api/register")
34
+ async def update_register_config(body: RegisterConfigRequest, authorization: str | None = Header(default=None)):
35
+ require_admin(authorization)
36
+ return {"register": register_service.update(body.model_dump(exclude_none=True))}
37
+
38
+ @router.post("/api/register/start")
39
+ async def start_register(authorization: str | None = Header(default=None)):
40
+ require_admin(authorization)
41
+ return {"register": register_service.start()}
42
+
43
+ @router.post("/api/register/stop")
44
+ async def stop_register(authorization: str | None = Header(default=None)):
45
+ require_admin(authorization)
46
+ return {"register": register_service.stop()}
47
+
48
+ @router.post("/api/register/reset")
49
+ async def reset_register(authorization: str | None = Header(default=None)):
50
+ require_admin(authorization)
51
+ return {"register": register_service.reset()}
52
+
53
+ @router.get("/api/register/events")
54
+ async def register_events(token: str = ""):
55
+ require_admin(f"Bearer {token}")
56
+
57
+ async def stream():
58
+ last = ""
59
+ while True:
60
+ payload = json.dumps(register_service.get(), ensure_ascii=False)
61
+ if payload != last:
62
+ last = payload
63
+ yield f"data: {payload}\n\n"
64
+ await asyncio.sleep(0.5)
65
+
66
+ return StreamingResponse(stream(), media_type="text/event-stream")
67
+
68
+ return router
config.json CHANGED
@@ -2,7 +2,7 @@
2
  "auth-key": "chatgpt2api",
3
  "refresh_account_interval_minute": 60,
4
  "image_retention_days": 15,
5
- "auto_remove_invalid_accounts": false,
6
  "log_levels": [
7
  "debug",
8
  "error",
 
2
  "auth-key": "chatgpt2api",
3
  "refresh_account_interval_minute": 60,
4
  "image_retention_days": 15,
5
+ "auto_remove_invalid_accounts": true,
6
  "log_levels": [
7
  "debug",
8
  "error",
services/register/__init__.py ADDED
File without changes
services/register/mail_provider.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import re
5
+ import string
6
+ import time
7
+ from datetime import datetime, timezone
8
+ from email import message_from_string, policy
9
+ from email.utils import parsedate_to_datetime
10
+ from threading import Lock
11
+ from typing import Any, Callable, TypeVar
12
+
13
+ import requests
14
+ from curl_cffi import requests as curl_requests
15
+
16
+
17
+ ResultT = TypeVar("ResultT")
18
+ domain_lock = Lock()
19
+ provider_lock = Lock()
20
+ domain_index = 0
21
+ provider_index = 0
22
+
23
+
24
+ def _config(mail_config: dict) -> dict:
25
+ return {
26
+ "request_timeout": float(mail_config.get("request_timeout") or 15),
27
+ "wait_timeout": float(mail_config.get("wait_timeout") or 30),
28
+ "wait_interval": float(mail_config.get("wait_interval") or 3),
29
+ "user_agent": str(mail_config.get("user_agent") or "Mozilla/5.0"),
30
+ }
31
+
32
+
33
+ def _random_mailbox_name() -> str:
34
+ return f"{''.join(random.choices(string.ascii_lowercase, k=5))}{''.join(random.choices(string.digits, k=random.randint(1, 3)))}{''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3)))}"
35
+
36
+
37
+ def _random_subdomain_label() -> str:
38
+ return "".join(random.choices(string.ascii_lowercase + string.digits, k=random.randint(4, 10)))
39
+
40
+
41
+ def _next_domain(domains: list[str]) -> str:
42
+ global domain_index
43
+ domains = [str(item).strip() for item in domains if str(item).strip()]
44
+ if not domains:
45
+ raise RuntimeError("mail.domain ไธ่ƒฝไธบ็ฉบ")
46
+ if len(domains) == 1:
47
+ return domains[0]
48
+ with domain_lock:
49
+ value = domains[domain_index % len(domains)]
50
+ domain_index = (domain_index + 1) % len(domains)
51
+ return value
52
+
53
+
54
+ def _parse_received_at(value: Any) -> datetime | None:
55
+ if isinstance(value, (int, float)):
56
+ try:
57
+ return datetime.fromtimestamp(float(value), tz=timezone.utc)
58
+ except Exception:
59
+ return None
60
+ text = str(value or "").strip()
61
+ if not text:
62
+ return None
63
+ try:
64
+ date = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
65
+ return date if date.tzinfo else date.replace(tzinfo=timezone.utc)
66
+ except Exception:
67
+ pass
68
+ try:
69
+ date = parsedate_to_datetime(text)
70
+ return date if date.tzinfo else date.replace(tzinfo=timezone.utc)
71
+ except Exception:
72
+ return None
73
+
74
+
75
+ def _extract_content(data: dict[str, Any]) -> tuple[str, str]:
76
+ text_content = str(data.get("text_content") or data.get("text") or data.get("body") or data.get("content") or "")
77
+ html_content = str(data.get("html_content") or data.get("html") or data.get("html_body") or data.get("body_html") or "")
78
+ if text_content or html_content:
79
+ return text_content, html_content
80
+ raw = data.get("raw")
81
+ if not isinstance(raw, str) or not raw.strip():
82
+ return "", ""
83
+ try:
84
+ parsed = message_from_string(raw, policy=policy.default)
85
+ except Exception:
86
+ return raw, ""
87
+ plain: list[str] = []
88
+ html: list[str] = []
89
+ for part in parsed.walk() if parsed.is_multipart() else [parsed]:
90
+ if part.get_content_maintype() == "multipart":
91
+ continue
92
+ try:
93
+ payload = part.get_content()
94
+ except Exception:
95
+ payload = ""
96
+ if not payload:
97
+ continue
98
+ if part.get_content_type() == "text/html":
99
+ html.append(str(payload))
100
+ else:
101
+ plain.append(str(payload))
102
+ return "\n".join(plain).strip(), "\n".join(html).strip()
103
+
104
+
105
+ def _extract_text_candidates(value: Any) -> list[str]:
106
+ if isinstance(value, str):
107
+ return [value]
108
+ if isinstance(value, dict):
109
+ out: list[str] = []
110
+ for key in ("address", "email", "name", "value"):
111
+ if value.get(key):
112
+ out.extend(_extract_text_candidates(value.get(key)))
113
+ return out
114
+ if isinstance(value, list):
115
+ out: list[str] = []
116
+ for item in value:
117
+ out.extend(_extract_text_candidates(item))
118
+ return out
119
+ return []
120
+
121
+
122
+ def _message_matches_email(data: dict[str, Any], email: str) -> bool:
123
+ target = str(email or "").strip().lower()
124
+ candidates: list[str] = []
125
+ for key in ("to", "mailTo", "receiver", "receivers", "address", "email", "envelope_to"):
126
+ if key in data:
127
+ candidates.extend(_extract_text_candidates(data.get(key)))
128
+ return not target or not candidates or any(target in str(item).strip().lower() for item in candidates if str(item).strip())
129
+
130
+
131
+ def _extract_code(message: dict[str, Any]) -> str | None:
132
+ content = f"{message.get('subject', '')}\n{message.get('text_content', '')}\n{message.get('html_content', '')}".strip()
133
+ if not content:
134
+ return None
135
+ match = re.search(r"background-color:\s*#F3F3F3[^>]*>[\s\S]*?(\d{6})[\s\S]*?</p>", content, re.I)
136
+ if match:
137
+ return match.group(1)
138
+ match = re.search(r"(?:Verification code|code is|ไปฃ็ ไธบ|้ชŒ่ฏ็ )[:\s]*(\d{6})", content, re.I)
139
+ if match and match.group(1) != "177010":
140
+ return match.group(1)
141
+ for code in re.findall(r">\s*(\d{6})\s*<|(?<![#&])\b(\d{6})\b", content):
142
+ value = code[0] or code[1]
143
+ if value and value != "177010":
144
+ return value
145
+ return None
146
+
147
+
148
+ class BaseMailProvider:
149
+ name = "unknown"
150
+
151
+ def __init__(self, conf: dict, provider_ref: str = ""):
152
+ self.conf = conf
153
+ self.provider_ref = provider_ref
154
+
155
+ def wait_for(self, mailbox: dict[str, Any], on_message: Callable[[dict[str, Any]], ResultT | None]) -> ResultT | None:
156
+ deadline = time.monotonic() + self.conf["wait_timeout"]
157
+ while time.monotonic() < deadline:
158
+ message = self.fetch_latest_message(mailbox)
159
+ if message:
160
+ result = on_message(message)
161
+ if result is not None:
162
+ return result
163
+ time.sleep(max(0.2, self.conf["wait_interval"]))
164
+ return None
165
+
166
+ def wait_for_code(self, mailbox: dict[str, Any]) -> str | None:
167
+ return self.wait_for(mailbox, _extract_code)
168
+
169
+ def close(self) -> None:
170
+ pass
171
+
172
+
173
+ class CloudflareTempMailProvider(BaseMailProvider):
174
+ name = "cloudflare_temp_email"
175
+
176
+ def __init__(self, entry: dict, conf: dict):
177
+ super().__init__(conf, str(entry.get("provider_ref") or ""))
178
+ self.api_base = str(entry["api_base"]).rstrip("/")
179
+ self.admin_password = str(entry["admin_password"]).strip()
180
+ self.domain = entry.get("domain") or []
181
+ self.session = curl_requests.Session(impersonate="chrome")
182
+
183
+ def _request(self, method: str, path: str, headers: dict | None = None, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200,)):
184
+ resp = self.session.request(method.upper(), f"{self.api_base}{path}", headers={"Content-Type": "application/json", "User-Agent": self.conf["user_agent"], **(headers or {})}, params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
185
+ if resp.status_code not in expected:
186
+ raise RuntimeError(f"CloudflareTempMail ่ฏทๆฑ‚ๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
187
+ return {} if resp.status_code == 204 else resp.json()
188
+
189
+ def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
190
+ data = self._request("POST", "/admin/new_address", headers={"x-admin-auth": self.admin_password}, payload={"enablePrefix": True, "name": username or _random_mailbox_name(), "domain": _next_domain(self.domain)})
191
+ address = str(data.get("address") or "").strip()
192
+ token = str(data.get("jwt") or "").strip()
193
+ if not address or not token:
194
+ raise RuntimeError("CloudflareTempMail ็ผบๅฐ‘ address ๆˆ– jwt")
195
+ return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": token}
196
+
197
+ def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
198
+ data = self._request("GET", "/api/mails", headers={"Authorization": f"Bearer {mailbox['token']}"}, params={"limit": 10, "offset": 0})
199
+ raw = list(data.get("results") or []) if isinstance(data, dict) else data if isinstance(data, list) else []
200
+ messages = [item for item in raw if isinstance(item, dict) and _message_matches_email(item, str(mailbox.get("address") or ""))]
201
+ if not messages:
202
+ return None
203
+ item = messages[0]
204
+ text_content, html_content = _extract_content(item)
205
+ sender = item.get("from") or item.get("sender") or ""
206
+ if isinstance(sender, dict):
207
+ sender = sender.get("address") or sender.get("email") or sender.get("name") or ""
208
+ return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or item.get("_id") or ""), "subject": str(item.get("subject") or ""), "sender": str(sender), "text_content": text_content, "html_content": html_content, "received_at": _parse_received_at(item.get("createdAt") or item.get("created_at") or item.get("receivedAt") or item.get("date") or item.get("timestamp")), "raw": item}
209
+
210
+ def close(self) -> None:
211
+ self.session.close()
212
+
213
+
214
+ class TempMailLolProvider(BaseMailProvider):
215
+ name = "tempmail_lol"
216
+
217
+ def __init__(self, entry: dict, conf: dict):
218
+ super().__init__(conf, str(entry.get("provider_ref") or ""))
219
+ self.api_key = str(entry.get("api_key") or "").strip()
220
+ self.domain = [str(item).strip() for item in (entry.get("domain") or []) if str(item).strip()]
221
+ self.session = requests.Session()
222
+ self.session.trust_env = False
223
+ self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json"})
224
+ if self.api_key:
225
+ self.session.headers["Authorization"] = f"Bearer {self.api_key}"
226
+
227
+ @staticmethod
228
+ def _resolve_domain(domain: str) -> tuple[str, bool]:
229
+ text = str(domain or "").strip().lower()
230
+ if text.startswith("*.") and len(text) > 2:
231
+ return f"{_random_subdomain_label()}.{text[2:]}", True
232
+ return text, False
233
+
234
+ def _request(self, method: str, path: str, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200,)):
235
+ resp = self.session.request(method.upper(), f"https://api.tempmail.lol/v2{path}", params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
236
+ if resp.status_code not in expected:
237
+ raise RuntimeError(f"TempMail.lol ่ฏทๆฑ‚ๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
238
+ data = resp.json()
239
+ if not isinstance(data, dict):
240
+ raise RuntimeError(f"TempMail.lol {method} {path} ่ฟ”ๅ›ž็ป“ๆž„ไธๆ˜ฏๅฏน่ฑก")
241
+ return data
242
+
243
+ def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
244
+ payload: dict[str, Any] = {}
245
+ if self.domain:
246
+ domain, force_random_prefix = self._resolve_domain(random.choice(self.domain))
247
+ payload["domain"] = domain
248
+ if force_random_prefix:
249
+ payload["prefix"] = _random_mailbox_name()
250
+ if username and "prefix" not in payload:
251
+ payload["prefix"] = username
252
+ data = self._request("POST", "/inbox/create", payload=payload, expected=(200, 201))
253
+ address = str(data.get("address") or "").strip()
254
+ token = str(data.get("token") or "").strip()
255
+ if not address or not token:
256
+ raise RuntimeError("TempMail.lol ็ผบๅฐ‘ address ๆˆ– token")
257
+ return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": token}
258
+
259
+ def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
260
+ data = self._request("GET", "/inbox", params={"token": mailbox["token"]})
261
+ items = data.get("emails") or data.get("messages") or []
262
+ messages = [item for item in items if isinstance(item, dict)] if isinstance(items, list) else []
263
+ if not messages:
264
+ return None
265
+ item = max(messages, key=lambda value: ((_parse_received_at(value.get("created_at") or value.get("createdAt") or value.get("date") or value.get("received_at") or value.get("timestamp")) or datetime.fromtimestamp(0, tz=timezone.utc)).timestamp(), str(value.get("id") or value.get("token") or "")))
266
+ text_content, html_content = _extract_content(item)
267
+ return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or item.get("token") or ""), "subject": str(item.get("subject") or ""), "sender": str(item.get("from") or item.get("from_address") or ""), "text_content": text_content, "html_content": html_content, "received_at": _parse_received_at(item.get("created_at") or item.get("createdAt") or item.get("date") or item.get("received_at") or item.get("timestamp")), "raw": item}
268
+
269
+ def close(self) -> None:
270
+ self.session.close()
271
+
272
+
273
+ class DuckMailProvider(BaseMailProvider):
274
+ name = "duckmail"
275
+
276
+ def __init__(self, entry: dict, conf: dict):
277
+ super().__init__(conf, str(entry.get("provider_ref") or ""))
278
+ self.api_key = str(entry["api_key"]).strip()
279
+ self.default_domain = str(entry.get("default_domain") or "duckmail.sbs").strip() or "duckmail.sbs"
280
+ self.session = requests.Session()
281
+ self.session.trust_env = False
282
+ self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json"})
283
+
284
+ def _request(self, method: str, path: str, token: str = "", use_api_key: bool = False, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200, 201, 204)):
285
+ headers = {"Authorization": f"Bearer {self.api_key if use_api_key else token}"} if use_api_key or token else {}
286
+ resp = self.session.request(method.upper(), f"https://api.duckmail.sbs{path}", headers=headers, params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
287
+ if resp.status_code not in expected:
288
+ raise RuntimeError(f"DuckMail ่ฏทๆฑ‚ๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
289
+ return {} if resp.status_code == 204 else resp.json()
290
+
291
+ @staticmethod
292
+ def _items(data):
293
+ return data if isinstance(data, list) else data.get("hydra:member") or data.get("member") or data.get("data") or []
294
+
295
+ def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
296
+ domains = self._items(self._request("GET", "/domains", use_api_key=True))
297
+ domain = random.choice(domains).get("domain") if domains else self.default_domain
298
+ password = "".join(random.choices(string.ascii_letters + string.digits, k=12))
299
+ address = f"{username or _random_mailbox_name()}@{domain}"
300
+ payload = {"address": address, "password": password}
301
+ account = self._request("POST", "/accounts", use_api_key=True, payload=payload)
302
+ token_data = self._request("POST", "/token", use_api_key=True, payload=payload)
303
+ return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": str(token_data.get("token") or ""), "password": password, "account_id": str(account.get("id") or "")}
304
+
305
+ def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
306
+ data = self._request("GET", "/messages", token=str(mailbox.get("token") or ""), params={"page": 1})
307
+ items = self._items(data)
308
+ if not items:
309
+ return None
310
+ item = items[0]
311
+ message_id = str(item.get("id") or item.get("@id") or "").replace("/messages/", "")
312
+ if message_id:
313
+ item = self._request("GET", f"/messages/{message_id}", token=str(mailbox.get("token") or ""))
314
+ sender = item.get("from") or ""
315
+ if isinstance(sender, dict):
316
+ sender = sender.get("address") or sender.get("name") or ""
317
+ html_content = item.get("html") or ""
318
+ if isinstance(html_content, list):
319
+ html_content = "".join(str(value) for value in html_content)
320
+ return {"provider": self.name, "mailbox": mailbox["address"], "message_id": message_id, "subject": str(item.get("subject") or ""), "sender": str(sender), "text_content": str(item.get("text") or item.get("text_content") or ""), "html_content": str(html_content), "received_at": _parse_received_at(item.get("createdAt") or item.get("created_at") or item.get("receivedAt") or item.get("date")), "raw": item}
321
+
322
+ def close(self) -> None:
323
+ self.session.close()
324
+
325
+
326
+ class GptMailProvider(BaseMailProvider):
327
+ name = "gptmail"
328
+
329
+ def __init__(self, entry: dict, conf: dict):
330
+ super().__init__(conf, str(entry.get("provider_ref") or ""))
331
+ self.api_key = str(entry["api_key"]).strip()
332
+ self.default_domain = str(entry.get("default_domain") or "").strip()
333
+ self.session = requests.Session()
334
+ self.session.trust_env = False
335
+ self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json", "X-API-Key": self.api_key})
336
+
337
+ def _request(self, method: str, path: str, params: dict | None = None, payload: dict | None = None):
338
+ query = dict(params or {})
339
+ resp = self.session.request(method.upper(), f"https://mail.chatgpt.org.uk{path}", params=query, json=payload, timeout=self.conf["request_timeout"], verify=False)
340
+ if resp.status_code != 200:
341
+ raise RuntimeError(f"GPTMail ่ฏทๆฑ‚ๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
342
+ data = resp.json()
343
+ return data["data"] if isinstance(data, dict) and "data" in data else data
344
+
345
+ def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
346
+ payload = {key: value for key, value in {"prefix": username, "domain": self.default_domain}.items() if value}
347
+ data = self._request("POST" if payload else "GET", "/api/generate-email", payload=payload or None)
348
+ return {"provider": self.name, "provider_ref": self.provider_ref, "address": str(data["email"])}
349
+
350
+ def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
351
+ data = self._request("GET", "/api/emails", params={"email": mailbox["address"]})
352
+ emails = data if isinstance(data, list) else data.get("emails") or []
353
+ if not emails:
354
+ return None
355
+ item = max(emails, key=lambda value: (float(value.get("timestamp") or 0), str(value.get("id") or "")))
356
+ if item.get("id"):
357
+ item = self._request("GET", f"/api/email/{item['id']}")
358
+ return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or ""), "subject": str(item.get("subject") or ""), "sender": str(item.get("from_address") or ""), "text_content": str(item.get("content") or ""), "html_content": str(item.get("html_content") or ""), "received_at": _parse_received_at(item.get("timestamp") or item.get("created_at")), "raw": item}
359
+
360
+ def close(self) -> None:
361
+ self.session.close()
362
+
363
+
364
+ def _entries(mail_config: dict) -> list[dict]:
365
+ return [{**item, "provider_ref": f"{item['type']}#{index + 1}"} for index, item in enumerate(mail_config["providers"])]
366
+
367
+
368
+ def _enabled_entries(mail_config: dict) -> list[dict]:
369
+ items = [item for item in _entries(mail_config) if item.get("enable")]
370
+ if not items:
371
+ raise RuntimeError("mail.providers ๆฒกๆœ‰ๅฏ็”จ็š„ provider")
372
+ return items
373
+
374
+
375
+ def _next_entry(mail_config: dict) -> dict:
376
+ global provider_index
377
+ items = _enabled_entries(mail_config)
378
+ if len(items) == 1:
379
+ return dict(items[0])
380
+ with provider_lock:
381
+ value = dict(items[provider_index % len(items)])
382
+ provider_index = (provider_index + 1) % len(items)
383
+ return value
384
+
385
+
386
+ def _create_provider(mail_config: dict, provider: str = "", provider_ref: str = "") -> BaseMailProvider:
387
+ entry = next((dict(item) for item in _entries(mail_config) if provider_ref and item["provider_ref"] == provider_ref), None)
388
+ entry = entry or next((dict(item) for item in _enabled_entries(mail_config) if provider and item["type"] == provider), None) or _next_entry(mail_config)
389
+ conf = _config(mail_config)
390
+ if entry["type"] == "cloudflare_temp_email":
391
+ return CloudflareTempMailProvider(entry, conf)
392
+ if entry["type"] == "tempmail_lol":
393
+ return TempMailLolProvider(entry, conf)
394
+ if entry["type"] == "duckmail":
395
+ return DuckMailProvider(entry, conf)
396
+ if entry["type"] == "gptmail":
397
+ return GptMailProvider(entry, conf)
398
+ raise RuntimeError(f"ไธๆ”ฏๆŒ็š„ mail.provider: {entry['type']}")
399
+
400
+
401
+ def create_mailbox(mail_config: dict, username: str | None = None) -> dict:
402
+ provider = _create_provider(mail_config)
403
+ try:
404
+ return provider.create_mailbox(username)
405
+ finally:
406
+ provider.close()
407
+
408
+
409
+ def wait_for_code(mail_config: dict, mailbox: dict) -> str | None:
410
+ provider = _create_provider(mail_config, str(mailbox.get("provider") or ""), str(mailbox.get("provider_ref") or ""))
411
+ try:
412
+ return provider.wait_for_code(mailbox)
413
+ finally:
414
+ provider.close()
services/register/openai_register.py ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import random
7
+ import secrets
8
+ import string
9
+ import threading
10
+ import time
11
+ import uuid
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from urllib.parse import parse_qs, urlencode, urlparse
15
+
16
+ import requests
17
+ import urllib3
18
+ from requests.adapters import HTTPAdapter
19
+ from urllib3.util.retry import Retry
20
+
21
+ from services.account_service import account_service
22
+ from services.register import mail_provider
23
+
24
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
25
+ base_dir = Path(__file__).resolve().parent
26
+ config = {
27
+ "mail": {
28
+ "request_timeout": 15,
29
+ "wait_timeout": 30,
30
+ "wait_interval": 3,
31
+ "providers": [],
32
+ },
33
+ "proxy": "",
34
+ "total": 20000,
35
+ "threads": 64,
36
+ }
37
+ register_config_file = base_dir.parents[1] / "data" / "register.json"
38
+ try:
39
+ saved_config = json.loads(register_config_file.read_text(encoding="utf-8"))
40
+ config.update({key: saved_config[key] for key in ("mail", "proxy", "total", "threads") if key in saved_config})
41
+ except Exception:
42
+ pass
43
+
44
+ auth_base = "https://auth.openai.com"
45
+ platform_base = "https://platform.openai.com"
46
+ platform_oauth_client_id = "app_2SKx67EdpoN0G6j64rFvigXD"
47
+ platform_oauth_redirect_uri = f"{platform_base}/auth/callback"
48
+ platform_oauth_audience = "https://api.openai.com/v1"
49
+ platform_auth0_client = "eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMjEuMCJ9"
50
+ user_agent = (
51
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
52
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
53
+ "Chrome/145.0.0.0 Safari/537.36"
54
+ )
55
+ sec_ch_ua = '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"'
56
+ sec_ch_ua_full_version_list = '"Chromium";v="145.0.0.0", "Not:A-Brand";v="99.0.0.0", "Google Chrome";v="145.0.0.0"'
57
+ default_timeout = 30
58
+ print_lock = threading.Lock()
59
+ stats_lock = threading.Lock()
60
+ stats = {"done": 0, "success": 0, "fail": 0, "start_time": 0.0}
61
+ register_log_sink = None
62
+
63
+ common_headers = {
64
+ "accept": "application/json",
65
+ "accept-language": "en-US,en;q=0.9",
66
+ "content-type": "application/json",
67
+ "origin": auth_base,
68
+ "priority": "u=1, i",
69
+ "user-agent": user_agent,
70
+ "sec-ch-ua": sec_ch_ua,
71
+ "sec-ch-ua-arch": '"x86_64"',
72
+ "sec-ch-ua-bitness": '"64"',
73
+ "sec-ch-ua-full-version-list": sec_ch_ua_full_version_list,
74
+ "sec-ch-ua-mobile": "?0",
75
+ "sec-ch-ua-model": '""',
76
+ "sec-ch-ua-platform": '"Windows"',
77
+ "sec-ch-ua-platform-version": '"10.0.0"',
78
+ "sec-fetch-dest": "empty",
79
+ "sec-fetch-mode": "cors",
80
+ "sec-fetch-site": "same-origin",
81
+ }
82
+
83
+ navigate_headers = {
84
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
85
+ "accept-language": "en-US,en;q=0.9",
86
+ "user-agent": user_agent,
87
+ "sec-ch-ua": sec_ch_ua,
88
+ "sec-ch-ua-arch": '"x86_64"',
89
+ "sec-ch-ua-bitness": '"64"',
90
+ "sec-ch-ua-full-version-list": sec_ch_ua_full_version_list,
91
+ "sec-ch-ua-mobile": "?0",
92
+ "sec-ch-ua-model": '""',
93
+ "sec-ch-ua-platform": '"Windows"',
94
+ "sec-ch-ua-platform-version": '"10.0.0"',
95
+ "sec-fetch-dest": "document",
96
+ "sec-fetch-mode": "navigate",
97
+ "sec-fetch-site": "same-origin",
98
+ "sec-fetch-user": "?1",
99
+ "upgrade-insecure-requests": "1",
100
+ }
101
+
102
+
103
+ def log(text: str, color: str = "") -> None:
104
+ colors = {"red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m"}
105
+ if register_log_sink:
106
+ try:
107
+ register_log_sink(text, color)
108
+ except Exception:
109
+ pass
110
+ with print_lock:
111
+ prefix = colors.get(color, "")
112
+ suffix = "\033[0m" if prefix else ""
113
+ print(f"{prefix}{datetime.now().strftime('%H:%M:%S')} {text}{suffix}")
114
+
115
+
116
+ def step(index: int, text: str, color: str = "") -> None:
117
+ log(f"[ไปปๅŠก{index}] {text}", color)
118
+
119
+
120
+ def _make_trace_headers() -> dict[str, str]:
121
+ trace_id = str(random.getrandbits(64))
122
+ parent_id = str(random.getrandbits(64))
123
+ return {
124
+ "traceparent": f"00-{uuid.uuid4().hex}-{format(int(parent_id), '016x')}-01",
125
+ "tracestate": "dd=s:1;o:rum",
126
+ "x-datadog-origin": "rum",
127
+ "x-datadog-parent-id": parent_id,
128
+ "x-datadog-sampling-priority": "1",
129
+ "x-datadog-trace-id": trace_id,
130
+ }
131
+
132
+
133
+ def _generate_pkce() -> tuple[str, str]:
134
+ code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii")
135
+ code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("ascii")).digest()).rstrip(b"=").decode("ascii")
136
+ return code_verifier, code_challenge
137
+
138
+
139
+ def _random_password(length: int = 16) -> str:
140
+ chars = string.ascii_letters + string.digits + "!@#$%"
141
+ value = list(
142
+ secrets.choice(string.ascii_uppercase)
143
+ + secrets.choice(string.ascii_lowercase)
144
+ + secrets.choice(string.digits)
145
+ + secrets.choice("!@#$%")
146
+ + "".join(secrets.choice(chars) for _ in range(max(0, length - 4)))
147
+ )
148
+ random.shuffle(value)
149
+ return "".join(value)
150
+
151
+
152
+ def _random_name() -> tuple[str, str]:
153
+ return random.choice(["James", "Robert", "John", "Michael", "David", "Mary", "Emma", "Olivia"]), random.choice(
154
+ ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller"]
155
+ )
156
+
157
+
158
+ def _random_birthdate() -> str:
159
+ return f"{random.randint(1996, 2006):04d}-{random.randint(1, 12):02d}-{random.randint(1, 28):02d}"
160
+
161
+
162
+ def _response_json(resp) -> dict:
163
+ try:
164
+ data = resp.json()
165
+ return data if isinstance(data, dict) else {}
166
+ except Exception:
167
+ return {}
168
+
169
+
170
+ def _decode_jwt_payload(token: str) -> dict:
171
+ try:
172
+ payload = token.split(".")[1]
173
+ padding = 4 - len(payload) % 4
174
+ if padding != 4:
175
+ payload += "=" * padding
176
+ return json.loads(base64.urlsafe_b64decode(payload))
177
+ except Exception:
178
+ return {}
179
+
180
+
181
+ def create_mailbox(username: str | None = None) -> dict:
182
+ return mail_provider.create_mailbox(config["mail"], username)
183
+
184
+
185
+ def wait_for_code(mailbox: dict) -> str | None:
186
+ return mail_provider.wait_for_code(config["mail"], mailbox)
187
+
188
+
189
+ class SentinelTokenGenerator:
190
+ MAX_ATTEMPTS = 500000
191
+ ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D"
192
+
193
+ def __init__(self, device_id: str, ua: str):
194
+ self.device_id = device_id
195
+ self.user_agent = ua
196
+ self.sid = str(uuid.uuid4())
197
+
198
+ @staticmethod
199
+ def _fnv1a_32(text: str) -> str:
200
+ h = 2166136261
201
+ for ch in text:
202
+ h ^= ord(ch)
203
+ h = (h * 16777619) & 0xFFFFFFFF
204
+ h ^= h >> 16
205
+ h = (h * 2246822507) & 0xFFFFFFFF
206
+ h ^= h >> 13
207
+ h = (h * 3266489909) & 0xFFFFFFFF
208
+ h ^= h >> 16
209
+ return format(h & 0xFFFFFFFF, "08x")
210
+
211
+ def _get_config(self) -> list:
212
+ perf_now = random.uniform(1000, 50000)
213
+ return [
214
+ "1920x1080",
215
+ time.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", time.gmtime()),
216
+ 4294705152,
217
+ random.random(),
218
+ self.user_agent,
219
+ "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js",
220
+ None,
221
+ None,
222
+ "en-US",
223
+ random.random(),
224
+ random.choice(["vendorSub-undefined", "plugins-undefined", "mimeTypes-undefined", "hardwareConcurrency-undefined"]),
225
+ random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]),
226
+ random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]),
227
+ perf_now,
228
+ self.sid,
229
+ "",
230
+ random.choice([4, 8, 12, 16]),
231
+ time.time() * 1000 - perf_now,
232
+ ]
233
+
234
+ @staticmethod
235
+ def _b64(data) -> str:
236
+ return base64.b64encode(json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8")).decode("ascii")
237
+
238
+ def generate_requirements_token(self) -> str:
239
+ data = self._get_config()
240
+ data[3] = 1
241
+ data[9] = round(random.uniform(5, 50))
242
+ return "gAAAAAC" + self._b64(data)
243
+
244
+ def generate_token(self, seed: str, difficulty: str) -> str:
245
+ start = time.time()
246
+ data = self._get_config()
247
+ difficulty = str(difficulty or "0")
248
+ for i in range(self.MAX_ATTEMPTS):
249
+ data[3] = i
250
+ data[9] = round((time.time() - start) * 1000)
251
+ payload = self._b64(data)
252
+ if self._fnv1a_32(seed + payload)[: len(difficulty)] <= difficulty:
253
+ return "gAAAAAB" + payload + "~S"
254
+ return "gAAAAAB" + self.ERROR_PREFIX + self._b64(str(None))
255
+
256
+
257
+ def build_sentinel_token(session: requests.Session, device_id: str, flow: str) -> str:
258
+ generator = SentinelTokenGenerator(device_id, user_agent)
259
+ resp = session.post(
260
+ "https://sentinel.openai.com/backend-api/sentinel/req",
261
+ data=json.dumps({"p": generator.generate_requirements_token(), "id": device_id, "flow": flow}),
262
+ headers={
263
+ "Content-Type": "text/plain;charset=UTF-8",
264
+ "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html",
265
+ "Origin": "https://sentinel.openai.com",
266
+ "User-Agent": user_agent,
267
+ "sec-ch-ua": sec_ch_ua,
268
+ "sec-ch-ua-mobile": "?0",
269
+ "sec-ch-ua-platform": '"Windows"',
270
+ },
271
+ timeout=20,
272
+ verify=False,
273
+ )
274
+ data = _response_json(resp)
275
+ token = str(data.get("token") or "").strip()
276
+ if resp.status_code != 200 or not token:
277
+ raise RuntimeError(f"sentinel_req_failed_{resp.status_code}")
278
+ pow_data = data.get("proofofwork") or {}
279
+ p_value = (
280
+ generator.generate_token(str(pow_data.get("seed") or ""), str(pow_data.get("difficulty") or "0"))
281
+ if pow_data.get("required") and pow_data.get("seed")
282
+ else generator.generate_requirements_token()
283
+ )
284
+ return json.dumps({"p": p_value, "t": "", "c": token, "id": device_id, "flow": flow}, separators=(",", ":"))
285
+
286
+
287
+ def create_session(proxy: str = "") -> requests.Session:
288
+ session = requests.Session()
289
+ retry = Retry(total=2, connect=2, read=2, backoff_factor=0.5, status_forcelist=(429, 500, 502, 503, 504))
290
+ adapter = HTTPAdapter(max_retries=retry, pool_connections=50, pool_maxsize=50)
291
+ session.mount("http://", adapter)
292
+ session.mount("https://", adapter)
293
+ session.verify = False
294
+ if proxy:
295
+ session.proxies.update({"http": proxy, "https": proxy})
296
+ return session
297
+
298
+
299
+ def request_with_local_retry(session: requests.Session, method: str, url: str, retry_attempts: int = 3, **kwargs):
300
+ last_error = ""
301
+ for _ in range(max(1, retry_attempts)):
302
+ try:
303
+ return session.request(method.upper(), url, timeout=default_timeout, **kwargs), ""
304
+ except Exception as error:
305
+ last_error = str(error)
306
+ time.sleep(1)
307
+ return None, last_error
308
+
309
+
310
+ def validate_otp(session: requests.Session, device_id: str, code: str):
311
+ headers = dict(common_headers)
312
+ headers["referer"] = f"{auth_base}/email-verification"
313
+ headers["oai-device-id"] = device_id
314
+ headers.update(_make_trace_headers())
315
+ resp, error = request_with_local_retry(session, "post", f"{auth_base}/api/accounts/email-otp/validate", json={"code": code}, headers=headers, verify=False)
316
+ if resp is not None and resp.status_code == 200:
317
+ return resp, ""
318
+ headers["openai-sentinel-token"] = build_sentinel_token(session, device_id, "authorize_continue")
319
+ resp, error = request_with_local_retry(session, "post", f"{auth_base}/api/accounts/email-otp/validate", json={"code": code}, headers=headers, verify=False)
320
+ return resp, error
321
+
322
+
323
+ def extract_oauth_callback_params_from_url(url: str) -> dict[str, str] | None:
324
+ if not url:
325
+ return None
326
+ try:
327
+ params = parse_qs(urlparse(url).query)
328
+ except Exception:
329
+ return None
330
+ code = str((params.get("code") or [""])[0]).strip()
331
+ if not code:
332
+ return None
333
+ return {"code": code, "state": str((params.get("state") or [""])[0]).strip(), "scope": str((params.get("scope") or [""])[0]).strip()}
334
+
335
+
336
+ def extract_oauth_callback_params_from_consent_session(session: requests.Session, consent_url: str, device_id: str) -> dict[str, str] | None:
337
+ if consent_url.startswith("/"):
338
+ consent_url = f"{auth_base}{consent_url}"
339
+ current_url = consent_url
340
+ for _ in range(10):
341
+ response = session.get(current_url, headers=navigate_headers, verify=False, timeout=30, allow_redirects=False)
342
+ callback_params = extract_oauth_callback_params_from_url(str(response.url)) or extract_oauth_callback_params_from_url(str(response.headers.get("Location") or "").strip())
343
+ if callback_params:
344
+ return callback_params
345
+ location = str(response.headers.get("Location") or "").strip()
346
+ if response.status_code not in (301, 302, 303, 307, 308) or not location:
347
+ break
348
+ current_url = f"{auth_base}{location}" if location.startswith("/") else location
349
+ raw = session.cookies.get("oai-client-auth-session", domain=".auth.openai.com") or session.cookies.get("oai-client-auth-session")
350
+ if not raw:
351
+ return None
352
+ try:
353
+ first_part = raw.split(".")[0]
354
+ padding = 4 - len(first_part) % 4
355
+ if padding != 4:
356
+ first_part += "=" * padding
357
+ payload = json.loads(base64.urlsafe_b64decode(first_part))
358
+ workspace_id = payload["workspaces"][0]["id"]
359
+ except Exception:
360
+ return None
361
+ headers = dict(common_headers)
362
+ headers["referer"] = consent_url
363
+ headers["oai-device-id"] = device_id
364
+ headers.update(_make_trace_headers())
365
+ ws_resp = session.post(f"{auth_base}/api/accounts/workspace/select", json={"workspace_id": workspace_id}, headers=headers, verify=False, timeout=30, allow_redirects=False)
366
+ callback_params = extract_oauth_callback_params_from_url(str(ws_resp.headers.get("Location") or "").strip())
367
+ if callback_params:
368
+ return callback_params
369
+ ws_data = _response_json(ws_resp)
370
+ orgs = ((ws_data.get("data") or {}).get("orgs") or []) if isinstance(ws_data, dict) else []
371
+ if not orgs:
372
+ return None
373
+ org_id = str((orgs[0] or {}).get("id") or "").strip()
374
+ project_id = str(((orgs[0] or {}).get("projects") or [{}])[0].get("id") or "").strip()
375
+ if not org_id:
376
+ return None
377
+ org_headers = dict(common_headers)
378
+ org_headers["referer"] = str(ws_data.get("continue_url") or consent_url)
379
+ org_headers["oai-device-id"] = device_id
380
+ org_headers.update(_make_trace_headers())
381
+ body = {"org_id": org_id}
382
+ if project_id:
383
+ body["project_id"] = project_id
384
+ org_resp = session.post(f"{auth_base}/api/accounts/organization/select", json=body, headers=org_headers, verify=False, timeout=30, allow_redirects=False)
385
+ return extract_oauth_callback_params_from_url(str(org_resp.headers.get("Location") or "").strip())
386
+
387
+
388
+ def exchange_platform_tokens(session: requests.Session, device_id: str, code_verifier: str, consent_url: str) -> dict | None:
389
+ callback_params = extract_oauth_callback_params_from_consent_session(session, consent_url, device_id)
390
+ if not callback_params:
391
+ return None
392
+ code = str(callback_params.get("code") or "").strip()
393
+ if not code:
394
+ return None
395
+ resp = create_session(config["proxy"]).post(
396
+ f"{auth_base}/oauth/token",
397
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
398
+ data={
399
+ "grant_type": "authorization_code",
400
+ "code": code,
401
+ "redirect_uri": platform_oauth_redirect_uri,
402
+ "client_id": platform_oauth_client_id,
403
+ "code_verifier": code_verifier,
404
+ },
405
+ verify=False,
406
+ timeout=60,
407
+ )
408
+ data = _response_json(resp)
409
+ if resp.status_code != 200 or not data.get("access_token") or not data.get("refresh_token") or not data.get("id_token"):
410
+ return None
411
+ payload = _decode_jwt_payload(str(data.get("id_token") or "")) or _decode_jwt_payload(str(data.get("access_token") or ""))
412
+ return {
413
+ "email": str(payload.get("email") or "").strip(),
414
+ "access_token": str(data.get("access_token") or "").strip(),
415
+ "refresh_token": str(data.get("refresh_token") or "").strip(),
416
+ "id_token": str(data.get("id_token") or "").strip(),
417
+ }
418
+
419
+
420
+ class PlatformRegistrar:
421
+ def __init__(self, proxy: str = "") -> None:
422
+ self.session = create_session(proxy)
423
+ self.device_id = str(uuid.uuid4())
424
+
425
+ def close(self) -> None:
426
+ self.session.close()
427
+
428
+ def _navigate_headers(self, referer: str = "") -> dict[str, str]:
429
+ headers = dict(navigate_headers)
430
+ if referer:
431
+ headers["referer"] = referer
432
+ return headers
433
+
434
+ def _json_headers(self, referer: str) -> dict[str, str]:
435
+ headers = dict(common_headers)
436
+ headers["referer"] = referer
437
+ headers["oai-device-id"] = self.device_id
438
+ headers.update(_make_trace_headers())
439
+ return headers
440
+
441
+ def _platform_authorize(self, email: str, index: int) -> None:
442
+ step(index, "ๅผ€ๅง‹ platform authorize")
443
+ self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com")
444
+ self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com")
445
+ _, code_challenge = _generate_pkce()
446
+ params = {
447
+ "issuer": auth_base,
448
+ "client_id": platform_oauth_client_id,
449
+ "audience": platform_oauth_audience,
450
+ "redirect_uri": platform_oauth_redirect_uri,
451
+ "device_id": self.device_id,
452
+ "screen_hint": "login_or_signup",
453
+ "max_age": "0",
454
+ "login_hint": email,
455
+ "scope": "openid profile email offline_access",
456
+ "response_type": "code",
457
+ "response_mode": "query",
458
+ "state": secrets.token_urlsafe(32),
459
+ "nonce": secrets.token_urlsafe(32),
460
+ "code_challenge": code_challenge,
461
+ "code_challenge_method": "S256",
462
+ "auth0Client": platform_auth0_client,
463
+ }
464
+ resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/authorize?{urlencode(params)}", headers=self._navigate_headers(f"{platform_base}/"), allow_redirects=True, verify=False)
465
+ if resp is None or resp.status_code != 200:
466
+ raise RuntimeError(error or f"platform_authorize_http_{getattr(resp, 'status_code', 'unknown')}")
467
+ step(index, "platform authorize ๅฎŒๆˆ")
468
+
469
+ def _register_user(self, email: str, password: str, index: int) -> None:
470
+ step(index, "ๅผ€ๅง‹ๆไบคๆณจๅ†Œๅฏ†็ ")
471
+ headers = self._json_headers(f"{auth_base}/create-account/password")
472
+ headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "username_password_create")
473
+ resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/user/register", json={"username": email, "password": password}, headers=headers, verify=False)
474
+ if resp is None or resp.status_code != 200:
475
+ raise RuntimeError(error or f"user_register_http_{getattr(resp, 'status_code', 'unknown')}")
476
+ step(index, "ๆไบคๆณจๅ†Œๅฏ†็ ๅฎŒๆˆ")
477
+
478
+ def _send_otp(self, index: int) -> None:
479
+ step(index, "ๅผ€ๅง‹ๅ‘้€้ชŒ่ฏ็ ")
480
+ resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/email-otp/send", headers=self._navigate_headers(f"{auth_base}/create-account/password"), allow_redirects=True, verify=False)
481
+ if resp is None or resp.status_code not in (200, 302):
482
+ raise RuntimeError(error or f"send_otp_http_{getattr(resp, 'status_code', 'unknown')}")
483
+ step(index, "ๅ‘้€้ชŒ่ฏ็ ๅฎŒๆˆ")
484
+
485
+ def _validate_otp(self, code: str, index: int) -> None:
486
+ step(index, f"ๅผ€ๅง‹ๆ ก้ชŒ้ชŒ่ฏ็  {code}")
487
+ resp, error = validate_otp(self.session, self.device_id, code)
488
+ if resp is None or resp.status_code != 200:
489
+ raise RuntimeError(error or f"validate_otp_http_{getattr(resp, 'status_code', 'unknown')}")
490
+ step(index, "้ชŒ่ฏ็ ๆ ก้ชŒๅฎŒๆˆ")
491
+
492
+ def _create_account(self, name: str, birthdate: str, index: int) -> None:
493
+ step(index, "ๅผ€ๅง‹ๅˆ›ๅปบ่ดฆๅท่ต„ๆ–™")
494
+ headers = self._json_headers(f"{auth_base}/about-you")
495
+ headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "oauth_create_account")
496
+ resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/create_account", json={"name": name, "birthdate": birthdate}, headers=headers, verify=False)
497
+ if resp is None or resp.status_code not in (200, 302):
498
+ raise RuntimeError(error or f"create_account_http_{getattr(resp, 'status_code', 'unknown')}")
499
+ step(index, "ๅˆ›ๅปบ่ดฆๅท่ต„ๆ–™ๅฎŒๆˆ")
500
+
501
+ def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, index: int) -> dict:
502
+ step(index, "ๅผ€ๅง‹็‹ฌ็ซ‹็™ปๅฝ•ๆข token")
503
+ code_verifier, code_challenge = _generate_pkce()
504
+ params = {
505
+ "issuer": auth_base,
506
+ "client_id": platform_oauth_client_id,
507
+ "audience": platform_oauth_audience,
508
+ "redirect_uri": platform_oauth_redirect_uri,
509
+ "device_id": self.device_id,
510
+ "screen_hint": "login_or_signup",
511
+ "max_age": "0",
512
+ "login_hint": email,
513
+ "scope": "openid profile email offline_access",
514
+ "response_type": "code",
515
+ "response_mode": "query",
516
+ "state": secrets.token_urlsafe(32),
517
+ "nonce": secrets.token_urlsafe(32),
518
+ "code_challenge": code_challenge,
519
+ "code_challenge_method": "S256",
520
+ "auth0Client": platform_auth0_client,
521
+ }
522
+ resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/authorize?{urlencode(params)}", headers=self._navigate_headers(f"{platform_base}/"), allow_redirects=True, verify=False)
523
+ if resp is None:
524
+ raise RuntimeError(error or "platform_login_authorize_failed")
525
+ step(index, "็™ปๅฝ• authorize ๅฎŒๆˆ")
526
+ headers = self._json_headers(f"{auth_base}/log-in/password")
527
+ headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "password_verify")
528
+ resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/password/verify", json={"password": password}, headers=headers, allow_redirects=False, verify=False)
529
+ if resp is None or resp.status_code != 200:
530
+ raise RuntimeError(error or f"password_verify_http_{getattr(resp, 'status_code', 'unknown')}")
531
+ step(index, "ๅฏ†็ ๆ ก้ชŒๅฎŒๆˆ")
532
+ payload = _response_json(resp)
533
+ continue_url = str(payload.get("continue_url") or "").strip()
534
+ page_type = str(((payload.get("page") or {}).get("type")) or "")
535
+ if page_type == "email_otp_verification" or "email-verification" in continue_url or "email-otp" in continue_url:
536
+ step(index, "็‹ฌ็ซ‹็™ปๅฝ•้œ€่ฆ้‚ฎ็ฎฑ้ชŒ่ฏ็ ")
537
+ code = wait_for_code(mailbox)
538
+ if not code:
539
+ raise RuntimeError("็‹ฌ็ซ‹็™ปๅฝ•็ญ‰ๅพ…้ชŒ่ฏ็ ่ถ…ๆ—ถ")
540
+ resp, reason = validate_otp(self.session, self.device_id, code)
541
+ if resp is None or resp.status_code != 200:
542
+ raise RuntimeError(reason or "็‹ฌ็ซ‹็™ปๅฝ•้ชŒ่ฏ็ ๆ ก้ชŒๅคฑ่ดฅ")
543
+ otp_payload = _response_json(resp)
544
+ continue_url = str(otp_payload.get("continue_url") or continue_url).strip()
545
+ step(index, "็‹ฌ็ซ‹็™ปๅฝ•้ชŒ่ฏ็ ๆ ก้ชŒๅฎŒๆˆ")
546
+ if not continue_url:
547
+ continue_url = f"{auth_base}/sign-in-with-chatgpt/codex/consent"
548
+ tokens = exchange_platform_tokens(self.session, self.device_id, code_verifier, continue_url)
549
+ if not tokens:
550
+ raise RuntimeError("tokenๆขๅ–ๅคฑ่ดฅ")
551
+ step(index, "token ๆขๅ–ๅฎŒๆˆ")
552
+ return tokens
553
+
554
+ def register(self, index: int) -> dict:
555
+ step(index, "ๅผ€ๅง‹ๅˆ›ๅปบ้‚ฎ็ฎฑ")
556
+ mailbox = create_mailbox()
557
+ email = str(mailbox.get("address") or "").strip()
558
+ if not email:
559
+ raise RuntimeError("้‚ฎ็ฎฑๆœๅŠกๆœช่ฟ”ๅ›ž address")
560
+ step(index, f"้‚ฎ็ฎฑๅˆ›ๅปบๅฎŒๆˆ: {email}")
561
+ password = _random_password()
562
+ first_name, last_name = _random_name()
563
+ self._platform_authorize(email, index)
564
+ self._register_user(email, password, index)
565
+ self._send_otp(index)
566
+ step(index, "ๅผ€ๅง‹็ญ‰ๅพ…ๆณจๅ†Œ้ชŒ่ฏ็ ")
567
+ code = wait_for_code(mailbox)
568
+ if not code:
569
+ raise RuntimeError("็ญ‰ๅพ…ๆณจๅ†Œ้ชŒ่ฏ็ ่ถ…ๆ—ถ")
570
+ step(index, f"ๆ”ถๅˆฐๆณจๅ†Œ้ชŒ่ฏ็ : {code}")
571
+ self._validate_otp(code, index)
572
+ self._create_account(f"{first_name} {last_name}", _random_birthdate(), index)
573
+ tokens = self._login_and_exchange_tokens(email, password, mailbox, index)
574
+ return {
575
+ "email": email,
576
+ "password": password,
577
+ "access_token": str(tokens.get("access_token") or "").strip(),
578
+ "refresh_token": str(tokens.get("refresh_token") or "").strip(),
579
+ "id_token": str(tokens.get("id_token") or "").strip(),
580
+ "created_at": datetime.now(timezone.utc).isoformat(),
581
+ }
582
+
583
+
584
+ def worker(index: int) -> dict:
585
+ start = time.time()
586
+ registrar = PlatformRegistrar(config["proxy"])
587
+ try:
588
+ step(index, "ไปปๅŠกๅฏๅŠจ")
589
+ result = registrar.register(index)
590
+ cost = time.time() - start
591
+ access_token = str(result["access_token"])
592
+ account_service.add_accounts([access_token])
593
+ account_service.refresh_accounts([access_token])
594
+ with stats_lock:
595
+ stats["done"] += 1
596
+ stats["success"] += 1
597
+ avg = (time.time() - stats["start_time"]) / stats["success"]
598
+ log(f'{result["email"]} ๆณจๅ†ŒๆˆๅŠŸ๏ผŒๆœฌๆฌก่€—ๆ—ถ{cost:.1f}s๏ผŒๅ…จๅฑ€ๅนณๅ‡ๆฏไธชๅทๆณจๅ†Œ่€—ๆ—ถ{avg:.1f}s', "green")
599
+ return {"ok": True, "index": index, "result": result}
600
+ except Exception as e:
601
+ cost = time.time() - start
602
+ with stats_lock:
603
+ stats["done"] += 1
604
+ stats["fail"] += 1
605
+ log(f"ไปปๅŠก{index} ๆณจๅ†Œๅคฑ่ดฅ๏ผŒๆœฌๆฌก่€—ๆ—ถ{cost:.1f}s๏ผŒๅŽŸๅ› : {e}", "red")
606
+ return {"ok": False, "index": index, "error": str(e)}
607
+ finally:
608
+ registrar.close()
services/register_service.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import threading
5
+ import time
6
+ import uuid
7
+ from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from services.account_service import account_service
12
+ from services.config import DATA_DIR
13
+ from services.register import openai_register
14
+
15
+
16
+ REGISTER_FILE = DATA_DIR / "register.json"
17
+
18
+
19
+ def _now() -> str:
20
+ return datetime.now(timezone.utc).isoformat()
21
+
22
+
23
+ def _default_config() -> dict:
24
+ return {**openai_register.config, "mode": "total", "target_quota": 100, "target_available": 10, "check_interval": 5, "enabled": False, "stats": {"success": 0, "fail": 0, "done": 0, "running": 0, "threads": openai_register.config["threads"], "elapsed_seconds": 0, "avg_seconds": 0, "success_rate": 0, "current_quota": 0, "current_available": 0}}
25
+
26
+
27
+ def _normalize(raw: dict) -> dict:
28
+ cfg = _default_config()
29
+ cfg.update({k: v for k, v in raw.items() if k not in {"stats", "logs"}})
30
+ cfg["total"] = max(1, int(cfg.get("total") or 1))
31
+ cfg["threads"] = max(1, int(cfg.get("threads") or 1))
32
+ cfg["mode"] = str(cfg.get("mode") or "total").strip() if str(cfg.get("mode") or "total").strip() in {"total", "quota", "available"} else "total"
33
+ cfg["target_quota"] = max(1, int(cfg.get("target_quota") or 1))
34
+ cfg["target_available"] = max(1, int(cfg.get("target_available") or 1))
35
+ cfg["check_interval"] = max(1, int(cfg.get("check_interval") or 5))
36
+ cfg["proxy"] = str(cfg.get("proxy") or "").strip()
37
+ cfg["enabled"] = bool(cfg.get("enabled"))
38
+ stats = {**_default_config()["stats"], **(raw.get("stats") if isinstance(raw.get("stats"), dict) else {}),
39
+ "threads": cfg["threads"]}
40
+ cfg["stats"] = stats
41
+ return cfg
42
+
43
+
44
+ class RegisterService:
45
+ def __init__(self, store_file: Path):
46
+ self._store_file = store_file
47
+ self._lock = threading.RLock()
48
+ self._runner: threading.Thread | None = None
49
+ self._logs: list[dict] = []
50
+ openai_register.register_log_sink = self._append_log
51
+ self._config = self._load()
52
+ if self._config["enabled"]:
53
+ self.start()
54
+
55
+ def _load(self) -> dict:
56
+ try:
57
+ return _normalize(json.loads(self._store_file.read_text(encoding="utf-8")))
58
+ except Exception:
59
+ return _normalize({})
60
+
61
+ def _save(self) -> None:
62
+ self._store_file.parent.mkdir(parents=True, exist_ok=True)
63
+ self._store_file.write_text(json.dumps(self._config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
64
+
65
+ def get(self) -> dict:
66
+ with self._lock:
67
+ return json.loads(json.dumps({**self._config, "logs": self._logs[-300:]}, ensure_ascii=False))
68
+
69
+ def update(self, updates: dict) -> dict:
70
+ with self._lock:
71
+ self._config = _normalize({**self._config, **updates})
72
+ openai_register.config.update({k: self._config[k] for k in ("mail", "proxy", "total", "threads")})
73
+ self._save()
74
+ return self.get()
75
+
76
+ def start(self) -> dict:
77
+ with self._lock:
78
+ if self._runner and self._runner.is_alive():
79
+ self._config["enabled"] = True
80
+ self._save()
81
+ return self.get()
82
+ self._config["enabled"] = True
83
+ self._logs = []
84
+ metrics = self._pool_metrics()
85
+ self._config["stats"] = {"job_id": uuid.uuid4().hex, "success": 0, "fail": 0, "done": 0, "running": 0, "threads": self._config["threads"], **metrics, "started_at": _now(), "updated_at": _now()}
86
+ openai_register.config.update({k: self._config[k] for k in ("mail", "proxy", "total", "threads")})
87
+ with openai_register.stats_lock:
88
+ openai_register.stats.update({"done": 0, "success": 0, "fail": 0, "start_time": time.time()})
89
+ self._save()
90
+ self._runner = threading.Thread(target=self._run, daemon=True, name="openai-register")
91
+ self._runner.start()
92
+ self._append_log(f"ๆณจๅ†ŒไปปๅŠกๅฏๅŠจ๏ผŒๆจกๅผ={self._config['mode']}๏ผŒ็บฟ็จ‹ๆ•ฐ={self._config['threads']}", "yellow")
93
+ return self.get()
94
+
95
+ def stop(self) -> dict:
96
+ with self._lock:
97
+ self._config["enabled"] = False
98
+ self._config["stats"]["updated_at"] = _now()
99
+ self._save()
100
+ self._append_log("ๅทฒ่ฏทๆฑ‚ๅœๆญขๆณจๅ†ŒไปปๅŠก๏ผŒๆญฃๅœจ็ญ‰ๅพ…ๅฝ“ๅ‰่ฟ่กŒไปปๅŠก็ป“ๆŸ", "yellow")
101
+ return self.get()
102
+
103
+ def reset(self) -> dict:
104
+ with self._lock:
105
+ self._logs = []
106
+ self._config["stats"] = {"success": 0, "fail": 0, "done": 0, "running": 0, "threads": self._config["threads"], "elapsed_seconds": 0, "avg_seconds": 0, "success_rate": 0, **self._pool_metrics(), "updated_at": _now()}
107
+ with openai_register.stats_lock:
108
+ openai_register.stats.update({"done": 0, "success": 0, "fail": 0, "start_time": 0.0})
109
+ self._save()
110
+ return self.get()
111
+
112
+ def _append_log(self, text: str, color: str = "") -> None:
113
+ with self._lock:
114
+ self._logs.append({"time": _now(), "text": str(text), "level": str(color or "info")})
115
+ self._logs = self._logs[-300:]
116
+
117
+ def _pool_metrics(self) -> dict:
118
+ items = account_service.list_accounts()
119
+ normal = [item for item in items if item.get("status") == "ๆญฃๅธธ"]
120
+ return {
121
+ "current_quota": sum(int(item.get("quota") or 0) for item in normal if not item.get("imageQuotaUnknown")),
122
+ "current_available": len(normal),
123
+ }
124
+
125
+ def _target_reached(self, cfg: dict, submitted: int) -> bool:
126
+ mode = str(cfg.get("mode") or "total")
127
+ metrics = self._pool_metrics()
128
+ self._bump(**metrics)
129
+ if mode == "quota":
130
+ reached = metrics["current_quota"] >= int(cfg.get("target_quota") or 1)
131
+ self._append_log(f"ๆฃ€ๆŸฅๅทๆฑ ๏ผšๅฝ“ๅ‰ๆญฃๅธธ่ดฆๅท={metrics['current_available']}๏ผŒๅฝ“ๅ‰ๅ‰ฉไฝ™้ขๅบฆ={metrics['current_quota']}๏ผŒ็›ฎๆ ‡้ขๅบฆ={cfg.get('target_quota')}๏ผŒ{'่ทณ่ฟ‡ๆณจๅ†Œ' if reached else '็ปง็ปญๆณจๅ†Œ'}", "yellow")
132
+ return reached
133
+ if mode == "available":
134
+ reached = metrics["current_available"] >= int(cfg.get("target_available") or 1)
135
+ self._append_log(f"ๆฃ€ๆŸฅๅทๆฑ ๏ผšๅฝ“ๅ‰ๆญฃๅธธ่ดฆๅท={metrics['current_available']}๏ผŒ็›ฎๆ ‡่ดฆๅท={cfg.get('target_available')}๏ผŒๅฝ“ๅ‰ๅ‰ฉไฝ™้ขๅบฆ={metrics['current_quota']}๏ผŒ{'่ทณ่ฟ‡ๆณจๅ†Œ' if reached else '็ปง็ปญๆณจๅ†Œ'}", "yellow")
136
+ return reached
137
+ return submitted >= int(cfg.get("total") or 1)
138
+
139
+ def _bump(self, **updates) -> None:
140
+ with self._lock:
141
+ self._config["stats"].update(updates)
142
+ stats = self._config["stats"]
143
+ started_at = str(stats.get("started_at") or "")
144
+ if started_at:
145
+ try:
146
+ elapsed = max(0.0, (datetime.now(timezone.utc) - datetime.fromisoformat(started_at)).total_seconds())
147
+ except Exception:
148
+ elapsed = 0.0
149
+ done = int(stats.get("done") or 0)
150
+ success = int(stats.get("success") or 0)
151
+ fail = int(stats.get("fail") or 0)
152
+ stats["elapsed_seconds"] = round(elapsed, 1)
153
+ stats["avg_seconds"] = round(elapsed / success, 1) if success else 0
154
+ stats["success_rate"] = round(success * 100 / max(1, success + fail), 1)
155
+ self._config["stats"]["updated_at"] = _now()
156
+ self._save()
157
+
158
+ def _run(self) -> None:
159
+ cfg = self.get()
160
+ threads = int(cfg["threads"])
161
+ submitted, done, success, fail = 0, 0, 0, 0
162
+ with ThreadPoolExecutor(max_workers=threads) as executor:
163
+ futures = set()
164
+ while True:
165
+ while self.get()["enabled"] and not self._target_reached(cfg, submitted) and len(futures) < threads:
166
+ submitted += 1
167
+ futures.add(executor.submit(openai_register.worker, submitted))
168
+ self._bump(running=len(futures), done=done, success=success, fail=fail)
169
+ if not futures and (not self.get()["enabled"] or str(cfg.get("mode") or "total") == "total"):
170
+ break
171
+ if not futures:
172
+ time.sleep(max(1, int(cfg.get("check_interval") or 5)))
173
+ continue
174
+ finished, futures = wait(futures, return_when=FIRST_COMPLETED)
175
+ for future in finished:
176
+ done += 1
177
+ try:
178
+ result = future.result()
179
+ success += 1 if result.get("ok") else 0
180
+ fail += 0 if result.get("ok") else 1
181
+ except Exception:
182
+ fail += 1
183
+ self._bump(running=0, done=done, success=success, fail=fail, finished_at=_now())
184
+ with self._lock:
185
+ self._config["enabled"] = False
186
+ self._save()
187
+ self._append_log(f"ๆณจๅ†ŒไปปๅŠก็ป“ๆŸ๏ผŒๆˆๅŠŸ{success}๏ผŒๅคฑ่ดฅ{fail}", "yellow")
188
+
189
+
190
+ register_service = RegisterService(REGISTER_FILE)
web/src/app/register/components/register-card.tsx ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AlertTriangle, LoaderCircle, Plus, Play, RotateCcw, Save, Square, Trash2, UserPlus } from "lucide-react";
4
+
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Checkbox } from "@/components/ui/checkbox";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
10
+ import { Textarea } from "@/components/ui/textarea";
11
+
12
+ import { useSettingsStore } from "../../settings/store";
13
+
14
+ export function RegisterCard() {
15
+ const config = useSettingsStore((state) => state.registerConfig);
16
+ const isLoading = useSettingsStore((state) => state.isLoadingRegister);
17
+ const isSaving = useSettingsStore((state) => state.isSavingRegister);
18
+ const setProxy = useSettingsStore((state) => state.setRegisterProxy);
19
+ const setTotal = useSettingsStore((state) => state.setRegisterTotal);
20
+ const setThreads = useSettingsStore((state) => state.setRegisterThreads);
21
+ const setMode = useSettingsStore((state) => state.setRegisterMode);
22
+ const setTargetQuota = useSettingsStore((state) => state.setRegisterTargetQuota);
23
+ const setTargetAvailable = useSettingsStore((state) => state.setRegisterTargetAvailable);
24
+ const setCheckInterval = useSettingsStore((state) => state.setRegisterCheckInterval);
25
+ const setMailField = useSettingsStore((state) => state.setRegisterMailField);
26
+ const addProvider = useSettingsStore((state) => state.addRegisterProvider);
27
+ const updateProvider = useSettingsStore((state) => state.updateRegisterProvider);
28
+ const deleteProvider = useSettingsStore((state) => state.deleteRegisterProvider);
29
+ const save = useSettingsStore((state) => state.saveRegister);
30
+ const toggle = useSettingsStore((state) => state.toggleRegister);
31
+ const reset = useSettingsStore((state) => state.resetRegister);
32
+
33
+ if (isLoading) {
34
+ return (
35
+ <div className="flex items-center justify-center rounded-xl border border-stone-200 bg-white/80 p-10">
36
+ <LoaderCircle className="size-5 animate-spin text-stone-400" />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ if (!config) return null;
42
+
43
+ const stats = config.stats || { success: 0, fail: 0, done: 0, running: 0, threads: config.threads };
44
+ const providers = config.mail.providers || [];
45
+ const logs = config.logs || [];
46
+ const updateProviderType = (index: number, type: string) => {
47
+ updateProvider(index, {
48
+ type,
49
+ enable: true,
50
+ ...(type === "cloudflare_temp_email" ? { api_base: "", admin_password: "", domain: [] } : {}),
51
+ ...(type === "tempmail_lol" ? { api_key: "", domain: [] } : {}),
52
+ ...(type === "duckmail" ? { api_key: "", default_domain: "duckmail.sbs" } : {}),
53
+ ...(type === "gptmail" ? { api_key: "", default_domain: "" } : {}),
54
+ });
55
+ };
56
+
57
+ return (
58
+ <div className="grid h-[calc(100vh-132px)] min-h-[640px] items-stretch gap-0 overflow-hidden rounded-xl border border-stone-200 bg-white/70 xl:grid-cols-2">
59
+ <section className="space-y-4 overflow-y-auto border-b border-stone-200 p-4 xl:border-r xl:border-b-0">
60
+ <div className="flex items-start justify-between gap-3">
61
+ <div className="flex items-center gap-3">
62
+ <div className="flex size-9 items-center justify-center rounded-md bg-stone-100">
63
+ <UserPlus className="size-5 text-stone-600" />
64
+ </div>
65
+ <div>
66
+ <h2 className="text-lg font-semibold tracking-tight">ๆณจๅ†Œ้…็ฝฎ</h2>
67
+ </div>
68
+ </div>
69
+ <Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => void save()} disabled={isSaving || config.enabled}>
70
+ {isSaving ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
71
+ ไฟๅญ˜้…็ฝฎ
72
+ </Button>
73
+ </div>
74
+
75
+ <div className="grid gap-4 md:grid-cols-3">
76
+ <div className="space-y-2">
77
+ <label className="text-sm text-stone-700">ๆณจๅ†Œๆจกๅผ</label>
78
+ <Select value={config.mode || "total"} onValueChange={(value) => setMode(value as "total" | "quota" | "available")} disabled={config.enabled}>
79
+ <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
80
+ <SelectValue />
81
+ </SelectTrigger>
82
+ <SelectContent>
83
+ <SelectItem value="total">ๆณจๅ†Œๆ€ปๆ•ฐ</SelectItem>
84
+ <SelectItem value="quota">ๅทๆฑ ๅ‰ฉไฝ™้ขๅบฆ</SelectItem>
85
+ <SelectItem value="available">ๅฏ็”จ่ดฆๅทๆ•ฐ้‡</SelectItem>
86
+ </SelectContent>
87
+ </Select>
88
+ </div>
89
+ <div className="space-y-2">
90
+ <label className="text-sm text-stone-700">ๆณจๅ†Œๆ€ปๆ•ฐ</label>
91
+ <Input value={String(config.total)} onChange={(event) => setTotal(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "total"} />
92
+ </div>
93
+ <div className="space-y-2">
94
+ <label className="text-sm text-stone-700">็บฟ็จ‹ๆ•ฐ</label>
95
+ <Input value={String(config.threads)} onChange={(event) => setThreads(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
96
+ </div>
97
+ <div className="space-y-2">
98
+ <label className="text-sm text-stone-700">ๆณจๅ†Œไปฃ็†</label>
99
+ <Input value={config.proxy} onChange={(event) => setProxy(event.target.value)} placeholder="http://127.0.0.1:7890" className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
100
+ </div>
101
+ <div className="space-y-2">
102
+ <label className="text-sm text-stone-700">็›ฎๆ ‡ๅ‰ฉไฝ™้ขๅบฆ</label>
103
+ <Input value={String(config.target_quota || "")} onChange={(event) => setTargetQuota(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "quota"} />
104
+ </div>
105
+ <div className="space-y-2">
106
+ <label className="text-sm text-stone-700">็›ฎๆ ‡ๅฏ็”จ่ดฆๅท</label>
107
+ <Input value={String(config.target_available || "")} onChange={(event) => setTargetAvailable(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "available"} />
108
+ </div>
109
+ <div className="space-y-2">
110
+ <label className="text-sm text-stone-700">ๆฃ€ๆŸฅ้—ด้š”๏ผˆ็ง’๏ผ‰</label>
111
+ <Input value={String(config.check_interval || "")} onChange={(event) => setCheckInterval(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode === "total"} />
112
+ </div>
113
+ </div>
114
+
115
+ <div className="space-y-3 border-t border-stone-200 pt-3">
116
+ <div className="flex items-center justify-between gap-3">
117
+ <div>
118
+ <h3 className="text-sm font-semibold text-stone-800">้‚ฎ็ฎฑ้…็ฝฎ</h3>
119
+ <p className="mt-1 text-xs text-stone-500">ๅฏ้…็ฝฎๅคšไธช provider๏ผŒๆŒ‰ๅฏ็”จ้กบๅบ่ฝฎๆขใ€‚</p>
120
+ </div>
121
+ <Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={addProvider} disabled={config.enabled}>
122
+ <Plus className="size-4" />
123
+ ๆทปๅŠ 
124
+ </Button>
125
+ </div>
126
+
127
+ <div className="grid gap-4 md:grid-cols-3">
128
+ <div className="space-y-2">
129
+ <label className="text-sm text-stone-700">่ฏทๆฑ‚่ถ…ๆ—ถ</label>
130
+ <Input value={String(config.mail.request_timeout || "")} onChange={(event) => setMailField("request_timeout", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
131
+ </div>
132
+ <div className="space-y-2">
133
+ <label className="text-sm text-stone-700">็ญ‰ๅพ…้ชŒ่ฏ็ ่ถ…ๆ—ถ</label>
134
+ <Input value={String(config.mail.wait_timeout || "")} onChange={(event) => setMailField("wait_timeout", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
135
+ </div>
136
+ <div className="space-y-2">
137
+ <label className="text-sm text-stone-700">่ฝฎ่ฏข้—ด้š”</label>
138
+ <Input value={String(config.mail.wait_interval || "")} onChange={(event) => setMailField("wait_interval", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
139
+ </div>
140
+ </div>
141
+
142
+ <div className="space-y-3">
143
+ {providers.map((provider, index) => {
144
+ const type = String(provider.type || "tempmail_lol");
145
+ const domains = Array.isArray(provider.domain) ? provider.domain.map(String).join("\n") : "";
146
+ return (
147
+ <div key={index} className="space-y-3 border-t border-stone-200 pt-3 first:border-t-0 first:pt-0">
148
+ <div className="flex items-center justify-between gap-3">
149
+ <label className="flex items-center gap-3 text-sm text-stone-700">
150
+ <Checkbox checked={Boolean(provider.enable)} onCheckedChange={(checked) => updateProvider(index, { enable: Boolean(checked) })} disabled={config.enabled} />
151
+ ๅฏ็”จ
152
+ </label>
153
+ <button type="button" className="rounded-lg p-2 text-stone-400 transition hover:bg-rose-50 hover:text-rose-500 disabled:opacity-50" onClick={() => deleteProvider(index)} disabled={config.enabled || providers.length <= 1} title="ๅˆ ้™ค provider">
154
+ <Trash2 className="size-4" />
155
+ </button>
156
+ </div>
157
+
158
+ <div className="grid gap-4 md:grid-cols-2">
159
+ <div className="space-y-2">
160
+ <label className="text-sm text-stone-700">็ฑปๅž‹</label>
161
+ <Select value={type} onValueChange={(value) => updateProviderType(index, value)} disabled={config.enabled}>
162
+ <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
163
+ <SelectValue />
164
+ </SelectTrigger>
165
+ <SelectContent>
166
+ <SelectItem value="cloudflare_temp_email">cloudflare_temp_email</SelectItem>
167
+ <SelectItem value="tempmail_lol">tempmail_lol</SelectItem>
168
+ <SelectItem value="duckmail">duckmail</SelectItem>
169
+ <SelectItem value="gptmail">gptmail(ๆœชๆต‹่ฏ•)</SelectItem>
170
+ </SelectContent>
171
+ </Select>
172
+ </div>
173
+ {type === "cloudflare_temp_email" ? (
174
+ <>
175
+ <div className="space-y-2">
176
+ <label className="text-sm text-stone-700">API Base</label>
177
+ <Input value={String(provider.api_base || "")} onChange={(event) => updateProvider(index, { api_base: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
178
+ </div>
179
+ <div className="space-y-2">
180
+ <label className="text-sm text-stone-700">Admin Password</label>
181
+ <Input value={String(provider.admin_password || "")} onChange={(event) => updateProvider(index, { admin_password: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
182
+ </div>
183
+ </>
184
+ ) : null}
185
+ {type === "tempmail_lol" || type === "duckmail" || type === "gptmail" ? (
186
+ <div className="space-y-2">
187
+ <label className="text-sm text-stone-700">API Key</label>
188
+ <Input value={String(provider.api_key || "")} onChange={(event) => updateProvider(index, { api_key: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
189
+ </div>
190
+ ) : null}
191
+ {type === "duckmail" || type === "gptmail" ? (
192
+ <div className="space-y-2">
193
+ <label className="text-sm text-stone-700">Default Domain</label>
194
+ <Input value={String(provider.default_domain || "")} onChange={(event) => updateProvider(index, { default_domain: event.target.value })} placeholder={type === "duckmail" ? "duckmail.sbs" : ""} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
195
+ </div>
196
+ ) : null}
197
+ </div>
198
+
199
+ {type === "tempmail_lol" || type === "cloudflare_temp_email" ? (
200
+ <div className="space-y-2">
201
+ <label className="text-sm text-stone-700">Domain</label>
202
+ <Textarea value={domains} onChange={(event) => updateProvider(index, { domain: event.target.value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) })} placeholder="ๆฏ่กŒไธ€ไธชๅŸŸๅ๏ผŒ็•™็ฉบๅˆ™ไฝฟ็”จๆœๅŠก้ป˜่ฎคๅŸŸๅ" className="min-h-20 rounded-xl border-stone-200 bg-white font-mono text-xs" disabled={config.enabled} />
203
+ </div>
204
+ ) : null}
205
+ </div>
206
+ );
207
+ })}
208
+ </div>
209
+ </div>
210
+
211
+ </section>
212
+
213
+ <section className="flex min-h-0 flex-col p-4">
214
+ <div className="space-y-3">
215
+ <div className="flex items-start justify-between gap-3">
216
+ <div>
217
+ <h2 className="text-lg font-semibold tracking-tight">่ฟ่กŒ็ป“ๆžœ</h2>
218
+ <p className="mt-1 text-sm text-stone-500">SSE ๅฎžๆ—ถๆŽจ้€ๅฝ“ๅ‰็Šถๆ€ใ€‚</p>
219
+ </div>
220
+ <Badge variant={config.enabled ? "success" : "secondary"} className="rounded-md">
221
+ {config.enabled ? "่ฟ่กŒไธญ" : "ๅทฒๅœๆญข"}
222
+ </Badge>
223
+ </div>
224
+ <div className="grid grid-cols-4 gap-2">
225
+ {[
226
+ ["ๆˆๅŠŸ / ๆˆๅŠŸ็އ", `${stats.success} / ${stats.success_rate || 0}%`],
227
+ ["ๅคฑ่ดฅ", stats.fail],
228
+ ["ๅฎŒๆˆ", stats.done],
229
+ ["่ฟ่กŒ / ็บฟ็จ‹", `${stats.running} / ${stats.threads}`],
230
+ ["่ฟ่กŒๆ—ถ้—ด", `${stats.elapsed_seconds || 0}s`],
231
+ ["ๅนณๅ‡ๆณจๅ†Œๅ•ไธช", `${stats.avg_seconds || 0}s`],
232
+ ["ๅฝ“ๅ‰้ขๅบฆ", stats.current_quota || 0],
233
+ ["ๆญฃๅธธ่ดฆๅท", stats.current_available || 0],
234
+ ].map(([label, value]) => (
235
+ <div key={label} className="border border-stone-200 bg-white/70 px-3 py-2">
236
+ <div className="text-xs text-stone-400">{label}</div>
237
+ <div className="mt-1 text-base font-semibold text-stone-800">{value}</div>
238
+ </div>
239
+ ))}
240
+ </div>
241
+ <div className="grid grid-cols-3 gap-2">
242
+ <Button className="h-10 rounded-xl bg-stone-950 px-3 text-white hover:bg-stone-800" onClick={() => void toggle()} disabled={isSaving}>
243
+ {isSaving ? <LoaderCircle className="size-4 animate-spin" /> : config.enabled ? <Square className="size-4" /> : <Play className="size-4" />}
244
+ {config.enabled ? "ๅœๆญข" : "ๅฏๅŠจ"}
245
+ </Button>
246
+ <Button variant="outline" className="h-10 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={() => void reset()} disabled={isSaving || config.enabled}>
247
+ <RotateCcw className="size-4" />
248
+ ้‡็ฝฎ
249
+ </Button>
250
+ <Button variant="outline" className="h-10 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={() => void save()} disabled={isSaving || config.enabled}>
251
+ <Save className="size-4" />
252
+ ไฟๅญ˜
253
+ </Button>
254
+ </div>
255
+ <div className="flex items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
256
+ <AlertTriangle className="size-4 shrink-0" />
257
+ ๅฏๅŠจไน‹ๅ‰ๆณจๆ„ๅ…ˆไฟๅญ˜้…็ฝฎใ€‚
258
+ </div>
259
+ </div>
260
+
261
+ <div className="mt-4 flex min-h-0 flex-1 flex-col space-y-3 overflow-hidden border-t border-stone-200 pt-4">
262
+ <div className="flex items-center justify-between">
263
+ <div>
264
+ <h3 className="text-sm font-semibold text-stone-900">ๅฎžๆ—ถๆ—ฅๅฟ—</h3>
265
+ <p className="mt-1 text-xs text-stone-500">ๅชไฟ็•™ๅ†…ๅญ˜ไธญ็š„ๆœ€่ฟ‘ 300 ๆกใ€‚</p>
266
+ </div>
267
+ <Badge variant="secondary" className="rounded-md">
268
+ {logs.length}
269
+ </Badge>
270
+ </div>
271
+ <div className="min-h-0 flex-1 overflow-y-auto border border-stone-200 bg-white/70 p-3 font-mono text-xs leading-6">
272
+ {logs.length === 0 ? (
273
+ <div className="text-stone-500">ๆš‚ๆ— ๆ—ฅๅฟ—</div>
274
+ ) : (
275
+ logs.slice().reverse().map((item, index) => (
276
+ <div key={`${item.time}-${index}`} className={item.level === "red" ? "text-rose-600" : item.level === "green" ? "text-emerald-700" : item.level === "yellow" ? "text-amber-700" : "text-stone-700"}>
277
+ <span className="text-stone-400">{new Date(item.time).toLocaleTimeString()}</span>
278
+ <span className="pl-2">{item.text}</span>
279
+ </div>
280
+ ))
281
+ )}
282
+ </div>
283
+ </div>
284
+ </section>
285
+ </div>
286
+ );
287
+ }
web/src/app/register/page.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { LoaderCircle } from "lucide-react";
5
+
6
+ import webConfig from "@/constants/common-env";
7
+ import { useAuthGuard } from "@/lib/use-auth-guard";
8
+ import type { RegisterConfig } from "@/lib/api";
9
+ import { getStoredAuthKey } from "@/store/auth";
10
+
11
+ import { useSettingsStore } from "../settings/store";
12
+ import { RegisterCard } from "./components/register-card";
13
+
14
+ function RegisterDataController() {
15
+ const didLoadRef = useRef(false);
16
+ const loadRegister = useSettingsStore((state) => state.loadRegister);
17
+ const setRegisterConfig = useSettingsStore((state) => state.setRegisterConfig);
18
+
19
+ useEffect(() => {
20
+ if (didLoadRef.current) return;
21
+ didLoadRef.current = true;
22
+ void loadRegister();
23
+ }, [loadRegister]);
24
+
25
+ useEffect(() => {
26
+ let source: EventSource | null = null;
27
+ let closed = false;
28
+ void getStoredAuthKey().then((token) => {
29
+ if (closed || !token) return;
30
+ const baseUrl = webConfig.apiUrl.replace(/\/$/, "");
31
+ source = new EventSource(`${baseUrl}/api/register/events?token=${encodeURIComponent(token)}`);
32
+ source.onmessage = (event) => {
33
+ setRegisterConfig(JSON.parse(event.data) as RegisterConfig);
34
+ };
35
+ });
36
+ return () => {
37
+ closed = true;
38
+ source?.close();
39
+ };
40
+ }, [setRegisterConfig]);
41
+
42
+ return null;
43
+ }
44
+
45
+ function RegisterPageContent() {
46
+ return (
47
+ <>
48
+ <RegisterDataController />
49
+ <section className="mb-2 flex flex-col gap-1 lg:flex-row lg:items-start lg:justify-between">
50
+ <div className="space-y-1">
51
+ <div className="text-xs font-semibold tracking-[0.18em] text-stone-500 uppercase">Register</div>
52
+ <h1 className="text-2xl font-semibold tracking-tight">ChatGPTๆณจๅ†Œๆœบ</h1>
53
+ </div>
54
+ </section>
55
+ <section>
56
+ <RegisterCard />
57
+ </section>
58
+ </>
59
+ );
60
+ }
61
+
62
+ export default function RegisterPage() {
63
+ const { isCheckingAuth, session } = useAuthGuard(["admin"]);
64
+
65
+ if (isCheckingAuth || !session || session.role !== "admin") {
66
+ return (
67
+ <div className="flex min-h-[40vh] items-center justify-center">
68
+ <LoaderCircle className="size-5 animate-spin text-stone-400" />
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return <RegisterPageContent />;
74
+ }
web/src/app/settings/store.ts CHANGED
@@ -8,12 +8,18 @@ import {
8
  deleteCPAPool,
9
  fetchCPAPoolFiles,
10
  fetchCPAPools,
 
 
11
  fetchSettingsConfig,
 
12
  startCPAImport,
 
13
  updateCPAPool,
 
14
  updateSettingsConfig,
15
  type CPAPool,
16
  type CPARemoteFile,
 
17
  type SettingsConfig,
18
  } from "@/lib/api";
19
 
@@ -55,6 +61,10 @@ type SettingsStore = {
55
  isLoadingConfig: boolean;
56
  isSavingConfig: boolean;
57
 
 
 
 
 
58
  pools: CPAPool[];
59
  isLoadingPools: boolean;
60
  deletingId: string | null;
@@ -87,6 +97,23 @@ type SettingsStore = {
87
  setProxy: (value: string) => void;
88
  setBaseUrl: (value: string) => void;
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  loadPools: (silent?: boolean) => Promise<void>;
91
  openAddDialog: () => void;
92
  openEditDialog: (pool: CPAPool) => void;
@@ -113,6 +140,10 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
113
  isLoadingConfig: true,
114
  isSavingConfig: false,
115
 
 
 
 
 
116
  pools: [],
117
  isLoadingPools: true,
118
  deletingId: null,
@@ -240,6 +271,159 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
240
  });
241
  },
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  loadPools: async (silent = false) => {
244
  if (!silent) {
245
  set({ isLoadingPools: true });
 
8
  deleteCPAPool,
9
  fetchCPAPoolFiles,
10
  fetchCPAPools,
11
+ fetchRegisterConfig,
12
+ resetRegister as resetRegisterApi,
13
  fetchSettingsConfig,
14
+ startRegister,
15
  startCPAImport,
16
+ stopRegister,
17
  updateCPAPool,
18
+ updateRegisterConfig,
19
  updateSettingsConfig,
20
  type CPAPool,
21
  type CPARemoteFile,
22
+ type RegisterConfig,
23
  type SettingsConfig,
24
  } from "@/lib/api";
25
 
 
61
  isLoadingConfig: boolean;
62
  isSavingConfig: boolean;
63
 
64
+ registerConfig: RegisterConfig | null;
65
+ isLoadingRegister: boolean;
66
+ isSavingRegister: boolean;
67
+
68
  pools: CPAPool[];
69
  isLoadingPools: boolean;
70
  deletingId: string | null;
 
97
  setProxy: (value: string) => void;
98
  setBaseUrl: (value: string) => void;
99
 
100
+ loadRegister: (silent?: boolean) => Promise<void>;
101
+ setRegisterConfig: (config: RegisterConfig) => void;
102
+ setRegisterProxy: (value: string) => void;
103
+ setRegisterTotal: (value: string) => void;
104
+ setRegisterThreads: (value: string) => void;
105
+ setRegisterMode: (value: "total" | "quota" | "available") => void;
106
+ setRegisterTargetQuota: (value: string) => void;
107
+ setRegisterTargetAvailable: (value: string) => void;
108
+ setRegisterCheckInterval: (value: string) => void;
109
+ setRegisterMailField: (key: "request_timeout" | "wait_timeout" | "wait_interval", value: string) => void;
110
+ addRegisterProvider: () => void;
111
+ updateRegisterProvider: (index: number, updates: Record<string, unknown>) => void;
112
+ deleteRegisterProvider: (index: number) => void;
113
+ saveRegister: () => Promise<void>;
114
+ toggleRegister: () => Promise<void>;
115
+ resetRegister: () => Promise<void>;
116
+
117
  loadPools: (silent?: boolean) => Promise<void>;
118
  openAddDialog: () => void;
119
  openEditDialog: (pool: CPAPool) => void;
 
140
  isLoadingConfig: true,
141
  isSavingConfig: false,
142
 
143
+ registerConfig: null,
144
+ isLoadingRegister: true,
145
+ isSavingRegister: false,
146
+
147
  pools: [],
148
  isLoadingPools: true,
149
  deletingId: null,
 
271
  });
272
  },
273
 
274
+ loadRegister: async (silent = false) => {
275
+ if (!silent) set({ isLoadingRegister: true });
276
+ try {
277
+ const data = await fetchRegisterConfig();
278
+ set({ registerConfig: data.register });
279
+ } catch (error) {
280
+ if (!silent) toast.error(error instanceof Error ? error.message : "ๅŠ ่ฝฝๆณจๅ†Œ้…็ฝฎๅคฑ่ดฅ");
281
+ } finally {
282
+ if (!silent) set({ isLoadingRegister: false });
283
+ }
284
+ },
285
+
286
+ setRegisterConfig: (config) => {
287
+ set({ registerConfig: config, isLoadingRegister: false });
288
+ },
289
+
290
+ setRegisterProxy: (value) => {
291
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, proxy: value } } : {});
292
+ },
293
+
294
+ setRegisterTotal: (value) => {
295
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, total: Number(value) || 0 } } : {});
296
+ },
297
+
298
+ setRegisterThreads: (value) => {
299
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, threads: Number(value) || 0 } } : {});
300
+ },
301
+
302
+ setRegisterMode: (value) => {
303
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, mode: value } } : {});
304
+ },
305
+
306
+ setRegisterTargetQuota: (value) => {
307
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_quota: Number(value) || 0 } } : {});
308
+ },
309
+
310
+ setRegisterTargetAvailable: (value) => {
311
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_available: Number(value) || 0 } } : {});
312
+ },
313
+
314
+ setRegisterCheckInterval: (value) => {
315
+ set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, check_interval: Number(value) || 0 } } : {});
316
+ },
317
+
318
+ setRegisterMailField: (key, value) => {
319
+ set((state) => state.registerConfig ? {
320
+ registerConfig: {
321
+ ...state.registerConfig,
322
+ mail: { ...state.registerConfig.mail, [key]: Number(value) || 0 },
323
+ },
324
+ } : {});
325
+ },
326
+
327
+ addRegisterProvider: () => {
328
+ set((state) => state.registerConfig ? {
329
+ registerConfig: {
330
+ ...state.registerConfig,
331
+ mail: {
332
+ ...state.registerConfig.mail,
333
+ providers: [
334
+ ...(state.registerConfig.mail.providers || []),
335
+ { enable: true, type: "tempmail_lol", api_key: "", domain: [] },
336
+ ],
337
+ },
338
+ },
339
+ } : {});
340
+ },
341
+
342
+ updateRegisterProvider: (index, updates) => {
343
+ set((state) => {
344
+ if (!state.registerConfig) return {};
345
+ const providers = [...(state.registerConfig.mail.providers || [])];
346
+ providers[index] = { ...(providers[index] || {}), ...updates };
347
+ return { registerConfig: { ...state.registerConfig, mail: { ...state.registerConfig.mail, providers } } };
348
+ });
349
+ },
350
+
351
+ deleteRegisterProvider: (index) => {
352
+ set((state) => state.registerConfig ? {
353
+ registerConfig: {
354
+ ...state.registerConfig,
355
+ mail: {
356
+ ...state.registerConfig.mail,
357
+ providers: (state.registerConfig.mail.providers || []).filter((_, itemIndex) => itemIndex !== index),
358
+ },
359
+ },
360
+ } : {});
361
+ },
362
+
363
+ saveRegister: async () => {
364
+ const { registerConfig } = get();
365
+ if (!registerConfig) return;
366
+ try {
367
+ set({ isSavingRegister: true });
368
+ const data = await updateRegisterConfig({
369
+ mail: registerConfig.mail,
370
+ proxy: registerConfig.proxy.trim(),
371
+ total: Math.max(1, Number(registerConfig.total) || 1),
372
+ threads: Math.max(1, Number(registerConfig.threads) || 1),
373
+ mode: registerConfig.mode,
374
+ target_quota: Math.max(1, Number(registerConfig.target_quota) || 1),
375
+ target_available: Math.max(1, Number(registerConfig.target_available) || 1),
376
+ check_interval: Math.max(1, Number(registerConfig.check_interval) || 5),
377
+ });
378
+ set({ registerConfig: data.register });
379
+ toast.success("ๆณจๅ†Œ้…็ฝฎๅทฒไฟๅญ˜");
380
+ } catch (error) {
381
+ toast.error(error instanceof Error ? error.message : "ไฟๅญ˜ๆณจๅ†Œ้…็ฝฎๅคฑ่ดฅ");
382
+ } finally {
383
+ set({ isSavingRegister: false });
384
+ }
385
+ },
386
+
387
+ toggleRegister: async () => {
388
+ const { registerConfig } = get();
389
+ if (!registerConfig) return;
390
+ set({ isSavingRegister: true });
391
+ try {
392
+ if (!registerConfig.enabled) {
393
+ await updateRegisterConfig({
394
+ mail: registerConfig.mail,
395
+ proxy: registerConfig.proxy.trim(),
396
+ total: Math.max(1, Number(registerConfig.total) || 1),
397
+ threads: Math.max(1, Number(registerConfig.threads) || 1),
398
+ mode: registerConfig.mode,
399
+ target_quota: Math.max(1, Number(registerConfig.target_quota) || 1),
400
+ target_available: Math.max(1, Number(registerConfig.target_available) || 1),
401
+ check_interval: Math.max(1, Number(registerConfig.check_interval) || 5),
402
+ });
403
+ }
404
+ const data = registerConfig.enabled ? await stopRegister() : await startRegister();
405
+ set({ registerConfig: data.register });
406
+ toast.success(registerConfig.enabled ? "ๆณจๅ†ŒไปปๅŠกๅทฒๅœๆญข" : "ๆณจๅ†ŒไปปๅŠกๅทฒๅฏๅŠจ");
407
+ } catch (error) {
408
+ toast.error(error instanceof Error ? error.message : "ๅˆ‡ๆขๆณจๅ†Œ็Šถๆ€ๅคฑ่ดฅ");
409
+ } finally {
410
+ set({ isSavingRegister: false });
411
+ }
412
+ },
413
+
414
+ resetRegister: async () => {
415
+ set({ isSavingRegister: true });
416
+ try {
417
+ const data = await resetRegisterApi();
418
+ set({ registerConfig: data.register });
419
+ toast.success("ๆณจๅ†Œ็ปŸ่ฎกๅทฒ้‡็ฝฎ");
420
+ } catch (error) {
421
+ toast.error(error instanceof Error ? error.message : "้‡็ฝฎๆณจๅ†Œ็ปŸ่ฎกๅคฑ่ดฅ");
422
+ } finally {
423
+ set({ isSavingRegister: false });
424
+ }
425
+ },
426
+
427
  loadPools: async (silent = false) => {
428
  if (!silent) {
429
  set({ isLoadingPools: true });
web/src/components/top-nav.tsx CHANGED
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
12
  const adminNavItems = [
13
  { href: "/image", label: "็”ปๅ›พ" },
14
  { href: "/accounts", label: "ๅทๆฑ ็ฎก็†" },
 
15
  { href: "/image-manager", label: "ๅ›พ็‰‡็ฎก็†" },
16
  { href: "/logs", label: "ๆ—ฅๅฟ—็ฎก็†" },
17
  { href: "/settings", label: "่ฎพ็ฝฎ" },
 
12
  const adminNavItems = [
13
  { href: "/image", label: "็”ปๅ›พ" },
14
  { href: "/accounts", label: "ๅทๆฑ ็ฎก็†" },
15
+ { href: "/register", label: "ๆณจๅ†Œๆœบ" },
16
  { href: "/image-manager", label: "ๅ›พ็‰‡็ฎก็†" },
17
  { href: "/logs", label: "ๆ—ฅๅฟ—็ฎก็†" },
18
  { href: "/settings", label: "่ฎพ็ฝฎ" },
web/src/lib/api.ts CHANGED
@@ -98,6 +98,44 @@ export type UserKey = {
98
  last_used_at: string | null;
99
  };
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  export async function login(authKey: string) {
102
  const normalizedAuthKey = String(authKey || "").trim();
103
  return httpRequest<LoginResponse>("/auth/login", {
@@ -245,6 +283,29 @@ export async function deleteUserKey(keyId: string) {
245
  });
246
  }
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  // โ”€โ”€ CPA (CLIProxyAPI) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
249
 
250
  export type CPAPool = {
 
98
  last_used_at: string | null;
99
  };
100
 
101
+ export type RegisterConfig = {
102
+ enabled: boolean;
103
+ mail: {
104
+ request_timeout: number;
105
+ wait_timeout: number;
106
+ wait_interval: number;
107
+ providers: Array<Record<string, unknown>>;
108
+ };
109
+ proxy: string;
110
+ total: number;
111
+ threads: number;
112
+ mode: "total" | "quota" | "available";
113
+ target_quota: number;
114
+ target_available: number;
115
+ check_interval: number;
116
+ stats: {
117
+ job_id?: string;
118
+ success: number;
119
+ fail: number;
120
+ done: number;
121
+ running: number;
122
+ threads: number;
123
+ elapsed_seconds?: number;
124
+ avg_seconds?: number;
125
+ success_rate?: number;
126
+ current_quota?: number;
127
+ current_available?: number;
128
+ started_at?: string;
129
+ updated_at?: string;
130
+ finished_at?: string;
131
+ };
132
+ logs?: Array<{
133
+ time: string;
134
+ text: string;
135
+ level: string;
136
+ }>;
137
+ };
138
+
139
  export async function login(authKey: string) {
140
  const normalizedAuthKey = String(authKey || "").trim();
141
  return httpRequest<LoginResponse>("/auth/login", {
 
283
  });
284
  }
285
 
286
+ export async function fetchRegisterConfig() {
287
+ return httpRequest<{ register: RegisterConfig }>("/api/register");
288
+ }
289
+
290
+ export async function updateRegisterConfig(updates: Partial<RegisterConfig>) {
291
+ return httpRequest<{ register: RegisterConfig }>("/api/register", {
292
+ method: "POST",
293
+ body: updates,
294
+ });
295
+ }
296
+
297
+ export async function startRegister() {
298
+ return httpRequest<{ register: RegisterConfig }>("/api/register/start", { method: "POST" });
299
+ }
300
+
301
+ export async function stopRegister() {
302
+ return httpRequest<{ register: RegisterConfig }>("/api/register/stop", { method: "POST" });
303
+ }
304
+
305
+ export async function resetRegister() {
306
+ return httpRequest<{ register: RegisterConfig }>("/api/register/reset", { method: "POST" });
307
+ }
308
+
309
  // โ”€โ”€ CPA (CLIProxyAPI) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
310
 
311
  export type CPAPool = {