OpenCLI 架构深度解析

OpenCLI 架构深度解析

基于源码 v1.7.7 分析,npm 包 @jackwener/opencli,GitHub 17K+ stars

一、OpenCLI 是什么

OpenCLI 是一个开源的 统一 CLI 集成平台,核心理念是把任何网站、Electron 桌面应用、本地 CLI 工具统一变成命令行接口。

覆盖范围

类别 平台(部分)
社交媒体 Twitter/X、小红书、微博、Instagram、Reddit
内容平台 B站、知乎、豆瓣、YouTube、HackerNews、掘金
电商 1688、闲鱼、Amazon
桌面应用 Cursor、ChatGPT、Notion、Discord、Doubao
开发工具 gh、docker、vercel
AI 工具 Gemini、元宝、NotebookLM

90+ 适配器,550+ 命令。

核心特性

  • 零 LLM 开销:运行时不消耗 token,纯确定性执行
  • 账号安全:复用 Chrome 登录态,凭证不离开浏览器
  • AI Agent 原生:为 Claude Code / Cursor / Codex 等设计了 skill 体系
  • 确定性输出:相同命令、相同输出 schema,可管道、可脚本、CI 友好

数据获取方式与优先级

常见误解:OpenCLI 用 Playwright?CLI 直接调网站 API?都不是。

OpenCLI 的数据获取完全依赖 Chrome 浏览器 + Extension 作为执行环境(公开 API 和本地工具除外)。它不使用 Playwright,不从 CLI 进程直接发请求,而是在你已登录的 Chrome 浏览器里操作。

每个适配器在编译时静态声明一种策略,不存在运行时自动降级或瀑布选择。但从适配器设计的角度,有一个清晰的优先级偏好:

sql 复制代码
优先级从高到低:

  PUBLIC(直接 API) > LOCAL(本地工具) > COOKIE(浏览器 DOM/Fetch)
      > HEADER(浏览器认证请求) > INTERCEPT(XHR 拦截) > UI(CDP 直连)

原则:能不开浏览器就不开,必须开时优先读 API 而非 DOM
优先级 策略 获取方式 需要浏览器 典型平台
1 PUBLIC Node.js 直接 fetch 公开 API HackerNews、GitHub Trending
2 LOCAL 调用本地已安装的 CLI/二进制 gh、docker、vercel
3 COOKIE 浏览器开页面,注入 JS 读 DOM 或 fetch 小红书、B站、知乎、Twitter
4 HEADER 浏览器获取认证 Header,带 Token 发请求 需 Bearer Token 的站点
5 INTERCEPT 注入拦截器,捕获页面自身的 API 响应 动态 Token 的 SPA 应用
6 UI CDP 直连 Electron 应用的调试端口 是(CDP) Cursor、Notion、ChatGPT 桌面版

为什么这个优先级有意义

  • PUBLIC/LOCAL(不开浏览器):最快、最稳定,毫秒级返回
  • COOKIE/HEADER(浏览器内操作):需要数秒启动浏览器连接,但复用登录态无需配置
  • INTERCEPT(被动捕获):最灵活但最脆弱,API URL pattern 变了就要更新适配器
  • UI(桌面应用):依赖应用暴露调试端口,版本升级可能 break

二、整体架构:三层通信模型

arduino 复制代码
┌──────────────┐     HTTP POST      ┌──────────────┐    WebSocket     ┌──────────────────┐
│   CLI 进程    │ ──────────────────→│    Daemon     │ ──────────────→ │  Chrome Extension │
│ (opencli xxx) │ ←────────────────── │ :19825       │ ←────────────── │  (Browser Bridge)  │
└──────────────┘   HTTP Response     └──────────────┘    WS Result     └──────────────────┘
                                                                              │
                                                                              │ Chrome Extension API
                                                                              │ (tabs.create, scripting.executeScript,
                                                                              │  debugger.attach)
                                                                              ▼
                                                                       ┌──────────────┐
                                                                       │   Chrome 页面  │
                                                                       │  (已登录状态)   │
                                                                       └──────────────┘

关键认知:OpenCLI 不直接使用 Playwright,也不是 CLI 直连 CDP。它通过 Chrome Extension 作为桥梁,Extension 才是真正操作浏览器的执行者。


三、各层详解

3.1 第一层:Daemon(消息中转站)

源码位置dist/src/daemon.js

Daemon 是一个轻量 HTTP + WebSocket 服务,运行在 localhost:19825

objectivec 复制代码
架构注释原文:
CLI → HTTP POST /command → daemon → WebSocket → Extension
Extension → WebSocket result → daemon → HTTP response → CLI
职责
功能 说明
命令中转 接收 CLI 的 HTTP 请求,通过 WS 转发给 Extension
结果回传 Extension 通过 WS 返回结果,Daemon 通过 HTTP 回给 CLI
连接管理 维护与 Extension 的 WebSocket 连接
心跳保活 每 15 秒 ping Extension,连续 2 次无 pong 则断开
安全防护 Origin 检查 + X-OpenCLI 自定义头 + 无 CORS 头
日志缓冲 保留最近 200 条日志,可通过 /logs 查询
安全设计(defense-in-depth)
markdown 复制代码
1. Origin 检查 --- 拒绝非 chrome-extension:// 来源的请求
2. 自定义头 --- 要求 X-OpenCLI 头(浏览器无法在简单请求中发送)
3. 无 CORS 头 --- 命令端点不返回 Access-Control-Allow-Origin
4. Body 限制 --- 最大 1MB 防止 OOM
5. WS 验证 --- 在连接建立前拒绝非法 WebSocket 升级
生命周期
  • 自动启动 :首次执行浏览器命令时由 BrowserBridge 自动 spawn
  • 持久运行:启动后常驻,不随 CLI 进程退出
  • 显式关闭opencli daemon stopSIGTERM
关键代码
javascript 复制代码
// daemon.js:154-183 --- 命令中转核心逻辑
if (req.method === 'POST' && url === '/command') {
    const body = JSON.parse(await readBody(req));

    // 检查 Extension 是否连接
    if (!extensionWs || extensionWs.readyState !== WebSocket.OPEN) {
        jsonResponse(res, 503, { error: 'Extension not connected' });
        return;
    }

    const result = await new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            pending.delete(body.id);
            reject(new Error('Command timeout'));
        }, timeoutMs);
        pending.set(body.id, { resolve, reject, timer });
        extensionWs.send(JSON.stringify(body));  // 转发给 Extension
    });

    jsonResponse(res, 200, result);  // Extension 结果回给 CLI
}

3.2 第二层:Chrome Extension(Browser Bridge)

Extension 是真正执行浏览器操作的地方

通信方式
  • 启动时通过 WebSocket 连接到 Daemon(ws://localhost:19825/ext
  • 连接后发送 hello 消息(包含版本号)进行握手
  • 收到命令后调用 Chrome Extension API 操作浏览器
  • 结果通过 WebSocket 返回给 Daemon
核心能力
Chrome API 用途
chrome.tabs.create() 创建新标签页(导航)
chrome.scripting.executeScript() 在页面中注入并执行 JS
chrome.debugger.attach() 附加到标签页(CDP 入口)
chrome.cookies.getAll() 读取指定域名的 Cookie
安全约束
javascript 复制代码
// daemon.js:198-208 --- 只接受 Extension 的 WebSocket 连接
const wss = new WebSocketServer({
    server: httpServer,
    path: '/ext',
    verifyClient: ({ req }) => {
        const origin = req.headers['origin'];
        return !origin || origin.startsWith('chrome-extension://');
    },
});

3.3 第三层:CLI 客户端

源码位置dist/src/browser/daemon-client.js

CLI 进程通过 HTTP 与 Daemon 通信。

重试机制
  • 最多 4 次重试
  • 网络错误(TypeError, AbortError):500ms 后重试
  • 瞬态浏览器错误:按 classifyBrowserError() 建议的延迟重试
  • 重复命令 ID(409):直接重试
javascript 复制代码
// daemon-client.js:72-99 --- 带重试的命令发送
async function sendCommandRaw(action, params) {
    const maxRetries = 4;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        const command = { id: generateId(), action, ...params };
        const res = await requestDaemon('/command', {
            method: 'POST',
            body: JSON.stringify(command),
            timeout: 30000,
        });
        const result = await res.json();
        if (!result.ok) {
            const advice = classifyBrowserError(new Error(result.error));
            if (advice.retryable && attempt < maxRetries) {
                await sleep(advice.delayMs);
                continue;
            }
            throw new Error(result.error);
        }
        return result;
    }
}

3.4 旁路:CDPBridge(直连模式)

源码位置dist/src/browser/cdp.js

专门为 Electron 桌面应用 设计,绕过 Daemon 和 Extension,直接通过 WebSocket 连接应用暴露的 CDP 端口:

javascript 复制代码
export class CDPBridge {
    async connect(opts) {
        const endpoint = opts?.cdpEndpoint ?? process.env.OPENCLI_CDP_ENDPOINT;
        // 获取可调试的 target
        const targets = await fetchJsonDirect(`${endpoint}/json`);
        const target = selectCDPTarget(targets);
        // 直接 WebSocket 连接
        const ws = new WebSocket(target.webSocketDebuggerUrl);
        // 启用 CDP 协议
        await this.send('Page.enable');
        await this.send('Page.addScriptToEvaluateOnNewDocument', {
            source: generateStealthJs()
        });
    }
}

用于控制 Cursor、Notion、ChatGPT 桌面版等 Electron 应用。


四、认证策略(Strategy)

源码位置dist/src/registry.js

javascript 复制代码
export var Strategy;
Strategy["PUBLIC"]    = "public";    // 公开 API,无需认证
Strategy["LOCAL"]     = "local";     // 本地二进制/API
Strategy["COOKIE"]    = "cookie";    // 复用浏览器 Cookie
Strategy["HEADER"]    = "header";    // 带 Auth Header
Strategy["INTERCEPT"] = "intercept"; // 注入拦截器捕获网络请求
Strategy["UI"]        = "ui";        // CDP 直连桌面应用

策略路由逻辑

策略在适配器代码中编译时声明,不是运行时自动判断:

javascript 复制代码
// registry.js:60-76 --- normalizeCommand
function normalizeCommand(cmd) {
    const strategy = cmd.strategy ?? (cmd.browser === false
        ? Strategy.PUBLIC
        : Strategy.COOKIE);

    // 非 PUBLIC/LOCAL 都需要浏览器
    const browser = strategy !== Strategy.PUBLIC
                 && strategy !== Strategy.LOCAL;

    // COOKIE/HEADER 自动预导航到域名首页
    let navigateBefore;
    if ((strategy === Strategy.COOKIE || strategy === Strategy.HEADER) && cmd.domain) {
        navigateBefore = `https://${cmd.domain}`;
    }

    return { ...cmd, strategy, browser, navigateBefore };
}

策略对比

Strategy 需要浏览器 预导航 数据来源 典型场景
PUBLIC Node.js 直接 fetch HackerNews、GitHub trending
LOCAL 本地二进制 gh、docker
COOKIE 域名首页 浏览器内 JS 执行 小红书、B站、知乎、Twitter
HEADER 域名首页 浏览器内带 Header fetch 需 Bearer Token 的站
INTERCEPT 触发页 注入拦截器捕获 API 响应 动态 Token 的 SPA 站
UI 是(CDP) CDP 直连 Electron Cursor、Notion、ChatGPT

Step 1: CLI 入口 → 适配器定位

bash 复制代码
main.js → cli.js → 查 registry → 找到 xiaohongshu/search

适配器声明:

javascript 复制代码
// clis/xiaohongshu/search.js
cli({
    site: 'xiaohongshu',
    name: 'search',
    strategy: Strategy.COOKIE,        // 需要浏览器 + Cookie
    domain: 'www.xiaohongshu.com',
    navigateBefore: false,             // 覆盖默认,直接去搜索页
    columns: ['rank', 'title', 'author', 'likes', 'published_at', 'url'],
    func: async (page, kwargs) => { ... }
})

Step 2: 建立浏览器连接

scss 复制代码
BrowserBridge.connect()
  └→ _ensureDaemon()
       ├→ getDaemonHealth()         // HTTP GET localhost:19825/status
       ├→ Daemon 没运行?spawn 新进程(detached, 常驻)
       ├→ Extension 没连?等待(poll 200ms 间隔,最多 10s)
       └→ 返回 Page 实例

Step 3: 导航到搜索页

lua 复制代码
page.goto("https://www.xiaohongshu.com/search_result?keyword=xxx")
  └→ sendCommandFull('navigate', { url, workspace })
       └→ HTTP POST localhost:19825/command
            └→ Daemon WS 转发给 Extension
                 └→ Extension: chrome.tabs.create({ url })
                      └→ ⚡ Chrome 自动携带 xiaohongshu.com 的 Cookie
                 └→ Extension 返回 { ok: true, page: targetId }
            └→ Daemon HTTP 200 回给 CLI

关键点:Cookie 不需要"提取"或"传递"。Chrome 自己开标签页,自动带上该域名的所有 Cookie。

Step 4: 注入 Stealth + DOM 稳定检测

javascript 复制代码
// page.js:62-66
const combinedCode = `${generateStealthJs()};\n${waitForDomStableJs(maxMs)}`;
await sendCommand('exec', { code: combinedCode });
  • Stealth:伪装自动化指纹(navigator.webdriver = false 等)
  • DOM Stable:MutationObserver 检测 DOM 变化停止

Step 5: 等待搜索结果渲染

javascript 复制代码
// search.js --- 用 MutationObserver 等待内容出现
const waitResult = await page.evaluate(`
    new Promise((resolve) => {
        const detect = () => {
            if (document.querySelector('section.note-item')) return 'content';
            if (/登录后查看/.test(document.body?.innerText)) return 'login_wall';
            return null;
        };
        // MutationObserver 监听 DOM 变化
        const observer = new MutationObserver(() => {
            const result = detect();
            if (result) { observer.disconnect(); resolve(result); }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setTimeout(() => resolve('timeout'), 5000);
    })
`);

Step 6: 滚动加载更多

javascript 复制代码
await page.autoScroll({ times: 2 });

Step 7: DOM 数据提取

javascript 复制代码
const payload = await page.evaluate(`
    (() => {
        const results = [];
        document.querySelectorAll('section.note-item').forEach(el => {
            const title = el.querySelector('.title')?.textContent;
            const author = el.querySelector('a.author .name')?.textContent;
            const likes = el.querySelector('.count')?.textContent;
            const url = el.querySelector('a.cover.mask')?.getAttribute('href');
            results.push({ title, author, likes, url });
        });
        return results;
    })()
`);

完整的 evaluate 链路:

php 复制代码
page.evaluate(js)
  └→ sendCommand('exec', { code: js })
       └→ HTTP POST /command → Daemon WS 转发 → Extension
            └→ chrome.scripting.executeScript({ target: tabId, func: ... })
                 └→ 在页面 V8 引擎里执行 JS,读 DOM
            └→ 返回 JSON 结果
       └→ 原路返回给 CLI

Step 8: 格式化输出

bash 复制代码
opencli xiaohongshu search "关键词" -f json   # JSON
opencli xiaohongshu search "关键词" -f table  # 表格(默认)
opencli xiaohongshu search "关键词" -f csv    # CSV
opencli xiaohongshu search "关键词" -f md     # Markdown

六、三种数据提取模式

scss 复制代码
导航到页面 → 等待渲染 → evaluate(JS 读 DOM) → 返回结构化数据

优点:所见即所得,不依赖 API 稳定性 缺点:DOM 结构变化会 break

模式 B:浏览器内 Fetch(pipeline/steps/fetch.js)

javascript 复制代码
// 在浏览器 V8 里发 fetch,自动带 Cookie
await page.evaluate(`
    async () => {
        const resp = await fetch(url, {
            credentials: "include"  // 关键:自动带 Cookie
        });
        return await resp.json();
    }
`)

优点:拿到 API 原始 JSON,比 DOM 稳定 关键:不是 CLI 进程发请求,是浏览器内发,所以自动带认证

模式 C:XHR 拦截(pipeline/steps/intercept.js)

markdown 复制代码
1. 注入拦截器:monkey-patch window.fetch + XMLHttpRequest
2. 触发动作(导航/点击/滚动)
3. 等待捕获:匹配 URL pattern 的响应体存入 window.__opencli_net[]
4. 取出数据:page.getInterceptedRequests()

拦截器代码(cli.js:567)会同时 hook window.fetchXMLHttpRequest.prototype,捕获所有 JSON/text 类型的网络响应。

优点:零侵入拿到原始 API 响应 缺点:需要知道 API URL pattern


Token 在哪里?

问题 答案
Token 存储位置 Chrome 浏览器进程里(Cookie jar、localStorage、session storage)
Token 是否经过 Daemon? 不经过。Daemon 只转发命令和结果,不接触凭证
Token 是否经过 CLI 进程? 不经过。CLI 只收到最终的数据结果
需要手动配置 Token 吗? 不需要。在 Chrome 里登录目标网站就行
凭证持久化在哪? Chrome 自身的 profile 目录

保活机制

  • 无独立保活:依赖 Chrome 浏览器的登录状态
  • Cookie 过期 → 在 Chrome 里重新登录即可
  • opencli doctor 检查连接状态
  • Daemon 有心跳(15s ping/pong)保持与 Extension 的连接

安全边界

scss 复制代码
┌─────────────────────────────────────────────┐
│              Chrome 浏览器进程                │
│  ┌────────────────────────────────────────┐  │
│  │  Cookie / Token / Session / LocalStorage│ ← 凭证只在这里
│  └────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────┐  │
│  │  Extension(Browser Bridge)            │ ← 操作浏览器
│  │  ↕ WebSocket                           │  │
│  └────────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
       ↕ WebSocket (localhost only)
┌─────────────────────────────────────────────┐
│  Daemon (:19825)                            │ ← 纯消息中转
│  ↕ HTTP (localhost only, X-OpenCLI header)  │
└─────────────────────────────────────────────┘
       ↕ HTTP
┌─────────────────────────────────────────────┐
│  CLI 进程 (opencli xxx)                     │ ← 只收到结果数据
└─────────────────────────────────────────────┘

八、Pipeline 执行器

源码位置dist/src/pipeline/executor.js

对于 YAML 声明式适配器(非 JS 函数式),通过 Pipeline 执行:

javascript 复制代码
// 顺序执行每个 step
for (const step of pipeline) {
    for (const [op, params] of Object.entries(step)) {
        data = await executeStepWithRetry(handler, page, params, data, args, op);
    }
}

可用的 Pipeline Steps

Step 说明
navigate 导航到 URL
click 点击元素
type 输入文本
wait 等待时间/文本出现
press 按键
snapshot DOM 快照
evaluate 执行 JS
fetch 浏览器内 HTTP 请求
intercept XHR/fetch 拦截
transform 数据转换
download 下载文件

Pipeline 有内置重试(浏览器类 step 默认重试 2 次)。


九、与 Claude Code 的集成

安装的 5 个 Skills

bash 复制代码
npx skills add jackwener/opencli --yes --global
Skill 用途
opencli-usage 命令总览和站点参考
opencli-adapter-author 为新站点编写适配器(端到端)
opencli-autofix 修复坏掉的适配器
opencli-browser 浏览器自动化参考
smart-search 智能搜索路由

Skills 通过 symlink 安装到 ~/.claude/skills/,Claude Code 启动时自动加载。

使用方式

直接用自然语言描述需求,Claude Code 会调用对应的 opencli 命令:

sql 复制代码
"帮我搜下小红书上关于 xxx 的笔记"
→ opencli xiaohongshu search "xxx" -f json

"看看 Twitter 上 xxx 的最新推文"
→ opencli twitter timeline xxx -f json

"下载这个B站视频"
→ opencli bilibili download BV1xxx --output ./

十、配置参考

环境变量 默认值 说明
OPENCLI_DAEMON_PORT 19825 Daemon 端口
OPENCLI_WINDOW_FOCUSED false 前台打开自动化窗口
OPENCLI_LIVE false 命令结束后保持窗口
OPENCLI_BROWSER_CONNECT_TIMEOUT 30 浏览器连接超时(秒)
OPENCLI_BROWSER_COMMAND_TIMEOUT 60 单条命令超时(秒)
OPENCLI_CDP_ENDPOINT --- CDP 端点(Electron 直连)
OPENCLI_VERBOSE false 详细日志
OPENCLI_DIAGNOSTIC false 失败时输出诊断上下文

Exit Codes

Code 含义
0 成功
2 参数错误
66 空结果
69 服务不可用(Extension 未连接)
75 临时失败(超时,可重试)
77 需要登录
78 配置错误

十一、安装步骤

bash 复制代码
# 1. 安装 CLI
npm install -g @jackwener/opencli

# 2. 安装 Chrome 扩展(手动)
# 下载:https://github.com/jackwener/opencli/releases
# chrome://extensions → 开发者模式 → 加载已解压的扩展

# 3. 验证
opencli doctor

# 4. 安装 AI Agent Skills
npx skills add jackwener/opencli --yes --global

# 5. 试用
opencli hackernews top --limit 5         # 公开 API,不需要浏览器
opencli xiaohongshu search "关键词"       # 需要浏览器+登录
相关推荐
珹洺2 小时前
C++AI多模型聊天系统(一)项目背景意义与整体架构、核心基类实现
c++·人工智能·架构
小江的记录本3 小时前
【微服务与云原生架构】DevOps、CI/CD流水线、GitOps 系统性知识体系
分布式·后端·ci/cd·微服务·云原生·架构·devops
qcx233 小时前
知识沉淀 | 2026 年 LLM 评测体系 & 主流开源模型架构全景
架构·开源
2603_954708314 小时前
微电网混合控制架构:主从与对等控制的优势融合
分布式·安全·架构·能源·需求分析
许愿OvO4 小时前
MySQL 8.3.0 运维与集群架构实战
运维·mysql·架构
凌云拓界4 小时前
青创赛终评手记:最后的成功
运维·科技·职场和发展·架构·创业创新
heimeiyingwang4 小时前
【架构实战】BFF架构:Backend For Frontends
架构
码点滴4 小时前
上下文压缩不是“丢数据“:Context Compressor 的血缘追踪与 Prefix Cache 保护
人工智能·python·架构·prompt·ai编程
会开花的二叉树5 小时前
项目架构与业务逻辑全解
架构