Spaces:
Paused
Paused
| """注册失败明细日志(持久化到 register_failures.json)。 | |
| 用户要求:失败账号不能污染账号列表,但失败原因必须能追溯 —— 比如 add-phone 触发了几次、 | |
| 哪些临时邮箱在 OAuth 阶段挂了、哪些被判 duplicate。本模块单独存这类明细,不与 accounts.json 混。 | |
| 记录只保留最近 N 条(RECORD_LIMIT),避免长期运行后文件膨胀。 | |
| 并发:`_cmd_fill_personal` 等任务跑在 ThreadPoolExecutor 里,多个 worker 会同时命中 | |
| record_failure —— 无锁的读-改-写会互相覆盖导致丢记录。全部写入走 _LOCK 串行化。 | |
| """ | |
| import json | |
| import logging | |
| import os | |
| import threading | |
| import time | |
| from pathlib import Path | |
| from autoteam.textio import read_text, write_text | |
| logger = logging.getLogger(__name__) | |
| PROJECT_ROOT = Path(__file__).parent.parent.parent | |
| FAILURES_FILE = PROJECT_ROOT / "register_failures.json" | |
| FAILURES_FILE_MODE = 0o666 | |
| RECORD_LIMIT = 500 | |
| _LOCK = threading.Lock() | |
| # Round 8 — Master Team 订阅降级 + OAuth Personal Workspace 显式选择 | |
| # 详见 prompts/0426/spec/shared/master-subscription-health.md §7 + oauth-workspace-selection.md §2.3 | |
| MASTER_SUBSCRIPTION_DEGRADED = "master_subscription_degraded" | |
| """母号 ChatGPT Team 订阅 cancel(eligible_for_auto_reactivation=true)。M-T1 / M-T2 fail-fast。""" | |
| OAUTH_WS_NO_PERSONAL = "oauth_workspace_select_no_personal" | |
| """workspaces[] 中找不到 personal 项 — user 在后端事实上只属于 Team。fail-fast,不重试。""" | |
| OAUTH_WS_ENDPOINT_ERROR = "oauth_workspace_select_endpoint_error" | |
| """POST /api/accounts/workspace/select 4xx/5xx 且 UI fallback 失败。端点变更 / 反爬 / DOM 漂移。""" | |
| OAUTH_PLAN_DRIFT_PERSISTENT = "oauth_plan_drift_persistent" | |
| """workspace/select 成功但 5 次 OAuth retry 后 bundle.plan_type 仍非 free — 后端最终一致性失败。""" | |
| # Round 12 S4 — 注册收尾双路径:mail provider rotation 触发分类(详见 | |
| # `src/autoteam/mail/register_dual_path.py`)。三类失败会触发 RegisterPathRotator | |
| # 切换到下一个 provider 重试,失败明细写入 register_failures.json,**extra 中带 | |
| # provider_chain_history 字段记录每个 provider 的尝试结果。 | |
| MAIL_OTP_TIMEOUT = "mail_otp_timeout" | |
| """注册时等待 OTP / 验证邮件超时 — 多见于 alias forwarding 链路转发延迟或 reader 失联。""" | |
| MAIL_INVITE_LINK_MISSING = "mail_invite_link_missing" | |
| """邀请邮件已到但 extract_invite_link 抓不到链接 — 邮件模板变体 / HTML 损坏 / sender 异常。""" | |
| MAIL_DOMAIN_REJECTED = "mail_domain_rejected" | |
| """OpenAI 拒绝当前 mail provider 的域名(disposable / not allowed) — 触发整体切到下一 provider。""" | |
| def _load(): | |
| if not FAILURES_FILE.exists(): | |
| return [] | |
| try: | |
| raw = read_text(FAILURES_FILE).strip() | |
| if not raw: | |
| return [] | |
| data = json.loads(raw) | |
| return data if isinstance(data, list) else [] | |
| except Exception as exc: | |
| # 不静默吞:文件损坏时保留原件便于人工排查,返回空让新一轮写入继续。 | |
| corrupt_path = FAILURES_FILE.with_suffix(f".corrupt-{int(time.time())}.json") | |
| try: | |
| FAILURES_FILE.rename(corrupt_path) | |
| logger.error("[register_failures] 解析失败, 已保留原文件为 %s: %s", corrupt_path.name, exc) | |
| except Exception as rename_exc: | |
| logger.error("[register_failures] 解析失败且无法重命名 (%s): %s", exc, rename_exc) | |
| return [] | |
| def _save(records): | |
| records = records[-RECORD_LIMIT:] | |
| target = FAILURES_FILE.resolve() | |
| write_text(target, json.dumps(records, indent=2, ensure_ascii=False)) | |
| try: | |
| os.chmod(target, FAILURES_FILE_MODE) | |
| except Exception: | |
| pass | |
| def record_failure(email, category, reason="", **extra): | |
| """追加一条失败记录。 | |
| category(原有): | |
| 'phone_blocked' / 'duplicate_exhausted' / 'register_failed' / 'oauth_failed' | |
| / 'kick_failed' / 'team_oauth_failed' / 'exception' | |
| category(SPEC-2 新增 — 注册/邀请生命周期相关): | |
| 'oauth_phone_blocked' OAuth 阶段触发 add-phone / duplicate(invite.RegisterBlocked) | |
| 'plan_unsupported' plan_type 不在白名单(team/free/plus/pro) | |
| 'no_quota_assigned' OAuth 成功但后端没发配额(primary_total=0 或 rate_limit 全空) | |
| 'plan_drift' reinvite 后 plan_type 漂移到非 team | |
| 'auth_error_at_oauth' post-register quota 探测返回 401/403 | |
| 'quota_probe_network_error' post-register quota 探测网络错误(允许下次重试) | |
| category(Round 8 SPEC-2 v1.5 新增 — Master 订阅降级 / OAuth Workspace 显式选择): | |
| 'master_subscription_degraded' 母号 Team 订阅已 cancel(M-T1 / M-T2 fail-fast) | |
| 'oauth_workspace_select_no_personal' OAuth session workspaces[] 中无 personal 项 | |
| 'oauth_workspace_select_endpoint_error' workspace/select 端点 + UI fallback 都失败 | |
| 'oauth_plan_drift_persistent' 5 次 OAuth retry 后 bundle.plan 仍非 free | |
| category(Round 12 S4 新增 — 注册双路径 mail provider rotation): | |
| 'mail_otp_timeout' OTP/验证邮件等待超时 → 切下一 provider | |
| 'mail_invite_link_missing' 邀请链接抓不到 → 切下一 provider | |
| 'mail_domain_rejected' OpenAI 拒绝当前 provider 域名 → 切下一 provider | |
| reason: 面向人的简短描述,显示在日志和面板。可留空,从 extra.detail 取代。 | |
| extra: 任意附加字段(attempts, duplicate_swaps, step, url, stage, detail ...) | |
| Round 12 S4 约定 extra 中可带: | |
| provider_chain_history: list[{provider, error_type, ts[, error]}] | |
| 旧记录无此字段,读路径用 `r.get("provider_chain_history", [])` 兼容。 | |
| """ | |
| with _LOCK: | |
| records = _load() | |
| records.append( | |
| { | |
| "timestamp": time.time(), | |
| "email": email or "", | |
| "category": category, | |
| "reason": reason or extra.get("detail", "") or "", | |
| **extra, | |
| } | |
| ) | |
| _save(records) | |
| def list_failures(limit=50): | |
| with _LOCK: | |
| records = _load() | |
| return records[-limit:][::-1] | |
| def count_by_category(since_ts=0): | |
| with _LOCK: | |
| records = _load() | |
| counts = {} | |
| for r in records: | |
| if r.get("timestamp", 0) < since_ts: | |
| continue | |
| cat = r.get("category", "unknown") | |
| counts[cat] = counts.get(cat, 0) + 1 | |
| return counts | |