Maka 逆向工程实录:从 Codex 二进制反汇编到 Electron 画中画镜像的实现移植
【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka
Maka(Apache Maka, Incubating)的 Computer Use 功能在驱动后台窗口时,需要一个画中画(Picture-in-Picture,PiP)镜像窗口来实时展示 Agent 正在操作的目标。本文完整记录了 Maka 团队如何对 Codex 的 PiP 窗口实现进行二进制级逆向工程——从sky.node原生插件和 XPC 服务反汇编中恢复窗口结构、弹簧运动常量与交互算法,再逐条移植到 Electron 主进程,并刻意在四个设计点上做出差异化选择。读完本文,你将掌握一套"从反汇编取证 → 行为实验验证 → 源码级移植"的完整方法论,以及 Maka PiP 镜像(pip-window.ts + pip-motion.ts)从窗口语义到运动物理的全部实现细节。
逆向工程概述:从两份二进制工件到完整行为模型
本文档(codex-pip-reverse-engineering.md)的核心立场是:确认的原生事实(confirmed native facts)与实现推断(implementation inference)必须分开记录。这一点与仓库中另一份取证文档 computer-use-cursor-provenance.md(Agent 光标溯源)采用同样的方法论——每个移植的常量都附上来源与反汇编出处,未经验证的猜测绝不混入事实层。
逆向工程的产出物分三类:
| 类别 | 内容 | 证据来源 |
|---|---|---|
| Confirmed(确认事实) | 面板属性、类结构、弹簧常量、抛出算法、尺寸/缩放/控件 | 反汇编逐条读出 |
| Measured(实测事实) | Electron 43 上的基线行为 | 真机行为实验 |
| Inference(实现推断) | 阻尼比 ζ 的物理含义、设计取舍动机 | 由常量推导 |
这种分层保证了移植代码可以被审计:任何后续维护者都能从 pip-motion.ts 的常量注释一路追溯到二进制里的某条指令。
检查的工件:为什么"只查一半"是致命的
逆向对象是两个进程,而两者之间的分工比任何单一个体都重要:
| 路径 | 职责 | |
|---|---|---|
| service | ~/.codex/computer-use/Codex Computer Use.app/Contents/MacOS/SkyComputerUseService | 采集与发布(capture and publish)半边 |
| host | /Applications/ChatGPT.app/Contents/Resources/native/sky.node | 窗口(window)半边 |
| main JS | /Applications/ChatGPT.app/Contents/Resources/app.asar→.vite/build/main-Be_0DBuv.js | 宿主注册 |
service 半边包含RemoteHostedPIPContentPublisher、RemoteHostedPIPCaptureStream、CUAServiceRemoteHostedPIPController等类,链接了 AVFoundation 与 ScreenCaptureKit,并使用AVSampleBufferDisplayLayer——但没有链接 AVKit。它只负责通过 XPC 捕获与发布帧,并不承载窗口。
真正的窗口生活在 ChatGPT 自己的原生插件sky.node中,属于PIPStack*类族。文档特别警告了一个认知陷阱:如果在 service 里搜不到窗口符号就断定"窗口不存在",是错误的。类名中的RemoteHosted前缀本身就说明另一半在另一个进程里——必须在得出结论前先找到那个进程。这个原则(先确认"另一半"存在,再下"缺失"结论)是本文档存在的部分原因。
Confirmed:面板(Panel)——setLevel: 0与addChildWindow:是设计支点
从RemoteHostedPIPContentCreateStackPanel的反汇编中逐条读出面板创建序列:
NSPanel initWithContentRect:… styleMask:0x80 // NSWindowStyleMaskNonactivatingPanel backing:2 defer:NO setTitle: setBackgroundColor: [NSColor clearColor] setOpaque: NO setHasShadow: NO setLevel: 0 // NSNormalWindowLevel — 刻意不置顶 setAcceptsMouseMovedEvents: YES setCollectionBehavior: 0x108 // FullScreenAuxiliary | Transient setHidesOnDeactivate: NO setMovableByWindowBackground: NO // 拖拽是手写的,见下文随后从-[PIPStackWindow attachToOwnerWindowForPositioning]调用addChildWindow:ordered:。
支撑整个设计的是两个事实:
setLevel: 0(NSNormalWindowLevel)——刻意不浮动(floating)。镜像窗口不应悬浮在所有无关应用之上。addChildWindow:——子窗口相对于父窗口排序,而非相对于桌面排序,并且随父窗口在同一次 window-server 事务中一起移动。
这两个选择在 Electron 端的对应实现见 pip-electron.ts 的pipWindowOptions:focusable: false(对应 nonactivating panel 的焦点契约)、hasShadow: false(原生阴影由pip.html自绘)、以及parent: parent as BrowserWindow——只有在找不到应用窗口时才退化为alwaysOnTop: true。
Confirmed:类结构(Structures)——PIPStack 家族与窗口服务器的分工
PIPStackHost { hostID, ownerWindow, anchorContentRect, anchors[], presentationScope } PIPStackHostAnchor{ contentPoint, alignment } PIPStackWindow attachToHost: / startFollowingOwnerWindow / ownerWindowFrameMayHaveChanged: PIPStackController drag beginDragAtContentPoint: → dragToContentPoint: → endDrag snap nearestTargetAnchorForCurrentAnchor:draggingVelocity: → moveStackToAnchorAlongCurve: motion configureMotionSpringsWithLeadItem:dragging:programmaticMove: hosts moveStackToHostID: / compatibleAnchorsIncludingDragHosts: clamp clampEnvelopeToVisibleScreen: PIPStackItemMotion{ springStiffness, springDamping, velocity, target, origin, restOffset } PIPStackContentView reinstallMouseEventMonitors → addGlobalMonitorForEventsMatchingMask:handler: addLocalMonitorForEventsMatchingMask:handler: updateHoverFromCurrentMouseLocation / contentPointForCurrentMouseLocation关键洞察来自ownerWindowFrameMayHaveChanged::它读取notification.object并比较notification.name,说明它是一个针对宿主窗口移动/缩放的通知处理器。它只负责重算锚点(recompute the anchor),不移动窗口——移动由 window-server 完成,因为面板是子窗口。
Maka 的移植对应物是 pip-window.ts 中subscribeAnchorChanges订阅回调的注释逻辑:只有 resize 被响应,move 被刻意忽略。因为子窗口随父窗口在同一次事务中移动,若再在此处重定位,既冗余又落后一帧——那一帧的滞后正是此前拖动时镜像"拖尾"(trailing)的根源。
Confirmed:常量(Constants)——弹簧、抛出、尺寸与缩放的逐条复原
弹簧:两组四列的fcsel
从configureMotionSpringsWithLeadItem:dragging:programmaticMove:反汇编出一个cmp w26, #0后跟四个fcsel,即两列四组常量:
| 非拖动(settling) | 拖动(dragging) | |
|---|---|---|
| lead stiffness(主项刚度) | 320 | 900 |
| lead damping(主项阻尼) | 42 | 55 |
| follower stiffness base(跟随项刚度基准) | 150 | 260 |
| follower damping base(跟随项阻尼基准) | 30 | 32 |
跟随项的值分别除以1 + 0.18·s与1 + 0.08·s,其中s = |index| · (1 + 0.45·|index|)是索引衰减因子。
阻尼比是这些数字而非其他数字的原因:非拖动时 ζ ≈ 1.17(略过阻尼),窗口永远不会越过目标角回弹;拖动时 ζ ≈ 0.92(略欠临界),指针拉动时窗口保留一丝"韧性"。Maka 只有一个镜像(没有堆叠),所以只使用 lead 列,follower 衰减没有适用对象——这一判断直接写在 pip-motion.ts 的注释里。
在 Maka 中,这两组常量以PIP_SPRING形式定义,并由stepSpring(半隐式 Euler 积分,每轴独立)驱动。选择半隐式而非显式 Euler 的原因也在注释中交代:显式形式在大dt下会增益能量——掉一帧就会让镜像飞走而不是迟到,这是动画绝不能出现的失败模式。同时dt被钳制在1/30秒以内,因为后台窗口可能给你数秒的间隙,没有任何弹簧能在那种步长下保持稳定。springAtRest用"半像素内且速度低于每秒半像素"判定到达,见 pip-motion.ts。
抛出(Throw):0.55 的比例与点积评分
从nearestTargetAnchorForCurrentAnchor:draggingVelocity:恢复的算法:
throw = velocity * 0.55 t = min(|throw| / 5000, 0.45) target = currentAnchor + unit(throw) * t score(a)= |a.point − target| − |throw| · max(0, dot(unit(throw), unit(a.point − current))) pick min score另有独立阈值hypot(velocity) >= 120决定其他宿主上的锚点是否进入候选集。
0.55 是"窗口飞向你扔的方向"与"窗口飞向你指的方向"之间的差别——只有前者有"重量感"。点积项则让一次有意的跨窗抛出落在瞄准的位置;单靠距离只会选到你正在离开的那个角。
Maka 移植在 pip-motion.ts 的pickPipAnchor:PIP_THROW_SCALE = 0.55、PIP_THROW_REFERENCE = 5000、PIP_THROW_MAX = 0.45。值得注意的差异化处理:Codex 的hypot(velocity) >= 120阈值被刻意不移植——它用于门控"其他显示器上的锚点"是否候选,而 Maka 的四个锚点是同一宿主窗口的四角,没有跨屏候选可门控;慢速释放本身已读作"放置"而非"抛出",其覆盖距离趋近于零,最近角凭距离即可胜出(源码注释还记录了一个讽刺的细节:该阈值曾移植过一次,没有任何读者,且断言把字面量和自己比较)。
尺寸:默认 200pt,钳制 [100, 400],内边距 24pt
默认最长边 200pt,钳制到 [100, 400],通过缩放到较短边保持宽高比;锚点内边距 24pt。三个边界就是二进制中的字面量双精度浮点0x4069…、0x4059…、0x4079…。Maka 以PIP_MIN_EDGE/PIP_DEFAULT_EDGE/PIP_MAX_EDGE/PIP_MARGIN定义在 pip-motion.ts,尺寸计算pipDisplaySize在 pip-electron.ts(按长边等比缩放,非有限值回退默认)。
缩放(Resize):四条指令的垂直手势
-[PIPStackResizeInteraction maxDisplaySizeForPointerScreenPoint:]只有四条指令:
sign = (alignment & ~1) == 2 ? +1 : -1 size = initialMaxDisplaySize + (pointer.y - initialPointer.y) * sign只有垂直分量驱动缩放,符号由镜像所靠的角决定,因此手势在任何角落读起来都一样:远离锚点即放大。交互保持开始时的边与指针高度,不做任何增量累加——抖动指针无法累积漂移。Maka 的pipResizeEdge(pip-motion.ts)等价实现,并补上了 CodexsnapPointToBackingScale:同样的整数化理由:透明窗口上的分数像素边缘会闪烁。
控件:三个标识符减为一个
performControlWithIdentifier:接受stop、hide、close;setHoveredControlIdentifier:与_pressedControlIdentifier驱动外观。
实测基线:写任何代码之前在 Electron 43 上测量
| 结果 | |
|---|---|
documentPictureInPicture | undefined,加不加--enable-features=DocumentPictureInPictureAPI都是 |
| 子窗口 + 父窗口移动 | 子窗口自动跟随,零代码,同一事务 |
| 子窗口 + 父窗口缩放 | 子窗口不动——锚点需要重算 |
子窗口isAlwaysOnTop() | false |
| 子窗口焦点 | 从不抢焦点 |
| 向点击穿透窗口注入鼠标事件 | 可送达但被合并且丢包——5 个中 2 个,随后 5 个中 1 个 |
这些测量直接定义了 pip-window.ts 的设计注释:alwaysOnTop默认floating级别会把镜像放到所有应用之上;在每次move时重定位则让第二个事务比拖动落后一帧,产生可见拖尾——子窗口排序解决前者,子窗口定位解决后者。
关于 Document Picture-in-Picture:它不仅是在 Electron 中被禁用,其实现位于 Chromium 的//chrome浏览器层(PictureInPictureWindowManager),而 Electron 根本没有这一层。这条路是关闭的,而不是变窄的。
Maka 复制了什么
- 应用窗口的子窗口,普通层级。镜像最初的两个缺陷——浮在无关应用之上、拖动时落后一帧——同源于一个根因:镜像自己定位而不是被父窗口携带。
hasShadow: false。pip.html自绘阴影;原生阴影会由 window-server 根据内容 alpha 在每一帧落地时重新计算——而这正是那个每帧都收到新帧的窗口。- 200pt 默认边、[100, 400] 钳制、24pt 内边距。
- 弹簧常量、抛出投影与锚点评分,全部实现在 pip-motion.ts,每个常量旁边都引用了反汇编。
- hover 由主进程侧指针决定,因为这正是 Codex 的 global/local
NSEventmonitor 所做之事——它从不询问窗口"指针是否在内部"。Electron 没有全局鼠标监视器,Maka 用 20Hz 轮询(scheduleHoverCheck)替代,见 pip-window.ts 与watchHover。 - 只对 resize 做出反应。move 是 window-server 的职责。
五处"Maka 刻意不同"及其理由
两个控件而非三个
Codex 的close关掉一块 tile、hide关掉整个栈。Maka 一次只镜像一个窗口,两者是同一手势,只有一个配得上按钮。PipControlId = 'stop' | 'hide'见 pip-window.ts。
指到才响应点击(click-through until pointed at)
Codex 的 tile 总是吃掉点击——acceptsFirstMouse:返回 YES,hitTest:声明整个视图。但 Codex 的 tile 是可选功能(设置文案为 "Show backgrounded apps that Computer Use is working on in Picture-in-Picture mode");Maka 的镜像在每次 run 开始时就会出现,必须做到不打扰才值得存在。因此用setIgnoreMouseEvents(true, { forward: true })——鼠标移动仍然送达,主进程可以判断何时收回点击权。setPointerInside逻辑见 pip-window.ts。
没有堆叠
Codex 用 lead item + followers 镜像多块 tile;follower 弹簧列及其索引衰减在单镜像场景无的放矢。
抓手(grip)随控件一起出现
Codex 的缩放把手属于同一套 hover 镀铬;静止时 tile 不携带任何常驻把手。Maka 同理——200pt 的小窗口承担不起常驻交互元素。
帧序列(flipbook)而非视频流
Codex 通过 ScreenCaptureKit 跨 XPC 流式传输。Maka 完全不需要:每个改变状态的 Computer Use 动作已经返回目标窗口的截图(在 settle 等待之后捕获,因此展示的是稳定后的结果),动作完成时帧已在手,镜像只是 post-action 帧的翻页簿,零额外采集成本。这条通路在 pip-feed.ts 中实现——withComputerUsePip包装 cursor 的onActionEnd钩子,消费每次动作返回的截图(含语义动作,光标自身的onActionEnd反而因无结束坐标而跳过它们),并把光标坐标从屏幕坐标经窗口缩放换算进截图像素。代价是真实且刻意的:动作之间镜像不更新——足够看到 Agent 在做什么,而这正是它的目的。
没有跨宿主移动
moveStackToHostID:在宿主窗口之间移动栈——Codex 注册了多个宿主,包括avatar-overlay。Maka 只有一个窗口,无可移动之物。跨显示器天然成立:因为镜像是应用窗口的子窗口,父窗口去哪它去哪。
验证:真机 smoke 与可精确测试的物理层
真窗口集成测试
scripts/pip-interaction-smoke.mjs 运行真实链路:真实父窗口、真实子面板、真实 preload 与 renderer、真实输入事件。它断言:
- 子窗口属性(不置顶
isAlwaysOnTop() === false、不抢焦点、getParentWindow() === parent); - 座席位置(应用窗口右下角、内缩 24pt);
- 随父窗口移动被携带(移动 160px 后断言跟随);
- hover 出控件(主进程侧指针移动触发
controls可见); - 一次抛出落在瞄准的锚点上(左上角),且控制器
currentAlignment()一致; - 应用窗口 resize 后回到用户选中的角而非默认角;
- grip 拖拽放大(实测 200x160 → 290x232,宽高比保持、仍在角上、在钳制范围内),且 grip 位于锚点对角——这个断言修复了一个真实 bug:grip 曾钉死在 CSS 的左上角,只有默认的右下锚点对齐,把镜像扔到左上角后指针在拖拽第一个像素就脱离抓手;
- hide 控件收起镜像(
pip.isVisible() === false)。
它不需要辅助功能权限、不需要解锁屏幕——只驱动 Electron 窗口——因此在其他真机检查无法运行时,它是唯一可持续的真机检查。文件头还记录了运行前置:npm --workspace @maka/desktop run build:main && npm --workspace @maka/desktop run build:overlay && npx electron scripts/pip-interaction-smoke.mjs。
脚本中值得学习的两处工程细节:settled()轮询要求连续 6 个相同采样才算静止(过阻尼弹簧的收尾段每帧移动不足 1 点,两个连续读取可能因取整相同而误判到达);拖拽夹具用三个递进采样点保证释放时被读作"抛出"而非"放置"。
物理层单元测试的现状
物理与锚点评分原本由apps/desktop/src/main/__tests__/computer-use-pip-motion.test.ts覆盖(无需桌面环境);PR #2478 删除了该测试文件,只结束了它提供的覆盖——没有替代品。但被测模块本身未受影响:pip-motion.ts在 #3293 光标替换(#3456 实现,重建了 Agent 光标引擎与字形——包括 PiP 字形)前后字节级一致,上文转录的拖动/落位弹簧与抛出系数仍在那里定义,仍驱动着pip-window.ts。文档同时划清了边界:光标溯源记录在 computer-use-cursor-provenance.md,只涉及 Agent 光标;PiP 窗口的运动模型是独立表面,没有需要记录溯源变更。
方法论总结:可复用的逆向-移植流程
- 先分进程,再下结论:
RemoteHosted前缀 = 另一半在另一个进程;找不到符号 ≠ 功能不存在。 - 事实分层:confirmed(反汇编逐条读出)/ measured(真机行为实验)/ inference(由常量推导的动机),三层永不混淆。
- 常量带出处移植:每个数字在源码注释中附上反汇编来源,后续维护者可审计。
- 用真实窗口冒烟测试锁定行为:单元测试证明物理与状态机,smoke 测试证明 Electron 集成,二者互补。
- 差异化而非照搬:Codex 的多 tile 栈、无条件点击、视频流等设计在 Maka 单镜像场景下各有明确的不适用理由——每处差异都写明了"为什么"。
这套流程沉淀在 pip-motion.ts(纯逻辑、可精确测试、零 Electron 依赖)、pip-window.ts(控制器与状态机)、pip-electron.ts(Electron 边界)、pip-feed.ts(帧来源)四个文件与 pip-interaction-smoke.mjs 一个真机验证脚本中,构成 Maka Computer Use 镜像的完整证据链。
【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考