添加会话模型和API Key配置功能

- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
This commit is contained in:
2026-05-11 20:01:03 +08:00
parent 0a42e700e2
commit 29838b030f
19 changed files with 1296 additions and 274 deletions

View File

@@ -7,10 +7,10 @@ import { getChangedFields } from '/@/utils/diffUtils';
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
/**
* 控制一次请求的错误提示归属:
* - global: 交给 request.ts 统一弹错,适合绝大多数接口
* - page: 页面自己在 catch 中决定提示文案,避免与全局重复
* - silent: 完全静默,适合轮询、后台刷新等不希望打扰用户的请求
* 控制一次请求的错误提示归属(默认 global
* - global: 由拦截器统一弹出后端返回的 message含 HTTP 与业务 JSON
* - page: 不自动弹窗,仅 reject请在页面 catch 内自行处理(应与全局择一,避免重复
* - silent: 完全静默(轮询等)
*/
export interface RequestOptions {
errorMode?: 'global' | 'page' | 'silent';
@@ -39,6 +39,26 @@ const ERROR_MESSAGE_INTERVAL = 2000;
const getErrorMode = (config?: InternalAxiosRequestConfig) => config?.requestOptions?.errorMode ?? 'global';
const shouldShowGlobalError = (config?: InternalAxiosRequestConfig) => getErrorMode(config) === 'global';
/**
* 从接口响应体解析可读错误文案JSON API、Spring 风格等)
*/
export function extractBackendMessage(data: unknown): string | undefined {
if (data == null) return undefined;
if (typeof data === 'string') {
const t = data.trim();
return t.length > 0 && t.length < 2000 ? t : undefined;
}
if (typeof data !== 'object') return undefined;
const o = data as Record<string, unknown>;
const pick = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
return (
pick(o.message) ||
pick(o.msg) ||
pick(o.error) ||
(typeof o.detail === 'string' ? pick(o.detail) : undefined)
);
}
const closeActiveErrorMessage = () => {
activeErrorMessage?.close();
activeErrorMessage = null;
@@ -174,7 +194,7 @@ const responseInterceptor = (response: AxiosResponse) => {
const res = response.data;
const httpStatus = response.status;
const code = res?.code;
const message = res?.message;
const message = extractBackendMessage(res);
const config = response.config;
if (isTokenExpiredError(httpStatus, code, message)) {
@@ -207,8 +227,8 @@ const responseInterceptor = (response: AxiosResponse) => {
if (knownErrorCodes.includes(code)) {
errorMsg = message || `请求失败(${code})`;
} else {
// 未知的 code,统一提示后端异常
errorMsg = '后端异常,请联系管理员';
// 未知的 code:优先使用后端 message便于排查业务含义
errorMsg = message || '后端异常,请联系管理员';
}
showErrorMessage(errorMsg, config);
@@ -221,7 +241,8 @@ const responseInterceptor = (response: AxiosResponse) => {
const responseErrorHandler = (error: any) => {
const config = error.config as InternalAxiosRequestConfig | undefined;
const httpStatus = error.response?.status;
const responseMessage = error.response?.data?.message;
const responseData = error.response?.data;
const responseMessage = extractBackendMessage(responseData);
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
showErrorMessage('请求超时,请检查网络连接', config);
@@ -232,7 +253,7 @@ const responseErrorHandler = (error: any) => {
return Promise.reject(error);
}
if (isTokenExpiredError(httpStatus, error.response?.data?.code, responseMessage)) {
if (isTokenExpiredError(httpStatus, error.response?.data?.code as number | undefined, responseMessage)) {
handleTokenExpired();
return Promise.reject(new Error('登录状态已过期'));
}
@@ -245,7 +266,7 @@ const responseErrorHandler = (error: any) => {
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
const now = Date.now();
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面', config);
showErrorMessage(responseMessage ?? '服务开通中,请稍后刷新页面', config);
return Promise.reject(new Error('模块开通中'));
}
@@ -253,30 +274,30 @@ const responseErrorHandler = (error: any) => {
handleModuleNotEnabled(currentPath);
return Promise.reject(new Error('模块未开通'));
}
showErrorMessage(responseMessage || '服务未开通', config);
showErrorMessage(responseMessage ?? '服务未开通', config);
break;
case 403:
showErrorMessage(responseMessage || '没有权限访问该资源', config);
showErrorMessage(responseMessage ?? '没有权限访问该资源', config);
break;
case 404:
showErrorMessage(responseMessage || '请求的资源不存在', config);
showErrorMessage(responseMessage ?? '请求的资源不存在', config);
break;
case 429:
// 429 是限流,不等于登录过期,这里只保留频率提示。
showErrorMessage(responseMessage || '请求过于频繁,请稍后再试', config);
showErrorMessage(responseMessage ?? '请求过于频繁,请稍后再试', config);
break;
case 500:
showErrorMessage(responseMessage || '服务器内部错误', config);
showErrorMessage(responseMessage ?? '服务器内部错误', config);
break;
case 502:
showErrorMessage(responseMessage || '网关错误', config);
showErrorMessage(responseMessage ?? '网关错误', config);
break;
case 503:
showErrorMessage(responseMessage || '服务不可用', config);
showErrorMessage(responseMessage ?? '服务不可用', config);
break;
default:
if (httpStatus >= 400) {
showErrorMessage(responseMessage || `请求失败(${httpStatus})`, config);
showErrorMessage(responseMessage ?? `请求失败(${httpStatus})`, config);
}
}
@@ -286,5 +307,27 @@ const responseErrorHandler = (error: any) => {
service.interceptors.request.use(requestInterceptor, requestErrorHandler);
service.interceptors.response.use(responseInterceptor, responseErrorHandler);
/**
* 从 axios / 业务 reject 中取出后端返回的提示文案与全局拦截器同源逻辑silent / 特殊场景下可在页面使用)。
*/
export function getApiErrorMessage(error: unknown, fallback = '操作失败'): string {
const e = error as any;
const fromBody = extractBackendMessage(e?.response?.data);
if (fromBody != null && fromBody !== '') {
return fromBody;
}
const msg = e?.message;
if (typeof msg === 'string' && msg.trim() !== '') {
if (/^Request failed with status code \d+$/i.test(msg)) {
return extractBackendMessage(e?.response?.data) ?? fallback;
}
if (msg === 'Network Error') {
return '网络异常,请检查网络连接';
}
return msg;
}
return fallback;
}
export default service;
export { closeActiveErrorMessage, showErrorMessage };