Files
yml2213 5c4ff8c4a1 feat(web): 靶子显示友好名 + 支持连打次数
- targets.py 增加 label 字段(1ms 签到 / mliev 登录),下拉框、说明、历史表统一用友好名
- POST /api/jobs 支持 count(1~20):一次任务连打 N 发,发间自动按靶子频控节流、可取消
- 每发结果记入 stats.rounds,EVT_VERIFY 带 round,结果区展示「每发结果(通过 x/y)」彩色一览
- 滑块不再生成标注图(红圈+序号只表达点击顺序,对滑块是误导)
- 观测台 README 补齐启动/后台运行/端口占用/自检说明
2026-10-15 11:20:24 +08:00

169 lines
7.2 KiB
Markdown
Raw Permalink 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
# 首次(或依赖变动后):把 web extra 同步进 .venv
uv sync --extra web
# 启动(默认 127.0.0.1:8770 —— 只有本机能访问)
uv run --extra web python -m tcaptcha.web.app
```
然后浏览器打开 <http://127.0.0.1:8770/>。前台运行,`Ctrl+C` 停止。
启动成功时终端会打印:
```
tcaptcha 观测台已启动 → http://127.0.0.1:8770
(Ctrl+C 停止;靶子带实测最小间隔,1ms 为 60s,后端会自动节流)
```
常用变体:
```bash
uv run --extra web python -m tcaptcha.web.app --port 8899 # 换端口
uv run --extra web python -m tcaptcha.web.app --host 0.0.0.0 # 局域网访问(⚠️ 界面无鉴权)
uv run --extra web python -m tcaptcha.web.app --no-warmup # 跳过 AI 引擎预热,启动更快
```
不想用 uv 也可以直接调项目 venv(两条路径等价,`uv run` 用的就是 `.venv`):
```bash
.venv/bin/python -m tcaptcha.web.app
```
**后台运行 + 停止**(`&` 直接后台会随终端退出而挂掉,所以用 `nohup` 并记下 PID):
```bash
nohup .venv/bin/python -m tcaptcha.web.app > /tmp/tcaptcha-web.log 2>&1 &
echo $! > /tmp/tcaptcha-web.pid
# 停止
kill "$(cat /tmp/tcaptcha-web.pid)"
```
**端口被占用**:启动时会直接给中文提示并以退出码 2 结束(不会打一堆 errno):
```
✗ 端口 8770 已被占用,服务没起来。
查占用进程:lsof -nP -iTCP:8770 -sTCP:LISTEN
停掉它 :kill <上面的 PID>
或换端口 :--port 8771
```
**自检**:`curl -s http://127.0.0.1:8770/api/health` 应返回
`{"status":"online","targets":["1ms","mliev"],"cooldown":{...}}`。
> `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
```