opencli-web-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOpenCLI Web Automation
OpenCLI 网页自动化
Skill by ara.so — Daily 2026 Skills collection.
OpenCLI turns any website into a command-line interface by reusing Chrome's logged-in browser session. It supports 19 sites and 80+ commands out of the box, and lets you add new adapters via TypeScript or YAML dropped into the folder.
clis/由 ara.so 开发的技能工具 — 属于Daily 2026技能合集。
OpenCLI通过复用Chrome已登录的浏览器会话,将任意网站转换为命令行界面(CLI)。它开箱即用地支持19个网站和80+条命令,还允许你通过将TypeScript或YAML文件放入文件夹来添加新的适配器。
clis/Installation
安装
bash
undefinedbash
undefinedInstall globally via npm
通过npm全局安装
npm install -g @jackwener/opencli
npm install -g @jackwener/opencli
One-time setup: discovers Playwright MCP token and distributes to all tools
一次性配置:发现Playwright MCP令牌并分发给所有工具
opencli setup
opencli setup
Verify everything is working
验证所有功能是否正常
opencli doctor --live
undefinedopencli doctor --live
undefinedPrerequisites
前置要求
- Node.js >= 18.0.0
- Chrome browser running and logged into the target site
- Playwright MCP Bridge extension installed in Chrome
- Node.js >= 18.0.0
- Chrome浏览器 已运行且已登录目标网站
- Playwright MCP Bridge 扩展程序已安装在Chrome中
Install from Source (Development)
从源码安装(开发环境)
bash
git clone git@github.com:jackwener/opencli.git
cd opencli
npm install
npm run build
npm linkbash
git clone git@github.com:jackwener/opencli.git
cd opencli
npm install
npm run build
npm linkEnvironment Configuration
环境配置
bash
undefinedbash
undefinedRequired: set in ~/.zshrc or ~/.bashrc after running opencli setup
必需:运行opencli setup后,在~/.zshrc或~/.bashrc中设置
export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<your-token-from-setup>"
MCP client config (Claude/Cursor/Codex `~/.config/*/config.json`):
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest", "--extension"],
"env": {
"PLAYWRIGHT_MCP_EXTENSION_TOKEN": "$PLAYWRIGHT_MCP_EXTENSION_TOKEN"
}
}
}
}export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<your-token-from-setup>"
MCP客户端配置(Claude/Cursor/Codex的`~/.config/*/config.json`):
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest", "--extension"],
"env": {
"PLAYWRIGHT_MCP_EXTENSION_TOKEN": "$PLAYWRIGHT_MCP_EXTENSION_TOKEN"
}
}
}
}Key CLI Commands
核心CLI命令
Discovery & Registry
命令发现与注册表
bash
opencli list # Show all registered commands
opencli list -f yaml # Output registry as YAML
opencli list -f json # Output registry as JSONbash
opencli list # 显示所有已注册的命令
opencli list -f yaml # 以YAML格式输出注册表
opencli list -f json # 以JSON格式输出注册表Running Built-in Commands
运行内置命令
bash
undefinedbash
undefinedPublic API commands (no browser login needed)
公共API命令(无需浏览器登录)
opencli hackernews top --limit 10
opencli github search "playwright automation"
opencli bbc news
opencli hackernews top --limit 10
opencli github search "playwright automation"
opencli bbc news
Browser commands (must be logged into site in Chrome)
浏览器命令(必须已在Chrome中登录目标网站)
opencli bilibili hot --limit 5
opencli twitter trending
opencli zhihu hot -f json
opencli reddit frontpage --limit 20
opencli xiaohongshu search "TypeScript"
opencli youtube search "browser automation"
opencli linkedin search "senior engineer"
undefinedopencli bilibili hot --limit 5
opencli twitter trending
opencli zhihu hot -f json
opencli reddit frontpage --limit 20
opencli xiaohongshu search "TypeScript"
opencli youtube search "browser automation"
opencli linkedin search "senior engineer"
undefinedOutput Formats
输出格式
All commands support / :
--format-fbash
opencli bilibili hot -f table # Rich terminal table (default)
opencli bilibili hot -f json # JSON (pipe to jq)
opencli bilibili hot -f yaml # YAML
opencli bilibili hot -f md # Markdown
opencli bilibili hot -f csv # CSV export
opencli bilibili hot -v # Verbose: show pipeline debug steps所有命令均支持 / 参数:
--format-fbash
opencli bilibili hot -f table # 富终端表格(默认格式)
opencli bilibili hot -f json # JSON格式(可通过管道传给jq处理)
opencli bilibili hot -f yaml # YAML格式
opencli bilibili hot -f md # Markdown格式
opencli bilibili hot -f csv # CSV导出格式
opencli bilibili hot -v # Verbose模式:显示流水线调试步骤AI Agent Workflow (Creating New Commands)
AI Agent工作流(创建新命令)
bash
undefinedbash
undefined1. Deep explore a site — discovers APIs, auth, capabilities
1. 深度探索网站 — 发现API、认证方式和功能
opencli explore https://example.com --site mysite
opencli explore https://example.com --site mysite
2. Synthesize YAML adapters from explore artifacts
2. 从探索产物中合成YAML适配器
opencli synthesize mysite
opencli synthesize mysite
3. One-shot: explore → synthesize → register in one command
3. 一站式操作:探索→合成→注册,一步完成
opencli generate https://example.com --goal "hot posts"
opencli generate https://example.com --goal "hot posts"
4. Strategy cascade — auto-probes PUBLIC → COOKIE → HEADER auth
4. 策略级联 — 自动探测PUBLIC→COOKIE→HEADER三种认证方式
opencli cascade https://api.example.com/data
Explore artifacts are saved to `.opencli/explore/<site>/`:
- `manifest.json` — site metadata
- `endpoints.json` — discovered API endpoints
- `capabilities.json` — inferred command capabilities
- `auth.json` — authentication strategy
---opencli cascade https://api.example.com/data
探索产物会保存到`.opencli/explore/<site>/`目录下:
- `manifest.json` — 网站元数据
- `endpoints.json` — 发现的API端点
- `capabilities.json` — 推断出的命令功能
- `auth.json` — 认证策略
---Adding a New Adapter
添加新适配器
Option 1: YAML Declarative Adapter
选项1:YAML声明式适配器
Drop a file into — auto-registered on next run:
.yamlclis/yaml
undefined将文件放入目录 — 下次运行时会自动注册:
.yamlclis/yaml
undefinedclis/producthunt.yaml
clis/producthunt.yaml
site: producthunt
commands:
- name: trending
description: Get trending products on Product Hunt
args:
- name: limit type: number default: 10 pipeline:
- type: navigate url: https://www.producthunt.com
- type: waitFor selector: "[data-test='post-item']"
- type: extract selector: "[data-test='post-item']" fields: name: selector: "h3" type: text tagline: selector: "p" type: text votes: selector: "[data-test='vote-button']" type: text url: selector: "a" attr: href
- type: limit count: "{{limit}}"
undefinedsite: producthunt
commands:
- name: trending
description: Get trending products on Product Hunt
args:
- name: limit type: number default: 10 pipeline:
- type: navigate url: https://www.producthunt.com
- type: waitFor selector: "[data-test='post-item']"
- type: extract selector: "[data-test='post-item']" fields: name: selector: "h3" type: text tagline: selector: "p" type: text votes: selector: "[data-test='vote-button']" type: text url: selector: "a" attr: href
- type: limit count: "{{limit}}"
undefinedOption 2: TypeScript Adapter
选项2:TypeScript适配器
typescript
// clis/producthunt.ts
import type { CLIAdapter } from "../src/types";
const adapter: CLIAdapter = {
site: "producthunt",
commands: [
{
name: "trending",
description: "Get trending products on Product Hunt",
options: [
{
flags: "--limit <n>",
description: "Number of results",
defaultValue: "10",
},
],
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://www.producthunt.com");
await page.waitForSelector("[data-test='post-item']");
const products = await page.evaluate(() => {
return Array.from(
document.querySelectorAll("[data-test='post-item']")
).map((el) => ({
name: el.querySelector("h3")?.textContent?.trim() ?? "",
tagline: el.querySelector("p")?.textContent?.trim() ?? "",
votes:
el
.querySelector("[data-test='vote-button']")
?.textContent?.trim() ?? "",
url:
(el.querySelector("a") as HTMLAnchorElement)?.href ?? "",
}));
});
return products.slice(0, Number(options.limit));
},
},
],
};
export default adapter;typescript
// clis/producthunt.ts
import type { CLIAdapter } from "../src/types";
const adapter: CLIAdapter = {
site: "producthunt",
commands: [
{
name: "trending",
description: "Get trending products on Product Hunt",
options: [
{
flags: "--limit <n>",
description: "Number of results",
defaultValue: "10",
},
],
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://www.producthunt.com");
await page.waitForSelector("[data-test='post-item']");
const products = await page.evaluate(() => {
return Array.from(
document.querySelectorAll("[data-test='post-item']")
).map((el) => ({
name: el.querySelector("h3")?.textContent?.trim() ?? "",
tagline: el.querySelector("p")?.textContent?.trim() ?? "",
votes:
el
.querySelector("[data-test='vote-button']")
?.textContent?.trim() ?? "",
url:
(el.querySelector("a") as HTMLAnchorElement)?.href ?? "",
}));
});
return products.slice(0, Number(options.limit));
},
},
],
};
export default adapter;Common Patterns
常见模式
Pattern: Authenticated API Extraction (Cookie Injection)
模式:已认证API提取(Cookie注入)
typescript
// When a site exposes a JSON API but requires login cookies
async run(options, browser) {
const page = await browser.currentPage();
// Navigate first to ensure cookies are active
await page.goto("https://api.example.com");
const data = await page.evaluate(async () => {
const res = await fetch("/api/v1/feed?limit=20", {
credentials: "include", // reuse browser cookies
});
return res.json();
});
return data.items;
}typescript
// 当网站暴露JSON API但需要登录Cookie时
async run(options, browser) {
const page = await browser.currentPage();
// 先导航以确保Cookie处于激活状态
await page.goto("https://api.example.com");
const data = await page.evaluate(async () => {
const res = await fetch("/api/v1/feed?limit=20", {
credentials: "include", // 复用浏览器Cookie
});
return res.json();
});
return data.items;
}Pattern: Header Token Extraction
模式:Header令牌提取
typescript
// Extract auth tokens from browser storage for API calls
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://example.com");
const token = await page.evaluate(() => {
return localStorage.getItem("auth_token") ||
sessionStorage.getItem("token");
});
const data = await page.evaluate(async (tok) => {
const res = await fetch("/api/data", {
headers: { Authorization: `Bearer ${tok}` },
});
return res.json();
}, token);
return data;
}typescript
// 从浏览器存储中提取认证令牌用于API调用
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://example.com");
const token = await page.evaluate(() => {
return localStorage.getItem("auth_token") ||
sessionStorage.getItem("token");
});
const data = await page.evaluate(async (tok) => {
const res = await fetch("/api/data", {
headers: { Authorization: `Bearer ${tok}` },
});
return res.json();
}, token);
return data;
}Pattern: DOM Scraping with Wait
模式:带等待的DOM爬取
typescript
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://news.ycombinator.com");
// Wait for dynamic content to load
await page.waitForSelector(".athing", { timeout: 10000 });
return page.evaluate((limit) => {
return Array.from(document.querySelectorAll(".athing"))
.slice(0, limit)
.map((row) => ({
title: row.querySelector(".titleline a")?.textContent?.trim(),
url: (row.querySelector(".titleline a") as HTMLAnchorElement)?.href,
score:
row.nextElementSibling
?.querySelector(".score")
?.textContent?.trim() ?? "0",
}));
}, Number(options.limit));
}typescript
async run(options, browser) {
const page = await browser.currentPage();
await page.goto("https://news.ycombinator.com");
// 等待动态内容加载完成
await page.waitForSelector(".athing", { timeout: 10000 });
return page.evaluate((limit) => {
return Array.from(document.querySelectorAll(".athing"))
.slice(0, limit)
.map((row) => ({
title: row.querySelector(".titleline a")?.textContent?.trim(),
url: (row.querySelector(".titleline a") as HTMLAnchorElement)?.href,
score:
row.nextElementSibling
?.querySelector(".score")
?.textContent?.trim() ?? "0",
}));
}, Number(options.limit));
}Pattern: Pagination
模式:分页处理
typescript
async run(options, browser) {
const page = await browser.currentPage();
const results = [];
let pageNum = 1;
while (results.length < Number(options.limit)) {
await page.goto(`https://example.com/posts?page=${pageNum}`);
await page.waitForSelector(".post-item");
const items = await page.evaluate(() =>
Array.from(document.querySelectorAll(".post-item")).map((el) => ({
title: el.querySelector("h2")?.textContent?.trim(),
url: (el.querySelector("a") as HTMLAnchorElement)?.href,
}))
);
if (items.length === 0) break;
results.push(...items);
pageNum++;
}
return results.slice(0, Number(options.limit));
}typescript
async run(options, browser) {
const page = await browser.currentPage();
const results = [];
let pageNum = 1;
while (results.length < Number(options.limit)) {
await page.goto(`https://example.com/posts?page=${pageNum}`);
await page.waitForSelector(".post-item");
const items = await page.evaluate(() =>
Array.from(document.querySelectorAll(".post-item")).map((el) => ({
title: el.querySelector("h2")?.textContent?.trim(),
url: (el.querySelector("a") as HTMLAnchorElement)?.href,
}))
);
if (items.length === 0) break;
results.push(...items);
pageNum++;
}
return results.slice(0, Number(options.limit));
}Maintenance Commands
维护命令
bash
undefinedbash
undefinedDiagnose token and config across all tools
诊断所有工具的令牌和配置
opencli doctor
opencli doctor
Test live browser connectivity
测试实时浏览器连接
opencli doctor --live
opencli doctor --live
Fix mismatched configs interactively
交互式修复不匹配的配置
opencli doctor --fix
opencli doctor --fix
Fix all configs non-interactively
非交互式修复所有配置
opencli doctor --fix -y
---opencli doctor --fix -y
---Testing
测试
bash
npm run buildbash
npm run buildRun all tests
运行所有测试
npx vitest run
npx vitest run
Unit tests only
仅运行单元测试
npx vitest run src/
npx vitest run src/
E2E tests only
仅运行端到端测试
npx vitest run tests/e2e/
npx vitest run tests/e2e/
Headless browser mode for CI
CI环境下使用无头浏览器模式
OPENCLI_HEADLESS=1 npx vitest run tests/e2e/
---OPENCLI_HEADLESS=1 npx vitest run tests/e2e/
---Troubleshooting
故障排查
| Symptom | Fix |
|---|---|
| Ensure extension is enabled in Chrome; restart Chrome after install |
Empty data / | Open Chrome, navigate to the site, log in or refresh the page |
| Node API errors | Upgrade to Node.js >= 18 |
| Token not found | Run |
| Stale login session | Visit the target site in Chrome and interact with it to prove human presence |
| 症状 | 解决方法 |
|---|---|
| 确保Chrome中已启用该扩展程序;安装后重启Chrome |
数据为空 / | 打开Chrome,导航到目标网站,登录或刷新页面 |
| Node API错误 | 升级到Node.js >= 18版本 |
| 未找到令牌 | 运行 |
| 登录会话过期 | 在Chrome中访问目标网站并进行交互,以验证人工身份 |
Debug Verbose Mode
调试Verbose模式
bash
undefinedbash
undefinedSee full pipeline execution steps
查看完整的流水线执行步骤
opencli bilibili hot -v
opencli bilibili hot -v
Check what explore discovered
查看探索过程发现的内容
cat .opencli/explore/mysite/endpoints.json
cat .opencli/explore/mysite/auth.json
---cat .opencli/explore/mysite/endpoints.json
cat .opencli/explore/mysite/auth.json
---Project Structure (for Adapter Authors)
项目结构(面向适配器开发者)
opencli/
├── clis/ # Drop .ts or .yaml adapters here (auto-registered)
│ ├── bilibili.ts
│ ├── twitter.ts
│ └── hackernews.yaml
├── src/
│ ├── types.ts # CLIAdapter, Command interfaces
│ ├── browser.ts # Playwright MCP bridge wrapper
│ ├── loader.ts # Dynamic adapter loader
│ └── output.ts # table/json/yaml/md/csv formatters
├── tests/
│ └── e2e/ # E2E tests per site
└── CLI-EXPLORER.md # Full AI agent exploration workflowopencli/
├── clis/ # 将.ts或.yaml适配器放入此处(自动注册)
│ ├── bilibili.ts
│ ├── twitter.ts
│ └── hackernews.yaml
├── src/
│ ├── types.ts # CLIAdapter、Command接口定义
│ ├── browser.ts # Playwright MCP桥接器封装
│ ├── loader.ts # 动态适配器加载器
│ └── output.ts # 表格/JSON/YAML/MD/CSV格式器
├── tests/
│ └── e2e/ # 按网站划分的端到端测试
└── CLI-EXPLORER.md # 完整的AI Agent探索工作流文档