- Updated AiRunStatus to include 'waiting_approval'. - Introduced ChatSubmitMode and ChatSubmitRequest types for chat submissions. - Added AiRunRecord type to track AI run details including approval status and artifacts. - Implemented ChatSessionRunSnapshot to encapsulate run and event data. - Enhanced ChatSubmitResponse to include additional response fields. - Added tests for chat run service persistence and navigation snapshot replay. - Improved error handling and assertions in existing tests for clarity and robustness.
25 KiB
NCurator-Desktop 数据库设计
存储与检索
- 存储引擎: LanceDB v0.23
- 全文检索: FTS 基于
text列,Tokenizer 使用 ngram (min:2, max:3) - 数据库存储路径: Electron
app.getPath('userData')/lancedb(见src/main/utils/paths.ts的LANCE_DB_PATH)
代码封装与表操作入口
本项目的数据库访问已做分层封装,避免业务代码直接依赖 LanceDB 连接/表细节。
分层结构(Main Process)
- 连接与表初始化(Core):
src/main/services/storage/core/LanceDbCore.ts- 负责连接创建、建表、索引创建、表打开、SQL 字符串转义、常用 where 拼装
- 表名常量:
LANCE_TABLES - 表结构与索引“代码层真相来源”:
getTableConfigs()
- 领域表操作(Domains):
src/main/services/storage/domains/*Store.tsDocumentsStore:document/chunk(导入、分页、混合检索、删除)WebSourceStore:web_source(网页资料来源 identity、网页爬取/索引状态与来源级元数据)WorkspaceStore:workspace_source/workspace_file_index(具名工作空间、可选文件目录绑定、文件索引状态、摘要统计)WorkspaceIndexJobStore:workspace_index_job(文件/网页检查与索引批次进度)ChatStore:chat_session/chat_message/chat_conversation_memory/chat_session_resume_snapshot(会话、消息、记忆与 human 恢复快照 CRUD)AiRunStore:ai_run/ai_run_event(Workbench AI run 状态、事件日志与 snapshot replay 数据)AiFileChangeStore:ai_file_change(AI 本轮实际修改文件记录、统一修改列表与跨能力回退状态)DocxApplyStore:docx_apply(docx 直接写回记录、checkpoint 与回退状态)LlmConfigStore:llm_config(配置 CRUD 与激活态切换)
- 组合根(Facade):
src/main/services/storage/StorageService.ts- 统一持有
core与各 domain store - 应用启动时在
src/main/index.ts调用await storageService.initialize() - 初始化阶段会执行一次旧本地导入清理迁移:清理未绑定当前工作空间文件索引记录的历史本地 file documents
- 统一持有
约束与最佳实践
- 不要在业务逻辑里直接
lancedb.connect/openTable/createIndex;新增/修改数据库行为优先在LanceDbCore或对应*Store内完成。 - where 条件中拼接外部输入时必须转义:使用
LanceDbCore.escapeSqlString();IN条件优先用buildInClause()。 - query 搜索条件优先复用:
LanceDbCore.buildWhereFromQuery(query, fields)。- 该方法会在检测到“像 SQL 的输入”时直接返回原字符串,用于内部高级筛选;不要把不可信输入当作 SQL 传入。
- 新增表/索引的入口:
- 在
LanceDbCore.ts的LANCE_TABLES与getTableConfigs()增加配置 - 在
domains/新增或扩展 store,对外提供领域方法
- 在
表定义
表: chunk(文档切片表)
存储文档被切分后的片段及其向量表示。
| 字段名 | 类型 | 描述 |
|---|---|---|
| vector | FixedSizeList (768, Float32) | 文本向量 (Embedding) |
| text | Utf8 | 切片文本内容(建立 FTS 索引) |
| id | Utf8 | 切片唯一标识 |
| document_id | Utf8 | 关联的文档 ID |
| workspace_id | Utf8 | 关联的工作区 ID |
| document_name | Utf8 | 文档名称快照 |
| source_type | Utf8 | 来源类型(如 'file', 'web') |
| metadata | Struct | 元数据(如页码等) |
| created_at | Int64 | 创建时间戳 |
索引配置:
- FTS: 列
text,Tokenizer: ngram,min:2,max:3
表: document(文档元数据表)
存储原始文档的信息。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 文档唯一标识 |
| name | Utf8 | 文件名 |
| source_type | Utf8 | 来源类型(如 'file', 'web') |
| workspace_id | Utf8(Nullable) | 所属工作区 ID |
| file_path | Utf8(Nullable) | 原始文件路径 |
| created_at | Int64 | 创建时间戳 |
| import_status | Int32 | 导入状态(1 导入中,2 导入成功,3 导入失败) |
导入状态取值:
- 1: 导入中
- 2: 导入成功
- 3: 导入失败
说明:
source_type='file'的 document 仅表示“已准备的本地文件索引文档”,其真相来源必须可由workspace_file_index.document_id反向找到。source_type='web'的 document 由web_source.document_id绑定到网页资料来源。- 当前实现要求本地文档与网页文档都归属于某个工作区,KB 默认检索直接按
workspace_id收口。
表: web_source(网页资料来源表)
存储网页资料的来源 identity、网页爬取/索引状态和范围选择元数据。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 网页来源唯一标识 |
| workspace_id | Utf8 | 所属工作区 ID |
| identity_kind | Utf8 | 当前固定为 normalized_url |
| normalized_url | Utf8 | URL 规范化后的 identity |
| url | Utf8 | 原始/最新 URL |
| title | Utf8(Nullable) | 最近一次抓取到的标题 |
| document_id | Utf8(Nullable) | 关联的已准备 document |
| index_status | Utf8 | 网页状态:needs_index / crawling / crawl_failed / indexing / indexed / index_failed |
| include_selectors_json | Utf8(Nullable) | 抓取 include selectors 的 JSON 字符串 |
| exclude_selectors_json | Utf8(Nullable) | 抓取 exclude selectors 的 JSON 字符串 |
| fetched_at | Int64(Nullable) | 最近抓取完成时间 |
| last_checked_at | Int64(Nullable) | 最近一次检查网页更新的时间 |
| last_crawled_at | Int64(Nullable) | 最近一次成功抓取正文的时间 |
| last_indexed_at | Int64(Nullable) | 最近一次成功完成网页索引的时间 |
| etag | Utf8(Nullable) | 最近一次检查时记录的 ETag |
| last_modified | Utf8(Nullable) | 最近一次检查时记录的 Last-Modified |
| content_length | Int64(Nullable) | 最近一次检查时记录的内容长度 |
| content_hash | Utf8(Nullable) | 最近一次检查到的正文签名 |
| last_error | Utf8(Nullable) | 最近一次抓取错误 |
| created_at | Int64 | 创建时间戳 |
| updated_at | Int64 | 更新时间戳 |
说明:
web_source不再是全局资源,而是工作区内资源。- 同一
normalized_url允许在不同工作区分别存在,各自维护抓取状态与document_id绑定。
表: workspace_source(工作空间容器表)
存储用户配置的具名工作空间,以及该容器当前绑定的可选本地目录。文件数、网页数与索引摘要全部由其他表实时派生,不再缓存在本表。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 工作空间唯一标识 |
| name | Utf8 | 显示名称 |
| root_path | Utf8(Nullable) | 当前工作空间绑定的本地文件目录 |
| created_at | Int64 | 创建时间戳 |
| updated_at | Int64 | 更新时间戳 |
说明:
workspace_source是纯工作空间容器记录,不承担文件摘要、网页数量或索引状态缓存。- 用户可先创建具名工作空间,再在文件视图内单独设置或替换
root_path。
表: workspace_file_index(工作空间文件索引状态表)
存储工作空间下支持索引文件的持久化索引状态;目录树本身运行时直接读取真实文件系统。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 索引记录唯一标识 |
| workspace_id | Utf8 | 所属工作空间 |
| relative_path | Utf8 | 相对工作空间根路径 |
| absolute_path | Utf8 | 最近一次检查到的绝对路径 |
| name | Utf8 | 文件名 |
| file_ext | Utf8 | 文件扩展名 |
| size | Int64(Nullable) | 文件大小 |
| mtime_ms | Int64(Nullable) | 文件修改时间 |
| content_hash | Utf8(Nullable) | 最近一次成功索引对应的内容哈希 |
| index_status | Utf8 | needs_index / indexing / indexed / failed |
| document_id | Utf8(Nullable) | 关联的已准备 document ID |
| indexed_at | Int64(Nullable) | 最近一次索引完成时间 |
| last_checked_at | Int64(Nullable) | 最近一次检查目录变化时间 |
| last_error | Utf8(Nullable) | 最近一次文件级索引错误 |
| created_at | Int64 | 创建时间戳 |
| updated_at | Int64 | 更新时间戳 |
表: workspace_index_job(工作空间索引批次任务表)
存储工作空间文件/网页检查与索引的持久化批次进度;文件/网页条目状态仍是资料级真相,workspace_index_job 只表示当前批次任务进度。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 任务唯一标识 |
| workspace_id | Utf8 | 所属工作空间 |
| source_kind | Utf8 | 资料类型:file / web |
| action | Utf8 | 动作:scan / prepare |
| status | Utf8 | 任务状态:running / succeeded / failed |
| phase | Utf8 | 当前阶段:scan / crawl / index / complete |
| step_index | Int64 | 当前步骤序号 |
| step_total | Int64 | 总步骤数 |
| total_count | Int64 | 当前阶段或任务总处理数量 |
| processed_count | Int64 | 当前阶段或任务已处理数量 |
| failed_count | Int64 | 当前阶段或任务失败数量 |
| target_ids_json | Utf8 | 本次任务目标文件索引记录 ID 或网页来源 ID 的 JSON 字符串 |
| message | Utf8(Nullable) | 当前进度文案 |
| last_error | Utf8(Nullable) | 最近失败原因 |
| started_at | Int64 | 任务开始时间戳 |
| updated_at | Int64 | 最近更新时间戳 |
| finished_at | Int64(Nullable) | 任务结束时间戳 |
说明:
- 同一
workspace_id + source_kind同时只允许一个running任务;重复启动时返回已有任务。 - 文件检查/索引和网页检查/索引执行中,页面通过该表恢复按钮禁用和进度条。
- 应用启动时会把遗留
running任务标记为failed,同时把文件indexing收敛为failed,网页crawling收敛为crawl_failed、indexing收敛为index_failed。
表: chat_session(对话会话表)
存储用户的对话会话列表。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 会话唯一标识 |
| title | Utf8 | 会话标题 |
| created_at | Int64 | 创建时间戳 |
| workspace_id | Utf8 | 会话绑定的工作区 ID |
表: chat_conversation_memory(会话记忆表)
存储会话级摘要记忆,用于运行时补充上下文。
| 字段名 | 类型 | 描述 |
|---|---|---|
| session_id | Utf8 | 关联的会话 ID |
| memory_json | Utf8 | 记忆 JSON 字符串 |
| updated_at | Int64 | 更新时间戳 |
表: chat_session_resume_snapshot(会话恢复快照表)
存储同一会话下最近一次可恢复中断的最小 human 语义快照,仅用于 timeout / network_error 后的自动恢复。
| 字段名 | 类型 | 描述 |
|---|---|---|
| session_id | Utf8 | 关联的会话 ID;同一会话只保留最新一条 |
| run_id | Utf8 | 触发快照的 run ID |
| reason | Utf8 | 失败原因,固定为 timeout 或 network_error |
| user_input | Utf8 | 上一次中断时的用户输入 |
| selected_references_json | Utf8 | 资料范围 JSON |
| workspace_json | Utf8(Nullable) | 工作区绑定 JSON |
| human_state_json | Utf8 | 仅包含恢复必需字段的最小 HumanState 片段 JSON |
| error_message | Utf8 | 最近失败信息 |
| updated_at | Int64 | 更新时间戳 |
表: chat_message(对话消息表)
存储会话中的每一条消息记录。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 消息唯一标识 |
| session_id | Utf8 | 关联的会话 ID |
| role | Utf8 | 角色(user / assistant / system) |
| content | Utf8 | 消息内容;AI 运行中作为 assistant 当前投影 |
| timestamp | Int64 | 消息时间戳 |
| selected_references_json | Utf8(Nullable) | 用户发送时选中的 @ 资料范围 JSON |
| artifacts_json | Utf8(Nullable) | assistant 关联的文件产物 JSON |
| error | Boolean(Nullable) | 是否出错 |
说明:
chat_message保存用户可见消息投影;AI 运行过程、审批等待和 replay 真相来源在ai_run/ai_run_event。- 搜索模式不创建
ai_run,只保存 user message 与 completed assistant summary message。
表: ai_run(AI 运行状态表)
存储 Workbench 智能模式每一轮 AI run 的持久化运行状态,以及 assistant message 的投影来源。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | run 唯一标识 |
| session_id | Utf8 | 所属会话 ID |
| user_message_id | Utf8 | 本轮用户消息 ID |
| assistant_message_id | Utf8 | 本轮 assistant 占位 / 投影消息 ID |
| status | Utf8 | running / waiting_approval / completed / failed / cancelled |
| input | Utf8 | 本轮用户输入 |
| selected_references_json | Utf8(Nullable) | 本轮选中的 @ 资料范围 JSON |
| workspace_json | Utf8(Nullable) | 本轮工作区绑定 JSON |
| model_name | Utf8(Nullable) | 本轮使用模型名 |
| partial_answer | Utf8 | 当前已收到的流式回答投影 |
| final_answer | Utf8(Nullable) | 最终回答 |
| error | Utf8(Nullable) | 失败或取消原因 |
| pending_approval_json | Utf8(Nullable) | 当前等待审批批次 JSON |
| artifacts_json | Utf8(Nullable) | 本轮文件产物 JSON |
| created_at | Int64 | 创建时间戳 |
| updated_at | Int64 | 最近更新时间戳 |
| finished_at | Int64(Nullable) | 结束时间戳 |
说明:
ChatRunService是ai_run的唯一写入编排入口;renderer 通过chatSubmit、chatSessionSnapshotGet和ai-run-event事件渲染投影。chatSessionSnapshotGet返回runs用于恢复当前会话可展示的 run 过程流与最终投影,返回activeRuns专门用于继续运行中 / 等待审批的交互状态;历史 replay 会过滤 token 级事件,最终文本以ai_run.partial_answer / final_answer为准。- 应用启动时会把数据库中遗留的
running / waiting_approvalrun 标记为failed,写入“运行已中断,请重新发送消息。”,并清空待审批状态;当前版本不恢复已退出进程的 AI 执行。 - 删除会话时由
ChatRunService同步清理该会话的ai_run与ai_run_event,避免遗留 orphan run。
表: ai_run_event(AI 运行事件日志表)
存储 AI run 的事件日志,用于 renderer 重新挂载、切路由返回或切会话时 replay 过程流。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 事件记录唯一标识 |
| run_id | Utf8 | 所属 run ID |
| session_id | Utf8 | 所属会话 ID |
| seq | Int64 | run 内递增事件序号 |
| type | Utf8 | 事件类型,取自 AiRunEvent.type |
| event_json | Utf8 | 完整事件 JSON |
| created_at | Int64 | 创建时间戳 |
说明:
- 每个 run event 先写入
ai_run_event,再更新ai_run/chat_message投影,最后广播给 renderer。 - run 内
seq由AiRunStore维护递增缓存;cache miss 时从历史事件恢复最大序号,避免流式 token 场景每次追加都全量扫描事件表。 - snapshot replay 只重建展示态,不触发实时 toast。
表: docx_apply(docx 直接写回记录表)
存储一次 AI 运行中某个 docx 文件的直接写回记录。写回前会保存当前磁盘文件 checkpoint,用于用户回退本次 AI 修改。
说明:docx_apply 是 docx 能力的细节表,保留 revision、diff 预览稿和 docx 专属 checkpoint 元数据;Workbench “修改的文件”列表和跨能力回退入口以 ai_file_change 为统一来源。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | AI 修改记录 ID |
| session_id | Utf8 | 所属会话 |
| run_id | Utf8 | 所属运行 |
| status | Utf8 | applied / reverted / failed |
| source_path | Utf8 | 已直接写回的源文件路径 |
| source_name | Utf8 | 源文件名 |
| operation | Utf8 | 实际写回语义,updated 表示覆盖现有文件,created 表示创建新文件 |
| final_revision_ref | Utf8 | 本次写回对应的任务内最终 revision |
| change_count | Int32 | 变更数量 |
| before_exists | Boolean | 写回前目标文件是否存在 |
| before_snapshot_path | Utf8(Nullable) | 写回前 checkpoint 快照路径 |
| before_checksum | Utf8(Nullable) | 写回前文件 checksum |
| before_byte_size | Int64(Nullable) | 写回前文件大小 |
| before_modified_at | Int64(Nullable) | 写回前修改时间 |
| preview_diff_artifact_path | Utf8 | 系统内 diff 预览稿路径 |
| preview_diff_checksum | Utf8 | 系统内 diff 预览稿 checksum |
| preview_diff_byte_size | Int64 | 系统内 diff 预览稿大小 |
| after_checksum | Utf8(Nullable) | 写回后文件 checksum |
| after_byte_size | Int64(Nullable) | 写回后文件大小 |
| after_modified_at | Int64(Nullable) | 写回后修改时间 |
| created_at | Int64 | 创建时间 |
| updated_at | Int64 | 更新时间 |
| reverted_at | Int64(Nullable) | 回退时间 |
| last_error | Utf8(Nullable) | 最近错误 |
表: ai_file_change(AI 修改文件统一记录表)
存储一次 AI run 中实际写入磁盘的文件记录,是 Workbench 修改文件列表、mouth 修改摘要和跨能力回退入口的统一来源。读过、检索过或仅被引用的文件不进入该表。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 修改记录 ID |
| session_id | Utf8 | 所属会话 |
| run_id | Utf8 | 所属运行 |
| status | Utf8 | applied / reverted / failed |
| capability | Utf8 | 写入来源能力,当前为 docx / terminal_exec / ppt |
| path | Utf8 | 实际被写入的文件路径 |
| file_name | Utf8 | 文件名 |
| operation | Utf8 | created / updated |
| change_count | Int32 | 变更数量;无法精确计数的能力先记录候选文件级数量 |
| before_snapshot_path | Utf8(Nullable) | 写入前 checkpoint 快照路径 |
| before_checksum | Utf8(Nullable) | 写入前 checksum |
| after_checksum | Utf8(Nullable) | 写入后 checksum |
| preview_path | Utf8(Nullable) | 可预览文件或 diff 预览稿路径 |
| created_at | Int64 | 创建时间 |
| updated_at | Int64 | 更新时间 |
| reverted_at | Int64(Nullable) | 回退时间 |
| last_error | Utf8(Nullable) | 最近错误 |
说明:
created回退时删除目标文件;updated回退时用before_snapshot_path原子替换目标文件。- 回退前默认校验当前文件 checksum 是否等于
after_checksum;不一致时 UI 必须提示继续回退会覆盖后续修改。 - docx apply 会同步写入
ai_file_change;docx 差异预览仍从docx_apply读取。 - terminal 写入只对声明或系统识别出的候选文件建立写前快照并记录,未识别的隐式副作用不进入该表,也不承诺可回退。
表: llm_config(大模型配置表)
存储用户配置的 LLM API 信息。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | Utf8 | 配置唯一标识 |
| providerKind | Utf8 | 服务商适配类型 |
| base_url | Utf8 | API Base URL |
| api_key | Utf8 | API Key |
| is_active | Boolean | 是否为当前激活配置(通常只有一个为 True) |
文档与代码冲突时的处理
- 文档未覆盖当前实现时,以
LanceDbCore的LANCE_TABLES/getTableConfigs()和各*Store为最终真相源。 - 修复数据库相关实现时,若发现文档缺项,应在同一变更中同步更新本文件。