> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/architecture/storage.md).

# 存储

位于 crates/openhuman-core/src/storage/ 的存储域：包括端口、URL 解析、Cargo 特性、按代理作用域、驱动以及磁盘布局。

OpenHuman 将持久状态保存在 [`tinystoragedrivers`](https://github.com/tinyhumansai/tinystoragedrivers)，打包于 `vendor/tinystoragedrivers`。该子模块提供一个经过筛选的表面——类型、API trait、总线契约和内存引擎——以及由特性标志启用的独立引擎 crate。根模块和应用 `[patch."https://github.com/tinyhumansai/tinystoragedrivers"]` 表会将每个声明重定向到同一个子模块检出，因此构建只保留一份副本。打包提交必须与各标签模块声明的语义化版本兼容； `scripts/ci/check-storage-owner.mjs` 强制执行两条规则：(1) 每个 `tinystoragedrivers*` 已解析图中的包都是位于 `vendor/tinystoragedrivers/`，且 (2) 任何非根的一方 crate 都不会通过任何普通或构建依赖接触到引擎 crate（`rusqlite`, `mongodb`, `bson`, `aws-sdk-s3`, `aws-smithy-*`, `lapin`, `amq-protocol`, `object_store`, `sqlx`, `redb`, `sled`, `rocksdb`, `fs2`）通过任何普通或构建依赖。经过合理解释的允许列表只覆盖组合根（`openhuman`, `openhuman-app`, `openhuman-cli`, `openhuman-tui`, `openhuman-rpc`, `tinychannels`, `tinybox-cli`, `tinyjuice`）。一个 URL 为整个进程选择后端。主机适配器是 `crates/openhuman-core/src/storage/`。它解析 URL，打开后端，将其保存在进程槽中，并决定调用在哪个作用域中运行。这些端口、驱动及其一致性测试套件都属于 `tinystoragedrivers`，而带类型的记录则保留在拥有它们的领域中。

在未配置 URL 时，这是桌面默认值，主机会打开 `sqlite:<workspace>/storage` 并安装它。URL 决定整个进程将状态保存在何处，而且不存在第二种布局：旧安装中按领域划分的数据库、转录和轮次状态文件会在后端安装时一次性以只读方式导入。仍然不在端口上的文件列在下面的“目前端口上尚未包含的内容”中。

## 端口

后端是一个 `StorageBackend`。它发放一个 `ScopedStorage` 用于每个 `Scope`，而其上的每个句柄都已绑定到该作用域，因此没有任何方法需要传入作用域参数。

| 端口              | 持有                                                                 | 用于                                 |
| --------------- | ------------------------------------------------------------------ | ---------------------------------- |
| `DocumentStore` | 带版本的 JSON 文档，存放在命名集合中，支持过滤、分页、唯一索引、比较并交换（`前置条件`）、原子 `认领` 以及可选的过期时间 | 审批、设备、通知、任务源、cron 作业及其运行、流程目录、图检查点 |
| `StreamStore`   | 仅追加日志，具有紧密偏移                                                       | 转录、日志                              |
| `KvStore`       | 按键寻址的 JSON 值                                                       | 由所有者管理的小型值                         |
| `BlobStore`     | 按键存取的不透明字节和树                                                       | 附件、制品及其他大型对象                       |
| `QueueStore`    | 带租约、已确认的队列投递                                                       | 持久化工作队列                            |
| `SecretStore`   | 带作用域的密钥值，并支持可选的条件写入                                                | 当所选后端配置了密钥引擎和密钥提供方时使用密钥            |

核心的存储大多是同步的，因此 `storage::block_on` 会在一个共享的运行时线程上运行一个 storage future。它从不使用 `block_in_place`，因为这会在 current-thread 运行时上 panic。 `storage::documents::Repo` 是领域存储所构建的基础。它会为每个进程只声明一次某个领域的集合，并执行每次调用。

## 选择后端

存储 URL 是其中第一个被设置的值。空值视为未设置。

1. `OPENHUMAN_STORAGE_URL`
2. `[storage] url` 在 `config.toml`
3. 默认值， `sqlite:<workspace>/storage` （每个工作区一个数据库）

已废弃的 `经典` opt-out 值会被视为未设置，并发出警告。

`[storage]` 是引导配置。它始终从环境或文件中读取，而绝不从存储本身读取。

| URL                                               | 驱动                                                                                                                                          | Cargo 特性          |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `记忆`                                              | 进程内映射，不保留任何内容                                                                                                                               | 始终                |
| `sqlite:<path>` （一个 `.db` 文件，或者一个包含 `storage.db`) | SQLite                                                                                                                                      | `storage-sqlite`  |
| `file:<dir>`                                      | `<dir>/scopes/<scope>/docs/<collection>/<id>.json` 用于文档，集合元数据位于 `<dir>/_meta/collections/<collection>.json`。流是 JSONL，blob 是位于同一作用域目录下的原始文件。 | `storage-file`    |
| `mongodb://…/<db>`, `mongodb+srv://…/<db>`        | MongoDB，所有作用域共享一个数据库                                                                                                                        | `storage-mongodb` |

构建中缺少的驱动对应的 URL 会在 `storage::open`，错误信息会指出相应的 Cargo 特性。因此，配置错误的部署会在启动时就停止，而不是等到首次写入时才出错。URL 中的凭据会在日志和 `Debug` 输出中被脱敏。

每个特性都会贯穿整个库链路传递（`openhuman-core`, `openhuman-embed`, `openhuman-tinyhumans`, `openhuman-rpc`，然后到 `openhuman-cli` 和 `openhuman-tui` 各主机）。 `scripts/ci/check-feature-forwarding.mjs` 会检查这些链接。桌面构建绝不会链接 MongoDB 客户端，而云构建不需要 SQLite。

主机会在核心启动前打开已配置的后端，并通过 `storage::install`。然后它再安装 TinyAgents 的 `DriverSessionStores` 到同一个后端中，因此转录、轮次状态、运行账本和日志也都保存在其中。库宿主使用 `RuntimeBuilder::storage(..)`，它只安装后端：对话会通过 `RuntimeBuilder::session_store(..)`，且单独配置。

## 作用域

每条记录都位于某个作用域中，同一后端上的两个作用域绝不会看到彼此的数据。只有一套作用域语法，即 tinystoragedrivers 的（`docs/specs/schema-v2.md` §1）：一个 **租户** (`本地` 用于桌面操作员， `u:<profile>` 用于 SaaS 配置文件），可选地进一步缩小到一个 **主体** (`<tenant>/agent:<id>` 或 `<tenant>/module:<id>`），以及不含租户的 `集群` 作用域，它保存配置文件注册表和进程租约。主机只通过以下方式构建作用域： `Scope::local()`, `Scope::tenant(id)`, `.agent(id)` 和 `.module(id)` (`storage::scope`: `tenant_scope`, `scope_for_profile`, `scope_for_agent`, `scope_for_module`），而绝不直接来自原始字符串。对于过长或包含语法之外字符的片段，会由 tsd 自身进行哈希（`~` 再加 26 个 base32 字符），而且只对该片段进行哈希，因此被哈希的 agent 或 module 仍然位于其租户之下。

该 **租户是隔离边界。** 某个配置文件的租约围栏会保护其租户作用域以及前缀 `<tenant>/`，它覆盖其下所有 agent 和 module 作用域，无论是否经过哈希（`profiles::fence::scopes_of`）。租户清除会覆盖租户作用域及其下的每个主体，枚举使用 `StorageBackend::scopes_of_tenant`。会话存储是 TinyAgents 的 `DriverSessionStores`，每个租户一个提供方（`agent::session_store::TenantSessionStores` 会将主机的扁平会话键 `<profile>~<agent>` 路由到该租户的提供方），因此会话记录始终位于 `local/agent:<id>` 或 `u:<profile>/agent:<id>`。领域存储将其记录保存在租户作用域中，而可加载模块的记录位于 `<tenant>/module:<id>`.

`storage::current_scope()` 根据任务的租户解析当前调用的作用域（`storage::scope_from(profile, agent, saas)`），按以下顺序：

1. A **配置文件**，当工作是为某个配置文件运行时：无论其中哪个 agent 执行，作用域都是该配置文件的。在 SaaS 模式下，同一配置文件的各个 agent 因此共享该配置文件的作用域。
2. 否则，执行的 **agent** (`CoreContext::session_agent`）：作用域是 `local/agent:<id>`.
3. 否则 `本地`：单用户模式下的操作员、CLI 和桌面 shell。

在 SaaS 模式下，如果没有配置文件的调用会被拒绝，而不是落入其他用户可能共享的桶中，无论是否有 agent 在执行。如果任务的租户完全无法解析（`current_tenant()` 失败）， `current_scope()` 会在所有模式下返回该错误，而不是回退到 `本地`.

后端决定如何强制执行作用域。SQLite 和文件驱动会将每个作用域的数据分开，而 `本地` 则是单用户安装所使用的作用域。MongoDB 会将作用域键注入每个过滤器和每个索引前缀。

### 进程状态

该 [进程状态清单](/openhuman/zh/kai-fa/architecture/process-state.md) 对命名的 shared-core 单例候选项进行分类，并记录它们的租户、持久性和路由边界。它还会识别待处理的回归问题和实现工作。

### 进程缓存

已连接集成目录属于调用它的配置文件，或单用户模式下的操作员。使用该操作员相同配置和凭据的 agent 会共享它。其缓存键还会绑定配置、后端端点和凭据标识，使用摘要而不是将这些密钥直接存入键中。相同的服务凭据不会让两个配置文件共享同一个快照。

| 缓存                   | 隔离与生命周期                                                       |
| -------------------- | ------------------------------------------------------------- |
| `INTEGRATIONS_CACHE` | 按配置文件/操作员作用域和凭据标识作为键的进程级快照。失效和连接协调只影响调用作用域。条目会从后端重建；它们不是持久记录。 |
| `CACHE_GENERATIONS`  | 针对相同配置文件/操作员作用域的进程级纪元。失效围栏会阻止该作用域中正在进行的获取，但不会拒绝其他配置文件已完成的获取。  |
| 存储 `KEYS`            | 进程主密钥提供方。每次密钥操作都会从捕获到的本机存储作用域派生数据密钥；缓存值不是租户数据密钥。更改主来源需要启动新进程。 |

没有配置文件的 SaaS 工作无法读取、填充、协调或失效集成缓存。设置连接轮询在通过……启动后台刷新时会保留其调用上下文 `spawn_scoped`.

### 后台工作

后台工作在进程默认上下文下运行，该上下文不指明任何 agent，单独来看它只能看到 `本地`. `storage::agents` 填补这一空缺。agent 在处于活动状态时（`AgentContextRegistry`）或当先前的进程已将其 id 记录在 `本地` 作用域（`storage_agents`). `for_each_scope` 会对以下对象各运行一次步骤： `本地` 以及在每个已知 agent 之下各运行一次。cron 调度器的 agent 轮次（`cron::scheduler::tick_live_agents`，在测试中通过公开的 `cron::scheduler::run_live_agent_pass`）会在每个活动 agent 自己的上下文下运行该 agent 到期的任务，因此结果会记录在该 agent 自己的作用域中。未处于活动状态但已记录的 agent 会等待直到其再次变为活动，因为它的任务需要其主机工具和提示。

在共享后端（MongoDB）上，其他进程可能会写入相同记录，因此会跳过启动时恢复，例如孤立运行清理（`storage::installed_is_shared`).

### 存储运行时

`storage::open` 会在其自己的进程级 tokio 运行时上连接驱动（两个 `openhuman-storage` 工作线程，且永不关闭）。 `storage::block_on` 而改为使用内部运行时。同步存储通过……到达这些端口 `storage::block_on`，它会挂起调用线程；当后端运行在存储运行时时，任意数量处于挂起状态的调用者都无法离开主机运行时，除非有后端回答它们所需的线程。希望获得这种隔离的嵌入方会将其存储作为 URL 传入（`StorageSource::Url`），而不是传入它在自己运行时上打开的后端。

## 驱动与磁盘布局

| 驱动                 | 数据存放位置                                                                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `记忆`               | 不存在于任何地方。记录的生命周期与后端值相同。                                                                                                                     |
| `sqlite:<dir>`     | `<dir>/storage.db` 采用 WAL 模式（连同其 `-wal` 和 `-shm` 文件）。作用域是每一行中的一列。大型关系型存储通过驱动的原生模式保存其表。                                                      |
| `sqlite:<file>.db` | 该文件，布局方式相同。                                                                                                                                 |
| `file:<dir>`       | `<dir>/scopes/<scope>/docs/<collection>/<id>.json` 用于文档，集合元数据位于 `<dir>/_meta/collections/<collection>.json`。流是 JSONL，blob 是位于同一作用域目录下的原始文件。 |
| `mongodb://…/<db>` | 一个数据库。每个命名数据库都是一个集合前缀，而每条记录都带有其作用域。                                                                                                         |

### 配置文件布局 v1 和 v2

桌面配置文件位于 `~/.openhuman/users/<id>/`，采用以下两种布局之一（`config/schema/profile_layout.rs`, `LayoutVersion`):

```
v1（storage schema v2 之前的所有安装）    v2（全新的配置文件）
users/<id>/                                    users/<id>/
  config.toml                                    .layout        标记：2
  workspace/    内部状态 + storage/        config.toml
                                                 storage.db     所有存储，共一个 SQLite 文件
                                                 files/         Config.workspace_dir
```

版本从磁盘读取，从不通过配置指定。一个 `.layout` 文件读取 `2` 是 v2。缺失或为空的配置文件目录被视为新的，并以 v2 开始（标记会在第一次解析时写入，因此一旦 `config.toml` 出现就会保持为 v2）。 `workspace_dir` 是 `users/<id>/workspace`，默认 URL 是 `sqlite:<workspace>/storage`。解析 v1 配置文件不会写入任何内容。

在 v2 下 `Config.workspace_dir` 是 `users/<id>/files`，因此拼接相对 workspace 路径的调用点仍然有效，而 `config.toml` 和 `storage.db` 则保持在配置文件根目录下，在那里 `workspace_dir.parent()` 可以找到它们。默认 URL 是 `sqlite:users/<id>/storage.db`。显式指定的 `OPENHUMAN_STORAGE_URL` 或 `[storage] url` 在两种布局下都会优先生效。重新绑定（不同用户登录）以及无后端的本地回退都会从新的 workspace 派生 URL，因此会遵循该布局。二进制程序会读取两种布局。SaaS 配置文件（`ProfileLayout::new`）保持 v1。将 v1 配置文件迁移到 v2 是 #7997 跟踪的迁移事项，不会在启动时执行。

### 默认值跟随 workspace

默认 URL 以 workspace 为根，因此不同的 workspace 就是不同的数据库。桌面在不同用户登录时会重新绑定其 workspace。 `storage::rebind` 然后为新的 workspace 重新解析 URL，打开其后端，并将会话存储（`ReboundSessionStores`）以及进程槽一并指向它，并在其上运行旧版导入器。这发生在任何其他存储重新绑定之前；如果重新打开失败，则会报错，并保留先前安装的后端，而不是继续使用它。显式 URL 为每个用户指定一个后端，且绝不会被替换。

没有 URL 的 SaaS 核心会为每个配置文件 workspace 保留一个默认数据库，其配置文件之间绝不会共享作用域。

### 目前尚未纳入端口的内容

密钥环或 `secrets.enc` 以及与路径绑定的制品字节和媒体仍然使用文件。成本跟踪、Composio 状态、子 agent 会话和 agent 制品记录都没有文件回退：在未安装后端时，它们会使用 workspace 自己的默认值（`sqlite:<workspace>/storage`） `meta.json`、Composio 状态文件或子 agent 会话文件会被导入一次（并且文件会被重命名为 `*.migrated`）。保险库监视器的 mtime 表特意保留为每台机器独有的 SQLite 文件，因为其行记录的是单个主机上的绝对路径。首次在已安装后端的情况下读取时，身份验证配置文件和 HTTP 凭据存储会将其文件中的记录复制到后端，而密钥环会采用它在进程后端中找到的密钥。源文件保持不变。

### 导入旧版小型存储

安装后端后，核心会在启动时执行一次性导入（`storage::import`，在任何领域存储打开之前）。它涵盖四个各自保存一个 SQLite 文件的小型存储：批准（`approval/approval.db`）、设备（`devices/devices.db`）、通知（`notifications/notifications.db`）和任务源（`task_sources/sources.db`）。导入是只读的。它会复制该文件及其 `-wal` 和 `-shm` 配套文件到暂存目录并读取副本，因此旧版读取器的架构迁移和 SQLite 驱动程序的表声明绝不会触及真实文件。两种磁盘格式都会被读取：原始表，以及较早的默认设置写入同一文件的驱动程序文档表（这些记录较新，因此在 ID 冲突时胜出）。核心的通知会在真实工作区的键下重新归档。

每条记录均使用仅在不存在时写入的前置条件，因此现有记录绝不会被覆盖。当某个领域的记录写入后，会存储一个标记文档（`storage_import/<domain>@<workspace digest>`，本地作用域），之后的启动不会执行任何操作。因此，被删除的记录不会从未改动的文件中恢复。导入失败不会留下标记，并会在下次启动时重试；无论结果如何，启动都会继续。SaaS 进程没有工作区文件，因此不导入任何内容。

tinyflows 存储也以相同方式通过 TinyFlows 自己的读取器导入（`tinyflows-import`）：带有运行记录和策略的 cron 作业（`cron/jobs.db`，以及每个代理的 `agents/<id>/cron/jobs.db`，归入该代理的作用域），以及包含草稿和检查点的流程目录（`flows/flows.db`, `flows/drafts/*.json`, `flows/checkpoints.db`）。记录保留其 ID，标记为 `cron`, `cron/<agent>` 和 `flows`，且未启用 `flows` 门控的构建不会导入任何 cron 作业。

TinyFlows 不再拥有自己的存储后端，因此无论 URL 如何表示，这些存储都位于端口上，旧版文件也绝不会再被修改或写入。

主机拥有旧版会话源及其生命周期（`storage::import::sessions`）。它会对 `session_db/sessions.db` 使用 `VACUUM INTO`创建快照，解码其行及 `session_raw` JSONL 文本记录，根据其词干解析文本记录代理，并将旧行映射到 TinyAgents 的类型化导入端口。默认代理接收旧版数据库的运行历史和回合状态 JSON；每份文本记录都会写入已解析代理的作用域，身份未知时则回退到默认代理。TinyAgents 负责目标写入及其所有者本地的存储内迁移，但不会打开这些源文件。标记为 `sessions`；跳过记录的运行不会留下标记，并将重试。

安装后端时，密钥是当前存储作用域中的加密文档：在桌面或 CLI 主机上为执行操作的代理的作用域，在 SaaS 下为配置文件的作用域，其中一个配置文件中的所有代理共享该配置文件的凭据和数据密钥。每个作用域的数据密钥均通过 HKDF-SHA256 从密钥环主密钥派生（`OPENHUMAN_KEYRING_MASTER_KEY` 或 `_FILE`，否则使用操作系统密钥链）；没有主密钥时，它们会以失败关闭方式处理。配置加密密钥保留在进程密钥环中，因为 `config.toml` 会在任何代理执行操作之前加载。

## 测试和 CI

主机的存储测试仅在 SQLite 上运行。它们测试主机在配置后端上的接线情况（哪些领域存储进入其中、使用哪个作用域、第二个进程读取到什么），而不是驱动程序。共享辅助工具是 `tests/support/storage_case.rs`：它在临时目录中打开 SQLite 后端，并在进程范围的存储槽上加锁，因此一个二进制文件的测试会轮流执行。

每个驱动程序是否遵循存储端口在上游得到验证，位于 `tinystoragedrivers`。它的 `一致性` 模块针对每个驱动程序运行相同的检查（嵌套作用域隔离、通过以下方式进行跨进程比较并交换和租约： `run_shared` ，适用于声明 `Capability::SharedAcrossProcesses`的驱动程序，以及围栏、文档密钥和迁移），其 CI 还会针对 MongoDB 副本集运行这些检查。主机在可能的情况下向后端查询 `Capability::SharedAcrossProcesses` ，而不是硬编码驱动程序名称（`storage::backend_has_cross_process_cas`）；启动守卫会在打开任何内容前判断 URL，因此它保留了一个由测试固定到真实驱动程序的名称检查。

目标为 `storage_approvals_e2e`, `storage_domains_e2e`, `storage_flows_e2e`, `storage_secrets_e2e`, `storage_scope_e2e`, `storage_agent_scopes_e2e`, `storage_desktop_upgrade_e2e` 和 `cli_storage_url_e2e`。在本地运行其中一个：

```bash
cargo test -p openhuman-cli --features storage-sqlite --test storage_approvals_e2e
```

CI 在核心覆盖率套件中运行这些目标，以及端口上领域的库测试（`scripts/ci/rust-coverage.sh`）。 `storage` 中的 `.github/ci-paths-filter.yml` 区域会在存储、cron、流程、批准、设备、通知、任务源、密钥环或凭据、编排、成本跟踪器、应用和桌面控制状态，或 RPC 会话存储发生变化时启用它。本仓库中没有单独的驱动程序流水线，也没有 MongoDB 工作流。 `storage-mongodb` 和 `storage-file` 功能仍随产品一起发布。

## 后续阅读位置

* `crates/openhuman-core/src/storage/README.md` 以了解入口点和使用方。
* 该 `tinystoragedrivers` 规范： `docs/specs/storage-ports.md` 和 `docs/plans/storage-rollout.md` 位于 tinystoragedrivers 仓库中（标签 `v2.1.0`).
* [安全](/openhuman/zh/kai-fa/architecture/security.md) 适用于密钥环和凭据存储。

## 架构、启动和迁移

存储 crate 暴露纯 v2 `CollectionSpec` 值。主机在以下位置按 crate 验证并保留这些声明： `storage::schema::collection_owners`，然后调用 `ensure_collection` 来处理其共享作用域中的每项声明，以及所有者打开作用域时的每项声明。生成的 [`docs/storage-schema.md`](https://github.com/tinyhumansai/openhuman/tree/main/docs/storage-schema.md) 列出每个 crate 的集合名称、索引、过期字段和搜索字段； `scripts/ci/check-storage-schema.mjs` 检查其名称和毫秒级时间戳字段。记录结构、旧版转换和迁移规则仍由所属领域负责。

`storage::open` 运行 `storage::boot::prepare_and_migrate` 于原始后端上，先于租约围栏将其包装：

1. `collection_owners` 在不合并所有者的情况下验证每个 crate 的声明。
2. 主机确保每个 crate 的集合存在于其本地和集群作用域中。配置文件、代理和模块作用域在打开时由其所有者准备；不使用全驱动程序架构注册表或作用域枚举。
3. 历史工作区和按领域划分的文件由单独受门控的 `legacy-import` 路径读取。其源格式和类型化所有者转换与正常后端打开过程保持分离。

模块的存储授权根据其所有者声明的集合生成（`modules::storage_grant_schemas`），因此它不会偏离模块所声明的内容。
