增加了mcp server
parent
d69b346f4c
commit
f44453d93e
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 头,没有则认为是普通请求,直接放行
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = '页面加载中...' }) => (
|
||||
<div className="app-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const withRouteSuspense = (node, text) => (
|
||||
<Suspense fallback={<RouteLoading text={text} />}>
|
||||
{node}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
// Layout Wrapper to inject user and handleLogout
|
||||
const AuthenticatedLayout = ({ user, handleLogout }) => {
|
||||
// 如果还在加载中或用户不存在,不渲染,避免闪烁
|
||||
|
|
@ -228,26 +242,28 @@ function App() {
|
|||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route path="/" element={
|
||||
user ? <DefaultMenuRedirect user={user} /> : <HomePage onLogin={handleLogin} />
|
||||
user
|
||||
? <DefaultMenuRedirect user={user} />
|
||||
: withRouteSuspense(<HomePage onLogin={handleLogin} />, '加载登录页中...')
|
||||
} />
|
||||
|
||||
<Route path="/meetings/preview/:meeting_id" element={<MeetingPreview />} />
|
||||
<Route path="/downloads" element={<ClientDownloadPage />} />
|
||||
<Route path="/meetings/preview/:meeting_id" element={withRouteSuspense(<MeetingPreview />, '加载会议预览中...')} />
|
||||
<Route path="/downloads" element={withRouteSuspense(<ClientDownloadPage />, '加载客户端下载页中...')} />
|
||||
|
||||
{/* Authenticated Routes */}
|
||||
<Route element={user ? <AuthenticatedLayout user={user} handleLogout={handleLogout} /> : <Navigate to="/" replace />}>
|
||||
<Route path="/dashboard" element={
|
||||
user?.role_id === 1
|
||||
? <AdminDashboard user={user} onLogout={handleLogout} />
|
||||
: <Dashboard user={user} onLogout={handleLogout} />
|
||||
? withRouteSuspense(<AdminDashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
|
||||
: withRouteSuspense(<Dashboard user={user} onLogout={handleLogout} />, '加载工作台中...')
|
||||
} />
|
||||
<Route path="/meetings/center" element={
|
||||
user?.role_id === 1
|
||||
? <Navigate to="/dashboard" replace />
|
||||
: <MeetingCenterPage user={user} />
|
||||
: withRouteSuspense(<MeetingCenterPage user={user} />, '加载会议中心中...')
|
||||
} />
|
||||
<Route path="/meetings/history" element={<Navigate to="/meetings/center" replace />} />
|
||||
<Route path="/meetings/:meeting_id" element={<MeetingDetails user={user} />} />
|
||||
<Route path="/meetings/:meeting_id" element={withRouteSuspense(<MeetingDetails user={user} />, '加载会议详情中...')} />
|
||||
<Route path="/meetings/create" element={<Navigate to="/meetings/center" replace />} />
|
||||
<Route path="/meetings/edit/:meeting_id" element={<Navigate to="/meetings/center" replace />} />
|
||||
<Route path="/admin/management" element={
|
||||
|
|
@ -256,18 +272,22 @@ function App() {
|
|||
: <Navigate to="/dashboard" replace />
|
||||
} />
|
||||
<Route path="/admin/management/:moduleKey" element={
|
||||
user?.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" replace />
|
||||
user?.role_id === 1
|
||||
? withRouteSuspense(<AdminManagement user={user} />, '加载管理模块中...')
|
||||
: <Navigate to="/dashboard" replace />
|
||||
} />
|
||||
<Route path="/prompt-management" element={
|
||||
user?.role_id === 1 ? <PromptManagementPage user={user} /> : <Navigate to="/dashboard" replace />
|
||||
user?.role_id === 1
|
||||
? withRouteSuspense(<PromptManagementPage user={user} />, '加载提示词页面中...')
|
||||
: <Navigate to="/dashboard" replace />
|
||||
} />
|
||||
<Route path="/prompt-config" element={<PromptConfigPage user={user} />} />
|
||||
<Route path="/prompt-config" element={withRouteSuspense(<PromptConfigPage user={user} />, '加载提示词配置中...')} />
|
||||
<Route path="/personal-prompts" element={
|
||||
user?.role_id === 1 ? <Navigate to="/dashboard" replace /> : <Navigate to="/prompt-config" replace />
|
||||
} />
|
||||
<Route path="/knowledge-base" element={<KnowledgeBasePage user={user} />} />
|
||||
<Route path="/knowledge-base/edit/:kb_id" element={<EditKnowledgeBase user={user} />} />
|
||||
<Route path="/account-settings" element={<AccountSettings user={user} onUpdateUser={handleLogin} />} />
|
||||
<Route path="/knowledge-base" element={withRouteSuspense(<KnowledgeBasePage user={user} />, '加载知识库中...')} />
|
||||
<Route path="/knowledge-base/edit/:kb_id" element={withRouteSuspense(<EditKnowledgeBase user={user} />, '加载知识库编辑页中...')} />
|
||||
<Route path="/account-settings" element={withRouteSuspense(<AccountSettings user={user} onUpdateUser={handleLogin} />, '加载账号设置中...')} />
|
||||
</Route>
|
||||
|
||||
{/* Catch all */}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<Card bordered styles={{ body: { padding: 16, minHeight: height, overflowY: 'auto' } }} style={{ borderRadius: '0 0 8px 8px' }}>
|
||||
<MarkdownRenderer content={value} />
|
||||
<Suspense fallback={<Spin tip="预览加载中..." />}>
|
||||
<MarkdownRenderer content={value} />
|
||||
</Suspense>
|
||||
</Card>
|
||||
) : (
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={editorExtensions}
|
||||
placeholder={placeholder}
|
||||
basicSetup={{ lineNumbers: false, foldGutter: false }}
|
||||
/>
|
||||
CodeMirror ? (
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={editorExtensions}
|
||||
placeholder={placeholder}
|
||||
basicSetup={{ lineNumbers: false, foldGutter: false }}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
bordered
|
||||
styles={{ body: { minHeight: height, display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
|
||||
style={{ borderRadius: '0 0 8px 8px' }}
|
||||
>
|
||||
<Spin tip="编辑器加载中..." />
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
|
||||
<input ref={imageInputRef} type="file" accept="image/*" onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -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 <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyMessage} />;
|
||||
}
|
||||
|
||||
if (!runtime) {
|
||||
return (
|
||||
<div
|
||||
className={`markdown-renderer-modern ${className}`}
|
||||
style={{
|
||||
minHeight: 120,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '15px',
|
||||
lineHeight: 1.8,
|
||||
color: '#262626',
|
||||
}}
|
||||
>
|
||||
<Spin tip="内容渲染中..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { ReactMarkdown, remarkGfm, rehypeRaw } = runtime;
|
||||
|
||||
return (
|
||||
<div className={`markdown-renderer-modern ${className}`} style={{ fontSize: '15px', lineHeight: 1.8, color: '#262626' }}>
|
||||
<ReactMarkdown
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Transformer } from 'markmap-lib';
|
||||
import { Markmap } from 'markmap-view';
|
||||
import { Spin, Empty, Button, Space } from 'antd';
|
||||
import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
|
||||
const transformer = new Transformer();
|
||||
|
||||
const hasRenderableSize = (element) => {
|
||||
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 <Empty description="暂无内容,无法生成思维导图" />;
|
||||
|
||||
if (loadError) {
|
||||
return <Empty description="思维导图加载失败,请稍后重试" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mindmap-container" style={{ position: 'relative', width: '100%', height: '100%', minHeight: 500 }}>
|
||||
{loading && (
|
||||
|
|
|
|||
|
|
@ -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: <UserOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: UserManagement,
|
||||
loader: () => import('../pages/admin/UserManagement'),
|
||||
group: '基础治理',
|
||||
},
|
||||
{
|
||||
|
|
@ -35,7 +28,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '菜单与角色授权矩阵',
|
||||
path: '/admin/management/permission-management',
|
||||
icon: <KeyOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: PermissionManagement,
|
||||
loader: () => import('../pages/admin/PermissionManagement'),
|
||||
group: '基础治理',
|
||||
},
|
||||
{
|
||||
|
|
@ -44,7 +37,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '码表、平台类型、扩展属性',
|
||||
path: '/admin/management/dict-management',
|
||||
icon: <DatabaseOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
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: <FontSizeOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: HotWordManagement,
|
||||
loader: () => import('../pages/admin/HotWordManagement'),
|
||||
group: '基础治理',
|
||||
},
|
||||
{
|
||||
|
|
@ -62,7 +55,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '系统参数统一配置',
|
||||
path: '/admin/management/parameter-management',
|
||||
icon: <SettingOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
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: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: ModelManagement,
|
||||
loader: () => import('../pages/admin/ModelManagement'),
|
||||
group: '平台配置',
|
||||
},
|
||||
{
|
||||
|
|
@ -80,7 +73,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '版本、下载地址、发布状态',
|
||||
path: '/admin/management/client-management',
|
||||
icon: <DesktopOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: ClientManagement,
|
||||
loader: () => import('../pages/ClientManagement'),
|
||||
group: '设备与应用',
|
||||
},
|
||||
{
|
||||
|
|
@ -89,7 +82,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '外部系统入口与图标配置',
|
||||
path: '/admin/management/external-app-management',
|
||||
icon: <AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: ExternalAppManagement,
|
||||
loader: () => import('../pages/admin/ExternalAppManagement'),
|
||||
group: '设备与应用',
|
||||
},
|
||||
{
|
||||
|
|
@ -98,7 +91,7 @@ export const SYSTEM_MANAGEMENT_MODULES = [
|
|||
desc: '专用设备、激活和绑定状态',
|
||||
path: '/admin/management/terminal-management',
|
||||
icon: <DeploymentUnitOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />,
|
||||
component: TerminalManagement,
|
||||
loader: () => import('../pages/admin/TerminalManagement'),
|
||||
group: '设备与应用',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 <Navigate to={`/admin/management/${DEFAULT_SYSTEM_MANAGEMENT_MODULE}`} replace />;
|
||||
}
|
||||
|
||||
if (!currentModule) {
|
||||
if (!currentModule || !CurrentComponent) {
|
||||
return (
|
||||
<Alert
|
||||
type="warning"
|
||||
|
|
@ -32,8 +40,11 @@ const AdminManagement = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const CurrentComponent = currentModule.component;
|
||||
return <CurrentComponent />;
|
||||
return (
|
||||
<Suspense fallback={<Alert type="info" showIcon message="管理模块加载中..." />}>
|
||||
<CurrentComponent />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminManagement;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue