9.1 KiB
Raw Blame History

name description tags version created
browser-automation 补充浏览器自动化缺失能力 — 等待元素、文件上传、多标签页、下载拦截
browser
automation
cdp
utility
1 2026-06-03

Browser Automation Extensions

补充内置浏览器工具缺失的高级能力。

1. 等待元素 (wait_for_element)

内置工具无显式等待,用轮询实现:

from hermes_tools import terminal
import time

def wait_for_element(selector, timeout=10, poll_interval=0.5):
    """等待元素出现在DOM中"""
    start = time.time()
    while time.time() - start < timeout:
        result = terminal(f"""
            node -e "
                const el = document.querySelector('{selector}');
                console.log(el ? 'found' : 'not_found');
            "
        """)
        if 'found' in result.get('output', ''):
            return True
        time.sleep(poll_interval)
    return False

# 使用示例
if wait_for_element('.loading-spinner', timeout=15):
    # 元素出现后继续操作
    pass

适用场景: SPA动态加载、异步渲染完成后操作

2. 文件上传 (upload_file)

通过CDP的DOM.setFileInputFiles实现:

from hermes_tools import terminal

def upload_file(selector, file_path):
    """上传文件到input[type=file]"""
    # 1. 获取元素的backendNodeId
    node_info = terminal(f"""
        node -e "
            const el = document.querySelector('{selector}');
            if (!el) {{ console.log('ERROR: element not found'); process.exit(1); }}
            // CDP会注入__cdpBinding
            if (typeof __cdpBinding !== 'undefined') {{
                __cdpBinding.getNodeId(el).then(id => console.log(id));
            }} else {{
                console.log('CDP_NOT_AVAILABLE');
            }}
        "
    """)
    
    # 2. 通过browser_cdp直接调用
    # 需要先获取target_id,然后调用DOM.setFileInputFiles
    terminal(f"""
        hermes browser cdp DOM.setFileInputFiles \\
            --params '{{"files": ["{file_path}"], "backendNodeId": NODE_ID}}'
    """)

简化版 (直接用browser_cdp工具):

# 假设已通过browser_snapshot获取input元素的ref
# 用browser_console获取backendNodeId
result = browser_console(expression="""
    (function() {
        const el = document.querySelector('input[type=file]');
        return el ? el.getAttribute('data-node-id') || 'need_cdp' : null;
    })()
""")

# 然后调用browser_cdp
browser_cdp(
    method='DOM.setFileInputFiles',
    params={'files': ['/path/to/file.pdf'], 'backendNodeId': 12345}
)

注意: 需要先从DOM获取backendNodeId,可通过DOM.getDocument + DOM.querySelector链式调用

3. 多标签页管理 (manage_tabs)

通过CDP的Target API:

from hermes_tools import terminal

def list_tabs():
    """列出所有标签页"""
    result = terminal("hermes browser cdp Target.getTargets --params '{}'")
    # 解析JSON获取target列表
    return result

def switch_tab(target_id):
    """切换到指定标签页"""
    terminal(f"hermes browser cdp Target.attachToTarget --params '{{\"targetId\": \"{target_id}\"}}'")

def create_tab(url):
    """创建新标签页"""
    result = terminal(f"hermes browser cdp Target.createTarget --params '{{\"url\": \"{url}\"}}'")
    # 返回targetId
    return result

# 使用示例
tabs = list_tabs()
# 解析tabs获取target_id
switch_tab('TARGET_ID_HERE')

直接用browser_cdp:

# 列出所有标签
targets = browser_cdp(method='Target.getTargets', params={})

# 创建新标签
new_tab = browser_cdp(method='Target.createTarget', params={'url': 'https://example.com'})
target_id = new_tab['targetId']

# 在指定标签执行JS
browser_cdp(
    method='Runtime.evaluate',
    params={'expression': 'document.title', 'returnByValue': True},
    target_id=target_id
)

4. 下载拦截 (download_config)

通过CDP配置下载行为:

def config_download(download_path, allow=True):
    """配置下载目录和行为"""
    browser_cdp(
        method='Page.setDownloadBehavior',
        params={
            'behavior': 'allow' if allow else 'deny',
            'downloadPath': download_path
        }
    )

# 使用示例
config_download('/tmp/downloads', allow=True)

# 然后点击下载链接,文件会保存到指定目录
# 可通过terminal检查文件是否存在

等待下载完成:

import os
import time

def wait_for_download(filename, timeout=30):
    """等待下载完成"""
    download_path = '/tmp/downloads'
    filepath = os.path.join(download_path, filename)
    start = time.time()
    
    while time.time() - start < timeout:
        if os.path.exists(filepath) and not os.path.exists(filepath + '.crdownload'):
            return filepath
        time.sleep(0.5)
    return None

5. 简单验证码识别 (captcha_ocr)

用vision_analyze识别简单验证码图片:

def solve_simple_captcha(image_selector):
    """识别简单验证码(数字/字母)"""
    # 1. 获取图片URL或base64
    img_info = browser_console(expression=f"""
        (function() {{
            const img = document.querySelector('{image_selector}');
            return img ? img.src : null;
        }})()
    """)
    
    # 2. 用vision分析
    result = vision_analyze(
        image_url=img_info,
        question="识别这个验证码图片中的字符,只返回字符本身,不要其他文字"
    )
    
    # 3. 提取识别结果(需要从result解析)
    return result  # 如 "a3Bx"

# 使用示例
captcha_text = solve_simple_captcha('img.captcha')
browser_type(ref='@e15', text=captcha_text)

局限: 复杂验证码(滑块、点选、reCAPTCHA)无法可靠解决

6. 网络请求拦截 (network_intercept)

通过CDP拦截和修改网络请求:

def enable_network_intercept():
    """启用网络拦截"""
    browser_cdp(method='Network.enable', params={})
    browser_cdp(
        method='Network.setRequestInterception',
        params={'patterns': [{'urlPattern': '*'}]}
    )

def mock_api_response(url_pattern, response_data):
    """Mock API响应"""
    # 需要监听Network.requestIntercepted事件
    # 然后用Network.continueInterceptedRequest返回mock数据
    pass  # 复杂场景,通常需要配合脚本

# 简化版:直接修改页面fetch/XHR
browser_console(expression="""
    const originalFetch = window.fetch;
    window.fetch = function(url, opts) {
        if (url.includes('/api/user')) {
            return Promise.resolve({
                json: () => Promise.resolve({name: 'Mock User', id: 123})
            });
        }
        return originalFetch(url, opts);
    };
""")

使用模式

场景1: SPA表单提交后等待结果

browser_click(ref='@e10')  # 点击提交
wait_for_element('.success-message', timeout=10)
browser_snapshot()  # 获取结果

场景2: 多步骤流程跨标签页

# 主标签操作
browser_navigate('https://app.com/dashboard')
browser_click(ref='@e5')  # 打开新标签的链接

# 获取新标签
targets = browser_cdp(method='Target.getTargets', params={})
new_tab_id = [t['targetId'] for t in targets['targetInfos'] if t['url'].endswith('/details')][0]

# 在新标签操作
browser_cdp(
    method='Runtime.evaluate',
    params={'expression': 'document.querySelector(".detail").innerText', 'returnByValue': True},
    target_id=new_tab_id
)

场景3: 文件上传+下载

config_download('/tmp/exports')
upload_file('input[type=file]', '/path/to/data.csv')
browser_click(ref='@e20')  # 点击处理按钮
wait_for_download('result.xlsx', timeout=60)

7. Bricks 框架兼容性

bricks widget 的 click 事件兼容 browser_click。 已验证通过(2026-08-07,pipeline.opencomputing.cn)。

bricks 通过 widget.bind('click', handler) → dom_element.addEventListener('click', handler) 绑定事件,browser_click 通过 CDP Input.dispatchMouseEvent 触发浏览器原生 click 事件合成,addEventListener 回调正常触发。

操作 结果
browser_click Menu 项 ✅ 触发 regen_menuitem_event()
browser_click toggle 按钮 ✅ 触发 idset= sidebar_menu
browser_console 查日志 ✅ 完整捕获

注意事项:

  • 页面 JS 完全加载后才可点击(约 2-3s),否则点击无响应
  • snapshot 中 bricks widget 的文本标签可能显示为 "generic",需 browser_console 查 DOM 确认

注意事项

  1. CDP需要活跃连接 - 确保browser已连接(/browser connect或配置browser.cdp_url)
  2. backendNodeId获取 - 文件上传需要先通过DOM.getDocument获取节点ID
  3. 多标签页状态隔离 - 每个标签页有独立的JS上下文,cookie/session共享但DOM隔离
  4. 下载路径权限 - 确保download_path目录存在且有写权限
  5. 验证码识别准确率 - 简单验证码~80-90%,复杂验证码<50%

调试技巧

# 查看当前CDP连接状态
browser_cdp(method='Browser.getVersion', params={})

# 列出所有可用CDP方法
# 参考: https://chromedevtools.github.io/devtools-protocol/

# 检查网络请求
browser_cdp(method='Network.enable', params={})
# 后续请求会触发事件,可通过browser_console监听