Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Navigate to `chrome://inspect/#remote-debugging` and toggle the switch. That's i

The CLI auto-detects Chrome, Chromium, Brave, Edge, and Vivaldi on macOS, Linux, and Windows. If your browser stores `DevToolsActivePort` in a non-standard location, set the `CDP_PORT_FILE` environment variable to the full path.

### Termux (Android) setup

See [TERMUX.md](TERMUX.md) for running chrome-cdp with headless Chromium on Termux. Key differences: set `CDP_PORT` instead of relying on `DevToolsActivePort` auto-detection, and use `chromium-browser --headless --no-sandbox`. Playwright does not work on Android.

## Usage

```bash
Expand Down
130 changes: 130 additions & 0 deletions TERMUX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# chrome-cdp on Termux (Android)

This guide covers running Chromium + chrome-cdp on Termux (Android aarch64/Linux).

## Prerequisites

- Termux from F-Droid (not Google Play)
- Node.js 22+ (install via `pkg install nodejs`)
- 512MB+ free RAM for Chromium headless

## Install Chromium

```bash
pkg install x11-repo # Chromium is in the x11 repo
pkg install chromium # installs chromium-browser binary
```

**Current version**: Chromium 146 (as of 2026-05)

## Known Pitfalls

### 1. Chromium requires `x11-repo`

Chromium is **not** in the default Termux repository. You must add `x11-repo` first:

```bash
pkg install x11-repo
```

### 2. Playwright does NOT work on Android

```bash
npm install playwright-core
npx playwright-core install chromium # Fails: "Unsupported platform: android"
```

Playwright's dependency graph (e.g., `@playwright/browser-chromium`) ships platform-specific binaries and refuses to install on Android. **Do not use Playwright** — use headless Chromium directly.

### 3. No `DevToolsActivePort` file

Termux's Chromium does **not** create the `DevToolsActivePort` file that chrome-cdp normally auto-discovers. You must use the `CDP_PORT` environment variable instead.

### 4. `--headless` has no display output

Headless Chromium on Termux works correctly — it renders HTML, evaluates JS, takes screenshots, etc. You just won't see a window.

## Quick Start

### 1. Launch Chromium with remote debugging

```bash
chromium-browser \
--headless \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--remote-debugging-port=9222 &
```

Key flags:
- `--headless` — no visible window (required on Termux, no display server)
- `--no-sandbox` — required in Termux (no user namespace support)
- `--disable-gpu` — no GPU available on most Android devices
- `--disable-dev-shm-usage` — avoid `/dev/shm` issues in containerized environments

### 2. Set the CDP port

```bash
export CDP_PORT=9222
```

This tells chrome-cdp to discover the WebSocket URL via `http://127.0.0.1:9222/json/version` instead of scanning for `DevToolsActivePort`.

### 3. Verify connection

```bash
node scripts/cdp.mjs list
```

You should see a list of open pages (may be empty on first launch — Chromium opens a blank page).

### 4. Navigate to a page and inspect

```bash
node scripts/cdp.mjs list # copy a target prefix
node scripts/cdp.mjs snap <target> # accessibility snapshot
node scripts/cdp.mjs eval <target> # evaluate JS
```

## Automation Script

Save this as `start-cdp.sh`:

```bash
#!/data/data/com.termux/files/usr/bin/bash
# Start headless Chromium with CDP on Termux
CHROMIUM=chromium-browser
PORT=${1:-9222}

pkill -f "$CHROMIUM.*remote-debugging" 2>/dev/null
sleep 1

$CHROMIUM \
--headless \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--remote-debugging-port=$PORT &

echo "Waiting for CDP on port $PORT..."
for i in $(seq 1 20); do
if curl -s http://127.0.0.1:$PORT/json/version >/dev/null 2>&1; then
echo "CDP ready: http://127.0.0.1:$PORT"
echo "Run: CDP_PORT=$PORT node scripts/cdp.mjs list"
exit 0
fi
sleep 1
done

echo "Timed out waiting for CDP"
exit 1
```

## Environment Variables for Termux

| Variable | Purpose | Example |
|----------|---------|---------|
| `CDP_PORT` | Port Chromium is listening on (required on Termux) | `export CDP_PORT=9222` |
| `CDP_HOST` | Host address (default: 127.0.0.1) | `export CDP_HOST=127.0.0.1` |
| `CDP_PORT_FILE` | Direct path to DevToolsActivePort (not used on Termux) | — |
167 changes: 167 additions & 0 deletions chrome-cdp-termux-article.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# 在手机上跑Chrome远程调试?我把不可能变成了可能

> 当AI编码助手遇上Android Termux,一个困扰我两周的问题终于有了答案

## 起因:一个"不可能"的需求

想象一下这个场景:你躺在沙发上,手里只有一台手机,却想继续调试白天没写完的代码。你的AI编码助手需要访问浏览器页面,但所有浏览器自动化工具都说"Android?不支持。"

这就是我两周前的困境。

我用的AI编码工具依赖一个叫 `chrome-cdp` 的精巧工具——它通过Chrome DevTools Protocol直接连接到浏览器,不需要Puppeteer,不需要重装浏览器,甚至不需要重新登录。但它有一个前提:Chrome必须开着远程调试端口。

而问题在于——我在Termux(Android上的终端模拟器)上。没有桌面环境,没有图形界面,连Chrome都得从x11仓库单独安装。

## 踩坑日记:每一步都是血泪

### 坑1:找不到Chromium

```
$ pkg install chromium
E: Unable to locate package chromium
```

一上来就吃闭门羹。搜索半天才发现,Chromium藏在 `x11-repo` 里——一个你根本不会想到要安装的仓库。

```bash
pkg install x11-repo # 先装仓库
pkg install chromium # 才能装浏览器
```

### 坑2:Playwright?不存在的

既然有了Chrome,那用Playwright来自动化吧?于是我天真地运行:

```bash
npm install playwright-core
npx playwright-core install chromium
```

结果:

```
Unsupported platform: android
```

Playwright直接拒了。它的底层依赖有平台特定二进制文件,Android不在支持列表里。这条路走不通。

### 坑3:DevToolsActivePort 在哪?

`chrome-cdp` 的工作原理是扫描操作系统各处的 `DevToolsActivePort` 文件来发现Chrome的调试端口。但在Termux上,Chromium根本不生成这个文件。

```
No DevToolsActivePort found.
Enable remote debugging at chrome://inspect/#remote-debugging
```

在手机上访问 `chrome://inspect`?这可是headless模式,根本没有界面。

## 灵光一现:换个思路

既然找不到端口文件,那我直接告诉它端口号不就行了?

Chrome headless模式下启动时,我可以指定 `--remote-debugging-port=9222`,然后通过 `http://127.0.0.1:9222/json/version` 获取WebSocket URL。

但原版 `cdp.mjs` 的 `getWsUrl()` 函数是同步的,只支持读取文件,不支持HTTP发现。源码大概长这样:

```javascript
function getWsUrl() {
// 扫描 DevToolsActivePort 文件...
const portFile = candidates.find(p => existsSync(p));
if (!portFile) throw new Error('Not found');
// 从文件读取端口和路径...
return `ws://${host}:${lines[0]}${lines[1]}`;
}
```

我需要做三件事:
1. 把 `getWsUrl()` 改成异步函数
2. 添加 `CDP_PORT` 环境变量支持
3. 通过HTTP API自动发现WebSocket URL

## 解决方案:一行环境变量搞定

改动其实不大,但效果显著。新增的fallback逻辑:

```javascript
const cdpPort = process.env.CDP_PORT;
if (cdpPort) {
const resp = await fetch(`http://${host}:${cdpPort}/json/version`);
const data = await resp.json();
if (data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
}
```

现在在Termux上只需要:

```bash
export CDP_PORT=9222
chromium-browser --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 &
cdp list
```

## 最终效果:手机上的Chrome调试

改动完成后,我在手机上做了个测试——访问GitHub issues页面:

```bash
$ cdp list
9CC23064 Issues · pasky/chrome-cdp-skill · GitHub

$ cdp snap 9CC23064
[RootWebArea] Issues · pasky/chrome-cdp-skill · GitHub
[link] Skip to content
[heading] Navigation Menu
...
[list] Search results
[listitem] Thanks for chrome-cdp-skill #38
[listitem] mouse drag over canvas #24
[listitem] Unix socket IPC fails with EPERM... #19
```

成功了。AI编码助手现在可以直接"看"到浏览器页面,理解页面结构,甚至交互操作——全部在手机上完成。

## 技术总结:给同样在Termux上挣扎的你

```
# 安装
pkg install x11-repo chromium nodejs

# 启动Chromium(headless)
chromium-browser \
--headless \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--remote-debugging-port=9222 &

# 设置环境变量
export CDP_PORT=9222

# 开始使用
cdp list
cdp snap <target>
cdp eval <target> "document.title"
```

### 完整踩坑清单

| 坑 | 解决方案 |
|----|---------|
| Chromium不在默认仓库 | `pkg install x11-repo` 先装x11仓库 |
| Playwright不支持Android | 放弃Playwright,用headless Chromium直连 |
| 没有DevToolsActivePort | 设置 `CDP_PORT=9222` 环境变量 |
| `--no-sandbox` 必须 | Termux不支持用户命名空间 |
| `--disable-gpu` 必须 | Android通常没有可用GPU |

## 开源的意义

这个改动我已经提交到了 `github.com/aresbit/chrome-cdp-skill`。原项目 `chrome-cdp` 本身就是一个精巧的作品——不用Puppeteer,直接通过WebSocket连接Chrome,每个标签页有独立的持久化守护进程,解决了同类工具的所有痛点。

而我做的只是在它优雅的设计基础上,加了一小块拼图:让 `CDP_PORT` 环境变量成为fallback发现机制。这样无论是Termux用户,还是手动启动Chrome的开发者,都能轻松使用。

这次经历让我更加确信:好的开源项目就像乐高,每个人都可以贡献一块属于自己的积木。

---

*如果你也在Termux上折腾过什么有意思的配置,欢迎评论区分享你的踩坑故事 👇*
42 changes: 33 additions & 9 deletions skills/chrome-cdp/scripts/cdp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function sockPath(targetId) {
: resolve(RUNTIME_DIR, `cdp-${targetId}.sock`);
}

function getWsUrl() {
async function getWsUrl() {
const home = homedir();
// macOS: ~/Library/Application Support/<name>/DevToolsActivePort
const macBrowsers = [
Expand Down Expand Up @@ -80,11 +80,35 @@ function getWsUrl() {
}) : []),
].filter(Boolean);
const portFile = candidates.find(p => existsSync(p));
if (!portFile) throw new Error('No DevToolsActivePort found. Enable remote debugging at chrome://inspect/#remote-debugging');
const lines = readFileSync(portFile, 'utf8').trim().split('\n');
if (lines.length < 2 || !lines[0] || !lines[1]) throw new Error(`Invalid DevToolsActivePort file: ${portFile}`);
const host = process.env.CDP_HOST || '127.0.0.1';
return `ws://${host}:${lines[0]}${lines[1]}`;
if (portFile) {
const lines = readFileSync(portFile, 'utf8').trim().split('\n');
if (lines.length >= 2 && lines[0] && lines[1]) {
const host = process.env.CDP_HOST || '127.0.0.1';
return `ws://${host}:${lines[0]}${lines[1]}`;
}
}

// Fallback: CDP_PORT env var (for Termux / manual browser launch)
// When browser is started with --remote-debugging-port=N, there's no
// DevToolsActivePort file — discover the WebSocket URL via HTTP.
const cdpPort = process.env.CDP_PORT;
if (cdpPort) {
const host = process.env.CDP_HOST || '127.0.0.1';
try {
const resp = await fetch(`http://${host}:${cdpPort}/json/version`);
const data = await resp.json();
if (data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
} catch (e) {
throw new Error(
`CDP_PORT=${cdpPort} but cannot connect to ${host}:${cdpPort}. ` +
`Is Chrome running with --remote-debugging-port=${cdpPort}?\n` +
` e.g. chromium-browser --headless --no-sandbox --disable-gpu --remote-debugging-port=${cdpPort}\n` +
` (${e.message})`
);
}
}

throw new Error('No DevToolsActivePort found. Enable remote debugging at chrome://inspect/#remote-debugging');
}

const sleep = (ms) => new Promise(r => setTimeout(r, ms));
Expand Down Expand Up @@ -488,7 +512,7 @@ async function runDaemon(targetId) {

const cdp = new CDP();
try {
await cdp.connect(getWsUrl());
await cdp.connect(await getWsUrl());
} catch (e) {
process.stderr.write(`Daemon: cannot connect to Chrome: ${e.message}\n`);
process.exit(1);
Expand Down Expand Up @@ -791,7 +815,7 @@ async function main() {

if (cmd === 'list' || cmd === 'ls') {
const cdp = new CDP();
await cdp.connect(getWsUrl());
await cdp.connect(await getWsUrl());
const pages = await getPages(cdp);
cdp.close();
writeFileSync(PAGES_CACHE, JSON.stringify(pages), { mode: 0o600 });
Expand All @@ -804,7 +828,7 @@ async function main() {
if (cmd === 'open') {
const url = args[0] || 'about:blank';
const cdp = new CDP();
await cdp.connect(getWsUrl());
await cdp.connect(await getWsUrl());
const { targetId } = await cdp.send('Target.createTarget', { url });
// Refresh cache; new tab may not appear in getTargets immediately, so add it manually
const pages = await getPages(cdp);
Expand Down