@@ -397,6 +397,23 @@ def extract_oauth_callback_params_from_consent_session(session: requests.Session
397397def exchange_platform_tokens (session : requests .Session , device_id : str , code_verifier : str , consent_url : str ) -> dict | None :
398398 callback_params = extract_oauth_callback_params_from_consent_session (session , consent_url , device_id )
399399 if not callback_params :
400+ # 回退方案:直接导航 consent URL(allow_redirects=True),从最终 URL 提取 code
401+ print (f"[exchange_platform_tokens] 主方案失败,尝试回退方案, continue_url={ consent_url [:120 ]} " )
402+ try :
403+ r = session .get (consent_url , headers = navigate_headers , allow_redirects = True , verify = False , timeout = 30 )
404+ final_url = str (r .url )
405+ print (f"[exchange_platform_tokens] 回退 final_url={ final_url [:120 ]} " )
406+ callback_params = extract_oauth_callback_params_from_url (final_url )
407+ if not callback_params :
408+ for hist in getattr (r , "history" , []) or []:
409+ loc = str (hist .headers .get ("Location" ) or "" )
410+ callback_params = extract_oauth_callback_params_from_url (loc )
411+ if callback_params :
412+ break
413+ except Exception as e :
414+ print (f"[exchange_platform_tokens] 回退方案异常: { e } " )
415+ if not callback_params :
416+ print ("[exchange_platform_tokens] 所有方案均无法提取 OAuth code" )
400417 return None
401418 code = str (callback_params .get ("code" ) or "" ).strip ()
402419 if not code :
@@ -516,9 +533,18 @@ def _create_account(self, name: str, birthdate: str, index: int) -> None:
516533 detail = f", detail={ json .dumps (data , ensure_ascii = False )} " if data else ""
517534 raise RuntimeError (error or f"create_account_http_{ getattr (resp , 'status_code' , 'unknown' )} { detail } " )
518535 step (index , "创建账号资料完成" )
519-
536+
520537 def _login_and_exchange_tokens (self , email : str , password : str , mailbox : dict , index : int ) -> dict :
521538 step (index , "开始独立登录换 token" )
539+
540+ # 关键:清除注册阶段残留的认证 cookie,避免 409 invalid_state
541+ for cookie in list (self .session .cookies ):
542+ if 'auth.openai.com' in cookie .domain :
543+ self .session .cookies .clear (domain = cookie .domain , path = cookie .path , name = cookie .name )
544+ # 重新设置设备 ID(必须)
545+ self .session .cookies .set ("oai-did" , self .device_id , domain = ".auth.openai.com" )
546+ self .session .cookies .set ("oai-did" , self .device_id , domain = "auth.openai.com" )
547+
522548 code_verifier , code_challenge = _generate_pkce ()
523549 params = {
524550 "issuer" : auth_base ,
@@ -538,24 +564,88 @@ def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, i
538564 "code_challenge_method" : "S256" ,
539565 "auth0Client" : platform_auth0_client ,
540566 }
541- 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 )
567+ resp , error = request_with_local_retry (
568+ self .session , "get" ,
569+ f"{ auth_base } /api/accounts/authorize?{ urlencode (params )} " ,
570+ headers = self ._navigate_headers (f"{ platform_base } /" ),
571+ allow_redirects = True , verify = False
572+ )
542573 if resp is None :
543574 raise RuntimeError (error or "platform_login_authorize_failed" )
544575 step (index , "登录 authorize 完成" )
576+
577+ # 提交邮箱(原样,不带 state)
578+ def _do_authorize_continue ():
579+ h = self ._json_headers (f"{ auth_base } /log-in?usernameKind=email" )
580+ h ["openai-sentinel-token" ] = build_sentinel_token (
581+ self .session , self .device_id , "authorize_continue"
582+ )
583+ return request_with_local_retry (
584+ self .session , "post" ,
585+ f"{ auth_base } /api/accounts/authorize/continue" ,
586+ json = {"username" : {"kind" : "email" , "value" : email }},
587+ headers = h ,
588+ allow_redirects = False ,
589+ verify = False
590+ )
591+
592+ step (index , "开始提交邮箱" )
593+ resp , error = _do_authorize_continue ()
594+ if resp is not None and resp .status_code == 409 :
595+ step (index , "邮箱提交 invalid_state,重新 authorize 后重试" )
596+ # 再次清除 cookie 并重新 authorize
597+ for cookie in list (self .session .cookies ):
598+ if 'auth.openai.com' in cookie .domain :
599+ self .session .cookies .clear (domain = cookie .domain , path = cookie .path , name = cookie .name )
600+ self .session .cookies .set ("oai-did" , self .device_id , domain = ".auth.openai.com" )
601+ self .session .cookies .set ("oai-did" , self .device_id , domain = "auth.openai.com" )
602+ resp , error = request_with_local_retry (
603+ self .session , "get" ,
604+ f"{ auth_base } /api/accounts/authorize?{ urlencode (params )} " ,
605+ headers = self ._navigate_headers (f"{ platform_base } /" ),
606+ allow_redirects = True , verify = False
607+ )
608+ if resp is None :
609+ raise RuntimeError (error or "platform_login_authorize_retry_failed" )
610+ resp , error = _do_authorize_continue ()
611+
612+ if resp is None or resp .status_code != 200 :
613+ data = _response_json (resp ) if resp is not None else {}
614+ detail = json .dumps (data , ensure_ascii = False ) if data else ""
615+ raise RuntimeError (
616+ error or f"email_submit_http_{ getattr (resp , 'status_code' , 'unknown' )} "
617+ + (f": { detail } " if detail else "" )
618+ )
619+ step (index , "邮箱提交完成" )
620+
621+ # 密码验证
622+ step (index , "开始密码校验" )
545623 headers = self ._json_headers (f"{ auth_base } /log-in/password" )
546- headers ["openai-sentinel-token" ] = build_sentinel_token (self .session , self .device_id , "password_verify" )
547- 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 )
624+ headers ["openai-sentinel-token" ] = build_sentinel_token (
625+ self .session , self .device_id , "password_verify"
626+ )
627+ resp , error = request_with_local_retry (
628+ self .session , "post" ,
629+ f"{ auth_base } /api/accounts/password/verify" ,
630+ json = {"password" : password },
631+ headers = headers ,
632+ allow_redirects = False ,
633+ verify = False
634+ )
548635 if resp is None or resp .status_code != 200 :
549- raise RuntimeError (error or f"password_verify_http_{ getattr (resp , 'status_code' , 'unknown ' )} " )
636+ raise RuntimeError (error or f"password_verify_http_{ getattr (resp , 'status_code' , '' )} " )
550637 step (index , "密码校验完成" )
638+
551639 payload = _response_json (resp )
552640 continue_url = str (payload .get ("continue_url" ) or "" ).strip ()
553641 page_type = str (((payload .get ("page" ) or {}).get ("type" )) or "" )
642+
554643 if page_type == "email_otp_verification" or "email-verification" in continue_url or "email-otp" in continue_url :
555644 step (index , "独立登录需要邮箱验证码" )
556645 code = wait_for_code (mailbox )
557646 if not code :
558647 raise RuntimeError ("独立登录等待验证码超时" )
648+ step (index , f"收到登录验证码: { code } " )
559649 resp , reason = validate_otp (self .session , self .device_id , code )
560650 if resp is None or resp .status_code != 200 :
561651 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
565655 otp_payload = _response_json (resp )
566656 continue_url = str (otp_payload .get ("continue_url" ) or continue_url ).strip ()
567657 step (index , "独立登录验证码校验完成" )
658+
568659 if not continue_url :
569660 continue_url = f"{ auth_base } /sign-in-with-chatgpt/codex/consent"
570661 tokens = exchange_platform_tokens (self .session , self .device_id , code_verifier , continue_url )
0 commit comments