Skip to Content

网络

卡片页面本身完全无法访问网络——它的沙箱会拦截包外的每一个请求、图片和字体。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 很像,只有几处不同:

选项含义
methodGET(默认)、HEAD、POST、PUT、PATCH、DELETE。有请求体但没指定方法时为 POST。
headers对象,或 [name, value] 对。可以包含 {{secret:<key>}}。
body字符串(原样发送)、字节(Uint8Array、ArrayBuffer),或其他任意 JSON 值(序列化后发送,除非你自己设置,否则带 content-type: application/json)。≤ 5 MB。
responseTypetext(默认)、json(由应用解析)、base64(二进制)或 stream。
timeoutMs1 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。

代理会强制执行的规则

  1. 协议。 只允许 https,唯一例外是在有 network.local 时对本机使用 http。URL 中不能带用户名或密码;#fragment 不会被发送。
  2. 主机。 严格匹配你声明的模式(api.example.com、*.example.com、host.example.com:8443)。其他任何主机都会以 HOST_NOT_ALLOWED 失败。
  3. 地址。 应用自己解析域名;对于公网主机授权,每个地址都必须是公网地址——私有网络、回环、链路本地和云元数据地址都会被拒绝, 并且请求会发往经过检查的那个地址(不会发生 DNS 重绑定)。
  4. 请求头。 你不能设置 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. 重定向。 由应用跟随,最多 5 次,每一跳都会按规则 1–4 重新检查。
  6. 限制。 请求体 ≤ 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。

安装对话框会告诉用户:你的卡片能看到的任何内容都可能被发送到你列出的主机。请让这个列表简短而具体: 通配符或通用主机(粘贴板服务、短链接服务)会让谨慎的用户拒绝安装。