Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions src/autoteam/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,13 +386,14 @@ def _record_auth_repair_failure(
error_detail: str | None = None,
*,
chatgpt_api=None,
release_team_seat: bool = False,
) -> dict:
now = time.time()
acc = find_account(load_accounts(), email) or {"email": email}
error_type = error_type or "login_failed"
error_detail = error_detail or _auth_repair_error_label(error_type)
retry_delays = _auth_repair_retry_delays()
release_team_seat = False
should_release_team_seat = bool(release_team_seat)

if error_type == "add_phone" and _auth_repair_retry_add_phone_enabled():
prev_count = int(acc.get("auth_retry_count") or 0) if acc.get("auth_last_error") == "add_phone" else 0
Expand All @@ -409,7 +410,7 @@ def _record_auth_repair_failure(
"auth_retry_after": None,
"auth_retry_paused": True,
}
release_team_seat = True
should_release_team_seat = True
else:
state = {
"auth_retry_count": next_count,
Expand Down Expand Up @@ -452,7 +453,7 @@ def _record_auth_repair_failure(
release_attempted = False
remove_status = None
seat_released = False
if release_team_seat and is_team_member:
if should_release_team_seat and is_team_member:
release_attempted = True
remove_status = _release_auth_repair_team_seat(email, chatgpt_api=chatgpt_api)
seat_released = remove_status in ("removed", "already_absent")
Expand Down Expand Up @@ -1349,7 +1350,12 @@ def _complete_registration(email, password, invite_link, mail_client):
logger.info("[注册] 账号就绪: %s", email)
return email
else:
result = _record_auth_repair_failure(email, login_result.get("error_type"), login_result.get("error_detail"))
result = _record_auth_repair_failure(
email,
login_result.get("error_type"),
login_result.get("error_detail"),
release_team_seat=True,
)
extra = _auth_repair_result_suffix(result)
logger.warning(
"[注册] 账号已加入 Team 但 Codex 登录失败,标记为 %s: %s(%s%s)",
Expand All @@ -1358,7 +1364,7 @@ def _complete_registration(email, password, invite_link, mail_client):
_auth_repair_error_label(result.get("auth_last_error")),
extra,
)
return email
return None


def _check_pending_invites(chatgpt_api, mail_client):
Expand Down Expand Up @@ -1671,6 +1677,8 @@ def _detect_direct_register_step(page):
url = (page.url or "").lower()
if _is_google_redirect(page):
return "google"
if "/api/auth/error" in url or url.endswith("/auth/error"):
return "error"

if "email-verification" in url:
return "code"
Expand Down Expand Up @@ -1716,6 +1724,8 @@ def _wait_for_direct_register_step(page, allowed_steps, timeout=15):
deadline = time.time() + timeout
while time.time() < deadline:
step = _detect_direct_register_step(page)
if step == "error":
return step
if step in allowed_steps:
return step
time.sleep(0.5)
Expand Down Expand Up @@ -1981,6 +1991,14 @@ def _register_direct_once(
logger.warning("[直接注册] 邮箱步骤仍停留在 Google 登录页")
browser.close()
return False
if current_step == "error":
logger.warning("[直接注册] 邮箱步骤进入认证错误页 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False
if current_step == "unknown":
logger.warning("[直接注册] 邮箱步骤进入未知状态 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False
if current_step == "email":
logger.warning("[直接注册] 邮箱步骤未推进 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
Expand All @@ -1994,6 +2012,14 @@ def _register_direct_once(
)
logger.info("[直接注册] 密码页检测状态: %s | URL: %s", password_step, page.url)
_safe_invite_screenshot(page, "direct_03b_before_password.png")
if password_step == "error":
logger.warning("[直接注册] 密码步骤进入认证错误页 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False
if password_step == "unknown":
logger.warning("[直接注册] 无法识别密码/验证码步骤 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False

try:
for attempt in range(2):
Expand Down Expand Up @@ -2042,6 +2068,14 @@ def _register_direct_once(
logger.warning("[直接注册] 密码步骤仍停留在 Google 登录页")
browser.close()
return False
if current_step == "error":
logger.warning("[直接注册] 密码步骤进入认证错误页 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False
if current_step == "unknown":
logger.warning("[直接注册] 密码步骤进入未知状态 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
return False
if current_step == "email":
logger.warning("[直接注册] 提交密码前流程回退到邮箱页 | URL: %s | body=%s", page.url, _page_excerpt(page))
browser.close()
Expand Down Expand Up @@ -2179,7 +2213,12 @@ def create_account_direct(mail_client):
logger.info("[直接注册] 账号就绪: %s", email)
return email
else:
result = _record_auth_repair_failure(email, login_result.get("error_type"), login_result.get("error_detail"))
result = _record_auth_repair_failure(
email,
login_result.get("error_type"),
login_result.get("error_detail"),
release_team_seat=True,
)
extra = _auth_repair_result_suffix(result)
logger.warning(
"[直接注册] 账号已加入 Team 但 Codex 登录失败,标记为 %s: %s(%s%s)",
Expand All @@ -2188,7 +2227,7 @@ def create_account_direct(mail_client):
_auth_repair_error_label(result.get("auth_last_error")),
extra,
)
return email
return None


def create_new_account(chatgpt_api, mail_client):
Expand Down Expand Up @@ -2233,6 +2272,7 @@ def reinvite_account(chatgpt_api, mail_client, acc):
login_result.get("error_type"),
login_result.get("error_detail"),
chatgpt_api=chatgpt_api,
release_team_seat=True,
)
extra = _auth_repair_result_suffix(result)
logger.warning(
Expand All @@ -2252,6 +2292,7 @@ def reinvite_account(chatgpt_api, mail_client, acc):
"non_team_plan",
f"登录后 plan={plan_type or 'unknown'}",
chatgpt_api=chatgpt_api,
release_team_seat=True,
)
logger.warning("[轮转] 旧账号保持状态为 %s: %s", result.get("status"), email)
return False
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_manager_auth_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,45 @@ def test_record_auth_repair_failure_releases_team_seat_after_add_phone_retries_e
]


def test_record_auth_repair_failure_can_force_release_team_seat_for_rejoin_failures(monkeypatch):
updates = []
monkeypatch.setattr(
manager,
"load_accounts",
lambda: [{"email": "user@example.com", "status": "standby", "auth_retry_count": 0}],
)
monkeypatch.setattr(manager, "update_account", lambda email, **kwargs: updates.append((email, kwargs)))
monkeypatch.setattr(manager.time, "time", lambda: 1_700_000_000)
monkeypatch.setattr(manager, "_auth_repair_retry_delays", lambda: (600, 1200, 1800))
monkeypatch.setattr(manager, "_is_email_in_team", lambda _email: True)
monkeypatch.setattr(manager, "_release_auth_repair_team_seat", lambda *_args, **_kwargs: "removed")

state = manager._record_auth_repair_failure(
"user@example.com",
"auth_code_missing",
"未获取到 auth code",
release_team_seat=True,
)

assert state["auth_retry_count"] == 1
assert state["status"] == "standby"
assert state["seat_released"] is True
assert updates == [
(
"user@example.com",
{
"auth_retry_count": 1,
"auth_last_error": "auth_code_missing",
"auth_last_error_detail": "未获取到 auth code",
"auth_last_failed_at": 1_700_000_000,
"auth_retry_after": 1_700_000_600,
"auth_retry_paused": False,
},
),
("user@example.com", {"status": "standby"}),
]


def test_login_codex_with_result_retries_retryable_failures_within_same_round(monkeypatch):
attempts = {"count": 0}

Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_manager_reinvite.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,26 @@ def test_reinvite_account_marks_auth_pending_when_oauth_login_fails_but_team_sea
)

assert result is False


def test_reinvite_account_requests_team_seat_release_when_oauth_fails_after_rejoin(monkeypatch):
captured = {}

monkeypatch.setattr(manager, "login_codex_via_browser", lambda *args, **kwargs: None)

def fake_record(*args, **kwargs):
captured["args"] = args
captured["kwargs"] = kwargs
return {"status": accounts.STATUS_STANDBY, "auth_last_error": "auth_code_missing", "seat_released": True}

monkeypatch.setattr(manager, "_record_auth_repair_failure", fake_record)

result = manager.reinvite_account(
types.SimpleNamespace(browser=False),
None,
{"email": "tmp-user@example.com", "password": ""},
)

assert result is False
assert captured["args"][:3] == ("tmp-user@example.com", "login_failed", "登录失败")
assert captured["kwargs"]["release_team_seat"] is True
Loading
Loading