ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

任务不怕重启丢:Browser Use Desktop 会话持久化(SQLite)设计与原理剖析

任务不怕重启丢:Browser Use Desktop 会话持久化(SQLite)设计与原理剖析

【免费下载链接】desktop

Browser Use Desktop App

项目地址:https://gitcode.com/gh_mirrors/desktop65/desktop
点击查看免费下载

Browser Use Desktop 是一款 AI Agent 浏览器桌面应用:你把任务丢给它,它会自己打开网页、点击、填写、抓取数据。但这类任务动辄跑几分钟甚至更久——如果应用在任务中途重启或崩溃,会话记录会不会全部丢失?答案是:不会。Browser Use Desktop 用一套基于 SQLite 的会话持久化(Session Persistence)机制,把会话状态、完整事件流和附件文件全部落盘到本地数据库,应用重启后会话列表、暂停状态、引擎对话 ID 都能原样找回。本文带你从零看懂这套设计的核心原理。

为什么选 SQLite 做会话持久化

桌面应用要持久化"会话"这种复杂数据(状态、消息流、二进制附件、成本统计),可选方案其实不少:JSON 文件、内存数据库、远程 API……而 Browser Use Desktop 选择了SQLite,原因很实在:

考量SQLite 给出的答案
部署复杂度单个.db文件,无需任何数据库服务器
崩溃安全WAL(预写日志)模式,进程被杀也不会丢已提交数据
并发读写WAL 模式下读写互不阻塞,主进程可高频写入事件
数据形态关系表 + BLOB,天然适合"元数据 + 事件流 + 附件"三层结构
迁移能力user_version+ 增量 migration,老库自动升级

数据库文件位于 Electron 的标准用户数据目录下,应用启动时由 index.ts 一行代码创建:

const sessionManager = new SessionManager(path.join(app.getPath('userData'), 'sessions.db'));

也就是说,你在 macOS 上可以在~/Library/Application Support/下找到这个sessions.db文件,它就是全部会话历史的"家"。

三层表结构:会话、事件、附件各司其职

打开 SessionDb.ts 的建表逻辑,数据库共有三张表,职责划分非常清晰:

1.sessions表:每个会话一行

这是"会话名片"。每个会话一行记录,列出了你关心的几乎所有元数据:

  • 身份与来源:id(UUID 主键)、prompt、origin_channel、origin_conversation_id
  • 状态机:status,取值限定为 6 种——draft / running / stuck / paused / idle / stopped(见 db-constants.ts),非法状态在写入前就会被拦截
  • 现场信息:primary_site(主要访问的站点,如example.com)、last_url(最后一个可恢复的 URL)
  • 引擎指纹:engine、engine_session_id、model、auth_mode——重启后凭这些字段让会话"回到原来的引擎里继续聊"
  • 成本统计:cost_usd、input_tokens、output_tokens、cached_input_tokens

2.session_events表:完整的事件流日志

会话执行过程中产生的每一条事件(用户输入、Agent 输出、通知、错误……)都以 JSON 形式追加进来,UNIQUE(session_id, seq)保证每个会话内序号唯一有序。这张表有两个巧妙设计:

  • 外键级联删除:REFERENCES sessions(id) ON DELETE CASCADE,删掉会话时事件自动清理,绝不留孤儿数据
  • 事件即回放素材:重启后 UI 要恢复聊天记录,直接按seq顺序读回事件流即可(hydrateOutput,SessionManager.ts)

3.session_attachments表:图片/PDF 等附件的 BLOB 存储

用户在对话里上传的附件不做外部文件管理,直接以BLOB存进数据库,同时记录name、mime、size和turn_index(属于第几轮对话)。同样配置了级联删除,会话删除即附件删除。

💡 一个直观的对应关系:sessions管"是什么",session_events管"发生了什么",session_attachments管"带了什么"。

启动即恢复:重启后的两步修复

真正让"任务不怕重启丢"成立的,是 SessionManager 在启动时执行的恢复流程。

第一步:把"假死"会话标记为已停止

上次应用是被强杀还是意外退出的?数据库里可能残留着状态还是running/stuck的会话。启动时recoverStaleSessions()会一条 UPDATE 把所有这类会话批量置为stopped,并附上错误信息:

App exited unexpectedly

(常量定义见 db-constants.ts)。于是重启后你在界面里看到的是"上次任务因应用退出而停止",而不是一个永远不会动的"运行中"假象。

第二步:重建内存中的会话

随后loadPersistedSessions()从库里读出最近的 100 个会话,逐一重建内存对象,并重点恢复三类"续命"信息:

  1. 引擎与对话 ID:engine_session_id是服务商侧的会话标识(如 Codex 的thread_id),恢复它,追问就能继续原对话而不是开新对话;
  2. 现场 URL:last_url+primary_site,重启后能定位到 Agent 上次停在哪;
  3. 暂停状态:paused的会话重启后仍是paused——你暂停的任务不会因为重启而"复活"或"蒸发"。

事件流则采用懒加载:只有当 UI 真正打开某个会话时,才从session_events读回完整记录(hydrateOutput),避免启动时把所有历史一次灌进内存。

WAL 模式:性能与崩溃安全的平衡

打开数据库的那一刻,SessionDb 构造函数 就设置了三个 pragma:

PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;
  • WAL(Write-Ahead Logging):写入先进追加式日志再合并,读写不再互相锁表。Agent 高频吐事件时,UI 的查询不会被卡住;
  • synchronous = NORMAL:在 WAL 模式下这是性能与安全的甜点——崩溃最多丢最后一个 checkpoint,而不会丢已提交事务;
  • foreign_keys = ON:SQLite 默认不启用外键,这里手动打开,才能让"删会话级联删事件/附件"真正生效。

此外,所有 SQL 语句在 prepareStatements() 中启动时一次性预编译(30 多条语句全部缓存为 prepared statement),之后每次写入都走缓存的语句对象,省掉反复解析编译的开销。批量写入事件时用db.transaction()把多条 INSERT 包进一个事务(appendEventsBatch),一次性提交,原子又快。

应用正常退出前还会执行wal_checkpoint(TRUNCATE)(close()),把 WAL 日志合并回主库并清空日志文件——下次启动更干净。

Schema 版本与指纹:数据库不会"越界"

数据库比应用新怎么办?老应用打开新数据库是危险的。runMigrations() 的策略很硬核:

if (current > DB_SCHEMA_VERSION) → 直接抛错,拒绝启动

即:只允许数据库比应用旧(触发迁移升级),绝不允许数据库比应用新(宁可罢工也不读乱)。当前 schema 版本号为 12(DB_SCHEMA_VERSION),migration 从 v1 的三张基础表一路演进到 v12 的last_url/engine_session_id,每个版本都在事务中执行、幂等可重入。

除了版本号,项目还做了一层"指纹"校验:schemaIdentity.ts 把所有表、索引、视图按规范顺序读出,序列化成 JSON 后做 SHA256,生成形如sessions:v12:sha256-xxxx的 schema 身份标识。任何对结构的意外改动(哪怕版本号没变)都能被哈希差异暴露出来——这是比user_version更细一级的保险。

附件怎么存:限制是算出来的

附件大小限制藏在 db-constants.ts,而且注释里写明了推导过程:

  • LLM API 对base64 编码后的 payload 有限制(如 5MB),base64 比原始体积大 4:3,所以原始上限要按floor(api_limit × 3/4) - 1024回推
  • 图片约 3.75MB、PDF 约 24MB、纯文本 1MB、其他类型 10MB
  • 每个会话最多 200 个附件、每条消息最多 10 个、单条消息附件总量不超过 50MB

这些上限在saveAttachment写入前强制检查,防止"无限制的追问上传"把数据库撑爆。

测试如何守住"重启不丢"这条底线

这套机制不是靠"应该没问题",而是有专门的测试用例守着:

  • 持久化单测:SessionManager.persistence.test.ts 模拟"创建 → 销毁 → 重新打开同一个库"的完整重启流程,逐一验证:导航信息(primarySite/lastUrl)恢复、引擎对话 ID(thread-123)恢复、暂停的会话重启后仍是paused、已停止的会话可以接受新的追问并重置输出……
  • E2E 回归:session-restore.spec.ts 用 Playwright 真实启动应用两次,验证"打开 3 个标签 → 退出 → 重启 → 标签数和 URL 原样还在",以及"会话文件损坏时优雅降级而不是崩溃"。
  • Schema 校验:SessionDb.schemaIdentity.test.ts 守护 schema 指纹计算的正确性。

另外提一句:浏览器标签页的恢复(session.json)与Agent 会话的恢复(sessions.db)是两套独立机制,前者管"你开着哪些网页",后者管"你和 Agent 聊到哪了",各管一摊、互不干扰。

小结:一套值得借鉴的持久化设计

回看 Browser Use Desktop 的会话持久化方案,它的完整拼图是:

  1. SQLite 单文件 + WAL:零部署、崩溃安全、读写不互锁
  2. 三张表分层:元数据 / 事件流 / 附件,外键级联保证一致性
  3. 启动恢复:先批量修复"假死"会话,再重建内存,事件懒加载
  4. 双重结构守卫:user_version迁移 + SHA256 schema 指纹
  5. 量化限制:附件大小和数量上限有明确推导,防止数据库膨胀
  6. 测试兜底:单测模拟重启、E2E 真实重启、损坏场景降级验证

对任何需要"会话可恢复"的桌面 Agent 应用来说,这套设计简洁、可验证,也足够坚固——任务跑一半,重启应用,回来继续。

延伸阅读:

  • 核心实现:SessionDb.ts、SessionManager.ts、db-constants.ts
  • 类型定义:shared/session-schemas.ts
  • 性能与启动耗时审计:PERFORMANCE.md
  • IPC 事件协议规范:shared/schemas/README.md

【免费下载链接】desktop

Browser Use Desktop App

项目地址:https://gitcode.com/gh_mirrors/desktop65/desktop
点击查看免费下载

相关推荐

上一篇:如何让你和设计文件用同一套语言说话?开源设计工具 Penpot 完全指南
下一篇:从Blender到实体:3D打印工作流的终极完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进