Skip to content
Merged
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
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### Features

- **Comprehensive CLI Tools** - 67 tools organized into 15 categories; Designed to integrate with any AI agent
- **Comprehensive CLI Tools** - 69 tools organized into 15 categories; Designed to integrate with any AI agent
- **Python-based Tools** - Used for agent / workflow code generation; Easier integration with [Bridgic](https://github.com/bitsky-tech/bridgic)
- **Snapshot with Semantic Invariance** - A representation of page snapshot based on accessibility tree and a specially designed ref-generation algorithm that ensures element refs remain unchanged across page reloads
- **Skills** - Used for guided exploration and code generation; Compatible with most of coding agents
Expand Down Expand Up @@ -168,7 +168,7 @@ if __name__ == "__main__":

### CLI Tools

`bridgic-browser` ships with a command-line interface for controlling a browser from the terminal (67 tools organized into 15 categories). A persistent daemon process holds a browser instance; each CLI invocation connects over a Unix domain socket and exits immediately.
`bridgic-browser` ships with a command-line interface for controlling a browser from the terminal (69 tools organized into 15 categories). A persistent daemon process holds a browser instance; each CLI invocation connects over a Unix domain socket and exits immediately.

#### Configuration

Expand Down Expand Up @@ -410,7 +410,7 @@ browser = Browser(cdp="ws://localhost:9222/devtools/browser/...")
| Wait | `wait [SECONDS] [TEXT] [--gone]` |
| Tabs | `tabs`, `new-tab`, `switch-tab`, `close-tab` |
| Evaluate | `eval`, `eval-on` |
| Capture | `screenshot`, `pdf` |
| Capture | `screenshot`, `pdf`, `downloads`, `wait-download` |
| Network | `network-start`, `network-stop`, `network`, `wait-network` |
| Dialog | `dialog-setup`, `dialog`, `dialog-remove` |
| Storage | `storage-save`, `storage-load`, `cookies-clear`, `cookies`, `cookie-set` |
Expand All @@ -427,7 +427,7 @@ bridgic-browser scroll -h

### Python Tools

Bridgic Browser provides 67 tools organized into 15 categories. Use `BrowserToolSetBuilder` with category/name selection for scenario-focused tool sets.
Bridgic Browser provides 69 tools organized into 15 categories. Use `BrowserToolSetBuilder` with category/name selection for scenario-focused tool sets.

#### Category-based Selection

Expand Down Expand Up @@ -533,9 +533,11 @@ tools = [*builder1.build()["tool_specs"], *builder2.build()["tool_specs"]]
**Wait (1 tool):**
- `wait_for(time_seconds, text, text_gone, selector, state, timeout)` - Wait for conditions

**Capture (2 tools):**
**Capture (4 tools):**
- `take_screenshot(filename=None, ref=None, full_page=False, type="png")` - Capture screenshot
- `save_pdf(filename)` - Save page as PDF
- `get_downloaded_files_text()` - Numbered list of all files downloaded in this session
- `wait_for_next_download(timeout=30.0)` - Block until next download completes; returns a one-line summary or a timeout message

**Network (4 tools):**
- `start_network_capture()` / `stop_network_capture()` / `get_network_requests()` - Network monitoring
Expand Down Expand Up @@ -609,6 +611,8 @@ tools = [*builder1.build()["tool_specs"], *builder2.build()["tool_specs"]]
| `wait` | `wait_for` |
| `screenshot` | `take_screenshot` |
| `pdf` | `save_pdf` |
| `downloads` | `get_downloaded_files_text` |
| `wait-download` | `wait_for_next_download` |
| `network-start` | `start_network_capture` |
| `network` | `get_network_requests` |
| `network-stop` | `stop_network_capture` |
Expand Down Expand Up @@ -702,6 +706,8 @@ browser = Browser(stealth=config, headless=False)

#### Downloads

`Browser` always creates a `DownloadManager` and always accepts downloads. Files are saved to `downloads_path` if configured, or `~/Downloads` by default.

bridgic preserves the original filename, suppresses the "Save As" dialog, and keeps the API the same across modes. Internally there are two pipelines — `DownloadManager` for non-CDP / CDP-owned, and `CdpDownloadRenamer` for CDP-borrowed (page-level CDP routing of `setDownloadBehavior(allowAndName)`). See [CLAUDE.md → Downloads](CLAUDE.md#downloads) for the full design.

##### Download path matrix
Expand All @@ -713,21 +719,30 @@ bridgic preserves the original filename, suppresses the "Save As" dialog, and ke
| **CLI** | CDP (`--cdp ...`) | yes | the explicit value |
| **CLI** | CDP | no | the CLI client's working directory at command time (`os.getcwd()`) — `curl -O`-style ergonomics |
| **SDK** (`Browser(...)`) | non-CDP | yes | the explicit value |
| **SDK** | non-CDP | no | downloads not captured (Playwright wipes the temp dir on close — pass `downloads_path`) |
| **SDK** | non-CDP | no | `~/Downloads` (DownloadManager auto-default) |
| **SDK** | CDP (`Browser(cdp=...)`) | yes | the explicit value |
| **SDK** | CDP | no | `~/Downloads` (SDK has no CLI CWD hint) |

```python
# Non-CDP (DownloadManager pipeline)
browser = Browser(downloads_path="./downloads", headless=True)
await browser.navigate_to("https://example.com")
# Programmatic access to completed downloads

# Trigger a download, then wait for it to complete
await browser.click_element_by_ref("8d4b03a9")
result = await browser.wait_for_next_download(timeout=30.0)
# "Download complete: report.pdf — 261.0 KB — /home/user/Downloads/report.pdf"

# Get the formatted list of everything downloaded in this session
print(await browser.get_downloaded_files_text())

# Or access the raw list
for f in browser.download_manager.downloaded_files:
print(f"Downloaded: {f.file_name} ({f.file_size} bytes)")

# CDP-borrowed (CdpDownloadRenamer pipeline; downloads land at downloads_path
# with real filenames; download_manager is None — wait_for_download is
# unsupported here).
# with real filenames; download_manager is None — wait_for_download /
# wait_for_next_download are unsupported here).
browser = Browser(cdp="auto", downloads_path="./downloads")
```

Expand Down
31 changes: 23 additions & 8 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### 特性

- **完善的 CLI 工具** — 67 个工具分为 15 类;可与各类 AI 智能体集成
- **完善的 CLI 工具** — 69 个工具分为 15 类;可与各类 AI 智能体集成
- **基于 Python 的工具** — 用于智能体 / 工作流代码生成;更易与 [Bridgic](https://github.com/bitsky-tech/bridgic) 集成
- **语义不变的快照** — 基于无障碍树与专门设计的 ref 生成算法,保证元素 ref 在页面重载后仍可对应同一元素
- **Skills** — 用于引导探索与代码生成;兼容多数编程类智能体
Expand Down Expand Up @@ -168,7 +168,7 @@ if __name__ == "__main__":

### CLI 工具

`bridgic-browser` 提供命令行界面,用于在终端控制浏览器(67 个工具、15 类)。持久化 daemon 进程持有浏览器实例;每次 CLI 调用通过 Unix 域套接字连接后立即退出。
`bridgic-browser` 提供命令行界面,用于在终端控制浏览器(69 个工具、15 类)。持久化 daemon 进程持有浏览器实例;每次 CLI 调用通过 Unix 域套接字连接后立即退出。

#### 配置

Expand Down Expand Up @@ -404,7 +404,7 @@ browser = Browser(cdp="ws://localhost:9222/devtools/browser/...")
| 等待 | `wait [SECONDS] [TEXT] [--gone]` |
| 标签页 | `tabs`, `new-tab`, `switch-tab`, `close-tab` |
| 执行 | `eval`, `eval-on` |
| 捕获 | `screenshot`, `pdf` |
| 捕获 | `screenshot`, `pdf`, `downloads`, `wait-download` |
| 网络 | `network-start`, `network-stop`, `network`, `wait-network` |
| 对话框 | `dialog-setup`, `dialog`, `dialog-remove` |
| 存储 | `storage-save`, `storage-load`, `cookies-clear`, `cookies`, `cookie-set` |
Expand All @@ -421,7 +421,7 @@ bridgic-browser scroll -h

### Python 工具

Bridgic Browser 提供 67 个工具,分为 15 类。使用 `BrowserToolSetBuilder` 按类别/名称选择,以适配不同场景。
Bridgic Browser 提供 69 个工具,分为 15 类。使用 `BrowserToolSetBuilder` 按类别/名称选择,以适配不同场景。

#### 按类别选择

Expand Down Expand Up @@ -527,9 +527,11 @@ tools = [*builder1.build()["tool_specs"], *builder2.build()["tool_specs"]]
**等待(1 个工具):**
- `wait_for(time_seconds, text, text_gone, selector, state, timeout)` - 等待条件

**捕获(2 个工具):**
**捕获(4 个工具):**
- `take_screenshot(filename=None, ref=None, full_page=False, type="png")` - 截图
- `save_pdf(filename)` - 将页面保存为 PDF
- `get_downloaded_files_text()` - 返回本次会话所有已下载文件的编号列表
- `wait_for_next_download(timeout=30.0)` - 等待下一个下载完成;返回单行摘要或超时提示

**网络(4 个工具):**
- `start_network_capture()` / `stop_network_capture()` / `get_network_requests()` - 网络监控
Expand Down Expand Up @@ -603,6 +605,8 @@ tools = [*builder1.build()["tool_specs"], *builder2.build()["tool_specs"]]
| `wait` | `wait_for` |
| `screenshot` | `take_screenshot` |
| `pdf` | `save_pdf` |
| `downloads` | `get_downloaded_files_text` |
| `wait-download` | `wait_for_next_download` |
| `network-start` | `start_network_capture` |
| `network` | `get_network_requests` |
| `network-stop` | `stop_network_capture` |
Expand Down Expand Up @@ -696,6 +700,8 @@ browser = Browser(stealth=config, headless=False)

#### 下载

`Browser` 始终创建 `DownloadManager` 并自动接受下载。文件保存至 `downloads_path`(若已配置),否则默认保存到 `~/Downloads`。

bridgic 在所有模式下都保留原始文件名、屏蔽"另存为"对话框,API 一致。内部有两条流水线 —— 非 CDP / CDP-owned 用 `DownloadManager`,CDP-borrowed 用 `CdpDownloadRenamer`(通过 page-level CDP session 下发 `setDownloadBehavior(allowAndName)`)。完整设计见 [CLAUDE.md → Downloads](CLAUDE.md#downloads)。

##### 下载路径矩阵
Expand All @@ -707,20 +713,29 @@ bridgic 在所有模式下都保留原始文件名、屏蔽"另存为"对话框,
| **CLI** | CDP (`--cdp ...`) | 有 | 显式值 |
| **CLI** | CDP | 无 | CLI 启动时的工作目录(`os.getcwd()`)—— `curl -O` 风格 |
| **SDK** (`Browser(...)`) | 非 CDP | 有 | 显式值 |
| **SDK** | 非 CDP | 无 | 下载不被捕获(Playwright 会清掉 temp dir —— 请显式传 `downloads_path`) |
| **SDK** | 非 CDP | 无 | `~/Downloads`(DownloadManager 自动默认) |
| **SDK** | CDP (`Browser(cdp=...)`) | 有 | 显式值 |
| **SDK** | CDP | 无 | `~/Downloads`(SDK 没有 CLI CWD 提示) |

```python
# 非 CDP(DownloadManager 流水线)
browser = Browser(downloads_path="./downloads", headless=True)
await browser.navigate_to("https://example.com")
# 程序化访问已完成的下载

# 触发下载,然后等待其完成
await browser.click_element_by_ref("8d4b03a9")
result = await browser.wait_for_next_download(timeout=30.0)
# "Download complete: report.pdf — 261.0 KB — /home/user/Downloads/report.pdf"

# 列出本次会话的所有下载记录
print(await browser.get_downloaded_files_text())

# 或直接访问原始列表
for f in browser.download_manager.downloaded_files:
print(f"已下载:{f.file_name}({f.file_size} 字节)")

# CDP-borrowed(CdpDownloadRenamer 流水线;文件以真名落到 downloads_path,
# download_manager 为 None —— wait_for_download 在此模式不支持)
# download_manager 为 None —— wait_for_download / wait_for_next_download 在此模式不支持)
browser = Browser(cdp="auto", downloads_path="./downloads")
```

Expand Down
6 changes: 5 additions & 1 deletion bridgic/browser/_cli_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
(ToolCategory.KEYBOARD, ["type", "press", "key-down", "key-up"]),
(ToolCategory.MOUSE, ["scroll", "mouse-click", "mouse-move", "mouse-drag", "mouse-down", "mouse-up"]),
(ToolCategory.WAIT, ["wait"]),
(ToolCategory.CAPTURE, ["screenshot", "pdf"]),
(ToolCategory.CAPTURE, ["screenshot", "pdf", "downloads", "wait-download"]),
(ToolCategory.NETWORK, ["network-start", "network", "network-stop", "wait-network"]),
(ToolCategory.DIALOG, ["dialog-setup", "dialog", "dialog-remove"]),
(ToolCategory.STORAGE, ["cookies", "cookie-set", "cookies-clear", "storage-save", "storage-load"]),
Expand Down Expand Up @@ -115,6 +115,8 @@
"close-tab": (ToolCategory.TABS, "Close a tab by page_id (or current tab if omitted); run 'tabs' first to list page IDs"),
"screenshot": (ToolCategory.CAPTURE, "Save a screenshot to PATH [--full-page]"),
"pdf": (ToolCategory.CAPTURE, "Save the current page as PDF"),
"downloads": (ToolCategory.CAPTURE, "List all files downloaded in this session"),
"wait-download": (ToolCategory.CAPTURE, "Wait up to SECONDS for the next download to complete (default: 30)"),
"console-start": (ToolCategory.DEVELOPER, "Start capturing browser console output"),
"console-stop": (ToolCategory.DEVELOPER, "Stop capturing browser console output"),
"console": (ToolCategory.DEVELOPER, "Get captured console messages [--filter TYPE] [--no-clear]"),
Expand Down Expand Up @@ -219,6 +221,8 @@
"video-stop": "stop_video",
"close": "close",
"resize": "browser_resize",
"downloads": "get_downloaded_files_text",
"wait-download": "wait_for_next_download",
}

# CLI commands that are informational and not backed by Browser tool methods.
Expand Down
26 changes: 26 additions & 0 deletions bridgic/browser/cli/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,32 @@ def cmd_video_stop(path: str | None) -> None:
_err(exc)


# ── Downloads ─────────────────────────────────────────────────────────────────

@cli.command("downloads", context_settings=CONTEXT_SETTINGS)
def cmd_downloads() -> None:
"""List all files downloaded in this browser session."""
try:
_ok(send_command("downloads", {}, start_if_needed=False))
except Exception as exc:
_err(exc)


@cli.command("wait-download", context_settings=CONTEXT_SETTINGS)
@click.argument("seconds", type=float, required=False, default=30.0)
def cmd_wait_download(seconds: float) -> None:
"""Wait up to SECONDS for the next download to complete (default: 30).

Run this immediately after clicking a download link or button.
Returns the file name, size, and path when the download finishes,
or an error message if the timeout expires with no download.
"""
try:
_ok(send_command("wait_download", {"timeout": seconds}, start_if_needed=False))
except Exception as exc:
_err(exc)


# ── Lifecycle ─────────────────────────────────────────────────────────────────

@cli.command("close", context_settings=CONTEXT_SETTINGS)
Expand Down
14 changes: 14 additions & 0 deletions bridgic/browser/cli/_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,17 @@ async def _handle_resize(browser: "Browser", args: Dict[str, Any]) -> str:
return await browser.browser_resize(args.get("width", 1280), args.get("height", 720))


# ── Downloads ─────────────────────────────────────────────────────────────────

async def _handle_downloads(browser: "Browser", _args: Dict[str, Any]) -> str:
return await browser.get_downloaded_files_text()


async def _handle_wait_download(browser: "Browser", args: Dict[str, Any]) -> str:
timeout = float(args.get("timeout", 30.0))
return await browser.wait_for_next_download(timeout=timeout)


_HANDLERS = {
# Navigation
"open": _handle_open,
Expand Down Expand Up @@ -611,6 +622,9 @@ async def _handle_resize(browser: "Browser", args: Dict[str, Any]) -> str:
# ("close" is intercepted in the connection handler — see comment above
# the lifecycle section.)
"resize": _handle_resize,
# Downloads
"downloads": _handle_downloads,
"wait_download": _handle_wait_download,
}


Expand Down
39 changes: 33 additions & 6 deletions bridgic/browser/session/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,11 @@ def __init__(
# than hit a misleading NO_ACTIVE_PAGE when `_page` is mid-teardown.
self._closing: bool = False

# Download manager - handles saving files with correct filenames
self._download_manager: Optional[DownloadManager] = None
if self._downloads_path:
self._download_manager = DownloadManager(downloads_path=self._downloads_path)
# Download manager - always active so CLI can query/wait for downloads.
# Uses the configured path if provided, otherwise defaults to ~/Downloads.
self._download_manager: Optional[DownloadManager] = DownloadManager(
downloads_path=self._downloads_path or None
)

# CDP-borrowed download infrastructure. Populated in `_start()` when
# connecting via CDP without owning the context. The renamer is the
Expand Down Expand Up @@ -723,6 +724,32 @@ def downloaded_files(self) -> List[DownloadedFile]:
return self._download_manager.downloaded_files
return []

async def get_downloaded_files_text(self) -> str:
"""Return a human-readable summary of all downloads in this session."""
files = self.downloaded_files
if not files:
return "No downloads in this session."
lines = []
for i, f in enumerate(files, 1):
size_kb = f.file_size / 1024
size_str = f"{size_kb / 1024:.2f} MB" if size_kb >= 1024 else f"{size_kb:.1f} KB"
lines.append(f"[{i}] {f.file_name} — {size_str} — {f.path}")
return "\n".join(lines)

async def wait_for_next_download(self, timeout: float = 30.0) -> str:
"""Wait up to *timeout* seconds for the next download to complete.

Returns a one-line summary of the downloaded file, or a timeout message.
"""
if not self._download_manager:
return "Download manager not available."
file = await self._download_manager.wait_for_next_download(timeout=timeout)
if file is None:
return f"No download completed within {timeout:.0f}s timeout."
size_kb = file.file_size / 1024
size_str = f"{size_kb / 1024:.2f} MB" if size_kb >= 1024 else f"{size_kb:.1f} KB"
return f"Download complete: {file.file_name} — {size_str} — {file.path}"

@property
def headless(self) -> bool:
"""Whether the user requested a windowless (headless) browser.
Expand Down Expand Up @@ -1041,8 +1068,8 @@ def _get_context_options(self) -> Dict[str, Any]:
if self._color_scheme is not None:
options["color_scheme"] = self._color_scheme

# Auto-enable downloads if downloads_path is configured
if self._downloads_path and "accept_downloads" not in self._extra_kwargs:
# Always accept downloads so DownloadManager can track them
if "accept_downloads" not in self._extra_kwargs:
options["accept_downloads"] = True

# Extract context-specific kwargs (user values override everything)
Expand Down
Loading
Loading