@gui-agent/operator-nutjs 实战指南:基于 nut.js 的桌面 GUI Agent 操作器
【免费下载链接】UI-TARS-desktopThe Open-Source Multimodal AI Agent Stack: Connecting Cutting-Edge AI Models and Agent Infra项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS-desktop
NutJS Operator是 UI-TARS 多模态开源仓库multimodal/gui-agent/operator-nutjs目录下提供的一个「电脑操作器」(Computer Operator):它以 nut.js(Native UI Testing)风格的底层库为驱动,把鼠标移动/点击、键盘输入、滚轮、截屏、等待等桌面原语统一封装成 GUI Agent 标准动作。本文以该包的官方 README 为骨架,结合仓库源码(NutJSOperator.ts、共享动作类型 与 操作器抽象基类)展开讲解,读完你将掌握它的安装、初始化、坐标换算、动作执行模型与关键实现细节,并能把它直接接入你自己的 GUI Agent 任务循环。
一、NutJS Operator 是什么
在 GUI Agent 的典型工作流中,Agent 模型观察屏幕截图、规划动作,随后需要有一个「操作器」真正把动作落到真实的桌面环境中。@gui-agent/operator-nutjs扮演的正是这个角色,官方 README 对其定位的描述是:
- 面向 GUI Agent 的桌面电脑操作器;
- 提供一套与桌面环境交互的 API;
- 覆盖截屏、鼠标操作、键盘操作等能力。
官方文档列出的核心特性如下:
- 截屏(Screenshot):捕获屏幕画面,并对高 DPI 显示器做正确缩放处理;
- 鼠标操作(Mouse Operations):移动、单击、双击、右键、拖拽等;
- 键盘操作(Keyboard Operations):键入文本、按下快捷键等;
- 滚动(Scroll):向上 / 向下滚动;
- 等待(Wait):等待指定时间。
从包元信息看,当前仓库中该包的版本为0.3.0,构建产物经 rslib 输出为 ESM/CJS/类型声明三份(见 package.json),并声明了@computer-use/nut-js、@agent-infra/logger、@gui-agent/shared(workspace 内联依赖)、jimp(图像缩放)等依赖。需要说明的是:仓库中另有一份旧版同名思路的 SDK packages/ui-tars/operators/nut-js,属于另一条产品线(@ui-tars/operator-nut-js),本文讨论的是multimodal/gui-agent下的@gui-agent/operator-nutjs。
二、安装与包结构
官方文档给出三种包管理器安装方式:
npm install @gui-agent/operator-nutjsyarn add @gui-agent/operator-nutjspnpm add @gui-agent/operator-nutjs包源码结构非常精简(对应仓库路径 multimodal/gui-agent/operator-nutjs):
| 路径 | 作用 |
|---|---|
src/index.ts | 仅一行核心导出:export { NutJSOperator } from './NutJSOperator'; |
src/NutJSOperator.ts | 操作器完整实现 |
examples/test-runner.ts | 可运行示例:初始化、截屏并把动作逐一跑在 Google 首页上 |
package.json | 提供pnpm example(tsx运行示例)与pnpm test(vitest)脚本 |
需要注意一点:代码中实际 import 的是@computer-use/nut-js(而非 README 中提到的 nut-tree 原仓库命名空间),它向操作器暴露了screen、mouse、keyboard、clipboard、Button、Key、Point、straightTo、sleep等底层能力。
三、快速开始:最小可用示例
README 提供了一个可直接照抄的最小示例:创建 logger 与操作器实例,先截一张屏,再依次执行「点击屏幕中心」「键入文本」两个动作:
import { NutJSOperator } from '@gui-agent/operator-nutjs'; import { ConsoleLogger, LogLevel } from '@agent-infra/logger'; // Create a logger const logger = new ConsoleLogger(undefined, LogLevel.DEBUG); // Create an operator instance const operator = new NutJSOperator(logger); // Take a screenshot const screenshot = await operator.screenshot(); console.log('Screenshot taken:', screenshot.status); // Execute actions const result = await operator.execute({ actions: [ { type: 'click', inputs: { point: { normalized: { x: 0.5, y: 0.5 } // Click at the center of the screen } } }, { type: 'type', inputs: { content: 'Hello, World!' } } ] });需要特别指出的是,README 示例中的screenshot()、execute()是操作器内部受保护/原始方法;在真实接入场景里更推荐使用抽象基类 operator.ts 暴露的三个带初始化保障的公共入口:
await operator.doInitialize():幂等初始化(内部_initialized/_initializing状态机保证只初始化一次、并发调用共享同一个 Promise);await operator.doScreenshot():先确保初始化再截屏,异常时返回status: 'failed'而非抛出;await operator.doExecute(params):先确保初始化再顺序执行动作列表,异常时同样折叠为status: 'failed'+errorMessage的返回结构。
官方示例 examples/test-runner.ts 就是这一写法的完整示范:operator.doInitialize()之后调用operator.doScreenshot()并把 base64 解码落盘为dumps/*.jpg,再对 Google 首页逐条执行move → click → type → hotkey(Enter) → wait → scroll → right_click → double_click → drag等动作,每一步结束后都会重新截屏留档。
四、类与 API 参考
4.1NutJSOperator类
NutJSOperator继承自共享抽象基类Operator(见 operator.ts),必须实现四个抽象方法,这也构成了理解全包的索引:
| 抽象方法 | NutJS 实现位置 | 职责 |
|---|---|---|
initialize(): Promise<void> | NutJSOperator.ts#L39-L48 | 抓取一帧屏幕并采集逻辑分辨率与像素密度,构建ScreenContext |
supportedActions(): SupportedActionType[] | NutJSOperator.ts#L50-L69 | 声明本操作器支持的动作类型 |
screenContext(): ScreenContext | NutJSOperator.ts#L71-L77 | 返回屏幕上下文(未初始化时抛错) |
screenshot(): Promise<ScreenshotOutput>/execute(params): Promise<ExecuteOutput> | NutJSOperator.ts#L79-L125 | 截屏与批量执行 |
4.2 构造函数
constructor(logger: ConsoleLogger = defaultLogger)logger:@agent-infra/logger的ConsoleLogger实例。构造函数内部会用logger.spawn('[NutJSOperator]')派生带前缀的子 logger,所有动作日志统一携带[NutJSOperator]标记;- 默认值为模块级常量
defaultLogger = new ConsoleLogger(undefined, LogLevel.DEBUG)。
4.3screenshot()输出
返回ScreenshotOutput,字段如下(类型定义见 agents.ts):
base64:base64 编码的图片数据(保持物理像素尺寸);contentType:'image/jpeg';status:'success' | 'failed';- 失败时额外带
errorMessage。
4.4execute()输入输出
execute(params: ExecuteParams)接收{ actions: BaseAction[] }(可附带模型原始回复等字段,见 agents.ts),按数组顺序逐条执行动作,全部成功返回{ status: 'success' }。任何一条动作参数非法或类型不识别都会让整批失败——源码中singleActionExecutor对「必需坐标缺失」「拖拽缺起点/终点」「type 内容为空」「非法快捷键」「非法滚动方向」等场景都会直接throw。
五、两种坐标系统与高 DPI 换算
NutJS Operator 的精髓之一是它屏蔽了「屏幕物理像素」与「Agent 视角归一化坐标」之间的换算,这也是 README 强调「截屏对高 DPI 显示器做正确缩放」的底层原因。
5.1Coordinates结构
共享类型 actions.ts 中定义了坐标结构:
export interface Coordinates { raw?: { x: number; y: number }; // 原始像素坐标 normalized?: { x: number; y: number }; // 归一化坐标(0–1) referenceBox?: { x1: number; y1: number; x2: number; y2: number }; referenceSystem?: 'screen' | 'window' | 'browserPage' | string; }5.2 归一化坐标如何换算成真实坐标
源码 calculateRealCoords 实现换算规则:
- 若提供
normalized:realX = normalized.x * screenContext.screenWidth,realY = normalized.y * screenContext.screenHeight; - 若未提供
normalized但提供raw:直接使用原始像素坐标; - 两者皆缺:抛出
Invalid coordinates。
screenWidth/screenHeight来自 initialize:截获一帧后读取pixelDensity.scaleX/scaleY,用物理分辨率除以缩放因子得到逻辑分辨率:
this._screenContext = { screenWidth: screenWithScale.width / screenWithScale.pixelDensity.scaleX, screenHeight: screenWithScale.height / screenWithScale.pixelDensity.scaleY, scaleX: screenWithScale.pixelDensity.scaleX, scaleY: screenWithScale.pixelDensity.scaleY, };5.3 截屏的反向缩放
截屏路径则与之相反(物理像素 → 逻辑像素),见 screenshot:
screen.grab()抓取原始帧;.toRGB()得到 RGB 位图与pixelDensity;- 用
jimp按目标宽高(物理宽 / scaleX、物理高 / scaleY)缩放; - 编码为 JPEG 并 base64 输出。
最终效果是:模型看到的截图尺寸与归一化坐标使用的逻辑分辨率严格一致,normalized: {x:0.5, y:0.5}永远指向截图正中心,无论 Windows/Linux 下的 125%/150% 缩放或 macOS Retina 屏如何设置。源码日志会打印screenshot: ${width}x${height}, scaleFactor: ${scaleFactor}便于核对。
六、Supported Actions 全量动作详解
6.1 声明 vs. 执行的差异
代码中有两层「动作清单」:
- 声明层:supportedActions() 返回 17 种:
click、right_click、middle_click、double_click、mouse_down、mouse_up、mouse_move、drag、scroll、type、hotkey、press、release、wait、call_user、finished。这一层用于向 Agent 描述可用的动作空间; - 执行层:singleActionExecutor 的 switch 分支。注意
mouse_down、mouse_up、call_user虽在声明列表中,但 switch 中并没有对应分支,落入default会抛Unsupported action——从源码结构可以推断,这类动作要么是留给上层 Agent 循环自行解释(如call_user表示"请求用户介入"),要么是尚待补全的执行能力,接入时需自行确认。
6.2 鼠标动作(README 别名表 + 源码实现)
README 给出的鼠标动作分组与别名如下,源码 switch 均予支持:
| 分组 | 动作名(含别名) | 源码行为 |
|---|---|---|
| 移动 | move、move_to、mouse_move、hover | 需point;先做坐标换算再mouse.move(straightTo(...)) |
| 单击 | click、left_click、left_single | 左键单击 |
| 双击 | left_double、double_click | 左键双击 |
| 右键 | right_click、right_single | 右键单击 |
| 中键 | middle_click | 中键单击 |
| 拖拽 | left_click_drag、drag、select | 需start与end坐标 |
点击类统一走 handleClick:先换算并移动到目标点,sleep(100)稳定指针后执行mouse.click(button)或mouse.doubleClick(button)。拖拽实现见 switch 中drag分支:移动到起点 → 停顿 100ms →mouse.drag(straightTo(new Point(endX, endY)))一气呵成,适合文本选中、拖动文件等场景。
6.3 键盘动作
type:键入文本。输入处理相当精细(NutJSOperator.ts#L186-L212):- 先
trim(),再剥离末尾的\n或字面量\\n; - 设置
keyboard.config.autoDelayMs = 0提升键入速度; - Windows 平台回退方案:把文本写入系统剪贴板后模拟
Ctrl+V粘贴(粘贴后恢复原剪贴板内容),以规避 nut.js 在 Windows 上的键入兼容问题;其他平台直接keyboard.type(content); - 若原内容以换行结尾,则补按一次
Enter; - 结束后将
autoDelayMs恢复为 500ms。
- 先
hotkey:一次性「按下并松开」组合键;press/release:仅按下或仅松开(用于组合出长按类操作)。
三者共用 getHotkeys 完成字符串到按键码的解析:
- 按键串按空白或
+分割(keyStr.split(/[\s+]/)),逐段小写后查表; - 内置别名表:
return → Enter、page down/page up、,→Key.Comma、方向键arrowup/arrowdown/arrowleft/arrowright等; - 平台化修饰键:
meta/win/command/cmd在 macOS 映射为LeftCmd、其他平台为LeftWin;ctrl在 macOS 映射为LeftCmd(代码如此实现,需要注意该约定)、其余平台为LeftControl; - 别名表未命中的键会继续用 nut.js
Key枚举的 lowercase 字典兜底(所以enter、escape、a、1等常规键名都能解析)。
因此{ type: 'hotkey', inputs: { key: 'ctrl+a' } }、{ type: 'hotkey', inputs: { key: 'command+space' } }这类写法天然可用。
6.4 滚动、等待与收尾动作
scroll(NutJSOperator.ts#L232-L251):可选的point会先把鼠标移到该位置,再按direction(大小写不敏感)执行滚动——up用mouse.scrollUp(500)、down用mouse.scrollDown(500),每次固定滚动 500 个刻度;left/right方向当前不被支持,会抛Unsupported scroll direction。注意:示例工程里传入的amount字段在此实现中并未被读取,属于预留字段;wait:等待指定秒数(inputs.time,单位秒),未传时默认 5000ms。源码同时打印 warning「The operator should not process wait action」,暗示理想情况下等待调度应发生在 Agent 层而非操作器内;finished:空操作,仅打日志,用于在动作序列结尾标识任务结束。
七、动作类型在共享层如何定义
前面表格中的动作并非只属于 NutJS Operator——它们定义在共享包@gui-agent/shared中(见 actions.ts)。NutJSOperator通过继承Operator、声明supportedActions()与整个 GUI Agent 体系对接。这种「动作 Schema 与具体执行器解耦」的设计,使得同一个动作描述可以被不同操作器解释:
- 桌面端操作器(本文的 NutJS);
- 移动端 ADB / 浏览器 / 浏览器底座等其他 operator 实现。
在共享动作类型中,每个动作被建模为统一三元组BaseAction { type, inputs, meta? },鼠标动作携带Coordinates点、键盘动作携带字符串内容或按键串,拖拽动作携带start/end两点。配套的ACTION_METADATA注册表(actions.ts)为每个动作标注了category(mouse/keyboard/navigation/mobile/system/wait)与语义描述,可用于自动生成系统提示词中的动作空间说明。
八、把它跑起来的完整链路(含验证建议)
综合 README、测试运行器示例 与源码,一条可自测的接入路径为:
# 在 monorepo 内安装依赖后,直接运行示例 pnpm example示例脚本会依次完成:创建实例 →doInitialize()→ 截屏落盘dumps/screenshot-*.jpg→ 打开浏览器到 Google 首页 → 顺序执行 move/click/type/hotkey/wait/scroll/right_click/double_click/drag → 每步后截屏校验。运行环境要求操作系统当前桌面可被鼠标键盘控制(真实显示器或虚拟桌面),因为它执行的是真实的系统级输入。
如果要写自己的验证脚本,建议沿用基类公共方法并做显式初始化:
const operator = new NutJSOperator(logger); await operator.doInitialize(); const ctx = await operator.getScreenContext(); console.log(ctx); // { screenWidth, screenHeight, scaleX, scaleY } const shot = await operator.doScreenshot(); // shot.base64 可直接喂给多模态大模型做下一步决策 const out = await operator.doExecute({ rawContent: 'click the search box', rawActionStrings: ['click'], actions: [{ type: 'click', inputs: { point: { normalized: { x: 0.5, y: 0.44 } } } }], });doExecute对参数格式比较宽容(ExecuteParams本身是{ actions }与ParsedGUIResponse可选字段的交叉并允许扩展字段),因此可以直接把模型解析结果原样透传。
九、总结
@gui-agent/operator-nutjs是 UI-TARS 多模态 Agent 技术栈中面向真实桌面操作系统的执行层组件:它用约 360 行实现覆盖了截屏、鼠标、键盘、滚动、等待等 GUI 原语,并通过归一化坐标与高 DPI 换算,让「模型看到什么坐标系,就点哪个点」这件事在不同缩放率的屏幕上保持正确。核心知识点可归纳为:
- 继承
Operator抽象基类,公共入口用doInitialize / doScreenshot / doExecute,自动处理幂等初始化与错误折叠; Coordinates同时支持raw像素坐标与normalized0–1 归一化坐标;- 动作类型是共享层 Schema,动作执行支持 README 中的全部别名(如
move_to、left_single、select等); type在 Windows 走剪贴板粘贴回退、hotkey/press/release通过别名表 +Key枚举解析按键串;- 当前实现的滚动仅支持
up/down,mouse_down/mouse_up/call_user处于「已声明、未落地执行」状态,接入时需留意。
如果你正在构建桌面端 GUI Agent(例如结合 apps/ui-tars 的界面能力),可以把该操作器作为动作执行后端,与@gui-agent/shared的动作解析、@agent-infra/logger的日志系统组合使用。项目采用 Apache-2.0 许可,源码路径:multimodal/gui-agent/operator-nutjs。
【免费下载链接】UI-TARS-desktopThe Open-Source Multimodal AI Agent Stack: Connecting Cutting-Edge AI Models and Agent Infra项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS-desktop
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考