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

View File

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

View File

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

View File

@@ -88,6 +88,28 @@ function ModelsPage() {
return filterFactory || []; return filterFactory || [];
}, [llmFactory, myLlm]); }, [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) => { const handleConfigureFactory = useCallback((factory: IFactory) => {
if (factory == null) { if (factory == null) {
@@ -126,7 +148,31 @@ function ModelsPage() {
if (factoryName == null) { if (factoryName == null) {
return; 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(); const dialog = useDialog();
@@ -247,7 +293,7 @@ function ModelsPage() {
variant='contained' color='primary' startIcon={<EditIcon />} variant='contained' color='primary' startIcon={<EditIcon />}
onClick={() => handleEditLlmFactory(factoryName)} onClick={() => handleEditLlmFactory(factoryName)}
> >
{t('setting.edit')} { showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}
</Button> </Button>
<Button <Button
variant='outlined' color='primary' startIcon={<DeleteIcon />} variant='outlined' color='primary' startIcon={<DeleteIcon />}

View File

@@ -129,43 +129,73 @@ request.interceptors.request.use(
// 响应拦截器 // 响应拦截器
request.interceptors.response.use( request.interceptors.response.use(
(response: AxiosResponse) => { (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]); snackbar.error(RetcodeMessage[status as ResultCode]);
} }
if (status == 401) {
logger.info('⚠️ Handling HTTP 401 unauthorized');
notification.error(i18n.t('message.401'));
redirectToLogin();
}
// 处理blob类型响应 // 处理blob类型响应
if (response.config.responseType === 'blob') { if (response.config.responseType === 'blob') {
logger.info('📁 Blob response detected, skipping data processing');
return response; return response;
} }
console.log('interceptors.response ------ response', response); logger.info('interceptors.response ------ response', response);
const data: ResponseType = response.data; 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')); notification.error(data?.message, i18n.t('message.401'));
redirectToLogin(); redirectToLogin();
} else if (data?.code !== 0) { } 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')); const error = new CustomError(data?.message || i18n.t('message.requestError'));
error.code = data?.code || -1; error.code = data?.code || -1;
error.response = data; error.response = data;
snackbar.warning(error.message); snackbar.warning(error.message);
throw error; throw error;
} }
logger.info('✅ Response processed successfully');
return response; return response;
}, },
(error) => { (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) { 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) { } else if (error.response) {
const { status, statusText } = error.response; const { status, statusText } = error.response;
const errorText = RetcodeMessage[status as ResultCode] || statusText; const errorText = RetcodeMessage[status as ResultCode] || statusText;
logger.info('🚫 HTTP error response:', { status, statusText, errorText });
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText); notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
} }
logger.info('🔄 Rejecting promise with error');
return Promise.reject(error); return Promise.reject(error);
} }
); );

View File

@@ -17,15 +17,32 @@ export default defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
output: { output: {
// 优化缓存策略,使用内容哈希,启用长期缓存
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks: (id) => { manualChunks: (id) => {
// 将所有 SVG 文件打包到一个单独的 chunk 中 // SVG 文件分割策略
if (id.includes('.svg') && id.includes('?react')) { 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: { server: {
host: '0.0.0.0', host: '0.0.0.0',
allowedHosts: ['154.9.253.114', 'localhost', 'teres.deep-pilot.chat'], allowedHosts: ['154.9.253.114', 'localhost', 'teres.deep-pilot.chat'],