fix(chv-muse-r11fix): browser 快照改 Page.aria_snapshot()(复测新发现 2)

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。
This commit is contained in:
host committed 2026-11-07 07:02:37 +00:00
1 parent 581e850eb7
commit 179a86d655
2 files changed
+35 -43

No files matched your search

+34 -42
View File
@@ -2,9 +2,9 @@
"""chv-browser:浏览器子代理(客机侧、单元外独立进程)。 """chv-browser:浏览器子代理(客机侧、单元外独立进程)。
对齐 Muse P2(见 docs/chv-muse-p2-gap.md):浏览器任务由专门子代理 对齐 Muse P2(见 docs/chv-muse-p2-gap.md):浏览器任务由专门子代理
完成,与 codex 单元分离。子代理只向单元返回页面无障碍树快照 完成,与 codex 单元分离。子代理只向单元返回页面无障碍快照
(Playwright accessibility.snapshot),不见原始 DOM,不能在页面 (Playwright Page.aria_snapshot() 的 ARIA 快照字符串),不见原始
上下文跑 JS,不暴露 evaluate/content,不开 DevTools。 DOM,不能在页面上下文跑 JS,不暴露 evaluate/content,不开 DevTools。
操作(Unix socket /run/chv/browser.sock,JSON 行): 操作(Unix socket /run/chv/browser.sock,JSON 行):
open {url} -> 导航并返回 {title, url, a11y, truncated} open {url} -> 导航并返回 {title, url, a11y, truncated}
@@ -64,43 +64,31 @@ def validate_url(url):
return True, "" 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): max_chars=MAX_A11Y_CHARS):
"""截断无障碍树:节点数超上限时剪掉多余子树。返回 (tree, truncated)。 """截断 ARIA 快照字符串:节点行(行首 "- ")超上限时剪掉多余
纯函数,可单测。节点计数按 dict 节点数(含 children 展开)。""" 行,字符超上限时截断。返回 (text, truncated)。纯函数,可单测。
state = {"count": 0, "truncated": False} round-11 复测纠正(第二轮):Playwright 根本没有
page.accessibility(那是 Puppeteer 的 API),原生快照是
def walk(n): Page.aria_snapshot() 返回的字符串;旧的 dict 版 truncate_a11y
if not isinstance(n, dict): 随之废止。a11y 字段仍叫 a11y,值改为 ARIA 快照字符串。"""
return n text = text or ""
state["count"] += 1 lines = text.splitlines()
if state["count"] > max_nodes: kept = []
state["truncated"] = True nodes = 0
return None truncated = False
out = {} for line in lines:
for key, value in n.items(): if line.lstrip().startswith("- "):
if key == "children" and isinstance(value, list): nodes += 1
children = [] if nodes > max_nodes:
for child in value: truncated = True
walked = walk(child) break
if walked is None: kept.append(line)
state["truncated"] = True out = "\n".join(kept)
break if len(out) > max_chars:
children.append(walked) out = out[:max_chars]
out[key] = children truncated = True
else: return out, truncated
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"]
def _load_playwright(): def _load_playwright():
@@ -166,12 +154,16 @@ class BrowserAgent:
try: try:
title = self._page.title() title = self._page.title()
url = self._page.url 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: except Exception as e:
return {"ok": False, "error": "取快照失败:%s" % 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, return {"ok": True, "title": title, "url": url,
"a11y": tree, "truncated": truncated} "a11y": aria, "truncated": truncated}
def screenshot(self): def screenshot(self):
if self._page is None: if self._page is None:
+1 -1
View File
@@ -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` | | 单元内 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` 须显式设置、打日志,绝不默认 | | 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--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 …`(转发给浏览器子代理)。其余一律拒绝 | | 单元内外 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,不在本轨冒充已做 | | 凭证 | `/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 | | 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |