Repository navigation
Releases
v6.4.0 — 工程债一轮:审计 / 方法语义 / 契约补全 / 前端去重
工程债一轮。9 项工程待办与 5 项 UI/UX 残留全部落地;对外契约补上此前缺失的两类响应。
对外契约
/detect有审计了:这个"替调用方访问任意站点"的动作此前一行审计都不留。现在成功 / 缓存命中 / 失败各落一行(event=detect),字段与/solve同族,GET /audit直接可读。- HTTP 方法语义补齐:
HEAD=GET的头、无 body;OPTIONS→204+Allow;PUT/DELETE/PATCH/TRACE/CONNECT→405+ JSON +Allow(原先是http.server的 501 +text/html,与"非 2xx 一律 JSON"的契约冲突)。带 body 的 405/OPTIONS 会先读掉 body,不会把同一条 keep-alive 连接上的下一条请求顶歪。 - 两个新错误码:
not-found(404) /method-not-allowed(405),进ErrorCode与STATUS_BY_CODE,docs/API.md与openapi.json同步。 - OpenAPI 补服务端错误分支:
/solve的 500/502/503/504、/detect的 500/503/504。新增门禁规则"非 2xx 响应码 ⊆STATUS_BY_CODE",码表从errors.py正则提取(提取失败即报错,不静默通过)。 - 清掉幻影参数:413 的提示曾指向从未实现的
--max-body-bytes,照提示操作会让 argparse 直接退出。改为"上限固定 65536 字节"。 - Go 客户端:新增
SolveOptions{Want,Route,Sitekey}与SolveWithOptions();Clearance补Token/AWSWAFToken,产物校验按want选字段。SolveWithOptions刻意不碰本地缓存——缓存键的前提是产物恒为cf_clearance,而 token 类产物是一次性的。
内部
do_POST的嫁接层旁路下沉为multicaptcha.handle_compat_bypass()(纯函数,注入check_target/entry_for),do_POST227 → 200 行。- 两页(
/与/console)共用solver/_page_common.py,收敛token/getJSON/setKV/setBanner/setState/setEmpty;顺带修掉两份副本已漂移的行为(getJSON统一到 text+parse,非 JSON body 也能显示原文)。 docs/facts.json增module_coverage与service_py_lines:engines.py57%→100%、service.py2073→3006 行两处陈年漂移清零。
验证
| 项 | 结果 |
|---|---|
| Python | 1065 收集 / 1058 passed / 7 skipped |
| 覆盖率 | solver/ 89.92%(门槛 80)、captchas/ 22.21%(门槛 20)、65 模块全部达标 |
| Go | 52 个 Test*,-race 全绿 |
| 门禁自测 | 21/21(新增 3 条负向样例) |
| 两页 E2E(真 Chrome) | 35/35 |
| 公开站 | 构建通过、CSP 17 HTML/20 hash、14 路由 axe 0、content 11/11 |
| 离线压测 | hot 5483 QPS / p99 3.14ms / 0 错;cold 32 并发 0 拒绝;share 4 进程每轮只解 1 次 |
独立审查两轮:首轮 1 P1 + 1 P2 + 6 P3 全部成立并修复(Go 产物解析、keep-alive 错位、不可达的 404 声明、204 的 Content-Length、状态页空态 class、CONNECT、detect 早退耗时、容差注释),二轮复验 Approve。
证据:evidence/v64-engineering-debt.json、plans/证据/(截图与 site-verify)。
升级提示
- 若你的接入方把"不支持的方法 → 501"写进了分支逻辑,需要改成 405(
error_code=method-not-allowed)。 - 未知路径在通过鉴权之后才返回 404;未带 token 仍是 401(不泄露路径是否存在)。
已知限制
do_POST仍有 200 行,未达项目自定的 50 行标准;剩余部分是状态机本体(P3)。CHANGELOG.md的 v5.0 历史条目仍写着--max-body-bytes(历史记录不追改)。- 真机动作(
captchas/求解器实跑、真实 CF 站复验、stage2 快照)与公开站部署仍属待授权项,不在本次发行范围内。
v6.3.2 — 独立审查修复:3D 场景、移动端布局与门禁盲区
主题
独立只读审查发现的三处真缺陷,以及它们暴露出的门禁盲区。
修复
idle 3DE2E 恒真:重构时删掉了生产端__sceneFrames埋点,测试读到的永远是 0,改回frameloop="always"也能通过。恢复埋点,断言改为「指针移动必须产生帧、静止 1s 内新增帧 ≤ 1」。- 320/360px 下流水线 5 个按钮被说明卡遮挡:
.scene__footer改列布局,窄屏把控件排到画布下方;新增 360px 逐按钮elementFromPoint+ 点击断言。 frameloop="demand"下指针视差永不运行:R3F 不会因 pointermove 调度帧,改为 canvas 上显式invalidate(),并补 pointerleave 回正。- 场景标签数值错误:丢
valueMax、输出枚举单位(463bytes、72mb),改用与说明卡统一的格式化。 - 站点版本与测试数漂移:站点 changelog 仍在 6.3.0、benchmarks 仍写 971;同步为 6.3.2 / 1030,并加 content 断言防止再漂移。
- 门禁自测从未接进 CI:
gates.selftest.mjs自称「CI 里跑一步」,实际没有任何 workflow 调用,已接入ci.yml。 importorskip依赖不全 / 裸导入检查可绕过:补 Pillow,改用ast.walk。- 覆盖率命令不一致:文档仍写
--cov=captchas(CI 已实证会被静默忽略),统一为--cov=captchas/。 - 占位符边界:补
localhost.、127.x.x.x、[::1]。
验证
| 门禁 | 结果 |
|---|---|
site-ci / ci |
全绿 |
| site e2e | 19/19(新增移动端控件、指针触发 + 静止停止) |
| site content | 11/11(新增版本/测试数同步断言) |
| verify:site | 14 路由,axe 0 |
| Lighthouse 桌面 | 100/100/100/100;LCP 0.3s、CLS 0、TBT 0ms |
| Python | 1023 passed / 7 skipped |
| 覆盖率 | 64 模块全过 |
| 门禁自测 | 18/18 |
已知限制
未变:真实托管头、真实低端设备、英文母语审校未做;dev 工具链 9 个 high 审计例外。
v6.3.1 — 公开站 V1.1 收口与 CI 覆盖率门禁修复
主题
公开站从"能读"变成"可核对",并把文档里声称的东西补齐;CI 的 captchas 覆盖率门禁真正跑起来。
公开站(site/)
- 3D 场景可操作:3 个挑战面板 / 5 个流水线节点各配 44×44 DOM 按钮与
aria-pressed,
说明卡显示实测值与条件;Backend展开 cfsolve → pydoll → Camoufox → FlareSolverr。 - 移除凭证自动旋转,
frameloop="demand";E2E 断言静止 1s 内新增帧 ≤ 1。 - 首页实测数据摘要 4 行 +
/benchmarks/链接;Hero 三个数字改为引用benchmarks.ts的 id。 - 导航栏滚动超过 24px 才加背景与 blur。
- 构建期门禁:
check-env.mjs拒绝缺失/占位的NEXT_PUBLIC_SITE_URL;
check-out.mjs断言 12 页 canonical/hreflang、sitemap、robots、JSON-LD、OG、占位残留。 - OG 固化为
/og/{zh,en}.png,12 个可索引页全部带og:image(此前只有 2 页)。 - 本地预览构建(
SITE_ALLOW_PLACEHOLDER=1)在页脚渲染提示。 test:e2e18 项:运行时错误、键盘焦点环、reduced-motion、场景控件、static 保留控件、静止无自动旋转、CLS。
现有两页(/、/console)
- 内联 base64 SVG favicon;真实 Chrome 不再因
/favicon.ico404 报错(UI E2E 31/35 → 35/35)。 - 单测 34 → 36。
工程与 CI
tests/test_detect_source.py子进程输出改 UTF-8 解码(Windows GBK 崩溃导致 3 个测试失败)。- captchas 离线测试的可选依赖(opencv-headless / numpy / pillow / onnxruntime)进
requirements-dev.txt;
缺依赖时importorskip而非红。 - 覆盖率门禁改用路径形式
--cov=captchas/,避免模块名解析在 CI 上被静默忽略。
验证
| 门禁 | 结果 |
|---|---|
| site-ci(Lint/Typecheck/Build/CSP/Content/Browser/Lighthouse) | 全绿 |
| ci(python 3.11 / 3.12 / go / findings) | 全绿 |
| Python 全量 | 1023 passed / 7 skipped |
| 覆盖率 | 64 模块全过;solver 89.65%、captchas 22.21% |
| 站点 e2e | 18/18 |
| Lighthouse 桌面 | 100/100/100/100;LCP 0.4s、CLS 0、TBT 0ms |
| 包体 | 初始 232,745 B;3D 241,853 B |
已知限制
- 真实托管头未验证(部署按用户选择停在本地)。
- 移动端 Lighthouse 仍是 11-06 数据。
- dev 工具链 9 个 high 审计例外(
@lhci/cli链,不进产物)。 - 英文文案未经母语审校。
v6.3.0 — 出口实测 / 审计可看 / 真机流量录制 / 出口进 UI
主题:补上两条断链 —— "出口到底是谁"和"挑战页到底在干什么"。
这两件事一直只能靠猜:
cf_clearance与 (domain + 出口 IP + UA) 三者绑定,任何一个不匹配都是静默 403。
但出口 IP 只能从--proxy参数和部署记录里"认为",池里登记的地址从没被实测过。
填错了不会报错,只会表现为"这个代理莫名过不去"。- 挑战页在什么时机、按什么顺序、下发哪个 cookie,是这套项目最需要的第一手材料。
而scripts/capture_sample.py走的是 urllib —— 能拿到 HTML 和脚本正文,但拿不到
真实浏览器会话的 header 顺序与真实值、JS 设的 cookie、子资源时序、状态码序列。
参考:proxypool 的出口 tester 思路(A)、logdy 的"日志要能直接看"(B)、
mitmproxy 的流量 addon 架构(C,用 Playwright page.on 等价实现,零新依赖)。
A. 出口 IP 实测校验
python run.py egress --proxy URL [--proxy-egress-ip URL=IP](= scripts/doctor.py --offline --verify-egress)。
ProxyPool.entries_snapshot():内部只读快照,含raw_url(校验出口必须用真地址)。
这个值永不进日志、永不进 HTTP 响应 —— 池对外的snapshot()仍然脱敏。parse_echo_ip():认三种回显格式(Cloudflare/cdn-cgi/trace的key=value行、
httpbin 的{"origin": ...}、纯 IP 文本)。拿不到合法 IPv4 就返回空串,
绝不把原文当 IP —— 否则回显页 302 到 HTML 时会把一堆标签当成"实测 IP"。verify_egress():五路判断。只有登记值 ≠ 实测值才判红(那是缓存键错位、
凭证发给错出口的源头);未登记 + 实测成功判绿并给出登记建议;探测失败不判红
(代理挂了是另一件事)。- 刻意不写回:实测结果只报告,不自动改池里的
egress_ip。自动写回会把
"登记错了"这种配置问题变成静默自愈,从此再没人发现出口换过。 doctor默认不出站。整个工具只有显式加--verify-egress才发请求。
B. 审计日志可看:GET /audit?lines=N
solver/eventlog.py::read_jsonl_tail():尾部有界读取(AUDIT_TAIL_MAX_BYTES = 512KB,
64KB 分块反向读)。最新在前;碰到写了一半的行就跳过(并发 append 时最后一行常是半截);
返回total_lines_estimate与truncated,让人知道"看到的是尾巴,不是全部"。- 端点要 token,
lines有上限钳制;没有审计文件时明确回"没有审计日志"而不是空数组。 - 控制台新增审计区块:时间/事件/结果/耗时/说明。耗时按人读的方式显示
(16.5 s,不是16530 ms),失败行有标记。
C. 真实浏览器会话流量录制:scripts/record_traffic.py
python scripts/record_traffic.py --url <target> --out samples/x # dry-run(默认)
python scripts/record_traffic.py --url <target> --out samples/x --real # 真录
--engine camoufox|chromium --max-requests N --wait-after SEC --ua UA
产物:traffic.jsonl(一行一个请求/响应配对,新的在后)+ summary.json(按资源类型/
状态码/主机计数、时序、Set-Cookie 名清单、失败原因)+ manifest.json(元数据与预算)。
三条硬约束:
- 默认 dry-run,不传
--real只打印"会去哪儿、会录什么",零网络; - 请求预算
--max-requests(默认 60,硬上限 200,突破需显式--allow-over-budget)。
真机预算不可再生,所以预算必须严格; - 凭证永不落盘:header 走白名单(
X-*一律不记);Cookie/Authorization/X-Api-Key
只记"有没有";Set-Cookie只记名字与个数,值一律不写。body只量大小不落盘。
D. 出口状态进 UI 与控制台
- 状态页新增「出口(代理池)」卡片:出口、
egress_id、评分、连续失败、冷却剩余、
成功/失败、出口 IP 是否已登记。 - 控制台
/health的代理行同步展示。 - 三种"没有 proxies"分开说,否则就是谎报:未带 token(读不到)/带 token 且确实没配/
服务端版本太老没有这个字段。
本轮自查发现并修掉的缺陷
| # | 位置 | 缺陷 |
|---|---|---|
| 1 | record_traffic.py |
录制预算被并发请求打穿:--max-requests 2 实跑录到 5 条 |
| 2 | record_traffic.py |
改成数 len(records)+len(pending) 仍录到 3 条 —— 真因是 resp.body() 切 greenlet 时本条响应还没入账;修法是放行瞬间自增的单调计数器 + 先入账再取可能切 greenlet 的东西 |
| 3 | record_traffic.py |
headers_array 是方法不是属性(写成属性访问 → Set-Cookie 一个都收不到) |
| 4 | record_traffic.py |
headers_array() 返回全部 header 不只是 set-cookie(漏过滤 → ['28','text/html','Thu, 05 Nov…'] 被当成 cookie 名) |
| 5 | console_html.py |
审计区块用 innerHTML + 一个不存在的 esc() → 整块永远显示失败;改成纯 DOM API |
| 6 | console_html.py |
审计表没有 CSS → 时间列独占 414px、其余挤成一坨 |
| 7 | statuspage.py |
没有 token 时谎报"未配置代理池"(实际只是读不到) |
| 8 | statuspage.py |
setEmptyEl 写死 class "chips" → 代理卡片被套上 flex 布局 |
| 9 | statuspage.py |
代理表列贴死(70 与 3 粘在一起)→ 补 padding: 6px 12px |
| 10 | run.py |
回显的命令行带出口口令(http://u:p@h:8080 会进 CI 日志/贴进 issue) |
| 11 | run.py / verify_ui_e2e.py |
ui --only 写错状态名甩 KeyError 栈;egress 的回显地址参数名是 --value(与 doctor 的 --egress-echo-url 不一致) |
| 12 | eventlog.py |
read_audit_tail 曾是 Handler 的 @staticmethod 却按实例属性调用 → AttributeError;移到 eventlog.py 作模块函数 |
| 13 | console_html.py |
顶部「刷新」只刷 /health,审计表停在旧数据 —— 同一页两个「刷新」一个全量一个局部,主操作竟是局部的 |
| 14 | record_traffic.py |
--url https://u:p@host/ 把口令逐字写进 traffic.jsonl / manifest.json / dry-run 输出 —— 违反本模块自己的「凭证永不落盘」 |
| 15 | service.py::_query_lines |
完全不夹取:/audit?lines=100000 吐 3506 行 / 524KB(实测),而 API.md 承诺与 /logs 同一套 [0, 200] 规则。/logs 的夹取在 Solver.logs() 里,/audit 走的 read_jsonl_tail() 只有 max(1, lines),没有上界 |
| 16 | 两条断言 | 上面这条缺陷被两条空转断言漏掉:单测 fixture 只造 6 行、真实调用用的是刚建的空审计文件,lines=99999 无论夹不夹都 <= 200。改法是 fixture 造 MAX_LOG_LINES + 300 行并断言恰好等于上限 |
独立复审(第 1 轮,claude -p 独立进程,范围收窄到 4 个交付文件)
详见 plans/证据/independent-review-v6.3-r1.md。4 条真缺陷,全部已修 + 回归用例 + 变异验证:
| # | 级别 | 缺陷 |
|---|---|---|
| R1 | P1 | nav_error 未脱敏:Playwright 的超时消息内嵌完整 URL,而"挑战页导航超时"正是本工具最常走的分支 → manifest.json 与 stdout 带出口令,traffic.jsonl 却是干净的 |
| R2 | P2 | --engine camoufox 仍强行塞 Windows/Firefox UA:求解侧不加 UA(身份由 persona 决定,缓存键取 navigator.userAgent 实测值),于是"与求解时同一套指纹"在这一维上是假的 |
| R3 | P2 | doctor.py 里任一 --proxy-egress-ip 匹配不上就 return → --verify-egress 静默不执行,报告里连"未实测"都不出现,读起来像"没问题" |
| R4 | P3 | item.partition("=") 按第一个等号切:代理口令含 =(base64 填充)时必然登记失败,还报"出口不在 --proxy 列表里"(它就在)。doctor.py 与 bootstrap.py 两处同源 |
复审另指出 /audit?lines=0 回 1 行而 /logs?lines=0 回 0 行与 API.md 的"同一套夹取规则"
不符 —— 已修(max(1, lines) → max(0, lines))。
这四条里有三条是同一个形状:承诺与主干路不一致(脱敏漏了最常走的分支、宣称同一套指纹
却改了那个维度、宣称实测却在参数写错时静默不测)。单跑一遍 happy path 永远发现不了。
第 1/2 项的回归测试用变异验证过:把实现退回任一旧版,三条预算用例当场变红。
第 13 项同样验证过:把「主刷新」改回不碰审计,audit/顶部刷新会带上审计区块 立刻红(rows=8,期望 9)。
验证
- 本机 Python:964 passed / 7 skipped(总 971);
solver/89.62%,23 个模块全部达标。 - UI 截图验收:31/31,6 种状态(新增
audit)+ 按钮交互 + 逐张看片。 - 真实调用验收:41/41(真进程 + 真 token;新增
/audit的 401、尾读结构、lines钳制与非数字容错)。 - 真实 E2E:40/40;服务契约 golden 13 端点一致;openapi 14 操作与源码一致。
- 录制真跑:本机 chromium 打本地「1 文档 + 1 CSS + 1 JS + 2 图(1 个 404)」站点,
预算 1/2/3/5/6 各档严格、连跑 10 轮无越界;Set-Cookie多值解析、
凭证不落盘(含--url https://u:p@host/)、白名单外 header 不落盘均实测确认。 - 两个引擎都真跑过,并且
manifest.user_agent记的是实测值:
chromium →HeadlessChrome/151;camoufox →Macintosh; Intel Mac OS X 10.15 … Firefox/156。
(旧默认硬塞的是Windows NT 10.0 … Firefox/156—— 与实际 persona 自相矛盾。) - 出口校验真跑:本地模拟回显代理,一致 → ✓、不一致 → ✗ FAIL(exit=2)、未登记 → WARN + 实测成功。
/audit大文件真跑(876KB / 6000 行):lines=100000由 3506 行 / 524KB / 56.7ms
收到 200 行 / 30KB / 3.3ms;lines=0→ 1 行;lines=abc→ 回落默认 50。
v6.2.2 — 修 peak_rss 断言(内核惰性记账,不是 bug)
替换 v6.2.0 / v6.2.1。两者 CI 均为红(本机 Windows 全绿掩盖了平台相关缺陷)。按
policy.md §5发补丁版,不移动历史 tag。
本次修的是什么
v6.2.1 的 CI 只红 python (3.11)(3.12 绿),失败断言:
assert 84922368 >= 84926464 # 差 4096 字节,正好一页
这不是 bug,是操作系统行为:Linux 的 ru_maxrss 按页记账惰性更新,会短暂滞后于 /proc/self/statm 报的当前 RSS。严格断言 peak >= rss 本身就是错的(3.12 绿、3.11 红也说明它时序敏感)。
两处修:
- 实现改为先读当前 RSS、再读峰值(
ru_maxrss单调,后读让窗口最小化); - 断言改成"两者同一量级"(容差 1MB),注释写明不能要求严格 ≥。
三个 CI 红的共同点(比单条修复更值得记)
| 轮次 | 症状 | 本机为什么绿 |
|---|---|---|
| 1 | monkeypatch.setattr(os, "name", "nt") → Linux 上 Path() 变 WindowsPath → pytest INTERNALERROR |
本机 os.name 本来就是 nt |
| 2 | 服务报告 golden 绑了本机研究产物(last_snapshot_at string vs null) |
本机有快照 |
| 3 | peak_rss >= rss 断言(内核惰性记账差一页) |
本机走 Windows 分支,采样方式不同 |
三次都是"环境相关"。这也是本轮把「确定性报告 golden」做成门禁时必须补的那一课:
shape() 的 volatile 判断要排在类型判断之前,且刻意不收计数器
(它们类型恒定,标 volatile 会连"这里是数字"这条检查一起丢掉)。
v6.2 的主体内容(本版包含,详见 v6.2.0 说明)
四类验收全部脚本化,一个命令就能重来:
| 类型 | 命令 | 实测 |
|---|---|---|
| UI 截图 | python run.py ui |
23/23(真 Chrome × 5 状态 + 按钮交互 + 逐张看片) |
| 真实调用 | python run.py realcalls |
37/37 |
| 真实 E2E | python run.py e2e |
40/40(含真浏览器段) |
| 服务契约 golden | python run.py report |
13 端点形状一致 |
| 真实代码复审 | claude -p 独立进程 |
两轮,问题全部关闭 |
加上:engines.py 覆盖率 57% → 100%(撤销 waiver)、FlareSolverr 会话生命周期、
vendors.py 单一事实源(治"命中某特征 ≠ 有盾"这类反复复发的错误)。
验证
- 本机:Python 905 passed / 7 skipped;
solver/89.4%,23 模块达标;文档 9 处一致;openapi 13 操作一致。 - CI:run
41956389364(commit8214c8e)4 job 全绿。 - Linux(WSL, 3.12):纯函数测试全绿;剩余失败经逐条核实全部源于 WSL 自身 loopback 不可用(连最小
ThreadingHTTPServer都连不上),非代码问题。
v6.2.1 — 修两处 CI 红(本机全绿、只有 CI 抓到的平台相关缺陷)
⚠️ 已被 v6.2.2 取代(CI 红:平台相关缺陷,本机 Windows 全绿掩盖了它们)。按 policy.md §5 保留历史 tag 不移动。请使用 v6.2.2。
替换 v6.2.0。v6.2.0 的 tag 指向的 commit CI 是红的,按
policy.md §5
「同类情况默认按发补丁版处理」发本版,不移动 v6.2.0 的 tag。
修复 1:测试改全局 os.name 把 pytest 自己打崩
tests/test_observability.py 用 monkeypatch.setattr(m.os, "name", "nt") 模拟平台 —— m.os 就是全局 os 模块。pathlib 实例化时读到 name == "nt",在 Linux runner 上 Path() 变成 WindowsPath → NotImplementedError,整个 pytest 会话 INTERNALERROR。
同一类问题还有:monkeypatch.setitem(sys.modules, "threading", fake) 换掉了 pytest 自己也要 import 的模块。
修法:平台判定可注入(process_metrics(*, platform_name=None)),生产路径不变;线程探测改成 patch 真正的 threading.active_count。另修一条过强断言(datetime.max.timestamp() 在 Windows 抛 OSError、Linux 能算出巨大值)。
修复 2:服务报告 golden 绑在了本机研究产物上
~ 类型变化:...research.fields.last_snapshot_at 'string' → 'null'
last_snapshot_at 的有无取决于"这台机器是否跑过 scripts/research.py"——golden 实际上拿"有快照的机器"生成,那不是门禁,是环境指纹。
修法:shape() 的 volatile 判断提到类型判断之前(None 曾先返回 "null")。刻意不收计数器(solves/failures/assertions_*):类型恒定,标 volatile 会连"这里是数字"这条检查一起丢掉,等于自己削弱门禁。
新增
tests/test_service_report.py(13 例)把门禁的两个方向锁死:「环境差异不能进契约」与「真实漂移必须被抓」。
验证
- 本机:Python 904 passed / 7 skipped;
solver/89.4%,23 模块达标;文档 9 处一致。 - CI:run
41954927353(commit50253c6)4 job 全绿。
v6.2.0 的主体内容(本版包含)
四类验收全部脚本化:python run.py ui(23/23,真 Chrome × 5 状态 + 按钮交互)、realcalls(37/37)、e2e(40/40)、report(服务契约 golden)。详见 v6.2.0 的说明。
v6.2.0 — 四类验收全脚本化 + 两轮独立复审收口
⚠️ 已被 v6.2.2 取代(CI 红:平台相关缺陷,本机 Windows 全绿掩盖了它们)。按 policy.md §5 保留历史 tag 不移动。请使用 v6.2.2。
本 tag 指向的 commit(
794fe80)CI 是红的:两处平台相关缺陷(测试改全局os.name
让 pytest 在 Linux 上崩;服务报告 golden 绑了本机研究产物)。本机 Windows 全绿掩盖了它们。
请使用 v6.2.1。保留本 Release 只为历史可追溯 —— 按
policy.md §5,tag 不移动。
主题
把「验收」从一次性快照变成可复跑的门禁,并修掉 v6.1/v6.2 交付后暴露的 UI 缺陷与两个公共决策函数的分叉。
四类验收:全部脚本化,一个命令就能重来
| 类型 | 命令 | 实测 |
|---|---|---|
| UI 截图 | python run.py ui |
23/23(真 Chrome × 5 种状态 + 按钮交互 + 逐张看片) |
| 真实调用 | python run.py realcalls |
37/37(真进程 + 真 token,全端点 × 鉴权两态) |
| 真实 E2E | python run.py e2e |
40/40(含真浏览器段) |
| 服务契约 golden | python run.py report |
13 端点形状一致 |
| 真实代码复审 | claude -p 独立进程 |
两轮,问题全部关闭 |
UI:真 Chrome 看截图才发现的 6 条
state=wedged 曾渲染成默认深色徽章(服务已死却看起来正常);控制台把 wedged 当"异常"、丢掉"需重启";键值对因 class 被改而退化成上下堆叠;「上次快照」显示 Invalid Date;11 个扩展类型把卡片撑变形。
决策分叉:最严重的一条会让服务对几乎全部真实站点失效
把「命中某特征」当成「有盾/有厂商」——任何现代站点都用了 localStorage / clipboard / Intl,于是普通 CF 站被判"解不了"。这类错误在开发中被复现了三次,最终收敛成一个谓词 vendors.is_shield(category),并新增 solver/vendors.py 作为「厂商能力表」单一事实源。
工程治理
solver/engines.py覆盖率 57% → 100%,撤销长期 waiver(门禁 56 → 95)。FlareSolverr后端补齐会话生命周期(sessions.create/destroy+ 并发保护)。此前等于每请求泄漏一个远端 driver 进程。tests/test_statuspage.py新增 12 例:该模块此前零行为测试,100% 覆盖率是假象。
破坏性变更
detect.recommend()的unsupported_or_browser语义收紧:命中里任何一家解不了即整页判不可解。protected只统计盾类(Anti-Bot/ANTIBOT/CAPTCHA)。/health状态词表从三态扩为四态(新增wedged)。
v6.0.1 — 修 CI 红(Python 3.11 f-string 兼容)
ℹ️ tag 说明:
v6.0.1已从bc1ccb3移到e413bbd(原 tag 指向的提交缺run.ps1转发改动,checkout 会得到不完整内容;按仓库 policy §5 的 tag 移动例外规则处理)。其后的7f647b5只是把这次发版与一次无效门禁尝试写进workflow_status.md的文档提交。
修 CI 红:run.py 跨行 f-string 在 Python 3.11 下是语法错误
无行为变更,纯兼容性修复。
修复
run.py::cmd_stop 里原本写成跨行 f-string:
print(f"强制终止 PID {pid}({'优雅退出未在 15s 内完成' if graceful
else '无 token 或端点不可用'})")f-string 内换行(PEP 701)是 Python 3.12+ 才允许的。CI 的 python (3.11) job
直接 SyntaxError: unterminated string literal → 测试与覆盖率门槛失败。
本机跑 Python 3.14,本地 652 passed 全绿,所以这个语法错误只有 CI 能抓到 ——
这正是 CI 存在的价值。已改为先算 reason 再插值。
防复发
对全仓 .py 跑 ast.parse(source, feature_version=(3, 11)):0 个不兼容文件
(本地 3.14 的 compileall 抓不到这类问题,用 feature_version 才能拦)。
验证
Python 652 passed / 7 skipped · 覆盖率 87.5% · 21 模块逐模块达标 ·
openapi 13 操作 · 文档数字 9 处一致 · findings 54 条 ·
CI run 41801357561 4 job 全绿。
v6.0.0 的功能内容不受影响,本版只是让它在 Python 3.11 上能跑。
v6.0.0 — 研究工程化(样本驱动、离线可复现的 CF 改版回归)
⚠️ 本 tag 的 CI 是红的:python (3.11)job 在run.py上报SyntaxError(跨行 f-string 是 Python 3.12+ 才有,PEP 701)。功能内容不受影响,请以 v6.0.1 为准 —— 该版只修这一个兼容性问题,CI 4 job 全绿。
主题:研究工程化 —— 把「CF 改版就重新赌一把」变成「样本驱动、离线可复现的回归」
breaking change:无。 纯增量:新增
GET /console、响应头X-Request-Id、
/health的research段、/metrics三条cfsolve_research_*序列。
既有字段、状态码、错误结构、鉴权语义均未改。
研究台(本版核心)
| 能力 | 命令 | 实测 |
|---|---|---|
| 构建前置自检(G0 门禁) | research.py doctor |
EXIT=0,13 项全 PASS |
| 编译 5 个离线分析 example | research.py build |
EXIT=0 / 4m24s |
| 结构断言矩阵 | research.py assertions |
真样本 pass=2 fail=1 skip=6(skip 有明确原因) |
| 两份快照差分 | research.py diff a b |
报出 counter: pass → fail 并映射到补丁 |
| VM 原语缺口 | research.py gaps <id> |
真样本 783 调用点 / 105 缺口 / 35 已实现 |
| 预算护栏 | research.py run --max-sec |
超时 2.023s 精确中止;200KB 输出不死锁 |
| 会话归档/脱敏 | corpus.py add/list/verify/redact |
真样本入库,redact_hits=0 |
其他新增
GET /console 单文件只读控制台(零依赖零构建、CSP、无 innerHTML)· X-Request-Id 贯穿
(响应头/envelope/审计/attempts,且绝不进 metrics label)· run.py 跨平台入口 ·
仓库卫生门禁 · detect 签名新鲜度 · dev 依赖 hypothesis + ruff。
Rust 侧(T-71/T-72)已落地
- T-71 别名规则数据驱动化(
src/alias_rules.json+include_str!),
CFSOLVE_ALIAS_RULES=off逐字恢复旧判定。cargo test --lib alias_rules8 passed
(含反向用例:obj[getKey()]必须判动态)。 - T-72
envelope.rs拆成「候选集 → 逐个校验」,错误带候选/行号并保留原错误串为detail。 - 上游
cargo test --lib:245 passed / 1 failed;失败项经核实为 pre-existing 环境失败
(src/capture/零改动、稳定失败 3/3),与本次改动无关。
本轮实测暴露并修复的 6 个真缺陷
反混淆产物名硬编码 5 处(真样本被拒收)· stage2 断言把「缺输入」报成 fail ·
doctor 不查 cmake/nasm/libclang · example 产物路径判断错(构建成功仍报 FAIL)·
中文 Windows 下乱码与 subprocess GBK 解码崩溃。
⚠️ 本版不含真机结论(必须读)
- 真机 1 次(steamdb.info):反混淆成功,stage2 阶段内存吃满 24.6 GB 后按 PID 终止;
样本仍脱敏入库。未拿到走到 stage2 的完整会话。 - 因此 stage2 的 6 条结构断言在真样本上仍是
skip(既非 pass 也非 fail)。 - 断言强度是字面量级(
literal-substring),不是 AST 级:pass只表示"该标记仍出现",
不表示"结构形态未变"。此 caveat 已写进断言矩阵输出、差分报告与/health。 - 本版不承诺 stage2 对最新变体收敛 —— 只交付「收敛过程可视化」。
验证(全部真实执行)
Python 652 passed / 7 skipped · 覆盖率 87.50% · 21 模块逐模块门禁达标 ·
Go ok · openapi 13 操作双向一致 · 文档数字 9 处一致 · findings 54 条 ·
仓库卫生 exit 0 · 真进程 E2E 32/32 · 热更新演练 17/17 · /console 真浏览器 E2E PASS。
独立复审 3 轮(claude -p 独立进程,只喂 diff 不喂结论)
- 第 1 轮 Request Changes:5 条 blocking(研究指标数据源 schema 错配、
/metrics三态伪造、
脱敏闸门名不副实且 JSON 形态漏检、diff 判据回退、断言强度被夸大)——全部经复核成立并修复。 - 第 2 轮 Request Changes:整篇正则脱敏会写坏 JSON/JS、
assertions_total语义错配致
多变体时三态清零、run_wrapped管道死锁、run.py stop端口子串误杀。 - 第 3 轮:关键项判定 FIXED,剩余 3 项(
last_diff_at取错文件、run子命令参数被覆盖、
rss 查询晚于 terminate)已修并实测。
v5.0.3 — 真机多站端到端确认 + 工具契约固化
主题:真机多站端到端确认 + 工具契约固化(无代码行为变更)。
真机验证(R-E2E-3,2 次目标请求,全部成功)
glassdoor.co.uk:求解 16.5s(12 条 cookie)→ 30s 后回放 200 / 735,957B / 真实标题 /
前 2048 字节 0 挑战 marker —— 端到端成功。- 结合 R-E2E-2(udemy 端到端成功):『求解 → 缓存 → 回放真实内容』在两个不同站点上成立。
- 累计预算 ≈58/80;累计失败仍 1/5。
新增
F-053:多站端到端结论(含"12 条 cookie 里大量是站点业务 cookie"的集成提示)。F-054:工具契约 —— 「是不是挑战页」必须复用生产客户端的同一份实现
(Gogocfsolve.LooksBlocked/ Pythonverify_real.is_challenge),禁止另写子串匹配;
验证脚本里的 cookie/凭证只允许存在内存。
验证
- Python 542 / Go 46 全绿;20 模块逐模块门禁达标;12 个 openapi 操作一致;findings 54 条 exit 0。
🤖 Generated with Claude Code