feat(captcha): 增加全题型图片落盘、字形一致性强校验与防秒换拟人交互机制
- 题目图片自动下载: 所有题型原图统一落盘至 scratch/captcha_debug,文字点选自动生成带编号红圈标注图便于肉眼核验 - 字形严格一致性校验: 在 SolveResult 增加 text_match_ok 检查,对图字不符或混淆干扰题直接拒答换题,避免错答引发 errorCode=12 - 防秒换与拟人延时: 遇到非预期题目换题等待 2.8~4.2s (对齐 HAR 真实抓包),求解失败等待 2.5~3.8s,作答前模拟交互 5.5~7.5s - 代理容错增强: 在 OneMsAdapter 捕获代理断流与鉴权异常,支持动态轮换新代理重试
This commit is contained in:
+99
-35
@@ -24,6 +24,7 @@ M32 = 0xFFFFFFFF
|
||||
|
||||
TDC_WORKER_PATH = Path(__file__).resolve().parent / "tdc_worker.js"
|
||||
TDC_BROWSER_PATH = Path(__file__).resolve().parent / "tdc_browser.js"
|
||||
TDC_PROBE_PATH = Path(__file__).resolve().parent.parent / "tools" / "tdc_probe.js"
|
||||
|
||||
# TDC VM Cryptographic Constants
|
||||
DELTA = 2654435769 # 0x9E3779B9
|
||||
@@ -806,63 +807,126 @@ def generate_telemetry_browser(
|
||||
"eks": payload.get("eks") or "", "source": "browser"}
|
||||
|
||||
|
||||
def generate_telemetry_probe(
|
||||
tdc_js_code: str,
|
||||
click_points: List[Tuple[int, int]],
|
||||
aid: str = "2039519451",
|
||||
user_agent: str = DEFAULT_USER_AGENT,
|
||||
referrer: str = "https://app.rainyun.com/",
|
||||
start_time: Optional[float] = None,
|
||||
node_bin: str = "node",
|
||||
timeout: float = 15.0,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
通过 tools/tdc_probe.js 深度 Proxy 探针在真实沙箱中执行服务端的真实 tdc.js。
|
||||
- 绝无 CDP/ChromeDriver 自动化标记泄露 ($cdc_ 探针绕过)
|
||||
- 真实 macOS Chrome ANGLE WebGL + Canvas 指纹注入
|
||||
- 动态派发连续鼠标移动增量轨迹到 document.onmousemove
|
||||
- 原生计算当前会话密钥、时间戳与校验和,彻底避免 errorCode 12。
|
||||
"""
|
||||
if not TDC_PROBE_PATH.exists():
|
||||
raise FileNotFoundError(f"缺少 tdc_probe.js: {TDC_PROBE_PATH}")
|
||||
|
||||
options = {
|
||||
"aid": str(aid),
|
||||
"referrer": referrer,
|
||||
"userAgent": user_agent,
|
||||
"clickPoints": [list(p) for p in click_points],
|
||||
}
|
||||
if start_time is not None:
|
||||
options["startTime"] = int(round(start_time * 1000))
|
||||
|
||||
cmd = [node_bin, str(TDC_PROBE_PATH), "-", json.dumps(options)]
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
input=tdc_js_code,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
env={**os.environ, "NODE_OPTIONS": ""},
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"tdc_probe 执行失败 (rc={proc.returncode}): {proc.stderr[:400]}")
|
||||
|
||||
for line in proc.stdout.splitlines():
|
||||
if line.startswith("{") and "collect" in line:
|
||||
payload = json.loads(line)
|
||||
if not payload.get("success"):
|
||||
raise RuntimeError(f"tdc_probe 内部报错: {payload.get('error')}")
|
||||
collect = payload.get("collect") or ""
|
||||
return {
|
||||
"collect": collect,
|
||||
"tlg": str(payload.get("tlg") or len(collect)),
|
||||
"eks": payload.get("eks") or "",
|
||||
"source": "probe",
|
||||
}
|
||||
|
||||
raise RuntimeError(f"tdc_probe 无有效输出: {proc.stdout[:300]}")
|
||||
|
||||
|
||||
def generate_telemetry(
|
||||
tdc_js_code: str,
|
||||
click_points: List[Tuple[int, int]],
|
||||
aid: str = "2039519451",
|
||||
user_agent: str = DEFAULT_USER_AGENT,
|
||||
referrer: str = "https://app.rainyun.com/",
|
||||
mode: str = "python",
|
||||
start_time: Optional[float] = None,
|
||||
mode: str = "auto",
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
统一遥测生成入口。
|
||||
|
||||
mode="python" : 纯 Python 算法还原 (内置固定密钥; 默认, 无 Node 依赖, ~1ms)
|
||||
mode="python_keyed": **Node 仅提取本次构建的 XTEA 密钥, 加密全部在 Python**
|
||||
mode="node" : Node VM 沙箱执行服务端下发的真实 tdc.js (~0.1s)
|
||||
mode="browser" : 真实 Chrome + CDP 执行真实 tdc.js (~50s)
|
||||
mode="auto" : python -> node -> browser 逐级回退
|
||||
|
||||
为什么默认仍然是纯 Python
|
||||
------------------------
|
||||
实测(2026-10-11)证明服务端 **并不按 tdc.js 的会话密钥解密 collect**:
|
||||
雨云 aid=2039519451 + 纯 Python(硬编码 DEFAULT_KEY) 的 collect + 正确答案
|
||||
→ errorCode=0 并返回真实 ticket (tr03jvqlp225MjNBxjDj_-mnM1VTS8O9)
|
||||
而 `DEFAULT_KEY` 只对仓库里那份 tdc_sample.js 有效(能解出明文
|
||||
`{"cd":[[],"1920-1080..."`), 对当天实时抓的同一 aid 的脚本已失效 —— 说明
|
||||
tdc.js 的密码学参数(4 个密钥字 + 2829068/328986 两个轮加盐)是**每次构建随机**的,
|
||||
但服务端并不校验收到的 collect 是否与 eks 同源。
|
||||
|
||||
因此纯 Python 方案在协议层完全够用; 需要 node/browser 的场景是目标 aid 启用了
|
||||
更严格的风控(例如 1ms.run 的 aid=195672344 在答案正确时返回 errorCode=12),
|
||||
详见 har/1ms_dianxuan/ANALYSIS.md。
|
||||
mode="probe" : 深度 Proxy 语义沙箱 (推荐, 零 CDP 泄露, 原生会话级时间戳/校验和, ~80ms)
|
||||
mode="python" : 纯 Python 算法还原 (内置固定密钥; 无 Node 依赖, 适合宽松 aid)
|
||||
mode="python_keyed": Node 仅提取当前构建的 XTEA 密钥, 模板由 Python 回填
|
||||
mode="node" : 原旧版 Node VM 沙箱
|
||||
mode="browser" : 真实 Chrome + CDP 执行 (~50s)
|
||||
mode="auto" : probe -> python_keyed -> node -> python 逐级回退
|
||||
"""
|
||||
chain = {"python": ["python"], "python_keyed": ["python_keyed"],
|
||||
"node": ["node"], "browser": ["browser"],
|
||||
"auto": ["python", "python_keyed", "node", "browser"],
|
||||
"auto_keyed": ["python_keyed", "node", "python"]}.get(mode, ["python"])
|
||||
chain = {
|
||||
"probe": ["probe"],
|
||||
"python": ["python"],
|
||||
"python_keyed": ["python_keyed"],
|
||||
"node": ["node"],
|
||||
"browser": ["browser"],
|
||||
"auto": ["probe", "python_keyed", "node", "python"],
|
||||
"auto_keyed": ["probe", "python_keyed", "node", "python"],
|
||||
}.get(mode, ["probe"])
|
||||
errors = []
|
||||
for kind in chain:
|
||||
try:
|
||||
if kind == "probe":
|
||||
return generate_telemetry_probe(
|
||||
tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer,
|
||||
start_time=start_time,
|
||||
)
|
||||
if kind == "browser":
|
||||
return generate_telemetry_browser(tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer)
|
||||
return generate_telemetry_browser(
|
||||
tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer,
|
||||
)
|
||||
if kind == "node":
|
||||
return generate_telemetry_node(tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer)
|
||||
return generate_telemetry_node(
|
||||
tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer,
|
||||
)
|
||||
if kind == "python_keyed":
|
||||
# Node 只负责提取当前构建的 XTEA 密钥; 明文/加密/组包全部纯 Python
|
||||
key = extract_xtea_key(tdc_js_code)
|
||||
collect = generate_collect_success(click_points, key)
|
||||
return {"collect": collect, "tlg": str(len(collect)),
|
||||
"eks": extract_eks(tdc_js_code), "source": "python_keyed",
|
||||
"key": key}
|
||||
return {
|
||||
"collect": collect, "tlg": str(len(collect)),
|
||||
"eks": extract_eks(tdc_js_code), "source": "python_keyed",
|
||||
"key": key,
|
||||
}
|
||||
collect = generate_collect(click_points, aid=aid, user_agent=user_agent)
|
||||
return {"collect": collect, "tlg": str(len(collect)),
|
||||
"eks": extract_eks(tdc_js_code), "source": "python"}
|
||||
return {
|
||||
"collect": collect, "tlg": str(len(collect)),
|
||||
"eks": extract_eks(tdc_js_code), "source": "python",
|
||||
}
|
||||
except Exception as exc:
|
||||
errors.append(f"{kind}: {exc}")
|
||||
if mode != "auto":
|
||||
if mode not in ("auto", "auto_keyed"):
|
||||
raise
|
||||
raise RuntimeError("所有遥测后端均失败: " + " | ".join(errors))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user