网络
卡片页面本身完全无法访问网络——它的沙箱会拦截包外的每一个请求、图片和字体。card.net.fetch 改为经由应用中的代理,只放行用户授权的内容:
- 带
hosts的network:对这些主机的https请求; network.local:对localhost、127.0.0.1和::1的http或https请求——NeuroSquad 自己的本地服务器 (它的 MCP 服务器、远程访问、开发者链接、浏览器卡片中 Chrome 的调试端口)除外,它们无法访问。
发起请求
const res = await card.net.fetch('https://api.github.com/repos/acme/app/issues?state=open', {
headers: { accept: 'application/vnd.github+json' },
responseType: 'json'
})
if (!res.ok) throw new Error(`GitHub said ${res.status} ${res.statusText}`)
const issues = await res.json<{ number: number; title: string }[]>()
render(issues.map((i) => `#${i.number} ${i.title}`))它的用法和 fetch 很像,只有几处不同:
| 选项 | 含义 |
|---|---|
method | GET(默认)、HEAD、POST、PUT、PATCH、DELETE。有请求体但没指定方法时为 POST。 |
headers | 对象,或 [name, value] 对。可以包含 {{secret:<key>}}。 |
body | 字符串(原样发送)、字节(Uint8Array、ArrayBuffer),或其他任意 JSON 值(序列化后发送,除非你自己设置,否则带 content-type: application/json)。≤ 5 MB。 |
responseType | text(默认)、json(由应用解析)、base64(二进制)或 stream。 |
timeoutMs | 1 000–120 000,默认 30 000。 |
signal | 一个 AbortSignal;中止时会在应用中取消该请求。 |
结果是一个 CardResponse:ok、status、statusText、url(重定向之后)、headers(get、has、可迭代;名称为小写)、truncated,
以及读取正文的方法 text()、json()、bytes()、arrayBuffer()、chunks()、lines()——和 Response 一样,正文只能读取一次。
// POST JSON, read binary.
const upload = await card.net.fetch('https://api.example.com/v1/render', {
method: 'POST',
body: { chart: 'coverage', width: 640 },
responseType: 'base64'
})
const png = await upload.bytes()
render(png.byteLength)条件请求与空响应体。 If-None-Match、If-Modified-Since 等条件请求头会被转发,除 set-cookie 外的所有响应头都会返回
(所以 etag 和限流相关的响应头都在)。空响应体——304 Not Modified 或 204 No Content——在 responseType: 'json' 时,res.json() 返回 null。
请求头中的密钥
永远不要把 API 密钥放进卡片代码或存储里。声明一个 secret 设置,让用户在应用的设置表单中输入,然后在请求头的值中引用它:
if (!card.settings.hasSecret('apiKey')) {
await card.settings.open()
} else {
const res = await card.net.fetch('https://api.example.com/v1/me', {
headers: { authorization: 'Bearer {{secret:apiKey}}' },
responseType: 'json'
})
render(await res.json())
}应用会在请求发出时把 {{secret:apiKey}} 替换为保存的密钥(先找这张卡片的,再找包级的)——只针对发往已授权互联网主机的请求,
永远不会针对本机(network.local)。你的卡片永远看不到这个值。
占位符只在请求头的值中有效——不能用于 URL 或请求体,因为它们会出现在服务器日志中——而且如果重定向指向另一个主机,带密钥的请求头会被丢弃。
引用用户尚未设置的密钥会以 INVALID_PARAMS 失败。
流式响应
对于服务器发送事件(SSE)、NDJSON 或大文件下载,请请求流式响应。收到响应头后 Promise 就会返回;正文以分块形式到达(每块 ≤ 64 KB):
const controller = new AbortController()
const stream = await card.net.fetch('https://api.example.com/v1/events', {
headers: { accept: 'text/event-stream' },
responseType: 'stream',
signal: controller.signal
})
for await (const line of stream.lines()) {
if (line.startsWith('data: ')) render(JSON.parse(line.slice(6)))
}
// controller.abort() ends it early.chunks() 对文本分块返回字符串,对二进制分块返回 Uint8Array;lines() 按换行拆分;for await (const chunk of response) 也可以用。
流失败时会抛出 NETWORK_ERROR。同时最多打开 4 个流;流在 5 分钟没有数据或达到 256 MB 后会被关闭。第 1 版不支持 WebSocket。
代理会强制执行的规则
- 协议。 只允许
https,唯一例外是在有network.local时对本机使用http。URL 中不能带用户名或密码;#fragment不会被发送。 - 主机。 严格匹配你声明的模式(
api.example.com、*.example.com、host.example.com:8443)。其他任何主机都会以HOST_NOT_ALLOWED失败。 - 地址。 应用自己解析域名;对于公网主机授权,每个地址都必须是公网地址——私有网络、回环、链路本地和云元数据地址都会被拒绝, 并且请求会发往经过检查的那个地址(不会发生 DNS 重绑定)。
- 请求头。 你不能设置
host、cookie、origin、referer、user-agent、content-length、connection、proxy-*、sec-*、x-forwarded-*等。应用会发送User-Agent: NeuroSquad-Card/<app version> (<your card name>)。Cookie 永远不会被保存或发送,set-cookie也永远不会返回给你。 - 重定向。 由应用跟随,最多 5 次,每一跳都会按规则 1–4 重新检查。
- 限制。 请求体 ≤ 5 MB;响应 ≤ 10 MB(更长的 text 或 base64 正文会被截断并带有
truncated: true;更长的 JSON 正文以TOO_LARGE失败); 同时 6 个请求;每分钟 120 个;超时最长 120 秒——而且超时涵盖读取整个正文,而不只是响应头。
错误:HOST_NOT_ALLOWED(主机或地址未授权)、NETWORK_ERROR(DNS、连接、TLS)、TIMEOUT、TOO_LARGE、RATE_LIMITED、
PERMISSION_DENIED(完全没有网络权限)。HTTP 错误状态码不是异常——请检查 res.ok。
安装对话框会告诉用户:你的卡片能看到的任何内容都可能被发送到你列出的主机。请让这个列表简短而具体: 通配符或通用主机(粘贴板服务、短链接服务)会让谨慎的用户拒绝安装。