优化gpt流程

This commit is contained in:
zhangchen
2026-04-04 09:20:26 +08:00
parent 286a50a629
commit 559447e2ca
3 changed files with 1111 additions and 156 deletions

View File

@@ -130,6 +130,7 @@ class ChatGPTClient:
# 设置 oai-did cookie
seed_oai_device_cookie(self.session, self.device_id)
self.last_registration_state = FlowState()
self.last_stage = ""
def _get_sentinel_token(self, flow: str, *, page_url: str | None = None):
prefer_browser = flow in {"username_password_create", "oauth_create_account"}
@@ -163,6 +164,14 @@ class ChatGPTClient:
if self.verbose:
print(f" {msg}")
def _enter_stage(self, stage: str, detail: str = ""):
self.last_stage = str(stage or "").strip()
if self.last_stage:
message = f"[stage={self.last_stage}]"
if detail:
message += f" {detail}"
self._log(message)
def _browser_pause(self, low=0.15, high=0.45):
"""在 headed 模式下加入轻微停顿,模拟有头浏览器节奏。"""
if self.browser_mode == "headed":
@@ -352,42 +361,103 @@ class ChatGPTClient:
def get_next_auth_session_token(self):
"""获取 ChatGPT next-auth 会话 Cookie。"""
return self._get_cookie_value("__Secure-next-auth.session-token", "chatgpt.com")
return (
self._get_cookie_value("__Secure-next-auth.session-token", "chatgpt.com")
or self._get_cookie_value("__Secure-authjs.session-token", "chatgpt.com")
)
def fetch_chatgpt_session(self):
def fetch_chatgpt_session(self, max_attempts=5, retry_delay=1.2):
"""请求 ChatGPT Session 接口并返回原始会话数据。"""
url = f"{self.BASE}/api/auth/session"
self._browser_pause()
response = self.session.get(
url,
headers=self._headers(
url,
accept="application/json",
referer=f"{self.BASE}/",
fetch_site="same-origin",
),
timeout=30,
)
if response.status_code != 200:
return False, f"/api/auth/session -> HTTP {response.status_code}"
last_error = ""
try:
data = response.json()
except Exception as exc:
return False, f"/api/auth/session 返回非 JSON: {exc}"
for attempt in range(max(1, int(max_attempts or 1))):
try:
self._browser_pause()
response = self.session.get(
url,
headers=self._headers(
url,
accept="application/json",
referer=f"{self.BASE}/",
fetch_site="same-origin",
),
timeout=30,
)
except Exception as exc:
last_error = f"/api/auth/session 请求异常: {exc}"
if attempt < max_attempts - 1:
self._log(
f"{last_error},等待 {retry_delay:.1f}s 后重试 "
f"({attempt + 1}/{max_attempts})"
)
time.sleep(retry_delay)
continue
return False, last_error
access_token = str(data.get("accessToken") or "").strip()
if not access_token:
return False, "/api/auth/session 未返回 accessToken"
return True, data
if response.status_code != 200:
last_error = f"/api/auth/session -> HTTP {response.status_code}"
if attempt < max_attempts - 1:
self._log(
f"{last_error},等待 {retry_delay:.1f}s 后重试 "
f"({attempt + 1}/{max_attempts})"
)
time.sleep(retry_delay)
continue
return False, last_error
try:
data = response.json()
except Exception as exc:
last_error = f"/api/auth/session 返回非 JSON: {exc}"
if attempt < max_attempts - 1:
self._log(
f"{last_error},等待 {retry_delay:.1f}s 后重试 "
f"({attempt + 1}/{max_attempts})"
)
time.sleep(retry_delay)
continue
return False, last_error
access_token = str(data.get("accessToken") or "").strip()
if access_token:
return True, data
last_error = "/api/auth/session 未返回 accessToken"
if attempt < max_attempts - 1:
self._log(
f"{last_error},等待 {retry_delay:.1f}s 后重试 "
f"({attempt + 1}/{max_attempts})"
)
try:
self.session.get(
f"{self.BASE}/",
headers=self._headers(
f"{self.BASE}/",
accept="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
referer=f"{self.BASE}/",
navigation=True,
),
allow_redirects=True,
timeout=30,
)
except Exception:
pass
time.sleep(retry_delay)
continue
return False, last_error
return False, last_error or "/api/auth/session 未返回 accessToken"
def reuse_session_and_get_tokens(self):
"""
复用注册阶段已建立的 ChatGPT 会话,直接读取 Session / AccessToken。
承接前序阶段已建立的 ChatGPT 会话,直接读取 Session / AccessToken。
Returns:
tuple[bool, dict|str]: 成功时返回标准化 token/session 数据;失败时返回错误信息。
"""
self._enter_stage("token_exchange", "reuse session -> /api/auth/session")
state = self.last_registration_state or FlowState()
self._log("步骤 1/4: 跟随注册回调 external_url ...")
if state.page_type == "external_url" or self._state_requires_navigation(state):
@@ -402,9 +472,33 @@ class ChatGPTClient:
self._log("注册回调已落地,跳过额外跟随")
self._log("步骤 2/4: 检查 __Secure-next-auth.session-token ...")
session_cookie = self.get_next_auth_session_token()
session_cookie = ""
for attempt in range(5):
session_cookie = self.get_next_auth_session_token()
if session_cookie:
break
self._log(
f"next-auth session cookie 尚未落地,补一次 ChatGPT 首页触达 "
f"({attempt + 1}/5)"
)
try:
self._browser_pause(0.2, 0.5)
self.session.get(
f"{self.BASE}/",
headers=self._headers(
f"{self.BASE}/",
accept="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
referer=state.current_url or f"{self.AUTH}/about-you",
navigation=True,
),
allow_redirects=True,
timeout=30,
)
except Exception as exc:
self._log(f"补触达 ChatGPT 首页异常: {exc}")
time.sleep(1.0)
if not session_cookie:
return False, "缺少 __Secure-next-auth.session-token注册回调可能未落地"
return False, "缺少 ChatGPT session-token注册回调可能未完全落地"
self._log("步骤 3/4: 请求 ChatGPT /api/auth/session ...")
ok, session_or_error = self.fetch_chatgpt_session()
@@ -444,7 +538,7 @@ class ChatGPTClient:
"raw_session": session_data,
}
self._log("步骤 4/4: 已从复用会话中提取 accessToken")
self._log("步骤 4/4: 已从当前会话中提取 accessToken")
if account_id:
self._log(f"Session Account ID: {account_id}")
if user_id:
@@ -622,6 +716,7 @@ class ChatGPTClient:
Returns:
tuple: (success, message)
"""
self._enter_stage("authorize_continue", f"register_user email={email}")
self._log(f"注册用户: {email}")
url = f"{self.AUTH}/api/accounts/user/register"
@@ -655,6 +750,7 @@ class ChatGPTClient:
if r.status_code == 200:
data = r.json()
self._log("注册成功")
self._log(f"authorize_continue/register_user 响应 URL: {str(r.url)[:120]}")
return True, "注册成功"
else:
try:
@@ -671,6 +767,7 @@ class ChatGPTClient:
def send_email_otp(self, referer=None):
"""触发发送邮箱验证码"""
self._enter_stage("otp", "send email otp")
self._log("触发发送验证码...")
url = f"{self.AUTH}/api/accounts/email-otp/send"
@@ -700,6 +797,7 @@ class ChatGPTClient:
if isinstance(payload, dict) and payload:
next_state = self._state_from_payload(payload, current_url=str(r.url) or url)
self._log(f"验证码发送响应: {describe_flow_state(next_state)}")
self._log(f"otp/send 当前 URL: {str(r.url)[:120]}")
else:
self._log("验证码发送响应: 非 JSON按已触发处理")
return True
@@ -717,6 +815,7 @@ class ChatGPTClient:
Returns:
tuple: (success, message)
"""
self._enter_stage("otp", f"verify email otp code={otp_code}")
self._log(f"验证 OTP 码: {otp_code}")
url = f"{self.AUTH}/api/accounts/email-otp/validate"
@@ -745,6 +844,7 @@ class ChatGPTClient:
data, current_url=str(r.url) or f"{self.AUTH}/about-you"
)
self._log(f"验证成功 {describe_flow_state(next_state)}")
self._log(f"otp/validate 当前 URL: {str(r.url)[:120]}")
return (True, next_state) if return_state else (True, "验证成功")
else:
error_msg = r.text[:200]
@@ -767,6 +867,7 @@ class ChatGPTClient:
Returns:
tuple: (success, message)
"""
self._enter_stage("about_you", "register create_account")
name = f"{first_name} {last_name}"
self._log(f"完成账号创建: {name}")
url = f"{self.AUTH}/api/accounts/create_account"
@@ -813,6 +914,7 @@ class ChatGPTClient:
data, current_url=str(r.url) or self.BASE
)
self._log(f"账号创建成功 {describe_flow_state(next_state)}")
self._log(f"about_you/create_account 当前 URL: {str(r.url)[:120]}")
return (True, next_state) if return_state else (True, "账号创建成功")
else:
error_code = ""
@@ -957,6 +1059,7 @@ class ChatGPTClient:
return True, "注册成功"
if self._state_is_password_registration(state):
self._enter_stage("authorize_continue", describe_flow_state(state))
self._log("全新注册流程")
if register_submitted:
return False, "注册密码阶段重复进入"
@@ -976,6 +1079,7 @@ class ChatGPTClient:
continue
if self._state_is_email_otp(state):
self._enter_stage("otp", describe_flow_state(state))
self._log("等待邮箱验证码...")
otp_code = skymail_client.wait_for_verification_code(
email, timeout=otp_wait_timeout
@@ -1008,6 +1112,7 @@ class ChatGPTClient:
continue
if self._state_is_about_you(state):
self._enter_stage("about_you", describe_flow_state(state))
if stop_before_about_you_submission:
self.last_registration_state = state
self._log(
@@ -1031,6 +1136,10 @@ class ChatGPTClient:
continue
if self._state_requires_navigation(state):
if "workspace" in f"{state.continue_url} {state.current_url}".lower() or "consent" in f"{state.continue_url} {state.current_url}".lower():
self._enter_stage("workspace_select", describe_flow_state(state))
elif state.page_type == "external_url":
self._enter_stage("token_exchange", describe_flow_state(state))
success, next_state = self._follow_flow_state(
state,
referer=state.current_url or f"{self.AUTH}/about-you",

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,9 @@
"""
ChatGPT Refresh Token 注册引擎。
新实现不再沿用旧的分步补丁式注册链路,而是直接复用
1. `ChatGPTClient.register_complete_flow()` 负责完整注册状态机
2. `OAuthClient.login_and_get_tokens()` 负责全新 OAuth + passwordless OTP 登录拿 RT
目标是让 refresh_token 模式与当前主状态机链路保持一致,不再以旧流程做兜底。
主链路采用两段式推进
1. `ChatGPTClient.register_complete_flow()` 负责注册状态机推进到 about_you
2. `OAuthClient.login_and_get_tokens()` 承接前序会话继续完成 about_you / workspace / token
"""
from __future__ import annotations
@@ -85,6 +83,46 @@ class EmailServiceAdapter:
self.email = email
self.log_fn = log_fn
self._used_codes: set[str] = set()
self._last_code: str = ""
self._last_code_at: float = 0.0
self._last_success_code: str = ""
self._last_success_code_at: float = 0.0
@property
def last_code(self) -> str:
return self._last_success_code or self._last_code
def _remember_code(self, code: str, *, successful: bool = False) -> None:
code = str(code or "").strip()
if not code:
return
now = time.time()
self._last_code = code
self._last_code_at = now
self._used_codes.add(code)
if successful:
self._last_success_code = code
self._last_success_code_at = now
def remember_successful_code(self, code: str) -> None:
self._remember_code(code, successful=True)
def get_recent_code(
self,
max_age_seconds: int = 180,
*,
prefer_successful: bool = True,
) -> str:
now = time.time()
if (
prefer_successful
and self._last_success_code
and now - self._last_success_code_at <= max_age_seconds
):
return self._last_success_code
if self._last_code and now - self._last_code_at <= max_age_seconds:
return self._last_code
return ""
def wait_for_verification_code(
self,
@@ -103,7 +141,7 @@ class EmailServiceAdapter:
)
if code:
code = str(code).strip()
self._used_codes.add(code)
self._remember_code(code, successful=False)
self.log_fn(f"成功获取验证码: {code}")
return code
@@ -177,18 +215,6 @@ class RefreshTokenRegistrationEngine:
self._log(f"创建邮箱失败: {e}", "error")
return False
@staticmethod
def _should_switch_to_login_after_register_failure(message: str) -> bool:
text = str(message or "").lower()
markers = (
"user_already_exists",
"account already exists",
"please login instead",
"add_phone",
"add-phone",
)
return any(marker in text for marker in markers)
def _read_int_config(
self,
primary_key: str,
@@ -210,6 +236,18 @@ class RefreshTokenRegistrationEngine:
return max(minimum, min(parsed, maximum))
return max(minimum, min(int(default), maximum))
@staticmethod
def _should_switch_to_login_after_register_failure(message: str) -> bool:
text = str(message or "").lower()
markers = (
"user_already_exists",
"account already exists",
"please login instead",
"add_phone",
"add-phone",
)
return any(marker in text for marker in markers)
def _build_chatgpt_client(self) -> ChatGPTClient:
client = ChatGPTClient(
proxy=self.proxy_url,
@@ -229,6 +267,27 @@ class RefreshTokenRegistrationEngine:
client._log = lambda msg: self._log(f"[登录链路] {msg}")
return client
def _reuse_register_browser_context(
self,
register_client: ChatGPTClient,
oauth_client: OAuthClient,
) -> None:
oauth_client.adopt_browser_context(
register_client.session,
device_id=getattr(register_client, "device_id", "") or "",
user_agent=getattr(register_client, "ua", None),
sec_ch_ua=getattr(register_client, "sec_ch_ua", None),
accept_language=(
getattr(register_client.session, "headers", {}).get("Accept-Language", "")
if getattr(register_client, "session", None) is not None
else ""
),
)
oauth_client.impersonate = str(
getattr(register_client, "impersonate", "") or ""
).strip()
self._log("已接入前序 session/cookie/fingerprint继续处理 OAuth 后续步骤")
def _extract_account_info(self, tokens: dict[str, Any]) -> dict[str, Any]:
id_token = str((tokens or {}).get("id_token") or "").strip()
if not id_token:
@@ -270,7 +329,7 @@ class RefreshTokenRegistrationEngine:
oauth_client: OAuthClient,
registration_message: str,
source: str,
register_client: ChatGPTClient,
register_client: Any,
) -> None:
account_info = self._extract_account_info(tokens)
workspace_id = self._extract_workspace_id(oauth_client)
@@ -332,7 +391,7 @@ class RefreshTokenRegistrationEngine:
self._log("=" * 60)
self._log("ChatGPT RT 全新主链路启动")
self._log(f"请求模式: {self.browser_mode}")
self._log("实现策略: 注册状态机 + 全新 OAuth session + 登录流程复刻")
self._log("实现策略: 注册状态机 + OAuth 接续流程")
self._log("=" * 60)
if not fixed_email:
@@ -353,7 +412,7 @@ class RefreshTokenRegistrationEngine:
self._log(f"邮箱: {result.email}")
self._log(f"密码: {self.password}")
self._log(f"注册信息: {first_name} {last_name}, 生日: {birthdate}")
self._log("流程策略: 注册阶段到 about_you 即停,改由 OAuth 新会话补全资料")
self._log("流程策略: 注册阶段推进到 about_you 后切换到 OAuth 流程继续完成后续步骤")
self._log(
"验证码等待策略: "
f"register_wait={register_otp_wait_seconds}s, "
@@ -382,17 +441,19 @@ class RefreshTokenRegistrationEngine:
)
if not registered:
if not self._should_switch_to_login_after_register_failure(registration_message):
if not self._should_switch_to_login_after_register_failure(
registration_message
):
last_error = f"注册状态机失败: {registration_message}"
result.error_message = last_error
return result
source = "login"
self._log(
"注册阶段命中可恢复终态,直接切换到全新 OAuth + passwordless 登录链路",
"注册阶段命中可继续处理的终态,改走 OAuth 登录流程",
"warning",
)
self._log(f"切换原因: {registration_message}")
source = "login"
else:
if registration_message == "pending_about_you_submission":
self._log("注册状态机已推进至 about_you符合预期。下一步进入 OAuth 会话补全资料")
@@ -403,43 +464,47 @@ class RefreshTokenRegistrationEngine:
)
oauth_client = self._build_oauth_client()
use_login_front_half = registration_message == "pending_about_you_submission"
oauth_client.config.setdefault(
"chatgpt_oauth_otp_wait_seconds",
register_otp_wait_seconds,
)
oauth_client.config.setdefault(
"chatgpt_oauth_otp_resend_wait_seconds",
register_otp_resend_wait_seconds,
)
if use_login_front_half:
self._log("3. 新开 OAuth session严格复刻 login_and_get_tokens 登录链路")
self._log("4. 本轮仅共享邮箱+密码,其它会话数据全新")
use_continued_session = registered and (
registration_message == "pending_about_you_submission"
)
if use_continued_session:
self._reuse_register_browser_context(register_client, oauth_client)
self._log("3. 承接前序 session继续走 OAuth passwordless 流程")
self._log("4. 沿用前序阶段的 cookie / device_id / 浏览器指纹")
self._log("5. 登录成功后提交 about_you并继续 workspace/token 流程")
tokens = oauth_client.login_and_get_tokens(
result.email,
self.password,
device_id="",
user_agent=None,
sec_ch_ua=None,
impersonate=None,
device_id=getattr(register_client, "device_id", "") or "",
user_agent=getattr(register_client, "ua", None),
sec_ch_ua=getattr(register_client, "sec_ch_ua", None),
impersonate=getattr(register_client, "impersonate", None),
skymail_client=email_adapter,
prefer_passwordless_login=False,
prefer_passwordless_login=True,
allow_phone_verification=False,
force_new_browser=True,
force_new_browser=False,
force_chatgpt_entry=False,
screen_hint="login",
force_password_login=True,
force_password_login=False,
complete_about_you_if_needed=True,
first_name=first_name,
last_name=last_name,
birthdate=birthdate,
login_source=(
"existing_account_recovery"
if source == "login"
else "post_register_workspace_recovery"
),
stop_after_login=False,
login_source="post_register_workspace_continue",
)
else:
self._log("3. 新开 OAuth session按 screen_hint=login + passwordless OTP 登录...")
self._log("4. 若命中 about_you则在 OAuth 会话内提交姓名+生日,再继续 workspace/token")
oauth_screen_hint = "login"
oauth_force_password_login = False
oauth_force_chatgpt_entry = False
tokens = oauth_client.login_and_get_tokens(
result.email,
self.password,
@@ -448,20 +513,18 @@ class RefreshTokenRegistrationEngine:
sec_ch_ua=getattr(register_client, "sec_ch_ua", None),
impersonate=getattr(register_client, "impersonate", None),
skymail_client=email_adapter,
prefer_passwordless_login=not oauth_force_password_login,
prefer_passwordless_login=True,
allow_phone_verification=False,
force_new_browser=True,
force_chatgpt_entry=oauth_force_chatgpt_entry,
screen_hint=oauth_screen_hint,
force_password_login=oauth_force_password_login,
force_chatgpt_entry=False,
screen_hint="login",
force_password_login=False,
complete_about_you_if_needed=True,
first_name=first_name,
last_name=last_name,
birthdate=birthdate,
login_source=(
"existing_account_recovery"
if source == "login"
else "post_register_workspace_recovery"
"existing_account_continue" if source == "login" else "post_register_workspace_continue"
),
)
@@ -474,7 +537,7 @@ class RefreshTokenRegistrationEngine:
result=result,
tokens=tokens,
oauth_client=oauth_client,
registration_message=registration_message or "register_complete_flow:ok",
registration_message=registration_message,
source=source,
register_client=register_client,
)