先说结论在自动化里遇到验证码/二次验证/账号锁定就自动重试是把一次验证升级成账号被挂起的最经典路径。这不是道德问题是概率问题。风控看的是行为模式一个正常用户遇到验证码会停顿、会换网络、会手动输入一个脚本遇到验证码会在 30 秒内重复请求三次每次的间隔精确到毫秒。## 错误写法长什么样pythonfor attempt in range(3): page.goto(url) if page.locator(#captcha).count(): time.sleep(2) continue # ← 问题在这一行 breakcontinue 看起来无害。但它表达的是这是可重试的临时故障而验证码**不是**临时故障——它是一个**需要人介入的终止条件**。## 我的写法把需要人建模成正常分支在状态枚举上直接给闸门一个判定pythonpropertydef is_gate(self) - bool: return self in (SessionState.NEEDS_2FA, SessionState.NEEDS_CAPTCHA, SessionState.BLOCKED)然后**重试动作单独做一个枚举交给人是一等公民**pythonclass RetryAction(str, Enum): NONE “none” # 当前步原样重试 RELOGIN “relogin” # 重新走登录 RESTART_ENV “restart_env” # 关掉浏览器重开一套干净环境 ESCALATE_HUMAN “escalate_human” # 交给人停止自动操作关键在于 ESCALATE_HUMAN **和 NONE 一样是正常返回值**而不是一个 throw 出去的异常。为什么这点重要因为异常会被外层的兜底 except 吃掉——而兜底逻辑通常就是重试。**把需要人写成异常等于把它交给了最不该处理它的那一层。**## 判定顺序也得配合闸门必须在登录标记**之前**判定。原因很反直觉**验证码和 OTP 是以浮层形式出现在已经登录的页面之上的。**python # ---- 硬闸门优先级最高 ---- if Marker.ACCOUNT_LOCKED in probe.markers: return Verdict(SessionState.BLOCKED, 0.95, …) if Marker.CAPTCHA in probe.markers: return Verdict(SessionState.NEEDS_CAPTCHA, 0.95, …) if Marker.OTP_INPUT in probe.markers: return Verdict(SessionState.NEEDS_2FA, 0.95, …) # ---- 登录态排在闸门之后 ---- if probe.has(Marker.SELLER_NAV): return Verdict(SessionState.LOGGED_IN, …)顺序反过来会怎样页面上的导航菜单还在因为**底层确实还是已登录状态**于是判成 LOGGED_IN闸门被完全漏掉。脚本高高兴兴地继续采集然后在一个永远不会前进的页面上超时。**这个 bug 在本地 mock 上是复现不出来的**只有真实站点才会暴露。## 还有一个语义坑HTTP 码别一律当页面坏了python# 这些 HTTP 码在亚马逊后台语境的含义是对方暂时不想理你不是页面坏了。# 当成网络错误处理退避重开环境而不是硬刷。_TRANSIENT_HTTP frozenset({0, 408, 429, 500, 502, 503, 504})429限流和 503 的正确反应是**退避 重开干净环境**而不是选择器变了我去改代码或者再刷一次。把限流误判成结构变化会让你去修一个根本没坏的东西。## 判定结果要带上置信度和证据判定结果不是布尔值而是带 **confidence** 和 **evidence** 的结构pythondataclass(frozenTrue, slotsTrue)class Verdict: state: SessionState confidence: float evidence: tuple[str, …] flags: frozenset[Flag] frozenset()信息不足时退化到 URL 推断并且**把置信度明确压低**python # ---- 退化到 URL 推断置信度明确压低别假装确定 ---- path probe.url.split(“?”, 1)[0] if “/ap/signin” in path: return Verdict(SessionState.LOGGED_OUT, 0.6, (“URL 落在 /ap/signin”,), …) flags.add(Flag.PARTIAL_RENDER) return Verdict(SessionState.UNKNOWN, 0.3, (“未命中任何标记”,), …)一个能报出我不确定conf0.30原因是没命中任何标记“的系统比一个永远返回布尔值的系统好 debug一个数量级——因为前者把排查线索留下来了。## 交付物要能证明这件事这类设计最容易出现的退步是代码里写了闸门但没人测它”。我的做法是把闸门判成转人工这件事写成端到端用例demo命令的最后一步就是验证验证码闸门被判成转人工。166 项测试里包含真开 Chromium 打本地 mock 后台的端到端——闸门行为有回归保护不是靠注释承诺。## 完整代码项目开源在这里测试可以自己跑https://github.com/Amouren7/amz-harnessPython Playwright只做只读采集。README 开头写了 2026-03-04 起亚马逊新协议下哪些能做、哪些不能做——尤其是多店铺代管这类形态已经被明确盯上做之前值得先看一眼。