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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ See **[TESTING.md](./TESTING.md)** for how to run and write tests.
- **"attach failed: Cannot access a chrome-extension:// URL"** — Another extension may be interfering. Try disabling other extensions temporarily.
- **Empty data or 'Unauthorized' error** — Your Chrome/Chromium login session may have expired. Navigate to the target site and log in again.
- **Node API errors** — Ensure Node.js >= 20. Some dependencies require modern Node APIs.
- **Daemon issues** — Check status: `curl localhost:19825/status` · View logs: `curl localhost:19825/logs`
- **Daemon issues** — Check status: `opencli daemon status` · View logs: `curl http://127.0.0.1:19825/logs`

## Star History

Expand Down
24 changes: 22 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ opencli hackernews top --limit 5
opencli bilibili hot --limit 5
```

如果你需要自定义 daemon 的监听地址或端口,可以新建 `~/.opencli/daemon.yaml`:

```yaml
host: 127.0.0.1
port: 19825
```

- `host`:daemon 监听地址
- `port`:daemon 监听端口

如果 `host` 配成 `0.0.0.0`,CLI 会自动回退用 `127.0.0.1` 连接。

浏览器扩展弹窗里也可以单独设置它要连接的 daemon 地址和端口。

> **Tip**:后续诊断和 daemon 管理:
> ```bash
> opencli doctor # 检查扩展和 daemon 连通性
> opencli daemon status # 查看 daemon 状态
> opencli daemon stop # 停止 daemon
> ```
## 给人类用户

如果你只是想稳定地调用网站或桌面应用能力,主路径很简单:
Expand Down Expand Up @@ -437,8 +457,8 @@ opencli cascade https://api.example.com/data
- **Node API 错误 (如 parseArgs, fs 等)**
- 确保 Node.js 版本 `>= 20`。
- **Daemon 问题**
- 检查 daemon 状态:`curl localhost:19825/status`
- 查看扩展日志:`curl localhost:19825/logs`
- 检查 daemon 状态:`opencli daemon status`
- 查看扩展日志:`curl http://127.0.0.1:19825/logs`


## Star History
Expand Down
14 changes: 9 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions docs/guide/browser-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ opencli daemon restart # Stop + restart
```

Override the timeout via the `OPENCLI_DAEMON_TIMEOUT` environment variable (milliseconds). Set to `0` to keep the daemon alive indefinitely.

To customize the daemon bind address and port persistently, create `~/.opencli/daemon.yaml`:

```yaml
host: 127.0.0.1
port: 19825
```

- `host`: the address the daemon listens on
- `port`: the daemon HTTP/WebSocket port

If `host` is set to `0.0.0.0`, the CLI automatically connects via `127.0.0.1`.

Environment variables still work and take precedence:

```bash
OPENCLI_DAEMON_HOST=0.0.0.0
OPENCLI_DAEMON_PORT=29876
```

The browser extension popup also lets you set the daemon host and port it should connect to.
3 changes: 2 additions & 1 deletion docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ OPENCLI_CDP_TARGET=detail.1688.com opencli 1688 item 841141931191 -f json
opencli daemon status

# View extension logs
curl localhost:19825/logs
curl http://127.0.0.1:19825/logs

# Stop or restart the daemon
opencli daemon stop
Expand All @@ -46,6 +46,7 @@ opencli doctor
```

> The daemon auto-exits after 4 hours of inactivity (no CLI requests and no extension connection). Override with `OPENCLI_DAEMON_TIMEOUT` (milliseconds, `0` = never timeout).
> If you changed the daemon address or port, use the values from `~/.opencli/daemon.yaml` or `opencli daemon status`.

### Desktop adapter connection issues

Expand Down
129 changes: 112 additions & 17 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
const DAEMON_PORT = 19825;
const DAEMON_HOST = "localhost";
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
const WS_RECONNECT_BASE_DELAY = 2e3;
const WS_RECONNECT_MAX_DELAY = 5e3;

const DEFAULT_DAEMON_HOST = "127.0.0.1";
const DEFAULT_DAEMON_PORT = 19825;
function normalizeHost(value) {
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function normalizePort(value) {
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number.parseInt(value, 10);
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
}
return void 0;
}
async function getDaemonEndpointConfig(storage = chrome.storage?.local) {
if (!storage) {
return {
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT
};
}
const raw = await storage.get(["daemonHost", "daemonPort"]);
return {
host: normalizeHost(raw.daemonHost) ?? DEFAULT_DAEMON_HOST,
port: normalizePort(raw.daemonPort) ?? DEFAULT_DAEMON_PORT
};
}
function buildDaemonUrls(config) {
return {
pingUrl: `http://${config.host}:${config.port}/ping`,
wsUrl: `ws://${config.host}:${config.port}/ext`
};
}

const attached = /* @__PURE__ */ new Set();
const networkCaptures = /* @__PURE__ */ new Map();
function isDebuggableUrl$1(url) {
Expand Down Expand Up @@ -248,8 +277,9 @@ function registerListeners() {
const state = networkCaptures.get(tabId);
if (!state) return;
if (method === "Network.requestWillBeSent") {
const requestId = String(params?.requestId || "");
const request = params?.request;
const networkParams = params;
const requestId = String(networkParams?.requestId || "");
const request = networkParams?.request;
const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, {
url: request?.url,
method: request?.method,
Expand All @@ -269,8 +299,9 @@ function registerListeners() {
return;
}
if (method === "Network.responseReceived") {
const requestId = String(params?.requestId || "");
const response = params?.response;
const networkParams = params;
const requestId = String(networkParams?.requestId || "");
const response = networkParams?.response;
const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, {
url: response?.url
});
Expand All @@ -281,7 +312,8 @@ function registerListeners() {
return;
}
if (method === "Network.loadingFinished") {
const requestId = String(params?.requestId || "");
const networkParams = params;
const requestId = String(networkParams?.requestId || "");
const stateEntryIndex = state.requestToIndex.get(requestId);
if (stateEntryIndex === void 0) return;
const entry = state.entries[stateEntryIndex];
Expand Down Expand Up @@ -325,14 +357,16 @@ console.error = (...args) => {
};
async function connect() {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
const endpoint = await getDaemonEndpointConfig();
const urls = buildDaemonUrls(endpoint);
try {
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
const res = await fetch(urls.pingUrl, { signal: AbortSignal.timeout(1e3) });
if (!res.ok) return;
} catch {
return;
}
try {
ws = new WebSocket(DAEMON_WS_URL);
ws = new WebSocket(urls.wsUrl);
} catch {
scheduleReconnect();
return;
Expand Down Expand Up @@ -479,10 +513,71 @@ chrome.alarms.onAlarm.addListener((alarm) => {
});
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === "getStatus") {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null
void getDaemonEndpointConfig().then((endpoint) => {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
host: endpoint.host,
port: endpoint.port
});
});
return true;
}
if (msg?.type === "getDaemonConfig") {
void getDaemonEndpointConfig().then((endpoint) => {
sendResponse(endpoint);
});
return true;
}
if (msg?.type === "setDaemonConfig") {
const nextHost = typeof msg.host === "string" ? msg.host.trim() : "";
const nextPort = Number.parseInt(String(msg.port ?? ""), 10);
const updates = {};
const removals = [];
if (nextHost) updates.daemonHost = nextHost;
else removals.push("daemonHost");
if (String(msg.port ?? "").trim()) {
if (!Number.isInteger(nextPort) || nextPort <= 0 || nextPort > 65535) {
sendResponse({ ok: false, error: "Invalid port" });
return true;
}
updates.daemonPort = nextPort;
} else {
removals.push("daemonPort");
}
void Promise.resolve().then(async () => {
if (Object.keys(updates).length > 0) {
await chrome.storage.local.set(updates);
}
if (removals.length > 0) {
await chrome.storage.local.remove(removals);
}
const endpoint = await getDaemonEndpointConfig();
if (ws) {
try {
ws.close();
} catch {
ws = null;
}
} else {
ws = null;
}
reconnectTimer = null;
void connect();
sendResponse({
ok: true,
host: endpoint.host,
port: endpoint.port,
connected: false,
reconnecting: true
});
}).catch((err) => {
sendResponse({
ok: false,
error: err instanceof Error ? err.message : String(err)
});
});
return true;
}
return false;
});
Expand Down Expand Up @@ -609,7 +704,7 @@ async function resolveTab(tabId, workspace, initialUrl) {
}
}
const existingSession = automationSessions.get(workspace);
if (existingSession?.preferredTabId !== null) {
if (existingSession && existingSession.preferredTabId !== null) {
try {
const preferredTab = await chrome.tabs.get(existingSession.preferredTabId);
if (isDebuggableUrl(preferredTab.url)) return { tabId: preferredTab.id, tab: preferredTab };
Expand Down Expand Up @@ -666,7 +761,7 @@ async function handleExec(cmd, workspace) {
if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
const tabId = await resolveTabId(cmd.tabId, workspace);
try {
const aggressive = workspace.startsWith("operate:");
const aggressive = workspace.startsWith("browser:") || workspace.startsWith("operate:");
const data = await evaluateAsync(tabId, cmd.code, aggressive);
return { id: cmd.id, ok: true, data };
} catch (err) {
Expand Down Expand Up @@ -870,7 +965,7 @@ async function handleCdp(cmd, workspace) {
}
const tabId = await resolveTabId(cmd.tabId, workspace);
try {
const aggressive = workspace.startsWith("operate:");
const aggressive = workspace.startsWith("browser:") || workspace.startsWith("operate:");
await ensureAttached(tabId, aggressive);
const data = await chrome.debugger.sendCommand(
{ tabId },
Expand Down
3 changes: 2 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"tabs",
"cookies",
"activeTab",
"alarms"
"alarms",
"storage"
],
"host_permissions": [
"<all_urls>"
Expand Down
Loading