openai_register.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. import json
  2. import os
  3. import re
  4. import sys
  5. import time
  6. import uuid
  7. import random
  8. import string
  9. import secrets
  10. import hashlib
  11. import base64
  12. import argparse
  13. from pathlib import Path
  14. from datetime import datetime, timedelta
  15. from dataclasses import dataclass
  16. from typing import Any, Dict, Optional, List
  17. import urllib.parse
  18. import urllib.request
  19. import urllib.error
  20. import asyncio
  21. import requests as py_requests
  22. try:
  23. import aiohttp
  24. except ImportError:
  25. aiohttp = None
  26. from curl_cffi import requests
  27. OUT_DIR = Path(__file__).parent.resolve()
  28. UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
  29. AUTH_URL = "https://auth.openai.com/oauth/authorize"
  30. TOKEN_URL = "https://auth.openai.com/oauth/token"
  31. CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
  32. DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback"
  33. DEFAULT_SCOPE = "openid email profile offline_access"
  34. # ========== 临时邮箱提供商:GPTMail + TempMail.lol ==========
  35. class GPTMailClient:
  36. def __init__(self, proxies: Any = None):
  37. self.session = requests.Session(proxies=proxies, impersonate="chrome")
  38. self.session.headers.update({
  39. "User-Agent": UA,
  40. "Accept": "application/json, text/plain, */*",
  41. "Accept-Language": "zh-CN,zh;q=0.9",
  42. "Referer": "https://mail.chatgpt.org.uk/",
  43. })
  44. self.base_url = "https://mail.chatgpt.org.uk"
  45. def _init_browser_session(self):
  46. try:
  47. resp = self.session.get(self.base_url, timeout=15)
  48. gm_sid = self.session.cookies.get("gm_sid")
  49. if gm_sid:
  50. self.session.headers.update({"Cookie": f"gm_sid={gm_sid}"})
  51. token_match = re.search(r'(eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)', resp.text)
  52. if token_match:
  53. self.session.headers.update({"x-inbox-token": token_match.group(1)})
  54. except Exception:
  55. pass
  56. def generate_email(self) -> str:
  57. self._init_browser_session()
  58. resp = self.session.get(f"{self.base_url}/api/generate-email", timeout=15)
  59. if resp.status_code == 200:
  60. data = resp.json()
  61. email = data["data"]["email"]
  62. self.session.headers.update({"x-inbox-token": data["auth"]["token"]})
  63. print(f"[+] 生成邮箱: {email} (GPTMail)")
  64. print("[*] 自动轮询已启动(GPTMail 会话已准备)")
  65. return email
  66. raise RuntimeError(f"GPTMail 生成失败: {resp.status_code}")
  67. def list_emails(self, email: str) -> List[Dict[str, Any]]:
  68. encoded_email = urllib.parse.quote(email)
  69. resp = self.session.get(f"{self.base_url}/api/emails?email={encoded_email}", timeout=15)
  70. if resp.status_code == 200:
  71. return resp.json().get("data", {}).get("emails", [])
  72. return []
  73. class Message:
  74. def __init__(self, data: dict):
  75. self.from_addr = data.get("from", "")
  76. self.subject = data.get("subject", "")
  77. self.body = data.get("body", "") or ""
  78. self.html_body = data.get("html", "") or ""
  79. class EMail:
  80. def __init__(self, proxies: Any = None):
  81. self.s = requests.Session(proxies=proxies, impersonate="chrome")
  82. self.s.headers.update({
  83. "User-Agent": UA,
  84. "Accept": "application/json",
  85. "Content-Type": "application/json",
  86. })
  87. r = self.s.post("https://api.tempmail.lol/v2/inbox/create", json={}, timeout=15)
  88. r.raise_for_status()
  89. data = r.json()
  90. self.address = data["address"]
  91. self.token = data["token"]
  92. print(f"[+] 生成邮箱: {self.address} (TempMail.lol)")
  93. print("[*] 自动轮询已启动(token 已保存)")
  94. def _get_messages(self) -> List[Dict[str, Any]]:
  95. r = self.s.get(f"https://api.tempmail.lol/v2/inbox?token={self.token}", timeout=15)
  96. r.raise_for_status()
  97. return r.json().get("emails", [])
  98. def get_email_and_code_fetcher(proxies: Any = None, provider: str = "auto"):
  99. provider = (provider or "auto").strip().lower()
  100. if provider not in {"auto", "gptmail", "tempmail"}:
  101. raise ValueError(f"不支持的邮箱提供商: {provider}")
  102. def _build_tempmail_bundle():
  103. inbox = EMail(proxies)
  104. email = inbox.address
  105. def _extract_all_codes() -> List[str]:
  106. results: List[str] = []
  107. try:
  108. msgs = inbox._get_messages()
  109. for msg_data in msgs:
  110. msg = Message(msg_data)
  111. body = msg.body or msg.html_body or msg.subject or ""
  112. results.extend(re.findall(r"\b(\d{6})\b", body))
  113. except Exception:
  114. pass
  115. return results
  116. def fetch_code(timeout_sec: int = 180, poll: float = 6.0, exclude_codes: Optional[List[str]] = None) -> str | None:
  117. exclude = set(exclude_codes or [])
  118. start = time.monotonic()
  119. attempt = 0
  120. while time.monotonic() - start < timeout_sec:
  121. attempt += 1
  122. try:
  123. msgs = inbox._get_messages()
  124. print(f"[otp][tempmail] 轮询 #{attempt}, 收到 {len(msgs)} 封邮件, 目标: {email}")
  125. for msg_data in msgs:
  126. msg = Message(msg_data)
  127. body = msg.body or msg.html_body or msg.subject or ""
  128. for code in re.findall(r"\b(\d{6})\b", body):
  129. if code not in exclude:
  130. return code
  131. except Exception:
  132. pass
  133. time.sleep(poll)
  134. return None
  135. return email, _gen_password(), fetch_code, _extract_all_codes, "tempmail"
  136. def _build_gptmail_bundle():
  137. client = GPTMailClient(proxies)
  138. email = client.generate_email()
  139. def _extract_all_codes() -> List[str]:
  140. regex = r"(?<!\d)(\d{6})(?!\d)"
  141. results: List[str] = []
  142. try:
  143. summaries = client.list_emails(email)
  144. for s in summaries:
  145. body = " ".join([
  146. str(s.get("subject", "") or ""),
  147. str(s.get("text", "") or ""),
  148. str(s.get("body", "") or ""),
  149. str(s.get("html", "") or ""),
  150. json.dumps(s, ensure_ascii=False),
  151. ])
  152. results.extend(re.findall(regex, body))
  153. except Exception:
  154. pass
  155. return results
  156. def fetch_code(timeout_sec: int = 180, poll: float = 6.0, exclude_codes: Optional[List[str]] = None) -> str | None:
  157. exclude = set(exclude_codes or [])
  158. start = time.monotonic()
  159. attempt = 0
  160. while time.monotonic() - start < timeout_sec:
  161. attempt += 1
  162. try:
  163. summaries = client.list_emails(email)
  164. print(f"[otp][gptmail] 轮询 #{attempt}, 收到 {len(summaries)} 封邮件, 目标: {email}")
  165. for s in summaries:
  166. body = " ".join([
  167. str(s.get("subject", "") or ""),
  168. str(s.get("text", "") or ""),
  169. str(s.get("body", "") or ""),
  170. str(s.get("html", "") or ""),
  171. json.dumps(s, ensure_ascii=False),
  172. ])
  173. for code in re.findall(r"(?<!\d)(\d{6})(?!\d)", body):
  174. if code not in exclude:
  175. return code
  176. except Exception:
  177. pass
  178. time.sleep(poll)
  179. return None
  180. return email, _gen_password(), fetch_code, _extract_all_codes, "gptmail"
  181. if provider == "tempmail":
  182. return _build_tempmail_bundle()
  183. if provider == "gptmail":
  184. return _build_gptmail_bundle()
  185. try:
  186. return _build_tempmail_bundle()
  187. except Exception as e:
  188. print(f"[邮箱] TempMail.lol 初始化失败,回退 GPTMail: {e}")
  189. return _build_gptmail_bundle()
  190. # ========== OAuth 核心逻辑 (对齐原版的完美重定向流) ==========
  191. def _gen_password() -> str:
  192. alphabet = string.ascii_letters + string.digits
  193. special = "!@#$%^&*.-"
  194. base = [random.choice(string.ascii_lowercase), random.choice(string.ascii_uppercase),
  195. random.choice(string.digits), random.choice(special)]
  196. base += [random.choice(alphabet + special) for _ in range(12)]
  197. random.shuffle(base)
  198. return "".join(base)
  199. def _random_name() -> str:
  200. return ''.join(random.choice(string.ascii_lowercase) for _ in range(7)).capitalize()
  201. def _random_birthdate() -> str:
  202. start = datetime(1975, 1, 1); end = datetime(1999, 12, 31)
  203. d = start + timedelta(days=random.randrange((end - start).days + 1))
  204. return d.strftime('%Y-%m-%d')
  205. def _b64url_no_pad(raw: bytes) -> str:
  206. return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
  207. def _sha256_b64url_no_pad(s: str) -> str:
  208. return _b64url_no_pad(hashlib.sha256(s.encode("ascii")).digest())
  209. def _pkce_verifier() -> str:
  210. return secrets.token_urlsafe(64)
  211. def _parse_callback_url(callback_url: str) -> Dict[str, Any]:
  212. candidate = (callback_url or "").strip()
  213. if not candidate:
  214. return {"code": "", "state": "", "error": "", "error_description": ""}
  215. if "://" not in candidate:
  216. if candidate.startswith("?"):
  217. candidate = f"http://localhost{candidate}"
  218. elif any(ch in candidate for ch in "/?#") or ":" in candidate:
  219. candidate = f"http://{candidate}"
  220. elif "=" in candidate:
  221. candidate = f"http://localhost/?{candidate}"
  222. parsed = urllib.parse.urlparse(candidate)
  223. query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
  224. fragment = urllib.parse.parse_qs(parsed.fragment, keep_blank_values=True)
  225. for key, values in fragment.items():
  226. if key not in query or not query[key] or not (query[key][0] or "").strip():
  227. query[key] = values
  228. def get1(k: str) -> str:
  229. return (query.get(k, [""])[0] or "").strip()
  230. code = get1("code")
  231. state = get1("state")
  232. error = get1("error")
  233. error_description = get1("error_description")
  234. if code and not state and "#" in code:
  235. code, state = code.split("#", 1)
  236. if not error and error_description:
  237. error, error_description = error_description, ""
  238. return {"code": code, "state": state, "error": error, "error_description": error_description}
  239. def _decode_jwt_segment(seg: str) -> Dict[str, Any]:
  240. try:
  241. pad = "=" * ((4 - (len(seg) % 4)) % 4)
  242. return json.loads(base64.urlsafe_b64decode((seg + pad).encode("ascii")).decode("utf-8"))
  243. except Exception:
  244. return {}
  245. def _jwt_claims_no_verify(token: str) -> Dict[str, Any]:
  246. if not token or token.count(".") < 2:
  247. return {}
  248. return _decode_jwt_segment(token.split(".")[1])
  249. def _post_form(url: str, data: Dict[str, str], timeout: int = 30) -> Dict[str, Any]:
  250. body = urllib.parse.urlencode(data).encode("utf-8")
  251. req = urllib.request.Request(
  252. url,
  253. data=body,
  254. method="POST",
  255. headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"},
  256. )
  257. try:
  258. with urllib.request.urlopen(req, timeout=timeout) as resp:
  259. raw = resp.read()
  260. if resp.status != 200:
  261. raise RuntimeError(f"Token 交换失败: {resp.status}: {raw.decode('utf-8', 'replace')}")
  262. return json.loads(raw.decode("utf-8"))
  263. except urllib.error.HTTPError as exc:
  264. raw = exc.read()
  265. raise RuntimeError(f"Token 交换失败: {exc.code}: {raw.decode('utf-8', 'replace')}") from exc
  266. def _to_int(v: Any) -> int:
  267. try:
  268. return int(v)
  269. except (TypeError, ValueError):
  270. return 0
  271. def _build_sentinel_payload(session, did: str, flow: str) -> str:
  272. req_body = json.dumps({"p": "", "id": did, "flow": flow})
  273. resp = session.post(
  274. "https://sentinel.openai.com/backend-api/sentinel/req",
  275. headers={
  276. "origin": "https://sentinel.openai.com",
  277. "referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html?sv=20260219f9f6",
  278. "content-type": "text/plain;charset=UTF-8",
  279. },
  280. data=req_body,
  281. timeout=15,
  282. )
  283. if resp.status_code != 200:
  284. raise RuntimeError(f"Sentinel 验证失败: {resp.status_code}: {resp.text[:200]}")
  285. token = (resp.json() or {}).get("token", "")
  286. return json.dumps({"p": "", "t": "", "c": token, "id": did, "flow": flow})
  287. @dataclass(frozen=True)
  288. class OAuthStart:
  289. auth_url: str; state: str; code_verifier: str; redirect_uri: str
  290. def generate_oauth_url(redirect_uri: str = DEFAULT_REDIRECT_URI) -> OAuthStart:
  291. state = secrets.token_urlsafe(16)
  292. verifier = _pkce_verifier()
  293. challenge = _sha256_b64url_no_pad(verifier)
  294. params = {
  295. "client_id": CLIENT_ID,
  296. "response_type": "code",
  297. "redirect_uri": redirect_uri,
  298. "scope": DEFAULT_SCOPE,
  299. "state": state,
  300. "code_challenge": challenge,
  301. "code_challenge_method": "S256",
  302. "prompt": "login",
  303. "id_token_add_organizations": "true",
  304. "codex_cli_simplified_flow": "true",
  305. }
  306. return OAuthStart(f"{AUTH_URL}?{urllib.parse.urlencode(params)}", state, verifier, redirect_uri)
  307. def fetch_sentinel_token(flow: str, did: str, proxies: Any = None) -> Optional[str]:
  308. try:
  309. session = requests.Session(proxies=proxies, impersonate="chrome")
  310. payload = _build_sentinel_payload(session, did, flow)
  311. return (json.loads(payload) or {}).get("c")
  312. except Exception:
  313. return None
  314. def submit_callback_url(callback_url: str, expected_state: str, code_verifier: str, redirect_uri: str, session=None) -> str:
  315. cb = _parse_callback_url(callback_url)
  316. if cb.get("error"):
  317. raise RuntimeError(f"OAuth 错误: {cb['error']}: {cb.get('error_description', '')}".strip())
  318. if not cb.get("code"):
  319. raise ValueError("Callback URL 缺少 ?code=")
  320. if not cb.get("state"):
  321. raise ValueError("Callback URL 缺少 ?state=")
  322. if cb.get("state") != expected_state:
  323. raise ValueError("State 校验不匹配")
  324. token_data = {
  325. "grant_type": "authorization_code",
  326. "client_id": CLIENT_ID,
  327. "code": cb["code"],
  328. "redirect_uri": redirect_uri,
  329. "code_verifier": code_verifier,
  330. }
  331. if session is not None:
  332. resp = session.post(
  333. TOKEN_URL,
  334. data=token_data,
  335. headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"},
  336. timeout=30,
  337. )
  338. if resp.status_code != 200:
  339. raise RuntimeError(f"Token 交换失败: {resp.status_code}: {resp.text[:200]}")
  340. token_resp = resp.json()
  341. else:
  342. token_resp = _post_form(TOKEN_URL, token_data)
  343. access_token = str(token_resp.get("access_token") or "").strip()
  344. refresh_token = str(token_resp.get("refresh_token") or "").strip()
  345. id_token = str(token_resp.get("id_token") or "").strip()
  346. expires_in = _to_int(token_resp.get("expires_in"))
  347. claims = _jwt_claims_no_verify(id_token)
  348. auth_claims = claims.get("https://api.openai.com/auth") or {}
  349. now = int(time.time())
  350. config = {
  351. "id_token": id_token,
  352. "access_token": access_token,
  353. "refresh_token": refresh_token,
  354. "account_id": str(auth_claims.get("chatgpt_account_id") or "").strip(),
  355. "last_refresh": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)),
  356. "email": str(claims.get("email") or "").strip(),
  357. "type": "codex",
  358. "expired": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + max(expires_in, 0))),
  359. }
  360. return json.dumps(config, ensure_ascii=False, indent=2)
  361. # ========== 轻量版 CPA 维护实现(内嵌,不依赖项目包) ==========
  362. DEFAULT_MGMT_UA = "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal"
  363. def _mgmt_headers(token: str) -> dict:
  364. clean = str(token or "").strip()
  365. if clean and not clean.lower().startswith("bearer "):
  366. clean = f"Bearer {clean}"
  367. return {"Authorization": clean, "Accept": "application/json"}
  368. def _join_mgmt_url(base_url: str, path: str) -> str:
  369. base = (base_url or "").rstrip("/")
  370. suffix = path if path.startswith("/") else f"/{path}"
  371. if base.endswith("/v0"):
  372. return f"{base}{suffix}"
  373. return f"{base}/v0{suffix}"
  374. def _safe_json(text: str):
  375. try:
  376. return json.loads(text)
  377. except Exception:
  378. return {}
  379. def _extract_account_id(item: dict):
  380. for key in ("chatgpt_account_id", "chatgptAccountId", "account_id", "accountId"):
  381. val = item.get(key)
  382. if val:
  383. return str(val)
  384. return None
  385. def _get_item_type(item: dict) -> str:
  386. return str(item.get("type") or item.get("typo") or "")
  387. class MiniPoolMaintainer:
  388. def __init__(self, base_url: str, token: str, target_type: str = "codex", used_percent_threshold: int = 95, user_agent: str = DEFAULT_MGMT_UA):
  389. self.base_url = (base_url or "").rstrip("/")
  390. self.token = token
  391. self.target_type = target_type
  392. self.used_percent_threshold = used_percent_threshold
  393. self.user_agent = user_agent
  394. def upload_token(self, filename: str, token_data: dict, proxy: str = "") -> bool:
  395. if not self.base_url or not self.token:
  396. return False
  397. content = json.dumps(token_data, ensure_ascii=False).encode("utf-8")
  398. files = {"file": (filename, content, "application/json")}
  399. headers = {"Authorization": f"Bearer {self.token}"}
  400. proxies = {"http": proxy, "https": proxy} if proxy else None
  401. for attempt in range(3):
  402. try:
  403. resp = py_requests.post(_join_mgmt_url(self.base_url, "/management/auth-files"), files=files, headers=headers, timeout=30, verify=False, proxies=proxies)
  404. if resp.status_code in (200, 201, 204):
  405. return True
  406. except Exception:
  407. pass
  408. if attempt < 2:
  409. time.sleep(2 ** attempt)
  410. return False
  411. def fetch_auth_files(self, timeout: int = 15):
  412. resp = py_requests.get(_join_mgmt_url(self.base_url, "/management/auth-files"), headers=_mgmt_headers(self.token), timeout=timeout)
  413. resp.raise_for_status()
  414. data = resp.json()
  415. return (data.get("files") if isinstance(data, dict) else []) or []
  416. async def probe_and_clean_async(self, workers: int = 20, timeout: int = 10, retries: int = 1):
  417. if aiohttp is None:
  418. raise RuntimeError("需要安装 aiohttp: pip install aiohttp")
  419. files = self.fetch_auth_files(timeout)
  420. candidates = [f for f in files if _get_item_type(f).lower() == self.target_type.lower()]
  421. if not candidates:
  422. return {"total": len(files), "candidates": 0, "invalid_count": 0, "deleted_ok": 0, "deleted_fail": 0}
  423. semaphore = asyncio.Semaphore(max(1, workers))
  424. connector = aiohttp.TCPConnector(limit=max(1, workers))
  425. client_timeout = aiohttp.ClientTimeout(total=max(1, timeout))
  426. async def probe_one(session, item):
  427. auth_index = item.get("auth_index")
  428. name = item.get("name") or item.get("id")
  429. res = {"name": name, "auth_index": auth_index, "invalid_401": False, "invalid_used_percent": False, "used_percent": None}
  430. if not auth_index:
  431. res["invalid_401"] = False
  432. return res
  433. account_id = _extract_account_id(item)
  434. header = {"Authorization": "Bearer $TOKEN$", "Content-Type": "application/json", "User-Agent": self.user_agent}
  435. if account_id:
  436. header["Chatgpt-Account-Id"] = account_id
  437. payload = {"authIndex": auth_index, "method": "GET", "url": "https://chatgpt.com/backend-api/wham/usage", "header": header}
  438. for attempt in range(retries + 1):
  439. try:
  440. async with semaphore:
  441. async with session.post(_join_mgmt_url(self.base_url, "/management/api-call"), headers={**_mgmt_headers(self.token), "Content-Type": "application/json"}, json=payload, timeout=timeout) as resp:
  442. text = await resp.text()
  443. if resp.status >= 400:
  444. raise RuntimeError(f"HTTP {resp.status}: {text[:200]}")
  445. data = _safe_json(text)
  446. sc = data.get("status_code")
  447. res["invalid_401"] = sc == 401
  448. if sc == 200:
  449. body = _safe_json(data.get("body", ""))
  450. used_pct = (body.get("rate_limit", {}).get("primary_window", {}).get("used_percent"))
  451. if used_pct is not None:
  452. res["used_percent"] = used_pct
  453. res["invalid_used_percent"] = used_pct >= self.used_percent_threshold
  454. return res
  455. except Exception as e:
  456. if attempt >= retries:
  457. res["error"] = str(e)
  458. return res
  459. return res
  460. async def delete_one(session, name: str):
  461. if not name:
  462. return False
  463. from urllib.parse import quote
  464. encoded = quote(name, safe="")
  465. try:
  466. async with semaphore:
  467. async with session.delete(f"{_join_mgmt_url(self.base_url, '/management/auth-files')}?name={encoded}", headers=_mgmt_headers(self.token), timeout=timeout) as resp:
  468. text = await resp.text()
  469. data = _safe_json(text)
  470. return resp.status == 200 and data.get("status") == "ok"
  471. except Exception:
  472. return False
  473. invalid_list = []
  474. async with aiohttp.ClientSession(connector=connector, timeout=client_timeout, trust_env=True) as session:
  475. tasks = [asyncio.create_task(probe_one(session, item)) for item in candidates]
  476. for task in asyncio.as_completed(tasks):
  477. r = await task
  478. if r.get("invalid_401") or r.get("invalid_used_percent"):
  479. invalid_list.append(r)
  480. delete_tasks = [asyncio.create_task(delete_one(session, r.get("name"))) for r in invalid_list if r.get("name")]
  481. deleted_ok = 0
  482. deleted_fail = 0
  483. for task in asyncio.as_completed(delete_tasks):
  484. if await task:
  485. deleted_ok += 1
  486. else:
  487. deleted_fail += 1
  488. return {
  489. "total": len(files),
  490. "candidates": len(candidates),
  491. "invalid_count": len(invalid_list),
  492. "deleted_ok": deleted_ok,
  493. "deleted_fail": deleted_fail,
  494. }
  495. def probe_and_clean_sync(self, workers: int = 20, timeout: int = 10, retries: int = 1):
  496. return asyncio.run(self.probe_and_clean_async(workers, timeout, retries))
  497. def _build_cpa_maintainer(args):
  498. base_url = (args.cpa_base_url or os.getenv("CPA_BASE_URL") or "").strip()
  499. token = (args.cpa_token or os.getenv("CPA_TOKEN") or "").strip()
  500. if not base_url or not token:
  501. print("[CPA] 未提供 cpa_base_url / cpa_token,跳过 CPA 上传/清理")
  502. return None
  503. try:
  504. return MiniPoolMaintainer(
  505. base_url,
  506. token,
  507. target_type="codex",
  508. used_percent_threshold=args.cpa_used_threshold,
  509. user_agent=DEFAULT_MGMT_UA,
  510. )
  511. except Exception as e:
  512. print(f"[CPA] 创建维护器失败: {e}")
  513. return None
  514. def _upload_token_to_cpa(pm, token_json: str, email: str, proxy: str = "") -> bool:
  515. if not pm:
  516. return False
  517. try:
  518. data = json.loads(token_json)
  519. except Exception as e:
  520. print(f"[CPA] 解析 token_json 失败: {e}")
  521. return False
  522. fname_email = email.replace("@", "_")
  523. filename = f"token_{fname_email}_{int(time.time())}.json"
  524. ok = pm.upload_token(filename=filename, token_data=data, proxy=proxy or "")
  525. if ok:
  526. print(f"[CPA] 已上传 {filename} 到 CPA")
  527. else:
  528. print("[CPA] 上传失败")
  529. return ok
  530. def _clean_invalid_in_cpa(pm, args):
  531. if not pm:
  532. return None
  533. try:
  534. res = pm.probe_and_clean_sync(
  535. workers=max(1, args.cpa_workers),
  536. timeout=max(5, args.cpa_timeout),
  537. retries=max(0, args.cpa_retries),
  538. )
  539. print(
  540. f"[CPA] 清理完成: total={res.get('total')} candidates={res.get('candidates')} "
  541. f"invalid={res.get('invalid_count')} deleted_ok={res.get('deleted_ok')} deleted_fail={res.get('deleted_fail')}"
  542. )
  543. return res
  544. except Exception as e:
  545. print(f"[CPA] 清理失败: {e}")
  546. return None
  547. def _count_valid_cpa_tokens(pm, args):
  548. if not pm:
  549. return 0
  550. try:
  551. files = pm.fetch_auth_files(timeout=max(5, args.cpa_timeout))
  552. target = pm.target_type.lower()
  553. valid = [f for f in files if _get_item_type(f).lower() == target]
  554. return len(valid)
  555. except Exception as e:
  556. print(f"[CPA] 统计 token 失败: {e}")
  557. return 0
  558. # 账号行清理:上传成功且开启 prune_local 后使用
  559. # 安全处理:文件不存在直接返回,写入保持末尾换行便于追加
  560. def _remove_account_entry(accounts_path: Path, email: str, real_pwd: str):
  561. if not accounts_path.exists():
  562. return
  563. try:
  564. lines = accounts_path.read_text(encoding="utf-8").splitlines()
  565. target = f"{email}----{real_pwd}"
  566. kept = [ln for ln in lines if ln.strip() != target]
  567. accounts_path.write_text("\n".join(kept) + ("\n" if kept else ""), encoding="utf-8")
  568. print(f"[本地清理] 已从 accounts.txt 移除: {email}")
  569. except Exception as e:
  570. print(f"[本地清理] 移除账号行失败: {e}")
  571. # ========== 主注册流程 (恢复详细日志与异常捕获) ==========
  572. def run(proxy: Optional[str], mail_provider: str = "auto"):
  573. proxies = {"http": proxy, "https": proxy} if proxy else None
  574. s = requests.Session(proxies=proxies, impersonate="chrome")
  575. s.headers.update({
  576. "user-agent": UA,
  577. "accept": "application/json, text/plain, */*",
  578. })
  579. print(f"\n{'='*20} 开启注册流程 {'='*20}")
  580. try:
  581. print(f"[步骤1] 正在初始化临时邮箱(provider={mail_provider})...")
  582. email, password, code_fetcher, extract_all_codes, actual_mail_provider = get_email_and_code_fetcher(proxies, provider=mail_provider)
  583. print(f"[*] 当前邮箱提供商: {actual_mail_provider}")
  584. if not email:
  585. print("[失败] 未能获取邮箱")
  586. return None
  587. print(f"[成功] 邮箱: {email} | 临时密码: {password}")
  588. print("[步骤2] 访问 OpenAI 授权页获取 Device ID...")
  589. oauth = generate_oauth_url()
  590. auth_page = s.get(oauth.auth_url, timeout=15)
  591. did = s.cookies.get("oai-did")
  592. if not did:
  593. print("[失败] 未能从 Cookie 获取 oai-did")
  594. return None
  595. print(f"[成功] Device ID: {did}")
  596. print("[步骤3] 获取 Sentinel 载荷并提交注册邮箱...")
  597. try:
  598. authorize_continue_sentinel = _build_sentinel_payload(s, did, "authorize_continue")
  599. except Exception as e:
  600. print(f"[失败] 获取 authorize_continue Sentinel 失败: {e}")
  601. return None
  602. continue_url = ""
  603. try:
  604. auth_json = auth_page.json() if hasattr(auth_page, "json") else {}
  605. continue_url = str((auth_json or {}).get("continue_url") or "").strip()
  606. except Exception:
  607. continue_url = ""
  608. if continue_url:
  609. try:
  610. s.get(continue_url, timeout=15)
  611. except Exception:
  612. pass
  613. signup_res = s.post(
  614. "https://auth.openai.com/api/accounts/authorize/continue",
  615. headers={
  616. "referer": "https://auth.openai.com/create-account",
  617. "accept": "application/json",
  618. "content-type": "application/json",
  619. "openai-sentinel-token": authorize_continue_sentinel,
  620. },
  621. data=json.dumps({"username": {"value": email, "kind": "email"}, "screen_hint": "signup"}),
  622. timeout=15,
  623. )
  624. print(f"[日志] 邮箱提交状态: {signup_res.status_code}")
  625. if signup_res.status_code != 200:
  626. print(f"[失败] 邮箱提交失败: {signup_res.text[:200]}")
  627. return None
  628. print("[步骤4] 设置账户密码...")
  629. pwd_res = s.post(
  630. "https://auth.openai.com/api/accounts/user/register",
  631. headers={
  632. "referer": "https://auth.openai.com/create-account/password",
  633. "accept": "application/json",
  634. "content-type": "application/json",
  635. },
  636. json={"password": password, "username": email},
  637. timeout=15,
  638. )
  639. print(f"[日志] 密码设置状态: {pwd_res.status_code}")
  640. if pwd_res.status_code != 200:
  641. print(f"[失败] 密码设置失败: {pwd_res.text[:200]}")
  642. return None
  643. print("[步骤5] 触发 OpenAI 发送验证邮件...")
  644. s.get("https://auth.openai.com/create-account/password", timeout=15)
  645. otp_send_res = s.get(
  646. "https://auth.openai.com/api/accounts/email-otp/send",
  647. headers={"referer": "https://auth.openai.com/create-account/password", "accept": "application/json"},
  648. timeout=15,
  649. )
  650. print(f"[日志] 发送指令状态: {otp_send_res.status_code}")
  651. if otp_send_res.status_code != 200:
  652. print(f"[失败] 发送验证码失败: {otp_send_res.text[:200]}")
  653. return None
  654. print("[步骤6] 等待邮箱接收 6 位验证码...")
  655. code = code_fetcher()
  656. if not code:
  657. print("[失败] 邮箱长时间未收到验证码")
  658. return None
  659. print(f"[成功] 捕获验证码: {code}")
  660. print("[步骤7] 提交验证码至 OpenAI...")
  661. val_res = s.post(
  662. "https://auth.openai.com/api/accounts/email-otp/validate",
  663. headers={
  664. "referer": "https://auth.openai.com/email-verification",
  665. "accept": "application/json",
  666. "content-type": "application/json",
  667. },
  668. json={"code": code},
  669. timeout=15,
  670. )
  671. print(f"[日志] 验证码校验状态: {val_res.status_code}")
  672. if val_res.status_code != 200:
  673. print(f"[失败] 验证码校验失败: {val_res.text[:200]}")
  674. return None
  675. print("[步骤8] 完善账户基本信息...")
  676. try:
  677. create_account_sentinel = _build_sentinel_payload(s, did, "authorize_continue")
  678. except Exception as e:
  679. print(f"[失败] 获取 create_account Sentinel 失败: {e}")
  680. return None
  681. acc_res = s.post(
  682. "https://auth.openai.com/api/accounts/create_account",
  683. headers={
  684. "referer": "https://auth.openai.com/about-you",
  685. "accept": "application/json",
  686. "content-type": "application/json",
  687. "openai-sentinel-token": create_account_sentinel,
  688. },
  689. data=json.dumps({"name": _random_name(), "birthdate": _random_birthdate()}),
  690. timeout=15,
  691. )
  692. print(f"[日志] 账户创建状态: {acc_res.status_code}")
  693. if acc_res.status_code != 200:
  694. print(f"[失败] 账户创建失败: {acc_res.text[:200]}")
  695. return None
  696. print("[步骤9] 注册完成,重新走登录流程获取 Workspace / Token...")
  697. first_code = code
  698. for login_attempt in range(3):
  699. try:
  700. print(f"[*] 正在通过登录流程获取 Token...{f' (重试 {login_attempt}/3)' if login_attempt else ''}")
  701. s2 = requests.Session(proxies=proxies, impersonate="chrome")
  702. oauth2 = generate_oauth_url()
  703. s2.get(oauth2.auth_url, timeout=15)
  704. did2 = s2.cookies.get("oai-did")
  705. if not did2:
  706. print("[失败] 登录会话未能获取 oai-did")
  707. continue
  708. lc = s2.post(
  709. "https://auth.openai.com/api/accounts/authorize/continue",
  710. headers={
  711. "referer": "https://auth.openai.com/log-in",
  712. "accept": "application/json",
  713. "content-type": "application/json",
  714. "openai-sentinel-token": _build_sentinel_payload(s2, did2, "authorize_continue"),
  715. },
  716. data=json.dumps({"username": {"value": email, "kind": "email"}, "screen_hint": "login"}),
  717. timeout=15,
  718. )
  719. print(f"[日志] 登录邮箱提交状态: {lc.status_code}")
  720. if lc.status_code != 200:
  721. print(f"[失败] 登录邮箱提交失败: {lc.text[:200]}")
  722. continue
  723. s2.get(str((lc.json() or {}).get("continue_url") or ""), timeout=15)
  724. pw = s2.post(
  725. "https://auth.openai.com/api/accounts/password/verify",
  726. headers={
  727. "referer": "https://auth.openai.com/log-in/password",
  728. "accept": "application/json",
  729. "content-type": "application/json",
  730. "openai-sentinel-token": _build_sentinel_payload(s2, did2, "authorize_continue"),
  731. },
  732. json={"password": password},
  733. timeout=15,
  734. )
  735. print(f"[日志] 登录密码验证状态: {pw.status_code}")
  736. if pw.status_code != 200:
  737. print(f"[失败] 登录密码验证失败: {pw.text[:200]}")
  738. continue
  739. existing_codes = list(extract_all_codes())
  740. s2.get(
  741. "https://auth.openai.com/email-verification",
  742. headers={"referer": "https://auth.openai.com/log-in/password"},
  743. timeout=15,
  744. )
  745. print("[*] 正在等待登录 OTP...")
  746. time.sleep(2)
  747. otp2 = None
  748. baseline_codes = set(existing_codes)
  749. baseline_codes.add(first_code)
  750. for _ in range(40):
  751. all_codes = extract_all_codes()
  752. new_codes = [c for c in all_codes if c not in baseline_codes]
  753. if new_codes:
  754. otp2 = new_codes[-1]
  755. break
  756. time.sleep(2)
  757. if not otp2:
  758. print("[失败] 未收到登录 OTP")
  759. continue
  760. print(f"[成功] 捕获登录 OTP: {otp2}")
  761. val2 = s2.post(
  762. "https://auth.openai.com/api/accounts/email-otp/validate",
  763. headers={
  764. "referer": "https://auth.openai.com/email-verification",
  765. "accept": "application/json",
  766. "content-type": "application/json",
  767. },
  768. json={"code": otp2},
  769. timeout=15,
  770. )
  771. print(f"[日志] 登录 OTP 校验状态: {val2.status_code}")
  772. if val2.status_code != 200:
  773. print(f"[失败] 登录 OTP 校验失败: {val2.text[:200]}")
  774. continue
  775. val2_data = val2.json() or {}
  776. print("[成功] 登录 OTP 验证成功")
  777. consent_url = str(val2_data.get("continue_url") or "").strip()
  778. if consent_url:
  779. s2.get(consent_url, timeout=15)
  780. auth_cookie = s2.cookies.get("oai-client-auth-session", domain=".auth.openai.com") or s2.cookies.get("oai-client-auth-session")
  781. if not auth_cookie:
  782. print("[失败] 登录后未能获取 oai-client-auth-session")
  783. continue
  784. auth_json = _decode_jwt_segment(auth_cookie.split(".")[0])
  785. if "workspaces" not in auth_json or not auth_json["workspaces"]:
  786. print(f"[失败] Cookie 中无 workspaces: {list(auth_json.keys())}")
  787. continue
  788. workspace_id = auth_json["workspaces"][0]["id"]
  789. print(f"[成功] Workspace ID: {workspace_id}")
  790. select_resp = s2.post(
  791. "https://auth.openai.com/api/accounts/workspace/select",
  792. headers={
  793. "referer": consent_url,
  794. "accept": "application/json",
  795. "content-type": "application/json",
  796. },
  797. json={"workspace_id": workspace_id},
  798. timeout=15,
  799. )
  800. print(f"[日志] Workspace 选择状态: {select_resp.status_code}")
  801. if select_resp.status_code != 200:
  802. print(f"[失败] Workspace 选择失败: {select_resp.text[:200]}")
  803. continue
  804. sel_data = select_resp.json() or {}
  805. if sel_data.get("page", {}).get("type", "") == "organization_select":
  806. orgs = sel_data.get("page", {}).get("payload", {}).get("data", {}).get("orgs", [])
  807. if orgs:
  808. org_sel = s2.post(
  809. "https://auth.openai.com/api/accounts/organization/select",
  810. headers={"accept": "application/json", "content-type": "application/json"},
  811. json={
  812. "org_id": orgs[0].get("id", ""),
  813. "project_id": orgs[0].get("default_project_id", ""),
  814. },
  815. timeout=15,
  816. )
  817. print(f"[日志] Organization 选择状态: {org_sel.status_code}")
  818. if org_sel.status_code != 200:
  819. print(f"[失败] Organization 选择失败: {org_sel.text[:200]}")
  820. continue
  821. sel_data = org_sel.json() or {}
  822. if "continue_url" not in sel_data:
  823. print(f"[失败] 未能获取 continue_url: {json.dumps(sel_data, ensure_ascii=False)[:500]}")
  824. continue
  825. print("[步骤10] 跟踪重定向并换取 Token...")
  826. r = s2.get(str(sel_data["continue_url"]), allow_redirects=False, timeout=15)
  827. cbk = None
  828. for i in range(20):
  829. loc = r.headers.get("Location", "")
  830. print(f" -> 重定向 #{i+1} 状态: {r.status_code} | 下一跳: {loc[:80] if loc else '无'}")
  831. if loc.startswith("http://localhost"):
  832. cbk = loc
  833. break
  834. if r.status_code not in (301, 302, 303) or not loc:
  835. break
  836. r = s2.get(loc, allow_redirects=False, timeout=15)
  837. if not cbk:
  838. print("[失败] 未能获取到 Callback URL")
  839. continue
  840. token_json = submit_callback_url(
  841. callback_url=cbk,
  842. expected_state=oauth2.state,
  843. code_verifier=oauth2.code_verifier,
  844. redirect_uri=oauth2.redirect_uri,
  845. session=s2,
  846. )
  847. print("[大功告成] 账号注册完毕!")
  848. return token_json, email, password
  849. except Exception as e:
  850. print(f"[失败] 登录补全流程异常: {e}")
  851. time.sleep(2)
  852. continue
  853. print("[失败] 登录补全流程 3 次均未完成。")
  854. return None
  855. except Exception as e:
  856. print(f"[致命错误] 流程崩溃: {e}")
  857. return None
  858. # ========== Main 保持原版完整结构与输出格式 ==========
  859. def main():
  860. parser = argparse.ArgumentParser()
  861. parser.add_argument("--proxy", help="代理地址")
  862. parser.add_argument("--mail-provider", choices=["auto", "gptmail", "tempmail"], default="auto", help="临时邮箱提供商:auto/gptmail/tempmail")
  863. parser.add_argument("--once", action="store_true", help="只运行一次")
  864. parser.add_argument("--sleep-min", type=int, default=5, help="最小间隔(秒)")
  865. parser.add_argument("--sleep-max", type=int, default=30, help="最大间隔(秒)")
  866. parser.add_argument("--cpa-base-url", default=os.getenv("CPA_BASE_URL"), help="CPA 基础地址")
  867. parser.add_argument("--cpa-token", default=os.getenv("CPA_TOKEN"), help="CPA 管理 token (Bearer)")
  868. parser.add_argument("--cpa-workers", type=int, default=20, help="CPA 清理并发")
  869. parser.add_argument("--cpa-timeout", type=int, default=12, help="CPA 请求超时")
  870. parser.add_argument("--cpa-retries", type=int, default=1, help="CPA 清理重试次数")
  871. parser.add_argument("--cpa-used-threshold", type=int, default=95, help="CPA used_percent 阈值")
  872. parser.add_argument("--cpa-clean", action="store_true", help="注册后自动清理 CPA 失效账号")
  873. parser.add_argument("--cpa-upload", action="store_true", help="注册后自动上传 CPA")
  874. parser.add_argument("--cpa-target-count", type=int, default=300, help="目标 token 数(有效)")
  875. parser.add_argument("--cpa-prune-local", action="store_true", help="上传成功后删除本地 token 文件与账号行")
  876. args = parser.parse_args()
  877. tokens_dir = OUT_DIR / "tokens"
  878. tokens_dir.mkdir(parents=True, exist_ok=True)
  879. pm = _build_cpa_maintainer(args)
  880. count = 0
  881. while True:
  882. count += 1
  883. print(f"\n[{datetime.now().strftime('%H:%M:%S')}] >>> 流程 #{count} <<<")
  884. if pm:
  885. if args.cpa_clean:
  886. _clean_invalid_in_cpa(pm, args)
  887. current_count = _count_valid_cpa_tokens(pm, args)
  888. print(f"[CPA] 当前有效 token: {current_count} / {args.cpa_target_count}")
  889. if current_count >= args.cpa_target_count:
  890. if args.once:
  891. break
  892. wait_time = random.randint(args.sleep_min, args.sleep_max)
  893. print(f"[*] 随机休息 {wait_time} 秒...")
  894. time.sleep(wait_time)
  895. continue
  896. res = run(args.proxy, args.mail_provider)
  897. if res:
  898. token_json, email, real_pwd = res
  899. print(f"[🎉] 成功! {email} ---- {real_pwd}")
  900. # 1. 保存账号密码到 tokens/accounts.txt
  901. with open(tokens_dir / "accounts.txt", "a", encoding="utf-8") as f:
  902. f.write(f"{email}----{real_pwd}\n")
  903. # 2. 保存详细 Token JSON
  904. fname_email = email.replace("@", "_")
  905. token_file = tokens_dir / f"token_{fname_email}_{int(time.time())}.json"
  906. token_file.write_text(token_json, encoding="utf-8")
  907. print(f"[*] Token 文件已保存: {token_file.name}")
  908. # 3. 上传 CPA(可选)
  909. upload_ok = False
  910. if args.cpa_upload:
  911. upload_ok = _upload_token_to_cpa(pm, token_json, email, proxy=args.proxy or "")
  912. # 4. 上传成功后按需删除本地文件/账号行
  913. if upload_ok and args.cpa_prune_local:
  914. try:
  915. if token_file.exists():
  916. token_file.unlink()
  917. print(f"[本地清理] 已删除 token 文件: {token_file.name}")
  918. except Exception as e:
  919. print(f"[本地清理] 删除 token 文件失败: {e}")
  920. _remove_account_entry(tokens_dir / "accounts.txt", email, real_pwd)
  921. # 5. 注册后再清理一次(可选)
  922. if pm and args.cpa_clean:
  923. _clean_invalid_in_cpa(pm, args)
  924. else:
  925. print("[-] 本次注册流程未能完成。")
  926. if args.once:
  927. break
  928. wait_time = random.randint(args.sleep_min, args.sleep_max)
  929. print(f"[*] 随机休息 {wait_time} 秒...")
  930. time.sleep(wait_time)
  931. if __name__ == "__main__":
  932. main()