Files
hello_js_reverse_skill/har/1ms_dianxuan/PROGRESS.md
T
yml2213 99565f65f0 feat(web): 新增 Web 观测台(形态 A)+ 抽出 render/targets 共用层
- tcaptcha/web/:FastAPI + SSE + 零构建单页。把同步阻塞的 solve() 包成可观测
  后台任务(工作线程 + 全局串行,因 onnxruntime session 非线程安全),用
  logging.Handler 零侵入把 client 日志变成事件流;按靶子实测 min_interval 强制节流。
- tcaptcha/render.py:标注图(点击顺序编号 + 质心偏差)与点位量尺,CLI 与 web 共用。
- tcaptcha/targets.py:靶子表提为单一来源(含实测频控间隔 1ms=60s / mliev=3s)。
- fastapi/uvicorn 走 optional extra `web`,核心包零 web 依赖(带回归断言)。
- 修两个真 bug:hook 求解器时 args[0] 是实例自身(真入参从 args[1] 起);
  client 默认每次新建 solver,预热实例未被复用(每发重载 56MB ONNX)。
- 复验:icon/text/slider 三题型各拿到 errorCode=0;selftest 全绿;依赖隔离通过。

用法:uv run --extra web python -m tcaptcha.web.app
2026-10-14 19:10:54 +08:00

475 lines
63 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.
# 反 VM 密钥还原 & 纯算交付 进度文档
> 目标: **算法 100% 纯 Python** —— collect 的明文组装 / 轨迹 / 时间戳 / XTEA 加密 / 编码
> 全部在 Python 内完成; ~~Node 只保留一个用途: 从当前 tdc.js 提取本次构建的 XTEA 密钥字~~
> **2026-10-11 起 Node 连提取都不再需要**(纯 Python 静态还原,见已知 #19~#22;Node 仅作兜底)。
> 依据: AGENTS.md §2.3(拒绝补环境) / §2.4(分层交付) / §3.3(字节码 VM) / §4.3(运行时捕获) / §5.8(标定式交付)。
> 最后更新: 2026-10-12 (**按 docs/tdc-案例.md 重做**:collect 四段加密判决 + 字符串指纹模块识别 37/37)
---
## 📁 2026-10-12 项目结构重组(下面的历史条目仍使用**旧路径**,对照本表)
包从扁平改为分层,并删除了已废弃的整条路线(雨云线 / 模板回填 / Node 兜底 /
"补环境"时代的浏览器 JS / 旧 VM 探索工具 / 训练标注脚本)。
回滚点:tag `backup/pre-refactor-20261012`。
```
tcaptcha/
protocol/ client.py transport.py proxy.py # 协议交互
telemetry/ crypto.py values.py key.py opcodes.py # collect 纯离线生成
disasm.py program.py layout.py modules.py # + 密钥静态还原
solvers/ text_click.py icon_click.py icon_click_rotate.py slider.py
ocr/ engines.py slicer.py
adapters/ one_ms.py
models/ 权重(onnx)
tests/
tools/ cdp_eval.js cdp_collect.js cdp_dump_scripts.js tdc_key_pure.py tdc_profile.py
scripts/verify/ align_session.py audit_names.py verify_values.py verify_offline.py send_test.py
```
| 历史条目里的旧路径 | 现在的位置 |
| --- | --- |
| `tcaptcha/client.py` | `tcaptcha/protocol/client.py` |
| `tcaptcha/http_transport.py` / `proxy.py` | `tcaptcha/protocol/transport.py` / `proxy.py` |
| `tcaptcha/crypto.py` | `tcaptcha/telemetry/crypto.py` |
| `tcaptcha/tdc_values.py` | `tcaptcha/telemetry/values.py` |
| `tcaptcha/tdc_key.py` / `tdc_opcodes.py` | `tcaptcha/telemetry/key.py` / `opcodes.py` |
| `tools/tdc_disasm.py` / `tdc_program.py` / `tdc_layout.py` / `tdc_modules.py` | `tcaptcha/telemetry/{disasm,program,layout,modules}.py` |
| `tcaptcha/glyph_solver.py` | `tcaptcha/solvers/text_click.py` |
| `tcaptcha/solver.py` | `tcaptcha/solvers/icon_click.py` |
| `tcaptcha/icon_click_solver.py` | `tcaptcha/solvers/icon_click_rotate.py` |
| `tcaptcha/slider_solver.py` | `tcaptcha/solvers/slider.py` |
| `tcaptcha/ocr_engines.py` / `slicer.py` | `tcaptcha/ocr/engines.py` / `slicer.py` |
| `tcaptcha/one_ms_adapter.py` | `tcaptcha/adapters/one_ms.py` |
**已删除**(历史条目仍会提到,但要按"已不存在"来读):
`tcaptcha/{chrome_fetch,manual_captcha,probe_captcha,solve_in_browser,tdc_browser}.js`、
`tcaptcha/tdc_worker.js`、`models/success_collect_template.b64`、`tcaptcha/core_js/**`、
`tcaptcha/TOOLCHAIN_REPORT.md`、
`tools/{tdc_collectgen,tdc_slotmap,tdc_lift,tdc_vm_run,tdc_query,tdc_probe.js,tdc_trace.js,tdc_keytrace.js,tdc_vm_disasm.js,unpack_webpack.js}`、
`tools/cdp_{capture,click,net_capture,script_source,shot,module_probe,show_captcha}.js`、
`dist/rainyun_qinglong/**`、`rainyun_checkin.py`、
`scripts/{analyze_collect_layout,rainyun_checkin,check_real_tencent_aid,experiment_layout_verify}.py`、
`scripts/verify/{verify_segment,verify_seglen,verify_reorder,verify_pair_correct,verify_fp_consistency,verify_control_negative,verify_40_9_disamb,balance_names_values,label_plaintext}.py`、
`tcaptcha/tests/{test_collectgen,test_xtea_key}.py`、
`tcaptcha/{train_classifier,train_detector,prepare_yolo,web_labeler,collector,siamese,evaluate,benchmark_verify}.py`
---
## ⚠️ 2026-10-12 路线修正(对照 `docs/tdc-案例.md`,全部基于**在线新抓样本**)
之前多轮「乱碰」的根因,是几条**基础假设错了**:
| 旧假设(已推翻) | 新结论(已判决) | 证据 |
| --- | --- | --- |
| collect = **整体 JSON 一次加密**(`\x00` 补齐) | **4 段独立加密**,前 3 段**空格 pad 到 24 字节**,sd 不 pad | 真机明文 3 处连续空格串 `[355:+5, 422:+10, 720:+24]`;密文长度算术 480+96+416+56 = **1048**,与真实 1048 精确相等;`==` 只出现在末尾(sd 段) |
| `kind=2` 的项"仍产出 1 槽" | **这条原本就是对的**(真机 cd 38 槽、轨迹在第 10 槽),本轮一度误判为"不进 cd"已撤回。补充一层理解:它**同时**是 chunk1/chunk2 的**分段触发点**(案例 §5.5) | 真机 cd 38 槽 + 轨迹槽位 10 + trajectory 独立成段 |
| 模块 id 可当跨构建锚点 | id **每份构建重排**:交集 28 个里 `id→语义` 相同仅 **1/11** | 模块 8 在 A 是 `navigator.platform`,在 B 是 canvas 指纹 |
| (自创)指令直方图是够用的锚点 | 字符串+结构指纹语义名 **37/37 唯一**;直方图只有 20/37 | 见已知 #43/#44 |
现在的代码路径:
```
tdc_modules.analyze() -> 每项的语义名(37/37,跨构建稳定)
tdc_collectgen.reorder(s, dst, src) -> 按语义名锚点把源样本对齐到本次构建
crypto.build_collect(c1,traj,c2,sd,key) -> 4 段独立加密后拼接
```
**数据纪律:本轮起所有新研究只用 `har/online/` 下在线抓取的样本**
(`scripts/fetch_online_samples.py --n 4`),不再依赖仓库里的旧抓包。
---
## 〇、路线变更(重要)
**已废弃「补环境」路线。** 曾经实现过 3 个"在伪造/真实环境里跑 tdc.js 产出 collect"的后端
(`node` VM 沙箱 / `browser` CDP Dispatch / `probe` 深度 Proxy),现已从代码中**全部移除**,
只保留纯算单一路径 (`python_keyed` / `python`)。
废弃依据(本项目自己的实验,见 `ANALYSIS.md` §7):
| 变量 | 结果 |
| --- | --- |
| collect 变体(空 / 垃圾 / 截断 / 真实环境值 / 真实轨迹 dt)| 全部 `errorCode=12` (E6) |
| 遥测后端 python / python_keyed / node / browser | 全部 12 |
| 出口 IP(4 个不同省份住宅代理)| 全部 12 |
| 静默 9 分钟 + 单发一次 | 仍 12 |
即"环境 / 指纹 / IP / 时序"都**不是** 12 的自变量 → 补环境没有收益,只会引入大量手写桩的脆弱面。
---
## 一、已知(附证据 + 确信度)
| # | 结论 | 证据 | 确信度 |
| --- | --- | --- | --- |
| 1 | `collect` 的加密算法 = **教科书 XTEA**(32 轮,`delta=0x9E3779B9`,sum 取 mod 2³²) | 用提取密钥做纯 Python 加密,与真实 tdc.js 产物**前 48 字节逐字节一致**(`f6a24e3e4fb5dd2b...`) | 确证 |
| 2 | 早期带加盐的写法与该算法**等价**,只是把两个密钥字折叠了:`K' = [k0, k1+salt1, k2+salt2, k3]` | 标准 XTEA + `K'=[1448563793,1850559083,1733513813,1364613714]` 解密样本 collect 得到 `{"cd":[[],"1920-1080..."` | 确证 |
| 3 | **密钥按构建固定,不随运行变化** | 同一 tdc.js 连续 3 次运行,密文首块均为 `f6a24e3e4fb5dd2b` | 确证 |
| 4 | 密钥不在脚本字面量里 | 正则清点全脚本 ≥7 位数字,无一个是密钥字 | 确证 |
| 5 | 密钥由 VM 运行时通过 ADD 算出:`(sum + K[j])` 有一操作数等于 `r*DELTA` | 插桩 ADD opcode 后 Top4 候选即 `K'` | 确证 |
| 6 | opcode 表的**栈变量名每份构建都不同**(样本 `g` / 雨云 `j` / 1ms `M`) | 同一正则在三份脚本分别命中;写死 `g` 时实时脚本匹配 0 | 确证 |
| 7 | tdc.js 至少两代明文布局,**新版字段序随构建变化** | 旧版首段 `{"cd":[[],...`;雨云实时 `{"cd":[1,[],...`;1ms 实时 `{"cd":[1791712995,0,...` | 确证 |
| 8 | 宽松 aid 下服务端**不校验** `collect` 与 `eks` 的同源性 | 雨云 aid + 硬编码旧密钥 + 旧版明文 → `errorCode=0` + 真实 ticket | 确证(仅宽松 aid,勿外推) |
| 9 | 「Node 跑密钥 + Python 全算法」端到端可用 | 雨云 aid 纯 Python 出包 → `errorCode=0` | 确证 |
| 10 | **XTEA 密钥字是 4 个「ASCII 打包的 4 字符串」**(每字 = 4 个可打印字符按大端拼成 32bit)| `core_js/tdc_sample.js` 的密钥 `[1818510928,1381395306,1245924197,1768384072]` 大端解码 = `ldFP`/`RVkj`/`JCKe`/`igfH`;root 样本 = `VWTQ`/`nMJk`/`gSRU`/`QVZR` | 确证 |
| 11 | 密钥字**既不是立即数、也不是字面字符串**(无论明文、大端字节序列、base64 都不在 tdc.js/字节码里)| 立即数 1.3e9~1.9e9 区间为空;`ldFPRVkjJCKeigfH` 及其 base64 均搜不到 | 确证 |
| 12 | 密钥数组在 XTEA 轮里通过**寄存器间接寻址**读取,且经闭包捕获传入 | 反汇编 `reg7[(sum>>>11)&3]`;`@09463 CREATE_CLOSURE target=@9109 caps=[r7=r8]` | 确证 |
| 13 | 轮加盐**每次构建都不同**(但都是可见立即数)| 本构建 = `723726`(j==2) / `855311`(j==3);台账另一构建 = `2829068` / `328986` | 确证 |
| 14 | **密钥字的构造算法(已完全还原)**:`word = Σ ((charCodeAt(源串,k) + off_i) & 0xFF) << (8k)`,4 个源串各自一个**独立偏移常量**,常量是打包点的立即数 | 神谕二分 + DUMP_PC 取到源串;反汇编读出常量:<br>`("IA_t", -8)` `("9:%!", +49)` `("nTLS", -9)` `("7XYf", +3)` | 确证 |
| 15 | **有效密钥 = base 字 + 两个轮加盐** | `real[0]-base[0]=855311`、`real[3]-base[3]=723726`(XTEA 轮里的立即数) | 确证 |
| 16 | **纯 Python 可完整复算密钥,与 `extract_xtea_key` 逐字一致** | `base=[1817655617,1381395306,1245924197,1767660346]` → 折盐 → `[1818510928,1381395306,1245924197,1768384072]` == Node 结果 | 确证 |
| 17 | 密钥链路的运行时位置(神谕实测) | 逐字首次出现 PC:7740/7526/7895/8852(均 reg9);数组组装 @8886(reg7);进 XTEA 闭包 @9030 `CALL_FUNC[2]`;入口 @9109 | 确证 |
| 18 | `extract_xtea_key`(Node)本身正确 | 其密钥把 probe 的真实 collect 解成 `{"cd":[0,10,"top",[360,360],"MacIntel",1,"undefined?rand…` | 确证 |
| 19 | **opcode 编号每份构建整体重排**(不是只有变量名/加盐在变) | 同一源码的 pc0 `SET_STACK_LEN` 在 6 份构建里 = 53/19/32/57/55/1;`JUMP` = 60/17/42/58/21/2;core 表去解其它构建会在 pc0 就 `RET_TRUE` 假象 | 确证 |
| 20 | **opcode 表可从该构建解释器源码推导**:主循环 `X[Y[Z++]]()` 的 `X` 就是 handler 数组,下标即 opcode;handler 规范化指纹可跨构建精确匹配 | 6/6 份构建 **58/58 handler 精确匹配**、0 未识别(`tcaptcha/tdc_opcodes.py`)| 确证 |
| 21 | **密钥字闭包只写"被 XTEA 闭包捕获的那个数组"**;脚本同时算了两套 4 字(真 + 诱饵),靠**两层捕获链**区分:字闭包捕获的 B 帧寄存器 `w_src`,经"定义 B 的那个闭包"的 caps 映射到 G 帧后须 == XTEA 闭包捕获的寄存器 | core/root/1ms×3/实时新抓 全部选对;诱饵组按同规则被排除 | 确证 |
| 22 | **纯 Python 静态提取 XTEA 密钥成功,零 Node** | `tcaptcha/tdc_key.py`:6/6 份构建与 Node 插桩 `extract_xtea_key` **逐字一致**;含 2026-10-11 现场新抓的 `tdc.js`(`har/1ms_dianxuan/live/tdc_fresh.js`,77967B,密钥 `[1397244740,1799507557,1399740484,1516463189]`)| 确证 |
| 23 | **纯 Python 密钥能解密 1ms 真实成功样本的 collect** | 用 `har/签到成功-1.har` 里那次 ec=0 之前的 tdc.js 提密钥 → 解出该次 collect 明文 `{"cd":[2560,"MacIntel",...` 1353B;且与 `models/success_collect_template.b64` **逐字节同构**(仅轨迹/时间戳/盐被回填)| 确证 |
| 24 | **1ms 真实成功链路已完整解析** | prehandle(带腾讯云登录 cookie) → tdc.js → getcapbysig → verify **ec=0**(ticket `tr03…`) → `POST /api/v1/mall/checkin`,body = `{captchaTicket:{provider,data:{ticket,randstr}}, sessionUuid, identity_context}`;`/api/v1/captcha/params` 的 body 带 `identity_context`(`emctx1.<b64>.<sig>`) | 确证 |
| 25 | **真实浏览器对 verify 从不吃 12** | `点选.har`(同一 cookie/同一 aid) 三次 verify = 50/50/9;`签到成功-1.har` = 0。而**当时的**客户端形态都是 12。⚠️ 2026-10-11 晚修复:该结论把"载荷"只理解成"答案",漏掉了 **`cd` 槽位序**(见已知 #31/#32),表述已过强 | 证据成立 / 归因已修正 |
| 26 | **纯算链路在真实 aid 上端到端成功出 ticket**(2026-10-11 实测)| 用新抓包 `har/原始抓包/登录抓包-点选.har` 的 aid=**190758559**(mliev.login.wang 短信登录),本客户端 `solve()` 返回 `errorCode=0` + 真 ticket:<br>`tr03lxdviLWtn4uXGjWjmrLQ` / `tr03A5zTSQ9bCog51sDm` / `tr039KeeQxXX-OdduJoY`。**关键:最原始的 httpx + 无 cookie 也 0** | 确证 |
| 27 | **1ms 的 12 与"请求侧保真度"无关**(原结论"12 是该 aid 的服务端策略"已过强) | 成立部分:aid 190758559 上未做任何保真的 httpx + 无 cookie → 0;aid 195672344 上 curl_cffi + 真机 cookie + 逐 endpoint 头保真 + 传输指纹 → 仍 12。→ **请求侧细节不是自变量**(§三 反证仍有效)。<br>不成立部分:把 12 归因为"服务端不放行该 aid"缺少机制;现在有**更强且可证伪**的解释 → 见 #31/#32(collect 槽位序与本次会话构建不匹配)| 部分成立 / 归因待 #31 验证 |
| 28 | 新抓包暴露另一套业务接入(可复现靶子)| `har/原始抓包/登录抓包-点选.har`:`POST /api/captcha/params`(scene=sendSms + fingerprint/userAgent/platform/language/timezone) → 腾讯点选 → `POST /api/oidc/send-register-verification-code`(`{type,receiver,captchaTicket:{provider,data:{ticket,randstr,fingerprint}},sessionUuid}`) | 确证 |
| 29 | **同一个 `tdc.js` URL 每次返回的构建都不同**(不是"按构建固定",而是**按请求随机化**)| 连抓 4 次 `tdc.js?app_data=…&t=…`:md5 = `b6db2555…` / `032afea5…` / `1b8b7d4d…` / `b1ad4bdd…`,四份长度 77917~78013B、**XTEA 密钥 4 个全不同**。→ 结论 #3"密钥按构建固定"要限定为"**同一份已下载的脚本**内固定",不能外推到"重新请求还是同一份" | 确证 |
| 30 | **真机某次成功会话的 collect 明文已完整解出** | 抓包同时拿到**同会话**的 `tdc.js`(`app_data=1793575418729725952`)与 `cap_union_new_verify` 的 collect,用该构建密钥解密:784B 密文 → 可打印率 0.991,得<br>`{"cd":["",[360,360],0,"Mozilla/5.0 (Macintosh…) Chrome/155…","https://turing.captcha.gtimg.com/1/template/drag_ele.html?rand=…",["zh-CN","zh"],641547489,1791732001,"GgoAAA…png…",2560,[[34,96,3284],[154,50,370],…],1023,1440,"+08",1,0,"ANGLE (Apple, ANGLE Metal Renderer: Apple M4, …)",10,"",1,1986013704,"iframe","MacIntel",0,1791733986,"1440-2560-2530-24-*-*-|-*",1791733982,"Google Inc. (Apple)",24,"",[],0,"https://1ms.run/console/personal/benefits/check-in","98k",2,0,0,"UTF-8"],"sd":{"od":"C","ft":"qf_7Pf__H"}}`<br>(`tdc.js?app_data=…&appid=<aid>`:真机 URL 带 `appid`,我们客户端不带)| 确证 |
| 31 | **`cd` 的槽位顺序按会话/构建随机化(不是按版本固定)** | 5 份同日样本(真机 cap2 / `签到成功-1` / `点选` / `login_checkin` / 仓库模板)全部 `len(cd)=38`、**同一批取值**、**同一个 `sd`**,但**槽位顺序 5/5 互不相同**(两两不同槽位 26~29 个)。复现:`.venv/bin/python scripts/analyze_collect_layout.py --json <cap>.json` | 确证 |
| 32 | **仓库在用的 collect 模板 = `签到成功-1.har` 那一次的槽位序** | 模板指纹与 `签到成功-1#0` **逐槽一致**;与晚些时候的真机会话 29 个槽不同。→ `generate_collect_success()` 虽然回填了新轨迹/时间戳,但**槽位序是那一次的** | 确证 |
| 33 | **VM 程序结构层已吃透:顶层就是 62 个模块工厂的注册表** | 顶层 `@0` 做 `registry[id] = function(module, exports, require){...}`(`DUP; PUSH_IMM [id]; REVERSE_TOP2; JUMP -> CREATE_CLOSURE; SET_PROP`)。`tools/tdc_program.py --modules` 直接导出。修了两个真 bug:① 反汇编器漏了 `RET_TRUE`/`THROW` 是终结符(控制流穿过函数结尾 → 子程序归属全错);② 提升器终结符判定写死 opcode 数值 | 确证 |
| 34 | **`cd` 槽位序的来源已定位并可静态导出** | 装配链:清单模块 `arr[arr.length] = require(N)`(**追加,所以 require 顺序就是数组顺序**,已核)→ 分发模块按 `.on`/`.reset`/`.get` 分三数组 → 装配模块 `r3='{"cd":['` + `for(...) r3 += arr[i].get()`。线上构建实测 **37 个 require + 1 个字面量槽 = 38**,与真机 `cd` 槽数完全吻合;两份构建的 require 串不同(与 #31 一致)。工具:`tools/tdc_layout.py` | 确证(顺序)/ 语义标签部分 |
| 35 | **运行时反证通过:静态导出的 require 序 == 运行时真实 require 序(逐项一致)** | 在真机 iframe(`tdc.js` 实际执行的上下文,`window.TDC.getData` 就在这里)里包一层 `Function.prototype.apply`,模块工厂调用形状 = **4 参 `[object,object,object,function]`**(require 是最后一个),按工厂实例分组记账。触发一次刷新后取运行时串,用 `tools/cdp_script_source.js`(Debugger.scriptParsed + getScriptSource)取到**运行时真正执行的那份 tdc.js**(exactly-once,不能用重新 fetch 代替——#29 每次返回不同构建),两边串:<br>静态 = 运行时 = `[8,9,10,13,14,15,16,17,18,19,20,21,23,24,25,30,31,32,34,36,37,1,38,39,40,41,42,43,44,47,48,49,50,51,56,57,58]`(37 项,逐位相同)<br>→ 由此可判定:之前"部分槽语义对不上"是**标注启发式**的问题,**顺序本身正确**。工具:`scripts/experiment_require_order.py` | 确证 |
| 36 | **「清单项 → cd 槽」的精确规则(同一构建端到端校验确立)** | 装配循环(线上构建 `@4362`,370 条)分支树已读通:<br>`if(!item) continue;` `v,w,kind = item.get();`<br>· **`kind===2`(`@4820`)**:把 `r3` 清空转存 `r4`(`@4860`),`r4` 在收尾 `@4905` 二次加工后**拼回**,因此**仍产出 1 槽**(实测第 0 项,随机串 `vXc29RxhvT34Q…`)<br>· `v===null/undefined`(`@4799`)→ `out += "null"`<br>· `typeof v==="number"`(`@4738`)→ `out += (v||0)`<br>· **`kind===1`(`@4724`)→ `out += v` 原样拼**:返回值是 JSON 碎片串;**只按括号外的逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽,而 `"641547489,1791732001"` → 2 槽<br>· 其余(`@4703`)→ `out += '"' + v + '"'`<br>· 收尾 `@4753`:`typeof w === "number"` 才再 +1 槽(实测 kind=2 的第二元素是 null,不触发;此路径本次未命中)<br>→ **37 项 → 38 槽**,仅第 33 项(模块 1)占 2 槽。工具:`tools/tdc_slotmap.py`(`--items` 运行时取样 + `--plain` 明文对拍) | 确证 |
| 37 | **按构建生成 collect 的工具链已跑通(含回环自测)** | `tools/tdc_collectgen.py`:静态导出槽位序 → 按 #36 规则算「项→槽」→ 重排 → 该构建密钥加密。<br>实测(同一构建):37 项 → **38 槽**,明文 686B / collect 920 字符;**解密回环逐字节一致**,cd 与真机样本逐槽相同。<br>规则修正(重要):`kind=1` 的串**只按括号外逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽;`kind=2` 的项**仍产出 1 槽**(转存 r4 后在收尾 @4905 拼回)。<br>回归:`tcaptcha/tests/test_collectgen.py`(已并入 selftest) | 确证 |
| 39 | **12 只在"答案正确"之后才判**(排查方法学上的关键事实)| 答错一律 50/9:本轮滑块题型下(我们按点选坐标答,必错)两组都是 50;换成真解(text/icon)后立刻全是 12。→ **任何用"答错"做的对照实验都没有区分度**,这解释了早期多轮对照为何无效。今后做 12 的 A/B 必须 `--solve` 真解题 | 确证 |
| 40 | ~~**"谁在发 HTTP"是 12 的剩余唯一自变量**~~ **已推翻(2026-10-12)→ 见已知 #58/#59**:自变量是「collect 有没有用本次会话的构建生成」,与谁发 HTTP 无关(Python + httpx 直接发本次构建生成的包 → 0) | 原证据链:① 槽位序证伪(§三);② 包内容证伪(本会话真实 collect 由 Python 发仍 12);③ 已知 #26:宽松 aid 190758559 上 **httpx + 无 cookie + 无头保真** 也能 0 —— 说明"请求侧细节"对宽松 aid 无所谓,但对 195672344 不够。④ 真机同 aid 只出 50/9/0(#25)。→ 剩余差在**会话↔请求方绑定**。**收口实验**:把同一 sess/eks/tkid + 同一 collect,分别由浏览器发与 Python 发,比较 errorCode | 结论(待收口) |
| 38 | ~~**纯离线生成还没打通**~~ **已解决(2026-10-12)→ 见 #57/#58/#59**:`tcaptcha/tdc_values.py` 已为 37 个语义名全部提供取值,并在 1ms aid 上 3/3 拿到 `errorCode=0`。当时状态:当前 `--from-sample` 依赖**一份同机的真机明文**做重排;`--values`(语义名→值)需要调用方提供取值字典。**仍缺**:纯算地为每个语义名算出值(多数是静态环境量可配置,但 canvas/WebGL 指纹只能取自真实环境)。这一步完成前,不能声称"纯离线过风控"。 | 状态说明 |
### 2026-10-12 新增(对照 `docs/tdc-案例.md`,全部基于 `har/online/` 在线新抓样本)
| # | 结论 | 证据 | 确信度 |
| --- | --- | --- | --- |
| 41 | **collect 是「4 段独立加密」拼接,不是整体一次加密**:`chunk1 + trajectory + chunk2 + sd`,前 3 段**空格 pad 到 24 字节对齐**,sd 不 pad;4 段 base64 **直接拼接无分隔符** | ① 真机明文 3 处连续空格串 `[355:+5, 422:+10, 720:+24]`,且 `pad == 24 - len%24` 三段全中;② **无需密钥的纯算术判决**:360/72/312/40 → base64 480+96+416+56 = **1048**,与真实 collect 的 1048 字符精确相等;③ `==` 只出现在位置 1046/1048(正是 sd 段 33B→补 7→40B 的产物);④ 段边界与案例 §5.5 逐字吻合(chunk1 以 `,2560,` 结尾、chunk2 以 `,"UTF-8"],` 结尾) | 确证 |
| 42 | **`kind=2`(`type_flag==2`)产出 1 槽(就是轨迹),并同时触发**分段加密**——它是 chunk1/chunk2 的分界** | 案例 §5.5;真机明文 cd **38 槽**、轨迹位于第 **10** 槽,trajectory 段 `[360:432]` 独立成段,结尾 `[1,1,12]`(案例 §6.1 的固定标记)。<br>→ 即「产槽」与「分段」**同时成立**:旧已知 #36 的"仍产出 1 槽"**未被推翻**,本轮早期我一度误判为"不进 cd",已用 cd 槽数(38)+ 轨迹槽位(10)纠正 | 确证(含一次自我纠错) |
| 43 | **模块 id 不能当跨构建锚点**(每份构建重排) | 两份构建清单交集 28 个模块里 `id→语义` 完全相同仅 **1/11**(模块 8 在 A 是 `navigator.platform`,在 B 是 canvas 指纹);案例 §1.2 亦明确列出"37 个模块的 ID 随机重新分配" | 确证 |
| 44 | **字符串+结构指纹可把 37 个采集器全部识别且唯一**(案例 §5.3 方案) | 4 份全新在线构建:`37/37` 识别、**0 unknown**、跨构建语义名集合**完全一致**、单构建内**0 重名**;端到端重排 3/3 正确且非恒等 | 确证 |
| 45 | **指纹必须按 specificity 排序 + 支持前缀匹配** | `?rand=` 在真机里是 `?rand=1519713624347`(带会话随机数),精确匹配永远命中不了 → `template_url` 被误判成 `location_href`;改为 `?rand=*` 前缀匹配 + 给 `location_href` 加禁用串后消歧 | 确证(踩坑修正) |
| 46 | **fp_salt 的随机串每构建都变**(`AyqhIdr`/`QtDKYSO`/`ryieWP_`/`mYmmgLx`),**不能**用字符串识别 | 4 份新构建各不相同;改用结构规则 `entry≥120 且 get≥100` 抓,每份构建恰好 1 个 | 确证 |
| 47 | **结构完全相同的 2 项可用「闭包最后写入什么」消歧** | 这 2 项 entry 40 条、.get 9 条、指令逐条相同(只差 JUMP 绝对地址);差异在**闭包 target**(`CREATE_CLOSURE.target`,不是 JUMP 目标):`new Array()[0] = ""` vs `new Array()[0] = <随机int32>`。实测每份构建恰好 empty×1 + rand×1 | 确证 |
| 48 | **配对正确性经独立证据 + 负对照验证** | 独立证据:同一归一化指令序列(抹掉地址/模块号/字符值)下 37/37 全等(唯一例外 fp_salt 的随机 int32,抹掉后 37/37);负对照:清单下标配对 2-3/37、模块 id 配对 2-3/37、随机配对 2/37 —— 对照组极低,证明归一化**没有过拟合** | 确证 |
| 49 | **`reorder` 旧实现是恒等映射(真 bug,已修)** | 旧代码 `sample[slot:slot+n]` 里的 `slot` 是**目标**构建自己的槽下标 → 顺序不同时全盘错位、相同时才碰巧对。原测试只断言"是置换",恒等函数也满足 → **弱断言把 bug 放过了**。已补强断言(== 已知正确答案 且 != 原样),并自查:旧实现过不了新断言 | 确证 |
| 50 | **空格 pad 可以只有 1 个字节** → 用 ` {2,}` 找 pad 会漏段,旧 `split_collect_segments` 在这种样本上直接抛异常 | 真机同会话明文:chunk1 = **287** 字节,`287 % 24 == 23` → pad = **1**,它就夹在 `...,0,0,0, [[1,1,12]]...` 之间;旧函数只找到 2 个空格串 → `ValueError: 期望 3 个空格填充串` | 确证(踩坑修正) |
| 51 | **pad 判据必须再加一条「不落在 JSON 字符串内部」** | 只用 `b%24==0 且 (b-a)==24-a%24` 会把 UA 串 `Mozilla/5.0 (Macintosh…` 里偏移 47 的**普通空格**判成 pad(47%24==23);也不能只靠「去掉 pad 后能否 json.loads」排除(少一个空格仍是合法 JSON 串)。用**引号奇偶**判定后,3 份样本候选恰好各 3 个 | 确证 |
| 52 | **轨迹是独立的一段,轨迹槽就是分段点**(3 份样本逐字节一致) | 真机成功样本 441 / **616** / 217 / 33;本会话两次 287 / **10**(`[[1,1,12]]`)/ 356 / 16 与 287 / **516**(61 点)/ 356 / 16。分段写法:chunk1 以 `,` 结尾、traj 是**一个**完整 JSON 值、chunk2 以 `,` 开头且以 `],` 结尾 | 确证 |
| 53 | **`trajectory` 语义名此前认错了模块** | 旧规则 `{"stringify"}` 抓到的是另一个只做 JSON.stringify 的模块(值恒为 `[]`);真轨迹模块的字符串集是 `mouseMoveEvent / mousemove / touchStartEvent / touchmove / tCaptchaDyContent`(entry 279 条)。改规则后仍 37/37、0 unknown,且该模块**正好落在明文分段点上** —— 两条独立证据互证 | 确证(踩坑修正) |
| 54 | **37 个语义名 → 真值全部拿到(38/38 槽全覆盖)** | 用真 Chrome 取「同会话构建 + 同会话 `TDC.getData(true)`」,按「require 序 == 槽位序」对齐(`scripts/verify/align_session.py`);再用取值生成器对 **4 次连续采集**逐槽复算,**152/152 全等**(`scripts/verify/verify_values.py`) | 确证 |
| 55 | **必须区分「会话量」与「每次采集量」** | 4 次 `getData` 实测:`itoken` / 会话随机盐(`struct_40_9#…_rand` = 1591912482)/ 页面加载时刻(`window_time#e48` = 1791745224)**四次完全不变**;而当前时刻(`window_time#e40`)、采集计数(`struct_46_9` = 1,2,3,4)、轨迹**每次都变**。另:`getData` 会把轨迹**清空**(61 点 → 下一次回到 `[[1,1,12]]`) | 确证 |
| 56 | **屏幕宽高方向纠正**:`screen_width = 1440`、`screen_height = 2560` | 浏览器真值 `screen.width/height = 1440/2560`(`availHeight` 2530、`colorDepth` 24、`dpr` 1、`hardwareConcurrency` 10、`getTimezoneOffset` -480),与明文槽位及 `1440-2560-2530-24-*-*-| -*` 串**顺序完全一致**;旧结论「串的 parts[0] → screen_height」是**反的** | 确证(含一次纠正) |
| 57 | **纯离线闭环已打通**(本任务的核心阻塞点) | `scripts/verify/verify_offline.py`:tdc.js → 37 语义名 → 38 槽取值 → 4 段明文 → 4 段加密拼接 → 解密/切段/解析 → **逐槽全等 + 分段几何一致**;产物末尾 `==` 只来自未 pad 的 sd 段 | 确证 |
| 58 | **纯离线生成的 collect 在 1ms aid=195672344 上端到端拿到 `errorCode=0` + 真 ticket**(3/3) | 2026-10-12 实测 `scripts/verify/send_test.py --mode python_values`(aid 195672344 / 签到页 / 2404 文字点选 / 零 Node)。三次分别拿到 `tr03tgI-J732…`、`tr03RLpt4OTMy…`、`tr03n_V7CXAFI…`。每颗 collect 都由**本次会话同一份 tdc.js** 纯离线生成:段几何 461/625/147/16、460/640/148/16、81/708/529/16(每份构建清单序不同 → 分界点不同),轨迹 61/63/71 点、尾点一律 `[1,1,12]` | 确证 |
| 59 | **A/B 隔离出真正自变量:collect 必须「用本次会话的构建」生成** | 同 aid / 同一天 / 同客户端 / 同求解器、答案都正确,只换 collect 构造方式:<br>`python_values`(本次会话构建 → 槽序与取值都对)→ **0**(3/3);<br>`python_keyed`(仓库里那份 **2026-10-11** 成功样本模板回填:槽序来自**另一份构建**,屏串还是**另一台设备**的 `2560-1440-1345-24`)→ **12**(2/2)。<br>→ **推翻已知 #40**("谁在发 HTTP");同时把 #31/#32 的"槽序/取值必须与本次会话构建同源"从假设升级为确证 | 确证(含推翻 #40) |
| 60 | **滑块答案格式 = `DynAnswerType_POS`,坐标用「背景图原始像素」** | 真机课题 JSON:拼图块元素(id=1)的 `move_cfg = {track_limit:"x>=50&&x<=552", move_factor:[1,0], data_type:["DynAnswerType_POS"]}`;`init_pos=[50,128]` 与提交答案的 data 是**同一坐标系**(`bg_elem_cfg.size_2d = 672x480`)。所以解题时用 `scale=1.0`,直接把 `target_x_image` 当答案,不要换成页面显示像素 | 确证 |
| 61 | **滑块的拼图块可直接从课题配置裁剪,不必解析 DOM** | 拼图块元素自带 `sprite_pos=[140,490]` + `size_2d=[120,120]`;与早先从 CSS `background-position/size` 反算出的 `sprite_region() == (140,490,120,120)` **逐像素相同**(测试里已加交叉断言) | 确证 |
| 62 | **ddddocr 的滑块距离识别在腾讯滑块上不可用** | fixture(真值 target_x=343 / dx=293):`slide_match` 给 402/352(**偏 59px**),`simple_target=True` 同样 402,`slide_comparison` 直接报尺寸不匹配;实时样本(真值 236)给 296(**偏 60px**)。判定手段:把拼图块 alpha 轮廓按候选位置叠到背景图上目视 —— 本项目 ZNCC 的 236 与缺口描边**严丝合缝**,ddddocr 的 296 整体右偏 | 确证(负结果,别再试) |
| 63 | **滑块端到端已打通**(纯离线 collect + ZNCC 定位) | `send_test.py --kind slider` **3/3 拿到 errorCode=0 + 真 ticket**;轨迹为拖动型(~34 点、尾点 `[1,1,12]`)。其中一次 `margin=0.024 < 阈值 0.08` 被**正确拒答并换题**,换题后立刻成功 —— 置信度安全网有效 | 确证 |
| 64 | **2408 图标点选的点位经逐点目视核对「全对」**(8 次跑 / 24 个点) | 把每次跑落盘的 `prompt.jpg` 用求解器同款切片器切 3 片,与「实际点击点 ±32px 背景 patch」并排放大逐一比对:`run1..run6`(`0,0,12,12,12,12`)+ 修复后 2 次(`50,50`)**24/24 全部落在与提示同序的目标图标上**。→ **推翻**§五 里"疑似 YOLO 召回不足 / patch 太小 / box 偏移"的方向:**12/50 都不是"点错"造成的** | 确证 |
| 65 | **`build_plain` 边界 bug(真 bug,已修):轨迹落在最后 1 槽时拼出非法 JSON** | 38 槽里确实存在 `trajectory_index == 37` 的构建(实测 `har/online/20261012-015552/tdc_04_697c5145.js`):旧写法 `chunk2 = ',' + '' + '],'` → 整段 = `{"cd":[<37 槽>,<traj>,],` —— **非法 JSON**。13 份构建扫描命中 **1/13**。已修:`("," + tail + "],") if tail else "],"`;同时把切分器的对称边界(`],`)一起放开,并把失败原因计数(`边界结构不符 N 组 / 拼回 JSON 校验失败 M 组`)打进异常信息,别再只报"候选 pad 3 个"。回归:`test_build_plain_trajectory_last`(含"旧写法必须 `json.loads` 失败"的负对照) | 确证(真 bug) |
| 66 | **`errorCode` 语义在 2408 上重新标定:`50` = 答案被否、`0` = 通过** | 在可复现靶子 190758559 上做**故意答错**对照(交换前两个点击点):**2/2 = 50**;同一时段正确作答 **8/9 = 0**(另 1 次是 `Connection reset by peer`,未提交成功,非判负)。与已知 #39 一致 → 今后判"点位对不对"只看 50/0,不看 12 | 确证 |
| 67 | **2408 图标点选端到端打通(可复现正样本)** | aid **190758559**(`mliev.login.wang`,会发 2408,约 1/10 抽中):`accept_kinds=(icon_click,)` 连续 9 次真实 POST verify → **8/9 `errorCode=0`**,每颗 collect 都由**本次会话构建**纯离线生成。→ 图标链路(YOLO + 分类器 + 全局指派 + 4 段 collect)本身**没有问题** | 确证 |
| 68 | **1ms aid=195672344 的现状:先 `12` 后 `50`,且与"答对没答对"无关** | 同一时段(13:15–13:31)、同一客户端 / 生成器 / 求解器:<br>① aid 195672344 图标×6 → `0,0,12,12,12,12`;<br>② aid 195672344 混合题型×6 → `12 ×6`(三题型**都** 12);<br>③ aid 195672344 图标×2 → `50,50`,而这两次的点位经目视**逐点正确**(#64);<br>④ **同小时** aid 190758559 图标×9 → `0 ×8`;<br>⑤ 故意答错在 190758559 上 2/2 = 50(#66)。<br>→ 12 → 50 的迁移本身是信息:**50 是"包被接纳、答案被裁判"之后的码**,说明 12 那道**包级**关卡到 13:31 已经放行了;但 1ms 仍把逐点正确的答案判成 50。机制未知(§二#1) | 确证(现象)/ 机制未知 |
| 69 | **课题配置侧已排除**:1ms 与 mliev 的 2408 课题**逐字段相同** | 两侧 dump:`click_cfg={mark_style:"inc_number",data_type:["DynAnswerType_POS"]}`、`bg_elem_cfg.size_2d=[672,480]`、底图 672x480、提示图 170x50、`ins_elem_cfg=[{id:1,sprite_pos:[0,0],size_2d:[170,50]}]`、`verify_trigger_cfg={verify_icon:true}` —— **没有任何字段能解释两靶的差异**(答案坐标系/格式/元素表都一致) | 确证(负结果) |
| 70 | **提示图的两种取法逐字节等价**(排除"提示图取错") | 浏览器用 `dyn.sprite_url`;本客户端是把 `bg_elem_cfg.img_url` 的 `img_index` 改写成 0。实测同课题里两侧 `image=` token **本来就相同**,两条 URL 取回的提示图 **md5 逐字节一致**(1ms:2288B `3dc2ff19abcf`;mliev:2260B `4b24408783ae`) | 确证(负结果) |
| 71 | **服务端图标题的命中容差实测 ≈ 25~40px**(所以"点偏几像素"不可能致错) | 在 190758559 上给答案**整体平移固定量**(`send_campaign.py --offset`,每档 2 次):<br>`10,0` → `0` ×2(点离质心 3.3~10.6px);`25,0` → `0` ×2(6.4~22.0px);`50,0` → **`50`** ×2(11.2~34.8px);`0,75` → **`50`**。<br>→ 容差落在 25 与 50px 之间。而我们正常作答的点击离图标笔画质心**只有 2.2px(1ms)/ 3.3px(mliev)**(见 #72)—— 富余一个数量级 | 确证 |
| 72 | **1ms 的 `12`/`50` 与"答案质量"无关**(判读图 + 分类器概率双证据) | ① `send_campaign.py --target 1ms --kind icon --n 8`(13:49–13:52):`0,0,0,0,None,12,12,12`;**三次 `12` 的 `solver_diag` 里每个目标类都有 top-1 概率 = 1.0 的候选**(run6 `[2,1,4]`→2:1.0/1:1.0/4:1.0;run7 `[8,9,11]`→8:1.0/9:1.0/11:1.0;run8 `[1,4,0]`→1:0.998/4:1.0/0:1.0)。<br>② `judge_icons.py` 判读图:8 次跑 **24/24 全部落在与提示同序的目标图标上**,`12` 那三次也一样(偏离 0~5px)。<br>③ 点击精度不区分 `0` 与 `12`:`0` 组的质心偏离均值 ≈2.1px、`12` 组 ≈2.4px。<br>④ 同一时段该 aid 在混合题型下 `12` ×6(三题型都不行)。→ **12 是包级/会话级,不是答案级** | 确证 |
| 73 | **轨迹的 dt 分布曾与真机严重不符(真 bug,已修)—— 用户"位置没问题那就是轨迹被校验"的判断正确** | 把 4 份真机抓包 / **6 条成功轨迹**(360 个非首点 dt)全部解出汇总:<br>① **点数硬不变量:6/6 全是 61 点**,而旧实现随机 57~65;<br>② **dt 紧密聚集在 16~17ms**(13~20ms 占 79%,0~4ms 仅 0.28%),旧实现 0~16ms 占 50%、0~4ms 占 14% —— 每发一枪都自带时序指纹异常。<br>修复 `tcaptcha/telemetry/values.py`:`_sample_dt()` 按 6 条轨迹重拟合(13~20ms 占 78%)、新增 `_fit_points()` 把点选轨迹**钉死 61 点**(补齐用 `[0,0,dt]` 驻留点,**不能**复制最后位移点——否则净位移被放大,实测 166 vs 139.5)。<br>**修复后 A/B**:1ms 图标 **4/8=0 → 7/8=0**(前 7 连 0);文字 2/3=0、滑块 1/3=0(样本少)。回归:selftest 全绿 | 确证(真 bug + A/B 见效) |
| 74 | **61 点不变量只属于点选轨迹,拖动轨迹不适用**(差点犯的错) | 修 61 点时曾把 `drag_trajectory_points`(滑块拖动)也套了 61 点 → 滑块测试立刻红(净位移 166 vs 139.5、点数 61 超范围)。**拖动是另一种交互(按下→横移→松手),61 点是从点选轨迹量出来的,不能外推**。已回退:拖动按 steps 走(实测 34 点、净位移 140≈139.5) | 确证(含一次自我纠错) |
| 75 | **1ms 的残余 `12` = aid+IP 级频控,已标定窗口;冷却足够后三种题型全过** | 2026-10-14 实测(`send_campaign.py --kind any --interval N`,每次 `prehandle` 次数都是 1,排除"抽题太多次"这条):<br>① **间隔 4s × 12 发** → `0,0,12,12,12,12,12,12,12,12,12,12`(只有**前 2 发**过,之后 **10 连 12**;icon/text/slider **三种题型都被打到 12**);<br>② **冷却 2 分钟 + 间隔 30s × 6** → **6 发全 12**(冷却不够);<br>③ **冷却 25 分钟 + 间隔 60s × 4** → **4 发全 `0`**(text×2 + slider×2)。<br>→ 频控**与题型无关、与 prehandle 次数无关**,只与"短时间内的请求量"有关。**结论:1ms 必须低频使用(≥60s 一发、连续打不超过 ~2 发),回归测试一律用 190758559。** | 确证(含恢复验证) |
| 76 | **Web 观测台上线**(形态 A:观测/调试台)+ 顺带抓到两个真 bug | 新增 `tcaptcha/web/`(FastAPI + SSE + 零构建单页)、`tcaptcha/render.py`(标注图 + 点位量尺,**CLI 与 web 共用**)、`tcaptcha/targets.py`(靶子表,含**实测频控间隔**)。设计要点:`solve()` 同步阻塞 → 工作线程 + **全局串行**(onnxruntime session 非线程安全);用 `logging.Handler` **零侵入**把 client 的日志变成事件流;按靶子 `min_interval` 强制节流。<br>**bug ①**:hook 求解器时把 `args[0]` 当成了题目图 —— 实例方法调用时 `args[0]` 是**求解器实例自身**(真入参从 `args[1]` 起),于是 `'CaptchaSolver' object has no attribute 'convert'`,标注图与点位全丢。<br>**bug ②**:`TencentCaptchaClient` 默认**每次新建** solver,预热加载的实例根本没被复用(每发重载 56MB ONNX)→ 改为把缓存实例传进 `solver=` / `text_solver=`。<br>另:滑块硬套"离笔画质心"量尺会给出 10~40px 噪声(滑块答案是缺口位置),已对滑块置 None。<br>复验:icon/text/slider **三题型各拿到 `errorCode=0`**;SSE 事件序列 + 3 张图全部可取;`import tcaptcha` **不引入 fastapi**(依赖隔离) | 确证 |
---
## 二、未知(附假设 + 确信度)
| # | 未知 | 当前假设 | 确信度 |
| --- | --- | --- | --- |
| 1 | ~~1ms aid=195672344 为何在答案正确时返回 12~~ **已解决(2026-10-12 晚)→ 已知 #73** | 真因是**轨迹遥测的时序指纹**:旧实现的 dt 分布(50% 落 0~16ms、14% 落 0~4ms)与真机(79% 落 13~20ms、0~4ms 仅 0.28%)严重不符,且点数随机 57~65 而真机恒为 61。按 6 条真机轨迹重拟合后,1ms 图标 **4/8=0 → 7/8=0**(修复前曾连吃 12)。残余的偶发 `12`(8 连发里最后 1 次,当时 prehandle 已被风控拖到 14 次)符合"连发触发频控"的形态,低频使用即可 | 已确证 |
| 2 | ~~新版明文字段语义(哪一位是时间戳/指纹/轨迹)~~ **已解决** | 见已知 #54:37 个语义名 → 38 槽真值全部对齐,且被生成器逐槽复现 | 已确证 |
| 3 | ~~明文里 `[360,360]`、`"1440-2560-2530-24-*-*-| -*"` 的作用~~ **已解决** | `[360,360]` = 验证码 iframe 可视区(`viewport_size`);屏串字段序 = **宽-高-可用高-色深-\\*-\\*-\\|-\\***,与浏览器真值 1440/2560/2530/24 一一对应 | 已确证 |
---
## 三、已排除的假设(负结果也是资产,§5.7)
| 假设 | 否定证据 |
| --- | --- |
| 密钥每次运行随机 | 同一脚本 3 次运行首块完全一致 |
| 密钥可从 `eks` 推导 | 34 种推导 + 40 种偏移切片全部失败 |
| 密钥是脚本里的字面量 | 全脚本 >1e9 的数只有 3 个 |
| 用 `Array.prototype.push` 打桩能抓密钥 | 算术 opcode 用 `g[g.length-2]=...` 直接下标赋值 |
| 实时构建复用旧版明文布局 | 旧版校验器对两份实时脚本全部失败 |
| **换出口 IP 能解决 1ms 的 12** | 4 个不同省份住宅 IP 全部 12 |
| **换遥测引擎能解决 1ms 的 12** | python / python_keyed / node / browser 全部 12 |
| **长静默窗口能解决 1ms 的 12** | 静默 9 分钟 + 单发 → 仍 12 |
| **"补环境"能解决 1ms 的 12** | 同上三条;环境不是自变量 |
| **captcha 域带腾讯云登录 cookie 能解决 12 (H3)** | 单变量 A/B:带/不带均 12(`scripts/experiment_h3_cookie_ab.py`,2026-10-11)|
| 轨迹 dt 太小是 12 的"直接原因" | 换成真实 ms 分布后仍 12(E6) |
| **opcode 数值是跨构建固定的 VM 常量** | 6 份构建实测 pc0 的 `SET_STACK_LEN`/`JUMP` 数值全不同(见已知 #19);`tools/tdc_disasm.py` 里固化的表**只对 core_js 有效**,已改为"可注入按构建推导的表" |
| **"密钥字必须靠 Node 插桩才能拿到"** | `tcaptcha/tdc_key.py` 纯 Python 零 Node 复算,6/6 构建与 Node 逐字一致(已知 #22)→ Node 降级为兜底 |
| **H3(captcha 域腾讯云登录 cookie)能解决 12** | 用**抓包里的真实 cookie 值**(uin/skey/qcmainCSRFToken/intl)重跑,仍 12(`scripts/experiment_h3_cookie_ab.py --only b`,2026-10-11)→ 与早期结论一致,H3 确凿排除 |
| **HTTP/TLS 传输指纹是 12 的自变量** | `curl_cffi`(impersonate=chrome: JA3/JA4 + HTTP/2 + Chrome 头序) + 真实 cookie,仍 12(`scripts/experiment_transport_ab.py --only b`)→ 传输层排除 |
| **collect 的"内容/取值"是 12 的自变量**(2026-10-12 **证伪**)| 用**本会话真机真实产物** (`TDC.getData(true)`,1012 字符,取值/顺序/密钥全对)原样交给 Python 发:**4/4 全是 12**。→ 包内容**不是**自变量;只剩 **"谁在发这个 HTTP"**(会话↔请求方绑定)|
| **H-LAYOUT:collect 的 `cd` 槽位序是 12 的自变量**(2026-10-11 晚提出,2026-10-12 **证伪**)|
两个独立实验都**否定**它:<br>① A/B(`scripts/experiment_layout_verify.py --solve`):按本次构建槽位序生成的 collect vs 仓库旧模板,**两组 9/9 全是 12**;<br>② **严格对照**(决定性):同一批取值、**同一长度 920**、只把槽位序**完全颠倒**,两组 **6/6 全是 12**。<br>→ **槽位序不影响 verify 结果**。已知 #31(槽位序按构建随机化)与 #34/#35/#36/#37(可静态导出)**仍然成立且是有价值的资产**,但它们**不是 12 的原因**。|
| **请求头保真度是 12 的自变量** | 按真实抓包逐 endpoint 保真:资源 GET 用 `Sec-Fetch-Dest=script/image` + `Mode=no-cors` + `Storage-Access=active` + cookie,POST 用 `empty/cors` + Origin 且**不带** cookie;仍 12 |
| **collect 明文结构/取值是 12 的自变量** | ~~我们的 collect 明文与 1ms 真实成功样本逐字节同构(同 1353B、同字段序、同取值,仅回填轨迹/时间戳/盐),仍 12~~ ⚠️ **该否定实验本身有缺陷(2026-10-11 晚推翻)**:所谓"同字段序"只是与**模板来源的那一次**样本比,而 `cd` 槽位序**按会话随机**(已知 #31),所以它没检验到真正会变的维度。修正后重开为:**"cd 槽位序必须与本次会话构建一致"是 12 的头号可疑自变量**(已知 #32)|
| **H5:1ms 后端用 `identity_context` 把会话标记为可信** | 拿抓包里**签名过的** `identity_context`(599B `emctx1.*`) 先调 `POST /api/v1/captcha/params`(响应与不带时**完全相同**,无额外 token)再解题,仍 12 → H5 排除 |
| **"请求侧保真度不够"这一整类** | 直接反证:aid 190758559 上 **未做任何保真的 httpx + 无 cookie** 也拿到 ticket(0);即请求侧细节本就足够,12 与保真度无关 |
### 2026-10-12 新增推翻的假设(对照 `docs/tdc-案例.md`)
| 假设 | 否定证据 |
| --- | --- |
| **"整体加密"是 collect 的结构**(旧 `crypto.encrypt_string` / `tdc_collectgen.encrypt_plain` 的做法) | 判决书据 3 条,互相独立:<br>① 真机明文有 **3 处连续空格串**(`+5 / +10 / +24`),`\x00` 补齐不会产生空格;<br>② **不需要密钥**的纯算术:4 段长度 360/72/312/40 → base64 480+96+416+56 = **1048**,与真实 collect 的 1048 字符精确相等(差一个字节都不行);<br>③ `==` 只出现在 1046/1048,恰好对应 sd 段 33B→XTEA 补 7→40B。<br>→ 改为 `crypto.build_collect()`(4 段独立加密 + 空格 pad 到 24)。旧函数**仍保留但已不是主路径**。<br>⚠️ 注意被推翻的是**加密结构**,不是槽数模型——cd 仍是 38 槽(见已知 #42)。 |
| ~~**`kind=2` 的项"仍产出 1 槽"是错的**~~ ⚠️ **本条是我 2026-10-12 的误判,已撤回** | 我曾据案例 §5.5 断言 `type_flag==2`"不进 cd"、并据此认为旧"37 项→38 槽"推导被推翻。**实测否定了这个断言**:真机明文 cd 就是 **38 槽**,轨迹位于**第 10 槽**。→ 真相是「产 1 槽」与「触发分段」**同时成立**,旧已知 #36 仍然正确。教训:引用外部案例时必须用本项目数据复核,不能拿案例文字直接覆盖已有实测(见已知 #42)。 |
| **模块 id 可当跨构建对齐锚点** | 清单交集 28 个模块里 `id→语义` 完全相同仅 **1/11**;案例 §1.2 明确"37 个模块的 ID 随机重新分配"。→ 改用**语义名**做锚点(已知 #44)。 |
| **自创的「指令直方图」是够用的锚点** | 直方图跨构建交集 37/37 但单构建内唯一仅 **20/37**;语义名方案 **37/37 唯一**。直方图降级为兜底锚点。 |
| **fp_salt 可用固定字符串识别**(旧规则写死了 `AyqhIdr`) | 4 份新构建里分别是 `AyqhIdr` / `QtDKYSO` / `ryieWP_` / `mYmmgLx` —— **每构建都变**。改用结构规则识别(已知 #46)。 |
| **"按模块 id 配对就能跨构建对齐"** | 同 #43;且我用"模块 id 相等"做校验时**自己误判过一次**(看到 35/37 不一致就以为配对错了)—— 实际上 id 不一致是**预期行为**,不能用来否定配对。正确校验见已知 #48。 |
### 2026-10-12 图标点选(2408)新增排除
| 假设 | 否定证据 |
| --- | --- |
| **"2408 的 12/50 是 YOLO 召回不足 / patch 太小 / box 偏移导致点错"**(旧 §五 的头号方向) | **目视逐点核对 8 次跑 / 24 个点 24/24 全对**(已知 #64):把落盘的提示图切 3 片,与"实际点击点 ±32px 的背景 patch"并排放大,每次每个点都落在与提示**同序**的目标图标上;连 4 次 `12` 与 2 次 `50` 也都是全对。→ **点错不是自变量**。 |
| **"1ms 与 mliev 的图标课题配置不同(答案坐标系 / 格式 / 元素表)"** | 两侧 dump **逐字段相同**(已知 #69):`data_type=["DynAnswerType_POS"]`、`size_2d=[672,480]`、底图 672x480、提示图 170x50、`ins_elem_cfg` 只有"提示图元素 id=1"、`verify_trigger_cfg={verify_icon:true}`。→ 配置侧解释不了两靶差异。 |
| **"提示图取错了(应当用 `dyn.sprite_url`,我们改写了底图 URL 的 `img_index`)"** | 同课题里两条 URL 的 `image=` token **本来就相同**,取回的提示图 **md5 逐字节一致**(已知 #70)。→ 这是个真实但不影响结果的偏差。 |
| **"轨迹落在末槽时 `chunk2` 仍要补前导逗号"** | 旧写法在该构建上拼出 `{"cd":[<37 槽>,<traj>,],` —— **非法 JSON**(已知 #65)。真机是 `JSON.stringify` 语义:逗号只在**槽与槽之间**,两端空区间都不补。已修 + 回归。 |
---
## 四、当前实现(纯算单一路径)
```
TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
├─ extract_xtea_key(tdc.js) **纯 Python 静态还原** (tcaptcha/tdc_key.py, 零 Node)
│ ├─ tdc_opcodes.py 按构建从解释器源码推导 opcode 表 (编号每份都重排)
│ ├─ tools/tdc_disasm.py 可达性 CFG 反汇编 (支持注入按构建的表)
│ ├─ 定位 XTEA 闭包 (delta 常数) -> 两层捕获链选出真密钥数组
│ └─ 读出 4 个字 (源串+偏移+下标) + 折入轮加盐
│ (Node 插桩版本保留为**兜底**: 纯 Python 失败时自动回落)
├─ generate_collect_success() 模板回填: 轨迹 / 当前时间戳 / 随机盐 → XTEA → Base64 (单层 URL 编码)
└─ extract_eks(tdc.js) 通用正则取最长 Base64 候选 (不再写死变量名)
```
* 纯 Python 提取器: `tcaptcha/tdc_key.py` (算法) + `tcaptcha/tdc_opcodes.py` (opcode 表推导)
* **2026-10-12 新增(按案例重做后的主路径):**
* `tools/tdc_modules.py` —— 字符串+结构指纹识别 37 个采集器(**37/37 唯一**,案例 §5.3)
* `tcaptcha/crypto.py::build_collect()` —— **4 段独立加密**(前 3 段空格 pad 到 24,sd 不 pad)
* `tcaptcha/crypto.py::split_collect_segments()` —— 从已解密明文切回 4 段(对拍/标定用)
* `tools/tdc_collectgen.py::reorder(sample, dst, src)` —— 按语义名锚点**跨构建**对齐(旧恒等 bug 已修)
* `scripts/fetch_online_samples.py` —— 在线抓取全新 tdc.js(**新研究的数据来源**)
* CLI: `tools/tdc_key_pure.py [tdc.js] [-v] [--opcodes]`
* 模板: `tcaptcha/models/success_collect_template.b64`(真实成功样本明文)
* 加密: `tcaptcha/crypto.py` 中**唯一**的教科书 XTEA
* Node 现存用途: 仅 `tdc_worker.js` 兜底提取密钥(主路径已不需要)
### 2026-10-12 起的新主路径(替代上面 `generate_collect_success` 的模板回填)
```
1. scripts/fetch_online_samples.py -> har/online/<ts>/tdc_*.js(本次会话的构建)
2. tdc_key.extract_xtea_key_pure(js) -> 本次构建的 XTEA 密钥(纯 Python,零 Node)
3. tdc_modules.analyze(js) -> 37 项的语义名 + require 顺序(37/37 唯一)
4. tdc_collectgen.reorder(cd, dst, src) -> 把标定样本对齐到本次构建(跨构建锚点)
5. crypto.build_collect(c1,traj,c2,sd,k) -> 4 段独立加密后拼接 = collect
```
✅ 第 4 步已可**纯离线**完成:`tcaptcha/tdc_values.py` 为 37 个语义名提供取值生成器(已知 #54/#57)。
---
## 五、下一步(可被下一轮直接执行)
### 🆕 2026-10-12 优先(按案例重做后的收尾,按成本排序)
#### 🔴 2026-10-12 晚:2408 图标点选收尾(下一轮从这里开始)
现状(判读见 §三 新增排除 + 已知 #64~#72 + 专文 **`har/1ms_dianxuan/ICON2408.md`**):
**图标链路本身已经通了,而且"点得不准"这条怀疑已被定量排除** ——
* 可复现靶子 aid **190758559**:本轮累计 **正确作答 22 次 → `0` × 20**(另 2 次 `Connection reset by peer` 网络层失败);
而**故意扰动 5 次(交换前两点 ×2 / 整体平移 50px ×2 / 75px ×1)→ `50` × 5** —— 靶子对我们非常稳定,是理想回归台;
* **点击精度**:点击点离「图标暗色笔画质心」仅 **2.2px(1ms)/ 3.3px(mliev)**;
* **服务端容差**:给答案整体平移 `25px` 仍 `0`、`50px` 才 `50`(已知 #71)→ 精度富余**一个数量级**;
* 1ms 上三次 `12` 的答案里,每个目标类都有 **top-1 概率 = 1.0** 的候选;判读图 **24/24 全对**(已知 #72)。
→ 所以 `12` / `50` **都不是"点错"**:`50` = 答案被否(#66 已用"故意答错"负对照标定),
`12` = 包级/会话级拒答(答案还没被裁判)。1ms 在同一时段、同一代码下会在
`0 / 50 / 12` 之间漂移,且一次爆发里呈现"前 2~4 次 `0`、之后 `12`"。
**下一轮只剩两件事**(按成本排序):
1. **判 `12` 是不是"按 aid+IP 的令牌桶/频控"**(唯一还有区分度的自变量)。做法:
静默 ≥20 分钟后**只发 1 次**看是否回到 `0`;再对"同一轮里成功次数是否有限"做复现。
本轮 `send_campaign.py` 已记录**每次求解的 prehandle 次数**(`points.json.prehandles`),
可直接把「请求量 ↔ errorCode」画出来。**注意:连打会自伤,测 1ms 时务必拉开间隔。**
若确认是频控 → 结论就是"1ms 需低频使用,回归一律用 190758559",不必再改代码。
2. **只在需要时**再追"为什么 1ms 会在正确答案上给 `50`"(`50` 的语义是答案被否,
但已知 #72 表明答案是对的)。优先级低于 1,因为它已不影响交付能力。
复验入口(**仓内工具**,2026-10-12 起替代 /tmp 一次性脚本):
```bash
# 批量真实发送 + 落图 + 统计(支持 --offset 容差标定 / --swap 故意答错 / --kind icon|text|slider|any)
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 8
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 2 --offset 25,0
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 2 --swap # 负对照,期望 50
# 目视判读图(列 = 提示图第 j 个目标 vs 第 j 个点击点 patch;红框上标了 #1/#2/#3 顺序)
.venv/bin/python scripts/verify/judge_icons.py har/1ms_dianxuan/icon_campaign/<tag>
```
产物默认写到项目内 `har/1ms_dianxuan/icon_campaign/<tag>/`(`har/**` 只跟踪 `.md`,
所以图片不入库但留在项目树里可复核);本轮的原始产物已就位。
---
### 既有优先项
0. ✅ **已完成(2026-10-12)—— 纯离线生成已打通**。做法:真 Chrome 取「同会话构建 +
同会话 `TDC.getData(true)`」按位置对齐,拿到 37 个语义名 -> 真值(38/38 槽,已知 #54),
再固化成 `tcaptcha/tdc_values.py`(分**静态画像 / 会话量 / 每次采集量**三层)。
复验:`verify_values.py` 对 4 份真机明文 **152/152 逐槽全等**;
`verify_offline.py` 纯离线闭环**逐槽全等 + 分段几何一致**。
仍未标定:**真实拖拽轨迹的采样分布**(本会话的 61 点轨迹是 CDP 注入 mousemove 得到的,
真机人手拖拽的 dt / 位移分布还没采过 —— 现有的 `_success_style_trajectory()` 是旧拟合)。
1. ✅ **已完成**:4 段结构已接进生成链路 —— `tdc_values.build_plain()` 按
「轨迹槽 = 分界点」产出 `chunk1 / trajectory / chunk2 / sd`,直接喂 `crypto.build_collect()`。
⚠️ 注意 pad **可能只有 1 个空格**(已知 #50),切分/对拍一律用修好的
`crypto.split_collect_segments()`,别再自己写 ` {2,}` 找 pad。
2. ✅ **已完成(2026-10-12)**:端到端验证直接上 1ms 签到页文字点选,**3/3 拿到 `errorCode=0` +
真 ticket**(见已知 #58)。不需要再走宽松 aid 做前置台阶 —— 直接打真靶即可。
3. ✅ **已完成(2026-10-12)—— 12 的归因收口**。结论与当时的预期**相反**:#40 假设的
"谁在发 HTTP" 被推翻。真正的自变量是**collect 有没有用本次会话的构建生成**:
同 aid / 同客户端 / 同求解器、答案都对,`python_values`(本次构建生成)**3/3 = 0**,
`python_keyed`(旧模板回填,槽序来自另一份构建、屏串是另一台设备的)**2/2 = 12**。
复现入口:`.venv/bin/python scripts/verify/send_test.py --mode python_values`
(对照加 `--mode python_keyed`)。
新暴露的可复现特性:每次 prehandle 的构建都不同 → 段几何随构建变(461/625/147/16、81/708/529/16…),
所以**任何"固定模板"的路线都不可能长期成立**,必须按会话构建生成。
### 既有项
0. ~~**静态提取 XTEA 密钥(去掉最后一个 Node 依赖)**~~ —— **已完成**(已知 #19~#22):
`tcaptcha/tdc_key.py` 纯 Python 零 Node,6/6 构建与 Node 逐字一致。
残留增强项(非阻塞):① 若服务端改了 handler 语义,`tdc_opcodes.derive_opcode_table`
会抛"指纹未识别",此时应更新参考指纹而非改定位逻辑;② 抗"多套诱饵数组 >2 层捕获"的
兜底(当前两层已覆盖实测全部 6 份)。
1. **拿下 1ms 的 12 —— 归因已改写(2026-10-11 晚,见已知 #29~#32)**
旧归因"1ms 就是不放行非真机页面的会话"**证据不足**,已被下面这条更可证伪的链取代:
* 硬事实:
① 同一个 tdc.js URL **每次请求都返回不同构建**(4/4 md5 与密钥全不同,已知 #29);
② 真机某次成功会话的 collect 明文已**完整解出**(已知 #30);
③ 5 份同日样本的 `cd` 都是 38 槽、同一批值、同一个 `sd`,但**槽位序 5/5 互不相同**(已知 #31);
④ 仓库在用的模板槽位序 == `签到成功-1.har` 那一次(已知 #32),而 `generate_collect_success()`
只回填轨迹/时间戳/entry_url —— **槽位序仍是那一次的**。
* ~~**H-LAYOUT(头号假设)**:槽位序错导致 12~~ → **已证伪(2026-10-12,见 §三)**:
按构建槽位序 vs 旧模板 **9/9 都 12**;同一批值只颠倒顺序(同长度 920)**6/6 都 12**。
**槽位序不是 12 的自变量。**
* **当前最可能的自变量(按成本排序)**:
(a) **collect 的内容/取值本身**(不是顺序):真机那份 38 槽里含 canvas/WebGL/时间戳等
**当次会话真实产物**;我们用"旧模板的值"无论怎么排都不是**本会话**的值。
判定实验:**在真机 iframe 里直接 `TDC.getData(true)` 拿到本会话的 collect,
原样交给 Python 发**(不重排、不改值)——若 Python 也 0,则是"值"的问题;
若仍 12,则排除包内容,只剩"请求方/会话绑定"。
(b) **会话↔请求方绑定**:sess/eks/tkid 与"发出这个 HTTP 的一端"存在绑定
(同包浏览器发 0、Python 发 12)。判定实验 = 上面 (a) 的同一发即可区分。
建议下一步(一条命令,能同时判定 a 与 b):
真机 iframe: `TDC.getData(true)` -> collect (本会话真实产物)
同一 sess/eks/tkid, 分别用浏览器发 / 用 Python 发, 比较 errorCode。
* 附带事实(重要,改变了排查方向):**12 只在"答案正确"之后才判**。
答错一律 50/9(本轮两组在滑块题型下都是 50)——所以任何拿"答错"跑出来的对照都无效。
这解释了为什么早期很多对照实验没有区分度。
2. **可复现靶子(新增资产)**:aid 190758559 / `mliev.login.wang` 短信登录 —— **可反复测**,
用来回归"纯算链路是否完好"(1ms 每天只能试一次,不适合做回归):
```bash
.venv/bin/python - <<'PY'
from tcaptcha.client import TencentCaptchaClient, KIND_TEXT_CLICK
c=TencentCaptchaClient(aid="190758559",
entry_url="https://mliev.login.wang/auth/#/oidc/authorize",
referer="https://mliev.login.wang/", wb="1", accept_kinds=(KIND_TEXT_CLICK,))
print(c.solve()["raw_response"].get("errorCode")) # 期望 0
c.close()
PY
```
3. **补齐新版明文语义 + 槽位表**:语义(哪一槽是 UA / 屏幕串 / WebGL / 轨迹 / 时间戳)已从
已知 #30 的真机明文基本对齐;**关键是每份构建的槽位序**(已知 #31)——
见上面 1(b)。分析入口:`scripts/analyze_collect_layout.py`。
4. ✅ **已完成(2026-10-12)—— 2408 图标点选端到端打通**:纯 ONNX 三件套
(YOLOv8 召回 + ResNet18 13 类分类器 + 全局非冲突组合搜索)直接吃现成权重,
**不需要重训**(原计划"按 1ms 图标类别重训"已被实测推翻:现权重在 1ms 与 mliev 上
点位都正确,见已知 #64)。端到端证据:可复现靶子 aid 190758559 **8/9 = errorCode=0**(#67)。
剩余:**1ms aid=195672344 的 12/50**(与图标链路无关,见本页最上方"2408 收尾")。
5. ✅ **已完成(2026-10-12)—— 滑块 (2401/2402)**:不用模板匹配,改用**拼图块内容与缺口做 ZNCC**
(缺口 = 同一块内容被压暗,真缺口相关性最高、干扰缺口次之)。答案格式从课题 JSON 的
`move_cfg.data_type` 取(`DynAnswerType_POS`,坐标=背景图原始像素),轨迹走`drag_trajectory_points`。
实测 3/3 拿到 `errorCode=0`。**ddddocr 的滑块距离识别已验证不可用**(偏 59~60px,见已知 #62)。
---
## 六、复验入口
```bash
# 一键全量
.venv/bin/python -m tcaptcha.selftest
# 单套
.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + 四段切分回归 + 纯离线闭环
.venv/bin/python -m tcaptcha.tests.test_tdc_key # 纯 Python 密钥提取(5 份构建,零 Node)
.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解
.venv/bin/python -m tcaptcha.tests.test_icon_click_solver # 图标点选求解
.venv/bin/python -m tcaptcha.tests.test_slider_solver # 滑块缺口求解
# 静态分析 CLI(原来的 tools/tdc_*.py 已并入包里)
.venv/bin/python tools/tdc_key_pure.py -v tcaptcha/tdc_sample.js # 取密钥 + 推导过程
.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 按构建推导的 opcode 表
.venv/bin/python -m tcaptcha.telemetry.modules tcaptcha/tdc_sample.js # 37 个语义名识别
.venv/bin/python -m tcaptcha.telemetry.layout tcaptcha/tdc_sample.js # 清单 require 顺序
# 在线抓全新样本(同一个 URL 每次返回不同构建,见 #29)
.venv/bin/python scripts/fetch_online_samples.py --n 4
```
### 标定 / 闭环 / 真实发送(2026-10-12 起的主入口)
```bash
# ⑧ 「同会话构建 + 同会话 collect」的取值标定(这一步打通了纯离线生成的最后阻塞点)
# 先备好同源配对(真 Chrome,9222):
# node tools/cdp_dump_scripts.js --match drag_ele --outdir /tmp/1ms_bridge/scripts
# cp /tmp/1ms_bridge/scripts/<含 VM 的那个>.js /tmp/1ms_bridge/session_build.js
# node tools/cdp_collect.js --match drag_ele --out /tmp/1ms_bridge/session_collect.json
.venv/bin/python scripts/verify/align_session.py # 按位置对齐 -> name_values.json
.venv/bin/python scripts/verify/audit_names.py # 语义名 / 模块结构 / 槽值 联合审计
.venv/bin/python scripts/verify/verify_values.py \
--collects /tmp/1ms_bridge/session_collect.json /tmp/1ms_bridge/session_collect2.json \
/tmp/1ms_bridge/session_collect3.json /tmp/1ms_bridge/session_collect4.json
# 期望: 逐槽全等 152/152(4 份 x 38 槽)
# ⑨ 纯离线闭环(tdc.js -> 37 语义名 -> 38 槽取值 -> 4 段明文 -> 4 段加密 -> 解回对拍)
.venv/bin/python scripts/verify/verify_offline.py /tmp/1ms_bridge/session_build.js
# 期望: 逐槽全等=是、分段几何=一致
# ⑩ 真实发送测试(1ms 签到页;会真的 POST verify)
.venv/bin/python scripts/verify/send_test.py --kind text --mode python_values # 文字点选,期望 0
.venv/bin/python scripts/verify/send_test.py --kind slider --mode python_values # 滑块,期望 0
.venv/bin/python scripts/verify/send_test.py --kind icon --mode python_values # 图标点选
# --kind any 表示三种都接。脚本会 hook generate_telemetry 打印这一颗 collect 的
# 段几何 + 轨迹点数,用来证明发出去的确实是"本次会话构建"生成的包。
# 注意 prehandle 三种题型混发,脚本会按 accept_kinds 自动刷新到目标题型
# (刷新耗尽就退出、不提交 —— 那是没抽到题,不是求解失败)。
```