Files
杨豪 cae37465d2 feat: 尺度精修上线,首次机器 PASS(live_160548 s=1.0375)
- method_l_shape: OK 候选尺度 ±0.0375 精修(步长 0.0125 + ±6px 窗口),
  GT live_165204 Δ 179→176(真人 177),离线 9/10 保持
- 插桩 JSON.stringify 捕获 SDK 日志明文;指针 probe 排除 coalesced/pressure
- 拖动不跟手指标作废:post_pos 晚于 SDK 重置滑块,鼠标事件流始终完整
- 剩余 VerifyErr = 候选整体选错(视觉确证),方向:多候选质量评估
- docs: experiments/solution/retrospective 同步收尾
2026-09-15 16:20:05 +08:00

158 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目复盘:抖音「拖动重叠」验证码离线求解与自动化通过
> 本文是整个项目的经验总复盘:任务、四个阶段的演进、决定性发现、
> 踩坑清单、方法论沉淀与未竟事项。细节以各专题文档为准
>(索引见 [README.md](README.md))。
## 1. 任务与最终状态
**任务**:给定验证码大图(552×344)与 mark 小图(110×110 RGBA),
离线求出 mark 应拖动重叠的目标区域偏移;进一步在浏览器里自动完成
「触发 → 采题 → 求解 → 拖动 → 通过」闭环。
**最终状态**:
| 层面 | 状态 |
| --- | --- |
| 离线求解(captchas/ 17 组样本) | ✅ 方向感知倒角法 10/10 精确命中,`src/solve.py` 交付 |
| 线上求解(URL 采题) | ✅ tip_y 锚定后 8 题 7 中(1 题差 1px 出容差) |
| 拖动位置换算 | ✅ 实测校准(按钮 1:1 带 mark,`x × 340/552`) |
| 拟人轨迹生成 | ✅ 三要素路径(非直线/非匀速/过冲回拉),`try/human_drag.py` |
| 行为风控(5014) | ✅ 定位关键指纹(CDP pressure=0)并修复(force=0.5) |
| 端到端一次通过 | ⏸ 被本机 IP 深度频控(5009)阻塞,需换环境冷启动验证 |
## 2. 四个阶段的演进
### 阶段一:离线求解方法调研(A → L)
从 RGB 模板匹配(A/A2)出发,历经 SIFT(B)、旋转 NCC(C)、倒角(D/E)、
轮廓匹配(F)、各类分割(G–K),共 12 条路线全部失败或受限后,
才确认两条**决定性事实**(详见 [findings](../try/findings.md)、[experiments](experiments.md)):
1. **mark 的 RGB 是"捐赠补丁"**——内部是场景纹理,与洞内内容无像素关系
(掩码 NCC 全图 ≤0.57),一切 RGB/灰度模板匹配原理上不可行;
2. **唯一可靠信号是 alpha 轮廓 + 尺度 + 旋转**——洞是叠加在照片上的实心
暗色覆盖物,形状=mark 的 alpha 轮廓,尺度≈1.0、旋转≈0;干扰项是同形的
缩放/旋转实例。
最终方案 L:方向感知倒角(9 法向桶 × 多尺度)+ 洞内部特征(边缘密度、
内-环亮度差)+ 局部旋转扫描,17 组样本 10/10 精确命中。
**教训**:12 条失败路线里没有一条是"实现 bug",全是**信号假设错误**。
早期就该做"掩码 NCC 上限"这类决定性实验先证伪 RGB 路线,而不是逐个换实现。
### 阶段二:浏览器触发与样本采集
- 抖音页面**预加载**验证码 iframe(display:none),「iframe 存在 ≠ 触发」,
判定要看 `offsetWidth > 0`;
- wasm/GPU 渲染让 `<img src>` 与截图法失效,但**图片 URL 会正常发 HTTP 请求**
→ 网络层抓 `captcha/get` 响应 + 下载 `url1/url2` 即得与离线同规格的样本对;
- 最可靠的触发法:密码登录弹窗(错误密码也必弹验证码);
- **tip_y 服务端真值**:`question.tip_y × 2 = 缺口顶 y`(mark 画布顶坐标系)。
### 阶段三:拖动闭环与行为风控(HOOK 路线)
闭环管线(`try/auto_solve_drag.py`):点刷新 → 采 get → solve →
拟人路径 → CDP 拖动 → 读 verify,get→verify 全程 <8s(challenge 有效期约 60–90s)。
拟人轨迹三要素(`try/human_drag.py`):贝塞尔弓形骨架+高斯抖动(非直线)、
三段速度结构(非匀速)、过冲后回拉瞄准。但最初 CDP 拖动仍全部 5014。
SDK 逆向(webmssdk/bdms/captcha.js 反混淆)确认:mousemove 入列规则、
X-Bogus 行为位(kMoveFast=16 等)、`isTrusted` 采集、captchaBody 在 XHR.send
前已 wasm 加密。**CDP 事件确实进入了 SDK 缓冲且 isTrusted=true**——问题不在
事件送达,在事件属性。
### 阶段四:决定性突破与 solve 增强
1. **pressure 指纹**:CDP 缺省产生的 MouseEvent `pressure=0`,W3C 规范里
真鼠标恒 0.5。三个鼠标事件全加 `force:0.5` 后,错误码从 5014(行为拒)
稳定变为 VerifyErr(位置判定被执行)——行为闸门通过。
2. **错误码语义分层**(多次连线实验归纳):
`5014 行为拒` → `VerifyErr 位置判了但错` → `5009 频控(压住一切)` →
`NotFoundChallengeId challenge 过期`。**任何改动的效果判定都必须在频控
冷却后做单次干净尝试**,连环重试的 5014/5009 是无效信号。
3. **拖动映射实测**:按钮 1:1 带动 mark 预览;mark 初始相对大图
`(0, tip_y×2)`(每题垂直自动对齐,拖动只控 x);拖动距离 = `solve.x × 340/552`。
4. **线上分布漂移与 tip_y 锚定**:线上 URL 题的覆盖物是**亮白半透明贴片**
(离线样本是暗色洞),方法 L 分数从 1–3 恶化到 3.6–7.7。实验发现亮度项
在亮贴片题上是反作用,而服务端免费下发的 `tip_y` 是硬真值——把它做成
y 锚(±8px)在候选中选精修分最低者:离线 10/10 不回归,线上 8 题 7 中,
3 个原来的错误/AMBIGUOUS 全部修正。已落地 `solve(big, mark, tip_y=...)`。
## 3. 踩坑清单(最有复用价值)
**图像算法**:
- mark RGB 与洞内容无关,别在 RGB 匹配上浪费时间——先用上限实验证伪信号;
- `TM_CCORR_NORMED` 带 mask 在边界产生 inf,要手写 filter2D 掩码 NCC;
- 倒角会被密纹理击败,必须加方向约束 + 洞内部特征;
- `np.arange` 浮点累积误差参与区间比较前先 `round(s,3)`。
**CDP / 浏览器自动化**:
- `Input.dispatchMouseEvent` 缺省 `pressure=0` 是硬指纹,拖动类风控必补 `force:0.5`;
- `button:"left"` 与 `buttons:0` 同时出现(矛盾状态)会毒化 SDK 拖动注册,
verify 根本不发出——无按键时用 `button:"none"`;
- 按下前不要发"带着 left 键的 hover",按下前后 `buttons` 状态要严格一致;
- 验证码「点刷新」不重建 iframe → iframe 内注入的 XHR hook 持续有效,
比 netcap 的 Network 域 attach(时机不稳)可靠得多;
- 验证通过后 iframe 自毁,判断通过与否最硬的信号是 `/json/list` 里 rmc iframe 消失。
**风控对抗**:
- 5009 频控阈值苛刻(几十次尝试/小时后窗口 >20 分钟),实验要"省着打",
低质量题(solve 分差)宁可跳过也不出手;
- 错误码会互相掩盖:频控期看到 5014 不代表行为判定就是这个结果。
## 4. 方法论沉淀
1. **先证伪信号,再写实现**:RGB 路线若有早期的"掩码 NCC 上限"实验,
方法 A–K 的大半工作量可以省掉;
2. **无真值时用交叉验证建锚**:SIFT 可用样本、bbox 与 alpha 尺寸一致性、
人工目检候选可视化;线上则直接用服务端 tip_y 当真值;
3. **服务端下发的一切字段都是潜在真值**(tip_y 是免费的 y 答案);
4. **错误响应是免费的判层探针**:5014/VerifyErr/5009 的分层让"行为关
过没过、位置关过没过"可独立观测;
5. **离线评估 ≠ 线上分布**:覆盖物样式一变(暗→亮),离线 10/10 的方法
也会跑飞,线上必须保留否决器(tip_y 一致性检查)。
## 5. 未竟事项
1. **端到端一次通过未闭环**:行为关(force=0.5)与 solve(tip_y 锚定)
都已各自验证,但本机 IP 已进深度频控,"冷启动单次拖动通过"需换环境
(新 IP / 无痕 profile)跑 `try/solo_attempt.py` 验证;
2. **AMBIGUOUS 题处理**:solve 置信度低的题目前只能跳过,可积累样本
校准"可信分数"阈值或引入第二方法仲裁;
3. **captchaBody 明文结构**:wasm 加密未破解,若 pressure 修复后仍被
行为层拒绝,需回退到 hook SDK 内部轨迹采集(wasm 前的 JS 层)注入轨迹。
## 6. 交付物与文档索引
| 交付物 | 位置 |
| --- | --- |
| 求解 API(含 tip_y 锚定) | `src/solve.py` |
| 核心算法(方法 L) | `src/method_l_shape.py` |
| 结果验证工具 | `src/verify_result.py` |
| 拟人路径生成 | `try/human_drag.py` |
| 自动闭环 / 单次尝试 | `try/auto_solve_drag.py` / `try/solo_attempt.py` |
| 网络层采集 | `try/captcha_net_capture.py` |
### 线上闭环最终状态(2026-05-16)
- **位置层**:flatness 方法经真人 GT 实证系统性偏差后禁用(`FLAT_DISABLED`),
tip_y 锚定 chamfer 为唯一选点路径(3 个可信 GT 全中);离线 9/10 保持;
- **输入层**:CDP 合成事件、事件属性指纹、xdotool OS 硬件级鼠标、真人轨迹
重放(含 0.7s 终点停顿修复)全部排除——不是根因;
- **触发层**:清 cookie 即解限流(无需等待)、错误正则去"验证码登录"误报、
清 cookie 后必须 Page.navigate 回站;
- **VerifyErr 收敛(2026-09-15 深夜)**:尺度精修上线后**首次机器 PASS**
(live_160548,s=1.0375 精修值,code=200「验证成功,正在为您登录」)。
社区资料确认 VerifyErr 语义 = 缺口距离错;剩余 VerifyErr 案例视觉确证为
候选整体选错(非小偏差)→ 后续方向是多候选质量评估 / 换题,而非拖动调参。
「拖动不跟手」指标作废(post_pos 读取晚于 SDK 重置滑块)。
专题文档:[solution.md](solution.md)(算法与 API)、
[experiments.md](experiments.md)(方法演进)、
[browser-automation/](browser-automation/)(触发、采集、拖动、CDP 经验)。