# BrowseWeb 詳細ガイド

ヘッドレスブラウザで Web ページを操作するツール。同一ジョブ内ではブラウザコンテキスト（Cookie・ログイン状態）が永続化される。

## 2 つのモード

### 1. 基本モード — URL を開いてテキスト取得

```js
BrowseWeb({ url: "https://example.com" })
```

ローカルで生成した HTML をブラウザで確認したい場合は、workspace ルートからの **相対パス** をそのまま渡す（推奨）。

```js
BrowseWeb({ url: "output/viewer.html" })
```

例:
- `output/viewer.html` を開く → `BrowseWeb({ url: "output/viewer.html" })`
- `input/sample.html` を開く → `BrowseWeb({ url: "input/sample.html" })`

内部的には実行中ジョブの workspace 絶対パスと結合され `file://` URL に変換される。`../` で workspace 外に出るパスは拒否される。`file:///` で始まる絶対 URL を直接渡すことも可能だが、workspace 外を指すものは拒否される。

オプション:
- `waitFor`: 待機する CSS セレクタ（省略時は load イベント完了まで待機）
- `extractSelector`: 特定要素のテキストだけ抽出する CSS セレクタ
- `screenshot`: スクリーンショットを保存するファイル名（例: `"page.png"` → `output/page.png`）
- `timeout`: タイムアウト（ms、デフォルト 60000）

### 2. アクションモード — 連続操作

```js
BrowseWeb({
  actions: [
    { type: "goto", url: "https://example.com/login" },
    { type: "fill", ref: "e3", value: "user@example.com" },
    { type: "click", ref: "e5" },
    { type: "getText" }
  ]
})
```

利用可能な `type`:
- `goto` — `url` で指定したページに遷移
- `click` — `selector` または `ref` で要素をクリック
- `fill` — `selector` または `ref` の input/textarea に `value` を入力
- `screenshot` — `value` で指定したファイル名で保存（省略時 `screenshot.png`）
- `getText` — 全ページのスナップショット（ref 注釈付き）または `selector` 内のテキストを取得
- `wait` — `ms` ミリ秒待機（最大 30000）
- `dumpHtml` — `ref` または `selector`（省略時 body）の outerHTML を取得（脱出口、後述）

## 長文ページの取得（preview + ファイル保存）

`getText` (selector 有無問わず) およびスナップショットの戻り値が **5000 文字を超える** 場合、フルテキストはワークスペースの `logs/browse/{ISO-timestamp}-{hash}.txt` に保存され、戻り値は **先頭 5000 文字 + 続きの取得方法案内** になる:

```
（先頭 5000 文字）
... (truncated; full 38214 chars saved to logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt — Read({file_path:"logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt", offset, limit}) で続きを取得可能)
```

続きを読みたい場合は `Read` ツールで `offset` / `limit` を指定:

```js
Read({ file_path: "logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt", offset: 200, limit: 200 })
```

5000 文字以下のページなら従来通り全文が直接返り、ファイルは作成されない。

## ref 注釈の仕組み（重要）

`BrowseWeb({ url })` や `getText` の出力には、操作可能な要素が以下のような注釈付きで埋め込まれる:

```
ようこそ
{e1 link "ホーム" href="/"} {e2 link "製品" href="/products"}
ログインしてください
{e3 textbox name="email" placeholder="メールアドレス"}
{e4 textbox name="password"}
{e5 button "ログイン"}
```

- `e1`, `e2`, ... の ID（ref）は出現順に自動採番される
- 各 ref は内部的に Playwright で解釈可能なセレクタ（`data-testid` / `id` / `[name]` / `aria-label` / nth-of-type CSS chain の優先順）にマッピングされている
- click/fill アクションで `ref: "e5"` のように指定するだけで操作できる
- **CSS セレクタを自分で組み立てる必要がない**

### 検出される要素の範囲

ref が振られるのは以下の要素:

- 標準 HTML タグ: `<a>` / `<button>` / `<input>` / `<select>` / `<textarea>` / `<label>` / `<summary>` / `<details>` / `<option>`
- ARIA role: `button` / `link` / `menuitem` / `menuitemcheckbox` / `menuitemradio` / `tab` / `option` / `checkbox` / `radio` / `switch` / `combobox` / `listbox` / `slider` / `spinbutton` / `textbox` / `searchbox` / `treeitem`
- `[onclick]` / `[tabindex>=0]` / `[contenteditable=true]` 属性
- JavaScript で `addEventListener('click'|'mousedown'|'pointerdown', ...)` 経由で listener が後付けされた要素（jQuery / vanilla JS / Vue / Svelte の compile 後コードで多用される）
- open shadow DOM 内部の上記要素
- iframe 内の上記要素（同一オリジン / cross-origin 共に対応。Stripe Elements / OAuth / reCAPTCHA など）

検出されないもの: closed shadow DOM、`<canvas>` / WebGL の描画内容、React の `onClick={...}`（ただし React コンポーネントは大抵 `<button>` か `role="button"` を使うので別経路で拾える）。

### iframe 内の要素

iframe を含むページの `getText` の出力は、メインフレームのテキストの後ろに **フレームごとのセクション** が並ぶ形式になる。メインフレーム本文中には iframe の位置に `[[IFRAME ...]]` プレースホルダーが残るので、フレームの出現順や種別が把握できる:

```
これは決済画面です
[[IFRAME name=card title=Card details src=https://js.stripe.com/v3/elements]]
[ボタン] {e3 button "支払う"}

--- iframe f1 url="https://js.stripe.com/v3/elements/..." name="card" ---
{f1.e1 textbox "Card number"}
{f1.e2 textbox "MM / YY"}
{f1.e3 textbox "CVC"}
--- end iframe f1 ---
```

iframe 内の要素を click / fill / dumpHtml したいときは、frame ID prefix 付きの ref を指定するだけ:

```js
BrowseWeb({
  actions: [
    { type: "fill", ref: "f1.e1", value: "4242 4242 4242 4242" },
    { type: "fill", ref: "f1.e2", value: "12 / 30" },
    { type: "fill", ref: "f1.e3", value: "123" },
    { type: "click", ref: "e3" }   // メインフレームの「支払う」ボタン
  ]
})
```

frame ID (`f1`, `f2`, …) は `getText` 取得時の出現順に採番される。同じページに同じ iframe が複数ある場合は src/name で見分けてセクションヘッダーで識別する。

cross-origin iframe (Stripe / OAuth / reCAPTCHA など) でも Playwright が内部で透過的に DOM を取得するので、同じ感覚で操作できる。ただし iframe の中身が完全に読み込まれる前に snapshot を取ると `[empty]` や `[cannot inspect: ...]` が出ることがあるので、その場合は `wait` を挟んで再取得する。

### 状態属性

ref 注釈の末尾には ARIA 状態が列挙される。エージェントは「いまトグルが開いてるか」「チェック済みか」「無効化されてるか」を判断できる:

```
{e3 tab "設定" selected}
{e7 button "保存" disabled}
{e2 combobox "国" expanded haspopup}
{e9 checkbox "規約に同意" checked}
{e5 button "メニュー" pressed}
```

利用される状態: `expanded` / `collapsed` / `pressed` / `selected` / `checked` / `mixed` / `disabled` / `required` / `haspopup`

### ref はいつリセットされる？

- ページ遷移（`goto` または click でナビゲーションが発生）したとき
- 同一ジョブ内でも、ナビゲーション後は **getText を呼んで新しいスナップショットを取得する**
- 同一ジョブが終わるとブラウザコンテキストごと破棄される

## ワークフロー例

### 例1: ログインしてダッシュボードのデータを取得

```js
// Step 1: ログインページを開いて要素を確認
BrowseWeb({ url: "https://app.example.com/login" })
// → 出力に {e3: input[email]}, {e4: input[password]}, {e5: button "ログイン"} が含まれる

// Step 2: フォーム入力 → 送信 → 遷移後の状態を取得
BrowseWeb({
  actions: [
    { type: "fill", ref: "e3", value: "user@example.com" },
    { type: "fill", ref: "e4", value: "p@ssword" },
    { type: "click", ref: "e5" },
    { type: "getText" }   // ← ダッシュボードの新 ref を取得
  ]
})

// Step 3: ダッシュボードでさらにナビゲート（Cookie が維持されているため再ログイン不要）
BrowseWeb({ url: "https://app.example.com/dashboard/orders" })
```

### 例2: 複数ページを順に巡回

```js
// 検索結果ページを開く
BrowseWeb({ url: "https://example.com/search?q=foo" })
// → {e1: link "結果1" href="/item/1"}, {e2: link "結果2" href="/item/2"} ...

// 各リンクの href を確認したら、url 直接指定で各ページへ
BrowseWeb({ url: "https://example.com/item/1" })
BrowseWeb({ url: "https://example.com/item/2" })
```

### 例3: 動的ページの読み込み待ち

```js
BrowseWeb({
  url: "https://app.example.com/spa",
  waitFor: ".content-loaded"   // この CSS セレクタが現れるまで待つ
})
```

## ユーザーに手動操作を委譲する（noVNC 経由のハンドオフ）

BrowseWeb で詰まったとき、エージェントは `InteractiveBrowse` を呼んでブラウザの操作権をユーザーに渡せる。

### 使うべき場面

1. **ログイン / 2FA / SSO 同意画面** — パスワードや TOTP / プッシュ通知を agent に持たせず、ユーザーに直接入力してもらう
2. **CAPTCHA / bot 検証** — reCAPTCHA、画像選択、Cloudflare チャレンジ等
3. **BrowseWeb の click が空振りし続ける** — `dumpHtml` でも構造が複雑すぎて selector が組めない、closed shadow DOM、ドラッグ&ドロップが必須等
4. **canvas / WebGL ベースの UI** — 地図ペインや図形エディタなど DOM では addressable でない領域
5. **画面状態を目視確認したい** — agent が想定通りの画面にいるか不安なとき

### フロー

```js
// Step 1: ユーザーに引き継ぐ宣言
InteractiveBrowse({
  url: "https://example.com/login",
  reason: "ログインが必要です。ID / パスワードを入力して、画面右下の release ボタンを押してください。"
})
// → ジョブが waiting_human に遷移し、UI に noVNC リンクが表示される
// → ユーザーがブラウザ画面で操作 → release を押すとジョブが再開
// → 戻り値に sessionId が含まれる
```

ジョブ再開後、agent は **同じ sessionId で `BrowseWithSession`** を呼んで続きを引き継ぐ:

```js
// Step 2: ユーザーが完了させた状態 (ログイン済み等) で続行
BrowseWithSession({
  sessionId: "abc-123",                      // InteractiveBrowse の戻り値の sessionId
  url: "https://example.com/dashboard",
  action: "getText"                           // または click / fill / screenshot
})
```

### `reason` の書き方

ユーザーに何をしてほしいかは `reason` フィールドで明確に伝えること。UI に表示される。良い例:

- 「ログインしてください。完了したら release を押してください」
- 「reCAPTCHA を解いてください。完了したら release を押してください」
- 「カートに入れたい商品を選んでください。完了したら release を押してください」

### 制約

- `InteractiveBrowse` は **ローカルタスク経由のジョブ** でのみ使える（`taskId` が必要）。Gitea Issue 直接実行や taskId が立たない subtask root では使えない
- noVNC が orchestrator にインストール / 設定されていない環境ではエラー（Xvfb / x11vnc / websockify が必要、`config.yaml` の `browser.display_mode: novnc` 設定。旧 `captcha_solve` は自動移行）
- ユーザーが release を押さない限りジョブは進まない。長時間放置すると `browser.auth_timeout`（デフォルト 10 分）で timeout

### 既存の Browser Sessions 機能との違い

| 機能 | 用途 |
|---|---|
| **Browser Sessions** (Settings UI から保存) | スケジュール実行や定期タスクなど **agent しか動いていない時間帯** に、過去にログイン済みの cookie / storageState を再利用 |
| **InteractiveBrowse** | **ジョブ実行中、その場で** ユーザーがブラウザを操作してログインや人間判断を行う |

定期タスクで毎回 InteractiveBrowse を呼ぶのは非効率なので、定常運用のサイトは Browser Sessions として登録するのが正解。「初回ログイン or セッション切れ時だけ InteractiveBrowse」のような使い分けが望ましい。

## トラブルシューティング

- **「ref "e5" not found in current snapshot」と出る**: ページ遷移後で ref がリセットされている。`getText` で新しいスナップショットを取得する
- **テキストが取れない / 空に近い**: ページが SPA で JavaScript で描画されている。`waitFor` で描画完了を待つ
- **ボタンを押せない / click しても何も起きない**:
  1. 要素が visible でない可能性。先に getText で本当に存在するか確認
  2. ref 注釈に `disabled` が出ていないか確認
  3. `dumpHtml({ ref: "..." })` で要素の生 HTML を見て、独自 selector を組む
  4. それでもダメなら `InteractiveBrowse` でユーザーに引き継ぐ
- **`<div>` に click 反応する独自 UI が ref に出ない**: addEventListener フックで多くは検出されるが、React の `onClick={...}` (root delegation) や `el.onclick = fn` 直接代入は捕捉できない。`dumpHtml` で構造を見て selector を直接組むか、`InteractiveBrowse` で渡す
- **ログインが維持されない**: 別ジョブから呼んでいる可能性。同一ジョブ内なら維持される。定常運用は Browser Sessions に保存する

## ファイルダウンロード

リンククリック等でブラウザがファイルダウンロードを開始すると、自動的に workspace の `output/` 配下に保存される。戻り値の末尾に以下の形式で通知される:

```
[download] saved output/report.csv (12345 bytes)
```

- ファイル名は server-suggested 名から path traversal 対策と禁則文字置換を経て決定される
- 衝突時は `foo-1.csv`, `foo-2.csv` 形式で番号付与される
- 失敗時は `[download] FAILED <name>: <reason>` と出る
- ダウンロードされたファイルは続く `Read`, `ReadPdf`, `ReadExcel`, `Bash` 等の tool で参照できる
- 履歴は `logs/downloads.jsonl` に追記される (DownloadFile と同じファイル、`source: 'BrowseWeb'` フィールドで区別)

ダウンロードを認証付きで行いたい場合は、Browser Sessions 機能で対象サイトのログインセッションを保存し、タスクで bind した状態で BrowseWeb を呼ぶこと。

## SSRF 保護

ローカル/プライベート IP（127.x.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, ::1, fc00::/7 等）へのアクセスはデフォルトでブロックされる。社内ホストへアクセスする必要がある場合は、Settings UI の「SSRF Allowed Hosts」に追加する。

### 既知の残留リスク: 時間差 DNS rebinding (TOCTOU)

リクエスト傍受時に全 DNS レコードを検証してから `route.continue()` するが、その後の接続では Chromium が独自に DNS を再解決する。検証と接続の間（サブ秒 TTL）でレコードを書き換える rebinding ホストは、理論上プライベート IP への接続を成立させられる。Playwright の route 傍受には接続先アドレスを pin する手段がなく、この方式に内在する制約として受容している（issue #467）。WebFetch / DownloadFile / MCP はソケットを pin する `pinnedFetch`（`src/net/ssrf-strict.ts`）経由のため影響を受けない。完全に塞ぐには pinning forward proxy が必要だが、意図的に導入していない。
