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

+53
View File
@@ -148,6 +148,59 @@ class Browser:
result["ok"] = True
return result
def click_at(self, x, y):
"""Click by viewport coordinates (CSS pixels, origin at the
top-left of the 1280x800 viewport). This is the tool the
console live view drives: the user clicks on a screenshot
and the coordinates arrive here. Bounds-checked so a
mis-scaled click cannot scroll the page into nowhere."""
try:
fx = float(x)
fy = float(y)
except (TypeError, ValueError):
raise BrowserError("click_at needs numeric x/y, got %r/%r"
% (x, y))
if not (0 <= fx <= 1280 and 0 <= fy <= 800):
raise BrowserError(
"click_at out of viewport 1280x800: %s,%s" % (x, y))
self._ensure()
try:
self._page.mouse.click(fx, fy)
self._current_url = self._page.url
except Exception as exc: # noqa: BLE001
raise BrowserError("click_at failed: %s" % exc)
result = self._info()
result["ok"] = True
result["x"] = fx
result["y"] = fy
return result
def live_shot(self, output_filename=None):
"""Screenshot to a FIXED live-view file plus a JSON status
sidecar, so a watcher (console, user) can poll one stable
path instead of chasing per-shot filenames. The agent
should call this after every navigate/click/fill while a
live view is wanted. Returns path, bytes, url, title and
a unix timestamp."""
self._ensure()
name = _validate_basename(
output_filename or "browser-live.png", "output_filename")
path = os.path.join(self.cwd, name)
try:
self._page.screenshot(path=path)
except Exception as exc: # noqa: BLE001
raise BrowserError("live_shot failed: %s" % exc)
info = self._info()
status = {"image": name, "path": path,
"bytes": os.path.getsize(path),
"url": info["url"], "title": info["title"],
"timestamp": int(time.time())}
status_path = os.path.join(self.cwd, "browser-live.json")
with open(status_path, "w", encoding="utf-8") as fh:
json.dump(status, fh, ensure_ascii=False)
status["status_path"] = status_path
return status
def evaluate(self, expression):
if not expression or not str(expression).strip():
raise BrowserError("expression is required")