# Ech0 胶囊(Capsule)使用说明 这份文档基于当前项目实现编写,目标是让你可以直接把内容导出、迁移,并编译成一个能白嫖静态托管的只读站点。 规范性定义见 [`../dev/capsule/spec.md`](../dev/capsule/spec.md),设计取舍见 [`../dev/capsule/capsule-design.md`](../dev/capsule/capsule-design.md)。 --- ## 1. 胶囊是什么 **胶囊是一个自包含的目录(或 zip),装着一个 Ech0 站点的全部公开内容,人类可读、可编辑、可用 git 管理。** ```text capsule/ ech0.yaml # 站点信息 + 归属 + 互联列表 echoes/2026/2026-08-02-b302099a.md # 一条 Echo = 一个 frontmatter-markdown 文件 comments.yaml # 评论快照(公开投影,不含 email / IP) files/images/… # 媒体字节,和实例本地存储同构 ``` 它解决三件事: - **搬家**:把内容从一个实例整体搬到另一个实例,不碰数据库文件。 - **长期保存**:十年后没有 Ech0,这些 markdown 和图片照样能读。 - **静态发布**:编译成纯静态站,扔到 GitHub Pages / Cloudflare Pages 就是一个只读的个人站。 ### 胶囊 vs 快照 | | 胶囊 capsule | 快照 snapshot | |---|---|---| | 内容 | 公开内容(Echo / 媒体 / 评论 / 站点设置) | 整个实例(含数据库文件、账号、凭据、运维配置) | | 形态 | 可读目录,逐条 markdown | 不透明 zip | | 导入语义 | **幂等合并**,按 id 跳过,不覆盖 | **破坏性替换**整库 | | 用途 | 搬家 / 备份内容 / 建静态站 / 换工具 | 灾备回滚 | 要备份「整台机器」用快照;要带走「我写的东西」用胶囊。 > **别拿胶囊当备份。** 胶囊里没有账号、没有密码、没有任何凭据。用它重建实例,文章和图片会回来,但「谁能登录这个站」不会回来——你得从零再注册一遍站主,原来的用户和登录方式全部丢失。要备份就导快照,胶囊只负责内容。 --- ## 2. 命令一览 所有命令都在**实例根目录**下执行(即 `data/` 所在目录)。 ```bash ech0 export capsule [-o ./capsule] [--include-private] [--zip] ech0 export snapshot [-o ./snapshot.zip] ech0 import capsule [=./capsule] [--include-private] [--dry-run] ech0 import snapshot --yes ech0 check [=./capsule] [--fix] ech0 build [=./capsule] [-o ./dist] [--base-url /] ``` 退出码:`0` 成功,`1` 校验错误或执行失败。警告不影响退出码。 `export capsule` 和 `import capsule` 现在还有一条**网页路径**:**面板 → 数据管理**,「导出」tab 选「胶囊」,「导入」tab 选「Ech0 胶囊」。两条路调用的是同一套实现,产物格式与落地规则完全一致,按手边方便挑一条就行。`check` 和 `build` 仍然只有 CLI(网页导入会在写库前自动校验一遍)。 --- ## 3. 导出 ```bash ech0 export capsule -o ./my-capsule ``` - 默认**排除**私密 Echo。要带走加 `--include-private`(胶囊不加密,别随手往公开仓库推)。 - 媒体是**记录驱动**导出:以 `files` 表为准,S3 上的对象会被下载进胶囊。 - **自包含是硬承诺**:只要有一个托管文件的字节取不回来(S3 不可达、对象丢了),导出就整体失败并列出清单,绝不产出一个「看起来完整、实际缺图」的胶囊。修好再跑即可,失败不会在输出目录留残档。 - `--zip` 产出单文件;zip 内布局与目录形态完全一致。 外链文件(`storage_type=external`)只带 URL,字节不随胶囊走——那些字节本来就不归你管。 ### 从网页导出 不想进终端就走面板:**面板 → 数据管理 → 导出**,格式卡选「胶囊」(默认停在「快照」)。 1. 选中胶囊后会多出一条警示——胶囊不含账号与任何凭据、不能用于灾难恢复——以及「包含私密内容」开关,默认关闭,等同于不加 `--include-private`。 2. 点导出。这是个异步作业,进度卡按 **准备 → 打包 → 完成** 走。 3. 完成后浏览器自动下载。产物行标明格式药丸(快照 / 胶囊)、文件名和大小,没接住可以点「重新下载」。 下载始终指向**作业实际产出的格式**:导完胶囊又把选择器拨回快照,「重新下载」拿回来的仍然是刚才那份胶囊。 网页导出的胶囊恒为 zip。要目录形态、要指定输出路径、要写进 cron 的,还是用 CLI。 ### 产物落在磁盘的哪里 自托管的话,这几条磁盘行为值得先知道。网页导出的产物按格式分开存放: ```text data/files/capsules/ # 胶囊 data/files/snapshots/ # 快照 ``` - 两个目录**各自只保留最新一份**,再导一次就顶掉上一份。想留历史版本,自己把文件挪走,或改用 CLI 指定输出路径。 - **快照不会把胶囊打包进去**。实测:盘上躺着 114 MB 胶囊时,快照仍是 110 MB 且不含胶囊,两者不会互相套娃越滚越大。 - 导出胶囊不会删掉已有快照,反之亦然。两条线互不干扰。 - 权限与既有导出一致:需要 `admin:settings`。 --- ## 4. 校验 ```bash ech0 check ./my-capsule # 目录或 .zip 都行 ech0 check ./my-capsule --fix ``` 分两级:**错误**会让 import / build 拒绝执行;**警告**只提示。 常见警告: - `embeds source instance URL` — 正文或 logo 里写死了原实例地址,迁移后可能断链。v1 不自动改写(保「逐字一致」契约),你自己决定要不要改。 - `dangling media` — 胶囊里有字节,但既没有 Echo 引用它、清单 `files` 块也没声明它。`ech0 export` 不会产生这种情况(未挂 Echo 的附件会进清单),基本只在手写胶囊里出现。 - `custom_js / custom_css is not empty` — 别人给的胶囊里带着脚本,导入等于执行对方代码。看一眼再说。 `--fix` **只**补一件事:缺失的 `id`(生成 UUIDv7 回写 frontmatter)。手写胶囊时很有用。zip 形态不可写,`--fix` 会直接拒绝。 --- ## 5. 导入 > **两个前提,踩空了会直接报错。** > > 1. **目标实例必须已有站主。** 胶囊不携带账号,内容得挂到既有站主名下,所以全新实例要先 > `ech0 serve` 跑一次、在页面上把站主建出来,然后再导入。CLI 没有建站主的命令。 > 2. **在目标实例的目录里执行。** `ech0` 按**当前工作目录**解析 `./data`——在别处执行会就地 > 新建一个空库,然后因为「没有站主」失败。报错里会打印它实际用的数据库绝对路径,对不上 > 就是走错目录了。 > > `--dry-run` 同样要满足这两条:它预演的是「导进这个实例会发生什么」,没有可用的实例就无从 > 预演。只想看看胶囊里有什么、格式对不对,用 `ech0 check`,那个不碰数据库。 ```bash ech0 import capsule ./my-capsule --dry-run # 先看清单,不写库 ech0 import capsule ./my-capsule ``` 落地规则,记住这几条就够: - **追加式、幂等、不清库**:按 `id` 判重,已存在的 Echo 直接跳过,不覆盖不合并;导入只往库里加东西,不会清空、不会替换任何现有数据。重复执行安全,也因此没有 `--overwrite`。注意别和 **`import snapshot`** 混为一谈——那个是破坏性替换整库,这个不是。 - **原样入库**:`username`、`fav_count`、正文一律逐字,不做任何转换。唯一的例外是补全内部必填的用户外键——按 `username` 找同名用户挂接,找不到就挂到 owner 名下,但**展示的作者名始终是胶囊里写的那个**。 - **站点设置只填空位**:你已经配过的项一个都不会被动;从没配过的(还是默认值)才用胶囊的值填上——所以「搬到新实例」能把站点标题、logo、备案号一并带过去。 - **评论**跟着宿主 Echo 走:库里有那条 Echo 就挂上(这轮新建的、上次导入的都算),没有就记为孤儿跳过。 - **不发事件**:导入不会触发 webhook、不会重放历史内容给订阅者。代价是向量索引不会自动跟进——导完去后台点一次索引重建。 `--dry-run` 的计数与真实运行完全一致(它照常走一遍事务再回滚),可以放心用来预演。 ### 从网页导入 **面板 → 数据管理 → 导入**,来源卡选「Ech0 胶囊」(另外两个是 Ech0 和 Memos,Memos 还是置灰的开发中项)。 1. 选中后页面会给出两条说明:胶囊导入是追加式且幂等的,按内容 id 去重,重复导入同一个胶囊不会产生重复内容、也不会清空或覆盖现有数据;以及胶囊不含账号与凭据,恢复不了用户和密码,导入的内容会归属**当前登录的站主**。旁边是「包含私密内容」开关,默认关闭。 2. 上传导出得到的胶囊 zip,提交。 3. 进度按 **校验(checking) → 导入(importing) → 完成** 走。 **写库前会强制跑一遍校验**,跑的就是 `ech0 check` 那一套。有错误级问题直接拒绝落库,前几条错误会附在错误信息里;改完重传即可,库里不会留下半截数据。 和 CLI 相比只少一个 `--dry-run`:要先看清单再决定,用命令行预演。判重、原样入库、只填空位、不发事件这些落地规则,两条路一模一样。 一个参考量级:287 条 Echo、170 个文件、15 条评论的实例,网页导出得到 114 MB 的 zip;导入一个全新实例耗时约 1.2 秒,287/287 全部落地;同一个胶囊再导一次是 created 0 / skipped 287。 --- ## 6. 编译静态站 ```bash ech0 build ./my-capsule -o ./dist ech0 build ./my-capsule -o ./dist --base-url /blog/ # 部署到子路径 ``` 产物是一个可以直接扔进任何静态托管的目录: ```text dist/ index.html 404.html assets/… # 内嵌的 Ech0 前端,原样复用 dataset.json # 烘焙好的全部数据 api/files/… # 媒体 api/connect # Connect 名片,别的 Ech0 实例可以来连你 rss.xml sitemap.xml ``` **不需要装 Node 或 pnpm** —— 前端产物已经内嵌在 `ech0` 二进制里。 静态站是**冻结展示**的:点赞数、评论都按导出时的状态只读呈现,发布 / 回复入口自动隐藏。互动痕迹是内容史的一部分,藏掉只会让存档站显得比原站「死」。 部署到 GitHub Pages / Cloudflare Pages 这类平台时,记得把 **SPA 深链兜底**指到 `404.html`,否则直接访问 `/echo/` 会 404。 --- ## 7. 手写与第三方胶囊 胶囊的格式是公开规范,你可以手写,也可以写个转换器把别家的数据变成胶囊。最小可用的一条 Echo: ```markdown --- id: 0198f0a0-0000-7000-8000-000000000001 created_at: 2026-08-02T10:00:00Z --- 正文写在这里,逐字入库。 ``` `id` 缺了可以用 `ech0 check --fix` 生成。时间接受任意合法 RFC3339 偏移(`+08:00` 也行),导出时会统一成 UTC。 引用媒体时,`key` 是扁平文件名,字节放在 `files/` 下按扩展名分好的目录里(`.png` → `files/images/`,其余见 spec §6): ```yaml files: - key: cover.png category: image ``` 写完先 `ech0 check` 一遍再导入。 --- ## 8. 常见问题 **导出报错说某个文件取不回来?** S3 配置变了或对象被删了。修好存储配置再跑;实在拿不回来的,把对应的 `files` 表记录清掉。 **导入后站点标题没变?** 说明目标实例已经配过标题了——导入不覆盖你配过的东西。要换就去后台改。 **静态站图片全裂?** 检查是不是漏了 `api/files/` 目录,或者部署到子路径时没带 `--base-url`。 **能用 git 管理胶囊吗?** 可以,这是设计目标之一。Echo 是独立文件,评论单独一个文件,所以加评论不会污染内容文件的 diff。