chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,645 @@
import { StreamRequestConfig, StreamChunk } from "@/types/interfaces/chat";
import { createSSEConnection } from "@/servers/useRequest.uts";
import { utf8ArrayToString } from "@/utils/utf8.uts";
import { t } from "@/utils/i18n";
// 全局错误处理 - 捕获未处理的 AbortError
// #ifdef H5
if (typeof window !== "undefined") {
window.addEventListener("unhandledrejection", (event) => {
if (event.reason && event.reason.name === "AbortError") {
console.log("捕获到未处理的 AbortError已静默处理");
event.preventDefault(); // 阻止默认的错误处理
}
});
}
// #endif
// 流式请求工具类
export class StreamRequest {
private requestTask: RequestTask | null = null;
private isAborted: boolean = false;
private abortController: AbortController | null = null;
private buffer: string = ""; // 用于缓存不完整的 SSE 消息(以 \n\n 为完整标记)
private pendingBytes: Uint8Array = new Uint8Array(0); // 用于缓存不完整的 UTF-8 字节序列
// 微信小程序数据流超时监控
private lastChunkReceivedTime: number = 0; // 最后一次接收数据的时间戳
private timeoutCheckTimer: any = null; // 超时检查定时器(使用 any 类型以兼容不同平台)
private readonly TIMEOUT_DURATION: number = 60000; // 超时时长 60 秒
private timeoutCallback: (() => void) | null = null; // 超时回调
// 发起流式请求
async request(config: StreamRequestConfig): Promise<void> {
this.isAborted = false;
this.buffer = ""; // 重置缓冲区
this.pendingBytes = new Uint8Array(0); // 重置字节缓冲区
// #ifdef H5
// 只在 H5 环境中创建 AbortController
this.abortController = new AbortController();
// #endif
// 清除可能存在的旧定时器(重要:避免定时器泄漏)
console.log("[超时监控] 新请求开始,清除旧定时器");
this.clearTimeoutMonitor();
// 保存超时回调
this.timeoutCallback = config.onTimeout || null;
try {
// #ifdef H5 || WEB
await this.requestH5(config);
// #endif
// #ifdef MP-WEIXIN
await this.requestWeapp(config);
// #endif
// #ifdef APP-PLUS
await this.requestApp(config);
// #endif
} catch (error) {
// 如果是用户主动中止,不触发错误回调
// #ifdef H5
if (error.name === "AbortError" || this.isAborted) {
console.log("请求已被中止");
return;
}
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
if (this.isAborted) {
console.log("请求已被中止");
return;
}
// #endif
config.onError?.(error);
}
}
// H5环境下的流式请求
private async requestH5(config: StreamRequestConfig): Promise<void> {
try {
// 关键修复:在创建 requestTask 后立即启动超时监控
// 因为 onChunkReceived 可能比 success 回调更早触发
console.log("[请求] requestTask 已创建,立即启动超时监控");
this.startTimeoutMonitor();
console.log("[请求] 超时监控启动完成");
await createSSEConnection({
url: config.url,
method: config.method,
headers: config.headers as any,
body: config.body,
abortController: this.abortController,
onMessage: (data: any) => {
if (this.isAborted) return;
// 更新最后接收数据的时间
this.updateLastChunkTime();
try {
// 检查是否完成
if (data && data.completed) {
console.log("[SSE] 收到完成标记 completed");
// 清除超时监控
console.log(
"[SSE] completed 标记触发,准备清除定时器,当前 timer ID:",
this.timeoutCheckTimer,
);
this.clearTimeoutMonitor();
console.log("[SSE] completed 标记处理完成,定时器已清除");
config.onComplete?.();
return;
}
// 处理数据块
const chunk: StreamChunk = {
type: "content",
data: data ? JSON.stringify(data) : "",
};
config.onChunk?.(chunk);
} catch (e) {
console.error("Parse chunk error:", e);
}
},
onError: (error: Error) => {
// 清除超时监控
this.clearTimeoutMonitor();
// 如果是中止错误,不触发错误回调
if (error.name === "AbortError" || this.isAborted) {
console.log("SSE连接已被中止");
return;
}
config.onError?.(error);
},
onOpen: (response: any) => {
console.log("SSE连接已建立:", response);
},
onClose: () => {
console.log("SSE连接已关闭");
// 调用超时回调-关闭loading状态以及恢复停止按钮
config.onTimeout?.();
},
});
} catch (error) {
// 捕获所有可能的错误,包括 AbortError
if (
error.message === "Connection aborted by user" ||
error.name === "AbortError" ||
this.isAborted
) {
console.log("SSE连接已被中止");
return;
}
// 只有真正的错误才触发错误回调
config.onError?.(error);
throw error; // 重新抛出非中止错误
}
}
// 处理完整的 SSE 消息
private processSSEMessage(
content: string,
config: StreamRequestConfig,
resolve: (value: void | PromiseLike<void>) => void,
): void {
// 检查是否完成
if (content === "[DONE]") {
console.log("[SSE] 收到完成标记 [DONE]");
// 清除超时监控
console.log(
"[SSE] [DONE] 标记触发,准备清除定时器,当前 timer ID:",
this.timeoutCheckTimer,
);
this.clearTimeoutMonitor();
console.log("[SSE] [DONE] 标记处理完成,定时器已清除");
config.onComplete?.();
resolve();
return;
}
// 检查是否看起来像 JSON (以 { 或 [ 开头)
const looksLikeJSON = content.startsWith("{") || content.startsWith("[");
if (looksLikeJSON) {
// 尝试解析 JSON 数据
try {
const data = JSON.parse(content);
if (data && data.completed) {
console.log("[SSE] 收到完成标记 completed");
// 清除超时监控
console.log(
"[SSE] completed 标记触发,准备清除定时器,当前 timer ID:",
this.timeoutCheckTimer,
);
this.clearTimeoutMonitor();
console.log("[SSE] completed 标记处理完成,定时器已清除");
config.onComplete?.();
resolve();
return;
}
// 处理数据块
const chunk: StreamChunk = {
type: "content",
data: content,
};
config.onChunk?.(chunk);
} catch (e) {
console.log("[SSE] JSON 解析失败,内容:", content);
}
} else {
// 不是 JSON 格式,直接作为内容处理
const chunk: StreamChunk = {
type: "content",
data: content,
};
config.onChunk?.(chunk);
}
}
// 启动超时监控
private startTimeoutMonitor(): void {
console.log(
"[超时监控] 准备启动超时监控,当前 timer ID:",
this.timeoutCheckTimer,
);
// 记录初始时间
this.lastChunkReceivedTime = Date.now();
// 先清除可能存在的旧定时器
if (
this.timeoutCheckTimer !== null &&
this.timeoutCheckTimer !== undefined
) {
console.log(
"[超时监控] 发现旧定时器先清除ID:",
this.timeoutCheckTimer,
);
clearInterval(this.timeoutCheckTimer);
this.timeoutCheckTimer = null;
}
// 每 10 秒检查一次是否超时
const timerId = setInterval(() => {
const now = Date.now();
const timeSinceLastChunk = now - this.lastChunkReceivedTime;
console.log(
`[超时监控] 检查中... 距离上次接收数据: ${timeSinceLastChunk}ms定时器 ID: ${this.timeoutCheckTimer}`,
);
if (timeSinceLastChunk >= this.TIMEOUT_DURATION) {
console.log(
`[超时监控] 数据流超时: ${timeSinceLastChunk}ms 未收到数据`,
);
this.handleTimeout();
}
}, 10000);
this.timeoutCheckTimer = timerId;
console.log("[超时监控] 定时器已创建,新 ID:", this.timeoutCheckTimer);
}
// 清除超时监控
private clearTimeoutMonitor(): void {
console.log("[超时监控] 尝试清除,当前 timer ID:", this.timeoutCheckTimer);
if (
this.timeoutCheckTimer !== null &&
this.timeoutCheckTimer !== undefined
) {
console.log("[超时监控] 清除超时监控timer ID:", this.timeoutCheckTimer);
clearInterval(this.timeoutCheckTimer);
this.timeoutCheckTimer = null;
} else {
console.log("[超时监控] 定时器 ID 为空,无需清除");
}
}
// 更新最后接收数据的时间
private updateLastChunkTime(): void {
this.lastChunkReceivedTime = Date.now();
}
// 处理超时
private handleTimeout(): void {
console.log("[超时监控] 触发超时处理");
// 清除定时器
this.clearTimeoutMonitor();
// 标记为中止状态
this.isAborted = true;
// 中止请求
if (this.requestTask) {
this.requestTask.abort();
this.requestTask = null;
}
// 显示轻提示
uni.showToast({
title: t("Mobile.Stream.timeoutDisconnected"),
icon: "none",
duration: 2000,
});
// 调用超时回调
if (this.timeoutCallback) {
this.timeoutCallback();
}
}
// 处理 UTF-8 字节流,处理分片造成的字符截断问题
private decodeUTF8(chunk: ArrayBuffer): string {
const newBytes = new Uint8Array(chunk);
let combined = newBytes;
if (this.pendingBytes.length > 0) {
combined = new Uint8Array(this.pendingBytes.length + newBytes.length);
combined.set(this.pendingBytes);
combined.set(newBytes, this.pendingBytes.length);
this.pendingBytes = new Uint8Array(0);
}
if (combined.length === 0) return "";
// 寻找最后一个安全的字节边界
let safeEnd = combined.length;
let i = combined.length - 1;
let backtrack = 0;
// 向前回溯,检查末尾是否被截断
// UTF-8 最大字符长度为 4 字节
while (backtrack < 4 && i >= 0) {
const b = combined[i];
// ASCII (0xxxxxxx) - 字符结束,且是单字节,安全
if ((b & 0x80) === 0) {
// 如果我们是回溯过程中遇到的 ASCII说明之前回溯过的字节在 ASCII 之后)是孤立的或不完整的?
// 比如 [ASCII, Cont, Cont],我们在 Cont 回溯。
// 遇到 ASCII说明后面的 Cont 没有 Start byte是无效的。
// 但如果是 [ASCII]backtrack=0safeEnd=length。
// 这里的逻辑:遇到 ASCII说明前面的序列都结束了。
// 那么从 i+1 到 length 的字节就是我们回溯过的“剩余字节”。
safeEnd = combined.length - backtrack;
break;
}
// Start byte (11xxxxxx)
if ((b & 0xc0) === 0xc0) {
let need = 2;
if ((b & 0xe0) === 0xe0) need = 3;
else if ((b & 0xf0) === 0xf0) need = 4;
const have = combined.length - i;
if (have < need) {
// 不完整,需要保留从 i 开始的所有字节
safeEnd = i;
} else {
// 完整
safeEnd = combined.length;
}
break;
}
i--;
backtrack++;
}
// 如果 safeEnd 小于总长度,保存剩余字节
if (safeEnd < combined.length) {
this.pendingBytes = combined.slice(safeEnd);
return utf8ArrayToString(combined.slice(0, safeEnd));
}
return utf8ArrayToString(combined);
}
// 微信小程序环境下的流式请求
private async requestWeapp(config: StreamRequestConfig): Promise<void> {
return new Promise((resolve, reject) => {
this.requestTask = uni.request({
url: config.url,
method: config.method,
header: config.headers as any,
data: config.body,
enableChunked: true, // 启用分块传输
responseType: "arraybuffer", // 响应类型
timeout: 1000 * 60 * 60 * 12, // 设置超时时间为 12小时
success: (res) => {
console.log("[请求] success 回调触发statusCode:", res.statusCode);
if (res.statusCode === 200) {
// 流式数据通过 onChunkReceived 接收success 仅表示请求建立成功
// 实际的流式数据处理在 onChunkReceived 回调中
config.onOpen?.(res);
resolve(); // 请求成功建立连接后即可 resolve
} else {
console.log("[请求] 请求失败statusCode:", res.statusCode);
// 清除超时监控
this.clearTimeoutMonitor();
reject(
new Error(
t("Mobile.Stream.httpErrorWithCode", {
code: res.statusCode,
}),
),
);
}
},
fail: (err) => {
// 清除超时监控
this.clearTimeoutMonitor();
if (!this.isAborted) {
// 忽略超时错误,因为对于长连接流式请求,超时是预期内的行为
// 只要 connection 还在接收数据,就不应该视为真正的错误
if (
err.errMsg &&
(err.errMsg.includes("time out") ||
err.errMsg.includes("timeout"))
) {
console.log("请求超时警告(流式请求正常现象):", err);
// 不 reject也不触发 onError保持连接继续接收数据
return;
}
config.onError?.(err);
reject(err);
}
},
});
// 关键修复:在创建 requestTask 后立即启动超时监控
// 因为 onChunkReceived 可能比 success 回调更早触发
console.log("[请求] requestTask 已创建,立即启动超时监控");
this.startTimeoutMonitor();
console.log("[请求] 超时监控启动完成");
// 监听分块数据接收
this.requestTask?.onChunkReceived?.((res) => {
// console.log('[数据接收] onChunkReceived 触发')
if (this.isAborted) return;
// 更新最后接收数据的时间
this.updateLastChunkTime();
try {
// 使用 decodeUTF8 处理分块数据,解决字符截断问题
const str = this.decodeUTF8(res.data as ArrayBuffer);
// console.log('[SSE] 接收到分块数据, 长度:', str.length)
// 将新数据添加到缓冲区
this.buffer += str;
// SSE 协议:完整的消息以 \n\n 结尾
// 按 \n\n 分割出完整的消息块
const messages = this.buffer.split("\n\n");
// 保存最后一个可能不完整的消息块
this.buffer = messages.pop() || "";
// 处理所有完整的消息块
for (const message of messages) {
if (!message.trim()) continue;
// 按行分割消息块,处理多行的情况
const lines = message.split("\n");
let dataContent = "";
// 提取所有 data: 开头的行
for (const line of lines) {
if (line.startsWith("data:")) {
// 移除 "data:" 前缀,保留后面的内容
const content = line.slice(5).trim();
// 拼接多个 data: 行的内容(用换行符连接)
if (dataContent.length > 0) {
dataContent += "\n" + content;
} else {
dataContent = content;
}
}
}
// 如果提取到了数据内容,进行处理
if (dataContent.length > 0) {
// 统一由 processSSEMessage 处理所有消息,包括 [DONE] 标记
this.processSSEMessage(dataContent, config, resolve);
}
}
} catch (error) {
console.error("处理分块数据失败:", error);
if (!this.isAborted) {
config.onError?.(error);
}
}
});
});
}
// App环境下的流式请求
private async requestApp(config: StreamRequestConfig): Promise<void> {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
this.requestTask = uni.request({
url: config.url,
method: config.method,
header: config.headers as any,
data: config.body,
success: (res) => {
if (res.statusCode === 200) {
// 处理流式响应
this.handleStreamResponse(res.data as string, config);
resolve();
} else {
reject(
new Error(
t("Mobile.Stream.httpErrorWithCode", {
code: res.statusCode,
}),
),
);
}
},
fail: (err) => {
if (!this.isAborted) {
reject(err);
}
},
});
});
// #endif
}
// 处理流式响应数据
private handleStreamResponse(
data: string,
config: StreamRequestConfig,
): void {
console.log("[SSE-APP] 处理流式响应数据, 长度:", data.length);
const lines = data.split("\n");
for (const line of lines) {
if (line.trim() === "") continue;
if (line.startsWith("data:")) {
const content = line.slice(5).trim();
// 跳过空内容
if (!content) {
console.log("[SSE-APP] 跳过空内容行");
continue;
}
console.log("[SSE-APP] 处理数据行:", content);
// 检查是否完成
if (content === "[DONE]") {
console.log("[SSE-APP] 收到完成标记 [DONE]");
config.onComplete?.();
return;
}
// 检查是否看起来像 JSON (以 { 或 [ 开头)
const looksLikeJSON =
content.startsWith("{") || content.startsWith("[");
if (looksLikeJSON) {
// 尝试解析 JSON 数据
try {
const parseData = JSON.parse(content);
if (parseData && parseData.completed) {
console.log("[SSE-APP] 收到完成标记 completed");
config.onComplete?.();
return;
}
} catch (e) {
// JSON 解析失败,继续处理为普通数据
console.log("[SSE-APP] JSON 解析失败,作为普通内容处理");
}
} else {
console.log("[SSE-APP] 非 JSON 格式,直接处理");
}
// 处理数据块
try {
const chunk: StreamChunk = {
type: "content",
data: content,
};
config.onChunk?.(chunk);
} catch (e) {
console.error("[SSE-APP] 处理数据块失败:", e);
}
}
}
}
// 中止请求
abort(): void {
this.isAborted = true;
this.buffer = ""; // 清空缓冲区
// 清除超时监控(所有平台都需要)
this.clearTimeoutMonitor();
// #ifdef H5
// 中止 H5 的 SSE 连接
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
// #endif
// #ifdef MP-WEIXIN
// 中止小程序的请求
if (this.requestTask) {
this.requestTask.abort();
this.requestTask = null;
}
// #endif
// #ifdef APP-PLUS
// 中止 App 的请求
if (this.requestTask) {
this.requestTask.abort();
this.requestTask = null;
}
// #endif
}
}
// 创建流式请求实例
export const streamRequest = new StreamRequest();