React 绑定
@neurosquad/card-sdk/react 用一个 provider 包裹卡片,并把它的实时状态以 hook 的形式提供出来。
React 18.2+ 或 19 是对等依赖(peer dependency);React 模板已经把一切配置好了。
import { createRoot } from 'react-dom/client'
import { CardProvider, useCardContext, useStorage } from '@neurosquad/card-sdk/react'
function App() {
const { instance, workspace } = useCardContext()
const [count, setCount] = useStorage('count', 0)
return (
<button className="ns-btn ns-btn--primary" onClick={() => setCount(count + 1)}>
{instance.displayName} in {workspace.name}: {count}
</button>
)
}
createRoot(document.getElementById('root')!).render(
<CardProvider fallback={<p>Connecting…</p>}>
<App />
</CardProvider>
)CardProvider
| 属性 | 含义 |
|---|---|
card | 你自己连接好的 card——例如来自模拟宿主。不提供时,provider 会调用 connect()。 |
connectOptions | 传给 connect() 的选项。 |
fallback | 连接期间渲染的内容。 |
errorFallback | (error) => ReactNode,连接失败时渲染。默认显示错误消息。 |
React 模板会先完成连接,再传入 card——这样同一个入口既能在应用中运行,也能配合模拟宿主在浏览器中预览。
Hook
| Hook | 返回 |
|---|---|
useCard() | Card——用于没有专门 hook 的一切操作。 |
useCardContext() | 实时的 HostContext;任何变化都会重新渲染。 |
useSettings() | [settings, setSettings]——settings.values、settings.secrets;setter 修改非密钥值。 |
useStorage(key, initial, { scope? }) | [value, setValue, { loading, error }]——和 useState 一样,但会持久化。setter 立即更新并在后台写入;也接受函数。scope: 'package' 会跟随卡片其他副本的写入。 |
useAgents() | { agents, loading, error, refresh }——随状态和变更事件保持最新。需要 agents.read。 |
useAgentStatus(agentId) | 某个智能体的状态,或 undefined。 |
usePort(input?) | { data, message }——某个输入(或任意输入)最后收到的值。 |
useEmit(output) | 一个稳定的 (data) => Promise<number>,用于在某个输出上发送。 |
usePortRequest(input, handler) | 挂载期间应答某个请求型输入。 |
usePeers() | 相连的卡片及其端口,实时更新。 |
useTool(name, handler) | 挂载期间实现清单中的某个工具;总是调用最新的处理函数。 |
useCardEvent(event, handler) | 挂载期间监听任意事件;总是调用最新的处理函数。 |
useTheme() | 实时的 ThemeSnapshot(CSS 变量已经应用好了)。 |
useLanguage() | 实时的 { language, locale }。 |
useTranslator(catalog) | 一个翻译函数,切换语言时会重新渲染。请在组件外定义 catalog。 |
useVisibility() | visible、offscreen、overview 或 hidden。 |
usePaused() | 没人能看到卡片正文时为 true——此时暂停动画和轮询。 |
useExpanded() | 卡片是否已展开。 |
更完整的示例
import type { Catalog } from '@neurosquad/card-sdk'
import {
useAgents,
useCard,
useEmit,
usePaused,
usePort,
useTool,
useTranslator
} from '@neurosquad/card-sdk/react'
import { useEffect } from 'react'
const catalog: Catalog = {
en: { waiting_one: '{{count}} agent waits for you', waiting_other: '{{count}} agents wait for you' },
ru: {
waiting_one: '{{count}} агент ждёт вас',
waiting_few: '{{count}} агента ждут вас',
waiting_many: '{{count}} агентов ждут вас',
waiting_other: '{{count}} агента ждут вас'
},
zh: { waiting_other: '{{count}} 个智能体在等你' }
}
export function Waiting() {
const card = useCard()
const t = useTranslator(catalog)
const paused = usePaused()
const { agents } = useAgents()
const waiting = agents.filter((a) => a.status === 'needs-input')
const { data: note } = usePort<string>('notes')
const emit = useEmit<string>('digest')
// Keep the overview tile and attention in step with the data.
useEffect(() => {
void card.setOverview({ primary: t('waiting', { count: waiting.length }), icon: 'bell' })
void card.attention(waiting.length > 0 ? 'needs-input' : 'none').catch(() => undefined)
}, [card, t, waiting.length])
useTool('list_waiting', () => waiting.map((a) => a.name))
return (
<div className="ns-stack">
<p className={paused ? '' : 'pulse'}>{t('waiting', { count: waiting.length })}</p>
{note ? <pre className="ns-mono">{note}</pre> : null}
<button className="ns-btn" onClick={() => void emit(waiting.map((a) => `- ${a.name}`).join('\n'))}>
Send digest
</button>
</div>
)
}会调用应用的 hook(useAgents、useStorage)不会抛出异常,而是在它们的 error 字段中报告失败——
缺少权限时,那里会出现一个带 PERMISSION_DENIED 的 CardSdkError。
使用本地 SDK 副本
如果卡片通过 file: 链接依赖 SDK,npm 会创建符号链接,Vite 可能在 SDK 旁边加载第二份 React——此时 hook 会报 “Invalid hook call”。
React 模板的 vite.config.ts 已经包含 resolve: { dedupe: ['react', 'react-dom'] };在你自己的配置中请加上它,或在卡片的 .npmrc 中设置 install-links=true。