fix(chatgpt): recover workspace after add_phone with one-shot oauth restart

This commit is contained in:
cong
2026-04-04 00:55:32 +08:00
parent 48f0243244
commit 20dc19e90d
2 changed files with 117 additions and 19 deletions

View File

@@ -716,6 +716,12 @@ class OAuthClient:
self._set_error(f"触发 passwordless OTP 异常: {e}")
return None
def _recreate_session(self):
"""重建会话,确保恢复链路使用全新 cookie 容器。"""
self.session = curl_requests.Session()
if self.proxy:
self.session.proxies = build_requests_proxy_config(self.proxy)
def login_and_get_tokens(
self,
email,
@@ -728,6 +734,7 @@ class OAuthClient:
prefer_passwordless_login=False,
allow_phone_verification=True,
login_source="",
_recovery_depth=0,
):
"""
完整的 OAuth 登录流程,获取 tokens
@@ -914,24 +921,52 @@ class OAuthClient:
if self._state_is_add_phone(state):
if not allow_phone_verification:
self._set_error(
"passwordless 登录后仍停留在 add_phone未获取到 workspace / callback"
if self._state_supports_workspace_resolution(state):
self._log(
"步骤5: add_phone 命中,但检测到 workspace 线索,继续尝试 workspace/org 选择"
)
elif prefer_passwordless_login and _recovery_depth < 1:
self._log(
"步骤5: add_phone 仍无 workspace/callback重启一次全新 OAuth session + 新 PKCE"
)
self._recreate_session()
return self.login_and_get_tokens(
email,
password,
device_id,
user_agent=user_agent,
sec_ch_ua=sec_ch_ua,
impersonate=impersonate,
skymail_client=skymail_client,
prefer_passwordless_login=prefer_passwordless_login,
allow_phone_verification=allow_phone_verification,
login_source=(
f"{login_source}:add_phone_recovery"
if login_source
else "add_phone_recovery"
),
_recovery_depth=_recovery_depth + 1,
)
else:
self._set_error(
"passwordless 登录后仍停留在 add_phone未获取到 workspace / callback"
)
return None
else:
next_state = self._handle_add_phone_verification(
device_id,
user_agent,
sec_ch_ua,
impersonate,
state,
)
return None
next_state = self._handle_add_phone_verification(
device_id,
user_agent,
sec_ch_ua,
impersonate,
state,
)
if not next_state:
if not self.last_error:
self._set_error("手机号验证后未进入下一步 OAuth 状态")
return None
referer = state.current_url or referer
state = next_state
continue
if not next_state:
if not self.last_error:
self._set_error("手机号验证后未进入下一步 OAuth 状态")
return None
referer = state.current_url or referer
state = next_state
continue
if self._state_requires_navigation(state):
code, next_state = self._follow_flow_state(
@@ -958,10 +993,18 @@ class OAuthClient:
if self._state_supports_workspace_resolution(state):
self._log("步骤6: 执行 workspace/org 选择")
code, next_state = self._oauth_submit_workspace_and_org(
consent_entry = (
state.continue_url
or state.current_url
or f"{self.oauth_issuer}/sign-in-with-chatgpt/codex/consent",
or f"{self.oauth_issuer}/sign-in-with-chatgpt/codex/consent"
)
if self._state_is_add_phone(state):
consent_entry = (
f"{self.oauth_issuer}/sign-in-with-chatgpt/codex/consent"
)
self._log("步骤6: 当前处于 add_phone改用 canonical consent URL 继续")
code, next_state = self._oauth_submit_workspace_and_org(
consent_entry,
device_id,
user_agent,
impersonate,

View File

@@ -295,6 +295,61 @@ class OAuthClientPasswordlessTests(unittest.TestCase):
follow_state.assert_called_once()
handle_phone.assert_not_called()
def test_login_and_get_tokens_uses_canonical_consent_url_when_state_is_add_phone(self):
client = self._make_client()
add_phone_state = FlowState(
page_type="add_phone",
continue_url="https://auth.openai.com/add-phone",
current_url="https://auth.openai.com/add-phone",
)
with mock.patch.object(client, "_bootstrap_oauth_session", return_value="https://auth.openai.com/log-in"), \
mock.patch.object(client, "_submit_authorize_continue", return_value=add_phone_state), \
mock.patch.object(client, "_state_supports_workspace_resolution", return_value=True), \
mock.patch.object(client, "_state_requires_navigation", return_value=False), \
mock.patch.object(client, "_oauth_submit_workspace_and_org", return_value=("auth-code", None)) as submit_workspace, \
mock.patch.object(client, "_exchange_code_for_tokens", return_value={"access_token": "at"}):
tokens = client.login_and_get_tokens(
"user@example.com",
"Secret123!",
"device-fixed",
prefer_passwordless_login=True,
allow_phone_verification=False,
skymail_client=mock.Mock(),
)
self.assertEqual(tokens["access_token"], "at")
self.assertEqual(
submit_workspace.call_args.args[0],
"https://auth.openai.com/sign-in-with-chatgpt/codex/consent",
)
def test_login_and_get_tokens_retries_once_when_add_phone_has_no_workspace(self):
client = self._make_client()
add_phone_state = FlowState(
page_type="add_phone",
continue_url="https://auth.openai.com/add-phone",
current_url="https://auth.openai.com/add-phone",
)
with mock.patch.object(client, "_bootstrap_oauth_session", return_value="https://auth.openai.com/log-in") as bootstrap, \
mock.patch.object(client, "_submit_authorize_continue", return_value=add_phone_state) as submit_continue, \
mock.patch.object(client, "_state_supports_workspace_resolution", return_value=False), \
mock.patch.object(client, "_state_requires_navigation", return_value=False):
tokens = client.login_and_get_tokens(
"user@example.com",
"Secret123!",
"device-fixed",
prefer_passwordless_login=True,
allow_phone_verification=False,
skymail_client=mock.Mock(),
)
self.assertIsNone(tokens)
self.assertEqual(bootstrap.call_count, 2)
self.assertEqual(submit_continue.call_count, 2)
self.assertIn("未获取到 workspace / callback", client.last_error)
def test_send_passwordless_login_otp_does_not_send_email_field(self):
client = self._make_client()
response = mock.Mock()