尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Cherry Studio 聊天域核心参考:消息树模型与 Composer 富剪贴板协议

发布时间:2026/9/20 8:37:20

资讯中心
01
ARTICLE

Cherry Studio 聊天域核心参考:消息树模型与 Composer 富剪贴板协议

Cherry Studio 聊天域核心参考:消息树模型与 Composer 富剪贴板协议
人工智能大模型AI 应用交互助手本地部署【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载本篇技术指南基于 CherryHQ/cherry-studio 仓库的 docs/references/chat/README.md 展开系统梳理聊天域chat domain的两大核心机制主进程 SQLite 支撑的主题消息树模型message表邻接表结构、虚拟根、兄弟组与删除语义以及渲染进程Composer 富剪贴板私有片段格式、令牌还原规则与安全边界。读完本文你将掌握 Cherry Studio 消息如何以树形持久化、activeNodeId如何驱动分支读取、复制/粘贴如何无损保留 skill/file/quote 等 Composer 令牌以及这些机制的源码落点与验证方式。聊天域模块全景所有权地图Cherry Studio 的聊天域没有单一的renderer/components/chat根桶barrel也不存在泛化的components/chat/adapters/目录消费方直接导入拥有该能力的模块。领域所有权划分如下路径职责src/renderer/components/chat/messages/共享的消息列表契约、渲染、操作、工具、markdown、流式与列表行为src/renderer/components/chat/actions/通用操作描述符/注册表以及当前主题与会话的操作集src/renderer/components/chat/{resourceList,shell,panes,flow}/共享资源导航、会话外壳、辅助面板与主题树可视化flow canvassrc/renderer/components/composer/共享 Composer 表面及其 Chat/Agent 变体src/renderer/pages/{home,agents}/messages/页面拥有的投影层把业务状态投影为共享消息列表契约src/main/data/services/MessageService.tsSQLite 支撑的主题消息树操作与不变量仓库早期曾规划过一套泛化适配器层与根包桶的目标架构文档但这些 API 并未落地当前参考文档只描述已实现的行为。因此在阅读本文时请以实际模块为基准例如消息操作能力由MessageListActions.copyRichContent这类共享动作面提供页面/窗口适配器再按需注入实现。领域内还有两份独立的深度参考文档文档覆盖内容Composer Rich Clipboard私有剪贴板格式跨复制/粘贴保留 Composer 令牌、还原规则与所有权边界Message Tree主题消息树模型邻接表、虚拟根、兄弟组、不变量、删除语义与消费方契约主题消息树模型Message Tree邻接表结构与核心列一个主题topic的消息构成一棵树以邻接表形式存储在message表中——每行通过parentId指向其父行。多模型响应一次用户回合、N 条助手回复表现为兄弟组sibling groups共享同一parentId且siblingsGroupId非零的行。表结构定义见 src/main/data/db/schemas/message.ts关键列语义列含义parentId父消息 id仅虚拟根virtual root为NULLtopicId所属主题外键ON DELETE CASCADEroleuser/assistant/system内容行或root虚拟根哨兵siblingsGroupId0 普通单分支0 同一父节点下的多模型组成员topic.activeNodeId当前选中的叶子——我们在哪指针读取路径从它向上回溯此外表结构还包含若干工程细节值得注意data列以 JSON 存放 AI SDKUIMessage.partssearchableText与ftsRowid由触发器维护并接入 FTS5 全文索引trigram 分词ftsRowid使用稳定整数而非隐式 rowid避免表重建如 VACUUM导致索引失同步。虚拟根Virtual Root每个主题拥有且仅拥有一个无内容虚拟根role root、parentId NULL、data { parts: [] }。所有真实消息都挂在它之下。第一个用户回合及其重发是共享父节点下的普通兄弟——因此重发第一条消息在结构上与任何兄弟创建完全一致不存在多个物理根root (roleroot, parentIdNULL, 无内容永不渲染) ├─ user v1 ┐ ├─ user v2 ├─ 同一个 siblingsGroup —— 重发首条消息 一个普通兄弟 └─ user v3 ┘ └─ assistant → user → assistant → …专用role root使该行自标识按角色过滤的内容查询WHERE role system等天然排除它无需parentId IS NOT NULL附加条件。role root与parentId IS NULL是等价的parentId IS NULL仍是有索引的根查找键。虚拟根在创建主题的同一事务里即时创建因此每个主题从出生起就有根。写入方仅有两处运行时MessageService.createRootMessageTx(tx, topicId) —— 由TopicService.create、TopicService.duplicate和TemporaryChatService持久化路径调用迁移ChatMigrator 为每个主题内联构建同一行并把旧 v1 物理根重挂到它之上使迁移后的主题与新建主题结构一致。消息创建路径从不创建根而是通过getRootMessageIdTx(tx, topicId)读取源码——缺失即抛错因为根缺失意味着主题创建路径出错属于必须大声暴露的 bug而非可粉饰的边界情况。持久化的等待输入分支awaiting-input branches在助手消息之下开启新分支通过POST /messages/:id/branches持久化空的成功role user叶子叶子助手会获得两个子节点这样首次预留才构成真实分支已有子节点的助手只新增一个节点同一助手下的多个空预留是有意的分支点而非重复数据等待输入状态由结构推导不存储任何草稿标记会话列表隐藏空的成功 user 行而getTree将空 user 叶子投影为isAwaitingInput供 flow canvas 渲染。源码实现见 MessageService.reserveBranch(anchorId, activate)它校验锚点必须是assistant行hasChild决定插入 1 还是 2 行并默认把新预留设为活动节点。空闲预留成为主题活动节点直播流期间渲染进程发送activate: false使创建预留不会移动活动流路径。若用户后续选中该预留并在主题仍直播时输入排队载荷会捕获预留 id 并等待主题转为空闲——它无法被手动引入正在进行的回合。下一次提交时Composer 复用空行的 id 而非新建 user 行并走常规submit-message流程。MessageService.createUserMessageWithPlaceholders(mode fill-reserved) 会校验目标仍是无回复的空成功 user 叶子然后在同一事务内填充它并创建助手占位行主进程的直播守卫在任何写入前拒绝预留分支提交从而关闭渲染进程的时序竞争。数据层不变量DB 级强制以下不变量由数据库结构而非服务层约定强制定义见 message 表 schema不变量强制方式每个主题恰好一个存活虚拟根message_topic_root_uniq——(topic_id)上的部分UNIQUE索引WHERE parent_id IS NULL AND deleted_at IS NULL插入时拒绝第二个存活根每条内容消息都有非空父DB CHECKmessage_root_parent_check((role root) (parent_id IS NULL))—— 内容行role ! root带空父在存储层即被拒绝role root⇔parentId IS NULL同一个message_root_parent_checkCHECK 约束createRootMessageTx运行时/ChatMigrator迁移是根行唯二写入方但该双条件本身由结构强制activeNodeId永不为虚拟根空主题为NULL否则必为内容消息读取路径从活动路径中丢弃根等待输入分支 空的成功 user 叶子reserveBranch按锚点是否为叶子决定建 2 行或 1 行createUserMessageWithPlaceholders(mode fill-reserved)重新校验所选叶子并原子填充删除等待输入节点不得误删已被填充的消息Canvas 请求DELETE /messages/:id?awaitingInputOnlytrueMessageService.delete在删除前重新校验空 parts、成功状态、user 角色且无存活子节点虚拟根只能随主题删除而删除delete()硬拒绝它见下主题外键ON DELETE CASCADE是唯一删除路径message_topic_root_uniq同时为根查找提供 O(1) 支撑WHERE topic_id? AND parent_id IS NULL并以deleted_at IS NULL限定作用域避免未来根被软删后与新根冲突。删除语义Delete Semantics删除行为矩阵目标行为虚拟根拒绝INVALID_OPERATION无论是否cascade。删除它会使首回合子节点孤儿化违反唯一索引或留下无根主题内容消息cascade false活动路径上的分组助手回复按默认父策略子节点转给同组下一条存活回复末尾取前一条按创建时间再按 ID 排序否则子节点重挂到被删节点的父节点。删除分组上下文回复时清除后代上下文锚点即使没有兄弟剩余。保留活动后代若被删节点本身是活动的则选后继者或回退到父节点。子节点携带其siblingsGroupId相对旧父每个不同的非零被移动组都会重基到目标位置已有组之上的新 id——不会并入目标处的无关组内容消息cascade true删除消息及其整个子树清空所有消息clearTopicMessages(topicId)DELETE /topics/:topicId/messages—— 一条语句删除主题全部非根行并清空activeNodeId无内容虚拟根保留。这是旧删根清主题现已被拒绝的结构性替代自引用外键parentId → message.id为ON DELETE CASCADE删除节点即一条语句移除整棵子树——无需叶子优先排序也无需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也无济于事——它推迟的是外键检查而非外键动作。消费方契约Consumer ContractrootId是权威的首回合信号。getBranchMessages与getTree每页都返回rootId: string | null主题虚拟根 id和activeNodeId。一条消息是首回合当且仅当message.parentId rootId——这是唯一可靠判断。不要用父节点不在已加载列表推断首回合分支是分页的根永不出现在响应中也不要用 v1 的askId字段与角色耦合user 消息为undefined。当rootId未知时什么也不当首回合处理fail-safe。getPathRowsToNodeTx从节点向上走到虚拟根并排除根——展示的会话从第一条用户消息开始。getTree查找虚拟根parentId IS NULL从活动路径丢弃它并把其子节点视为逻辑根。首回合节点在响应中保留真实父虚拟根 id虚拟根永不作为节点返回。因此TreeNode.parentId与SiblingsGroup.parentId是非空string。Flow canvas见 src/renderer/components/chat/flow/TopicMessageFlowCanvas.tsx跳过父节点未渲染的边——首回合挂靠的虚拟根不是节点——因此首回合仍以图根形式渲染。持久化的等待输入分支保持为真实可选的树节点。按角色查询内容无需特殊处理根根是role root构造上即被排除。相关延伸阅读Database Patterns、DataApi in Main。Composer 富剪贴板协议Composer Rich Clipboard设计目标与三格式写入私有剪贴板格式用于用户在 Cherry Studio 消息表面与 Composer 之间复制/粘贴时保留 Composer 令牌覆盖skill、file、command、knowledge、reference、quote、promptVariable七类内容。设计目标在 Cherry Studio 内部复制/粘贴时保留 Composer 令牌通过text/plain与text/html保持 Cherry Studio 之外的常规剪贴板行为可用绝不在任一载荷中暴露未消毒的令牌 JSON、可解析的 Composer 令牌元数据或本地文件路径当不存在 Composer 令牌片段时保留既有富 HTML 复制如 markdown 表格复制。富复制会写入三种载荷格式用途text/plain人类可读的回退文本text/html无解析性 Composer 令牌元数据的人类可读 HTMLweb application/x-cherry-composer-fragmentjsonCherry Studio 私有令牌片段MIME 常量定义于 src/renderer/utils/message/composerClipboard.tsCOMPOSER_CLIPBOARD_FRAGMENT_MIME web application/x-cherry-composer-fragmentjson。私有片段结构与消毒私有片段是带版本的 JSON由有序的 text/token 段组成interface ComposerClipboardFragment { version: 1 segments: ComposerClipboardSegment[] // { type: text, text } | { type: token, token, fallbackText } }片段在写入前经过一次且仅一次的消毒createComposerClipboardFragmenttoken 需有合法 id、kind、labelfile token 的 id 若形似路径hasUnsafeComposerClipboardFileTokenId则被拒绝。文件 token 载荷永不携带本地路径或路径派生的 id可还原的文件 token 只携带不可猜测的 handle加显示字段对应文件元数据保存在当前渲染会话的内存还原上下文restoration context中。还原上下文由两部分组成文件还原 handle 注册表fileRestorationRegistryhandle →{ sourceId, file, expiresAt }TTL 为 30 分钟COMPOSER_CLIPBOARD_FILE_HANDLE_TTL_MS 30 * 60 * 1000过期即剪除会话缓存保存最近一次经异步剪贴板 API 写入的富复制片段以其纯文本为键——粘贴该复制内容时无需读取系统剪贴板即可还原令牌。此外quote/promptVariable这类会原样还原 promptText的令牌受会话私有 nonce 保护COMPOSER_CLIPBOARD_PROMPT_NONCE_TTL_MS同为 30 分钟由于任何应用都能伪造剪贴板 MIME一个短可见标签可能隐藏注入的 promptText 并在发送时静默到达模型因此仅在片段携带本渲染进程写出的会话私有 nonce 时才信任其 promptText否则降级为可见回退文本。复制 → 粘贴完整流程写入侧的核心是projectTokensOverText把草稿或消息 parts 中的 token按textOffset与index排序投影为文本段 token 段同时生成纯文本getTokenFallbackText为每类 token 生成回退文本——例如 skill 用/marker/纯文本标记、knowledge 用#marker#标记、quote/promptVariable 优先用 promptText。之后createComposerRichClipboardContentFromProjection组装三格式载荷writeComposerClipboardData把载荷写入DataTransfer同步 paste 事件writeComposerRichClipboardContent则走异步navigator.clipboard.write。同步粘贴与设计取舍粘贴处理完全同步永不调用navigator.clipboard.read()。经异步剪贴板 API 写入的片段不会出现在paste 事件的DataTransfer中因此writeComposerRichClipboardContent会把写入片段记录到会话缓存粘贴时若纯文本与最近一次富复制匹配先做行尾归一化\r\n → \n兼容 Windows 剪贴板往返则从缓存还原。备选方案均被否决粘贴时读取系统剪贴板 → 使每次外部粘贴变异步且会读取无关剪贴板数据通过合成 copy 事件写入 → 需要已废弃的execCommand对text/html做指纹识别 → 泄露来源标记。接受的代价来自消息复制的quote与promptVariable令牌在应用重启后或另一个 Cherry Studio 实例中丢失令牌身份skill与knowledge令牌仍可通过纯文本标记还原/marker/、#marker#。还原规则Restore Rulesskill与knowledge令牌仅通过当前表面的 resolver 还原——Chat 与 Agent 保持各自的令牌所有权边界reference令牌以及无还原规则的私有令牌种类如command回退为可见文本file令牌仅在私有载荷含 handle 且在当前渲染会话的还原上下文中可解析时还原还原的文件按id:path去重文件 handle 不是可信的剪贴板数据——它们只定位本渲染会话已持有的还原上下文缺失、未知、过期、跨窗口、重启后或伪造的 handle 一律回退为可见文本从用户消息复制的 file 令牌仅当消息文件 part 携带的 file token 源与文本 token 源精确匹配时才可还原文件名、显示名、令牌标签永不用作回退身份路径派生的 file token id永不写入剪贴板若当前渲染会话持有原文件元数据令牌仍可通过 handle 还原quote与promptVariable从消毒后的 token 字段还原受 nonce 保护见上文不支持、不安全或无法解析的令牌段回退为可见文本片段只从 paste 事件剪贴板数据或会话缓存还原粘贴永不读取系统剪贴板若浏览器无法写入私有自定义格式剪贴板写入回退为text/htmltext/plain再在ClipboardItem不可用时回退纯文本每次此类回退都会清空会话缓存。所有权边界BoundariesMessageListActions.copyRichContent是富剪贴板写入的共享动作面消息组件请求该能力页面/窗口适配器提供实现composerClipboard.ts 拥有私有片段解析、序列化、HTML 转义、还原上下文文件还原 handle 与会话缓存以及系统剪贴板写入辅助函数ComposerSurface拥有编辑器 copy/cut/paste 事件处理并把片段解析/投影委托给工具函数。Cut 执行与复制相同的富复制后删除选区——因此被剪切令牌保持可还原而不是退化为默认的去令牌 HTML 剪贴板普通 OS 文件粘贴与拖放是独立流程使用浏览器或 Electron 文件 API不会从私有 Composer 片段还原文件文件还原不会重读文件或重跑支持扩展名检查后续发送/文件处理路径仍负责文件可用性文件 part 的mediaType推断不属于本特性如需 MIME 归一化请保持该改动独立。令牌能力表剪贴板支持由单一契约派生哪些令牌种类支持剪贴板、哪些种类的 promptText 可被信任并非散落的硬编码而是由 src/renderer/utils/composerTokenPolicy.ts 中的单一能力表COMPOSER_TOKEN_CAPABILITIES派生令牌种类剪贴板剪贴板 promptTextskill✅❌link✅✅file✅❌folder✅✅command❌❌knowledge✅❌reference✅✅quote✅✅promptVariable✅✅ComposerClipboardTokenKind ComposerTokenKindWithCapabilityclipboard即由此表推导测试见 composerTokenPolicy.test.ts例如command不具剪贴板能力、unknown一律拒绝。聚焦验证本地迭代命令富剪贴板与消息树涉及大量渲染进程与主进程代码文档建议本地迭代时使用聚焦检查而非全量测试套件# 富剪贴板核心ComposerSurface 粘贴事件 片段解析/序列化工具 pnpm test:renderer src/renderer/components/composer/__tests__/ComposerSurface.test.tsx src/renderer/utils/message/__tests__/composerClipboard.test.ts # 富剪贴板动作面菜单栏动作、选区、平台动作钩子、选区控制器 pnpm test:renderer src/renderer/components/chat/messages/frame/__tests__/messageMenuBarActions.test.tsx src/renderer/components/chat/messages/utils/__tests__/messageSelection.test.ts src/renderer/components/chat/messages/hooks/__tests__/useMessagePlatformActions.test.tsx src/renderer/components/chat/messages/hooks/__tests__/useMessageSelectionController.test.tsx测试文件均已在仓库确认存在如 ComposerSurface.test.tsx、composerClipboard.test.ts、messageMenuBarActions.test.tsx 等。消息树侧的完整覆盖还包括 MessageService 相关的服务层测试与 message schema 中的 CHECK/UNIQUE 约束测试。小结Cherry Studio 的聊天域把业务状态与渲染契约清晰分层主进程 MessageService.ts 以邻接表 虚拟根 兄弟组维护主题消息树并用 DB CHECK 与部分唯一索引把树的不变量下沉到存储层渲染进程以 composerClipboard.ts 为核心实现了一套不读取系统剪贴板、不泄露路径与可解析元数据的富剪贴板协议配合单一令牌能力表与 30 分钟 TTL 的会话还原上下文在内部无损还原令牌、对外保持普通剪贴板行为。两条主线分别由 Message Tree 与 Composer Rich Clipboard 两份文档详细记载本文是其面向工程实践的索引与源码级印证。赞分享人工智能大模型AI 应用交互助手本地部署【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载相关推荐Cherry Studio 聊天域技术参考消息树模型与 Composer 富文本剪贴板深度解析Cherry Studio 聊天域技术参考消息树模型与 Composer 富文本剪贴板深度解析 Cherry Studio 的聊天域横跨渲染进程可复用模块、页AI 应用大模型桌面应用本地部署RAGA2UI 消息类型完全参考v0.8/v0.9 协议消息格式、数据模型与消息顺序实战指南A2UI 消息类型完全参考v0.8/v0.9 协议消息格式、数据模型与消息顺序实战指南 本文是基于开源仓库 A2UIAgent to UI官方参考文档 m人工智能AI AgentAI 应用前端UI组件UFO 项目 AIP 消息协议完全参考Pydantic 消息模型、关联机制与最佳实践UFO 项目 AIP 消息协议完全参考Pydantic 消息模型、关联机制与最佳实践 AIPAgent Interaction Protocol是 UFO人工智能AI Agent自主智能体GUI 自动化Agent 编排多智能体RAG创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。