- 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
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""靶子表 —— aid / 入口 / referer / wb,以及每个 aid **实测出来的频控节流参数**。
|
||
|
||
为什么靶子要带 `min_interval`:`aid=195672344`(1ms) 有 **aid+IP 级频控**,
|
||
短时间连打会被判 `errorCode=12`(2026-10-14 实测,见 har/1ms_dianxuan/PROGRESS.md 已知 #75):
|
||
|
||
间隔 4s × 12 发 -> 只有前 2 发过,之后 10 连 12(icon/text/slider 一起中招)
|
||
冷却 2min + 间隔 30s -> 6 发全 12(冷却不够)
|
||
冷却 25min + 间隔 60s -> 4 发全 0(恢复)
|
||
|
||
所以靶子不是"随便打"的:CLI 与界面都必须按 `min_interval` 节流,否则看到的一片红
|
||
全是频控噪声、会被误读成"求解失败"。而 `190758559`(mliev) 是宽松靶,
|
||
正确作答 22 次拿到 20 次 `errorCode=0`,适合做回归。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Dict, TypedDict
|
||
|
||
|
||
class Target(TypedDict):
|
||
aid: str
|
||
entry_url: str
|
||
referer: str
|
||
wb: str
|
||
min_interval: float # 两次求解之间的最小间隔(秒)—— 实测频控节流
|
||
note: str
|
||
|
||
|
||
TARGETS: Dict[str, Target] = {
|
||
# 用户主靶:1ms 签到页。频控很紧,必须低频。
|
||
"1ms": {
|
||
"aid": "195672344",
|
||
"entry_url": "https://1ms.run/console/personal/benefits/check-in",
|
||
"referer": "https://1ms.run/",
|
||
"wb": "1",
|
||
"min_interval": 60.0,
|
||
"note": "主靶(2404/2408/2402 三种题型混发);有 aid+IP 频控,≥60s 一发、连打不超过 ~2 发",
|
||
},
|
||
# 可复现正样本靶:mliev 短信登录。题率低但宽松,适合回归。
|
||
"mliev": {
|
||
"aid": "190758559",
|
||
"entry_url": "https://mliev.login.wang/auth/#/oidc/authorize",
|
||
"referer": "https://mliev.login.wang/",
|
||
"wb": "1",
|
||
"min_interval": 3.0,
|
||
"note": "回归靶(约 1/10 抽中 2408);宽松,正确作答 22 次→0×20",
|
||
},
|
||
}
|
||
|
||
|
||
def get_target(key: str) -> Target:
|
||
if key not in TARGETS:
|
||
raise KeyError("未知靶子 %r(可选:%s)" % (key, ", ".join(TARGETS)))
|
||
return TARGETS[key]
|
||
|
||
|
||
def describe() -> list:
|
||
"""给界面用的靶子列表。"""
|
||
return [{"key": k, **v} for k, v in TARGETS.items()]
|