vlm.md
← 所有食谱 · Computer Use · 进阶

文件浏览器导航自动化

让 agent 在 Windows Explorer、macOS Finder 或 Linux 文件管理器中自动定位文件、执行移动/重命名/删除等操作,基于窗口视觉状态决策。

2026/4/30 · vlm.md · 推荐模型: Claude 3.5 SonnetGPT-4o

场景

Agent 需要在图形文件浏览器中完成任务,例如:

  • 定位特定文件或文件夹
  • 重命名文件
  • 将文件从一个目录移动到另一个目录
  • 删除临时文件

与直接调用 os.rename()shutil.move() 不同,这里的场景是 agent 只能通过屏幕视觉操作 GUI(例如远程桌面、无代码执行权限的环境)。Agent 需要识别文件浏览器的当前路径、可见文件列表,并通过点击、双击、右键菜单完成操作。

推荐模型

模型适用场景
Claude 3.5 Sonnet对文件浏览器 UI 元素(地址栏、文件列表、面板)识别稳定
GPT-4o跨平台(Windows/macOS/Linux)泛化性好

两者差距不大,任选其一。如果目标是跨平台场景,GPT-4o 更稳定。

Prompt 模板

你是一个操作文件浏览器的 computer-use agent。

请分析截图中的文件浏览器窗口,以 JSON 返回:

{
  "platform": "windows" | "macos" | "linux" | "unknown",
  "current_path": "地址栏显示的当前路径,如 /Users/alice/Documents",
  "visible_items": [
    {"name": "文件或文件夹名", "type": "file" | "folder", "is_selected": true 或 false}
  ],
  "target_visible": true 或 false,
  "target_item": "目标文件/文件夹名,如果可见的话",
  "needs_scroll": true 或 false,
  "needs_show_hidden": false,
  "next_action": {
    "type": "double_click" | "right_click" | "click" | "type" | "key" | "scroll" | "wait" | "done" | "ask_human",
    "target": "操作目标描述",
    "coordinate": [x, y],
    "text": "输入文字(如果 type)",
    "key": "按键名(如果 key)",
    "reason": "操作理由"
  }
}

任务目标:{goal}

代码示例

import base64
import io
import json
import subprocess
import sys
import time

import mss
import pyautogui
from PIL import Image
from openai import OpenAI

client = OpenAI()


def take_screenshot() -> str:
    """截图并返回 base64 编码的 PNG 字符串。"""
    with mss.mss() as sct:
        monitor = sct.monitors[1]
        shot = sct.grab(monitor)
        img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
        buf = io.BytesIO()
        img.save(buf, format="PNG")
        return base64.b64encode(buf.getvalue()).decode()


def open_file_browser(path: str = None) -> None:
    """打开系统文件浏览器,可选指定起始路径。"""
    if sys.platform == "darwin":
        cmd = ["open", path or "~"]
    elif sys.platform == "win32":
        cmd = ["explorer", path or "C:\\"]
    else:
        # Linux:尝试 nautilus / thunar / nemo
        for fm in ("nautilus", "thunar", "nemo", "dolphin"):
            try:
                subprocess.Popen([fm, path or str(Path.home())])
                return
            except FileNotFoundError:
                continue
        raise RuntimeError("未找到可用的文件管理器")
    subprocess.Popen(cmd)
    time.sleep(1.5)  # 等待窗口打开


SYSTEM_PROMPT = """你是操作文件浏览器的 computer-use agent。
分析截图中的文件浏览器并以 JSON 返回操作决策。只输出 JSON,不要有多余文字。"""


def analyze_and_decide(screenshot_b64: str, goal: str, history: list[dict]) -> dict:
    """调用 VLM 分析文件浏览器状态并决定下一步操作。"""
    history_text = "\n".join(
        f"步骤 {i+1}: {json.dumps(h, ensure_ascii=False)}" for i, h in enumerate(history)
    )

    prompt = f"""分析文件浏览器截图,以 JSON 返回:

{{
  "platform": "windows" | "macos" | "linux" | "unknown",
  "current_path": "当前路径",
  "visible_items": [{{"name": "名称", "type": "file|folder", "is_selected": false}}],
  "target_visible": true 或 false,
  "needs_scroll": true 或 false,
  "needs_show_hidden": false,
  "next_action": {{
    "type": "double_click|right_click|click|type|key|scroll|wait|done|ask_human",
    "target": "目标描述",
    "coordinate": [x, y],
    "text": "输入内容(如适用)",
    "key": "按键(如适用)",
    "reason": "操作理由"
  }}
}}

任务目标:{goal}

已执行的操作历史:
{history_text if history else "(无)"}

只输出 JSON。"""

    response = client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:image/png;base64,{screenshot_b64}"},
                    },
                    {"type": "text", "text": prompt},
                ],
            },
        ],
        max_tokens=1024,
    )
    return json.loads(response.choices[0].message.content)


def execute_file_action(action: dict) -> str:
    """执行文件浏览器操作,返回操作描述。"""
    t = action.get("type")
    coord = action.get("coordinate")

    if t == "double_click" and coord:
        pyautogui.doubleClick(coord[0], coord[1])
        return f"双击 ({coord[0]}, {coord[1]})"

    elif t == "right_click" and coord:
        pyautogui.rightClick(coord[0], coord[1])
        time.sleep(0.3)
        return f"右键 ({coord[0]}, {coord[1]})"

    elif t == "click" and coord:
        pyautogui.click(coord[0], coord[1])
        return f"点击 ({coord[0]}, {coord[1]})"

    elif t == "type":
        pyautogui.typewrite(action["text"], interval=0.05)
        return f"输入: {action['text']!r}"

    elif t == "key":
        pyautogui.press(action["key"])
        return f"按键: {action['key']}"

    elif t == "scroll":
        direction = action.get("direction", "down")
        clicks = action.get("clicks", 5)
        if coord:
            pyautogui.scroll(clicks if direction == "up" else -clicks, x=coord[0], y=coord[1])
        else:
            pyautogui.scroll(clicks if direction == "up" else -clicks)
        return f"滚动 {direction}"

    elif t == "wait":
        duration = action.get("duration", 1)
        time.sleep(duration)
        return f"等待 {duration}s"

    elif t == "done":
        return "DONE"

    elif t == "ask_human":
        print(f"\n[需要人工介入] {action.get('reason')}")
        input("请处理后按 Enter 继续...")
        return "人工介入完成"

    return f"未知操作: {t}"


def navigate_file_browser(goal: str, start_path: str = None, max_steps: int = 25) -> None:
    """
    打开文件浏览器并让 agent 自动完成文件导航任务。

    参数:
        goal: 任务描述,例如 "找到 ~/Documents/report.pdf 并重命名为 report_final.pdf"
        start_path: 起始路径,None 表示默认位置
        max_steps: 最大操作步数
    """
    print(f"任务: {goal}")
    open_file_browser(start_path)

    history: list[dict] = []

    for step in range(1, max_steps + 1):
        print(f"\n--- 步骤 {step} ---")

        screenshot = take_screenshot()
        result = analyze_and_decide(screenshot, goal, history)

        print(f"当前路径: {result.get('current_path')}")
        print(f"目标可见: {result.get('target_visible')} | 需滚动: {result.get('needs_scroll')}")

        next_action = result.get("next_action", {})
        print(f"下一步: {next_action.get('type')} — {next_action.get('reason')}")

        if next_action.get("type") == "done":
            print("\n任务完成!")
            break

        desc = execute_file_action(next_action)
        history.append({"step": step, "action": next_action, "desc": desc})
        time.sleep(0.6)
    else:
        print(f"\n已达最大步数 ({max_steps})。")


if __name__ == "__main__":
    from pathlib import Path
    navigate_file_browser(
        goal="在 Downloads 文件夹中找到名为 'report.pdf' 的文件,将其移动到 Documents 文件夹",
        start_path=str(Path.home() / "Downloads"),
    )

安装依赖:

pip install openai mss pillow pyautogui

踩坑记录

坑 1:文件排序方式影响目标文件是否可见

文件浏览器默认按名称排序,但也可能按修改时间、大小或类型排序。如果目标文件排在列表中部或末尾,窗口默认视图可能看不到它。Agent 如果没有滚动就找不到文件,会误报”文件不存在”。

解决方案:在 prompt 中加入 needs_scroll 字段,VLM 判断目标文件是否不在当前可见区域。如果 needs_scrolltrue,先执行滚动,再重新截图判断。也可以让 agent 使用文件浏览器的搜索功能(Cmd+F 或 Ctrl+F)直接定位,避免依赖滚动。

坑 2:隐藏文件默认不显示

macOS 和 Linux 的配置文件通常以 . 开头(.bashrc.env.ssh/),Windows 也有系统隐藏文件。文件浏览器默认不显示这些文件,agent 会误以为它们不存在。

解决方案:在 prompt 中加入 needs_show_hidden 字段。如果任务目标文件名以 . 开头或明确是配置文件,VLM 应将该字段设为 true,agent 在继续前先开启”显示隐藏文件”(macOS: Cmd+Shift+. ,Windows: 视图 → 隐藏项目,Linux: Ctrl+H)。

坑 3:网络驱动器和符号链接视觉上与普通文件夹相同

网络共享目录(SMB、NFS)和符号链接在 Finder/Explorer 中看起来和普通文件夹几乎一模一样,只有图标有细微差异。Agent 如果对网络驱动器执行”移动”操作,可能触发跨网络传输,速度极慢且可能失败;对符号链接执行”删除”只会删除链接本身,不影响原文件,但 agent 可能误以为任务失败(文件依然存在于原路径)。

解决方案:对于移动、删除等破坏性操作,在 prompt 中要求 VLM 说明目标是否可能是网络驱动器或符号链接,如果不确定则 recommended_action 设为 "ask_human"