lasso-mcp 1.24.0 → 1.25.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 +4 -1
- package/README.md +4 -1
- package/dist/channels/BrowseChannel.d.ts +33 -2
- package/dist/channels/BrowseChannel.d.ts.map +1 -1
- package/dist/channels/BrowseChannel.js +163 -17
- package/dist/channels/BrowseChannel.js.map +1 -1
- package/dist/channels/LoggedInChannel.d.ts.map +1 -1
- package/dist/channels/LoggedInChannel.js +17 -0
- package/dist/channels/LoggedInChannel.js.map +1 -1
- package/dist/doctor/doctor.d.ts +1 -1
- package/dist/doctor/doctor.js +1 -1
- package/dist/fallback/outcome.d.ts.map +1 -1
- package/dist/fallback/outcome.js +6 -0
- package/dist/fallback/outcome.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/annotations.d.ts +4 -1
- package/dist/tools/annotations.d.ts.map +1 -1
- package/dist/tools/annotations.js +4 -1
- package/dist/tools/annotations.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +27 -9
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/descriptions.d.ts.map +1 -1
- package/dist/tools/descriptions.js +43 -6
- package/dist/tools/descriptions.js.map +1 -1
- package/dist/tools/screenshot.d.ts +2 -2
- package/dist/tools/screenshot.d.ts.map +1 -1
- package/dist/tools/screenshot.js +30 -15
- package/dist/tools/screenshot.js.map +1 -1
- package/doc/bugs/07-2026-09-10-/346/266/210/350/264/271/346/226/271/345/217/260/350/264/246L4-/346/210/252/345/233/276/345/277/205/347/273/217/351/207/215/345/257/274/350/210/252/346/227/240/346/263/225/346/210/252/345/275/223/345/211/215/351/241/265/351/235/242/347/212/266/346/200/201.md +340 -0
- package/package.json +1 -1
- package/src/channels/BrowseChannel.ts +183 -19
- package/src/channels/LoggedInChannel.ts +17 -0
- package/src/doctor/doctor.ts +1 -1
- package/src/fallback/outcome.ts +6 -0
- package/src/index.ts +1 -1
- package/src/invariants/check-invariants.mjs +208 -1
- package/src/tools/annotations.ts +4 -1
- package/src/tools/browse.ts +35 -17
- package/src/tools/descriptions.ts +43 -6
- package/src/tools/screenshot.ts +32 -17
package/README.en.md
CHANGED
|
@@ -168,6 +168,8 @@ When you don't need to render a full page, direct HTTP is **~4× faster and ~4×
|
|
|
168
168
|
|
|
169
169
|
All images and PDFs are **saved to disk and a path is returned** — no giant blob dumped into your chat to waste context. Oversized text output (fetch_url / network, etc.) beyond 48 KiB is also written to disk automatically, returning a preview plus an `@oN` continuation handle — page through it with the `read_text` tool (directly callable over MCP since v1.8). To **choose the screenshot output path yourself** (`options.screenshot.filePath`): rejected by default (no arbitrary-path writes) — set `LASSO_SCREENSHOT_DIR=/your/output/dir` to opt in, and writes are confined to the allowlisted subtrees (auto-created nested dirs included); with no path specified the default `/tmp/lasso-screenshot-*.png` behavior is unchanged.
|
|
170
170
|
|
|
171
|
+
> **Shoot "the page's current state"** (mid-frame animation forensics): `action=screenshot` on `browse_headless` / `browse_logged_in` **with url omitted** shoots the page that channel is already managing — **zero re-navigation**, so page state built by earlier evaluate calls (e.g. a CSS animation frozen at 35% via `document.getAnimations().forEach(a=>a.pause())`) survives into the PNG. Recommended loop: any url-bearing action to open the session → `evaluate` to freeze/inject → `{action:"screenshot"}` (no url) for the current frame → `console` for forensics — four channels, one session, one tool. The standalone `screenshot` tool with url omitted does the same (bound to the headless session; for a logged_in session's current frame use `browse_logged_in`). With no active session (cold channel / page closed / rotated by self-heal) it fails explicitly (`didnt` + a rebuild hint) — it **never silently opens a browser and never shoots a blank page fabricating state**; url-bearing usage is byte-for-byte unchanged.
|
|
172
|
+
|
|
171
173
|
### See What a Page Loads
|
|
172
174
|
|
|
173
175
|
> You: "What third-party trackers did this page load?" → resource list with tracker-domain counts
|
|
@@ -210,7 +212,7 @@ Goes to the Internet Archive (Wayback Machine) to find the last archived copy of
|
|
|
210
212
|
|
|
211
213
|
## Install
|
|
212
214
|
|
|
213
|
-
**Current version v1.
|
|
215
|
+
**Current version v1.25.0** (changelog in the collapsed block at the end of this section).
|
|
214
216
|
|
|
215
217
|
Prerequisites: Node.js ≥ 20 + Claude Code (or any MCP-capable client).
|
|
216
218
|
|
|
@@ -228,6 +230,7 @@ Restart Claude Code → `/mcp` → `lasso ✓ Connected`. **That's the one line
|
|
|
228
230
|
<summary>📋 Changelog (v1.8 → v1.18.6 — click to see what each version changed)</summary>
|
|
229
231
|
|
|
230
232
|
- **v1.24.0 (unreleased, merged in this working tree)**: 🔴 **behavior change (BUG-06 — root fix for the 12-hour ghost-resident incident, doc/bugs/06)** — ① `--idle-ms 0` / `LASSO_LAUNCH_IDLE_MS=0` semantics change from "never reap" to "**hard-cap reap after 24 hours of no activity**" (counted from the last activity signal — lasso browse now writes the touch file automatically, external consumers keep it alive with `touch ~/.cache/lasso/chrome-touch-<port>`, so **active use never triggers it**; new key `LASSO_LAUNCH_HARD_CAP_MS` tunes or disables it); truly never reap = the two-flag deliberate form `--idle-ms 0 --no-hard-cap`, or deployment-level `LASSO_LAUNCH_HARD_CAP_MS=0`. visible / user-claimed (chrome-show) exemptions unchanged (r2: a claim landing between the reap decision and the kill is honored too — the reaper's default stop now re-evaluates userTakenAt at kill time, closing the adversarial-review P2 TOCTOU); the render tier is untouched (`LASSO_RENDER_IDLE_MS` stays independent, pinned by the INV-94 tripwire). **On upgrade, pre-existing over-age idle-0 ledger records are reaped by the first reaper tick (≤15s)** — this is intended (such instances are exactly the incident's ghost form); nohup recipes relying on 0=infinite without touching should migrate (see TROUBLESHOOTING §9.3 / KEY-GUIDE). ② cross-process activity source of truth (A-7): `lasso browse` activity now lands in the touch file on disk — visible to the enforcer reaper host (previously it only touched in-server process memory, leaving a 24h/30min mis-reap blind spot whenever both hosts coexist, fixed in the same stroke); ③ over-age observability (B): chrome-status/doctor continuously emit an advisory warn block for capped records past 50% of the cap (expected reap time + touch keep-alive recipe + the user's own chrome-stop exit) with zero new kill/directive surface; ④ with global `LASSO_LAUNCH_IDLE_MS=0` the reaper now runs in cap-only mode (D2 — disabling idle reaping no longer also dismantles the hard-cap backstop). Adversarial round r1 adopted both P1 findings (N1 cross-process blind spot / N2 default flipped to 0 and sunk into the assembly layers); round r2 closed the reaper TOCTOU (P2 — the default reap exit now re-evaluates userTakenAt at kill time); new invariant INV-94.
|
|
233
|
+
- **v1.25.0**: current-page screenshot batch (BUG-07, novel-engine animation-forensics driven) — `action=screenshot` **with url omitted now screenshots the current managed page directly** (the standalone `screenshot` tool gains the same semantics): previously both screenshot paths re-navigated unconditionally, wiping whatever page state evaluate had established (a CSS animation paused mid-frame, injected temporary DOM) — state-sensitive screenshots were unreachable and the consumer had to detour through a second browser-automation stack. Now the same session closes the loop across four channels: open with url → `evaluate` freeze/inject → screenshot without url → `console` forensics. With no active session (cold channel / page closed / healed-over), it errors explicitly with guidance — **never silently opening a browser, never faking state with a blank page**; the old url-bearing behavior is unchanged (pinned by regression anchors); three clean adversarial rounds — 6/6 mutations red + session-drift attacks on live dual channels with zero penetration (concurrent races refused / popup vectors pinned to the managed page / silent page-swap invalidated at the commit point). Upstream research verdict: the industry-universal screenshot semantic IS "capture the current state" — this version realigns lasso with the standard.
|
|
231
234
|
- **v1.24.0**: ghost-instance root-fix batch (BUG-06, driven by a live 12-hour instance) — ① 🔴 **behavior change**: `launch-chrome --idle-ms 0` changes from "never reap" to "**no self-reaping, but a 24-hour hard cap as a safety net**" — a windowless hidden instance can no longer linger indefinitely as an "invisible, uncloseable ghost" (this batch's trigger: a 12-hour instance invisible in the Dock and menu bar with no way to close it). True unlimited now requires the additional explicit `--no-hard-cap` (**double intent** — prevents scripts passing 0 casually); deployments can tune or disable via `LASSO_LAUNCH_HARD_CAP_MS` (`=0` disables entirely); failure direction is always safe (invalid values fall back to capped). Old scripts relying on "0 = forever" should add `--no-hard-cap`. ② **cross-process liveness truth**: before hard-cap reaping, the touch-file heartbeat is consulted (any session / any consumer still using it → no wrongful kill); userTakenAt / visible exemptions carry over natively (re-evaluated at kill time, the race window sealed). ③ **over-age observability**: `doctor` / `chrome-status` warn early on instances past half the hard cap (no new kill surface added). Six live-machine acceptance checks all PASS + two clean adversarial rounds (three mutations all red / wrongful-kill panorama replayed with live stand-in processes, zero casualties / render tier zero cross-contamination).
|
|
232
235
|
- **v1.23.0**: local-file & debugging batch (BUG-05, novel-engine field driven) — ① **`LASSO_ALLOW_FILE_FROM` file:// directory allowlist**: testing local single-file HTML (a staple for offline-artifact acceptance) used to be flat-blocked by the SSRF guard — now, with `LASSO_ALLOW_FILE_FROM=/your/project/dir`, `browse_*` can navigate/snapshot/extract/screenshot local files **inside the designated directory subtree** (still denied by default; zero-config = zero behavior change; `../` traversal, symlink escapes, prefix forgery and trailing-symlink write escapes all sealed — 18 read-side + 10 write-side attack variants, zero escapes); ② new **`console` action**: read the current page's console messages since the last navigation (severity filter + most-recent-N) — no more window.onerror injection detours when debugging rendering defects; ③ **`ignored_options` honesty contract**: options keys you passed that the current action doesn't consume are listed verbatim in the response (never silently dropped — one root cause of the "worked but no screenshot" trap the consumer hit); and to be explicit: navigate-then-screenshot in one step **is already what `action=screenshot` does**; ④ **`LASSO_SCREENSHOT_DIR` screenshot write-root constraint**: custom screenshot output paths are denied by default (arbitrary-write protection), opt-in and confined to the designated subtree. Two clean adversarial rounds: 6/6 mutations killed + proof the core SSRF guard is unweakened + consumer scenario (real single-file HTML over both http and file://) fully verified on a live machine.
|
|
233
236
|
- **v1.22.1**: audit & stability batch — ① six consumer-deep-inspection callback items closed: the cc-control hook sample **fixed for three defects and now ships in-repo with 17 vector tests** (samples must live in-repo, docs only point — deliverable-quality rule established), doctor port-probe detail wording made honest across four outcome shapes, version-alignment tests generalized (assertions read package.json as the single source of truth), `npm run gate` gate hardening (explicit count assertions — pipes can no longer swallow exit codes); ② full review per the 03 audit checklist (four-track six-dimension + Gauntlet verification + clean double-blind adversarial); ③ three timing flakes fixed (wall-clock boundary → fixed-clock injection, sleep-for-disk → polling, threshold aligned to the discrimination surface) — verified by five consecutive full-suite green runs + gate.
|
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ key 怎么申请、免费额度多少 → [**Key 配置指南**](./doc/usage/01-
|
|
|
169
169
|
|
|
170
170
|
所有图片和 PDF 都**存到本地、返回路径**,不会把一大坨图片数据塞进对话浪费上下文。超大文本输出(fetch_url / network 等)超过 48 KiB 也会自动落盘,返回预览 + `@oN` 续页句柄——用 `read_text` 工具按页续读(v1.8 起经 MCP 可直接调用)。要**自定截图落盘位置**(`options.screenshot.filePath`):默认拒(防任意路径写盘)——设 `LASSO_SCREENSHOT_DIR=/你的输出目录` 开放,且只允许写进指定目录子树(嵌套新目录自动创建也限在内);不指定路径时默认落 `/tmp/lasso-screenshot-*.png`,行为不变。
|
|
171
171
|
|
|
172
|
+
> **截「当前页面状态」**(动画中间帧取证):`browse_headless` / `browse_logged_in` 的 `action=screenshot` **省略 url** 即对当前受管页面直接截屏——**零重导航**,前面 evaluate 建立的页面状态(如 `document.getAnimations().forEach(a=>a.pause())` 冻结在 35% 的 CSS 动画)原样保留。推荐循环:带 url 的 action 建会话 → `evaluate` 冻结/注入 → `{action:"screenshot"}`(无 url)截当前帧 → `console` 取证——同会话四通道闭环。独立 `screenshot` 工具省略 url 同义(绑 headless 会话;logged_in 会话的当前帧用 `browse_logged_in`)。无活动会话(冷通道/页面已关/被自愈换页)会显式报错(`didnt` + 下一步指引),**绝不静默新开浏览器、绝不截空白页伪造状态**;带 url 的旧用法行为零变化。
|
|
173
|
+
|
|
172
174
|
### 看一个页面加载了什么
|
|
173
175
|
|
|
174
176
|
> 你:「这页加载了哪些第三方跟踪?」 → 资源列表 + 跟踪域名计数
|
|
@@ -219,7 +221,7 @@ macOS 上能控 Finder / Mail / Safari / Notes / 系统设置等任何原生 app
|
|
|
219
221
|
|
|
220
222
|
## 安装
|
|
221
223
|
|
|
222
|
-
**当前版本 v1.
|
|
224
|
+
**当前版本 v1.25.0**(更新日志见本节末尾折叠块)。
|
|
223
225
|
|
|
224
226
|
前提:Node.js ≥ 20 + Claude Code(或任何支持 MCP 的客户端)。
|
|
225
227
|
|
|
@@ -237,6 +239,7 @@ claude mcp add lasso -- npx -y lasso-mcp
|
|
|
237
239
|
<summary>📋 更新日志(v1.8 → v1.23.0,点开看每版改了什么)</summary>
|
|
238
240
|
|
|
239
241
|
- **v1.24.0(未发版,本批已合入工作树)**:🔴 **行为变更(BUG-06,12h 幽灵常驻事故根治,doc/bugs/06)**——① `--idle-ms 0` / `LASSO_LAUNCH_IDLE_MS=0` 语义从「永不回收」改为「**无活动 24 小时硬顶回收**」(自最近活动起算——lasso browse 自动落盘 touch 文件或外部 `touch ~/.cache/lasso/chrome-touch-<端口>` 续命,**在用永不触发**;`LASSO_LAUNCH_HARD_CAP_MS` 新键可调/禁);真·无限常驻 = `--idle-ms 0 --no-hard-cap` 双旗,或部署级 `LASSO_LAUNCH_HARD_CAP_MS=0`。visible/用户认领(chrome-show)档豁免不变(r2:收割判定与执行间隙落下的认领同样豁免——reaper 默认收割出口在 kill 时刻重估 userTakenAt,对抗复审 P2 TOCTOU 闭口);渲染档零变化(`LASSO_RENDER_IDLE_MS` 独立,INV-94 tripwire 钉死)。**升级瞬间存量超龄 idle-0 记录将被首个收割 tick(≤15s)回收**——属预期(该类实例即事故幽灵形态);依赖 0=无限且不 touch 的 nohup 配方请迁移(TROUBLESHOOTING §9.3 / KEY-GUIDE)。② 跨进程活动真源(A-7):`lasso browse` 活动现自动落盘 touch 文件——执守收割宿主可见(此前只写 server 进程内存,双宿主并时有 24h/30min 误杀盲区,同型同修);③ 超龄可观测(B):chrome-status/doctor 对硬顶过半(>50%)的在案实例持续给出 warn 咨询块(预期回收时刻 + touch 续命法 + 用户 chrome-stop 出口),零新增 kill/指令面;④ 全局 `LASSO_LAUNCH_IDLE_MS=0` 下 reaper 以 cap-only 模式运行(D2——不再连硬顶兜底一起拆)。对抗复审 r1 两项 P1 采纳(N1 跨进程盲区 / N2 缺省反转 0 下沉装配层);r2 闭 reaper TOCTOU(P2——默认收割出口 kill 时刻对 userTakenAt 重估);新不变量 INV-94。
|
|
242
|
+
- **v1.25.0**:当前页截图批(BUG-07,novel-engine 动画取证驱动)——`action=screenshot` **省略 url 即对当前受管页面直接截屏**(独立 `screenshot` 工具同义):此前两条截图路径都必经重导航,把 evaluate 建立的页面现场(冻结的 CSS 动画中间帧、注入的临时 DOM)全部洗掉——状态敏感截图不可达,消费方被迫借道第二套浏览器栈。现在同会话四通道闭环:带 url 建会话 → `evaluate` 冻结/注入 → 无 url 截当前帧 → `console` 取证。无活动会话(冷通道/页面已关/被自愈换页)显式报错带指引,**绝不静默新开、绝不截空白页伪造状态**;带 url 的旧行为零变化(回归锚钉死);对抗三轮 clean——6 变异全红 + 会话漂移攻击真机双通道零穿透(并发竞态拒/弹窗钉受管页/静默换页 invalidate 封口)。上游调研定谳:截图的业界普适语义就是「截当前状态」——本版把 lasso 对齐回行业标准。
|
|
240
243
|
- **v1.24.0**:幽灵常驻根治批(BUG-06,用户 12 小时实例实锤驱动)——① 🔴 **行为变更**:`launch-chrome --idle-ms 0` 的语义从「永不回收」改为「**不自收,但 24 小时硬顶保命**」——无窗口的隐藏实例从此不可能无限常驻成「看不见也关不掉的幽灵」(本轮用户实锤:一个 12 小时实例,Dock/菜单栏不可见无从关闭)。要真正无限需再显式 `--no-hard-cap`(**双重意图**,防脚本顺手传 0);部署级可用 `LASSO_LAUNCH_HARD_CAP_MS` 调整或 `=0` 全禁;失效方向恒安全(非法值回退有顶)。旧脚本若依赖「0=无限」请加 `--no-hard-cap`。② **跨进程活动真源**:硬顶收割前看 touch 文件心跳(任何会话/任何消费方还在用 → 不误杀);userTakenAt / visible 豁免天然继承(kill 时刻二次重估,竞态窗口封口)。③ **超龄可观测**:`doctor` / `chrome-status` 对过半硬顶的实例提前 warn(不新增任何 kill 面)。真机验收六项全 PASS + 对抗两轮 clean(三变异全红 / 误杀全景真活替身重放零误伤 / 渲染档零交叉)。
|
|
241
244
|
- **v1.23.0**:本地文件与排障能力批(BUG-05,novel-engine 实测驱动)——① **`LASSO_ALLOW_FILE_FROM` file:// 目录白名单**:测本地单文件 HTML(离线产物验收的标配场景)此前被 SSRF 守卫一刀切拦——现在设 `LASSO_ALLOW_FILE_FROM=/你的项目目录` 后 `browse_*` 可导航/快照/抽取/截图**指定目录子树内**的本地文件(默认仍拒、零配置零行为变化;`../` 穿越、symlink 逃逸、目录前缀伪造、末段 symlink 写逃逸全封口——读面 18+写面 10 攻击变体零逃逸);② 新 **`console` action**:直接读当前页自上次导航以来的 console 消息(按严重度过滤 + 最近 N 条)——渲染类缺陷排查不再需要注入 window.onerror 绕路;③ **`ignored_options` 诚实契约**:你传了但当前 action 用不上的 options 键会在响应里如实列出(不再静默丢弃——novel-engine 实测踩到的「worked 但截图没生成」根因之一);顺带明确:navigate+截图一步走**本就是 `action=screenshot` 的行为**(先导航再截图);④ **`LASSO_SCREENSHOT_DIR` 截图写根约束**:自定义截图落盘路径默认拒(防任意写盘),opt-in 且限指定目录子树内。对抗两轮 clean:6 变异全红+SSRF 守卫本体零削弱证明+消费方场景(真实单文件 HTML http/file:// 双路径)真机全通。
|
|
242
245
|
- **v1.22.1**:审查与稳定性批——① 消费方深检回告六项收口:给 cc-control 的 hook 硬拦样例**回修三处缺陷并落仓自带 17 向量测试**(样例必落仓、文档只指路——交付物质量制度成型)、doctor 端口探测 detail 文案四形态如实化、版本对齐测试泛化(断言读 package.json 单一真源,不再逐版手改)、`npm run gate` 门禁固化(显式计数断言防管道吞退出码);② 03 审查测试清单全面审查(四轨六维+Gauntlet 验证+双盲对抗 clean);③ 三个时序 flake 收口(墙钟边界改固定时钟注入/sleep 赌落盘改轮询/阈值贴判别面)——全量连跑 5 轮 + gate 全绿实证。
|
|
@@ -75,7 +75,13 @@ export declare abstract class BrowseChannel extends UiChannel {
|
|
|
75
75
|
* INV-12:两条路径都经 withOperation() ALS 包裹(请求级隔离 + StateStore.epoch 派生)。
|
|
76
76
|
* 永不抛异常——所有失败路径走 InteractResult。
|
|
77
77
|
*/
|
|
78
|
-
browse(url: string, action: string, options: BrowseOptions): Promise<InteractResult<BrowseResult>>;
|
|
78
|
+
browse(url: string | undefined, action: string, options: BrowseOptions): Promise<InteractResult<BrowseResult>>;
|
|
79
|
+
/**
|
|
80
|
+
* BUG-07 决议 A⁺(§5.2②):url 省略 + 非 current-page action(含 steps 链)的
|
|
81
|
+
* 显式拒(与 unknown_action 同族——「策略确定性拒」early-return,不 throw:
|
|
82
|
+
* throw 形态会被 classifyBrowseError 落 unknown 触发熔断污染级联)。
|
|
83
|
+
*/
|
|
84
|
+
private urlRequiredResult;
|
|
79
85
|
/**
|
|
80
86
|
* v0.2 单 action 路径(原 browse() 实装,零行为变更;仅迁出便于 browse() 入口分流)。
|
|
81
87
|
*/
|
|
@@ -100,6 +106,10 @@ export declare abstract class BrowseChannel extends UiChannel {
|
|
|
100
106
|
* = 上游新开空白页,L3 实证:此时单发 extract/snapshot 返 about:blank 空内容
|
|
101
107
|
* + outcome=worked)+ url 非占位 about:blank(forest 调度器缺 subtitle 时的
|
|
102
108
|
* 兜底值,无可导航目标)→ 返 true 需先导航。
|
|
109
|
+
*
|
|
110
|
+
* BUG-07 决议 A⁺(§5.2③):签名放宽 url: string | undefined 并前置
|
|
111
|
+
* url !== undefined 守卫——防 undefined 走 `url !== "about:blank"` 求值出
|
|
112
|
+
* true 的隐性边(对抗复审自查项)。
|
|
103
113
|
*/
|
|
104
114
|
private needsFreshPageNav;
|
|
105
115
|
/**
|
|
@@ -108,6 +118,23 @@ export declare abstract class BrowseChannel extends UiChannel {
|
|
|
108
118
|
* 不构成共享 mutable state 耦合面。
|
|
109
119
|
*/
|
|
110
120
|
private readonly navSeenClients;
|
|
121
|
+
/**
|
|
122
|
+
* BUG-07 决议 A⁺(§5.3):current-page 模式的会话真源——最后一次成功导航的
|
|
123
|
+
* client(可空强引用)。与 navSeenClients **同两行写点、单写者**
|
|
124
|
+
* (dispatchAction 现有两处 add),只增只读判定(R-INT-07 教训:不构成
|
|
125
|
+
* 多消费者 mutable state 耦合面)。唯一例外写点 = 自愈成功路径的
|
|
126
|
+
* invalidateCurrentPageSession()(三处调用点,LoggedInChannel;写者仍是
|
|
127
|
+
* 通道自身 protected 方法)。守卫语义 =「client 身份 ∧ 未被 heal 换页」。
|
|
128
|
+
*/
|
|
129
|
+
private lastNavigatedClient;
|
|
130
|
+
/**
|
|
131
|
+
* BUG-07 决议 A⁺(§5.3 r1 铁则 (b)):current-page 会话失效通知。
|
|
132
|
+
* 自愈成功路径(楔死 heal 层 1 new_page / 层 2 respawn / P6 恢复)都会把
|
|
133
|
+
* 「当前受管页面」换成新空白页——client 身份可能不变(层 1 返回原 client),
|
|
134
|
+
* 必须显式置 null 才能让 current-page 截图的守卫拒绝(绝不静默截 about:blank
|
|
135
|
+
* 伪造状态)。navSeenClients(FRESH_PAGE_NAV golden)不动。
|
|
136
|
+
*/
|
|
137
|
+
protected invalidateCurrentPageSession(): void;
|
|
111
138
|
/**
|
|
112
139
|
* P6(v1.18.1,得到实战问题集 P6):"No page selected" 零页面自愈钩子。
|
|
113
140
|
* 默认 false(HeadlessChannel 上游自管浏览器无 0-page 形态,行为零变化);
|
|
@@ -255,6 +282,10 @@ export declare function truncatePreviewKeepingRefs(s: string): string;
|
|
|
255
282
|
/**
|
|
256
283
|
* BUG-05 决议 D1:计算「传入但该 action 未消费」的 options 键(导出供测试)。
|
|
257
284
|
* 只对 worked 出口调用(didnt/unknown 路径不标——错误已自解释)。
|
|
285
|
+
*
|
|
286
|
+
* BUG-07 决议 A⁺(§5.2③):mode="current_page"(url 省略的 screenshot)时
|
|
287
|
+
* 从消费集派生剔除 NAV_ONLY_OPTION_KEYS——该模式下无导航,no_cache 只经
|
|
288
|
+
* doNavigate 消费 → 传入必须出现在 ignored_options(INV-91 精神:禁死键豁免)。
|
|
258
289
|
*/
|
|
259
|
-
export declare function computeIgnoredOptions(action: string, options: BrowseOptions): string[];
|
|
290
|
+
export declare function computeIgnoredOptions(action: string, options: BrowseOptions, mode?: "current_page"): string[];
|
|
260
291
|
//# sourceMappingURL=BrowseChannel.d.ts.map
|
|
@@ -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;AA0C5E;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;AAcpC,8BAAsB,aAAc,SAAQ,SAAS;IACnD,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAE/B,wDAAwD;IACxD,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAErD;;;;;;OAMG;IACH,SAAS,CAAC,cAAc,IAAI,IAAI;IAOhC;;;;;;;;OAQG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,6BAkB9B;IAEH;;;;;;;;OAQG;cACa,cAAc,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE;;;;;;OAMG;cACa,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhE;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC,qDAAqD;IAC/C,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/B,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IAWhC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IAOpC;;;;;;;;;OASG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,
|
|
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;AA0C5E;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;AAcpC,8BAAsB,aAAc,SAAQ,SAAS;IACnD,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAE/B,wDAAwD;IACxD,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAErD;;;;;;OAMG;IACH,SAAS,CAAC,cAAc,IAAI,IAAI;IAOhC;;;;;;;;OAQG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,6BAkB9B;IAEH;;;;;;;;OAQG;cACa,cAAc,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE;;;;;;OAMG;cACa,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhE;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC,qDAAqD;IAC/C,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/B,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC;IAWhC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IAOpC;;;;;;;;;OASG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAwExC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;IACH;;;;;;;;;;;;;;OAcG;YACW,cAAc;IAoC5B;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB;IAazB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;IAE3D;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB,CAA0B;IAErD;;;;;;OAMG;IACH,SAAS,CAAC,4BAA4B,IAAI,IAAI;IAI9C;;;;;OAKG;cACa,qBAAqB,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAItE;;;;;;;;;;;;OAYG;cACa,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAI3E;;;OAGG;YACW,qBAAqB;IAkBnC,+CAA+C;IAC/C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAGlC;YAEU,YAAY;IAuM1B;;;;;;OAMG;IACG,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,EAAE;IACb,+EAA+E;IAC/E,QAAQ,GAAE,MAAgC,GACzC,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;IASvC;;;OAGG;IACH,SAAS,CAAC,kBAAkB,IAAI,gBAAgB,GAAG,IAAI;IAIvD;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAmFvB;;;OAGG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC;IAwDhE;;;OAGG;IACG,SAAS,CACb,IAAI,EAAE,eAAe,EACrB,GAAG,CAAC,EAAE,iBAAiB,EACvB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,UAAU,GAAG,aAAa,GAAG,QAAQ,CAAC;IAMjD;;;;;OAKG;YACW,aAAa;IAwC3B;;;;;;;;;;;;;OAaG;IACG,SAAS,IAAI,OAAO,CACxB,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAC1D;IAkBD;;;;OAIG;YACW,YAAY;CAa3B;AAwnBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAStD;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAO/C;AAyFD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5D;AA6FD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,MAAM,EAAE,CAQV"}
|
|
@@ -189,24 +189,41 @@ export class BrowseChannel extends UiChannel {
|
|
|
189
189
|
* 永不抛异常——所有失败路径走 InteractResult。
|
|
190
190
|
*/
|
|
191
191
|
async browse(url, action, options) {
|
|
192
|
+
// --------------------------------------------------------------
|
|
193
|
+
// BUG-07 决议 A⁺(doc/bugs/07 §5.2②,2026-09-10):url 省略分发语义门。
|
|
194
|
+
// browse() 是唯一入口(tools / forest dispatcher / serp 全经此)——steps
|
|
195
|
+
// 分流**之前**拒绝,防 steps 链在隐式 current-page 上跑出未定义语义。
|
|
196
|
+
// 有 url → 全部现状路径字节级不变(§5.7 向后兼容证明义务 2)。
|
|
197
|
+
// --------------------------------------------------------------
|
|
198
|
+
if (url === undefined && !CURRENT_PAGE_ACTIONS.has(action)) {
|
|
199
|
+
return this.urlRequiredResult(action);
|
|
200
|
+
}
|
|
201
|
+
if (url === undefined &&
|
|
202
|
+
Array.isArray(options.steps) &&
|
|
203
|
+
options.steps.length > 0) {
|
|
204
|
+
return this.urlRequiredResult(action);
|
|
205
|
+
}
|
|
192
206
|
// --------------------------------------------------------------
|
|
193
207
|
// v0.3 入口分流:options.steps 非空 → StepEngine.runChain
|
|
194
208
|
// --------------------------------------------------------------
|
|
195
209
|
if (Array.isArray(options.steps) && options.steps.length > 0) {
|
|
210
|
+
// 入口门已拒「url 省略 + steps」(上方第二个 guard)——steps 分支内 url
|
|
211
|
+
// 不变量为 string;TS 无法跨复合条件收窄,显式收紧一次。
|
|
212
|
+
const stepsUrl = url;
|
|
196
213
|
// resourceKey:channel 全名 + url(粗粒度隔离;StepEngine 内每个 step 独立 stateId)
|
|
197
|
-
const resourceId = `${this.name}:${
|
|
214
|
+
const resourceId = `${this.name}:${stepsUrl}`;
|
|
198
215
|
// epoch = 0(v0.3 不接 ResourceScheduler;parse3 §4.3 推迟到 v0.5+)
|
|
199
216
|
return withOperation(resourceId, 0, async () => {
|
|
200
217
|
// W1-DEF-2b(v1.8):action="navigate" + steps 时**先导航再跑链**
|
|
201
218
|
// (此前 steps 分支丢弃 action,链跑在 about:blank 上——expectPoll 在
|
|
202
219
|
// 空白页 30s 全 false,wave2 smoke 实证)。链内首 step 为 navigate 的
|
|
203
220
|
// 旧范式(U-03-1)不受影响(StepEngine 自行处理)。
|
|
204
|
-
if (action === "navigate" &&
|
|
221
|
+
if (action === "navigate" && stepsUrl) {
|
|
205
222
|
const nav = this.actionDispatch.get("navigate");
|
|
206
223
|
if (nav) {
|
|
207
224
|
try {
|
|
208
225
|
const c = await this.getMcpClient();
|
|
209
|
-
await nav(c,
|
|
226
|
+
await nav(c, stepsUrl, options);
|
|
210
227
|
}
|
|
211
228
|
catch (e) {
|
|
212
229
|
// 导航失败(404 / DNS / 落盘类 didnt)→ 整链诚实终止
|
|
@@ -222,7 +239,7 @@ export class BrowseChannel extends UiChannel {
|
|
|
222
239
|
}
|
|
223
240
|
}
|
|
224
241
|
}
|
|
225
|
-
const chain = await this.runChain(
|
|
242
|
+
const chain = await this.runChain(stepsUrl, options.steps,
|
|
226
243
|
// v1.18.2(doc/governance/10 F3+Y1):budget_ms 显式放宽(钳制 600s;缺省 DEFAULT 120s)
|
|
227
244
|
clampChainBudgetMs(options.budget_ms));
|
|
228
245
|
return this.wrapChainResult(chain);
|
|
@@ -231,9 +248,25 @@ export class BrowseChannel extends UiChannel {
|
|
|
231
248
|
// --------------------------------------------------------------
|
|
232
249
|
// v0.2 单 action 路径(保留;INV-12 包裹)
|
|
233
250
|
// --------------------------------------------------------------
|
|
234
|
-
const resourceId = `${this.name}:${url}`;
|
|
251
|
+
const resourceId = `${this.name}:${url ?? CURRENT_PAGE_URL_LITERAL}`;
|
|
235
252
|
return withOperation(resourceId, 0, async () => this.browseSingle(url, action, options));
|
|
236
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* BUG-07 决议 A⁺(§5.2②):url 省略 + 非 current-page action(含 steps 链)的
|
|
256
|
+
* 显式拒(与 unknown_action 同族——「策略确定性拒」early-return,不 throw:
|
|
257
|
+
* throw 形态会被 classifyBrowseError 落 unknown 触发熔断污染级联)。
|
|
258
|
+
*/
|
|
259
|
+
urlRequiredResult(action) {
|
|
260
|
+
return {
|
|
261
|
+
outcome: "didnt",
|
|
262
|
+
data: null,
|
|
263
|
+
served_by: this.name,
|
|
264
|
+
fallback_used: false,
|
|
265
|
+
retrieval_method: this.retrievalMethod(),
|
|
266
|
+
error: `url_required_for_action:${action}`,
|
|
267
|
+
hint: "url is optional only for action=screenshot (current-page mode); pass an explicit http(s) url",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
237
270
|
/**
|
|
238
271
|
* v0.2 单 action 路径(原 browse() 实装,零行为变更;仅迁出便于 browse() 入口分流)。
|
|
239
272
|
*/
|
|
@@ -253,20 +286,27 @@ export class BrowseChannel extends UiChannel {
|
|
|
253
286
|
* act(@pN) 经 InteractDispatcher 也走本路径,同享此保证。
|
|
254
287
|
*/
|
|
255
288
|
async dispatchAction(c, action, url, options, handler) {
|
|
256
|
-
|
|
257
|
-
|
|
289
|
+
// BUG-07 决议 A⁺(§5.2③):加法守卫——url defined 时与旧代码求值全等
|
|
290
|
+
//(current-page 已在 browseSingle 早退,url === undefined 永不进导航分支)。
|
|
291
|
+
if (url !== undefined &&
|
|
292
|
+
(NAV_FIRST_ACTIONS.has(action) ||
|
|
293
|
+
this.needsFreshPageNav(c, action, url))) {
|
|
258
294
|
const nav = this.actionDispatch.get("navigate");
|
|
259
295
|
let navFinalUrl;
|
|
260
296
|
if (nav)
|
|
261
297
|
navFinalUrl = (await nav(c, url, options)).final_url;
|
|
262
298
|
this.navSeenClients.add(c);
|
|
299
|
+
this.lastNavigatedClient = c; // BUG-07 A⁺:与 navSeenClients 同两行写点(单写者)
|
|
263
300
|
const partial = await handler(c, url, options);
|
|
264
301
|
return { ...partial, final_url: partial.final_url ?? navFinalUrl };
|
|
265
302
|
}
|
|
266
|
-
|
|
303
|
+
// current-page 模式:ActionHandler 契约收 string——传字面量(handler 均不读)
|
|
304
|
+
const partial = await handler(c, url ?? CURRENT_PAGE_URL_LITERAL, options);
|
|
267
305
|
// navigate 自身成功后才标记会话已导航(失败不标——下次 snapshot/extract 仍门控导航)
|
|
268
|
-
if (action === "navigate")
|
|
306
|
+
if (action === "navigate") {
|
|
269
307
|
this.navSeenClients.add(c);
|
|
308
|
+
this.lastNavigatedClient = c; // BUG-07 A⁺:同上,第二写点
|
|
309
|
+
}
|
|
270
310
|
return partial;
|
|
271
311
|
}
|
|
272
312
|
/**
|
|
@@ -274,9 +314,14 @@ export class BrowseChannel extends UiChannel {
|
|
|
274
314
|
* = 上游新开空白页,L3 实证:此时单发 extract/snapshot 返 about:blank 空内容
|
|
275
315
|
* + outcome=worked)+ url 非占位 about:blank(forest 调度器缺 subtitle 时的
|
|
276
316
|
* 兜底值,无可导航目标)→ 返 true 需先导航。
|
|
317
|
+
*
|
|
318
|
+
* BUG-07 决议 A⁺(§5.2③):签名放宽 url: string | undefined 并前置
|
|
319
|
+
* url !== undefined 守卫——防 undefined 走 `url !== "about:blank"` 求值出
|
|
320
|
+
* true 的隐性边(对抗复审自查项)。
|
|
277
321
|
*/
|
|
278
322
|
needsFreshPageNav(c, action, url) {
|
|
279
|
-
return (
|
|
323
|
+
return (url !== undefined &&
|
|
324
|
+
FRESH_PAGE_NAV_ACTIONS.has(action) &&
|
|
280
325
|
url !== "about:blank" &&
|
|
281
326
|
!this.navSeenClients.has(c));
|
|
282
327
|
}
|
|
@@ -286,6 +331,25 @@ export class BrowseChannel extends UiChannel {
|
|
|
286
331
|
* 不构成共享 mutable state 耦合面。
|
|
287
332
|
*/
|
|
288
333
|
navSeenClients = new WeakSet();
|
|
334
|
+
/**
|
|
335
|
+
* BUG-07 决议 A⁺(§5.3):current-page 模式的会话真源——最后一次成功导航的
|
|
336
|
+
* client(可空强引用)。与 navSeenClients **同两行写点、单写者**
|
|
337
|
+
* (dispatchAction 现有两处 add),只增只读判定(R-INT-07 教训:不构成
|
|
338
|
+
* 多消费者 mutable state 耦合面)。唯一例外写点 = 自愈成功路径的
|
|
339
|
+
* invalidateCurrentPageSession()(三处调用点,LoggedInChannel;写者仍是
|
|
340
|
+
* 通道自身 protected 方法)。守卫语义 =「client 身份 ∧ 未被 heal 换页」。
|
|
341
|
+
*/
|
|
342
|
+
lastNavigatedClient = null;
|
|
343
|
+
/**
|
|
344
|
+
* BUG-07 决议 A⁺(§5.3 r1 铁则 (b)):current-page 会话失效通知。
|
|
345
|
+
* 自愈成功路径(楔死 heal 层 1 new_page / 层 2 respawn / P6 恢复)都会把
|
|
346
|
+
* 「当前受管页面」换成新空白页——client 身份可能不变(层 1 返回原 client),
|
|
347
|
+
* 必须显式置 null 才能让 current-page 截图的守卫拒绝(绝不静默截 about:blank
|
|
348
|
+
* 伪造状态)。navSeenClients(FRESH_PAGE_NAV golden)不动。
|
|
349
|
+
*/
|
|
350
|
+
invalidateCurrentPageSession() {
|
|
351
|
+
this.lastNavigatedClient = null;
|
|
352
|
+
}
|
|
289
353
|
/**
|
|
290
354
|
* P6(v1.18.1,得到实战问题集 P6):"No page selected" 零页面自愈钩子。
|
|
291
355
|
* 默认 false(HeadlessChannel 上游自管浏览器无 0-page 形态,行为零变化);
|
|
@@ -345,8 +409,39 @@ export class BrowseChannel extends UiChannel {
|
|
|
345
409
|
error: `unknown_action:${action}`,
|
|
346
410
|
};
|
|
347
411
|
}
|
|
412
|
+
// ==============================================================
|
|
413
|
+
// BUG-07 决议 A⁺(doc/bugs/07 §5.2-5.3,2026-09-10):current-page
|
|
414
|
+
// 守卫块——与 unknown_action / upstream_unsupported 同位(dispatchAction
|
|
415
|
+
// 之前、try 之外)。错误契约必须是本函数的 early-return(完整
|
|
416
|
+
// InteractResult 原样直达调用方),**禁止以 dispatchAction throw 形态
|
|
417
|
+
// 落地**(throw → classifyBrowseError 无规则 → unknown → 熔断污染级联
|
|
418
|
+
// + fallback_exhausted 吞 hint——§5.3 r1 发现 ② 的三级劣化)。
|
|
419
|
+
// ==============================================================
|
|
420
|
+
/** no-session 完整 InteractResult(level-1 / level-2 / 自愈禁令共用形状)。 */
|
|
421
|
+
const noSessionResult = () => ({
|
|
422
|
+
outcome: "didnt",
|
|
423
|
+
data: null,
|
|
424
|
+
served_by: this.name,
|
|
425
|
+
fallback_used: false,
|
|
426
|
+
retrieval_method: "current_page_no_session",
|
|
427
|
+
error: "no_active_session:current_page_screenshot",
|
|
428
|
+
hint: "current-page screenshot requires an active session on this channel: run any url-bearing action (navigate / snapshot / evaluate / ...) first, or pass url for the one-step navigate+shoot form. Refusing to shoot a blank page — the previous page is gone (closed / rotated by self-heal / cold channel), which would fabricate state.",
|
|
429
|
+
});
|
|
430
|
+
// level-1 pre-check(不获取 client):冷通道**不 spawn 浏览器子进程**就完成
|
|
431
|
+
// 拒绝——「不静默新开」落到实处(headless 不拉 Chromium;logged_in 不拉 npx)。
|
|
432
|
+
if (url === undefined && this.lastNavigatedClient === null) {
|
|
433
|
+
return noSessionResult();
|
|
434
|
+
}
|
|
348
435
|
try {
|
|
349
436
|
const c = await this.getMcpClient();
|
|
437
|
+
// level-2 复核(获取 client 后):`lastNavigatedClient === c && navSeenClients
|
|
438
|
+
// .has(c)` 不满足 → 同错误契约。封 respawn 边(上游子进程重启换 McpClient
|
|
439
|
+
// 实例=新浏览器空白页)和 heal 换页边(invalidation 置 null——同 client 但
|
|
440
|
+
// 选中页已被自愈换成新空白页,§5.3 r1 铁则 (b) 的穿透口全落这里)。
|
|
441
|
+
if (url === undefined &&
|
|
442
|
+
(this.lastNavigatedClient !== c || !this.navSeenClients.has(c))) {
|
|
443
|
+
return noSessionResult();
|
|
444
|
+
}
|
|
350
445
|
this.touchKeepalive(); // v1.9:action dispatch 后保活(防 idle watchdog 误杀)
|
|
351
446
|
// P10(v1.18.1,得到实战问题集 P10):上游工具缺失前置门(导航**之前**)。
|
|
352
447
|
// 锁定的 chrome-devtools-mcp@1.7.0 实测不暴露 `pdf` 工具——此前单 action pdf
|
|
@@ -380,7 +475,16 @@ export class BrowseChannel extends UiChannel {
|
|
|
380
475
|
// 失败方向:heal 失败 → `upstream_wedge_selected_page_closed` 透明前缀
|
|
381
476
|
// (unknown);heal 成功但重试仍失败 → `upstream_wedge_unhealed`(同
|
|
382
477
|
// unknown——fallback 到 headless 语义正确)。
|
|
478
|
+
//
|
|
479
|
+
// BUG-07 决议 A⁺(§5.3 r1 铁则 (a)):current-page 模式(url === undefined)
|
|
480
|
+
// **禁两条自愈重试**——选中页已死 = 受管状态定义性消亡,heal 出的新页
|
|
481
|
+
// (无论同 client 还是 respawn)对状态敏感截图毫无意义,重试的唯一可能
|
|
482
|
+
// 产出就是空白页伪造。铁则:仅在两条自愈分支内前置判定(不做成 catch
|
|
483
|
+
// 顶部的裸 if——那会把无关错误误报成 no-session,制造新的错误归因
|
|
484
|
+
// 伪造);url-present 路径的自愈重试逐字节不动(INV-89(c) 锚不破坏)。
|
|
383
485
|
if (isUpstreamWedgeError(String(e))) {
|
|
486
|
+
if (url === undefined)
|
|
487
|
+
return noSessionResult(); // 铁则 (a):禁楔死自愈
|
|
384
488
|
const healed = await this.healUpstreamWedge(c);
|
|
385
489
|
if (healed === null) {
|
|
386
490
|
throw new Error(`upstream_wedge_selected_page_closed:${String(e).slice(0, 200)}`);
|
|
@@ -399,8 +503,14 @@ export class BrowseChannel extends UiChannel {
|
|
|
399
503
|
// silent bail)下所有页级调用抛 "No page selected"(getSelectedMcpPage)。
|
|
400
504
|
// 自愈钩子(LoggedInChannel:CDP 预建 background tab + select_page)成功 →
|
|
401
505
|
// 原样重试一次;失败/不支持 → 原错误路径(classify 落 unknown)。
|
|
402
|
-
NO_PAGE_SELECTED_RE.test(String(e))
|
|
403
|
-
|
|
506
|
+
NO_PAGE_SELECTED_RE.test(String(e))) {
|
|
507
|
+
// BUG-07 铁则 (a) 同款:current-page 模式禁 P6 自愈(r1 发现 ③ 的
|
|
508
|
+
// 第三条同型穿透口——LoggedInChannel override 新建 about:blank tab +
|
|
509
|
+
// select_page 后同 client 重试,对 current-page 同样空白页伪造)。
|
|
510
|
+
if (url === undefined)
|
|
511
|
+
return noSessionResult(); // 铁则 (a):禁 P6 换页恢复
|
|
512
|
+
if (!(await this.recoverNoPageSelected(c)))
|
|
513
|
+
throw e;
|
|
404
514
|
partial = await this.dispatchAction(c, action, url, options, handler);
|
|
405
515
|
}
|
|
406
516
|
else {
|
|
@@ -408,25 +518,30 @@ export class BrowseChannel extends UiChannel {
|
|
|
408
518
|
}
|
|
409
519
|
}
|
|
410
520
|
// 写盘 + 短指针(v0.1 简化版;v0.3 升 StateStore LRU + stateId 反查)
|
|
521
|
+
// BUG-07 A⁺(§5.2③):current-page 模式 data.url = "current-page" 字面量
|
|
522
|
+
//(诚实+可 grep,不伪造 URL;解析真实页面 URL = 显式非目标,决议 §5.10 记档)
|
|
523
|
+
const effUrl = url ?? CURRENT_PAGE_URL_LITERAL;
|
|
411
524
|
const stateId = randomUUID();
|
|
412
525
|
const contentPath = await writeState(this.name, stateId, {
|
|
413
|
-
url,
|
|
526
|
+
url: effUrl,
|
|
414
527
|
action,
|
|
415
528
|
...partial,
|
|
416
529
|
});
|
|
417
530
|
// BUG-05 决议 D1(INV-91):传入但该 action 未消费的 options 键诚实标注
|
|
418
531
|
//(空省略 = byte-identical;消费表单一真源 CONSUMED_OPTIONS)
|
|
419
|
-
|
|
532
|
+
// BUG-07 A⁺(§5.2③):模式感知——current-page 模式无导航,仅经导航消费的
|
|
533
|
+
// 键(no_cache)从消费集派生剔除(调用方传 no_cache 必须出现在标注里)。
|
|
534
|
+
const ignored = computeIgnoredOptions(action, options, url === undefined ? "current_page" : undefined);
|
|
420
535
|
return {
|
|
421
536
|
outcome: "worked",
|
|
422
537
|
data: {
|
|
423
|
-
url,
|
|
538
|
+
url: effUrl,
|
|
424
539
|
action,
|
|
425
540
|
state_id: stateId,
|
|
426
541
|
content_path: contentPath,
|
|
427
542
|
preview: truncatePreviewKeepingRefs(partial.preview ?? ""),
|
|
428
543
|
title: partial.title,
|
|
429
|
-
final_url: partial.final_url ??
|
|
544
|
+
final_url: partial.final_url ?? effUrl,
|
|
430
545
|
// v1.1(parse12 §3.3.1):markdown 档元数据透传(仅 extract_mode=markdown* 时 partial 含这些字段)
|
|
431
546
|
...(partial.byline ? { byline: partial.byline } : {}),
|
|
432
547
|
...(partial.citations ? { citations: partial.citations } : {}),
|
|
@@ -1441,6 +1556,15 @@ export function truncatePreviewKeepingRefs(s) {
|
|
|
1441
1556
|
// browseSingle 先 navigate 再 dispatch(工具注释自 v0.5 起就承诺「URL → navigate + X」,
|
|
1442
1557
|
// v1.8.1 补上真实导航)。wait/click 等保持原语义(作用于当前页)。
|
|
1443
1558
|
const NAV_FIRST_ACTIONS = new Set(["network", "screenshot", "pdf"]);
|
|
1559
|
+
// BUG-07 决议 A⁺(doc/bugs/07 §5.0/§5.2,2026-09-10):url 省略时合法的
|
|
1560
|
+
// current-page action 集(对当前受管页面直接执行,零导航)。house pattern 同
|
|
1561
|
+
// NAV_FIRST_ACTIONS(顶级 const 预留增项位,未来 network/pdf 的 current-page
|
|
1562
|
+
// 形态=加一行,决议 §5.10 记档本期不开)。有 url → NAV_FIRST 照旧字节级不变。
|
|
1563
|
+
const CURRENT_PAGE_ACTIONS = new Set(["screenshot"]);
|
|
1564
|
+
// BUG-07 决议 A⁺(§5.2③):current-page 模式的 data.url / final_url 字面量
|
|
1565
|
+
//(诚实+可 grep,不伪造 URL;上游 take_screenshot 本就无 url 参数——本决议
|
|
1566
|
+
// 删除的是 lasso 自创组合形态里的导航,§5.5 零上游变更)。
|
|
1567
|
+
const CURRENT_PAGE_URL_LITERAL = "current-page";
|
|
1444
1568
|
// review-r3 F3:URL 驱动但**会话语义敏感**的采集 action——只在会话页仍空白时
|
|
1445
1569
|
// 先导航(needsFreshPageNav 门控),已导航会话作用于当前页。理由:
|
|
1446
1570
|
// - snapshot/extract 是工具默认 action + descriptions 承诺 URL 定向——空白会话
|
|
@@ -1500,15 +1624,29 @@ const CONSUMED_OPTIONS = Object.freeze({
|
|
|
1500
1624
|
/** network_include_bodies / network_timeout_ms 死键不入表(决议 r1) */
|
|
1501
1625
|
network: ["network_filter", "no_cache"],
|
|
1502
1626
|
});
|
|
1627
|
+
/**
|
|
1628
|
+
* BUG-07 决议 A⁺(§5.2③):仅经导航消费的 options 键(doNavigate 读 no_cache;
|
|
1629
|
+
* 无导航即死键)。current-page 模式下运行时从 CONSUMED_OPTIONS 表派生剔除
|
|
1630
|
+
* (消费表仍是单一真源,模式变体不落表)。
|
|
1631
|
+
*/
|
|
1632
|
+
const NAV_ONLY_OPTION_KEYS = new Set(["no_cache"]);
|
|
1503
1633
|
/**
|
|
1504
1634
|
* BUG-05 决议 D1:计算「传入但该 action 未消费」的 options 键(导出供测试)。
|
|
1505
1635
|
* 只对 worked 出口调用(didnt/unknown 路径不标——错误已自解释)。
|
|
1636
|
+
*
|
|
1637
|
+
* BUG-07 决议 A⁺(§5.2③):mode="current_page"(url 省略的 screenshot)时
|
|
1638
|
+
* 从消费集派生剔除 NAV_ONLY_OPTION_KEYS——该模式下无导航,no_cache 只经
|
|
1639
|
+
* doNavigate 消费 → 传入必须出现在 ignored_options(INV-91 精神:禁死键豁免)。
|
|
1506
1640
|
*/
|
|
1507
|
-
export function computeIgnoredOptions(action, options) {
|
|
1641
|
+
export function computeIgnoredOptions(action, options, mode) {
|
|
1508
1642
|
const consumed = CONSUMED_OPTIONS[action];
|
|
1509
1643
|
if (!consumed)
|
|
1510
1644
|
return Object.keys(options); // 未注册 action 由 unknown_action 早退拦下,此处防御
|
|
1511
1645
|
const set = new Set(consumed);
|
|
1646
|
+
if (mode === "current_page") {
|
|
1647
|
+
for (const k of NAV_ONLY_OPTION_KEYS)
|
|
1648
|
+
set.delete(k);
|
|
1649
|
+
}
|
|
1512
1650
|
return Object.keys(options).filter((k) => !set.has(k));
|
|
1513
1651
|
}
|
|
1514
1652
|
function classifyBrowseError(msg, _action) {
|
|
@@ -1533,6 +1671,14 @@ function classifyBrowseError(msg, _action) {
|
|
|
1533
1671
|
// 不同下一步(前者改参数/配 env 可解,后者重试无益)。
|
|
1534
1672
|
if (m.includes("screenshot_path_not_allowed"))
|
|
1535
1673
|
return "didnt";
|
|
1674
|
+
// BUG-07 决议 A⁺(doc/bugs/07 §5.3 belt-and-braces):current-page 无会话 /
|
|
1675
|
+
// url-required 是**策略确定性拒** → didnt。铁则本使 no-session 以 browseSingle
|
|
1676
|
+
// early-return 落地(不经 throw);本规则防未来重构回退——若残余 throw 路径
|
|
1677
|
+
// 产出含该码的 unknown,此处兜底归 didnt(免熔断污染 + fallback_exhausted 吞
|
|
1678
|
+
// hint 的三级劣化)。**必须置于 upstream_wedge 规则之前**(防复合串
|
|
1679
|
+
// `upstream_wedge_unhealed:no_active_session:x` 被截胡落 unknown)。
|
|
1680
|
+
if (m.includes("no_active_session") || m.includes("url_required_for_action"))
|
|
1681
|
+
return "didnt";
|
|
1536
1682
|
// E④(BUG-03 决议 E④):会话轮换("No page selected" 类)是**瞬态可重试**信号
|
|
1537
1683
|
//(页面被轮换/清空,重 snapshot 即恢复)——显式归 unknown(可重试档)+
|
|
1538
1684
|
// session_rotated 前缀供 agent 透明识别下一步(重 snapshot),不再落泛 unknown
|