# 瀏覽器

`dozin.capabilities` 裡有 `browser` 時，Connector 或任務會得到 `ctx.browser`，可以讀取並操作 Cowork 分頁裡的網頁。只能使用使用者交出的分頁；有提供 `dozin.origins` 時，只限這些網站上的頁面。

## ctx.browser.adoptTab(tabId)

接管一個分頁。使用者要求 Extension 接管某個頁面時，Cowork 會把 `tabId` 交給 Extension；Extension 不能自己挑選分頁。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | Cowork 交過來的分頁 |

回傳 `Promise<{ tabId, url, documentId }>`。頁面每次載入新文件時，`documentId` 都會改變。同一時間最多 8 個分頁。

```js
const tab = await ctx.browser.adoptTab(tabId);
ctx.log(`watching ${tab.url}`);
```

## ctx.browser.releaseTab(tabId)

交還分頁。它的注入腳本和音訊會停止。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 這個 Extension 接管的分頁 |

回傳 `Promise<void>`。

```js
await ctx.browser.releaseTab(tabId);
```

## ctx.browser.evaluate(tabId, script)

在頁面裡執行一次腳本，並回傳結果。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 已接管的分頁 |
| `script.source` | `string` | 是 | 一段函式原始碼，最多 64 KB；它會收到 `args` |
| `script.args` | `unknown` | 否 | 以 JSON 傳入，不會貼進原始碼 |

回傳 `Promise<unknown>`：結果，為 JSON，最多 64 KB。

```js
const title = await ctx.browser.evaluate(tabId, { source: '(args) => document.title' });
```

## ctx.browser.inject(tabId, script)

安裝一段留在頁面裡、能回傳訊息的腳本。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 已接管的分頁 |
| `script.id` | `string` | 是 | 1–80 個字母、數字、`_` 或 `-`；在這個分頁內唯一 |
| `script.source` | `string` | 是 | 一個函式 `(args, { emit, onDispose }) => cleanup?`，最多 64 KB；`emit(data)` 會以 `message` 事件送達 |
| `script.args` | `unknown` | 否 | 以 JSON 傳入 |

回傳 `Promise<void>`。每個分頁最多 16 段腳本。

```js
await ctx.browser.inject(tabId, {
  id: 'chat-watch',
  source: `(args, { emit }) => {
    const observer = new MutationObserver(() => emit({ count: document.querySelectorAll(args.selector).length }));
    observer.observe(document.body, { childList: true, subtree: true });
    return () => observer.disconnect();
  }`,
  args: { selector: '.message' }
});
```

## ctx.browser.uninject(tabId, injectionId)

移除一段注入腳本，並執行它的清理。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 已接管的分頁 |
| `injectionId` | `string` | 是 | 傳給 `inject` 的 `id` |

回傳 `Promise<void>`。

```js
await ctx.browser.uninject(tabId, 'chat-watch');
```

## ctx.browser.captureAudio(tabId)

還需要 `audio`。開始擷取分頁的聲音。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 已接管且已載入完成的分頁 |

回傳 `Promise<AudioStream>`：

| 欄位 | 類型 | 說明 |
|---|---|---|
| `tabId` · `url` · `documentId` | | 這個分頁，與 `adoptTab` 相同 |
| `streamId` | `string` | 這次擷取 |
| `frames` | `AsyncIterable<{ streamId, sequence, timestamp, sampleRate, channels, pcm: Float32Array }>` | 聲音，一段一段送來 |
| `ended` | `Promise` | 擷取結束時完成 |
| `stop()` | `() => Promise<void>` | 停止擷取 |

每個分頁同一時間只能有一個擷取。

```js
const stream = await ctx.browser.captureAudio(tabId);
for await (const chunk of stream.frames) handle(chunk.pcm, chunk.sampleRate);
```

## ctx.browser.stopAudio(tabId)

停止分頁的擷取。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `tabId` | `string` | 是 | 正在擷取的分頁 |

回傳 `Promise<void>`。

```js
await ctx.browser.stopAudio(tabId);
```

## ctx.browser.onEvent(listener)

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `listener` | `(event) => void` | 是 | Extension 的分頁每發生一個事件就呼叫一次 |

回傳 `() => void`，用來停止監聽。每個事件都帶有 `tabId`、`url` 和 `documentId`，以及一個 `type`：

| `type` | 額外欄位 | 時機 |
|---|---|---|
| `url-changed` · `navigation-start` · `ready` | — | 頁面跳轉或載入完成 |
| `closed` · `crashed` | — | 分頁已不存在 |
| `injected` | `injectionId` | 一段腳本已安裝 |
| `message` | `injectionId`、`data` | 注入腳本呼叫了 `emit(data)` |
| `error` | `code` | 分頁裡有操作失敗 |
| `audio-ended` | `streamId`、`lastSequence` | 一次擷取結束 |

```js
const off = ctx.browser.onEvent((event) => {
  if (event.type === 'message') ctx.emit({ text: String(event.data.count) });
  if (event.type === 'closed') off();
});
```

## 錯誤

這些函式會以下列其中一個代碼拒絕：

| 代碼 | 意思 |
|---|---|
| `capability_denied` | 沒有宣告 `browser`（或 `audio`） |
| `invalid_tab` · `tab_not_granted` · `tab_not_owned` | 分頁 id 有誤、Cowork 沒有交出這個分頁，或它已被交還 |
| `origin_not_allowed` | 頁面的網站不在 `dozin.origins` 裡 |
| `tab_limit` · `injection_limit` | 超過 8 個分頁或 16 段腳本 |
| `tab_unavailable` · `document_not_ready` · `stale_document` | 分頁已關閉、仍在載入，或已載入另一個頁面 |
| `invalid_script` · `invalid_injection_id` · `injection_exists` · `invalid_json` · `result_too_large` | 輸入或輸出有誤 |
| `audio_already_started` · `audio_transport_unavailable` · `invalid_audio_request` | 無法開始擷取音訊 |
| `session_closed` · `browser_operation_failed` | Extension 正在停止，或 Cowork 無法完成這次呼叫 |

## 相關

- [Injection](https://docs.micromeet.ai/zh-tw/extend/injections.md): 網頁裡的腳本
- [Connector](https://docs.micromeet.ai/zh-tw/extension-sdk/connector.md): 最常使用 `ctx.browser` 的地方
