lasso-mcp 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/browse/upstream-wedge.d.ts +36 -0
- package/dist/browse/upstream-wedge.d.ts.map +1 -0
- package/dist/browse/upstream-wedge.js +40 -0
- package/dist/browse/upstream-wedge.js.map +1 -0
- package/dist/channels/BrowseChannel.d.ts +44 -4
- package/dist/channels/BrowseChannel.d.ts.map +1 -1
- package/dist/channels/BrowseChannel.js +128 -7
- package/dist/channels/BrowseChannel.js.map +1 -1
- package/dist/channels/LoggedInChannel.d.ts +20 -0
- package/dist/channels/LoggedInChannel.d.ts.map +1 -1
- package/dist/channels/LoggedInChannel.js +97 -1
- package/dist/channels/LoggedInChannel.js.map +1 -1
- package/dist/doctor/chrome-status.d.ts +129 -0
- package/dist/doctor/chrome-status.d.ts.map +1 -0
- package/dist/doctor/chrome-status.js +467 -0
- package/dist/doctor/chrome-status.js.map +1 -0
- package/dist/doctor/doctor.d.ts +29 -16
- package/dist/doctor/doctor.d.ts.map +1 -1
- package/dist/doctor/doctor.js +93 -60
- package/dist/doctor/doctor.js.map +1 -1
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/launcher/chrome-ledger.d.ts +18 -0
- package/dist/launcher/chrome-ledger.d.ts.map +1 -1
- package/dist/launcher/chrome-ledger.js +23 -0
- package/dist/launcher/chrome-ledger.js.map +1 -1
- package/dist/launcher/chrome-stop.d.ts +22 -0
- package/dist/launcher/chrome-stop.d.ts.map +1 -1
- package/dist/launcher/chrome-stop.js +45 -2
- package/dist/launcher/chrome-stop.js.map +1 -1
- package/dist/launcher/launch-chrome.d.ts.map +1 -1
- package/dist/launcher/launch-chrome.js +42 -3
- package/dist/launcher/launch-chrome.js.map +1 -1
- package/dist/logged-in/TabRegistry.d.ts.map +1 -1
- package/dist/logged-in/TabRegistry.js +19 -0
- package/dist/logged-in/TabRegistry.js.map +1 -1
- package/dist/runtime/runtime-types.d.ts +1 -1
- package/dist/runtime/runtime-types.d.ts.map +1 -1
- package/dist/tools/admin.d.ts +9 -1
- package/dist/tools/admin.d.ts.map +1 -1
- package/dist/tools/admin.js +21 -0
- package/dist/tools/admin.js.map +1 -1
- package/dist/tools/descriptions.d.ts.map +1 -1
- package/dist/tools/descriptions.js +13 -4
- package/dist/tools/descriptions.js.map +1 -1
- package/doc/bugs/04-2026-09-08-/351/200/211/344/270/255/351/241/265/346/255/273/351/224/201/344/270/216/345/275/222/345/261/236/351/211/264/345/256/232chrome-status.md +229 -0
- package/doc/bugs/04-/351/231/204/345/275/225E-cc-control/347/255/224/345/244/215/344/270/216hook/344/272/244/344/273/230/345/214/205.md +216 -0
- package/package.json +1 -1
- package/src/browse/upstream-wedge.ts +43 -0
- package/src/channels/BrowseChannel.ts +124 -12
- package/src/channels/LoggedInChannel.ts +94 -1
- package/src/doctor/chrome-status.ts +607 -0
- package/src/doctor/doctor.ts +101 -68
- package/src/index.ts +29 -2
- package/src/invariants/check-invariants.mjs +273 -5
- package/src/launcher/chrome-ledger.ts +28 -0
- package/src/launcher/chrome-stop.ts +77 -2
- package/src/launcher/launch-chrome.ts +50 -3
- package/src/logged-in/TabRegistry.ts +19 -0
- package/src/runtime/runtime-types.ts +6 -1
- package/src/tools/admin.ts +29 -0
- package/src/tools/descriptions.ts +13 -4
package/README.en.md
CHANGED
|
@@ -206,7 +206,7 @@ Goes to the Internet Archive (Wayback Machine) to find the last archived copy of
|
|
|
206
206
|
|
|
207
207
|
## Install
|
|
208
208
|
|
|
209
|
-
**Current version v1.
|
|
209
|
+
**Current version v1.22.0** (changelog in the collapsed block at the end of this section).
|
|
210
210
|
|
|
211
211
|
Prerequisites: Node.js ≥ 20 + Claude Code (or any MCP-capable client).
|
|
212
212
|
|
|
@@ -223,6 +223,7 @@ Restart Claude Code → `/mcp` → `lasso ✓ Connected`. **That's the one line
|
|
|
223
223
|
<details>
|
|
224
224
|
<summary>📋 Changelog (v1.8 → v1.18.6 — click to see what each version changed)</summary>
|
|
225
225
|
|
|
226
|
+
- **v1.22.0**: ownership-verdict & channel-self-heal batch (BUG-04, consumer-field driven) — ① new subcommand **`chrome-status [--port N]`**: takes "who occupies this port and may I touch it" out of the AI's hands — lasso classifies it directly in ten categories (ledger zombie / stale / suspected user asset …) and outputs a human decision message + machine-readable directives (`allowed_commands` / `must_report` — on a "suspected user asset" verdict the AI is explicitly told it has **zero allowed actions and must report to you**) + a paste-ready report pack; companion `chrome-stop --zombie-gate` is the only machine kill exit (in-ledger + fingerprint match + ledger re-read at kill time — triple gate). Existing commands unchanged. ② fixes the `browse_logged_in` **selected-page wedge** (P1, channel-level): root cause is a structural flaw in the upstream engine — once the selected page closes, every later navigate/evaluate/snapshot fails forever until restart; the wedge signature is now detected and healed by quietly taking over with a background page of our own, with an honest prefix when healing fails. ③ `evaluate` accepts the `(async () => {...})()` form (previously "fn is not a function"); ④ your own bad JS no longer trips the fallback chain (script errors vs channel errors classified); ⑤ doctor port-probe wording made honest ("HTTP 404" → four real outcome shapes). Two clean adversarial rounds: 5/5 mutations killed + live-machine sovereignty checks against a real user Chrome + report-pack weaponization tests. For heavy automation users: a PreToolUse hook sample (hard-blocking AI killing browsers via Bash) is in doc/bugs/04 appendix E.
|
|
226
227
|
- **v1.21.0**: browser-sovereignty & lifecycle root-fix batch (BUG-03, driven by a live-machine incident) — ① 🔴 **behavior change**: `launch-chrome` CLI default idle reaping changes from "never reap" to **auto-reap after 30 minutes** (the old never-reap behavior is restored explicitly with `--idle-ms 0`); ② **user-activation yielding gate**: when you un-hide lasso's hidden Chrome via Dock/Spotlight, lasso recognizes it as a **human at the machine** via a dual predicate (HID idle + frontmost window) — lets it through, then steps aside after a 30-second confirmation window (page-spawned popups are still pressed back; the v1.18.3 silence contract is unchanged); ③ **no collateral kill on shutdown**: fixes a P0 — any lasso server process exiting used to sweep-kill **every hidden Chrome** in the global ledger (violating the `--idle-ms 0` keep-alive promise); collection is now filtered by ownership in three dimensions, so CLI-launched instances survive server exits and are still reaped on idle; ④ **zombie-port self-healing**: when 9222 is occupied by lasso's own hung instance, it is no longer misreported as "a non-CDP process" — the ledger is consulted, the zombie collected, and a fresh instance relaunched; doctor classifies port occupiers three ways (own zombie / stale / user asset) and **never suggests killing a user asset** (forged ledger entries and impersonating pids are also refused; new invariants INV-85/86/87); ⑤ consumer-field fixes (cc-control feedback): `browse` screenshot upstream-contract adaptation (filePath direct-write for large images + Access-denied fallback retry, closing the in-session screenshot gap), `evaluate` always-returning-undefined fixed (dual-form function-expression passthrough), session rotation classified as retryable `session_rotated`; ⑥ optional `launch-chrome --mode headless` tier (for unattended use — honest caveat: headless still shares the Chrome bundle slot). Three adversarial rounds closed: 28/28 mutations killed + 3 CRITICALs fixed and re-verified PASS; tests 2595→2676 / invariants 84→87.
|
|
227
228
|
- **v1.20.0**: render-tier parallel-safety batch (stop/doctor port scoping) — ① 🔴 **behavior change**: `render-chrome --stop` previously ignored `LASSO_RENDER_PORT` and killed **every render tier on the machine** (the #1 entry point for parallel acceptance agents killing each other's instances); now, **an explicitly set `LASSO_RENDER_PORT` stops only that port** (symmetric with `--ensure`/`--status`), unset = collect-all unchanged, an invalid value exits 1 as a usage error. Old scripts that set the port yet expect a full sweep should switch to `chrome-stop --modes render`; the `--stop` output shape is unchanged (idempotent `{"stopped":[...]}`). ② `render-chrome doctor` orphan judgment is scoped the same way with double exemption: an explicit port scans only that port, and other namespaces' in-use instances are no longer misjudged as orphans (out-of-scope port + touch heartbeat within 10 minutes, both exempt — `--clean` no longer kills in-use instances). ③ the **multi-agent parallel acceptance** three-env recipe (`LASSO_RENDER_PORT` + `LASSO_LAUNCHED_CHROMES_PATH` + `LASSO_RENDER_GUARDIAN_PID_PATH`) verified on a live machine: two terminals tearing down concurrently, mutually invisible, zero cross-kills. Consumers like media-gen-mcp only call `--ensure` and are unaffected (verified at source level).
|
|
228
229
|
- **v1.19.0**: deterministic render tier + performance/accuracy batch — ① new `lasso-mcp render-chrome`: a **deterministic rendering browser** for other AI tools (e.g. media-gen-mcp) — one `--ensure` line launches a headless Chrome with a frozen flag snapshot (the browser survives consumer kills; auto-reclaimed after 10 min idle with profile cleanup; see README render section); ② perf: handshake budget on engine cold start (fast-fail instead of 60s hangs, tunable via `LASSO_MCP_HANDSHAKE_TIMEOUT_MS`), first-install npx tax removed, 2FA probe made idempotent, fast-path for truncated large evaluations; ③ accuracy: SERP layout-change detection chain live (selector rot auto-detected with baseline recording). Tool interfaces unchanged (purely additive subcommand and flags).
|
|
@@ -320,6 +321,7 @@ The first time, log in to your accounts in that window (handle 2FA yourself) —
|
|
|
320
321
|
- Hide is **sticky** (v1.18.3; full lifecycle in v1.18.5): a hidden window un-hidden by any source (an upstream page popping itself, a tab opened by an external tool, a system focus switch) is pressed back into the background within ~1.5 s; use `chrome-show` when you want to look at it (explicit release, no more pressing back). The sticky state survives restarts; since v1.18.5 the hidden tier is **protected from birth** (the launch itself writes the sticky ledger), and a standalone enforcer process backs it — **even when the Claude session / server is gone, an un-hidden Chrome gets pressed back** (the enforcer is a short-lived process that self-exits on an empty ledger — no resident overhead).
|
|
321
322
|
- **When YOU open it by hand, the enforcer yields** (BUG-03 B1): the press-back decision has a user-activation yield gate — a Dock click / your manual activation (detected as this Chrome being frontmost + recent HID input, confirmed over ~30 s) gets claimed: no more pressing back, no idle collection; the only close exits are you closing it or an explicit `chrome-stop`. Programmatic pop-ups still get pressed back as before. Re-arm anytime with `lasso chrome-hide`.
|
|
322
323
|
- **Port taken by your own Chrome? Lasso never touches it** (BUG-03 C): on `port_in_use_non_cdp`, a Lasso-owned dead instance that is **not user-owned** self-heals automatically (`ledger_zombie_collected`); one the user has taken over (`user_taken_asset` claimed / visible login window) is **never auto-cleared** — its only exits are the user closing it or running `chrome-stop` themselves; if the occupier is your manually-opened Chrome or another program, the error says so honestly — "user asset, will not kill, decide yourself or pick another port" — Lasso provides no kill escape hatch for non-ledger assets.
|
|
324
|
+
- **Want to know WHO occupies a port? `lasso chrome-status [--port N] [--json]`** (BUG-04 A, read-only): Lasso does the ownership attribution for you — real listener pid, process name, uptime, cmdline, ledger and CDP probe in one shot, answering in 10 classifications (free / lasso-live / still-starting / ledger zombie / user-claimed / stale ledger / suspected lasso orphan / suspected user asset / external process / insufficient-evidence), plus a **user-facing decision pack** (`user_paste_pack` listing YOUR exits: close it yourself / run `chrome-stop --port N` yourself / have the agent pick another port). For agents it **never outputs any kill command** (the only cleanup directive is the gated `chrome-stop --zombie-gate --port N` variant — re-evaluating your claim state at kill time, zombie branch only); failed/empty probes honestly report "insufficient evidence, report only" — empty output does NOT mean unoccupied. MCP-side equivalent: `admin {action:"chrome_status"}`.
|
|
323
325
|
- For pure scraping without login state on an **unattended** machine, `--mode headless` (BUG-03 B2) gives a zero-window, zero-AX footprint. Real-machine correction (BUG-03 adversarial review): on macOS it does **not** free the Dock slot — while it runs, clicking the Chrome icon produces no visible response (the activation is absorbed by the headless instance, which has no window to show); on machines a human may use, prefer the default hidden mode (its yield gate lets you take the window over). The tradeoffs: no window, `chrome-show` ineffective — use hidden/visible for login flows.
|
|
324
326
|
|
|
325
327
|
</details>
|
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ macOS 上能控 Finder / Mail / Safari / Notes / 系统设置等任何原生 app
|
|
|
215
215
|
|
|
216
216
|
## 安装
|
|
217
217
|
|
|
218
|
-
**当前版本 v1.
|
|
218
|
+
**当前版本 v1.22.0**(更新日志见本节末尾折叠块)。
|
|
219
219
|
|
|
220
220
|
前提:Node.js ≥ 20 + Claude Code(或任何支持 MCP 的客户端)。
|
|
221
221
|
|
|
@@ -232,6 +232,7 @@ claude mcp add lasso -- npx -y lasso-mcp
|
|
|
232
232
|
<details>
|
|
233
233
|
<summary>📋 更新日志(v1.8 → v1.18.6,点开看每版改了什么)</summary>
|
|
234
234
|
|
|
235
|
+
- **v1.22.0**:归属鉴定与通道自愈批(BUG-04,消费方实战驱动)——① 新子命令 **`chrome-status [--port N]`**:把「这个端口被谁占着、我能不能动它」的鉴定权从 AI 手里收走——lasso 直接十分类(账本僵尸/陈留/疑似用户资产…),输出人类决策文案 + 机器可读指令(`allowed_commands` / `must_report`——AI 拿到「疑似用户资产」分类时被明确告知**零允许操作、必须上报你**)+ 可直接粘贴的上报包;配套 `chrome-stop --zombie-gate` 唯一机器代杀出口(台账在案+指纹对+kill 时刻重读台账三重门)。旧命令行为零变化。② 修复 `browse_logged_in` **选中页死锁**(P1 通道级):根因是上游引擎的结构性缺陷——选中页一旦被关闭,后续 navigate/evaluate/snapshot 永久报错只能重启;现在自动检测楔死签名并后台自建页接管自愈,修不掉时如实前缀上报。③ `evaluate` 兼容 `(async () => {...})()` 形态(此前报 fn is not a function);④ 调用方自己写错 JS 不再拉响备用通道(脚本错误与通道错误分类);⑤ doctor 端口探测措辞如实化(「HTTP 404」→ 四形态真实归因)。对抗两轮 clean:5 变异全杀 + 真实用户 Chrome 主权实机验证 + 上报包武器化测试不破防。给重度自动化用户的配套:PreToolUse hook 硬拦样例(拦 AI 经 Bash kill 浏览器)见 doc/bugs/04-附录E。
|
|
235
236
|
- **v1.21.0**:浏览器主权与生命周期根治批(BUG-03,真机事故驱动)——① 🔴 **行为变更**:`launch-chrome` CLI 默认空闲回收从「永不回收」改为 **30 分钟自动回收**(旧永不回收行为用 `--idle-ms 0` 显式恢复);② **用户激活让位门**:你点 Dock/Spotlight 掀出 lasso 隐藏的 Chrome 时,lasso 以「HID 空闲 + 前置窗口」双判据识别这是**人在操作**——放行、30 秒确认窗后退位(页面自弹的窗口照常压回,v1.18.3 静默契约零回归);③ **停机不连坐**:修复一个 P0——此前任意 lasso server 进程退出会按全局台账**扫杀全部隐藏 Chrome**(违背 `--idle-ms 0` 保活承诺);现在按归属三维过滤,CLI 拉起的实例在 server 退出后照常存活、照常被回收;④ **僵尸占位自愈**:9222 被自家挂死实例占用时不再误报「非 CDP 进程」,自动按台账归因收尸重拉;doctor 端口占用三分类(自家僵尸/陈留/用户资产),**永不建议代杀用户资产**(台账伪造 + pid 冒名也拒杀,新不变量 INV-85/86/87);⑤ 消费方实战修复(cc-control 反馈):`browse` 截图上游契约适配(大图 filePath 直写 + Access-denied 降级重试,会话内截图通路补齐)、`evaluate` 恒返 undefined 根治(函数表达式双形态透传)、会话轮换归类为可重试的 `session_rotated`;⑥ `launch-chrome --mode headless` 可选档(无人值守专用——诚实声明:headless 也共享 Chrome bundle 槽位)。对抗三轮闭环:28 变异全杀 + 3 个 CRITICAL 当场修复复核 PASS;测试 2595→2676 / 不变量 84→87。
|
|
236
237
|
- **v1.20.0**:渲染档并行安全批(stop/doctor 端口作用域化)——① 🔴 **行为变更**:`render-chrome --stop` 此前无视 `LASSO_RENDER_PORT`、会收掉**全机**渲染档(同机多 agent 并行验收时互杀对方实例的第一入口);现在**显式设了 `LASSO_RENDER_PORT` 只收该 port**(与 `--ensure`/`--status` 对称),未设=维持全收不变,非法值 exit 1 报用法错。设了 port 却想全收的旧脚本请改用 `chrome-stop --modes render`;`--stop` 输出形状不变(幂等 `{"stopped":[...]}`)。② `render-chrome doctor` 孤儿判定同样作用域化 + 双豁免:显式 port 只判该 port,他人命名空间实例不再被误判孤儿(范围外 port + touch 心跳 10 分钟内新鲜双重豁免,`--clean` 不再误杀在用实例)。③ 配套**同机多 agent 并行验收**三 env 隔离配方(`LASSO_RENDER_PORT` + `LASSO_LAUNCHED_CHROMES_PATH` + `LASSO_RENDER_GUARDIAN_PID_PATH`)真机验证:双终端并发各自清场,并行互不可见零互杀。media-gen-mcp 等消费方只调 `--ensure` 不受影响(已源码级实证)。
|
|
237
238
|
- **v1.19.0**:确定性渲染档 + 性能/准确率批——① 新增 `lasso-mcp render-chrome`:给其它 AI 工具(如 media-gen-mcp)当**确定性渲染浏览器**——`--ensure` 一行拉起带冻结旗标快照的 headless Chrome(消费方进程被强杀浏览器照常存活,空闲 10 分钟自动回收并清理 profile),详见 README 渲染档节;② 性能:浏览器引擎冷启动握手加预算(卡死快速失败不再挂 60 秒,`LASSO_MCP_HANDSHAKE_TIMEOUT_MS` 可调)、首装 npx 下载税消除、2FA 探测幂等化、大求值截断快路;③ 准确率:搜索页改版检测链上线(选择器失效自动发现并记录基线)。工具接口零变化(纯新增子命令与旗标)。
|
|
@@ -341,6 +342,7 @@ lasso launch-chrome
|
|
|
341
342
|
- hide 是**粘滞**的(v1.18.3,v1.18.5 补全生命周期):已 hide 的窗口无论被什么来源掀出(上游页面自己弹的、外部工具开的 tab、系统焦点切换),约 1.5 秒内自动压回后台;想看窗口用 `chrome-show`(明示解除,不再压回)。粘滞状态跨重启保留;v1.18.5 起 hidden 档**出生即受保护**(拉起落账即写粘滞),且由独立执守进程兜底——**即使 Claude 会话/server 不在,隐藏的 Chrome 被掀出也会被压回**(执守是账空自退的短命进程,不留常驻开销)。
|
|
342
343
|
- **你亲手点开它时,执守会让位**(BUG-03 B1):压回决策带「用户激活让位门」——Dock 点击 / 你手动点开的激活(判定 = 本 Chrome 处于前台 + 键鼠刚有输入,连续约 30 秒确认)会被认领:不再压回、不再自动收,关它只剩你自己关或显式 `chrome-stop`。程序掀出(页面自己弹的)照常压回。误认领可用 `lasso chrome-hide` 重新武装。
|
|
343
344
|
- **端口被你自己的 Chrome 占了?lasso 永不动它**(BUG-03 C):报 `port_in_use_non_cdp` 时,自家挂死且**未被用户使用**的实例会自动清掉重拉(`ledger_zombie_collected`);已被用户拿去用的实例(已认领 `user_taken_asset` / visible 登录窗)**绝不自动清理**——唯一出口是你自己关掉或你本人跑 `chrome-stop`;是你手开的 Chrome / 其他程序占用则明确说「用户资产,不会 kill,请自行裁决或换口」——lasso 不提供任何针对非自家资产的 kill 出口。
|
|
345
|
+
- **端口被占了想知道是谁?`lasso chrome-status [--port N] [--json]`**(BUG-04 A,只读):lasso 直接替你做「归属鉴定」——真实监听 pid / 进程名 / 已运行时长 / 命令行 / 台账 / CDP 探测一次查清,分 10 类作答(空闲 / lasso 在用 / 慢启动中 / 台账僵尸 / 用户已认领 / 台账陈留 / 疑似 lasso 无主实例 / 疑似用户资产 / 外部进程 / 证据不足),并给出**给你看的裁决包**(`user_paste_pack`,含你能走的出口:自己关 / 本人跑 `chrome-stop --port N` / 让 Claude 换口)。对 Claude/agent 侧它**永不输出任何 kill 命令**(唯一的清账指引是 `chrome-stop --zombie-gate --port N` 门槛变体——kill 时刻重估你的认领状态,且仅限 lasso 自家僵尸分支);探针失败/空输出一律如实报「证据不足、只上报」——空输出≠没占用。MCP 侧等价入口:`admin {action:"chrome_status"}`。
|
|
344
346
|
- 纯抓取不需要登录态时可用 `--mode headless`(BUG-03 B2):无头实例零窗口/零打扰,适合**无人值守**机器的纯抓取。🔴 真机复核订正(BUG-03 对抗复审):macOS 上它**并不**让出 Dock 槽位——它活着时你点 Chrome 图标不会有任何反应(激活被无头实例吸收、没有窗口可显示);有人会用的机器请用默认 hidden(有让位门,你点开它会让位)。代价:没有窗口、`chrome-show` 无效,登录流用 hidden/visible。
|
|
345
347
|
|
|
346
348
|
</details>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upstream-wedge.ts(BUG-04 决议 B,doc/bugs/04 §5,2026-09-08)
|
|
3
|
+
*
|
|
4
|
+
* 上游 chrome-devtools-mcp@1.7.0「选中页死锁」签名单一真源。
|
|
5
|
+
*
|
|
6
|
+
* 根因(npm tarball 逐行复核,doc/bugs/04 §2):上游 ToolHandler.js 的
|
|
7
|
+
* `const targetPage = page ?? context.getSelectedMcpPage()`(:189)无条件执行——
|
|
8
|
+
* 选中页被关(用户手关 / 页面 window.close / 会话收尾关自建 tab)后,**任何**
|
|
9
|
+
* 工具调用(含 list_pages 自身)都在此处 throw 落入外层 catch,绕过唯一自愈点
|
|
10
|
+
* `createPagesSnapshot()`——通道级死锁,MCP 进程重启才解。
|
|
11
|
+
*
|
|
12
|
+
* 本模块只放签名常量 + 判定函数(零依赖叶子,BrowseChannel / TabRegistry /
|
|
13
|
+
* LoggedInChannel 三方共用——签名定义不允许第二处漂移)。
|
|
14
|
+
*
|
|
15
|
+
* INV-89 机械锚。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 上游 McpContext.getSelectedMcpPage() 的整串错误子串(build/src/McpContext.js:253):
|
|
19
|
+
* `The selected page has been closed. Call list_pages to see open pages.`
|
|
20
|
+
*
|
|
21
|
+
* 窄匹配(只用前半句)防误伤:页面正文/标题正常场景不会以这个精确短语出现;
|
|
22
|
+
* 若未来页面内容真含此串,被动检测面(错误文本)不受影响——只有主动
|
|
23
|
+
* reconcile 面(list_pages 全响应)可能命中,而该响应在健康通道上恒为页列表。
|
|
24
|
+
*/
|
|
25
|
+
export declare const UPSTREAM_WEDGE_SIGNATURE: RegExp;
|
|
26
|
+
/** 判定错误/响应文本是否上游选中页死锁签名。 */
|
|
27
|
+
export declare function isUpstreamWedgeError(text: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* reconcile 类型化信号前缀(doc/bugs/04 §5 主动接入点):
|
|
30
|
+
* `TabRegistry.reconcile` 命中签名 → throw `upstream_wedge:<原文摘录>`;
|
|
31
|
+
* LoggedInChannel.getMcpClient 捕获该前缀 → 返回 client 前触发 heal。
|
|
32
|
+
*/
|
|
33
|
+
export declare const UPSTREAM_WEDGE_SIGNAL_PREFIX = "upstream_wedge:";
|
|
34
|
+
/** 判定是否 reconcile 抛出的类型化楔死信号(区别于普通 reconcile 失败)。 */
|
|
35
|
+
export declare function isUpstreamWedgeTypedSignal(msg: string): boolean;
|
|
36
|
+
//# sourceMappingURL=upstream-wedge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream-wedge.d.ts","sourceRoot":"","sources":["../../src/browse/upstream-wedge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAAsC,CAAC;AAE5E,4BAA4B;AAC5B,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,oBAAoB,CAAC;AAE9D,qDAAqD;AACrD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upstream-wedge.ts(BUG-04 决议 B,doc/bugs/04 §5,2026-09-08)
|
|
3
|
+
*
|
|
4
|
+
* 上游 chrome-devtools-mcp@1.7.0「选中页死锁」签名单一真源。
|
|
5
|
+
*
|
|
6
|
+
* 根因(npm tarball 逐行复核,doc/bugs/04 §2):上游 ToolHandler.js 的
|
|
7
|
+
* `const targetPage = page ?? context.getSelectedMcpPage()`(:189)无条件执行——
|
|
8
|
+
* 选中页被关(用户手关 / 页面 window.close / 会话收尾关自建 tab)后,**任何**
|
|
9
|
+
* 工具调用(含 list_pages 自身)都在此处 throw 落入外层 catch,绕过唯一自愈点
|
|
10
|
+
* `createPagesSnapshot()`——通道级死锁,MCP 进程重启才解。
|
|
11
|
+
*
|
|
12
|
+
* 本模块只放签名常量 + 判定函数(零依赖叶子,BrowseChannel / TabRegistry /
|
|
13
|
+
* LoggedInChannel 三方共用——签名定义不允许第二处漂移)。
|
|
14
|
+
*
|
|
15
|
+
* INV-89 机械锚。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 上游 McpContext.getSelectedMcpPage() 的整串错误子串(build/src/McpContext.js:253):
|
|
19
|
+
* `The selected page has been closed. Call list_pages to see open pages.`
|
|
20
|
+
*
|
|
21
|
+
* 窄匹配(只用前半句)防误伤:页面正文/标题正常场景不会以这个精确短语出现;
|
|
22
|
+
* 若未来页面内容真含此串,被动检测面(错误文本)不受影响——只有主动
|
|
23
|
+
* reconcile 面(list_pages 全响应)可能命中,而该响应在健康通道上恒为页列表。
|
|
24
|
+
*/
|
|
25
|
+
export const UPSTREAM_WEDGE_SIGNATURE = /The selected page has been closed/;
|
|
26
|
+
/** 判定错误/响应文本是否上游选中页死锁签名。 */
|
|
27
|
+
export function isUpstreamWedgeError(text) {
|
|
28
|
+
return UPSTREAM_WEDGE_SIGNATURE.test(text);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* reconcile 类型化信号前缀(doc/bugs/04 §5 主动接入点):
|
|
32
|
+
* `TabRegistry.reconcile` 命中签名 → throw `upstream_wedge:<原文摘录>`;
|
|
33
|
+
* LoggedInChannel.getMcpClient 捕获该前缀 → 返回 client 前触发 heal。
|
|
34
|
+
*/
|
|
35
|
+
export const UPSTREAM_WEDGE_SIGNAL_PREFIX = "upstream_wedge:";
|
|
36
|
+
/** 判定是否 reconcile 抛出的类型化楔死信号(区别于普通 reconcile 失败)。 */
|
|
37
|
+
export function isUpstreamWedgeTypedSignal(msg) {
|
|
38
|
+
return msg.startsWith(UPSTREAM_WEDGE_SIGNAL_PREFIX);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=upstream-wedge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream-wedge.js","sourceRoot":"","sources":["../../src/browse/upstream-wedge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,mCAAmC,CAAC;AAE5E,4BAA4B;AAC5B,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,iBAAiB,CAAC;AAE9D,qDAAqD;AACrD,MAAM,UAAU,0BAA0B,CAAC,GAAW;IACpD,OAAO,GAAG,CAAC,UAAU,CAAC,4BAA4B,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -115,6 +115,20 @@ export declare abstract class BrowseChannel extends UiChannel {
|
|
|
115
115
|
* 成功返 true → browseSingle 原样重试一次该 action。
|
|
116
116
|
*/
|
|
117
117
|
protected recoverNoPageSelected(_c: McpClient): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* BUG-04 决议 B(doc/bugs/04 §5):上游选中页死锁("The selected page has
|
|
120
|
+
* been closed"——上游 ToolHandler.js:189 结构性死锁,通道级)自愈钩子。
|
|
121
|
+
* 默认 null(HeadlessChannel 同样可能楔死但通过 spec 自管 respawn 兜底,
|
|
122
|
+
* 行为零变化);LoggedInChannel override 两层:
|
|
123
|
+
* 层 1 `new_page {background:true}`——上游 pages.js handler 内
|
|
124
|
+
* `context.newPage → selectPage`(McpContext.js:212)先于 :189 执行,
|
|
125
|
+
* 结构性逃逸口;成功返回**原 client**(选中页已重置)。
|
|
126
|
+
* 层 2 respawn upstream spec(SubprocessManager.restart——只杀 npx 上游
|
|
127
|
+
* 子进程,**永不触碰 Chrome**);成功返回**新 client**。
|
|
128
|
+
* 返 null = 自愈失败 → 调用方走 `upstream_wedge_selected_page_closed`
|
|
129
|
+
* 透明错误(unknown:通道错,fallback 语义正确)。
|
|
130
|
+
*/
|
|
131
|
+
protected healUpstreamWedge(_c: McpClient): Promise<McpClient | null>;
|
|
118
132
|
/**
|
|
119
133
|
* P10(v1.18.1):上游工具缺失探测(带 per-client 缓存——同一上游子进程
|
|
120
134
|
* 生命周期内工具集不变;listTools 抛错 → false 放行,让真实调用浮出错误)。
|
|
@@ -188,20 +202,46 @@ export declare abstract class BrowseChannel extends UiChannel {
|
|
|
188
202
|
private persistState;
|
|
189
203
|
}
|
|
190
204
|
/**
|
|
191
|
-
* E④(BUG-03 决议 E
|
|
205
|
+
* E④(BUG-03 决议 E④)+ BUG-04 决议 C1(doc/bugs/04 §7):evaluate 的 js 入参 →
|
|
206
|
+
* 上游 function 参数归一(导出供测试)。
|
|
192
207
|
*
|
|
193
|
-
*
|
|
208
|
+
* 三形态兼容:
|
|
194
209
|
* - **函数表达式**(起手 `(` / `function` / `async`,或单标识符箭头 `x => x`)
|
|
195
210
|
* → 原样透传——上游 evaluate_script 契约本就吃函数表达式并自调用;旧 wrapper
|
|
196
211
|
* 会把它包成「函数体内的函数表达式语句」(求值不 return → 恒 undefined 静默
|
|
197
212
|
* 错值——消费方④实战毒点)。
|
|
213
|
+
* - **IIFE 立即执行式**(`(async () => {...})()` / `(() => {...})()` /
|
|
214
|
+
* `(function () { ... })()`)→ 包成表达式体箭头 `() => (\n${t}\n)`——上游
|
|
215
|
+
* performEvaluation(script.js:158-165)= `evaluateHandle('(' + fnString + ')')`
|
|
216
|
+
* 后 `fn(...args)`:IIFE 串求值成**结果**而非函数 → `fn is not a function`
|
|
217
|
+
* (报告 §9-②a)。包裹成 `() => (iife)` 后外层箭头求值即得 IIFE 结果
|
|
218
|
+
* (Promise 由上游 await fn() 解开,语义不变)。
|
|
198
219
|
* - **语句体**(`return ...` / 声明 / 多语句 / 裸表达式)→ 维持包裹
|
|
199
220
|
* `() => { ... }`(W1-DEF-1b:直接透传 `return` 会被当函数表达式语法错,
|
|
200
221
|
* wave2 smoke 实证 "Unexpected token 'return'")。
|
|
201
|
-
*
|
|
202
|
-
*
|
|
222
|
+
*
|
|
223
|
+
* IIFE 判定(C1 结构化规则,防误伤尾调用箭头):起手函数表达式 token + 剥尾 `;`
|
|
224
|
+
* 后以 `()` 收尾 + 其前一字符属于平衡的 `)`/`}` 组。反例锚:
|
|
225
|
+
* - `() => document.getElementById('x').click()`(箭头尾调用)——末尾 `()` 前是
|
|
226
|
+
* 标识符 → 透传(上游直接自调用该箭头,语义正确);
|
|
227
|
+
* - `() => (foo)(x)` —— 尾参非空括号组(`(x)` 而非 `()`)→ 维持透传
|
|
228
|
+
* (文档化取向:静默变更风险大于响亮报错——若真是 IIFE 形态,调用方改写
|
|
229
|
+
* 起手 `(` 形态即可)。
|
|
230
|
+
* 判定规则刻意保守:误判方向 = 语句体被透传(上游报语法错,响亮可修),优于
|
|
231
|
+
* 函数表达式被包裹(静默 undefined,毒中之毒)。IIFE 误包裹方向同样安全——
|
|
232
|
+
* 表达式位置包 `() => (expr)` 求值不变。
|
|
203
233
|
*/
|
|
204
234
|
export declare function evaluateFunctionArg(js: string): string;
|
|
235
|
+
/**
|
|
236
|
+
* BUG-04 决议 C1:IIFE 立即执行式结构化判定(导出供测试)。
|
|
237
|
+
*
|
|
238
|
+
* 规则(三条全中才判 IIFE):
|
|
239
|
+
* 1. 剥尾 `;`(含尾随空白)后以空参调用 `()` 收尾;
|
|
240
|
+
* 2. 其前一字符是 `)` 或 `}`(闭合的函数体/括号组——排除 `fn()` / `.click()`
|
|
241
|
+
* 等标识符尾调用);
|
|
242
|
+
* 3. 全串括号/花括号平衡(排除截断/畸形输入——畸形按响亮上游错误处理)。
|
|
243
|
+
*/
|
|
244
|
+
export declare function isIifeString(t: string): boolean;
|
|
205
245
|
/**
|
|
206
246
|
* v1.17(doc/governance/06 verify ⑤,真机实证):refs 附录感知截断——长页正文超
|
|
207
247
|
* PREVIEW_MAX_CHARS 时朴素 truncatePreview 会把**缀在末尾**的
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowseChannel.d.ts","sourceRoot":"","sources":["../../src/channels/BrowseChannel.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,MAAM,EACN,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"BrowseChannel.d.ts","sourceRoot":"","sources":["../../src/channels/BrowseChannel.ts"],"names":[],"mappings":"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"}
|
|
@@ -43,6 +43,8 @@ import { expectPoll, } from "../browse/ExpectPoll.js";
|
|
|
43
43
|
import { StepEngine } from "../browse/StepEngine.js";
|
|
44
44
|
import { BudgetTracker, DEFAULT_CHAIN_BUDGET_MS, clampChainBudgetMs } from "../fallback/BudgetTracker.js";
|
|
45
45
|
import { applyOutputEnvelope } from "../util/output-envelope.js";
|
|
46
|
+
// BUG-04 决议 B(doc/bugs/04 §5):上游选中页死锁签名单一真源(INV-89 锚)
|
|
47
|
+
import { isUpstreamWedgeError } from "../browse/upstream-wedge.js";
|
|
46
48
|
import { parseEvalResult, evalFence, imageBlock, firstText, } from "../browse/upstream-response.js";
|
|
47
49
|
// v0.5 M0.5b/M0.5c(parse6 §2.1 + §3.3.3 + §3.4.2):doPdf + doConsole + doNetwork
|
|
48
50
|
// 追加进 actionDispatch Map
|
|
@@ -290,6 +292,22 @@ export class BrowseChannel extends UiChannel {
|
|
|
290
292
|
async recoverNoPageSelected(_c) {
|
|
291
293
|
return false;
|
|
292
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* BUG-04 决议 B(doc/bugs/04 §5):上游选中页死锁("The selected page has
|
|
297
|
+
* been closed"——上游 ToolHandler.js:189 结构性死锁,通道级)自愈钩子。
|
|
298
|
+
* 默认 null(HeadlessChannel 同样可能楔死但通过 spec 自管 respawn 兜底,
|
|
299
|
+
* 行为零变化);LoggedInChannel override 两层:
|
|
300
|
+
* 层 1 `new_page {background:true}`——上游 pages.js handler 内
|
|
301
|
+
* `context.newPage → selectPage`(McpContext.js:212)先于 :189 执行,
|
|
302
|
+
* 结构性逃逸口;成功返回**原 client**(选中页已重置)。
|
|
303
|
+
* 层 2 respawn upstream spec(SubprocessManager.restart——只杀 npx 上游
|
|
304
|
+
* 子进程,**永不触碰 Chrome**);成功返回**新 client**。
|
|
305
|
+
* 返 null = 自愈失败 → 调用方走 `upstream_wedge_selected_page_closed`
|
|
306
|
+
* 透明错误(unknown:通道错,fallback 语义正确)。
|
|
307
|
+
*/
|
|
308
|
+
async healUpstreamWedge(_c) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
293
311
|
/**
|
|
294
312
|
* P10(v1.18.1):上游工具缺失探测(带 per-client 缓存——同一上游子进程
|
|
295
313
|
* 生命周期内工具集不变;listTools 抛错 → false 放行,让真实调用浮出错误)。
|
|
@@ -353,13 +371,32 @@ export class BrowseChannel extends UiChannel {
|
|
|
353
371
|
partial = await this.dispatchAction(c, action, url, options, handler);
|
|
354
372
|
}
|
|
355
373
|
catch (e) {
|
|
374
|
+
// BUG-04 决议 B(doc/bugs/04 §5,被动接入点):上游选中页死锁签名 →
|
|
375
|
+
// heal 一次 + 原样重试一次(P6 自愈重试先例)。楔死是通道级瞬态
|
|
376
|
+
// (选中页句柄失效,new_page/respawn 可解),不是页面语义否定。
|
|
377
|
+
// 失败方向:heal 失败 → `upstream_wedge_selected_page_closed` 透明前缀
|
|
378
|
+
// (unknown);heal 成功但重试仍失败 → `upstream_wedge_unhealed`(同
|
|
379
|
+
// unknown——fallback 到 headless 语义正确)。
|
|
380
|
+
if (isUpstreamWedgeError(String(e))) {
|
|
381
|
+
const healed = await this.healUpstreamWedge(c);
|
|
382
|
+
if (healed === null) {
|
|
383
|
+
throw new Error(`upstream_wedge_selected_page_closed:${String(e).slice(0, 200)}`);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
partial = await this.dispatchAction(healed, action, url, options, handler);
|
|
387
|
+
}
|
|
388
|
+
catch (e2) {
|
|
389
|
+
throw new Error(`upstream_wedge_unhealed:${String(e2).slice(0, 200)}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else if (
|
|
356
393
|
// P6(v1.18.1,得到实战问题集 P6):上游 chrome-devtools-mcp 在 0 page
|
|
357
394
|
// target 状态(--no-startup-window 起的 Chrome;台账被上一代 server 停机
|
|
358
395
|
// 清空的遗留 Chrome——precreate 判定门跳过 + ensureOwnPageSelected 零页
|
|
359
396
|
// silent bail)下所有页级调用抛 "No page selected"(getSelectedMcpPage)。
|
|
360
397
|
// 自愈钩子(LoggedInChannel:CDP 预建 background tab + select_page)成功 →
|
|
361
398
|
// 原样重试一次;失败/不支持 → 原错误路径(classify 落 unknown)。
|
|
362
|
-
|
|
399
|
+
NO_PAGE_SELECTED_RE.test(String(e)) &&
|
|
363
400
|
(await this.recoverNoPageSelected(c))) {
|
|
364
401
|
partial = await this.dispatchAction(c, action, url, options, handler);
|
|
365
402
|
}
|
|
@@ -1210,27 +1247,98 @@ const UPSTREAM_EVAL_ERROR_SIGNATURES = [
|
|
|
1210
1247
|
/^No page selected$/,
|
|
1211
1248
|
];
|
|
1212
1249
|
/**
|
|
1213
|
-
* E④(BUG-03 决议 E
|
|
1250
|
+
* E④(BUG-03 决议 E④)+ BUG-04 决议 C1(doc/bugs/04 §7):evaluate 的 js 入参 →
|
|
1251
|
+
* 上游 function 参数归一(导出供测试)。
|
|
1214
1252
|
*
|
|
1215
|
-
*
|
|
1253
|
+
* 三形态兼容:
|
|
1216
1254
|
* - **函数表达式**(起手 `(` / `function` / `async`,或单标识符箭头 `x => x`)
|
|
1217
1255
|
* → 原样透传——上游 evaluate_script 契约本就吃函数表达式并自调用;旧 wrapper
|
|
1218
1256
|
* 会把它包成「函数体内的函数表达式语句」(求值不 return → 恒 undefined 静默
|
|
1219
1257
|
* 错值——消费方④实战毒点)。
|
|
1258
|
+
* - **IIFE 立即执行式**(`(async () => {...})()` / `(() => {...})()` /
|
|
1259
|
+
* `(function () { ... })()`)→ 包成表达式体箭头 `() => (\n${t}\n)`——上游
|
|
1260
|
+
* performEvaluation(script.js:158-165)= `evaluateHandle('(' + fnString + ')')`
|
|
1261
|
+
* 后 `fn(...args)`:IIFE 串求值成**结果**而非函数 → `fn is not a function`
|
|
1262
|
+
* (报告 §9-②a)。包裹成 `() => (iife)` 后外层箭头求值即得 IIFE 结果
|
|
1263
|
+
* (Promise 由上游 await fn() 解开,语义不变)。
|
|
1220
1264
|
* - **语句体**(`return ...` / 声明 / 多语句 / 裸表达式)→ 维持包裹
|
|
1221
1265
|
* `() => { ... }`(W1-DEF-1b:直接透传 `return` 会被当函数表达式语法错,
|
|
1222
1266
|
* wave2 smoke 实证 "Unexpected token 'return'")。
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1267
|
+
*
|
|
1268
|
+
* IIFE 判定(C1 结构化规则,防误伤尾调用箭头):起手函数表达式 token + 剥尾 `;`
|
|
1269
|
+
* 后以 `()` 收尾 + 其前一字符属于平衡的 `)`/`}` 组。反例锚:
|
|
1270
|
+
* - `() => document.getElementById('x').click()`(箭头尾调用)——末尾 `()` 前是
|
|
1271
|
+
* 标识符 → 透传(上游直接自调用该箭头,语义正确);
|
|
1272
|
+
* - `() => (foo)(x)` —— 尾参非空括号组(`(x)` 而非 `()`)→ 维持透传
|
|
1273
|
+
* (文档化取向:静默变更风险大于响亮报错——若真是 IIFE 形态,调用方改写
|
|
1274
|
+
* 起手 `(` 形态即可)。
|
|
1275
|
+
* 判定规则刻意保守:误判方向 = 语句体被透传(上游报语法错,响亮可修),优于
|
|
1276
|
+
* 函数表达式被包裹(静默 undefined,毒中之毒)。IIFE 误包裹方向同样安全——
|
|
1277
|
+
* 表达式位置包 `() => (expr)` 求值不变。
|
|
1225
1278
|
*/
|
|
1226
1279
|
export function evaluateFunctionArg(js) {
|
|
1227
1280
|
const t = js.trim();
|
|
1228
|
-
if (/^(?:async\b|function\b|\()/.test(t))
|
|
1229
|
-
|
|
1281
|
+
if (/^(?:async\b|function\b|\()/.test(t)) {
|
|
1282
|
+
// 函数表达式起手 token;IIFE 形态(结构化尾部调用判定)包成表达式体箭头
|
|
1283
|
+
//(剥尾 `;`——表达式体内 `expr;` 是语法错;其余 byte 保留)
|
|
1284
|
+
return isIifeString(t) ? `() => (\n${t.replace(/;\s*$/, "")}\n)` : t;
|
|
1285
|
+
}
|
|
1230
1286
|
if (/^[A-Za-z_$][\w$]*\s*=>/.test(t))
|
|
1231
1287
|
return t; // 单标识符箭头 x => x
|
|
1232
1288
|
return `() => {\n${js}\n}`; // 语句体维持包裹
|
|
1233
1289
|
}
|
|
1290
|
+
/**
|
|
1291
|
+
* BUG-04 决议 C1:IIFE 立即执行式结构化判定(导出供测试)。
|
|
1292
|
+
*
|
|
1293
|
+
* 规则(三条全中才判 IIFE):
|
|
1294
|
+
* 1. 剥尾 `;`(含尾随空白)后以空参调用 `()` 收尾;
|
|
1295
|
+
* 2. 其前一字符是 `)` 或 `}`(闭合的函数体/括号组——排除 `fn()` / `.click()`
|
|
1296
|
+
* 等标识符尾调用);
|
|
1297
|
+
* 3. 全串括号/花括号平衡(排除截断/畸形输入——畸形按响亮上游错误处理)。
|
|
1298
|
+
*/
|
|
1299
|
+
export function isIifeString(t) {
|
|
1300
|
+
const s = t.replace(/;\s*$/, "");
|
|
1301
|
+
if (!s.endsWith("()"))
|
|
1302
|
+
return false;
|
|
1303
|
+
const before = s.slice(0, -2);
|
|
1304
|
+
const last = before.charAt(before.length - 1);
|
|
1305
|
+
if (last !== ")" && last !== "}")
|
|
1306
|
+
return false;
|
|
1307
|
+
return bracketsBalanced(s);
|
|
1308
|
+
}
|
|
1309
|
+
/** 括号/花括号平衡(含字符串字面量感知——字符串内的括号不计数)。 */
|
|
1310
|
+
function bracketsBalanced(s) {
|
|
1311
|
+
let round = 0;
|
|
1312
|
+
let curly = 0;
|
|
1313
|
+
let quote = null;
|
|
1314
|
+
let escaped = false;
|
|
1315
|
+
for (const ch of s) {
|
|
1316
|
+
if (quote !== null) {
|
|
1317
|
+
if (escaped)
|
|
1318
|
+
escaped = false;
|
|
1319
|
+
else if (ch === "\\")
|
|
1320
|
+
escaped = true;
|
|
1321
|
+
else if (ch === quote)
|
|
1322
|
+
quote = null;
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1326
|
+
quote = ch;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (ch === "(")
|
|
1330
|
+
round++;
|
|
1331
|
+
else if (ch === ")")
|
|
1332
|
+
round--;
|
|
1333
|
+
else if (ch === "{")
|
|
1334
|
+
curly++;
|
|
1335
|
+
else if (ch === "}")
|
|
1336
|
+
curly--;
|
|
1337
|
+
if (round < 0 || curly < 0)
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
return round === 0 && curly === 0 && quote === null;
|
|
1341
|
+
}
|
|
1234
1342
|
/** navigate_page 返回里偶尔带 final_url;找不到就用 undefined(上游 fallback url)。 */
|
|
1235
1343
|
function extractFinalUrl(r) {
|
|
1236
1344
|
const txt = firstText(r);
|
|
@@ -1339,6 +1447,12 @@ function classifyBrowseError(msg, _action) {
|
|
|
1339
1447
|
// session_rotated 前缀供 agent 透明识别下一步(重 snapshot),不再落泛 unknown
|
|
1340
1448
|
if (m.includes("session_rotated"))
|
|
1341
1449
|
return "unknown";
|
|
1450
|
+
// BUG-04 决议 B(doc/bugs/04 §5):上游选中页死锁(upstream_wedge_* 前缀:
|
|
1451
|
+
// selected_page_closed / unhealed)是**通道级瞬态**(选中页句柄失效,
|
|
1452
|
+
// heal/fallback 换通道可解)——unknown(可重试 + fallback-worthy),区别于
|
|
1453
|
+
// C2 的 eval_upstream_error(调用方坏 JS,didnt)。
|
|
1454
|
+
if (m.includes("upstream_wedge"))
|
|
1455
|
+
return "unknown";
|
|
1342
1456
|
if (m.includes("dns_or_nav_error"))
|
|
1343
1457
|
return "unknown";
|
|
1344
1458
|
if (m.includes("http_404"))
|
|
@@ -1353,6 +1467,13 @@ function classifyBrowseError(msg, _action) {
|
|
|
1353
1467
|
// pdf step 死后整链 unknown(extract-batch1.mjs 实测 chain_failed:unknown:*)。
|
|
1354
1468
|
if (m.includes("upstream_unsupported:"))
|
|
1355
1469
|
return "didnt";
|
|
1470
|
+
// BUG-04 决议 C2(doc/bugs/04 §7,报告 §9-②b):调用方坏 JS(脚本语法/执行错、
|
|
1471
|
+
// 协议超时类 evaluate 上游错)是**确定性不可得**——换通道救不了坏 JS,fallback
|
|
1472
|
+
// 只会拉响备用通道空转(复验报告实测「主通道因调用方坏 JS 失败也拉响 headless」)。
|
|
1473
|
+
// 同 P10 upstream_unsupported 先例 → didnt(直达错误,不 fallback)。
|
|
1474
|
+
// 区分:upstream_wedge_*(决议 B)维持 unknown——那是通道错,fallback 正确。
|
|
1475
|
+
if (m.includes("eval_upstream_error"))
|
|
1476
|
+
return "didnt";
|
|
1356
1477
|
if (/tool \S+ not found/.test(m) || m.includes("unknown tool"))
|
|
1357
1478
|
return "didnt";
|
|
1358
1479
|
return "unknown";
|