在上一篇文章中,我们分析了 parse_action_to_structure_output。
它的作用是把模型输出的文本:
text
Thought: 我需要点击搜索框。
Action: click(point='<point>850 120</point>')
转换成结构化动作字典:
json
[
{
"thought": "我需要点击搜索框。",
"action_type": "click",
"action_inputs": {
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
}
}
]
但这个结构化动作还不能直接操作电脑。
真正操作鼠标键盘的,是 action_parser.py 里的另一个核心函数:
python
parsing_response_to_pyautogui_code(...)
这篇文章就来分析它如何把:
text
click
type
hotkey
drag
scroll
hover
finished
这些结构化动作,转换成真实的 pyautogui 自动化代码。
一、这个函数在 UI-TARS 链路中的位置
UI-TARS 的后处理链路可以分成两步:
text
第一步:
模型输出文本
↓
parse_action_to_structure_output
↓
结构化 actions
第二步:
结构化 actions
↓
parsing_response_to_pyautogui_code
↓
pyautogui 代码
官方 codes/README.md 也明确说明,parse_action_to_structure_output 负责把输出动作解析成结构化字典并处理坐标缩放与格式转换;parsing_response_to_pyautogui_code 则负责把结构化动作转换成 pyautogui 脚本,支持 click、type、hotkey、drag、scroll 等动作。
所以,如果说:
text
parse_action_to_structure_output
负责"理解模型输出"
那么:
text
parsing_response_to_pyautogui_code
负责"把理解结果变成可执行代码"
这是 UI-TARS 从"模型会说动作"走向"电脑真的动起来"的最后一步。
二、函数签名说明
源码中的函数签名是:
python
def parsing_response_to_pyautogui_code(
responses,
image_height: int,
image_width: int,
input_swap: bool = True
) -> str:
从 README 的 API 文档看,这个函数接收结构化 actions,可以是 dict,也可以是 list;image_height 和 image_width 用于把归一化坐标还原成真实图像坐标;input_swap 表示输入文本时是否使用剪贴板粘贴,默认值是 True。函数最终返回一个 pyautogui 脚本字符串。
这几个参数可以这样理解:
text
responses:
parse_action_to_structure_output 生成的结构化动作。
image_height:
当前截图或执行坐标系的高度。
image_width:
当前截图或执行坐标系的宽度。
input_swap:
输入文本时是否优先使用剪贴板粘贴。
这里最关键的是 image_height 和 image_width。
因为前面的结构化动作里保存的是归一化坐标:
text
[0.4427, 0.1111, 0.4427, 0.1111]
而 pyautogui 需要的是真实屏幕坐标:
text
x = 850
y = 120
所以这个函数必须完成:
text
归一化坐标
↓
真实像素坐标
三、函数一开始做了什么?
源码开头会先初始化代码字符串:
python
pyautogui_code = f"import pyautogui\nimport time\n"
也就是说,它生成的不是直接执行动作,而是一段 Python 代码字符串。
如果 responses 是单个 dict,它会被包装成列表:
python
if isinstance(responses, dict):
responses = [responses]
这说明函数既支持单动作,也支持多动作。源码中确实先初始化 import pyautogui 和 import time,然后在 responses 是 dict 时转成列表,便于后续统一循环处理。
例如输入:
json
{
"action_type": "hotkey",
"action_inputs": {
"key": "ctrl v"
}
}
函数内部会当成:
json
[
{
"action_type": "hotkey",
"action_inputs": {
"key": "ctrl v"
}
}
]
统一处理。
四、为什么生成代码,而不是直接执行?
这个函数返回的是字符串,不是直接调用 pyautogui。
例如它会生成:
python
import pyautogui
import time
pyautogui.click(850, 120, button='left')
而不是在函数里直接执行:
python
pyautogui.click(...)
这种设计有几个好处。
第一,便于调试。
你可以先打印生成的代码,看模型到底准备执行什么。
第二,便于审计。
如果动作涉及删除、提交、支付等高风险行为,可以在执行前检查代码。
第三,便于接入其他执行环境。
例如 OSWorld、远程桌面、沙箱环境,可以拿到代码后再决定是否执行。
第四,便于保存日志。
每一步都可以保存:
text
原始模型输出
结构化 action
生成的 pyautogui 代码
执行结果
这比直接执行更适合 GUI Agent 调试。
五、Observation 和 Thought 会被写进注释
在循环处理每个 response 时,源码会读取:
python
observation = response.get("observation", "")
thought = response.get("thought", "")
如果是第一个 response,会把它们写进 Python 三引号注释:
python
'''
Observation:
...
Thought:
...
'''
如果不是第一个动作,则会先加:
python
time.sleep(1)
源码中确实对第一个 response 写入 Observation 和 Thought 注释,对后续 response 增加 time.sleep(1)。
这有两个意义。
第一,生成的 pyautogui 代码更容易读。
例如:
python
'''
Observation:
当前页面是搜索结果页。
Thought:
我需要点击 UI-TARS 官方 GitHub 仓库。
'''
pyautogui.click(520, 310, button='left')
第二,多动作之间加等待,可以减少连续执行过快带来的问题。
因为 GUI 动作执行后,界面通常需要时间响应。
不过从产品设计角度看,多动作连续执行仍然要谨慎。更稳的做法通常是一轮只执行一个动作,然后重新截图,再让模型判断下一步。
六、hotkey:快捷键如何生成?
当动作类型是:
text
hotkey
源码会先从 action_inputs 中读取:
python
key
如果没有 key,再读取:
python
hotkey
这是为了兼容不同结构化动作字段。
例如:
json
{
"action_type": "hotkey",
"action_inputs": {
"key": "ctrl v"
}
}
或者:
json
{
"action_type": "hotkey",
"action_inputs": {
"hotkey": "ctrl v"
}
}
都可以处理。
然后源码会把一些方向键名称转换成 pyautogui 能识别的形式:
text
arrowleft → left
arrowright → right
arrowup → up
arrowdown → down
如果 key 是 space,则转换成空格字符。最后通过空格拆分多个按键,生成:
python
pyautogui.hotkey('ctrl', 'v')
源码中的 hotkey 分支正是按这个逻辑处理:读取 key 或 hotkey,转换 arrow 方向键和 space,再用 pyautogui.hotkey(...) 生成代码。
所以模型输出:
text
Action: hotkey(key='ctrl v')
最终会变成:
python
pyautogui.hotkey('ctrl', 'v')
这就是模型动作到键盘快捷键的转换。
七、press、keydown、release、keyup:单键按下和释放
除了 hotkey,源码还支持几类更底层的键盘动作:
text
press
keydown
release
keyup
其中:
text
press / keydown
会生成 pyautogui.keyDown(...)
release / keyup
会生成 pyautogui.keyUp(...)
源码里,press 和 keydown 被放在同一个分支;release 和 keyup 被放在另一个分支。它们同样会处理 arrowleft、arrowright、arrowup、arrowdown、space 等按键名称映射。
这类动作适合更细粒度的键盘控制。
例如某些拖拽或组合操作可能需要:
text
按住 Shift
点击某个元素
松开 Shift
不过在 UI-TARS 的主 Prompt 里,桌面端更常见的是 hotkey,而不是显式的 keydown / keyup。
这说明源码执行层比 Prompt 里列出的动作略宽一些,保留了一些底层扩展能力。
八、type:为什么默认使用剪贴板粘贴?
type 是一个非常重要的动作。
模型可能输出:
text
Action: type(content='UI-TARS\n')
源码中会读取:
python
content = action_inputs.get("content", "")
然后调用:
python
escape_single_quotes(content)
处理单引号。
如果内容以 \n 或真实换行结尾,会先去掉结尾换行,后面再单独生成回车动作。
最关键的是 input_swap 参数。
如果:
python
input_swap=True
源码会生成:
python
import pyperclip
pyperclip.copy('UI-TARS')
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.5)
pyautogui.press('enter')
如果:
python
input_swap=False
则会生成:
python
pyautogui.write('UI-TARS', interval=0.1)
time.sleep(0.5)
pyautogui.press('enter')
源码中 type 分支确实默认在 input_swap=True 时使用 pyperclip.copy(...) 加 pyautogui.hotkey('ctrl', 'v'),并在内容以换行结尾时额外生成 pyautogui.press('enter')。
为什么默认用剪贴板?
因为 pyautogui.write 对中文、特殊符号、长文本、代码片段的兼容性通常不如剪贴板粘贴。
对于 GUI Agent 来说,模型可能要输入:
text
中文
路径
网址
多行文本
带引号的代码
JSON
用剪贴板更稳。
这也是 UI-TARS 执行层里一个很实用的工程细节。
九、drag / select:拖拽如何生成?
当动作类型是:
text
drag
select
源码会读取:
python
start_box
end_box
这两个字段都是归一化坐标。
例如:
json
{
"start_box": "[0.2, 0.5, 0.2, 0.5]",
"end_box": "[0.7, 0.5, 0.7, 0.5]"
}
源码会先解析 start_box,计算起点中心:
python
sx = ((x1 + x2) / 2) * image_width
sy = ((y1 + y2) / 2) * image_height
再解析 end_box,计算终点中心:
python
ex = ((x1 + x2) / 2) * image_width
ey = ((y1 + y2) / 2) * image_height
最后生成:
python
pyautogui.moveTo(sx, sy)
pyautogui.dragTo(ex, ey, duration=1.0)
源码中 drag 和 select 分支确实通过 start_box 和 end_box 分别计算中心点,然后生成 pyautogui.moveTo(...) 与 pyautogui.dragTo(...)。
这对应的模型动作是:
text
Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
经过前面的结构化解析后变成:
json
{
"action_type": "drag",
"action_inputs": {
"start_box": "[0.1562, 0.4630, 0.1562, 0.4630]",
"end_box": "[0.3646, 0.4630, 0.3646, 0.4630]"
}
}
再由 pyautogui 生成阶段还原成真实坐标拖拽。
十、scroll:滚动为什么也需要坐标?
scroll 动作通常长这样:
json
{
"action_type": "scroll",
"action_inputs": {
"start_box": "[0.5, 0.7, 0.5, 0.7]",
"direction": "down"
}
}
源码会读取 start_box,计算滚动位置:
python
x = center_x * image_width
y = center_y * image_height
然后根据 direction 生成:
python
pyautogui.scroll(5, x=x, y=y)
或者:
python
pyautogui.scroll(-5, x=x, y=y)
如果没有 start_box,则生成不带坐标的滚动:
python
pyautogui.scroll(5)
pyautogui.scroll(-5)
源码中 scroll 分支只处理 up 和 down,向上滚动使用正数,向下滚动使用负数,并且在有坐标时把 x/y 传给 pyautogui.scroll(...)。
为什么滚动也要坐标?
因为 GUI 里经常有多个滚动区域:
text
左侧菜单
主内容区
表格区域
弹窗区域
文件列表
鼠标停在哪里,滚轮就可能作用在哪里。
所以 start_box 表示:
在哪个区域附近滚动。
这和 click 不同。click 的坐标是目标点,scroll 的坐标是滚动发生区域。
十一、click、left_double、right_single、hover:鼠标动作如何生成?
鼠标类动作被放在一个分支里:
python
["click", "left_single", "left_double", "right_single", "hover"]
这些动作都会读取:
python
start_box
然后解析 box。
如果 box 长度为 4:
text
[x1, y1, x2, y2]
就取中心点。
如果长度为 2:
text
[x, y]
就把它当成一个点。
然后计算真实坐标:
python
x = ((x1 + x2) / 2) * image_width
y = ((y1 + y2) / 2) * image_height
最后根据动作类型生成不同代码:
python
pyautogui.click(x, y, button='left')
pyautogui.doubleClick(x, y, button='left')
pyautogui.click(x, y, button='right')
pyautogui.moveTo(x, y)
源码中 click、left_single、left_double、right_single、hover 分支正是这样处理:从 start_box 中计算中心点,再分别生成左键单击、左键双击、右键单击或鼠标移动代码。
例如结构化动作:
json
{
"action_type": "click",
"action_inputs": {
"start_box": "[0.5, 0.2, 0.5, 0.2]"
}
}
如果图片尺寸是:
text
image_width = 1920
image_height = 1080
则:
text
x = 0.5 * 1920 = 960
y = 0.2 * 1080 = 216
最终生成:
python
pyautogui.click(960, 216, button='left')
这就是模型坐标变成真实鼠标点击的核心过程。
十二、finished:任务完成如何表示?
当动作类型是:
text
finished
源码会直接把:
python
pyautogui_code = f"DONE"
也就是说,返回结果不再是 pyautogui 脚本,而是:
text
DONE
源码中 finished 分支确实直接将 pyautogui_code 设置为 DONE。
这个动作的意义不是操作界面,而是通知外层 Agent Loop:
任务已经完成,可以停止循环。
例如模型输出:
text
Thought: 当前页面已经打开 UI-TARS GitHub 仓库,任务完成。
Action: finished(content='已打开仓库。')
后处理最终可以得到:
text
DONE
外层系统看到 DONE 后,就不再继续截图和调用模型。
十三、未知动作如何处理?
如果 action_type 不属于任何已知分支,源码会生成一行注释:
python
# Unrecognized action type: xxx
源码中最后的 else 分支确实会把无法识别的动作类型写入注释。
例如:
json
{
"action_type": "open_app",
"action_inputs": {
"app_name": "Chrome"
}
}
如果当前 pyautogui 生成函数没有实现 open_app,就不会真正执行,而是生成未识别动作注释。
这也提醒我们:
Prompt 中允许模型输出的动作,必须和 parser、executor 支持的动作保持一致。
否则模型输出了动作,执行层却不会执行。
十四、从测试文件看它的最小验证
官方测试文件中有一个专门测试:
python
responses = {"action_type": "hotkey", "action_inputs": {"hotkey": "ctrl v"}}
code = parsing_response_to_pyautogui_code(responses, 224, 224)
self.assertIn('pyautogui.hotkey', code)
这个测试验证了一个最小链路:结构化 hotkey 动作可以被转换成包含 pyautogui.hotkey 的代码。
虽然测试很短,但它说明官方把 parsing_response_to_pyautogui_code 当成整个 action parser 链路的关键出口。
前面的测试验证:
text
parse_action
parse_action_to_structure_output
最后这个测试验证:
text
结构化动作 → pyautogui 代码
这正是 UI-TARS 后处理链路的三段式结构。
十五、完整示例:从模型文本到 pyautogui 点击
假设模型输出:
text
Thought: 我需要点击搜索框。
Action: click(point='<point>850 120</point>')
第一步,parse_action_to_structure_output 生成结构化动作:
json
[
{
"thought": "我需要点击搜索框。",
"action_type": "click",
"action_inputs": {
"start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
}
}
]
第二步,调用:
python
code = parsing_response_to_pyautogui_code(
responses=parsed_dict,
image_height=1080,
image_width=1920
)
第三步,还原坐标:
text
x = 0.4427 * 1920 ≈ 850
y = 0.1111 * 1080 ≈ 120
第四步,生成代码:
python
import pyautogui
import time
'''
Observation:
Thought:
我需要点击搜索框。
'''
pyautogui.click(850, 120, button='left')
这就是模型动作变成真实鼠标点击的完整过程。
十六、完整示例:输入文本并回车
模型输出:
text
Thought: 搜索框已经获得焦点,现在输入关键词并提交。
Action: type(content='UI-TARS\n')
结构化动作:
json
{
"action_type": "type",
"action_inputs": {
"content": "UI-TARS\\n"
}
}
当 input_swap=True 时,生成代码类似:
python
import pyperclip
pyperclip.copy('UI-TARS')
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.5)
pyautogui.press('enter')
这说明 UI-TARS 默认不是逐字敲键盘,而是更倾向于:
text
复制到剪贴板
粘贴
必要时回车
这对中文和复杂文本输入更稳定。
十七、完整示例:拖拽滑块
模型输出:
text
Thought: 我需要把滑块拖到右侧。
Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
结构化动作:
json
{
"action_type": "drag",
"action_inputs": {
"start_box": "[0.1562, 0.4630, 0.1562, 0.4630]",
"end_box": "[0.3646, 0.4630, 0.3646, 0.4630]"
}
}
如果图片尺寸是 1920×1080,则:
text
sx ≈ 300
sy ≈ 500
ex ≈ 700
ey ≈ 500
生成代码:
python
pyautogui.moveTo(300, 500)
pyautogui.dragTo(700, 500, duration=1.0)
这就是 drag 的完整执行映射。
十八、这个函数的核心设计思想
parsing_response_to_pyautogui_code 的核心设计可以概括为三句话。
第一,动作类型决定代码分支。
text
hotkey → pyautogui.hotkey
type → pyperclip.copy + ctrl v
drag → moveTo + dragTo
scroll → pyautogui.scroll
click → pyautogui.click
finished → DONE
第二,坐标统一从 box 中心点还原。
text
start_box / end_box
↓
取中心点
↓
乘以 image_width / image_height
↓
真实屏幕坐标
第三,生成代码而不是直接执行。
text
结构化动作
↓
pyautogui 代码字符串
↓
外层系统决定是否执行
这三个设计让 UI-TARS 的执行层保持了比较清晰的边界。
十九、源码中的几个产品化风险
如果你准备把这段逻辑用于真实产品,需要特别注意几个风险。
1. eval(start_box) 风险
源码中多处使用:
python
eval(start_box)
eval(end_box)
来把字符串形式的 box 转成 Python list。click、drag、scroll 等分支都存在这个逻辑。
在研究代码中这样写方便,但生产环境中,模型输出属于不可信输入,最好不要直接 eval。
更安全的方式是:
python
import ast
coords = ast.literal_eval(start_box)
或者自己写严格坐标解析器,只允许数字、小数点、逗号、中括号和负号。
2. 没有动作白名单校验
虽然函数通过分支处理 action_type,但如果前面 parser 传入奇怪动作,只会生成未识别注释。
产品化时最好在执行前增加明确白名单:
python
ALLOWED_ACTIONS = {
"click",
"left_double",
"right_single",
"hover",
"drag",
"scroll",
"hotkey",
"type",
"finished"
}
3. 坐标缺少边界检查
执行前最好检查:
text
0 <= x <= image_width
0 <= y <= image_height
否则模型输出异常坐标,可能导致 pyautogui 点到错误区域。
4. 剪贴板输入会覆盖用户剪贴板
type 默认使用 pyperclip,这很稳定,但也会覆盖用户原本剪贴板内容。
真实产品里最好:
text
先保存原剪贴板
粘贴完成后恢复
5. 高风险动作需要用户确认
例如模型要点击:
text
删除
确认支付
发送邮件
提交表单
格式化
关闭安全软件
即使 pyautogui 代码生成成功,也不应该直接执行。
执行前应增加安全拦截。
二十、如果接入自己的 Windows 自动化项目,应该怎么改?
如果你要把这套逻辑接入自己的桌面自动化 App,可以考虑做几层改造。
第一,把"生成代码"和"执行代码"分开。
text
模型输出
↓
结构化动作
↓
生成代码
↓
安全检查
↓
执行
第二,把 eval 换成安全解析。
第三,加窗口坐标转换。
UI-TARS 当前主要按截图宽高还原坐标,但真实桌面软件里还要考虑:
text
窗口左上角偏移
标题栏高度
多显示器
DPI 缩放
截图区域裁剪
远程桌面缩放
第四,把每一步执行结果记录下来。
例如:
json
{
"action_type": "click",
"normalized_box": "[0.4427, 0.1111, 0.4427, 0.1111]",
"screen_coordinate": [850, 120],
"pyautogui_code": "pyautogui.click(850, 120, button='left')",
"execution_result": "success"
}
第五,加入人工接管机制。
一旦模型连续失败、坐标异常、界面变化不符合预期,就暂停自动执行。
二十一、UI-TARS 执行链路的完整闭环
到这里,我们已经可以把 UI-TARS 的执行闭环串起来了。
text
用户任务
↓
截图
↓
模型输出 Thought + Action
↓
parse_action_to_structure_output
↓
结构化 action dict
↓
parsing_response_to_pyautogui_code
↓
pyautogui 代码
↓
鼠标键盘执行
↓
新截图
↓
下一轮推理
其中:
text
prompt.py
负责约束模型输出格式。
parse_action
负责解析函数调用。
parse_action_to_structure_output
负责把完整模型文本转成结构化动作。
parsing_response_to_pyautogui_code
负责把结构化动作变成真实可执行的鼠标键盘代码。
这就是 UI-TARS 从"看屏幕、想动作"到"真实操作电脑"的完整工程链路。
总结
这篇文章我们分析了 parsing_response_to_pyautogui_code。
它的核心作用是:
text
把结构化 action dict 转换成 pyautogui 自动化脚本字符串。
它支持的主要动作包括:
text
hotkey
press / keydown
release / keyup
type
drag / select
scroll
click
left_single
left_double
right_single
hover
finished
其中:
text
hotkey 转成 pyautogui.hotkey;
type 默认用 pyperclip + ctrl v;
drag 转成 moveTo + dragTo;
scroll 转成 pyautogui.scroll;
click / double click / right click / hover 从 start_box 还原坐标后执行;
finished 返回 DONE。
从工程角度看,这个函数的价值在于:
它把模型输出的抽象动作,真正落到了鼠标、键盘、滚轮这些可执行操作上。
但如果要用于真实产品,还需要增强:
text
安全坐标解析
动作白名单
坐标边界检查
高风险动作确认
剪贴板保护
DPI 和窗口偏移处理
执行日志与失败恢复
UI-TARS 的源码已经给出了从模型动作到 pyautogui 代码的基础链路。
真正产品化时,重点不是"能不能点击",而是:
text
点得准不准;
点之前是否安全;
点完之后是否能验证结果;
失败后能不能恢复。