feat: 浏览器一期:坐标点击 + live 截图

- browser_click_at(x, y):视口坐标点击(1280x800、左上角
  原点),越界与非数字拒绝;live 视图里用户在截图上的
  点击落点就走这个工具。
- browser_live_shot:固定文件 browser-live.png + 状态
  sidecar browser-live.json(URL/标题/时间戳),watcher
  轮询一个稳定路径即可;技能要求每次动作后都调一次,
  保持画面最新。
- MCP 新增 browser_click_at / browser_live_shot 两个工具;
  技能补 live 视图一节;测试 9 项(含 fake mouse、越界、
  live 文件与状态读回)。
This commit is contained in:
host committed 2026-10-30 06:19:55 +08:00
1 parent cb6de0e3aa
commit 814fe8f7c2
4 files changed
+151 -1

No files matched your search

+43 -1
View File
@@ -26,11 +26,20 @@ class _FakeLocator:
return self._text
class _FakeMouse:
def __init__(self, page):
self._page = page
def click(self, x, y):
self._page.calls.append(("mouse_click", x, y))
class _FakePage:
def __init__(self):
self.url = "https://example.com/"
self.calls = []
self.shot_path = None
self.mouse = _FakeMouse(self)
def title(self):
return "Example Domain"
@@ -146,11 +155,43 @@ class BrowserTest(unittest.TestCase):
self.assertTrue(self.browser.close()["closed"])
self.assertTrue(self.browser.close()["closed"])
def test_click_at_and_bounds(self):
self.browser.navigate("https://example.com")
out = self.browser.click_at(100, 200)
self.assertTrue(out["ok"])
self.assertEqual(out["x"], 100)
self.assertIn(("mouse_click", 100, 200), self.page.calls)
with self.assertRaises(BrowserError):
self.browser.click_at(2000, 10)
with self.assertRaises(BrowserError):
self.browser.click_at("left", 10)
def test_live_shot_writes_fixed_file_and_status(self):
self.browser.navigate("https://example.com")
out = self.browser.live_shot()
self.assertEqual(out["image"], "browser-live.png")
self.assertTrue(out["bytes"] > 0)
self.assertTrue(os.path.exists(out["path"]))
with open(os.path.join(self._tmp.name,
"browser-live.json")) as fh:
status = json.load(fh)
self.assertEqual(status["image"], "browser-live.png")
self.assertEqual(status["url"], "https://example.com")
class FakeMCPBrowser:
def navigate(self, url):
return {"url": url, "title": "T", "ok": True}
def click_at(self, x, y):
return {"url": "https://x/", "title": "T", "ok": True,
"x": x, "y": y}
def live_shot(self, name=None):
return {"image": name or "browser-live.png",
"path": "/w/browser-live.png", "bytes": 456,
"url": "https://x/", "title": "T", "timestamp": 1}
def snapshot(self):
return {"url": "https://x/", "title": "T", "text": "hi",
"truncated": False}
@@ -168,7 +209,8 @@ class MCPTest(unittest.TestCase):
for name in ("browser_navigate", "browser_snapshot",
"browser_click", "browser_fill",
"browser_evaluate", "browser_screenshot",
"browser_close"):
"browser_close", "browser_click_at",
"browser_live_shot"):
self.assertIn(name, names)
def test_tools_call_dispatches(self):