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:
+59
-26
@@ -158,6 +158,8 @@ class SolveJob:
|
||||
self._cancel = threading.Event()
|
||||
self._challenge_no = 0
|
||||
self._solves: List[Dict[str, Any]] = []
|
||||
self._rounds: List[Dict[str, Any]] = [] # 每一发的结果(count>1 时)
|
||||
self._count = 1
|
||||
self._images: Dict[str, bytes] = {}
|
||||
self._img_lock = threading.Lock()
|
||||
|
||||
@@ -188,11 +190,17 @@ class SolveJob:
|
||||
|
||||
def stats(self) -> Dict[str, Any]:
|
||||
offs = [v for s in self._solves for v in (s.get("off_px") or []) if isinstance(v, (int, float))]
|
||||
ews = [r["errorCode"] for r in self._rounds if r.get("errorCode") is not None]
|
||||
ok_n = sum(1 for r in self._rounds if r.get("success"))
|
||||
return {
|
||||
"challenges": self._challenge_no,
|
||||
"solves": len(self._solves),
|
||||
"points": sum(len(s.get("points") or []) for s in self._solves),
|
||||
"mean_offset_px": round(sum(offs) / len(offs), 1) if offs else None,
|
||||
"count": self._count,
|
||||
"rounds": list(self._rounds),
|
||||
"ok": ok_n,
|
||||
"errorCodes": ews,
|
||||
}
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
@@ -227,21 +235,12 @@ class SolveJob:
|
||||
def _execute(self) -> None:
|
||||
tgt = get_target(self.target_key)
|
||||
kind = str(self.params.get("kind") or "icon")
|
||||
self._count = max(1, min(20, int(self.params.get("count") or 1)))
|
||||
self.events.append(EVT_STARTED, target=self.target_key, aid=tgt["aid"],
|
||||
kind=kind, mode="python_values",
|
||||
label=tgt.get("label", self.target_key),
|
||||
kind=kind, mode="python_values", count=self._count,
|
||||
note=tgt["note"], params={k: v for k, v in self.params.items()})
|
||||
|
||||
if self._throttle:
|
||||
waited = self._throttle(self)
|
||||
if waited:
|
||||
self.events.append(EVT_LOG, level="INFO", logger="web.runner",
|
||||
message="频控节流结束(等待 %.1fs)" % waited)
|
||||
if self.cancelled:
|
||||
self.state = "cancelled"
|
||||
self.events.append(EVT_FINISHED, success=False, errorCode=None, ticket="",
|
||||
elapsed_ms=0, error="任务已取消", stats=self.stats())
|
||||
return
|
||||
|
||||
self.state = "running"
|
||||
bridge = _LogBridge(self._emit_log)
|
||||
loggers = [logging.getLogger("tcaptcha"), logging.getLogger("tcaptcha_client")]
|
||||
@@ -250,12 +249,35 @@ class SolveJob:
|
||||
lg.setLevel(logging.INFO)
|
||||
lg.addHandler(bridge)
|
||||
try:
|
||||
with _Hooks(self):
|
||||
res = self._call_client(tgt, kind)
|
||||
for i in range(1, self._count + 1):
|
||||
if self.cancelled:
|
||||
break
|
||||
if i > 1:
|
||||
self.events.append(EVT_LOG, level="INFO", logger="web.runner",
|
||||
message="—— 第 %d/%d 发 ——" % (i, self._count))
|
||||
if self._throttle:
|
||||
waited = self._throttle(self)
|
||||
if waited:
|
||||
self.events.append(EVT_LOG, level="INFO", logger="web.runner",
|
||||
message="频控节流结束(等待 %.1fs)" % waited)
|
||||
if self.cancelled:
|
||||
break
|
||||
with _Hooks(self):
|
||||
res = self._call_client(tgt, kind)
|
||||
self._finalize(res, round_no=i)
|
||||
finally:
|
||||
for lg in loggers:
|
||||
lg.removeHandler(bridge)
|
||||
self._finalize(res)
|
||||
|
||||
if self.cancelled:
|
||||
self.state = "cancelled"
|
||||
self.events.append(EVT_FINISHED, success=any(r.get("success") for r in self._rounds),
|
||||
errorCode=self._rounds[-1]["errorCode"] if self._rounds else None,
|
||||
ticket=self._rounds[-1].get("ticket") or "",
|
||||
elapsed_ms=self._rounds[-1].get("elapsed_ms"),
|
||||
error=self.error or ("已取消(完成 %d/%d 发)" % (len(self._rounds), self._count)
|
||||
if self.cancelled else ""),
|
||||
stats=self.stats())
|
||||
|
||||
def _emit_log(self, level: str, name: str, message: str) -> None:
|
||||
self.events.append(EVT_LOG, level=level, logger=name, message=message)
|
||||
@@ -278,7 +300,7 @@ class SolveJob:
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
def _finalize(self, res: Dict[str, Any]) -> None:
|
||||
def _finalize(self, res: Dict[str, Any], round_no: int = 1) -> None:
|
||||
raw = res.get("raw_response") or {}
|
||||
ec = raw.get("errorCode")
|
||||
self.result = {
|
||||
@@ -293,13 +315,19 @@ class SolveJob:
|
||||
}
|
||||
self.error = res.get("error") or ""
|
||||
self.state = "done" if res.get("success") else ("error" if self.error else "done")
|
||||
self.events.append(EVT_VERIFY, success=bool(res.get("success")), errorCode=ec,
|
||||
ticket=res.get("ticket") or "", randstr=res.get("randstr") or "",
|
||||
self._rounds.append({"round": round_no, "success": bool(res.get("success")),
|
||||
"errorCode": ec, "kind": res.get("kind") or "",
|
||||
"ticket": res.get("ticket") or "",
|
||||
"elapsed_ms": res.get("elapsed_ms"),
|
||||
"error": res.get("error") or ""})
|
||||
self.events.append(EVT_VERIFY, round=round_no, success=bool(res.get("success")),
|
||||
errorCode=ec, ticket=res.get("ticket") or "",
|
||||
randstr=res.get("randstr") or "",
|
||||
elapsed_ms=res.get("elapsed_ms"))
|
||||
self.events.append(EVT_FINISHED, success=bool(res.get("success")), errorCode=ec,
|
||||
ticket=res.get("ticket") or "",
|
||||
elapsed_ms=res.get("elapsed_ms"),
|
||||
error=self.error, stats=self.stats())
|
||||
self.events.append(EVT_LOG, level="INFO", logger="web.runner",
|
||||
message="第 %d/%d 发结束:errorCode=%s %s"
|
||||
% (round_no, self._count, ec,
|
||||
"✅" if res.get("success") else (self.error or "未通过")))
|
||||
|
||||
# --------------------------------------------------------- 求解器 hook
|
||||
def _wrap_solver(self, key: str, original: Callable) -> Callable:
|
||||
@@ -369,10 +397,15 @@ class SolveJob:
|
||||
data: Dict[str, Any] = {"index": n, "kind": kind, "points": [list(p) for p in points],
|
||||
"off_px": offsets, "centroids": centroids, "note": note}
|
||||
try:
|
||||
data["annotated_url"] = self.add_image(
|
||||
"c%d_annotated.jpg" % n,
|
||||
annotate_points(bg, points, prompt=prompt, offsets=offsets,
|
||||
centroids=centroids, title="第 %d 题" % n))
|
||||
# title 不画(卡片标题里已有),提示图不贴(界面上是独立缩略图,
|
||||
# 贴进去会遮挡右上角的目标图标)—— 详见 render.annotate_points。
|
||||
# 标注图只给点选题画:红圈+序号表达的是"点击顺序",滑块答案是
|
||||
# 缺口位置,画上去只会误导(界面上底图+滑块图已足够核对)。
|
||||
if measurable:
|
||||
data["annotated_url"] = self.add_image(
|
||||
"c%d_annotated.jpg" % n,
|
||||
annotate_points(bg, points, offsets=offsets, centroids=centroids,
|
||||
embed_prompt=False))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
self._solves.append(data)
|
||||
|
||||
Reference in New Issue
Block a user