Ai问答之微信小程序中的流请求实现

1. 前言

目前会话Ai发展迅速,产品丰富,甚至还可以在一些平台上创建自己的智能体。

那么作为一个开发者,如果遇到项目中需要对接会话Ai的需求,还是需要一点学习成本的。本文总结和分享了一个基于微信小程序的会话Ai实现思路,给出了实现功能的核心代码,有需要的小伙伴直接复杂代码就可以直接运行。

另因笔者技术能力有限,不足之处欢迎评论区交流。

2. 小程序编译第三方依赖包

要实现流数据处理,必须要用到TextDecoder对象,但是这个对象只有Web端才有,微信小程序没有提供,所以这里需要我们自己在项目中安装编译,具体操作如下:

bash 复制代码
# 安装 text-encoding
npm install text-encoding

到这一步还不能使用,因为小程序框架是无法直接理解npm包的。我们需要在微信开发者工具中做一次编译后才可以使用。

首先要引入依赖包

js 复制代码
const { TextDecoder } = require('text-encoding');

然后在微信开发者工具中进行如下图的编译就可以了。

3. 小程序流请求实现

页面效果如下图

实现流请求的核心代码如下:

js 复制代码
data: {
    messages: [
        {
            id: Date.now() + 1,
            role: "assistant",
            content: "您好,请问有什么可以帮您?"
        }
    ],
    scrollTop: 0,
    bufferQueue: [], // 缓冲队列
    isRendering: false, // 渲染状态锁
    currentMsgId: null // 当前处理的消息ID
},

// 发起流请求
streamRequest(url, params) {
    let buffer = ""; // 当前数据缓冲区
    const decoder = new TextDecoder('utf-8');

    // 创建请求任务
    const requestTask = wx.request({
        url,
        method: "POST",
        timeout: 900000, // 设置请求事件,根据自己实际情况设置
        data: params,
        enableChunked: true,
        responseType: "arraybuffer",
        header: {
            "Content-Type": "application/json",
            Accept: "text/event-stream",
        },
        success: (res) => {
            if (res.statusCode !== 200) {
                this.handleError(new Error(`请求失败,状态码:${res.statusCode}`));
            }
        },
        fail: (err) => {
            console.error("请求失败:", err);
            wx.showToast({ title: "网络错误,请重试", icon: "none" });
        }
    });
	
    // 数据块接收处理
    requestTask.onChunkReceived((res) => {
        // 解码并处理数据块
        buffer += decoder.decode(res.data, { stream: true });
        // SSE协议解析
        const events = [];
        let pos = 0;
        while (pos < buffer.length) {
            // 查找事件边界
            const endPos = buffer.indexOf("\n\n", pos);
            if (endPos === -1) break;
            const rawEvent = buffer.slice(pos, endPos);
            pos = endPos + 2; // 跳过两个换行符
            // 解析事件内容
            const event = { data: "" };
            rawEvent.split("\n").forEach((line) => {
                const colonIdx = line.indexOf(":");
                if (colonIdx <= 0) return;
                const field = line.slice(0, colonIdx).trim();
                const value = line.slice(colonIdx + 1).trim();
                if (field === "data") event.data = value;
            });
            if (event.data) events.push(event);
        }

        buffer = buffer.slice(pos); // 保留未处理数据
        // 处理有效事件
        events.forEach((event) => {
            if (event.data === "[DONE]") {
                // ai回答完成后在这里处理相关的逻辑,然后return终止程序
                return;
            }

            try {
                const payload = JSON.parse(event.data);
                this.processContentChunk(
                    payload.id,
                    payload.choices[0].delta.content || '',
                    payload // 传递完整响应对象
                );
            } catch (e) {
                console.error("JSON解析失败:", e);
            }
        });
    });

    return requestTask;
},

/**
* 处理内容片段
*/
processContentChunk(messageId, contentChunk, payload) {
    // 初始化新消息
    if (!this.data.currentMsgId) {
        this.data.currentMsgId = messageId;
        // 从服务端响应中获取角色信息,若不存在则设置默认值
        const role = payload.choices[0].delta.role || 'assistant';
        // 使用setData保证视图同步更新
        this.setData({
            messages: [
                ...this.data.messages,
                {
                    id: messageId,
                    role: role, // 动态设置角色
                    content: '',
                    timestamp: Date.now()
                }
            ]
        });
    }

    // 将内容加入缓冲队列
    this.data.bufferQueue.push(...contentChunk.split(""));

    // 启动渲染流水线
    if (!this.data.isRendering) {
        this.startRenderingPipeline();
    }
},

/**
* 启动渲染流水线
*/
startRenderingPipeline() {
    if (this.data.isRendering || this.data.bufferQueue.length === 0) return;
        this.data.isRendering = true;
        const render = () => {
            if (this.data.bufferQueue.length === 0) {
                    this.data.isRendering = false;
                    return;
            }
            // 每次渲染5个字符(平衡流畅性和性能)
            const chunk = this.data.bufferQueue.splice(0, 5).join("");
            const msgIndex = this.data.messages.findIndex((msg) => msg.id === this.data.currentMsgId);
            if (msgIndex > -1) {
            this.setData(
                {
                    [`messages[${msgIndex}].content`]:
                    this.data.messages[msgIndex].content + chunk,
                },
                () => {
                    // 使用定时器保持约60fps的渲染速度
                    setTimeout(render, 16);
                    this.scrollToBottom(); // 自动滚动到界面最下面
                }
            );
        }
    };
    render();
},

/**
* 自动滚动到底部
*/
scrollToBottom() {
    this.setData({
        scrollTop: this.data.scrollTop + 10000, // 设置足够大的值确保滚动到底
    });
}

4. 总结

概括一下实现思路,其实很简单。第一步获取接口的流数据,第二步对数据进行处理,第三步实时更新到用户界面上。这里需要注意的是,Ai回答的打字效果是在流数据返回后实时更新到界面上的效果,不需要再自己实现。

相关推荐
boy快快长大6 分钟前
【VUE】day08黑马头条小项目
前端·javascript·vue.js
猫猫头有亿点炸32 分钟前
vue.js前端条件渲染指令相关知识点
java·前端·javascript
程序员老冯头39 分钟前
第十一节 MATLAB关系运算符
开发语言·前端·数据结构·算法·matlab
程序饲养员43 分钟前
注意Tailwind CSS 4.0 自定义颜色方式变更了
前端·css·postcss
Java&Develop1 小时前
vue2拦截器 拦截后端返回的数据,并判断是否需要登录
前端·javascript·vue.js
神奇大叔1 小时前
前端国际化-插件模式
前端
90后的晨仔1 小时前
iOS 中的 RunLoop 详解
前端·ios
zru_96021 小时前
HTML 标签类型全面介绍
前端·html
zru_96021 小时前
java替换html中的标签
java·前端·html