在前两篇文章中,我们分别分析了 UI-TARS 执行层里的两类动作。
第 #16 篇讲的是鼠标类动作:
text
click
left_single
left_double
right_single
hover
第 #17 篇讲的是键盘类动作:
text
hotkey
type
press / keydown
release / keyup
这一篇继续分析 parsing_response_to_pyautogui_code 中更复杂的 GUI 操作:
text
drag
select
scroll
它们比单击和输入更复杂。
因为 click 只需要一个目标点,而 drag 至少需要两个位置:
text
起点
终点
scroll 虽然看起来只是滚轮动作,但在真实 GUI 中还要知道:
text
在哪个区域滚动?
向上还是向下滚动?
所以,这篇文章的核心问题是:
UI-TARS 如何把模型输出的拖拽、选择、滚动动作,转换成 pyautogui 可以执行的真实操作?
一、drag 和 scroll 在 UI-TARS 动作空间中的位置
在 UI-TARS 的 Prompt 设计中,桌面端 COMPUTER_USE 明确支持:
text
drag(start_point='<point>x1 y1</point>', end_point='<point>x2 y2</point>')
scroll(point='<point>x1 y1</point>', direction='down or up or right or left')
这说明拖拽和滚动不是附属功能,而是 GUI Agent 动作空间中的基础动作。prompt.py 中的桌面端模板确实把 drag 和 scroll 放入 Action Space,并要求模型按照函数调用格式输出动作。
如果 GUI Agent 只支持点击和输入,它能完成的任务会很有限。
真实界面里经常需要:
text
拖动滑块
拖拽文件
框选文本
调整窗口大小
移动地图
滚动网页
滚动表格
滚动弹窗
滚动侧边栏
这些都不是单个 click 能解决的。
所以,drag 和 scroll 是 GUI Agent 从"会点按钮"走向"能操作复杂界面"的关键动作。
二、从整体链路看 drag 和 scroll
UI-TARS 的完整动作执行链路是:
text
模型输出 Thought + Action
↓
parse_action_to_structure_output
↓
结构化 action dict
↓
parsing_response_to_pyautogui_code
↓
pyautogui 代码
官方 codes/README.md 也说明,parse_action_to_structure_output 负责把模型输出解析成结构化动作,并处理坐标缩放和 box/point 格式转换;parsing_response_to_pyautogui_code 则把结构化 actions 转换成 pyautogui 脚本,支持 click、type、hotkey、drag、scroll 等动作。
对于 drag 来说,链路大致是:
text
模型输出:
drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
↓
结构化动作:
{
"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 代码:
pyautogui.moveTo(300, 500)
pyautogui.dragTo(700, 500, duration=1.0)
对于 scroll 来说,链路大致是:
text
模型输出:
scroll(point='<point>600 720</point>', direction='down')
↓
结构化动作:
{
"action_type": "scroll",
"action_inputs": {
"start_box": "[0.3125, 0.6667, 0.3125, 0.6667]",
"direction": "down"
}
}
↓
pyautogui 代码:
pyautogui.scroll(-5, x=600, y=720)
也就是说,复杂操作最终还是会落到 pyautogui 的基础能力上:
text
移动鼠标
拖拽鼠标
滚动滚轮
三、drag 和 select 共用同一个源码分支
在 action_parser.py 的 parsing_response_to_pyautogui_code 中,drag 和 select 被放在同一个分支:
python
elif action_type in ["drag", "select"]:
start_box = action_inputs.get("start_box")
end_box = action_inputs.get("end_box")
...
pyautogui.moveTo(sx, sy)
pyautogui.dragTo(ex, ey, duration=1.0)
源码中确实把 drag 和 select 放在同一个分支,读取 start_box 和 end_box,分别计算起点和终点中心坐标,然后生成 pyautogui.moveTo(...) 和 pyautogui.dragTo(...)。
为什么 drag 和 select 可以共用逻辑?
因为它们底层动作一样:
text
鼠标移动到起点
按住鼠标
移动到终点
松开鼠标
区别只在语义上。
drag 更偏向:
text
拖动滑块
拖动文件
拖动窗口
拖动地图
select 更偏向:
text
框选文本
框选区域
选中多个对象
但从 pyautogui 执行角度看,两者都是:
text
moveTo + dragTo
所以源码把它们放在同一个分支里,是合理的代码复用。
四、drag 为什么必须有 start_box 和 end_box?
点击动作只需要一个点:
text
click(start_box='...')
但拖拽动作必须知道两个位置:
text
start_box:从哪里开始拖
end_box:拖到哪里结束
例如:
text
Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
经过 parse_action_to_structure_output 后,会先统一字段名:
text
start_point → start_box
end_point → end_box
源码中 parse_action_to_structure_output 确实会把 start_point= 替换为 start_box=,把 end_point= 替换为 end_box=,并把 point= 替换为 start_box=。
结构化后变成:
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]"
}
}
这里的 start_box 和 end_box 都是归一化坐标。
也就是说,它们不是直接的屏幕像素,而是:
text
0 到 1 之间的比例坐标
执行阶段还要乘以 image_width 和 image_height 才能得到真实坐标。
五、drag 的坐标还原逻辑
源码中,drag/select 分支会分别解析 start_box 和 end_box。
对于 start_box:
python
x1, y1, x2, y2 = eval(start_box)
sx = round(float((x1 + x2) / 2) * image_width, 3)
sy = round(float((y1 + y2) / 2) * image_height, 3)
对于 end_box:
python
x1, y1, x2, y2 = eval(end_box)
ex = round(float((x1 + x2) / 2) * image_width, 3)
ey = round(float((y1 + y2) / 2) * image_height, 3)
这和前面鼠标点击分支的思路一样:
text
box → 取中心点 → 乘以真实宽高 → 得到真实坐标
区别在于,drag 需要算两次:
text
start_box → sx, sy
end_box → ex, ey
最后生成:
python
pyautogui.moveTo(sx, sy)
pyautogui.dragTo(ex, ey, duration=1.0)
源码中的 drag/select 分支正是按照这个逻辑生成 pyautogui 代码。
六、为什么 drag 要先 moveTo,再 dragTo?
源码生成的是:
python
pyautogui.moveTo(sx, sy)
pyautogui.dragTo(ex, ey, duration=1.0)
而不是直接:
python
pyautogui.dragTo(ex, ey)
原因很简单:
拖拽动作必须明确起点。
如果鼠标当前位置不确定,直接 dragTo 可能会从错误位置开始拖。
例如,模型想拖动一个滑块:
text
起点:滑块当前位置
终点:目标位置
如果不先 moveTo(sx, sy),鼠标可能从上一次操作的位置开始拖,结果就完全错误。
所以正确流程是:
text
先移动到起点
再拖到终点
这也是 UI-TARS 源码的实现方式。
七、duration=1.0 的作用
源码中 dragTo 带了一个参数:
python
duration=1.0
这表示拖拽动作持续 1 秒。
为什么不瞬间拖过去?
因为很多 GUI 对拖拽动作的识别需要一定过程。
如果拖得太快,可能出现:
text
滑块没有跟上
文件没有被拖起
文本选择失败
目标区域没有触发 hover 状态
页面没有响应拖动
设置 duration=1.0 可以让拖拽更像真实用户操作。
当然,1 秒不是绝对最优。
不同场景可以调整:
text
拖动滑块:0.3 ~ 1.0 秒
拖动文件:0.5 ~ 1.5 秒
框选文本:0.5 ~ 1.0 秒
拖动地图:0.2 ~ 0.8 秒
如果做产品化,可以把 duration 作为可配置参数,而不是写死。
八、drag 可以覆盖哪些真实 GUI 场景?
drag 的应用非常广。
1. 拖动滑块
例如验证码、音量条、进度条、范围选择器:
text
Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
2. 拖动文件
例如把文件拖到另一个文件夹:
text
先定位文件图标
再定位目标文件夹
执行 drag
3. 框选文本
例如从一段文字开始拖到另一段文字:
text
Action: select(start_box='...', end_box='...')
源码里 select 和 drag 共用执行逻辑,因此底层都是 moveTo + dragTo。
4. 调整窗口大小
例如拖动窗口边缘或分隔线:
text
start_box:窗口边缘
end_box:目标位置
5. 拖动地图或画布
例如地图平移、设计软件画布移动、白板拖动。
这些操作都不能只靠 click 完成。
所以,drag 是 GUI Agent 处理复杂界面的关键动作。
九、drag 比 click 更难在哪里?
从源码看,drag 只是多了一个 end_box。
但从模型能力看,drag 比 click 难得多。
因为 click 只需要回答:
text
目标点在哪里?
drag 需要回答:
text
从哪里开始?
拖到哪里结束?
拖动方向是什么?
拖多远?
拖动过程中是否会触发状态变化?
比如拖动滑块,模型必须知道:
text
滑块当前位置
目标位置
拖动方向
拖动距离
如果起点错了,拖不到目标。
如果终点错了,拖动距离不够或过头。
如果拖动速度太快,UI 可能不响应。
所以,drag 是对 GUI grounding 能力更高阶的考验。
这也是为什么很多 GUI Agent 任务里,拖拽比点击更容易失败。
十、scroll 的源码分支
接下来分析 scroll。
源码中 scroll 分支大致是:
python
elif action_type == "scroll":
start_box = action_inputs.get("start_box")
if start_box:
x1, y1, x2, y2 = eval(start_box)
x = round(float((x1 + x2) / 2) * image_width, 3)
y = round(float((y1 + y2) / 2) * image_height, 3)
else:
x = None
y = None
direction = action_inputs.get("direction", "")
if x == None:
if "up" in direction.lower():
pyautogui.scroll(5)
elif "down" in direction.lower():
pyautogui.scroll(-5)
else:
if "up" in direction.lower():
pyautogui.scroll(5, x=x, y=y)
elif "down" in direction.lower():
pyautogui.scroll(-5, x=x, y=y)
源码中确实会读取 start_box,有坐标时计算中心点并生成带 x、y 的 pyautogui.scroll(...);没有坐标时生成不带坐标的滚动;方向上只处理了 up 和 down。
这段逻辑可以分成三步:
text
1. 计算滚动发生位置;
2. 读取 direction;
3. 根据方向生成滚轮正负值。
十一、scroll 为什么也需要 start_box?
很多人可能会以为滚动不需要坐标。
因为鼠标滚轮不就是上下滚吗?
但真实 GUI 中,滚动和鼠标所在位置高度相关。
例如一个网页里可能同时存在:
text
左侧菜单
中间正文
右侧目录
弹窗内部
表格区域
代码编辑器
鼠标停在哪里,滚动的就是哪个区域。
所以模型输出 scroll 时,不只要说明方向,还要说明滚动位置:
text
scroll(point='<point>600 720</point>', direction='down')
经过解析后:
json
{
"action_type": "scroll",
"action_inputs": {
"start_box": "[0.3125, 0.6667, 0.3125, 0.6667]",
"direction": "down"
}
}
这里的 start_box 表示:
把鼠标放到哪个区域附近,然后滚动。
这和 click 不一样。
click 的坐标是目标点。
scroll 的坐标是滚动作用区域。
十二、scroll 的方向如何映射?
源码中,滚动方向主要处理两类:
text
up
down
如果 direction 包含 up:
python
pyautogui.scroll(5)
如果 direction 包含 down:
python
pyautogui.scroll(-5)
有坐标时则是:
python
pyautogui.scroll(5, x=x, y=y)
pyautogui.scroll(-5, x=x, y=y)
也就是说:
text
up → 正数滚动
down → 负数滚动
这符合 pyautogui 的常见用法。
不过这里有一个细节值得注意。
在 prompt.py 中,scroll 的方向说明包含:
text
down or up or right or left
但当前 parsing_response_to_pyautogui_code 的源码分支只处理了 up 和 down,并没有生成左右滚动代码。prompt.py 的动作空间包含四个方向,而 action_parser.py 的 scroll 分支只根据 up/down 生成 pyautogui.scroll 正负值。
这说明 Prompt 和执行层之间存在一个小的不完全对齐点。
如果模型输出:
text
scroll(point='<point>600 720</point>', direction='right')
当前这段执行层不会生成有效的左右滚动 pyautogui 代码。
产品化时,这个地方需要补齐。
十三、为什么 pyautogui.scroll 使用 5 和 -5?
源码里写死了:
text
up → 5
down → -5
这个数字表示滚轮滚动的 click 数。
5 不是模型输出的参数,而是源码中的固定值。
这是一种简化设计。
它让模型只需要输出:
text
direction='down'
而不需要输出:
text
amount=5
好处是 Prompt 更简单。
坏处是滚动距离不可控。
有些页面可能只需要轻微滚动,有些页面需要滚动很远。
如果统一使用 5,可能出现:
text
滚得太少,看不到目标
滚得太多,越过目标
滚动区域没有响应
所以,产品化时可以考虑扩展:
text
scroll(point='...', direction='down', amount=5)
或者:
text
scroll(point='...', direction='down', distance='small')
scroll(point='...', direction='down', distance='large')
但只要扩展 Prompt,就必须同步扩展 Parser 和 Executor。
十四、scroll 和 drag 的核心区别
drag 和 scroll 都是复杂鼠标操作,但它们的语义不同。
drag 是:
text
从 A 点拖到 B 点。
需要:
text
start_box
end_box
执行方式是:
text
moveTo(start)
dragTo(end)
scroll 是:
text
在某个区域向某个方向滚动。
需要:
text
start_box
direction
执行方式是:
text
scroll(amount, x, y)
也就是说:
text
drag 强调路径;
scroll 强调区域和方向。
这也是为什么 drag 需要两个坐标,而 scroll 通常只需要一个坐标。
十五、scroll 为什么执行前没有 click?
源码中有一段被注释掉的代码:
python
# 先点对应区域,再滚动
# pyautogui_code += f"\npyautogui.click({x}, {y}, button='left')"
这说明作者考虑过一种做法:
text
先点击滚动区域
再执行滚动
但最终注释掉了。
为什么?
可能是因为滚动前点击区域有风险。
例如:
text
滚动正文区域时,点击可能打开链接;
滚动表格时,点击可能选中单元格;
滚动弹窗时,点击可能触发按钮;
滚动文件列表时,点击可能选中文件;
所以源码选择只在指定坐标处滚动,而不是先点击。
这个细节很重要。
它说明:
滚动动作应该尽量只改变滚动位置,不额外触发点击副作用。
当然,有些 GUI 框架需要先让区域获得焦点才能滚动。
这时是否先 click,应该作为可配置策略,而不是默认行为。
十六、scroll 最容易失败的场景
滚动看起来简单,但在 GUI Agent 中非常容易失败。
1. 滚动区域判断错误
模型想滚动主内容区,但坐标落在侧边栏。
结果滚动的是侧边栏。
2. 页面没有焦点
某些窗口需要先获得焦点才能响应滚轮。
3. 弹窗遮挡
模型以为滚动的是页面,但其实弹窗挡住了页面。
4. 方向错了
目标在下面,模型却向上滚。
5. 滚动距离固定
源码使用固定值 5 或 -5,可能不适合所有页面。
6. 横向滚动未实现
Prompt 允许 left/right,但当前 pyautogui 生成逻辑只处理 up/down。
这些都是产品化时需要补强的地方。
十七、drag 最容易失败的场景
拖拽失败的情况更多。
1. 起点不是可拖动对象
模型输出的 start_box 落在滑块附近,但没有落在滑块上。
结果拖不动。
2. 终点不合理
拖到太远、太近,或者拖到无效区域。
3. 拖拽速度不适配
duration=1.0 对某些界面可以,对某些界面可能太慢或太快。
4. 需要按住某个键配合
例如某些多选或复制拖拽需要 Ctrl 或 Shift。
5. 拖拽过程中触发 hover 或弹窗
拖到中途界面变了,最终结果和预期不一致。
6. 远程桌面或虚拟机拖拽不稳定
远程环境中拖拽动作经常比点击更不稳定。
所以拖拽执行后,一定要重新截图验证结果。
十八、完整示例:拖动滑块
假设用户任务是:
text
把音量调高。
模型看到当前界面后输出:
text
Thought: 我需要把音量滑块向右拖动。
Action: drag(start_point='<point>300 500</point>', end_point='<point>600 500</point>')
经过 parse_action_to_structure_output 后:
json
{
"action_type": "drag",
"action_inputs": {
"start_box": "[0.1562, 0.4630, 0.1562, 0.4630]",
"end_box": "[0.3125, 0.4630, 0.3125, 0.4630]"
}
}
假设图像尺寸是 1920×1080,执行层还原:
text
sx = 0.1562 * 1920 ≈ 300
sy = 0.4630 * 1080 ≈ 500
ex = 0.3125 * 1920 = 600
ey = 0.4630 * 1080 ≈ 500
生成:
python
pyautogui.moveTo(300, 500)
pyautogui.dragTo(600, 500, duration=1.0)
执行后重新截图,模型判断音量是否已经调高。
如果还不够,再继续拖动。
这就是拖拽动作的闭环。
十九、完整示例:滚动网页
假设用户任务是:
text
向下滚动页面,找到下载按钮。
模型当前没有看到下载按钮,于是输出:
text
Thought: 当前页面没有看到下载按钮,我需要在主内容区向下滚动。
Action: scroll(point='<point>960 720</point>', direction='down')
结构化后:
json
{
"action_type": "scroll",
"action_inputs": {
"start_box": "[0.5, 0.6667, 0.5, 0.6667]",
"direction": "down"
}
}
执行层还原坐标:
text
x = 0.5 * 1920 = 960
y = 0.6667 * 1080 ≈ 720
生成:
python
pyautogui.scroll(-5, x=960, y=720)
执行后重新截图。
如果下载按钮出现,下一步模型再输出 click。
如果还没出现,可能继续 scroll。
这就是滚动动作的典型使用方式。
二十、为什么 drag 和 scroll 都需要执行后验证?
drag 和 scroll 都是"状态改变动作"。
它们不像 hotkey(ctrl c) 那样通常没有明显界面变化。
拖拽和滚动会改变界面状态:
text
滑块位置变化
页面内容变化
文本被选中
文件位置变化
地图视图变化
表格滚动位置变化
但这些变化是否符合预期,不能只靠 pyautogui 执行结果判断。
pyautogui.dragTo(...) 执行成功,只表示鼠标动作完成。
不代表滑块真的移动成功。
pyautogui.scroll(...) 执行成功,只表示滚轮事件发出了。
不代表页面真的滚动到了目标位置。
所以 GUI Agent 必须:
text
执行 drag / scroll
↓
重新截图
↓
观察界面是否变化
↓
判断是否达到目标
这才是完整闭环。
二十一、源码中的 eval 风险
和鼠标点击分支一样,drag 和 scroll 分支里也使用了:
python
eval(start_box)
eval(end_box)
这用于把字符串形式的 box 转成 Python list。源码中的 drag/select 分支和 scroll 分支都使用了 eval 来解析 box 字符串。
这在研究代码中很方便,但在真实产品中不建议直接使用。
因为 start_box 和 end_box 最初来自模型输出,属于不可信输入。
更安全的方式是:
python
import ast
coords = ast.literal_eval(start_box)
或者自己写严格解析函数:
text
只允许 list 或 tuple;
长度必须是 4;
每个元素必须是 int 或 float;
归一化坐标必须在 0 到 1 之间;
真实坐标不能超出屏幕范围。
对于 drag,还要额外检查:
text
起点和终点不能完全相同;
拖拽距离不能异常过大;
拖拽不能跨越高风险区域;
二十二、产品化建议:封装 drag 执行
如果你要做自己的桌面自动化项目,可以把 drag 封装成独立函数。
例如:
python
def build_drag_code(start_box, end_box, image_width, image_height, duration=1.0):
sx1, sy1, sx2, sy2 = safe_parse_box(start_box)
ex1, ey1, ex2, ey2 = safe_parse_box(end_box)
sx = round(((sx1 + sx2) / 2) * image_width, 3)
sy = round(((sy1 + sy2) / 2) * image_height, 3)
ex = round(((ex1 + ex2) / 2) * image_width, 3)
ey = round(((ey1 + ey2) / 2) * image_height, 3)
validate_coordinate(sx, sy, image_width, image_height)
validate_coordinate(ex, ey, image_width, image_height)
return (
f"pyautogui.moveTo({sx}, {sy})\n"
f"pyautogui.dragTo({ex}, {ey}, duration={duration})"
)
这样可以集中处理:
text
安全解析
坐标还原
边界检查
duration 配置
日志记录
比在一个大函数里直接写所有逻辑更适合产品化。
二十三、产品化建议:增强 scroll 能力
当前源码中的 scroll 比较简单:
text
up → pyautogui.scroll(5)
down → pyautogui.scroll(-5)
如果要做真实产品,可以增强几项。
1. 支持滚动距离
例如:
text
scroll(point='...', direction='down', amount=3)
scroll(point='...', direction='down', amount=10)
2. 支持横向滚动
Prompt 中已经允许 right 和 left,但当前执行层只处理 up/down。
可以考虑映射到:
python
pyautogui.hscroll(...)
或者在特定平台下用 Shift + 滚轮模拟横向滚动。
3. 支持先聚焦再滚动
某些场景可以配置:
text
focus_before_scroll = True
但不要默认点击,以免触发副作用。
4. 支持滚动后验证
例如检测页面是否发生变化:
text
滚动前截图
滚动后截图
对比内容变化
判断是否到达目标
二十四、drag 和 scroll 与传统 RPA 的区别
传统 RPA 的拖拽和滚动通常来自录制:
text
录制时从 A 拖到 B
回放时继续从 A 拖到 B
UI-TARS 的方式是:
text
模型观察当前截图
判断当前滑块、页面或目标区域在哪里
输出 start_box / end_box / direction
执行层还原真实坐标
pyautogui 执行拖拽或滚动
执行后重新截图
两者最终都可能调用:
python
pyautogui.dragTo(...)
pyautogui.scroll(...)
但坐标来源不同。
传统 RPA 的坐标来自人工录制。
UI-TARS 的坐标来自模型对当前屏幕状态的动态理解。
这也是 GUI Agent 与传统坐标脚本的核心区别。
二十五、drag 和 scroll 的调试日志建议
对复杂动作,一定要记录完整日志。
drag 日志可以包含:
json
{
"action_type": "drag",
"start_box": "[0.1562, 0.4630, 0.1562, 0.4630]",
"end_box": "[0.3125, 0.4630, 0.3125, 0.4630]",
"start_screen": [300, 500],
"end_screen": [600, 500],
"duration": 1.0,
"before_screenshot": "step_003_before.png",
"after_screenshot": "step_003_after.png"
}
scroll 日志可以包含:
json
{
"action_type": "scroll",
"start_box": "[0.5, 0.6667, 0.5, 0.6667]",
"screen_position": [960, 720],
"direction": "down",
"amount": -5,
"before_screenshot": "step_004_before.png",
"after_screenshot": "step_004_after.png"
}
这样一旦失败,可以快速判断:
text
起点是否正确?
终点是否正确?
滚动区域是否正确?
方向是否正确?
滚动距离是否合适?
界面是否真的变化?
复杂 GUI 操作没有日志,很难排查。
二十六、从源码看 drag 和 scroll 的设计取舍
UI-TARS 当前源码实现比较轻量。
drag 的设计取舍是:
text
使用 start_box + end_box;
统一取中心点;
固定 duration=1.0;
用 moveTo + dragTo 实现。
scroll 的设计取舍是:
text
使用 start_box 表示滚动区域;
使用 direction 表示方向;
固定滚动量 5 / -5;
只实现 up/down;
不默认先点击滚动区域。
这些设计很适合 demo、研究和基础自动化链路。
但如果要做稳定产品,还需要增强:
text
可配置拖拽速度;
可配置滚动距离;
支持横向滚动;
更安全的坐标解析;
滚动前焦点策略;
执行后视觉验证;
失败重试;
高风险区域拦截。
总结
这篇文章我们分析了 UI-TARS 中 drag、select 和 scroll 的源码实现。
drag/select 的核心链路是:
text
start_box + end_box
↓
分别取中心点
↓
乘以 image_width / image_height
↓
得到 sx, sy, ex, ey
↓
pyautogui.moveTo(sx, sy)
↓
pyautogui.dragTo(ex, ey, duration=1.0)
scroll 的核心链路是:
text
start_box + direction
↓
取 start_box 中心点作为滚动区域
↓
direction=up → pyautogui.scroll(5)
direction=down → pyautogui.scroll(-5)
↓
如果有 x/y,则在指定区域滚动
从源码可以看到,UI-TARS 把复杂 GUI 操作统一成了几个基础动作:
text
拖拽:moveTo + dragTo
滚动:scroll
但真正难的不是 pyautogui 调用本身,而是:
text
模型能不能找准拖拽起点;
模型能不能判断拖拽终点;
模型能不能选对滚动区域;
模型能不能判断滚动方向;
执行后能不能验证状态变化。
所以,drag 和 scroll 是 GUI Agent 从"点按钮"走向"操作复杂界面"的关键能力。