ChatBuddy 存储文档
July 25, 2026 · View on GitHub
最后更新:2026-07-25
本文档描述 ChatBuddy 的数据持久化机制,包括 Compass 结构化存储格式、文件布局、迁移策略和备份恢复。
目录
存储演进
ChatBuddy 的存储经历了三代演进:
| 版本 | 存储方式 | 状态 | 说明 |
|---|---|---|---|
| v1 | VS Code globalState | 已废弃 | VS Code 的 Memento 存储,4MB 限制 |
| v2 | SQLite (sql.js) | 已废弃 | 单文件 SQLite 数据库,通过 sql.js 操作 |
| v3 | Compass 结构化存储 | 当前 | 多文件 JSON + JSONL 结构 |
Compass 存储架构
Compass 采用多文件结构化存储设计,将不同类型的数据分离到独立的文件中,解决了 VS Code globalState 的 4MB 限制问题。
设计原则
- 原子写入:所有写操作通过 "写临时文件 → 重命名" 两步完成,防止数据损坏
- 分离敏感数据:API Key 单独存储,与状态数据分离
- 向后兼容:支持从旧版 SQLite 自动迁移
- 可验证:每次加载时验证数据完整性
- 容错隔离:结构化文件与 API keys 文件写入独立 try-catch,一类文件失败不影响另一类
存储根目录
{ExtensionContext.globalStorageUri.fsPath}/
├── meta/ ← 元数据和状态文件
│ ├── state.core.json ← 核心状态(分组 + 助手)
│ ├── ui.selection.json ← UI 选择状态
│ ├── settings.general.json ← 通用设置
│ ├── settings.model-config.json ← 模型配置(Provider + 模型列表)
│ ├── settings.default-models.json ← 默认模型绑定
│ ├── settings.mcp.json ← MCP 设置
│ ├── providers.api-keys.json ← Provider API Key(敏感数据)
│ ├── kv.compass.json ← 键值存储(兼容性数据)
│ ├── state.commit.json ← 提交标记(验证用)
│ └── chatbuddy.migration.compass.json ← 迁移记录
│
├── images/ ← 图片文件存储(base64 数据)
│ └── {sessionId}_{messageId}_{index}.{ext} ← 单张图片文件
│
└── sessions/ ← 会话数据
├── index.compass.json ← 会话索引
└── {assistantId}/
└── {sessionId}.jsonl ← 会话消息(每行一个 JSON 对象)
文件布局
结构化状态文件
状态被拆分为 6 个独立的 JSON 文件:
state.core.json
包含应用的核心实体数据:
{
"groups": [
{
"id": "group_default",
"name": "默认",
"kind": "default",
"createdAt": 1713500000000,
"updatedAt": 1713500000000
}
],
"assistants": [
{
"id": "asst_xxx",
"name": "通用助手",
"groupId": "group_default",
"systemPrompt": "",
"greeting": "",
"questionPrefix": "",
"modelRef": "provider_xxx|model_xxx",
"temperature": 0.7,
"topP": 1,
"maxTokens": 4000,
"contextCount": 10,
"presencePenalty": 0,
"frequencyPenalty": 0,
"streaming": true,
"enabledMcpServerIds": [],
"pinned": false,
"isDeleted": false,
"createdAt": 1713500000000,
"updatedAt": 1713500000000,
"lastInteractedAt": 1713500000000
}
]
}
ui.selection.json
包含用户的 UI 选择状态:
{
"selectedAssistantId": "asst_xxx",
"selectedSessionIdByAssistant": {
"asst_xxx": "sess_xxx"
},
"sessionPanelCollapsed": false,
"collapsedGroupIds": []
}
settings.general.json
通用聊天设置:
{
"temperature": 0.7,
"topP": 1,
"maxTokens": 4000,
"presencePenalty": 0,
"frequencyPenalty": 0,
"timeoutMs": 0,
"streamingDefault": true,
"locale": "auto",
"sendShortcut": "enter",
"chatTabMode": "single"
}
settings.model-config.json
Provider 和模型配置(不含 API Key):
{
"providers": [
{
"id": "provider_xxx",
"kind": "openai",
"name": "OpenAI",
"apiKey": "",
"baseUrl": "https://api.openai.com/v1",
"apiType": "chat_completions",
"enabled": true,
"models": [
{
"id": "gpt-4o",
"name": "GPT-4o",
"source": "fetched"
}
]
}
]
}
settings.default-models.json
默认模型绑定:
{
"defaultModels": {
"assistant": { "providerId": "xxx", "modelId": "xxx" },
"titleSummary": { "providerId": "xxx", "modelId": "xxx" },
"titleSummaryPrompt": ""
}
}
settings.mcp.json
MCP 服务器配置:
{
"mcp": {
"servers": [
{
"id": "mcp_xxx",
"name": "文件系统",
"enabled": true,
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"cwd": "",
"env": [],
"url": "",
"headers": [],
"timeoutMs": 60000,
"remotePassthroughEnabled": false
}
],
"maxToolRounds": 10
}
}
会话存储
sessions/index.compass.json
会话索引文件,存储所有会话的元数据:
{
"sessions": [
{
"id": "sess_xxx",
"assistantId": "asst_xxx",
"title": "未命名会话",
"titleSource": "default",
"createdAt": 1713500000000,
"updatedAt": 1713500000000,
"messageCount": 5,
"preview": "最后一条消息预览..."
}
]
}
sessions/{assistantId}/{sessionId}.jsonl
会话消息以 JSON Lines 格式存储,每行一个消息对象:
{"id":"msg_1","role":"user","content":"你好","timestamp":1713500000000}
{"id":"msg_2","role":"assistant","content":"你好!有什么可以帮你的?","timestamp":1713500001000,"model":"gpt-4o"}
消息格式支持以下可选字段:
model— 响应该消息的模型 IDreasoning— 推理过程内容toolRounds— 工具调用回合(序列化为 JSON 字符串存储)images— 图片附件数组,每个元素包含base64(运行时填充)、mimeType和可选的path(持久化时 base64 被清空,仅保留path指向images/目录中的文件)files— 文件附件数组,每个元素包含name、content和可选的language
提交标记文件
state.commit.json
用于验证结构化状态完整性的标记文件:
{
"name": "compass-structured-state",
"layoutVersion": 3,
"generation": 42,
"writtenAt": "2026-04-21T10:00:00.000Z"
}
layoutVersion— 存储布局版本号(当前为 3)generation— 单调递增的写入世代号writtenAt— ISO 8601 时间戳
API Key 存储
providers.api-keys.json
Provider API Key 单独存储(与状态分离):
{
"provider_xxx": "sk-xxxxxxxx"
}
迁移记录
chatbuddy.migration.compass.json
记录存储迁移历史:
{
"name": "compass",
"layoutVersion": 3,
"source": "sqlite",
"migratedAt": "2026-04-01T00:00:00.000Z",
"legacyPath": "/path/to/chatbuddy.sqlite"
}
source 可能的值:
fresh— 全新安装,无历史数据sqlite— 从 SQLite 数据库迁移existing-compass— 从旧版 Compass 格式迁移existing-structured— 已有结构化格式数据
核心模块
CompassPaths (paths.ts)
定义所有 Compass 存储文件的路径常量,通过 createCompassPaths(globalStoragePath) 生成路径对象。
CompassSettingsStore (settingsStore.ts)
管理结构化状态文件的读写,核心职责:
- 加载:从 6 个 JSON 文件并行加载,验证结构
- 持久化:原子写入 6 个文件 + 提交标记;写前对全部 6 文件创建快照(每 key 独立、各保留 3 代),崩溃恢复时优先还原而非默认空值
- 转换:
PersistedStateLite↔StructuredStateDocument双向转换 - 兼容层:支持旧版单文件 JSON 状态(
legacyStatePayload) - API Key 管理:单独读写
providers.api-keys.json
CompassSessionStore (sessionStore.ts)
管理会话数据的读写,核心职责:
- 加载:读取索引文件 + 所有
.jsonl会话文件 - 持久化:原子写入索引 + 清理孤儿文件(受
ChatStorage.enqueuePersist的withFileLock跨进程锁保护,串行化多 IDE 窗口并发写入) - CRUD:会话的创建、读取、更新、删除、搜索
- 消息操作:追加、更新、截断、删除消息
- 验证:检查索引完整性、文件一致性
内存与增量写入机制:
| 机制 | 说明 |
|---|---|
| LRU 消息缓存 | 内存中最多驻留 50 个会话的消息(MAX_SESSIONS_IN_MEMORY),超出时淘汰最久未使用的干净会话 |
| 淘汰守卫 | 存在未持久化变更(pendingAppends/pendingRewrites)的脏会话禁止淘汰;全脏时允许超容量(正确性优先于内存上限) |
| 惰性重载 | 被淘汰的干净会话在访问时从磁盘异步重载(ensureSessionMessages 为 async),保证任何操作都基于完整消息且不阻塞 Extension Host 主线程(调用链需 await) |
| 增量持久化 | persist() 仅写入有变更的会话(pendingAppends 追加 / pendingRewrites 全量重写),跳过干净会话——既避免无谓重写,也防止被淘汰会话被不完整的内存数据覆盖 |
| 搜索索引 | 倒排索引在 load() 时基于完整消息集构建,覆盖全部会话(含未驻留内存的);token 数超过 MAX_SEARCH_INDEX_TOKENS(200k)时剔除低频 token 控制内存 |
CompassKvStore (kvStore.ts)
简单的键值存储,用于兼容性数据:
- 数据存储在
kv.compass.json中 - 仅支持字符串值
- 用于存储少量非结构化数据
CompassMigrator (migrator.ts)
负责存储迁移的决策和执行:
migrateIfNeeded()
│
├── 读取迁移标记
│
├── 有当前版本标记?
│ ├── 是 → 严格验证快照 → 清理旧 SQLite
│ └── 否 → 继续
│
├── 宽松验证快照
│ ├── 无效 → 尝试从 SQLite 恢复
│ └── 有效 → 继续
│
├── 检测到结构化格式?
│ ├── 是 → 持久化 → 写标记
│ └── 否 → 继续
│
├── 有旧版 Compass payload?
│ ├── 是 → 迁移到结构化 → 持久化 → 写标记
│ └── 否 → 继续
│
├── 有 SQLite 数据库?
│ ├── 是 → 加载数据 → 持久化 → 写标记 (source=sqlite)
│ └── 否 → 继续
│
├── 有内存数据?
│ ├── 是 → 持久化 → 写标记 (source=existing-compass)
│ └── 否 → 写标记 (source=fresh)
│
└── 完成
IO 工具 (io.ts)
Compass 存储的原子 I/O 工具集:
| 函数 | 说明 |
|---|---|
writeTextAtomic() | 先写 .tmp 文件,再 rename 到目标路径 |
writeJsonAtomic() | writeTextAtomic() 的 JSON 封装 |
readJsonFile() | 读取 JSON 文件,ENOENT 返回 undefined,解析失败返回 undefined |
readTextFile() | 读取文本文件,ENOENT 返回 undefined |
fileExists() | 检查文件是否存在 |
ensureDir() | 递归创建目录 |
removeFileIfExists() | 安全删除文件(忽略 ENOENT) |
listFilesRecursively() | 递归列出匹配后缀的文件 |
moveDirectoryContents() | 安全移动目录内容(跳过已存在文件) |
removeEmptyDirectoriesRecursively() | 递归清理空目录 |
迁移机制
从 SQLite 迁移
当检测到 chatbuddy.sqlite 文件存在时:
- 使用 sql.js 加载 SQLite 数据库
- 查询
sessions_meta、messages、kv三个表 - 将数据导入到
CompassSessionStore和CompassKvStore - 将 KV 中的状态 JSON 解析并转换为结构化格式
- 持久化到 Compass 文件
- 删除 SQLite 数据库文件
- 写入迁移标记
从旧版 Compass 迁移
当检测到旧版 Compass 单文件状态(state.compass.json)时:
- 解析旧版 JSON
- 转换为
PersistedStateLite - 通过
persistedStateLiteToStructuredStateDocument()转换为结构化文档 - 持久化到多个结构化文件
- 写入迁移标记
备份与恢复
备份格式
备份是一个 ZIP 压缩文件,内部结构:
chatbuddy-backup-2026-04-21-10-00-00.zip
└── chatbuddy.backup.compass
├── schema: "chatbuddy.backup.compass"
├── version: 2
├── exportedAt: "2026-04-21T10:00:00.000Z"
└── storage:
├── layout: "compass"
├── layoutVersion: 3
├── structuredState: { ...StructuredStateDocument... }
├── providerApiKeys: { "providerId": "key" }
├── sessions: [ ...ChatSession[]... ]
└── kv: { "key": "value" }
导出流程
repository.exportBackupData()收集所有数据createBackupArchive()将数据压缩为 ZIP- 用户选择保存路径,写入文件
导入流程
- 用户选择 ZIP 文件
extractBackupPayloadFromArchive()解压并解析 JSONrepository.importBackupData()验证并导入- 验证 schema 和 version
- 导入结构化状态
- 导入会话数据
- 导入 API Key
- 触发
refreshAll()刷新所有视图
旧版 JSON 导入
支持直接导入旧版单文件 JSON 备份(非 ZIP),用于兼容早期版本。
数据验证
启动时验证
每次扩展激活时,CompassMigrator 执行以下验证:
- 迁移标记检查:确认存储版本是否匹配
- 结构化状态完整性:所有 6 个状态文件是否齐全
- 提交标记验证:
state.commit.json是否存在且格式正确 - 会话索引验证:
index.compass.json是否为有效 JSON,会话是否有id和assistantId - 会话文件验证:每个索引中的会话对应的
.jsonl文件是否存在,内容是否为有效 JSONL - KV 验证:
kv.compass.json是否为有效的字符串键值对象
验证失败处理
如果验证失败且存在 SQLite 数据库:
- 自动回退到 SQLite 数据库
- 从 SQLite 重新加载数据
- 再次尝试持久化到 Compass
如果验证失败且无 SQLite 数据库:
- 抛出错误,阻止扩展激活
故障恢复
场景 1:结构化文件部分损坏
如果某个结构化状态文件损坏(如被截断的 JSON):
- 启动验证会检测到结构不完整
- 如果有 SQLite 备份,自动回退恢复
- 如果无备份,需要用户手动导入备份
场景 2:会话索引丢失
如果 index.compass.json 丢失但会话文件存在:
- 验证失败,报告 "Session index is missing while session files still exist"
- 自愈机制:
CompassMigrator检测到会话相关验证失败时,自动执行 load → persist → re-validate 流程,重建干净的索引文件 - 如果自愈成功,扩展正常启动;自愈失败时回退到 SQLite 恢复(如果存在)
场景 3:孤儿会话文件
如果存在未被索引引用的 .jsonl 文件:
- 验证失败,报告 "Found orphan session file not referenced by the index"
- 自愈机制:同场景 2,自动触发 load → persist → re-validate 修复
persist()时会自动清理孤儿文件
自愈机制详情
CompassMigrator.trySelfHealCompassSnapshot() 在检测到以下可恢复的验证错误时自动触发:
"Session file is missing"— 索引引用了不存在的会话文件"Found orphan session file"— 存在未被索引引用的会话文件
自愈流程:
- 重新加载所有 store(
sessionStore.load()、kvStore.load()、settingsStore.load()) - 调用
persistStores()重建干净的索引和状态文件 - 再次执行
validateCompassSnapshot()验证修复结果 - 修复成功则正常启动,失败则按原有逻辑回退到 SQLite 或报错
设计原则:自愈仅针对会话文件类问题,不对设置文件损坏等更严重的问题尝试自愈,避免掩盖真实数据损坏。
场景 4:写入过程中断电/崩溃
由于所有写操作都是原子写入(先写 .tmp 再重命名):
- 写入中的
.tmp文件不会影响已有数据 - 下次启动时,损坏的
.tmp文件会被忽略 - 数据保持一致性
版本兼容性
| 布局版本 | 说明 |
|---|---|
| 1 | 初始 Compass 格式(单文件状态 + JSONL 会话) |
| 2 | 引入结构化拆分 |
| 3 | 当前版本,引入 state.commit.json 提交标记 |
如果检测到比当前支持的版本更新的 layoutVersion,拒绝加载并提示用户升级扩展。