UFO² 之 HostUIExecutor:面向 HostAgent 的 Windows 窗口选择与桌面级 UI 自动化 MCP 动作服务器
【免费下载链接】UFOUFO³: Weaving the Digital Agent Galaxy项目地址: https://gitcode.com/GitHub_Trending/uf/UFO
本篇技术指南聚焦 UFO² 仓库中为HostAgent提供桌面级 UI 自动化能力的动作服务器(Action Server)——HostUIExecutor。它以 MCP(Model Context Protocol)工具的形式对外暴露select_application_window,负责在 Windows 桌面环境中完成窗口发现、焦点切换、最大化与 UI 状态初始化,是 HostAgent 将任务委派给 AppAgent 之前的关键"选窗"环节。读完本文,你将掌握该服务器的工具契约、配置方法、调用模式、错误处理与故障排查,并能结合仓库源码理解其底层实现原理。
服务器定位与基本信息
HostUIExecutor 是 UFO² 中由 HostAgent 侧调用的本地(in-process)动作服务器,提供系统级的 UI 自动化能力:窗口管理、窗口切换以及跨应用交互。它在 HostAgent 工作流中的核心职责是"选中一个应用窗口",使后续的 UICollector 数据采集与 AppUIExecutor 动作执行能够作用于该窗口。
| 属性 | 值 |
|---|---|
| Namespace | HostUIExecutor |
| 服务器名称 | UFO UI HostAgent Action MCP Server |
| 服务器类型 | Action(动作) |
| 部署方式 | Local(进程内) |
| 服务 Agent | HostAgent |
| LLM 可选择 | ✅ 是(由 LLM 决定何时执行) |
| 平台 | Windows |
| 后端 | UIAutomation(UIA)或 Win32 |
| 工具类型 | action |
| 工具键格式 | action::{tool_name} |
从源码看,该服务器由 ui_mcp_server.py 中的create_host_action_mcp_server工厂函数创建,通过@MCPRegistry.register_factory_decorator("HostUIExecutor")注册到 MCP 注册表,并以FastMCP("UFO UI HostAgent Action MCP Server")实例暴露工具。值得注意的细节是:该模块在非 Windows 平台(如 Linux)上会打印警告并跳过初始化,因为pywinauto等依赖与窗口自动化能力都要求 Windows 环境。
核心工具:select_application_window
select_application_window是 HostUIExecutor 提供的唯一工具,也是 HostAgent 工作流中窗口选择的主入口。它按 ID 与名称查找目标窗口,依次执行以下步骤:
- 按 ID 和名称找到目标窗口;
- 将焦点(focus)设置到该窗口;
- 可选地最大化窗口(取决于
MAXIMIZE_WINDOW); - 可选地在窗口周围绘制红色轮廓(用于调试);
- 为后续 AppAgent 操作初始化 UI 状态。
⚠️前置条件:必须先调用 UICollector 的
get_desktop_app_info获取有效的窗口 ID 与名称,否则无法获得合法参数。
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | str | ✅ 是 | 目标应用窗口的精确标注 ID,必须与get_desktop_app_info返回的 ID 一致 |
name | str | ✅ 是 | 目标应用窗口的精确名称,必须与所选 ID 对应的名称一致 |
在 ui_mcp_server.py 中,这两个参数的 Annotated 描述明确要求 LLM"严格遵守应用信息中 id 字段给出的选项",且 name 必须与所选 id 匹配。工具内部通过_verify_id(id, name, app_window_dict)校验:ID 为空、窗口列表为空、ID 不存在等都会抛出ToolError。
返回值与 WindowInfo 结构
返回值类型为Dict[str, Any]:
{ "root_name": str, # 应用根名称(例如 "WINWORD.EXE") "window_info": dict # WindowInfo 对象,包含窗口详细信息 }其中window_info为WindowInfo结构,其字段与 messages.py 中定义的WindowInfo数据模型对应(源码中由_window2window_info将pywinauto的UIAWrapper转换为该结构,见 ui_mcp_server.py):
{ "annotation_id": str, # 窗口标识符 "name": str, # 窗口元素名称 "title": str, # 窗口标题文本 "handle": int, # 窗口句柄 (HWND) "class_name": str, # 窗口类名 "process_id": int, # 进程 ID "is_visible": bool, # 可见状态 "is_minimized": bool, # 最小化状态 "is_maximized": bool, # 最大化状态 "is_active": bool, # 是否为活动窗口 "rectangle": { # 窗口包围矩形 "x": int, "y": int, "width": int, "height": int }, "text_content": str, # 窗口文本 "control_type": str # 控件类型(通常为 "Window") }完整调用示例
# Step 1: 获取桌面可用窗口(UICollector) windows = await computer.run_actions([ MCPToolCall( tool_key="data_collection::get_desktop_app_info", tool_name="get_desktop_app_info", parameters={"remove_empty": True} ) ]) # windows[0].data = [ # {"id": "1", "name": "Calculator", "type": "Window", "kind": "window"}, # {"id": "2", "name": "Notepad", "type": "Window", "kind": "window"} # ] # Step 2: 选中 Calculator 窗口(HostUIExecutor) result = await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", tool_name="select_application_window", parameters={ "id": "1", "name": "Calculator" } ) ]) # 结果示例: { "root_name": "ApplicationFrameHost.exe", "window_info": { "annotation_id": "1", "title": "Calculator", "handle": 12345678, "class_name": "ApplicationFrameWindow", "process_id": 9876, "is_visible": True, "is_minimized": False, "is_maximized": False, "is_active": True, "rectangle": {"x": 100, "y": 100, "width": 400, "height": 600} } }run_actions是 computer.py 中定义的核心执行入口,负责将MCPToolCall列表路由到对应 MCP 服务器执行。
错误处理契约
该工具在以下场景抛出ToolError,错误信息对排查问题具有直接的指导意义:
# 错误 1:缺少 ID ToolError("Window id is required for select_application_window") # 错误 2:无可用窗口 ToolError("No application windows available. Please call get_desktop_app_info first.") # 错误 3:ID 无效 ToolError("Control with id '99' not found. Available control ids: ['1', '2', '3']") # 错误 4:设置焦点失败 ToolError("Failed to set focus on window: {error_details}")这些错误信息与源码中_verify_id的实现一一对应(见 ui_mcp_server.py):前三种属于参数校验失败,第四种则是在执行window.set_focus()、window.maximize()、window.draw_outline()时捕获异常后包装抛出(ui_mcp_server.py)。值得注意的是错误 3 会附带当前可用 ID 列表,帮助 LLM 或开发者纠正参数。
配置行为:MAXIMIZE_WINDOW 与 SHOW_VISUAL_OUTLINE_ON_SCREEN
工具执行时尊重两个系统配置项,它们直接控制窗口选中后的行为:
MAXIMIZE_WINDOW(是否最大化窗口)
# config.yaml MAXIMIZE_WINDOW: true # 窗口选中后自动最大化SHOW_VISUAL_OUTLINE_ON_SCREEN(是否绘制红色轮廓)
# config.yaml SHOW_VISUAL_OUTLINE_ON_SCREEN: true # 在窗口周围绘制红色轮廓需要说明的是:原文档给出的默认值为MAXIMIZE_WINDOW: False、SHOW_VISUAL_OUTLINE_ON_SCREEN: True,而当前仓库的 system.yaml 实际配置为两者均为False("Skip rendering visual outline on screen if not necessary")。以仓库实际配置为准,生产环境默认不绘制轮廓,开发调试时可手动开启。源码中通过configs.get("MAXIMIZE_WINDOW", False)与configs.get("SHOW_VISUAL_OUTLINE_ON_SCREEN", True)读取这些配置(ui_mcp_server.py)。
副作用与内部状态变更
⚠️副作用提示
- ✅改变焦点:将目标窗口带到前台
- ✅可能最大化:若启用了
MAXIMIZE_WINDOW- ✅视觉反馈:若启用了
SHOW_VISUAL_OUTLINE_ON_SCREEN,绘制红色轮廓(源码中为colour="red", thickness=3)- ✅状态初始化:为该窗口设置 AppPuppeteer
执行成功后,共享 UI 状态发生如下变化(对应源码中UIServerState.initialize_for_window,见 ui_mcp_server.py):
ui_state.selected_app_window被设置为该窗口对象;ui_state.puppeteer以AppPuppeteer初始化(构造函数参数为窗口文本与应用根名称);- 可用命令会被记录日志(
Available commands: ...)用于调试; - 后续 UICollector 与 AppUIExecutor 的工具即可作用于该窗口。
UIServerState是单例(Singleton)设计:UICollector、HostUIExecutor、AppUIExecutor 三个服务器共享同一份 UI 状态,从而保证"选窗 → 采集 → 动作"整条链路的状态一致性。UICollector 的get_desktop_app_info会把窗口列表缓存在last_app_windows,select_application_window正是从这份缓存中按 ID 查找窗口。
在 mcp.yaml 中的注册配置
HostUIExecutor 在 mcp.yaml 中作为 HostAgent 的 action 服务器注册:
HostAgent: default: data_collection: - namespace: UICollector type: local start_args: [] reset: false # 切换电脑时是否重置 MCP 服务器状态 action: - namespace: HostUIExecutor type: local start_args: [] reset: false - namespace: CommandLineExecutor type: local start_args: [] reset: false配置项说明
| 配置项 | 类型 | 说明 |
|---|---|---|
namespace | str | 必须为"HostUIExecutor" |
type | str | 部署类型:"local"(进程内) |
start_args | list | 启动参数(local 类型通常为空) |
reset | bool | 任务之间是否重置服务器状态(对 HostUIExecutor 通常为false,因为窗口选择状态需要在流程中保持) |
对比同一配置文件中 AppAgent 的注册(AppUIExecutor+CommandLineExecutor)可以看出:HostAgent 与 AppAgent 共享 UICollector 数据服务器,但动作服务器是分离的——HostUIExecutor 专司"选窗",AppUIExecutor 专司"窗内动作"。
三种典型使用模式
模式一:基础窗口选择流程
这是最标准的 HostAgent 流程——发现窗口 → 选中窗口 → 窗内交互:
# 1. 发现桌面窗口 windows = await computer.run_actions([ MCPToolCall(tool_key="data_collection::get_desktop_app_info", ...) ]) # 2. 选中目标窗口 await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": "1", "name": "Calculator"} ) ]) # 3. 此后 AppAgent 即可与该窗口交互 controls = await computer.run_actions([ MCPToolCall(tool_key="data_collection::get_app_window_controls_info", ...) ])模式二:多窗口工作流(窗口切换)
HostAgent 的核心价值之一就是跨应用协同:先操作 Word,再切换到 Excel:
# 先操作第一个窗口 await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": "1", "name": "Word"} ) ]) # ... 在 Word 上执行动作 ... # 切换到第二个窗口 await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": "2", "name": "Excel"} ) ]) # ... 在 Excel 上执行动作 ...每次调用都会重新初始化 AppPuppeteer 与控件字典,因此窗口切换是干净且隔离的。
模式三:选择前校验(防御式编程)
在调用前先确认目标窗口确实存在,避免向不存在的窗口发起选择:
# 获取窗口列表 windows = await computer.run_actions([ MCPToolCall(tool_key="data_collection::get_desktop_app_info", ...) ]) # 校验目标窗口是否存在 target_windows = [w for w in windows[0].data if "Calculator" in w["name"]] if not target_windows: logger.error("Calculator not found") else: # 选中窗口 await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={ "id": target_windows[0]["id"], "name": target_windows[0]["name"] } ) ])最佳实践
1. 始终使用 get_desktop_app_info 返回的精确 ID 与名称
窗口 ID 是采集阶段动态分配的标注号,硬编码或猜测必然失败:
# ✅ 正确:使用 get_desktop_app_info 返回的精确 ID 和名称 windows = await computer.run_actions([ MCPToolCall(tool_key="data_collection::get_desktop_app_info", ...) ]) window = windows[0].data[0] # 第一个窗口 await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={ "id": window["id"], # 来自响应的精确 ID "name": window["name"] # 来自响应的精确名称 } ) ]) # ❌ 错误:硬编码或猜测 ID await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": "1", "name": "Some Window"} # 可能并不存在 ) ])2. 处理选择失败
对is_error标志进行检查,失败时重试或选择替代窗口:
try: result = await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": window_id, "name": window_name} ) ]) if result[0].is_error: logger.error(f"Failed to select window: {result[0].content}") # 重试或选择其他窗口 else: logger.info(f"Selected window: {result[0].data['root_name']}") except Exception as e: logger.error(f"Window selection exception: {e}")3. 选中后等待窗口就绪
窗口激活存在短暂延迟,选中后先等待再截图或操作:
# 选中窗口 await computer.run_actions([ MCPToolCall(tool_key="action::select_application_window", ...) ]) # 等待窗口变为活动状态 await asyncio.sleep(0.5) # 现在再与窗口交互 await computer.run_actions([ MCPToolCall(tool_key="data_collection::capture_window_screenshot", ...) ])这与仓库 system.yaml 中SLEEP_TIME: 1(每步之间等待窗口就绪的休眠时间)的配置思路一致,说明窗口就绪等待是 UFO² 动作链路中的常规考虑。
4. 用视觉轮廓辅助调试
开发期开启红色轮廓确认选中对象,生产期关闭以减少视觉干扰:
# config.yaml - 开发期开启 SHOW_VISUAL_OUTLINE_ON_SCREEN: true # 在选中窗口上显示红色轮廓 # config.yaml - 生产期关闭 SHOW_VISUAL_OUTLINE_ON_SCREEN: false与 AppAgent 的集成链路
select_application_window成功之后,该窗口即成为AppAgent的操作目标。从源码角度看,这一链条由 host_agent_processing_strategy.py 中的HostActionExecutionStrategy驱动:当 LLM 输出的函数名为select_application_window(类常量SELECT_APPLICATION_COMMAND,见 该文件第 726 行)时,会走_execute_application_selection分支,从返回结果中提取root_name作为selected_application_root,并同步更新selected_target_id等上下文状态,为后续 AppAgent 接管窗口铺路。一个完整的跨 Agent 协作示例:
# HostAgent:选中窗口 host_result = await computer.run_actions([ MCPToolCall( tool_key="action::select_application_window", parameters={"id": "1", "name": "Calculator"} ) ]) # AppAgent:获取选中窗口内的控件 app_controls = await computer.run_actions([ MCPToolCall(tool_key="data_collection::get_app_window_controls_info", ...) ]) # AppAgent:点击选中窗口内的按钮 app_click = await computer.run_actions([ MCPToolCall( tool_key="action::click_input", tool_name="click_input", parameters={"id": "5", "name": "Seven", "button": "left"} ) ])故障排查指南
场景一:窗口未找到
现象:ToolError("Control with id 'X' not found")
解决方案:
- 以
refresh_app_windows=True重新调用get_desktop_app_info,刷新窗口缓存(源码中该参数为 False 时会复用last_app_windows缓存); - 确认窗口未被最小化或隐藏(
remove_empty会过滤掉无可见内容的窗口); - 确认窗口仍然存在(未被关闭)。
场景二:焦点设置失败
现象:ToolError("Failed to set focus on window")
解决方案:
- 检查窗口是否处于禁用或无响应状态;
- 确认窗口对应进程仍在运行;
- 确保没有模态对话框阻塞焦点;
- 稍等片刻后重试。
场景三:选错了同名的窗口
现象:选中了名称相似但并非目标的窗口
解决方案:
- 使用更具体的名称匹配逻辑;
- 结合返回
window_info中的process_id或class_name进行甄别; - 在选中前用额外条件过滤窗口列表。
相关文档导航
- UICollector —— 窗口发现服务器(
get_desktop_app_info的来源,选窗的前置依赖) - AppUIExecutor —— 窗口内交互服务器(选窗后动作的执行者)
- 动作服务器概念 —— Action 服务器的通用概念
- HostAgent 架构 —— HostAgent 的整体架构
- MCP 服务器配置总览 —— 服务器在 mcp.yaml 中的注册方式
总结
HostUIExecutor 虽然只暴露一个工具,却是 UFO² 双 Agent(HostAgent + AppAgent)协作链路上的枢纽:它把"桌面级任意窗口"与"窗内 UI 自动化"这两个层次衔接起来,通过共享单例 UI 状态实现选窗、采集、动作的无缝衔接。理解它的工具契约(精确的 id/name 匹配)、配置行为(MAXIMIZE_WINDOW、SHOW_VISUAL_OUTLINE_ON_SCREEN)与状态副作用,是编写可靠 HostAgent 工作流、排查"窗口选不中""焦点抢不过"等高频问题的前提。结合 ui_mcp_server.py 的源码与 mcp.yaml 的注册配置,你可以在自己的 Windows 环境中快速复现并扩展这套窗口选择能力。
【免费下载链接】UFOUFO³: Weaving the Digital Agent Galaxy项目地址: https://gitcode.com/GitHub_Trending/uf/UFO
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考