在上一篇文章中,我们分析了 parsing_response_to_pyautogui_code。
这个函数负责把结构化动作:
json
{
"action_type": "click",
"action_inputs": {
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
}
}
转换成真正的 pyautogui 代码:
python
pyautogui.click(850, 120, button='left')
从这一篇开始,我们把不同动作类型拆开讲。
本篇先分析最基础、也最重要的鼠标类动作:
text
click
left_single
left_double
right_single
hover
它们在 UI-TARS 里都走同一个源码分支,都依赖 start_box,最后分别映射成:
text
左键单击
左键双击
右键单击
鼠标悬停
这几个动作看起来简单,但它们是 GUI Agent 真正"控制屏幕"的基础。
一、鼠标动作在 UI-TARS 中的位置
UI-TARS 的整体链路可以这样理解:
text
用户任务
↓
当前截图
↓
模型输出 Thought + Action
↓
parse_action_to_structure_output
↓
结构化动作
↓
parsing_response_to_pyautogui_code
↓
pyautogui 鼠标键盘操作
官方 README 也说明,UI-TARS 的后处理流程会先用 parse_action_to_structure_output 解析模型输出,再用 parsing_response_to_pyautogui_code 生成 pyautogui 代码;COMPUTER_USE 支持桌面端的单击、双击、右键、拖拽、快捷键、输入、滚动等操作。
在这些动作里,鼠标动作是最基础的。
因为 GUI Agent 操作图形界面,最常见的动作就是:
text
点击按钮
双击文件
右键菜单
移动鼠标到某个区域
所以,鼠标动作分支可以看作 UI-TARS 执行层的核心之一。
二、源码中的鼠标动作分支
在 action_parser.py 的 parsing_response_to_pyautogui_code 函数中,鼠标类动作被放在同一个分支:
python
elif action_type in [
"click", "left_single", "left_double", "right_single", "hover"
]:
# Parsing mouse click actions
start_box = action_inputs.get("start_box")
start_box = str(start_box)
if start_box:
start_box = eval(start_box)
if len(start_box) == 4:
x1, y1, x2, y2 = start_box
elif len(start_box) == 2:
x1, y1 = start_box
x2 = x1
y2 = y1
x = round(float((x1 + x2) / 2) * image_width, 3)
y = round(float((y1 + y2) / 2) * image_height, 3)
if action_type == "left_single" or action_type == "click":
pyautogui_code += f"\npyautogui.click({x}, {y}, button='left')"
elif action_type == "left_double":
pyautogui_code += f"\npyautogui.doubleClick({x}, {y}, button='left')"
elif action_type == "right_single":
pyautogui_code += f"\npyautogui.click({x}, {y}, button='right')"
elif action_type == "hover":
pyautogui_code += f"\npyautogui.moveTo({x}, {y})"
源码中确实把 click、left_single、left_double、right_single、hover 放在同一个分支里处理,并且都从 start_box 中计算中心点,再分别生成 pyautogui.click、pyautogui.doubleClick 或 pyautogui.moveTo。
这一段代码可以拆成四步:
text
1. 读取 start_box;
2. 把 start_box 转成坐标列表;
3. 计算 box 中心点;
4. 根据 action_type 生成不同鼠标操作代码。
下面我们逐步看。
三、为什么鼠标动作都使用 start_box?
模型可能输出的是:
text
click(point='<point>850 120</point>')
但前面 parse_action_to_structure_output 会把它统一成:
text
click(start_box='(850,120)')
再进一步归一化成:
json
{
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
}
所以到了 pyautogui 生成阶段,所有鼠标类动作都只需要关心:
text
start_box
而不是:
text
point
start_point
coordinate
bbox
官方 codes/README.md 也说明,parse_action_to_structure_output 会处理坐标缩放和 box/point 格式转换,返回包含 action_type、action_inputs、thought 等字段的结构化 actions。
这就是 UI-TARS 的一个重要设计:
模型输出层可以使用 point,但执行层统一使用 start_box。
这样做的好处是,执行层不需要理解模型原始格式,只要处理统一结构。
四、start_box 可以是 4 个数,也可以是 2 个数
源码中有一段判断:
python
if len(start_box) == 4:
x1, y1, x2, y2 = start_box
elif len(start_box) == 2:
x1, y1 = start_box
x2 = x1
y2 = y1
也就是说,start_box 支持两种格式。
第一种是标准 box:
text
[x1, y1, x2, y2]
例如:
text
[0.40, 0.10, 0.50, 0.15]
它表示一个区域。
第二种是单点坐标:
text
[x, y]
例如:
text
[0.4427, 0.1111]
如果是两个数,源码会把它扩展成:
text
[x, y, x, y]
这和前面文章讲过的逻辑一致:
一个点可以看作宽高为 0 的 box。
这样后续统一计算中心点。
五、为什么要计算 box 中心点?
源码中计算真实坐标的核心代码是:
python
x = round(float((x1 + x2) / 2) * image_width, 3)
y = round(float((y1 + y2) / 2) * image_height, 3)
也就是说:
text
center_x = (x1 + x2) / 2
center_y = (y1 + y2) / 2
再乘以真实图片宽高:
text
real_x = center_x * image_width
real_y = center_y * image_height
为什么要取中心点?
因为模型可能输出的是一个元素区域,而不是一个单点。
例如按钮区域:
text
[x1, y1, x2, y2]
如果要点击这个按钮,通常点中心最稳。
例如:
text
按钮左上角:0.40, 0.10
按钮右下角:0.50, 0.15
中心点是:
text
x = (0.40 + 0.50) / 2 = 0.45
y = (0.10 + 0.15) / 2 = 0.125
如果屏幕是 1920×1080,则真实坐标是:
text
real_x = 0.45 * 1920 = 864
real_y = 0.125 * 1080 = 135
点击按钮中心,比点击边缘更安全。
所以 UI-TARS 统一用 box 中心点作为鼠标动作目标。
六、为什么坐标要乘以 image_width 和 image_height?
前面结构化动作里的坐标是归一化坐标:
text
0 到 1 之间的比例
例如:
json
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
这个坐标不是 pyautogui 可以直接使用的像素坐标。
pyautogui 需要:
text
x = 850
y = 120
所以必须乘以当前图像宽高:
text
x = 0.4427 * 1920
y = 0.1111 * 1080
parsing_response_to_pyautogui_code 的函数签名里也明确要求传入 image_height 和 image_width,README 说明这两个参数就是用于生成 pyautogui 脚本时处理图像尺寸的参数。
这一步完成的是:
text
归一化坐标
↓
真实像素坐标
如果传入的 image_width / image_height 不对,最终点击位置也会错。
七、click 和 left_single:左键单击
源码中:
python
if action_type == "left_single" or action_type == "click":
pyautogui_code += f"\npyautogui.click({x}, {y}, button='left')"
也就是说,click 和 left_single 被视为同义动作。
它们最终都会生成:
python
pyautogui.click(x, y, button='left')
例如结构化动作:
json
{
"action_type": "click",
"action_inputs": {
"start_box": "[0.5, 0.2, 0.5, 0.2]"
}
}
如果截图尺寸是:
text
1920 × 1080
则:
text
x = 0.5 * 1920 = 960
y = 0.2 * 1080 = 216
生成:
python
pyautogui.click(960, 216, button='left')
click 是 GUI Agent 最常见的动作。
它可以用于:
text
点击按钮
选中输入框
打开菜单
勾选复选框
点击链接
确认弹窗
但从源码角度看,click 并不关心"按钮"是什么。
它只关心:
text
目标坐标在哪里?
"哪个按钮值得点击"是模型负责判断的。
"如何点击这个坐标"是 pyautogui 负责执行的。
八、left_double:左键双击
源码中:
python
elif action_type == "left_double":
pyautogui_code += f"\npyautogui.doubleClick({x}, {y}, button='left')"
left_double 会生成:
python
pyautogui.doubleClick(x, y, button='left')
双击主要用于桌面端场景。
例如:
text
双击桌面图标打开应用
双击文件打开文档
双击文件夹进入目录
双击列表项打开详情
这也是为什么 COMPUTER_USE 里有 left_double,而移动端 Prompt 里没有。
因为手机端没有"鼠标双击打开文件"这种通用交互。
在 UI-TARS 的 README 中,COMPUTER_USE 明确面向 Windows、Linux、macOS 等桌面环境,并支持 single、double、right mouse clicks 等桌面常见操作。
所以 left_double 是典型的桌面端动作。
九、right_single:右键单击
源码中:
python
elif action_type == "right_single":
pyautogui_code += f"\npyautogui.click({x}, {y}, button='right')"
它最终生成:
python
pyautogui.click(x, y, button='right')
右键在桌面端非常重要。
常见场景包括:
text
右键文件打开菜单
右键图片另存为
右键桌面新建文件夹
右键窗口调用上下文菜单
右键链接复制地址
和 left_double 一样,right_single 也是桌面端特有交互。
在移动端,类似语义通常由:
text
long_press
完成。
比如长按图片保存、长按消息复制。
所以可以这样理解:
text
桌面端 right_single
对应移动端 long_press 的一部分语义。
但在 pyautogui 执行层里,right_single 就是:
text
移动鼠标到目标点
点击右键
十、hover:悬停动作
源码中:
python
elif action_type == "hover":
pyautogui_code += f"\npyautogui.moveTo({x}, {y})"
hover 不点击,只移动鼠标:
python
pyautogui.moveTo(x, y)
这个动作在 GUI 自动化里也很有用。
例如:
text
鼠标悬停后显示下拉菜单
悬停后出现 tooltip
悬停到侧边栏后展开菜单
悬停到视频进度条后显示控制按钮
悬停到文件上显示预览
很多界面元素不是一直可见的,需要鼠标移动过去才出现。
这时如果只有 click,模型可能会误点。
hover 的价值是:
只改变鼠标位置,不触发点击。
不过当前 COMPUTER_USE_DOUBAO Prompt 中主要列出的是 click、left_double、right_single、drag、hotkey、type、scroll、wait、finished 等动作;执行层支持 hover,说明 action_parser.py 的可执行动作集合比基础 Prompt 里列出的动作略宽一些。源码分支中确实包含 hover。
如果你想让模型稳定使用 hover,最好在 Prompt 的 Action Space 中明确加入它。
十一、从模型输出到鼠标点击的完整流程
以 click 为例,完整链路是:
text
模型输出:
Action: click(point='<point>850 120</point>')
↓
parse_action_to_structure_output:
point → start_box
坐标归一化
↓
结构化动作:
{
"action_type": "click",
"action_inputs": {
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
}
}
↓
parsing_response_to_pyautogui_code:
start_box → 中心点
归一化坐标 × image_width / image_height
↓
pyautogui 代码:
pyautogui.click(850, 120, button='left')
也就是说,鼠标动作不是直接从模型输出坐标执行,而是经过:
text
格式统一
坐标归一化
中心点计算
真实坐标还原
代码生成
这几层处理。
十二、为什么所有鼠标动作共用一个分支?
源码把:
text
click
left_single
left_double
right_single
hover
放在同一个分支里。
原因很简单:
它们的坐标计算完全一样,只是最后的鼠标操作不同。
共同部分是:
text
读取 start_box
解析坐标
计算中心点
乘以 image_width / image_height
得到真实 x、y
不同部分是:
text
click → pyautogui.click(..., button='left')
left_single → pyautogui.click(..., button='left')
left_double → pyautogui.doubleClick(..., button='left')
right_single → pyautogui.click(..., button='right')
hover → pyautogui.moveTo(...)
这就是很典型的代码复用。
如果每个动作都单独写坐标解析逻辑,代码会重复很多,而且更容易出 bug。
十三、box 中心点的设计对 UI 自动化有什么启发?
很多自动化脚本喜欢直接记录固定坐标:
python
pyautogui.click(850, 120)
但 UI-TARS 的链路不是这样。
它更像:
text
模型识别目标元素
↓
输出点或区域
↓
统一成 box
↓
点击 box 中心点
这给我们做桌面自动化产品一个启发:
尽量不要只保存死坐标,而要保存目标区域或相对坐标。
例如在你的自动化工具里,可以保存:
json
{
"target": "搜索框",
"box": [0.40, 0.08, 0.60, 0.12],
"click_point": "center"
}
这样在不同分辨率下,可以通过比例坐标还原。
当然,这还不能解决所有问题,比如窗口位置、DPI、多显示器、网页缩放都会影响真实坐标,但至少比直接保存绝对坐标更稳。
十四、鼠标动作最容易出错的地方
鼠标动作看起来简单,但在真实 GUI Agent 里最容易出错。
1. 坐标参考系错了
模型输出坐标可能基于 resized image。
如果你直接按原图坐标执行,就会偏移。
2. image_width / image_height 传错
如果传入的是缩放图尺寸,而 pyautogui 执行的是屏幕尺寸,坐标会错。
3. 窗口偏移没处理
如果截图是窗口区域,但 pyautogui 点击的是全屏坐标,需要加窗口左上角偏移。
例如:
text
截图内坐标:850,120
窗口左上角:100,50
全屏坐标:950,170
4. DPI 缩放导致坐标错位
Windows 125%、150% 缩放会导致截图坐标和鼠标坐标不一致。
5. 目标区域太小
按钮很小、图标很小、文字链接很细时,中心点计算也可能不稳。
6. UI 状态变化
模型决定点击时按钮还在,但执行时页面已经刷新或弹窗遮挡。
所以鼠标动作执行后,必须重新截图验证。
十五、源码中的 eval 风险
鼠标动作分支里有一行:
python
start_box = eval(start_box)
这在研究代码里很方便,但如果做真实产品,需要谨慎。
因为 start_box 来自模型输出,属于不可信输入。
更安全的写法是:
python
import ast
start_box = ast.literal_eval(start_box)
或者自己写严格解析函数:
text
只允许 list/tuple;
长度只能是 2 或 4;
每个元素必须是 int 或 float;
归一化坐标必须在 0 到 1 之间;
最终真实坐标不能超出屏幕。
源码中 click、drag、scroll 分支都使用 eval 解析 box 字符串,产品化时建议统一替换为安全解析。
十六、建议增加鼠标动作安全检查
真实产品里,不建议模型生成坐标后马上点击。
中间最好加一层安全检查。
例如:
python
def validate_mouse_action(action_type, x, y, image_width, image_height):
if action_type not in {"click", "left_single", "left_double", "right_single", "hover"}:
raise ValueError("Unsupported mouse action")
if not (0 <= x <= image_width and 0 <= y <= image_height):
raise ValueError("Mouse coordinate out of bounds")
return True
还可以增加高风险区域检查:
text
删除按钮
付款按钮
发送按钮
确认提交按钮
格式化按钮
关闭安全软件按钮
如果模型准备点击这些区域,暂停执行,请用户确认。
GUI Agent 的风险在于:
它不是只生成建议,而是真的可以操作电脑。
所以鼠标动作一定要加安全边界。
十七、建议增加点击前后截图
为了调试鼠标动作,建议每一步都保存:
text
点击前截图
模型输出坐标
计算后的真实坐标
点击位置可视化图
点击后截图
例如日志:
json
{
"action_type": "click",
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]",
"image_width": 1920,
"image_height": 1080,
"screen_x": 850,
"screen_y": 120,
"pyautogui_code": "pyautogui.click(850, 120, button='left')",
"before_screenshot": "step_001_before.png",
"after_screenshot": "step_001_after.png"
}
这样一旦点错,可以快速判断问题来自哪里:
text
模型识别错?
坐标缩放错?
窗口偏移错?
DPI 错?
页面变化了?
没有这些日志,GUI Agent 很难排查。
十八、click、doubleClick、rightClick、hover 的产品化封装
如果你要做自己的桌面自动化项目,可以把 UI-TARS 的鼠标分支封装成统一函数:
python
def build_mouse_code(action_type, start_box, image_width, image_height):
x1, y1, x2, y2 = safe_parse_box(start_box)
x = round(((x1 + x2) / 2) * image_width, 3)
y = round(((y1 + y2) / 2) * image_height, 3)
if action_type in ("click", "left_single"):
return f"pyautogui.click({x}, {y}, button='left')"
if action_type == "left_double":
return f"pyautogui.doubleClick({x}, {y}, button='left')"
if action_type == "right_single":
return f"pyautogui.click({x}, {y}, button='right')"
if action_type == "hover":
return f"pyautogui.moveTo({x}, {y})"
raise ValueError(f"Unsupported mouse action: {action_type}")
这样比把所有逻辑写在一个大函数里更容易维护。
同时还可以在 safe_parse_box 中集中做安全解析和边界检查。
十九、鼠标动作和传统 RPA 的区别
传统 RPA 常见做法是:
text
录制时点击 850,120
回放时继续点击 850,120
UI-TARS 的方式是:
text
模型看当前截图
判断目标元素在哪里
输出 point 或 start_box
Parser 归一化坐标
Executor 还原真实坐标
pyautogui 执行点击
两者最终都可能调用:
python
pyautogui.click(850, 120)
但坐标来源完全不同。
传统 RPA 的坐标来自人工录制。
UI-TARS 的坐标来自模型对当前截图的视觉理解。
所以 UI-TARS 不是"不用坐标",而是:
坐标不是死记的,而是每轮根据当前界面动态生成的。
这也是 GUI Agent 和传统坐标脚本的核心区别。
二十、一个完整例子:右键保存图片
假设用户任务是:
text
右键网页中的图片,选择另存为。
模型可能第一步输出:
text
Thought: 我需要先在目标图片上点击右键,打开上下文菜单。
Action: right_single(point='<point>640 360</point>')
结构化后:
json
{
"action_type": "right_single",
"action_inputs": {
"start_box": "[0.3333, 0.3333, 0.3333, 0.3333]"
}
}
假设屏幕是 1920×1080。
执行层计算:
text
x = 0.3333 * 1920 ≈ 640
y = 0.3333 * 1080 ≈ 360
生成:
python
pyautogui.click(640, 360, button='right')
执行后重新截图,模型看到右键菜单,再输出下一步点击"图片另存为"。
这个例子说明,右键本身只是第一步,真正的 GUI Agent 必须通过多轮截图和动作继续完成任务。
二十一、一个完整例子:悬停展开菜单
假设用户任务是:
text
打开网页顶部的产品菜单。
某些网页的产品菜单需要鼠标悬停才展开。
模型可以输出:
text
Thought: 顶部导航栏中有"产品"菜单,需要先将鼠标移动到该菜单上,让下拉菜单展开。
Action: hover(point='<point>520 80</point>')
结构化后:
json
{
"action_type": "hover",
"action_inputs": {
"start_box": "[0.2708, 0.0741, 0.2708, 0.0741]"
}
}
执行层生成:
python
pyautogui.moveTo(520, 80)
然后外层系统重新截图。
如果下拉菜单出现,模型再点击目标菜单项。
这个例子说明,hover 是一种非常重要的"状态触发动作"。
它不直接完成任务,但会改变界面状态。
总结
这篇文章我们拆解了 UI-TARS 中鼠标类动作的源码实现。
在 parsing_response_to_pyautogui_code 中,以下动作共用同一个分支:
text
click
left_single
left_double
right_single
hover
它们的共同处理流程是:
text
1. 从 action_inputs 中读取 start_box;
2. 将 start_box 解析成坐标列表;
3. 如果是 2 个数,就扩展成点状 box;
4. 如果是 4 个数,就取 box 中心点;
5. 将归一化坐标乘以 image_width / image_height;
6. 根据 action_type 生成不同 pyautogui 鼠标代码。
对应关系是:
text
click / left_single
→ pyautogui.click(x, y, button='left')
left_double
→ pyautogui.doubleClick(x, y, button='left')
right_single
→ pyautogui.click(x, y, button='right')
hover
→ pyautogui.moveTo(x, y)
从工程角度看,这个分支体现了 UI-TARS 的核心思路:
text
模型负责判断目标元素;
Parser 负责统一坐标格式;
Executor 负责还原真实坐标并执行鼠标动作。
真正产品化时,还需要补上:
text
安全坐标解析
边界检查
窗口偏移处理
DPI 适配
点击前后截图
高风险点击确认
失败重试机制