ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cherry Studio 消息树模型深度解析:邻接表、虚拟根节点与分支删除语义

Cherry Studio 消息树模型深度解析:邻接表、虚拟根节点与分支删除语义 Cherry Studio 消息树模型深度解析邻接表、虚拟根节点与分支删除语义【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio导读本文是 Cherry Studio 聊天会话Topic中消息树模型Message Tree的权威技术参考。它讲解了一个主题Topic下的消息在 SQLite 中如何以邻接表adjacency list形式组织、哪些数据库级不变量invariants保证树结构合法、虚拟根节点virtual root如何让重发首条消息多模型并列回复等操作在结构上与普通分支完全一致以及删除语义cascade/ 重挂载 / 整主题清空的底层原理。读完本文你将掌握 Cherry Studio 消息表message的完整列语义、siblingsGroupId分组机制、awaiting-input 保留分支的工作流以及 flow 画布等消费端依赖的rootId契约可直接用于阅读 message.ts 与 MessageService.ts 的源码或在此基础上扩展新的读写路径。范围说明本文只覆盖主题聊天消息message表。Agent 会话消息agent_session_message是独立的扁平模型不在本文范围内。结构一棵以parentId为指针的邻接表树一个主题的消息构成一棵树存储形式为邻接表——每一行通过parentId指向其父行。多模型响应一次用户输入、N 条助手回复表现为兄弟组sibling groups即共享同一个parentId且siblingsGroupId非零的多行。核心列语义如下表列含义parentId父消息 id。仅虚拟根节点见下节为NULL。topicId所属主题外键ON DELETE CASCADE。roleuser/assistant/system内容行或root虚拟根哨兵行。siblingsGroupId0 普通单分支0 同一父节点下某个多模型组的成员。topic.activeNodeId当前选中的叶子——我们当前在哪的指针读路径从它向上回溯。在 message.ts 中可以看到这张表的完整定义。除了文档列出的核心字段源码还给出了若干与树模型配合的附加列与索引值得一并了解datatext({ mode: json })以 JSON 存储 AI SDKUIMessage.parts内容role root的虚拟根行内容是空对象{ parts: [] }。searchableText由触发器填充的纯文本列非 SQLite GENERATED 列供 FTS5 全文检索使用在 message.ts 中定义了message_fts虚拟表与message_ai/message_ad/message_au三个同步触发器。statuspending/success/error/paused由message_status_check约束。modelId/messageSnapshot/stats模型外键、创建时模型快照与 token 用量统计。compactionSummary持久化压缩标记滚动摘要只读使用绝不作为树节点喂给模型。与树结构直接相关的数据库级约束和索引包括message_parent_id_idxparentId索引、message_topic_created_idxtopicId, createdAt索引。message_topic_root_uniq部分唯一索引(topic_id) WHERE parent_id IS NULL AND deleted_at IS NULL——保证每主题至多一个存活的根行同时让根查找WHERE topic_id? AND parent_id IS NULL达到 O(1)。message_root_parent_check((role root) (parent_id IS NULL))的 CHECK 约束——roleroot与parentId IS NULL互为充要条件。message_role_checkrole IN (user, assistant, system, root)。虚拟根节点每主题一棵自识别的无内容根每个主题恰好拥有一个无内容的虚拟根节点role root、parentId NULL、data { parts: [] }。所有真实消息都挂在它之下。首条用户消息及其重发版本只是这个共享父节点下的普通兄弟——因此重发首条消息在结构上与创建任何其他兄弟完全一致不存在多个物理根。示意如下root (roleroot, parentIdNULL, 无内容, 永不渲染) ├─ user v1 ┐ ├─ user v2 ├─ 同一个 siblingsGroup —— 重发首条消息 一个普通兄弟 └─ user v3 ┘ └─ assistant → user → assistant → …虚拟根行是自我标识self-identifying的role root让所有按角色过滤的内容查询如WHERE role system天然排除它无需额外追加parentId IS NOT NULL条件。role root与parentId IS NULL等价而parentId IS NULL仍然是索引化的根查找键即message_topic_root_uniq索引。根节点的创建与写入者虚拟根节点是急切eagerly创建的——在创建主题的同一个事务里完成因此每个主题从诞生起就有根。从源码看写入者共有两类运行时MessageService.createRootMessageTx(tx, topicId)MessageService.ts插入一行{ topicId, parentId: null, role: root, data: { parts: [] }, status: success, siblingsGroupId: 0 }。它由三条主题创建路径调用TopicService.createTopicService.ts、TopicService.duplicateTopicService.ts与TemporaryChatService的 persistTemporaryChatService.ts。迁移ChatMigrator在迁移中为每个主题内联创建同样的行并把 v1 时代的物理根重挂到它之下使迁移后的主题与新建主题形态一致。消息创建路径绝不创建根——它们通过getRootMessageIdTx(tx, topicId)读取根MessageService.ts。该方法在根缺失时抛出异常根缺失是主题创建路径漏调createRootMessageTx的响亮 bug绝不静默掩盖。测试 MessageService.test.ts 还验证了同一主题第二次调用createRootMessageTx会违反message_topic_root_uniq唯一索引——单根不变量在存储层被强制执行。根对内容消息的父解析MessageService.createMessageService.ts对parentId有三种解析策略dto.parentId undefined自动解析——以topic.activeNodeId为权威的当前在哪标记追加空主题无 active 节点则把首轮消息挂到虚拟根下resolvedParentId topic.activeNodeId ?? this.getRootMessageIdTx(tx, topicId)。dto.parentId null显式首轮消息——挂到主题的虚拟根下。dto.parentId为字符串校验父消息存在且属于同一主题跨主题父引用不是受支持的形态。createUserMessageWithPlaceholders的mode: create分支MessageService.ts遵循同样规则首轮消息一律以getRootMessageIdTx的返回值为父。持久化的 awaiting-input 分支在助手消息下方开启新分支时客户端通过POST /messages/:id/branches持久化空的role user成功叶子而不是在渲染层临时标记草稿。规则是叶子助手会获得两个子节点这样首次预留就能形成真实分支已有子节点的助手只新增一个节点同一助手下方多个空预留是有意的分支点不是重复数据。等待输入awaiting-input状态完全由这个结构推导——不存储任何草稿标记。会话列表会隐藏空的成功 user 行而getTree会把空的 user 叶子投影为isAwaitingInput供 flow 画布使用。与活动流的交互空闲的预留会变成主题的 active 节点。但在直播流live stream期间渲染层发送activate: false因此创建预留不会移动当前正在播放的流路径。如果用户之后选中该预留、在主题仍处于直播状态时输入内容排队的载荷会捕获该预留 id并等待主题变为空闲后才真正写入——它不能被手动引导进正在运行的 turn 中。提交与填充下一次 composer 提交会使用这个空行的 id而不是再创建一条 user 行并走标准的submit-message流程。MessageService.createUserMessageWithPlaceholders的mode: fill-reserved分支MessageService.ts会重新校验目标仍是空的、成功的、无回复的 user 叶子然后在一个事务里原子地填充它并创建助手占位符。主进程的直播流守卫会在任何写入之前拒绝针对已预留分支的提交从而关闭渲染层的时序竞争。isAwaitingInputLeafTxMessageService.ts的实现给出了awaiting-input 叶子的精确定义内容是空 user turnrole user、status success、parts为空且没有任何存活子节点。不变量Invariants下表汇总了消息树模型必须始终成立的约束及其强制者不变量强制方式每主题恰好一个存活虚拟根message_topic_root_uniq——(topic_id) WHERE parent_id IS NULL AND deleted_at IS NULL部分唯一索引插入第二个存活根即被拒绝。每条内容消息都有非空父DB CHECKmessage_root_parent_check((role root) (parent_id IS NULL))——内容行role ! root若父为空在存储层就被拒绝而不是靠约定。首轮内容消息的parentId 虚拟根。role root⇔parentId IS NULL同一个DB CHECKmessage_root_parent_check。createRootMessageTx运行时/ChatMigrator迁移是根行的唯一写入者但该双条件本身由结构强制。activeNodeId永不为虚拟根空主题为NULL否则必为内容消息读路径会把根从 active path 中剔除。awaiting-input 分支必须是空的成功 user 叶子MessageService.reserveBranch对叶子锚点创建两条不同行否则创建一条createUserMessageWithPlaceholders(mode fill-reserved)重新校验选中的叶子并原子地填充它与其助手占位符。删除 awaiting-input 节点绝不能删掉期间已被填充的消息画布请求DELETE /messages/:id?awaitingInputOnlytrueMessageService.delete在删除前重新校验空 parts、成功状态、user 角色与无存活子节点。虚拟根只能通过删除主题来移除delete()硬拒绝它见下主题外键ON DELETE CASCADE是唯一能移除它的路径。其中message_root_parent_check的威力在于内容必有父与根 ⇔ 无父是数据库约束不是服务层的纪律。MessageService.update也据此提供了更友好的错误信息虚拟根不能被重挂会丢失 null 父、导致主题无根内容消息不能被移动到parentId null会产生第二行 null 父、违反唯一索引见 MessageService.ts。删除语义Delete Semantics删除逻辑集中在MessageService.deleteMessageService.ts按目标分四种行为目标行为虚拟根拒绝INVALID_OPERATION无论cascade为何值。删除它要么让首轮子节点成为孤儿违反唯一索引要么留下一个无根主题。内容消息cascade false若被删的是 active 路径上的分组助手回复且使用默认 parent 策略则把子节点转移给同组的下一条存活回复末尾时取前一条按创建时间再按 id 排序否则把子节点重挂到被删节点的父节点。删除分组上下文回复时清空后代上下文锚点即使没有兄弟剩余也清。保留 active 后代若被删节点本身是 active则选择后继者或回退到父节点。子节点携带其siblingsGroupId相对旧父因此每个非零移动组都会被**重基rebased**到目标位置已有任何组之上的新 id——绝不会合并进目标处无关的组。内容消息cascade true删除该消息及其整棵子树。清空全部消息clearTopicMessages(topicId)DELETE /topics/:topicId/messages——一条语句删除主题的所有非根行并清空activeNodeId无内容的虚拟根保留。这是旧的删除根来清空主题现已拒绝的结构性替代。自引用外键与 CASCADE 的正确性根源message.parentId → message.id的自引用外键是ON DELETE CASCADEmessage.ts。删除一个节点即一条语句删除整棵子树——无需叶子优先排序也没有SET NULL来制造冲突的parentId NULL行。这正是cascade true、clearTopicMessages、purgeByTopicIdsTx主题删除和topic外键级联全部可以退化为单次无序删除而依然正确的原因。而cascade false会在删除节点之前重挂其子节点因此级联触发时已无子节点可删。删除首轮消息时cascade false会把子节点重挂到虚拟根上——结构上合法它们成为新的首轮节点。文档特别强调了一个历史教训SET NULL在message_topic_root_uniq下是积极错误的它会在删除中途把集合内幸存的子节点parentId置空瞬态地制造第二行parentId NULL违反唯一索引删除任意多模型主题时都可能触发崩溃。PRAGMA defer_foreign_keys也无济于事——它推迟的是外键检查而不是动作本身。删除实现中的细节印证delete开头同时检查message.role root || message.parentId nullMessageService.ts双保险地拒绝根删除。awaitingInputOnly参数MessageService.ts在删除前调用isAwaitingInputLeafTx重新校验防止误删已被填充的消息。isContextReply判定MessageService.ts只有 active 路径上的分组助手回复才允许把续接权交给同组兄弟后继者按下一条末尾回绕取前一条的展示顺序解析。reparentChildrenTxMessageService.ts实现了组重基以{child.parentId}:{child.siblingsGroupId}为源组键为目标父下每个被移动的非零组分配全新递增 idnextGroupId max(0, 目标已有组, 移动组) 1起步确保移动组不与目标处已有组冲突。getDescendantIdsTxMessageService.ts使用递归 CTE 在单条查询中取得全部后代供cascade true与响应计数使用。clearTopicMessagesMessageService.ts先经getRootMessageIdTx定位根删除topic_id ? AND id ! root的所有行再清空activeNodeId。消费端契约Consumer Contract以下是读路径与 flow 画布依赖的核心契约rootId是权威的首轮信号。getBranchMessages与getTree在每一页返回rootId: string | null主题虚拟根的 id同时返回activeNodeId。一条消息是首轮当且仅当message.parentId rootId——这是唯一可靠的判断。不要用父不在已加载列表中推断首轮分支是分页的、根永远不在响应中也不要用 v1 的askId字段它与角色耦合对 user 消息为undefined。当rootId未知时一律把任何消息都当作非首轮处理fail-safe。getPathRowsToNodeTxMessageService.ts从节点向上走到虚拟根并排除根——展示的对话从第一条 user 消息开始。实现用递归 CTE 收集祖先 id再经 ORM 取全行以保证 camelCase 映射最后把根行切掉chain[0]?.parentId null ? chain.slice(1) : chain。getTreeMessageService.ts定位虚拟根parentId IS NULL把它从 active path 剔除并把它的子节点当作逻辑根。首轮节点在响应中保留真实父虚拟根 id虚拟根永不作为节点返回。因此TreeNode.parentId与SiblingsGroup.parentId是非空string。若调用方显式传入rootId virtualRootIdgetTree会抛INVALID_OPERATION虚拟根不可渲染为树节点。Flow 画布跳过父节点不是已渲染节点的边——首轮消息挂在虚拟根下但根从不渲染——因此首轮仍作为图的根渲染。持久化的 awaiting-input 分支仍是真实可选中的树节点。基于角色的内容查询无需特殊的根处理根是role root天然被排除。与相邻模块的关系消息树模型不是孤立设计它与 Cherry Studio 的数据层架构深度咬合递归 CTE取祖先、取后代、取子树是消息树读路径的核心手段相关 ORM-for-rows 模式与数据库模式约定参见 database-patterns.md。所有写路径在withWriteTx事务中串行化执行fts_rowid的MAX1分配、activeNodeId 更新都依赖这一串行化事务提交后通过notifyDataApiDataChange发布/topics/:topicId/messages、/topics/:topicId/tree等端点的读模型变更供渲染层订阅刷新主进程侧的数据 API 总览见 contenteditable="false">【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表