connect() 与卡片对象
卡片做的一切都经过同一个对象——card,你从 connect() 得到它:
import { connect } from '@neurosquad/card-sdk'
const card = await connect()
card.setStatus(`Hello, ${card.workspace.name}`, { tone: 'success' })connect() 会等待应用把卡片的专用通道交过来,完成自我介绍,然后返回一个 Card。再次调用会返回同一个 card。
请在入口脚本中导入 SDK。 SDK 一加载就开始监听应用的握手消息。延迟加载 SDK 的卡片(例如在定时器之后动态 import())
可能会错过握手,并以 UNAVAILABLE 失败。
connect(options?)
import { connect } from '@neurosquad/card-sdk'
const card = await connect({
theme: true, // apply the app theme as --ns-* CSS variables on <html> (default true)
syncLang: true, // keep <html lang> equal to the app language (default true)
forwardErrors: true, // send uncaught errors and rejections to the card log (default true)
timeoutMs: 10_000 // how long to wait for the app (default 10 000)
})| 选项 | 默认值 | 含义 |
|---|---|---|
theme | true | 设为 false 则不改动你的 CSS;传入一个元素则把 --ns-* 变量写到该元素上,而不是 <html>。参见主题。 |
syncLang | true | 让 <html lang> 始终与应用语言一致。 |
forwardErrors | true | 把 error 和 unhandledrejection 转发到卡片日志。 |
timeoutMs | 10000 | 等待应用通道、再等待应用应答的时长。 |
port | — | 使用这个通道,而不是等待应用——用于模拟宿主。 |
它会以 CardSdkError 拒绝:
UNAVAILABLE——页面不在 NeuroSquad 卡片里(在浏览器中单独打开)。要预览,请像模板那样使用模拟宿主。PROTOCOL_MISMATCH——应用比你的 SDK 使用的卡片协议旧。卡片会显示“needs a newer NeuroSquad”。
卡片对象
| 成员 | 说明 |
|---|---|
card.context | 应用告诉卡片的一切,实时更新。 |
card.setTitle / setStatus / setBadge / setOverview / attention / requestResize / openLink / focusCard / spawn | 卡片外框 |
card.ui | 提示条、确认对话框、卡片菜单 |
card.storage | 每卡片与每卡片包的存储 |
card.settings | 设置表单的值 |
card.agents | 智能体、状态、输出和提示词 |
card.terminals | 在相连终端中执行命令 |
card.ports | 与相连卡片交换类型化数据 |
card.tools | 给相连智能体的工具 |
card.net | 经由应用代理的 HTTP |
card.fs | 工作区文件夹中的文件 |
card.permissions | 授权状态、申请可选权限 |
card.lifecycle | 可见性、暂停、展开、调整大小 |
card.log | 写入卡片日志 |
card.copyText / usage / getWorkspace / getTheme / getI18n | 剪贴板、用量、获取最新快照 |
card.host | 功能检测 |
card.call / on / once / waitFor | 任意方法、任意事件 |
card.close() / closed | 关闭通道;之后的调用会以 UNAVAILABLE 失败。 |
上下文
card.context 是一个 HostContext:卡片连接时应用发来的内容,并随每个事件更新(可见性、大小、主题、语言、设置、权限、对端)。
每次变化时整个对象都会被替换,而不是原地修改——因此可以放心地配合 React 的 useSyncExternalStore 使用。
import type { HostContext } from '@neurosquad/card-sdk'
function describe(context: HostContext): string {
const { instance, workspace, visibility, i18n, launch } = context
return `${instance.displayName} ${instance.version} in "${workspace.name}", ${visibility}, ${i18n.language}, started: ${launch}`
}
card.onContextChange((context) => render(describe(context)))| 字段 | 类型 | 含义 |
|---|---|---|
instance | CardInstanceInfo | instanceId(这张卡片在画布上的 id)、packageId、name、displayName、version、commit(链接文件夹时为 null)、title、size、dev。 |
workspace | WorkspaceInfo | id、name,以及仅在有 fs.read 时提供的 path(绝对路径)。 |
visibility | 'visible'、'offscreen'、'overview' 或 'hidden' | 参见生命周期。 |
expanded | boolean | 卡片是否已展开铺满画布。 |
theme | ThemeSnapshot | 应用的颜色、圆角和字体。 |
i18n | { language, locale } | en、ru 或 zh,以及用于 Intl 的 locale。 |
settings | SettingsSnapshot | values,以及 secrets(哪些密钥已设置)。 |
permissions | PermissionState[] | 每项已声明的权限及其 granted、optional、hosts、reason。 |
ports | { inputs, outputs } | 你自己的端口,标签已解析为当前语言。 |
peers | PeerInfo[] | 通过箭头相连的卡片。 |
launch | 'created'、'opened'、'resumed'、'reloaded' 或 'updated' | 这次页面启动的原因。 |
spawnInit | JSON 或不存在 | 创建这张卡片的卡片传来的数据,仅在首次启动时提供。 |
chrome | CardChromeState 或不存在 | 应用此刻为这张卡片显示的内容——title、status、badge、overview、attention——在重新加载之间保留。参见卡片外框。旧版本的应用中不存在。 |
appVersion | string | NeuroSquad 的版本。 |
limits | LIMITS | 协议的所有限制,参见限制。 |
protocol | number | 应用使用的卡片协议。 |
card 上的快捷属性:card.instanceId、card.instance、card.workspace、card.visibility、card.expanded、card.size、
card.theme、card.i18n、card.language、card.launch、card.spawnInit、card.limits、card.appVersion。
launch 告诉你发生了什么:created(刚被添加到画布)、opened(打开了工作区)、resumed(暂停后恢复)、
reloaded(用户点了 Reload、开发模式下文件有改动,或某项权限被撤销)、updated(安装了你的卡片的新版本)。
任意方法、任意事件
各个命名空间只是两个基本操作的语法糖,这两个操作都根据协议完整地带有类型:
// Any method: params and result are typed from the method name.
const { keys } = await card.call('storage.keys', { scope: 'instance', prefix: 'draft:' })
const info = await card.call('card.getInfo')
// Any event: the payload is typed from the event name. Returns a function that removes the listener.
const off = card.on('agents.turn', (turn) => {
if (turn.phase === 'end') render(`${turn.agentId} finished a turn`)
})
off()
// Once, or as a promise.
card.once('lifecycle.expanded', ({ expanded }) => render(expanded))
const next = await card.waitFor('agents.status', (e) => e.status === 'needs-input', { timeoutMs: 60_000 })
render(next.agentId)可订阅的主题(agents.status、agents.turn、agents.changed、storage.changed)会在至少有一个监听器时自动向应用订阅,
最后一个监听器移除时自动退订。agents.output 需要智能体 id——请使用 card.agents.onOutput()。
如果某个主题需要你没有的权限,监听器只是收不到任何东西,同时卡片日志中会出现一条警告。
可能在你添加监听器之前就到达的事件——ports.message、ui.menu、fs.changed——会被暂存(最多 100 条)并交给第一个监听器,
所以在 connect() 和你的 on() 之间不会丢失任何东西。
全部事件
| 事件 | 负载 | 何时发生 |
|---|---|---|
lifecycle.visibility | { state } | 可见性变化。 |
lifecycle.suspend | { graceMs } | 页面即将被卸载。请使用 card.lifecycle.onSuspend。 |
lifecycle.expanded | { expanded } | 展开或还原。 |
lifecycle.resized | { w, h } | 卡片大小被调整。 |
settings.changed | SettingsSnapshot | 设置改变(表单或 settings.set)。 |
theme.changed | ThemeSnapshot | 应用主题改变。 |
i18n.changed | { language, locale } | 应用语言改变。 |
permissions.changed | PermissionState[] | 某项授权改变。 |
ui.menu | { id } | 你添加的某个菜单项被选中。 |
ports.message | 见端口 | 某个输入收到了值。 |
ports.request | 见端口 | 对端向请求型输入提问。请使用 card.ports.onRequest。 |
ports.peersChanged | PeerInfo[] | 箭头或对端发生变化。 |
tools.call、tools.cancel | 见工具 | 智能体调用工具。请使用 card.tools.handle。 |
net.chunk | 见网络 | 流式响应的一个分块。请使用 response.chunks()。 |
fs.changed | { watchId, path, type } | 被监视的文件发生变化。请使用 card.fs.watch。 |
storage.changed | { scope, key, byInstance } | 卡片的另一份副本写入了包级键。主题。 |
agents.status | { agentId, status, at } | 智能体状态变化。主题,需 agents.read。 |
agents.turn | { agentId, phase, at } | 一轮开始或结束。主题,需 agents.read。 |
agents.changed | { agents } | 智能体被添加、移除或重命名。主题,需 agents.read。 |
agents.output | { agentId, text, at } | 相连智能体的输出。需 agents.output。 |
host.ping | { seq } | 心跳——SDK 会替你应答。 |
功能检测
新方法和新事件会在同一协议版本内陆续加入。使用用户的应用可能还没有的功能之前,请先检查:
if (await card.host.supports('usage.summary')) {
const usage = await card.usage('today')
render(usage.totalCostMicroUsd)
}
const { protocol, methods, events } = await card.host.capabilities()
render(protocol, methods.length, events.length)
// A method newer than your SDK's types:
const result = await card.callUnchecked('some.newMethod', { any: 'params' })
render(result)约定
- Id。 卡片和智能体的 id 就是画布上的 id——与你从
card.ports.peers、card.agents.list()或card.spawn()拿到的cardId相同。 - 相连指两张卡片之间有一条箭头,方向不限。端口还会额外关心方向(参见端口)。
- 每次调用都检查两遍。 SDK 用与应用相同的 schema 校验参数,出错时立即以
INVALID_PARAMS失败并给出精确路径; 应用会再检查一遍,外加权限、可见性、箭头和速率限制。 - 只有 JSON 能跨越边界。 不能传
Blob或ArrayBuffer:接受字节的辅助方法(fs.writeBytes、net.fetch的请求体)会替你编码为 base64。 - 回复可能乱序到达; 事件则按应用发送的顺序到达。
- 永远不要信任
window的message事件。 画布上的任何其他卡片都能对你的框架调用postMessage。唯一可信的通道是 SDK 在connect()时从应用收到的那个; 不要自己添加根据收到的内容行事的message监听器。