chore: initialize qiming workspace repository
This commit is contained in:
93
qiming-mobile/servers/account.uts
Normal file
93
qiming-mobile/servers/account.uts
Normal file
@@ -0,0 +1,93 @@
|
||||
import request from "./useRequest";
|
||||
import { RequestResponse } from "../types/interfaces/request";
|
||||
import type {
|
||||
BindEmailParams,
|
||||
CodeLogin,
|
||||
ILoginResult,
|
||||
LoginFieldType,
|
||||
ResetPasswordParams,
|
||||
SendCode,
|
||||
SetPasswordParams,
|
||||
TenantConfigInfo,
|
||||
UserInfo,
|
||||
UserUpdateParams,
|
||||
WechatLoginParams,
|
||||
IWechatLoginResult,
|
||||
} from "@/types/interfaces/login";
|
||||
|
||||
// 租户配置信息查询接口
|
||||
export function apiTenantConfig() {
|
||||
return request<RequestResponse<TenantConfigInfo>>({
|
||||
url: "/api/tenant/config",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 查询当前登录用户信息
|
||||
export function apiUserInfo() {
|
||||
return request<RequestResponse<UserInfo>>({
|
||||
url: "/api/user/getLoginInfo",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
export function apiWechatLogin(data: WechatLoginParams) {
|
||||
return request<RequestResponse<IWechatLoginResult>>({
|
||||
url: "/api/user/mp/codeLogin",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 账号密码登录
|
||||
export function apiLogin(data: LoginFieldType) {
|
||||
return request<RequestResponse<ILoginResult>>({
|
||||
url: "/api/user/passwordLogin",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
export function apiSendCode(data: SendCode) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: "/api/user/code/send",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 验证码登录/注册接口
|
||||
export function apiLoginCode(data: CodeLogin) {
|
||||
return request<RequestResponse<ILoginResult>>({
|
||||
url: "/api/user/codeLogin",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 首次登录设置密码
|
||||
export function apiSetPassword(data: SetPasswordParams) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: "/api/user/password/set",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 退出登录接口
|
||||
export function apiLogout() {
|
||||
return request<RequestResponse<null>>({
|
||||
url: "/api/user/logout",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取动态认证码
|
||||
export function apiUserDynamicCode() {
|
||||
return request<RequestResponse<string>>({
|
||||
url: "/api/user/dynamicCode",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
191
qiming-mobile/servers/agentDev.uts
Normal file
191
qiming-mobile/servers/agentDev.uts
Normal file
@@ -0,0 +1,191 @@
|
||||
import request from "./useRequest";
|
||||
import { RequestResponse } from "@/types/interfaces/request";
|
||||
import {
|
||||
HomeAgentCategoryInfo,
|
||||
StaticFileListResponse,
|
||||
} from "@/types/interfaces/agentConfig";
|
||||
import type { ListParams } from "@/types/interfaces/common";
|
||||
import type {
|
||||
AgentAkDeleteParams,
|
||||
ApiAgentConversationChatPageResultParams,
|
||||
AgentDetailDto,
|
||||
AgentInfo,
|
||||
ShareFileInfo,
|
||||
AgentConversationShareParams,
|
||||
AgentModelOption,
|
||||
} from "@/types/interfaces/agent";
|
||||
import { ACCESS_TOKEN } from "@/constants/home.constants";
|
||||
import { API_BASE_URL } from "@/constants/config";
|
||||
import { COMMON_HEADERS } from "@/constants/common.constants";
|
||||
import { t } from "@/utils/i18n";
|
||||
|
||||
// 主页智能体列表接口 - 数据列表查询
|
||||
export async function apiHomeCategoryList() {
|
||||
return request<RequestResponse<HomeAgentCategoryInfo>>({
|
||||
url: "/api/home/list",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 已发布的智能体详情接口
|
||||
export function apiPublishedAgentInfo(
|
||||
agentId: number,
|
||||
withConversationId: boolean = false,
|
||||
) {
|
||||
return request<RequestResponse<AgentDetailDto>>({
|
||||
url: `/api/published/agent/${agentId}`,
|
||||
method: "GET",
|
||||
data: {
|
||||
withConversationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 查询用户最近使用过的智能体列表
|
||||
export function apiUserUsedAgentList(params: ListParams) {
|
||||
const { size, pageIndex, keyword } = params;
|
||||
// 对关键词进行编码
|
||||
const _keyword = keyword ? `&kw=${encodeURIComponent(keyword)}` : "";
|
||||
return request<RequestResponse<AgentInfo[]>>({
|
||||
url: `/api/user/agent/used/list/${size}?pageIndex=${pageIndex}${_keyword}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户最近智能体使用记录
|
||||
export function apiUserUsedAgentDelete(agentId: number) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/user/agent/used/delete/${agentId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 智能体收藏
|
||||
export function apiCollectAgent(agentId: number) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/user/agent/collect/${agentId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 智能体取消收藏
|
||||
export function apiUnCollectAgent(agentId: number) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/user/agent/unCollect/${agentId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 页面请求结果回写
|
||||
export function apiAgentComponentPageResultUpdate(
|
||||
data: ApiAgentConversationChatPageResultParams,
|
||||
) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: "/api/agent/conversation/chat/page/result",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建用户临时票据
|
||||
export function apiUserTicketCreate() {
|
||||
const accessToken = uni.getStorageSync(ACCESS_TOKEN) || "";
|
||||
return request<RequestResponse<string>>({
|
||||
url: "/api/user/ticket/create",
|
||||
method: "POST",
|
||||
data: {
|
||||
token: accessToken,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 查询文件列表
|
||||
export async function apiGetStaticFileList(cId: number) {
|
||||
return request<RequestResponse<StaticFileListResponse>>({
|
||||
url: "/api/computer/static/file-list",
|
||||
method: "GET",
|
||||
data: {
|
||||
cId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 分享文件
|
||||
export function apiAgentConversationShare(data: AgentConversationShareParams) {
|
||||
return request<RequestResponse<ShareFileInfo>>({
|
||||
url: "/api/agent/conversation/share",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 下载全部文件
|
||||
export async function apiDownloadAllFiles(cId: number): Promise<{
|
||||
data: Blob | ArrayBuffer;
|
||||
headers: {
|
||||
"content-disposition"?: string;
|
||||
"content-length"?: string;
|
||||
"content-type"?: string;
|
||||
};
|
||||
}> {
|
||||
// #ifdef H5 || WEB
|
||||
// H5 环境:直接使用 uni.request 获取完整响应
|
||||
const accessToken = uni.getStorageSync(ACCESS_TOKEN);
|
||||
const header: any = {
|
||||
// ...COMMON_HEADERS,
|
||||
};
|
||||
if (accessToken) {
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: API_BASE_URL + `/api/computer/static/download-all-files`,
|
||||
method: "GET",
|
||||
header,
|
||||
responseType: "arraybuffer", // H5 使用 arraybuffer
|
||||
data: {
|
||||
cId,
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 将 ArrayBuffer 转换为 Blob
|
||||
const blob = new Blob([res.data as ArrayBuffer], {
|
||||
type: "application/zip",
|
||||
});
|
||||
resolve({
|
||||
data: blob,
|
||||
headers: res.header || {},
|
||||
});
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
t("Mobile.AgentDev.downloadFailedWithCode", {
|
||||
code: String(res.statusCode),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef H5 || WEB
|
||||
// 小程序环境:返回下载 URL(实际下载由调用方处理)
|
||||
return Promise.resolve({
|
||||
data: null as any,
|
||||
headers: {},
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
// 智能体会话可选模型列表
|
||||
export function apiGetAgentModelOptions(agentId: number) {
|
||||
return request<RequestResponse<AgentModelOption[]>>({
|
||||
url: `/api/agent/conversation/model/options/${agentId}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
556
qiming-mobile/servers/audioUploader.uts
Normal file
556
qiming-mobile/servers/audioUploader.uts
Normal file
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* 音频文件上传器
|
||||
* 处理音频文件上传和语音转文字功能
|
||||
*/
|
||||
import { ACCESS_TOKEN } from "@/constants/home.constants";
|
||||
import { REDIRECT_LOGIN, SUCCESS_CODE } from "@/constants/codes.constants";
|
||||
import { API_BASE_URL } from "../constants/config";
|
||||
import { t } from "@/utils/i18n";
|
||||
// 音频文件接口
|
||||
export interface AudioFile {
|
||||
tempFilePath: string;
|
||||
duration: number;
|
||||
fileSize: number;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
// 转录结果接口
|
||||
export interface TranscriptResult {
|
||||
success: boolean;
|
||||
text: string;
|
||||
confidence: number;
|
||||
duration: number;
|
||||
error?: string;
|
||||
segments?: Array<{
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
confidence: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 上传配置接口
|
||||
export interface UploadConfig {
|
||||
apiUrl?: string; // API地址
|
||||
timeout?: number; // 超时时间(毫秒)
|
||||
language?: string; // 语言类型
|
||||
enableRetry?: boolean; // 是否启用重试
|
||||
maxRetries?: number; // 最大重试次数
|
||||
retryDelay?: number; // 重试延迟(毫秒)
|
||||
}
|
||||
|
||||
// 上传进度回调接口
|
||||
export interface UploadProgress {
|
||||
loaded: number; // 已上传字节数
|
||||
total: number; // 总字节数
|
||||
percent: number; // 上传进度百分比
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频上传器类
|
||||
*/
|
||||
export class AudioUploader {
|
||||
private config: UploadConfig = {
|
||||
apiUrl: API_BASE_URL + "/api/audio/stt",
|
||||
// apiUrl: 'https://stt-api.gd.ddnsto.com' + '/transcribe',
|
||||
timeout: 30000,
|
||||
language: "zh-CN",
|
||||
enableRetry: true,
|
||||
maxRetries: 1,
|
||||
retryDelay: 1000,
|
||||
};
|
||||
|
||||
constructor(config: Partial<UploadConfig> = {}) {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传音频文件并转换为文字
|
||||
*/
|
||||
async uploadAudio(
|
||||
audioFile: AudioFile,
|
||||
onProgress?: (progress: UploadProgress) => void,
|
||||
): Promise<TranscriptResult> {
|
||||
if (!audioFile.tempFilePath) {
|
||||
throw new Error(t("Mobile.AudioUploader.pathRequired"));
|
||||
}
|
||||
|
||||
if (audioFile.fileSize === 0) {
|
||||
throw new Error(t("Mobile.AudioUploader.fileEmpty"));
|
||||
}
|
||||
|
||||
// 检查文件大小限制(10MB)
|
||||
const maxFileSize = 10 * 1024 * 1024;
|
||||
if (audioFile.fileSize > maxFileSize) {
|
||||
throw new Error(t("Mobile.AudioUploader.fileTooLarge"));
|
||||
}
|
||||
|
||||
// 检查录音时长(最短0.5秒,最长10分钟)
|
||||
// if (audioFile.duration < 0.5) {
|
||||
// throw new Error('录音时长过短,至少需要0.5秒')
|
||||
// }
|
||||
|
||||
/**
|
||||
* 此处的duration是录音停止时的duration,而不是录音开始时的duration, 单位为秒
|
||||
*/
|
||||
if (audioFile.duration > 600) {
|
||||
throw new Error(t("Mobile.AudioUploader.durationTooLong"));
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
return this.performUpload(audioFile, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行上传操作
|
||||
*/
|
||||
private async performUpload(
|
||||
audioFile: AudioFile,
|
||||
onProgress?: (progress: UploadProgress) => void,
|
||||
retryCount: number = 0,
|
||||
): Promise<TranscriptResult> {
|
||||
try {
|
||||
// 模拟上传进度
|
||||
// if (onProgress) {
|
||||
// this.simulateProgress(onProgress)
|
||||
// }
|
||||
|
||||
// 暂时使用模拟实现,后续可替换为真实API调用
|
||||
// if (this.config.apiUrl === '/api/voice-to-text') {
|
||||
// return await this.mockUpload(audioFile)
|
||||
// }
|
||||
|
||||
// 真实的上传实现
|
||||
return await this.realUpload(audioFile, onProgress);
|
||||
} catch (error: any) {
|
||||
// console.error(`上传失败 (尝试 ${retryCount + 1}/${this.config.maxRetries + 1}):`, error)
|
||||
|
||||
// // 如果启用重试且未达到最大重试次数
|
||||
// if (
|
||||
// this.config.enableRetry &&
|
||||
// retryCount < this.config.maxRetries! &&
|
||||
// this.shouldRetry(error)
|
||||
// ) {
|
||||
// // 等待一段时间后重试
|
||||
// await this.delay(this.config.retryDelay! * Math.pow(2, retryCount))
|
||||
// return this.performUpload(audioFile, onProgress, retryCount + 1)
|
||||
// }
|
||||
|
||||
// 不重试或重试次数已达上限
|
||||
throw this.formatError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实的上传实现
|
||||
*/
|
||||
private async realUpload(
|
||||
audioFile: AudioFile,
|
||||
onProgress?: (progress: UploadProgress) => void,
|
||||
): Promise<TranscriptResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const accessToken = uni.getStorageSync(ACCESS_TOKEN);
|
||||
const header = {};
|
||||
if (accessToken) {
|
||||
// #ifdef H5 || WEB
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
// #endif
|
||||
}
|
||||
// uEnvProd
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// #ifdef H5
|
||||
//仅H5需要添加fragment参数
|
||||
const fragment = (window.location.hash || "#/").replace("#/", "/");
|
||||
if (fragment) {
|
||||
header["fragment"] = fragment;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
const uploadTask = uni.uploadFile({
|
||||
header,
|
||||
url: this.config.apiUrl,
|
||||
filePath: audioFile.tempFilePath,
|
||||
name: "file",
|
||||
// formData: {
|
||||
// format: audioFile.format || 'mp3',
|
||||
// duration: audioFile.duration.toString(),
|
||||
// language: this.config.language || 'zh-CN',
|
||||
// fileSize: audioFile.fileSize.toString()
|
||||
// },
|
||||
timeout: this.config.timeout,
|
||||
success: (res) => {
|
||||
try {
|
||||
const { code, data } = JSON.parse(res.data);
|
||||
if (code === SUCCESS_CODE) {
|
||||
if (data?.text) {
|
||||
resolve({
|
||||
success: true,
|
||||
text: data.text,
|
||||
});
|
||||
} else {
|
||||
reject(
|
||||
new Error(t("Mobile.AudioUploader.noTextRecognized")),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
t("Mobile.AudioUploader.requestFailedWithCode", {
|
||||
code: String(res.statusCode),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(t("Mobile.AudioUploader.responseParseFailed")),
|
||||
);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(
|
||||
new Error(
|
||||
err.errMsg || t("Mobile.AudioUploader.networkFailed"),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 监听上传进度
|
||||
// if (onProgress) {
|
||||
// uploadTask.onProgressUpdate((res) => {
|
||||
// onProgress({
|
||||
// loaded: res.totalBytesSent,
|
||||
// total: res.totalBytesExpectedToSend,
|
||||
// percent: res.progress
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟上传实现(用于测试)
|
||||
*/
|
||||
// private async mockUpload(audioFile: AudioFile): Promise<TranscriptResult> {
|
||||
// // 模拟网络延迟
|
||||
// await this.delay(1500 + Math.random() * 1000)
|
||||
|
||||
// // 模拟转换结果
|
||||
// const mockTexts = [
|
||||
// "这是一段测试语音转文本的内容",
|
||||
// "语音识别功能正在正常工作",
|
||||
// "请检查录音质量是否清晰",
|
||||
// "您好,这是语音转文本测试",
|
||||
// "录音功能已经成功集成",
|
||||
// "人工智能语音识别技术日趋成熟",
|
||||
// "语音交互将成为未来的主要方式",
|
||||
// "欢迎使用语音录制功能",
|
||||
// "系统正在处理您的语音信息",
|
||||
// "语音转文字功能测试成功"
|
||||
// ]
|
||||
|
||||
// // 根据录音时长选择不同长度的文本
|
||||
// let selectedText: string
|
||||
// if (audioFile.duration < 2) {
|
||||
// selectedText = mockTexts[Math.floor(Math.random() * 3)]
|
||||
// } else if (audioFile.duration < 5) {
|
||||
// selectedText = mockTexts[Math.floor(Math.random() * 6) + 3]
|
||||
// } else {
|
||||
// selectedText = mockTexts[Math.floor(Math.random() * mockTexts.length)]
|
||||
// }
|
||||
|
||||
// // 模拟成功率(95% 成功率)
|
||||
// const isSuccess = Math.random() > 0.05
|
||||
|
||||
// if (isSuccess) {
|
||||
// return {
|
||||
// success: true,
|
||||
// text: selectedText,
|
||||
// confidence: 0.80 + Math.random() * 0.20,
|
||||
// duration: audioFile.duration,
|
||||
// segments: this.generateMockSegments(selectedText, audioFile.duration)
|
||||
// }
|
||||
// } else {
|
||||
// // 模拟不同类型的错误
|
||||
// const errorTypes = [
|
||||
// '网络连接异常,请检查网络设置',
|
||||
// '语音内容不清晰,请重新录制',
|
||||
// '服务器繁忙,请稍后重试',
|
||||
// '音频格式不支持,请使用标准格式'
|
||||
// ]
|
||||
|
||||
// const errorMessage = errorTypes[Math.floor(Math.random() * errorTypes.length)]
|
||||
|
||||
// return {
|
||||
// success: false,
|
||||
// text: '',
|
||||
// confidence: 0,
|
||||
// duration: audioFile.duration,
|
||||
// error: errorMessage
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 生成模拟的分段结果
|
||||
*/
|
||||
// private generateMockSegments(text: string, duration: number): Array<{
|
||||
// text: string
|
||||
// start: number
|
||||
// end: number
|
||||
// confidence: number
|
||||
// }> {
|
||||
// const words = text.split('')
|
||||
// const segments = []
|
||||
// const wordsPerSegment = Math.ceil(words.length / Math.max(1, Math.floor(duration / 2)))
|
||||
|
||||
// let startTime = 0
|
||||
// for (let i = 0; i < words.length; i += wordsPerSegment) {
|
||||
// const segmentWords = words.slice(i, i + wordsPerSegment)
|
||||
// const segmentDuration = duration * segmentWords.length / words.length
|
||||
|
||||
// segments.push({
|
||||
// text: segmentWords.join(''),
|
||||
// start: startTime,
|
||||
// end: startTime + segmentDuration,
|
||||
// confidence: 0.75 + Math.random() * 0.25
|
||||
// })
|
||||
|
||||
// startTime += segmentDuration
|
||||
// }
|
||||
|
||||
// return segments
|
||||
// }
|
||||
|
||||
/**
|
||||
* 模拟上传进度
|
||||
*/
|
||||
// private simulateProgress(onProgress: (progress: UploadProgress) => void): void {
|
||||
// let progress = 0
|
||||
// const interval = setInterval(() => {
|
||||
// progress += Math.random() * 20
|
||||
// if (progress >= 100) {
|
||||
// progress = 100
|
||||
// clearInterval(interval)
|
||||
// }
|
||||
|
||||
// const fakeTotal = 1024 * 1024 // 1MB
|
||||
// const fakeLoaded = Math.floor(fakeTotal * progress / 100)
|
||||
|
||||
// onProgress({
|
||||
// loaded: fakeLoaded,
|
||||
// total: fakeTotal,
|
||||
// percent: Math.floor(progress)
|
||||
// })
|
||||
// }, 100)
|
||||
// }
|
||||
|
||||
/**
|
||||
* 判断是否应该重试
|
||||
*/
|
||||
// private shouldRetry(error: any): boolean {
|
||||
// // 网络错误可以重试
|
||||
// if (error.message?.includes('网络') || error.message?.includes('timeout')) {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// // 服务器错误可以重试
|
||||
// if (error.message?.includes('服务器') || error.message?.includes('繁忙')) {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// // 其他错误不重试
|
||||
// return false
|
||||
// }
|
||||
|
||||
/**
|
||||
* 格式化错误信息
|
||||
*/
|
||||
private formatError(error: any): Error {
|
||||
let message = error.message || t("Mobile.AudioUploader.uploadFailed");
|
||||
|
||||
// 根据错误类型返回用户友好的提示
|
||||
if (message.includes("timeout")) {
|
||||
message = t("Mobile.AudioUploader.networkTimeout");
|
||||
} else if (message.includes("network")) {
|
||||
message = t("Mobile.AudioUploader.networkException");
|
||||
} else if (message.includes("500")) {
|
||||
message = t("Mobile.AudioUploader.serverError");
|
||||
} else if (message.includes("413")) {
|
||||
message = t("Mobile.AudioUploader.fileTooLargeDuration");
|
||||
} else if (message.includes("415")) {
|
||||
message = t("Mobile.AudioUploader.formatNotSupported");
|
||||
}
|
||||
|
||||
return new Error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
*/
|
||||
// private delay(ms: number): Promise<void> {
|
||||
// return new Promise(resolve => setTimeout(resolve, ms))
|
||||
// }
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config: Partial<UploadConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
getConfig(): UploadConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建音频上传器实例
|
||||
*/
|
||||
export function createAudioUploader(
|
||||
config?: Partial<UploadConfig>,
|
||||
): AudioUploader {
|
||||
return new AudioUploader(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存管理器
|
||||
* 用于清理临时音频文件和管理内存使用
|
||||
*/
|
||||
export class MemoryManager {
|
||||
private static audioFiles: Set<string> = new Set();
|
||||
private static readonly MAX_FILES = 3;
|
||||
|
||||
/**
|
||||
* 添加音频文件到管理列表
|
||||
*/
|
||||
static addAudioFile(filePath: string): void {
|
||||
this.audioFiles.add(filePath);
|
||||
|
||||
// 如果文件数量超过限制,清理最老的文件
|
||||
if (this.audioFiles.size > this.MAX_FILES) {
|
||||
const oldestFile = this.audioFiles.values().next().value;
|
||||
this.cleanupAudioFile(oldestFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理音频文件
|
||||
*/
|
||||
static cleanupAudioFile(filePath: string): void {
|
||||
if (!filePath) return;
|
||||
|
||||
try {
|
||||
// 从管理列表中移除
|
||||
this.audioFiles.delete(filePath);
|
||||
|
||||
// 如果是临时文件URL,释放内存(仅H5平台)
|
||||
// #ifdef H5
|
||||
if (filePath.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(filePath);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 尝试删除临时文件
|
||||
uni.removeSavedFile({
|
||||
filePath: filePath,
|
||||
success: () => {
|
||||
console.log("清理音频文件成功:", filePath);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.warn("清理音频文件失败:", error);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("清理音频文件时发生错误:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有音频文件
|
||||
*/
|
||||
static cleanupAllAudioFiles(): void {
|
||||
for (const filePath of this.audioFiles) {
|
||||
this.cleanupAudioFile(filePath);
|
||||
}
|
||||
this.audioFiles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件数量
|
||||
*/
|
||||
static getFileCount(): number {
|
||||
return this.audioFiles.size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:将音频文件转换为Base64
|
||||
*/
|
||||
export function audioFileToBase64(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileManager = uni.getFileSystemManager();
|
||||
|
||||
fileManager.readFile({
|
||||
filePath: filePath,
|
||||
encoding: "base64",
|
||||
success: (res) => {
|
||||
resolve(res.data as string);
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(
|
||||
new Error(
|
||||
t("Mobile.AudioUploader.fileReadFailed", {
|
||||
reason: error.errMsg || "",
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:检查音频文件格式
|
||||
*/
|
||||
export function validateAudioFormat(format: string): boolean {
|
||||
const supportedFormats = ["mp3", "wav", "aac", "PCM", "webm"];
|
||||
return supportedFormats.includes(format.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:格式化文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:格式化时长
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
}
|
||||
134
qiming-mobile/servers/conversation.uts
Normal file
134
qiming-mobile/servers/conversation.uts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { RequestResponse } from "@/types/interfaces/request";
|
||||
import {
|
||||
ConversationChatSuggestParams,
|
||||
ConversationCreateParams,
|
||||
ConversationInfo,
|
||||
ConversationListParams,
|
||||
MessageInfo,
|
||||
ConversationChatResponse,
|
||||
} from "@/types/interfaces/conversationInfo";
|
||||
import { AgentConversationUpdateParams } from "@/types/interfaces/agent";
|
||||
import {
|
||||
TempConversationQueryDto,
|
||||
TempConversationCreateDto,
|
||||
TempChatCompletionsParams,
|
||||
} from "@/types/interfaces/tempChat";
|
||||
import request from "@/servers/useRequest";
|
||||
|
||||
// 查询会话
|
||||
export async function apiAgentConversation(conversationId: number) {
|
||||
return request<RequestResponse<ConversationInfo>>({
|
||||
url: `/api/agent/conversation/${conversationId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 停止会话
|
||||
export async function apiAgentConversationChatStop(conversationId: string) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/agent/conversation/chat/stop/${conversationId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 根据用户消息更新会话主题
|
||||
export async function apiAgentConversationUpdate(
|
||||
data: AgentConversationUpdateParams,
|
||||
) {
|
||||
return request<RequestResponse<ConversationInfo>>({
|
||||
url: "/api/agent/conversation/update",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询用户历史会话
|
||||
export async function apiAgentConversationList(data: ConversationListParams) {
|
||||
return request<RequestResponse<ConversationInfo[]>>({
|
||||
url: "/api/agent/conversation/list",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
export async function apiAgentConversationDelete(conversationId: number) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/agent/conversation/delete/${conversationId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
export async function apiAgentConversationCreate(
|
||||
data: ConversationCreateParams,
|
||||
) {
|
||||
return request<RequestResponse<ConversationInfo>>({
|
||||
url: "/api/agent/conversation/create",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 智能体会话问题建议
|
||||
export async function apiAgentConversationChatSuggest(
|
||||
data: ConversationChatSuggestParams,
|
||||
) {
|
||||
return request<RequestResponse<string[]>>({
|
||||
url: "/api/agent/conversation/chat/suggest",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询临时会话详细
|
||||
export async function apiTempChatConversationQuery(
|
||||
data: TempConversationQueryDto,
|
||||
) {
|
||||
return request<RequestResponse<ConversationInfo>>({
|
||||
url: "/api/temp/chat/conversation/query",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建临时会话
|
||||
export async function apiTempChatConversationCreate(
|
||||
data: TempConversationCreateDto,
|
||||
) {
|
||||
return request<RequestResponse<ConversationInfo>>({
|
||||
url: "/api/temp/chat/conversation/create",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 临时消息会话
|
||||
export async function apiTempChatCompletions(data: TempChatCompletionsParams) {
|
||||
return request<RequestResponse<ConversationChatResponse>>({
|
||||
url: "/api/temp/chat/completions",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 停止临时会话
|
||||
export async function apiTempChatConversationStop(requestId: string) {
|
||||
return request<RequestResponse<null>>({
|
||||
url: `/api/temp/chat/conversation/${requestId}`,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// 查询会话消息列表(加载更多历史消息)
|
||||
export async function apiAgentConversationMessageList(data: {
|
||||
conversationId: number;
|
||||
index: number;
|
||||
size: number;
|
||||
}) {
|
||||
return request<RequestResponse<MessageInfo[]>>({
|
||||
url: "/api/agent/conversation/message/list",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
45
qiming-mobile/servers/event.uts
Normal file
45
qiming-mobile/servers/event.uts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 事件轮询 API 服务
|
||||
* 用于 batch 轮询获取服务端推送事件
|
||||
*
|
||||
* 与 PC 端 @/services/event.ts 保持一致
|
||||
*/
|
||||
import request from './useRequest';
|
||||
import { EventTypeEnum } from '@/types/enums/event';
|
||||
|
||||
/**
|
||||
* 事件列表项
|
||||
*/
|
||||
interface EventListItem {
|
||||
event: any;
|
||||
type: EventTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件收集响应
|
||||
*/
|
||||
export interface ApiCollectEventResponse {
|
||||
hasEvent: boolean;
|
||||
eventList: EventListItem[];
|
||||
version?: string; // 版本号
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询获取事件
|
||||
*/
|
||||
export async function apiCollectEvent(): Promise<ApiCollectEventResponse> {
|
||||
return request<ApiCollectEventResponse>({
|
||||
url: '/api/notify/event/collect/batch',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除事件
|
||||
*/
|
||||
export async function apiClearEvent(): Promise<any> {
|
||||
return request({
|
||||
url: '/api/notify/event/clear',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
35
qiming-mobile/servers/i18n.uts
Normal file
35
qiming-mobile/servers/i18n.uts
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from "./useRequest";
|
||||
import type { RequestResponse } from "@/types/interfaces/request";
|
||||
import type { I18nLangDto } from "@/types/interfaces/i18n";
|
||||
import { I18N_SET_LANG_API } from "@/constants/i18n.constants";
|
||||
|
||||
export type LangMap = Record<string, string>;
|
||||
|
||||
// 查询语言列表
|
||||
export function apiI18nLangList() {
|
||||
return request<RequestResponse<I18nLangDto[]>>({
|
||||
url: "/api/i18n/lang/list",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 查询语言字典
|
||||
export function apiI18nQuery(lang: string = "") {
|
||||
const side = "Mobile";
|
||||
const query = lang
|
||||
? `?lang=${encodeURIComponent(lang)}&side=${side}`
|
||||
: `?side=${side}`;
|
||||
return request<RequestResponse<LangMap>>({
|
||||
url: `/api/i18n/query${query}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// 设置用户语言(后端专用接口)
|
||||
export function apiI18nSetLang(lang: string) {
|
||||
return request<RequestResponse<any>>({
|
||||
url: I18N_SET_LANG_API,
|
||||
method: "POST",
|
||||
data: { lang },
|
||||
});
|
||||
}
|
||||
33
qiming-mobile/servers/square.uts
Normal file
33
qiming-mobile/servers/square.uts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Page, RequestResponse } from '../types/interfaces/request';
|
||||
import { SquarePublishedListParams, SquarePublishedItemInfo, SquareCategoryInfo } from '../types/interfaces/square';
|
||||
import request from './useRequest';
|
||||
import { AgentDetailDto } from '../types/interfaces/agent';
|
||||
|
||||
// 广场-已发布智能体列表接口
|
||||
export async function apiPublishedAgentList(
|
||||
data: SquarePublishedListParams,
|
||||
) {
|
||||
return request<RequestResponse<Page<SquarePublishedItemInfo>>>({
|
||||
url: '/api/published/agent/list',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取广场分类列表
|
||||
export async function apiPublishedCategoryList() {
|
||||
return request<RequestResponse<SquareCategoryInfo[]>>({
|
||||
url: '/api/published/category/list',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取已发布的智能体详情
|
||||
export async function apiSquareAgentDetail(
|
||||
agentId: number,
|
||||
) {
|
||||
return request<RequestResponse<AgentDetailDto>>({
|
||||
url: `/api/published/agent/${agentId}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
244
qiming-mobile/servers/useRequest.uts
Normal file
244
qiming-mobile/servers/useRequest.uts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
CONVERSATION_CONNECTION_URL,
|
||||
COMMON_HEADERS,
|
||||
} from "@/constants/common.constants";
|
||||
import { ACCESS_TOKEN, EXPIRE_DATE } from "../constants/home.constants";
|
||||
import {
|
||||
DEFAULT_LANG,
|
||||
I18N_LANG_STORAGE_KEY,
|
||||
} from "@/constants/i18n.constants";
|
||||
import { API_BASE_URL, LOGIN_WHITELIST_URL } from "../constants/config";
|
||||
import {
|
||||
REDIRECT_LOGIN,
|
||||
SUCCESS_CODE,
|
||||
USER_NO_LOGIN,
|
||||
} from "@/constants/codes.constants";
|
||||
|
||||
import {
|
||||
fetchEventSource,
|
||||
EventSourceMessage,
|
||||
} from "@microsoft/fetch-event-source";
|
||||
|
||||
import { handleExternalLink } from "@/utils/system.uts";
|
||||
import { getCurrentPageFullPath } from "@/utils/common";
|
||||
|
||||
// 微信小程序:request
|
||||
export default function request<T = any>(options: any) {
|
||||
const { url, method, ...rest } = options;
|
||||
const accessToken = uni.getStorageSync(ACCESS_TOKEN);
|
||||
let currentLang = uni.getStorageSync(I18N_LANG_STORAGE_KEY) || DEFAULT_LANG;
|
||||
currentLang = String(currentLang || DEFAULT_LANG).toLowerCase();
|
||||
const header = {
|
||||
...COMMON_HEADERS,
|
||||
"Accept-Language": currentLang,
|
||||
};
|
||||
if (accessToken) {
|
||||
// #ifdef H5 || WEB
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
// #endif
|
||||
}
|
||||
|
||||
// uEnvProd
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// #ifdef H5
|
||||
//仅H5需要添加fragment参数
|
||||
const fragment = (window.location.hash || "#/").replace("#/", "/");
|
||||
if (fragment) {
|
||||
header["fragment"] = fragment;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// uni.request 在 UTS 里返回 RequestTask(非 Promise),这里手动包装为 Promise
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
uni.request<T>({
|
||||
header,
|
||||
url: API_BASE_URL + url,
|
||||
method: method || "POST",
|
||||
...rest,
|
||||
success: (res: any) => {
|
||||
const responseData = res.data as any;
|
||||
const { code, message: errorMessage } = responseData || {};
|
||||
|
||||
// batch 接口不做错误消息与跳转相关逻辑
|
||||
const isBatchUrl = url?.includes("/api/notify/event/collect/batch");
|
||||
if (isBatchUrl) {
|
||||
resolve(responseData as T);
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户未登录,跳转到登录页
|
||||
if (code === USER_NO_LOGIN) {
|
||||
uni.setStorageSync(ACCESS_TOKEN, ""); // 清空token【兼容鸿蒙系统】
|
||||
uni.clearStorageSync(); // 清空所有缓存
|
||||
|
||||
// #ifdef H5 || WEB
|
||||
uni.reLaunch({
|
||||
url: "/subpackages/pages/login/login",
|
||||
});
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.reLaunch({
|
||||
url: "/subpackages/pages/login-weixin/login-weixin",
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
// 重定向到登录页
|
||||
if (code === REDIRECT_LOGIN) {
|
||||
uni.setStorageSync(ACCESS_TOKEN, ""); // 清空token【兼容鸿蒙系统】
|
||||
uni.clearStorageSync(); // 清空所有缓存
|
||||
let redirectUrl = errorMessage;
|
||||
// 如果错误信息包含/login开头,则替换为登录页,否则直接打开完整链接
|
||||
if (errorMessage?.startsWith("/login")) {
|
||||
// #ifdef H5 || WEB
|
||||
// 如果url在登录白名单地址中,则不跳转到登录页
|
||||
if (LOGIN_WHITELIST_URL.includes(url)) {
|
||||
resolve(responseData as T);
|
||||
return;
|
||||
}
|
||||
redirectUrl = errorMessage?.replace(
|
||||
"/login",
|
||||
"/subpackages/pages/login/login",
|
||||
);
|
||||
uni.reLaunch({ url: redirectUrl });
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序:获取当前页面的 url
|
||||
const currentUrl = getCurrentPageFullPath();
|
||||
if (currentUrl) {
|
||||
if (globalThis.appConfig.redirectUrl) {
|
||||
const otherQueryStr = globalThis.appConfig.redirectUrl;
|
||||
uni.reLaunch({
|
||||
url:
|
||||
"/subpackages/pages/login-weixin/login-weixin?redirect=" +
|
||||
encodeURIComponent(
|
||||
"/subpackages/pages/webview/webview?url=" + otherQueryStr,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url:
|
||||
"/subpackages/pages/login-weixin/login-weixin?redirect=" +
|
||||
encodeURIComponent("/" + currentUrl),
|
||||
});
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
// #ifdef H5 || WEB
|
||||
window.location.href = errorMessage;
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
handleExternalLink(errorMessage);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
resolve(responseData as T);
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error("Request error:", error);
|
||||
// 网络错误或其他错误,也返回错误信息
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface SSEOptions<T = any> {
|
||||
url: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: { [key: string]: string };
|
||||
body?: any;
|
||||
onMessage: (data: T) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onOpen?: (response: any) => void;
|
||||
onClose?: () => void;
|
||||
abortController?: any;
|
||||
}
|
||||
|
||||
// H5 创建SSE连接
|
||||
export async function createSSEConnection<T = any>(options: SSEOptions<T>) {
|
||||
const controller = options.abortController || new AbortController();
|
||||
const currentLang = String(
|
||||
uni.getStorageSync(I18N_LANG_STORAGE_KEY) || DEFAULT_LANG,
|
||||
).toLowerCase();
|
||||
//仅H5需要添加fragment参数
|
||||
const fragment = (window.location.hash || "#/").replace("#/", "/");
|
||||
try {
|
||||
// 通过 Parameters<typeof fetchEventSource>[1] 对齐第三方库参数类型
|
||||
const fetchOptions = {
|
||||
method: options.method || "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Language": currentLang,
|
||||
...options.headers,
|
||||
...(fragment && { fragment }),
|
||||
},
|
||||
body:
|
||||
typeof options.body === "object"
|
||||
? JSON.stringify(options.body)
|
||||
: options.body,
|
||||
signal: controller.signal,
|
||||
openWhenHidden: true, // 页面不可见时保持连接
|
||||
|
||||
onopen: async (response) => {
|
||||
if (response.status >= 400) {
|
||||
throw new Error(
|
||||
String(response.statusText || response.status || 0),
|
||||
);
|
||||
}
|
||||
options.onOpen?.(response);
|
||||
},
|
||||
|
||||
onmessage: (event: EventSourceMessage) => {
|
||||
try {
|
||||
const data = event.data ? (JSON.parse(event.data) as T) : null;
|
||||
// 无消息体时不回调,避免把 null 传给强类型的 T
|
||||
if (data !== null) {
|
||||
options.onMessage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
const normalizedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
options.onError?.(normalizedError);
|
||||
}
|
||||
},
|
||||
|
||||
onclose: () => {
|
||||
options.onClose?.();
|
||||
},
|
||||
|
||||
onerror: (error) => {
|
||||
// 如果是中止错误,不抛出异常
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
console.log("SSE连接已被中止");
|
||||
return;
|
||||
}
|
||||
const normalizedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
options.onError?.(normalizedError);
|
||||
throw normalizedError; // 停止自动重试
|
||||
},
|
||||
} as Parameters<typeof fetchEventSource>[1];
|
||||
await fetchEventSource(options.url, fetchOptions);
|
||||
} catch (error) {
|
||||
// 静默处理所有 AbortError
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
console.log("SSE连接已被中止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有真正的错误才触发错误回调
|
||||
const normalized =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
options.onError?.(normalized);
|
||||
throw normalized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user