ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DeepChat Tape 分层重构解析:从 SessionTape 单体到领域、端口与应用三层边界

2026/9/17 3:09:32 拓冰建站 浏览量
DeepChat Tape 分层重构解析:从 SessionTape 单体到领域、端口与应用三层边界 DeepChat Tape 分层重构解析从 SessionTape 单体到领域、端口与应用三层边界【免费下载链接】deepchatDeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchatDeepChat 的会话执行历史由 Tape 子系统承载而 docs/architecture/tape-layering/spec.md 定义了该子系统一次以“分层 能力端口”为核心的重构契约。本文围绕这份规格展开先讲清 Tape 在 DeepChat 中的定位与三条数据族划分再逐层解析 domain / ports / application / SQLite infrastructure 的边界设计并结合仓库中真实的端口定义、生命周期事务实现与架构守护测试说明追加式事实append-only facts、锚点anchor、ViewManifest、fork 契约等关键机制如何在分层约束下保持完整帮助读者掌握“用结构类型端口替代裸表依赖”的架构实践。一、Tape 是什么三个数据族的边界Tape 重构的第一原则是分清三类数据的归属。规格中的 “Data Families” 表格给出了明确的权威划分数据族角色权威存储Tape facts追加式执行事实、锚点、manifest、lineage 与 fork receiptsTapeTranscript projection面向 UI 的结构化消息以及一次性的旧数据回填来源Session 数据Trace evidence供 replay 使用的 provider 请求与终态执行证据Session trace storage这份划分直接回答了“Tape 该存什么、不该存什么”replay 可以通过显式的只读端口把 Tape 事实与 trace 证据组合起来但这不构成把 trace 证据当成 transcript 数据、或搬进 Tape entry 模式的理由。从 src/main/tape 目录结构看仓库已经按此原则落地为domain/、ports/、application/、infrastructure/sqlite/四个子目录分别对应纯领域策略、能力契约、应用服务与 SQLite 基础设施。规格还强调了一个负面约束SQLite 表本身不是向业务消费者导出的通用能力领域类型不得导入 Agent loop 端口。这意味着任何“我想要一行原始 Tape 记录”的冲动都要被端口语法拦截——这正是后文 Capability Boundaries 章节要展开的核心机制。二、七项目标与总体架构规格列出的 7 项 Goals 可以归纳为三层意图建立边界在src/main/tape/下确立 domain、port、application、SQLite infrastructure 的显式边界Goal 1拆分门面按既有内聚行为组拆分SessionTape同时保留兼容门面Goal 2最小能力下发用每个消费者所需的最小能力替换裸表依赖Goal 3危险操作隔离破坏性 reset/delete 置于追加式入口存储契约之外Goal 4切断反向依赖删除 Tape 对 Agent 的反向依赖以及未使用的TapeRecorder能力Goal 5行为不变保持持久化数据、公共 IPC 行为、运行时顺序、事务边界、失败回退与性能特征全部不变Goal 6契约可执行加入可强制的依赖与行为契约防止分层回退Goal 7。从源码看SessionTape门面仍然存在于 src/main/tape/application/sessionTape.ts它组合了TapeFactService、TapeReconcilerService、TapeRecallService、TapeLineageService、ExecutionJournalService等一组应用服务但对外只暴露SessionTapeCapabilities这个类型——它是十多个能力端口的交集TapeToolFactWriter TapeMessageFactWriter TapeReconciliationPort ...。类型注释里写得很直白“composition exposes the facade under this type, so a consumer can only reach what some port declares”。也就是说消费者能触达的上限由端口声明决定门面的内部管线被挡在共享表面之外。三、必要不变量append-only 语义的完整清单规格中的 “Required Invariants” 是这套分层设计的行为内核全部 10 条可验证活动 Tape 中的条目是追加式的已知事实绝不在原地更新对投影消息的修正与删除都以追加 replacement 或 retraction 事实表示锚点是重建点从不暗示删除更早的条目Compaction 改变的是“被选中的视图”而非保留的历史Fork 合并只向父 Tape追加 fork delta 与 merge receipt跨 Tape 读取需要显式的 direct-child lineage 事实且以存储的 child head 为上界搜索投影是可重建的派生物投影失败时保留既有的有界 effective-view 回退破坏性 Session 清理属于生命周期操作不是 Tape 存储的常规操作创建新 Tape 代incarnation的 reset会在同一个 SQLite 事务中删除条目、变更投影状态、搜索投影状态并追加新的 bootstrap 锚点被丢弃的 fork 对合并与标识符复用是fail-closed的——即使尽力而为的物理清理失败也如此失败的清理只留下永久无害的残留不安排自动重试。这些不变量在代码中有直接对应。src/main/tape/ports/storage.ts 中TapeEntryStore接口的注释写着 “Append/read/query persistence only. Physical deletion belongs to TapeEntryLifecycleStore”——追加式入口存储与物理删除在接口层面就被拆开了规格验收标准第 4 条“TapeEntryStore不暴露任何 reset 或 delete 方法”正是靠这种接口拆分来保证的。追加式修正在能力端口上也有体现src/main/tape/ports/capabilities.ts 中TapeMessageFactWriter的三个方法——export interface TapeMessageFactWriter { appendMessageRecord(record: ChatMessageRecord): number appendMessageReplacement( record: ChatMessageRecord, options: TapeMessageReplacementOptions ): number appendMessageRetraction(record: ChatMessageRecord, reason: string): number }分别对应“新增消息事实”“追加替换事实”“追加撤回事实”方法名全部以append开头返回值是新条目的 entry id——类型签名本身就是 append-only 语义的可编译表达。四、能力边界每个消费者拿到什么端口规格用一张表规定了“消费者 → 允许能力”的映射这是整个重构最实用的部分消费者允许的能力DeepChat loop runnerTapeReconciliationPort、TapeViewManifestReader、TapeViewManifestWriter、TapeToolFactWriterTurn coordinator 与 ACP 兼容适配器TapeReconciliationPortSession transcriptTapeMessageFactWriterMemory runtimeTapeRawEntryReader与TapeAnchorWriterSession settings 与 compactionTapeAnchorReader、TapeAnchorWriter、TapeLifecycleAdminMemory 管理路由TapeInspectionReaderSession IPC既有SessionTapePort门面关键设计点有三个1. 结构类型而非具体类。“一个实现可以满足多个端口但每个消费者只拿到它需要的结构类型。”例如TapeRawEntryReader只暴露getBySessionTapeAnchorReader只暴露 settings 所需的最新重建锚点。2. 检查端口返回 DTO绝不返回物理行。TapeInspectionReader返回的是 purpose-built 的 effective-message 与 Memory ViewManifest DTO。从 src/main/tape/ports/capabilities.ts 可以看到其返回类型TapeMemoryViewManifestInspection是精心挑选的字段集合policyVersion、tokenBudget、estimatedTokens、selectedIds、预算分配明细等物理表行根本没有穿过这条边界。3. void 契约是刻意的。TapeViewManifestWriter.appendViewManifest返回void因为其消费者不观察存储的行具体门面返回更丰富的结果只是内部兼容细节。命名规范同样被写死TapeViewManifestAssemblySources指应用服务装配的完整源集合包含latestEntryId、anchorEntryIds、entryIdByMessageId等字段见 capabilities.tsTapeViewManifestLookupMaps指纯 ViewManifest 构造器使用的更小领域查找表两个历史名称TapeViewManifestSourceMaps只保留在各自的 legacy 兼容模块中且都是显式标记 deprecated 的别名。DeepChat provider loop 是一个特例它需要整套协作契约因此仓库提供了DeepChatLoopTapePort——一个继承十几个端口的组合类型并在 createDeepChatAgentHarness.ts 这个组合根中一次性下发。组合类型上的注释解释了理由“splitting it into individual fields describes the capability types rather than the dependency”拆成单独字段只是描述能力类型并未改变依赖关系。五、直接存储访问清单谁还允许碰物理表规格要求实现对每一处现存物理表访问给出交代这在仓库中形成了一份白名单式的清单session/data/tape.ts兼容再导出生产 import 直接使用/tape/*session/data/transcript.ts合法的消息事实生产者迁移到TapeMessageFactWriter同时保留同连接事务session/data/settings.tsbootstrap、重建锚点读取、summary/reset 锚点与破坏性清理迁移到锚点与生命周期能力agent/deepchat/harness/createDeepChatAgentHarness.ts组合根把 reconciliation、fact、manifest、raw-read、anchor 能力分发给更窄的消费者DeepChat provider loop 以组合好的DeepChatLoopTapePort接收memory/routes.ts与 app composition使用TapeInspectionReadereffective source spans 与 Memory ViewManifest 记录只以领域 DTO 跨界memory/data/tables/deepchatMemoryIngestionProjection.tsTape head 与投影 head 之间的单语句新鲜度比较保留为显式只读基础设施例外以维持原子性与查询次数app/startupMigrations/legacyChatImportService.ts整库重建的破坏性启动迁移例外同时复用 composition 拥有的消息事实写入器Schema catalog 与 database security 表名列表属于元数据不是运行时 Tape 访问。这份清单可以直接在测试中得到印证。test/main/tape/layerBoundaries.test.ts 用 TypeScript 编译器 API 解析src/main的 import 图维护了一份ALLOWED_STORAGE_EXCEPTIONS白名单其中app/startupMigrations/legacyChatImportService.ts“migration-only full-table replacement and projection cleanup”与memory/data/tables/deepchatMemoryIngestionProjection.ts“read-only single-statement Tape-head consistency check”正是上述两个显式例外白名单之外的文件若引用物理表名正则deepchat_tape_(entries|search_...)或DeepChatTapeEntriesTable等符号测试即失败。六、生成与失败语义事务如何兜底规格的 “Generation and Failure Semantics” 一节定义了四个失败路径每一条都有明确的原子性归属resetSessionTape 单事务。条目删除、变更投影删除、搜索投影与 FTS 删除、新 bootstrap 创建都在共享 Session SQLite 连接上的一个事务中完成任何生命周期、清理或 bootstrap 失败都会恢复完整的上一代状态。既有的 fail-open 变更投影追加策略保持不变若把新 bootstrap 应用到派生投影失败在旧行删除后其元数据会被作废新 Tape 无需信任部分投影状态即可提交。Fork discard 的单次原子清理。清理成功则清理与 discard receipt 一起提交失败则清理回滚父 Tape 仍追加 discard receipt失败不阻塞。合并时先查既有 merge receipt 做幂等再在读取 fork 前拒绝 discard receipt用显式已丢弃的标识符创建 fork 也会 fail closed。上下文投影读取的版本校验。getByEntryIdsIfCurrent在与读取行相同的 SQL 语句中用同步调用方提供的当前 Tape head 校验投影版本与投影元数据 head非当前投影被忽略摘要或引用上下文从当前 effective Tape 重建。投影版本 3 会作废版本 2 数据它可能来自同 entry-id head 下中断的 pre-atomic reset当前搜索按需重建该派生物只读链接搜索则保留 effective-Tape 回退。FTS 损坏不得阻塞权威状态删除。FTS 行删除失败会使 FTS 元数据作废并丢弃可重建的虚拟表后继续生命周期事务若该恢复操作本身无法完成外层 Tape 生成事务原子失败。启动时删除仅由 pre-version-3 元数据拥有的 base 与 FTS 投影行让惰性遗留文本不滞留磁盘。此外clearMessages把 pending-input 删除、transcript 删除与 Tape reset 放进同一连接上的一个外层事务Tape 生成事务作为嵌套 savepoint——reset、投影清理或 bootstrap 任何一处失败都会把三个数据族整体恢复而不是留下 transcript 与 Tape 处于不同代次。这些事务语义在代码中有最小而清晰的落点。src/main/tape/application/generationLifecycle.ts 中resetTapeGeneration完整实现了 reset 单事务export function resetTapeGeneration( providers: TapeGenerationLifecycleProviders, sessionId: string ): void { const table providers.getEntryStore() table.runInTransaction(() { providers.getEntryLifecycleStore().deleteBySession(sessionId) providers.getSearchProjectionStore().deleteBySession(sessionId) table.ensureBootstrapAnchor(sessionId) }) }三个操作条目生命周期删除、搜索投影删除、bootstrap 锚点被包进runInTransaction与规格中“一个 SQLite 事务”的要求逐字对应。七、可执行的架构契约分层不会回退规格 Goal 7 的“可强制契约”由 test/main/tape/layerBoundaries.test.ts 这类测试兑现。该测试的核心机制domain 层禁导清单FORBIDDEN_DOMAIN_SQLITE_IMPORTSbetter-sqlite3、better-sqlite3-multiple-ciphers、bun:sqlite、node:sqlite、sql.js、sqlite3、FORBIDDEN_DOMAIN_LOGGING_IMPORTSshared/logger、electron-log、pino、winston等以及 Electron 运行时包任何src/main/tape/domain下的文件命中即失败能力范围消费者清单CAPABILITY_SCOPED_CONSUMER_FILES逐一点名session/data/settings.ts、session/data/transcript.ts、memory/routes.ts、agent/deepchat/runtime/deepChatLoopRunner.ts等文件校验它们不得绕过端口直接引用具体门面或物理表负面 fixture验收标准第 11 条要求“negative fixtures prove that each guard recognizes the prohibited dependency”——每个守护规则都有证明它能识别被禁依赖的负样本用例。这对应规格 15 条验收标准中的多条tape/domain/不导入 Agent/Session/Memory/App/SQLite/Electron/logging 运行时模块Agent 执行消费者针对端口而非具体SessionTape或已删除的宽泛TapeRecorder接口编译runtime、transcript、settings、routes 与正常应用组合不得接收DeepChatTapeEntriesTable实例。规格同时给出行为基线重构前基线为 7 个文件共 120 个通过、26 个环境门控跳过的 Tape 测试保持绿色Tape 规模测试确认有界尾部物化、Memory 投影快路径不新增全历史查询原生 Memory CI 在测试拆分后仍能发现并执行每个 SQLite 门控的 Tape 套件。八、约束与非目标重构的边界纪律规格的 “Constraints” 与 “Non-Goals” 两节划清了这条重构不做什么对读者判断适用范围很重要当前 SQLite 操作是同步的端口就保持同步不引入人为的异步事务边界保持顺序、幂等键、哈希、错误类别与回退日志语义不变兼容再导出可以留在旧模块路径以控制 import 变动不做数据库 schema 或数据迁移不做reset 时的归档行为不改变compaction、上下文选择或 ViewManifest 策略不重新设计transcript 或 trace 存储不改变渲染层或 IPC 功能。九、小结从这张规格里能学到什么Tape 分层重构给出的是一套可复用的架构手法用数据族表先定权责把“谁对哪份数据权威”写成表格避免后续争论能力端口按消费者裁剪getBySession只有一个方法的 reader 端口就是最小授权结构类型让“只拿到你需要的”可编译、可测试把危险操作从主存储接口上拿掉TapeEntryStore无 reset/delete生命周期删除单独成接口TapeLifecycleAdmin提供initializeSessionTape/deleteSessionTape/resetSessionTape见 src/main/tape/ports/capabilities.ts显式例外清单优于口头约定每一处保留的物理表访问都要能指到文件用架构测试把白名单固化配合负面 fixture 保证守护规则真的在守护。对于维护或扩展 DeepChat 会话执行历史相关功能的工程师这份规格加上 src/main/tape 源码与 test/main/tape/layerBoundaries.test.ts 守护测试构成了理解“哪些数据该进 Tape、哪些端口该发给谁、哪些事务必须一起提交”的完整入口。【免费下载链接】deepchatDeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考