Skip to content

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 对话链路是否可见、可输入、可流式输出。
  • 验证 textreasoningtoolskill-triggergen-uireferenceserrorunknown 等默认内容类型渲染。
  • 验证自定义 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 的 AgentBaseUrl prop 使用大写命名,模板中既可以写 :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 check

3. 最小接入

在 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-triggerSkill 触发过程渲染。
gen-uiGenUI 卡片兜底渲染和交互桥接。
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 查看 contentIndexrendererTypedeltaContent

会话历史异常或旧数据无法加载

DemoChat 默认使用 localStorage。旧 schema 或损坏数据可能导致恢复失败。调试时可以清理当前站点 localStorage,或临时传入内存版 conversationRepository 隔离问题。

10. 使用检查清单

  • mock 服务已启动,AgentBaseUrl 指向正确地址。
  • 页面通过 @agent-nexus-ui/chatui 导入 NexusChatDemo,没有导入 packages/chatui/src/demo 内部文件。
  • 页面外层给 DemoChat 留出足够高度。
  • 调试 renderer 时已开启 debug-renderer-chunk
  • 内容扩展的 segmentHandlersrendererMatches 都已注册。
  • 扩展 renderer 对缺字段、未知字段和未命中内容有可见兜底。
  • 会话存储替换实现完整覆盖 DemoConversationRepository 的所有方法。
  • 如果页面看不到响应,应优先检查真实页面挂载、mock 服务、SSE 流、resolver/renderer 命中和最终 DOM 可见状态。

11. 接口文档

本节集中列出 DemoChat 对调用方暴露的组件接口、事件接口、扩展接口和 runtime 接口。前面章节已经给出常用场景示例;需要查字段时再阅读本节。

组件导入

ts
import { NexusChatDemo } from '@agent-nexus-ui/chatui'

Props

Prop类型必填默认值说明
AgentBaseUrlstringChat runtime 根地址。组件会请求 ${AgentBaseUrl}/chat/abilities/meta-agents${AgentBaseUrl}/chat/completions
debugRendererChunkbooleanfalse开启 Renderer chunk 调试包裹层。开启后 hover renderer 内容会触发调试事件。
contentTypeExtensionsContentTypeExtension[][]注入 demo-only 内容类型扩展,包含 segment handlers 和 renderer matches。
conversationRepositoryDemoConversationRepositorylocalStorage repository替换会话存储实现,可用于测试、沙箱或禁用持久化。
layout'auto' | 'portrait'auto控制横版或竖版布局。

Events

事件触发时机Payload
renderer-chunk-hoverdebugRendererChunk 开启后,鼠标移入 renderer 调试区域。DemoRendererChunkDebugPayload
renderer-chunk-leavedebugRendererChunk 开启后,鼠标离开 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 类型名。
chunkrenderer 当前绑定的内容片段。
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>
}

字段说明:

字段说明
namehandler 名称,用于调试和排序定位。
typehandler 处理的内容类型。
order处理顺序,数字越小越靠前。
failureModehandler 异常时的处理方式,默认走可见错误兜底。
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
}

字段说明:

字段说明
namerenderer match 名称,用于调试和排序定位。
order匹配顺序,数字越小越靠前。
failureModerenderer 异常时的处理方式,默认走可见错误兜底。
find判断当前 content 是否由该 renderer 渲染。
rendererVue 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

用途:加载 @ 工作智能体能力列表。

兼容返回形状可以是数组,也可以把数组包在 datametaAgentsitemsresultObjVO 字段下。

单个智能体会读取:

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-agent

SSE 流结束时应发送:

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