设备抽象与动作落地:从 InputPrimitives 到真实桌面
30 秒导读: 模型看着截图说「点这个按钮」,但它并不知道你这台机器有鼠标还是触摸屏、点击最终由谁执行。本章讲 Midscene 怎么架这座桥:设备只实现一层最小的输入原语(点/敲键/滚),框架就自动把它编译成一份动作清单(actionSpace)喂给规划;computer 设备再把这些动作落到你本机的鼠标键盘上。这是 computer-use 的「落地层」。
本章聚焦两件事:
- 模型能做哪些动作 —— 动作空间(actionSpace)是怎么从设备能力自动长出来的。
- 像素点怎么变成真实点击 —— computer 设备如何经
libnut/ AppleScript 把一个(x, y)落到真实桌面。
对外工具封装(
computer_*MCP 工具)和远程后端(RDP)不在本章,见 05-surfaces-cli-mcp-rdp。模型「看截图定坐标」的过程见 02-vision-grounding,主循环怎么调用这些动作见 01-agent-loop。
1. 这是什么(零基础也能懂)
-
一句话定义: 一套「设备只描述能力、框架负责翻译成模型动作、再由驱动落到真实硬件」的三段式分层。
-
它解决什么问题: 一个 VLM(视觉语言模型)看着屏幕截图,输出的是自然语言意图——「双击那个文件夹图标」。但不同设备(安卓手机、浏览器、整台电脑)能做的动作完全不一样:手机有
swipe/pinch,电脑有右键和悬停,浏览器还能读 DOM。如果每种设备都自己拼一份「我支持哪些动作」的清单给模型,再各写一遍执行代码,既重复又容易漂移。 -
Midscene 的答案: 设备只需实现一个可选方法的接口
InputPrimitives——里面是最原始的动作原语(点一下、敲一个键、滚一段)。框架有一个编译器defineActionsFromInputPrimitives,读到设备实现了哪些原语,就自动生成对应的动作(有tap就生成Tap动作,有swipe才生成Swipe)。生成出来的这份DeviceAction[]就是 actionSpace,连同参数 schema、给模型的描述、示例一起喂给规划。 -
一句话直觉/类比: 把它想成打印机驱动。应用程序(模型)只说「打印这一页」,不关心是激光还是喷墨;每台打印机只要实现标准驱动接口(输入原语),系统就自动把它登记进「可用打印机列表」(actionSpace),真正喷墨落纸的是驱动(
ComputerInputDriver+libnut)。 -
用起来什么样: 你几乎不直接碰本章的代码——它在
aiTap("登录按钮")这类调用底下。但理解它能回答两个常见疑问:「为什么 computer 设备没有Swipe动作?」(因为它没实现touch原语)、「为什么我的点击移到了对的位置却没生效?」(多显示器坐标偏移,或 Windows 权限,见 §3.6/§5)。
本节不出现底层代码。记住一句话:设备报能力,框架编动作,驱动落硬件。
2. 顶层全景(它大概怎么转)
整条链路是一根从上到下的翻译流水线:模型在最上层用自然语言规划,硬件在最底层被真实操作,中间三次翻译把两端对齐。
怎么读下面这张图:从上往下是「运行时数据流」,从下往上是「构建期编译」(actionSpace 是启动时由能力编译出来的)。
模型规划(看截图,输出意图 + 归一化像素坐标)
│ 按动作名 + 参数调用
▼
┌───────── ──── actionSpace(): DeviceAction[] ─────────────┐
│ 默认动作(从原语编译) + 平台动作(ListDisplays) + 自定义动作 │ ← 构建期由下方能力编译而来
└───────────────────────────┬───────────────────────────────┘
│ action.call(param) 触发对应原语
▼
inputPrimitives { pointer / keyboard / scroll } ← 设备只需实现这一层
│ 归一化像素点 → 全局坐标 → 驱动调用
▼
ComputerInputDriver(统一封装,可测试)
│
┌─────────────┼──────────────────┐
▼ ▼ ▼
libnut AppleScript phased-scroll 原生助手
(moveMouse/ (osascript, (macOS 手势相位滚动)
mouseClick/ 键盘更可靠)
keyTap/
scrollMouse)
└─────────────┴──────────────────┘
▼
真实桌面(鼠标 / 键盘 / 滚轮)
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
InputPrimitives | 设备能力的接口定义:pointer/keyboard/touch/scroll/system 五组可选原语 | packages/core/src/device/index.ts:93-133 |
AbstractInterface | 所有设备的抽象基类,规定 screenshotBase64/size/actionSpace 三大必选 + inputPrimitives 可选 | packages/core/src/device/index.ts:135-219 |
defineActionsFromInputPrimitives | 编译器:读原语 → 产出 DeviceAction[] | packages/core/src/device/index.ts:989-1061 |
ComputerDevice | 电脑设备实现:实现 inputPrimitives,拼出 actionSpace | packages/computer/src/device.ts:696 |
ComputerInputDriver | 把原语调用统一封装成对 libnut / AppleScript 的调用,可注入可测试 | packages/computer/src/input-driver.ts:40 |
libnut | 原生绑定:真正 moveMouse/mouseClick/keyTap/scrollMouse | @computer-use/libnut(见 device.ts:293 getLibnut) |
主线走一遍(高层): 模型说「Tap 那个按钮」并给出归一化像素坐标 → 框架在 actionSpace 里找到名为 Tap 的 DeviceAction,校验参数,调它的 call → call 触发 inputPrimitives.pointer.tap({x,y}) → tap 把点位换成多显示器全局坐标、拟人地移动鼠标、按下/抬起 → ComputerInputDriver 调 libnut.moveMouse / mouseToggle → 硬件上鼠标真的点了下去。
3. 核心机制(由浅入深)
3.1 五类输入原语:设备只描述「我能做什么」
它要解决的小问题: 怎样用一套最小接口覆盖手机、浏览器、电脑三种差异巨大的设备?
思路: 按输入通道切成五组,每组内部 方法大多可选(?)。设备实现哪个,就代表它有哪个能力;没实现的,框架后面就不会为它生成动作。
五组原语一览:
| 原语组 | 覆盖的动作 | 关键方法 |
|---|---|---|
pointer | 点 / 双击 / 右键 / 悬停 / 长按 / 拖拽 | tap(必选)、doubleClick?、rightClick?、hover?、longPress?、dragAndDrop? |
keyboard | 输入文本 / 敲键 / 清空 / 移光标 | typeText、keyboardPress、clearInput、cursorMove? |
touch | 滑动 / 双指缩放 | swipe、pinch? |
scroll | 滚动 | scroll |
system | 返回 / Home / 最近任务(移动端系统键) | backButton?、homeButton?、recentAppsButton? |
真实定义(注意 pointer 里只有 tap 无问号,其余都可选):
// packages/core/src/device/index.ts:46-99 PointerInputPrimitives / InputPrimitives
export interface PointerInputPrimitives {
tap(p: PointerPoint, opts?: { duration?: number }): Promise<void>;
doubleClick?(p: PointerPoint): Promise<void>;
rightClick?(p: PointerPoint): Promise<void>;
hover?(p: PointerPoint): Promise<void>;
// ... longPress? / dragAndDrop?
}
export interface InputPrimitives {
pointer?: PointerInputPrimitives;
keyboard?: KeyboardInputPrimitives;
touch?: TouchInputPrimitives;
scroll?: ScrollInputPrimitives;
system?: SystemInputPrimitives;
}
上面这段是能力清单的类型:每个 ? 都是一个「设备可以不实现」的开关。
关键细节:坐标的语义是「设备像素」。 PointerPoint 明确注释为「屏幕上的设备像素坐标」(index.ts:40-44)。也就是说,原语拿到的 (x, y) 已经是这块屏幕内的像素点,不是归一化的 0~1——把归一化坐标还原成像素是更上游(定位层)的事,见 02-vision-grounding。
各设备的「专用画像」: 框架还给每种设备预设了更严格的子接口,把「这个平台必有」的能力从可选提升为必选。例如 computer 设备用 ComputerInputPrimitives——它要求 pointer 必须有 doubleClick/rightClick/hover/dragAndDrop(桌面就该有这些),但没有 longPress、没有 touch、没有 system(index.ts:124-133)。这个「有什么、没什么」直接决定了下一节编译出的动作集。
3.2 编译器:defineActionsFromInputPrimitives 把能力变成动作
它要解决的小问题: 有了能力清单,怎么自动变成一份「模型能读、能调」的动作列表,而不用每个设备手写?
思路: 一个纯函数,顺序检查每一组原语,有则 push 对应动作。每个动作由一个 defineActionXxx 工厂产出,内部封装了三样东西:参数的 zod schema、给模型看的自然语言 description、以及最终调用原语的 call。
真实实现(节选,看它「探测—生成」的骨架):
// packages/core/src/device/index.ts:989-1035 defineActionsFromInputPrimitives
export function defineActionsFromInputPrimitives(input, options = {}) {
const actions = [];
const { pointer, keyboard, scroll, touch, system } = input;
if (pointer) {
actions.push(defineActionTap(pointer.tap)); // tap 必有
if (pointer.doubleClick) actions.push(defineActionDoubleClick(pointer.doubleClick));
if (pointer.rightClick) actions.push(defineActionRightClick(pointer.rightClick));
// ... hover / dragAndDrop / longPress 各自 if 判断
}
if (keyboard) {
actions.push(defineActionInput(keyboard), defineActionClearInput(keyboard.clearInput),
defineActionKeyboardPress(keyboard.keyboardPress),
defineActionCursorMove({ keyboard, sleep: options.sleep }));
}
if (scroll) actions.push(defineActionScroll(scroll.scroll));
if (touch?.swipe && options.size) actions.push(defineActionSwipe(...)); // 还需要 size()
// ...
return actions.filter(Boolean);
}
重点看那些 if:能力驱动动作。touch?.swipe 还额外要求传入 options.size(滑动要按屏幕尺寸归一化终点),这是「有能力还不够、还得有上下文」的例子。
一个动作长什么样?以 Tap 为例——它把一个 locate(定位结果)转成像素点再交给原语:
// packages/core/src/device/index.ts:293-309 defineActionTap
export const defineActionTap = (tap) =>
defineLocatedPointAction({
name: 'Tap',
description: 'Tap the element',
interfaceAlias: 'aiTap', // 对外 API 名
paramSchema: actionTapParamSchema, // { locate } 的 zod 校验
sample: { locate: { prompt: 'the "Submit" button' } }, // 给模型的示例
missingLocateMessage: 'Element not found, cannot tap',
call: async (point) => { await tap(point); }, // 最终落到原语
});
四个字段各有用途:description/sample 是给模型读的(规划时它据此选动作、填 参数),paramSchema 是校验模型输出的,call 是真正执行的。interfaceAlias(如 aiTap)是暴露给用户脚本的方法名。
computer 设备编译出的动作集(具体): 因为 ComputerInputPrimitives 只有 pointer(含 doubleClick/rightClick/hover/dragAndDrop,无 longPress)、keyboard、scroll,编译结果就是这 10 个:
| 动作 | 来自原语 | 对外别名 |
|---|---|---|
| Tap / DoubleClick / RightClick / Hover / DragAndDrop | pointer.* | aiTap / aiDoubleClick / aiRightClick / aiHover / aiDragAndDrop |
| Input / ClearInput / KeyboardPress / CursorMove | keyboard.* | aiInput / aiClearInput / aiKeyboardPress |
| Scroll | scroll.scroll | aiScroll |
注意没有 Swipe / Pinch / LongPress / 系统键——computer 没实现对应原语。这就是「动作空间随设备能力自动裁剪」的直接体现。
一个巧思——CursorMove 的软降级: computer 的 keyboard 没实现 cursorMove,但 defineActionCursorMove 仍会被 push。它内部先看有没有原生 cursorMove,没有就退化成「循环敲方向键」(index.ts:830-846)。于是「移动光标」这个动作对 computer 依然可用,只是用箭头键模拟。
3.3 actionSpace():默认 + 平台 + 自定义
它要解决的小问题: 编译出的默认动作之外,设备还想加几个不来自输入原语的动作(如「列出显示器」),以及用户自定义动作,怎么拼一起?
思路: 三段拼接。看 computer 的实现:
// packages/computer/src/device.ts:1481-1490 ComputerDevice.actionSpace
actionSpace(): DeviceAction<any>[] {
const defaultActions = [...defineActionsFromInputPrimitives(this.inputPrimitives)]; // ① 编译默认动作
const platformActions = Object.values(createPlatformActions()); // ② 平台专属
const customActions = this.options?.customActions || []; // ③ 用户注入
return [...defaultActions, ...platformActions, ...customActions];
}
三段来源:
| 段 | 是什么 | 例子 |
|---|---|---|
| ① 默认动作 | §3.2 从 inputPrimitives 编译 | Tap / Input / Scroll … |
| ② 平台动作 | 不走原语、设备特有的能力 | ListDisplays(枚举显示器,device.ts:1522-1532) |
| ③ 自定义动作 | 构造时 customActions 传入 | 业务方自己的动作 |
ListDisplays 是个好例子:它跟「点/敲/滚」无关,是纯查询——所以不放进原语体系,而作为平台动作直接 defineAction。这就是「原语管交互、平台动作管其余」的分工。
3.4 落地:原语如何经 ComputerInputDriver 打到真实桌面
前面都在「生成动作」,现在看动作被调用后怎么真的动起来。以 pointer.tap 为例,它是本章最核心的一段落地代码。
tap 做的三件事:(device.ts:723-784)
- 换算坐标:
toGlobalPoint({x,y})把「本显示器内的像素」变成「跨显示器全局像素」(见 §3.6),再取整。 - 拟人移动 + 按下抬起:
smoothMoveMouse分步滑过去(而非瞬移),然后pressMouseAtGlobalPoint按住holdDuration毫秒再松开。 - macOS 焦点补偿: 点击前后各读一次「最前台应用」,若焦点变了(点击本身把窗口带到前台),就再点一次——因为第一次往往只是「激活窗口」而没真正命中控件。
// packages/computer/src/device.ts:749-782 tap 的核心(节选)
await this.inputDriver.smoothMoveMouse(targetX, targetY,
SMOOTH_MOVE_STEPS_TAP, SMOOTH_MOVE_DELAY_TAP); // 拟人滑过去
await pressMouseAtGlobalPoint(this.inputDriver, targetX, targetY, holdDuration, 'primary');
if (frontmostBefore && process.platform === 'darwin') {
await sleep(CLICK_FOCUS_SETTLE_DELAY);
const focusChanged = /* 前台 pid 变了? */;
if (focusChanged) { /* moveMouse + 再按一次 'focus-follow-up' */ }
}
其余原语同一套路(都先 toGlobalPoint 再驱动):doubleClick 用 mouseClick('left', true)、rightClick 用 mouseClick('right')、hover 只 smoothMoveMouse 不按键、dragAndDrop 用 withMouseButton('left', ...) 在按住状态下移动(device.ts:785-820)。
键盘为什么不逐字敲? keyboard.typeText 最终走的是 smartTypeString → typeViaClipboard——先写剪贴板,再模拟粘贴(device.ts:1264-1305)。原因写在注释里:逐字 keystroke 会经过当前输入法(IME),中文/日文 IME 会吞字或转码;粘贴则绕开 IME。粘完还会恢复旧剪贴板内容,不污染用户剪贴板。
ComputerInputDriver 的角色 —— 一层可测试的封装: ComputerDevice 不直接调 libnut,而是全部经 this.inputDriver(device.ts:705-711 注入)。这一层做三件事:
- 归一化 libnut 的怪癖: libnut 是原生绑定,分不清「不传参」和「传
undefined」,后者会触发类型报错。所以mouseClick/keyTap里显式判断哪些参数存在再决定怎么调(input-driver.ts:121-133、166-174)。 - 销毁保护: 设备 destroy 后,任何调用抛「已销毁」错误,并把在途的
delay全部 reject(input-driver.ts:203-220、282-289),避免动作打到已释放的设备上。 - 组合原子操作:
withMouseButton保证「按下—执行—松开」即使中途抛错也会在finally里松开鼠标(input-driver.ts:241-251),不会留下「鼠标卡在按下态」的坏状态。
3.5 拟人移动:smoothMoveMouse
它要解决的小问题: 直接 moveMouse 到目标是瞬移,很多应用的悬停/焦点逻辑对瞬移不响应(没有中间的 mouseenter),某些反自动化检测也会盯瞬移。
思路: 把「从当前点到目标点」的直线均分成 N 步,每步移一点、停一小会儿,模拟人手划过。
// packages/computer/src/input-driver.ts:222-239 smoothMoveMouse
async smoothMoveMouse(targetX, targetY, steps, stepDelay) {
const currentPos = this.getMousePos();
for (let i = 1; i <= steps; i++) {
const stepX = Math.round(currentPos.x + ((targetX - currentPos.x) * i) / steps);
const stepY = Math.round(currentPos.y + ((targetY - currentPos.y) * i) / steps);
this.moveMouse(stepX, stepY);
await this.delay(stepDelay); // 每步之间停一下
}
}
tap 用 8 步、hover 用 10 步(SMOOTH_MOVE_STEPS_*,device.ts:70-73)。这是纯线性插值,不是贝塞尔曲线——够骗过大多数悬停逻辑,又不至于慢。
3.6 滚动:原生相位助手 vs libnut 逐 detent(以及 Windows 的坑)
滚动是本章平台差异最大的动作,Midscene 为它准备了三条后端,按优先级降级。
怎么读这条降级链(命中即停):
performScroll
│ ①首选
├──▶ phased-scroll 原生助手(仅 macOS,带手势相位)── 成功 → 结束
│ ②macOS 次选(边界滚/整页滚)
├──▶ AppleScript 敲 Home/End 或 PageUp/PageDown ──── 成功 → 结束
│ ③兜底(Windows / Linux / 无助手的 macOS)
└──▶ libnut.scrollMouse,逐 detent 发、按 tick 节流
① 为什么要一个原生二进制? libnut 的滚动是无相位的 CGScrollEvent,WebKit 和现代 AppKit 滚动视图要求键盘焦点才认;而这个自带的小助手 phased-scroll 会发带手势相位标记的、类触控板事件,不需要焦点就被接受(device.ts:345-355 注释)。它按「像素 + 步数」调用(runPhasedScroll,device.ts:555-602),失败会打一次警告并降级——失败通常是没给 Accessibility 权限,或 npm 解包丢了可执行位(代码会自愈 chmod,device.ts:378-388)。
③ Windows 的 WHEEL_DELTA 坑(经典细节): libnut 的 scrollMouse(x, y) 不是可移植的像素 API,同一个数字三平台含义不同(device.ts:92-108 注释):
| 平台 | scrollMouse(0, y) 里 y 的含义 |
|---|---|
| macOS | CG 像素滚动增量 |
| Linux | XButton4/5 的按放对数(1 = 一个滚轮档) |
| Windows | 直接塞进 MOUSEEVENTF_WHEEL 的 mouseData,单位是 WHEEL_DELTA = 120 / 每档 |
坑在于:在 Windows 上调 scrollMouse(0, 6),mouseData=6 远小于一档(120),会被累积、且经常被 Chromium 的 WheelEventQueue 丢弃——表现为「Electron 应用(如飞书)里滚了个寂寞」。
修法: 永远一次只发一个整档,并在档与档之间节流,让 blink 别把它们合并成一次 tick。所以每档的量在 Windows 是 120、其它平台是 1:
// packages/computer/src/device.ts:108
const LIBNUT_FALLBACK_DETENT_AMOUNT = process.platform === 'win32' ? 120 : 1;
再由 emitScrollDetents 逐档发送、每档间 delay(input-driver.ts:151-164)。这就是「 一个看似平凡的 scrollMouse 调用背后,藏着三平台单位不统一 + 浏览器事件合并」两层坑的完整故事。
3.7 坐标空间:toGlobalPoint 与多显示器几何
它要解决的小问题: 模型是对着某一块显示器的截图定的坐标,但 libnut 的 moveMouse 用的是跨所有显示器的全局坐标系。副屏往往在主屏右边或上边,原点带偏移。不校正就会点到隔壁屏幕。
思路: 每个 ComputerDevice 记住自己绑定显示器的几何 bounds,把「屏内局部点」加上 bounds 原点偏移,得到全局点。
// packages/computer/src/device.ts:1252-1254 + 542-551
private toGlobalPoint(point: Point): Point {
return mapDisplayLocalPointToGlobal(point, this.displayGeometry);
}
// mapDisplayLocalPointToGlobal:
// return { x: point.x + geometry.bounds.x, y: point.y + geometry.bounds.y };
displayGeometry 从哪来?连接时 resolveDisplayGeometry(displayId) 在 macOS 上调原生 display-info 助手,读回每块屏的 bounds(readDarwinDisplayGeometries,device.ts:440-457、531-539)。size() 也据此返回该屏的宽高(有几何就用 bounds,否则退回 libnut 全屏尺寸,device.ts:1232-1250)。截图 screenshotBase64 则按 displayId 选屏抓图(device.ts:1087-1119)。
为什么这三者要一致? 因为它们构成一个闭环:截图给模型看的是哪块屏、模型据此定的坐标是那块屏的局部像素、size() 告诉上游那块屏多大、toGlobalPoint 再把局部点搬回全局去点击。任何一环用错屏,点击就会漂到别处。所以 displayId 一旦选定,截图/尺寸/坐标换算必须锁在同一块屏上。
4. 巧妙之处(可借鉴)
-
能力即声明,动作即编译。 设备不写「我支持哪些动作」的清单,只实现原语;动作列表由
defineActionsFromInputPrimitives从原语推出来(index.ts:989)。新增一种设备只需实现原语子集,动作空间自动裁剪对齐——手机有swipe就多出Swipe,电脑没touch就自动没有。 -
一份原语,三处消费。 同一份
inputPrimitives既编译成给模型的actionSpace(§3.3),又是手动脚本aiTap的落点,还是驱动执行的入口——避免了「规划用的动作」和「执行用的代码」两套逻辑漂移。 -
单一事实源的边界滚动表。 四种边界滚(到顶/底/左/右)对三条后端(原生助手 / AppleScript 键 / libnut 方向向量)所需的参数,集中在一张
EDGE_SCROLL_SPEC表里(device.ts:146-151)。加一种滚动类型只改一处。 -
把平台怪癖写进常量 + 注释,而非散落在逻辑里。 Windows
WHEEL_DELTA、libnut 三平台单位差异、npm 丢可执行位——这些都以命名常量 + 大段注释沉淀(device.ts:92-108),读代码的人一眼看到「为什么是 120」。 -
驱动层可注入、可测试。
ComputerInputDriver通过构造参数拿到getLibnut/useAppleScript/...(input-driver.ts:28-47),测试时能塞假 libnut,不必真的动鼠标。
5. 边界与局限(诚实)
-
computer 设备不支持触摸类动作。
ComputerInputPrimitives无touch/system,所以没有Swipe/Pinch/LongPress/系统键——桌面场景够用,但别指望在电脑上用手势类 API。 -
强依赖原生绑定与系统权限。 libnut 加载失败(未编译/平台不匹配)会直接抛错(device.ts:305-311);macOS 缺 Screen Recording 权限截图失败、缺 Accessibility 权限原生滚动助手失败并降级(device.ts:1155-1169、571-583)。
-
Windows 上「点得到却点不动」。 非管理员进程对已提权(admin)窗口注入的点击/键入会被 UIPI 静默丢弃——鼠标看得见地移到位,但点击无效。健康检查会给一次条件式提醒,但代码无法从外部判断目标窗口是否提权(device.ts:1025-1041)。
-
Windows 截图是逻辑分辨率。 为绕开
screenshot-desktop的 polyglot.bat(在 Claude Code/Git Bash 环境下坏,#2150),Windows 改走 PowerShell 截图,但进程保持 DPI-unaware,缩放屏上抓到的是逻辑(缩放后)分辨率(device.ts:1173-1189 注释),坐标精度是后续待办。 -
坐标几何主要在 macOS 有原生支持。
display-info原生助手是 darwin-only(device.ts:395-401);非 macOS 走 libnut 全屏尺寸,多显示器偏移的精细处理较弱。
6. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 输入原语接口定义 | packages/core/src/device/index.ts:93 | InputPrimitives |
| computer 设备的原语画像 | packages/core/src/device/index.ts:124 | ComputerInputPrimitives |
| 设备抽象基类 | packages/core/src/device/index.ts:135 | AbstractInterface |
| 能力→动作 编译器 | packages/core/src/device/index.ts:989 | defineActionsFromInputPrimitives |
| 单个动作工厂(样板) | packages/core/src/device/index.ts:293 | defineActionTap |
| CursorMove 软降级 | packages/core/src/device/index.ts:814 | defineActionCursorMove |
| 电脑设备实现 | packages/computer/src/device.ts:696 | ComputerDevice |
| 电脑设备的原语 实现 | packages/computer/src/device.ts:721 | inputPrimitives |
| tap 落地(拟人+焦点补偿) | packages/computer/src/device.ts:723 | pointer.tap |
| 剪贴板输入(绕 IME) | packages/computer/src/device.ts:1264 | typeViaClipboard |
| actionSpace 三段拼接 | packages/computer/src/device.ts:1481 | actionSpace |
| 平台动作 ListDisplays | packages/computer/src/device.ts:1522 | createPlatformActions |
| 局部点→全局点 | packages/computer/src/device.ts:542 | mapDisplayLocalPointToGlobal |
| 显示器几何读取(macOS) | packages/computer/src/device.ts:440 | readDarwinDisplayGeometries |
| 滚动分派(三后端降级) | packages/computer/src/device.ts:1375 | performScroll |
| 原生相位滚动助手 | packages/computer/src/device.ts:555 | runPhasedScroll |
| Windows WHEEL_DELTA 常量 | packages/computer/src/device.ts:108 | LIBNUT_FALLBACK_DETENT_AMOUNT |
| 驱动封装层 | packages/computer/src/input-driver.ts:40 | ComputerInputDriver |
| 逐 detent 滚动 | packages/computer/src/input-driver.ts:151 | emitScrollDetents |
| 拟人移动 | packages/computer/src/input-driver.ts:222 | smoothMoveMouse |
| libnut 原生绑定接口 | packages/computer/src/input-driver.ts:4 | LibNut |