Chapter 06

桌面应用 Agent 开发

控制任意桌面应用——从文件管理器到 IDE,实现跨应用自动化

桌面应用控制基础

为什么桌面自动化更复杂?

与 Web 应用相比,桌面应用的自动化有几个额外挑战:没有 DOM 结构可以查询、窗口可能被遮挡或不在焦点、需要操作系统级别的 API 才能可靠地控制、不同平台(Windows/macOS/Linux)需要不同的实现。

应用程序启动

import subprocess
import time
import sys


def launch_application(app_path: str, args: list[str] = []) -> subprocess.Popen:
    """启动应用程序并等待其加载"""
    if sys.platform == "darwin":  # macOS
        cmd = ["open", "-a", app_path] + args
    elif sys.platform == "win32":  # Windows
        cmd = [app_path] + args
    else:  # Linux
        cmd = [app_path] + args

    process = subprocess.Popen(cmd)
    time.sleep(2)  # 等待应用启动
    return process


# 常用应用启动示例
def open_in_vscode(path: str):
    launch_application("code", [path])

def open_terminal():
    if sys.platform == "darwin":
        subprocess.Popen(["open", "-a", "Terminal"])
    elif sys.platform == "linux":
        subprocess.Popen(["gnome-terminal"])

窗口管理

pygetwindow:跨平台窗口控制

pip install pygetwindow  # Windows/macOS
# Linux 需要额外的 X11 支持
pip install ewmh  # Linux EWMH 支持
import pygetwindow as gw
import time


class WindowManager:
    """跨平台窗口管理工具"""

    def find_window(self, title_contains: str) -> Optional[object]:
        """通过标题查找窗口"""
        windows = gw.getWindowsWithTitle(title_contains)
        if not windows:
            return None
        return windows[0]

    def focus_window(self, window) -> bool:
        """将指定窗口带到前台并聚焦"""
        try:
            window.activate()
            time.sleep(0.3)  # 等待窗口激活
            return True
        except Exception:
            return False

    def maximize_window(self, window):
        """最大化窗口"""
        window.maximize()
        time.sleep(0.3)

    def get_window_position(self, window) -> tuple[int, int, int, int]:
        """获取窗口位置和大小 (x, y, width, height)"""
        return window.left, window.top, window.width, window.height

    def list_all_windows(self) -> list[str]:
        """列出所有可见窗口的标题"""
        return [w.title for w in gw.getAllWindows() if w.title]

macOS Accessibility API

使用 applescript 控制 macOS 应用

import subprocess


def run_applescript(script: str) -> str:
    """执行 AppleScript 并返回结果"""
    result = subprocess.run(
        ["osascript", "-e", script],
        capture_output=True,
        text=True
    )
    return result.stdout.strip()


# 常用 AppleScript 操作
def activate_app_macos(app_name: str):
    """激活 macOS 应用程序"""
    run_applescript(f'tell application "{app_name}" to activate')
    time.sleep(0.5)

def get_frontmost_app_macos() -> str:
    """获取当前最前台的应用名称"""
    return run_applescript(
        'tell application "System Events" to get name of first process whose frontmost is true'
    )

def click_menu_item_macos(app_name: str, menu: str, item: str):
    """点击 macOS 应用的菜单项"""
    script = f"""
    tell application "{app_name}" to activate
    tell application "System Events"
        tell process "{app_name}"
            click menu item "{item}" of menu "{menu}" of menu bar 1
        end tell
    end tell
    """
    run_applescript(script)


# 使用示例:打开 VS Code 的命令面板
activate_app_macos("Visual Studio Code")
click_menu_item_macos("Code", "View", "Command Palette...")

系统对话框处理

文件打开/保存对话框

import pyautogui
import time


def handle_file_dialog(file_path: str, dialog_type: str = "open") -> bool:
    """
    处理系统文件对话框(打开或保存)。

    Args:
        file_path: 要打开或保存的文件路径
        dialog_type: "open" 或 "save"
    """
    time.sleep(1)  # 等待对话框出现

    if sys.platform == "darwin":  # macOS
        # 使用 AppleScript 直接在对话框中输入路径
        script = f"""
        tell application "System Events"
            keystroke "g" using {{command down, shift down}}
            delay 0.5
            keystroke "{file_path}"
            keystroke return
        end tell
        """
        run_applescript(script)
    elif sys.platform == "win32":  # Windows
        # 在地址栏输入路径
        pyautogui.typewrite(file_path, interval=0.05)
        pyautogui.press("enter")

    time.sleep(0.5)
    return True

跨应用工作流示例

实战:Excel 数据处理工作流

"""
典型的跨应用工作流:
1. 打开邮件附件(Excel 文件)
2. 在 Excel 中处理数据
3. 将结果粘贴到内部 Web 系统
"""

async def process_excel_to_web_system(excel_path: str) -> None:
    task = f"""
    请执行以下工作流:

    1. 打开文件:{excel_path}
    2. 在 Excel/Numbers 中:
       - 找到 A 列(姓名)和 B 列(金额)
       - 计算 B 列的总和(放入 B 最后一行下方)
       - 截图确认数据正确

    3. 打开浏览器,导航到 http://internal.company.com/orders
    4. 在"批量导入"功能中:
       - 点击"新建导入"
       - 将 Excel 中的数据逐行录入表单
       - 在"备注"字段填写今天的日期

    5. 提交并确认成功
    """
    await run_computer_use_agent(task)
桌面应用操作的安全注意事项

桌面应用 Agent 拥有极大的权限,请特别注意:不要授权 Agent 在生产环境直接操作(先在测试环境验证);涉及文件删除/修改的操作要有备份;涉及金融操作(如转账)要增加人工确认步骤;运行 Agent 时全程监视屏幕。

窗口识别与 UI Automation

Windows UI Automation API

Windows 提供了 UI Automation(简称 UIA)API,可以用编程方式访问 Windows 应用的 UI 元素,类似于 Web 的 DOM。这比纯视觉操作更精确,但需要应用支持 UIA 接口:

# pip install pywinauto  # Windows 专用库
from pywinauto import Application, Desktop
from pywinauto.findwindows import ElementNotFoundError
import time


class WindowsUIController:
    """Windows UI Automation 控制器(仅 Windows 平台)"""

    def find_and_control_app(self, title_contains: str):
        """连接到已运行的应用程序"""
        try:
            app = Application(backend="uia").connect(title_re=f".*{title_contains}.*")
            return app
        except ElementNotFoundError:
            return None

    def click_button_by_name(self, app, window_title: str, button_name: str):
        """
        通过按钮名称(Accessibility Name)点击按钮。
        比坐标点击更可靠——即使 UI 布局变化,只要名称不变就能点到。
        """
        window = app.window(title_re=f".*{window_title}.*")
        button = window.child_window(title=button_name, control_type="Button")
        button.click_input()  # click_input() 模拟真实点击,wrapper_object().click() 是 API 调用

    def fill_form_field(self, app, window_title: str, field_name: str, value: str):
        """
        通过字段名称填写表单。
        先获取 Edit 控件,然后设置文本值。
        """
        window = app.window(title_re=f".*{window_title}.*")
        edit = window.child_window(title=field_name, control_type="Edit")
        edit.set_edit_text(value)

    def get_all_controls(self, app, window_title: str) -> list[str]:
        """
        列出窗口中所有可交互的控件及其名称和类型。
        用于"探索"一个未知应用的 UI 结构。
        """
        window = app.window(title_re=f".*{window_title}.*")
        controls = []
        for ctrl in window.descendants():
            try:
                name = ctrl.window_text()
                ctrl_type = ctrl.element_info().control_type
                if name:
                    controls.append(f"{ctrl_type}: {name}")
            except:
                pass
        return controls

macOS Accessibility API(PyAutoGUI 替代方案)

# macOS 上更可靠的窗口控制方案
import subprocess


class MacOSAccessibilityController:
    """使用 AppleScript 和 Accessibility API 控制 macOS 应用"""

    def get_window_elements(self, app_name: str) -> list:
        """
        获取应用窗口的所有 UI 元素。
        比截图+坐标更精确,特别是当 UI 会动态调整布局时。
        """
        script = f"""
        tell application "System Events"
            tell process "{app_name}"
                set uiElements to entire contents of window 1
                set elementList to {{}}
                repeat with elem in uiElements
                    try
                        set end of elementList to {{class of elem, name of elem, position of elem}}
                    end try
                end repeat
                return elementList
            end tell
        end tell
        """
        result = subprocess.run(
            ["osascript", "-e", script],
            capture_output=True, text=True
        )
        return result.stdout.strip()

    def click_element_by_description(
        self,
        app_name: str,
        element_description: str
    ) -> bool:
        """
        通过描述(如按钮文字)点击元素。
        比坐标点击更鲁棒,适合 UI 频繁更新的应用。
        """
        script = f"""
        tell application "System Events"
            tell process "{app_name}"
                try
                    click button "{element_description}" of window 1
                    return true
                on error
                    try
                        click menu item "{element_description}" of menu bar 1
                        return true
                    end try
                    return false
                end try
            end tell
        end tell
        """
        result = subprocess.run(
            ["osascript", "-e", script],
            capture_output=True, text=True
        )
        return "true" in result.stdout.lower()

常见桌面应用的操作模式

Excel/Numbers 操作

async def manipulate_excel(
    agent_controller,
    file_path: str,
    task_description: str
) -> str:
    """
    让 Computer Use Agent 操作 Excel 文件。
    相比直接用 openpyxl 读写,适合需要执行宏、
    使用 Excel 特殊功能(如数据透视表)的场景。
    """
    # 首先通过 bash 工具检查文件是否存在
    check_task = f"""
    请检查文件 {file_path} 是否存在,如果存在请打开它。
    使用 bash 工具执行:ls -la "{file_path}"
    如果文件存在(返回码为 0),使用 computer 工具打开该文件。
    打开后执行以下任务:{task_description}
    """
    return await agent_controller.run_task(check_task)

# 常见 Excel 操作的提示词模板
EXCEL_TASK_TEMPLATES = {
    "sum_column": """在 Excel 中:
1. 截图查看当前状态
2. 找到 {column_name} 列
3. 在该列最后一行的下一行输入 SUM 公式
4. 确认公式计算结果正确
5. 按 Ctrl+S 保存文件""",

    "filter_and_export": """在 Excel 中:
1. 截图查看数据结构
2. 在 {column} 列上应用筛选,条件:{condition}
3. 全选筛选后的数据(Ctrl+A)
4. 复制(Ctrl+C)
5. 打开新的 Excel 文件
6. 粘贴数据(Ctrl+V)
7. 保存为 {output_file}"""
}
何时用 Accessibility API,何时用视觉操作

决策原则:如果目标应用支持 Accessibility API(Windows UIA 或 macOS NSAccessibility),优先使用 API 方式操作——更可靠、更快、不受 UI 布局变化影响。当 API 方式不可行(如老旧应用、游戏、自定义渲染引擎),才回退到视觉+坐标操作。Computer Use 的截图操作是"万能后备",不是首选方案。

章节小结

本章讲解了桌面应用 Agent 的核心技术和模式。关键要点: