From 179a86d65598b8b3cb922a16daf6ddfe5940bf35 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 7 Nov 2026 07:02:37 +0000 Subject: [PATCH] =?UTF-8?q?fix(chv-muse-r11fix):=20browser=20=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E6=94=B9=20Page.aria=5Fsnapshot()=EF=BC=88=E5=A4=8D?= =?UTF-8?q?=E6=B5=8B=E6=96=B0=E5=8F=91=E7=8E=B0=202=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playwright 1.64.1 的 Page 根本没有 accessibility 属性(那是 Puppeteer 的 API),open/snapshot 在真实库上必报 'Page' object has no attribute 'accessibility'。原生快照是 Page.aria_snapshot()(返回 ARIA 快照字符串),a11y 字段仍叫 a11y,值改为字符串;截断改字符串版 truncate_aria(200 节点 行/64KB)。FakePage 同步改真形状(旧 fake 自带 .accessibility 与真实库分叉,是与 sync_playwright bug 同款模式)。 wheel 1.64.1 实测:sync_api/_generated.py 无 def accessibility, Page 与 Locator 均有 aria_snapshot() -> str。 --- chv-guest/chv-browser.py | 76 ++++++++++++++++++---------------------- docs/chv-muse-p2-gap.md | 2 +- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/chv-guest/chv-browser.py b/chv-guest/chv-browser.py index 483ae80..8b20c80 100644 --- a/chv-guest/chv-browser.py +++ b/chv-guest/chv-browser.py @@ -2,9 +2,9 @@ """chv-browser:浏览器子代理(客机侧、单元外独立进程)。 对齐 Muse P2(见 docs/chv-muse-p2-gap.md):浏览器任务由专门子代理 -完成,与 codex 单元分离。子代理只向单元返回页面无障碍树快照 -(Playwright accessibility.snapshot),不见原始 DOM,不能在页面 -上下文跑 JS,不暴露 evaluate/content,不开 DevTools。 +完成,与 codex 单元分离。子代理只向单元返回页面无障碍快照 +(Playwright Page.aria_snapshot() 的 ARIA 快照字符串),不见原始 +DOM,不能在页面上下文跑 JS,不暴露 evaluate/content,不开 DevTools。 操作(Unix socket /run/chv/browser.sock,JSON 行): open {url} -> 导航并返回 {title, url, a11y, truncated} @@ -64,43 +64,31 @@ def validate_url(url): return True, "" -def truncate_a11y(node, max_nodes=MAX_A11Y_NODES, +def truncate_aria(text, max_nodes=MAX_A11Y_NODES, max_chars=MAX_A11Y_CHARS): - """截断无障碍树:节点数超上限时剪掉多余子树。返回 (tree, truncated)。 - 纯函数,可单测。节点计数按 dict 节点数(含 children 展开)。""" - state = {"count": 0, "truncated": False} - - def walk(n): - if not isinstance(n, dict): - return n - state["count"] += 1 - if state["count"] > max_nodes: - state["truncated"] = True - return None - out = {} - for key, value in n.items(): - if key == "children" and isinstance(value, list): - children = [] - for child in value: - walked = walk(child) - if walked is None: - state["truncated"] = True - break - children.append(walked) - out[key] = children - else: - out[key] = value - return out - - tree = walk(node) - text = json.dumps(tree, ensure_ascii=False) - if len(text) > max_chars: - state["truncated"] = True - # 字符超限时只留顶层角色/名字/URL 信息,不灌整棵树。 - tree = {"role": (node or {}).get("role"), - "name": (node or {}).get("name"), - "truncated_chars": True} - return tree, state["truncated"] + """截断 ARIA 快照字符串:节点行(行首 "- ")超上限时剪掉多余 + 行,字符超上限时截断。返回 (text, truncated)。纯函数,可单测。 + round-11 复测纠正(第二轮):Playwright 根本没有 + page.accessibility(那是 Puppeteer 的 API),原生快照是 + Page.aria_snapshot() 返回的字符串;旧的 dict 版 truncate_a11y + 随之废止。a11y 字段仍叫 a11y,值改为 ARIA 快照字符串。""" + text = text or "" + lines = text.splitlines() + kept = [] + nodes = 0 + truncated = False + for line in lines: + if line.lstrip().startswith("- "): + nodes += 1 + if nodes > max_nodes: + truncated = True + break + kept.append(line) + out = "\n".join(kept) + if len(out) > max_chars: + out = out[:max_chars] + truncated = True + return out, truncated def _load_playwright(): @@ -166,12 +154,16 @@ class BrowserAgent: try: title = self._page.title() url = self._page.url - tree = self._page.accessibility.snapshot() + # round-11 复测纠正(第二轮):Playwright 没有 + # page.accessibility(Puppeteer 的 API),原生快照是 + # Page.aria_snapshot()(字符串);a11y 字段仍叫 a11y, + # 值改为 ARIA 快照字符串(截断到 200 节点行/64KB)。 + aria = self._page.aria_snapshot() except Exception as e: return {"ok": False, "error": "取快照失败:%s" % e} - tree, truncated = truncate_a11y(tree or {}) + aria, truncated = truncate_aria(aria or "") return {"ok": True, "title": title, "url": url, - "a11y": tree, "truncated": truncated} + "a11y": aria, "truncated": truncated} def screenshot(self): if self._page is None: diff --git a/docs/chv-muse-p2-gap.md b/docs/chv-muse-p2-gap.md index 1d06ad6..3238b70 100644 --- a/docs/chv-muse-p2-gap.md +++ b/docs/chv-muse-p2-gap.md @@ -26,7 +26,7 @@ CDP 由单元外 broker 管理。 | 单元内 codex 沙箱 | `host.py` 默认 `sandbox="workspace-write"`(bwrap) | 单元本身就是边界 | 单元内 codex 用 `danger-full-access`:bwrap 的 `--proc /proc` 挂载在嵌套命名空间里是已知走不通的形状(见 AGENTS.md 内层 proc 定位),单元内再套 bwrap 只会假隔离。执行器侧改 `--sandbox` 透传是 P1 的文件,本轨只定义接口(见 §4),不改 `executor.py` | | 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--network-veth`:宿主侧 `ve-chv-cell`(10.200.0.1/24)、单元侧 `host0`(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站(MASQUERADE 走客机默认路由的上行口,动态发现,不写死口名)。单元内配网经 nsenter 进单元 init 的 netns 做(`chv-cell.py` 内实现)。veth 建不起来即大声失败,不静默回落共享网络;调试逃生口 `CHV_CELL_NETWORK=shared` 须显式设置、打日志,绝不默认 | | 单元内外 IPC | 无(SSH 直达,host key 钉死) | Unix socket + SO_PEERCRED + 对端 ACL 的最小权限 IPC | `chv-broker`(客机侧、单元外):Unix socket `/run/chv/broker.sock`(bind 进单元)。SO_PEERCRED 校验:对端 uid 须在单元映射范围 1000–66535,且对端 pid 的 pid ns 须与 `/run/chv/cell.pid` 记的单元 init 同 ns(cell.pid 不存在时只做 uid 检查,并打日志)。操作白名单:`ping`、`sync pull|push|status`(仓目录只允许 `~/workspace` 下,`chv-sync` 以 `chv` 身份跑,不进 shell)、`browser …`(转发给浏览器子代理)。其余一律拒绝 | -| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `accessibility.snapshot()` 的无障碍树(截断到 200 节点/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker | +| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv-browser/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `Page.aria_snapshot()` 的 ARIA 快照字符串(截断到 200 节点行/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker | | 凭证 | `/etc/chv/guest-env`(0600 root)+ `~/.git-credentials`(0600 chv),codex 在客机内直接可读后者 | 凭证由 authd 保管,代理只见替身令牌 | 本轨只做“单元看不见”:`/etc/chv` 与 `~/.git-credentials` 都 `--inaccessible`,同步走 broker 的 `sync` 操作(broker 在单元外跑 `chv-sync`,token 不进单元)。替身化本身是 P3,不在本轨冒充已做 | | 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |