微信小程序对接php通过stream流式输出AI大模型结果

php端代码参考(调用豆包)

<?php

header('Content-Type: text/event-stream'); // 以事件流的形式告知浏览器进行显示
header('Cache-Control: no-cache'); // 告知浏览器不进行缓存
header('X-Accel-Buffering: no'); //关闭加速缓冲
header('Connection: keep-alive');

$keyword = '你是谁';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true);
    if (isset($input['message'])) {
        $message = $input['message'];
    }
}

$url="https://ark.cn-beijing.volces.com/api/v3/chat/completions";
$json='{
"model":"doubao-seed-2-0-pro-260215",
"messages": [
    {
        "role": "system",
        "content": "请回答用户提出的植物名称或其他相关问题,如果提问仅包含植物名称则结合百度百科返回植物相关信息信息,删除返回结果中的citation代码内容,需包含:植物中文名称、别名、外文名、分布区域、功能特征、形态特征、生长环境及习性、病虫防治事项、栽培技术、松针腐植土是否适合种植等信息。如果用户提出的非植物相关问题,则正常输出结果。返回结果为html模式,支持的标签包含<p><strong><br/><b>等,不要使用复杂的css样式。以下是用户提问:"
    },
    {
        "content": "'.$message.'",
        "role": "user"
    }
],
"thinking": {"type": "disabled"},
"stream":true
}';
$arr=json_decode($json,true);
$header=['Authorization: Bearer ark-xxx','Content-Type: application/json'];

/**
    * 示例回调函数,用于处理接收到的数据并返回给客户端
    *
    * @param string $data 接收到的数据片段
    */
function handleResponseData($data) {
    // 在这里,你可以将数据写入输出缓冲区或直接发送给客户端-例如,使用 echo 或 SSE 发送数据
    //sleep(3);
    echo $data; // 假设这里直接将数据发送给客户端
    echo "\n";
    //刷新输出缓冲区---把数据输出给浏览器
    ob_flush();
    flush();
}
// 使用示例
$res=curlStreamRequest(
    $url, // 替换为实际的 API URL
    json_encode($arr), // 替换为实际的 POST 数据(如果需要)
    $header, // 替换为实际的请求头
    //直接传递闭包函数
    function($data) {
        handleResponseData($data);
    }
    //'handleResponseData' // 传递回调函数名作为字符串(如果回调函数在全局作用域中)
);

/**
 * 流式请求--通过 cURL 发起流式请求并处理响应
 *
 * @param string $url 请求的 URL
 * @param array $headers 请求头数组
 * @param array|string|null $postData POST 数据
 * @param callable $callback 处理响应数据的回调函数
 * @throws Exception 如果回调函数不是有效的 Callable
 */
function curlStreamRequest(string $url,$postData = null, 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($postData) || !empty($postData));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    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);
}

微信小程序端代码

export default {
    data() {
        return {
            searchValue: '',
            searchResult: '',
            currentRequest: null, // 请求任务对象
        };
    },
    methods: {
        //搜索
        async searchBut() {
            let that = this;
            if (that.searchValue.length > 0) {
                //发起流式请求
                uni.showLoading({
                    title: '检索中'
                })
                await this.streamChat(that.searchValue)
            } else {
                return this.$util.Tips({
                    title: '请输入要搜索的内容',
                    icon: 'none',
                    duration: 1000,
                    mask: true,
                });
            }
        },

        // 流式聊天
        streamChat(message) {
            this.searchValue = '';
            this.searchResult = '';
            
            return new Promise((resolve, reject) => {
                const requestTask = wx.request({
                    url: "https://xxx/stream.php",
                    method: 'POST',
                    data: { message: message },
                    header: {
                        'Content-Type': 'application/json',
                        'Accept': 'text/event-stream'
                    },
                    responseType: 'text',
                    enableChunked: true, // 启用分块传输
                    success: (res) => {
                        this.finishStreaming()
                    },
                    fail: (err) => {
                        console.error('SSE请求失败', err)
                        this.handleStreamError('网络请求失败,请重试')
                    }
                })

                // 监听chunked数据(流式数据接收)
                let buffer = ''  // 数据缓冲区
                // 注意:小程序需要基础库2.20.1以上支持onChunkReceived
                requestTask.onChunkReceived((res) => {
                    uni.hideLoading();
                    // res.data 是ArrayBuffer,需要转为字符串
                    const chunk = this.arrayBufferToString(res.data)
                    buffer += chunk
                    
                    // 按行分割处理SSE格式数据
                    const lines = buffer.split('\n')
                    // 保留最后一个不完整的行
                    buffer = lines.pop() || ''
                    for (const line of lines) {
                        if (line.startsWith('data: ')) {
                            const dataStr = line.slice(6)
                            if (dataStr === '[DONE]') {
                                // 流结束标记
                                this.finishStreaming()
                                return
                            }
                            try {
                                const data = JSON.parse(dataStr)
                                if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) {
                                    // 追加内容到当前AI消息
                                    this.searchResult += data.choices[0].delta.content;
                                }
                            } catch (e) {
                                // 如果不是JSON格式,直接作为纯文本追加
                                this.searchResult += dataStr
                            }
                        } else if (line.trim() === '') {
                            // 空行忽略
                            continue
                        }
                    }
                })
                
                // 存储requestTask以便可能的中断
                this.currentRequest = requestTask
            })
        },
        // ArrayBuffer 转字符串
        arrayBufferToString(buffer) {
            // 微信小程序环境下的转换方法
            const dataView = new DataView(buffer)
            let result = ''
            for (let i = 0; i < dataView.byteLength; i++) {
                result += String.fromCharCode(dataView.getUint8(i))
            }
            // 如果是UTF-8编码,需要使用decodeURIComponent(escape(result))
            // 简单处理:如果乱码可尝试以下转换
            try {
                return decodeURIComponent(escape(result))
            } catch (e) {
                return result
            }
        },
        // 完成流式接收
        finishStreaming() {
            console.log('done')
            this.currentRequest = null
        },

        // 错误处理
        handleStreamError(errorMsg) {
            wx.showToast({
                title: errorMsg,
                icon: 'none',
                duration: 2000
            })
            this.currentRequest = null
        },
        
        abortStream() {
            if (this.currentRequest) {
                this.currentRequest.abort()
                this.currentRequest = null
            }
        },
    },
    onUnload() {
        this.abortStream()
    }
};

附:html页面处理流式输出

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>火山引擎AI模型流式输出演示</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            background-color: #f5f5f5;
            padding: 20px;
        }
        
        .container {
            max-width: 800px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 20px;
        }
        
        .input-section {
            margin-bottom: 20px;
        }
        
        textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
            min-height: 100px;
            font-size: 16px;
        }
        
        button {
            display: block;
            width: 100%;
            padding: 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
            margin-top: 10px;
        }
        
        button:hover {
            background-color: #0069d9;
        }
        
        button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
        }
        
        .output-section {
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 15px;
            min-height: 300px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
            font-size: 16px;
        }
        
        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(0, 123, 255, 0.3);
            border-radius: 50%;
            border-top-color: #007bff;
            animation: spin 1s ease-in-out infinite;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        .status {
            margin-top: 10px;
            font-size: 14px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>火山引擎AI模型流式输出演示</h1>
        
        <div class="input-section">
            <textarea id="userInput" placeholder="请输入您的问题..."></textarea>
            <button id="sendBtn" onclick="sendRequest()">发送请求</button>
        </div>
        
        <div class="status" id="status">
            <span id="statusText">就绪</span>
            <span id="loading" class="loading" style="display: none;"></span>
        </div>
        
        <div class="output-section" id="output"></div>
    </div>
    
    <script>
        async function sendRequest() {
            const userInput = document.getElementById('userInput').value;
            const output = document.getElementById('output');
            const sendBtn = document.getElementById('sendBtn');
            const statusText = document.getElementById('statusText');
            const loading = document.getElementById('loading');
            
            if (!userInput.trim()) {
                alert('请输入问题');
                return;
            }
            
            // 重置状态
            output.textContent = '';
            sendBtn.disabled = true;
            statusText.textContent = '处理中...';
            loading.style.display = 'inline-block';
            
            try {
                // 发送请求到PHP后端
                const response = await fetch('doubao.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ keyword: userInput })
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    
                    const chunk = decoder.decode(value, { stream: true });
                    const lines = chunk.split('\n');
                    
                    for (const line of lines) {
                        if (line.trim() === '') continue;
                        
                        if (line === 'data: [DONE]') {
                            statusText.textContent = '完成';
                            loading.style.display = 'none';
                            sendBtn.disabled = false;
                            break;
                        }
                        
                        if (line.startsWith('data: ')) {
                            const jsonStr = line.substring(6);
                            try {
                                const data = JSON.parse(jsonStr);
                                if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) {
                                    output.textContent += data.choices[0].delta.content;
                                    // 滚动到底部
                                    output.scrollTop = output.scrollHeight;
                                }
                            } catch (e) {
                                console.error('解析JSON错误:', e);
                            }
                        } else {
                            // 直接输出非JSON数据(如PHP的echo输出)
                            output.textContent += line;
                            output.scrollTop = output.scrollHeight;
                        }
                    }
                }
            } catch (error) {
                console.error('请求错误:', error);
                output.textContent = `错误: ${error.message}`;
                statusText.textContent = '错误';
                loading.style.display = 'none';
                sendBtn.disabled = false;
            }
        }
        
        // 按Enter键发送请求(Shift+Enter换行)
        document.getElementById('userInput').addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendRequest();
            }
        });
    </script>
</body>
</html>

Tags: none

添加新评论