
基于 Vue 3 + Element Plus 的 AI 对话组件库,复刻 element-plus-x 与 open-webui 对话框:从底层气泡、发送框、Markdown 渲染,到上层的对话整框(<FvAiChat>)、分支树消息、Artifacts 预览,一次给全。
- 14 个基础组件(Bubble/Sender/XMarkdown/Thinking/...)自由拼装
- 对话应用层整框
<FvAiChat>:顶栏模型多选、消息列表、分支切换、Artifacts 面板、发送停止一体化
- 树形消息模型(同构 open-webui):一条提问可挂多个模型回答,分支可切换、可重生成
ChatAdapter 适配器协议:内置 OpenAI 兼容实现,可替换自研后端/Ollama
- 流式打字机、思考链折叠、KaTeX/代码高亮(Prism)开箱即用
pnpm add @fv-ui/ai-chat
peerDependencies:vue ^3.4.0、element-plus ^2.7.0、@element-plus/icons-vue ^2.3.0。
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).use(ElementPlus).mount('#app')
<script setup lang="ts">
import { FvAiChat } from '@fv-ui/ai-chat'
import '@fv-ui/ai-chat/dist/style.css'
</script>
<template>
<FvAiChat
title="AI 助手"
base-url="https://api.deepseek.com/v1"
api-key="sk-xxx"
model="deepseek-chat"
style="height: 600px"
/>
</template>
<FvAiChat> 未传 adapter 时,用 baseURL/apiKey/model 构造内置 OpenAIAdapter(走 /chat/completions,SSE 流式)。两个都缺省时发送会报"未配置 adapter 或 baseURL"。
Markdown 渲染(Sanitized by DOMPurify),内置 Prism 代码高亮与 KaTeX。
| Prop |
类型 |
默认 |
说明 |
| content |
string |
—(必填) |
Markdown 源文本 |
| mdPlugins |
PluginWithParams[] |
— |
额外 markdown-it 插件 |
| highlight |
(str, lang) => string |
内置 Prism |
自定义代码高亮 |
| isTyping |
boolean |
— |
打字机模式样式钩子(透传占位) |
| Ref |
说明 |
| getHtml() |
取当前渲染后的 HTML 字符串 |
打字机输出,支持续打/重打/雾化遮罩。
| Prop |
类型 |
默认 |
说明 |
| content |
string |
—(必填) |
全文(流式时增量传入,子集扩展自动续打) |
| isMarkdown |
boolean |
false |
走 XMarkdown 渲染(此时 suffix 不追加) |
| isFog |
boolean | { bgColor?; width? } |
false |
打字中尾部雾化遮罩 |
| typing |
boolean | { step?; interval?; suffix? } |
false |
开启打字机;对象形态可配步长/间隔/光标后缀 |
| Event |
说明 |
| start / writing / finish |
开始打字 / 每次推进 / 全部打完 |
| Ref |
说明 |
| interrupt() / continue() / restart() / destroy() |
暂停 / 继续 / 重打 / 重置清空 |
| renderedContent / isTyping / progress |
已输出文本 / 是否打字中 / 进度 0-1 |
单条气泡(内部按需挂载 Typewriter/XMarkdown)。
| Prop |
类型 |
默认 |
说明 |
| content |
string |
'' |
气泡内容 |
| placement |
'start' | 'end' |
'start' |
start 靠左 / end 靠右 |
| loading |
boolean |
false |
加载态(三点动画,可被 #loading 覆盖) |
| typing |
boolean | TypingConfig |
false |
打字机模式(透传 Typewriter) |
| isMarkdown |
boolean |
false |
内容按 Markdown 渲染 |
| isFog |
boolean | { bgColor?; width? } |
false |
雾化(自定义气泡背景时传 bgColor) |
| variant |
'filled' | 'borderless' | 'outlined' | 'shadow' |
'filled' |
气泡样式 |
| shape |
'round' | 'corner' |
'round' |
圆角 / 方角 |
| avatar |
string |
'' |
头像图片地址 |
| avatarSize |
ElAvatar size | number |
'' |
头像尺寸 |
| avatarGap |
string |
'12px' |
头像与内容间距 |
| avatarShape |
'circle' | 'square' |
'circle' |
头像形状 |
| avatarIcon |
Component |
— |
头像图标(与 avatar 冲突时优先) |
| avatarSrcSet / avatarAlt |
string |
'' / '' |
头像 srcSet/alt |
| avatarFit |
'fill' | 'contain' | 'cover' | 'none' | 'scale-down' |
'cover' |
头像填充方式(el-avatar fit) |
| noPadding |
boolean |
false |
内容区去内边距 |
| noStyle |
boolean |
false |
去气泡样式(透明/无框/无阴影) |
| Event |
说明 |
| avatar-error |
头像加载失败 |
| start / writing / finish |
typing 模式下的打字机事件透传 |
| Slot |
说明 |
| avatar / header / content / loading / footer |
自定义头像 / 头部 / 内容(优先于 content prop)/ 加载态 / 底部 |
| Ref |
说明 |
| interrupt / continue / restart / destroy |
Typewriter 方法转发(非 typing 模式为 no-op) |
| renderedContent / isTyping / progress |
Typewriter 状态转发 |
| twInstance |
内部 Typewriter 原始实例(BubbleList @complete 透传用) |
气泡列表 + 吸底/回底按钮。
| Prop |
类型 |
默认 |
说明 |
| list |
BubbleListItemProps[] |
[] |
列表项(Bubble props + 必填 key) |
| maxHeight |
string |
'' |
滚动容器最大高度(如 '350px'/'60vh') |
| alwaysShowScrollbar |
boolean |
false |
始终显示滚动条 |
| showBackButton |
boolean |
true |
是否显示回底按钮 |
| backButtonThreshold |
number |
80 |
距底超过该像素显示回底按钮 |
| btnLoading |
boolean |
false |
回底按钮加载态 |
| btnColor |
string |
'' |
回底按钮颜色(el-button color) |
| backButtonPosition |
{ bottom?; left? } |
{} |
回底按钮位置偏移 |
| btnIconSize |
number |
16 |
回底按钮图标尺寸 |
| triggerIndices |
'only-last' | 'all' | number[] |
'only-last' |
@complete 触发索引 |
| Event |
说明 |
| complete |
(typewriterInstance, index) 打字完成(按 triggerIndices 过滤后上抛) |
| Slot |
说明 |
| avatar / header / content / loading / footer |
透传给每个 Bubble,作用域 { item, index } |
| Ref |
说明 |
| scrollToTop() / scrollToBottom() / scrollToBubble(index) |
滚动控制 |
输入发送框。
| Prop |
类型 |
默认 |
说明 |
| modelValue |
string(v-model) |
'' |
输入值(缺省内部自持) |
| placeholder |
string |
'' |
占位文案 |
| autosize |
{ minRows?; maxRows? } |
{} |
自增高(行高 22px) |
| submitType |
'enter' | 'shiftEnter' | 'cmdOrCtrlEnter' | 'altEnter' |
'enter' |
提交按键模式 |
| loading |
boolean |
false |
加载态(发送键变停止键,输入区禁用) |
| disabled |
boolean |
false |
禁用(阻断提交) |
| readOnly |
boolean |
false |
只读 |
| clearable |
boolean |
false |
显示清空按钮 |
| inputWidth |
string |
'' |
输入区宽度(variant=default 时有效) |
| variant |
'default' | 'updown' |
'default' |
default 同排 / updown 上输入下操作栏 |
| submitBtnDisabled |
boolean |
false |
附加禁用发送按钮 |
| triggerStrings |
string[] |
[] |
指令触发串(光标前命中弹指令框) |
| triggerPopoverVisible |
boolean(v-model:triggerPopoverVisible) |
false |
指令弹框显隐 |
| triggerPopoverWidth / triggerPopoverLeft |
string |
'' |
指令弹框宽度 / left 偏移 |
| triggerPopoverOffset |
number |
0 |
弹框距触发字符偏移量 |
| triggerPopoverPlacement |
ElPopover placement |
'top-start' |
弹框位置 |
| allowSpeech |
boolean |
false |
显示语音按钮(当前为桩,点击仅 warn) |
| allowEmptySubmit |
boolean |
false |
允许空内容提交(附件场景:只发文件不输文字) |
| Event |
说明 |
| update:modelValue / change |
输入值变化 |
| submit |
(value) 提交(提交后内部清空) |
| cancel |
点击停止按钮 |
| trigger |
(keyword, visible) 命中指令串 |
| recording-change |
语音录制状态变化(预留) |
| paste-file |
(file) 粘贴文件 |
| update:triggerPopoverVisible |
指令弹框显隐同步 |
| Slot |
说明 |
| header / footer |
头部/底部(配合 ref.openHeader/closeHeader) |
| prefix |
输入区前缀 |
| action-list |
自定义操作区(存在时隐藏内置按钮) |
| action-left |
操作栏左侧附加区(与提交按钮同行靠左,如附件回形针) |
| trigger |
指令弹框内容(作用域 { keyword }) |
| Ref |
说明 |
| submit() / cancel() / clear() |
提交 / 停止 / 清空 |
| focus(pos?) / blur() |
pos: 'all' 全选 | 'start' 头 | 'end' 尾(缺省 end) |
| openHeader() / closeHeader() |
显隐 #header 插槽 |
带 @提及 的 Sender(键盘 ↑↓ 导航 / Enter 确认 / Esc 关闭)。
| Prop |
类型 |
默认 |
说明 |
| modelValue |
string(v-model) |
'' |
输入值 |
| userList |
MentionUserInfo[] |
[] |
@ 触发的候选用户(id/name/avatar/pinyin) |
| customTrigger |
MentionCustomTrigger[] |
[] |
自定义触发组 { dialogTitle, prefix, tagList } |
| asyncMatchFun |
(search) => Promise<MentionUserInfo[]> |
— |
异步候选(存在时防抖 300ms,后端过滤) |
事件:update:modelValue / change / submit / cancel / trigger / recording-change / paste-file / update:triggerPopoverVisible / show-at-dialog(弹层显隐);其余 props/事件经 $attrs 透传给 Sender,slot 经 forwardedSlots 显式转发(header/footer/prefix/action-list/trigger)。
| Ref |
说明 |
| submit / cancel / clear / focus / blur / openHeader / closeHeader |
Sender 方法转发 |
| setUserTag(userId) |
光标处程序化插入 @用户 |
| setCustomTag(prefix, id) |
光标处插入自定义触发标签 |
富文本标签编辑器(contenteditable),open-webui 风格标签芯片。
| Prop |
类型 |
默认 |
说明 |
| placeholder |
string |
'' |
占位文案 |
| device |
'pc' | 'h5' |
'pc' |
设备形态(仅影响样式) |
| autoFocus |
boolean |
false |
挂载后聚焦到末尾 |
| variant |
'default' | 'updown' |
'default' |
布局形态 |
| userList |
EditorUser[] |
[] |
@ 候选用户 |
| customTrigger |
EditorCustomTrigger[] |
[] |
自定义触发组 |
| selectList |
EditorSelectGroup[] |
[] |
选择标签分组(openSelectDialog 弹窗数据) |
| maxLength |
number |
0 |
纯文本最大长度(0 不限;性能开销大,仅显式开启) |
| submitType |
'enter' | 'shiftEnter' |
'enter' |
提交按键模式 |
| customStyle |
Record<string, any> |
{} |
根节点自定义样式 |
| loading |
boolean |
false |
加载态(发送键变停止键) |
| disabled |
boolean |
false |
禁用 |
| clearable |
boolean |
false |
显示清空按钮 |
| headerAnimationTimer |
number |
300 |
头部过渡时长(ms) |
| asyncMatchFun |
(search) => Promise<EditorUser[]> |
— |
异步 @ 候选(防抖 300ms) |
| customDialog |
boolean |
false |
选择标签不自弹内置弹窗,仅上抛 show-select-dialog |
| Event |
说明 |
| submit / change |
(SubmitResult) SubmitResult = { text, html, tags: { userTags, selectTags, inputTags, customTags } } |
| cancel |
点击停止按钮 |
| show-at-dialog / show-select-dialog / show-tag-dialog |
各弹层显隐 |
| Slot |
说明 |
| header / footer / prefix / action-list |
同 Sender |
| tag-tip |
输入标签 tip 弹层内容(作用域 { nodeId }) |
| Ref |
说明 |
| getCurrentValue() |
取 { text, html, tags } |
| focusToStart() / focusToEnd() / blur() / selectAll() / clear() |
光标与清空 |
| setText(text) / setHtml(html) / setMixTags(rows) |
程序化写入 |
| setUserTag(id) / setCustomTag(prefix, id) / setInputTag(id, name) / setSelectTag(list) |
插入各类标签 |
| customSetUser(user) / customSetTag(tag) |
外部直传对象插入(不经列表查表) |
| updateSelectTag(method, options) |
选择标签更新:'insertTag'/'updateTagValue'/'deleteTag'/'updateTagStyle' |
| openSelectDialog() / openTipTag(nodeId?) / closeTipTag() |
弹窗控制 |
| openHeader() / closeHeader() |
显隐 #header |
| chat / opNode(method, options) / chatState() |
底层文档模型:chat.insert/update/delete/append;opNode 按方法名分发 |
思考链折叠面板。
| Prop |
类型 |
默认 |
说明 |
| modelValue |
boolean(v-model) |
false |
展开/收起 |
| content |
string |
'' |
思考内容(常配合 #content 插槽嵌 XMarkdown) |
| status |
'start' | 'thinking' | 'end' | 'error' |
'start' |
思考状态 |
| disabled |
boolean |
false |
禁用切换 |
| autoCollapse |
boolean |
false |
status 变为 end 时自动收起 |
| buttonWidth |
string |
'' |
切换按钮区宽度 |
| maxWidth |
string |
'' |
内容区最大宽度 |
| color / backgroundColor |
string |
'' |
前景色 / 背景色 |
| Event |
说明 |
| update:modelValue |
展开/收起同步 |
| Slot |
说明 |
| status-icon / label / arrow |
自定义状态图标 / 文案 / 箭头 |
| content / error |
思考内容 / error 态内容 |
| Ref |
说明 |
| modelValue / toggle() |
展开状态 / 手动切换 |
思维链步骤条。
| Prop |
类型 |
默认 |
说明 |
| items |
ThoughtChainItem[] |
[] |
步骤项 { key, title?, status?: 'pending'/'success'/'loading'/'error', icon?, description? } |
| direction |
'vertical' | 'horizontal' |
'vertical' |
排列方向 |
| Slot |
说明 |
| content |
覆盖默认 title/description(作用域 { item, index }) |
空态欢迎页。
| Prop |
类型 |
默认 |
说明 |
| icon |
string | Component |
'' |
图标(字符串为图片地址;未传渲染内置对话图标) |
| name |
string |
'' |
名称 |
| description |
string |
'' |
描述(纯文本) |
| Slot |
说明 |
| icon / name / description |
覆盖对应区块 |
建议 prompt 网格。
| Prop |
类型 |
默认 |
说明 |
| prompts |
PromptItem[] |
[] |
建议项 { key(必填), label?, description?, icon?(EP 图标名字符串或组件), disabled? } |
| title |
string |
'' |
网格标题 |
| Event |
说明 |
| select |
(item) 点击(disabled 项不触发) |
| Slot |
说明 |
| default |
覆盖整卡(作用域 { item, index }) |
单个文件卡片。文档模式按扩展名自动映射类型图标:pdf/doc/txt→文档、xls/csv→图表、ppt→演示、mp4→胶片、mp3→耳机、zip→文件夹、html/url→链接、apk→手机、exe→芯片,未命中回落通用文档图标。
| Prop |
类型 |
默认 |
说明 |
| name |
string |
'' |
文件名 |
| fileType |
'image' | 'file' |
'file' |
image 渲染缩略图,file 渲染文档图标卡 |
| size |
number | string |
0 |
文件大小(字节自动格式化为 B/KB/MB;字符串原样展示;0 不显示) |
| url |
string |
'' |
图片地址(image 模式缩略图) |
| status |
'loading' | 'success' | 'error' |
'success' |
loading spinner / error 红边 |
| imageSize |
string |
'' |
缩略图尺寸(width/height 同值) |
| onDelete |
() => void |
— |
删除按钮回调 |
| Slot |
说明 |
| image |
覆盖图片区(默认 url 缩略图,无 url 渲染 Picture 占位) |
附件选择与列表(v-model)。
| Prop |
类型 |
默认 |
说明 |
| modelValue |
object |
[] |
附件列表(项含 uid/name/size/url/fileType/status/raw) |
| accept |
string |
'' |
input accept,如 'image/*' |
| tip |
string |
'' |
内联提示文案 |
| count |
number |
0 |
最大附件数(超出走 @exceed;0 不限) |
| beforeUpload |
(file) => boolean | Promise |
— |
上传前校验,返回 false 拦截 |
| layout |
'inline' | 'triggerOnly' | 'listOnly' |
'inline' |
布局:inline 回形针+列表+提示一行;triggerOnly 只渲染回形针按钮;listOnly 只渲染文件列表(拆分布局供整框组合:按钮进操作栏、列表进输入区上方) |
| Event |
说明 |
| update:modelValue / change |
列表变化(全量数组) |
| exceed |
(file) 超出 count |
| delete |
(item) 删除某项 |
| Slot |
说明 |
| list-item |
覆盖项渲染(作用域 { item, index }) |
| list-thumb |
覆盖图片缩略图(作用域 { item, index }) |
| Ref |
说明 |
| openSelect() |
程序化打开文件选择 |
| retry(item) |
error 项重新置为 loading(上传逻辑由外部接线) |
会话列表(按 group 分组)。
| Prop |
类型 |
默认 |
说明 |
| items |
ConversationItem[] |
[] |
会话项 { key(必填), label(必填), group?, disabled? } |
| active |
string | number(v-model:active) |
undefined |
当前激活会话 key |
| showBuiltInMenuType |
'hover' | 'always' |
'hover' |
内置菜单按钮显隐方式 |
| groupable |
(a, b) => number |
— |
分组排序比较器(缺省保持插入顺序) |
| Event |
说明 |
| update:active / change |
选中会话(key / item) |
| menu-command |
({ item, command: 'rename'/'delete', value? }) 菜单指令;rename 确认后带新名称 |
| Slot |
说明 |
| group-title |
分组标题(作用域 { group }) |
| item |
覆盖行内容(作用域 { item, active }) |
| menu |
覆盖菜单项(默认重命名/删除;作用域 { item }) |
| Ref |
说明 |
| handleMenuCommand({ item, command }) |
程序化触发菜单指令转发(纯转发,同步上抛 menu-command) |
| renamePrompt(key) |
弹重命名输入框,确认后上抛 menu-command |
| deleteConfirm(key) |
弹删除确认框,确认后上抛 menu-command |
对话整框:顶栏(标题+模型多选)、消息区(空态 Welcome/Prompts)、输入区(Sender)、可选 Artifacts 面板。
| Prop |
类型 |
默认 |
说明 |
| model |
string |
'' |
默认模型(未传 models 时作为单模型) |
| models |
string[] |
[] |
可选模型列表(有值时顶栏多选选择器) |
| baseURL |
string |
'' |
OpenAI 兼容服务地址(未传 adapter 时构造 OpenAIAdapter) |
| apiKey |
string |
'' |
服务密钥 |
| adapter |
ChatAdapter |
undefined |
聊天适配器(与 baseURL 二选一,优先) |
| messages |
MessageNode |
undefined |
消息链(半受控,见下) |
| title |
string |
'' |
顶栏标题(同时作空态欢迎页名称) |
| placeholder |
string |
'' |
输入框占位 |
| suggestions |
AiChatSuggestion[] |
[] |
空态建议列表(Prompts 项,key 必填) |
| features |
AiChatFeatures |
{} |
特性开关 { artifacts?, tts?, queue?, continueGenerate?, fullscreenEditor? }(当前仅 artifacts 生效) |
| allowAttachments |
boolean |
false |
开启输入区附件上传(回形针在操作栏左侧与提交按钮同行,已选列表在输入框上方;只传附件不输文字也可提交) |
| accept |
string |
'' |
附件 input accept 属性(透传 Attachments),如 'image/*' |
| attachmentLimit |
number |
0 |
附件数量上限(0 不限,超限走 @attachment-exceed) |
| theme |
'light' | 'dark' | 'auto' |
'light' |
主题(向 html 注入/移除 dark class) |
v-model:messages 半受控语义:外部 messages 仅在 setup 时一次性深拷贝灌入(用于恢复历史);组件内部每次变更全量上抛 update:messages(同帧去抖),父组件需经 v-model 回写维持单向数据流;父组件后续直接改 prop 不会回灌生效。
| Event |
说明 |
| update:messages |
(MessageNode[]) 全量消息链(去抖后) |
| send |
(text, files?) 用户发送(allowAttachments 开启时 files 为降维后的 ChatFile[]) |
| message-complete |
(message) 链尾 assistant 生成完成 |
| error |
(err, message?) 生成出错 |
| branch-change |
(dir, message) 分支切换 |
| attachment-exceed |
(file) 附件超过 attachmentLimit 上限 |
| Slot |
说明 |
| message-avatar / message-actions |
透传给 AiMessages(作用域见 AiMessage) |
| empty |
覆盖空态整区 |
| sender-prefix |
输入区前缀 |
| Ref |
说明 |
| sendMessage(text, files?) |
程序化发送 |
| stop() |
停止生成(保留半截内容,静默收口不发 complete/error) |
| regenerate(messageId?) |
重生成(缺省链尾 assistant) |
| appendToMessage(id, text) |
向指定消息追加文本 |
| clearMessages() |
清空消息树 |
| exportTxt() |
导出当前链为纯文本 |
| openArtifact(key) |
程序化打开某个 artifact |
消息列表(空态渲染 Welcome + Prompts),吸底滚动。
| Prop |
类型 |
默认 |
说明 |
| messages |
MessageNode[] |
—(必填) |
当前链消息(根 → 焦点) |
| nodes |
Record<string, MessageNode> |
{} |
全量节点表(分支计算需查父 childrenIds) |
| icon |
string | Component |
'' |
空态欢迎页图标 |
| name |
string |
'' |
空态欢迎页名称 |
| description |
string |
'' |
空态欢迎页描述 |
| suggestions |
AiMessagesSuggestion[] |
[] |
空态建议列表 |
| suggestionsTitle |
string |
'' |
空态建议区标题 |
| scrollThreshold |
number |
40 |
吸底阈值(px):距底不超过该值才自动跟随 |
| showActions |
boolean |
true |
显示消息操作按钮(复制/重生成) |
| showBranchSwitch |
boolean |
true |
显示分支切换器 |
| Event |
说明 |
| select / copy / regenerate / branch-change |
建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat |
| Slot |
说明 |
| empty |
覆盖空态整区 |
| message-avatar / message-actions |
透传给每条 AiMessage |
| Ref |
说明 |
| scrollToBottom(smooth?) / scrollToTop() |
滚动控制 |
单条消息(Markdown 正文 + 思考链折叠 + 附件 + 操作区)。
| Prop |
类型 |
默认 |
说明 |
| message |
MessageNode |
—(必填) |
消息节点 |
| branch |
AiMessageBranchInfo |
undefined |
分支元数据 { count, index };缺省视为单分支 |
| showActions |
boolean |
true |
显示复制/重生成(仅 assistant) |
| showBranchSwitch |
boolean |
true |
显示分支切换器(branch.count > 1 时) |
| autoCollapse |
boolean |
true |
Thinking autoCollapse 透传 |
| Event |
说明 |
| copy / regenerate / branch-change |
(message) / (message) / (dir, message) |
| Slot |
说明 |
| message-avatar |
自定义头像(作用域 { message }) |
| message-files |
自定义附件渲染(作用域 { message, files }) |
| message-actions |
自定义操作区(作用域 { message }) |
| Ref |
说明 |
| message |
当前消息节点(computed) |
Artifacts 预览面板(iframe 沙箱,从消息 html / svg 代码块提取)。
| Prop |
类型 |
默认 |
说明 |
| artifacts |
AiArtifactItem[] |
[] |
artifact 列表 { key(必填), type: 'html'/'svg', content, title? } |
| activeKey |
string |
'' |
当前展示的 key;为空时面板隐藏 |
| Event |
说明 |
| close / change |
关闭 / 切换(key) |
| navigate |
(href) iframe 内点击外链被拦截上抛 |
| Ref |
说明 |
| open(key) / close() |
程序化显隐 |
聊天编排:发送 → 流式回填 → 队列/重生成/续写。
const chat = useChat({
adapter: () => OpenAIAdapter({ baseURL, apiKey }),
models: () => selectedModels.value,
initial: { messages, currentId },
onToolEvent: (ev) => { }
})
send(text, files?):生成中调用被拒;多模型时各模型一个 assistant 节点并发回填
stop():中止全部在途请求,在途节点置 done(保留半截内容)
regenerate(messageId?):回退焦点到父节点重发生成新分支(旧回答保留)
continueGenerate(messageId?):以既有内容为前缀续写,增量追加到同一节点
enqueue(text, files?):生成中入队,完成后自动 flush 队首
onToolEvent:适配器 onTool 回调的透传挂点(如 DetOmsChatAdapter 的 tool{status} 事件)
det-oms(/api/ai-chat/*)风格聊天适配器:五事件 SSE 协议(message_start / tool / delta / message_end / error),POST /ai-chat/message/send。会话管理(列表/归档/删除等)由消费方直接调后端,不经此适配器。
const adapter = DetOmsChatAdapter({
baseURL: '/api',
headers: () => ({ Authorization: 'Bearer ' + token }),
sessionId: () => currentSessionId.value,
errorMessages: { 90602: '已有进行中的对话' }
})
- 同步段失败(Content-Type 非
text/event-stream)按 JSON 错误 {code,message} 走 onError;命中 errorMessages 时用映射文案(带 code 的 DetOmsError)
message_end 的 messageId(最终文本行 id)经 onBackendMessageId 回填到 MessageNode.backendId——多轮工具循环下与 message_start 的首轮 id 不同,UI 落库/对账以它为准
tool 事件经 onTool → useChat.onToolEvent → AiChat @tool-event 上抛,消费方展示「正在查询库存…」等中间态
- 配套 DTO 类型:
DetOmsSessionItem / DetOmsMessage / DetOmsSessionDetail / DetOmsMessagePage(消费方直接调会话管理端点时用)
树形消息状态中枢(响应式包装 createMessageTree)。
返回:messages(全量节点表)、currentId、chain(当前链 computed)、add、remove、edit、appendContent、navigate(id?, dir?)、setCurrent(id)、setStorage(adapter)、load()、flush()、toJSON()。绑定存储后树变更自动去抖(150ms)落盘。
SSE 流消费:startStream({ readableStream, transformStream? })、cancel()、data: Ref<SSEEvent[]>、error、isLoading。内部完成 \r\n/跨 chunk 边界归一化。
useSend():send({ url, method?, headers?, body?, transformStream?, onChunk? })(SSE data 为 JSON 且含 content 字段时逐块回调)、abort()、loading、error。在飞行守卫:上一个 send 未结束时新 send 被忽略。
XRequest(url, options):Promise 化,resolve 累计全文。
语音录制 + 实时转写:start() / stop() / isRecording / text / audioUrl / error。SpeechRecognition 不可用时降级为仅录音。
实现 ChatAdapter 接口即可接入任意后端(自研网关、Ollama、非 OpenAI 协议):
import type { ChatAdapter } from '@fv-ui/ai-chat'
const myAdapter: ChatAdapter = {
async send(req, handlers) {
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(req),
signal: req.signal
})
if (!res.ok) { handlers.onError(new Error(`HTTP ${res.status}`)); return }
handlers.onDelta('一段正文')
handlers.onReasoning('一段思考链')
handlers.onUsage({ prompt_tokens: 10, completion_tokens: 20 })
handlers.onDone()
} catch (e) {
if (req.signal?.aborted) return
handlers.onError(e instanceof Error ? e : new Error(String(e)))
}
},
abort() {
}
}
<FvAiChat title="AI" :adapter="myAdapter" />
一次 send 对应一次完整生成:持续回调 onDelta 增量,最后必须 onDone() 或 onError();stop() 时组件会调 abort()。
持久化可选 StorageAdapter(get/set/remove,async),内置 createLocalStorageAdapter()(localStorage 不可用时自动降级内存)与 createMemoryAdapter();经 useMessages().setStorage() 绑定。
组件样式基于 Element Plus CSS 变量,覆盖 EP 变量即完成主题定制:
:root {
--el-color-primary: #7c3aed;
}
高级定制可直接 @use 源 SCSS 入口:
@use '@fv-ui/ai-chat/src/styles/index.scss' with (
// scss 变量(若组件有定义)
);
源码 SCSS 入口仅供源码依赖场景;npm 消费方推荐使用构建产物 dist/style.css + CSS 变量覆盖。
<FvAiChat theme="dark"> 或 theme="auto"(跟随 prefers-color-scheme):组件向 <html> 注入/移除 dark class,配合 element-plus 的暗色变量生效:
import 'element-plus/theme-chalk/dark/css-vars.css'
<FvAiChat theme="auto" ... />
注意:组件卸载时会移除 html.dark(单实例假设);多实例并用暗色需自行管理 class。
代码高亮默认配色由组件样式提供;要换成其他 Prism 主题,在 dist/style.css 之后引入覆盖:
import '@fv-ui/ai-chat/dist/style.css'
import '@fv-ui/ai-chat/styles/prism-tomorrow.min.css'
内置主题:prism(默认)、prism-coy、prism-dark、prism-funky、prism-okaidia、prism-solarizedlight、prism-tomorrow、prism-twilight。
Q:页面是白屏/样式错乱?
A:确认已引入 element-plus/dist/index.css 与 @fv-ui/ai-chat/dist/style.css,且 app.use(ElementPlus)。
Q:<FvAiChat> 发送报"未配置 adapter 或 baseURL"?
A:adapter 与 baseURL 至少传一个。
Q:外层改了 v-model:messages 为什么界面没变?
A:半受控语义:外部 messages 只在初始灌入一次,后续界面变化全量上抛 update:messages,请经 v-model 回写而不是直接改源数组。
Q:多模型怎么用?
A:<FvAiChat model="a" :models="['a', 'b']"> 顶栏出现多选,选中的多个模型并行生成,回答互为分支可切换。
Q:features 里的 tts/queue/continueGenerate/fullscreenEditor?
A:接口已预留,当前版本仅 artifacts 生效,其余待后续版本接线。
Q:KaTeX 公式没渲染?
A:XMarkdown 内置 @vscode/markdown-it-katex,确认公式语法正确即可;无需额外引入。
Q:Nuxt/SSR 环境首渲染报错 "DOMPurify 不可用:当前环境无 window"?
A:XMarkdown 依赖浏览器 window(DOMPurify 消毒),不支持服务端首渲染。Nuxt 下请用 <ClientOnly> 包裹(或客户端挂载后再渲染):
<ClientOnly>
<FvAiChat ... />
</ClientOnly>
以下能力数据层已就绪,组合 UI 待后续版本接线(v0.2 计划):
- 消息列表懒加载:
useMessages 数据面支持分页注入,AiMessages 暂全量渲染;超长对话建议自行分页。
- 消息操作条:当前内置复制/重新生成;编辑/删除/继续生成/朗读请经
useChat(editMessage/removeMessage/continueGenerate)或 #message-actions 插槽自建入口。
- 队列卡片:
useChat.enqueue 可用,features.queue 展示 UI 待接线。
- 语音输入:
Sender 的 allowSpeech 当前为桩(点击仅提示);useRecord 可独立使用。
- 多实例暗色互斥:
theme 采用单写者假设,多实例同屏请自行管理 html.dark。
包入口导出全部组件、useChat/useMessages/useXStream/useSend/XRequest/useRecord、OpenAIAdapter、DetOmsChatAdapter(+ DetOmsError 及配套 DTO 类型)、createMessageTree、createLocalStorageAdapter/createMemoryAdapter、createSSEParser 及全部公共类型(MessageNode、ChatAdapter、ChatFile、MessageUsage、SSEEvent 等),消费方:
import { FvAiChat, OpenAIAdapter, useChat } from '@fv-ui/ai-chat'
import type { MessageNode, ChatAdapter } from '@fv-ui/ai-chat'