From f44453d93ef52351e4a519b7a1d9a8fbf27fa5c3 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Tue, 14 Apr 2026 12:37:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86mcp=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/app_factory.py | 5 +- backend/app/core/middleware.py | 20 +++ backend/app/mcp/server.py | 22 +++- frontend/nginx.conf | 22 ++++ frontend/src/App.jsx | 74 +++++++---- frontend/src/components/MarkdownEditor.jsx | 80 +++++++++--- frontend/src/components/MarkdownRenderer.jsx | 62 +++++++++- frontend/src/components/MindMap.jsx | 115 +++++++++++++----- .../src/config/systemManagementModules.jsx | 29 ++--- frontend/src/pages/AdminManagement.jsx | 23 +++- frontend/vite.config.js | 69 +++++++++++ 11 files changed, 411 insertions(+), 110 deletions(-) diff --git a/backend/app/app_factory.py b/backend/app/app_factory.py index cd96791..9f42093 100644 --- a/backend/app/app_factory.py +++ b/backend/app/app_factory.py @@ -24,7 +24,7 @@ from app.api.endpoints import ( voiceprint, ) from app.core.config import UPLOAD_DIR -from app.core.middleware import TerminalCheckMiddleware +from app.core.middleware import MCPPathNormalizeMiddleware, TerminalCheckMiddleware from app.mcp import create_mcp_http_app, get_mcp_session_manager from app.services.system_config_service import SystemConfigService @@ -38,6 +38,7 @@ def create_app() -> FastAPI: redoc_url=None, ) + app.add_middleware(MCPPathNormalizeMiddleware) app.add_middleware(TerminalCheckMiddleware) app.add_middleware( CORSMiddleware, @@ -50,8 +51,8 @@ def create_app() -> FastAPI: if UPLOAD_DIR.exists(): app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads") - mcp_session_manager = get_mcp_session_manager() mcp_asgi_app = create_mcp_http_app() + mcp_session_manager = get_mcp_session_manager() if mcp_asgi_app is not None: app.mount("/mcp", mcp_asgi_app, name="mcp") diff --git a/backend/app/core/middleware.py b/backend/app/core/middleware.py index a987219..b123ac7 100644 --- a/backend/app/core/middleware.py +++ b/backend/app/core/middleware.py @@ -5,6 +5,26 @@ from app.services.terminal_service import terminal_service from app.services.jwt_service import jwt_service from app.core.response import create_api_response + +class MCPPathNormalizeMiddleware: + """Normalize the public MCP endpoint to /mcp while keeping the mounted app happy.""" + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] == "http" and scope.get("path") == "/mcp": + normalized_scope = dict(scope) + normalized_scope["path"] = "/mcp/" + + raw_path = scope.get("raw_path") + if raw_path in (None, b"/mcp"): + normalized_scope["raw_path"] = b"/mcp/" + + scope = normalized_scope + + await self.app(scope, receive, send) + class TerminalCheckMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行 diff --git a/backend/app/mcp/server.py b/backend/app/mcp/server.py index b075bf2..c7de8e0 100644 --- a/backend/app/mcp/server.py +++ b/backend/app/mcp/server.py @@ -6,6 +6,7 @@ from __future__ import annotations from datetime import datetime import hmac import json +import logging from typing import Any, Dict, Optional from fastapi.responses import JSONResponse @@ -16,7 +17,7 @@ try: except ImportError: # pragma: no cover - runtime dependency FastMCP = None -from app.core.config import APP_CONFIG +from app.core.config import APP_CONFIG, API_CONFIG from app.core.database import get_db_connection from app.mcp.context import ( MCPRequestContext, @@ -26,6 +27,8 @@ from app.mcp.context import ( ) from app.services import meeting_service +logger = logging.getLogger(__name__) + def _build_absolute_url(path: str) -> str: normalized_base = APP_CONFIG["base_url"].rstrip("/") @@ -181,6 +184,9 @@ class MCPHeaderAuthApp: reset_current_mcp_request(token) +_mcp_http_app = None + + if FastMCP is not None: mcp_server = FastMCP( "iMeeting MCP", @@ -188,6 +194,9 @@ if FastMCP is not None: stateless_http=True, streamable_http_path="/", ) + # Keep FastMCP's server settings aligned with the outer Uvicorn process. + mcp_server.settings.host = API_CONFIG["host"] + mcp_server.settings.port = API_CONFIG["port"] @mcp_server.tool() def get_my_meetings() -> Dict[str, Any]: @@ -237,6 +246,7 @@ if FastMCP is not None: } else: # pragma: no cover - graceful fallback without runtime dependency mcp_server = None + logger.warning("FastMCP is unavailable because the 'mcp' package is not installed; /mcp will not be mounted.") def get_mcp_server(): @@ -244,15 +254,21 @@ def get_mcp_server(): def get_mcp_session_manager(): - if mcp_server is None: + if create_mcp_http_app() is None: return None return mcp_server.session_manager def create_mcp_http_app(): + global _mcp_http_app + if mcp_server is None: return None - return MCPHeaderAuthApp(mcp_server.streamable_http_app()) + if _mcp_http_app is None: + # FastMCP initializes its session manager when the Streamable HTTP app + # is created, so cache the app and reuse it across startup/mounting. + _mcp_http_app = MCPHeaderAuthApp(mcp_server.streamable_http_app()) + return _mcp_http_app def get_mcp_asgi_app(): diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 2a1bd61..1d755d6 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -33,6 +33,28 @@ server { proxy_send_timeout 300s; } + # MCP Streamable HTTP 代理 + # 对外以 /mcp 作为标准入口,避免 MCP Client 处理尾斜杠重定向。 + location = /mcp { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + chunked_transfer_encoding on; + } + + location = /mcp/ { + return 308 /mcp; + } + location = /docs { proxy_pass http://backend:8000/docs; proxy_http_version 1.1; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c271183..103b68e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,22 +1,9 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { Suspense, lazy, useCallback, useEffect, useState } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { ConfigProvider, theme, App as AntdApp } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import apiClient from './utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from './config/api'; -import HomePage from './pages/HomePage'; -import Dashboard from './pages/Dashboard'; -import AdminDashboard from './pages/AdminDashboard'; -import MeetingDetails from './pages/MeetingDetails'; -import MeetingPreview from './pages/MeetingPreview'; -import AdminManagement from './pages/AdminManagement'; -import PromptManagementPage from './pages/PromptManagementPage'; -import PromptConfigPage from './pages/PromptConfigPage'; -import KnowledgeBasePage from './pages/KnowledgeBasePage'; -import EditKnowledgeBase from './pages/EditKnowledgeBase'; -import ClientDownloadPage from './pages/ClientDownloadPage'; -import AccountSettings from './pages/AccountSettings'; -import MeetingCenterPage from './pages/MeetingCenterPage'; import MainLayout from './components/MainLayout'; import menuService from './services/menuService'; import { @@ -28,6 +15,33 @@ import configService from './utils/configService'; import './App.css'; import './styles/console-theme.css'; +const HomePage = lazy(() => import('./pages/HomePage')); +const Dashboard = lazy(() => import('./pages/Dashboard')); +const AdminDashboard = lazy(() => import('./pages/AdminDashboard')); +const MeetingDetails = lazy(() => import('./pages/MeetingDetails')); +const MeetingPreview = lazy(() => import('./pages/MeetingPreview')); +const AdminManagement = lazy(() => import('./pages/AdminManagement')); +const PromptManagementPage = lazy(() => import('./pages/PromptManagementPage')); +const PromptConfigPage = lazy(() => import('./pages/PromptConfigPage')); +const KnowledgeBasePage = lazy(() => import('./pages/KnowledgeBasePage')); +const EditKnowledgeBase = lazy(() => import('./pages/EditKnowledgeBase')); +const ClientDownloadPage = lazy(() => import('./pages/ClientDownloadPage')); +const AccountSettings = lazy(() => import('./pages/AccountSettings')); +const MeetingCenterPage = lazy(() => import('./pages/MeetingCenterPage')); + +const RouteLoading = ({ text = '页面加载中...' }) => ( +
+
+

{text}

+
+); + +const withRouteSuspense = (node, text) => ( + }> + {node} + +); + // Layout Wrapper to inject user and handleLogout const AuthenticatedLayout = ({ user, handleLogout }) => { // 如果还在加载中或用户不存在,不渲染,避免闪烁 @@ -228,26 +242,28 @@ function App() { {/* Public Routes */} : + user + ? + : withRouteSuspense(, '加载登录页中...') } /> - } /> - } /> + , '加载会议预览中...')} /> + , '加载客户端下载页中...')} /> {/* Authenticated Routes */} : }> - : + ? withRouteSuspense(, '加载工作台中...') + : withRouteSuspense(, '加载工作台中...') } /> - : + : withRouteSuspense(, '加载会议中心中...') } /> } /> - } /> + , '加载会议详情中...')} /> } /> } /> } /> : + user?.role_id === 1 + ? withRouteSuspense(, '加载管理模块中...') + : } /> : + user?.role_id === 1 + ? withRouteSuspense(, '加载提示词页面中...') + : } /> - } /> + , '加载提示词配置中...')} /> : } /> - } /> - } /> - } /> + , '加载知识库中...')} /> + , '加载知识库编辑页中...')} /> + , '加载账号设置中...')} /> {/* Catch all */} diff --git a/frontend/src/components/MarkdownEditor.jsx b/frontend/src/components/MarkdownEditor.jsx index ca2cbfb..b90e512 100644 --- a/frontend/src/components/MarkdownEditor.jsx +++ b/frontend/src/components/MarkdownEditor.jsx @@ -1,9 +1,6 @@ -import React, { useState, useRef, useMemo } from 'react'; -import CodeMirror from '@uiw/react-codemirror'; -import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; -import { EditorView } from '@codemirror/view'; +import React, { Suspense, lazy, useEffect, useMemo, useRef, useState } from 'react'; import { - Space, Button, Tooltip, Card, + Space, Button, Tooltip, Card, Spin, Divider, Dropdown, Typography } from 'antd'; import { @@ -13,7 +10,8 @@ import { UnorderedListOutlined, LineOutlined, EyeOutlined, EditOutlined } from '@ant-design/icons'; -import MarkdownRenderer from './MarkdownRenderer'; + +const MarkdownRenderer = lazy(() => import('./MarkdownRenderer')); const { Text } = Typography; @@ -28,11 +26,41 @@ const MarkdownEditor = ({ const editorRef = useRef(null); const imageInputRef = useRef(null); const [showPreview, setShowPreview] = useState(false); + const [editorRuntime, setEditorRuntime] = useState(null); + + useEffect(() => { + let active = true; + + Promise.all([ + import('@uiw/react-codemirror'), + import('@codemirror/lang-markdown'), + import('@codemirror/view'), + ]) + .then(([codeMirrorModule, markdownModule, viewModule]) => { + if (!active) { + return; + } + + setEditorRuntime({ + CodeMirror: codeMirrorModule.default, + markdown: markdownModule.markdown, + markdownLanguage: markdownModule.markdownLanguage, + EditorView: viewModule.EditorView, + }); + }) + .catch((error) => { + console.error('Load markdown editor runtime failed:', error); + }); + + return () => { + active = false; + }; + }, []); const editorExtensions = useMemo(() => [ - markdown({ base: markdownLanguage }), - EditorView.lineWrapping, - EditorView.theme({ + ...(editorRuntime ? [editorRuntime.markdown({ base: editorRuntime.markdownLanguage })] : []), + editorRuntime?.EditorView?.lineWrapping, + editorRuntime?.EditorView?.theme({ "&": { fontSize: "14px", border: "1px solid #d9d9d9", @@ -50,7 +78,9 @@ const MarkdownEditor = ({ boxShadow: "0 0 0 2px rgba(22, 119, 255, 0.1)", } }) - ], [height]); + ].filter(Boolean), [editorRuntime, height]); + + const CodeMirror = editorRuntime?.CodeMirror; const insertMarkdown = (before, after = '', placeholder = '') => { if (!editorRef.current?.view) return; @@ -133,17 +163,29 @@ const MarkdownEditor = ({ {showPreview ? ( - + }> + + ) : ( - + CodeMirror ? ( + + ) : ( + + + + ) )} { diff --git a/frontend/src/components/MarkdownRenderer.jsx b/frontend/src/components/MarkdownRenderer.jsx index 4c710a1..d8fb600 100644 --- a/frontend/src/components/MarkdownRenderer.jsx +++ b/frontend/src/components/MarkdownRenderer.jsx @@ -1,16 +1,68 @@ -import React from 'react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import rehypeRaw from 'rehype-raw'; -import { Typography, Empty } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { Typography, Empty, Spin } from 'antd'; const { Paragraph } = Typography; const MarkdownRenderer = ({ content, className = "", emptyMessage = "暂无内容" }) => { + const [runtime, setRuntime] = useState(null); + + useEffect(() => { + if (!content || runtime) { + return undefined; + } + + let active = true; + + Promise.all([ + import('react-markdown'), + import('remark-gfm'), + import('rehype-raw'), + ]) + .then(([reactMarkdownModule, remarkGfmModule, rehypeRawModule]) => { + if (!active) { + return; + } + + setRuntime({ + ReactMarkdown: reactMarkdownModule.default, + remarkGfm: remarkGfmModule.default, + rehypeRaw: rehypeRawModule.default, + }); + }) + .catch((error) => { + console.error('Load markdown renderer runtime failed:', error); + }); + + return () => { + active = false; + }; + }, [content, runtime]); + if (!content) { return ; } + if (!runtime) { + return ( +
+ +
+ ); + } + + const { ReactMarkdown, remarkGfm, rehypeRaw } = runtime; + return (
{ if (!element) return false; const rect = element.getBoundingClientRect(); @@ -16,35 +12,80 @@ const MindMap = ({ content }) => { const svgRef = useRef(null); const markmapRef = useRef(null); const latestRootRef = useRef(null); + const runtimeRef = useRef(null); + const runtimePromiseRef = useRef(null); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + + const ensureRuntime = async () => { + if (runtimeRef.current) { + return runtimeRef.current; + } + + if (!runtimePromiseRef.current) { + runtimePromiseRef.current = Promise.all([ + import('markmap-lib'), + import('markmap-view'), + ]).then(([markmapLibModule, markmapViewModule]) => { + runtimeRef.current = { + transformer: new markmapLibModule.Transformer(), + Markmap: markmapViewModule.Markmap, + }; + + return runtimeRef.current; + }); + } + + return runtimePromiseRef.current; + }; useEffect(() => { - if (!content || !svgRef.current) return; + if (!content || !svgRef.current) return undefined; + + let active = true; setLoading(true); - try { - const { root } = transformer.transform(content); - latestRootRef.current = root; + setLoadError(null); - if (markmapRef.current) { - markmapRef.current.setData(root); - } else { - markmapRef.current = Markmap.create(svgRef.current, { - autoFit: false, - duration: 500, - }, root); - } - - requestAnimationFrame(() => { - if (svgRef.current && hasRenderableSize(svgRef.current)) { - markmapRef.current?.fit(); + (async () => { + try { + const { transformer, Markmap } = await ensureRuntime(); + if (!active || !svgRef.current) { + return; } - }); - } catch (error) { - console.error('Markmap error:', error); - } finally { - setLoading(false); - } + + const { root } = transformer.transform(content); + latestRootRef.current = root; + + if (markmapRef.current) { + markmapRef.current.setData(root); + } else { + markmapRef.current = Markmap.create(svgRef.current, { + autoFit: false, + duration: 500, + }, root); + } + + requestAnimationFrame(() => { + if (svgRef.current && hasRenderableSize(svgRef.current)) { + markmapRef.current?.fit(); + } + }); + } catch (error) { + console.error('Markmap error:', error); + if (active) { + setLoadError(error); + } + } finally { + if (active) { + setLoading(false); + } + } + })(); + + return () => { + active = false; + }; }, [content]); useEffect(() => { @@ -59,10 +100,20 @@ const MindMap = ({ content }) => { } if (!markmapRef.current && latestRootRef.current) { - markmapRef.current = Markmap.create(svgElement, { - autoFit: false, - duration: 500, - }, latestRootRef.current); + ensureRuntime() + .then(({ Markmap }) => { + if (markmapRef.current || !latestRootRef.current) { + return; + } + + markmapRef.current = Markmap.create(svgElement, { + autoFit: false, + duration: 500, + }, latestRootRef.current); + }) + .catch((error) => { + console.error('Resize observer markmap init failed:', error); + }); } requestAnimationFrame(() => { @@ -80,6 +131,10 @@ const MindMap = ({ content }) => { if (!content) return ; + if (loadError) { + return ; + } + return (
{loading && ( diff --git a/frontend/src/config/systemManagementModules.jsx b/frontend/src/config/systemManagementModules.jsx index 3d075f5..e56903e 100644 --- a/frontend/src/config/systemManagementModules.jsx +++ b/frontend/src/config/systemManagementModules.jsx @@ -9,15 +9,8 @@ import { DesktopOutlined, DeploymentUnitOutlined, } from '@ant-design/icons'; -import UserManagement from '../pages/admin/UserManagement'; -import PermissionManagement from '../pages/admin/PermissionManagement'; -import DictManagement from '../pages/admin/DictManagement'; -import HotWordManagement from '../pages/admin/HotWordManagement'; -import ParameterManagement from '../pages/admin/ParameterManagement'; -import ModelManagement from '../pages/admin/ModelManagement'; -import ClientManagement from '../pages/ClientManagement'; -import ExternalAppManagement from '../pages/admin/ExternalAppManagement'; -import TerminalManagement from '../pages/admin/TerminalManagement'; + +export const SYSTEM_MANAGEMENT_OVERVIEW_LOADER = () => import('../pages/admin/SystemManagementOverview'); export const SYSTEM_MANAGEMENT_MODULES = [ { @@ -26,7 +19,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '账号、角色、密码重置', path: '/admin/management/user-management', icon: , - component: UserManagement, + loader: () => import('../pages/admin/UserManagement'), group: '基础治理', }, { @@ -35,7 +28,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '菜单与角色授权矩阵', path: '/admin/management/permission-management', icon: , - component: PermissionManagement, + loader: () => import('../pages/admin/PermissionManagement'), group: '基础治理', }, { @@ -44,7 +37,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '码表、平台类型、扩展属性', path: '/admin/management/dict-management', icon: , - component: DictManagement, + loader: () => import('../pages/admin/DictManagement'), group: '基础治理', }, { @@ -53,7 +46,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: 'ASR 热词与同步', path: '/admin/management/hot-word-management', icon: , - component: HotWordManagement, + loader: () => import('../pages/admin/HotWordManagement'), group: '基础治理', }, { @@ -62,7 +55,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '系统参数统一配置', path: '/admin/management/parameter-management', icon: , - component: ParameterManagement, + loader: () => import('../pages/admin/ParameterManagement'), group: '平台配置', }, { @@ -71,7 +64,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '音频模型与 LLM 模型配置', path: '/admin/management/model-management', icon: , - component: ModelManagement, + loader: () => import('../pages/admin/ModelManagement'), group: '平台配置', }, { @@ -80,7 +73,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '版本、下载地址、发布状态', path: '/admin/management/client-management', icon: , - component: ClientManagement, + loader: () => import('../pages/ClientManagement'), group: '设备与应用', }, { @@ -89,7 +82,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '外部系统入口与图标配置', path: '/admin/management/external-app-management', icon: , - component: ExternalAppManagement, + loader: () => import('../pages/admin/ExternalAppManagement'), group: '设备与应用', }, { @@ -98,7 +91,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [ desc: '专用设备、激活和绑定状态', path: '/admin/management/terminal-management', icon: , - component: TerminalManagement, + loader: () => import('../pages/admin/TerminalManagement'), group: '设备与应用', }, ]; diff --git a/frontend/src/pages/AdminManagement.jsx b/frontend/src/pages/AdminManagement.jsx index 2dd1e25..2cdfe6f 100644 --- a/frontend/src/pages/AdminManagement.jsx +++ b/frontend/src/pages/AdminManagement.jsx @@ -1,9 +1,9 @@ -import React, { useMemo } from 'react'; +import React, { Suspense, lazy, useMemo } from 'react'; import { Alert } from 'antd'; import { Navigate, useParams } from 'react-router-dom'; -import SystemManagementOverview from './admin/SystemManagementOverview'; import { DEFAULT_SYSTEM_MANAGEMENT_MODULE, + SYSTEM_MANAGEMENT_OVERVIEW_LOADER, SYSTEM_MANAGEMENT_MODULE_MAP, } from '../config/systemManagementModules'; @@ -13,15 +13,23 @@ const AdminManagement = () => { const currentModule = useMemo(() => ( currentModuleKey === DEFAULT_SYSTEM_MANAGEMENT_MODULE - ? { component: SystemManagementOverview } + ? { loader: SYSTEM_MANAGEMENT_OVERVIEW_LOADER } : SYSTEM_MANAGEMENT_MODULE_MAP[currentModuleKey] ), [currentModuleKey]); + const CurrentComponent = useMemo(() => { + if (!currentModule?.loader) { + return null; + } + + return lazy(currentModule.loader); + }, [currentModule]); + if (!moduleKey) { return ; } - if (!currentModule) { + if (!currentModule || !CurrentComponent) { return ( { ); } - const CurrentComponent = currentModule.component; - return ; + return ( + }> + + + ); }; export default AdminManagement; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 91902a6..8f65be3 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,9 +1,78 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +const manualChunks = (id) => { + if (!id.includes('node_modules')) { + return undefined; + } + + const normalizedId = id.split('node_modules/')[1]; + + if ( + normalizedId.startsWith('react/') || + normalizedId.startsWith('react-dom/') || + normalizedId.startsWith('scheduler/') + ) { + return 'react-vendor'; + } + + if ( + normalizedId.startsWith('@ant-design/icons/') || + normalizedId.startsWith('@ant-design/icons-svg/') + ) { + return 'ant-icons-vendor'; + } + + if (normalizedId.startsWith('axios/')) { + return 'http-vendor'; + } + + if (normalizedId.startsWith('yaml/')) { + return 'yaml-vendor'; + } + + if ( + normalizedId.startsWith('markdown-it/') || + normalizedId.startsWith('markdown-it-') || + normalizedId.startsWith('linkify-it/') || + normalizedId.startsWith('mdurl/') || + normalizedId.startsWith('uc.micro/') || + normalizedId.startsWith('entities/') || + normalizedId.startsWith('punycode.js/') || + normalizedId.startsWith('katex/') || + normalizedId.startsWith('@vscode/markdown-it-katex/') + ) { + return 'markdown-parser-vendor'; + } + + if ( + normalizedId.startsWith('html2canvas/') || + normalizedId.startsWith('jspdf/') || + normalizedId.startsWith('canvg/') + ) { + return 'capture-vendor'; + } + + if ( + normalizedId.startsWith('qrcode.react/') || + normalizedId.startsWith('qrcode/') + ) { + return 'qrcode-vendor'; + } + + return undefined; +}; + // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + build: { + rollupOptions: { + output: { + manualChunks, + }, + }, + }, server: { host: true, // Optional: Allows the server to be accessible externally allowedHosts: ['dev.imeeting.unisspace.com','nonperforated-ivonne-fezzy.ngrok-free.dev'], // Add the problematic hostname here