OpenCLI 食用指北

0 阅读6分钟

简介

让任何网站成为你的 CLI,零配置,AI 友好。 OpenCLI 是一个将网站转换为 CLI 命令的工具,专为 AI Agent 设计。搭配OpenClaw食用风味更佳。

核心特性

  • 零配置 - 无需 Playwright/Puppeteer,复用已登录的 Chrome
  • AI 友好 - 输出结构化数据,适合 AI 解析
  • 反检测 - 内置 stealth.js,避免被网站检测
  • 扩展系统 - 支持自定义插件

适用场景

场景说明
数据抓取从网站提取结构化数据
自动化操作自动填写表单、提交数据
AI Agent为 AI 提供浏览器操作能力
网站监控定期检查网站状态

架构

架构示意

┌─────────────────────────────────────────────────────────────────┐
                        Chrome 浏览器                             
├──────────────────────────────┬──────────────────────────────────┤
   用户浏览器窗口                   Automation Window             
   (正常使用)                      (OpenCLI 管理)                 
                                                                
  ┌─────────────────────┐       ┌─────────────────────────┐     
   Tab: 掘金页面                Tab: data:text/html          
   Tab: GitHub                 Tab: Google搜索                 
   Tab: Google                 Tab: ...                     
  └─────────────────────┘       └─────────────────────────┘     
   OpenClaw Browser 操作这里   |                                  
   用户可见,正常交互               focused: false (不抢焦点)       
                                 30秒无操作自动关闭                
└──────────────────────────────┴──────────────────────────────────┘
                                          
                                           CDP WebSocket
                                          
                              ┌─────────────────────────┐
                                 OpenCLI Daemon        
                                 (Node.js, :19825)     
                              └─────────────────────────┘
                                          
                                          
                                          
                              ┌─────────────────────────┐
                                 OpenCLI CLI           
                                 (命令行工具)            
                              └─────────────────────────┘

核心组件

组件说明
CLI命令行工具,用户交互入口
DaemonHTTP + WebSocket 服务,默认端口 19825
Browser BridgeChrome 扩展,连接浏览器和 Daemon
Automation Window独立的 Chrome 窗口,OpenCLI 操作区域

关键特性

Automation Window

// OpenCLI 创建独立窗口
chrome.windows.create({
  url: 'data:text/html,<html></html>',
  focused: false,  // 不抢焦点
  width: 1280,
  height: 900,
  type: 'normal'
});
  • 隔离性:不干扰用户的正常浏览
  • 安全性:用户数据不会被意外修改
  • 自动清理:30 秒无操作自动关闭

安装

1. 安装 CLI

npm install -g @jackwener/opencli

2. 安装 Browser Bridge 扩展

  1. 下载 OpenCLI Browser Bridge
  2. 打开 chrome://extensions/
  3. 启用「开发者模式」
  4. 点击「加载已解压的扩展程序」
  5. 选择扩展目录

3. 验证安装

opencli doctor

期望输出:

[OK] Daemon: running on port 19825
[OK] Extension: connected (v1.5.5)
[OK] Connectivity: connected in 0.2s

基本命令

查看帮助

opencli --help
opencli <site> --help

列出所有命令

opencli list
opencli <site> list

内置命令示例

# Hacker News
opencli hackernews top --limit 10

# Bilibili
opencli bilibili hot --limit 5

# GitHub
opencli gh repo list --limit 10

# 天气
opencli weather beijing

通用选项

选项说明
--verbose详细输出
--jsonJSON 格式输出
--help显示帮助

插件开发

插件基本结构

~/.opencli/plugins/<plugin-name>/
├── <plugin-name>.ts    # 插件源码
├── <plugin-name>.js    # 编译后的代码
├── package.json        # 依赖
└── opencli-plugin.json # 插件元数据

最小示例

// ~/.opencli/plugins/my-plugin/my-plugin.ts
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'my-site',
  name: 'hello',
  description: '打招呼',
  strategy: Strategy.PUBLIC,
  browser: true,
  args: [
    { name: 'name', type: 'string', default: 'World', help: '名字' }
  ],
  columns: ['message'],
  func: async (page, kwargs) => {
    const name = kwargs.name as string;
    return [{ message: `Hello, ${name}!` }];
  },
});

参数类型

args: [
  { name: 'text', type: 'string', default: '', help: '文本' },
  { name: 'count', type: 'int', default: 10, help: '数量' },
  { name: 'enabled', type: 'boolean', default: false, help: '是否启用' },
  { name: 'url', type: 'string', required: true, help: 'URL' },
]

浏览器操作

func: async (page, kwargs) => {
  // 导航
  await page.goto('https://example.com');
  
  // 等待
  await page.wait(2);  // 2 秒
  
  // 执行 JS
  const title = await page.evaluate('document.title');
  
  // 截图
  await page.screenshot({ path: '/tmp/screenshot.png' });
  
  // 获取 Cookie
  const cookies = await page.getCookies({ domain: 'example.com' });
  
  // Tab 操作
  const tabs = await page.tabs();
  await page.selectTab(0);
  await page.newTab();
  await page.closeTab();
  
  return [{ success: true }];
}

编译插件

# 方法1:使用 opencli 命令
opencli plugin update <plugin-name>

# 方法2:使用 esbuild
esbuild ~/.opencli/plugins/<name>/<name>.ts \
  --outfile=~/.opencli/plugins/<name>/<name>.js \
  --platform=node --format=esm --bundle

插件发现

OpenCLI 自动扫描以下文件:

  • *.yaml - YAML 格式命令定义
  • *.ts - TypeScript 插件(需要编译)
  • *.js - JavaScript 插件

API 参考

IPage 接口

export interface IPage {
  // 导航
  goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void>;
  getCurrentUrl(): Promise<string | null>;
  
  // 执行代码
  evaluate(js: string): Promise<any>;
  
  // 等待
  wait(options: number | WaitOptions): Promise<void>;
  
  // Tab 管理
  tabs(): Promise<any[]>;
  selectTab(index: number): Promise<void>;
  newTab(): Promise<void>;
  closeTab(index?: number): Promise<void>;
  closeWindow(): Promise<void>;
  
  // 截图
  screenshot(options?: ScreenshotOptions): Promise<string>;
  
  // Cookie
  getCookies(options?: { domain?: string; url?: string }): Promise<BrowserCookie[]>;
  
  // 快照
  snapshot(options?: SnapshotOptions): Promise<any>;
  
  // 交互
  click(ref: string): Promise<void>;
  typeText(ref: string, text: string): Promise<void>;
  pressKey(key: string): Promise<void>;
  scrollTo(ref: string): Promise<any>;
  
  // 滚动
  scroll(direction?: string, amount?: number): Promise<void>;
  autoScroll(options?: { times?: number; delayMs?: number }): Promise<void>;
  
  // 表单
  getFormState(): Promise<any>;
  setFileInput(files: string[], selector?: string): Promise<void>;
  
  // 网络
  networkRequests(includeStatic?: boolean): Promise<any[]>;
  installInterceptor(pattern: string): Promise<void>;
  getInterceptedRequests(): Promise<any[]>;
  waitForCapture(timeout?: number): Promise<void>;
  
  // 控制台
  consoleMessages(level?: string): Promise<any>;
}

WaitOptions

interface WaitOptions {
  time?: number;      // 等待时间(秒)
  selector?: string;  // 等待元素出现
  text?: string;      // 等待文本出现
  timeout?: number;   // 超时时间(秒)
}

ScreenshotOptions

interface ScreenshotOptions {
  format?: 'png' | 'jpeg';  // 图片格式
  quality?: number;          // JPEG 质量 (0-100)
  fullPage?: boolean;        // 全页面截图
  path?: string;             // 保存路径
}

page.wait() 时间单位

⚠️ 重要page.wait() 参数单位是 ,不是毫秒!

// 正确 ✅
await page.wait(2);     // 等待 2 秒
await page.wait(3);     // 等待 3 秒

// 错误 ❌
await page.wait(2000);  // 等待 2000 秒(33 分钟)

方法说明

滚动相关

// 向下滚动 500px(默认)
await page.scroll('down', 500);

// 向上滚动
await page.scroll('up', 300);

// 自动滚动(用于长页面截图)
await page.autoScroll({ times: 3, delayMs: 2000 });  // 滚动3次,每次间隔2秒

文件上传

// 设置文件输入框的文件
await page.setFileInput(
  ['/path/to/file1.pdf', '/path/to/file2.pdf'],
  'input[type="file"]'  // 可选选择器
);

网络拦截

// 安装请求拦截器
await page.installInterceptor('/api/**');

// 导航到页面
await page.goto('https://example.com');

// 获取拦截的请求
const requests = await page.getInterceptedRequests();

// 等待请求捕获完成
await page.waitForCapture(5000);  // 等待5秒

控制台消息

// 获取所有控制台消息
const messages = await page.consoleMessages();

// 获取特定级别的消息
const errors = await page.consoleMessages('error');
const warnings = await page.consoleMessages('warning');

窗口管理

// 关闭当前窗口
await page.closeWindow();

调试方法

1. 使用 --verbose

opencli <site> <command> --verbose

2. 使用 DEBUG 环境变量

DEBUG=* opencli <site> <command>

3. 检查连接状态

opencli doctor

4. 创建调试命令

cli({
  site: 'my-site',
  name: 'debug-example',
  func: async (page) => {
    const results = [];
    
    // Step 1
    await page.goto('https://example.com');
    const url = await page.getCurrentUrl();
    results.push({ step: 'navigate', url });
    
    // Step 2
    const title = await page.evaluate('document.title');
    results.push({ step: 'title', title });
    
    // Step 3
    await page.wait(2);
    results.push({ step: 'wait', done: true });
    
    return results;
  }
});

5. 分步执行

问题:一次性 evaluate 太多操作可能失败

解决:分成多个步骤,用 page.wait() 等待

// ❌ 错误:所有操作在一次 evaluate 中
await page.evaluate(`
  (async () => {
    await delay(1000);
    btn.click();
    await delay(1000);
    // ...
  })()
`);

// ✅ 正确:分步执行
await page.evaluate(`btn.click()`);
await page.wait(1);  // 外部等待
const result = await page.evaluate(`checkResult()`);

常见问题

Q: Extension not connected

原因:Browser Bridge 扩展未安装或未启用

解决

  1. 确认扩展已安装
  2. 确认扩展 badge 显示 ON
  3. 重启 Chrome

Q: Tab 操作不生效

原因:OpenCLI 在独立的 Automation Window 操作

解决

  • page.tabs() 返回 Automation Window 的标签页
  • 使用 Browser 工具操作用户窗口的标签页

Q: 截图保存在哪里?

解决:通过 path 参数指定

await page.screenshot({ 
  path: '/path/to/screenshot.png' 
});

Q: page.wait() 等待时间不对

原因:单位是秒,不是毫秒

解决

await page.wait(2);  // 2 秒

Q: Cookie 获取失败

解决:指定 domain 或 url

await page.getCookies({ domain: 'example.com' });
await page.getCookies({ url: 'https://example.com' });

Q: 插件更新后代码没变

原因:.js 文件缓存

解决

rm -f ~/.opencli/plugins/<name>/<name>.js
opencli plugin update <name>

与 Playwright 对比

特性OpenCLIPlaywright
依赖仅 ws 包完整浏览器二进制
连接需要运行中的 Chrome自动启动浏览器
扩展需要 Browser Bridge无需扩展
窗口Automation Window可配置
反检测内置 stealth.js需要插件
AI 友好✅ 设计目标⚠️ 需要适配
Tab 管理Chrome Extension API原生支持

相关链接


文档版本: 2026-04-03