refactor(build): improve caching strategy with content hashing

feat(header): add translation support for menu items
fix(models): enhance model factory handling with proper dialogs
This commit is contained in:
2025-10-29 10:29:08 +08:00
parent 303715f82c
commit ef0a99ea30
9 changed files with 158 additions and 145 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.3 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 MiB

View File

@@ -2,8 +2,26 @@ import React from "react";
import { SvgIcon, type SvgIconProps } from "@mui/material";
import logger from "@/utils/logger";
// 使用 import.meta.glob 预加载所有 SVG 文件
const svgModules = import.meta.glob('/src/assets/svg/**/*.svg', {
// 按目录分别预加载 SVG 文件,实现按需加载
const llmSvgModules = import.meta.glob('/src/assets/svg/llm/*.svg', {
query: '?react',
import: 'default',
eager: false
});
const chunkSvgModules = import.meta.glob('/src/assets/svg/chunk-method/*.svg', {
query: '?react',
import: 'default',
eager: false
});
const fileSvgModules = import.meta.glob('/src/assets/svg/file-icon/*.svg', {
query: '?react',
import: 'default',
eager: false
});
const commonSvgModules = import.meta.glob('/src/assets/svg/*.svg', {
query: '?react',
import: 'default',
eager: false
@@ -27,8 +45,22 @@ const getPointPath = (point: AppSvgIconProps['point']) => {
return ''
}
// 根据类型获取对应的模块映射
const getSvgModules = (point: AppSvgIconProps['point']) => {
switch (point) {
case 'llm':
return llmSvgModules;
case 'chunk':
return chunkSvgModules;
case 'file':
return fileSvgModules;
default:
return commonSvgModules;
}
}
/**
* 通用 SVG 图标组件
* 通用 SVG 图标组件 - 优化版本,支持按需加载
* <AppSvgIcon name="add" sx={{ color: 'primary.main', fontSize: 30 }} />
*/
export default function AppSvgIcon(props: AppSvgIconProps) {
@@ -46,8 +78,10 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
setLoading(true);
const iconPath = `/src/assets/svg${pointPath}/${name}.svg`;
// 使用预定义的模块映射
// 根据类型获取对应的模块映射
const svgModules = getSvgModules(point);
const moduleLoader = svgModules[iconPath];
if (moduleLoader) {
const iconModule = await moduleLoader();
setIcon(() => iconModule as React.FC<React.SVGProps<SVGSVGElement>>);
@@ -63,10 +97,11 @@ export default function AppSvgIcon(props: AppSvgIconProps) {
};
loadIcon();
}, [name, pointPath]);
}, [name, pointPath, point]);
if (loading) {
return null; // 或者返回一个加载占位符
// 返回一个简单的占位符,避免布局跳动
return <SvgIcon {...rest} style={{ opacity: 0 }} />;
}
if (!Icon) {

View File

@@ -20,6 +20,7 @@ import {
import LanguageSwitcher from '../LanguageSwitcher';
import { useAuth } from '@/hooks/login-hooks';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Header = () => {
const { userInfo, logout } = useAuth();
@@ -27,6 +28,8 @@ const Header = () => {
const open = Boolean(anchorEl);
const navigate = useNavigate();
const {t} = useTranslation();
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
@@ -193,14 +196,14 @@ const Header = () => {
<ListItemIcon>
<PersonIcon fontSize="small" />
</ListItemIcon>
<ListItemText></ListItemText>
<ListItemText>{t('setting.personalProfile')}</ListItemText>
</MenuItem>
{/* 模型配置 */}
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
<ListItemIcon>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText></ListItemText>
<ListItemText>{t('setting.modelSettings')}</ListItemText>
</MenuItem>
<Divider />
@@ -209,7 +212,7 @@ const Header = () => {
<ListItemIcon>
<LogoutIcon fontSize="small" sx={{ color: '#d32f2f' }} />
</ListItemIcon>
<ListItemText>退</ListItemText>
<ListItemText>{t('setting.logout')}</ListItemText>
</MenuItem>
</Menu>
</Box>

View File

@@ -161,7 +161,7 @@ export const useApiKeyDialog = (onSuccess?: () => void) => {
};
// Ollama 对话框管理
export const useOllamaDialog = () => {
export const useOllamaDialog = (onSuccess?: () => void) => {
const dialogState = useDialogState();
const showMessage = useMessage();
@@ -179,6 +179,11 @@ export const useOllamaDialog = () => {
});
showMessage.success('Ollama 模型添加成功');
dialogState.closeDialog();
// 调用成功回调
if (onSuccess) {
onSuccess();
}
} catch (error) {
logger.error('Ollama 模型添加失败:', error);
showMessage.error('Ollama 模型添加失败');
@@ -186,7 +191,7 @@ export const useOllamaDialog = () => {
} finally {
dialogState.setLoading(false);
}
}, [dialogState, showMessage]);
}, [dialogState, showMessage, onSuccess]);
return {
...dialogState,
@@ -340,7 +345,7 @@ export const useSystemModelSetting = (onSuccess?: () => void) => {
// 统一的模型对话框管理器
export const useModelDialogs = (onSuccess?: () => void) => {
const apiKeyDialog = useApiKeyDialog(onSuccess);
const ollamaDialog = useOllamaDialog();
const ollamaDialog = useOllamaDialog(onSuccess);
const configurationDialog = useConfigurationDialog(onSuccess);
const systemDialog = useSystemModelSetting(onSuccess);
const deleteOps = useDeleteOperations(onSuccess);

View File

@@ -88,6 +88,28 @@ function ModelsPage() {
return filterFactory || [];
}, [llmFactory, myLlm]);
const showAddModel = (factoryName: string) => {
const configurationFactories: LLMFactory[] = [
LLM_FACTORY_LIST.AzureOpenAI,
LLM_FACTORY_LIST.Bedrock,
LLM_FACTORY_LIST.BaiduYiYan,
LLM_FACTORY_LIST.FishAudio,
LLM_FACTORY_LIST.GoogleCloud,
LLM_FACTORY_LIST.TencentCloud,
LLM_FACTORY_LIST.TencentHunYuan,
LLM_FACTORY_LIST.XunFeiSpark,
LLM_FACTORY_LIST.VolcEngine,
]
const fN = factoryName as LLMFactory;
if (!fN) {
return false;
}
return LocalLlmFactories.includes(fN) ||
configurationFactories.includes(fN);
}
// 处理配置模型工厂
const handleConfigureFactory = useCallback((factory: IFactory) => {
if (factory == null) {
@@ -126,7 +148,31 @@ function ModelsPage() {
if (factoryName == null) {
return;
}
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
const factoryN = factoryName as LLMFactory;
const configurationFactories: LLMFactory[] = [
LLM_FACTORY_LIST.AzureOpenAI,
LLM_FACTORY_LIST.Bedrock,
LLM_FACTORY_LIST.BaiduYiYan,
LLM_FACTORY_LIST.FishAudio,
LLM_FACTORY_LIST.GoogleCloud,
LLM_FACTORY_LIST.TencentCloud,
LLM_FACTORY_LIST.TencentHunYuan,
LLM_FACTORY_LIST.XunFeiSpark,
LLM_FACTORY_LIST.VolcEngine,
]
if (LocalLlmFactories.includes(factoryN)) {
// local llm
modelDialogs.ollamaDialog.openDialog({
llm_factory: factoryN,
}, true);
} else if (configurationFactories.includes(factoryN)) {
// custom configuration llm
modelDialogs.configurationDialog.openConfigurationDialog(factoryN);
} else {
// llm set api
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryN, {}, true);
}
logger.debug('handleEditLlmFactory', factoryN);
}, []);
const dialog = useDialog();
@@ -247,7 +293,7 @@ function ModelsPage() {
variant='contained' color='primary' startIcon={<EditIcon />}
onClick={() => handleEditLlmFactory(factoryName)}
>
{t('setting.edit')}
{ showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}
</Button>
<Button
variant='outlined' color='primary' startIcon={<DeleteIcon />}

View File

@@ -129,43 +129,73 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
const { status } = response;
logger.info('🔍 Response interceptor triggered:', {
url: response.config.url,
status: response.status,
responseType: response.config.responseType,
hasData: !!response.data
});
const { status } = response;
// 处理特定状态码
if (status === 413 || status === 504) {
if (status == 413 || status == 504) {
logger.info('⚠️ Handling HTTP error status:', status);
snackbar.error(RetcodeMessage[status as ResultCode]);
}
if (status == 401) {
logger.info('⚠️ Handling HTTP 401 unauthorized');
notification.error(i18n.t('message.401'));
redirectToLogin();
}
// 处理blob类型响应
if (response.config.responseType === 'blob') {
logger.info('📁 Blob response detected, skipping data processing');
return response;
}
console.log('interceptors.response ------ response', response);
logger.info('interceptors.response ------ response', response);
const data: ResponseType = response.data;
// 处理业务错误码
if (data?.code === 401) {
if (data?.code == 401) {
logger.info('⚠️ Handling business code 401 unauthorized');
notification.error(data?.message, i18n.t('message.401'));
redirectToLogin();
} else if (data?.code !== 0) {
// 处理其他业务错误
logger.info('❌ Handling business error:', { code: data?.code, message: data?.message });
const error = new CustomError(data?.message || i18n.t('message.requestError'));
error.code = data?.code || -1;
error.response = data;
snackbar.warning(error.message);
throw error;
}
logger.info('✅ Response processed successfully');
return response;
},
(error) => {
logger.info('❌ Response interceptor error handler triggered:', {
message: error.message,
status: error.response?.status,
url: error.config?.url,
hasResponse: !!error.response
});
const { status } = error.response || {};
if (status == 401) {
redirectToLogin();
}
// 处理网络错误
if (error.message === FAILED_TO_FETCH || !error.response) {
// notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
logger.info('🌐 Network error detected:', error.message);
} else if (error.response) {
const { status, statusText } = error.response;
const errorText = RetcodeMessage[status as ResultCode] || statusText;
logger.info('🚫 HTTP error response:', { status, statusText, errorText });
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
}
logger.info('🔄 Rejecting promise with error');
return Promise.reject(error);
}
);

View File

@@ -17,14 +17,31 @@ export default defineConfig({
build: {
rollupOptions: {
output: {
// 优化缓存策略,使用内容哈希,启用长期缓存
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks: (id) => {
// 将所有 SVG 文件打包到一个单独的 chunk 中
// SVG 文件分割策略
if (id.includes('.svg') && id.includes('?react')) {
return 'svg-icons';
// 排除 chunk-method 文件夹的 SVG
if (id.includes('/chunk-method/')) {
// 不打包 chunk-method 的 SVG让它们保持为独立的资源文件
return undefined;
}
// 其他 SVG 文件正常分割
if (id.includes('/llm/')) {
return 'svg-llm';
} else if (id.includes('/file-icon/')) {
return 'svg-file';
} else {
return 'svg-common';
}
}
}
}
}
},
},
server: {
host: '0.0.0.0',