AI Developer Blog

AI Agent远程控制电脑完整方案:4种方法对比与实战

May 1, 2026

文件上传是浏览器自动化中最让人头疼的功能之一。

当你在页面上看到一个漂亮的"上传文件"按钮,点击它会弹出系统文件选择对话框。这个对话框,CDP协议是无法操作的。

本文介绍4种经过实战验证的文件上传方案。

方案1:CDP Page.setInputFiles(最推荐)

原理

CDP提供了Page.setInputFiles命令,可以直接设置<input type="file">元素的文件,绕过系统对话框。

前置条件

代码实现


// cdp-upload.ts
import puppeteer from 'puppeteer';

async function uploadFileWithCDP(page: Page, filePath: string) {
  // 方案A:使用input[type="file"]选择器
  const inputElement = await page.$('input[type="file"]');
  if (inputElement) {
    const elementId = await inputElement.boundingBox();
    await (inputElement as any).uploadFile(filePath);
    return;
  }
  
  // 方案B:使用page.uploadFile(更可靠)
  await page.uploadFile('input[type="file"]', filePath);
}

# Python + Playwright
from playwright.sync_api import Page

def upload_file(page: Page, selector: str, file_path: str):
    """
    使用Playwright上传文件
    selector: input[type="file"]的选择器
    """
    page.set_input_files(selector, file_path)

问题:隐藏的input元素

很多网页的上传按钮是自定义的UI,真正的input被隐藏了:


<!-- 常见模式1: display:none -->
<input type="file" style="display:none">

<!-- 常见模式2: opacity:0 -->
<input type="file" style="opacity:0; position:absolute">

<!-- 常见模式3: 父元素隐藏 -->
<div style="display:none">
  <input type="file">
</div>

对于隐藏的input,需要先让它可见,或者直接用JS定位:


// 定位隐藏的input并上传
const hiddenInput = await page.evaluateHandle(() => {
  const inputs = document.querySelectorAll('input[type="file"]');
  return inputs[0]; // 或者更精确的选择逻辑
});

await hiddenInput.uploadFile('/path/to/file.pdf');

多文件上传


// 上传多个文件
await page.setInputFiles('input[type="file"]', [
  '/path/to/file1.pdf',
  '/path/to/file2.pdf',
  '/path/to/file3.pdf'
]);

方案2:本地HTTP服务器(适合大量文件)

原理

启动一个本地HTTP服务器,托管要上传的文件,然后让网页从这个服务器拉取文件。

代码实现


# local_server.py
import http.server
import socketserver
import os
from pathlib import Path

class UploadHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        self.upload_dir = kwargs.pop('upload_dir', '.')
        super().__init__(*args, **kwargs)
    
    def do_POST(self):
        # 处理文件上传到服务器的请求
        content_length = int(self.headers['Content-Length'])
        file_data = self.rfile.read(content_length)
        
        filename = self.headers['X-Filename']
        filepath = os.path.join(self.upload_dir, filename)
        
        with open(filepath, 'wb') as f:
            f.write(file_data)
        
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'OK')

def start_server(port=8765, upload_dir='/tmp/uploads'):
    os.makedirs(upload_dir, exist_ok=True)
    
    with socketserver.TCPServer(
        ("", port), 
        lambda *args, **kwargs: UploadHTTPRequestHandler(*args, directory=upload_dir, **kwargs)
    ) as httpd:
        print(f"Serving at http://localhost:{port}")
        httpd.serve_forever()

在浏览器中使用


// 通过拖放API上传
const fileUrl = 'http://localhost:8765/myfile.pdf';

await page.evaluateHandle(async (url) => {
  const response = await fetch(url);
  const blob = await response.blob();
  
  // 创建DataTransfer
  const dataTransfer = new DataTransfer();
  const file = new File([blob], 'myfile.pdf', { type: 'application/pdf' });
  dataTransfer.items.add(file);
  
  // 找到drop zone
  const dropZone = document.querySelector('.upload-dropzone');
  
  // 触发drop事件
  const dropEvent = new DragEvent('drop', {
    bubbles: true,
    dataTransfer: dataTransfer
  });
  dropZone.dispatchEvent(dropEvent);
}, fileUrl);

方案3:xdotool模拟键盘(适合系统对话框)

原理

当无法用CDP直接上传时,可以用xdotool模拟键盘输入文件路径,然后按Enter。

代码实现


# xdotool_upload.py
import subprocess
import time

def upload_with_xdotool(file_path: str, delay: float = 0.5):
    """
    使用xdotool模拟文件对话框输入
    """
    # 聚焦到文件输入框(通常是对话框打开后自动聚焦的)
    subprocess.run(['xdotool', 'key', 'ctrl+l'], check=False)  # 聚焦地址栏
    
    time.sleep(delay)
    
    # 输入文件路径
    subprocess.run(['xdotool', 'type', file_path], check=False)
    
    time.sleep(delay)
    
    # 按Enter确认
    subprocess.run(['xdotool', 'key', 'Return'], check=False)

更复杂的场景


# 处理文件选择对话框
def upload_file_dialog(file_path: str):
    """
    使用xdotool处理GTK/Qt文件选择对话框
    """
    time.sleep(0.5)
    
    # 清空现有路径
    subprocess.run(['xdotool', 'key', 'ctrl+a', 'Delete'], check=False)
    
    time.sleep(0.2)
    
    # 输入完整路径
    subprocess.run(['xdotool', 'type', '--', file_path.replace(' ', '\\ ')], check=False)
    
    time.sleep(0.3)
    
    # 按Enter打开/确认
    subprocess.run(['xdotool', 'key', 'Return'], check=False)

方案4:DataTransfer API(适合拖放上传)

原理

对于使用拖放区域的上传组件,可以直接用JS创建DataTransfer对象并触发drop事件。

代码实现


// drag_drop_upload.js
async function uploadViaDropZone(dropZoneSelector: string, filePath: string) {
  // 读取文件为ArrayBuffer
  const response = await fetch(`file://${filePath}`);
  const blob = await response.blob();
  
  // 获取文件名
  const fileName = filePath.split('/').pop();
  
  // 创建DataTransfer
  const dataTransfer = new DataTransfer();
  const file = new File([blob], fileName, { type: blob.type || 'application/octet-stream' });
  dataTransfer.items.add(file);
  
  // 触发drop事件
  const dropZone = document.querySelector(dropZoneSelector);
  const dropEvent = new DragEvent('drop', {
    bubbles: true,
    cancelable: true,
    dataTransfer: dataTransfer,
    clientX: dropZone.getBoundingClientRect().left + 50,
    clientY: dropZone.getBoundingClientRect().top + 50
  });
  
  dropZone.dispatchEvent(dropEvent);
}

处理带API的文件上传


// 通过服务器中转
async function uploadViaServer(dropZoneSelector: string, localFilePath: string) {
  // 启动临时HTTP服务器(在另一个进程中)
  const serverUrl = await startTempServer('/path/to/dir');
  
  // 获取服务器上的文件URL
  const fileName = localFilePath.split('/').pop();
  const fileUrl = `${serverUrl}/${fileName}`;
  
  // 使用fetch下载并创建DataTransfer
  await page.evaluateHandle(async (url) => {
    const response = await fetch(url);
    const blob = await response.blob();
    
    const dataTransfer = new DataTransfer();
    const file = new File([blob], url.split('/').pop(), { type: blob.type });
    dataTransfer.items.add(file);
    
    const dropZone = document.querySelector('.dropzone');
    dropZone.dispatchEvent(new DragEvent('drop', {
      bubbles: true,
      dataTransfer: dataTransfer
    }));
  }, fileUrl);
}

方案对比

方案成功率速度适用场景复杂度
CDP setInputFiles~85%标准input元素
本地HTTP服务器~95%拖放上传、大文件
xdotool~70%系统对话框
DataTransfer API~90%拖放区域

推荐工作流


// smart-uploader.ts
async function smartUpload(
  page: Page, 
  options: {
    filePath: string;
    uploadButtonSelector?: string;
    dropZoneSelector?: string;
    fileInputSelector?: string;
  }
) {
  const { filePath, fileInputSelector, dropZoneSelector } = options;
  
  // 尝试方案1:直接用CDP上传
  if (fileInputSelector) {
    try {
      await page.setInputFiles(fileInputSelector, filePath);
      console.log('✓ CDP upload成功');
      return;
    } catch (e) {
      console.log('CDP upload失败,尝试其他方案...');
    }
  }
  
  // 尝试方案2:DataTransfer API
  if (dropZoneSelector) {
    try {
      await page.evaluateHandle(
        (dz, fp) => uploadViaDropZone(dz, fp),
        dropZoneSelector,
        filePath
      );
      console.log('✓ DataTransfer upload成功');
      return;
    } catch (e) {
      console.log('DataTransfer upload失败,尝试其他方案...');
    }
  }
  
  // 尝试方案3:隐藏input + uploadFile
  try {
    await page.evaluateHandle(
      (fp) => {
        const input = document.querySelector('input[type="file"]') as HTMLInputElement;
        if (input) {
          // 临时让input可见
          input.style.display = 'block';
          input.style.opacity = '1';
          input.style.position = 'static';
          (input as any).uploadFile(fp);
        }
      },
      filePath
    );
    console.log('✓ Hidden input upload成功');
    return;
  } catch (e) {
    console.log('所有方案都失败了');
    throw new Error('Upload failed');
  }
}

相关开源项目