feat(collect): 纯离线生成打通 1ms 签到文字点选 —— errorCode=0 收口
做了什么:
- 新增 tcaptcha/tdc_values.py: 37 个语义名的取值生成器(静态画像 / 会话量 /
每次采集量三层)+ build_plain 四段组装 + generate_collect 纯离线出包
- crypto: 新增 generate_telemetry(mode="python_values");轨迹生成统一到
trajectory_points()(首点绝对坐标 + 增量 + 固定收尾 [1,1,12])
- crypto.split_collect_segments 重写: pad 只有 1 个空格时旧 ` {2,}` 判据会漏段,
且 UA 串里的普通空格会假阳性 —— 改为「模 24 算术 + 不落在 JSON 字符串内部 + 结构校验」
- tools/tdc_modules: trajectory 指纹原先指向只做 JSON.stringify 的模块,改为
mouseMoveEvent/touchStartEvent(真轨迹模块),37/37 不变
- client 默认 telemetry_mode 改为 python_values
- 新增 scripts/verify/{align_session,audit_names,verify_values,verify_offline,send_test}.py
与 tools/{cdp_collect,cdp_dump_scripts,tdc_profile}.py
为什么:
- 账本 #40「谁在发 HTTP 是 12 的剩余自变量」被推翻。同 aid/同客户端/同求解器、
答案都对,只换 collect 构造方式: 本次会话构建生成 -> 0 (4/4),
旧模板回填 -> 12 (2/2)。真因是 collect 必须与本次会话构建同源。
怎么验证:
- .venv/bin/python -m tcaptcha.selftest 全绿(含新增四段切分与纯离线闭环回归)
- scripts/verify/verify_values.py 4 份真机明文 152/152 逐槽全等
- scripts/verify/verify_offline.py 逐槽全等 + 分段几何一致
- scripts/verify/send_test.py --mode python_values 1ms 签到页文字点选 errorCode=0 x4
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""send_test.py —— 真实发送测试:1ms 签到页的**文字点选**。
|
||||
|
||||
只打这一条链路(用户指定):
|
||||
aid 195672344 (1ms.run)
|
||||
入口 https://1ms.run/console/personal/benefits/check-in
|
||||
题型 2404 文字点选(prehandle 会混发滑块/图标,脚本按 accept_kinds 刷新到文字点选为止)
|
||||
遥测 --mode python_values(默认)= tcaptcha/tdc_values.py 的**纯离线**生成
|
||||
|
||||
脚本会 hook 客户端的 generate_telemetry,把这一颗 collect 的段几何打出来 ——
|
||||
用来证明"发出去的确实是新链路生成的包",而不是旧模板回填。
|
||||
|
||||
用法:
|
||||
.venv/bin/python scripts/verify/send_test.py
|
||||
.venv/bin/python scripts/verify/send_test.py --mode python_keyed # 对照组:旧模板回填
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
import tcaptcha.client as client_mod # noqa: E402
|
||||
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
|
||||
|
||||
AID = "195672344"
|
||||
ENTRY = "https://1ms.run/console/personal/benefits/check-in"
|
||||
REFERER = "https://1ms.run/"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="1ms 签到页文字点选 · 真实发送测试")
|
||||
ap.add_argument("--mode", default="python_values",
|
||||
choices=["python_values", "python_keyed", "python"])
|
||||
ap.add_argument("--max-refreshes", type=int, default=12)
|
||||
ap.add_argument("--max-retries", type=int, default=2)
|
||||
args = ap.parse_args()
|
||||
|
||||
seen = {}
|
||||
real_gen = client_mod.generate_telemetry
|
||||
|
||||
def hooked(tdc_code, points, **kw):
|
||||
t0 = time.time()
|
||||
out = real_gen(tdc_code, points, **kw)
|
||||
seen.update({
|
||||
"source": out.get("source"),
|
||||
"collect_len": len(out.get("collect") or ""),
|
||||
"eks_len": len(out.get("eks") or ""),
|
||||
"gen_ms": int((time.time() - t0) * 1000),
|
||||
"points": [list(p) for p in points],
|
||||
})
|
||||
# 段几何(新链路才有;旧模板回填没有 segments)
|
||||
if out.get("source") == "python_values":
|
||||
try:
|
||||
from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes
|
||||
import base64
|
||||
plain = xtea_decrypt_bytes(base64.b64decode(out["collect"]),
|
||||
out["key"]).decode("latin1").rstrip("\x00")
|
||||
c1, traj, c2, sd = split_collect_segments(plain)
|
||||
seen["segments"] = [len(c1), len(traj), len(c2), len(sd)]
|
||||
seen["traj_points"] = len(json.loads(traj))
|
||||
seen["traj_tail"] = json.loads(traj)[-1]
|
||||
except Exception as e: # noqa: BLE001
|
||||
seen["segments_err"] = str(e)[:120]
|
||||
print("[hook] %s" % json.dumps(seen, ensure_ascii=False))
|
||||
return out
|
||||
|
||||
client_mod.generate_telemetry = hooked
|
||||
|
||||
c = TencentCaptchaClient(
|
||||
aid=AID, entry_url=ENTRY, referer=REFERER, wb="1",
|
||||
accept_kinds=(KIND_TEXT_CLICK,),
|
||||
telemetry_mode=args.mode,
|
||||
proxy=None,
|
||||
)
|
||||
t0 = time.time()
|
||||
try:
|
||||
res = c.solve(max_retries=args.max_retries, max_refreshes=args.max_refreshes)
|
||||
finally:
|
||||
c.close()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("模式 : %s" % args.mode)
|
||||
print("耗时 : %.1fs" % (time.time() - t0))
|
||||
print("success : %s" % res.get("success"))
|
||||
print("kind : %s 指令=%r" % (res.get("kind"), res.get("instruction")))
|
||||
print("points : %s" % res.get("points"))
|
||||
print("ticket : %r" % res.get("ticket"))
|
||||
print("randstr : %r" % res.get("randstr"))
|
||||
print("errorCode : %s" % (res.get("raw_response") or {}).get("errorCode"))
|
||||
print("原始响应 : %s" % json.dumps(res.get("raw_response") or {},
|
||||
ensure_ascii=False)[:400])
|
||||
print("本颗遥测 : %s" % json.dumps(seen, ensure_ascii=False))
|
||||
print("error : %s" % (res.get("error") or "")[:200])
|
||||
print("=" * 70)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user