"""登录失败分型:把「失败消息」映射成稳定的型别。 ## 为什么要单独一个模块 这个分类原先只活在 `automation/jobs/ck_relogin.py`(自动重登作业)里。但 **手动/批量登录**同样需要它: 1. 账号时间线要写「登录失败」事件,写的时候得带型别,否则以后想筛 「验证码失败的账号」只能去猜中文; 2. 导出 CSV 要给运营一列可直接筛选的型别。 放 service 而不是留在 job 里,是沿用仓库已有的分层决定 (`MANUAL_KINDS` 当初就是从 job 挪到 `account_lifecycle.py` 的, 注释写着「service 不该反过来依赖 job」)。 ## 一条重要推论:`interrupted` ≠ 失败 分型把「已停止 / 被中断 / 服务重启」都归到 `interrupted`。**这不是账号的失败**: 调用方(`_mark_attempt_failed`)据此决定**不往账号时间线写失败事件** —— 否则运营按一次停止就能把几十个账号的时间线刷满假失败。 """ from __future__ import annotations #: 型别 → 中文标签(导出 CSV 与页面上用同一份,避免两处各写一遍漂移)。 FAILURE_KIND_LABELS: dict[str, str] = { "captcha": "极验/验证码拦截", "credential": "账号凭证问题", "email_code": "邮箱取码失败", "email_quota": "邮箱配额已满", "network": "网络/未知异常", "interrupted": "被中断(停止/重启)", } #: 不写进账号时间线的型别:这些是**人的动作或进程重启**,不是账号状况。 NOT_AN_ACCOUNT_FAULT: frozenset[str] = frozenset({"interrupted"}) def classify_failure(message: str | None) -> str: """把重登/登录失败消息映射成分型(见规划 §7.4.3)。 顺序很重要:「邮箱验证码发送次数已达上限」既含「验证码」也含「上限」, 必须先在 ``email_quota`` 命中,否则会被归到可重试的 ``email_code`` 上, 导致次日才该重试的账号被按小时反复撞配额。 """ text = message or "" if "上限" in text or "配额" in text: return "email_quota" if "极验" in text or "验证" in text and "滑块" in text: return "captcha" if "验证码" in text: return "email_code" if "中断" in text or "已停止" in text or "任务已停止" in text: return "interrupted" if "密码" in text or "账号不存在" in text or "缺少密码或邮箱" in text: return "credential" # 请求失败/超时/连接异常,以及未知文案:都按可重试处理。 # 宁可多给一次机会(有每日上限兜底),也不要把可能只是抖动的账号推给人工。 return "network" def is_account_fault(kind: str | None) -> bool: """这个型别算不算「账号出了问题」——不算就不该写进账号一生。""" return (kind or "") not in NOT_AN_ACCOUNT_FAULT def failure_kind_label(kind: str | None) -> str: return FAILURE_KIND_LABELS.get(kind or "", "")