Files
YGChatCS/request/api/AgentChatStream.js
2025-08-08 15:31:16 +08:00

290 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BASE_URL } from "../../constant/base";
import { goLogin } from "@/hooks/useGoLogin";
/// 请求流式数据的API
const API = '/agent/assistant/chat';
/**
* 获取AI聊天流式信息仅微信小程序支持
* @param {Object} params 请求参数
* @param {Function} onChunk 回调,每收到一段数据触发
* @returns {Object} 包含Promise和requestTask的对象
*/
// 在文件顶部重新设计状态追踪
let requestTask = null;
let isAborted = false;
let currentPromiseReject = null;
let lastRequestId = null; // 记录上一次请求ID
let activeRequestId = null; // 记录当前活动请求ID
/**
* 终止的请求
*/
const stopAbortTask = () => {
console.log("🛑 开始强制终止请求... ");
isAborted = true;
// 将当前活动请求ID保存为上一次请求ID
lastRequestId = activeRequestId;
// 清除当前活动请求ID
activeRequestId = null;
if (currentPromiseReject) {
currentPromiseReject(new Error('请求已被用户终止'));
currentPromiseReject = null;
}
if (requestTask) {
// 先移除监听器,再终止请求
const cleanupListeners = () => {
try {
if(requestTask.offChunkReceived) {
console.log("======>offChunkReceived")
requestTask.offChunkReceived();
}
if(requestTask.offHeadersReceived) {
console.log("======>offHeadersReceived")
requestTask.offHeadersReceived();
}
} catch (e) {
console.error('清理事件监听器失败:', e);
}
};
cleanupListeners();
// 终止请求
try {
if (requestTask.abort) {
console.log("======>abort")
requestTask.abort();
}
} catch (e) {
console.log('🛑 终止网络请求失败:', e);
}
requestTask = null;
}
console.log('🛑 请求强制终止完成');
}
const agentChatStream = (params, onChunk) => {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token');
const requestId = Date.now().toString(); // 生成唯一请求ID
// 重置状态
isAborted = false;
currentPromiseReject = reject;
// 更新请求ID追踪
lastRequestId = activeRequestId; // 保存上一次请求ID
activeRequestId = requestId; // 设置新的活动请求ID
console.log(`🚀 发送请求 [${requestId}], 上一次请求ID [${lastRequestId}]`);
// 检查数据块是否来自已终止的请求
const isStaleData = (dataRequestId) => {
return dataRequestId === lastRequestId || dataRequestId !== activeRequestId;
};
// #ifdef MP-WEIXIN
requestTask = uni.request({
url: BASE_URL + API, // 替换为你的接口地址
method: 'POST',
data: params,
enableChunked: true,
header: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, // 如需token可加
},
responseType: 'arraybuffer',
success(res) {
if (!isAborted && !isStaleData(requestId)) {
console.log(`✅ 请求 [${requestId}] 成功`);
resolve(res.data);
} else {
console.log(`❌ 请求 [${requestId}] 已终止或过期忽略success回调`);
}
},
fail(err) {
if (!isAborted && activeRequestId === requestId) {
console.log(`❌ 请求 [${requestId}] 失败:`, JSON.stringify(err));
reject(err);
} else {
console.log(`❌ 请求 [${requestId}] 已终止或过期忽略fail回调`);
}
},
complete(res) {
// 使用 requestId 来验证请求有效性
if (!isAborted && activeRequestId === requestId) {
if (res.statusCode !== 200) {
console.log(`❌ 请求 [${requestId}] 完成但状态错误,状态:`, res.statusCode);
if(res.statusCode === 424) {
uni.setStorageSync('token', '');
goLogin();
}
if (onChunk) {
onChunk({
error: true,
message: '服务器错误',
detail: res
});
}
reject(res);
}
} else {
console.log(`❌ 请求 [${requestId}] ${isAborted ? '已终止' : '已过期'}忽略complete回调`);
}
}
});
requestTask.onHeadersReceived(res => {
// 使用 requestId 验证请求有效性
if (isAborted || activeRequestId !== requestId) {
console.log(`🚫 Headers [${requestId}] ${isAborted ? '已终止' : '已过期'},忽略`);
return;
}
console.log(`📡 请求 [${requestId}] Headers接收:`, res);
const status = res.statusCode || (res.header && res.header.statusCode);
if (status && status !== 200) {
console.log(`❌ 请求 [${requestId}] 服务器返回错误状态:`, status);
if (onChunk && !isAborted && activeRequestId === requestId) {
onChunk({
error: true,
message: `服务器错误(${status})`,
detail: res
});
}
// 终止异常请求
if (requestTask && requestTask.abort) {
requestTask.abort();
}
}
});
requestTask.onChunkReceived(res => {
// 立即验证请求有效性
if (isAborted || isStaleData(requestId)) {
console.log(`🚫 数据块 [${requestId}] 已终止或过期,忽略`);
return;
}
const base64 = uni.arrayBufferToBase64(res.data);
let data = '';
try {
data = decodeURIComponent(escape(weAtob(base64)));
} catch (e) {
console.error('Base64解码失败:', e);
return;
}
console.log("📦 onChunkReceivedres:", data)
// 再次验证请求有效性
if (isAborted || activeRequestId !== requestId) {
console.log(`🚫 解析后数据 [${requestId}] 已终止或过期,忽略`);
return;
}
const messages = parseSSEChunk(data);
messages.forEach(msg => {
if (!isAborted && !isStaleData(requestId) && onChunk) {
onChunk(msg);
}
});
});
// #endif
});
}
// window.atob兼容性处理
const weAtob = (string) => {
const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
// 去除空白字符
string = String(string).replace(/[\t\n\f\r ]+/g, '');
// 验证 Base64 编码
if (!b64re.test(string)) {
throw new TypeError(
// eslint-disable-next-line quotes
"Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded."
);
}
// 填充字符
string += '=='.slice(2 - (string.length & 3));
let bitmap,
result = '',
r1,
r2,
i = 0;
for (; i < string.length;) {
bitmap =
(b64.indexOf(string.charAt(i++)) << 18) |
(b64.indexOf(string.charAt(i++)) << 12) |
((r1 = b64.indexOf(string.charAt(i++))) << 6) |
(r2 = b64.indexOf(string.charAt(i++)));
if (r1 === 64) {
result += String.fromCharCode((bitmap >> 16) & 255);
} else if (r2 === 64) {
result += String.fromCharCode(
(bitmap >> 16) & 255,
(bitmap >> 8) & 255
);
} else {
result += String.fromCharCode(
(bitmap >> 16) & 255,
(bitmap >> 8) & 255,
bitmap & 255
);
}
}
return result;
};
// 解析SSE分段数据
const parseSSEChunk = (raw) => {
const results = [];
// 按一个或多个连续换行分段,表示每一个事件块
const chunks = raw.split(/\n\n+/);
for (const chunk of chunks) {
const lines = chunk.split(/\r?\n/);
let dataLines = [];
for (const line of lines) {
if (line.startsWith('data:')) {
// 提取data:后面的内容并去除首尾空格
dataLines.push(line.slice(5).trim());
}
}
if (dataLines.length > 0) {
// 拼接多行数据
const fullData = dataLines.join('\n');
try {
const obj = JSON.parse(fullData);
results.push(obj);
} catch (e) {
console.warn('⚠️ SSE数据解析失败:', e, '原始数据:', fullData);
// 解析失败忽略
}
}
}
return results;
}
export { agentChatStream, stopAbortTask }