Skip to content

软件崩溃 #404

@bylt-max

Description

@bylt-max

让AI查询资料时崩溃,重启该条对话整个丢失,记得崩溃前AI似乎在读取网页
下面是用AI读取日志后生成的ISSUE

Bug: SQLiteBlobTooBigException - 聊天消息加载失败

概述

在加载聊天消息时,出现 SQLiteBlobTooBigException 异常,导致消息无法正常加载。

错误信息

E/ChatHistoryManager: 加载聊天消息失败
android.database.sqlite.SQLiteBlobTooBigException: Row too big to fit into CursorWindow requiredPos=1, totalRows=2
    at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
    at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:1055)
    at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:873)
    at android.database.sqlite.SQLiteQuery.fillWindow(SQLite.java:69)
    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:153)
    at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:123)
    at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:269)
    at android.database.AbstractCursor.moveToNext(AbstractCursor.java:301)
    at com.ai.assistance.operit.data.dao.MessageDao_Impl$15.call(MessageDao_Impl.java:398)
    ...

复现路径

  1. 应用启动时正常初始化(启动耗时约23ms)
  2. 数据库预加载显示现有聊天数:14
  3. 切换到某个聊天会话时触发消息加载
  4. 抛出异常,消息加载失败
  5. 界面显示消息数为 0

环境信息

  • 应用版本: 1.9.1+12
  • Android SDK: 36
  • 设备: Android 设备
  • 日期: 2026-03-10

根因分析

技术原因

  1. CursorWindow 容量限制: Android 的 CursorWindow 默认缓冲区大小约为 2MB
  2. 单行数据过大: 某条聊天消息的数据量超过了此限制
  3. 数据存储方式: Room DAO 执行查询时,尝试将整行数据加载到 CursorWindow 中失败

可能触发的场景

  • AI 回复了超长文本(如代码分析、日志输出、长篇文档)
  • 消息中包含大量 Base64 编码的图片数据
  • 消息附件被序列化后存入数据库字段
  • 多轮对话累积导致单条消息上下文过长

影响范围

  • 用户无法查看该聊天的历史消息
  • 严重情况下可能影响整个应用的聊天功能
  • 数据无法恢复(除非手动修复数据库)

建议的解决方案

方案一:分块存储(推荐)

将长消息拆分成多个 chunk 存储:

@Entity(tableName = "message_chunks")
data class MessageChunk(
    val messageId: String,
    val chunkIndex: Int,
    val content: String,
    val totalChunks: Int
)

方案二:外部存储

大型内容(如长文本、图片数据)存储为独立文件,数据库仅保存引用路径:

@Entity(tableName = "messages")
data class Message(
    val id: String,
    val contentRef: String?,  // 文件路径引用,用于大内容
    val contentPreview: String, // 前 N 字符预览
    val contentSize: Long,    // 内容大小标记
    val isLarge: Boolean      // 是否为大内容标记
)

方案三:分页查询优化

在 DAO 层使用分页加载,避免一次性加载大数据:

@Query("SELECT * FROM messages WHERE chatId = :chatId LIMIT :limit OFFSET :offset")
suspend fun getMessagesPaged(chatId: String, limit: Int, offset: Int): List<Message>

方案四:内容压缩

对超过阈值的内容进行压缩存储:

// 存储时
if (content.length > THRESHOLD) {
    val compressed = GzipUtil.compress(content)
    // 存储压缩后的数据,并标记 isCompressed = true
}

// 读取时
if (message.isCompressed) {
    content = GzipUtil.decompress(message.content)
}

临时解决方案

用户层面

  1. 删除问题聊天记录
  2. 清除应用数据重新开始
  3. 定期清理历史对话,避免单条消息过长

开发者层面

  1. 增加 try-catch 处理,避免崩溃
  2. 跳过问题消息,加载其他正常消息
  3. 提供数据导出/修复工具

相关文件

  • MessageDao.kt / MessageDao_Impl.java
  • ChatHistoryManager.kt
  • ChatHistoryDelegate.kt

优先级

高 (High) - 影响核心功能,用户可能丢失聊天数据

标签

bug database sqlite room chat data-loss


补充信息

如果选择方案一(分块存储),需要进行数据迁移:

  1. 检查现有消息大小
  2. 将超过阈值的消息拆分成 chunk
  3. 更新查询逻辑以支持 chunk 合并

需要考虑的边界情况:

  • 合并 chunk 时的顺序保证
  • 部分 chunk 丢失的处理
  • 搜索功能的兼容性

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions