Files
hello_js_reverse_skill/tcaptcha/web/README.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

122 lines
5.8 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.
# tcaptcha Web 观测台
把 CLI 的黑盒变成**实时可见的过程**:启一次任务,就能看到
「抽到哪道题 → PoW 命中 → 求解出点位(带标注图与偏离量)→ collect 段几何 → verify 的 errorCode / ticket」。
它**不是**另一套求解逻辑:后端就是 `TencentCaptchaClient.solve()`,与
`scripts/verify/send_test.py` / `send_campaign.py` 跑的是同一条链路。
## 启动
```bash
uv run --extra web python -m tcaptcha.web.app # 默认 127.0.0.1:8770
uv run --extra web python -m tcaptcha.web.app --port 9000 --host 0.0.0.0
uv run --extra web python -m tcaptcha.web.app --no-warmup # 不预热 AI 引擎
```
> `fastapi` / `uvicorn` 装在 **optional extra `web`** 里,核心包(协议 / 遥测 / 求解器 / CLI)
> 完全不依赖它们 —— `import tcaptcha` 不会把 fastapi 拉进来(有回归断言:见本文件末)。
## 架构
```mermaid
flowchart LR
FE["前端(零构建单页)<br/>index.html + app.js"]
API["app.py(FastAPI)<br/>建任务 / SSE / 图片"]
RUN["runner.py<br/>SolveJob + JobManager"]
HOOK["_Hooks / _LogBridge<br/>(临时、成对安装)"]
CORE["protocol.client.solve()<br/>solvers/*"]
R["render.py<br/>标注图 + 点位量尺"]
FE -->|"POST /api/jobs"| API
API --> RUN
RUN -->|"工作线程 + 全局串行 + 频控节流"| CORE
RUN --> HOOK
HOOK -.->|"题型/点位/段几何"| RUN
CORE -.->|"日志"| HOOK
RUN --> R
RUN -->|"SSE 事件"| FE
```
| 文件 | 职责 |
| --- | --- |
| `events.py` | 事件模型;`EVT_*` 类型名前后端共用;`EventLog` 只追加 + 带锁(保证 `seq` 不重复) |
| `runner.py` | `SolveJob`(一次任务)与 `JobManager`(注册表 + 串行 + 节流 + solver 复用) |
| `app.py` | 路由;SSE 支持 `Last-Event-ID` 续传 |
| `static/` | 原生 JS 单页,无打包器、无框架 |
## 接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/` | 单页界面 |
| `GET` | `/api/targets` | 靶子列表(含实测 `min_interval`)+ 冷却倒计时 |
| `POST` | `/api/jobs` | 建任务 `{target, kind, max_retries, max_refreshes}` → 快照 |
| `GET` | `/api/jobs` | 最近任务列表 |
| `GET` | `/api/jobs/{id}` | 任务快照 |
| `GET` | `/api/jobs/{id}/events` | **SSE** 事件流(`?since=N`,兼容 `Last-Event-ID`) |
| `GET` | `/api/jobs/{id}/image/{name}` | 题目图 / 提示图 / 标注图(内存里,任务结束后仍可取) |
| `POST` | `/api/jobs/{id}/cancel` | 取消(**当前这一发结束后**生效,见"已知限制") |
| `GET` | `/api/health` | 健康检查 |
## 事件
信封统一为 `{"seq", "ts", "type", "data"}`,前端按 `data.type` 分派:
| type | 关键字段 |
| --- | --- |
| `job.started` | `target` `aid` `kind` `mode` `note` |
| `challenge` | `index` `kind` `instruction` `bg_url` `prompt_url` |
| `solve` | `points`(**按点击顺序**)`off_px` `centroids` `annotated_url` `note` |
| `telemetry` | `source` `collect_len` `segments`(4 段几何)`traj_points` |
| `verify` | `success` `errorCode` `ticket` `randstr` `elapsed_ms` |
| `log` | `level` `logger` `message`(client 原始日志,见下) |
| `job.finished` | `success` `errorCode` `ticket` `elapsed_ms` `error` `stats` |
**errorCode 语义**(界面里有配色与图例,别误读):
`0` 通过 / `50`·`9` 答案被否(点位或目标错)/ `12` 包级拒答(轨迹指纹或频控)。
## 三个关键设计(都是被约束逼出来的,不是偏好)
1. **工作线程 + 全局串行**
`solve()` 同步阻塞 7~60s,不能挂 HTTP 请求上;且 `onnxruntime` 的 session **不是线程安全的**
—— 所以 `JobManager._serial` 保证同一时刻只跑一个求解。**去掉这把锁就会踩 ONNX 并发**。
2. **日志桥接(零侵入)**
`_LogBridge` 把 `tcaptcha.*` / `tcaptcha_client` 的日志原样变成 `log` 事件。client 里已有 20+ 条
`logger.info`(抽题、PoW 命中、题型、求解结论、遥测长度、verify…),接上它就等于免费得到
一份"和 CLI 一模一样"的轨迹 —— **不需要改 `protocol/client.py` 一行**。
3. **频控节流**
靶子自带**实测**的 `min_interval`(1ms=60s、mliev=3s,依据见 `tcaptcha/targets.py`)。
到点前任务停在 `throttled` 状态并在事件流里说明,否则界面上会出现"一片红"的假故障
—— 那些 `12` 全是频控噪声,不是求解失败。
## 已知限制 / 演进接口
* **取消是"软取消"**:`solve()` 内部不可中断,所以取消只在节流等待中、或当前这一发结束后生效。
* **任务在内存里**(上限 50 个,图片每任务 24 张):重启即丢。要持久化(sqlite)属于 P2。
* **单机自用定位**:默认只监听 `127.0.0.1`,没做鉴权。要暴露出去必须先加认证。
* **B/C 形态的预留接口**:`POST /api/jobs` 的 `params` 会原样进 `SolveJob.params`,
所以"人机协作"(回填人工坐标)与"批量/队列"(多靶子调度)都可以在不改事件模型的前提下接上
—— 前端多一个交互面板、后端多一个 `params` 分支即可。
## 回归
```bash
.venv/bin/python -m tcaptcha.selftest # 核心四套(不受 web 影响)
.venv/bin/python -c "import sys, tcaptcha; assert 'fastapi' not in sys.modules" # 依赖隔离
# SSE 端到端(建任务 -> 事件流 -> 图片可访问)
.venv/bin/python - <<'PY'
import json, httpx
c = httpx.Client(base_url="http://127.0.0.1:8770", timeout=120)
j = c.post("/api/jobs", json={"target": "mliev", "kind": "any"}).json()
print("job", j["id"])
with c.stream("GET", f"/api/jobs/{j['id']}/events") as r:
for line in r.iter_lines():
if line.startswith("data: "):
e = json.loads(line[6:])
print(e["type"], (e["data"].get("errorCode") if e["type"] == "verify" else ""))
PY
```