进阶 / Advanced

Canvas / WebGL / 游戏

游戏画面、语音房麦位、地图瓦片、3D 查看器、设计画布(Figma / 墨刀)——这些界面把一切都画在 <canvas> 上,背后没有 DOM 节点、也没有无障碍节点snapshot / find / eval querySelector 永远拿不到它们的 @ref。 这不是缺失的功能,而是硬限制。本页教你换一套打法:看像素、按坐标驱动。

为什么 canvas 拿不到 ref

<canvas> / WebGL 的命中区域,以及闭合 shadow root,都不会为画出来的控件暴露任何 DOM / AX 节点。 因此无论 snapshot 怎么扫都造不出 @ref——坐标是唯一的抓手。snapshot 检测到这种页面时会打印一行提示。

ℹ️ 什么才是 coordinate-only
只有 canvas闭合 shadow root 需要靠坐标。开放 shadow root、以及同源 / 跨源 iframe 都会被 snapshot -i 正常呈现,优先用 @ref
⚠️ relay 下坐标点击可能漂移
走扩展 relay 时,坐标点击可能落到用户的前台标签页上。做大量 canvas 坐标操作前,优先用 --launch 或自己拥有的标签页,或者干脆绕过画布、通过 App 后端 / API 确认真实状态。 参见 驱动真实 Chrome

读取画面:canvas capture 与 screenshot

canvas 页面的 eval / get text 基本返回不了有用信息,所以是你唯一的读取路径。 先枚举画布,再把渲染像素抓下来,然后由你来解读:

读取画布像素
# 枚举页面上的 <canvas> 元素(尺寸、类型)
$ chrome-use canvas list

# 把画布的「渲染像素」存成 PNG —— toDataURL 拿到完整
# 后备存储分辨率(如 Figma 的 2522x1904);对没开
# preserveDrawingBuffer / 被污染的 WebGL 回退到 screenshot
$ chrome-use canvas capture out.png

# 或者直接截一张区域图来「看」当前状态
$ chrome-use screenshot /tmp/s.png
ℹ️ 拿到的是「渲染结果」,不是源数据
canvas capture 抓的是画出来的画面。Figma 的源资产、游戏的隐藏状态等都活在 App 的二进制存储 / API 里, 画布上并没有。需要源数据时走那个 App 自己的接口。

症状:get text 里有文字,却没有 @ref

语音房麦位(Zego / Agora)、原型画布(墨刀 / modao)、游戏 HUD 和某些 Web Component 会把控件出来—— 于是文字出现在 get text / read_page 里(比如一串 "Add Add Add…"),但 snapshot -i 什么都列不出、 querySelectorAll 返回 0。没有可寻址节点,@ref / find 就够不着它。按位置驱动:

先排除闭合 shadow,再按坐标点
# 先试:如果只是「闭合」shadow root(不是 canvas),
# 这条能读穿它 —— 便宜,值得先试一下
$ chrome-use get text --pierce

# 否则先「看」控件在哪
$ chrome-use screenshot /tmp/s.png

# 点那个像素(裸数字 = 坐标)
$ chrome-use click <x> <y>

按坐标交互:click 与 press --hold

canvas 上一切靠坐标。算出目标点直接 click x y;需要先知道某个容器的 CSS-px 盒子时,用 box @ref 拿到它的位置再换算。坐标在这里是正确的工具—— snapshot-first 规则明确为 canvas 开了这个口子。

坐标点击与按住移动
# 按视口坐标交互
$ chrome-use click 640 360

# 按住移动,精确定时(在 daemon 内计时 ——
# 不是 keydown + shell-sleep + keyup,那样每次往返
# 会多出约 250ms 抖动)
$ chrome-use press d --hold 800

# 离散动作(跳 / 攻击 / 确认)
$ chrome-use press Space
✅ press --hold 而不是拼 sleep
持续动作用 press --hold <ms>,计时发生在 daemon 内部,比 shell 里 keydown / sleep / keyup 拼出来的 稳得多。更多按键与输入见 交互操作

batch:一次往返跑完定时序列

别一条 CLI 一个动作地逐帧驱动——那是最慢、保真度最低的方式,每次调用都是一次进程启动 + 往返。 用 batch 把一段定时序列塞进单次往返:它把每一步发给正在运行的 daemon, press --holdwait 都在 daemon 内阻塞,所以时序很准。

单次往返的定时序列
$ chrome-use batch "press d --hold 900" "press j" "press j" "wait 200" "press d --hold 500"
🪜 batch 只跑固定序列
batch 的步骤之间不能传值,也没有循环和条件——它就是一串定死的动作。要按上一步结果决定下一步、要循环或条件, 用 script(步骤间传值 + 循环 + 条件 + 断言,仍是一次往返);要持续实时驱动,用下面的 stream WebSocket。选到刚好够用的那层就行。

读引擎全局变量,而不是猜像素

与其从截图里猜,不如直接读真实状态。eval 运行在页面的 main world, 对基于框架 / 引擎的游戏,你往往能直接够到它的全局对象——比如一个 Phaser / PIXI / Three 实例、 一个 store、或者 window.__GAME__——直接读出位置 / 分数,远胜于对着截图猜。

直接读游戏状态
# eval 在 main world,能读到引擎挂在 window 上的实例
$ chrome-use eval "window.__GAME__.player.position"

实时驱动:WebSocket 流

要真正做实时驱动,就彻底抛开 CLI,改用 WebSocket。 chrome-use stream enable 打开一条双向 WS(stream status 会打印 ws://127.0.0.1:<port>)。连上一次,你就同时拿到 ~60fps 的实时投屏, 并且能在同一条 socket 上发输入——没有逐动作的进程启动、没有往返,而且能跨扩展 relay 工作。

开流并读取地址
$ chrome-use stream enable
$ chrome-use stream status   # 打印 ws://127.0.0.1:<port>
node —— 实时帧 + 本地计时输入
// node(全局 WebSocket):实时帧 + 本地计时输入
const ws = new WebSocket("ws://127.0.0.1:PORT")
ws.onmessage = e => { const m = JSON.parse(e.data); if (m.type==="frame") {/* base64 jpeg */} }
const k = (eventType,key,code,vk) => ws.send(JSON.stringify({type:"input_keyboard",eventType,key,code,windowsVirtualKeyCode:vk}))
k("keyDown"," ","Space",32); setTimeout(()=>k("keyUp"," ","Space",32), 80)   // 一次跳跃
// 鼠标同理:{type:"input_mouse",eventType:"mousePressed",x,y,button:"left",clickCount:1}
✅ 什么时候用 WS,什么时候用截图
这是「看幻灯片」和「真正玩游戏」的区别。一次性检查留给 screenshot;任何持续的实时控制都用 WS。

虚拟化富文本编辑器:先探针,再落笔

Google Docs / Sheets、Notion、Figma、飞书文档——这些和 canvas 不同,有 DOM,但 DOM 会骗你: 真正的编辑面是虚拟化层,周围散落着一堆诱饵——隐藏的 <textarea> 镜像、屏外输入框、工具栏 / 搜索框、标题输入。 对着 snapshot -i 挑出来的 ref 直接 fill / type,文字常常落进标题栏或某个隐藏框,而不是文档里。 导航(菜单、按钮、对话框)照旧 snapshot-first;但对主编辑面,落一整段之前先证明键盘落在哪。

写探针 → 核对 → 再落正文
# 1. 写探针:真实按键打一个丢弃 token,别一上来就灌整段
$ chrome-use click 520 300              # 按坐标点进正文
$ chrome-use keyboard type "zzprobe"    # 真实 keydown/keypress/keyup,不是 fill/insertText
# 2. 核对它落在文档里,而不是标题 / 工具栏 / 隐藏输入
$ chrome-use screenshot /tmp/probe.png  # 看 "zzprobe" 到底出现在哪(或走 App 的导出/API 回读)
# 3a. 落在文档里 → 用真实按键继续打正文
# 3b. 落错了 → 这个面别再用 DOM/fill:按坐标点正文,之后只用真实键盘
⚠️ 三条经验
主编辑面别信 fill @ref / type @ref,先探针证明目标(工具栏、菜单、评论框、分享弹窗是真 DOM,@ref 没问题,坑在画布 / 表格本身); 优先真实按键keyboard type / keyboard press)而不是 fill/insertText(虚拟化编辑器只听 key 事件,insertText 常静默失败或写进镜像); 用回读核对而不是假设——截图或走 App 导出 / API 确认内容真落下了,再报完成。

决策速查