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,绝了。

相关推荐
mosen8686 分钟前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
qq22951165021 小时前
微信小程序的汽车维修预约管理系统
微信小程序·小程序·汽车
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai2 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9152 小时前
【JavaScript】模块化开发
前端·javascript·vue.js