- Introduced unit tests for partner digest coordinator to validate run completion and digest handling. - Enhanced chat run service contract tests to ensure proper handling of pending digests during retraction. - Updated llm presets contract tests to reflect changes in preset provider options and model handling. - Improved workbench model selection tests to ensure correct behavior with preset catalogs. - Added checks for app update publish configuration to include preset models catalog URL. - Refactored e2e tests for workbench result application to streamline visibility checks. - Updated harness backend tests to utilize UUID for run IDs and improve context handling.
15 KiB
NCurator-Desktop 数据库架构
本文说明项目的本地存储边界、SQLite 与 LanceDB 的职责关系,以及索引一致性规则。具体表字段和查询细节以 migration、Store 和测试为准。
总体原则
- SQLite 是业务真相,文件位于
userData/app.sqlite。 - LanceDB 是可重建索引,目录位于
userData/lancedb,只保存vector_chunk。 - SQLite schema 通过 migration 向前演进;LanceDB 按文档 generation 写入后删除旧 chunk,检索时库内只留最新数据。
- 同一业务状态不能同时以 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 保存以下业务数据:
- 伙伴(
partner)以及用于中断恢复、压缩投影的 append-only 伙伴消息记录(chat_session_message_log,表名保留,行挂partner_id);不保存独立恢复快照。 - AI run、审批和文件修改记录(均挂
partner_id)。不存在chat_search_turn,Composer 已删除搜索模式。 - 消息中的图片只保存运行时路径或 HTTP(S) 地址,以及 MIME、字节数和尺寸;SQLite 不保存 Base64,也不为临时图片建立独立文件表。运行时文件被清理后允许恢复为不可用占位。
- 工作地址(
workspace_source,无 name,root_path UNIQUE)、文件索引任务、文档元数据和全局网页源(web_source,无workspace_id,UNIQUE(normalized_url))。 - LLM 配置和 AI runtime 设置。
- AI runtime 设置中的
systemNotificationsEnabled,默认值为1,控制 AI 运行终态系统通知。 - 文件内容和预览 artifact 的 SHA-256 对象引用;实际对象保存在
userData/partners/{id}/objects/sha256,相同内容只存一份。 - Workbench 伙伴 UI 快照(
partner_ui_state)、当前活动伙伴(workbench_ui_state.active_view_key)和内置浏览器标签状态(browser_tab_state.owner_key= partner_id)。 - 定时任务(
scheduled_task.partner_id绑定专属伙伴,保存时可为空,到点或立即执行时再创建并回写;无外键 CASCADE;busy_cancel_notice_at记录因占用取消的待展示提示)。
关键规则:
partner保存名称、头像外形与颜色,以及pinned/pinned_at置顶状态;列表按置顶优先、置顶时间、更新时间、创建时间排序。标题就是伙伴name,不因消息改名。chat_session_message_log使用partner_id + seq保证伙伴内消息转录的稳定顺序(user/assistant/tool/annotation条目);plan_updated、compaction_checkpoint、run_digest、context_scope_decided和discard_checkpoint是强类型 annotation。compaction_checkpoint与discard_checkpoint都用throughSeq记下到哪一条为止的原文不再发给主模型。run_digest先写pending,后台再追加completed(一句话摘要 +fileChangePaths)或failed。context_scope_decided每轮记录是否带历史(模型判定时带一句话reason);needsHistory=false时另写discard_checkpoint(含conversationHistory)。user.content是发给模型的 build 全文,user.input是人打的字。已经丢掉或已经压进摘要的原文不再发给主模型,再按书签合成 Compact User 或 Discard User。气泡文案用ai_run.input。用户「清空消息记录」会物理删除该伙伴全部 message log 行及对应ai_run,不删除ai_file_change。编辑最新轮会取消该轮 digest 并删除该runId的 run 与全部 log。ai_run挂partner_id,不再保存workspace_json。partial_answer与partial_answer_role只保存当前流式 UI 草稿;它们不参与下一次模型上下文恢复。failure_code保存互斥终止原因(llm_request_failed/process_interrupted/output_truncated/input_too_long/runtime_limit/internal_error/unexpected_error等);failure_raw_message保存失败原文供 UI「查看详情」。- 待审批状态以 SQLite 中的 run 记录为准。
document.active_generation_id是当前有效索引版本,只用于写入后删除旧 chunk 和启动/任务结束时的孤儿清理,不进入检索where。
Workbench 伙伴 UI 状态
partner_ui_state以partner_id为主键保存版本化state_json,伙伴删除时通过外键级联删除。workbench_ui_state是id = 1的单例,只保存active_view_key(当前伙伴 id);不再保存draft_state_json或__draft__。browser_tab_state以owner_key隔离伙伴标签(owner_key= partner_id),保存 URL、标题、缩放、导航历史、顺序和更新时间。- 伙伴在创建弹窗提交时即落库,不存在草稿会话提升事务。
- 删除伙伴级联删除 message log、
ai_run、ai_file_change、docx_apply、partner_ui_state,并清除browser_tab_state、将active_view_key置空,同时删除userData/partners/{id}。定时任务不级联删除:专属伙伴被删后任务仍保留,到点或立即执行时由服务自动重建伙伴。 - 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从 v9 升级时 DROP 旧表再建最终结构:新增partner;删除chat_session、chat_session_ui_state(以及chat_conversation_memory、chat_session_resume_snapshot、ai_run_event、chat_session_skill_state、chat_message);chat_session_message_log/ai_run/ai_file_change/docx_apply改挂partner_id;新增partner_ui_state;workbench_ui_state只留active_view_key,删除draft_state_json;scheduled_task改挂partner_id(无外键 CASCADE,专属伙伴删除后任务可保留并由服务重建),并含busy_cancel_notice_at;workspace_source去掉 name 且root_path UNIQUE;web_source去掉workspace_id且UNIQUE(normalized_url);ai_run不再保存workspace_json,并含failure_raw_message(失败原文,与短标题error/failure_code分离)。不存在、也不新建chat_search_turn。新增schema_migration_hooks,供版本声明提交后副作用。v10 的afterCommit在 SQLite 事务提交后、LanceDB 连接前删除userData/lancedb;hook 记pending/done,失败可重试,done后不重跑。空库仍执行 version1–10,跑完后结构与 v9 升级路径一致。 - 哪些 migration 已正式发布,以
migrations/released-migrations.json为唯一边界。未进入清单的连续迁移可以在发布前合并或重写;进入清单后只能新增更高版本修复。
版本身份与执行规则
- 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。任一步失败,整批升级回滚,数据库不会停留在半升级状态。声明了afterCommit的版本在同一事务中写入schema_migration_hooks(status=pending);事务提交后、LanceDB 连接前执行这些 hook,成功后标done。afterCommit不在 SQLite 事务里,不得回写 schema。 - migration 必须可重复初始化:已经完整执行的版本不会再次执行。
afterCommit只在 hook 仍为pending时重试。不提供 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 中应只留下最新 chunk;搜索按 document_id / source_type / workspace_id 收窄,不再加 generation_id。
搜索流程:
- 若设置页正在「更新索引」(
workspace_index_job.action = prepare且running),kb_search/kb_list_documents直接失败。 - 否则到 LanceDB 搜索当前表中的 chunk。
- 用 SQLite document 元数据补来源路径或 URL。
「检查更新」只扫目录或网页变化,不写向量,不拦截检索。
Generation 机制
文件和网页索引先写入新 generation_id,成功后切换 SQLite active_generation_id 并 await 删除旧 generation。失败则删除刚写入的新 generation,保留旧 chunk。启动恢复和索引任务结束后会清理非当前 generation 以及没有对应 document 的孤儿 chunk。generation 只用于这次删除,不用于检索过滤。
Fragment 和 Optimize
LanceDB fragment 是一次或多次写入形成的数据片段,不等于一行数据。
维护规则:
- chunk 必须批量写入。
- 避免大量小 batch 写入。
- 大量写入或删除后可以执行 optimize。
- optimize 只用于整理向量索引文件,不承担业务 migration。
- 不允许启动时自动删表重建。
性能约束
- 伙伴首屏只读取必要数据,不回放完整 run/event。
- 完整 run/event replay、context usage、file change detail 不进入切换首屏关键路径。
- chunk 写入必须批量进行。
- 慢查询优先检查 workspace 过滤、source type 过滤、documentIds 过滤和 fragment 数量。
- 不用缓存掩盖错误的数据边界。