废话不多说直接上代码
// 小程序专用流式服务
export const streamChatMiniProgram = (messages, options = {
secret: ""
}) => {
return new Promise((resolve, reject) => {
// 构建请求数据
const requestData = {
model: 'deepseek-chat',
messages,
stream: true,
max_tokens: 2048,
temperature: 0.7
};
// 平台特定配置
const requestConfig = {
url: 'https://api.deepseek.com/v1/chat/completions',
method: 'POST',
header: {
'Accept-Charset': 'utf-8',
'Content-Type': 'application/json',
'Authorization': `Bearer ${options.secret}`
},
data: JSON.stringify(requestData),
// responseType: 'text',
enableChunked: true, // 关键配置:启用分块传输
// enableHttp2: true,
timeout: 30000
};
// 跨平台适配
// #ifdef MP-WEIXIN || MP-QQ
requestConfig.enableChunked = true;
// #endif
// #ifdef MP-ALIPAY || MP-BAIDU
requestConfig.enableChunked = false;
// #endif
// 发起请求
const requestTask = wx.request({
...requestConfig,
// 分块数据接收处理
chunked: requestConfig.enableChunked,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error(`API错误: ${res.statusCode}`));
}
},
fail: (err) => {
reject(new Error(`请求失败: ${err.errMsg}`));
}
});
try {
// requestTask.onHeadersReceived((chunk)=>{
// console.log("onHeadersReceived")
// })
// requestTask.onProgressUpdate((chunk)=>{
// console.log("onProgressUpdate")
// })
// const decoder = new TextDecoder('utf-8') // 显式指定UTF-8
requestTask.onChunkReceived((chunk) => {
try {
if (!requestTaskMap.get(requestTask.uniqueId)) {
return;
}
// 缓冲区初始化为空字符串
let buffer = '';
buffer += utf8Decode(chunk
.data
) //decoder.decode(chunk.data, { stream: true });// String.fromCharCode.apply(null, new Uint8Array(chunk.data));
// SSE格式解析
const lines = buffer.split('\n');
buffer = '';
for (const line of lines) {
if (line.trim() === '') continue;
if (line.startsWith('data:')) {
const dataStr = line.replace('data:', '').trim();
// 结束标记处理
if (dataStr === '[DONE]') {
resolve(fullResponse);
return;
}
// 解析JSON内容
try {
const data = JSON.parse(dataStr);
if (data.choices?.[0]?.delta?.content) {
const content = data.choices[0].delta.content;
fullResponse += content;
// 实时事件通知
uni.$emit('deepseek_stream_update', {
partial: content,
full: fullResponse
});
}
} catch (e) {
console.error('JSON 解析错误', e);
uni.$emit('deepseek_stream_update', {
partial: 'JSON 解析错误',
e,
full: 'JSON 解析错误',
e
});
}
}
}
} catch (err) {
uni.$emit('deepseek_stream_update', {
partial: JSON.stringify(err),
full: JSON.stringify(err)
});
}
});
} catch (err) {
uni.$emit('deepseek_stream_update', {
partial: JSON.stringify(err),
full: JSON.stringify(err)
});
}
// 存储任务引用以便中断
console.log("requestTask=", requestTask)
requestTaskMap.set(requestTask.uniqueId, requestTask);
let fullResponse = '';
});
};
export function utf8Decode(buffer) {
let uint8 = new Uint8Array(buffer);
let str = '';
let i = 0;
while (i < uint8.length) {
const byte = uint8[i++];
// 单字节字符 (0-127)
if (byte < 0x80) {
str += String.fromCharCode(byte);
}
// 双字节字符
else if ((byte & 0xE0) === 0xC0) {
const byte2 = uint8[i++];
str += String.fromCharCode(
((byte & 0x1F) << 6) | (byte2 & 0x3F)
);
}
// 三字节字符(支持中文)
else if ((byte & 0xF0) === 0xE0) {
const byte2 = uint8[i++];
const byte3 = uint8[i++];
str += String.fromCharCode(
((byte & 0x0F) << 12) |
((byte2 & 0x3F) << 6) |
(byte3 & 0x3F)
);
}
// 四字节字符(简单兼容)
else if ((byte & 0xF8) === 0xF0) {
i += 3; // 跳过后续字节
str += ''; // 替换字符占位
}
}
return str;
}
// 请求任务管理器
const requestTaskMap = new Map();
// 中断指定请求
export const abortStreamRequest = (requestId) => {
const task = requestTaskMap.get(requestId);
if (task) {
task.abort();
requestTaskMap.delete(requestId);
}
};
// 中断所有请求
export const abortAllRequests = () => {
requestTaskMap.forEach(task => {
task.abort()
});
requestTaskMap.clear();
};
调用
async startStream() {
if (!this.message) {
return;
}
const userMessage = {
id: Date.now(),
role: 'user',
content: this.message
};
this.messages.push(userMessage);
this.message = ""
// 构建对话历史
const messages = this.messages.map(m => ({
role: m.role,
content: m.content
}));
this.currMessage = {
content: "",
role: "assistant",
thinking: true
}
this.messages.push(this.currMessage)
// 发起流式请求
this.loading = true
const response = await streamChatMiniProgram(messages, {
secret: this.deepSeekSecret
});
this.loading = false
this.currMessage = undefined
this.$nextTick(()=>{
this.scrollBottom()
})
}