H5和微信小程序对接Ai流式输出SSE

先把坑提前说一遍:

1、H5的解决方案EventSourcePolyfill,无法在微信小程序里面使用;

2、微信小程序的解决方案requestTask.onChunkReceived,又分为真机和开发者工具两个方案:

decoder.decode和arrayBufferToString两者互不兼容,也就是说,你在电脑端调试的时候需要使用decoder.decode,真机上需要使用arrayBufferToString;

3、微信小程序的分块返回的是多个包数据,需要根据换行,拆成多个数组。

下面开始完整的代码片段:

第一、H5的EventSourcePolyfill解决方案

先安装EventSourcePolyfill

npm install EventSourcePolyfill

然后引入

import { EventSourcePolyfill } from "event-source-polyfill";

 具体代码

// 获取用户的token
const token = uni.getStorageSync("token");
// 添加SSE连接
const url = `/api/homeapi/Chat/index?query=${message.value}&conversationId=${conversationId.value}`;
    const eventSource = new EventSourcePolyfill(url, {
        heartbeatTimeout: 300000, //超时时间,毫秒为单位,以5分钟为例
        headers: {
            Authorization: `Bearer ${token}`, // 添加身份验证令牌
        },
    });
    eventSource.onopen = function (e) {
        console.log("onopen", e);
    };
    // 流式监听
    eventSource.onmessage = (event) => {
        const data = JSON.parse(event.data); // 解析接收到的数据

        switch (data.event) {
            case "workflow_started":
                // 处理工作流开始事件
                console.log("工作流已开始:", data);
                break;
            case "node_started":
                // 处理节点开始事件
                console.log("节点已开始:", data);
                break;
            case "node_finished":
                // 处理节点完成事件
                console.log("节点已完成:", data);
                break;
            case "workflow_finished":
                // 处理工作流完成事件
                console.log("工作流已完成:", data);
                break;
            case "message":
                // 处理消息事件
                if (data.answer) {
                    // 检查list是否为空
                    // 更新最新的元素
					const lastItem = list.value[list.value.length - 1];
                    lastItem.returnSourceAnswer += data.answer; // 将新消息追加到最后一个元素的message中
                }
                break;
            case "message_end":
                // 处理消息结束事件
                console.log("消息结束:", data);
                loading.value = false;
                conversationId.value = data.conversation_id;
                // 更新历史记录中最新记录的 conversationId
                let history = uni.getStorageSync("searchHistory");
                if (history.length > 0) {
                    history[0].conversationId = conversationId.value;
                    uni.setStorageSync("searchHistory", history); // 更新存储
                }
                break;
            case "tts_message":
                // 处理文本转语音消息
                console.log("文本转语音消息:", data);
                break;
            case "tts_message_end":
                // 处理文本转语音消息结束
                console.log("文本转语音消息结束:", data);
                break;
            default:
                console.log("未知事件:", data);
        }
    };

    eventSource.onerror = (error) => {
        console.error("SSE错误:", error); // 处理错误
        eventSource.close(); // 关闭连接
    };
// 在适当的地方关闭连接
onUnload(() => {
     eventSource.close(); // 关闭SSE连接
});

第二、微信小程序解决方案

使用微信自己的wx.request,并开启enableChunked: true, // 启用分块接收

const token = uni.getStorageSync("token");
    const requestTask = wx.request({
        url: `${config.baseURL}/api/homeapi/Chat/index?query=${message.value}&conversationId=${conversationId.value}`,
        header: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
        },
        enableChunked: true, // 启用分块接收
    });
    // 监听服务端返回的数据
    requestTask.onChunkReceived((res) => {
        // Uint8Array转为text格式
        const arrayBuffer = res.data;
        // let decoder = new TextDecoder("utf-8");    // 本地开发者工具使用
        // let texts = decoder.decode(arrayBuffer);   // 本地开发者工具使用
        let texts = arrayBufferToString(arrayBuffer); // 真机使用
        // 看一下 打印出来的结果
        // console.log(text);
        texts = texts.split("\n"); // 将接收到的字符串按换行符分割成数组
        // 处理每个以 'data: ' 开头的字符串
        texts.forEach((text) => {
            if (text.trim().length > 0) {
                console.log("原始文本:", text);
                try {
                    const jsonString = text.replace(/^data:\s*/, ""); // 去掉前缀
                    // 检查 jsonString 是否有效 JSON
                    if (
                        jsonString.trim().startsWith("{") &&
                        jsonString.trim().endsWith("}")
                    ) {
                        const jsonData = JSON.parse(jsonString);
                        console.log(jsonData);
                        listener(jsonData);
                    } else {
                        console.error("无效的 JSON 格式:", jsonString);
                    }
                } catch (error) {
                    console.error("解析 JSON 时出错:", error);
                    console.log("原始文本:", text); // 输出原始文本以便调试
                }
            }
        });
        // listener(text)
    });

    requestTask.onComplete(() => {
        console.log("请求完成");
        // 这里可以处理请求完成后的逻辑
    });

    requestTask.onError((error) => {
        console.error("请求错误:", error);
    });
    // 流式监听
    const listener = (data) => {
        // const data = JSON.parse(parsedData); // 解析接收到的数据
        // console.log("解析接收到的数据:", data);
        switch (data.event) {
            case "workflow_started":
                // 处理工作流开始事件
                console.log("工作流已开始:", data);
                break;
            case "node_started":
                // 处理节点开始事件
                console.log("节点已开始:", data);
                break;
            case "node_finished":
                // 处理节点完成事件
                console.log("节点已完成:", data);
                break;
            case "workflow_finished":
                // 处理工作流完成事件
                console.log("工作流已完成:", data);
                break;
            case "message":
                // 处理消息事件
                if (data.answer) {
                    uni.hideLoading();
                    // 检查list是否为空
                    // 更新最新的元素
                    const lastItem = list.value[list.value.length - 1];
                    lastItem.returnSourceAnswer += data.answer; // 将新消息追加到最后一个元素的message中
                }
                break;
            case "message_end":
                // 处理消息结束事件
                console.log("消息结束:", data);
                loading.value = false;
                conversationId.value = data.conversation_id;
                // 更新历史记录中最新记录的 conversationId
                let history = uni.getStorageSync("searchHistory");
                if (history.length > 0) {
                    history[0].conversationId = conversationId.value;
                    uni.setStorageSync("searchHistory", history); // 更新存储
                }
                break;
            case "tts_message":
                // 处理文本转语音消息
                console.log("文本转语音消息:", data);
                break;
            case "tts_message_end":
                // 处理文本转语音消息结束
                console.log("文本转语音消息结束:", data);
                break;
            default:
                console.log("未知事件:", data);
        }
    };

arrayBufferToString函数,把 微信返回的arrayBuffer格式,转成字符串

const arrayBufferToString = (arr) => {
    if (typeof arr === "string") {
        return arr;
    }
    var dataview = new DataView(arr);
    var ints = new Uint8Array(arr.byteLength);
    for (var i = 0; i < ints.length; i++) {
        ints[i] = dataview.getUint8(i);
    }
    var str = "",
        _arr = ints;
    for (var i = 0; i < _arr.length; i++) {
        if (_arr[i]) {
            var one = _arr[i].toString(2),
                v = one.match(/^1+?(?=0)/);
            if (v && one.length == 8) {
                var bytesLength = v[0].length;
                var store = _arr[i].toString(2).slice(7 - bytesLength);
                for (var st = 1; st < bytesLength; st++) {
                    if (_arr[st + i]) {
                        store += _arr[st + i].toString(2).slice(2);
                    }
                }
                str += String.fromCharCode(parseInt(store, 2));
                i += bytesLength - 1;
            } else {
                str += String.fromCharCode(_arr[i]);
            }
        }
    }
    return str;
};

最后是后端,我是php:

/**
     * @api {get} Chat/index 发送聊天消息
     * @apiDescription 发送聊天消息
     * @apiName index
     * @apiGroup Chat
     * @apiParam {String} query 消息内容
     * @apiParam {String} conversationId 会话ID
     * @apiSuccess {Object} code success => 200, fail => 500
     * @apiSampleRequest /api/homeapi/Chat/index
     * @apiVersion 1.0.0 
     */
    public function index (
        string $query,
        string $conversationId = ''
    ) {
        header('Content-Type: text/event-stream'); // 以事件流的形式告知浏览器进行显示
        header('Cache-Control: no-cache'); // 告知浏览器不进行缓存
        header('X-Accel-Buffering: no'); //关闭加速缓冲
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Headers: *');
        $userId = session('user_id');
        $response = DifyHandler::sendChatMessage([], $query, $conversationId, $userId);
        return $this->echoJson($response);
    }

然后我封装了流式请求:

    /**
     * 发送聊天消息到 Dify API
     * @static
     * @access public
     * @param array $inputs
     * @param string $query
     * @param string $conversationId
     * @param string $user
     * @param array $files
     * @return array
     */
    public static function sendChatMessage(array $inputs = [], string $query, string $conversationId = '', string $user = '', array $files = []): array
    {
        $url = self::DIFY_API_URL;
        $data = [
            'inputs' => $inputs,
            'query' => $query,
            'response_mode' => 'streaming',
            'conversation_id' => $conversationId,
            'user' => $user,
            'files' => $files
        ];

        $headers = [
            'Authorization: Bearer ' . self::API_KEY,
            'Content-Type: application/json'
        ];

        $response = Request::stream($url, json_encode($data, JSON_UNESCAPED_UNICODE), $headers, function ($data) {
            self::handleResponseData($data);
        });
        return $response ?? []; // 如果 $response 为 null,则返回空数组
    }
    /**
     * 示例回调函数,用于处理接收到的数据并返回给客户端
     *
     * @param string $data 接收到的数据片段
     */
    private static function handleResponseData($data)
    {
        // 在这里,你可以将数据写入输出缓冲区或直接发送给客户端-例如,使用 echo 或 SSE 发送数据
        //sleep(3);
        echo $data; // 假设这里直接将数据发送给客户端
        //刷新输出缓冲区---把数据输出给浏览器
        ob_flush();
        flush();
    }
/**
     * 流式请求--通过 cURL 发起流式请求并处理响应
     *
     * @param string $url 请求的 URL
     * @param array $headers 请求头数组
     * @param array|string|null $params POST 数据
     * @param callable $callback 处理响应数据的回调函数
     * @throws Exception 如果回调函数不是有效的 Callable
     */
    public static function stream(string $url, $params = [], array $headers = [],  callable $callback)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 不将响应保存为字符串,直接处理
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 注意:在生产环境中应启用 SSL 验证
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 注意:同上
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        curl_setopt($ch, CURLOPT_POST, is_array($params) || !empty($params));
        curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($callback) {
            // 调用回调函数处理数据
            $callback($data);
            return strlen($data); // 返回接收到的数据长度
        });
        // 执行请求并获取响应
        curl_exec($ch);
        // 检查是否有错误发生
        if (curl_errno($ch)) {
            throw new \Exception(curl_error($ch));
        }
        // 关闭 cURL 句柄
        curl_close($ch);
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值