diff --git a/services/register/openai_register.py b/services/register/openai_register.py index 15724449..1219d8d9 100644 --- a/services/register/openai_register.py +++ b/services/register/openai_register.py @@ -397,6 +397,23 @@ def extract_oauth_callback_params_from_consent_session(session: requests.Session def exchange_platform_tokens(session: requests.Session, device_id: str, code_verifier: str, consent_url: str) -> dict | None: callback_params = extract_oauth_callback_params_from_consent_session(session, consent_url, device_id) if not callback_params: + # 回退方案:直接导航 consent URL(allow_redirects=True),从最终 URL 提取 code + print(f"[exchange_platform_tokens] 主方案失败,尝试回退方案, continue_url={consent_url[:120]}") + try: + r = session.get(consent_url, headers=navigate_headers, allow_redirects=True, verify=False, timeout=30) + final_url = str(r.url) + print(f"[exchange_platform_tokens] 回退 final_url={final_url[:120]}") + callback_params = extract_oauth_callback_params_from_url(final_url) + if not callback_params: + for hist in getattr(r, "history", []) or []: + loc = str(hist.headers.get("Location") or "") + callback_params = extract_oauth_callback_params_from_url(loc) + if callback_params: + break + except Exception as e: + print(f"[exchange_platform_tokens] 回退方案异常: {e}") + if not callback_params: + print("[exchange_platform_tokens] 所有方案均无法提取 OAuth code") return None code = str(callback_params.get("code") or "").strip() if not code: @@ -516,9 +533,18 @@ def _create_account(self, name: str, birthdate: str, index: int) -> None: detail = f", detail={json.dumps(data, ensure_ascii=False)}" if data else "" raise RuntimeError(error or f"create_account_http_{getattr(resp, 'status_code', 'unknown')}{detail}") step(index, "创建账号资料完成") - + def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, index: int) -> dict: step(index, "开始独立登录换 token") + + # 关键:清除注册阶段残留的认证 cookie,避免 409 invalid_state + for cookie in list(self.session.cookies): + if 'auth.openai.com' in cookie.domain: + self.session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) + # 重新设置设备 ID(必须) + self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com") + self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com") + code_verifier, code_challenge = _generate_pkce() params = { "issuer": auth_base, @@ -538,24 +564,88 @@ def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, i "code_challenge_method": "S256", "auth0Client": platform_auth0_client, } - 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) + 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 + ) if resp is None: raise RuntimeError(error or "platform_login_authorize_failed") step(index, "登录 authorize 完成") + + # 提交邮箱(原样,不带 state) + def _do_authorize_continue(): + h = self._json_headers(f"{auth_base}/log-in?usernameKind=email") + h["openai-sentinel-token"] = build_sentinel_token( + self.session, self.device_id, "authorize_continue" + ) + return request_with_local_retry( + self.session, "post", + f"{auth_base}/api/accounts/authorize/continue", + json={"username": {"kind": "email", "value": email}}, + headers=h, + allow_redirects=False, + verify=False + ) + + step(index, "开始提交邮箱") + resp, error = _do_authorize_continue() + if resp is not None and resp.status_code == 409: + step(index, "邮箱提交 invalid_state,重新 authorize 后重试") + # 再次清除 cookie 并重新 authorize + for cookie in list(self.session.cookies): + if 'auth.openai.com' in cookie.domain: + self.session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) + self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com") + self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com") + 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 + ) + if resp is None: + raise RuntimeError(error or "platform_login_authorize_retry_failed") + resp, error = _do_authorize_continue() + + if resp is None or resp.status_code != 200: + data = _response_json(resp) if resp is not None else {} + detail = json.dumps(data, ensure_ascii=False) if data else "" + raise RuntimeError( + error or f"email_submit_http_{getattr(resp, 'status_code', 'unknown')}" + + (f": {detail}" if detail else "") + ) + step(index, "邮箱提交完成") + + # 密码验证 + step(index, "开始密码校验") headers = self._json_headers(f"{auth_base}/log-in/password") - headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "password_verify") - 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) + headers["openai-sentinel-token"] = build_sentinel_token( + self.session, self.device_id, "password_verify" + ) + 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 + ) if resp is None or resp.status_code != 200: - raise RuntimeError(error or f"password_verify_http_{getattr(resp, 'status_code', 'unknown')}") + raise RuntimeError(error or f"password_verify_http_{getattr(resp, 'status_code', '')}") step(index, "密码校验完成") + payload = _response_json(resp) continue_url = str(payload.get("continue_url") or "").strip() page_type = str(((payload.get("page") or {}).get("type")) or "") + if page_type == "email_otp_verification" or "email-verification" in continue_url or "email-otp" in continue_url: step(index, "独立登录需要邮箱验证码") code = wait_for_code(mailbox) if not code: raise RuntimeError("独立登录等待验证码超时") + step(index, f"收到登录验证码: {code}") resp, reason = validate_otp(self.session, self.device_id, code) if resp is None or resp.status_code != 200: print("独立登录验证码校验失败响应:", resp.text if resp is not None else "None") @@ -565,6 +655,7 @@ def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, i otp_payload = _response_json(resp) continue_url = str(otp_payload.get("continue_url") or continue_url).strip() step(index, "独立登录验证码校验完成") + if not continue_url: continue_url = f"{auth_base}/sign-in-with-chatgpt/codex/consent" tokens = exchange_platform_tokens(self.session, self.device_id, code_verifier, continue_url)