| name | screen-pilot |
| description | 用 peekaboo + System Events 可靠地操控 macOS 原生应用与手机 App(搜索、点击、播放、调音量、点外卖、点咖啡、下单、支付等)。核心是「归一化窗口几何 + 优先键盘/菜单、坐标只兜底、点前自检」。覆盖:网易云音乐等桌面软件;以及经 iPhone 镜像操控 iOS App——点外卖/点咖啡/美团外卖下单付款(profile: meituan-waimai-ios-mirror)、微信收发消息(profile: wechat-ios-mirror)等防截屏/反自动化的手机 App。当任务是操控某个本地 App 或手机 App 界面(点外卖、点咖啡、美团、微信、各种桌面/手机软件)时【必须先调用本技能并读对应 profile,按归一化+grab.sh截图+cliclick 那套来,别直接抓 peekaboo MCP 自由发挥、别用 peekaboo see/image(本机 bridge error)】。不适用于浏览器内的事(走 Playwright MCP,且别去开美团网页版 waimai.meituan.com——要登录、付不了款)。 |
screen-pilot — 桌面界面操控
目标:少截图、不靠会漂移的绝对坐标、不点错按钮。把一个 App「学一次」,之后照 profile 精准复用。
我们其实在替这些不暴露可访问性树的 App 从外面反向重建一张交互地图:第一次睁着眼学(慢),之后照这张「内心的坐标」图走(快)。
坐标是消耗品会过期,真正的资产是这套会学、会验、会自愈的回路。库(profiles/)越用越大。
术语:内心链路(inner chain)。 指「学好之后,凭已记/已算的坐标,胸有成竹地一口气把一串动作执行完,中途不截图」。
本文里凡说「跑内心链路 / 直点 / 直发 / 直做 / 闭眼快做」都是这个意思——不是"盲"、不是冒险,是照内心那张图走。只在卡点(算不出的内容相关步、或链路断了)才睁眼截图。
渠道选择(step 0:先判断,再让用户定)
本 skill 有两条渠道,动手前先定走哪条:
- (A) 直接操控 Mac 原生窗口(如网易云音乐、各种桌面软件)。更快、无镜像延迟。
- (B) 经 iPhone 镜像操控 iOS App(如微信、美团外卖)。动作发生在手机上。
判断规则:
- 该 App 有能用的 Mac 版、内容能
screencapture、不反自动化 → 走 A。
- Mac 版防截屏/反自动化(微信),或只有 iOS 版/小程序(美团),或用户账号数据在手机上 → 走 B。
- 两条都可行时:给出判断+推荐,然后一句话让用户拍板走哪条(如「这个我能用 Mac 版,也能用你手机镜像,走哪个?」)。别替用户默选,也别为这个反复纠结——问一句即可。
生命周期(每次要操控某个 App 都走这条)
- 认出:查
profiles/_index.json 有没有这个 App。
- 没学过 → 问用户「要不要花 ~30s 学一次?学完以后就秒级」。同意 → 进「睁眼学」。不同意 → 当场临时操作不入库。
- 学过 → 跑
lib/normalize.sh <进程> [W H] 拿指纹:
- 几何对齐(窗口被挪/拉也被它弹回固定矩形,缓存坐标复活)。
- 核版本:和 profile 的
learnedVersion 一致 → 放心闭眼飞快做;不一致 = 可能改版 → 点前更要自检,错了就把那个元素重学、更新 profile。
- 闭眼快做(exploit):normalize → 优先快捷键 → 菜单 → 缓存坐标(相对偏移+窗口position) → 每个动作后验证。
- 自愈:任一验证失败 = 地图过期 → 退回「睁眼学」补那一处 → 写回 profile + 更新
_index.json 的 version。
「睁眼学」= 截图定位元素 → 记成相对窗口原点的偏移 + 记下达成它的最稳方式(快捷键/菜单/坐标) → 写进该 App 的 profile。
「内心 figma」原则(顺手记全屏):你为了点一个键,已经截图看清了整屏。那就别只记那一个键——把这一屏所有可见功能的位置都顺手记进 profile,包括这次没点的(标题、tab、分类项、其它按钮、列表结构)。代价几乎为零(图已在眼前),收益是下次到同一屏不必再裁图找,脑内直接定位任意功能。
- 边界:记你确实路过的每一屏的全部可见键;但别为没用上的功能专门去翻新页面(增量长,按需访问新屏)。两者不矛盾:访问到的屏记满,不刻意扩张访问范围。
- 坐标随滚动/内容浮动的(列表项、分类项),记下来同时标注「随滚动变,用前截图重定位」;不动的骨架(搜索框、tab、底栏、键盘)记成稳定坐标。
- 从录制/截图学时,逐帧仔细看,别扫缩略图把看不清的当「噪声」跳过。 每一屏都是一个 state、每个跳转都是一条边,全画进图——没有噪声帧。(2026-06-21 实测教训:我把 montage 一扫,把「付款成功后怎么回首页」那几帧当噪声跳过、漏画了那条边,被用户抓到。)
学完就落脚本(最重要的产物)。 光在 profile 里记坐标 ≠ 能一次跑完——那等于把「重组链路 + 逐个点」留给模型,它一组装就触发「做一步看一眼」、一张张截图。真正的可执行产物是脚本:一段学好的稳定链路,落成一条 lib/<app>-<flow>.sh(含所有点击+等待,中文用 pbcopy 设剪贴板再 cliclick 真实 Cmd+V),参数化(店名/商品/规格…)。agent 跑这一条命令就走完整段,物理上没有「中间」可截,逐步观察的反射被绕开。
- 秘密边界:脚本跑到密码/付款这一步必须【停】,密码绝不写进脚本。 脚本只含公开的【路径】(点击坐标+等待);到支付密码这种只有用户能给的秘密,脚本停下,由 agent 问用户、把口令实时映射到密码键盘坐标逐位点入——键位在 profile(哪个数字在哪),数字本身不进任何脚本/文件。脚本=可共享的路径,密码=用完即弃的实时口令。
- 错了修脚本,不重学。 某个坐标漂了 → 改脚本里那一个数;这是脚本级自愈,比重跑一遍学习快得多。
- profile 仍要(记屏图/坑/哪稳哪变),但它是给脚本当说明书和坐标来源;脚本才是跑起来的东西。
- 见
lib/meituan-order.sh(参数化全链路)、lib/meituan-search.sh(搜索段)为范例。
链路置信度(confidence-gated execution)—— 截不截图由数据决定,不由模型临场决定
默认翻转:学过/演示过的链路 = 高置信(trusted),下次就该代码级一次性通过、零截图、不唤起大模型逐步判断。 不是"先攒够次数证明给我看才信"——信任是默认给的(你都学过/演示过了),出错了才收回。模型每介入一步就手痒想截图;把"截不截图"这个决策从模型手里拿走、交给链路的历史一次性成功率这个硬数据,多余截图就从机制上消失。
每条链路(一个 flow,对应一条 lib/<app>-<flow>.sh 脚本)带一个 confidence 状态:
trusted(默认,学过/演示过即是) → 跑脚本,代码级一条命令通过。零截图、不逐步唤起模型判断。开局那张定位截图也尽量省掉——脚本第一步无条件复位到锚点(如退回列表页/首页)就不必看"现在停在哪"。只认脚本退出码:exit 0 = 成功,非 0(如 FOCUS_LOST)才回头。
observing(仅在出错后降级到此) → 才回到"开局定位一张 + 内心链路 + 卡点睁眼"的模式,睁眼补那一处、修脚本里那个坐标,跑通一次(一次性、无需中途修正)→ 自动升回 trusted。
oneShotSuccesses 计数器:累计"一次性成功(中途无需睁眼/无需修正)"的次数。它不是准入门槛,是健康度记录:连续成功累加、信心越来越足;任一次失败 → confidence: observing、计数清零、记 lastFailure。
- 演示/录制是最强信任来源:用户亲自录过的链路(如 wechat-flowmap 的发消息路径),直接
trusted、demonstratedBy: user,第一次就走脚本,别再睁眼学一遍。
判据一句话:这条链路有成品脚本且 confidence: trusted → 直接 bash lib/<app>-<flow>.sh 参数...,看退出码,不截图。 没脚本/observing 才睁眼。内容相关的一步(读对方发来什么、列表第几行有红点)天然算不出,仍要截一张看——那不算"链路截图",是输入采集。
范例:lib/wechat-send.sh <联系人> <消息>(发消息已演示,trusted)、lib/meituan-order.sh(下单全链路)。profiles/_index.json 每条 learned 记 confidence/oneShotSuccesses/demonstratedBy。
两条铁律(实测踩过的坑,必须遵守)
- 任何键盘输入前,先
activate 目标 App。 坐标点击(peekaboo click)不管焦点都落在目标窗口上,
但键盘事件(⌘A / ⌘V / 回车 / type)只发给当前最前台 App。不先聚焦,你的文字会打进终端/别处,
搜索框纹丝不动还以为成功了。每段键盘操作前都补一句:
osascript -e 'tell application "PROC" to activate'(sleep 0.4)。
- 每一步动作后给页面渲染时间,再做下一步;关键步点前先自检。 搜索回车后结果要 ~2s 才渲染好,
太快点「单曲」标签会点在没加载完的页面上、落空。搜索→等 2s→切 tab→等→双击行→验证。
页面跳转/加载要等更久(实测:搜索结果 ~2s;进店/结算/收银台等大跳转 ~2.5-3.5s;支付结果页 ~3.5-4s)。
宁可慢半拍:跳转类操作后多等一会儿,再截图确认目标已出现,没出现就再等、别赌时机、别点空。
(用户明确反馈:这类流程跳转/等待很常见,等待纪律比手快更重要。)
自动执行原则(用户要自动化时,默认这样)
用户明确叫你「自动/帮我做完/一路做到X」时,他要的是自动化结果,不是被反复追问。默认姿势:
- 最快 = 一次性推理整条链路 + 只在卡点截图学习。 开始前可以(也最好)跟用户对齐一句「我要做什么、到哪一步停」;一旦跑起来,只在真正的卡点才停(算不出的内容相关步、或链路断了)。
- 别中途反复问。 能用已记/已算的坐标做的,绝不停下来问、绝不再学一遍。链路一旦确认,就默认一口气直达目标,多往前走几步,别一步一确认(那是胆小,不是稳)。
- 隐私/安全的停与不停:只有当用户可能忽略了某个隐私或安全后果时才停下提醒(如「这会真扣款/把隐私发出去,你确定?」);如果用户已经明确要这个结果,就别问——他知道自己在干嘛。花钱/不可逆的临门一脚仍可截一张图自检(看,不是问)。
- 唯一例外是支付密码这类必须由用户给的秘密:到那一步问用户要,不存、不猜。
定位一次 → 一口气跑内心链路(localize-then-batch,profile 已存在时的正确姿势)
核心执行模型:每一次都「进行到底」。 不是一步一停、一张张截图——那是第一次睁眼学的节奏。学过一遍、profile/脚本在手后,模型的默认行为应当是:
- 固定节点理想只有一个:开局定位——看镜像状态 + App 停在哪屏。这是唯一算不出来的(App 停在上次离开的地方)。定位完、复位到锚点(如首页),之后整条链路全是常量/可算的,一路跑到底、不再截图。
- 每次尝试都一路推理内心链路跑到底,中间不回头逐个确认。
- 某步还会漂 = 它还没被换成常量、或脚本还没修好,不是"该多截一张图"的理由。复盘修掉它:从当前屏反推「断在哪一步、为什么」→ 把那步换成常量 / 改脚本里那个坐标 → 从断点一次性再跑到底。绝不退回「逐步点」。
具体怎么走:
学过一遍、profile 在手后,别再「点一下截一下」——那是第一次睁眼学的节奏,慢且没必要。换成:
- 入口定位(截一张):App 常停在上次离开的那屏(不会复位)。进来先截一张,对照 profile 认出「现在停在哪屏」。
- 复位到锚点 或 就地接链路:要么先导航回规范起点(如美团点底部「首页」tab、或连点返回退到已知屏),从锚点起跑最稳;要么从当前屏直接接。
- 跑内心链路一串:链路里稳定骨架的坐标(搜索框、tab、底栏、固定按钮、密码键盘、规格弹窗里位置固定的选项)直接按 profile 坐标连续点,中途不截图。
state 感知(内心坐标自觉)—— 你随时知道自己停在哪个节点,走最短边,别无脑复位
你不是每次都从零开始的截图机器人,你是已经接入这个 App 的程序:心里时刻有一个「当前 state」变量。 你刚做完什么动作、现在停在 flowmap 的哪个节点,你是知道的(不用截图问自己)——做完一个动作就在心里把当前 state 推进到下一个节点。要执行下一件事时:从当前 state 在 flowmap 上找到目标节点,走最短的那几条边,而不是条件反射式地先退回锚点再导航过去。
- 复位到锚点是有成本的(退两次返回 + 重新搜索 + 等待渲染),只在「当前 state 真的不确定」或「就地路径反而更绕/更脆」时才值得。已经站在目标节点上,就地把事做完——别先回主菜单再绕回来。
- 典型反面教训(2026-06-24 小刍抓到):我已经在「对方会话页」读完消息,要回他,却退回列表→重新搜索→重新进会话才发——等于一个明知自己在哪的程序却每次回主菜单重新导航。正解:就地接链路(
wechat-send.sh ... here 跳过导航直接发,发完留在会话页继续盯)。
- 脚本要支持「起点参数」反映这一点:同一条发送链路,
from=here(已在会话页就地发)和 from=list(从列表搜索进入)是两条不同长度的路径;agent 按自己当前 state 选对应入口,而不是只有「从锚点起跑」一种。
- 回复就地,监测回首页看角标(2026-06-24 小刍定)。 两件事用两种 state:①回复——在目标会话页就用
here 就地发(别退回去重搜,那是无脑复位);②监测对方有没有新消息——回首页(列表页),靠 App 自己的未读红角标判断,角标是 App 给的现成「有无新消息」信号,比每轮进会话猜省。流程:回首页等角标 → 某行角标更新 → 进那个会话(here 读+回)→ 回完退回首页继续等角标。别守在会话页空转,也别每轮无脑进会话。
- ★核未读角标必须用
lib/grab-badges.sh,别看 grab.sh 全图(2026-06-24 血泪)。 微信未读角标 = 联系人头像右上角的小红点(不是行右侧,那是时间戳!)。这红点只有几像素,grab.sh 截全窗口后整图传给模型会被降采样压糊、红点糊没 → 连漏好几次误判"没新消息",小刍发高清图才发现。grab-badges.sh 只截左侧头像那一窄列 + 放大 2x,小区域保留原始像素密度,哪行头像有红点一眼可辨。通用教训:核任何几像素的小细节(角标/小图标/勾选态)都别看整图缩略——裁那一小块放大单独 Read。
- 冷热双模式(2026-06-24 小刍定,对话热时提速到 ~15s)。
ScheduleWakeup 下限是 60s,对话冷时够用;但对方聊得快时 60s 太慢。两挡自适应:
- 冷模式(默认):回首页,每 60s
ScheduleWakeup + grab-badges.sh 核角标。省电、适合零星消息。
- 热模式(对方连发、聊得快时切入):守在会话页别退首页,
run_in_background 跑 lib/wechat-watch.sh 15 20——每 15s 截会话消息区比对 md5,对方一发新消息→画面变→脚本退出→harness 当场唤醒我→看图直接 here 回→再续一个 watch。延迟 ~15s,远快于 60s。
- 切换:watch 报
IDLE(连续 20 轮≈5 分钟没动静)= 对方安静 → 退回首页转冷模式;冷模式发现对方又开始连发 → 切回热模式。
- 基线坑:watch 第一帧作 md5 基线,所以发完自己的回复后再启动 watch,基线含自己消息、之后变化才是对方的(否则自己的回复会触发一次空唤醒)。动图/语音波形会持续触发,看一眼无新内容续 watch 即可。
- 监视区只盯下半屏气泡:watch 截图区从
y+300 起(避开顶部「对方正在输入…」提示——它一闪一闪会误触发),到输入框上方。新消息总滚到底部,下半屏一定抓得到。
- 解读焊进监视器,主 agent 只在真消息时被唤醒并拿到现成文字(2026-06-24 小刍想法,已落地)。 别让主 agent 自己 Read 会话截图(图进主上下文,聊几十轮就爆),也别每轮被空触发唤醒。
wechat-watch.sh 升级版自带解读:md5 测到画面变 → 脚本内部调 headless claude -p 解读那帧 → 解读=NONE(撤回/正在输入/自己消息渲染等空触发)就自己消化、继续盯、不唤醒主 agent;解读出对方新消息才 echo "NEW_MSG: …" 退出唤醒主 agent。主 agent 被唤醒时 task 输出里就是「对方说了X」,直接 here 回,全程不碰图。
- headless 解读命令(安全且实测可行):
claude -p "<读图prompt>" --allowedTools Read --model sonnet --output-format text。只白名单只读 Read、绝不加 --dangerously-skip-permissions(那会造无门禁自主 agent,被 auto-mode 正确拦截)。
- 模型别用 haiku:实测 haiku 读中文微信截图会读错气泡原文(连「有无新消息」之外的内容都读错),拿它解读直接回会答非所问 → sonnet 起步。
- 代价:每次画面变化调一次 sonnet 解读(~14s、~15k tokens),但全在 background;过滤掉空触发后主 agent 只在真消息时醒一次,省主 agent 回合 + 主上下文不堆图。md5 没变就不解读,省调用。
- 并行终极版:常驻 daemon + 哨兵(2026-06-24 小刍洞察「sonnet 盯截图,你盯 sonnet」,最优形态)。 单进程
wechat-watch.sh 的病根是串行:它 exit 唤醒主 agent 后,到主 agent 处理完重启它之间,sonnet 是停的、空档漏消息。终极版拆成两个常驻 background,让 sonnet 和主 agent 真并行:
wechat-watch-daemon.sh(常驻,sonnet 盯截图):一直跑不退,每 15s 截图比 md5、变化就 sonnet 解读,对方新消息 append 到收件箱 /tmp/wechat_inbox.txt,自己继续盯。主 agent 处理/打字时它不停。
inbox-wait.sh(哨兵,主 agent 盯 sonnet):盯收件箱,一有新行就打印新消息并 exit → 唤醒主 agent。
- 主 agent 被唤醒后的固定动作:读哨兵输出的对方新消息 →
here 回复 → touch /tmp/wechat_reply_marker(通知 daemon 基准前移、防把旧消息重复入箱)→ 只重启 inbox-wait.sh,daemon 绝不重启。
- 去重靠 marker:daemon 解读「我方最后一条之后对方的全部新消息」,按尾部增量入箱;主 agent 一回复就 touch marker,daemon 把基准移到我方新气泡之后,旧消息不再算新。
- 开关时机:热对话开 daemon(
run_in_background 跑 wechat-watch-daemon.sh 15 240 12,第三参=连续冷多少轮报 COLD);用户说撤 → TaskStop 掉 daemon + 哨兵(它常驻烧 sonnet token)。
- 冷了退主界面扫全局、切活跃会话(2026-06-25 小刍定的混合模式闭环)。 别死守一个冷掉的会话、错过别处找你的人。两个唤醒源各有处理:
- 哨兵退出(
NEW_FROM_THEM:)= 当前会话有新消息 → 读文字 → here 回 → touch marker → 重启哨兵(daemon 不动)。
- daemon 退出(
COLD:)= 当前会话连续 ~3 分钟冷 → ① TaskStop 掉哨兵(它还盯着旧 inbox)② 退回主界面(列表页,连点返回)③ grab-badges.sh 扫红角标看哪个会话有新消息 ④ 有 → 进那个会话 → 重挂 daemon + 哨兵(切到新活跃会话);都没有 → 退到 60s grab-badges 冷轮询,有人冒头再切回热模式开 daemon。
- 本质:守会话(并行快) 与 盯列表(全局) 在单界面下不可兼得,用「冷却 3 分钟自动切换」在两者间跳——热聊享并行、冷了不漏别处。
- NEW_MSG 是自己的就别回:解读偶尔仍可能把我方消息误判带出,主 agent 看到收件箱内容是自己刚说的话,直接跳过不回,别自己截图核对(小刍明确:不要自己读图)。
核心判据(按置信度走):能算出来的链路就不截图;截图只在两种意外时跑——①操作疑似失误(点完该变没变、焦点可能丢了、价格/件数对不上预期)②碰到完全的新东西(profile 没记过的屏/弹窗/字段)。
- 即:下一步状态你能预测 → 直点、连发一批,不看;不能预测或结果不符预期 → 截一张再说。
- 内容相关的坐标(列表第几行、滚动后的分类、随行变的「选规格」按钮)本质就是「算不出来」,归入「新东西/需确认」,看一眼。
- 唯一即便有把握也值得截一张的例外:花钱/不可逆的临门一脚(确认支付、提交订单、删项)——一张图的保险,便宜。
原则:常量永远胜过变量。 能把「内容相关、会漂移」的步骤换成「固定锚点」就一定换——如美团店内用搜索框搜商品名(搜索框坐标固定、结果第一条稳定),别去赌随滚动浮动的分类/列表坐标。该处的「大胆」= 走确定路径,不是盲点一个记忆里的位置;盲点变量必漂。(2026-06-21 实测教训:进店没走店内搜索、去盲点分类栏,分类落到「冰爽冷饮」、选规格点中「玫瑰香椰冷萃」,整条链漂掉。)
边界处理(实测)
- 窗口被拉伸/挪动 → 一句 set bounds 复位。 normalize 不只开局用,每次复用缓存坐标前都先跑一遍,
几何就回到指纹。注意有些 App 有最小窗口尺寸(网易云 ~1056x752),所以归一化后回读实际几何。
- 冷启动可能吞输入。
open 返回不等于能交互;先轮询到窗口出现(count windows ≥ 1),再小等一下。
但不要赌时机——动作后一律验证:搜索后确认结果渲染出来了,没有就说明还没就绪 → 等一下重发。
这条「验证+重试」自愈,不管机器快慢都稳。
- 快捷键优先于点坐标(前提仍是先 activate):能用 ⌘→/空格 这种就别去点按钮。
终极绕法:iPhone 镜像驱动 iOS App(对付防截屏/反自动化的 App,如微信)
某些 App 在 Mac 上既防截屏(截到空白)又反自动化(会风控)。绕法:在 iPhone 镜像窗口里操控手机上的那个 App——
动作发生在 iOS(App 只看到一次正常触摸,不触发 Mac 风控),而镜像窗口是普通 macOS 窗口,grab.sh 能截到内容、peekaboo 能点。两道防线一起绕开,已对微信全自动跑通。要点:
- 前提:iPhone 镜像配好;接力/Handoff 必开(Universal Clipboard 靠它把 Mac 剪贴板同步到 iOS,不开则镜像里粘贴是空的);
brew install cliclick。
- 截图/点击:
grab.sh "iPhone Mirroring" 截图(手机屏可见)、peekaboo click 点 —— 跟普通窗口一样。
- 任何输入前先清空输入框。 框里可能残留上次的字(搜过的词、历史),不清空就粘会变成「皮爷咖啡皮爷咖啡」。聚焦后先
cliclick kd:cmd t:a ku:cmd(Cmd+A 全选)→ cliclick kp:delete(删),再粘。空框也无害。
- 输入中文:剪贴板 +
cliclick kd:cmd t:v ku:cmd(真实 Cmd+V)。peekaboo/osascript 发 Cmd+V 会被镜像把 Cmd 吃掉只剩 v —— 必须 cliclick 这种真实 CGEvent 才带得动修饰键。中文用 pbcopy 设剪贴板(避开 osascript 引号坑)。
- 发送:
cliclick kp:return(普通 Return)。绝不用 Cmd+Return —— 那会把输入框顶进多行模式,Return 变换行发不出。
- ★代聊就必挂轮询(铁律):只要还在代someone聊天,发完当前回复就立刻挂轮询盯新消息——别发完一条就停手等用户来催。 推荐形态是「列表扫描 + 白名单 + 退避」的自管循环(用定时器每轮唤醒自己):
- 列表扫描:每轮回到微信会话列表页(不死守一个会话),
grab.sh 截一张看哪行有红色未读角标,命中就点进去回、回完退回列表。坐标见 wechat-flowmap.json 列表页节点(会话行 y、返回键)。点前必新截一张——公众号推送会把列表行序顶乱,凭上一张的行号点会点错(实测点成被顶上来的公众号)。
- 行位/对象拿不准时,别赌行坐标——搜全名。 点搜索框→粘联系人/群全名→点第一个联系人结果(固定坐标)必命中:微信极少重名,「最常使用/联系人」分区第一条就是本人。这是把「会漂的行坐标」换成「固定锚点(搜索框+第一结果)」,比数行点行稳得多(合「常量永远胜过变量」)。
- 白名单:只回【用户配置的名单】(联系人/群名存本地、不入库),名单外有红点也只看不回。
- 退避节奏:1 分钟一轮起;同档连续 3 轮没新消息就退一档(1m→5m→10m 封顶);任何时候有新消息→立刻打回 1 分钟(对话热就跟得紧)。
- 永不自动撤:只有用户【明确】说撤才停;别因为「像收尾了/几轮没消息」就自己判断着撤,也别每轮催问要不要撤。
- 每轮截图用之即弃(读完 rm,不留盘)。状态(档位/空轮数/定时器 id/上轮列表快照)存本地一个 state 文件,跨轮不丢。
- 详见
profiles/wechat-ios-mirror.json。
决策顺序(从稳到不稳,能用上面就别用下面)
- 键盘快捷键 —— 最稳,零坐标,布局变了也不怕。
- 菜单按名字点(System Events)—— 播放/暂停/音量/上一首下一首这类几乎都在菜单里。
菜单项的名字本身还能当状态验证(如「控制」菜单首项是「播放」=已暂停,是「暂停」=正在播放)。
- a11y 树(
peekaboo see --app X)—— 能读到就按元素 role/label 拿实时坐标,免缓存。
- 缓存坐标(本 profile)—— 只在上面都不行(a11y 读不到的烂 App)时兜底,且只缓存不动的骨架
(搜索框、标签、列表第 N 行的位置),绝不缓存会变的内容(具体某首歌、某条消息)。点之前先自检。
归一化窗口(坐标可复现的前提)
不要用绿灯/全屏(全屏会进独立 Space + 隐藏菜单栏,坑)。用 System Events 把窗口摆到固定矩形,再回读实际几何当指纹:
osascript -e 'tell application "System Events" to tell process "PROC"
set frontmost to true
set position of front window to {0, 25}
set size of front window to {1440, 900}
end tell'
osascript -e 'tell application "System Events" to tell process "PROC" to get {position, size} of front window'
指纹 = {bundleId, 进程名, 实际 position, 实际 size, 屏幕缩放}。复用缓存前先核指纹,任一不符就重学(换分辨率/App 更新会让坐标整体偏,闭眼点很危险)。
缓存坐标怎么存怎么用
- 存相对窗口左上角的偏移,不存绝对屏幕坐标:
dx = 元素x - 窗口x,dy = 元素y - 窗口y。
- 用时读实时窗口 position,
点击点 = (窗口x + dx, 窗口y + dy)。窗口被挪了也不错位。
学一个新元素坐标(贴左上角 + 只截窗口那块,别全屏+裁剪)
把窗口 normalize 到贴左上角的固定矩形(set position {0,25} → 回读实际 y,菜单栏高度被回读自动吸收,这台是 30)。
然后只截窗口那块,不全屏(小 3 倍、快、还省掉裁剪):
lib/grab.sh NeteaseMusic /tmp/w.png
坐标换算变得极简:截图像素 (px,py) ÷ 缩放(2) = 窗口相对坐标(截图原点就是窗口原点,不用算全屏偏移)。
存进 profile 的就是这个相对偏移;用时 绝对 = 窗口position + 偏移。
peekaboo 的 image 在本机 beta 版常报 bridge error,所以用系统 screencapture -R。
防截屏的 App(微信)用 grab.sh 照样空白 —— 那种靠用户全屏截图(用户自己截能看到内容),我按固定窗口几何反算坐标。
点前自检 + 点后验证
- 点列表项前:先截一张确认「第 1 行确实是我要的那个」(标题文字对不对),再双击。
- 动作后:用菜单项名 / 一张小截图确认状态真的变了(如播放→暂停)。不对就重来,别假设成功。
本机注意事项(实测)
peekaboo image / see / menu list 在这台机的 3.0.0-beta3 上对部分 App 报
PeekabooBridge ... error 1 —— 改用系统 screencapture 截图、用 System Events 读/点菜单。
peekaboo click --coords "x,y"(加 --double 双击)工作正常,坐标是逻辑点(和 AppleScript 一致)。
- 中文输入走剪贴板,别直接 type(避开输入法):
osascript -e 'set the clipboard to "中文"' 然后 peekaboo hotkey --keys cmd,v。
profiles/
每个 App 一个 JSON,见 profiles/neteasemusic.json(已实测跑通:搜索→单曲 tab→双击第一首播放,
播放/暂停/音量全走「控制」菜单)。新 App 照它的结构学一份。