openai_register.py 48 KB

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