我原本只想让飞书帮我“截个图”,结果被 macOS 权限狠狠干了一课

4 阅读6分钟

我以为自己在做一个小功能。
最后才发现,我其实是在和 macOS 的权限链、进程宿主、图形会话狠狠干架。

我想做的事听起来非常简单:

我有一台远端 Mac,Chrome 里会打开微信登录二维码页面。
机器一重启,登录态可能丢,需要重新扫码。
但我看不到这台 Mac 的屏幕——我唯一能操作它的入口,是飞书。

所以我冒出一个特别直接的想法:

能不能我在飞书里发一句“截图”,这台 Mac 就把当前页面截下来,再回传给我?

真正做起来我才发现:难的根本不是“截图”本身,难的是它背后的系统边界:

  • 飞书消息怎么进到本机服务
  • 本机服务怎么调用 macOS 的 GUI 截图能力
  • 截图怎么回传到飞书
  • 为什么同样一段代码,Terminal 里能跑,后台启动却死活不行
  • 为什么“自动启动”不等于“开机后立刻可用”

最终我落地了两个能力:

  1. 飞书私聊发截图指令 → 本机截图 → 回传图片
  2. 登录 macOS 后自动用 Terminal 启动 longconn → 保住截图权限链

这篇文章把过程、踩坑和最终方案一次讲清楚。


一、这个需求表面是截图,实质是远程可视化能力

我最终想要的效果其实是一句话:

在飞书里发“截图”,远端 Mac 把当前页面截回来给我。

因为现实就是:

  • Chrome 会出现微信登录二维码页面
  • 重启后登录态可能丢
  • 人不在机器旁边,看不到屏幕
  • 飞书是唯一可用的远程入口

所以“截图”不是一个孤立脚本,而是一条完整链路:

  • 入口:飞书消息
  • 执行:Mac 本机(且要能碰 GUI)
  • 回传:飞书图片消息

一句话:**这不是单点脚本问题,是在线链路能力。**我最初只是想做一个简单功能:在飞书里发一句“截图”,远端 Mac 就把当前页面截回来。

真正落地时才发现,难点根本不是截图命令本身,而是完整链路:飞书消息如何进入本机服务、本机进程是否具备 macOS 图形会话与录屏权限、截图结果如何稳定回传。

这次排障给我最大的教训是:

  • 同样代码,Terminal 启动能截图,后台启动可能失败;
  • 这不是代码对错,而是进程宿主与权限链差异;
  • 先把链路边界画清楚,再谈功能优雅。

最终我把截图能力直接接进 longconn 的在线消息链,支持“截图 / 截图 微信 / 截图 登录 / 截图 全屏”,并把启动方式固化为:登录后由 Terminal 拉起 longconn,保证权限稳定。

如果你也在做远程自动化,这件事非常值得先搞明白:你以为在做“一个小功能”,其实你在做“系统边界工程”。

二、我一开始差点走错:这事不能先做 skill

第一反应很容易是:做个截图 skill。
但很快我意识到不对,因为用户需要的不是“指导你怎么截图”,而是:

飞书消息一到,本机立刻截图,并把结果发回去。

这必须接入“正在运行的飞书长连接服务”,也就是 longconn.js
所以我最终选择:直接改工程,把截图能力接进 longconn 的消息处理链路里,而不是先做 skill。

结论:这个需求的核心不是 skill,而是在线服务能力。


三、最终我把截图入口接在了 longconn 里

现在截图命令由 longconn.js 直接处理,支持:

  • 截图
  • 截图 微信
  • 截图 登录
  • 截图 全屏

整体链路:飞书消息 → longconn.js 收到事件 → 解析截图命令 → 激活 Chrome/Chromium → 读取窗口 bounds → 调用 screencapture → 上传飞书图片 → 回发图片+文字说明。

这意味着:截图能力被“内嵌”进 longconn 的在线链路里,而不是绕一圈再调外部工具。

四、代码层面我加了什么

1)新增环境变量

  • SCREENSHOT_DIR=./outbox-screenshots
  • SCREENSHOT_APP_NAMES=Google Chrome,Chromium
  • SCREENSHOT_WINDOW_TITLE_KEYWORD=
  • COMMANDER_DISPATCH_URL=http://127.0.0.1:18801/dispatch

2)新增关键函数(longconn.js)

  • parseScreenshotCommand
  • focusChromeWindow
  • captureChromeScreenshot
  • uploadFeishuImageFromFile
  • sendFeishuImageByOpenId

到这里功能“看上去”已经完整了,但真正折磨人的坑,从这儿开始。


五、踩坑 1:我以为在写截图,其实在写“谁有资格截图”

一开始我把问题想成:怎么把 screencapture 调起来。
但真正的问题是:

  • 谁在执行截图?
  • 执行者有没有录屏权限?
  • 这个进程是不是处在可用的图形会话里?

结论:这件事不是命令执行,而是“在线进程在特定宿主中执行 GUI 操作”。

六、踩坑 2:AppleScript 读 Chrome 标签页标题不稳定,别硬刚

我最初想做得更“聪明”:用户发“截图 微信”,就去找标题包含“微信”的标签页精准截图。

但在这台机器上,title of active tab of front window 会直接报 AppleScript 语法错误(-2741)。

最终我做了取舍:

  • 命令格式保留“关键字”(截图 微信/登录)
  • 实现不再依赖标题匹配
  • 统一退化为:激活前台浏览器窗口 → 直接截图

结论:不要过早依赖 Chrome 标签页词典,前台窗口截图更稳。

七、踩坑 3(最核心):同样代码,Terminal 能截图,后台启动却不行

我遇到过这个报错:

screencapture exit 1: could not create image from display

关键对比:

  • launchctl 后台启动 longconn:截图失败
  • 在已授权录屏的 Terminal.app 手动启动 longconn:截图成功

这说明问题不在代码,而在进程宿主与权限链

结论:在 macOS 上,“同一份代码由谁拉起来”会直接决定截图能不能成功。

八、踩坑 4:截图链路通了,不代表普通消息链路也通了

截图命令在 longconn.js 内本地完成,不依赖 commander;普通文本消息则依赖 COMMANDER_DISPATCH_URL -> commander -> openclaw

因此会出现“截图能用,但普通消息报 ECONNREFUSED 127.0.0.1:18800”的情况。

根因:启动时没显式设置 COMMANDER_DISPATCH_URL,进程回退默认 18800。

结论:同一个服务里有多条链路,验证了一条不代表另一条没问题。

九、为什么改截图逻辑,不需要重启整个 openclaw

边界要分清:

  • 截图逻辑在 longconn.js 内执行 → 改完只重启 longconn
  • 普通问答/任务转发依赖 commander/openclaw → 才需要动 commander/openclaw

结论:别为了截图去重启全套系统,排障会被自己拖慢。

十、当前最稳定的启动方式:手动从 Terminal 拉起

cd /Users/bonnie/.openclaw/workspace/feishu-bridge
set -a
source ./.env
set +a
/opt/homebrew/bin/node ./longconn.js

启动成功会看到:

[longconn] connected. waiting for events...

写在最后

这次经历让我彻底意识到:

你以为在做“截图功能”,其实你在做的是“远程可视化能力的系统工程”。

如果你也在做类似的远程自动化,记住一句话:

先把链路边界画清楚,再谈功能优雅;先把权限宿主打通,再谈自动启动。

欢迎留言交流你踩过的权限坑。