加载中...

小程序对接SSE接口记录

小程序对接SSE接口记录

背景

公司的小程序需要实现一个智能在线问诊的功能,这就需要接入大模型。由于调用第三方大模型的接口返回比较慢,所以后端采用了数据流的形式返回给前端,由于是第一次接触SSE形式的接口,踩了不少坑,特地记录一下

尝试过程

1. RequestTask

使用wx.request发起 HTTPS 网络请求

wx.request会返回一个请求任务对象RequestTask,微信官方说在wx.request中开启enableChunked之后,会在响应头中开启 transfer-encoding: chunked,直接支持流式传输,只要在RequestTask中调用onChunkReceived 方法即可监听Transfer-Encoding Chunk Received事件。

直接狂喜,立即推:

js
代码解读
复制代码
const requestTask = wx.request({ url: "https://xxx", enableChunked: true, header: { "Content-Type": "application/json;charset=UTF-8", }, method: "POST", responseType: "text", data: params, timeout: 200000, success: (res) => { console.info("发送成功: ", res); }, fail(err) { console.log(err, "err"); wx.showToast({ title: "发送失败,请重试", icon: "error", }); }, }); // 监听 Transfer-Encoding Chunk Received requestTask.onChunkReceived((res) => {}); requestTask.onHeadersReceived((response) => {});

结果发现,后台接口一直在等待响应,直到超时:

如下图:等待1分多之后,返回为空 模拟器测试: New Image

真机:

New Image

疑惑

请求无返回?为什么呢?
猜测1:接口有问题 解决方案:使用apifox测试接口 => 访问正常

New Image 猜测2:客户端无法解析响应体
解决方案:
1. 上网找资料:
全是说小程序支持sse的,除了解码部分有点问题,其他都没问题。放几篇各位感受一下:
a: 微信小程序对接SSE接口记录-csdn
b: 小程序支持sse吗-微信开发者社区
开始疑惑,怀疑人生,为啥别人这么顺利?
猜测3:返回内容虽然都是流的形式,是不是sse和普通数据流不一样?
如何对接流式接口中发现,他们的Content-Type似乎并不是sse(如下图)

New Image

于是我把疑惑抛给了后台老师,后台老师说他们是服务器拿到所有数据之后,再以流的形式给前台,而不是真正的使用sse接口下发到前台。\

2. web-view

由于小程序中又没有EventSource, 微信官方的api好像也无法支持sse,于是只能转为内嵌web-view的形式实现了。

使用h5的话就简单多了

解决

技术栈使用react + antd-mobile + microsoft/fetch-event-source

主要代码如下:\

  1. sse
jsx
代码解读
复制代码
import { fetchEventSource } from "@microsoft/fetch-event-source"; const responseRef = useRef(""); // AI回答的内容 // searchKey: 问题 const sseLink = (searchKey: any) => { // 对话列表 question:提问;answer:大模型回复 chatList.push({ question: searchKey.trim(), answer: "", }); let createTime: string = dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"); setLoading(true); let requestParams = { terminal: "terminal", largeModel: "BAIDU", userCode: customerId, prompt: searchKey.trim() || "", window: `window`, }; const ctrlAbout: any = new AbortController(); //建立 sse连接; getAnswer:url fetchEventSource(getAnswer, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestParams), signal: ctrlAbout.signal, onmessage(e: any) { //说明当前问题sse回答结束 if (e.data === "[DONE]") { ctrlAbout.abort(); responseRef.current = ""; //清空答案 //查询成功结束后重置搜索的问题 setSearchKey(""); setLoading(false); //结束loading localKeyRef.current = ""; //结束 重置localKey chatList[chatList.length - 1].createTime = createTime; chatList[chatList.length - 1].showLike = true; chatList[chatList.length - 1].useful = 0; chatList[chatList.length - 1].answerDone = true; console.log("chatList: ", chatList); setChatList([...chatList]); return; } //说明有消息过来 if (e.data !== "[DONE]" && e.data) { let responseString = e.data; // 空格符替换 if (responseString.includes("(@n@)")) { responseString = responseString.replaceAll("(@n@)", "\n"); } responseRef.current += responseString; let result = { question: searchKeyRef.current.trim(), //查询语 answer: responseRef.current, //返回值 }; chatList[chatList.length - 1] = result; setChatList([...chatList]); } }, onerror(error: any) { // 处理错误 ctrlAbout.abort(); setLoading(false); throw error; }, }); };
  1. ai回复,自动滚动到底部
jsx
代码解读
复制代码
// gptRef: 聊天界面 useEffect(() => { if (gptRef.current) { const chatListElement = gptRef.current; const isScrolledToBottom = chatListElement.scrollHeight - chatListElement.clientHeight <= chatListElement.scrollTop + 1; if (isScrolledToBottom) { gptRef.current.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest", }); } } }, [chatList.length]);
  1. 用户刷新界面,或者返回到小程序,都需要保存聊天历史记录上下文
jsx
代码解读
复制代码
// 重载页面 const reloadHandler = (e: any) => { if (animate.isReload) return; // 刷新动画 setAnimate((pre) => ({ ...pre, isReload: true, })); setTimeout(() => { if (chatList?.length) saveRecord(); // 保存 window.location.reload(); }, 900); }; // 用户返回小程序 useDeepCompareEffect(() => { window.history.pushState(null, "", "#"); window.addEventListener( "popstate", // 再次向历史堆栈添加一个条目,这样做的目的是阻止用户实际导航离开当前页面 (e: any) => { //为了避免只调用一次 window.history.pushState(null, "", "#"); if ( window.navigator.userAgent.toLocaleLowerCase().includes("miniprogram") ) { wx.miniProgram.postMessage({ data: chatList, // 通过postMessage向小程序环境发送`chatList`数据。 }); wx.miniProgram.navigateBack({ url: "", // 使小程序返回上一个页面。 }); } }, false // 表示事件捕获阶段不使用。 ); window.addEventListener("unload", () => { wx.miniProgram.postMessage({ data: chatList, // 当页面卸载时,同样通过`postMessage`发送`chatList`数据。 }); }); }, [chatList]);
  1. 小程序中
wxml
代码解读
复制代码
<web-view src="{{ url }}" bindmessage="getMessage"/>
js
代码解读
复制代码
async onShow() { // 等待 app 的 onLaunch 完成 然后获取全局变量的值globalData await app.onLaunch() const customerId = encodeURIComponent(app.globalData?.userInfo?.openId); const headPic = encodeURIComponent(app.globalData?.userInfo?.headPic); const url = `${webView_GPT}?customerId=${customerId}&headPic=${headPic}` this.setData({ url, }) }, // 获取消息 async getMessage(e) { const customerId = app.globalData?.userInfo?.openId; const { data } = e.detail if (data && data.length && data[data.length - 1].length) { const paramsData = data[data.length - 1].map((item) => ({ terminal: ''terminal'', largeModel: ''BAIDU'', userCode: customerId, window: `window`, question: item.question, answer: item.answer, id: item.id || '''', questionType: 0, useful: item.useful, createTime: item.createTime, })) await saveChatRecord(paramsData); // 从web-view跳转回小程序,无法触发react生命周期,只会触发unload事件。但是unload事件中是不允许调用接口的,所以保存历史记录,需要放到小程序中做。 } },

New Image

结束语

小程序中到底支不支持sse接口,这个还是让人很无语的,实测下来是只是支持流式数据,而不是sse,但是官方又说支持,期待有做过这方面的同学能指点一二,不胜感激。因为感觉腾讯元宝这种ai助手确实是在小程序原生中实现的,可能还是自己技术太菜。
第一次折腾小程序,有很多不懂的,谢谢同事和朋友的帮助~阿里嘎多٩(๑òωó๑)۶

解释代码