diff --git a/chv-guest/chv-browser.py b/chv-guest/chv-browser.py index 1866e26..15db7b4 100644 --- a/chv-guest/chv-browser.py +++ b/chv-guest/chv-browser.py @@ -24,9 +24,11 @@ 返回 changed=false(整帧相同跳过;变化区裁剪是阶段 4 的事, 这里不冒充已做)。 -对齐 Muse P2 的不变口径:子代理只向单元返回页面无障碍树快照 -(Playwright accessibility.snapshot),不见原始 DOM,不能在页面 -上下文跑 JS,不暴露 evaluate/content,不开 DevTools。 +对齐 Muse P2 的不变口径:子代理只向单元返回页面无障碍快照, +不见原始 DOM,不能在页面上下文跑 JS,不暴露 evaluate/content, +不开 DevTools。快照 API:当前 Playwright(1.64.1)已移除 +page.accessibility,改用 page.aria_snapshot()(返回 YAML 式 +字符串);旧版 accessibility.snapshot()(返回 dict)只作回落。 操作(Unix socket /run/chv/browser.sock,JSON 行): open {url, session?} -> 导航并返回 {title, url, a11y, truncated} @@ -342,10 +344,25 @@ class BrowserAgent: try: title = page.title() url = page.url - tree = page.accessibility.snapshot() + # 当前 Playwright(1.64.1):page.accessibility 已移除, + # 用 page.aria_snapshot()(字符串);旧版 dict 树只作回落。 + if hasattr(page, "aria_snapshot"): + snap = page.aria_snapshot() + if isinstance(snap, dict): + tree, truncated = truncate_a11y(snap) + else: + text = snap or "" + truncated = len(text) > MAX_A11Y_CHARS + tree = text[:MAX_A11Y_CHARS] if truncated else text + elif hasattr(page, "accessibility"): + tree = page.accessibility.snapshot() + tree, truncated = truncate_a11y(tree or {}) + else: + return {"ok": False, + "error": "浏览器不支持无障碍快照(无 " + "aria_snapshot/accessibility)"} except Exception as e: return {"ok": False, "error": "取快照失败:%s" % e} - tree, truncated = truncate_a11y(tree or {}) return {"ok": True, "title": title, "url": url, "a11y": tree, "truncated": truncated, "session": session} diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 63b0157..eeef465 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -554,7 +554,8 @@ class BrowserTest(unittest.TestCase): self.assertTrue(resp["ok"]) self.assertEqual(resp["title"], "Example Domain") self.assertEqual(resp["url"], "https://example.com/") - self.assertEqual(resp["a11y"]["role"], "WebArea") + self.assertIsInstance(resp["a11y"], str) + self.assertIn("Example Domain", resp["a11y"]) snap = agent.snapshot() self.assertTrue(snap["ok"]) shot = agent.screenshot() @@ -590,6 +591,11 @@ class FakePage: return {"role": "WebArea", "name": "Example Domain", "children": [{"role": "heading", "name": "Example"}]} + def aria_snapshot(self): + # 当前 Playwright(1.64.1):快照是字符串,不是 dict 树。 + return ("- heading \"Example Domain\" [level=1]\n" + "- paragraph \"Example\"") + def screenshot(self, path=None, type=None, quality=None): data = b"\xff\xd8\xff\xe0FAKEJPEG" if path: diff --git a/cloud/tests/test_chv_opt_stage3.py b/cloud/tests/test_chv_opt_stage3.py index 4fc0516..7bc062c 100644 --- a/cloud/tests/test_chv_opt_stage3.py +++ b/cloud/tests/test_chv_opt_stage3.py @@ -55,6 +55,9 @@ class FakePage: return {"role": "WebArea", "name": self._title, "children": [{"role": "heading", "name": self._title}]} + def aria_snapshot(self): + return "- heading \"%s\" [level=1]" % self._title + def screenshot(self, path=None, type=None, quality=None): assert type == "jpeg", type assert quality == 70, quality @@ -201,6 +204,46 @@ class MultiSessionTest(unittest.TestCase): {"A", "B"}) +class SnapshotApiTest(unittest.TestCase): + def test_snapshot_returns_aria_string(self): + with tempfile.TemporaryDirectory() as tmp: + agent, _c, _h = _agent([FakePage("Example Domain")], tmp) + agent.open("https://example.com") + snap = agent.snapshot() + self.assertTrue(snap["ok"]) + self.assertIsInstance(snap["a11y"], str) + self.assertIn("Example Domain", snap["a11y"]) + self.assertFalse(snap["truncated"]) + + def test_snapshot_truncates_long_aria_string(self): + class LongPage(FakePage): + def aria_snapshot(self): + return "x" * (BROWSER.MAX_A11Y_CHARS + 100) + + with tempfile.TemporaryDirectory() as tmp: + agent, _c, _h = _agent([LongPage()], tmp) + agent.open("https://example.com") + snap = agent.snapshot() + self.assertTrue(snap["truncated"]) + self.assertLessEqual(len(snap["a11y"]), + BROWSER.MAX_A11Y_CHARS) + + def test_snapshot_falls_back_to_legacy_accessibility(self): + # 旧版页只有 accessibility.snapshot()(dict 树)时回落。 + class LegacyPage(FakePage): + @property + def aria_snapshot(self): + raise AttributeError("no aria_snapshot") + + with tempfile.TemporaryDirectory() as tmp: + agent, _c, _h = _agent([LegacyPage("Legacy")], tmp) + agent.open("https://example.com") + snap = agent.snapshot() + self.assertTrue(snap["ok"]) + self.assertIsInstance(snap["a11y"], dict) + self.assertEqual(snap["a11y"]["role"], "WebArea") + + class ScreenshotTest(unittest.TestCase): def test_jpeg_q70_width_1280(self): with tempfile.TemporaryDirectory() as tmp: