Composer 上下文引用¶
面向维护者。正在使用 T3 Code?请参阅 docs/user。
内联上下文引用可以让用户消息指向其正文中某个精确位置的类型化载荷:一张图片、一个文件、一段终端摘录、一个选中的页面元素、一条预览批注、一条审查评论、一个文件提及,或一项技能。本文档涵盖传输契约和纯编解码器。编辑器、渲染和剪贴板行为将在后续 PR 中实现,并在到来时在此获得各自的小节。
两个相互关联的概念¶
- 上下文记录(context record) 是载荷。它存在于
message.context.records中,以contextId为键。记录从不包含字节:图片和文件记录通过 id 绑定到已有的ChatAttachment。 - 上下文引用(context reference) 是文档中的一次出现。它是
message.text中的一个 Markdown 链接,只携带 kind 和contextId。两个引用可以指向同一条记录。引用的标签只是显示文本,绝不是身份标识。
[composerContext.ts][contract] 定义了记录联合体的版本 1。每条记录都有 version、contextId、kind 和 label,以及长度受限的 kind 专属字段。该联合体是开放式的:当前构建版本无法识别的 kind 会解码为 UnknownContextRecord,并保留其 payload;已知 kind 会被排除在该成员之外,因此一条格式错误的图片记录会按其自身 schema 失败,而不会未经检查就滑过去。OrchestrationMessageContext 使用 ForwardCompatibleArray 包装这些记录,因此某条无法解码的记录会被丢弃,而不是让整条消息失败。该字段在 V2 会话消息以及启动或排队一个回合的命令中是可选的;编排器会原样传递它。
身份命名空间¶
ComposerContextId: 持久的载荷身份标识。带品牌标记。值匹配[a-z0-9_-]+,且不需要ctx_前缀。ComposerContextReferenceId(ref_…): 文档中的一次出现。带品牌标记。存在于编辑器状态中,不参与线上传输,并在重新解析规范 Markdown 时重新生成。ChatAttachmentId: 已有的由服务器拥有的附件资源。记录的attachmentId是一个绑定,而不是 chip 的身份标识,因此上传规范化可以在不重写引用的前提下重命名资源。
客户端会生成上下文 ID 和引用 ID。共享代码不会生成,因为 Effect lint 插件在那里拒绝直接使用 crypto.randomUUID()。
规范引用语法¶
[composerContextReferences.ts][shared] 负责定义该语法:
解析器只接受 t3-context: 方案、v1 主机、一个匹配 [a-z][a-z0-9-]{0,39} 的 kind 段,以及一个匹配 [a-z0-9_-]{1,128} 的 id 段(不区分大小写)。查询字符串、片段、凭据和额外的段都会被拒绝。标签会被清理,以便在 Markdown 链接中存活(没有方括号或换行,最多 200 个字符,不能为空)。无法解析的链接会被当作普通文本。collectComposerInlineTokens 已经拒绝文件链接的 URI 方案,因此上下文链接绝不会被误认为是提及。
Provider 投影¶
projectComposerContextForProvider({ text, records }) 构建 provider 会读取的内容:
- 每个引用都会变成一个就地标记:
[Image: shot.png; ref=ctx_1]。 - 尾部的
<t3_context version="1">封装(envelope)会按照首次引用顺序,为每个唯一被引用的 id 包含一个<context kind id>条目。从未被引用的记录不会被输出。被引用但没有对应记录的 id 会变成<context … unavailable="true"/>。提及(mention)和技能(skill)记录会产生标记,但不会产生条目。未知 kind 会将其 payload 以 JSON 形式输出。 - 捕获到的文本是数据:任何可能打开或关闭
t3_context或context的<都会被转义,因此终端输出行或 PR 评论无法伪造记录。
附件字节通过现有的附件通道传输;封装只携带元数据。没有引用的文本会原样返回。
旧版消息¶
在此功能之前发送的消息会带有尾部的 <terminal_context>、<element_context>、<preview_annotation> 块、<review_comment> 块,以及 U+FFFC 终端占位符。[composerContextLegacy.ts][legacy] 会在内存中升级它们:
- 审查块(review blocks)就地变为引用。原本位于文本末尾的块会被追加到最后,以匹配旧的发送顺序。
- 尾部的块会按发送顺序的逆序从末尾剥离(preview、element、terminal)。
- 占位符会按顺序绑定到终端条目;没有占位符的条目会被追加。
- ID 是确定性的(
legacy_<kind>_<n>),因此重新运行升级是幂等的。
事件历史永远不会被重写。apps/web/src/lib/ 中现有的 Web 解析器会一直保留,直到转录渲染器改用记录。
编辑器模型(Web 和桌面端)¶
ComposerPromptEditorTiptap.tsx 中的 ComposerContextReferenceExtension 是每种上下文 kind 的内联 Tiptap 节点。它存储 kind、contextId、label 以及每次出现的 referenceId,其文本内容就是规范链接。提示字符串携带载荷身份和位置,因此重建编辑器可以恢复等效的 chip;它不会保留编辑器本地的出现 ID。使用了 U+FFFC 序数占位符的旧草稿会在水合(hydration)时迁移:占位符按数组顺序绑定到终端上下文,然后提示字符串未提及的任何上下文都会以链接形式前置。
记录目前仍保留在草稿存储的类型化数组中。编辑器会从它们构建一个以 contextId 为键的 Map(composerContextRecordsFromDraft),并通过 ComposerContextRecordsContext 提供该 Map。ComposerContextReferenceChip 会查找记录并渲染该 kind 的 chip;未知 kind 或缺失记录会渲染未解析的 chip,而不是消失。移除一个 chip 只会移除那一次出现;composer 的变更处理器会比较引用的 ID 与草稿数组,并丢弃没有任何 chip 指向的记录。
版本 1 刻意让每种 kind 的呈现在各客户端中显式实现,而不是暴露运行时处理器注册表。契约和编解码器是共享的;web/桌面端渲染富 chips,移动端渲染可读标签。仅当第三方或运行时定义的 kind 必须提供无法随客户端一起发布的行为时,才需要添加注册表。同样,只有当未来功能需要跨序列化边界定位某一条 occurrence 时,持久化的 occurrence id 才应纳入规范引用语法。
终端上下文现在与其他所有上下文记录走同一条发送路径:持久化消息保留其规范链接和结构化记录,而 provider 投影会用可读标记替换链接,并在上下文信封中附带一次摘录。旧的尾部 <terminal_context> 形式仅在读取旧客户端发送的消息时解析。
发送和读取消息¶
composer 将 message.text 作为带有引用链接的规范正文发送,并根据草稿构建 message.context.records(见 apps/web/src/lib/composerContextRecords.ts 中的 buildMessageContext)。已过期的终端摘录会从这两处移除。服务器在轮次开始时投影 provider 文本(projectComposerContextForProvider),因此持久化消息保持可读,而 provider 接收标记外加一个信封。
审阅评论和预览批注通过 store 的 mutator 进入草稿。已挂载的 composer 会注册上下文插入处理器,使来自面板的引用落在其当前或最后已知的光标位置;当没有 composer 挂载时,store 会追加这些引用。终端摘录和附件使用同样的光标优先行为。在编辑器中删除一个 chip 会移除对应记录;删除预览截图缩略图会移除其批注和 chip。
对话记录通过 resolveUserMessageContext 解析消息:结构化上下文按原样使用,较旧的消息在内存中升级。ChatMarkdown 通过 renderContextReference 渲染 t3-context:// 链接,时间线再通过 web 上下文呈现注册表将这些链接映射为 chips。该注册表为每个已知 kind 声明 compact、details 和 expanded 能力,拒绝重复的 surface 处理器,并提供未解析时的回退。终端摘录、元素、审阅评论和预览批注会打开结构化详情弹出层;图片和视频使用共享媒体模态框。移动端将上下文链接渲染为对应标签。
拉取请求摘要目前以审阅评论记录的形式传递,并带有可选的类型化 pullRequest 元数据。该元数据是附加时对编号、标题、URL、分支、状态和 draft 标志的快照。web 和桌面端渲染紧凑的 #number 标签,并从该快照推导其状态色调。悬停显示快照详情;激活时根据当前环境解析 URL,并在会话线程的右侧面板中打开拉取请求。在元数据加入之前写入的记录保留其旧有详情和中性的拉取请求色调。
附件¶
图片和文件记录使用草稿附件的本地 id 作为 contextId,并携带 attachmentId 绑定。composer 发送上传的 pending id(在 data-URL 路径上,则通过 UploadChatImageAttachment 上可选的 id 发送本地 id);服务器的 Normalizer 将每个图片和文件记录重写为它所分配的持久化 id,从而让存储的消息把记录绑定到真实资源。乐观行绑定到本地 id,并由服务器副本替换。
在 composer 中,附加文件或图片会在光标处插入一个 chip(当编辑器无法接收输入时则追加)。文件仅以 chip 形式存在:某个文件的最后一个 chip 被删除时,该文件会被移除,其上传也会被释放;早于引用机制存在的草稿在 hydration 时会被追加一个 chip。图片以缩略图栏作为其清单;删除 chip 会保留图片,而删除仍被引用的缩略图时,会先要求确认,然后同时移除两者。旧草稿不会获得图片 chips。
在对话记录中,图片 chip 打开图库预览,视频 chip 打开媒体预览,其他文件 chip 打开或下载文件。图库仍显示所有图片;文件行仅保留给没有任何 chip 引用的文件。
剪贴板¶
所有复制路径都将规范 Markdown 写为 text/plain;当选中内容包含 chips 时,还会在 web application/x-t3-context-fragment+json 下写入结构化片段(ComposerContextClipboardFragment:版本、来源环境/会话线程/消息、记录;不含字节、不含 URL)。composer 的复制和剪切通过 Lexical 命令监听器添加该片段;对话记录的选择复制通过用户消息主体上的 onCopyCapture 添加,同时 chips 通过 data-markdown-copy 重新发出其链接;整条消息按钮通过 ClipboardItem 同时写入两者,并回退到纯文本。
粘贴时,composer 会先解码片段,再处理纯文本。草稿尚未持有的记录会被导入:终端摘录和审阅评论按原样导入,预览批注根据其记录重建,图片或文件通过来源环境的资源 URL 重新获取,并以新的本地 id 附加,粘贴的链接会被重写为该 id。粘贴的二进制内容在字节到达前显示为未解析的 chip;来自其他环境的片段会让二进制内容保持未解析。已渲染的 chips 和已发送的消息绝不会因粘贴而被修改。
跨会话线程、项目或环境的粘贴使用相同路径:客户端根据来源环境生成资源 URL,下载字节,然后在此处附加。没有服务器端克隆;如果来源不可达或附件已不存在,toast 会提示,chip 保持未解析。
ID、持久化与暂存¶
生产者各自保留自己的 id 语法;toComposerContextId 在引用和记录的边界处,将 [a-z0-9_-] 之外的任何内容确定性地折叠为 slug 加哈希。预览批注的 context id 由 annotation-<id> 派生,因此它与截图图片保持不同——截图的附件 id 就是批注 id;记录通过 screenshotContextId 将两者关联。
projection_thread_messages.context_json 持久化记录,因此重启或投影重新加载后,标签仍然可解析。提示词暂存条目携带 records,用于终端摘录、审查评论和预览注释;暂存会将这些内容移出草稿,恢复时则通过粘贴路径所使用的同一个导入器将它们重新导入。
其他面板生成的上下文通过 setContextInsertionHandler 到达光标处:已挂载的消息编辑器为其草稿注册一个插入器,而 store 则回退为追加模式。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code