function EVENT_SOURCE(url, params = {}) {
const Token = uni.getStorageSync("token");
const queryParams = queryString.stringify({
...params,
access_token: Token,
store_id: getStoreId(),
});
const fullUrl = API_URL(url) + '&' + queryParams;
console.log('SSE 连接 URL:', fullUrl);
return new Promise((resolve, reject) => {
// H5 环境
// #ifdef H5
if (typeof EventSource !== 'undefined') {
try {
const eventSource = new EventSource(fullUrl);
let connected = false;
eventSource.onopen = () => {
console.log('H5 SSE 连接成功');
connected = true;
resolve(eventSource);
};
eventSource.onerror = (error) => {
console.error('H5 SSE 错误:', error);
eventSource.close();
if (!connected) {
reject(error);
}
};
} catch (error) {
console.error('H5 SSE 创建失败:', error);
reject(error);
}
} else {
reject(new Error('当前浏览器不支持 EventSource'));
}
// #endif
// 非 H5 环境
// #ifndef H5
let buffer = '';
const customEventSource = {
onmessage: null,
onerror: null,
close: null,
readyState: 0,
isCompleted: false
};
// APP 环境
// #ifdef APP-PLUS
const requestTask = uni.request({
url: fullUrl,
method: 'GET',
header: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
},
responseType: 'text',
success: (res) => {
try {
const lines = res.data.split('\n');
let index = 0;
function processLine() {
if (index >= lines.length) return;
const line = lines[index];
if (line.startsWith('data: ')) {
const eventData = line.slice(6).trim();
if (eventData === '[DONE]') {
customEventSource.isCompleted = true;
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: '[DONE]' });
}
} else {
try {
const parsedData = JSON.parse(eventData);
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: JSON.stringify(parsedData) });
}
} catch (error) {
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: eventData });
}
}
}
}
index++;
setTimeout(processLine, 50);
}
processLine();
} catch (error) {
if (!customEventSource.isCompleted && customEventSource.onerror) {
customEventSource.onerror(error);
}
}
},
fail: (err) => {
if (!customEventSource.isCompleted) {
customEventSource.readyState = 2;
if (customEventSource.onerror) {
customEventSource.onerror(err);
}
reject(err);
}
}
});
// #endif
// 小程序环境 小程序可以使用 text-encoding 代替 二进制字符串处理方式
// #ifdef MP
const requestTask = uni.request({
url: fullUrl,
method: 'GET',
header: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
},
enableChunked: true,
success: () => {
customEventSource.readyState = 1;
},
fail: (err) => {
if (!customEventSource.isCompleted) {
customEventSource.readyState = 2;
if (customEventSource.onerror) {
customEventSource.onerror(err);
}
reject(err);
}
}
});
requestTask.onChunkReceived((res) => {
try {
let chunk = '';
const data = res.data;
// 处理二进制数据,确保UTF-8编码正确解析
if (data instanceof ArrayBuffer) {
// 使用更安全的方式处理UTF-8字符
let str = '';
const bytes = new Uint8Array(data);
const len = bytes.byteLength;
// 手动处理UTF-8编码
for (let i = 0; i < len;) {
if (bytes[i] < 128) {
// ASCII字符
str += String.fromCharCode(bytes[i++]);
} else if (bytes[i] >= 192 && bytes[i] < 224) {
// 2字节UTF-8
if (i + 1 < len) {
str += String.fromCharCode(((bytes[i] & 31) << 6) | (bytes[i+1] & 63));
}
i += 2;
} else if (bytes[i] >= 224 && bytes[i] < 240) {
// 3字节UTF-8
if (i + 2 < len) {
str += String.fromCharCode(
((bytes[i] & 15) << 12) |
((bytes[i+1] & 63) << 6) |
(bytes[i+2] & 63)
);
}
i += 3;
} else if (bytes[i] >= 240 && bytes[i] < 248) {
// 4字节UTF-8 (转为UTF-16代理对)
if (i + 3 < len) {
const codePoint =
((bytes[i] & 7) << 18) |
((bytes[i+1] & 63) << 12) |
((bytes[i+2] & 63) << 6) |
(bytes[i+3] & 63);
if (codePoint >= 0x10000) {
const highSurrogate = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800;
const lowSurrogate = ((codePoint - 0x10000) % 0x400) + 0xDC00;
str += String.fromCharCode(highSurrogate, lowSurrogate);
} else {
str += String.fromCharCode(codePoint);
}
}
i += 4;
} else {
// 未知字节,跳过
i++;
}
}
chunk = str;
} else {
chunk = data;
}
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
lines.forEach(line => {
if (line.startsWith('data: ')) {
const eventData = line.slice(6).trim();
if (eventData === '[DONE]') {
customEventSource.isCompleted = true;
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: '[DONE]' });
}
} else {
try {
const parsedData = JSON.parse(eventData);
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: JSON.stringify(parsedData) });
}
} catch (error) {
if (customEventSource.onmessage) {
customEventSource.onmessage({ data: eventData });
}
}
}
}
});
} catch (error) {
console.error('数据处理错误:', error);
if (!customEventSource.isCompleted && customEventSource.onerror) {
customEventSource.onerror(error);
}
}
});
// #endif
customEventSource.close = () => {
if (customEventSource.readyState !== 2) {
console.log('关闭 SSE 连接');
customEventSource.readyState = 2;
customEventSource.isCompleted = true;
requestTask && requestTask.abort();
}
};
resolve(customEventSource);
// #endif
});
}
页面上使用
async chatStream() {
const safeMarked = (text) => {
try {
return text.replace(/\n/g, '<br>');
return marked(text);
} catch (error) {
console.error('marked解析错误:', error);
return text;
}
};
if (!this.value.trim()) {
this.$utils.toast('请输入问题');
return;
}
uni.showLoading({
title: '正在思考中...',
mask: true,
});
try {
const value = JSON.parse(JSON.stringify(this.value));
this.value = this.$options.data().value;
this.list.push({
text: value,
type: 'user',
time: this.$utils.formatDate(new Date()),
nickname: this.userInfo.nickname,
avatar_url: this.userInfo.avatar_url,
});
let aiText = '';
const aiMessage = {
type: 'ai',
text: '',
time: this.$utils.formatDate(new Date()),
nickname: this.stores.ai_help_name,
avatar_url: this.stores.logo
};
const eventSource = await this.$allrequest.chart.questionTemplateStream({
question: value
});
let isFirstContent = true;
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
const code = data.code;
if (code === 1) {
eventSource.close();
uni.hideLoading();
this.$u.toast(data.msg);
return;
}
if (data.choices?.[0]?.finish_reason === 'stop') {
eventSource.close();
return;
}
const content = data.choices?.[0]?.delta?.content || '';
if (content) {
if (isFirstContent) {
uni.hideLoading();
isFirstContent = false;
aiMessage.text = content;
aiMessage.html = safeMarked(content);
this.list.push(aiMessage);
} else {
aiMessage.text += content;
aiMessage.html = safeMarked(aiMessage.text);
}
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 1000 * this.list.length,
duration: 300
});
});
}
} catch (error) {
uni.hideLoading();
}
};
eventSource.onerror = (error) => {
console.error('SSE连接失败:', error);
eventSource.close();
uni.hideLoading();
aiMessage.text = '出错啦,请稍后再试';
aiMessage.html = safeMarked(aiMessage.text);
this.list.push(aiMessage);
};
} catch (error) {
console.error('SSE请求失败:', error);
uni.hideLoading();
this.$u.toast('发送失败,请重试');
}
}