1. 理解核心机制:拼接而非替换
Dify 的 streaming 模式下,服务器会不断推送形如 data: {"event": "message", "answer": "字"} 的数据包。
核心逻辑是: 收到一个包,解析出 answer 字段,将其**追加(Append)**到当前正在显示的对话变量后,而不是直接替换。
2. 关键数据解析逻辑
Dify 返回的数据流格式如下:
text
data: {"event": "message", "answer": "我", ...}\n\n
data: {"event": "message", "answer": "是", ...}\n\n
data: {"event": "message_end", ...}\n\n
处理难点:
- 前缀处理 :每行数据都以
data:开头,解析 JSON 前必须去掉。 - 粘包处理 :有时候一次网络请求回调会收到多条
data,需要用\n\n分割。 - 事件区分 :必须判断
event字段。message: 文本块,核心展示内容。message_replace: 敏感词替换,需要替换整段文本。message_end: 结束标志。ping: 心跳,忽略即可。
3. UniApp 代码实现方案
在 UniApp 中(特别是微信小程序端),不能直接使用浏览器原生的 EventSource。推荐使用 uni.request 的 enableChunked: true 参数。
以下是一个完整的处理示例代码:
javascript
// 假设这是发送消息的方法
sendMessage(userQuery) {
const that = this;
// 1. 在界面先创建一个空的回答占位(为了立刻显示 loading 或光标)
this.messageList.push({
role: 'user',
content: userQuery
});
this.messageList.push({
role: 'assistant',
content: '' // 初始为空,稍后拼接
});
// 获取当前正在更新的这条消息在数组中的索引
const currentMsgIndex = this.messageList.length - 1;
// 2. 发起请求
const requestTask = uni.request({
url: 'http://47.243.127.167:4010/v1/chat-messages',
method: 'POST',
header: {
'Authorization': 'Bearer {API_KEY}', // 替换为真实 Key
'Content-Type': 'application/json'
},
data: {
inputs: {},
query: userQuery,
response_mode: "streaming", // 必须是 streaming
user: "uni-user-123",
conversation_id: that.conversationId || "" // 如果是连续对话,需传入
},
enableChunked: true, // 【关键】开启流式传输支持
success: (res) => {
// 这里是请求完成后的回调,流式通常不在这里处理数据
}
});
// 3. 监听流式数据头(可选)
requestTask.onHeadersReceived((headers) => {
// console.log('Header received', headers);
});
// 4. 【核心】监听分片数据
requestTask.onChunkReceived((res) => {
// res.data 是 ArrayBuffer,需要转换
const arrayBuffer = res.data;
// 小程序/App端需要 TextDecoder,或者使用第三方库转换
// 如果环境不支持 TextDecoder,需使用类似 text-encoding 的 polyfill
const uint8Array = new Uint8Array(arrayBuffer);
let text = "";
// 简易转换 (注意:中文可能乱码,生产环境建议用专业库如 fast-text-encoding)
// 微信小程序基础库高版本已支持 TextDecoder
try {
const decoder = new TextDecoder('utf-8');
text = decoder.decode(uint8Array, { stream: true });
} catch (e) {
// 兼容写法,逐字节处理(此处仅为示意,建议引入库)
text = String.fromCharCode.apply(null, uint8Array);
// 实际开发请务必处理 UTF-8 多字节中文乱码问题
text = decodeURIComponent(escape(text));
}
// 5. 处理 Dify 返回的原始数据字符串
that.processDifyStream(text, currentMsgIndex);
});
},
// 处理 Dify 数据流的专用函数
processDifyStream(chunkText, msgIndex) {
// Dify 的数据块以 \n\n 分隔
const lines = chunkText.split('\n\n');
lines.forEach(line => {
// 去掉 data: 前缀
if (line.startsWith('data: ')) {
const jsonStr = line.replace('data: ', '');
try {
const data = JSON.parse(jsonStr);
// 根据 Dify 文档判断 event 类型
if (data.event === 'message') {
// 【关键步骤】拼接 answer 字段到当前消息
this.messageList[msgIndex].content += data.answer;
// 保存 conversation_id 以便下一轮对话
if (!this.conversationId && data.conversation_id) {
this.conversationId = data.conversation_id;
}
}
else if (data.event === 'message_replace') {
// 内容审查替换,直接覆盖
this.messageList[msgIndex].content = data.answer;
}
else if (data.event === 'message_end') {
console.log('生成结束', data);
// 可以在这里处理 metadata,比如 token 消耗
}
else if (data.event === 'error') {
console.error('Dify 报错:', data);
this.messageList[msgIndex].content += "\n[出错: " + data.message + "]";
}
// 【重要】强制触发 Vue 视图更新(如果在某些层级深的结构中)
// 这一步在 Vue2 中可能不需要,但在某些 UniApp 场景下需要
// this.$forceUpdate();
} catch (e) {
// JSON 解析失败通常是因为数据包不完整(被截断),
// 生产环境需要做一个 buffer 缓存上一块未解析完的字符串
// 暂时忽略或存入 buffer
console.log('JSON parse error (ignore partial chunk):', e);
}
}
});
}
4. 常见坑排查清单
如果还是展示不出来,请按以下顺序检查:
-
ArrayBuffer 解码乱码:
- UniApp 的
onChunkReceived返回的是ArrayBuffer。如果不进行 UTF-8 解码直接转字符串,中文会显示乱码或空白。 - 解决 :确保使用了
TextDecoder或者decodeURIComponent(escape(String.fromCharCode(...)))这种方式正确解码。
- UniApp 的
-
Vue 响应式失效:
- 如果在
onChunkReceived这种异步回调中,this指向可能丢失。 - 解决 :确保在外部定义了
const that = this;,或者使用箭头函数。 - 解决 :如果是 Vue 2,修改数组索引可能不会触发视图更新。使用
this.$set(this.messageList, index, newValue)或者直接修改对象属性this.messageList[index].content += '...'通常是有效的,但要确保messageList是在data中定义的。
- 如果在
-
Markdown 渲染:
- Dify 输出的是 Markdown 格式(包含
**加粗**,Code Block等)。 - 如果直接用
<text>{``{ content }}</text>,只能显示纯文本。 - 建议 :在 UniApp 中引入
mp-html或towxml等组件来渲染 Markdown,这样能正确展示代码块和格式。
- Dify 输出的是 Markdown 格式(包含
-
JSON 解析报错:
- 流式传输网络抖动时,JSON 可能会被截断(比如
{"answer": "你好后面断了)。 - 解决 :需要实现一个
buffer变量,如果JSON.parse失败,将当前字符串存起来,等下一个 chunk 来了拼接到头部再解析。
- 流式传输网络抖动时,JSON 可能会被截断(比如