扩展数据与存储规范
大约 6 分钟guideextdatastorageextension方案/已实施
扩展数据与存储规范
❝ 写任何数据前先问一句:删掉它,用户会失去什么?——答案决定它住哪一层。 ❞
每个扩展都有自己的数据目录 ~/.pi/web/data/<extId>/(env PI_WEB_DATA_DIR 覆盖;dev 实例隔离在 ~/.pi/web-dev/data)。宿主把它按用途切成三层(对齐 Linux 的 XDG / systemd 目录规范;2026-09-13 起三目录对称):
| 层 | API | 位置 | 一句话定义 | 试金石 |
|---|---|---|---|---|
| 配置 | ctx.configDir | config/ | 用户/管理员的设置输入 | 删了要重新配置 |
| 持久数据 | ctx.stateDir | state/ | 不可再生的数据 | 删了永久丢失 |
| 缓存 | ctx.cacheDir | cache/ | 可再生的数据 | 删了自动重建 |
目录形态:
data/<extId>/{config,state,cache}/(三目录对称)。ctx.dataDir已移除(访问即抛)——旧代码请按本页迁移到三层。
1. 判定决策树
准备落盘一份数据,依次问自己:
1. 删掉它,下次运行能自动重建吗?(索引/缩略图/venv/下载物/日志)
能 ──────────────▶ ctx.cacheDir
2. 它是「用户显式设置/输入的少量键值」吗?(settings/api-key/模型选择/标签/字典/热词)
是 ──────────────▶ ctx.configDir(`config/` 子目录)
3. 其余(归档/历史/统计记录/数据库/用户创作物/导出物)
──────────────────▶ ctx.stateDir边界细则:
- 数据库:只含可重建索引 →
cache/;含不可再生记录(历史、统计)→state/; - 日志:纯调试 →
cache/;审计/历史价值 →state/; - 可重建的索引/统计:有完整重灌机制(可从原始数据/会话/归档重建)的库 →
cache/——重建能力本身就是「可再生」的证据(实例:aiti-ai-usage 的usage.sqlite可从 pi 会话与.pwarc归档全量重灌,实测重灌结果比增量旧库更全)。判定要点:问「重灌源还在吗」——源在(sessions/归档/配置)→ cache;无重灌源的独有记录 → state。
2. 行为契约
| 维度 | config | state | cache |
|---|---|---|---|
rm -rf 后果 | 需重新配置(可恢复) | 永久丢失(不可恢复) | 无损失(自动重建) |
| 扩展自愈义务 | —(视为长存) | 不得假设丢失(读不到 = 异常) | 必须下次访问自行重建 |
| 被 aiti-configs 备份 | ✅ 默认纳入 | ❌ 排除 | ❌ 排除 |
| 被 aiti-backup 备份 | —(configs 已覆盖) | ✅ 默认纳入(规划中) | ❌ 不需要 |
| 允许自动清理 | ❌ | ❌(仅按扩展自设的保留策略) | ✅ 随时 |
| 体积预期 | KB 级 | MB–GB 级 | 任意 |
硬性红线:
cache/目录必须可整体rm -rf后自愈——你的扩展要对「缓存全空」这一状态健壮(db 重建索引、venv 重建环境、日志重新累积);state/里放数据前确认不可再生性——这里的数据删了就没了,扩展不许把它当缓存处理(不许自动清理、不许"重建"式兜底);- 禁止在 config 根级写 db / 日志 / 大文件(它们会进入配置备份链路,大文件会把备份拖垮——这是有过 18G 事故教训的)。
3. 宿主支持状态(ctx.stateDir)
ctx.stateDir 已支持(宿主 2026-09 7e3e992 起);三目录对称(config/ + state/ + cache/)自 2026-09-13 起(ctx.configDir = data/<id>/config,三台测试机全量迁移)。 三层已在三台机器(48 / 155 / 23)全量落地。
老宿主(未注入 stateDir)上的回退写法(语义一致,可长期使用):
import path from "node:path";
const stateDir = ctx.stateDir ?? path.join(ctx.configDir, "state");4. 数据保护矩阵(谁替你兜底)
| 层 | 备份机制 | 说明 |
|---|---|---|
| config | ✅ aiti-configs | git 仓版本化:diff 可看、可回滚、崩溃哨兵 |
| state | ✅ aiti-backup(方案已评审,待实施) | 快照备份:硬链接增量 + 保留轮转 + 恢复;放对层 = 自动被保护,扩展零接入 |
| cache | — | 不需要——删了自动重建 |
写扩展时的分工口诀:配置给 configs,数据给 backup,缓存谁也不给。
5. 迁移铁律(来自真实事故)
改动数据路径或搬迁存量数据时,逐条对照:
- 写 == 读同源:数据路径抽成单一常量/函数,写入侧与读取侧引用同一个来源——任何单侧路径改动都会造成「数据在但用不了」的静默故障(有扩展为此读写分裂、恢复功能静默挂了 30+ 小时);
- 迁移走一次性脚本(扩展不背包袱):存量搬迁由
scripts/migrate-state-tier.mjs在部署流程中执行(停服 → 升级 → 迁移 → 重启),扩展代码不含运行时迁移逻辑;脚本逐条目、幂等、只搬不删——不得用「目标目录是否存在」判断已迁移(预设空目录会永久冻结迁移); - 双写分裂要合并而非覆盖:若升级先于迁移(运行期写入),新位置会出现从零重建/新增的数据——按 key/行去重合并(参考
scripts/merge-split-ledger.mjs与迁移脚本的 merge 型条目),绝不用较新文件直接覆盖; - 测试断言物理位置:写入后断言文件精确落点(不只断言逻辑状态)——「生产写到哪」必须有测试覆盖;
- 不可再生数据不得留在
rm -rf语义的目录:迁移/重构前先确认数据的可再生产性,别把 state 类数据「顺手」搬进缓存区; - 低频操作失败必须可见:恢复/导出/回滚这类长尾操作的失败要有用户可见反馈(别静默回滚样式),并有回归用例。
6. 常见错误清单
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 大文件/venv/db 放 config 根级 | 配置备份膨胀/变慢,牵连全体 | 按决策树:state 或 cache |
把归档/历史放 cache/ | 一次清理 = 永久丢失 | state/ |
假设 cache/ 永远在 | 清理后崩溃/报错 | 实现自愈重建 |
| 假设 state 可重建 | 用户数据丢失 | 不得假设;按 §2 契约 |
把可重建索引放 state/(如用量统计) | 语义错误:不可删层放可再生数据,重建/扩容两难 | 有重灌源 → cache/(§1 边界) |
硬编码 ~/.pi/web/data | dev/prod 隔离失效、迁移困难 | 一律用 ctx.configDir / stateDir / cacheDir |
迁移用 existsSync(to) 判「已迁移」 | 迁移静默冻结 | 逐条目判断(§5.2) |
写读路径各写一份 join(...) | 单侧改动 → 静默故障 | 单一路径常量(§5.1) |
7. 参考
- 跨扩展调用:扩展服务与跨扩展调用规范
- 懒加载:懒加载契约
- 方案与背景:config/cache 拆分(已实施);state 第三层(已实施,含三机迁移记录);数据备份由 aiti-backup 承接(方案已评审)