# 存储与设置

> 按卡片和按卡片包的持久化键值存储，以及读取和修改应用根据清单绘制的设置表单。

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

## 存储

卡片页面没有 `localStorage`、`indexedDB` 或 Cookie——它的沙箱没有可以保存它们的源（origin）。`card.storage` 就是替代品：
由应用以原子方式保存在磁盘上的 JSON 值，在重新加载、暂停和重启之后依然存在。不需要任何权限。

```ts
// This card on the canvas (instance scope).
const draft = await card.storage.get('draft', '')          // string, '' when missing
await card.storage.set('draft', `${draft}\nmore`)
await card.storage.set('runs', [{ at: Date.now(), failed: 3 }])
const runs = await card.storage.get<{ at: number; failed: number }[]>('runs')  // undefined when missing
await card.storage.delete('draft')
const keys = await card.storage.keys('run:')               // keys starting with a prefix
const { bytes, quota } = await card.storage.usage()
render(runs?.length, keys, bytes, quota)

// Shared by every copy of this card, on every canvas (package scope).
await card.storage.package.set('lastSync', Date.now())
card.storage.onChange(({ key, byInstance }) => {
  if (key === 'lastSync') render(`updated by card ${byInstance}`)
})
```

| 方法 | 结果 |
| --- | --- |
| `get(key)` | 值，或 `undefined`。 |
| `get(key, fallback)` | 值，或 `fallback`。结果的类型与 fallback 一致。 |
| `set(key, value)` | 保存任意 JSON 值。 |
| `delete(key)` | |
| `keys(prefix?)` | `string[]` |
| `clear()` | 删除该作用域下的所有键。 |
| `usage()` | `{ bytes, quota, keys }` |
| `onChange(handler)` | 这张卡片的另一份副本写入了**包级**键：`{ scope, key, byInstance }`。 |

`card.storage.package` 上有同样的方法。

**限制：** 键 ≤ 256 个字符，值 ≤ 1 MB，≤ 10 000 个键；每张卡片 5 MB，每个卡片包 20 MB。超出时以 `QUOTA_EXCEEDED` 失败。
写入会在大约四分之一秒内落盘，应用退出时会全部刷写。

**它会怎样：** 实例存储随卡片一起删除。包级存储在卸载后依然保留，除非用户勾选 **Also delete its saved data and secrets**。
存储在更新后保留——请让新版本仍能读懂你存下的数据结构（如果以后可能改动，就存一个 `version` 字段）。

> 在被卸载前保存：隐藏一段时间的卡片会被暂停，它的页面会被丢弃。用
> [`card.lifecycle.onSuspend`](https://docs.neurosquad.ai/zh/card-sdk/api/environment#lifecycle) 把仍在内存中的内容写下来。

## 设置

在清单的 [`settings`](https://docs.neurosquad.ai/zh/card-sdk/manifest#settings) 中声明字段；应用负责绘制表单（从卡片的 **⋯ → Settings…** 打开，
或在你调用 `card.settings.open()` 时打开）、校验并保存这些值。你的卡片读取它们：

```ts
const command = card.settings.value<string>('command') ?? 'npm test'
const all = card.settings.values                         // defaults filled in, secrets never included
const hasKey = card.settings.hasSecret('apiKey')         // true when the user saved one

card.settings.onChange((snapshot) => {
  render(snapshot.values['command'], snapshot.secrets['apiKey'])
})

// Change your own non-secret settings (a toggle in your UI, say). Validated like the form.
await card.settings.set({ compact: true })

// Open the form on the card (only while it is visible).
if (!hasKey) await card.settings.open()

render(command, all)
```

| 成员 | 说明 |
| --- | --- |
| `values` | 当前值，已填入默认值。实时更新。 |
| `secrets` | `Record<key, boolean>`：哪些密钥设置已填写。 |
| `value(key)` | 单个值，类型由你指定。 |
| `hasSecret(key)` | 某个密钥设置是否已填写。 |
| `get()` | 从应用获取最新的 `SettingsSnapshot`。 |
| `set(values)` | 修改非密钥设置；卡片的每份副本都会收到 `settings.changed`。无效值会以 `INVALID_PARAMS` 失败。每分钟最多 60 次。 |
| `open()` | 在卡片上打开设置表单。卡片不在屏幕上时为 `NOT_VISIBLE`。 |
| `onChange(handler)` | 用户保存了表单，或 `set` 改动了某些值。 |

**作用域。** 带有 `"scope": "package"` 的字段对你卡片的所有副本只有一个值；其余字段按卡片各自保存。

**密钥。** `secret` 字段只能在应用自己的对话框中输入——在设置表单中，它那一行只显示“已设置”或“未设置”，并会打开那个覆盖整个窗口的对话框——并由应用加密保存。
卡片永远读不到它——无论通过 `values` 还是任何方法。要使用它，把 `{{secret:<key>}}` 放进[网络请求头](https://docs.neurosquad.ai/zh/card-sdk/api/network#secrets)；
应用会在请求发出时填入，且只针对你的卡片被授权的互联网主机（永远不会针对 `localhost`）。
不要在卡片里自己做一个“粘贴你的 API 密钥”的输入框：那样一来泄露密钥的责任就在你身上，而且用户已被告知 NeuroSquad 从不在卡片里索要密钥。
