H5与小程序的SSE实践

场景

  • AI对话,uni-app

逻辑

  • 具体逻辑:对话模式,用户输入后,监听流。
  • 实现一个类,内部包含所有逻辑操作

h5

  • EventSource

小程序

  • 流请求时需要携带 enableChunked 参数为true
  • 获取到流信息之后得到的是 uint8Array,需要进行解码,这里依赖 iconv-lite 插件
  • 插件会返回正确的内容,但是内容是断断续续的,需要手动拼接

实现:

ts 复制代码
// dialogContainer.ts
// h5实现插件
import { EventSourcePolyfill } from 'event-source-polyfill';
// 小程序实现插件
import iconvLite from 'iconv-lite'

import * as api from '../../../api/freeDialoging'
import utils from '../../../utils'
import storage from '../../../utils/storage';
// 小程序内容组合
import { combindStr } from './wxMpStreamGenerateStr';


// 待处理的对话组
export interface IPreDialogItem {
	type : '0' | '1'
	content : string // 内容
	desc ?: string // 二级文字描述
	answer ?: (ls : IDialogItem[]) => void // 回话cb
	load ?: boolean // 是否加载中
}

// 对话组
export interface IDialogItem {
	time : string
	type : '0' | '1' // 0 --- 机器人 1---人
	content : string
	desc ?: string
	id ?: string
	load ?: boolean
}

class DialogControler {
        // 对话组列表
	list : IDialogItem[] = []
        // h5流
	sseH5Source : EventSource
        // 小程序流
	sseWxMpStream : UniApp.RequestTask
        // ai回复回调 返回最新对话列表
	sseAnswerCb : (ls : IDialogItem[]) => void
        
        // 创建新的对话
	createNewDialog({ answer: cb, ...preItem } : IPreDialogItem) : string {
		const time = utils.formatNowTime(new Date().valueOf())
		const id = (Math.random() * 100000000).toFixed().toString()
		const item : IDialogItem = {
			...preItem,
			time,
			id
		}
		this.addToList(item)
                
		if (preItem.type === '1') {
			// 开始回话
			this.sseAnswerCb = cb
			this.getAnswer(preItem.content)
		}
		return id
	}
	private addToList(item : IDialogItem) {
		this.list.push(item)
	}
	getList() : IDialogItem[] {
		return this.list
	}
	// 回话生成
	private async getAnswer(str : string) {
		const res = await api.getSseId()
		const id = res.data.sseId
		// 从这里要根据环境决定如何处理
		// #ifdef MP-WEIXIN
		this.createWxMpDialog(id, str)
		// #endif

		// #ifdef H5
		this.createH5SSE(id, str)
		// #endif
	}
	// 创建 小程序 对话
	private async createWxMpDialog(id : string, str : string) {
                // 在请求流的时候,一定要带上 enableChunked 参数 为 true 来确认是流传输,api具体内容和下面的h5请求差不多
		const reqTask = api.getWxMpStreamMessage(id)
		this.sseWxMpStream = reqTask
		const currentId = this.createNewDialog({
			content: '',
			type: '0',
			load: true
		})
		this.wxMpDialogListener(currentId)
		await api.sendMessage(id, str)
	}
	// 小程序流监听
	private wxMpDialogListener = (currentId) => {
		let str = ''
		let isDone = false
                // 流传输监听
		this.sseWxMpStream.onChunkReceived(e => {
			if (isDone) {
				return
			}
			try {
                                // 得到解析后的str,但是这时的str是断断续续的,需要进行拼接
				const strData = iconvLite.decode(e.data, 'utf-8')
				/*
					{
						paragraphIsEnd: 该段落是否结束
						isDone: 回话结束
						content: 段落内容
					}
				*/
				const tObj = combindStr(strData, str)
				if (tObj.isDone) {
					isDone = true
					this.streamPush(currentId, '', true)
				} else if (tObj.paragraphIsEnd) {
					this.streamPush(currentId, tObj.content, false)
					str = ''
				} else {
					// 未结束,先记录内容,下次带入
					str += tObj.content
				}
			} catch {
				this.streamPush(currentId, '内容生成错误,请重试!', true)
			}
		})
	}
	// 创建h5 sse
	private async createH5SSE(id : string, str : string) {
		if (!('EventSource' in window)) {
			throw Error('该浏览器不支持SSE!!')
		}
		const baseUrl = utils.returnEnvConstants('baseUrl')
		const that = this
		this.sseH5Source = new EventSourcePolyfill(`${baseUrl}sse/create/${id}`,
			{
				headers: {
					'Authorization': storage.get('token', ''),
					'Content-Type': 'text/event-stream',
					'Cache-Control': 'no-cache',
					'Connection': 'keep-alive'
				},
			}
		)
		await api.sendMessage(id, str)
		// 创建对应对话
		const currentId = this.createNewDialog({
			content: '',
			type: '0',
			load: true
		})
		this.sseH5Source.onopen = function () {
			//当连接正式建立时触发 
			// console.log('连接打开.')
		}
		this.sseH5Source.onmessage = function (msg) {
			//当连接正式建立时触发 
			// console.log('onmessage cb!!', msg, msg.data);
			const isDone = msg.lastEventId === '[DONE]'
			that.streamPush(currentId, isDone ? '' : msg.data, isDone)
		}
		this.sseH5Source.onerror = function (e) {
			//当连接发生error时触发
		}
	}
	// 持续推送内容
	streamPush(currentId : string, str : string, isDone = false) {
		const findIdx = this.list.findIndex(({ id }) => id === currentId)
		const cloneList = utils.deepClone(this.list)
		cloneList[findIdx].content += str
		cloneList[findIdx].load = !isDone
		this.list = cloneList
		this.sseAnswerCb(cloneList)
		if (isDone) {
			this.closeSSE()
		}
	}
	closeSSE() {
		// #ifdef MP-WEIXIN
		this.sseWxMpStream.abort()
		this.sseWxMpStream = null
		// #endif

		// #ifdef H5
		this.sseH5Source.close()
		this.sseH5Source = null
		// #endif
	}

}

export default DialogControler
ts 复制代码
// ./wxMpStreamGenerateStr.ts
export interface IStreamGenerateStrReturn {
	paragraphIsEnd ?: boolean
	isDone ?: boolean
	content ?: string
}
// 解析str return:[是否传输结束]
/*
	插件解析的内容是断断续续的,所以需要拼接组装。
        这里的拼接逻辑是根据数据结构决定,大家看看大概实现就好,使用需要修改
*/
export function combindStr(strData : string, latestStr : string) : IStreamGenerateStrReturn {
	const strs = strData.split('\n')
	const idItem = strs.find((item : string) => item.startsWith('id:'))
	if (idItem) {
		// 这个块存在id
		const [, id] = idItem.split('id:')
		if (id === '[DONE]') {
			return {
				isDone: true
			}
		}
	}
	// content
	const [, strStart] = strData.split('data:')
	// 有开头
	if (strStart) {
		const [strContnet, isEnd] = strStart.split('retry:')
		// 有内容
		if (strContnet) {
			return {
				// 有 retry: 说明结尾了
				paragraphIsEnd: !!isEnd,
				content: latestStr + strContnet
			}
		}
	} else if (strData.endsWith('data:')) {
		// 在结尾,但是没有实际数据
		return {
			// 还没有结束,下次继续拼接
			paragraphIsEnd: false,
			content: latestStr
		}
	} else {
		// 接着上次接受的信息进行拼接
		const [strContnet, isEnd] = strData.split('retry:')
		return {
			// 还没有结束,下次继续拼接
			paragraphIsEnd: !!isEnd,
			content: latestStr + strContnet
		}
	}
}

使用

小结

h5的话还行,主要是小程序经历了一番波折,一直纠结如何解析 uint8Array,结果最终在ai上找到答案了hh,绝了。

相关推荐
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅15 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊15 小时前
jwt介绍
前端
爱敲代码的小鱼15 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte15 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc