增加了mcp server
parent
dbf6f72dac
commit
d69b346f4c
|
|
@ -157,8 +157,3 @@ async def test_audio_model_config(request: AudioModelTestRequest, current_user=D
|
|||
@router.get("/system-config/public")
|
||||
async def get_public_system_config():
|
||||
return admin_settings_service.get_public_system_config()
|
||||
|
||||
|
||||
@router.get("/admin/system-config")
|
||||
async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
|
||||
return admin_settings_service.get_system_config_compat()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
|||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import contextlib
|
||||
|
||||
from app.api.endpoints import (
|
||||
admin,
|
||||
|
|
@ -24,6 +25,7 @@ from app.api.endpoints import (
|
|||
)
|
||||
from app.core.config import UPLOAD_DIR
|
||||
from app.core.middleware import TerminalCheckMiddleware
|
||||
from app.mcp import create_mcp_http_app, get_mcp_session_manager
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
|
||||
|
||||
|
|
@ -48,6 +50,11 @@ 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()
|
||||
if mcp_asgi_app is not None:
|
||||
app.mount("/mcp", mcp_asgi_app, name="mcp")
|
||||
|
||||
app.include_router(auth.router, prefix="/api", tags=["Authentication"])
|
||||
app.include_router(users.router, prefix="/api", tags=["Users"])
|
||||
app.include_router(meetings.router, prefix="/api", tags=["Meetings"])
|
||||
|
|
@ -88,6 +95,19 @@ def create_app() -> FastAPI:
|
|||
"version": "1.1.0",
|
||||
}
|
||||
|
||||
if mcp_session_manager is not None:
|
||||
@app.on_event("startup")
|
||||
async def startup_mcp_session_manager():
|
||||
exit_stack = contextlib.AsyncExitStack()
|
||||
await exit_stack.enter_async_context(mcp_session_manager.run())
|
||||
app.state.mcp_exit_stack = exit_stack
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_mcp_session_manager():
|
||||
exit_stack = getattr(app.state, "mcp_exit_stack", None)
|
||||
if exit_stack is not None:
|
||||
await exit_stack.aclose()
|
||||
|
||||
SystemConfigService.ensure_builtin_parameters()
|
||||
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from app.mcp.server import create_mcp_http_app, get_mcp_session_manager, MCPHeaderAuthApp
|
||||
|
||||
__all__ = ["create_mcp_http_app", "get_mcp_session_manager", "MCPHeaderAuthApp"]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextvars import ContextVar, Token
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MCPRequestContext:
|
||||
user: Dict[str, Any]
|
||||
bot_id: str
|
||||
credential_id: int
|
||||
authenticated_at: datetime
|
||||
|
||||
|
||||
current_mcp_request: ContextVar[Optional[MCPRequestContext]] = ContextVar(
|
||||
"current_mcp_request",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def set_current_mcp_request(request: MCPRequestContext) -> Token:
|
||||
return current_mcp_request.set(request)
|
||||
|
||||
|
||||
def reset_current_mcp_request(token: Token) -> None:
|
||||
current_mcp_request.reset(token)
|
||||
|
||||
|
||||
def require_mcp_request() -> MCPRequestContext:
|
||||
request = current_mcp_request.get()
|
||||
if request is None:
|
||||
raise RuntimeError("MCP request context is unavailable")
|
||||
return request
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
"""
|
||||
Backend-integrated MCP Streamable HTTP server.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import hmac
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.datastructures import Headers
|
||||
|
||||
try:
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
except ImportError: # pragma: no cover - runtime dependency
|
||||
FastMCP = None
|
||||
|
||||
from app.core.config import APP_CONFIG
|
||||
from app.core.database import get_db_connection
|
||||
from app.mcp.context import (
|
||||
MCPRequestContext,
|
||||
require_mcp_request,
|
||||
reset_current_mcp_request,
|
||||
set_current_mcp_request,
|
||||
)
|
||||
from app.services import meeting_service
|
||||
|
||||
|
||||
def _build_absolute_url(path: str) -> str:
|
||||
normalized_base = APP_CONFIG["base_url"].rstrip("/")
|
||||
normalized_path = path if path.startswith("/") else f"/{path}"
|
||||
return f"{normalized_base}{normalized_path}"
|
||||
|
||||
|
||||
def _parse_api_response(response: JSONResponse) -> Dict[str, Any]:
|
||||
body = response.body.decode("utf-8") if isinstance(response.body, (bytes, bytearray)) else response.body
|
||||
return json.loads(body)
|
||||
|
||||
|
||||
def _load_user_meetings(current_user: Dict[str, Any], filter_type: str) -> list[Dict[str, Any]]:
|
||||
meetings: list[Dict[str, Any]] = []
|
||||
page = 1
|
||||
page_size = 100
|
||||
|
||||
while True:
|
||||
payload = _parse_api_response(
|
||||
meeting_service.get_meetings(
|
||||
current_user=current_user,
|
||||
user_id=current_user["user_id"],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
filter_type=filter_type,
|
||||
)
|
||||
)
|
||||
if payload.get("code") != "200":
|
||||
raise ValueError(payload.get("message") or "获取会议列表失败")
|
||||
|
||||
data = payload.get("data") or {}
|
||||
meetings.extend(data.get("meetings") or [])
|
||||
if not data.get("has_more"):
|
||||
break
|
||||
page += 1
|
||||
|
||||
return meetings
|
||||
|
||||
|
||||
def _find_user_meeting(current_user: Dict[str, Any], meeting_id: int) -> Dict[str, Any]:
|
||||
for filter_type in ("created", "attended"):
|
||||
for meeting in _load_user_meetings(current_user, filter_type):
|
||||
if int(meeting.get("meeting_id")) == int(meeting_id):
|
||||
return meeting
|
||||
|
||||
raise ValueError("会议不存在,或当前账号无权访问该会议")
|
||||
|
||||
|
||||
def _get_meeting_preview_payload(meeting_id: int, access_password: Optional[str]) -> Dict[str, Any]:
|
||||
return _parse_api_response(
|
||||
meeting_service.get_meeting_preview_data(
|
||||
meeting_id=meeting_id,
|
||||
password=access_password,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _get_mcp_user(bot_id: str, bot_secret: str) -> Optional[Dict[str, Any]]:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
m.id,
|
||||
m.user_id,
|
||||
m.bot_id,
|
||||
m.bot_secret,
|
||||
m.status,
|
||||
u.username,
|
||||
u.caption,
|
||||
u.email,
|
||||
u.role_id
|
||||
FROM sys_user_mcp m
|
||||
JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE m.bot_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(bot_id,),
|
||||
)
|
||||
record = cursor.fetchone()
|
||||
if not record:
|
||||
return None
|
||||
|
||||
if int(record.get("status") or 0) != 1:
|
||||
return None
|
||||
|
||||
if not hmac.compare_digest(str(record.get("bot_secret") or ""), bot_secret):
|
||||
return None
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE sys_user_mcp SET last_used_at = NOW() WHERE id = %s",
|
||||
(record["id"],),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
return {
|
||||
"credential_id": record["id"],
|
||||
"bot_id": record["bot_id"],
|
||||
"user": {
|
||||
"user_id": record["user_id"],
|
||||
"username": record["username"],
|
||||
"caption": record["caption"],
|
||||
"email": record["email"],
|
||||
"role_id": record["role_id"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class MCPHeaderAuthApp:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
headers = Headers(scope=scope)
|
||||
bot_id = (headers.get("x-bot-id") or "").strip()
|
||||
bot_secret = (headers.get("x-bot-secret") or "").strip()
|
||||
|
||||
if not bot_id or not bot_secret:
|
||||
response = JSONResponse(
|
||||
status_code=401,
|
||||
content={"error": "Missing X-Bot-Id or X-Bot-Secret"},
|
||||
)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
|
||||
auth_record = _get_mcp_user(bot_id, bot_secret)
|
||||
if not auth_record:
|
||||
response = JSONResponse(
|
||||
status_code=401,
|
||||
content={"error": "Invalid MCP credentials"},
|
||||
)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
|
||||
token = set_current_mcp_request(
|
||||
MCPRequestContext(
|
||||
user=auth_record["user"],
|
||||
bot_id=auth_record["bot_id"],
|
||||
credential_id=auth_record["credential_id"],
|
||||
authenticated_at=datetime.utcnow(),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await self.app(scope, receive, send)
|
||||
finally:
|
||||
reset_current_mcp_request(token)
|
||||
|
||||
|
||||
if FastMCP is not None:
|
||||
mcp_server = FastMCP(
|
||||
"iMeeting MCP",
|
||||
json_response=True,
|
||||
stateless_http=True,
|
||||
streamable_http_path="/",
|
||||
)
|
||||
|
||||
@mcp_server.tool()
|
||||
def get_my_meetings() -> Dict[str, Any]:
|
||||
"""获取当前登录用户的会议列表,并按我创建/我参加分开返回。"""
|
||||
current_user = require_mcp_request().user
|
||||
created_meetings = _load_user_meetings(current_user, "created")
|
||||
attended_meetings = _load_user_meetings(current_user, "attended")
|
||||
|
||||
return {
|
||||
"user": {
|
||||
"user_id": current_user["user_id"],
|
||||
"username": current_user.get("username"),
|
||||
"caption": current_user.get("caption"),
|
||||
},
|
||||
"created_meetings": created_meetings,
|
||||
"attended_meetings": attended_meetings,
|
||||
"counts": {
|
||||
"created": len(created_meetings),
|
||||
"attended": len(attended_meetings),
|
||||
"all": len(created_meetings) + len(attended_meetings),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@mcp_server.tool()
|
||||
def get_meeting_preview_url(meeting_id: int) -> Dict[str, Any]:
|
||||
"""获取指定会议的预览地址。"""
|
||||
current_user = require_mcp_request().user
|
||||
meeting = _find_user_meeting(current_user, meeting_id)
|
||||
preview_payload = _get_meeting_preview_payload(meeting_id, meeting.get("access_password"))
|
||||
|
||||
if preview_payload.get("code") != "200":
|
||||
return {
|
||||
"meeting_id": meeting_id,
|
||||
"title": meeting.get("title"),
|
||||
"message": preview_payload.get("message"),
|
||||
"status": (preview_payload.get("data") or {}).get("processing_status"),
|
||||
}
|
||||
|
||||
return {
|
||||
"meeting_id": meeting_id,
|
||||
"title": meeting.get("title"),
|
||||
"preview_url": _build_absolute_url(f"/meetings/preview/{meeting_id}"),
|
||||
"requires_password": bool(meeting.get("access_password")),
|
||||
"access_password": meeting.get("access_password"),
|
||||
"preview_data": preview_payload.get("data"),
|
||||
}
|
||||
else: # pragma: no cover - graceful fallback without runtime dependency
|
||||
mcp_server = None
|
||||
|
||||
|
||||
def get_mcp_server():
|
||||
return mcp_server
|
||||
|
||||
|
||||
def get_mcp_session_manager():
|
||||
if mcp_server is None:
|
||||
return None
|
||||
return mcp_server.session_manager
|
||||
|
||||
|
||||
def create_mcp_http_app():
|
||||
if mcp_server is None:
|
||||
return None
|
||||
return MCPHeaderAuthApp(mcp_server.streamable_http_app())
|
||||
|
||||
|
||||
def get_mcp_asgi_app():
|
||||
return create_mcp_http_app()
|
||||
|
|
@ -628,35 +628,3 @@ def get_public_system_config():
|
|||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
||||
|
||||
|
||||
def get_system_config_compat():
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT param_key, param_value
|
||||
FROM sys_system_parameters
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
data = {row["param_key"]: row["param_value"] for row in rows}
|
||||
|
||||
if "max_audio_size" in data:
|
||||
try:
|
||||
data["MAX_FILE_SIZE"] = int(data["max_audio_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_FILE_SIZE"] = 100 * 1024 * 1024
|
||||
if "max_image_size" in data:
|
||||
try:
|
||||
data["MAX_IMAGE_SIZE"] = int(data["max_image_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_IMAGE_SIZE"] = 10 * 1024 * 1024
|
||||
else:
|
||||
data.setdefault("MAX_IMAGE_SIZE", 10 * 1024 * 1024)
|
||||
|
||||
return create_api_response(code="200", message="获取系统配置成功", data=data)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取系统配置失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class AsyncKnowledgeBaseService:
|
|||
"""
|
||||
cursor.execute(query, (task_id, user_id, kb_id, user_prompt, prompt_id))
|
||||
else:
|
||||
# Fallback to the old method if no cursor is provided
|
||||
# 无外部事务时,使用服务内的持久化入口。
|
||||
self._save_task_to_db(task_id, user_id, kb_id, user_prompt, prompt_id)
|
||||
|
||||
current_time = datetime.now().isoformat()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class JWTService:
|
|||
|
||||
@staticmethod
|
||||
def _get_access_token_expire_minutes() -> int:
|
||||
expire_days = SystemConfigService.get_token_expire_days(default=7)
|
||||
expire_days = SystemConfigService.get_token_expire_days()
|
||||
return max(1, expire_days) * 24 * 60
|
||||
|
||||
def create_access_token(self, data: Dict[str, Any]) -> str:
|
||||
|
|
|
|||
|
|
@ -421,10 +421,6 @@ class LLMService:
|
|||
print(error_msg)
|
||||
yield f"error: {error_msg}"
|
||||
|
||||
def _call_llm_api_stream(self, prompt: str) -> Generator[str, None, None]:
|
||||
"""兼容旧调用入口。"""
|
||||
return self.stream_llm_api(prompt)
|
||||
|
||||
def call_llm_api(
|
||||
self,
|
||||
prompt: Optional[str] = None,
|
||||
|
|
@ -524,10 +520,6 @@ class LLMService:
|
|||
except Exception as e:
|
||||
raise LLMServiceError(str(e)) from e
|
||||
|
||||
def _call_llm_api(self, prompt: str) -> Optional[str]:
|
||||
"""兼容旧调用入口。"""
|
||||
return self.call_llm_api(prompt)
|
||||
|
||||
def test_model(self, config: Dict[str, Any], prompt: Optional[str] = None) -> Dict[str, Any]:
|
||||
params = self.build_call_params_from_config(config)
|
||||
test_prompt = prompt or "请用一句中文回复:LLM测试成功。"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
from fastapi import APIRouter, UploadFile, File, Form, Depends, BackgroundTasks, Request, Header
|
||||
from fastapi import UploadFile, BackgroundTasks
|
||||
from fastapi.responses import StreamingResponse, Response
|
||||
from app.models.models import Meeting, TranscriptSegment, TranscriptionTaskStatus, CreateMeetingRequest, UpdateMeetingRequest, SpeakerTagUpdateRequest, BatchSpeakerTagUpdateRequest, BatchTranscriptUpdateRequest, Tag
|
||||
from app.core.database import get_db_connection
|
||||
from app.core.config import BASE_DIR, AUDIO_DIR, MARKDOWN_DIR, ALLOWED_EXTENSIONS, ALLOWED_IMAGE_EXTENSIONS
|
||||
import app.core.config as config_module
|
||||
from app.services.llm_service import LLMService
|
||||
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||
from app.services.async_meeting_service import async_meeting_service
|
||||
from app.services.audio_upload_task_service import audio_upload_task_service
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
from app.core.auth import get_current_user, get_optional_current_user
|
||||
from app.core.response import create_api_response
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
|
@ -19,7 +17,6 @@ from collections import defaultdict
|
|||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
import mimetypes
|
||||
|
||||
|
||||
llm_service = LLMService()
|
||||
|
|
@ -291,7 +288,7 @@ def _get_meeting_overall_status(meeting_id: int) -> dict:
|
|||
return _build_meeting_overall_status(transcription_status, llm_status)
|
||||
|
||||
def get_meetings(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
current_user: dict,
|
||||
user_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: Optional[int] = None,
|
||||
|
|
@ -301,7 +298,7 @@ def get_meetings(
|
|||
):
|
||||
# 使用配置的默认页面大小
|
||||
if page_size is None:
|
||||
page_size = SystemConfigService.get_page_size(default=10)
|
||||
page_size = SystemConfigService.get_page_size()
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -426,7 +423,7 @@ def get_meetings(
|
|||
})
|
||||
|
||||
def get_meetings_stats(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
current_user: dict,
|
||||
user_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
|
|
@ -473,7 +470,7 @@ def get_meetings_stats(
|
|||
"attended_meetings": attended_count
|
||||
})
|
||||
|
||||
def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_meeting_details(meeting_id: int, current_user: dict):
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = '''
|
||||
|
|
@ -523,7 +520,7 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
|||
|
||||
return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
|
||||
|
||||
def get_meeting_transcript(meeting_id: int, current_user: Optional[dict] = Depends(get_optional_current_user)):
|
||||
def get_meeting_transcript(meeting_id: int, current_user: Optional[dict]):
|
||||
"""获取会议转录内容(支持公开访问用于预览)"""
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -543,7 +540,7 @@ def get_meeting_transcript(meeting_id: int, current_user: Optional[dict] = Depen
|
|||
) for s in segments]
|
||||
return create_api_response(code="200", message="获取转录内容成功", data=transcript_segments)
|
||||
|
||||
def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = Depends(get_current_user)):
|
||||
def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict):
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
# 使用 _process_tags 来处理标签创建
|
||||
|
|
@ -570,7 +567,7 @@ def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = D
|
|||
connection.commit()
|
||||
return create_api_response(code="200", message="Meeting created successfully", data={"meeting_id": meeting_id})
|
||||
|
||||
def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, current_user: dict = Depends(get_current_user)):
|
||||
def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, current_user: dict):
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id, prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
|
|
@ -602,7 +599,7 @@ def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, curre
|
|||
async_meeting_service._export_summary_md(meeting_id, meeting_request.summary)
|
||||
return create_api_response(code="200", message="Meeting updated successfully")
|
||||
|
||||
def delete_meeting(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def delete_meeting(meeting_id: int, current_user: dict):
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
|
|
@ -619,7 +616,7 @@ def delete_meeting(meeting_id: int, current_user: dict = Depends(get_current_use
|
|||
connection.commit()
|
||||
return create_api_response(code="200", message="Meeting deleted successfully")
|
||||
|
||||
def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_meeting_for_edit(meeting_id: int, current_user: dict):
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = '''
|
||||
|
|
@ -658,13 +655,13 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre
|
|||
return create_api_response(code="200", message="获取会议编辑信息成功", data=meeting_data)
|
||||
|
||||
async def upload_audio(
|
||||
audio_file: UploadFile = File(...),
|
||||
meeting_id: int = Form(...),
|
||||
auto_summarize: str = Form("true"),
|
||||
prompt_id: Optional[int] = Form(None), # 可选的提示词模版ID
|
||||
model_code: Optional[str] = Form(None), # 可选的总结模型编码
|
||||
audio_file: UploadFile,
|
||||
meeting_id: int,
|
||||
auto_summarize: str = "true",
|
||||
prompt_id: Optional[int] = None, # 可选的提示词模版ID
|
||||
model_code: Optional[str] = None, # 可选的总结模型编码
|
||||
background_tasks: BackgroundTasks = None,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
current_user: dict | None = None
|
||||
):
|
||||
"""
|
||||
音频文件上传接口
|
||||
|
|
@ -713,7 +710,7 @@ async def upload_audio(
|
|||
)
|
||||
|
||||
# 2. 文件大小验证
|
||||
max_file_size = SystemConfigService.get_max_audio_size(default=100) * 1024 * 1024 # MB转字节
|
||||
max_file_size = SystemConfigService.get_max_audio_size() * 1024 * 1024 # MB转字节
|
||||
if audio_file.size > max_file_size:
|
||||
return create_api_response(
|
||||
code="400",
|
||||
|
|
@ -757,7 +754,7 @@ async def upload_audio(
|
|||
}
|
||||
)
|
||||
|
||||
def get_audio_file(meeting_id: int, current_user: Optional[dict] = Depends(get_optional_current_user)):
|
||||
def get_audio_file(meeting_id: int, current_user: Optional[dict]):
|
||||
"""获取音频文件信息(支持公开访问用于预览)"""
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -769,7 +766,7 @@ def get_audio_file(meeting_id: int, current_user: Optional[dict] = Depends(get_o
|
|||
|
||||
async def stream_audio_file(
|
||||
meeting_id: int,
|
||||
range: Optional[str] = Header(None, alias="Range")
|
||||
range: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
音频文件流式传输端点,支持HTTP Range请求(Safari浏览器必需)
|
||||
|
|
@ -880,7 +877,7 @@ async def stream_audio_file(
|
|||
}
|
||||
)
|
||||
|
||||
def get_meeting_transcription_status(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_meeting_transcription_status(meeting_id: int, current_user: dict):
|
||||
try:
|
||||
status_info = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
if not status_info:
|
||||
|
|
@ -892,7 +889,7 @@ def get_meeting_transcription_status(meeting_id: int, current_user: dict = Depen
|
|||
def start_meeting_transcription(
|
||||
meeting_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
current_user: dict
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
|
|
@ -923,11 +920,11 @@ def start_meeting_transcription(
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to start transcription: {str(e)}")
|
||||
|
||||
async def upload_image(meeting_id: int, image_file: UploadFile = File(...), current_user: dict = Depends(get_current_user)):
|
||||
async def upload_image(meeting_id: int, image_file: UploadFile, current_user: dict):
|
||||
file_extension = os.path.splitext(image_file.filename)[1].lower()
|
||||
if file_extension not in ALLOWED_IMAGE_EXTENSIONS:
|
||||
return create_api_response(code="400", message=f"Unsupported image type. Allowed types: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}")
|
||||
max_image_size = getattr(config_module, 'MAX_IMAGE_SIZE', 10 * 1024 * 1024)
|
||||
max_image_size = SystemConfigService.get_max_image_size() * 1024 * 1024
|
||||
if image_file.size > max_image_size:
|
||||
return create_api_response(code="400", message=f"Image size exceeds {max_image_size // (1024 * 1024)}MB limit")
|
||||
with get_db_connection() as connection:
|
||||
|
|
@ -952,7 +949,7 @@ async def upload_image(meeting_id: int, image_file: UploadFile = File(...), curr
|
|||
"file_name": image_file.filename, "file_path": '/'+ str(relative_path)
|
||||
})
|
||||
|
||||
def update_speaker_tag(meeting_id: int, request: SpeakerTagUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
def update_speaker_tag(meeting_id: int, request: SpeakerTagUpdateRequest, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -968,7 +965,7 @@ def update_speaker_tag(meeting_id: int, request: SpeakerTagUpdateRequest, curren
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to update speaker tag: {str(e)}")
|
||||
|
||||
def batch_update_speaker_tags(meeting_id: int, request: BatchSpeakerTagUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
def batch_update_speaker_tags(meeting_id: int, request: BatchSpeakerTagUpdateRequest, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -985,7 +982,7 @@ def batch_update_speaker_tags(meeting_id: int, request: BatchSpeakerTagUpdateReq
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to batch update speaker tags: {str(e)}")
|
||||
|
||||
def batch_update_transcript(meeting_id: int, request: BatchTranscriptUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
def batch_update_transcript(meeting_id: int, request: BatchTranscriptUpdateRequest, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -1005,7 +1002,7 @@ def batch_update_transcript(meeting_id: int, request: BatchTranscriptUpdateReque
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to update transcript: {str(e)}")
|
||||
|
||||
def get_meeting_summaries(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_meeting_summaries(meeting_id: int, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -1017,7 +1014,7 @@ def get_meeting_summaries(meeting_id: int, current_user: dict = Depends(get_curr
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to get summaries: {str(e)}")
|
||||
|
||||
def get_summary_detail(meeting_id: int, summary_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_summary_detail(meeting_id: int, summary_id: int, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -1030,7 +1027,7 @@ def get_summary_detail(meeting_id: int, summary_id: int, current_user: dict = De
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to get summary detail: {str(e)}")
|
||||
|
||||
def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequest, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
|
||||
def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequest, background_tasks: BackgroundTasks, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -1067,7 +1064,7 @@ def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequ
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to start summary generation: {str(e)}")
|
||||
|
||||
def get_meeting_llm_tasks(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def get_meeting_llm_tasks(meeting_id: int, current_user: dict):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -1081,7 +1078,7 @@ def get_meeting_llm_tasks(meeting_id: int, current_user: dict = Depends(get_curr
|
|||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"Failed to get LLM tasks: {str(e)}")
|
||||
|
||||
def list_active_llm_models(current_user: dict = Depends(get_current_user)):
|
||||
def list_active_llm_models(current_user: dict):
|
||||
"""获取所有激活的LLM模型列表(供普通用户选择)"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
|
|
@ -1095,7 +1092,7 @@ def list_active_llm_models(current_user: dict = Depends(get_current_user)):
|
|||
return create_api_response(code="500", message=f"获取模型列表失败: {str(e)}")
|
||||
def get_meeting_navigation(
|
||||
meeting_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
current_user: dict,
|
||||
user_id: Optional[int] = None,
|
||||
filter_type: str = "all",
|
||||
search: Optional[str] = None,
|
||||
|
|
@ -1359,7 +1356,7 @@ class VerifyPasswordRequest(BaseModel):
|
|||
def update_meeting_access_password(
|
||||
meeting_id: int,
|
||||
request: AccessPasswordRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
current_user: dict
|
||||
):
|
||||
"""
|
||||
设置或关闭会议访问密码(仅创建人可操作)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ from typing import Optional, Dict, Any
|
|||
from app.core.database import get_db_connection
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置服务 - 优先从新配置表读取,兼容 dict_data(system_config) 回退"""
|
||||
"""系统配置服务。"""
|
||||
|
||||
DICT_TYPE = 'system_config'
|
||||
PUBLIC_CATEGORY = 'public'
|
||||
DEFAULT_LLM_ENDPOINT_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
||||
CACHE_TTL_SECONDS = 60
|
||||
|
|
@ -17,14 +16,15 @@ class SystemConfigService:
|
|||
PAGE_SIZE = 'page_size'
|
||||
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
||||
MAX_AUDIO_SIZE = 'max_audio_size'
|
||||
MAX_IMAGE_SIZE = 'max_image_size'
|
||||
TOKEN_EXPIRE_DAYS = 'token_expire_days'
|
||||
|
||||
# 品牌配置
|
||||
APP_NAME = 'app_name'
|
||||
BRANDING_CONSOLE_SUBTITLE = 'branding_console_subtitle'
|
||||
BRANDING_PREVIEW_TITLE = 'branding_preview_title'
|
||||
BRANDING_LOGIN_WELCOME = 'branding_login_welcome'
|
||||
BRANDING_FOOTER_TEXT = 'branding_footer_text'
|
||||
CONSOLE_SUBTITLE = 'console_subtitle'
|
||||
PREVIEW_TITLE = 'preview_title'
|
||||
LOGIN_WELCOME = 'login_welcome'
|
||||
FOOTER_TEXT = 'footer_text'
|
||||
|
||||
# 声纹配置
|
||||
VOICEPRINT_TEMPLATE_TEXT = 'voiceprint_template_text'
|
||||
|
|
@ -42,6 +42,98 @@ class SystemConfigService:
|
|||
_config_cache: Dict[str, tuple[float, Any]] = {}
|
||||
_category_cache: Dict[str, tuple[float, Dict[str, Any]]] = {}
|
||||
_all_configs_cache: tuple[float, Dict[str, Any]] | None = None
|
||||
BUILTIN_PARAMETERS = [
|
||||
{
|
||||
"param_key": TOKEN_EXPIRE_DAYS,
|
||||
"param_name": "Token过期时间",
|
||||
"param_value": "7",
|
||||
"value_type": "number",
|
||||
"category": "system",
|
||||
"description": "控制登录 token 的过期时间,单位:天。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": DEFAULT_RESET_PASSWORD,
|
||||
"param_name": "默认重置密码",
|
||||
"param_value": "123456",
|
||||
"value_type": "string",
|
||||
"category": "system",
|
||||
"description": "管理员重置用户密码时使用的默认密码。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": PAGE_SIZE,
|
||||
"param_name": "系统分页大小",
|
||||
"param_value": "10",
|
||||
"value_type": "number",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "系统通用分页数量。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": MAX_AUDIO_SIZE,
|
||||
"param_name": "音频上传大小限制",
|
||||
"param_value": "100",
|
||||
"value_type": "number",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "音频上传大小限制,单位:MB。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": MAX_IMAGE_SIZE,
|
||||
"param_name": "图片上传大小限制",
|
||||
"param_value": "10",
|
||||
"value_type": "number",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "图片上传大小限制,单位:MB。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": APP_NAME,
|
||||
"param_name": "系统名称",
|
||||
"param_value": "iMeeting",
|
||||
"value_type": "string",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "前端应用标题。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": CONSOLE_SUBTITLE,
|
||||
"param_name": "控制台副标题",
|
||||
"param_value": "智能会议协作平台",
|
||||
"value_type": "string",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "登录后控制台副标题。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": PREVIEW_TITLE,
|
||||
"param_name": "会议预览标题",
|
||||
"param_value": "会议预览",
|
||||
"value_type": "string",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "会议预览页标题。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": LOGIN_WELCOME,
|
||||
"param_name": "登录欢迎语",
|
||||
"param_value": "欢迎登录 iMeeting,请输入您的账号信息。",
|
||||
"value_type": "string",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "登录页欢迎语。",
|
||||
"is_active": 1,
|
||||
},
|
||||
{
|
||||
"param_key": FOOTER_TEXT,
|
||||
"param_name": "页脚文案",
|
||||
"param_value": "©2026 iMeeting",
|
||||
"value_type": "string",
|
||||
"category": PUBLIC_CATEGORY,
|
||||
"description": "系统页脚文案。",
|
||||
"is_active": 1,
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _is_cache_valid(cls, cached_at: float) -> bool:
|
||||
|
|
@ -244,10 +336,7 @@ class SystemConfigService:
|
|||
audio_row = cursor.fetchone()
|
||||
cursor.close()
|
||||
if audio_row:
|
||||
cfg = cls._build_audio_runtime_config(audio_row)
|
||||
if cfg.get("max_size_bytes") is not None and cfg.get("voiceprint_max_size") is None:
|
||||
cfg["voiceprint_max_size"] = cfg["max_size_bytes"]
|
||||
return cfg
|
||||
return cls._build_audio_runtime_config(audio_row)
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
|
|
@ -275,35 +364,8 @@ class SystemConfigService:
|
|||
cls._set_cached_config(dict_code, value)
|
||||
return value
|
||||
|
||||
# 2) 兼容旧 sys_dict_data
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT extension_attr
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = %s AND dict_code = %s AND status = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query, (cls.DICT_TYPE, dict_code))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if result and result['extension_attr']:
|
||||
try:
|
||||
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
|
||||
resolved_value = ext_attr.get('value', default_value)
|
||||
cls._set_cached_config(dict_code, resolved_value)
|
||||
return resolved_value
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
cls._set_cached_config(dict_code, default_value)
|
||||
return default_value
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting config {dict_code}: {e}")
|
||||
return default_value
|
||||
cls._set_cached_config(dict_code, default_value)
|
||||
return default_value
|
||||
|
||||
@classmethod
|
||||
def get_config_attribute(cls, dict_code: str, attr_name: str, default_value: Any = None) -> Any:
|
||||
|
|
@ -325,32 +387,7 @@ class SystemConfigService:
|
|||
if model_json is not None:
|
||||
return model_json.get(attr_name, default_value)
|
||||
|
||||
# 2) 兼容旧 sys_dict_data
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT extension_attr
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = %s AND dict_code = %s AND status = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query, (cls.DICT_TYPE, dict_code))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if result and result['extension_attr']:
|
||||
try:
|
||||
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
|
||||
return ext_attr.get(attr_name, default_value)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
return default_value
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting config attribute {dict_code}.{attr_name}: {e}")
|
||||
return default_value
|
||||
return default_value
|
||||
|
||||
@classmethod
|
||||
def get_model_runtime_config(cls, model_code: str) -> Optional[Dict[str, Any]]:
|
||||
|
|
@ -433,49 +470,7 @@ class SystemConfigService:
|
|||
except Exception as e:
|
||||
print(f"Error setting config in sys_system_parameters {dict_code}: {e}")
|
||||
|
||||
# 2) 回退写入旧 sys_dict_data
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 检查配置是否存在
|
||||
cursor.execute(
|
||||
"SELECT id FROM sys_dict_data WHERE dict_type = %s AND dict_code = %s",
|
||||
(cls.DICT_TYPE, dict_code)
|
||||
)
|
||||
existing = cursor.fetchone()
|
||||
|
||||
extension_attr = json.dumps({"value": value}, ensure_ascii=False)
|
||||
|
||||
if existing:
|
||||
# 更新现有配置
|
||||
update_query = """
|
||||
UPDATE sys_dict_data
|
||||
SET extension_attr = %s, update_time = NOW()
|
||||
WHERE dict_type = %s AND dict_code = %s
|
||||
"""
|
||||
cursor.execute(update_query, (extension_attr, cls.DICT_TYPE, dict_code))
|
||||
else:
|
||||
# 插入新配置
|
||||
if not label_cn:
|
||||
label_cn = dict_code
|
||||
|
||||
insert_query = """
|
||||
INSERT INTO sys_dict_data (
|
||||
dict_type, dict_code, parent_code, label_cn,
|
||||
extension_attr, status, sort_order
|
||||
) VALUES (%s, %s, 'ROOT', %s, %s, 1, 0)
|
||||
"""
|
||||
cursor.execute(insert_query, (cls.DICT_TYPE, dict_code, label_cn, extension_attr))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
cls.invalidate_cache()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting config {dict_code}: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_all_configs(cls) -> Dict[str, Any]:
|
||||
|
|
@ -491,7 +486,6 @@ class SystemConfigService:
|
|||
if cls._all_configs_cache and not cls._is_cache_valid(cls._all_configs_cache[0]):
|
||||
cls._all_configs_cache = None
|
||||
|
||||
# 1) 新参数表
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
|
@ -512,39 +506,7 @@ class SystemConfigService:
|
|||
return dict(configs)
|
||||
except Exception as e:
|
||||
print(f"Error getting all configs from sys_system_parameters: {e}")
|
||||
|
||||
# 2) 兼容旧 sys_dict_data
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT dict_code, label_cn, extension_attr
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = %s AND status = 1
|
||||
ORDER BY sort_order
|
||||
"""
|
||||
cursor.execute(query, (cls.DICT_TYPE,))
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
configs = {}
|
||||
for row in results:
|
||||
if row['extension_attr']:
|
||||
try:
|
||||
ext_attr = json.loads(row['extension_attr']) if isinstance(row['extension_attr'], str) else row['extension_attr']
|
||||
configs[row['dict_code']] = ext_attr.get('value')
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
configs[row['dict_code']] = None
|
||||
else:
|
||||
configs[row['dict_code']] = None
|
||||
|
||||
with cls._cache_lock:
|
||||
cls._all_configs_cache = (time.time(), configs)
|
||||
return dict(configs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting all configs: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_configs_by_category(cls, category: str) -> Dict[str, Any]:
|
||||
|
|
@ -598,22 +560,10 @@ class SystemConfigService:
|
|||
@classmethod
|
||||
def ensure_builtin_parameters(cls) -> None:
|
||||
"""确保内建系统参数存在,避免后台参数页缺少关键配置项。"""
|
||||
builtin_parameters = [
|
||||
{
|
||||
"param_key": cls.TOKEN_EXPIRE_DAYS,
|
||||
"param_name": "Token过期时间",
|
||||
"param_value": "7",
|
||||
"value_type": "number",
|
||||
"category": "system",
|
||||
"description": "控制登录 token 的过期时间,单位:天。",
|
||||
"is_active": 1,
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for item in builtin_parameters:
|
||||
for item in cls.BUILTIN_PARAMETERS:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sys_system_parameters
|
||||
|
|
@ -644,10 +594,7 @@ class SystemConfigService:
|
|||
audio_cfg = cls.get_active_audio_model_config("asr")
|
||||
if audio_cfg.get("vocabulary_id"):
|
||||
return audio_cfg["vocabulary_id"]
|
||||
audio_vocab = cls.get_config_attribute('audio_model', 'vocabulary_id')
|
||||
if audio_vocab:
|
||||
return audio_vocab
|
||||
return cls.get_config(cls.ASR_VOCABULARY_ID)
|
||||
return cls.get_config_attribute('audio_model', 'vocabulary_id')
|
||||
|
||||
# 声纹配置获取方法(直接使用通用方法)
|
||||
@classmethod
|
||||
|
|
@ -658,9 +605,7 @@ class SystemConfigService:
|
|||
@classmethod
|
||||
def get_voiceprint_max_size(cls, default: int = 5242880) -> int:
|
||||
"""获取声纹文件大小限制 (bytes), 默认5MB"""
|
||||
value = cls.get_config_attribute('voiceprint_model', 'max_size_bytes', None)
|
||||
if value is None:
|
||||
value = cls.get_config_attribute('voiceprint_model', 'voiceprint_max_size', default)
|
||||
value = cls.get_config_attribute('voiceprint_model', 'max_size_bytes', default)
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
|
|
@ -694,90 +639,93 @@ class SystemConfigService:
|
|||
return default
|
||||
|
||||
@classmethod
|
||||
def get_page_size(cls, default: int = 10) -> int:
|
||||
def get_page_size(cls) -> int:
|
||||
"""获取系统通用分页数量。"""
|
||||
value = cls.get_config(cls.PAGE_SIZE, str(default))
|
||||
value = cls.get_config(cls.PAGE_SIZE)
|
||||
if value is None:
|
||||
raise RuntimeError("系统参数 page_size 缺失")
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
raise RuntimeError(f"系统参数 page_size 非法: {value!r}") from None
|
||||
|
||||
@classmethod
|
||||
def get_default_reset_password(cls, default: str = "111111") -> str:
|
||||
def get_default_reset_password(cls) -> str:
|
||||
"""获取默认重置密码"""
|
||||
return cls.get_config(cls.DEFAULT_RESET_PASSWORD, default)
|
||||
value = cls.get_config(cls.DEFAULT_RESET_PASSWORD)
|
||||
if value is None:
|
||||
raise RuntimeError("系统参数 default_reset_password 缺失")
|
||||
normalized = str(value).strip()
|
||||
if not normalized:
|
||||
raise RuntimeError("系统参数 default_reset_password 不能为空")
|
||||
return normalized
|
||||
|
||||
@classmethod
|
||||
def get_max_audio_size(cls, default: int = 100) -> int:
|
||||
def get_max_audio_size(cls) -> int:
|
||||
"""获取上传音频文件大小限制(MB)"""
|
||||
value = cls.get_config(cls.MAX_AUDIO_SIZE, str(default))
|
||||
value = cls.get_config(cls.MAX_AUDIO_SIZE)
|
||||
if value is None:
|
||||
raise RuntimeError("系统参数 max_audio_size 缺失")
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
raise RuntimeError(f"系统参数 max_audio_size 非法: {value!r}") from None
|
||||
|
||||
@classmethod
|
||||
def get_token_expire_days(cls, default: int = 7) -> int:
|
||||
def get_max_image_size(cls) -> int:
|
||||
"""获取上传图片大小限制(MB)"""
|
||||
value = cls.get_config(cls.MAX_IMAGE_SIZE)
|
||||
if value is None:
|
||||
raise RuntimeError("系统参数 max_image_size 缺失")
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise RuntimeError(f"系统参数 max_image_size 非法: {value!r}") from None
|
||||
|
||||
@classmethod
|
||||
def get_token_expire_days(cls) -> int:
|
||||
"""获取访问 token 过期时间(天)。"""
|
||||
value = cls.get_config(cls.TOKEN_EXPIRE_DAYS, str(default))
|
||||
value = cls.get_config(cls.TOKEN_EXPIRE_DAYS)
|
||||
if value is None:
|
||||
raise RuntimeError("系统参数 token_expire_days 缺失")
|
||||
try:
|
||||
normalized = int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
return normalized if normalized > 0 else default
|
||||
raise RuntimeError(f"系统参数 token_expire_days 非法: {value!r}") from None
|
||||
if normalized <= 0:
|
||||
raise RuntimeError(f"系统参数 token_expire_days 非法: {value!r}")
|
||||
return normalized
|
||||
|
||||
@classmethod
|
||||
def get_public_configs(cls) -> Dict[str, Any]:
|
||||
"""获取提供给前端初始化使用的公开参数。"""
|
||||
cls.ensure_builtin_parameters()
|
||||
public_configs = cls.get_configs_by_category(cls.PUBLIC_CATEGORY)
|
||||
required_keys = [
|
||||
cls.APP_NAME,
|
||||
cls.CONSOLE_SUBTITLE,
|
||||
cls.PREVIEW_TITLE,
|
||||
cls.LOGIN_WELCOME,
|
||||
cls.FOOTER_TEXT,
|
||||
cls.PAGE_SIZE,
|
||||
cls.MAX_AUDIO_SIZE,
|
||||
cls.MAX_IMAGE_SIZE,
|
||||
]
|
||||
missing_keys = [key for key in required_keys if str(public_configs.get(key) or "").strip() == ""]
|
||||
if missing_keys:
|
||||
raise RuntimeError(f"公开系统参数缺失: {', '.join(missing_keys)}")
|
||||
|
||||
app_name = str(public_configs.get(cls.APP_NAME) or "智听云平台")
|
||||
console_subtitle = str(
|
||||
public_configs.get("console_subtitle")
|
||||
or public_configs.get(cls.BRANDING_CONSOLE_SUBTITLE)
|
||||
or "iMeeting控制台"
|
||||
)
|
||||
preview_title = str(
|
||||
public_configs.get("preview_title")
|
||||
or public_configs.get(cls.BRANDING_PREVIEW_TITLE)
|
||||
or "会议预览"
|
||||
)
|
||||
login_welcome = str(
|
||||
public_configs.get("login_welcome")
|
||||
or public_configs.get(cls.BRANDING_LOGIN_WELCOME)
|
||||
or "欢迎回来,请输入您的登录凭证。"
|
||||
)
|
||||
footer_text = str(
|
||||
public_configs.get("footer_text")
|
||||
or public_configs.get(cls.BRANDING_FOOTER_TEXT)
|
||||
or "©2026 智听云平台"
|
||||
)
|
||||
|
||||
raw_page_size = public_configs.get(cls.PAGE_SIZE, "10")
|
||||
try:
|
||||
page_size = int(raw_page_size)
|
||||
except (ValueError, TypeError):
|
||||
page_size = 10
|
||||
|
||||
raw_max_audio_size = public_configs.get(cls.MAX_AUDIO_SIZE, "100")
|
||||
try:
|
||||
max_audio_size_mb = int(raw_max_audio_size)
|
||||
except (ValueError, TypeError):
|
||||
max_audio_size_mb = 100
|
||||
|
||||
raw_max_image_size = public_configs.get("max_image_size", "10")
|
||||
try:
|
||||
max_image_size_mb = int(raw_max_image_size)
|
||||
except (ValueError, TypeError):
|
||||
max_image_size_mb = 10
|
||||
page_size = cls.get_page_size()
|
||||
max_audio_size_mb = cls.get_max_audio_size()
|
||||
max_image_size_mb = cls.get_max_image_size()
|
||||
|
||||
return {
|
||||
**public_configs,
|
||||
"app_name": app_name,
|
||||
"console_subtitle": console_subtitle,
|
||||
"preview_title": preview_title,
|
||||
"login_welcome": login_welcome,
|
||||
"footer_text": footer_text,
|
||||
"app_name": str(public_configs[cls.APP_NAME]).strip(),
|
||||
"console_subtitle": str(public_configs[cls.CONSOLE_SUBTITLE]).strip(),
|
||||
"preview_title": str(public_configs[cls.PREVIEW_TITLE]).strip(),
|
||||
"login_welcome": str(public_configs[cls.LOGIN_WELCOME]).strip(),
|
||||
"footer_text": str(public_configs[cls.FOOTER_TEXT]).strip(),
|
||||
"page_size": str(page_size),
|
||||
"PAGE_SIZE": page_size,
|
||||
"max_audio_size": str(max_audio_size_mb),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Core Application Framework
|
||||
fastapi
|
||||
uvicorn
|
||||
mcp
|
||||
|
||||
# Database & Cache
|
||||
mysql-connector-python
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { 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';
|
||||
|
|
@ -88,26 +88,30 @@ const DefaultMenuRedirect = ({ user }) => {
|
|||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [bootstrapError, setBootstrapError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const bootstrapApp = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setBootstrapError(null);
|
||||
setUser(getStoredUser());
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
configService.getBrandingConfig().then((branding) => {
|
||||
if (active && branding?.app_name) {
|
||||
try {
|
||||
const branding = await configService.getBrandingConfig();
|
||||
if (branding?.app_name) {
|
||||
document.title = branding.app_name;
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Load public config failed:', error);
|
||||
setBootstrapError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
bootstrapApp();
|
||||
}, [bootstrapApp]);
|
||||
|
||||
const handleLogin = (authData) => {
|
||||
if (authData) {
|
||||
menuService.clearCache();
|
||||
|
|
@ -136,6 +140,30 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
if (bootstrapError) {
|
||||
return (
|
||||
<div className="app-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>系统公开配置加载失败</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={bootstrapApp}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: '10px 18px',
|
||||
borderRadius: 999,
|
||||
border: 'none',
|
||||
background: '#1d4ed8',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import menuService from '../services/menuService';
|
||||
import { renderMenuIcon } from '../utils/menuIcons';
|
||||
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||
import configService from '../utils/configService';
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
|
@ -21,7 +21,7 @@ const MainLayout = ({ children, user, onLogout }) => {
|
|||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState([]);
|
||||
const [activeMenuKey, setActiveMenuKey] = useState(null);
|
||||
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||
const [branding, setBranding] = useState(() => configService.getCachedBrandingConfig());
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const screens = useBreakpoint();
|
||||
|
|
@ -43,8 +43,23 @@ const MainLayout = ({ children, user, onLogout }) => {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||
}, []);
|
||||
if (branding) {
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
configService.getBrandingConfig().then((nextBranding) => {
|
||||
if (active) {
|
||||
setBranding(nextBranding);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Load branding config failed:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [branding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
|
|
@ -332,6 +347,10 @@ const MainLayout = ({ children, user, onLogout }) => {
|
|||
</>
|
||||
);
|
||||
|
||||
if (!branding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="main-layout-shell">
|
||||
{isMobile ? (
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
|||
<div>
|
||||
<Text type="secondary">
|
||||
支持 {AUDIO_UPLOAD_ACCEPT.replace(/\./g, '').toUpperCase()};
|
||||
<br/>音频文件最大 {configService.formatFileSize(maxAudioSize)}。
|
||||
<br/>音频文件最大 {maxAudioSize ? configService.formatFileSize(maxAudioSize) : '加载中'}。
|
||||
</Text>
|
||||
</div>
|
||||
<Upload accept={AUDIO_UPLOAD_ACCEPT} showUploadList={false} beforeUpload={handleAudioBeforeUpload}>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ const API_CONFIG = {
|
|||
LLM_MODELS: '/api/llm-models/active'
|
||||
},
|
||||
ADMIN: {
|
||||
SYSTEM_CONFIG: '/api/admin/system-config',
|
||||
PARAMETERS: '/api/admin/parameters',
|
||||
PARAMETER_DETAIL: (paramKey) => `/api/admin/parameters/${paramKey}`,
|
||||
PARAMETER_DELETE: (paramKey) => `/api/admin/parameters/${paramKey}`,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default function useMeetingDetailsPage({ user }) {
|
|||
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
|
||||
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
|
||||
const [savingAccessPassword, setSavingAccessPassword] = useState(false);
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(() => configService.getCachedMaxAudioSize());
|
||||
|
||||
const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false);
|
||||
const [editingSegments, setEditingSegments] = useState({});
|
||||
|
|
@ -146,24 +146,24 @@ export default function useMeetingDetailsPage({ user }) {
|
|||
const loadAudioUploadConfig = useCallback(async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
setMaxAudioSize(nextMaxAudioSize);
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载音频上传限制失败');
|
||||
}
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
const fetchPromptList = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
||||
setPromptList(res.data.prompts || []);
|
||||
const defaultPrompt = res.data.prompts?.find((prompt) => prompt.is_default) || res.data.prompts?.[0];
|
||||
const defaultPrompt = res.data.prompts?.find((prompt) => prompt.is_default);
|
||||
if (defaultPrompt) {
|
||||
setSelectedPromptId(defaultPrompt.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('加载提示词列表失败:', error);
|
||||
message.error(error?.message || '加载提示词列表失败');
|
||||
}
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
const fetchLlmModels = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -175,9 +175,9 @@ export default function useMeetingDetailsPage({ user }) {
|
|||
setSelectedModelCode(defaultModel.model_code);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('加载模型列表失败:', error);
|
||||
message.error(error?.message || '加载模型列表失败');
|
||||
}
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
const fetchSummaryResources = useCallback(async () => {
|
||||
setSummaryResourcesLoading(true);
|
||||
|
|
@ -479,6 +479,12 @@ export default function useMeetingDetailsPage({ user }) {
|
|||
}, [currentHighlightIndex, transcriptVisibleCount]);
|
||||
|
||||
const validateAudioBeforeUpload = (file) => {
|
||||
if (!maxAudioSize) {
|
||||
const configErrorMessage = '系统音频上传限制未加载完成,请稍后重试';
|
||||
message.error(configErrorMessage);
|
||||
return configErrorMessage;
|
||||
}
|
||||
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function useMeetingFormDrawer({ open, onClose, onSuccess, meeting
|
|||
const [audioUploading, setAudioUploading] = useState(false);
|
||||
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
||||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(() => configService.getCachedMaxAudioSize());
|
||||
|
||||
const isEdit = Boolean(meetingId);
|
||||
|
||||
|
|
@ -36,11 +36,11 @@ export default function useMeetingFormDrawer({ open, onClose, onSuccess, meeting
|
|||
const loadAudioUploadConfig = useCallback(async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
setMaxAudioSize(nextMaxAudioSize);
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载音频上传限制失败');
|
||||
}
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
const fetchMeeting = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -80,6 +80,11 @@ export default function useMeetingFormDrawer({ open, onClose, onSuccess, meeting
|
|||
}, [fetchMeeting, fetchOptions, form, isEdit, loadAudioUploadConfig, open]);
|
||||
|
||||
const handleAudioBeforeUpload = (file) => {
|
||||
if (!maxAudioSize) {
|
||||
message.error('系统音频上传限制未加载完成,请稍后重试');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import configService from '../utils/configService';
|
||||
|
||||
const normalizePageSize = (value, fallback) => {
|
||||
const normalizePageSize = (value) => {
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return fallback;
|
||||
return null;
|
||||
}
|
||||
return Math.min(100, Math.max(5, Math.floor(numericValue)));
|
||||
};
|
||||
|
||||
const useSystemPageSize = (fallback = 10, options = {}) => {
|
||||
const useSystemPageSize = (fallbackOrOptions = {}, maybeOptions = {}) => {
|
||||
const options = typeof fallbackOrOptions === 'object' && fallbackOrOptions !== null
|
||||
? fallbackOrOptions
|
||||
: maybeOptions;
|
||||
const { suspendUntilReady = false } = options;
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const cachedPageSize = configService.getCachedPageSize();
|
||||
if (cachedPageSize) {
|
||||
return normalizePageSize(cachedPageSize, fallback);
|
||||
return normalizePageSize(cachedPageSize);
|
||||
}
|
||||
return suspendUntilReady ? null : normalizePageSize(fallback, 10);
|
||||
return null;
|
||||
});
|
||||
const [isReady, setIsReady] = useState(() => pageSize !== null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
|
@ -27,26 +31,27 @@ const useSystemPageSize = (fallback = 10, options = {}) => {
|
|||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setPageSize(normalizePageSize(size, fallback));
|
||||
setPageSize(normalizePageSize(size));
|
||||
setError(null);
|
||||
setIsReady(true);
|
||||
}).catch(() => {
|
||||
}).catch((nextError) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setPageSize((prev) => prev ?? normalizePageSize(fallback, 10));
|
||||
setError(nextError);
|
||||
setIsReady(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [fallback]);
|
||||
}, []);
|
||||
|
||||
if (suspendUntilReady) {
|
||||
return { pageSize, isReady };
|
||||
return { pageSize, isReady, error };
|
||||
}
|
||||
|
||||
return pageSize ?? normalizePageSize(fallback, 10);
|
||||
return pageSize;
|
||||
};
|
||||
|
||||
export default useSystemPageSize;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const CreateMeeting = () => {
|
|||
const [audioUploading, setAudioUploading] = useState(false);
|
||||
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
||||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(() => configService.getCachedMaxAudioSize());
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -50,11 +50,11 @@ const CreateMeeting = () => {
|
|||
const loadAudioUploadConfig = useCallback(async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
setMaxAudioSize(nextMaxAudioSize);
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载音频上传限制失败');
|
||||
}
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
|
|
@ -63,6 +63,11 @@ const CreateMeeting = () => {
|
|||
}, [fetchPrompts, fetchUsers, loadAudioUploadConfig]);
|
||||
|
||||
const handleAudioBeforeUpload = (file) => {
|
||||
if (!maxAudioSize) {
|
||||
message.error('系统音频上传限制未加载完成,请稍后重试');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
|
|
@ -184,7 +189,7 @@ const CreateMeeting = () => {
|
|||
<Text strong style={{ display: 'block', marginBottom: 4 }}>上传会议音频</Text>
|
||||
<Text type="secondary">
|
||||
创建后会自动沿用会议详情页同一套上传与转录逻辑。支持 {AUDIO_UPLOAD_ACCEPT.replace(/\./g, '').toUpperCase()},
|
||||
最大 {configService.formatFileSize(maxAudioSize)}。
|
||||
最大 {maxAudioSize ? configService.formatFileSize(maxAudioSize) : '加载中'}。
|
||||
</Text>
|
||||
</div>
|
||||
<Upload accept={AUDIO_UPLOAD_ACCEPT} showUploadList={false} beforeUpload={handleAudioBeforeUpload}>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import httpService from '../services/httpService';
|
|||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import menuService from '../services/menuService';
|
||||
import BrandLogo from '../components/BrandLogo';
|
||||
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||
import configService from '../utils/configService';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
|
|
@ -25,12 +25,31 @@ const HOME_TAGLINE = '让每一次谈话都产生价值';
|
|||
|
||||
const HomePage = ({ onLogin }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||
const [branding, setBranding] = useState(() => configService.getCachedBrandingConfig());
|
||||
const { message } = App.useApp();
|
||||
|
||||
useEffect(() => {
|
||||
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||
}, []);
|
||||
if (branding) {
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
configService.getBrandingConfig().then((nextBranding) => {
|
||||
if (active) {
|
||||
setBranding(nextBranding);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Load branding config failed:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [branding]);
|
||||
|
||||
if (!branding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleLogin = async (values) => {
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -101,10 +101,11 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
try {
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
||||
setAvailablePrompts(res.data.prompts || []);
|
||||
const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0];
|
||||
const def = res.data.prompts?.find((prompt) => prompt.is_default);
|
||||
if (def) setSelectedPromptId(def.id);
|
||||
} catch {
|
||||
setAvailablePrompts([]);
|
||||
setSelectedPromptId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const MeetingCenterPage = ({ user }) => {
|
|||
const [searchValue, setSearchValue] = useState('');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [page, setPage] = useState(1);
|
||||
const { pageSize, isReady: pageSizeReady } = useSystemPageSize(10, { suspendUntilReady: true });
|
||||
const { pageSize, isReady: pageSizeReady, error: pageSizeError } = useSystemPageSize({ suspendUntilReady: true });
|
||||
const [total, setTotal] = useState(0);
|
||||
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
|
||||
const [editingMeetingId, setEditingMeetingId] = useState(null);
|
||||
|
|
@ -153,6 +153,12 @@ const MeetingCenterPage = ({ user }) => {
|
|||
meetingCacheService.clearAll();
|
||||
}, [pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pageSizeError) {
|
||||
message.error(pageSizeError.message || '分页配置加载失败');
|
||||
}
|
||||
}, [message, pageSizeError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageSizeReady || !pageSize) {
|
||||
return;
|
||||
|
|
@ -248,6 +254,10 @@ const MeetingCenterPage = ({ user }) => {
|
|||
<div className="meeting-center-empty">
|
||||
<Empty description="加载分页配置中..." />
|
||||
</div>
|
||||
) : !pageSize ? (
|
||||
<div className="meeting-center-empty">
|
||||
<Empty description="分页配置加载失败,请刷新后重试" />
|
||||
</div>
|
||||
) : meetings.length ? (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import MindMap from '../components/MindMap';
|
|||
import AudioPlayerBar from '../components/AudioPlayerBar';
|
||||
import TranscriptTimeline from '../components/TranscriptTimeline';
|
||||
import tools from '../utils/tools';
|
||||
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||
import configService from '../utils/configService';
|
||||
import './MeetingPreview.css';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
|
@ -26,15 +26,30 @@ const MeetingPreview = () => {
|
|||
const [passwordError, setPasswordError] = useState('');
|
||||
const [passwordRequired, setPasswordRequired] = useState(false);
|
||||
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||
const [branding, setBranding] = useState(() => configService.getCachedBrandingConfig());
|
||||
const [transcript, setTranscript] = useState([]);
|
||||
const [audioUrl, setAudioUrl] = useState('');
|
||||
const [activeSegmentIndex, setActiveSegmentIndex] = useState(-1);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||
}, []);
|
||||
if (branding) {
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
configService.getBrandingConfig().then((nextBranding) => {
|
||||
if (active) {
|
||||
setBranding(nextBranding);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Load branding config failed:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [branding]);
|
||||
|
||||
const fetchTranscriptAndAudio = useCallback(async () => {
|
||||
const [transcriptRes, audioRes] = await Promise.allSettled([
|
||||
|
|
@ -154,6 +169,10 @@ const MeetingPreview = () => {
|
|||
fetchPreview(password);
|
||||
};
|
||||
|
||||
if (!branding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading && !meeting) {
|
||||
return (
|
||||
<div className="preview-container">
|
||||
|
|
|
|||
|
|
@ -3,13 +3,62 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
|||
|
||||
const PUBLIC_CONFIG_CACHE_KEY = 'imeeting_public_config_cache';
|
||||
const PUBLIC_CONFIG_CACHE_TTL = 5 * 60 * 1000;
|
||||
const REQUIRED_PUBLIC_CONFIG_KEYS = [
|
||||
'app_name',
|
||||
'console_subtitle',
|
||||
'preview_title',
|
||||
'login_welcome',
|
||||
'footer_text',
|
||||
'PAGE_SIZE',
|
||||
'MAX_FILE_SIZE',
|
||||
'MAX_IMAGE_SIZE',
|
||||
];
|
||||
|
||||
export const DEFAULT_BRANDING_CONFIG = {
|
||||
app_name: '智听云平台',
|
||||
console_subtitle: 'iMeeting控制台',
|
||||
preview_title: '会议预览',
|
||||
login_welcome: '欢迎回来,请输入您的登录凭证。',
|
||||
footer_text: '©2026 智听云平台',
|
||||
const parsePositiveInteger = (value) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const validatePublicConfig = (configs) => {
|
||||
if (!configs || typeof configs !== 'object' || Array.isArray(configs)) {
|
||||
throw new Error('公开配置返回格式非法');
|
||||
}
|
||||
|
||||
const missingKeys = REQUIRED_PUBLIC_CONFIG_KEYS.filter((key) => String(configs[key] ?? '').trim() === '');
|
||||
if (missingKeys.length > 0) {
|
||||
throw new Error(`公开配置缺失: ${missingKeys.join(', ')}`);
|
||||
}
|
||||
|
||||
const pageSize = parsePositiveInteger(configs.PAGE_SIZE ?? configs.page_size);
|
||||
if (!pageSize) {
|
||||
throw new Error('公开配置 PAGE_SIZE 非法');
|
||||
}
|
||||
|
||||
const maxFileSize = parsePositiveInteger(configs.MAX_FILE_SIZE);
|
||||
if (!maxFileSize) {
|
||||
throw new Error('公开配置 MAX_FILE_SIZE 非法');
|
||||
}
|
||||
|
||||
const maxImageSize = parsePositiveInteger(configs.MAX_IMAGE_SIZE);
|
||||
if (!maxImageSize) {
|
||||
throw new Error('公开配置 MAX_IMAGE_SIZE 非法');
|
||||
}
|
||||
|
||||
return {
|
||||
...configs,
|
||||
app_name: String(configs.app_name).trim(),
|
||||
console_subtitle: String(configs.console_subtitle).trim(),
|
||||
preview_title: String(configs.preview_title).trim(),
|
||||
login_welcome: String(configs.login_welcome).trim(),
|
||||
footer_text: String(configs.footer_text).trim(),
|
||||
PAGE_SIZE: pageSize,
|
||||
page_size: String(pageSize),
|
||||
MAX_FILE_SIZE: maxFileSize,
|
||||
MAX_IMAGE_SIZE: maxImageSize,
|
||||
};
|
||||
};
|
||||
|
||||
class ConfigService {
|
||||
|
|
@ -36,10 +85,11 @@ class ConfigService {
|
|||
window.localStorage.removeItem(PUBLIC_CONFIG_CACHE_KEY);
|
||||
return null;
|
||||
}
|
||||
return parsed.data;
|
||||
return validatePublicConfig(parsed.data);
|
||||
}
|
||||
return parsed;
|
||||
return validatePublicConfig(parsed);
|
||||
} catch {
|
||||
window.localStorage.removeItem(PUBLIC_CONFIG_CACHE_KEY);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +97,7 @@ class ConfigService {
|
|||
writeCachedPublicConfig(configs) {
|
||||
try {
|
||||
window.localStorage.setItem(PUBLIC_CONFIG_CACHE_KEY, JSON.stringify({
|
||||
data: configs || {},
|
||||
data: configs,
|
||||
cached_at: Date.now(),
|
||||
}));
|
||||
} catch {
|
||||
|
|
@ -57,27 +107,29 @@ class ConfigService {
|
|||
|
||||
extractPageSize(configs) {
|
||||
const raw = configs?.PAGE_SIZE ?? configs?.page_size;
|
||||
const val = parseInt(raw, 10);
|
||||
if (isNaN(val) || val <= 0) {
|
||||
const value = parsePositiveInteger(raw);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return Math.min(100, Math.max(5, val));
|
||||
return Math.min(100, Math.max(5, value));
|
||||
}
|
||||
|
||||
buildBrandingConfig(configs) {
|
||||
return {
|
||||
...DEFAULT_BRANDING_CONFIG,
|
||||
...(configs || {}),
|
||||
app_name: configs.app_name,
|
||||
console_subtitle: configs.console_subtitle,
|
||||
preview_title: configs.preview_title,
|
||||
login_welcome: configs.login_welcome,
|
||||
footer_text: configs.footer_text,
|
||||
};
|
||||
}
|
||||
|
||||
applyPublicConfig(configs) {
|
||||
this.configs = configs || null;
|
||||
const validatedConfigs = validatePublicConfig(configs);
|
||||
this.configs = validatedConfigs;
|
||||
this._pageSize = this.extractPageSize(this.configs);
|
||||
this.brandingConfig = this.buildBrandingConfig(this.configs);
|
||||
if (this.configs) {
|
||||
this.writeCachedPublicConfig(this.configs);
|
||||
}
|
||||
this.writeCachedPublicConfig(this.configs);
|
||||
return this.configs;
|
||||
}
|
||||
|
||||
|
|
@ -97,54 +149,62 @@ class ConfigService {
|
|||
}
|
||||
|
||||
async loadConfigsFromServer() {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
||||
return this.applyPublicConfig(response.data || {});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load system configs, using defaults:', error);
|
||||
// 返回默认配置
|
||||
return this.applyPublicConfig({
|
||||
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB
|
||||
MAX_IMAGE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
PAGE_SIZE: 10,
|
||||
page_size: '10',
|
||||
});
|
||||
}
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
||||
return this.applyPublicConfig(response.data || {});
|
||||
}
|
||||
|
||||
async getMaxFileSize() {
|
||||
const configs = await this.getConfigs();
|
||||
return configs.MAX_FILE_SIZE || 100 * 1024 * 1024;
|
||||
const maxFileSize = parsePositiveInteger(configs.MAX_FILE_SIZE);
|
||||
if (!maxFileSize) {
|
||||
throw new Error('公开配置 MAX_FILE_SIZE 非法');
|
||||
}
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
async getMaxAudioSize() {
|
||||
return this.getMaxFileSize();
|
||||
}
|
||||
|
||||
getCachedMaxAudioSize() {
|
||||
if (!this.configs) {
|
||||
return null;
|
||||
}
|
||||
return parsePositiveInteger(this.configs.MAX_FILE_SIZE);
|
||||
}
|
||||
|
||||
async getMaxImageSize() {
|
||||
const configs = await this.getConfigs();
|
||||
return configs.MAX_IMAGE_SIZE || 10 * 1024 * 1024;
|
||||
const maxImageSize = parsePositiveInteger(configs.MAX_IMAGE_SIZE);
|
||||
if (!maxImageSize) {
|
||||
throw new Error('公开配置 MAX_IMAGE_SIZE 非法');
|
||||
}
|
||||
return maxImageSize;
|
||||
}
|
||||
|
||||
async getPageSize() {
|
||||
if (this._pageSize) return this._pageSize;
|
||||
try {
|
||||
const configs = await this.getConfigs();
|
||||
const cachedPageSize = this.extractPageSize(configs);
|
||||
if (cachedPageSize) {
|
||||
this._pageSize = cachedPageSize;
|
||||
return cachedPageSize;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load page_size config:', error.message);
|
||||
if (this._pageSize) {
|
||||
return this._pageSize;
|
||||
}
|
||||
return 10;
|
||||
|
||||
const configs = await this.getConfigs();
|
||||
const pageSize = this.extractPageSize(configs);
|
||||
if (!pageSize) {
|
||||
throw new Error('公开配置 PAGE_SIZE 非法');
|
||||
}
|
||||
|
||||
this._pageSize = pageSize;
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
getCachedPageSize() {
|
||||
return this._pageSize;
|
||||
}
|
||||
|
||||
getCachedBrandingConfig() {
|
||||
return this.brandingConfig;
|
||||
}
|
||||
|
||||
async getBrandingConfig() {
|
||||
if (this.brandingConfig) {
|
||||
return this.brandingConfig;
|
||||
|
|
@ -161,13 +221,8 @@ class ConfigService {
|
|||
}
|
||||
|
||||
async loadBrandingConfigFromServer() {
|
||||
try {
|
||||
const configs = await this.getConfigs();
|
||||
return this.buildBrandingConfig(configs);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load branding configs, using defaults:', error);
|
||||
return { ...DEFAULT_BRANDING_CONFIG };
|
||||
}
|
||||
const configs = await this.getConfigs();
|
||||
return this.buildBrandingConfig(configs);
|
||||
}
|
||||
|
||||
// 格式化文件大小为可读格式
|
||||
|
|
|
|||
Loading…
Reference in New Issue