deer-flow/docs/superpowers/plans/2026-06-29-community-browserless-web-capture.md
Ryker_Feng a8f950feb6
feat(community): add Browserless web_capture screenshot tool (#3881)
* feat(community): add Browserless web_capture screenshot tool

Add a web_capture tool that renders a page via Browserless /screenshot and
presents it through the artifact system, alongside the existing Browserless
web_fetch provider.

Hardening:
- SSRF guard: reject URLs resolving to private/loopback/link-local (incl. the
  169.254.169.254 cloud-metadata endpoint)/reserved/multicast/unspecified
  addresses; opt out via allow_private_addresses for internal targets.
- Surface a warning when Browserless renders a target page that itself
  responded with a non-2xx/3xx status (X-Response-Code), so an error/anti-bot
  page is not mistaken for valid visual evidence.
- Dedupe colliding output filenames instead of silently overwriting prior
  captures.

Docs: comment out token: $BROWSERLESS_TOKEN in tool examples (an unset $VAR
fails AppConfig startup) and document allow_private_addresses.

* fix(community): format web_capture guard + document local Browserless startup

Address PR #3881 review: fix the lint-backend failure (ruff format on
browserless/tools.py) and add local Browserless startup instructions to
CONFIGURATION.md so reviewers can run the service to try web_fetch/web_capture.
2026-07-01 23:41:58 +08:00

248 lines
9.3 KiB
Markdown

# Community Browserless Web Capture Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:test-driven-development for code changes. Commit and push steps are intentionally deferred because this local branch must stay uncommitted until user review.
**Goal:** Add an opt-in community `web_capture` tool backed by Browserless `/screenshot` that renders a URL in headless Chrome, saves a PNG/JPEG/WebP screenshot into the current thread outputs directory, and exposes the file as a DeerFlow artifact path.
**Architecture:** Keep the integration inside the existing `deerflow.community.browserless` package. `BrowserlessClient` owns HTTP request construction and binary response handling; `tools.py` owns DeerFlow runtime/config resolution, output filename generation, file persistence, and artifact-state updates. The feature uses existing thread-data outputs semantics rather than inventing a new artifact channel.
**Tech Stack:** Python 3.12, LangChain tool decorators, LangGraph `Command`, `httpx.AsyncClient`, Browserless REST `/screenshot`, pytest.
---
## Technical Design
### User-Facing Tool Contract
Add a new configured tool:
```yaml
tools:
- name: web_capture
group: web
use: deerflow.community.browserless.tools:web_capture_tool
base_url: http://localhost:3032
timeout_s: 30
output_format: png
full_page: true
viewport_width: 1280
viewport_height: 720
# token: $BROWSERLESS_TOKEN
# wait_for_selector: main
# wait_for_selector_timeout_ms: 5000
# wait_for_timeout_ms: 1000
# best_attempt: true
```
The model-callable arguments should stay narrow:
```python
async def web_capture_tool(
runtime: Runtime,
url: str,
tool_call_id: Annotated[str, InjectedToolCallId],
filename: str | None = None,
full_page: bool | None = None,
output_format: str | None = None,
viewport_width: int | None = None,
viewport_height: int | None = None,
) -> Command:
```
Behavior:
- Accept only explicit `http://` or `https://` URLs.
- Save the screenshot under `runtime.state["thread_data"]["outputs_path"]`.
- Return a `Command` that appends the virtual artifact path `/mnt/user-data/outputs/<filename>` and a short `ToolMessage`.
- Use tool config defaults when optional callable args are omitted.
- Read Browserless credentials from the configured `token` value or the `BROWSERLESS_TOKEN` environment variable.
### Browserless API Mapping
Official Browserless docs describe `POST /screenshot` with JSON body:
```json
{
"url": "https://example.com/",
"options": {
"fullPage": true,
"type": "png"
}
}
```
Browserless returns binary image content with `Content-Type: image/png`, `image/jpeg`, or `image/webp` depending on `options.type`. Shared request fields such as `waitForSelector`, `waitForTimeout`, `bestAttempt`, `gotoOptions`, `rejectResourceTypes`, and `rejectRequestPattern` are top-level fields.
This PR supports:
- `url`
- `options.fullPage`
- `options.type`
- `options.quality` for JPEG/WebP only when configured
- `viewport` from config/callable width and height
- `waitForSelector` from config
- `waitForTimeout` from config
- `bestAttempt` from config
This PR intentionally does not support:
- inline `html`
- `addScriptTag`
- `addStyleTag`
- authenticated profiles
- arbitrary launch parameters
- proxy parameters
Those omitted fields are useful, but exposing them directly to an agent increases the chance of unexpected side effects or credential/session leakage. They can be added later behind explicit config if maintainers want them.
### File and Artifact Boundary
The tool writes to the host-side outputs path that `ThreadDataMiddleware` provides, with filesystem writes offloaded through `asyncio.to_thread`:
```python
thread_data = runtime.state.get("thread_data") or {}
outputs_path = thread_data.get("outputs_path")
```
The final user-facing artifact path is always:
```text
/mnt/user-data/outputs/<safe-filename>.<ext>
```
Filename handling:
- If `filename` is provided, strip directories and normalize it to a safe stem.
- If omitted, derive a readable stem from the URL host/path and append a UTC timestamp.
- Enforce extension from `output_format`.
- Use only ASCII letters, digits, dot, dash, and underscore in the final basename.
### Error Handling
`BrowserlessClient.capture_screenshot()` sends `token` as a query parameter, matching the current Browserless `/screenshot` documentation, and returns a small result object instead of raising expected API/network errors:
```python
@dataclass(frozen=True)
class BrowserlessScreenshotResult:
content: bytes
content_type: str
target_status_code: str
target_status: str
final_url: str
```
Expected failures return strings beginning with `Error:` from the client, matching existing `fetch_html()` behavior:
- non-200 Browserless response
- empty image response
- timeout
- request error
The tool converts any error string into a `ToolMessage` and does not mutate artifacts. Unexpected exceptions are logged and returned as `Error: ...`.
### Tests
Use TDD with `backend/tests/test_browserless_client.py`.
Client tests:
- `capture_screenshot` posts to `/screenshot` with `url`, `options.fullPage`, `options.type`, `viewport`, and wait fields.
- Token is sent as the Browserless `token` query parameter.
- Non-200 status returns an error string with status and response snippet.
- Empty binary content returns a clear error.
Tool tests:
- Successful `web_capture_tool` writes bytes to `outputs_path` and returns artifact path in `Command.update["artifacts"]`.
- Tool reads `web_capture` config block, not `web_fetch`.
- Invalid URL is rejected before calling Browserless.
- Missing runtime thread outputs returns a clear error and writes no file.
- Unsafe filename is sanitized to a basename under outputs.
### Documentation Updates
Update:
- `config.example.yaml`: add commented `web_capture` Browserless block.
- `backend/docs/CONFIGURATION.md`: include `web_capture` in tool list and mention Browserless.
- `frontend/src/content/en/harness/tools.mdx`: add Browserless tab for web fetch and a web capture section.
- `frontend/src/content/zh/harness/tools.mdx`: mirror the English documentation.
- `scripts/doctor.py`: recognize `web_capture` and `browserless` token/config status.
- `backend/packages/harness/deerflow/community/browserless/__init__.py`: export `web_capture_tool`.
No `README.md` update is planned because this is a provider-specific opt-in community tool, and existing provider additions in this area document through config/docs pages.
## Execution Tasks
### Task 1: Write Failing Client Tests
**Files:**
- Modify: `backend/tests/test_browserless_client.py`
Steps:
- Add tests for `BrowserlessClient.capture_screenshot()`.
- Run `cd backend && uv run pytest tests/test_browserless_client.py::TestBrowserlessClient -q`.
- Expected red state: `AttributeError: 'BrowserlessClient' object has no attribute 'capture_screenshot'`.
### Task 2: Implement Browserless Screenshot Client
**Files:**
- Modify: `backend/packages/harness/deerflow/community/browserless/browserless_client.py`
- Modify: `backend/tests/test_browserless_client.py`
Steps:
- Add `BrowserlessScreenshotResult`.
- Add `capture_screenshot()` using `httpx.AsyncClient.post(f"{base_url}/screenshot", ...)`.
- Preserve existing `fetch_html()` behavior.
- Run client tests until green.
### Task 3: Write Failing Tool Tests
**Files:**
- Modify: `backend/tests/test_browserless_client.py`
Steps:
- Add tests for `web_capture_tool`.
- Use a fake runtime with `state={"thread_data": {"outputs_path": str(tmp_path)}}`.
- Patch `_get_browserless_client()` and `_get_tool_config()`.
- Run the new tool tests.
- Expected red state: `AttributeError` or missing `web_capture_tool`.
### Task 4: Implement `web_capture_tool`
**Files:**
- Modify: `backend/packages/harness/deerflow/community/browserless/tools.py`
- Modify: `backend/packages/harness/deerflow/community/browserless/__init__.py`
Steps:
- Add helpers for config lookup, URL validation, format validation, filename sanitization, and virtual artifact path generation.
- Add `@tool("web_capture", parse_docstring=True)`.
- Write the screenshot bytes to outputs.
- Return `Command(update={"artifacts": [virtual_path], "messages": [ToolMessage(...)]})`.
- Run Browserless tests until green.
### Task 5: Update Config, Doctor, and Docs
**Files:**
- Modify: `config.example.yaml`
- Modify: `scripts/doctor.py`
- Modify: `backend/docs/CONFIGURATION.md`
- Modify: `frontend/src/content/en/harness/tools.mdx`
- Modify: `frontend/src/content/zh/harness/tools.mdx`
Steps:
- Add commented config block for `web_capture`.
- Extend doctor provider map.
- Update docs with concise config snippets.
- Run targeted tests and lint/format checks.
### Task 6: Verification and Self-Review
Run:
```bash
cd backend && uv run pytest tests/test_browserless_client.py -q
cd backend && uv run ruff check packages/harness/deerflow/community/browserless tests/test_browserless_client.py
cd backend && uv run ruff format --check packages/harness/deerflow/community/browserless tests/test_browserless_client.py
```
Manual review checklist:
- No tracked changes outside the scoped files above.
- No commit created.
- `web_capture` reads the `web_capture` config block.
- Tool does not expose inline HTML, script injection, style injection, or Browserless profile.
- Tool writes only under current thread outputs path.
- Errors do not mutate artifacts.