vue2实现类似chatgpt和deepseek的AI对话流打字机效果,实现多模型同时对话

实现多模型同时对话

功能特点:

1、抽离对话框成单独组件ChatBox.vue,在新增模型对比窗口时可重复利用

2、通过sse与后台实时数据流,通过定时器实现打字效果

3、适应深度思考内容输出,可点击展开与闭合

4、可配置模型参数,本地存储当前模型参数和对话记录,页面关闭时清除

5、通过是否响应<think>标签来识别是否有深度思考内容

6、通过响应的finishReason字段,识别回答是否已停止(null正常回答,stop回答结束,length超出文本)

安装插件

highlight.js、markdown-it

创建对话窗口组件ChatBox.vue

<template><el-card class="box-card"><div slot="header" class="clearfix"><div class="header-item-box"><vxe-select v-model="modelType"><vxe-optionv-for="(item, i) in modelTypeList":key="i":value="item.id":label="item.modelName"></vxe-option></vxe-select><div><vxe-button@click="handleDeleteCurModel"type="text"icon="iconfont icon-zhiyuanfanhui9"v-if="modelIndex !== 1"></vxe-button><vxe-button@click="handleParamsConfig"type="text"icon="vxe-icon-setting"></vxe-button></div></div></div><div ref="logContainer" class="talk-box"><div class="talk-content"><el-rowv-for="(item, i) in contentList":key="i"class="chat-assistant"><transition name="fade"><div:class="['answer-cont', item.type === 'user' ? 'end' : 'start']"><img v-if="item.type == 'assistant'" :src="welcome.icon" /><div :class="item.type === 'user' ? 'send-item' : 'answer-item'"><divv-if="item.type == 'assistant'"class="hashrate-markdown"v-html="item.message"/><div v-else>{{ item.message }}</div></div></div></transition></el-row></div></div><ModelParamConfig ref="ModelParamConfigRef"></ModelParamConfig></el-card>
</template><script>
import hljs from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import { chatSubmit, chatCancel } from '@/api/evaluationServer/modelTalk.js'
import { mapGetters } from 'vuex'
import { sse } from '@/utils/sse.js'
import ModelParamConfig from '@/views/evaluationServer/modelTalk/components/ModelParamConfig.vue'
window.hiddenThink = function (index) {// 隐藏思考内容if (document.getElementById(`think_content_${index}`).style.display == 'none') {document.getElementById(`think_content_${index}`).style.display = 'block'document.getElementById(`think_icon_${index}`).classList.replace('vxe-icon-arrow-up', 'vxe-icon-arrow-down')} else {document.getElementById(`think_content_${index}`).style.display = 'none'document.getElementById(`think_icon_${index}`).classList.replace('vxe-icon-arrow-down', 'vxe-icon-arrow-up')}
}
export default {props: {modelTypeList: {type: Array,default: () => []},modelIndex: {type: Number,default: 1},/*** 模型窗口*/modelDomIndex: {type: Number,default: 0}},components: { ModelParamConfig },computed: {...mapGetters(['token'])},data() {return {modelType: '',inputMessage: '',contentList: [],answerTitle: '',thinkTime: null,startAnwer: false,startTime: null,endTime: null,typingInterval: null,msgHight: null,welcome: {title: '',desc: '',icon: require('@/assets/images/kubercloud-logo.png')},markdownIt: {},historyList: [], //记录发送和回答的纯文本 。user提问者,assistant回答者lastScrollHeight: 0}},mounted() {setTimeout(() => {this.markdownIt = MarkdownIt({html: true,linkify: true,highlight: function (str, lang) {if (lang && hljs.getLanguage(lang)) {try {return hljs.highlight(str, { language: lang }).value} catch (__) {console.log(__)}}return ''}})if (this.modelTypeList && this.modelTypeList.length) {this.modelType = this.modelTypeList[0].id}}, 500)},methods: {sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms))},handleDeleteCurModel() {this.$emit('handleDeleteCurModel', this.modelIndex)},clearHistory() {this.contentList = []this.historyList = []},async sendMessage({ message }) {this.inputMessage = messageconst name = this.modelTypeList.find(item => item.id === this.modelType)?.nameif (!name) returnlet params = {historyList: [...this.historyList],text: this.inputMessage,deployId: this.modelType,temperature: 1,maxTokens: 1024,topP: 1,seed: '',stopSequence: '',modelName: name}let modelParams = sessionStorage.getItem('modelTalkParams-' + this.modelIndex)if (modelParams) {modelParams = JSON.parse(modelParams)params = {...params,...modelParams,modelName: name}}const res = await chatSubmit(params)const { code, obj } = res.dataif (code == 1) {this.chatId = objconst thinkIngTxt = this.$t('modelTalk.tips.thinkIng') //思考中……this.contentList.push({ type: 'user', message: this.inputMessage })this.historyList.push({ role: 'user', content: this.inputMessage })this.contentList.push({type: 'assistant',message: `<div class="think-time">${thinkIngTxt}</div>`})this.answerTitle =this.answerTitle || this.contentList[0].message.substring(0, 20)this.scrollToBottom()this.lastScrollHeight = 0this.connectSSE(obj)}},// 启动连接connectSSE(chatId) {this.inputMessage = ''let buffer = ''let displayBuffer = ''this.startTime = nullthis.endTime = nullthis.thinkTime = nulllet len = this.contentList.lengthlet index = len % 2 === 0 ? len - 1 : lenlet historylen = this.historyList.lengthlet historyIndex = historylen % 2 === 0 ? historylen - 1 : historylenthis.isTalking = truelet anwerContent = ''this.connectionId = sse.connect({url: '/api/stream/chat',params: {chatId}},{onOpen: id => {console.log(`连接[${id}]已建立`)},onMessage: async (data, id) => {await this.sleep(10)try {var { content, finishReason } = data} catch (e) {console.log('e: ', e)}if (data && content) {let answerCont = contentbuffer += answerContanwerContent += answerContthis.$set(this.historyList, historyIndex, {role: 'assistant',content: anwerContent})const thinkIngTxt = this.$t('modelTalk.tips.thinkIng') //思考中……const deeplyPonderedTxt = this.$t('modelTalk.tips.deeplyPondered') //已深度思考// 单独记录时间if (answerCont.includes('<think>') ||answerCont.includes('</think>')) {// 执行替换逻辑if (answerCont.includes('<think>')) {answerCont = `<div class="think-time">${thinkIngTxt}</div><section id="think_content_${index}">`buffer = buffer.replaceAll('<think>', answerCont)this.startTime = Math.floor(new Date().getTime() / 1000)}if (answerCont.includes('</think>')) {answerCont = `</section>`this.endTime = Math.floor(new Date().getTime() / 1000)// 获取到结束直接后,直接展示收起按钮this.thinkTime = this.endTime - this.startTimebuffer = buffer.replaceAll(`<div class="think-time">${thinkIngTxt}</div>`,`<div class="think-time">${deeplyPonderedTxt}(${this.thinkTime}S)<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="vxe-icon-arrow-down"></i></div>`).replaceAll('</think>', answerCont).replaceAll(`<section id="think_content_${index}"></section>`,'')}// 避免闪动 直接修改数据,这里不需要打字效果displayBuffer = buffer // 同步displayBuffer避免断层this.$set(this.contentList, index, {type: 'assistant',message: this.markdownIt.render(buffer)})this.scrollToBottomIfAtBottom()} else {// 逐字效果if (!this.typingInterval) {this.typingInterval = setInterval(() => {if (displayBuffer.length < buffer.length) {const remaining = buffer.length - displayBuffer.length// 暂定一次性加3个字符const addChars = buffer.substr(displayBuffer.length,Math.min(3, remaining))displayBuffer += addCharslet markedText = this.markdownIt.render(displayBuffer)this.$set(this.contentList, index, {type: 'assistant',message: markedText})this.scrollToBottomIfAtBottom()} else {clearInterval(this.typingInterval)this.typingInterval = null}}, 40)}}} else {if (['stop', 'length'].includes(finishReason)) {this.scrollToBottomIfAtBottom()this.isTalking = falsethis.$emit('handleModelAnswerEnd', {modelIndex: this.modelIndex,contentList: this.contentList,finishReason: finishReason})}}},onError: (err, id) => {console.error(`连接[${id}]错误:`, err)},onFinalError: (err, id) => {console.log(`连接[${id}]已失败`)}})},disconnectSSE() {sse.close()},async handleModelStop() {const res = await chatCancel({ chatId: this.chatId })const { code } = res.dataif (code == 1) {this.handleCleanOptionAndData()}},handleCleanOptionAndData() {this.disconnectSSE()this.isTalking = falsesetTimeout(() => {//清除强制停止的对话记录this.historyList = this.historyList.slice(0, -2)}, 100)},scrollToBottom() {this.$nextTick(() => {const logContainer = document.querySelectorAll(`.chat-content-box .el-card__body`)[this.modelDomIndex]if (logContainer) {logContainer.scrollTop = logContainer.scrollHeight}})},scrollToBottomIfAtBottom() {this.$nextTick(() => {const logContainer = document.querySelectorAll(`.chat-content-box .el-card__body`)[this.modelDomIndex]if (!logContainer) returnconst threshold = 100const distanceToBottom =logContainer.scrollHeight -logContainer.scrollTop -logContainer.clientHeight// 获取上次滚动位置const lastScrollHeight = this.lastScrollHeight || 0// 计算新增内容高度const deltaHeight = logContainer.scrollHeight - lastScrollHeight// 如果新增内容高度超过阈值50%,强制滚动if (deltaHeight > threshold / 2) {logContainer.scrollTop = logContainer.scrollHeight}// 否则正常滚动逻辑else if (distanceToBottom <= threshold) {logContainer.scrollTop = logContainer.scrollHeight}// 更新上次滚动位置记录this.lastScrollHeight = logContainer.scrollHeight})/* logContainer.scrollTo({top: logContainer.scrollHeight,behavior: 'smooth'}) */},handleParamsConfig() {const modelRow = this.modelTypeList.find(item => item.id === this.modelType)this.$refs.ModelParamConfigRef.handleShow({...modelRow,modelIndex: this.modelIndex})}}
}
</script><style lang="scss" scoped>
.box-card {flex: 1;margin-bottom: 5px;height: 100%;display: flex;flex-direction: column;.header-item-box {display: flex;justify-content: space-between;align-items: center;}.chat-text-box {overflow: hidden;overflow-y: auto;}::v-deep .el-card__body {padding: 20px;flex: 1;overflow-y: auto;.talk-box {.talk-content {background-color: #fff;color: #324659;overflow-y: auto;box-sizing: border-box;padding: 0px 20px;.chat-assistant {display: flex;margin-bottom: 10px;.send-item {max-width: 60%;word-break: break-all;padding: 10px;background: #eef6ff;border-radius: 10px;color: #000000;white-space: pre-wrap;font-size: 13px;}.answer-item {line-height: 30px;color: #324659;}}.answer-cont {position: relative;display: flex;width: 100%;> img {width: 32px;height: 32px;margin-right: 10px;}&.end {justify-content: flex-end;}&.start {justify-content: flex-start;}}}.chat-sse {min-height: 100px;max-height: 460px;}.chat-message {height: calc(100vh - 276px);}.thinking-bubble {height: calc(100vh - 296px);}}.chat-add {width: 111px;height: 33px;background: #dbeafe;border-radius: 6px !important;font-size: 14px !important;border: 0px;color: #516ffe !important;&:hover {background: #ebf0f7;}.icon-tianjia1 {margin-right: 10px;font-size: 14px;}}.talk-btn-cont {text-align: right;height: 30px;margin-top: 5px;}}
}
</style>

创建主页面index.vue

<template><div class="x-container-wrapper chat-page-box"><div class="chat-content-box"><template v-for="(item, i) in chatBoxs"><ChatBox:key="item.id"v-if="item.show":ref="el => setChatBoxRef(el, item.id)":modelTypeList="modelTypeList":modelIndex="item.id":modelDomIndex="getModelDomIndex(i)"@handleDeleteCurModel="handleDeleteCurModel"@handleModelAnswerEnd="handleModelAnswerEnd"></ChatBox></template></div><div class="middle-option-box"><vxe-buttontype="text"icon="iconfont icon-qingchu":disabled="hasAnsweringStatus"@click="handelAllHistoryAnswer"></vxe-button><vxe-buttontype="text"icon="vxe-icon-square-plus-square":disabled="hasAnsweringStatus"style="font-size: 24px"@click="handleAddModel"></vxe-button></div><div class="bottom-send-box"><div class="talk-send"><textarea@keydown="handleKeydown"ref="input"v-model="inputMessage"@input="adjustInputHeight":placeholder="$t('modelTalk.placeholder.sendMessage')":rows="2"/><div class="talk-btn-cont" style="text-align: right; font-size: 18px"><!-- 发送消息 --><vxe-buttonv-if="!hasAnsweringStatus"type="text"@click="sendMessage":disabled="sendBtnDisabled"icon="vxe-icon-send-fill"style="font-size: 24px"></vxe-button><!-- 停止回答 --><vxe-buttonv-elsetype="text"@click="handleModelStop"icon="vxe-icon-radio-checked"style="font-size: 24px"></vxe-button></div></div></div></div>
</template><script>
import 'highlight.js/styles/a11y-dark.css'
import { listModels } from '@/api/evaluationServer/modelTalk.js'
import ChatBox from '@/views/evaluationServer/modelTalk/components/ChatBox.vue'
import * as notify from '@/utils/notify'
export default {components: { ChatBox },data() {return {modelType: '',modelTypeList: [],inputMessage: '',eventSourceChat: null,answerTitle: '',thinkTime: null,startAnwer: false,startTime: null,endTime: null,typingInterval: null,msgHight: null,chatBoxs: [{ id: 1, content: '', show: true, isAnswerIng: false },{ id: 2, content: '', show: false, isAnswerIng: false },{ id: 3, content: '', show: false, isAnswerIng: false }]}},mounted() {this.getModelList()},methods: {async getModelList() {const params = { offset: 0, limit: '10000' }const res = await listModels(params)const { code, rows } = res.dataif (code == 1) {this.modelTypeList = rowsif (rows && rows.length) {this.modelType = rows[0].id}}},/* 清除提问和回答记录 */handelAllHistoryAnswer() {this.chatBoxs.forEach(item => {item.content = ''const ref = this.$refs[`ChatBoxRef${item.id}`]if (ref && ref.clearHistory) {ref.clearHistory()}})notify.success(this.$t('modelTalk.tips.cleanrecorded'))},/* 增加模型窗口,最多三个 */handleAddModel() {const hasUnShow = this.chatBoxs.some(item => !item.show)if (hasUnShow) {const unShowRow = this.chatBoxs.filter(item => !item.show)if (unShowRow && unShowRow.length) {const index = this.chatBoxs.findIndex(item => item.id === unShowRow[0].id)this.chatBoxs[index].show = true}} else {notify.warning(this.$t('modelTalk.tips.maxModelNum3'))}},/* 获取当前模型窗口位于第几个dom */getModelDomIndex(i) {if (!i) return iconst hasShowModels = this.chatBoxs.filter(res => res.show)const hasShowLength = hasShowModels.lengthif (hasShowLength === 3) return iif (hasShowLength === 2) return 1},// enter键盘按下的换行赋值为空adjustInputHeight(event) {if (event.key === 'Enter' && !event.shiftKey) {this.inputMessage = ''event.preventDefault()return}this.$nextTick(() => {const textarea = this.$refs.inputtextarea.style.height = 'auto'// 最高200pxtextarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'this.msgHight = textarea.style.height})},/* 如果按下Enter键 */handleKeydown(event) {if (event.isComposing) {return}if (event.key === 'Enter') {//Enter+shift 换行if (event.shiftKey) {return} else {// 按Enter,阻止默认行为,发送event.preventDefault()this.sendMessage()}}},/* 主动停止模型回答 */handleModelStop() {this.chatBoxs.forEach(item => {if (!item.isAnswerIng) returnconst ref = this.$refs[`ChatBoxRef${item.id}`]if (ref?.handleModelStop) {ref.handleModelStop().finally(() => {this.handleModelAnswerEnd({modelIndex: item.id,finishReason: 'stop'})})}})},/* 处理模型回答结束,更改回答状态 */handleModelAnswerEnd(data) {const { modelIndex, finishReason } = data//stop正常响应结束,length输出长度达到限制if (['stop', 'length'].includes(finishReason)) {this.$set(this.chatBoxs[modelIndex - 1], 'isAnswerIng', false)}},setChatBoxRef(el, id) {if (el) {this.$refs[`ChatBoxRef${id}`] = el} else {delete this.$refs[`ChatBoxRef${id}`]}},/* 发送消息 */sendMessage() {if (!this.inputMessage.trim()) returnif (!this.modelTypeList.length || !this.modelType) {//请选择模型notify.warning(this.$t('modelTalk.tips.seleModel'))return}this.$nextTick(() => {this.chatBoxs.forEach((item, i) => {const ref = this.$refs[`ChatBoxRef${item.id}`]if (ref && ref.sendMessage) {this.chatBoxs[i].isAnswerIng = trueref.sendMessage({message: this.inputMessage,modelIndex: item.id})}})this.inputMessage = ''})},/* 删除当前对话模型 */handleDeleteCurModel(index) {this.chatBoxs[index - 1].show = falseif (sessionStorage.getItem(`modelTalkParams-${index}`)) {sessionStorage.removeItem(`modelTalkParams-${index}`)}}},//清除sessionStorage中存储的模型参数beforeDestroy() {this.chatBoxs.forEach(item => {sessionStorage.removeItem(`modelTalkParams-${item.id}`)})},computed: {//输入框文本不为空,且不是回答中的状态:发送按钮可用sendBtnDisabled() {return !this.inputMessage.trim()},//存在回答中的状态hasAnsweringStatus() {return this.chatBoxs.some(item => item.show && item.isAnswerIng)}}
}
</script><style lang="scss">
// 尝试使用 @import 替代 @use 引入文件
@import '~@/assets/css/styles/chat-box-markdown.scss';
</style>
<style lang="scss" scoped>
.chat-page-box {display: flex;flex-direction: column;.chat-content-box {flex: 1;overflow: hidden;padding-top: 10px;display: grid;grid-template-columns: repeat(auto-fit, minmax(0, 1fr));gap: 10px;}
}
.middle-option-box {height: 30px;line-height: 30px;margin-top: 10px;::v-deep .vxe-button {.iconfont {font-size: 24px !important;}}
}
.bottom-send-box {width: 100%;min-height: 124px;padding: 10px 0;.talk-send {height: 100%;background: #f1f2f7;border-radius: 10px;border: 1px solid #e9e9eb;padding: 5px 10px;img {cursor: pointer;}textarea {width: 100%;padding: 10px;resize: none;overflow: auto;// min-height: 48px;height: 60px !important;line-height: 1.5;box-sizing: border-box;font-family: inherit;border: 0px;background: #f1f2f7;}textarea:focus {outline: none !important;}}
}
</style>

样式scss

.hashrate-markdown {font-size: 14px;}.hashrate-markdown ol,.hashrate-markdown ul {padding-left: 2em;}.hashrate-markdown pre {border-radius: 6px;line-height: 1.45;overflow: auto;display: block;overflow-x: auto;background: #2c2c36;color: rgb(248, 248, 242);padding: 16px 8px;}.hashrate-markdown h1,.hashrate-markdown h2,.hashrate-markdown h3 {// font-size: 1em;}.hashrate-markdown h4,.hashrate-markdown h5,.hashrate-markdown h6 {font-weight: 600;line-height: 1.7777;margin: 0.57142857em 0;}.hashrate-markdown li {margin: 0.5em 0;}.hashrate-markdown strong {font-weight: 600;}.hashrate-markdown p {white-space: pre-wrap;word-break: break-word;line-height: 24px;color: #324659;font-size: 14px;}.hashrate-markdown hr {background-color: #e8eaf2;border: 0;box-sizing: content-box;height: 1px;margin: 12px 0;min-width: 10px;overflow: hidden;padding: 0;}.hashrate-markdown table {border-collapse: collapse;border-spacing: 0;display: block;max-width: 100%;overflow: auto;width: max-content;}.hashrate-markdown table tr {border-top: 1px solid #e8eaf2;}.hashrate-markdown table td,.hashrate-markdown table th {border: 1px solid #e8eaf2;padding: 6px 13px;}.hashrate-markdown table th {background-color: #f3f2ff;font-weight: 600;}.hashrate-markdown section {margin-inline-start: 0px;border-left: 2px solid #e5e5e5;padding-left: 10px;color: #718096;margin-bottom: 5px;font-size: 12px;p {color: #718096;font-size: 12px;margin: 8px 0;}}.think-time {height: 36px;background: #f1f2f7;border-radius: 10px;line-height: 36px;font-size: 12px;display: inline-flex;padding: 0px 15px;margin-bottom: 20px;color: #1e1e1e;>i{line-height: 36px;margin-left: 5px;}}

封装sse.js

import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getToken } from '@/utils/auth' // 假设从auth工具获取tokenclass SSEService {constructor() {this.connections = new Map() // 存储所有连接 { connectionId: { controller, config } }this.DEFAULT_ID_PREFIX = 'sse_conn_'this.MAX_RETRIES = 3 // 最大自动重试次数this.BASE_RETRY_DELAY = 1000 // 基础重试延迟(ms)// 默认请求头(可通过setDefaultHeaders动态更新)this.DEFAULT_HEADERS = {'Content-Type': 'application/json','X-Auth-Token': getToken() || ''}}/*** 创建SSE连接* @param {Object} config - 连接配置* @param {String} config.url - 接口地址* @param {'GET'|'POST'} [config.method='GET'] - 请求方法* @param {Object} [config.params={}] - 请求参数* @param {Object} [config.headers={}] - 自定义请求头* @param {String} [config.connectionId] - 自定义连接ID* @param {Object} handlers - 事件处理器* @returns {String} connectionId*/connect(config = {}, handlers = {}) {const connectionId = config.connectionId || this._generateConnectionId()this.close(connectionId)// 合并headers(自定义优先)const headers = {...this.DEFAULT_HEADERS,...(config.headers || {}),'X-Auth-Token': getToken() || '' // 确保token最新}// 构建请求配置const requestConfig = {method: config.method || 'GET',headers,signal: this._createController(connectionId),openWhenHidden: true // 页面隐藏时保持连接}// 处理URL和参数const requestUrl = this._buildUrl(config.url,config.params,requestConfig.method)if (requestConfig.method === 'POST') {requestConfig.body = JSON.stringify(config.params || {})}// 存储连接信息this.connections.set(connectionId, {config: { ...config, connectionId },controller: requestConfig.signal.controller,retryCount: 0})// 发起连接this._establishConnection(requestUrl, requestConfig, connectionId, handlers)return connectionId}/*** 实际建立连接(含自动重试逻辑)*/async _establishConnection(url, config, connectionId, handlers) {const connection = this.connections.get(connectionId)try {await fetchEventSource(url, {...config,onopen: async response => {if (response.ok) {connection.retryCount = 0handlers.onOpen?.(connectionId)} else {throw new Error(`SSE连接失败: ${response.status}`)}},onmessage: msg => {try {const data = msg.data ? JSON.parse(msg.data) : nullhandlers.onMessage?.(data, connectionId)} catch (err) {handlers.onError?.(err, connectionId)}},onerror: err => {if (connection.retryCount < this.MAX_RETRIES) {const delay =this.BASE_RETRY_DELAY * Math.pow(2, connection.retryCount)setTimeout(() => {connection.retryCount++this._establishConnection(url, config, connectionId, handlers)}, delay)} else {handlers.onFinalError?.(err, connectionId)this.close(connectionId)}throw err // 阻止库默认的重试逻辑}})} catch (err) {console.error(`[SSE ${connectionId}] 连接异常:`, err)}}/*** 关闭指定连接*/close(connectionId) {const conn = this.connections.get(connectionId)if (conn?.controller) {conn.controller.abort()this.connections.delete(connectionId)}}/*** 关闭所有连接*/closeAll() {this.connections.forEach(conn => conn.controller?.abort())this.connections.clear()}/*** 更新默认请求头*/setDefaultHeaders(headers) {this.DEFAULT_HEADERS = { ...this.DEFAULT_HEADERS, ...headers }}// -------------------- 工具方法 --------------------_generateConnectionId() {return `${this.DEFAULT_ID_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 7)}`}_buildUrl(baseUrl, params = {}, method) {const url = new URL(baseUrl, window.location.origin)if (method === 'GET' && params) {Object.entries(params).forEach(([key, value]) => {if (value !== undefined) url.searchParams.set(key, value)})}return url.toString()}_createController(connectionId) {const controller = new AbortController()const conn = this.connections.get(connectionId)if (conn) conn.controller = controllerreturn controller.signal}
}export const sse = new SSEService()

 sse响应数据格式

{"content":"<think>","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"我是","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"Deep","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"</think>","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"我可以","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"理解","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"","reasoningContent":null,"created":1754036930,"finishReason":"stop","modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}

ModelParamConfig组件属于参数配置表单,可根据实际需求开发

最终效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917673.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917673.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电脑上不了网怎么办?【图文详解】wifi有网络但是电脑连不上网?网络设置

一、问题背景 你有没有遇到过这种情况&#xff1a;电脑右下角的网络图标明明显示连接正常&#xff0c;可打开浏览器就是加载不出网页&#xff0c;聊天软件也刷不出新消息&#xff1f; 这种 "网络已连接但无法上网" 的问题特别常见&#xff0c;既不是没插网线&#xf…

思途Spring学习 0804

SpringBoot 核心概念与开发实践SpringBoot 是一个基于 Spring 框架的快速开发脚手架&#xff0c;通过约定大于配置的原则简化了传统 Spring 应用的初始化配置。其核心目标是整合 Spring 生态&#xff08;如 SSM&#xff09;并支持微服务架构开发。控制反转&#xff08;IoC&…

Hutool工具类:Java开发必备神器

Hutool工具类使用说明Hutool是一个Java工具类库&#xff0c;提供了丰富的功能模块&#xff0c;包括字符串处理、日期时间操作、IO流、加密解密、HTTP客户端等。以下是一些常用模块的具体使用方法。字符串工具&#xff08;StrUtil&#xff09;字符串处理是开发中的常见需求&…

Node.js中Buffer的用法

// Buffer 与字符串的转换示例 // Buffer 是 Node.js 中用于处理二进制数据的类&#xff0c;字符串与 Buffer 之间的转换是常见操作// 1. 从字节数组创建 Buffer 并转换为字符串 // Buffer.from(array) 接收一个字节数值数组&#xff0c;创建对应的 Buffer let buf_4 Buffer.f…

【Java 基础】Java 源代码加密工具有哪些?

👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区:个人社区 💞 个人主页:个人主页 🙉 专栏地址: ✅ Java 中级 🙉八股文专题:剑指大厂,手撕 J…

es的histogram直方图聚合和terms分组聚合

你提到的这两个 Elasticsearch aggs 聚合语句&#xff1a;第一种&#xff1a;histogram 直方图聚合 "aggs": {"DayDiagram": {"histogram": {"field": "${FiledName}","interval": ${TimeInterval},"extende…

基于Java的AI/机器学习库(Smile、Weka、DeepLearning4J)的实用

基于Java和AI技术处理动漫视频 以下是一些基于Java和AI技术处理动漫视频(如《亚久斗》)的实用案例和实现方法,涵盖视频分析、风格转换、角色识别等方向。每个案例均提供技术思路和关键代码片段。 视频关键帧提取 使用OpenCV提取动漫视频中的关键帧,保存为图片供后续分析…

笔记本电脑联想T14重启后无法识别外置红米屏幕

【原先是可以连接重启后不行】按照以下步骤排查和解决&#xff1a;✅ 1. 基础排查确认连接方式&#xff1a;检查是否使用 USB-C转DP/HDMI线 或 HDMI/DP直连&#xff0c;尝试更换线缆或接口&#xff08;如换另一个USB-C口或HDMI口&#xff09;。测试显示器&#xff1a;将红米显示…

vue+ts 基础面试题 (一 )

目录 1.Vue3 响应式原理 一、 响应式的基本概念 二、 核心机制&#xff1a;Proxy 和依赖追踪 三、 触发更新的过程 四、 代码示例 五、 优势总结 2.如何实现组件间通信&#xff1f; 一、父子组件通信 1. 父传子&#xff1a;Props 传递 2. 子传父&#xff1a;自定义事…

Spring AI实战:SpringBoot项目结合Spring AI开发——提示词(Prompt)技术与工程实战详解

&#x1fa81;&#x1f341; 希望本文能给您带来帮助&#xff0c;如果有任何问题&#xff0c;欢迎批评指正&#xff01;&#x1f405;&#x1f43e;&#x1f341;&#x1f425; 文章目录一、前言二、提示词前置知识2.1 提示词要素2.2 设计提示词的通用技巧2.2.1 从简单开始2.2.…

【后端】Java static 关键字详解

在 Java 中&#xff0c;static 是一个修饰符&#xff0c;用于定义与类相关&#xff08;而非对象实例相关&#xff09;的成员。以下是核心知识点和用法&#xff1a;一、四大用途静态变量&#xff08;类变量&#xff09; 作用&#xff1a;属于类&#xff0c;而非实例。所有实例共…

算法训练营DAY50 第十一章:图论part01

98. 所有可达路径 98. 所有可达路径 【题目描述】 给定一个有 n 个节点的有向无环图&#xff0c;节点编号从 1 到 n。请编写一个程序&#xff0c;找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。 【输入描述】 第一行包含两个整数 N&#…

OpenCV:从入门到实战的全方位指南

目录 一、OpenCV 简介 &#xff08;一&#xff09;特点 &#xff08;二&#xff09;应用场景 二、OpenCV 的核心模块 &#xff08;一&#xff09;core 模块 &#xff08;二&#xff09;imgproc 模块 &#xff08;三&#xff09;video 模块 &#xff08;四&#xff09;f…

如何在 Ubuntu 24.04 上安装和配置 TFTP 服务器

了解如何在 Ubuntu 24.04 Linux 上安装 TFTP 以执行基本的文件传输。 简单文件传输协议(TFTP)是标准 FTP 的轻量级替代方案,用于在联网设备之间传输文件。与 FTP 和 HTTP 相比,TFTP 更简单,无需复杂的客户端-服务器模型即可操作。这就是为什么该协议用于执行基本文件传输…

基于 AXI-Lite 实现可扩展的硬件函数 RPC 框架(附完整源码)

AXI-Lite 实现RPC调用硬件函数服务 &#x1f44b; 本文介绍如何基于 AXI-Lite 总线设计一个通用的“硬件函数调用框架”。主机端&#xff08;PS&#xff09;只需通过寄存器写入参数与启动标志&#xff0c;即可触发 PL 模块执行指定算法逻辑&#xff0c;并将结果返回。 该机制本…

[spring-cloud: NamedContextFactory ClientFactoryObjectProvider]-源码阅读

依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-commons</artifactId><version>4.3.0</version> </dependency>源码 NamedContextFactory NamedContextFactory 类通过创建多个子…

HBase MOB技术特点及使用场景介绍

在 HBase 2.0 版本之前,虽然 HBase 能够存储从 1 字节到 10MB 大小的二进制对象 ,但其读写路径主要针对小于 100KB 的值进行了优化。当面对大量大小在 100KB - 10MB 之间的数据时,传统的存储方式就会暴露出问题。例如,当存储大量的图片、文档或短视频等中等大小对象时,由于…

Ubuntu 配置密钥+密码登录

目录 1、密钥生成 2、发送公钥至 需要连接的服务器 3、选用私钥登录 1、密钥生成 ssh-keygen -t rsa -b 4096 -C "angindem"2、发送公钥至 需要连接的服务器 将.ssh中的id_rsa.pub 的密钥&#xff0c;放在authorized_keys中 注意&#xff1a;.ssh 文件夹一定赋予…

谷歌浏览器Chrome 缓存迁移

步骤 1&#xff1a;准备数据迁移1. 关闭 Chrome 及所有后台进程在任务管理器&#xff08;CtrlShiftEsc&#xff09;中结束所有 chrome.exe 进程。 2. 备份并移动原数据- 将 C:\Users\xxx\AppData\Local\Google\Chrome\User Data **整个文件夹**复制到新位置&#xff08;如 G:\…

Java中的RabbitMQ完全指南

Java中的RabbitMQ完全指南 1. 引言 什么是RabbitMQ RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;实现了高级消息队列协议&#xff08;AMQP&#xff09;。它充当应用程序之间的消息中间件&#xff0c;允许分布式系统中的不同组件进行异步通信。RabbitMQ使用Erlang语言…