Files
hello_js_reverse_skill/har/1ms_dianxuan/ANALYSIS.md
T
yml2213 e2485c5ea6 docs(账本): 记录 H3 单变量实验结论(captcha 域 cookie 与 errorCode=12 无关)
做了什么:
- ANALYSIS.md §7.1 新增 E8: captcha 域"带/不带"腾讯云登录 cookie, 单变量 A/B 均为 12
- ANALYSIS.md §7.4 假设表更新: H3 标注为已排除, 只剩 H1/H2
- ANALYSIS.md §4.3 更正: extract_eks 改通用匹配后实测 248 字符, 与真实浏览器一致
- PROGRESS.md §三 已排除表新增 H3 一行

为什么:
- 彻底关闭"换 cookie/cookie 同源"这条猜测路线, 避免重复投入

怎么验证:
- 单变量 A/B: scripts/experiment_h3_cookie_ab.py --both  -> A=12, B=12
  (同一次运行, 题目 2404 求解 posterior=1.000 全对, collect_len=1856)
2026-10-11 21:47:27 +08:00

464 lines
25 KiB
Markdown
Raw 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.
# 毫秒 1ms.run 腾讯天御「文字点选」协议分析
分析样本:`/Users/yml/Desktop/抓包/毫秒/点选.har`(20 条记录,WebInspector 导出)
分析时间:2026-10-11
目标题型:`subcapclass = 2404`(腾讯天御 **文字点选**,提示语为「请依次点击:甲 乙 丙」)
---
## 一、请求链
抓包中与验证码相关的记录共 12 条,可还原为 **1 个业务侧引导请求 + 11 个腾讯验证码请求**:
```
① POST https://1ms.run/api/v1/captcha/params ← 业务侧:取验证码配置
body: {"scene":"checkin","identity_context":"emctx1.<b64>.<b64>"}
resp: {"code":0,"data":{"enabled":true,"provider":"tencent",
"params":{"captchaAppId":195672344},
"sessionUuid":"01a12a04-e5e7-726b-b00c-2f54b2e1c167"}}
② GET https://turing.captcha.qcloud.com/cap_union_prehandle ← 建立会话 + 首题
?aid=195672344&protocol=https&accver=1&showtype=popup&ua=<base64 UA>
&noheader=1&fb=1&aged=0&enableAged=0&enableDarkMode=0&grayscale=1
&clientype=2&cap_cd=&uid=&lang=zh-cn
&entry_url=https%3A%2F%2F1ms.run%2Fconsole%2Fpersonal%2Fbenefits%2Fcheck-in
&elder_captcha=0&js=%2Ftcaptcha-frame.ba387dd1.js&login_appid=
&wb=1&tkid=407017912&subsid=1&callback=_aq_260580&sess=
resp: _aq_260580({... "capclass":"1","subcapclass":"2404","sess":"s0Rr8i01...",
"sid":"1793111442254233600",
"data":{"comm_captcha_cfg":{"tdc_path":"/tdc.js?app_data=<sid>&t=<rand>",
"pow_cfg":{"prefix":"a79f541e7c1ca571#",
"md5":"0264f7a4329e2b6f9f9076c53294c277"}},
"dyn_show_info":{"instruction":"请依次点击:虫 呈 般 ",
"bg_elem_cfg":{"size_2d":[672,480],
"click_cfg":{"mark_style":"inc_number",
"data_type":["DynAnswerType_POS"]},
"img_url":"/cap_union_new_getcapbysig?img_index=1&image=...&sess=..."}}}})
③ GET .../tdc.js?app_data=1793111442254233600&t=1309148276 ← 会话专属混淆脚本
④ GET .../cap_union_new_getcapbysig?img_index=1&image=...&sess=... ← 672x480 背景图
── 第 1 次作答 ──
⑤ POST .../cap_union_new_verify (ans + collect + eks + sess + pow_*) → errorCode 50
⑥ POST .../cap_union_new_getsig (sess + tkid) → 新 sess + 新题
⑦ GET .../cap_union_new_getcapbysig?... 第 2 张图
── 第 2 次作答 ── ⑧ verify → errorCode 9
⑨ getsig ⑩ getcapbysig 第 3 张图
── 第 3 次作答 ── ⑪ verify → errorCode 50
⑫ getsig ⑬ getcapbysig 第 4 张图(抓包结束时未提交)
```
> 抓包中的 `y.clarity.ms/collect`(微软 Clarity 埋点)、`collector.entmesh.mliev.com`
> 与验证码无关,属于站点自身的监控上报,本文不纳入实现。
---
## 二、会话状态机(关键!错一个就永远失败)
`sess` 在三处出现,且**互不相同**,必须严格区分:
| 位置 | 说明 |
| --- | --- |
| `prehandle` 响应体的 `sess` | **会话主 sess**,首次 `verify` 必须用它 |
| `dyn_show_info.bg_elem_cfg.img_url` 里的 `sess=` | 只作用于**取图**,与 verify 用的 sess 不是同一个 |
| `verify` 响应的 `sess` | 上一轮失败后的**新会话 sess** |
| `getsig` 请求体 `sess` | 用的是上一步 `verify` 响应里的新 sess(不是会话主 sess) |
| `getsig` 响应体的 `sess` | 下一轮的会话 sess,下一轮 verify 用它 |
实测(`点选.har`):
```
prehandle.sess = s0Rr8i01...(482) ← verify#1 用它
verify#1 resp.sess = s0TMSpmIpMCH3...(482) ← getsig#1 用它
getsig#1 resp.sess = s0VSknLJwQHTwRC...(494) ← verify#2 用它,同时也是该轮 img_url 的 sess
verify#2 resp.sess = s05b9OQJg2z6nx...(494) ← getsig#2 用它
getsig#2 resp.sess = s0nrIxkexDWO0Rgd...(482) ← verify#3 用它
```
**题型在会话内固定**:对同一会话连续 `getsig`,返回的 `subcapclass` 不变(实测滑块会话
连发 8 次 getsig 全是滑块)。想换题型只能**重新 `prehandle` 开新会话**。
---
## 三、参数分类
### 3.1 固定/常量参数
| 参数 | 值 | 说明 |
| --- | --- | --- |
| `protocol` | `https` | 固定 |
| `accver` | `1` | 协议版本 |
| `showtype` | `popup` | 弹窗模式 |
| `noheader` / `fb` | `1` / `1` | 固定 |
| `aged` / `enableAged` / `enableDarkMode` | `0` | 固定 |
| `grayscale` | `1` | 灰度模式开启 |
| `clientype` | `2` | PC 端 |
| `lang` | `zh-cn` | 语言 |
| `js` | `/tcaptcha-frame.ba387dd1.js` | 验证码 frame 脚本名 |
| `wb` | `1` | 1ms.run 侧取值(雨云为 2) |
| `subsid` | `1` | 抓包中为 1(雨云实现里随机 1~5,实测不影响) |
### 3.2 每会话动态参数
| 参数 | 生成规律 |
| --- | --- |
| `aid` | 业务侧下发:`1ms.run/api/v1/captcha/params` → `captchaAppId = 195672344` |
| `tkid` | 0~9 数字串,9 位;**同一会话内所有请求复用同一个值**(抓包:407017912) |
| `callback` | `_aq_` + 6 位随机数;仅 `prehandle` 使用(JSONP) |
| `entry_url` | 触发页 URL(1ms 为签到页),URL-encoded |
| `ua` | 请求 UA 的 base64(抓包解码 = `Chrome/155.0.0.0` on macOS) |
| `sid` / `app_data` | `prehandle` 响应返回,仅用于拼 `tdc_path` |
| `sess` | `prehandle` / `getsig` / `verify` 三级传递,见第二节 |
| `image` | 服务端下发在 `img_url` 中的图片令牌,逐题变化 |
### 3.3 动态参数变化规律(多题对比)
| 题号 | `image` | `instruction` | `ans` | `pow_answer` | `pow_calc_time` |
| --- | --- | --- | --- | --- | --- |
| 1 | `026409000000001300000015aa3f011427ce` | 请依次点击:虫 呈 般 | `162,249` / `104,37` / `561,156` | `a79f541e7c1ca571#16447` | 46 |
| 2 | `026409000090e146000000159f1391142763` | 请依次点击:佰 衬 稗 | `426,47` / `332,156` / `517,332` | 同题 1 | 46 |
| 3 | `0264090000857147000000159f13911427e7` | 请依次点击:簿 蚕 沧 | `498,136` / `45,118` / `252,146` | 同题 1 | 46 |
结论:
* **`pow_answer` / `pow_calc_time` 在整个会话内恒定** —— 它来自 `prehandle` 的 `pow_cfg`,
是一次性的工作量证明,而不是每轮刷新。
* **`collect` / `eks` 在整个会话内恒定**(三轮 verify 的 `collect` 长度都是 1784 且字节一致)
—— 它们由 `tdc.js` 在页面初始化时一次性生成,之后每次 verify 复用,**不随后续点击坐标变化**。
* `ans` 逐题变化,且坐标随图片令牌变化,**不存在可复用规律**。
---
## 四、签名 / 加密参数分析
### 4.1 `pow_answer` —— MD5 工作量证明(已完全还原)
服务端给 `pow_cfg = {"prefix": "a79f541e7c1ca571#", "md5": "0264f7a4329e2b6f9f9076c53294c277"}`。
反推验证:
```
md5("a79f541e7c1ca571#16447") = 0264f7a4329e2b6f9f9076c53294c277 ✅ 命中
```
即 `pow_answer = prefix + str(i)`,其中 `i` 从 0 递增直到 MD5 命中;`pow_calc_time`
是浏览器自己测量的耗时(抓包 46ms,本机 Python 约 120~190ms)。
### 4.2 `collect` —— 自定义 XTEA 分组加密 + 行为轨迹(结构与编码已还原)
`collect` 是 URL-encoded 的 Base64,**不是** 4 段分别 base64 后拼接的产物:
| 来源 | Base64 长度 | 解码字节数 | 内部 `=` |
| --- | --- | --- | --- |
| 点选.har 真实浏览器 | 1784 | 1336 = 48 + **768** + 504 + 16 | 仅末位 `==` |
| 本仓库 Node 沙箱执行真实 tdc.js | 1654 | 1168 = 48 + **600** + 504 + 16 | 仅末位 `==` |
| 本仓库纯 Python 还原 | 1712~1822 | 随轨迹长度浮动 | **块内出现 `=`** ❌ |
明文结构(按 8 字节分组、32 轮自定义 XTEA 加密,密钥 4 个 32bit word):
```
[0 .. 48) cd 前缀 {"cd":[[],"1920-1080-1055-24-*-*-|-*",0,0,"", (空格补齐)
[48 .. 816) 鼠标轨迹 [[x0,y0,2],[dx1,dy1,dt1],...] 长度必须是 24 的倍数
[816 .. 1320) cd 后缀 画布指纹/UA/平台/分辨率/时区/语言/…
[1320 .. 1336) sd 结尾 "sd":{"od":"C"}}
```
**已修掉的实现缺陷**:原 `generate_trajectory_payload()` 把轨迹补齐到 8 的倍数,
导致分块 base64 后出现**块内 `=` 填充**(真实样本只有末尾一个 `==`)。已改为补齐到
**24(= lcm(8,3))的倍数**,与真实样本结构一致。
**已定论(2026-10-11 实测修正)**:`DEFAULT_KEY = [1448563793, 1847730015, 1733184827, 1364613714]`
以及两个轮加盐 `2829068 / 328986`,是**当时逆向的那一份 tdc.js 构建**里的密码学参数。
实测三份脚本(每份都先跑真实 tdc.js 拿到 collect,再尝试解密):
| tdc.js 来源 | 轮加盐(脚本内联值) | 用 DEFAULT_KEY + 该加盐解密 |
| --- | --- | --- |
| `tcaptcha/tdc_sample.js`(雨云样本) | `2829068 / 328986` | ✅ 明文 `{"cd":[[],"1920-1080..."` |
| 雨云 aid=2039519451 **当天实时** | `394753 / 657930` | ❌ 随机字节 |
| 1ms aid=195672344 当天实时 | `197386 / 1513230` | ❌ 随机字节 |
结论:**XTEA 的 4 个密钥字与 2 个轮加盐都是"每次构建随机"的,且不是可 grep 的字面量**
(它们藏在 JSVMP 的操作数流里,DELTA `2654435769` 与 `DELTA*32 = 84941944608` 是唯一可见的常量)。
所以「纯 Python 硬编码密钥」本质上不是算法还原,而是**单份脚本的参数固化**。
**但关键反证**:服务端**并不按 tdc.js 的会话密钥去解密 `collect`**。实测雨云 aid 用
纯 Python(硬编码密钥)的 collect 提交正确答案 → `errorCode=0` + 真实 ticket。
因此参数随机化**不构成阻塞**,纯 Python 交付形态依然成立(默认 `telemetry_mode="python"`)。
node / browser 后端保留为「目标 aid 风控更严格」时的备选。
### 4.3 `eks` —— 会话密钥信封
`eks` 由 tdc.js 内部 `TDC.getInfo().info` 产出,形如 Base64 的 236~248 字符
(= 176~184 字节),推测是用服务端公钥包裹的会话密钥,服务端用它解密 `collect`。
* 早期用写死变量名的正则(`window.KQnlRHY...`)提取,得到 236 字符,与真实浏览器的 248 不一致。
现已改为**通用**匹配(任取最长的 `window.<随机名>='<base64>'`),在 2026-10-11 的实时构建上
稳定取到 **248 字符**,与真实浏览器观测一致(见 E8 那次运行 `eks_len=248`)。
`eks` 的来源与 `collect` 必须**同源**(同一份 tdc.js、同一次执行),不可混用。
### 4.4 `ans` —— 点击坐标(明文,无需签名)
```json
[{"elem_id":1,"type":"DynAnswerType_POS","data":"162,249"},
{"elem_id":2,"type":"DynAnswerType_POS","data":"104,37"},
{"elem_id":3,"type":"DynAnswerType_POS","data":"561,156"}]
```
坐标位于 **672x480 原图坐标系**,顺序即提示语顺序(`mark_style: inc_number`)。
### 4.5 无法确定的内容(明确声明,不虚构)
1. **XTEA 会话密钥的派生过程**:tdc.js 每次加载都会重新混淆,密钥常量未以字面量出现
(`1448563793` 等 4 个常量在 tdc.js 中检索不到),需要源码级插桩才能完全还原。
本实现改为执行真实 tdc.js,绕过该问题。
2. **`eks` 的加密算法与字段布局**:未逆向,直接复用 tdc.js 产出。
3. **`click_cfg` 之外的题型**(2408 图标点选、2402/2401 滑块)尚未实现求解器。
---
## 五、文字点选题型的视觉求解(2404)
与雨云那套「图标点选」完全不同,本题型:
* **没有提示图**:`img_url` 换成 `img_index=0` 返回 **0 字节**(实测多次)
* 提示是纯文本:`instruction = "请依次点击:虫 呈 般 "`
* **背景图里只有这 3 个汉字**(无干扰字),位置随机,部分题目带斜切变形
* 汉字颜色恒定:BGR≈(58,176,228) → HSV H≈20 S≈185 V≈228
因此求解 = `颜色分割定位` → `汉字识别` → `按提示顺序做最优一对一指派`,实现见
`tcaptcha/glyph_solver.py`:
1. HSV 阈值分割(H∈[8,34] S>85 V>145)+ 形态学闭运算 + 13px 膨胀把偏旁并成整字
2. 连通域得到包围盒,按面积/长宽比过滤,得到候选字形
3. 掩膜 → 白底黑字 → 3 倍放大,交给 OCR **集成**打分
4. 得分 = 每个引擎在 CTC 各时间步上目标字概率的**最大值**,取多预处理变体最大值后加权求和
5. 剔除全部目标字得分都 < 0.02 的伪字形(照片暖色区域会误检)
6. 3! 全排列求最优指派,并用归一化后验概率作为置信度
### OCR 集成为什么必要
| 字形 | ddddocr max P(正确字) | PP-OCRv4 |
| --- | --- | --- |
| 筹(生僻,带斜切) | **0.0046** ❌ | 正确 ✅ |
| 雹 | 正确 ✅ | 误识为「电」❌ |
| 笆 | 误识为「芭」❌ | 正确(or 集成后仍正确指派)✅ |
单引擎都不够;概率级集成后 6/6 真实样本全部正确且后验置信度 = 1.000。
### 6/6 回归样本
| 样本 | 提示 | 求解结果 |
| --- | --- | --- |
| `har_round1` | 虫 呈 般 | (149,252) (48,55) (549,351) ✅ |
| `har_round2` | 佰 衬 稗 | (449,53) (549,353) (349,153) ✅ |
| `har_round3` | 簿 蚕 沧 | (549,153) (50,255) (48,154) ✅ |
| `har_round4` | 沉 驰 齿 | (150,154) (550,153) (49,53) ✅ |
| `live_probe` | 笆 辩 裁 | (549,153) (50,54) (351,152) ✅ |
| `live_rare` | 筹 炽 雹 | (449,253) (49,353) (250,255) ✅ |
用户抓包里失败的 3 次,回看坐标可确认是**人工点错**,不是协议问题:
* 第 1 题「虫 呈 般」:`虫`(162,249) ✅、`呈`(104,37) ✅、`般` 点在 (561,**156**),
而 `般` 实际在 (549,**351**) —— 第 3 击差 195px → 50
* 第 2 题「佰 衬 稗」:`佰` ✅,但第 2、3 击把 `稗` 和 `衬` 点反了 → 50
* 第 3 题「簿 蚕 沧」:第 2 击点在 (45,118) 是 `沧` 而非 `蚕`,第 3 击 (252,146) 落空 → 9
---
## 六、线上验证记录(errorCode 语义)—— 以及一个必须纠正的初判
### 6.1 1ms aid=195672344 的返回规律
| 提交内容 | errorCode | 复现次数 |
| --- | --- | --- |
| 三击全部落在图外的乱坐标 `(10,10)(20,20)(30,30)` | `50` | 4 次 |
| **正确坐标**(求解器命中 3/3) | `12` | 8 次 |
| **正确坐标但故意把第 1、2 击顺序点反** | `50` | 1 次 |
```
[REAL ] 请依次点击:安 胺 猜 post=1.000 -> 12
[BOGUS] 请依次点击:睬 持 层 -> 50
[REAL ] 请依次点击:半 陈 补 坐标正确但 1<->2 点反 -> 50
```
**`errorCode 12` 只在答案完全正确时出现** → 答题链路(坐标空间、顺序、`elem_id`、
`pow_answer`、`sess` 状态机)100% 正确,服务端是在答案通过后进入风控评估才拒绝的。
### 6.2 三个对照实验(决定了最终结论)
**对照 A —— 同一套代码换 aid,直接拿到 ticket:**
```
雨云 aid=2039519451, 图标点选(2408), 纯 Python 硬编码密钥 collect
→ errorCode=0, ticket=tr03jvqlp225MjNBxjDj_-mnM1VTS8O9, randstr=@wBc ✅
```
这一条同时证明了两件事:
1. **纯 Python 交付形态完全有效**,服务端不校验 collect 与 eks 的同源性;
2. 1ms 的 12 **不是**遥测密钥/算法的问题。
**对照 B —— 三种遥测后端在 1ms 上结果一致:**
| 后端 | 说明 | 1ms 结果 |
| --- | --- | --- |
| `python` | 硬编码密钥纯算法还原 | 12 |
| `node` | Node VM 伪造环境执行真实 tdc.js | 12 |
| `browser` | **真实 Chrome** + CDP 可信鼠标事件执行真实 tdc.js | 12 |
**对照 C —— 换出口 IP(住宅代理),彻底排除 IP 滚动风控:**
| 出口 IP | 归属 | 结果 |
| --- | --- | --- |
| `118.248.52.65` | 本机(与抓包 IP `118.248.47.92` 同 /16) | 12 |
| `123.181.235.161` | 河北秦皇岛(住宅代理) | 12 |
| `114.222.19.181` | 江苏南京(住宅代理) | 12 |
| `123.181.235.170` | 住宅代理 | 12 |
每个新 IP 上都是「后验置信度 1.000 的正确答案 → 12」,其中还夹杂一次
「后验 0.789 的题 → 50」(说明这道题确实点错了,反向印证了 12/50 的语义)。
另外还做过「静默 9 分钟 + 单发一次」,同样是 12。
### 6.3 结论(诚实版)
* ✅ **文字点选求解器**:16 道实时题全部命中,后验置信度均 ≈1.000
(尝辨长 / 佰簿班 / 半陈补 / 安胺猜 / 侈持钡 / 稗伴瓣 / 乘匙层 / 辰本虫 /
秤遍稗 / 背岸拔 / 奔薄芭 / 倍玻畅 / 趁骋薄 / 筹辩彻 …)
* ✅ **协议与答题提交**:正确(服务端只在答案正确时才进入风控评估)
* ✅ **纯 Python 遥测**:在雨云 aid 上端到端产出真实 ticket,交付形态成立
* ❌ **1ms aid 的票据发放**:被该 aid 的风控拦在 `errorCode=12`
已经排除的原因:算法、XTEA 参数随机化、遥测引擎(python/node/browser)、
请求参数与 `sess` 状态机、短时频控、**出口 IP 滚动风控**。
剩余唯一可能:**该 aid 配置了更严格的风控,要求会话具备"真实验证码页面上下文"**
(tdc.js 运行在真实验证码 iframe 内:真实 iframe location、真实 Canvas/WebGL 指纹、
`isTrusted` 鼠标事件、真实 `document.referrer`),而我们的 node/browser 后端
都是把 tdc.js 注入到"空白页"执行,缺少这层上下文。
### 6.4 下一步(能真正拿到 ticket 的路)
**完整浏览器内求解**:用 CDP/Playwright 打开真实页面触发验证码,让原生 SDK 自己跑
tdc.js;复用本仓库已完成的视觉求解器算坐标,用 `Input.dispatchMouseEvent`
在 iframe 内真实点击,从 SDK 回调取 `ticket` / `randstr`。
`tcaptcha/tdc_browser.js` 已经把 CDP 骨架写好了(启动 Chrome / 注入脚本 /
派发可信事件 / 读回 `TDC.getData()`),只需把「注入 tdc.js 的空白页」换成
「真实验证码页面」。换 IP 的能力也已经内置(`tcaptcha/proxy.py` +
`--proxy-api`,遇 12 自动换 IP),届时可直接复用。
### 6.5 求解器鲁棒性修复(本次线上测试暴露的真实缺陷)
代理测试暴露出 2 例定位失败,根因是 **HSV 下限 `V>=145` 把暖色草地/沙地并进了字形掩膜**:
| 样本 | 现象 | 根因 |
| --- | --- | --- |
| `长 澄 班` | 只检出 1~2 个字形 | 草地 `H≈25 S≈170 V≈156` 落在阈值内,把 `澄/班` 一起吞进大块背景连通域 |
| `菜 倍 钵` | 只检出 2 个字形 | `倍/钵` 竖排相邻,13px 膨胀把它们粘成 `332x80` 长条,被长宽比过滤器丢弃 |
实测颜色统计:**汉字 `H≈20.5 S≈200 V≈232`;草地 `V≈122~156`** —— 亮度 `V` 才是干净的分隔维度。
把下限收紧为 `H>=12 S>=145 V>=190` 后,8 张图(含这 2 张失败样本)**全部稳定检出 3 个字形**,
两张失败样本也已加入 `tcaptcha/tests/fixtures/` 作为回归用例。
另外把 `min_posterior` 从 `0.6` 提到 `0.95`:实测全部正确解的后验都是 `1.000`,
而那次后验 `0.789` 的提交确实被判 50(点错),门限收紧可避免提交不可靠答案。
### 6.6 题型覆盖
20 次连续抽题统计(aid=195672344):
| subcapclass | 题型 | 占比 | 状态 |
| --- | --- | --- | --- |
| 2408 | 图标点选(提示为 `img_index=0` 图标图) | 35% | 需按 1ms 的图标类别重训模型 |
| 2404 | **文字点选**(本次目标题型) | 15% | ✅ 求解器完成并验证 (8/8 回归) |
| 2402 / 2401 | 滑块拼图 | 50% | 未实现(需模板匹配找缺口) |
> 题型在**会话内固定**:`getsig` 不换题型,只有重新 `prehandle` 才能抽到别的题型。
> `client.solve` 已内置「抽到目标题型再提交」的循环(默认 `max_refreshes` 次)。
---
## 七、errorCode=12 根因分析 —— 证据账本(v2,推倒重写)
> v1 版本(已废弃)里我下了多个无证据结论,本节按 §1.3 重写:
> 只写有证据的"已知"、明确的"未定"、以及被推翻的假设。
### 7.1 已确证事实(每条附证据)
| # | 事实 | 证据 |
| --- | --- | --- |
| E1 | 答案判错→`50`,判对→`12`,稳定复现 | 乱坐标 4 次→50;正确 8+ 次→12;正确但顺序点反→50 |
| E2 | 同一套纯协议代码在雨云 aid 上 → `0` + 真实 ticket | aid=2039519451 端到端实测 |
| E3 | 真实签到流程(已登录、真页面、真浏览器、同 IP `118.248.52.65`)→ `0` + 真实 ticket | `签到成功-1.har` 条目 66/67 |
| E4 | 官方 SDK 注入在 `1ms.run` 首页(未登录)+ 真人点击 → `12` | `solve_in_browser.js` 抓到的 verify 响应 |
| E5 | **域不在白名单时 SDK 直接短路**:不出题、立刻回 `trerror_1006_...` 错误票据 | `127.0.0.1` 页面上 SDK 加载 1 秒内即回调 `ret:0`,无任何挑战 UI |
| E6 | 纯 Python 的 collect 各种变体(空/垃圾/截断/真实环境值/真实轨迹 dt)→ 全部 `12` | 变异矩阵 + 多轮实测 |
| E7 | 成功流程的 captcha 请求带 `qcloud.com` 域 cookie(`uin`/`skey`/`qcmainCSRFToken`/`intl`);我们不带 | HAR 条目 60/61/62 请求头 |
| E8 | **captcha 域"带 / 不带"腾讯云登录 cookie,结果完全相同**:单变量 A/B 均为 `12` | 2026-10-11 `scripts/experiment_h3_cookie_ab.py --both`:A=12、B=12(同一次运行、同一 tdc.js 构建,题目 2404 求解 posterior=1.000 全对,collect_len=1856) |
### 7.2 显式撤回的错误结论(§5.7)
| 曾下的结论 | 撤回理由 |
| --- | --- |
| ❌ "服务端不看 collect" | E6 里 collect 置空也 12,更可能是"校验失败即 12",不能反推"不看" |
| ❌ "环境指纹取值是瓶颈" | 填入真实值后仍 12,未证实 |
| ❌ "CDP/临时 profile 不被检测" | **依据无效**:E5 显示 127.0.0.1 上根本不出题,`ret:0` 是域授权短路,与风控无关 |
| ❌ "12 是业务账号上下文问题"(腾讯云 cookie) | 仅一次用可能过期的 skey 测试,不能定论 |
### 7.3 关键混淆变量(必须承认)
E3(成功)与 E4(失败)之间**同时变了三个变量**:
① 真实签到页 vs 注入首页 ② 已登录 vs 未登录 ③ 无 CDP vs 有 CDP
所以目前**无法**把 12 归因于其中任何一个 —— 之前归因"腾讯云 cookie"是过度解读。
### 7.4 剩余假设(互相独立,需单变量实验区分)
| # | 假设 | 状态 |
| --- | --- | --- |
| H1 | 需要**已登录业务会话 / 真实验证码页面上下文**(页面自己初始化的验证码) | 唯一未被单变量区分的剩余假设 |
| H2 | **CDP / 自动化被检测** | 唯一未被单变量区分的剩余假设 |
| H3 | 需要 `qcloud.com` 账号 cookie(有效 `uin`/`skey`) | ❌ **已排除**(E8:带/不带 cookie 单变量 A/B 均为 12) |
### 7.5 决定性实验(单变量,二选一即可闭合)
**在同一浏览器里登录 1ms,走真实签到页,让页面自己的验证码弹出并人工完成**(我方仅监听 verify):
- 结果 `0` → **H1 成立**(CDP/自动化无嫌疑),修复方向 = 复刻登录态与业务会话
- 结果 `12` → **H2 成立**(CDP 被检测),纯算+CDP 均不可行,只能半自动
若两者都排除,再做 H3(取有效 `qcloud.com` cookie 复测)。
### 7.6 与本题无关但已完成的资产
* XTEA 算法逐字节还原 + 每构建密钥提取(`tcaptcha/crypto.py`、`tdc_worker.js` captureKey)
* 文字点选求解器 8/8 回归(`tcaptcha/tests/test_glyph_solver.py`)
* 模板标定 + 纯 Python 回填管线(`generate_collect_success`)
---
## 八、路线修订(2026-10-11 晚)
### 8.1 已废弃「补环境」路线
曾实现 3 个"在伪造/真实环境里跑 tdc.js 产出 collect"的后端(node VM / browser CDP /
probe 深度 Proxy),**现已全部从代码移除**。依据:§7.1 E6 与 §6.2 对照 B/C 已证明
collect 内容、遥测后端、出口 IP、静默时长 **都不是** 12 的自变量。
废弃后唯一路径:`python_keyed`(Node 仅提取 XTEA 密钥字)+ 纯 Python 模板回填。
已删除的模块级符号:`apply_real_environment`、4 段拼接旧结构、`human_dt_ms`、
`generate_telemetry_node/browser/probe`、`generate_collect_pure`。
### 8.2 一处必须留意的置信度缺陷(已修)
`glyph_solver` 的"字形一致性强校验"此前用**单引擎 `ocr_text`** 判断,会把正确解误判为
不匹配(实测 `簿→垂`、`笆→芭`、`筹→努`),导致回归从 8/8 掉到 5/8。
现改为用**用于指派的集成得分矩阵行 argmax** 做自洽校验(每个字形的行 argmax 必须等于
其被指派目标),8/8 恢复,且该校验仍能拦住真正的"图字不符"。
### 8.3 下一步(唯一有价值的实验)
按 §7.5 做**单变量实验**,不要再用环境假说:
1. captcha 域带上成功样本的 `uin/skey/qcmainCSRFToken/intl` cookie,其余不变;
2. 登录态真页面触发 vs 未登录触发。
二选一闭合即可判定 H1(页面上下文)还是 H2(自动化检测)。