先把坑提前说一遍:
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);
}