feat(web): 靶子显示友好名 + 支持连打次数

- targets.py 增加 label 字段(1ms 签到 / mliev 登录),下拉框、说明、历史表统一用友好名
- POST /api/jobs 支持 count(1~20):一次任务连打 N 发,发间自动按靶子频控节流、可取消
- 每发结果记入 stats.rounds,EVT_VERIFY 带 round,结果区展示「每发结果(通过 x/y)」彩色一览
- 滑块不再生成标注图(红圈+序号只表达点击顺序,对滑块是误导)
- 观测台 README 补齐启动/后台运行/端口占用/自检说明
This commit is contained in:
yml2213
2026-10-15 11:20:24 +08:00
parent 99565f65f0
commit 5c4ff8c4a1
8 changed files with 534 additions and 222 deletions
+33 -4
View File
@@ -57,6 +57,7 @@ app = FastAPI(title="tcaptcha 观测台", version="0.1.0")
class JobRequest(BaseModel):
target: str = Field(default="mliev", description="靶子 key")
kind: str = Field(default="icon", description="text | icon | slider | any")
count: int = Field(default=1, ge=1, le=20, description="连打次数(发间自动按靶子频控节流)")
max_retries: int = Field(default=2, ge=1, le=10)
max_refreshes: int = Field(default=20, ge=1, le=60)
@@ -87,7 +88,8 @@ def create_job(req: JobRequest) -> Dict[str, Any]:
raise HTTPException(400, "未知靶子 %r(可选:%s)" % (req.target, ", ".join(TARGETS)))
if req.kind not in KIND_MAP:
raise HTTPException(400, "未知题型 %r(可选:%s)" % (req.kind, ", ".join(sorted(KIND_MAP))))
job = MANAGER.submit(req.target, {"kind": req.kind, "max_retries": req.max_retries,
job = MANAGER.submit(req.target, {"kind": req.kind, "count": req.count,
"max_retries": req.max_retries,
"max_refreshes": req.max_refreshes})
return job.snapshot()
@@ -174,9 +176,21 @@ if STATIC_DIR.is_dir():
# --------------------------------------------------------------- 启动入口
def port_in_use(host: str, port: int) -> bool:
"""端口是否已被占用(用来给中文提示,而不是把 uvicorn 的 errno 48 直接甩给用户)。"""
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind((host, port))
return False
except OSError:
return True
def main() -> int:
parser = argparse.ArgumentParser(description="tcaptcha Web 观测台")
parser.add_argument("--host", default="127.0.0.1", help="监听地址(默认本机)")
parser.add_argument("--host", default="127.0.0.1",
help="监听地址(默认 127.0.0.1 仅本机;0.0.0.0 供局域网访问,注意无鉴权)")
parser.add_argument("--port", type=int, default=8770, help="监听端口(默认 8770)")
parser.add_argument("--no-warmup", action="store_true", help="不预热 AI 推理引擎")
args = parser.parse_args()
@@ -185,6 +199,16 @@ def main() -> int:
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%H:%M:%S")
if port_in_use(args.host, args.port):
print("\n ✗ 端口 %d 已被占用,服务没起来。\n"
" 查占用进程:lsof -nP -iTCP:%d -sTCP:LISTEN\n"
" 停掉它 :kill <上面的 PID>\n"
" 或换端口 :--port 8771\n" % (args.port, args.port), flush=True)
return 2
if args.host not in ("127.0.0.1", "localhost"):
print("\n ⚠️ 监听地址是 %s —— 界面**没有任何鉴权**,别暴露到公网。\n" % args.host, flush=True)
if not args.no_warmup:
def _warm() -> None:
try:
@@ -198,8 +222,13 @@ def main() -> int:
threading.Thread(target=_warm, name="warmup", daemon=True).start()
import uvicorn
print("\n tcaptcha 观测台: http://%s:%d\n" % (args.host, args.port))
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
print("\n tcaptcha 观测台已启动 → http://%s:%d\n"
" (Ctrl+C 停止;靶子带实测最小间隔,1ms 为 60s,后端会自动节流)\n"
% (args.host, args.port), flush=True)
try:
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
except KeyboardInterrupt:
pass
return 0