# 浏览器

`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` | 采集结束时 settle |
| `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();
});
```

## 错误

这些函数以下列代码之一 reject：

| 代码 | 含义 |
|---|---|
| `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/extend/injections.md): 网页里的脚本
- [Connector](https://docs.micromeet.ai/zh/extension-sdk/connector.md): 最常使用 `ctx.browser` 的地方
