feat(icon): 轨迹时序指纹修复 —— 1ms 图标 4/8→7/8=0,errorCode 语义与轨迹对拍收口
- 真因:dt 分布与真机严重不符(旧 50% 落 0~16ms,真机 79% 落 13~20ms)+ 点数 随机 57~65(真机 6/6 恒 61)。按 6 条真机轨迹重拟合 _sample_dt,新增 _fit_points 钉死 61 点(补齐用 [0,0,dt] 驻留点,不能复制位移点——净位移会被放大)。 - A/B:1ms 图标 0×4→0×7(前 7 连 0);残余偶发 12 为连发频控形态,低频使用即可。 - errorCode 语义标定:50=答案被否(容差实测 25~40px,我们点偏仅 2~3px), 12=包级拒答;判点位只看 50/0。 - 顺带修 build_plain:轨迹落末槽(k=37)时 chunk2 恒补前导逗号 → 非法 JSON。 - 新增 scripts/verify/send_campaign.py(批量真发+落图+统计)与 judge_icons.py (提示目标 vs 点击点并排判读);账本 #64~#74 + 专文 ICON2408.md。 复验:selftest 全绿;判读图 24/24 点位全对;mliev 图标 9/10=0。
This commit is contained in:
@@ -98,17 +98,31 @@ def default_trajectory() -> List[List[int]]:
|
||||
|
||||
|
||||
def _sample_dt() -> int:
|
||||
"""采样一个事件间隔(ms)。分布按真机成功样本拟合:浏览器合并事件会出现 0。"""
|
||||
"""采样一个事件间隔(ms)——**按 6 条真机轨迹 / 360 个非首点 dt 实测重拟合**(2026-10-12)。
|
||||
|
||||
旧分布(账本里记为"旧拟合")是拿**一条**样本估的,偏差很大:它给了
|
||||
≥50% 概率落在 0~16ms、约 6% 落在 41~200ms。真机实测分布是**紧密聚集在 16~17ms**:
|
||||
|
||||
0-4 0.28% 5-8 1.39% 9-12 6.39%
|
||||
13-16 35.56% 17-20 43.61% 21-25 4.44%
|
||||
26-40 2.78% 41-80 1.39% 81-200 2.22% >200 1.94%
|
||||
(众数 17ms×126 / 16ms×93;均值 25.2,是被极少数长停顿拉高的)
|
||||
|
||||
判据:真机 **79%** 落在 13~20ms,而我们旧实现只有约 36% ——
|
||||
"轨迹事件间隔"是服务端最便宜的一类时序特征,别再拿单条样本估分布。
|
||||
"""
|
||||
r = random.random()
|
||||
if r < 0.50:
|
||||
return random.randint(0, 16)
|
||||
if r < 0.75:
|
||||
return random.randint(17, 24)
|
||||
if r < 0.90:
|
||||
return random.randint(25, 60)
|
||||
if r < 0.98:
|
||||
return random.randint(61, 160)
|
||||
return random.randint(161, 400)
|
||||
if r < 0.0628: # ~6%:偏快的一档(9~12ms)
|
||||
return random.randint(9, 12)
|
||||
if r < 0.8472: # ~78%:主峰 13~20ms(真机 79%)
|
||||
return random.randint(13, 20)
|
||||
if r < 0.9000: # ~5%:21~25ms
|
||||
return random.randint(21, 25)
|
||||
if r < 0.9300: # ~3%:26~40ms
|
||||
return random.randint(26, 40)
|
||||
if r < 0.9700: # ~4%:41~200ms(偶尔的思考停顿)
|
||||
return random.randint(41, 200)
|
||||
return random.randint(201, 600) # ~3%:罕见的长时间歇
|
||||
|
||||
|
||||
def _to_display(x: float, y: float, img_w: int) -> Tuple[float, float]:
|
||||
@@ -117,16 +131,38 @@ def _to_display(x: float, y: float, img_w: int) -> Tuple[float, float]:
|
||||
return 20 + x * scale, 60 + y * scale
|
||||
|
||||
|
||||
REAL_TRAJECTORY_LEN = 61 # 真机硬不变量:6/6 条成功轨迹都是 61 点(见 _fit_points)
|
||||
|
||||
|
||||
def _fit_points(events: List[List[int]]) -> List[List[int]]:
|
||||
"""把轨迹**补齐/截断到真机的 61 点**,末尾再钉上固定收尾 `[1,1,12]`。
|
||||
|
||||
为什么点数是硬约束:4 份真机抓包 / **6 条点选成功轨迹**,点数**统统是 61**
|
||||
(`点选.har` 里 3 次提交也一样),一条都不差 —— 这是服务端最容易查的一类
|
||||
结构特征,而旧实现每次随机 57~65 点,等于每发一枪都自带一个异常值。
|
||||
|
||||
⚠️ 补齐必须用 `[0,0,dt]`(驻留/微抖动),**不能**复制最后一个 `[dx,dy,dt]` ——
|
||||
复制会把位移量重复计入,净位移被放大(实测 166 vs 期望 139.5)。
|
||||
"""
|
||||
body = [e for e in events if e != [1, 1, 12]]
|
||||
target = REAL_TRAJECTORY_LEN - 1
|
||||
if len(body) >= target:
|
||||
body = body[:target]
|
||||
else:
|
||||
while len(body) < target: # 驻留/微抖动:位移为 0
|
||||
body.append([0, 0, _sample_dt()])
|
||||
return body + [[1, 1, 12]]
|
||||
|
||||
|
||||
def trajectory_points(click_points: Sequence[Sequence[int]],
|
||||
img_w: int = 672) -> List[List[int]]:
|
||||
"""按点击点生成鼠标轨迹:首点绝对坐标 + 增量点 + 固定收尾 `[1,1,12]`。
|
||||
|
||||
形状与真机一致(3 份样本逐字核对):
|
||||
* 首点是**绝对坐标**(含 300~600ms 初始停顿)
|
||||
* 之后全是**增量** `[dx,dy,dt]`
|
||||
* **最后一点固定是 `[1,1,12]`** —— 真机成功样本与"没动鼠标"样本都这么收尾
|
||||
(没动鼠标时整条轨迹就是 `[[1,1,12]]`)
|
||||
dt 分布按成功样本实测拟合(旧 crypto._success_style_trajectory 的统计)。
|
||||
形状与真机一致(真机 6 条逐条核对,2026-10-12):
|
||||
* 首点是**绝对坐标**(含一个"进入前"的停顿,真机实测 7~14766ms)
|
||||
* 之后全是**增量** `[dx,dy,dt]`,dt 分布见 `_sample_dt`
|
||||
* **最后一点固定是 `[1,1,12]`**
|
||||
* **总点数固定 61**(真机 6/6 硬不变量,见 `_fit_points`)
|
||||
"""
|
||||
scale = 320.0 / img_w
|
||||
targets = [(int(round(20 + x * scale)), int(round(60 + y * scale)))
|
||||
@@ -143,8 +179,7 @@ def trajectory_points(click_points: Sequence[Sequence[int]],
|
||||
cur = [int(nx), int(ny)]
|
||||
for _ in range(random.randint(2, 5)): # 命中后短暂驻留
|
||||
events.append([random.randint(-1, 1), random.randint(-1, 1), _sample_dt()])
|
||||
events.append([1, 1, 12]) # 固定收尾(真机如此)
|
||||
return events
|
||||
return _fit_points(events)
|
||||
|
||||
|
||||
def drag_trajectory_points(start: Sequence[float], end: Sequence[float],
|
||||
@@ -152,7 +187,11 @@ def drag_trajectory_points(start: Sequence[float], end: Sequence[float],
|
||||
"""滑块**拖动**轨迹:按下 -> 横向移动 -> 松手。
|
||||
|
||||
与 `trajectory_points` 同一套口径(输入是背景图原始像素,输出是 iframe 显示坐标),
|
||||
同样以固定 `[1,1,12]` 收尾。速度曲线用 ease-out:人手拖动是先快后慢、末尾微调。
|
||||
同样以固定 `[1,1,12]` 收尾、dt 同一套分布。速度曲线用 ease-out:
|
||||
人手拖动是先快后慢、末尾微调。
|
||||
|
||||
⚠️ **拖动轨迹不套 61 点**:那个硬不变量是从**点选**轨迹量出来的(6/6),
|
||||
拖动是另一种交互(按下→横移→松手),点数按 steps 走。别外推。
|
||||
"""
|
||||
sx, sy = _to_display(float(start[0]), float(start[1]), img_w)
|
||||
ex, ey = _to_display(float(end[0]), float(end[1]), img_w)
|
||||
@@ -374,9 +413,14 @@ def build_plain(cd: Sequence[Any], rows: Sequence[Dict],
|
||||
head = ",".join(js(v) for v in cd[:k])
|
||||
tail = ",".join(js(v) for v in cd[k + 1:])
|
||||
return {
|
||||
"chunk1": '{"cd":[' + head + ",",
|
||||
# ⚠️ 逗号是**槽与槽之间**的分隔符(真机是 JSON.stringify 语义),所以两端的空区间
|
||||
# 都**不能**再补分隔逗号,否则会拼出 `{"cd":[,` 或 `...,],` 这种非法 JSON。
|
||||
# · 轨迹正好是第 0 槽 -> chunk1 就是 '{"cd":['(真机成功包实测,见账本已知 #58)
|
||||
# · 轨迹正好是最后 1 槽 -> chunk2 就是 '],'(38 槽里 k=37 的构建实测,
|
||||
# 2026-10-12;此前这里恒补前导逗号 -> 拼出非法 JSON)
|
||||
"chunk1": '{"cd":[' + (head + "," if head else ""),
|
||||
"trajectory": js(cd[k]),
|
||||
"chunk2": "," + tail + "],",
|
||||
"chunk2": ("," + tail + "],") if tail else "],",
|
||||
"sd": '"sd":' + js(sd if sd is not None else {"od": "C"}) + "}",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user