# connect() 与卡片对象

> 把卡片连接到 NeuroSquad、类型化的 Card 对象、它实时更新的上下文，以及调用任意方法、监听任意事件。

Source: https://docs.neurosquad.ai/zh/card-sdk/api

卡片做的一切都经过同一个对象——**card**，你从 `connect()` 得到它：

```ts
import { connect } from '@neurosquad/card-sdk'

const card = await connect()
card.setStatus(`Hello, ${card.workspace.name}`, { tone: 'success' })
```

`connect()` 会等待应用把卡片的专用通道交过来，完成自我介绍，然后返回一个 [`Card`](#the-card-object)。再次调用会返回同一个 card。

> **请在入口脚本中导入 SDK。** SDK 一加载就开始监听应用的握手消息。延迟加载 SDK 的卡片（例如在定时器之后动态 `import()`）
> 可能会错过握手，并以 `UNAVAILABLE` 失败。

## `connect(options?)`

```ts
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>`。参见[主题](https://docs.neurosquad.ai/zh/card-sdk/api/environment#theme)。 |
| `syncLang` | `true` | 让 `<html lang>` 始终与应用语言一致。 |
| `forwardErrors` | `true` | 把 `error` 和 `unhandledrejection` 转发到[卡片日志](https://docs.neurosquad.ai/zh/card-sdk/api/environment#log)。 |
| `timeoutMs` | `10000` | 等待应用通道、再等待应用应答的时长。 |
| `port` | — | 使用这个通道，而不是等待应用——用于[模拟宿主](https://docs.neurosquad.ai/zh/card-sdk/testing)。 |

它会以 [`CardSdkError`](https://docs.neurosquad.ai/zh/card-sdk/api/errors) 拒绝：

- `UNAVAILABLE`——页面不在 NeuroSquad 卡片里（在浏览器中单独打开）。要预览，请像模板那样使用[模拟宿主](https://docs.neurosquad.ai/zh/card-sdk/testing)。
- `PROTOCOL_MISMATCH`——应用比你的 SDK 使用的卡片协议旧。卡片会显示“needs a newer NeuroSquad”。

## 卡片对象

| 成员 | 说明 |
| --- | --- |
| `card.context` | 应用告诉卡片的一切，[实时更新](#context)。 |
| `card.setTitle / setStatus / setBadge / setOverview / attention / requestResize / openLink / focusCard / spawn` | [卡片外框](https://docs.neurosquad.ai/zh/card-sdk/api/card-ui) |
| `card.ui` | [提示条、确认对话框、卡片菜单](https://docs.neurosquad.ai/zh/card-sdk/api/card-ui#ui) |
| `card.storage` | [每卡片与每卡片包的存储](https://docs.neurosquad.ai/zh/card-sdk/api/storage-settings#storage) |
| `card.settings` | [设置表单的值](https://docs.neurosquad.ai/zh/card-sdk/api/storage-settings#settings) |
| `card.agents` | [智能体、状态、输出和提示词](https://docs.neurosquad.ai/zh/card-sdk/api/agents) |
| `card.terminals` | [在相连终端中执行命令](https://docs.neurosquad.ai/zh/card-sdk/api/agents#terminals) |
| `card.ports` | [与相连卡片交换类型化数据](https://docs.neurosquad.ai/zh/card-sdk/api/ports) |
| `card.tools` | [给相连智能体的工具](https://docs.neurosquad.ai/zh/card-sdk/api/tools) |
| `card.net` | [经由应用代理的 HTTP](https://docs.neurosquad.ai/zh/card-sdk/api/network) |
| `card.fs` | [工作区文件夹中的文件](https://docs.neurosquad.ai/zh/card-sdk/api/files) |
| `card.permissions` | [授权状态、申请可选权限](https://docs.neurosquad.ai/zh/card-sdk/api/environment#permissions) |
| `card.lifecycle` | [可见性、暂停、展开、调整大小](https://docs.neurosquad.ai/zh/card-sdk/api/environment#lifecycle) |
| `card.log` | [写入卡片日志](https://docs.neurosquad.ai/zh/card-sdk/api/environment#log) |
| `card.copyText / usage / getWorkspace / getTheme / getI18n` | [剪贴板、用量](https://docs.neurosquad.ai/zh/card-sdk/api/files)、获取最新快照 |
| `card.host` | [功能检测](#feature-detection) |
| `card.call / on / once / waitFor` | [任意方法、任意事件](#any-method-any-event) |
| `card.close() / closed` | 关闭通道；之后的调用会以 `UNAVAILABLE` 失败。 |

## 上下文

`card.context` 是一个 `HostContext`：卡片连接时应用发来的内容，并**随每个事件更新**（可见性、大小、主题、语言、设置、权限、对端）。
每次变化时整个对象都会被替换，而不是原地修改——因此可以放心地配合 React 的 `useSyncExternalStore` 使用。

```ts
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'` | 参见[生命周期](https://docs.neurosquad.ai/zh/card-sdk/api/environment#lifecycle)。 |
| `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 或不存在 | [创建](https://docs.neurosquad.ai/zh/card-sdk/api/card-ui#spawn)这张卡片的卡片传来的数据，仅在首次启动时提供。 |
| `chrome` | `CardChromeState` 或不存在 | 应用此刻为这张卡片显示的内容——`title`、`status`、`badge`、`overview`、`attention`——在重新加载之间保留。参见[卡片外框](https://docs.neurosquad.ai/zh/card-sdk/api/card-ui#attention)。旧版本的应用中不存在。 |
| `appVersion` | `string` | NeuroSquad 的版本。 |
| `limits` | `LIMITS` | 协议的所有限制，参见[限制](https://docs.neurosquad.ai/zh/card-sdk/api/errors#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`（[暂停](https://docs.neurosquad.ai/zh/card-sdk/api/environment#lifecycle)后恢复）、
`reloaded`（用户点了 Reload、开发模式下文件有改动，或某项权限被撤销）、`updated`（安装了你的卡片的新版本）。

## 任意方法、任意事件

各个命名空间只是两个基本操作的语法糖，这两个操作都根据协议完整地带有类型：

```ts
// 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()`](https://docs.neurosquad.ai/zh/card-sdk/api/agents#output)。
如果某个主题需要你没有的权限，监听器只是收不到任何东西，同时卡片日志中会出现一条警告。

可能在你添加监听器之前就到达的事件——`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` | 见[端口](https://docs.neurosquad.ai/zh/card-sdk/api/ports#receiving) | 某个输入收到了值。 |
| `ports.request` | 见[端口](https://docs.neurosquad.ai/zh/card-sdk/api/ports#requests) | 对端向请求型输入提问。请使用 `card.ports.onRequest`。 |
| `ports.peersChanged` | `PeerInfo[]` | 箭头或对端发生变化。 |
| `tools.call`、`tools.cancel` | 见[工具](https://docs.neurosquad.ai/zh/card-sdk/api/tools) | 智能体调用工具。请使用 `card.tools.handle`。 |
| `net.chunk` | 见[网络](https://docs.neurosquad.ai/zh/card-sdk/api/network#streaming) | 流式响应的一个分块。请使用 `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 会替你应答。 |

## 功能检测

新方法和新事件会在同一协议版本内陆续加入。使用用户的应用可能还没有的功能之前，请先检查：

```ts
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` 相同。
- **相连**指两张卡片之间有一条箭头，方向不限。端口还会额外关心方向（参见[端口](https://docs.neurosquad.ai/zh/card-sdk/api/ports#direction)）。
- **每次调用都检查两遍。** SDK 用与应用相同的 schema 校验参数，出错时立即以 `INVALID_PARAMS` 失败并给出精确路径；
应用会再检查一遍，外加权限、可见性、箭头和速率限制。
- **只有 JSON 能跨越边界。** 不能传 `Blob` 或 `ArrayBuffer`：接受字节的辅助方法（`fs.writeBytes`、`net.fetch` 的请求体）会替你编码为 base64。
- **回复可能乱序到达；** 事件则按应用发送的顺序到达。
- **永远不要信任 `window` 的 `message` 事件。** 画布上的任何其他卡片都能对你的框架调用 `postMessage`。唯一可信的通道是 SDK 在 `connect()` 时从应用收到的那个；
不要自己添加根据收到的内容行事的 `message` 监听器。
