11 KiB
NCurator-Desktop 数据库架构
本文说明项目的本地存储边界、SQLite 与 LanceDB 的职责关系,以及索引一致性规则。具体表字段和查询细节以 migration、Store 和测试为准。
总体原则
- SQLite 是业务真相,文件位于
userData/app.sqlite。 - LanceDB 是可重建索引,目录位于
userData/lancedb,只保存vector_chunk。 - SQLite schema 通过 migration 向前演进;LanceDB 索引按 SQLite 中的 document generation 重建。
- 同一业务状态不能同时以 SQLite 和 LanceDB 为真相。
SQLite
SQLite 使用 better-sqlite3。启动时 SqliteCore 负责:
- 打开
app.sqlite。 - 设置
PRAGMA foreign_keys = ON。 - 设置
PRAGMA busy_timeout = 5000。 - 在任何迁移写入前读取并校验
schema_migrations。 - 数据库版本兼容时设置
PRAGMA journal_mode = WAL,并执行全部待执行 migration。
SQLite 保存以下业务数据:
- 会话、消息,以及用于中断恢复和压缩投影的 append-only 会话消息日志;不保存独立恢复快照。
- AI run、审批和文件修改记录。
- 会话级 append-only 的消息转录(
chat_session_message_log),作为模型上下文的唯一真相源。 - 搜索模式对话轮次(
chat_search_turn),与 AI log 解耦;Workbench 打开会话时合并投影。 - 消息中的图片只保存运行时路径或 HTTP(S) 地址,以及 MIME、字节数和尺寸;SQLite 不保存 Base64,也不为临时图片建立独立文件表。运行时文件被清理后允许恢复为不可用占位。
- 工作区、文件索引任务、文档元数据和网页源。
- LLM 配置和 AI runtime 设置。
- AI runtime 设置中的
systemNotificationsEnabled,默认值为1,控制 AI 运行终态系统通知。 - 文件内容和预览 artifact 的 SHA-256 对象引用;实际对象保存在工作区
.ncurator/objects/sha256,相同内容只存一份。 - Workbench 会话 UI 快照、最后活动会话、未发送草稿和内置浏览器标签状态。
关键规则:
chat_search_turn使用会话内seq保证搜索轮次稳定顺序。chat_session_message_log使用session_id + seq保证会话内消息转录的稳定顺序(user/assistant/tool/annotation条目);plan_updated、brain_user_snapshot和compaction_checkpoint是强类型 annotation。恢复投影读取记录自身的seq/run_id,不会修改历史行;历史 build user 必须来自brain_user_snapshot冻结原文。ai_run.partial_answer与partial_answer_role只保存当前流式 UI 草稿;它们不参与下一次模型上下文恢复。failure_code保存process_interrupted、output_truncated、input_too_long等终止原因。- 待审批状态以 SQLite 中的 run 记录为准。
document.active_generation_id是当前有效索引版本。
Workbench 会话 UI 状态
chat_session_ui_state以session_id为主键保存版本化state_json,会话删除时通过外键级联删除。workbench_ui_state是id = 1的单例,保存最后活动会话和未发送的__draft__UI 快照。browser_tab_state以owner_key隔离真实会话或__draft__的标签,保存 URL、标题、缩放、导航历史、顺序和更新时间。- 首次发送创建真实会话时,创建会话、复制草稿 UI 快照、清空草稿和转移浏览器标签在同一个 SQLite transaction 中完成。
- Renderer 内存只负责当前交互,SQLite 是重启恢复的真相;浏览器
WebContentsView只负责运行期页面实例,不是持久化真相。
SQLite Migration
当前边界
- 当前代码支持的最终 schema version 是
10,由CURRENT_SCHEMA_VERSION定义。 - version
1–9已随正式版发布,永久保留且不可改写。已知历史例外只有 version 2 的旧名称workspace_change_sets;version 9 继续负责修复通知字段。 - version
10把上次正式版之后尚未发布的变更合并为一次upgrade_from_v9:创建最终版定时任务和消息日志表,删除chat_conversation_memory、chat_session_resume_snapshot、ai_run_event与废弃字段,并为ai_run增加失败原因及流式 UI 草稿角色字段;同时删除chat_session_skill_state与chat_message,新增chat_search_turn。 - 哪些 migration 已正式发布,以
migrations/released-migrations.json为唯一边界。未进入清单的连续迁移可以在发布前合并或重写;进入清单后只能新增更高版本修复。 - 当前开发机若已经执行过被合并掉的旧 version
10–14,直接删除开发环境的app.sqlite和lancedb后重建,不提供兼容或修复路径。
版本身份与执行规则
- migration 记录在
schema_migrations,身份由version + name共同确定,不能只看最大版本号。 - migration 定义统一放在
src/main/services/storage/sqlite/migrations/00X_*.ts。版本必须从 1 开始连续、严格递增且唯一;文件版本、定义版本和执行顺序必须一致。 - 已执行记录必须是当前 migration 列表的合法连续前缀。除明确登记的历史别名外,缺号、名称冲突或未知记录都视为数据库历史损坏,拒绝启动。
- 数据库版本高于
CURRENT_SCHEMA_VERSION时,在写 migration、切换 WAL 或初始化业务服务前抛出DatabaseVersionTooNewError,错误同时携带检测版本和支持版本。 - 一次启动中的全部待执行 migration、对应的
schema_migrations记录、PRAGMA quick_check和PRAGMA foreign_key_check位于同一个 SQLite transaction。任一步失败,整批升级回滚,数据库不会停留在半升级状态。 - migration 必须可重复初始化:已经完整执行的版本不会再次执行。不提供 down migration、自动数据库复制、自动备份或自动恢复。
启动状态机与禁止降级
应用先初始化错误日志与 AppUpdateService,再检查 SQLite:
- 数据库兼容且迁移成功:启动状态为
{ mode: 'normal' },继续初始化 Storage、LanceDB、AI、Workbench、定时任务和业务 IPC。 - 数据库版本高于当前应用支持版本:启动状态为
{ mode: 'update_required', databaseVersion, supportedDatabaseVersion, downloadPageUrl },只创建更新恢复页,不初始化任何业务服务。downloadPageUrl指向官网中文下载页,由官网提供平台选择和安装说明,不在恢复页维护安装包直链。 - 其他迁移错误:按启动错误处理并保留错误日志,不能把 schema 损坏误判成可更新状态。
恢复页会自动检查更新,只允许检查、下载、重启安装、打开官网中文下载页或退出。更新不可用、网络失败或用户尚未完成升级时,都不能进入 Workbench。官方最新版本必须支持所有已公开发布的数据库版本。
Windows 从本方案发布的 NSIS 安装包开始,会在卸载或写文件前读取 HKCU/HKLM 已安装版本:允许更高版本安装和同版本修复,拒绝低版本安装包。macOS DMG 无法阻止手动用旧 .app 覆盖,由上述运行时版本检查兜底。历史上已经发布的旧安装包无法追溯增加安装器保护。
DDL 与数据安全规则
- 新增普通字段优先使用
ALTER TABLE ... ADD COLUMN。 - 删除无索引、外键、唯一约束等依赖的普通字段,可以直接使用 SQLite
DROP COLUMN。 - 修改类型、主键、唯一约束、外键,或进行复杂数据转换时,使用“建新表 → 复制并校验 → 替换旧表”。
- 新增的正式 schema 迁移不使用宽泛的
IF EXISTS/IF NOT EXISTS掩盖结构异常;预期表或字段不存在时应让事务失败并暴露问题。已发布的 version 1–9 保留历史源码(包括 version 8 的DROP TABLE IF EXISTS),不为满足新规则而改写;该历史例外由迁移锁清单固定。 - 启动迁移应保持可控时长。确实需要处理大量数据时,应先设计可恢复的产品流程,不能把长时间分批任务偷偷塞进启动 migration。
- 本项目不做自动数据库备份。备份无法替代版本兼容和原子事务,而且应用无法替用户判断何时恢复。对“SQL 本身错误但成功提交”的风险,通过已发布迁移冻结、正式 v9 fixture 升级测试、完整性检查和发布门禁控制。
发布锁与测试门禁
released-migrations.json保存已发布 migration 的 version、name、文件和源码 SHA-256。release:start在 Tag 前先验证所有已锁 migration 未被修改;Unit 全部通过后,把当前连续 migration 纳入锁清单,再与版本号一起提交。任一已锁源码变化都会直接阻止发布。- 独立迁移 Unit 必须覆盖:空库到当前版本、正式 v9 带数据升级、两条路径最终 schema 一致、重复初始化、批量失败整批回滚、高版本零写入拒绝、历史缺号/重名/名称冲突、已知别名、锁文件篡改以及两项 SQLite 完整性检查。
database-version-update-requiredE2E 使用隔离userData写入高版本 migration 记录并重启,验证只出现恢复页、Workbench 不启动、更新全流程、中文失败提示和官网下载入口仍可用。这个 fixture 只准备启动前数据库,不改生产 schema 检查逻辑。
Native 依赖
better-sqlite3 是 native 模块,Node backend 测试和 Electron e2e 使用不同 ABI。项目通过独立缓存保存两套 binding,不覆盖 node_modules 中的同一个 .node 文件。
测试 runner 和打包流程会自动准备并注入正确 binding,正常开发与回归不手工 rebuild。
LanceDB
LanceDB 只创建和使用 vector_chunk 表。
vector_chunk 保存 chunk、document、workspace、source type、generation、顺序、正文、文档名、metadata、创建时间和 embedding vector。
允许操作:
- 批量写入 chunks。
- 向量搜索和全文搜索 chunks。
- 按
document_id / workspace_id / generation_id删除 chunks。 - 对向量表执行 optimize。
禁止操作:
- 高频业务 update。
- 业务状态 mergeInsert。
- 用 LanceDB 保存会话、消息、run、审批、网页源、工作区状态或配置。
SQLite 与 LanceDB 的关系
SQLite 决定什么数据有效,LanceDB 只负责在有效范围内检索内容。
搜索流程:
- 从 SQLite 读取当前 workspace 和 document 元数据。
- 确认当前有效的
active_generation_id。 - 到 LanceDB 搜索
workspace_id + source_type + generation_id。 - 返回命中的 chunk 和来源信息。
搜索结果不能返回非 active generation 的 chunks。
Generation 机制
文件和网页索引先写入新 generation_id,成功后由 SQLite transaction 切换 active_generation_id,再清理旧 chunks;失败时不切换,因此已有检索结果仍可用。
Fragment 和 Optimize
LanceDB fragment 是一次或多次写入形成的数据片段,不等于一行数据。
维护规则:
- chunk 必须批量写入。
- 避免大量小 batch 写入。
- 大量写入或删除后可以执行 optimize。
- optimize 只用于整理向量索引文件,不承担业务 migration。
- 不允许启动时自动删表重建。
性能约束
- 会话首屏只读取必要数据,不回放完整 run/event。
- 完整 run/event replay、context usage、file change detail 不进入切换首屏关键路径。
- chunk 写入必须批量进行。
- 慢查询优先检查 generation 过滤、workspace 过滤、source type 过滤和 fragment 数量。
- 不用缓存掩盖错误的数据边界。