svg
复刻 Codex 的权限引导,然后开源了

复刻 Codex 的权限引导,然后开源了

2026 年 4 月 20 日 9 min read
R Written by Riko

仓库:riko2chen/AskForPermission

先看效果

背景

Codex 更新 Computer Use 之后,让我感觉惊艳的是它的 onboarding:点「Enable Accessibility」,系统设置窗口弹出来,一张引导卡从按钮位置沿着抛物线飞出,贴在设置窗口旁边;卡上露出宿主 app 的图标,拖进去就完成了授权;落位时卡片和箭头各自越过再回来,用户移动设置窗口时,上面的蓝色箭头像被惯性拽着拉长。

macOS TCC 是一套对用户和开发者都不友好的系统。大部分 app 的做法是弹一个对话框,写一句「请去系统设置授权」,然后假装这件事已经尽力了。Codex 这段 onboarding 则是体现出了对 macOS 系统深刻的理解,通过引导用户的目光、动作,顺滑地完成授权的整个过程。

因为 Computer Use 刚发布几个小时,社区里还没有找到复刻的,所以我觉得自己做一个。(不过实现到一半的时候发现有人逆向了一个,但看了一下效果不够理想,很多细节没还原)。

第一版:对照视频还原

一开始我想既然录了视频,就按照视频效果来实现吧。需要还原的主要是两块:飞行动画和拖动引导。细节层面要照顾的有这些:

  • 卡片动画的起点是否严格保持在按钮原位置
  • 箭头的回弹效果
  • 台前调度(Stage Manager)下的行为
  • 卡片圆角在飞行过程中的过渡
  • 飞行曲线的顶点高度和到位时的回弹幅度

按这份清单做完第一版,视觉直觉上就不对。两个最明显的问题:

  • 动画应该从按钮位置开始,但是实际上是从半途中出现,或者是从窗口边缘出现。后来我对比了 GitHub 上几个模仿 Codex onboarding 的工程,有不少都是从半路开始飞行动画。
  • Stage Manager 开启后,系统设置被唤起那一刻,当前 app 被推到屏幕左侧的 pile,飞行从一个消失的源头起飞,卡片落到屏幕外。这一块我后来发现其他开源方案没注意这个细节。

想了想,我觉得还是得逆向找找思路。

第二版:反编译

Codex 的 macOS 插件产物就躺在本地缓存里,用 Hopper、IDA、Ghidra 各过了一遍,把符号、字符串常量、布局数值逐项核对。做完这些我们获得了如下的信息:

  • 飞行是两段式交叉淡入,两个控件同时在空中靠透明度切换
  • 卡片尺寸、圆角、margin 有了一批参考值
  • 箭头是独立的一条动画,和卡片本身的位移解耦

按这些参数再实现一次,差距缩小了,但还没到完全一致。原因有几个:反编译的符号只能告诉我用了哪些 API,缓动和弹簧阻尼这一层属于运行时组合,看不到;Codex 有一部分走的是自定义渲染,绕过了标准动画 API;我的显示器缩放、字体设置和 Codex 录屏机器不同,肉眼像素比对本来就有偏差。

考虑到继续反编译的话收效甚微,于是换了一个方向,在目前的这个基础上,把体验按照我们直观的感受进行细致的优化,重新设计几处交互。

换一个方向

单控件飞行

Codex 飞行用的是两个控件做交叉淡入:一个承载原卡片样式,一个承载目标卡片样式,两个控件同步沿抛物线移动,靠透明度切换完成内容变换。

我们改成单一飞行卡片的方案:从头飞到尾只用一张卡片,源图和目标图在同一个窗口里做交叉淡入,圆角、尺寸、阴影都是同步插值。

这样做的几处改善:

  • 一个窗口、一套阴影、一次模糊采样,层级更简单
  • 飞行过程中不会出现两个控件错位
  • 圆角从 12 点渐变到 24 点,尺寸同步变化,视觉上是一张卡在长大,比两张卡互换更连贯

模糊做成钟形曲线:起点和终点归零,抛物线顶点达到峰值。顶点位置的模糊和图像交叉淡入落在同一时间点,内容切换被最深的模糊遮住,观感是一次「模糊里完成了变身」。

从原位置开始起飞动画

Codex 原版和网上大部分复刻在起飞瞬间都有一小段真空:源行已经消失,但飞行控件还没到位。我们用两步处理这段衔接。

  • 点击那一刻同步对源行拍一张快照,读的是窗口自己的渲染内容,不需要 Screen Recording 权限
  • 飞行前一帧把真实行切换成虚线占位态,同时把飞行卡片用同一张快照摆到原位置。两个动作发生在同一帧,飞行卡片遮住原行的切换,肉眼看到的是同一张卡从原地起飞

反向返回对称:在抛物线顶点那一帧触发回调,把行的占位态切到最终形态(已完成或者回到原按钮),飞行卡片正好在最高点做反向交叉淡入,行的变化被整张卡遮住,落地时不会闪。

台前调度

Stage Manager 开启时,系统设置被唤起会把当前 app 从屏幕中央推到左侧 pile。如果不处理,飞行从一个被推走的源位置起飞,卡片直接落到屏幕外。

最早想过抢回焦点,让 app 始终在前台。实测和 Stage Manager 的调度逻辑冲突,体验反而更差:app 一会儿被推走一会儿被拉回,光标位置也跟着来回跳。

最终的顺序是:

  1. 触发打开系统设置后,先等系统把设置窗口在 Stage Manager 下摆好
  2. 拿到稳定的窗口位置,再把宿主 app 激活并拉回前台
  3. 从源行当前的屏幕位置起飞

顺序反过来会错:先动再等,源行会在 Stage Manager 挪动的中途被采样,飞行起点就对不上。

另一处容易踩的坑:飞行卡片和引导卡的窗口属性刻意不带 transient 标志,否则 Stage Manager 会把它们当成宿主 app 的一部分一起推走。

等窗口落位

用户把系统设置最小化到 Dock 之后,再次点击权限按钮,系统会用 genie 动画把窗口从 Dock 恢复。如果直接读窗口位置,读到的是动画中间帧,飞行卡片会落在窗口飞出的半路。

判定条件改成两个,任一满足才返回:两次采样到的窗口位置像素相同,或者持续观察累计超过 1.2 秒。这个时间窗口足够覆盖 genie 动画的全部过程,飞行终点一定落在设置窗口真正停住的位置。

箭头的弹性

引导卡贴在系统设置窗口旁边,用户拖动系统设置时,引导卡 1:1 跟随。如果箭头也完全刚性,视觉上会「糊」在一起。我们给向上箭头加了一个纵向的惯性形变:

  • 持续采样卡片位置,把位移差分成纵向速度
  • 速度越大,箭头被拉得越长;向上最大拉伸到 2 倍,向下压缩到 0
  • 窗口停住 150ms 后箭头弹回原始大小,欠阻尼弹簧会让它越过再回来
  • 再等 2 秒补一次 2 倍幅度的小跳,作为一次交互的收束

方向故意反过来:窗口向下移动时箭头顶部向上拉伸,观感像橡皮筋被拖拽出来的后坐力。刚性跟随会让箭头和窗口看起来是同一块,加了反向形变之后,箭头变成一个单独的物体,有自己的惯性。

卡片本身的落位也用欠阻尼弹簧:卡片和箭头会越过目标位置再回来。调到接近临界值看起来更稳重,但那一下弹的节奏正是 Codex 最有辨识度的手感,保留了这一点。

更大的覆盖面

Codex Computer Use 的权限引导覆盖两项:Accessibility 和 Screen Recording。这两项是 macOS 上最常见、也是必须走「拖 app 进列表」的权限。AskForPermission 把覆盖面扩到六项:

权限PermissionKind
Accessibility(辅助功能).accessibility
Screen & System Audio Recording(屏幕与系统音频录制).screenRecording
Input Monitoring(输入监控).inputMonitoring
Full Disk Access(完全磁盘访问权限).fullDiskAccess
Developer Tools(开发者工具).developerTools
App Management(App 管理).appManagement

其中「开发者工具」和「App 管理」没有公开的查询 API,我们在运行时从 TCC 私有框架里解析出对应的符号,不加编译期依赖。未来 macOS 如果移掉这些符号,调用退回到「未授权」状态,不会闪退。

不在覆盖范围内的三类权限各有原因:

  • 自动弹出型的 TCC(摄像头、麦克风、日历、联系人等)只有在 app 调用过对应框架之后才会出现在列表里,没有拖拽动作可以引导,走官方的 request API 更合适
  • Automation、Files and Folders 这类权限有嵌套条目,拖拽目标是多层结构
  • 访达、共享等扩展走的是另一套登录项和扩展机制,和 TCC 不是一套

TCC 弹窗的判定

用户把图标拖进系统设置的权限列表之后,macOS 会弹一个 TCC 确认弹窗。系统不会通知我们这个弹窗的结果,需要自己判断用户是点了允许还是取消。

我们同时监听四个信号,哪个先命中以哪个为准:

  1. 权限状态翻为已授权:用户点了允许
  2. 系统设置的在屏窗口数从峰值下降:TCC 弹窗被关闭(允许、不允许、取消都会触发)
  3. 宿主 app 从后台变前台:用户切回来了,但没完成授权
  4. 10 秒安全超时

任何一路「弹窗消失类」信号命中后,会额外等 200ms 再读一次权限状态,因为 TCC 的更新偶尔比弹窗消失的 UI 慢半拍。整个判断不需要额外权限:窗口列表信息公开可读,app 前后台也是本地查询。

几处取舍

  • 拖拽结束后不再做反向飞行,直接把引导卡片和飞行卡片隐藏。此刻 macOS 在弹自己的 TCC 对话框,反向动画会和系统弹窗抢注意力
  • 源行在用户等待决定期间保留占位态。点允许后切换为「已完成」,超时或取消则切换回原按钮
  • 点引导卡上的返回按钮时才走反向飞行,路径对称,卡片回到源行位置
  • AppKit 没有跨 app 的窗口移动事件,引导卡的跟随只能靠定时采样实现

回头看 Codex

做完这一圈,我对 Codex 这段 onboarding 的评价比一开始更高。

AppKit 其实早就提供了这些能力:精准唤起系统设置对应页面、把宿主 app 塞进拖拽面板、让浮动窗口在 Stage Manager 下稳定跟随。这些机制散落在文档里,很少被串起来。Codex 的工程师知道每一层能干什么,也知道它们组合起来可以做成什么。

用户不需要在系统设置里艰难寻找开关的位置,整个流程轻松丝滑地完成了。

我们在这条路径上做的事情,是把几个容易被忽略的细节做干净:前后无缝切换的时机、Stage Manager 下的顺序、最小化复原时的窗口落位、单卡片的飞行路径、箭头的惯性方向,以及更大的权限覆盖面。

一个优秀的交互一旦被人们知晓,会提高大家审美的阈值,也会让更多产品在起步的时候更容易靠近「优雅」的标准。最后希望这个开源项目可以被更多产品用上,无论是引用或者是蒸馏,希望未来都有更多 macOS 的软件用上这个交互。

仓库:riko2chen/AskForPermission

让我们为世界创造一些美妙和优雅

If you have anything you'd like to discuss, any ideas you want to bounce around, please send me a message.

svg