微信小程序对接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>