diff --git a/.gitignore b/.gitignore index d363e39..f222358 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules/ tests/.artifacts/ *.log *.bak.* + +# Model blobs live outside the repo (~/.jev-browser/models); this only guards a stray manual download. +*.gguf diff --git a/README.md b/README.md index 83468e8..68380da 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,24 @@ whether the page changed, cost), `requests.jsonl` and `run.json`, with secrets r | `chrome` | unattended runs, CI, no window | own profile dir, `--headless`, or `--cdp-url http://127.0.0.1:9222` to attach | | `safari` | WebKit | enable Develop → Allow Remote Automation once | +### Judging tiers + +Three judging backends serve the same `/v1/systemone` contract, and all three are first-class +options — but **hosted Jev is the default**: with no config file and no environment, a run uses +`https://api.typesafe.ai` (and a loopback `baseUrl` — a local model — is priced at $0). + +```bash +jev-browser tier list # the three tiers: what each is, what it needs, how to start it, its port, its score, its bar +jev-browser tier status # what a run would use right now: baseUrl, tier, resolved goal_done bar, endpoint +jev-browser tier use kev # one tier's export line and start command (nothing is written unless you add --persist) +``` + +The GGUF readout (`node skills/jev-browser/bin/jev-local.mjs`, port 8092) needs no Python and +serves an Apple Silicon `llama.cpp` server; Kev (`node skills/jev-browser/bin/jev-kev.mjs`, port +8008) trades a Python venv, a 9.34 GB base and tens of GB of GPU memory for the accuracy tier. The +tier table with every measured number — scores, latency, disk, memory, each backend's `goal_done` +bar and the known holes — lives in [SKILL.md](skills/jev-browser/SKILL.md#judging-tiers). + ## MCP server `jev-browser mcp` speaks MCP over stdio with zero dependencies. Tools: `jev_browse`, `jev_observe`, diff --git a/README.zh-CN.md b/README.zh-CN.md index d53caf2..be62ea6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -197,6 +197,19 @@ jev-browser pick --question "Which link opens the plans page?" --candidate prici | `chrome` | 无人值守、CI、不要窗口 | 独立 profile;`--headless`;或 `--cdp-url http://127.0.0.1:9222` 附着到已开启调试端口的 Chrome | | `safari` | WebKit | 需在开发菜单打开一次"允许远程自动化" | +### 判定后端(tier) + +三种判定后端共用同一个 `/v1/systemone` 契约,都是平级选项 —— 但**默认仍然是托管版 Jev**:不写配置、不设环境变量,运行走 `https://api.typesafe.ai`(loopback `baseUrl` 即本地模型,按 $0 计价)。 + +```bash +jev-browser tier list # 三个 tier:是什么、需要什么、怎么启动、端口、得分、goal_done 阈值 +jev-browser tier status # 当前一次运行会用哪个:baseUrl、tier、解析出的阈值、端点状态 +jev-browser tier use kev # 打印某个 tier 的 export 行和启动命令(除非加 --persist,否则不写配置) +``` + +表格与全部实测数字(得分、延迟、磁盘、内存、各后端 `goal_done` 阈值、已知短板)见 +[`SKILL.md`](skills/jev-browser/SKILL.md#judging-tiers);`references/config.md` 只保留阈值 profile 细节。 + ## MCP server `jev-browser mcp` 通过 stdio 提供 MCP,零依赖。工具:`jev_browse`、`jev_observe`、`jev_judge`、`jev_pick`、`jev_doctor`、`jev_config`。结果同时以 JSON 文本和 `structuredContent` 返回。见 [`references/mcp.md`](skills/jev-browser/references/mcp.md)。 diff --git a/docs/local-backend-run-smoke.md b/docs/local-backend-run-smoke.md new file mode 100644 index 0000000..2bc865e --- /dev/null +++ b/docs/local-backend-run-smoke.md @@ -0,0 +1,468 @@ +# 本地后端「真跑一轮」实测:`jev-browser run` 能不能被本地模型驱动完成 + +目标:回答一个此前从未测过的问题 —— 本地 GGUF 后端(`node skills/jev-browser/bin/jev-local.mjs`) +除了能回答单次 judge,能不能被 `jev-browser run` 的 code controller 真正驱动,跑完一整轮浏览器任务。 + +- 机器:M3 Max / 48 GB / macOS 25.6.0 arm64,node v26.9.0,`llama-server` 0.4.0(Homebrew) +- 后端:本机全本地 `Qwen3.5-4B-Q4_K_M`(registry 默认项),llama-server `:8090` + Jev 契约服务 `:8092` +- 浏览器:`--backend chrome --headless`(每次 run 独立 profile,可复现) +- 实测日期:2026-09-24 +- 原始件(15 个 journal、16 项校准探针、39 步请求日志):`/tmp/jev-local-smoke/` +- **`skills/**` 与 `docs/**` 之外一行未改:§6 列出的观测/代码层问题只报告、不改动,本次也没有提交任何 commit** + +--- + +## 0. 结论(先说结果) + +**能跑完,但不能可靠地「自己判断跑完了」。** + +| 问题 | 答案 | +|---|---| +| 本地后端能被 `run` 驱动到 `success` 吗? | **能**:R7 在真实站点(Wikipedia 搜索结果页,64 个可点选项)4 步达成 `status=success`、退出码 0;另有 3 次 1 步达成(example.com / 商品页 / 目录页) | +| 传输层/契约层有没有坏? | **没有**:39 个步骤请求全部首次尝试即成功(`attempts` 全为 1)、0 次超时、0 次 422 `LOW_LABEL_MASS`(最低 label_mass 0.870,阈值 0.5) | +| 15 次 run 的结局 | `success` 4 · `stuck` 8 · `needs_user` 2 · `max_steps` 1(无 `error`、无 `timeout`) | +| 卡点在哪 | 不是选元素(`click_target`),而是 **`action` 选择** 与 **`goal_done` 语义**:9 个非 `success` 的 run 里,8 个的失败点都是 `action` 把 `stop`/`click`/`wait`/`go_back` 排在正确动作前面,剩下 1 个(R3)是 `type` 结构上没被提供 | +| `goal_done >= 0.85` 这条终止规则本地能不能用 | **阈值 0.85 是错的值,不是错的问题**:按终止语义(首次越线即 success,§9)**`goal_done >= 0.25`(可用带 0.12–0.28)7/7 正确 success、0 假 success、0 假 stuck**;现行 0.85 只有 4/7 且假 stuck 3 次。逐「步」分类确实不可分(达成 0.058–0.995 vs 未达成 ≤0.111),代码侧替代(实体/环检测)更差(各 4 次假 success) | + +判定边界(详见 §8): +- **可用**:`judge` 单问、`click_target` 选元素、「当前页面是不是已经是目标状态」这一问; +- **不可用**:把 `run` 的 `success` 终止交给 `goal_done`(动作型目标会漏判成 `stuck`)、`action` 动作选择(需要填表/输入时几乎必错)、`blocker` 在表单/报价页上的判定(会假 `needs_user`)。 + +--- + +## 1. 环境与启动(逐字) + +```bash +# 本地后端(8090 llama-server + 8092 jev 契约服务);启动器只复用「同一个 gguf」的已启动实例 +node skills/jev-browser/bin/jev-local.mjs +# → [local] model ready: ~/.jev-browser/models/Qwen3.5-4B-Q4_K_M.gguf (2.6 GiB) +# → [local] llama-server: /opt/homebrew/bin/llama-server +# → [local] starting llama-server: ... -m .../Qwen3.5-4B-Q4_K_M.gguf --host 127.0.0.1 --port 8090 -c 16384 -ngl 99 --cache-type-k q8_0 --cache-type-v q8_0 -t 8 +# → [local] llama.cpp serving on http://127.0.0.1:8090 (pid 38548, -c 16384) +# → [local] serving /v1/systemone on http://127.0.0.1:8092 (qwen3.5-4b-q4-k-m · Qwen3.5-4B Q4_K_M); Ctrl-C to stop +# → TYPESAFE_BASE_URL=http://127.0.0.1:8092 TYPESAFE_API_KEY=local +``` + +模型加载 2.7 s(Metal),服务就绪后 `/health` 返回 `{"status":"ok","service":"jev-local","model":"qwen3.5-4b-q4-k-m"}`。 + +```bash +# fixture 站点(tests/fixtures/server.mjs,固定 3111 端口) +node --input-type=module -e "const {createSite}=await import('./tests/fixtures/server.mjs');const s=createSite();await new Promise(r=>s.server.listen(3111,'127.0.0.1',r));" +``` + +每次 run 的环境(覆盖仓库外的 user config,不写仓库): + +```bash +export TYPESAFE_BASE_URL=http://127.0.0.1:8092 TYPESAFE_API_KEY=local +export JEV_BROWSER_CONFIG=/tmp/jev-local-smoke/config-.json # journalDir + chrome.userDataDir 指向 /tmp +node skills/jev-browser/bin/jev-browser.mjs run --goal "" --url \ + --backend chrome --headless --max-steps <8|5|6> --json +``` + +`--max-steps` 全部取 4–8;`timeoutMs` 保持**默认 20000**(除 M 组 2 次刻意放大,见 §5 注)。 +退出码:`success=0`、`needs_user=3`、其余 2(`bin/jev-browser.mjs:132-134`)。 + +--- + +## 2. 各次 run:目标、结局、步数、墙钟、停在哪 + +`steps` = controller 真正执行的动作数(`memory.history.length`);`needs_user`/首步 `stop` 会是 0 步但已花掉 1 次 judge。 + +| # | goal(简写) | 站点 | 状态 | 退出码 | steps | 墙钟 | in-app | 停在哪 | +|---|---|---|---|---|---|---|---|---| +| R1 | 确认主标题是 "Example Domain" | example.com | **success** | 0 | 1 | 9.1 s | 5.9 s | step1 `goal_done=0.992` | +| R2 | 把 Red Gadget 加入购物车 | fixture /products | stuck | 2 | 1 | 10.1 s | 7.8 s | step2 模型选 `stop`(`goal_done=0.057`),实际只差一次点击 | +| R3 | DDG 搜 python → 打开 python.org | duckduckgo.com | stuck | 2 | 4 | 59.1 s | 56.1 s | step5 模型选 `stop`;全程**没机会输入**(§6.1) | +| R4 | 打开 Docs 并点按钮显示密码 | fixture / | stuck | 2 | 4 | 31.4 s | 26.6 s | step5 模型选 `stop`,**目标其实第 2 步就达成了** | +| R5 | 点 Accept 关掉 cookie 弹窗 | fixture /consent | stuck | 2 | 1 | 16.3 s | 12.6 s | step2 模型选 `stop`,**目标第 1 步已达成** | +| R6 | 从搜索结果打开 python.org | duckduckgo.com/?q=… | **needs_user** | 3 | 0 | 29.5 s | 26.1 s | step1 `blocker=verification_challenge(0.973)` —— **真阳性**(DDG 出人机验证,§4) | +| R7 | 打开 Alan Turing 条目 | en.wikipedia.org Special:Search | **success** | 0 | 4 | 46.4 s | 45.2 s | step4 `goal_done=0.995`(state 65 个元素 / 64 个可点选项里点对 `e20`) | +| R8 | 搜 Wikipedia → 打开条目 | en.wikipedia.org Main_Page | max_steps | 2 | 8 | 96.4 s | 94.8 s | 8 步用尽,末次校验 `goal_done=0.005` | +| p1 | 确认商品页已打开并显示价格 | fixture /products/red-gadget | **success** | 0 | 1 | 5.5 s | 4.5 s | step1 `goal_done=0.982` | +| p2 | 确认目录页已打开 | fixture /products | **success** | 0 | 1 | 4.4 s | 3.5 s | step1 `goal_done=0.988` | +| p3 | 给 Team 套餐开免费试用 | fixture /pricing | **needs_user** | 3 | 0 | 5.5 s | 4.7 s | step1 `blocker=missing_information(0.684)` —— **假阳性**,§4 | +| p4 | 用给的邮箱/密码登录 | fixture /login | stuck | 2 | 3 | 17.1 s | 16.2 s | 连续 3 次无变化动作;**全程没输入**(§5) | +| g1 | 把 Red Gadget 加入购物车(商品页起步) | fixture /products/red-gadget | stuck | 2 | 0 | 6.0 s | 6.0 s | step1 模型选 `stop(0.43)`,页面正中间就是 "Add to cart" | +| g3 | 点按钮显示密码并确认可见 | fixture /docs | stuck | 2 | 1 | 9.6 s | 9.6 s | step2 模型选 `stop`,**目标第 1 步已达成**(`goal_done=0.329`) | +| g4 | 填联系表单并发送 | fixture /contact | stuck | 2 | 0 | 8.3 s | 7.2 s | step1 模型选 `stop(0.30)`,四个字段的值都已提供 | + +补充: +- R6 / p3 的 `steps=0` 但 journal 里有一条 step1 —— 判定发生在动作之前,`steps` 只数动作。 +- R6、p3 的 `handOff` 均为 `null`;`resume` 里只有 `{backend,cdpUrl,targetId}`,没有 `spaceId`。CLI 仍会打印 + “the browser was handed to you; resume with --space-id …”(`bin/jev-browser.mjs:128`),而 chrome 后端在 + `success=false` 且 headless 时会关掉浏览器(`lib/backends/chrome.mjs:406`)—— 这条 `needs_user` 提示对 chrome 无意义(§6.4)。 + +### 2.1 逐 run 归因(journal `steps.jsonl` + `requests.jsonl`) + +| run | 归因 | 依据 | +|---|---|---| +| R1 / p1 / p2 / R7 | **无问题**(正常终止) | R7 由 `goal_done=0.995` 收尾;其余 3 次首步 `goal_done≥0.982` | +| R2 / g1 | **模型误判(action)** | 商品页 `action=stop(0.43/0.32)`,而 `click_target` 的第一名就是页面正中的 `button 'Add to cart'`;元素不缺、也没报错 | +| g4 | **模型误判(action)** | 联系表单页,4 个字段的值都在 `inputs` 里,`editable` 4 个、`type` 在 `allowedActions` 内,`action` 仍选 `stop(0.30)` | +| R4 / R5 / g3 | **模型误判(goal_done 语义)** | 动作本身做对了(R4 第 2 步、R5 第 1 步、g3 第 1 步的目标均已达成,见 `finalTextExcerpt`),但 `goal_done` 只有 0.281/0.586/0.329(<`goalDoneFinal` 0.7)→ 记 `stuck` | +| p4 | **模型误判(action 排序)** | `action` 四选一里 `type` 垫底(0.105),虽然 `type_target`=Email(0.851)、`type_value`=email(0.933) 都对;controller 按模型序执行 ⇒ 3 次无变化 | +| R8 | **模型误判 + controller 放大** | `type` 已提供(100 元素/99 可点/1 可编辑,`allowedActions` 含 `type`)却没被选;controller 的 progress 规则又两次选了走不通的 `go_back`(`no previous page in history`) | +| R3 | **证据缺口(missing evidence)** | `` 在观测里不是 `editable`(§6.1,dry-run 实测 `editable=0`),`type`/`type_target`/`type_value` 三问根本没生成 ⇒ 模型只能在 click/scroll/wait/stop 里挑 | +| p3 | **模型误判(blocker 假阳性)** | 定价页给出 `missing_information(0.684)`;页面三个套餐与按钮都列在元素表里,`inputs` 也不缺 | +| R6 | **真阳性,但 run 无事可做** | DDG 返回人机验证(`visible_text` 逐字可查),`verification_challenge(0.973)` 判对了;这次没有可自动化空间 | +| — | **API 失败:0 次** | 39/39 请求 `status=succeeded`、`attempts=1`;无 `TIMEOUT`/`LOW_LABEL_MASS`/`CONNECTION` | +| — | **后端/driver 异常:0 次** | 无 `error` 状态、无 Chrome 启动失败;唯一反复出现的 `no previous page in history` 是 controller 主动 `go_back` 后被 driver 正确拒绝(§6.3) | + +--- + +## 3. 逐步延迟画像(39 个步骤请求,全部取自 `requests.jsonl`) + +单步 = 一次 HTTP 批次调用里本地 provider 顺序跑完该步的所有问题;`prompt_tokens` 是**未命中前缀缓存、真正需要重算**的 token(`timings.prompt_n`)。 + +| 指标 | p50 | mean | max | +|---|---|---|---| +| 单步请求(客户端观测) | **4,312 ms** | 5,984 ms | **18,151 ms** | +| 本地服务 `ms_total` | 4,310 ms | 5,978 ms | 18,147 ms | +| 单步 prompt tokens(去缓存后) | 3,555 | 4,680 | 12,254 | +| 单步问题数 | 5 | 5.6 | 8 | + +按问题拆(同一批内,第一问承担 state 预填充,其余问题命中 llama.cpp 前缀缓存): + +| 问题 | n | p50 | mean | max | prompt p50 | cold/warm | label_mass p50 / min | +|---|---|---|---|---|---|---|---| +| `goal_done` | 39 | **1,165 ms** | 2,317 ms | 8,887 ms | 933 | 29/10 | 0.966 / 0.939 | +| `blocker` | 38 | 725 ms | 737 ms | 953 ms | 559 | 1/37 | 0.995 / 0.990 | +| `click_target` | 38 | 713 ms | 1,373 ms | 4,499 ms | 490 | 0/38 | 0.992 / 0.926 | +| `progress` | 23 | 606 ms | 587 ms | 771 ms | 442 | 0/23 | 0.995 / 0.984 | +| `action` | 38 | 595 ms | 606 ms | 798 ms | 464 | 0/38 | 0.985 / 0.870 | +| `select_target` | 7 | 568 ms | 547 ms | 721 ms | 362 | 0/7 | 0.962 / 0.932 | +| `submit_after_type` | 12 | 567 ms | 596 ms | 807 ms | 465 | 0/12 | 0.990 / 0.977 | +| `type_target` | 12 | 547 ms | 596 ms | 1,013 ms | 433 | 0/12 | 0.983 / 0.971 | +| `type_value` | 12 | 546 ms | 553 ms | 748 ms | 418 | 0/12 | 0.977 / 0.968 | + +结论与既有文献的两点修正: + +1. **`click_target` 不再是最贵的**。`experiments/gguf-provider/RESULTS.md` 里 “click_target 6.1 s、整步 9 s(热)/18.3 s(冷)” + 是在**不命中前缀缓存**的口径下量的;真实 run 里 provider 顺序发问,state 只算一次(`goal_done` 那一问承担, + p50 1.2 s、max 8.9 s),后面每一问只付「问题块」的增量(400–500 tok,0.5–0.7 s)。整步 p50 落在 **4.3 s**。 +2. **但默认 20 s 客户端超时是真的紧**。R8 step8 的状态是 12,254 tok,单步 **18,151 ms**,距 `timeoutMs=20_000` + 只剩 **1.85 s**;`lib/typesafe.mjs:201-207` 的超时标 `retryable`,一旦触发会把整批 5–8 问重跑(最多 3 次), + 本次 15 次 run 没有触发,但更大页面/更慢机型会踩。39 步里 0 次超时、0 次 `LOW_LABEL_MASS`。 + +--- + +## 4. `goal_done` 分离分析:有没有一条阈值能分开「已达成 / 未达成」 + +数据两部分:(a) **首步校准探针 16 项**——用 skill 自己的 `run --dry-run` 构造第一步 state(含 goal/inputs/page), +只把 `goal_done` 这一问发给本地服务,真值由页面构造决定;(b) **15 次 run 的 39 个步骤判定**,真值由 journal + 最终页面人工判定。 + +### 4.1 首步校准(16 项,state 与 controller 实际发送的完全一致) + +| id | 真值 | goal_done | id | 真值 | goal_done | +|---|---|---|---|---|---| +| T1 商品页已打开 | 达成 | **0.981** | F1 加入购物车 | 未达成 | 0.008 | +| T2 目录页已打开 | 达成 | **0.988** | F2 登录成功并看 dashboard | 未达成 | 0.007 | +| T3 联系表单在页面上 | 达成 | **0.954** | F3 Team 试用已开始 | 未达成 | 0.008 | +| T4 登录页在问邮箱密码 | 达成 | **0.969** | F4 已显示密码 | 未达成 | 0.032 | +| T5 三个套餐已列出 | 达成 | **0.987** | F5 弹窗已关闭 | 未达成 | 0.096 | +| T6 页面说需要管理员权限 | 达成 | **0.936** | F6 已打开定价页且 $29 | 未达成 | 0.011 | +| T7 页面在问 cookie 同意 | 达成 | **0.839** | F7 已打开 Docs | 未达成 | 0.043 | +| T8 主标题 Example Domain | 达成 | **0.997** | F8 已从链接打开 IANA | 未达成 | 0.036 | + +**达成 0.839–0.997(n=8);未达成 0.007–0.096(n=8)。最大负例 0.096 ≪ 最小正例 0.839,中间是一段空档。** +即「这页是不是已经是目标状态」这一问,本地 4B 是可用的;官方阈值 0.85 只有 1/8 真例踩线落空(T7 = 0.839), +0/8 假阳性。任何落在 **[0.10, 0.83]** 的阈值都能把这 16 项全部分开。 + +### 4.2 run 内的判定(含「做了一步之后」的状态)——分离失效 + +| 类别 | 样本(goal_done) | +|---|---| +| 未达成,首步 | R2 .008 · R3 .004/.011/.012/.006/.006 · R4 .010 · R5 .096 · R6 .005 · p3 .008 · p4 .010 · R7 .023/.015/.014 · R8 .003/.022/.015/.009/.009/.008/.010/.011(+末次 .005)· g1 .050 · g3 .027 · g4 .012 | +| 未达成,但**做了一步**(最危险的一档) | **R4 step2 = 0.111** | +| 已达成,且是首步就能判 | R1 .992 · p1 .982 · p2 .988 | +| 已达成,**靠动作达成** | R7 step4 **.995** · R5 step2 **.586** · g3 step2 **.329** · R4 step3 **.273** · R4 step5 **.281** · **R4 step4 = 0.058** | + +- 未达成侧最大 **0.111**(R4 step2,点了按钮但页面刚变);已达成侧最小 **0.058**(R4 step4,同一页、密码已可见)。 + **两个区间重叠 ⇒ 不存在能把「已达成 / 未达成」分开的阈值。** +- 这不是「阈值调低一点就行」:把阈值放到 0.2 能救回 g3(0.329)/R4(0.273),但会救不回 R4 step4(0.058),而 + 同一页 step2 的 0.111 又高于它 —— 判定顺序本身不稳定(同页同目标:0.111 → 0.273 → 0.058 → 0.281)。 +- 反过来说,`goal_done` 在「页面本身就是答案」的场景(R1/R7/p1/p2、T1–T8)非常干净,0.98 上下; + **它坏掉的正是「做完一个动作之后」这一类**——4B 对「动作产生了结果」的确认能力不足。 + +### 4.3 对控制器的直接后果 + +- `goal_done >= 0.85`(`lib/config.mjs:27`)在动作型目标上**永远不会触发**:本次 6 个「已达成」样本里 5 个 < 0.85。 +- 于是 run 只能靠 `stop` 分支收尾,而 `stop` 只有在 `goal_done >= goalDoneFinal(0.7)`(`lib/config.mjs:28`)时才记 `success`: + R4(0.281)、R5(0.586)、g3(0.329) 三个**实际已经完成**的 run 被判 `stuck`,退出码 2。 +- 末次校验(`maxSteps` 用尽后补问一次)用的正是同一个 0.7:R8 得 0.005,判 `max_steps`(这次判对了)。 +- **所以「本地后端能不能跑完」与「本地后端能不能承认自己跑完」是两件事**:前者可以(R7),后者不可信。 + +--- + +## 5. `action` 选择:与页面明显允许的动作不符 + +按严重度排序(全部有 journal 逐字证据): + +1. **p4(登录页)——模型知道该填哪儿、填什么,就是不选「type」。** 同一 state 的完整判读(`/tmp/jev-local-smoke/login.dryrun.json` 重放,确定性复现): + ``` + action click 0.430 · stop 0.246 · wait 0.220 · type 0.105 ← 四选一里 type 垫底 + type_target e2 (Email) 0.851 ← 正确 + type_value email 0.933 ← 正确 + click_target e4 ("Sign in" 按钮) 0.892 + submit_after_type 0.183(= "不按回车",也对) + ``` + controller 按模型动作序建候选(`lib/controller.mjs:263` 的 `for (const [action] of decision.actions)` → `:320-323` 的取候选),于是先挑 `click e4`(空表单,无变化), + 再挑 `wait`(无变化),第三个才轮到 `type` —— 3 次无变化后 stuck(`noChangeLimit=3`)。 +2. **R3(DDG 首页)/ R8(Wikipedia 主页)——同样从不输入**:R3 依次点了搜索框 combobox、Search 单选、" + Set As Default Search" 链接;R8 点了 Search 按钮、搜索框 combobox、滚屏、返回,最后点进一篇无关条目 + (Hashim Thaçi)。两者原因不同:R3 的 `type` 是**结构上就没被提供**(§6.1);R8 的 `type` 提供了 + (Main_Page dry-run:100 个元素 / 99 个可点 / 1 个可编辑,`allowedActions=click,type,scroll_down,wait,stop`, + `type_target`/`type_value`/`submit_after_type` 三问齐全,`inputKeys=[query]`)却没被选。 +3. **提前 `stop`(3 次)**:g1/g4 在「页面正中就是 Add to cart / 四个字段都已给值」时第一问就选 `stop` + (0.43 / 0.30);R2 在商品页选 `stop`(0.32)。`action` 的 confidence 只有 0.06–0.13(无信心), + 但 controller 不用 confidence 做弃权(`lib/typesafe.mjs:57` 归一化后只给 `confidence` 字段,无人消费)。 +4. **R7 的 `go_back` 空转**:第 1–2 步 `action` 把 `go_back`(0.28) 排在 `click`(e20 = 正确的 Alan Turing 链接) 前面, + 白走两步(`go_back` 的 `click_target` 一直是 e20 —— 选元素没问题,选动作有问题)。 + 第 2 步 `go_back` 失败(`no previous page in history`)。 +5. **R8 的重复无效 `go_back`**:step5/7/8 由 controller 的「progress 退化」规则(`lib/controller.mjs:257-261`) + 自己选了 `go_back`,其中两次报 `no previous page in history`;因为 blocked 记忆的键是 + `${stateHash}|${actionKey}` 而 state 每步都变(含 `previous_page`/`last_action`,`lib/controller.mjs:193-196`), + 同一台不可能完成的动作被反复重试。 + +对照:**`click_target` 是本次最稳的一环**。R7 在 65 个元素的真实页面上,每一步都把正确条目排在第一 +(`e20 = link 'Alan Turing' → /wiki/Alan_Turing`,0.784 / 0.742 / 0.713;第二名 `e22` 是同一个 `/wiki/Alan_Turing` 的另一处链接, +0.17 / 0.22 / 0.25,两者合计 0.95 —— 即使第一名失手,结果页也是对的);R2 step1、R4 step1/2、R5 step1、g3 step1 在 fixture 上也都点对。 +RESULTS.md 里 “`action + click_target` 5 题只对 2 题” 的短板,在真机 run 里重现的是其中的 **action 半边**。 + +--- + +## 6. 观测/代码层问题(只报告,未改任何 `skills/**` 文件) + +> **状态更新 2026-09-24 ~01:25(本次 run 之后,工作区并发改动、未提交)**:6.1(combobox 不是 textbox)、 +> 6.4(`needs_user` 的 ego 专属提示)、6.5(本地 token 按 hosted 价格计费)三条**已在 `skills/**` 里被修**: +> `lib/observe.mjs` 新增 `isComboboxField()` + `tests/e2e/combobox.test.mjs`;`bin/jev-browser.mjs` 的 `needs_user` +> 分支改为有 `spaceId` 才提示 `--space-id`,否则说明浏览器已关闭;`lib/typesafe.mjs` 新增 +> `isLoopbackBaseUrl()`/`pricePerMtokFor()`,loopback 计费为 0。6.2/6.3/6.6 未被改动。下面保留的是 +> **实测时的行为**(run 时间 00:54–01:08),行号按当时的工作区版本。 + +### 6.1 `role="combobox"` 的 `` 被当成非文本字段 → 永远没有 `type` 动作 + +> **状态更新 2026-09-24 ~01:25**:本条已被并发修复(工作区未提交)—— `lib/observe.mjs` 新增 `isComboboxField()`, +> 把 `role=combobox` 的 `/