Appearance
DemoChat 使用说明书
DemoChat 是 ChatUI M-DEMO 阶段的演进验证组件,用于在没有真实后端时验证 TinyRobot 多消息渲染、SSE 流式输出、多会话历史、@ 工作智能体选择、Renderer chunk 调试和 demo-only 内容扩展能力。
当前公共导出名为 NexusChatDemo,本文将它称为 DemoChat。它不代表最终正式 ChatUI 公共 API,也不应把本地 mock 数据当作真实后端协议。
阅读路径
本文按由浅入深组织:
- 先认识 DemoChat 的定位和边界。
- 再启动 mock 服务和 playground,把页面跑起来。
- 然后用最小代码接入组件。
- 接着了解布局、调试、内容扩展和会话存储替换。
- 最后在文末查看完整 Props、Events、类型和 runtime 接口文档。
如果只是想先看到效果,读到“最小接入”即可。如果要扩展内容类型或对接兼容后端,再继续阅读后面的章节。
1. 认识 DemoChat
DemoChat 适合以下场景:
- 本地验证 ChatUI 对话链路是否可见、可输入、可流式输出。
- 验证
text、reasoning、tool、skill-trigger、gen-ui、references、error、unknown等默认内容类型渲染。 - 验证自定义 content item 的 segment handler 与 renderer 是否能接入 DemoChat。
- 调试 renderer 与原始 SSE chunk 的对应关系。
- 在 playground 或临时页面中演示多会话历史、停止输出、删除会话和
@工作智能体。
使用前需要明确这些边界:
- DemoChat 依赖 Chat runtime mock 或兼容 mock 形状的后端接口。
- DemoChat 默认会把会话列表、当前会话 id 和消息内容存入浏览器
localStorage。 - DemoChat 组件内部使用
@opentiny/tiny-robot,调用方只需要使用@agent-nexus-ui/chatui的公共入口。 - DemoChat 的
AgentBaseUrlprop 使用大写命名,模板中既可以写:AgentBaseUrl="agentBaseUrl",也可以写:agent-base-url="agentBaseUrl"。项目现有示例统一使用AgentBaseUrl。 - DemoChat 当前是 demo 演进验证组件,不承诺正式公共 API 兼容;沉淀为正式 ChatUI 前,扩展代码应保持 demo-only 标识。
2. 启动本地环境
在仓库根目录安装依赖:
bash
npm install启动本地 Chat runtime mock 服务:
bash
npm run dev:mock:chat服务默认监听:
text
http://localhost:3000启动 playground 查看现有 DemoChat 页面:
bash
npm run dev:playground现有演示路由:
- 横版 DemoChat:
/chatui/tinyrobot-demo - 竖版 DemoChat:
/chatui/tinyrobot-demo-portrait
Agent 自动化启停 mock 服务时应直接使用脚本包装器:
bash
node scripts/mock/chat-runtime-service.mjs start
node scripts/mock/chat-runtime-service.mjs status
node scripts/mock/chat-runtime-service.mjs stop
node scripts/mock/chat-runtime-service.mjs restart
node scripts/mock/chat-runtime-service.mjs check3. 最小接入
在 Vue 页面中从 @agent-nexus-ui/chatui 导入 NexusChatDemo,传入 mock runtime 地址即可。
vue
<script setup lang="ts">
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
const agentBaseUrl = 'http://localhost:3000'
</script>
<template>
<NexusChatDemo :AgentBaseUrl="agentBaseUrl" />
</template>如果页面需要随部署环境切换 runtime 地址,可以读取 Vite 环境变量:
vue
<script setup lang="ts">
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
const DEFAULT_AGENT_BASE_URL = 'http://localhost:3000'
const agentBaseUrl = import.meta.env.VITE_AGENT_BASE_URL || DEFAULT_AGENT_BASE_URL
</script>
<template>
<NexusChatDemo :AgentBaseUrl="agentBaseUrl" />
</template>线上 playground 构建时可通过 VITE_AGENT_BASE_URL 指向 Cloudflare mock Worker;本地调试默认保持 http://localhost:3000。
不要导入内部源码:
ts
import NexusChatDemo from '../../packages/chatui/src/demo/NexusChatDemo.vue'应该使用公共 package 入口:
ts
import { NexusChatDemo } from '@agent-nexus-ui/chatui'4. 调整布局
DemoChat 默认使用横版布局,适合桌面控制台场景。需要在窄容器或移动端预览中使用时,传入 layout="portrait"。
vue
<script setup lang="ts">
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
const agentBaseUrl = 'http://localhost:3000'
</script>
<template>
<NexusChatDemo :AgentBaseUrl="agentBaseUrl" layout="portrait" />
</template>建议外层容器给 DemoChat 留出明确高度,避免输入区和消息区被页面其它内容挤压:
vue
<template>
<main class="demo-chat-page">
<NexusChatDemo :AgentBaseUrl="agentBaseUrl" />
</main>
</template>
<style scoped>
.demo-chat-page {
min-height: 720px;
}
</style>5. 验证默认内容类型
DemoChat 已内置常见的 ChatUI 内容渲染能力,通常不需要调用方额外配置。
| 类型 | 用途 |
|---|---|
text | 普通文本、Markdown 文本渲染。 |
reasoning | 思考过程渲染。 |
tool | 工具调用和工具响应渲染。 |
waiting | 流式输出前的等待态。 |
skill-trigger | Skill 触发过程渲染。 |
gen-ui | GenUI 卡片兜底渲染和交互桥接。 |
references | 参考来源列表渲染。 |
error | 错误片段渲染。 |
unknown | 未识别 content item 的可见兜底渲染。 |
推荐先在输入框中发送:
text
执行一个完整复杂的订单异常复合场景这个输入会让本地 mock 推断为复合场景,用于集中验证文本、思考、工具调用、Markdown、GenUI 和参考来源等渲染链路。
6. 调试 Renderer Chunk
当需要排查“某个 SSE content item 是否被正确转换为 segment,并被正确 renderer 命中”时,开启 debug-renderer-chunk 并监听 hover 事件。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
import type { DemoRendererChunkDebugPayload } from '@agent-nexus-ui/chatui'
const agentBaseUrl = 'http://localhost:3000'
const hoveredRendererChunk = ref<DemoRendererChunkDebugPayload>()
function handleRendererChunkHover(payload: DemoRendererChunkDebugPayload): void {
hoveredRendererChunk.value = payload
}
function handleRendererChunkLeave(): void {
hoveredRendererChunk.value = undefined
}
</script>
<template>
<section class="chat-debug-page">
<NexusChatDemo
:AgentBaseUrl="agentBaseUrl"
debug-renderer-chunk
@renderer-chunk-hover="handleRendererChunkHover"
@renderer-chunk-leave="handleRendererChunkLeave"
/>
<aside class="chat-debug-panel" aria-label="Renderer chunk 调试面板">
<template v-if="hoveredRendererChunk">
<p>Renderer:{{ hoveredRendererChunk.rendererType }}</p>
<p>消息:{{ hoveredRendererChunk.messageId ?? '未知消息' }}</p>
<p>序号:{{ hoveredRendererChunk.contentIndex }}</p>
<pre>{{ JSON.stringify(hoveredRendererChunk.deltaContent ?? [], null, 2) }}</pre>
</template>
<p v-else>暂无 Renderer hover 数据。</p>
</aside>
</section>
</template>7. 扩展内容类型
默认内容类型不能覆盖自定义 demo 数据时,可以通过 contentTypeExtensions 追加扩展。
下面示例注册一个 demo-only 的 external_notice 类型。它会在 runtime 返回 type: 'external_notice' 的 content item 时,把原始 item 转换为 DemoChat segment,并使用自定义 renderer 渲染。
vue
<script setup lang="ts">
import { h } from 'vue'
import type { FunctionalComponent } from 'vue'
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
import type {
ContentTypeExtension,
DemoContentRendererProps,
DemoExtensionSegment,
} from '@agent-nexus-ui/chatui'
const agentBaseUrl = 'http://localhost:3000'
type ExternalNoticeSegment = DemoExtensionSegment & {
id: string
type: 'external_notice'
status: 'complete'
title: string
description: string
debugDeltaContent?: unknown[]
}
const ExternalNoticeRenderer: FunctionalComponent<
DemoContentRendererProps<DemoExtensionSegment>
> = (props) => {
const content = props.message.content[props.contentIndex]
if (!isExternalNoticeSegment(content)) {
return h('section', { class: 'external-notice' }, [h('p', 'external_notice 内容缺少必要字段')])
}
return h('section', { class: 'external-notice' }, [
h('p', { class: 'external-notice__label' }, '演示扩展 external_notice'),
h('h3', { class: 'external-notice__title' }, content.title),
h('p', { class: 'external-notice__description' }, content.description),
])
}
const externalNoticeExtension: ContentTypeExtension<DemoExtensionSegment> = {
name: 'external_notice',
segmentHandlers: [
{
name: 'external_notice',
type: 'external_notice',
order: 120,
match: ({ item }) => item.type === 'external_notice',
handle: ({ message, item, debugDeltaContent, helpers }) => ({
action: 'handled',
message: helpers.appendSegment(message, {
id: helpers.createSegmentId(message),
type: 'external_notice',
status: 'complete',
title: helpers.toText(item.title) || '外部扩展通知',
description: helpers.toText(item.description) || '这是 Demo-only 内容类型扩展。',
...helpers.withDebugDeltaContent(debugDeltaContent),
}),
}),
},
],
rendererMatches: [
{
name: 'external_notice',
order: 120,
find: (_message, content) => content.type === 'external_notice',
renderer: ExternalNoticeRenderer,
},
],
}
const contentTypeExtensions: ContentTypeExtension<DemoExtensionSegment>[] = [
externalNoticeExtension,
]
function isExternalNoticeSegment(
content: DemoExtensionSegment | undefined
): content is ExternalNoticeSegment {
return content?.type === 'external_notice'
}
</script>
<template>
<NexusChatDemo
:AgentBaseUrl="agentBaseUrl"
:content-type-extensions="contentTypeExtensions"
debug-renderer-chunk
/>
</template>扩展编写建议:
segmentHandlers[].order用于控制处理顺序,默认扩展通常在前,业务扩展可使用大于默认值的排序,例如120。match应只判断当前 handler 能处理的item.type,避免吞掉其它类型。handle应返回新的 message,不要原地修改传入对象。- 缺字段时 renderer 应显示可见兜底状态,不要只在控制台报错。
- 扩展类型当前属于 demo-only,名称、字段和 renderer 需要与正式协议保持隔离。
8. 替换会话存储
默认情况下,DemoChat 使用 localStorage 保存会话。测试或临时沙箱可以通过 conversationRepository 替换存储实现。
vue
<script setup lang="ts">
import { NexusChatDemo } from '@agent-nexus-ui/chatui'
import type { DemoMessage } from '@agent-nexus-ui/chatui'
interface DemoConversationSummary {
id: string
title: string
createdAt: number
updatedAt: number
}
interface DemoConversationRepository {
loadConversations(): Promise<{
status: 'ready'
conversations: DemoConversationSummary[]
activeConversationId?: string
}>
loadMessages(conversationId: string): Promise<DemoMessage[]>
saveConversation(conversation: DemoConversationSummary): Promise<void>
saveMessages(conversationId: string, messages: DemoMessage[]): Promise<void>
deleteConversation(conversationId: string): Promise<void>
saveActiveConversationId(conversationId: string | undefined): Promise<void>
clear(): Promise<void>
}
const agentBaseUrl = 'http://localhost:3000'
const conversations = new Map<string, DemoConversationSummary>()
const messages = new Map<string, DemoMessage[]>()
let activeConversationId: string | undefined
const memoryRepository: DemoConversationRepository = {
async loadConversations() {
return {
status: 'ready',
conversations: Array.from(conversations.values()),
activeConversationId,
}
},
async loadMessages(conversationId) {
return messages.get(conversationId) ?? []
},
async saveConversation(conversation) {
conversations.set(conversation.id, conversation)
},
async saveMessages(conversationId, nextMessages) {
messages.set(conversationId, nextMessages)
},
async deleteConversation(conversationId) {
conversations.delete(conversationId)
messages.delete(conversationId)
if (activeConversationId === conversationId) {
activeConversationId = undefined
}
},
async saveActiveConversationId(conversationId) {
activeConversationId = conversationId
},
async clear() {
conversations.clear()
messages.clear()
activeConversationId = undefined
},
}
</script>
<template>
<NexusChatDemo :AgentBaseUrl="agentBaseUrl" :conversation-repository="memoryRepository" />
</template>注意:上面是内存示例,页面刷新后会话会丢失。正式调试时仍建议使用默认 localStorage repository。
9. 排查常见问题
组件显示“能力加载失败”
通常是 mock 服务没有启动、AgentBaseUrl 错误或浏览器无法访问 mock 服务。先确认:
bash
node scripts/mock/chat-runtime-service.mjs status也可以访问:
text
http://localhost:3000/chat/abilities/meta-agents能发送消息但没有助手响应
检查 POST /chat/completions 是否返回 text/event-stream,并确认 SSE 每个事件块之间有空行分隔。结束事件需要包含:
text
event: done
data: [DONE]自定义内容类型没有渲染
优先检查四件事:
- runtime 输出的 content item 是否有正确的
type。 segmentHandlers[].match是否命中该type。handle是否通过helpers.appendSegment追加了 segment。rendererMatches[].find是否能命中最终 message.content 中的 segment。
如果仍无法定位,开启 debug-renderer-chunk 并监听 renderer-chunk-hover 查看 contentIndex、rendererType 和 deltaContent。
会话历史异常或旧数据无法加载
DemoChat 默认使用 localStorage。旧 schema 或损坏数据可能导致恢复失败。调试时可以清理当前站点 localStorage,或临时传入内存版 conversationRepository 隔离问题。
10. 使用检查清单
- mock 服务已启动,
AgentBaseUrl指向正确地址。 - 页面通过
@agent-nexus-ui/chatui导入NexusChatDemo,没有导入packages/chatui/src/demo内部文件。 - 页面外层给 DemoChat 留出足够高度。
- 调试 renderer 时已开启
debug-renderer-chunk。 - 内容扩展的
segmentHandlers和rendererMatches都已注册。 - 扩展 renderer 对缺字段、未知字段和未命中内容有可见兜底。
- 会话存储替换实现完整覆盖
DemoConversationRepository的所有方法。 - 如果页面看不到响应,应优先检查真实页面挂载、mock 服务、SSE 流、resolver/renderer 命中和最终 DOM 可见状态。
11. 接口文档
本节集中列出 DemoChat 对调用方暴露的组件接口、事件接口、扩展接口和 runtime 接口。前面章节已经给出常用场景示例;需要查字段时再阅读本节。
组件导入
ts
import { NexusChatDemo } from '@agent-nexus-ui/chatui'Props
| Prop | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
AgentBaseUrl | string | 是 | 无 | Chat runtime 根地址。组件会请求 ${AgentBaseUrl}/chat/abilities/meta-agents 和 ${AgentBaseUrl}/chat/completions。 |
debugRendererChunk | boolean | 否 | false | 开启 Renderer chunk 调试包裹层。开启后 hover renderer 内容会触发调试事件。 |
contentTypeExtensions | ContentTypeExtension[] | 否 | [] | 注入 demo-only 内容类型扩展,包含 segment handlers 和 renderer matches。 |
conversationRepository | DemoConversationRepository | 否 | localStorage repository | 替换会话存储实现,可用于测试、沙箱或禁用持久化。 |
layout | 'auto' | 'portrait' | 否 | auto | 控制横版或竖版布局。 |
Events
| 事件 | 触发时机 | Payload |
|---|---|---|
renderer-chunk-hover | debugRendererChunk 开启后,鼠标移入 renderer 调试区域。 | DemoRendererChunkDebugPayload |
renderer-chunk-leave | debugRendererChunk 开启后,鼠标离开 renderer 调试区域。 | DemoRendererChunkDebugPayload |
DemoRendererChunkDebugPayload
ts
interface DemoRendererChunkDebugPayload {
messageId?: string
contentIndex: number
rendererType: string
chunk: unknown
deltaContent?: unknown[]
}字段说明:
| 字段 | 说明 |
|---|---|
messageId | 当前 renderer 所属的助手消息 id。 |
contentIndex | 当前 renderer 对应的 message.content 序号。 |
rendererType | 命中的 renderer 类型名。 |
chunk | renderer 当前绑定的内容片段。 |
deltaContent | 该片段对应的原始 SSE delta content 数据。 |
ContentTypeExtension
contentTypeExtensions 用于扩展 DemoChat 的内容处理与渲染能力。
ts
interface ContentTypeExtension<TSegment extends DemoSegmentLike = DemoExtensionSegment> {
name: string
segmentHandlers: SegmentHandlerDefinition<TSegment>[]
rendererMatches: RendererMatchDefinition<TSegment>[]
}SegmentHandlerDefinition
ts
interface SegmentHandlerDefinition<TSegment extends DemoSegmentLike = DemoSegmentLike> {
name: string
type: string
order: number
failureMode?: 'visible-error' | 'throw'
match?(context: SegmentHandlerContext<TSegment>): boolean
handle(context: SegmentHandlerContext<TSegment>): SegmentHandlerResult<TSegment>
}字段说明:
| 字段 | 说明 |
|---|---|
name | handler 名称,用于调试和排序定位。 |
type | handler 处理的内容类型。 |
order | 处理顺序,数字越小越靠前。 |
failureMode | handler 异常时的处理方式,默认走可见错误兜底。 |
match | 可选匹配函数,返回 true 时执行 handle。 |
handle | 把 runtime content item 转换为 DemoChat segment。 |
RendererMatchDefinition
ts
interface RendererMatchDefinition<TSegment = DemoExtensionSegment> {
name: string
order: number
failureMode?: 'visible-error' | 'throw'
find: (message: unknown, content: TSegment, contentIndex: number) => boolean
renderer: DemoContentRenderer<TSegment>
attributes?: Record<string, unknown>
shouldWrapDebug?: (message: unknown, content: TSegment, contentIndex: number) => boolean
}字段说明:
| 字段 | 说明 |
|---|---|
name | renderer match 名称,用于调试和排序定位。 |
order | 匹配顺序,数字越小越靠前。 |
failureMode | renderer 异常时的处理方式,默认走可见错误兜底。 |
find | 判断当前 content 是否由该 renderer 渲染。 |
renderer | Vue renderer 组件。 |
attributes | 透传给 TinyRobot renderer match 的附加属性。 |
shouldWrapDebug | 控制是否包裹 debug hover 区域。 |
DemoConversationRepository
conversationRepository 用于替换 DemoChat 的会话持久化实现。
ts
interface DemoConversationRepository<TSegment extends DemoSegmentLike = DemoSegmentLike> {
loadConversations(): Promise<DemoConversationLoadResult>
loadMessages(conversationId: string): Promise<DemoMessage<TSegment>[]>
saveConversation(conversation: DemoConversationSummary): Promise<void>
saveMessages(conversationId: string, messages: DemoMessage<TSegment>[]): Promise<void>
deleteConversation(conversationId: string): Promise<void>
saveActiveConversationId(conversationId: string | undefined): Promise<void>
clear(): Promise<void>
}Runtime 接口概览
DemoChat 会基于 AgentBaseUrl 请求两个 runtime 接口:
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /chat/abilities/meta-agents | 加载 @ 工作智能体能力列表。 |
| POST | /chat/completions | 发送用户输入并接收 SSE 流式响应。 |
GET /chat/abilities/meta-agents
用途:加载 @ 工作智能体能力列表。
兼容返回形状可以是数组,也可以把数组包在 data、metaAgents、items 或 resultObjVO 字段下。
单个智能体会读取:
ts
interface MetaAgentPayload {
alias?: string
displayName?: string
desc?: string
descript?: string
}字段说明:
| 字段 | 说明 |
|---|---|
alias | 工作智能体别名,必需;缺失时该项会被忽略。 |
displayName | 展示名称;缺失时使用 alias。 |
desc | 能力描述。 |
descript | 能力描述兼容字段。 |
示例响应:
json
{
"data": [
{
"alias": "order-agent",
"displayName": "订单智能体",
"desc": "查询订单、售后和物流状态"
}
]
}POST /chat/completions
用途:发送用户输入并接收 SSE 流式响应。
DemoChat 发送的请求体:
json
{
"conversationId": "conv-example",
"stream": true,
"messages": [{ "role": "user", "content": "执行一个完整复杂的订单异常复合场景" }]
}如果用户通过 @ 选择了工作智能体,DemoChat 会额外发送请求头:
text
x-agent-alias: order-agentSSE 流结束时应发送:
text
event: done
data: [DONE]DemoChat 能解析 OpenAI 风格的 choices[0].delta.content,也能解析直接位于 chunk 根对象的 content 数组。
示例 SSE 数据:
text
data: {"content":[{"type":"text","text":"正在分析订单异常。"}]}
data: {"content":[{"type":"tool","name":"queryOrder","status":"complete","text":"订单状态查询完成。"}]}
event: done
data: [DONE]Mock 场景参数
本地 mock 的 POST /chat/completions 支持通过 scenario 查询参数显式选择场景:
text
scenario=text|tool|markdown|gen-ui|error|complex|waiting自然输入时,不传 scenario 也会按关键字推断场景。
也可以直接用 curl 调试 mock SSE:
bash
curl -N -X POST "http://localhost:3000/chat/completions?scenario=complex" \
-H "content-type: application/json" \
-H "x-agent-alias: order-agent" \
-d "{\"conversationId\":\"conv-complex\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"执行一个完整复杂的订单异常复合场景\"}]}"更多 mock 服务细节见 docs/api/chat-runtime-mock-server.md。