插件运行在客户端的渲染进程里(没有 webview / iframe),因此插件里的浏览器 fetch 受同源策略约束:目标服务不返回 CORS 头就直接失败,典型表现是 Failed to fetch。用户自建的模型网关通常就是这种情况。
api.network.fetch 解决这个约束:插件的请求由宿主主进程发出,不受 CORS 限制;响应体边收边推,所以流式对话(SSE)的逐字输出与直连完全一致。用法与浏览器 fetch 同名同形。
在 manifest.json 的 permissions 里声明 network:fetch:
api.network.fetch 与文件能力不同,不返回空信封:未声明 network:fetch 时它直接抛 TypeError,消息点明缺少哪个权限,并且不会发出任何请求。插件应在调用前用可选链判断能力是否存在(见下文「兼容与降级」)。
| 参数 | 类型 | 说明 |
|---|---|---|
input |
string | Request |
目标地址,或一个 Request 对象;只接受 http: / https: |
init.method |
string |
HTTP 方法,默认 GET |
init.headers |
[string, string][] | Record<string, string> | Headers |
请求头,推荐按 [名, 值] 成对传 |
init.body |
string |
请求体;只支持字符串,FormData / Blob / URLSearchParams 等会抛 TypeError |
init.signal |
AbortSignal |
中断请求;中断以 AbortError 收尾 |
init.redirect、init.cache 等 |
— | 按标准 RequestInit 语义透传,宿主不额外解释 |
请求头推荐写成 [名, 值] 数组:同名头不会被覆盖,会按传入顺序全部保留,因此可以重复发送 Accept 这类头。普通对象和 Headers 实例同样接受,只是无法表达同名多值。
需要 JSON 请自己序列化:body: JSON.stringify(payload),并记得带上 Content-Type。
返回的是真正的 Response,不是宿主自造的替身:
| 成员 | 说明 |
|---|---|
status / statusText |
HTTP 状态码与状态文本 |
ok |
200–299 为 true |
headers |
响应头,按标准 Headers 读取 |
body |
ReadableStream<Uint8Array>,可 getReader() 边收边读 |
text() / json() |
照常可用,等价于一次性读完 body |
注意 Response 的 body 只能消费一次:text() / json() 与 getReader() 不要混用,读过一次再读会抛错。
resp.body.getReader() 与直连一致。下面把响应原样拼接成文本:
宿主把字节块边收边推,所以按 SSE 的 data: 行切分就能实现逐字输出。下面是一个 OpenAI 兼容接口的最小实现:
用标准的 AbortController:
用户点击「停止」时,在别处调用 controller.abort() 即可结束这次请求。
中断和失败分两种收尾方式:
init.signal 触发的中断以 AbortError(DOMException)收尾,error.name === 'AbortError',这是用户取消,不是故障。TypeError,并带 code 字段:code |
含义 |
|---|---|
ENOTFOUND |
域名解析不了。 |
ETIMEDOUT |
连接或读取响应头超时。 |
ECONNREFUSED |
目标端口拒绝连接。 |
ABORT_ERR |
宿主内部的中止路径(区别于 init.signal 的 AbortError)。 |
EFBIG |
响应体超过单次 256 MB 上限。 |
EINVALID_HEADER |
某个请求头的值不合法(含换行等),请求被拒绝、未发出。 |
NOT_SUPPORTED |
当前环境没有主进程(例如浏览器预览),能力不可用。 |
系统网络栈还可能透出其他错误码,判断失败时不要只匹配表中这几项,error.code ?? error.message 一起显示更稳妥。
http: / https:。相对路径、file:、data:、blob: 等一律拒绝。FormData / Blob / ReadableStream 等会抛 TypeError;上传文件请自行编码,例如 Base64 后放进 JSON。init.signal 结束。长时间对话需要自己控制中断时机。EFBIG 失败。204 / 205 / 304 的 resp.body === null,读之前先判断。NOT_SUPPORTED,插件要做降级。| 维度 | 浏览器 fetch |
api.network.fetch |
|---|---|---|
| 发起方 | 渲染层 | 宿主主进程 |
| CORS | 受同源策略约束,跨域需目标返回 CORS 头 | 不受 CORS 限制 |
| 权限 | 不需要 | 必须声明 network:fetch,否则抛 TypeError |
| Cookie | 按 credentials 带上当前站点 cookie |
不带宿主自身 cookie |
| 请求体 | 支持 FormData / Blob / ReadableStream 等 |
只支持字符串 |
| 允许的协议 | 相对路径与 data: / blob: 等 |
仅 http: / https: |
| 超时 | 由浏览器网络栈决定 | 等响应头默认 2 分钟、上限 10 分钟;响应体靠中断结束 |
| 响应体上限 | 没有固定上限 | 单次 256 MB(EFBIG) |
| 浏览器预览 | 可用 | 抛 NOT_SUPPORTED |
| 返回值 | Response |
同样是真正的 Response |
| 流式读取 | resp.body.getReader() |
一致,SSE 逐字输出可用 |
宿主是逐步更新的,不能假定 api.network.fetch 一定存在。按能力检测选取实现:优先用宿主代发,没有该能力时退回全局 fetch(此时只能访问同源地址)。
浏览器预览里 api 为空、或 api.network 不存在时都会走到 fetch 分支;这个分支在预览环境能用,但在跨域目标上仍会被 CORS 拦下。声明了 network:fetch 的插件在旧宿主上不会因此报错,只是会退回浏览器的同源限制。