Back to Prompt Optimizer

存储运行时架构与边界说明

docs/architecture/storage-runtime-architecture.md

2.10.216.3 KB
Original Source

存储运行时架构与边界说明

📋 文档目的

本文档用于说明当前仓库在 Web / Extension / Desktop 三种运行环境下的真实存储结构,重点回答以下问题:

  • 现在有哪些存储方式
  • 各存储里有哪些逻辑区域
  • 不同类型的内容会落到哪里
  • 每个区域的大小限制、配额和清理策略是什么
  • 当前已经建立了哪些防线
  • 还存在哪些灰区和后续必须补齐的边界

本文档描述的是 当前运行态事实,不是理想设计图。

🎯 核心原则

当前系统必须严格区分两类数据:

  1. 结构化业务数据

    • 例如设置、会话快照、模型配置、收藏文本、上下文文档。
    • 应存入结构化主存储。
  2. 图片二进制资产

    • 例如上传图、生成图、收藏封面、收藏示例图。
    • 必须存入独立图片资产库。
    • 结构化主存储中只允许保存图片引用和轻量元数据,不允许保存大体积 inline base64。

这条边界是本轮存储事故复盘后的第一原则。

🧱 一、物理存储介质

1. Web / Extension 结构化主存储

  • 介质:IndexedDB
  • 实现:DexieStorageProvider
  • 数据库名:PromptOptimizerDB
  • 表结构:单表 storage
  • 本质:一个共享的 KV 桶

相关文件:

  • packages/core/src/services/storage/dexieStorageProvider.ts

2. Desktop 结构化主存储

  • 介质:主进程 JSON 文件
  • 实现:FileStorageProvider
  • 主文件:<userData>/prompt-optimizer-data.json
  • 备份文件:<userData>/prompt-optimizer-data.json.backup
  • 写入方式:内存镜像 + 延迟写盘 + 原子替换 + 备份恢复

相关文件:

  • packages/core/src/services/storage/fileStorageProvider.ts
  • packages/desktop/main.js

3. Session 图片资产库

  • 介质:IndexedDB
  • 实现:ImageStorageService
  • 数据库名:PromptOptimizerImageDB
  • 目标:保存会话使用的上传图、生成图和相关图片资产

表结构分为两张表:

  • imageMetadata
  • imageData

相关文件:

  • packages/core/src/services/image/storage.ts
  • packages/ui/src/composables/system/useAppInitializer.ts

4. Favorite 图片资产库

  • 介质:IndexedDB
  • 实现:ImageStorageService
  • 数据库名:PromptOptimizerFavoriteImageDB
  • 目标:保存收藏项相关的封面图、示例图、引用图资产

表结构同样分为两张表:

  • imageMetadata
  • imageData

相关文件:

  • packages/core/src/services/image/storage.ts
  • packages/ui/src/composables/system/useAppInitializer.ts

5. 非主线路径 provider

代码里还存在两个 provider,但不是当前线上主路径:

  • LocalStorageProvider
    • 浏览器本地存储实现
    • 代码声明能力上限约 5MB
  • MemoryStorageProvider
    • 主要用于测试或临时环境

相关文件:

  • packages/core/src/services/storage/localStorageProvider.ts
  • packages/core/src/services/storage/factory.ts

🗂️ 二、结构化主存储里的逻辑分区

虽然 Web 物理上只有 PromptOptimizerDB.storage 一张表,Desktop 物理上只有一个 JSON 文件,但逻辑上可以拆成以下几块。

1. pref:* 命名空间

PreferenceService 统一管理。逻辑 key 会被自动加上 pref: 前缀后再落盘。

示例:

  • global-settings/v1 -> pref:global-settings/v1
  • session/v1/basic-system -> pref:session/v1/basic-system
  • variableManager.storage -> pref:variableManager.storage

相关文件:

  • packages/core/src/services/preference/service.ts

2. Core 直接 key

这些 key 不经过 PreferenceService,直接写入主存储:

  • models
  • image-models
  • user-templates
  • prompt_history

相关文件:

  • packages/core/src/constants/storage-keys.ts

3. Context 单文档区

上下文不是按多个 key 分散存,而是集中在一个文档里:

  • ctx:store

这个文档同时保存:

  • 所有 context 数据
  • 当前激活的 context id
  • 文档版本号

相关文件:

  • packages/core/src/services/context/constants.ts
  • packages/core/src/services/context/repo.ts

4. Favorites 直接 key 区

收藏系统也是独立 key,不经过 PreferenceService

  • favorites
  • favorite_categories
  • favorite_stats
  • favorite_tags
  • favorite_categories_initialized

相关文件:

  • packages/core/src/services/favorite/manager.ts

5. 旧 UI key / 混合态 key

当前仓库仍存在一批历史 key 和新快照并存的情况,包括但不限于:

  • global-settings/v1
  • app:settings:ui:function-mode
  • app:settings:ui:basic-sub-mode
  • app:settings:ui:pro-sub-mode
  • app:settings:ui:builtin-template-language

需要特别注意:

  • imageSubMode 现在以路由为真源,不再直接依赖 preference 持久化
  • 但应用外壳仍会把当前路由状态镜像回 global-settings/v1

相关文件:

  • packages/ui/src/stores/settings/useGlobalSettings.ts
  • packages/ui/src/composables/mode/useFunctionMode.ts
  • packages/ui/src/composables/mode/useBasicSubMode.ts
  • packages/ui/src/composables/mode/useProSubMode.ts
  • packages/ui/src/composables/mode/useImageSubMode.ts
  • packages/ui/src/components/app-layout/PromptOptimizerApp.vue

📦 三、不同内容现在实际存到哪里

1. 会话快照

各功能区会话存储在 pref:session/v1/* 下,包括:

  • basic-system
  • basic-user
  • pro-multi
  • pro-variable
  • image-text2image
  • image-image2image
  • image-multiimage

这些快照保存的是结构化 JSON,例如:

  • 原始 prompt
  • 优化后 prompt
  • 模型选择
  • 模板选择
  • 评测结果
  • 变量值
  • 图片引用 id

相关文件:

  • packages/ui/src/stores/session/useSessionManager.ts
  • packages/ui/src/stores/session/useBasicSystemSession.ts
  • packages/ui/src/stores/session/useBasicUserSession.ts
  • packages/ui/src/stores/session/useProMultiMessageSession.ts
  • packages/ui/src/stores/session/useProVariableSession.ts
  • packages/ui/src/stores/session/useImageText2ImageSession.ts
  • packages/ui/src/stores/session/useImageImage2ImageSession.ts
  • packages/ui/src/stores/session/useImageMultiImageSession.ts

2. Session 图片

会话中真正的图片字节不应该放进 session 快照,而是写入 PromptOptimizerImageDB

session 快照里应该只保留:

  • assetId
  • image-ref
  • 轻量 metadata

图片写入和引用转换相关文件:

  • packages/ui/src/utils/image-asset-storage.ts
  • packages/ui/src/stores/session/imageStorageMaintenance.ts

3. 收藏文本和收藏元数据

收藏项本体仍保存在结构化主存储中的 favorites key 下。

其中包含:

  • 标题
  • 正文内容
  • 标签
  • 分类
  • functionMode
  • optimizationMode
  • imageSubMode
  • metadata

注意:

  • favorites 不是 pref:* 命名空间的一部分
  • 因此它不受 PreferenceService 针对 session 的大小防线保护

相关文件:

  • packages/core/src/services/favorite/manager.ts

4. 收藏图片资产

收藏相关图片应该写入 PromptOptimizerFavoriteImageDB,然后在收藏 metadata 中保存引用信息:

  • coverAssetId
  • assetIds
  • imageAssetIds
  • inputImageAssetIds

相关文件:

  • packages/ui/src/utils/favorite-media.ts
  • packages/ui/src/components/SaveFavoriteDialog.vue
  • packages/ui/src/composables/app/useAppPromptGardenImport.ts

5. 收藏图片 fallback

当前收藏元数据 schema 仍允许 URL 型 fallback:

  • coverUrl
  • urls

这意味着当前系统允许:

  • 远程 URL fallback

但现在已经明确禁止:

  • data:image/...;base64,... 这种 inline 数据 URL 被写入 favorite metadata

相关文件:

  • packages/ui/src/utils/favorite-media.ts
  • packages/core/src/services/favorite/manager.ts

6. Prompt Garden 导入收藏

Prompt Garden 导入链路已经改成严格模式:

  • 优先把图片落到图片资产库
  • 不再允许把 inline 图片内容回退进收藏 metadata
  • 自动收藏路径在图片落盘失败时会跳过收藏保存
  • 确认弹窗路径在图片落盘失败时不再附带 media fallback

相关文件:

  • packages/ui/src/composables/app/useAppPromptGardenImport.ts

7. 模型、模板、历史、上下文

这几类数据都属于结构化主存储:

  • 模型配置 -> models
  • 图像模型配置 -> image-models
  • 用户模板 -> user-templates
  • 历史记录 -> prompt_history
  • 上下文文档 -> ctx:store

相关文件:

  • packages/core/src/services/model/manager.ts
  • packages/core/src/services/template/manager.ts
  • packages/core/src/services/history/manager.ts
  • packages/core/src/services/context/repo.ts

📏 四、大小限制、配额和清理策略

1. Session 快照大小限制

PreferenceService 现在对 session/* key 建立了硬限制:

  • 单条 session 快照最大 1 MiB

限制覆盖:

  • 写入时校验
  • 读取时校验

超限行为:

  • 写入失败,抛出结构化存储错误
  • 读取失败时,SessionManager 会清理超限的单个 session key,而不是清空整库

相关文件:

  • packages/core/src/services/preference/service.ts
  • packages/ui/src/stores/session/useSessionManager.ts

2. Session 图片库配额

PromptOptimizerImageDB 默认配额:

  • maxCacheSize = 50 MB
  • maxAge = 7 天
  • maxCount = 100
  • autoCleanupThreshold = 0.8

清理顺序:

  1. 清理过期图片
  2. 超过数量上限时按最旧访问时间删除
  3. 超过总容量时继续按最旧访问时间删除,直到降到目标阈值

相关文件:

  • packages/core/src/services/image/storage.ts
  • packages/ui/src/composables/system/useAppInitializer.ts

3. Favorite 图片库配额

PromptOptimizerFavoriteImageDB 默认配额:

  • maxCacheSize = 200 MB
  • maxAge = 365 天
  • maxCount = 1000
  • autoCleanupThreshold = 0.9

这套配置比 session 图片库更宽松,因为收藏被视为长期保留资产。

相关文件:

  • packages/core/src/services/image/storage.ts
  • packages/ui/src/composables/system/useAppInitializer.ts

4. 历史记录限制

prompt_history 最多保留:

  • 50 条记录

相关文件:

  • packages/core/src/services/history/manager.ts

5. Desktop 文件存储运行特性

Desktop 主存储没有应用层固定大小上限,但有运行时写盘策略:

  • 延迟写入:500ms
  • 最大 flush 超时:3s
  • 自动备份恢复:主文件失败时尝试 backup

这意味着 Desktop 不是“无限安全”,只是没有像 session 那样的显式 size cap。

相关文件:

  • packages/core/src/services/storage/fileStorageProvider.ts

6. LocalStorage provider 能力上限

LocalStorageProvider 报告的能力上限约为:

  • 5MB

但它不是当前主线路径。

相关文件:

  • packages/core/src/services/storage/localStorageProvider.ts

🔄 五、导入导出边界

当前 DataManager.exportAllData() 导出的不是整套运行态存储,而是一个经过裁剪的业务数据集合。

当前导出包含:

  • history
  • models
  • userTemplates
  • userSettings
  • contexts

当前不包含:

  • favorites
  • favorite image assets
  • session snapshots
  • session image assets

因此必须明确:

  • 导入导出边界 != 运行时存储边界
  • 当前导入导出不能视为完整灾备备份

相关文件:

  • packages/core/src/services/data/manager.ts
  • packages/core/src/services/preference/service.ts

🚨 六、当前已经建立的防线

1. Session 超限防线

  • session/* 超过 1 MiB 直接拒绝
  • 恢复时只清理超限单 key,不再整库清空

2. Favorite inline 图片防线

  • FavoriteManager 现在拒绝 data:image/... inline data URL 进入 metadata

3. Prompt Garden 收藏严格落盘策略

  • 图片资产落盘失败时不再用 inline 图片兜底

4. Session 图片与结构化快照分层

  • 图片二进制进入独立图片库
  • session 快照只保留引用

⚠️ 七、当前仍存在的灰区和风险

1. 结构化主存储物理上仍是共享桶

无论在 Web 还是 Desktop,本质上都还是单一主存储容器:

  • Web 是一张 Dexie 表
  • Desktop 是一个 JSON 文件

这意味着只要某条链路把大对象写错位置,影响范围就不是单个业务模块,而是整个主存储。

2. Favorites 不经过 PreferenceService

favorites 是直接 key,不走 PreferenceService,因此:

  • session 的 1 MiB 防线不覆盖 favorites
  • favorites 必须在自身 manager 和调用方建立独立边界

3. Favorite 图片资产目前更像“配额库”,不是“强引用回收库”

已确认:

  • session 图片库会根据 session 快照引用做 GC
  • 收藏删除链路当前只删除 favorites 文本记录
  • 没有看到收藏删除后同步清理 PromptOptimizerFavoriteImageDB 孤儿资产的强引用回收逻辑

这意味着当前 favorite 图片库仍存在孤儿资产积累风险,更多依赖配额清理,而不是引用级删除。

相关文件:

  • packages/ui/src/stores/session/imageStorageMaintenance.ts
  • packages/ui/src/components/FavoriteButton.vue
  • packages/ui/src/components/FavoriteManager.vue
  • packages/core/src/services/favorite/manager.ts

4. UI 设置仍处于混合态

当前存在:

  • 新的 global-settings/v1
  • 旧的 app:settings:ui:*
  • 路由真源与持久化镜像并存

这不一定立即导致数据损坏,但会提高认知成本,也会增加后续修改时误写双真源的风险。

5. 导出数据不覆盖图片资产

当前导出文件不能还原:

  • 收藏图片库
  • session 图片库

这也是为什么“运行时数据完整性”和“导出文件完整性”必须分开讨论。

🛡️ 八、必须遵守的存储红线

后续所有功能开发必须遵守以下规则:

红线 1:结构化主存储禁止承载大体积二进制内容

禁止把以下内容直接写入主存储:

  • 图片 base64
  • data URL
  • 大型二进制序列化字符串

红线 2:图片只能进入图片资产库

所有图片相关能力都必须遵循:

  • 先落图片资产库
  • 再把 assetId 写入业务对象

红线 3:任何 fallback 都不能回退到 inline 图片

允许的 fallback:

  • 外部 URL
  • 跳过保存
  • 显式失败

禁止的 fallback:

  • data:image/... 写回 favorites 或 session metadata

红线 4:删除业务对象必须考虑资产回收

如果一个对象持有图片 assetId,那么删除该对象时必须明确以下策略之一:

  • 立即删除资产
  • 标记引用减少后异步 GC
  • 周期性按引用扫描清理

不能只删文本记录,不考虑资产生命周期。

红线 5:provider 级测试必须覆盖真实请求体和真实落盘结果

仅验证“调用链路经过某个 adapter”是不够的。

必须补齐:

  • provider 级 payload 断言
  • 存储层实际写入结果断言
  • 大对象拒绝与回退行为断言

🧭 九、建议的后续治理方向

1. Favorite 图片库补引用级 GC

目标:

  • 删除收藏时同步处理收藏图片资产
  • 或建立 favorites 专属的引用扫描 + 清理任务

2. 为 favorites 建立独立 size guard

建议:

  • 不依赖 PreferenceService
  • FavoriteManager 或更底层 provider 边界增加单项和总量限制

3. 收口 UI 设置双真源

建议逐步统一:

  • 哪些由路由主导
  • 哪些由 global-settings/v1 主导
  • 哪些旧 key 只保留迁移读,不再写入

4. 补齐“结构化存储禁止 inline 图片”的系统性测试

建议至少覆盖:

  • session
  • favorites
  • Prompt Garden import
  • SaveFavoriteDialog
  • 未来任何带图片快照的新功能

✅ 十、结论

当前系统的真实运行时存储结构可以总结为:

  1. 结构化主存储

    • 保存设置、会话快照、模型、模板、历史、上下文、收藏文本等结构化数据。
  2. Session 图片资产库

    • 保存会话期间使用的图片字节。
  3. Favorite 图片资产库

    • 保存收藏相关的图片资产。

当前已经建立了两条关键防线:

  • session/*1 MiB 硬限制
  • favorites metadata 禁止 inline 图片 data URL

但要彻底避免同类事故再次发生,还需要继续补齐:

  • favorite 图片资产回收
  • favorites 的独立容量防线
  • 旧 UI key 的收口
  • provider / storage 边界的强测试

只有当“结构化数据”和“图片资产”在架构、代码、测试、删除回收四个层面都被强制分层后,这类问题才算真正从根上解决。