diff --git a/api/app/controllers/memory_storage_controller.py b/api/app/controllers/memory_storage_controller.py index 826724c9..9708b3a5 100644 --- a/api/app/controllers/memory_storage_controller.py +++ b/api/app/controllers/memory_storage_controller.py @@ -2,7 +2,7 @@ from typing import Optional from uuid import UUID from fastapi import APIRouter, Depends, Query -from fastapi.responses import StreamingResponse +from fastapi.responses import StreamingResponse, JSONResponse from sqlalchemy.orm import Session from app.core.error_codes import BizCode @@ -85,6 +85,7 @@ def create_config( payload: ConfigParamsCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), + x_language_type: Optional[str] = Header(None, alias="X-Language-Type"), ) -> dict: workspace_id = current_user.current_workspace_id # 检查用户是否已选择工作空间 @@ -100,6 +101,15 @@ def create_config( result = svc.create(payload) return success(data=result, msg="创建成功") except Exception as e: + from sqlalchemy.exc import IntegrityError + if isinstance(e, IntegrityError) and "uq_workspace_config_name" in str(getattr(e, 'orig', '')): + api_logger.warning(f"重复的配置名称 '{payload.config_name}' 在工作空间 {workspace_id}") + lang = get_language_from_header(x_language_type) + if lang == "en": + msg = fail(BizCode.BAD_REQUEST, "Config name already exists", f"A config named \"{payload.config_name}\" already exists in the current workspace. Please use a different name.") + else: + msg = fail(BizCode.BAD_REQUEST, "配置名称已存在", f"当前工作空间下已存在名为「{payload.config_name}」的记忆配置,请使用其他名称") + return JSONResponse(status_code=400, content=msg) api_logger.error(f"Create config failed: {str(e)}") return fail(BizCode.INTERNAL_ERROR, "创建配置失败", str(e)) diff --git a/api/app/controllers/ontology_controller.py b/api/app/controllers/ontology_controller.py index c892b013..3d2a1bdb 100644 --- a/api/app/controllers/ontology_controller.py +++ b/api/app/controllers/ontology_controller.py @@ -25,7 +25,7 @@ from typing import Dict, Optional, List from urllib.parse import quote from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Form, Header -from fastapi.responses import StreamingResponse +from fastapi.responses import StreamingResponse, JSONResponse from sqlalchemy.orm import Session from app.core.config import settings @@ -289,7 +289,8 @@ async def extract_ontology( async def create_scene( request: SceneCreateRequest, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + current_user: User = Depends(get_current_user), + x_language_type: Optional[str] = Header(None, alias="X-Language-Type") ): """创建本体场景 @@ -360,8 +361,18 @@ async def create_scene( return fail(BizCode.BAD_REQUEST, "请求参数无效", str(e)) except RuntimeError as e: - api_logger.error(f"Runtime error in scene creation: {str(e)}", exc_info=True) - return fail(BizCode.INTERNAL_ERROR, "场景创建失败", str(e)) + err_str = str(e) + if "UniqueViolation" in err_str or "uq_workspace_scene_name" in err_str: + api_logger.warning(f"Duplicate scene name '{request.scene_name}' in workspace {current_user.current_workspace_id}") + from app.core.language_utils import get_language_from_header + lang = get_language_from_header(x_language_type) + if lang == "en": + msg = fail(BizCode.BAD_REQUEST, "Scene name already exists", f"A scene named \"{request.scene_name}\" already exists in the current workspace. Please use a different name.") + else: + msg = fail(BizCode.BAD_REQUEST, "场景名称已存在", f"当前工作空间下已存在名为「{request.scene_name}」的场景,请使用其他名称") + return JSONResponse(status_code=400, content=msg) + api_logger.error(f"Runtime error in scene creation: {err_str}", exc_info=True) + return fail(BizCode.INTERNAL_ERROR, "场景创建失败", err_str) except Exception as e: api_logger.error(f"Unexpected error in scene creation: {str(e)}", exc_info=True) @@ -661,7 +672,8 @@ async def get_scenes( async def create_class( request: ClassCreateRequest, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + current_user: User = Depends(get_current_user), + x_language_type: Optional[str] = Header(None, alias="X-Language-Type") ): """创建本体类型 @@ -676,7 +688,7 @@ async def create_class( ApiResponse: 包含创建的类型信息 """ from app.controllers.ontology_secondary_routes import create_class_handler - return await create_class_handler(request, db, current_user) + return await create_class_handler(request, db, current_user, x_language_type) @router.put("/class/{class_id}", response_model=ApiResponse) diff --git a/api/app/controllers/ontology_secondary_routes.py b/api/app/controllers/ontology_secondary_routes.py index 600aacc7..0d752006 100644 --- a/api/app/controllers/ontology_secondary_routes.py +++ b/api/app/controllers/ontology_secondary_routes.py @@ -7,7 +7,7 @@ from uuid import UUID from typing import Optional -from fastapi import Depends +from fastapi import Depends, Header from sqlalchemy.orm import Session from app.core.error_codes import BizCode @@ -231,7 +231,8 @@ async def scenes_handler( async def create_class_handler( request: ClassCreateRequest, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + current_user: User = Depends(get_current_user), + x_language_type: Optional[str] = None ): """创建本体类型(统一使用列表形式,支持单个或批量)""" @@ -327,8 +328,20 @@ async def create_class_handler( return fail(BizCode.BAD_REQUEST, "请求参数无效", str(e)) except RuntimeError as e: - api_logger.error(f"Runtime error in class creation: {str(e)}", exc_info=True) - return fail(BizCode.INTERNAL_ERROR, "类型创建失败", str(e)) + err_str = str(e) + if "UniqueViolation" in err_str or "uq_scene_class_name" in err_str: + api_logger.warning(f"Duplicate class name in scene {request.scene_id}") + from app.core.language_utils import get_language_from_header + from fastapi.responses import JSONResponse + lang = get_language_from_header(x_language_type) + class_name = request.classes[0].class_name if request.classes else "" + if lang == "en": + msg = fail(BizCode.BAD_REQUEST, "Class name already exists", f"A class named \"{class_name}\" already exists in this scene. Please use a different name.") + else: + msg = fail(BizCode.BAD_REQUEST, "类型名称已存在", f"当前场景下已存在名为「{class_name}」的类型,请使用其他名称") + return JSONResponse(status_code=400, content=msg) + api_logger.error(f"Runtime error in class creation: {err_str}", exc_info=True) + return fail(BizCode.INTERNAL_ERROR, "类型创建失败", err_str) except Exception as e: api_logger.error(f"Unexpected error in class creation: {str(e)}", exc_info=True) diff --git a/api/app/models/memory_config_model.py b/api/app/models/memory_config_model.py index 1095a386..b4b441d5 100644 --- a/api/app/models/memory_config_model.py +++ b/api/app/models/memory_config_model.py @@ -1,6 +1,6 @@ import datetime -from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String +from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from app.db import Base @@ -9,6 +9,9 @@ from app.db import Base class MemoryConfig(Base): """记忆配置表 - 用于存储记忆系统的配置参数""" __tablename__ = "memory_config" + __table_args__ = ( + UniqueConstraint('workspace_id', 'config_name', name='uq_workspace_config_name'), + ) # 主键 config_id = Column(UUID(as_uuid=True), primary_key=True, comment="配置ID") diff --git a/api/app/models/ontology_class.py b/api/app/models/ontology_class.py index a8468090..eb38d06f 100644 --- a/api/app/models/ontology_class.py +++ b/api/app/models/ontology_class.py @@ -9,7 +9,7 @@ Classes: import datetime import uuid -from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean +from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from app.db import Base @@ -18,6 +18,9 @@ from app.db import Base class OntologyClass(Base): """本体类型表 - 用于存储某个场景提取出来的本体类型信息""" __tablename__ = "ontology_class" + __table_args__ = ( + UniqueConstraint('scene_id', 'class_name', name='uq_scene_class_name'), + ) # 主键 class_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True, comment="类型ID") diff --git a/api/app/services/workspace_service.py b/api/app/services/workspace_service.py index 2f8cdc70..e93c0c5c 100644 --- a/api/app/services/workspace_service.py +++ b/api/app/services/workspace_service.py @@ -107,6 +107,7 @@ def get_user_workspaces(db: Session, user: User) -> List[Workspace]: for workspace in workspaces: if workspace.storage_type == 'neo4j': _ensure_default_memory_config(db, workspace) + _ensure_default_ontology_scenes(db, workspace) business_logger.info(f"用户 {user.username} 的工作空间数量: {len(workspaces)}") return workspaces @@ -1104,6 +1105,52 @@ def _fill_workspace_configs_model_defaults( ) +def _ensure_default_ontology_scenes(db: Session, workspace: Workspace) -> None: + """Ensure a workspace has default ontology scenes, creating them if missing. + + Checks whether any is_system_default scene exists for the workspace. + If not, runs the DefaultOntologyInitializer to create them. + + Args: + db: Database session + workspace: The workspace to check + """ + from app.models.ontology_scene import OntologyScene + + # 幂等检查:是否已存在系统默认场景 + existing = db.query(OntologyScene).filter( + OntologyScene.workspace_id == workspace.id, + OntologyScene.is_system_default.is_(True) + ).first() + + if existing: + return + + business_logger.info( + f"Workspace {workspace.id} missing default ontology scenes, creating them" + ) + + try: + initializer = DefaultOntologyInitializer(db) + success, error_msg = initializer.initialize_default_scenes( + workspace.id, language="zh" + ) + if success: + db.commit() + business_logger.info( + f"为工作空间 {workspace.id} 补建默认本体场景成功" + ) + else: + business_logger.warning( + f"为工作空间 {workspace.id} 补建默认本体场景失败: {error_msg}" + ) + except Exception as e: + db.rollback() + business_logger.error( + f"为工作空间 {workspace.id} 补建默认本体场景异常: {str(e)}" + ) + + def _create_default_memory_config( db: Session, workspace_id: uuid.UUID,