多模态消息渲染实战:TinyRobot Bubble 内容解析与 contentResolver 用法
AI 时代,消息不再只是纯文本。一张图片、一段代码、一个表格、一个工具调用的结果------这些多模态内容需要在同一个对话界面中和谐共存。TinyRobot Bubble 通过 contentResolver 和渲染器体系,为多模态消息渲染提供了优雅的解决方案。
本文将深入 contentResolver 的原理与实战,带你掌握从自定义数据结构到多模态渲染的完整链路。
一、理解消息内容模型
1.1 ChatMessageContent 类型
TinyRobot 采用 OpenAI 风格的消息结构,内容类型定义如下:
typescript
type ChatMessageContent = string | ChatMessageContentItem[]
interface ChatMessageContentItem {
type: string // 内容类型标识,用于匹配渲染器
[key: string]: any // 其他字段自由扩展
}
这意味着消息内容可以是:
- 纯字符串 :
"这是一段文字" - 内容数组 :
[{ type: 'text', text: '...' }, { type: 'image_url', image_url: { url: '...' } }]
1.2 渲染链路
从原始消息到最终渲染,Bubble 内部经过以下步骤:
go
原始消息 (BubbleMessage)
→ contentResolver 解析 → 得到 ChatMessageContent
→ 统一化处理 → string 转为 [{type:'text', text: string}]
→ 渲染器匹配 → 根据 type/role/loading 找到对应渲染器
→ 渲染器执行 → 产出 VNode
contentResolver 是这条链路的入口------它决定"从消息的哪个字段取内容"。
二、contentResolver 基础用法
2.1 默认行为
默认情况下 ,Bubble 直接使用 message.content 作为内容。这是最常见的场景,无需任何配置:
vue
<tr-bubble content="直接展示的内容" />
2.2 从 state 字段提取内容
AI 消息常常在 content 字段存储主要回复,而把额外信息放在 state 中。contentResolver 让你从任意字段提取渲染内容:
vue
<template>
<tr-bubble v-bind="message" :content-resolver="customResolver" />
</template>
<script setup lang="ts">
import type { BubbleMessage, ChatMessageContent } from '@opentiny/tiny-robot'
const message: BubbleMessage = {
role: 'ai',
content: '这是默认 content 字段',
state: {
text: '这是从 state.text 提取的内容',
extra: '这是 state.extra 中的自定义数据',
},
}
// 从 state.text 提取内容替代默认 content
const customResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
return msg.state?.text as string | undefined
}
</script>
2.3 组合多个字段
更复杂的场景:将 content 和 state.extra 合并展示:
typescript
const combinedResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
const content = (msg.content as string) || ''
const extra = (msg.state?.extra as string) || ''
return `${content}\n\n状态数据:${extra}`
}
2.4 在 BubbleList 中使用
BubbleList 也支持 contentResolver,对所有消息统一生效:
vue
<tr-bubble-list
:messages="messages"
:role-configs="roleConfigs"
:content-resolver="myResolver"
/>
三、多模态内容实战
3.1 图文混合消息
OpenAI 风格的多模态消息,content 为数组:
typescript
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: '这张照片是在哪里拍的?' },
{ type: 'image_url', image_url: { url: 'https://example.com/beach.jpg' } },
],
},
]
Bubble 自动识别 type: 'image_url',使用 Image 渲染器。
3.2 contentRenderMode 控制
contentRenderMode 决定数组内容的渲染方式:
'single'(默认):所有内容在同一个 box 中'split':每个内容项独立一个 box
vue
<!-- 图文分离渲染 -->
<tr-bubble :content="imageMessage" content-render-mode="split" />
<!-- 图文混合渲染 -->
<tr-bubble :content="imageMessage" content-render-mode="single" />
在 BubbleList 中的特殊规则:
contentRenderMode 与组内消息数共同决定渲染方式:
| contentRenderMode | 组内消息数 | 数组内容渲染 |
|---|---|---|
split |
1 | 每项独立 box |
split |
>1 | 所有内容在一个 box |
single |
任意 | 所有内容在一个 box |
这个设计保证了:单条消息时图文独立展示更美观,多条消息时紧凑不凌乱。
3.3 自定义内容类型
当你的后端返回非标准的消息格式时,contentResolver 可以将其转换为 TinyRobot 可识别的结构:
typescript
// 后端返回的自定义格式
interface CustomMessage {
role: string
data: {
summary: string
chart?: { type: string; config: Record<string, any> }
}
}
const customResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
const data = msg.state?.data as CustomMessage['data']
if (!data) return msg.content
const items: ChatMessageContentItem[] = [
{ type: 'text', text: data.summary },
]
if (data.chart) {
items.push({ type: 'chart', chart: data.chart })
}
return items
}
然后通过渲染器匹配规则,为 type: 'chart' 注册自定义渲染器(下一篇详细讲解)。
四、contentResolver 与渲染器的协作
contentResolver 和渲染器体系是互补的:
- contentResolver 决定"取什么内容"
- 渲染器 决定"怎么渲染内容"
完整的自定义流程:
vue
<template>
<tr-bubble-provider
:content-renderer-matches="rendererMatches"
>
<tr-bubble-list
:messages="messages"
:role-configs="roleConfigs"
:content-resolver="myResolver"
/>
</tr-bubble-provider>
</template>
contentResolver 先把自定义数据转为标准 ChatMessageContentItem,渲染器再根据 type 字匹选择对应的渲染组件。
五、实战案例:多模态 AI 助手
以下是一个支持文本、图片、代码、工具调用的完整多模态对话:
typescript
const messages = [
{ role: 'user', content: '帮我看看这张图片,并给出代码示例' },
{
role: 'user',
content: [
{ type: 'text', text: '这张截图有什么问题?' },
{ type: 'image_url', image_url: { url: 'https://example.com/screenshot.png' } },
],
},
{
role: 'assistant',
content: [
{ type: 'text', text: '我看到截图中有一个布局问题,以下是修复代码:' },
{ type: 'text', text: '```css\n.container { display: flex; }\n```' },
],
reasoning_content: '首先分析图片内容...',
tool_calls: [
{ id: 'call_1', type: 'function', function: { name: 'analyze_image', arguments: '{}' } },
],
},
]
对应的 roleConfigs 和 contentResolver:
typescript
const roleConfigs = {
user: { placement: 'end', avatar: userAvatar },
assistant: { placement: 'start', avatar: aiAvatar },
tool: { hidden: true }, // 隐藏工具结果消息
}
const myResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
// 优先使用 content,如果为空则尝试 reasoning_content
if (msg.content) return msg.content
if (msg.reasoning_content) return msg.reasoning_content
return undefined
}
六、CSS 变量定制
多模态渲染的样式同样通过 CSS 变量控制:
文本样式:
css
--tr-bubble-text-color: #333;
--tr-bubble-text-font-size: 14px;
--tr-bubble-text-line-height: 1.6;
图片样式:
css
--tr-bubble-image-max-width: 300px;
--tr-bubble-image-max-height: 200px;
--tr-bubble-image-border-radius: 8px;
--tr-bubble-image-space-y: 8px;
嵌入在其他 box 中的图片:
css
--tr-bubble-image-embedded-border: 1px solid #ddd;
--tr-bubble-image-embedded-border-radius: 6px;
--tr-bubble-image-embedded-margin-block: 8px;
七、总结
contentResolver 是多模态消息渲染的关键入口:
- 默认 :直接使用
message.content - 自定义提取:从 state、metadata 等任意字段取内容
- 组合解析:将多个字段合并为渲染内容
- 格式转换:将后端自定义格式转为标准 ChatMessageContentItem
配合渲染器体系,contentResolver 构成了"取内容 → 化格式 → 选渲染 → 出视觉"的完整链路。下一篇我们将深入渲染器体系与状态管理,揭秘 Bubble 的架构内核。
TinyRobot 官网 :https://opentiny/tiny-robot GitHub 仓库 :github.com/opentiny/ti...