---
title: 解析 Cursor SQLite：从表与键排查缺失消息
description: 沿 Cursor 适配器检查两个数据库、键前缀、未知版本和孤立会话，区分读不到、解析遗漏与真正没有记录。
image: "https://zengliangyi.com/og/blog/cursor-sqlite-parser-pitfalls.png"
---

导入缺少消息时，先找出是哪一层没有得到预期数据：文件读取、表查询、会话键匹配，还是字段提取。空结果不能直接解释为“源工具删除了记录”，版本警告也不能直接忽略。

本文按 [ChatCrystal 的 Cursor 适配器](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/cursor.ts) 提交 [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8) 解释排查方法。安装、目录和完整导入步骤见 [Cursor 导入指南](/blog/cursor-conversation-import)；这里关注解析器的诊断与覆盖边界。

## 确认文件属于同一份用户数据

工作区数据库提供 Composer 元数据，全局数据库提供 bubble 消息。适配器还使用 `workspace.json` 获取项目目录。只复制其中一份，或混入另一台机器的全局数据库，都可能导致会话 ID 找不到消息。

[数据库读取函数](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/vscdb.ts) 把主文件读入 sql.js 内存实例，失败后等待 500 毫秒重试一次，再失败返回 null。这不是精确的锁、权限、损坏分类，也不是 WAL 重放。

SQLite 的 [WAL 说明](https://www.sqlite.org/wal.html)解释了已提交修改可能仍在日志文件中。调查实时数据库时，需要一致的数据库视图；可参考 [官方备份机制](https://www.sqlite.org/backup.html)。不要把单独读取主文件的结果当作所有最新记录的证明，或在原数据库上试验性删除文件。

## 查询正确的表和键

先在测试副本上检查表，再针对适配器认识的键查询。下面的会话 ID 是构造示例：



```sql
-- 工作区数据库  
  SELECT   value   FROM   ItemTable  
  WHERE   [key]   =   'composer.composerData'  ;  
  
  -- 全局数据库  
  SELECT   [key],   value   FROM   cursorDiskKV  
  WHERE   [key]   LIKE   'bubbleId:sample-session:%'  ;
```



缺少 `cursorDiskKV` 与该表存在但没有匹配行，是不同的失败。会话键的形状是 `bubbleId:<composerId>:<bubbleId>`，要查整个会话需要匹配前缀；一个已知完整键也可以用等值查询，不能说这种存储“只能用 LIKE”。

自己实现查询时，应绑定参数，另行处理 LIKE 中 `%`、`_` 的通配语义。当前适配器的查询使用字符串插入，不能把这条建议写成应用已经实现的保护。常规 UUID 不含这些通配字符，也不代表任意异常 ID 都能安全地按同一方式处理。

## 版本与角色不是完整 schema 校验

`_v > 3` 时，代码发出警告并继续读取已知字段；它没有证明新版本结构完全兼容。构造 `_v: 4`、字段改名、text 类型错误的样本，检查是否留下正确内容及能否定位错误。

当前代码把 `type === 1` 作为用户，其余值作为助手，并跳过无正文的助手 bubble。这是实现规则，不是对所有未知类型的正确分类。空正文也不能单凭这一字段就断定是流式中间态。

## thinking 的保留有前提

当 `allThinkingBlocks` 为非空数组时，代码收集块中的 `thinking` 或 `text`；否则可回退到顶层 `thinking`。但助手 bubble 如果没有正文，会在提取 thinking 之前被跳过。只有推理而没有正文的助手记录，因此可能不在导入消息中。

分别测试正文加推理、只有顶层推理、非空块数组却无可用文本等情况。多格式支持应描述为这些已知路径，不能扩展为“所有思考过程都能完整保留”。

## 孤立会话检查是采样

全局扫描用键提取 Composer ID，排除已知工作区会话，再检查候选的最多 20 个 bubble 是否有非空文本。`bubbleId:` 前缀共 9 个字符，因此 SQL 的 `SUBSTR([key], 10)` 从 ID 开始。

这个候选检查没有排序，也不会检查该会话所有 bubble。把可用文本放在前 20 条之后，是重要的失败样本；不能把 LIMIT 20 的优化解释成不损失覆盖率。候选被发现后，解析阶段才读取全部匹配行。

## 比较更新后的记录，而不只看数量

工作区路径去前缀并 URL 解码后用于项目显示。当前转换会把 `file:///tmp/project` 变为 `tmp/project`，丢失 POSIX 根斜杠，不能保证保留可用的绝对路径。坏编码可导致工作区跳过；目录哈希不是项目名称的可靠替代。还要注意增量导入使用的元数据：已知 Composer 的创建时间可能被用作 `fileMtime`，配合全局文件大小不能保证识别每次内容变化。

准备一个有已知消息的会话，修改测试副本中的内容后再次导入，比较实际消息是否更新。记录文件、表、键、字段、版本和导入 ID，才能把“没找到”收敛为具体问题。通过解析检查后，再核对生成的笔记是否保留关键条件；相关的事件过滤方法见 [Claude Code JSONL 过滤](/blog/claude-code-jsonl-noise-filter)。

分享：

##  相关文章

[##  导入 Cursor 对话：工作区元数据与全局消息

核对 ChatCrystal 如何关联 Cursor 的两类 SQLite 数据、发现孤立会话和提取消息，并用真实会话验证导入范围。          曾亮懿       2026年10月3日      阅读约 4 分钟

#ChatCrystal

#Cursor

#SQLite

#数据解析](/blog/cursor-conversation-import)    [##  AI 对话格式差异：事件、快照与跨库消息定位

对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-format-divergence)    [##  AI 对话管理方案：四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入，区分原始副本、可读导出与知识库，并用合成清单验证身份和信息保留。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-management-options)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "解析 Cursor SQLite：从表与键排查缺失消息",
  "description": "沿 Cursor 适配器检查两个数据库、键前缀、未知版本和孤立会话，区分读不到、解析遗漏与真正没有记录。",
  "url": "https://zengliangyi.com/blog/cursor-sqlite-parser-pitfalls/",
  "image": "https://zengliangyi.com/images/blog/cursor-sqlite-parser-pitfalls.svg",
  "datePublished": "2026-10-03T00:00:00.000Z",
  "dateModified": "2026-10-03T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/cursor-sqlite-parser-pitfalls/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "解析 Cursor SQLite：从表与键排查缺失消息",
      "item": "https://zengliangyi.com/blog/cursor-sqlite-parser-pitfalls/"
    }
  ]
}
```
