Files
ks-app-employment-service/utils/streamRequest.js

626 lines
31 KiB
JavaScript
Raw Normal View History

2025-03-28 15:19:42 +08:00
import config from "@/config.js"
import useUserStore from '@/stores/useUserStore';
/**
* @param url String请求的地址默认none
* @param data Object请求的参数默认{}
* @returns promise
**/
export default function StreamRequest(url, data = {}, onDataReceived, onError, onComplete) {
2025-10-21 22:58:47 +08:00
// #ifdef MP-WEIXIN
return StreamRequestMiniProgram(url, data, onDataReceived, onError, onComplete);
// #endif
// #ifdef H5
return StreamRequestH5(url, data, onDataReceived, onError, onComplete);
// #endif
// #ifndef H5 || MP-WEIXIN
return StreamRequestH5(url, data, onDataReceived, onError, onComplete);
// #endif
}
// 微信小程序流式请求实现
function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onComplete) {
const userStore = useUserStore();
const Authorization = userStore.token ? encodeURIComponent(userStore.token) : '';
return new Promise((resolve, reject) => {
let buffer = '';
2026-01-22 12:57:01 +08:00
let hasReceivedContent = false;
2025-10-21 22:58:47 +08:00
const requestTask = uni.request({
url: config.StreamBaseURl + url,
method: 'POST',
data: data,
header: {
"Authorization": Authorization,
"Accept": "text/event-stream",
"Content-Type": "application/json;charset=UTF-8"
},
enableChunked: true, // 启用分块传输
success: (res) => {
console.log('📡 Stream request completed');
onComplete && onComplete();
resolve();
},
fail: (err) => {
console.error('Stream 请求失败:', err);
onError && onError(err);
reject(err);
}
});
2026-01-23 12:12:51 +08:00
// UTF-8解码函数用于微信小程序真机环境
function utf8Decode(uint8Array) {
let result = '';
let i = 0;
const len = uint8Array.length;
while (i < len) {
const byte1 = uint8Array[i];
// 1字节字符 (0xxxxxxx)
if (byte1 < 0x80) {
result += String.fromCharCode(byte1);
i++;
}
// 2字节字符 (110xxxxx 10xxxxxx)
else if (byte1 >= 0xC0 && byte1 < 0xE0) {
if (i + 1 < len) {
const byte2 = uint8Array[i + 1];
if (byte2 >= 0x80 && byte2 < 0xC0) {
const codePoint = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
result += String.fromCharCode(codePoint);
i += 2;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 3字节字符 (1110xxxx 10xxxxxx 10xxxxxx)
else if (byte1 >= 0xE0 && byte1 < 0xF0) {
if (i + 2 < len) {
const byte2 = uint8Array[i + 1];
const byte3 = uint8Array[i + 2];
if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0)) {
const codePoint = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
result += String.fromCharCode(codePoint);
i += 3;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 4字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
else if (byte1 >= 0xF0 && byte1 < 0xF8) {
if (i + 3 < len) {
const byte2 = uint8Array[i + 1];
const byte3 = uint8Array[i + 2];
const byte4 = uint8Array[i + 3];
if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0) && (byte4 >= 0x80 && byte4 < 0xC0)) {
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
// 处理UTF-16代理对
if (codePoint >= 0x10000) {
codePoint -= 0x10000;
const highSurrogate = (codePoint >> 10) + 0xD800;
const lowSurrogate = (codePoint & 0x3FF) + 0xDC00;
result += String.fromCharCode(highSurrogate, lowSurrogate);
} else {
result += String.fromCharCode(codePoint);
}
i += 4;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 无效的UTF-8序列跳过
else {
result += '<27>';
i++;
}
}
return result;
}
// 监听分块数据
requestTask.onChunkReceived((res) => {
try {
// 微信小程序兼容处理微信小程序不支持TextDecoder使用自定义UTF-8解码
let chunk = '';
if (typeof TextDecoder !== 'undefined') {
// 支持TextDecoder的环境如开发者工具
const decoder = new TextDecoder('utf-8');
chunk = decoder.decode(new Uint8Array(res.data));
} else {
// 微信小程序真机环境使用自定义UTF-8解码函数
const uint8Array = new Uint8Array(res.data);
chunk = utf8Decode(uint8Array);
}
console.log('📦 收到分块数据:', chunk);
2025-10-21 22:58:47 +08:00
buffer += chunk;
let lines = buffer.split("\n");
buffer = lines.pop() || ''; // 保留不完整的行
2026-01-22 12:57:01 +08:00
console.log('📝 解析到行:', lines.length, '行,缓冲区剩余:', buffer.length, '字符');
2025-10-21 22:58:47 +08:00
for (let line of lines) {
2026-01-22 12:57:01 +08:00
console.log('🔍 处理行:', line);
// 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的JSON数据:', jsonData);
2025-10-21 22:58:47 +08:00
if (jsonData === "[DONE]") {
2026-01-22 12:57:01 +08:00
console.log('✅ 收到结束标记 [DONE]');
2025-10-21 22:58:47 +08:00
onComplete && onComplete();
resolve();
return;
}
if (jsonData && jsonData.trim()) {
try {
const parsedData = JSON.parse(jsonData);
2026-01-22 12:57:01 +08:00
console.log('🔧 解析后的JSON:', parsedData);
2025-10-21 22:58:47 +08:00
2026-01-22 12:57:01 +08:00
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
2025-10-21 22:58:47 +08:00
// 处理标准的choices格式
2026-01-22 12:57:01 +08:00
else if (parsedData?.choices?.[0]?.delta?.content) {
2025-10-21 22:58:47 +08:00
const content = parsedData.choices[0].delta.content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
2025-10-21 22:58:47 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理reasoning_content
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
2025-10-21 22:58:47 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理tool响应
else if (parsedData?.tool?.response) {
const content = parsedData.tool.response;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
2025-10-21 22:58:47 +08:00
onDataReceived && onDataReceived(content);
}
}
2026-01-22 12:57:01 +08:00
else {
console.warn('⚠️ 未匹配到任何内容格式:', parsedData);
}
2025-10-21 22:58:47 +08:00
} catch (e) {
2026-01-22 12:57:01 +08:00
console.error("JSON 解析失败:", e.message, "原始数据:", jsonData);
2025-10-21 22:58:47 +08:00
}
}
}
2026-01-22 12:57:01 +08:00
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的行:', processedLine);
}
2025-10-21 22:58:47 +08:00
}
} catch (error) {
console.error('处理分块数据失败:', error);
}
});
});
}
// H5 流式请求实现(原有的 fetch 实现)
function StreamRequestH5(url, data = {}, onDataReceived, onError, onComplete) {
2025-03-28 15:19:42 +08:00
const userStore = useUserStore();
const Authorization = userStore.token ? encodeURIComponent(userStore.token) : '';
const headers = {
"Authorization": Authorization,
"Accept": "text/event-stream",
"Content-Type": "application/json;charset=UTF-8"
};
return new Promise(async (resolve, reject) => {
try {
const response = await fetch(config.StreamBaseURl + url, {
method: "POST",
headers,
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
2025-10-13 16:01:49 +08:00
let retryCount = 0;
const maxRetries = 3;
2026-01-22 12:57:01 +08:00
let hasReceivedContent = false;
2025-10-13 16:01:49 +08:00
2025-03-28 15:19:42 +08:00
while (true) {
const {
done,
value
} = await reader.read();
2025-10-13 16:01:49 +08:00
if (done) {
console.log("📡 Stream reading completed");
break;
}
2025-03-28 15:19:42 +08:00
buffer += decoder.decode(value, {
stream: true
});
let lines = buffer.split("\n");
buffer = lines.pop(); // 可能是不完整的 JSON 片段,留待下次解析
2025-10-13 16:01:49 +08:00
console.log(`📦 Processing ${lines.length} lines, buffer length: ${buffer.length}`);
2025-03-28 15:19:42 +08:00
for (let line of lines) {
2026-01-22 12:57:01 +08:00
console.log('🔍 处理行:', line);
// 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的JSON数据:', jsonData);
2025-03-28 15:19:42 +08:00
if (jsonData === "[DONE]") {
2026-01-22 12:57:01 +08:00
console.log('✅ 收到结束标记 [DONE]');
2025-03-28 15:19:42 +08:00
onComplete && onComplete();
resolve();
return;
}
try {
2025-10-13 16:01:49 +08:00
// 检查JSON数据是否完整
if (jsonData && jsonData.trim() && jsonData !== "[DONE]") {
const parsedData = JSON.parse(jsonData);
2026-01-22 12:57:01 +08:00
console.log('🔧 解析后的JSON:', parsedData);
2025-10-13 16:01:49 +08:00
2026-01-22 12:57:01 +08:00
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
2025-10-13 16:01:49 +08:00
// 处理标准的choices格式
2026-01-22 12:57:01 +08:00
else if (parsedData?.choices?.[0]?.delta?.content) {
2025-10-13 16:01:49 +08:00
const content = parsedData.choices[0].delta.content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理reasoning_content
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理tool响应
else if (parsedData?.tool?.response) {
const content = parsedData.tool.response;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理其他格式的数据如jobs_array_number, durationSeconds等
else {
console.log("📦 收到非内容数据:", Object.keys(parsedData));
}
2025-03-28 15:19:42 +08:00
}
} catch (e) {
2025-10-13 16:01:49 +08:00
console.error("JSON 解析失败:", e.message, "原始数据长度:", jsonData.length, "数据预览:", jsonData.substring(0, 100) + "...");
// 不抛出错误,继续处理下一个数据块
2025-03-28 15:19:42 +08:00
}
}
2026-01-22 12:57:01 +08:00
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的行:', processedLine);
}
2025-03-28 15:19:42 +08:00
}
}
2025-10-13 16:01:49 +08:00
// 处理剩余的缓冲区数据
if (buffer.trim()) {
console.log("📦 Processing remaining buffer:", buffer.substring(0, 100) + "...");
const lines = buffer.split("\n");
for (let line of lines) {
2026-01-22 12:57:01 +08:00
console.log('🔍 处理剩余缓冲区行:', line);
// 处理重复的 data: 前缀
let processedLine = line;
// 移除所有开头的 data: 前缀(无论是否有空格),直到只剩下一个或没有
while (processedLine.startsWith("data:")) {
// 检查是否还有另一个 data: 前缀
const nextPart = processedLine.slice(5).trimStart();
if (nextPart.startsWith("data:")) {
processedLine = nextPart;
} else {
break;
}
}
if (processedLine.startsWith("data: ")) {
const jsonData = processedLine.slice(6).trim();
console.log('📄 提取的剩余JSON数据:', jsonData);
2025-10-13 16:01:49 +08:00
if (jsonData && jsonData !== "[DONE]") {
try {
const parsedData = JSON.parse(jsonData);
2026-01-22 12:57:01 +08:00
console.log('🔧 解析后的剩余JSON:', parsedData);
2025-10-13 16:01:49 +08:00
2026-01-22 12:57:01 +08:00
// 检查是否有错误信息
const finishReason = parsedData?.choices?.[0]?.finish_reason;
if (finishReason === "error") {
let errorContent = parsedData?.choices?.[0]?.delta?.content || "流式请求失败";
console.error('❌ 收到错误信息:', errorContent);
// 优化token超限错误提示
if (errorContent.includes("maximum input ids length")) {
errorContent = "对话历史过长,请尝试清除部分历史记录或简化问题";
}
// 只有当未收到正常内容时才显示错误信息
if (!hasReceivedContent) {
// 显示错误信息给用户
uni.showToast({
title: errorContent,
icon: 'none',
duration: 3000
});
}
}
2025-10-13 16:01:49 +08:00
// 处理标准的choices格式
2026-01-22 12:57:01 +08:00
else if (parsedData?.choices?.[0]?.delta?.content) {
2025-10-13 16:01:49 +08:00
const content = parsedData.choices[0].delta.content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(content):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理reasoning_content
else if (parsedData?.choices?.[0]?.delta?.reasoning_content) {
const content = parsedData.choices[0].delta.reasoning_content;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(reasoning_content):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
// 处理tool响应
else if (parsedData?.tool?.response) {
const content = parsedData.tool.response;
if (content) {
2026-01-22 12:57:01 +08:00
hasReceivedContent = true;
console.log('📤 调用onDataReceived(tool.response):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理其他可能的内容格式
else if (parsedData?.content) {
// 直接返回content字段的情况
const content = parsedData.content;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(direct content):', content);
onDataReceived && onDataReceived(content);
}
}
// 处理完整的text字段非流式
else if (parsedData?.choices?.[0]?.text) {
const content = parsedData.choices[0].text;
if (content) {
hasReceivedContent = true;
console.log('📤 调用onDataReceived(full text):', content);
2025-10-13 16:01:49 +08:00
onDataReceived && onDataReceived(content);
}
}
} catch (e) {
console.warn("处理剩余数据时JSON解析失败:", e.message);
}
}
}
2026-01-22 12:57:01 +08:00
else if (processedLine.trim()) {
// 处理非data:开头的行
console.warn('⚠️ 收到非data:开头的剩余行:', processedLine);
}
2025-10-13 16:01:49 +08:00
}
}
2025-03-28 15:19:42 +08:00
onComplete && onComplete();
resolve();
} catch (error) {
console.error("Stream 请求失败:", error);
onError && onError(error);
reject(error);
}
});
}
/**
* @param url String请求的地址默认none
* @param data Object请求的参数默认{}
* @param method String请求的方式默认GET
* @param loading Boolean是否需要loading 默认false
* @param header Objectheaders默认{}
* @returns promise
**/
export function chatRequest(url, data = {}, method = 'GET', loading = false, headers = {}) {
if (loading) {
uni.showLoading({
title: '请稍后',
mask: true
})
}
let Authorization = ''
if (useUserStore().token) {
Authorization = `${useUserStore().token}`
}
const header = headers || {};
header["Authorization"] = encodeURIComponent(Authorization);
return new Promise((resolve, reject) => {
uni.request({
url: config.StreamBaseURl + url,
method: method,
data: data,
header,
success: resData => {
// 响应拦截
if (resData.statusCode === 200) {
const {
code,
msg
} = resData.data
if (code === 200) {
resolve(resData.data)
return
}
uni.showToast({
2026-01-22 12:57:01 +08:00
title: msg || '请求失败',
icon: 'none'
})
// 拒绝Promise并提供详细错误信息
const err = new Error(msg || '请求失败,服务器返回错误码: ' + code)
err.error = resData
reject(err)
} else {
// 处理非200状态码
const errorMsg = `请求失败HTTP状态码: ${resData.statusCode}`
uni.showToast({
title: errorMsg,
2025-03-28 15:19:42 +08:00
icon: 'none'
})
2026-01-22 12:57:01 +08:00
const err = new Error(errorMsg)
err.error = resData
reject(err)
2025-03-28 15:19:42 +08:00
}
if (resData.data?.code === 401 || resData.data?.code === 402) {
useUserStore().logOut()
}
},
fail: (err) => {
reject(err)
},
complete: () => {
if (loading) {
uni.hideLoading();
}
}
});
})
}