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 对话链路是否可见、可输入、可流式输出。
  • 验证 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 的 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 查看 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类型必填默认值说明
AgentBaseUrlstring是无Chat runtime 根地址。组件会请求 ${AgentBaseUrl}/chat/abilities/meta-agents 和 ${AgentBaseUrl}/chat/completions。
debugRendererChunkboolean否false开启 Renderer chunk 调试包裹层。开启后 hover renderer 内容会触发调试事件。
contentTypeExtensionsContentTypeExtension[]否[]注入 demo-only 内容类型扩展,包含 segment handlers 和 renderer matches。
conversationRepositoryDemoConversationRepository否localStorage 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 ​

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

兼容返回形状可以是数组,也可以把数组包在 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-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。