数据截至 (上游 commit ead48da2032c)
动作落地引擎:AX 优先的点击级联与输入合成(最深一章)
30 秒导读: 模型看完一屏 UI,吐出一句"点第 12 号元素"或"点 (340, 512)"。本章讲的就是这句话怎么变成一次真实点击——而且尽量不去动物理鼠标:先用系统无障碍(Accessibility,下称 AX)接口直接对控件发"按下/确认/打开"这种语义动作,只有当所有语义路径都失败,才退回去合成真的指针事件。这是整个项目工程含量最高的一章。
本章是 Open Codex Computer Use 系列的第 3 章。前置背景:
01-state-snapshot.md 讲每回合怎么把 UI 拍成"无障碍树 + 截图"的快照,本章的输入(ElementRecord、AppSnapshot)就来自那里;
02-mcp-tools-surface.md 讲对外的 9 个工具,本章是 click/type_text/scroll/drag/press_key/set_value 这几个工具的内部实现。
1. 这是什么(零基础也能懂)
一句话定义
动作落地(action grounding) = 把模型的抽象意图("点这个按钮")翻译成一次能真正触发 UI 反应的操作。
为什么这事很难
难点从来不是"调用模型",而是**"把模型说的那段话,精确、可靠地落到某个真实控件上"**。
一个按钮在屏幕上,你看到的是像素;可程序要触发它,有两条完全不同的路子:
- 物理路子: 把鼠标移到那个像素,合成"按下—抬起"事件。简单,但侵入——它真的会抢走你的鼠标、可能点错、后台窗口点不到。
- 语义路子: 绕过鼠标,直接问系统"这个控件叫什么、能干什么",然后对它发一个"按下(AXPress)"的抽象命令。控件自己去响应,鼠标一动不动。
本项目的第一性原理:非侵入优先
先用语义化 AX 动作,物理指针只作兜底。
这句话是本章所有代码的总纲。它带来的直接好处:
| 好处 | 为什么 |
|---|---|
| 后台也能操作 | AX 动作不要求窗口在最前、不抢鼠标 |
| 更准 | 直接命中控件对象,不受坐标换算/缩放误差影响 |
| 更安全 | 默认根本不发全局物理事件,除非你显式打开开关 |
用起来什么样
对外就是一次工具调用(见 02),两种点法二选一:
# 按快照里的元素编号点(首选,最准)
click(app="Notes", element_index="12")
# 按截图像素坐标点(模型只会"看图指点"时的退路)
click(app="Notes", x=340, y=512)
这两种入参,进到 ComputerUseService.click 后走的是两条不同的落地路径——下一节讲。
2. 顶层全景(一次点击怎么转)
两条入口路径
click 一进来先校验 click_method(ComputerUseService.swift:440-452,新增 sky_click 等方法枚举 ClickMethod,:11),再按参数分叉(ComputerUseService.swift:436-620,ComputerUseService.click):
| 入参 | 走哪条 | 拿谁去点 | 关键区别 |
|---|---|---|---|
element_index | 编号路径 | 快照里那条 ElementRecord | 允许"就近命中测试"和"激活兜底" |
x / y | 坐标路径 | 坐标处命中的候选元素列表 | 不允许就近扩散、不允许激活兜底 |
两条路径最后都调用同一个核心 performAXClickSequence,只是把 includeNearbyHitTesting / allowActivationFallback 两个开关设得不一样(编号路径两个都为 true,坐标路径都为 false)。为什么坐标路径更保守?因为坐标本身就模糊——再往四周扩散命中,极易点错;编号路径 已经锁定了具体控件,可以更激进地找它的可点子孙。
一次点击的分层(从上到下)
click(工具入口)
│ 按 element_index / x,y 分叉
▼
performAXClickSequence ← 语义层核心:多级 AX 动作级联
│ 逐级尝试:选中列表项 → Press → Confirm → Open
│ → 扫子孙 → 就近命中 → 含它的 web 行 → 激活
│ 任一级成功即返回 true,整个链条命中即停
▼ (全部失败,返回 false)
performNonAXClickFallback ← 物理层兜底:合成指针事件
│ 默认:clickTargeted(postToPid 定向,只发给目标进程)
│ 开关开:clickGlobally(cghidEventTap 全局,真抢鼠标)
▼
InputSimulation ← 真正造 CGEvent 的地方
怎么读这张图: 从上往下是"越来越暴力"的降级顺序。上面是"跟控件对象讲道理"(语义),下面是"直接合成硬件事件"(物理)。命中即停,绝大多数点击在语义层就解决了,根本走不到物理层。
部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
ComputerUseService.click | 工具入口、click_method 校验、路径分叉、驱动视觉光标 | ComputerUseService.swift:436 |
performAXClickSequence | AX 语义点击的多级 fallback 级联 | ComputerUseService.swift:957 |
performPreferredClick | 对单个控件按 press/confirm/open 顺序试 | ComputerUseService.swift:851 |
performNonAXClickFallback | 语义全失败后合成物理指针事件 | ComputerUseService.swift:1795 |
InputSimulation | 底层造 CGEvent:定向 vs 全局 | InputSimulation.swift:45 |
KeyPressParser | 把 "cmd+shift+a" 解析成键码+修饰键 | KeyMapping.swift:16 |
3. 核心机制一:AX 点击级联(本章的心脏)
3.1 它要解决的小问题
模型说"点第 12 号元素",但第 12 号可能是一段纯文本(没有任何可点动作)、可能是个外壳容器(真正能点的是它的子节点)、也可能它自己就是个规规矩矩的按钮。你不能只对它硬发一个 AXPress 就完事——很多时候会失败。于是要有一条降级级联:从"最像它本身"一路退到"它周围/它的容器/纯激活"。
3.2 单控件怎么点:performPreferredClick
级联的每一格,最终都落到 performPreferredClick(ComputerUseService.swift:851)——对一个控件,按固定优先级试几种语义动作:
// ComputerUseService.swift:705-722(节选,真实源码)
if clickCount <= 1,
!hasAncestorRole("AXWebArea", of: element),
try selectContainingListItem(for: element) { return true } // ① 先试"选中所在列表项"
if try performAction(named: kAXPressAction ...) { return true } // ② AXPress
if try performAction(named: kAXConfirmAction ...) { return true }// ③ AXConfirm
if try performAction(named: "AXOpen" ...) { return true } // ④ AXOpen
四步的意图:
| 步 | 动作 | 什么时候有用 |
|---|---|---|
| ① | selectContainingListItem | 侧边栏/列表:选中某一行(设 AXSelectedChildren),而不是"按"它。跳过 web 区域和多击 |
| ② | AXPress | 最标准的"按下",按钮/链接绝大多数吃这个 |
| ③ | AXConfirm | 有些控件用"确认"语义(如某些输入框回车行为) |
| ④ | AXOpen | 文件/项目"打开"语义;还特判了 attributeUnsupported 也算成功(:904) |
右键走的是另一支:只试 AXShowMenu(:725);中键无 AX 动作可发,直接返回 false 落到物理层。
3.3 五级降级级联:整条链
performAXClickSequence(:805)把上面的"点单个控件"包成一条多级 fallback 流水。这是全章最该看懂的一张图:
performAXClickSequence(on: record)
│
├─0 含 web 行特判? shouldPreferContainingWebRowAXClick == true
│ └─ 是 → performContainingWebRowClick ── 命中即停 ✓ (Electron/Lark 专用,见 3.5)
│
│ (以下是常规链,仅当没走 web 行特判)
├─1 首选目标 performPreferredClick(on: record) ── 命中即停 ✓
│
├─2 扫它的子孙 descendantClickCandidates(for: record) ── 逐个 performPreferredClick,命中即停 ✓
│ (深度≤3,按 clickPriority 排序,过滤掉"疑似副动作")
│
├─3 就近命中测试 (仅 includeNearbyHitTesting=true,即编号路径)
│ 对 record 里 的几个点做 hitTest / bestElement:
│ 3a 命中的元素本身 performPreferredClick(hitRecord) ── 命中即停 ✓
│ 3b 命中元素的子孙 descendantClickCandidates(hitRecord) ── 命中即停 ✓
│ (先过 shouldScanDescendantsOfHitRecord 防止命中巨块)
│
└─4 激活兜底 (仅 allowActivationFallback=true 且是窗口角色 AXWindow 且非合成文本)
activateClickTarget: AXRaise / 设 AXMain / 设 AXFocused ── 命中即停 ✓
全部失败 → 返回 false → 上层落到 performNonAXClickFallback(物理层)
怎么读这张图: 从上到下是降级顺序,任一级 performPreferredClick 返回 true 就整个 return true、后面都不跑。层级越靠下,越是"死马当活马医"——第 4 级的"激活"甚至不点任何东西,只是把窗口举到前面/设为主窗口,算作"至少让它响应了"。
各级真实锚点:
| 级 | 符号 | 行 |
|---|---|---|
| 0 | performContainingWebRowClick | ComputerUseService.swift:968-973 |
| 1 | performPreferredClick(on: record) | :824 |
| 2 | descendantClickCandidates | :830 |
| 3 | hitTestElement / bestElement 循环 | :838-868 |
| 4 | activateClickTarget | :882 |
3.4 子孙候选怎么排:clickPriority
第 2/3 级要"扫子孙找一个能点的",子孙可能有好几个,谁先试?靠 clickPriority(:997)打分,分低者优先:
| 优先级 | 判据 | 含义 |
|---|---|---|
| 0 | 有 AXPress/AXConfirm/AXShowMenu/AXRaise 动作 | 明确可点,最优先 |
| 1 | AXMain 或 AXFocused 可设 | 至少能聚焦/激活 |
| 2 | 其它 | 兜底 |
同优先级再按面积从小到大排(frameArea,:1016)——小控件通常是更精确的目标(一个具体按钮 vs 一大块容器)。这条"面积小者优先"在 bestElement(:962)选坐标命中元素时也用同一套逻辑。
3.5 Electron / Lark(飞书)特判:含它的 web 行
问题背景: 在 Electron 套壳的 App(尤其飞书 Lark)里,一行列表项在无障碍树里常常是"一段 AXStaticText 文字",文字本身没有 press 动作;真正可点的是包着这段文字的那一整行(某个祖先容器)。直接点文字点不动。
判定入口 shouldPreferContainingWebRowAXClick(:1107)→ 自由函数 shouldPreferContainingWebRowAXClickCandidate(:327),同时满足才开这条特判:
- 有
AXWebArea祖先(hasWebAreaAncestor); - App 命中
isElectronScopedWebRowClickOptimizationTarget(:307)——bundle id 含electron/lark/feishu,或应用名是lark/feishu/飞书; - 角色是
AXStaticText/AXGroup,或是合成文本。
命中后 performContainingWebRowClick(:1123)从该文本向上找最多 6 层祖先,对第一个满足 isLikelyContainingRowActionFrame(:257,即"几乎包住目标中心、宽高≥目标、又没高到离谱"、且自身有 press 动作)的祖先发 AXPress。这就把"点这段字"翻译成了"按这一行"。
3.6 抑制误点:isLikelySyntheticSideAction
问题背景: 扫子孙时,一行的右侧常有个小按钮——"完成"、"archive"、"标记完成"。模型本意是点整行,若级联手 滑点到这个右侧小按钮,就会误触"归档/完成"这种破坏性副动作。
isLikelySyntheticSideAction(:1089)→ 自由函数 isLikelySyntheticSideActionCandidate(:191)把这类候选排除掉。判据两条,满足其一即视为"副动作"并跳过:
- 标签命中:标签是/含
完成、done、complete、archive、mark ... done/complete(:203-215),且父有主动作、且候选够"小巧"(compact); - 位置命中:候选落在父容器右侧带内(trailing band,宽度
父宽*0.22夹在 56~140 之间)、又 compact、且有动作或副动作标签(:224-234)。
一句话:"靠右 + 小 + 长得像'完成/归档'" = 大概率是副动作,别点它。
3.7 就近扩散的刹车:shouldScanDescendantsOfHitRecord
第 3 级做就近命中时,hitTest 可能命中一个巨大的容器(比如整个列表、整个窗口)。若还去扫它的子孙,就等于满屏乱点。shouldScanDescendantsOfHitRecord(:237)是刹车:命中块面积 > 原目标 12 倍(或 > 20000)、或高宽都远超原目标,就不扫子孙,避免扩散到无关区域。
4. 核心机制二:文本输入(typeText / setValue)
4.1 typeText:先设焦点值,再退回键盘
同样是"非侵入优先"。typeText(ComputerUseService.swift:714)的两级:
typeText(text)
├─1 typeTextBySettingFocusedValueIfAvailable ← 首选:直接把值写进焦点控件
│ 焦点元素的 AXValue 可设(isSettable)? → 设 baseValue+text,成功即停 ✓
│
└─2 (不可设值时) canTypeTextUsingKeyboardFallback?
能 → InputSimulation.typeText(合成 Unicode 键盘事件,定向 postToPid)
不能 → 报错:请先点一个可编辑文 本区
第 1 级(:1214 typeTextBySettingFocusedValueIfAvailable)是精髓:如果焦点控件的 AXValue 可设,就直接 AXUIElementSetAttributeValue 把 已有值 + 新文本 写进去——一个键盘事件都不发。
这里有个易忽略的坑:直接设值会覆盖原值,所以要先算出"已有的真实值" editableBaseValue(:1264),还要剔除占位符(placeholder)——否则会把灰色提示文字也当成真内容拼进去。项目甚至硬编码了一个飞书占位符 沟通时请保持"公开可接受"(looksLikeEditablePlaceholder,:1316)做特判。
第 2 级键盘兜底的门槛 canUseKeyboardTextFallback(:285):值可设、或角色是 AXTextField/AXTextArea/AXTextView、或角色描述含 "text field/area/entry"——否则拒绝,免得往一个根本不能打字的地方乱敲。
4.2 setValue:先校验 settable
setValue(:605)更直接——它是"我明确知道要设哪个元素的值"。关键是动手前先校验:isSettableForSetValue(:952)调 AXUIElementIsAttributeSettable 查 AXValue 是否可设,不可设直接抛 Cannot set a value for an element that is not settable(nonSettableSetValueErrorMessage,:159)。校验通过才 AXUIElementSetAttributeValue。全程无物理事件。
4.3 scroll:AXScroll…ByPage 优先
scroll(:510)也贯彻语义优先。当滚动量是整数页且目标控件恰好支持 AXScrollUpByPage / AXScrollDownByPage(等)时,就直接对控件发这个语义动作、循环 N 次(:531-537)——不合成滚轮。只有拿不到整数页动作时,才落到 performScrollEvent(:1601)去发真的滚轮事件。
5. 核心机制三:物理层——定向 vs 全局
语义层全失败,才轮到"真的合成硬件事件"。这里 InputSimulation 有两套并行实现,区别只在事件发给谁。
5.1 两种投递:postToPid vs cghidEventTap
| 维度 | 定向(targeted) | 全局(global) |
|---|---|---|
| 投递 API | event.postToPid(pid) | event.post(tap: .cghidEventTap) |
| 事件源 | .combinedSessionState | .hidSystemState |
| 谁收到 | 只有目标进程 | 整个系统(真抢你的鼠标) |
| 侵入性 | 低——后台也能点,不动真鼠标 | 高——等同你自己动手 |
| 代表函数 | clickTargeted/scrollTargeted/dragTargeted | clickGlobally/scrollGlobally/dragGlobally |
postToPid 把 CGEvent 只塞给某个进程的事件队列,系统层面的鼠标指针纹丝不动——这是"非侵入"能在物理层依然成立的关键。真实锚点:clickTargeted(InputSimulation.swift:70)用 postMouseEventToPid(:234,内部 event.postToPid(pid));clickGlobally(:58)用 postMouseEvent(:224,内部 event.post(tap: .cghidEventTap))。
5.2 开关:OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS
走定向还是全局,由一个环境变量开关决定 —— globalPointerFallbacksEnabled(ComputerUseService.swift:177,取值 1/true/yes/on 才算开)。逻辑(以 performNonAXClickFallback,:1647 为例):
performNonAXClickFallback(point)
├─ 开关开? ──是──▶ prepareAppForGlobalPointerInput(先把窗口举到前台)
│ → clickGlobally(全局 HID 事件)
│
└─是否(默认) ─▶ try clickTargeted(定向 postToPid)
└─ 抛错 且 开关仍关 ─▶ 报错并提示:
"set OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS=1"
默认关意味着:开箱即用只会走"定向 postToPid",连兜底都不抢你鼠标;真需要"像人一样物理操作"才手动打开开关(:1656、:1676)。scroll/drag 同款开关判断在 performScrollEvent(:1610)、performDragEvent(:1633)。另有一个纯日志开关 OPEN_COMPUTER_USE_DEBUG_INPUT_FALLBACKS(inputFallbackDebugEnabled,:101),打开后每次走物理兜底都往 stderr 打一行,方便排查"为什么这次没走语义"。
5.3 全局兜底前的准备:prepareAppForGlobalPointerInput
真 要发全局事件,得先保证目标窗口在最前(否则事件打到别的 App)。prepareAppForGlobalPointerInput(InputSimulation.swift:48)先试用 AX 举窗口(raiseAppWindowViaAccessibility:AXRaise → 设 AXMain → 设 AXFocused,:244),失败才退回 NSRunningApplication.activate。—— 连"把窗口举起来"这种小事都优先走 AX,可见"非侵入"是贯穿到底的信条。
6. 核心机制四:键盘合成(typeText / pressKey)
6.1 typeText:Unicode 分块直发
键盘兜底不走"字符→键码"映射(那对中文/emoji 根本没法映),而是直接把 Unicode 塞进键盘事件。InputSimulation.typeText(:142)对每个 chunk 造一对 keyDown/keyUp,用 keyboardSetUnicodeString 把该段 Unicode 挂上去,再 postToPid。
为什么要分块?keyboardUnicodeChunks(:164)把文本按 ≤64 个 UTF-16 单元切段(maxKeyboardUnicodeChunkLength,:46),且不切断单个字符(先把一个 Character 的 UTF-16 单元凑齐再判断是否超限,:170-178)——避免超长字符串一次性灌进单个事件出问题,也避免把一个 emoji 的代理对(surrogate pair) 劈成两半。
6.2 pressKey:KeyPressParser 解析组合键
pressKey(:187)处理的是 "cmd+shift+a" 这种组合键。KeyPressParser.parse(KeyMapping.swift:16)按 + 拆词、normalize(小写去空格,:54)后:最后一个词查 keyCodeMap(:61,一张涵盖字母/数字/功能键/方向键/小键盘的大表)当主键,前面的词经 parseModifier(:39)映射成 cmd/shift/option/control(带各自的 CGEventFlags 和键码)。
发送顺序很讲究(:191-219):按下所有修饰键 → 主键 down/up → 逆序抬起修饰键,且每个事件都带上当前累积的 activeFlags。这就还原了人手"按住 cmd+shift 再敲 a 再松手"的真实时序。这些键盘事件全程 postToPid(定向),不受 5.2 的全局开关影响。
7. 巧妙之处(可带走的技术)
-
"讲道理优先于动手"贯彻到每一层。 点击、输入、滚动、甚至"把窗口举到前面",一律先试 AX 语义动作,物理事件是最后手段——而且默认连兜底都用定向
postToPid不抢鼠标。performAXClickSequence:805/typeTextBySettingFocusedValueIfAvailable:1214/raiseAppWindowViaAccessibility:244。 -
点击级联是"由准到糙"的漏斗。 首选目标 → 子孙 → 就近命中 → 含它的行 → 纯激活,命中即停(
:805)。把"模型指的那个不一定能直接点"的现实,用层层退让消化掉。 -
两个"副作用刹车"很实用。
isLikelySyntheticSideAction(:191)防误触"完成/归档"这类破坏性小按钮;shouldScanDescendantsOfHitRecord(:237)防命中巨块后满屏乱扫。都是被真实 App 坑过才会写的启发式。 -
针对真实生态的特判不回避。 直接把 Electron / Lark(飞书)的"点文字→按整行"写进代码(
:307、:327、:1123),甚至硬编码飞书占位符(:1318)。诚实地承认"通用逻辑搞不定这些壳",专门开小灶。 -
Unicode 分块 + 不切字符(
:164)是打字兜底能正确处理中文/emoji 的关键细节。 -
一个环境变量守住安全边界。
OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS(:112)让"会抢鼠标的全局事件"默认关闭、需显式启用——安全默认(secure by default)。
8. 边界与局限(诚实)
-
强依赖 App 的无障碍实现质量。 控件不暴露 AX 动作、frame 不准,语义级联就会一路降级到物理兜底;而物理兜底默认还是关的, 可能直接报错。
-
默认关全局兜底 = 有些点击开箱点不动。 若某 App 既没有可用 AX 动作、又需要真实指针,默认配置下
performNonAXClickFallback会抛错并提示你去开环境变量(:1676),而不是"悄悄帮你点"。 -
副动作启发式是"经验值",可能误伤。
isLikelySyntheticSideAction的标签清单(完成/done/archive/...)和位置阈值(右侧带宽、compact 判定)是硬编码的,遇到布局古怪的界面可能误判——该点的没点,或该躲的没躲。 -
占位符识别很局部。
looksLikeEditablePlaceholder只硬编码了一个飞书中文占位符(:1318),其它 App 的占位符靠通用AXPlaceholder属性比对(:1280),覆盖不全时打字可能把提示文字一起拼进去。 -
坐标路径天然更弱。 走
x/y时关掉了就近扩散和激活兜底(:446-453),命中不到就直接落物理层,精度不如element_index。
9. 横向对比(同货架)
同为"把模型意图落到真实目标",本项目的取舍在 computer-use 一支里颇具代表性:
-
落点是整台电脑的原生 GUI,而非浏览器 DOM。 浏览器类 agent 有 DOM 选择器可依,能精确定位;本项目只有 macOS 无障碍树 + 截图,所以才需要这么一大套"AX 级联 + 启发式"来补偿定位的不确定性。
-
"非侵入"是它区别于经 典 computer-use 的最大特色。 很多 computer-use 实现直接合成全局指针事件(抢鼠标、要求前台);本项目把物理事件降为默认关闭的最后兜底,主路径是
postToPid定向 + AX 语义动作,因此能后台操作、不打扰用户。
配合本章阅读:01-state-snapshot.md(动作的输入从哪来)、02-mcp-tools-surface.md(动作的对外契约)、04-software-cursor.md(既然不抢真鼠标,靠软件光标让后台操作"看得见")。
10. 代码地图(导航索引)
| 主题 | 文件 | 符号 |
|---|---|---|
| 点击入口·路径分叉 | packages/OpenComputerUseKit/Sources/OpenComputerUseKit/ComputerUseService.swift:436 | ComputerUseService.click |
| AX 点击五级级联 | ComputerUseService.swift:957 | performAXClickSequence |
| 单控件语义点击 | ComputerUseService.swift:851 | performPreferredClick |
| 选中所在列表项 | ComputerUseService.swift:913 | selectContainingListItem |
| 子孙候选枚举+排序 | ComputerUseService.swift:1192 | descendantClickCandidates |
| 候选优先 级打分 | ComputerUseService.swift:1149 | clickPriority |
| 副动作抑制 | ComputerUseService.swift:256 / :1089 | isLikelySyntheticSideActionCandidate |
| 就近扫描刹车 | ComputerUseService.swift:302 | shouldScanDescendantsOfHitRecord |
| Electron/Lark 特判 | ComputerUseService.swift:372 / :327 | isElectronScopedWebRowClickOptimizationTarget / shouldPreferContainingWebRowAXClickCandidate |
| 含它的 web 行点击 | ComputerUseService.swift:1275 | performContainingWebRowClick |
| 激活兜底 | ComputerUseService.swift:1068 | activateClickTarget |
| 物理点击兜底 | ComputerUseService.swift:1795 | performNonAXClickFallback |
| 打字·设焦点值优先 | ComputerUseService.swift:1366 | typeTextBySettingFocusedValueIfAvailable |
| 打字·键盘兜底门槛 | ComputerUseService.swift:350 | canUseKeyboardTextFallback |
| setValue·settable 校验 | ComputerUseService.swift:1104 | isSettableForSetValue |
| scroll·ByPage 优先 | ComputerUseService.swift:673 | ComputerUseService.scroll |
| 全局兜底开关 | ComputerUseService.swift:177 | globalPointerFallbacksEnabled |
| 定向 vs 全局点击 | InputSimulation.swift:70 / :58 | clickTargeted / clickGlobally |
| 定向事件投递 | InputSimulation.swift:254 | postMouseEventToPid(postToPid) |
| 全局事件投递 | InputSimulation.swift:244 | postMouseEvent(cghidEventTap) |
| 打字·Unicode 分块 | InputSimulation.swift:184 | keyboardUnicodeChunks |
| 全局兜底前举窗口 | InputSimulation.swift:48 | prepareAppForGlobalPointerInput |
| 组合键解析 | KeyMapping.swift:16 | KeyPressParser.parse |