Merge pull request #522 from SuanmoSuanyangTechnology/fix/bug-patch

feat(workspace, app, agent): add duplicate name validation and restrict model/memory config on agent publish
This commit is contained in:
Mark
2026-03-10 11:31:54 +08:00
committed by GitHub
5 changed files with 233 additions and 160 deletions

View File

@@ -1,7 +1,6 @@
import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Annotated, Any, Dict, Optional from typing import Annotated, Optional
from dotenv import load_dotenv from dotenv import load_dotenv
from pydantic import Field, TypeAdapter from pydantic import Field, TypeAdapter

View File

@@ -1,10 +1,11 @@
from sqlalchemy.orm import Session
from typing import List, Optional
import uuid import uuid
from typing import List
from app.models.app_model import App from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.logging_config import get_db_logger from app.core.logging_config import get_db_logger
from app.models.app_model import App
# 获取数据库专用日志器 # 获取数据库专用日志器
db_logger = get_db_logger() db_logger = get_db_logger()
@@ -35,11 +36,27 @@ class AppRepository:
except Exception as e: except Exception as e:
raise raise
def get_apps_by_name(self, app_name: str, app_type: str, workspace_id: uuid.UUID) -> List[App]:
try:
stmt = select(App).where(
App.name == app_name,
App.workspace_id == workspace_id,
App.type == app_type,
App.is_active.is_(True),
)
apps = self.db.execute(stmt).scalars().all()
return list(apps)
except Exception as e:
db_logger.error(f"查询名称 {app_name} 应用异常: {str(e)}")
raise
def get_apps_by_workspace_id(db: Session, workspace_id: uuid.UUID) -> List[App]: def get_apps_by_workspace_id(db: Session, workspace_id: uuid.UUID) -> List[App]:
"""根据工作空间ID查询应用""" """根据工作空间ID查询应用"""
repo = AppRepository(db) repo = AppRepository(db)
return repo.get_apps_by_workspace_id(workspace_id) return repo.get_apps_by_workspace_id(workspace_id)
def get_apps_by_id(db: Session, app_id: uuid.UUID) -> App: def get_apps_by_id(db: Session, app_id: uuid.UUID) -> App:
"""根据工作空间ID查询应用""" """根据工作空间ID查询应用"""
repo = AppRepository(db) repo = AppRepository(db)

View File

@@ -1,10 +1,13 @@
from sqlalchemy.orm import Session, joinedload
from app.models.user_model import User
from typing import List, Optional
import uuid import uuid
from app.models.workspace_model import Workspace, WorkspaceMember, WorkspaceRole from typing import List, Optional
from app.schemas.workspace_schema import WorkspaceCreate, WorkspaceUpdate
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import select
from app.core.logging_config import get_db_logger from app.core.logging_config import get_db_logger
from app.models.user_model import User
from app.models.workspace_model import Workspace, WorkspaceMember, WorkspaceRole
from app.schemas.workspace_schema import WorkspaceCreate
# 获取数据库专用日志器 # 获取数据库专用日志器
db_logger = get_db_logger() db_logger = get_db_logger()
@@ -34,7 +37,8 @@ class WorkspaceRepository:
) )
self.db.add(db_workspace) self.db.add(db_workspace)
self.db.flush() self.db.flush()
db_logger.info(f"工作空间记录创建成功: {workspace_data.name} (ID: {db_workspace.id}), storage_type: {workspace_data.storage_type}") db_logger.info(
f"工作空间记录创建成功: {workspace_data.name} (ID: {db_workspace.id}), storage_type: {workspace_data.storage_type}")
return db_workspace return db_workspace
except Exception as e: except Exception as e:
db_logger.error(f"创建工作空间记录失败: name={workspace_data.name} - {str(e)}") db_logger.error(f"创建工作空间记录失败: name={workspace_data.name} - {str(e)}")
@@ -144,7 +148,25 @@ class WorkspaceRepository:
db_logger.error(f"查询租户工作空间失败: tenant_id={tenant_id} - {str(e)}") db_logger.error(f"查询租户工作空间失败: tenant_id={tenant_id} - {str(e)}")
raise raise
def add_member(self, workspace_id: uuid.UUID, user_id: uuid.UUID, role: WorkspaceRole = WorkspaceRole.member) -> WorkspaceMember: def get_workspaces_by_name(self, tenant_id: uuid.UUID, workspace_name: str) -> List[Workspace]:
try:
stmt = (
select(Workspace)
.where(
Workspace.tenant_id == tenant_id,
Workspace.name == workspace_name,
Workspace.is_active.is_(True)
)
)
workspaces = self.db.execute(stmt).scalars().all()
return list(workspaces)
except Exception as e:
db_logger.error(f"查询工作空间失败: workspace_name={workspace_name} - {str(e)}")
raise
def add_member(self, workspace_id: uuid.UUID, user_id: uuid.UUID,
role: WorkspaceRole = WorkspaceRole.member) -> WorkspaceMember:
"""添加工作空间成员""" """添加工作空间成员"""
db_logger.debug(f"添加工作空间成员: user_id={user_id}, workspace_id={workspace_id}, role={role}") db_logger.debug(f"添加工作空间成员: user_id={user_id}, workspace_id={workspace_id}, role={role}")
@@ -173,7 +195,8 @@ class WorkspaceRepository:
WorkspaceMember.is_active.is_(True), WorkspaceMember.is_active.is_(True),
).first() ).first()
if member: if member:
db_logger.debug(f"工作空间成员查询成功: user_id={user_id}, workspace_id={workspace_id}, role={member.role}") db_logger.debug(
f"工作空间成员查询成功: user_id={user_id}, workspace_id={workspace_id}, role={member.role}")
else: else:
db_logger.debug(f"工作空间成员不存在: user_id={user_id}, workspace_id={workspace_id}") db_logger.debug(f"工作空间成员不存在: user_id={user_id}, workspace_id={workspace_id}")
return member return member
@@ -214,7 +237,8 @@ class WorkspaceRepository:
.first() .first()
) )
if member: if member:
db_logger.debug(f"成员查询成功: member_id={member_id}, workspace_id={member.workspace_id}, role={member.role}") db_logger.debug(
f"成员查询成功: member_id={member_id}, workspace_id={member.workspace_id}, role={member.role}")
else: else:
db_logger.debug(f"成员不存在: member_id={member_id}") db_logger.debug(f"成员不存在: member_id={member_id}")
return member return member
@@ -222,7 +246,8 @@ class WorkspaceRepository:
db_logger.error(f"查询成员列表失败: member_id={member_id} - {str(e)}") db_logger.error(f"查询成员列表失败: member_id={member_id} - {str(e)}")
raise raise
def update_member_role(self, workspace_id: uuid.UUID, user_id: uuid.UUID, role: WorkspaceRole) -> Optional[WorkspaceMember]: def update_member_role(self, workspace_id: uuid.UUID, user_id: uuid.UUID, role: WorkspaceRole) -> Optional[
WorkspaceMember]:
try: try:
member = self.db.query(WorkspaceMember).filter( member = self.db.query(WorkspaceMember).filter(
WorkspaceMember.workspace_id == workspace_id, WorkspaceMember.workspace_id == workspace_id,
@@ -288,12 +313,18 @@ class WorkspaceRepository:
db_logger.error(f"更新成员角色失败: id={id} - {str(e)}") db_logger.error(f"更新成员角色失败: id={id} - {str(e)}")
raise raise
# 保持向后兼容的函数 # 保持向后兼容的函数
def get_workspace_by_id(db: Session, workspace_id: uuid.UUID) -> Workspace | None: def get_workspace_by_id(db: Session, workspace_id: uuid.UUID) -> Workspace | None:
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.get_workspace_by_id(workspace_id) return repo.get_workspace_by_id(workspace_id)
def get_workspaces_by_name(db: Session, tenant_id: uuid.UUID, name: str) -> List[Workspace]:
repo = WorkspaceRepository(db)
return repo.get_workspaces_by_name(tenant_id, name)
def get_workspaces_by_user(db: Session, user_id: uuid.UUID) -> List[Workspace]: def get_workspaces_by_user(db: Session, user_id: uuid.UUID) -> List[Workspace]:
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.get_workspaces_by_user(user_id) return repo.get_workspaces_by_user(user_id)
@@ -325,10 +356,12 @@ def get_members_by_workspace(db: Session, workspace_id: uuid.UUID) -> List[Works
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.get_members_by_workspace(workspace_id) return repo.get_members_by_workspace(workspace_id)
def get_member_by_id(db: Session, member_id: uuid.UUID) -> WorkspaceMember | None: def get_member_by_id(db: Session, member_id: uuid.UUID) -> WorkspaceMember | None:
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.get_member_by_id(member_id) return repo.get_member_by_id(member_id)
def update_member_role_in_workspace( def update_member_role_in_workspace(
db: Session, db: Session,
user_id: uuid.UUID, user_id: uuid.UUID,
@@ -338,6 +371,7 @@ def update_member_role_in_workspace(
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.update_member_role(workspace_id, user_id, role) return repo.update_member_role(workspace_id, user_id, role)
def remove_member_from_workspace( def remove_member_from_workspace(
db: Session, db: Session,
user_id: uuid.UUID, user_id: uuid.UUID,
@@ -346,6 +380,7 @@ def remove_member_from_workspace(
repo = WorkspaceRepository(db) repo = WorkspaceRepository(db)
return repo.deactivate_member(workspace_id, user_id) return repo.deactivate_member(workspace_id, user_id)
def remove_member_from_workspace_by_id( def remove_member_from_workspace_by_id(
db: Session, db: Session,
member_id: uuid.UUID, member_id: uuid.UUID,

View File

@@ -33,7 +33,7 @@ from app.models import (
Workspace, Workspace,
) )
from app.models.app_model import AppStatus, AppType from app.models.app_model import AppStatus, AppType
from app.repositories.app_repository import get_apps_by_id from app.repositories.app_repository import get_apps_by_id, AppRepository
from app.repositories.workflow_repository import WorkflowConfigRepository from app.repositories.workflow_repository import WorkflowConfigRepository
from app.schemas import app_schema from app.schemas import app_schema
from app.schemas.workflow_schema import WorkflowConfigUpdate from app.schemas.workflow_schema import WorkflowConfigUpdate
@@ -59,6 +59,7 @@ class AppService:
db: 数据库会话 db: 数据库会话
""" """
self.db = db self.db = db
self.app_repo = AppRepository(self.db)
# ==================== 私有辅助方法 ==================== # ==================== 私有辅助方法 ====================
@@ -521,6 +522,9 @@ class AppService:
"创建应用", "创建应用",
extra={"app_name": data.name, "type": data.type, "workspace_id": str(workspace_id)} extra={"app_name": data.name, "type": data.type, "workspace_id": str(workspace_id)}
) )
apps = self.app_repo.get_apps_by_name(data.name, data.type, workspace_id)
if apps:
raise BusinessException(message="已存在同名应用", code=BizCode.RESOURCE_ALREADY_EXISTS)
try: try:
now = datetime.datetime.now() now = datetime.datetime.now()
@@ -1368,6 +1372,15 @@ class AppService:
if not agent_cfg: if not agent_cfg:
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING) raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
miss_params = []
if agent_cfg.default_model_config_id is None:
miss_params.append("model config")
if agent_cfg.memory.get("enabled") and not agent_cfg.memory.get("memory_config_id"):
miss_params.append("memory config")
if miss_params:
raise BusinessException(f"{', '.join(miss_params)} is required")
config = { config = {
"system_prompt": agent_cfg.system_prompt, "system_prompt": agent_cfg.system_prompt,
"model_parameters": model_parameters_to_dict(agent_cfg.model_parameters), "model_parameters": model_parameters_to_dict(agent_cfg.model_parameters),

View File

@@ -2,11 +2,11 @@ import datetime
import hashlib import hashlib
import secrets import secrets
import uuid import uuid
from os import getenv
from typing import List, Optional from typing import List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.config.default_ontology_initializer import DefaultOntologyInitializer
from app.core.config import settings from app.core.config import settings
from app.core.error_codes import BizCode from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException, PermissionDeniedException from app.core.exceptions import BusinessException, PermissionDeniedException
@@ -30,13 +30,11 @@ from app.schemas.workspace_schema import (
WorkspaceModelsUpdate, WorkspaceModelsUpdate,
WorkspaceUpdate, WorkspaceUpdate,
) )
from app.config.default_ontology_initializer import DefaultOntologyInitializer
# 获取业务逻辑专用日志器 # 获取业务逻辑专用日志器
business_logger = get_business_logger() business_logger = get_business_logger()
from dotenv import load_dotenv
load_dotenv()
def switch_workspace( def switch_workspace(
db: Session, db: Session,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
@@ -65,7 +63,7 @@ def delete_workspace_member(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
member_id: uuid.UUID, member_id: uuid.UUID,
user: User, user: User,
): ):
"""删除工作空间成员""" """删除工作空间成员"""
business_logger.debug(f"用户 {user.username} 请求删除工作空间 {workspace_id} 的成员 {member_id}") business_logger.debug(f"用户 {user.username} 请求删除工作空间 {workspace_id} 的成员 {member_id}")
_check_workspace_admin_permission(db, workspace_id, user) _check_workspace_admin_permission(db, workspace_id, user)
@@ -74,7 +72,8 @@ def delete_workspace_member(
raise BusinessException(f"工作空间成员 {member_id} 不存在", BizCode.WORKSPACE_NOT_FOUND) raise BusinessException(f"工作空间成员 {member_id} 不存在", BizCode.WORKSPACE_NOT_FOUND)
if workspace_member.workspace_id != workspace_id: if workspace_member.workspace_id != workspace_id:
raise BusinessException(f"工作空间成员 {member_id} 不存在于工作空间 {workspace_id}", BizCode.WORKSPACE_NOT_FOUND) raise BusinessException(f"工作空间成员 {member_id} 不存在于工作空间 {workspace_id}",
BizCode.WORKSPACE_NOT_FOUND)
try: try:
workspace_member.is_active = False workspace_member.is_active = False
@@ -138,9 +137,14 @@ def create_workspace(
f"创建工作空间: {workspace.name}, 创建者: {user.username}, " f"创建工作空间: {workspace.name}, 创建者: {user.username}, "
f"storage_type: {workspace.storage_type}" f"storage_type: {workspace.storage_type}"
) )
llm=workspace.llm if workspace_repository.get_workspaces_by_name(db=db, name=workspace.name, tenant_id=user.tenant_id):
embedding=workspace.embedding raise BusinessException(
rerank=workspace.rerank message="同名工作空间已存在",
code=BizCode.RESOURCE_ALREADY_EXISTS
)
llm = workspace.llm
embedding = workspace.embedding
rerank = workspace.rerank
try: try:
# Create the workspace without adding any members # Create the workspace without adding any members
business_logger.debug(f"创建工作空间: {workspace.name}") business_logger.debug(f"创建工作空间: {workspace.name}")
@@ -256,10 +260,10 @@ def create_workspace(
avatar='', avatar='',
type=KnowledgeType.General, type=KnowledgeType.General,
permission_id=PermissionType.Memory, permission_id=PermissionType.Memory,
embedding_id=uuid.UUID(getenv('KB_embedding_id')) if None else embedding, embedding_id=embedding,
reranker_id=uuid.UUID(getenv('KB_reranker_id')) if None else rerank, reranker_id=rerank,
llm_id=uuid.UUID(getenv('KB_llm_id')) if None else llm, llm_id=llm,
image2text_id=uuid.UUID(getenv('KB_llm_id')) if None else llm, image2text_id=llm,
parser_config={ parser_config={
"layout_recognize": "DeepDOC", "layout_recognize": "DeepDOC",
"chunk_token_num": 256, "chunk_token_num": 256,
@@ -308,7 +312,7 @@ def update_workspace(
) -> Workspace: ) -> Workspace:
business_logger.info(f"更新工作空间: workspace_id={workspace_id}, 操作者: {user.username}") business_logger.info(f"更新工作空间: workspace_id={workspace_id}, 操作者: {user.username}")
db_workspace = _check_workspace_admin_permission(db,workspace_id,user) db_workspace = _check_workspace_admin_permission(db, workspace_id, user)
try: try:
# 更新工作空间 # 更新工作空间
business_logger.debug(f"执行工作空间更新: {db_workspace.name} (ID: {workspace_id})") business_logger.debug(f"执行工作空间更新: {db_workspace.name} (ID: {workspace_id})")
@@ -372,7 +376,6 @@ def get_workspace_members(
return members return members
# ==================== 邀请相关服务方法 ==================== # ==================== 邀请相关服务方法 ====================
def _generate_invite_token() -> tuple[str, str]: def _generate_invite_token() -> tuple[str, str]:
@@ -471,7 +474,8 @@ def create_workspace_invite(
user: User user: User
) -> WorkspaceInviteResponse: ) -> WorkspaceInviteResponse:
"""创建工作空间邀请""" """创建工作空间邀请"""
business_logger.info(f"创建工作空间邀请: workspace_id={workspace_id}, email={invite_data.email}, 创建者: {user.username}") business_logger.info(
f"创建工作空间邀请: workspace_id={workspace_id}, email={invite_data.email}, 创建者: {user.username}")
try: try:
# 检查权限 # 检查权限
@@ -534,7 +538,8 @@ def create_workspace_invite(
except Exception as e: except Exception as e:
db.rollback() db.rollback()
business_logger.error(f"创建工作空间邀请失败: workspace_id={workspace_id}, email={invite_data.email} - {str(e)}") business_logger.error(
f"创建工作空间邀请失败: workspace_id={workspace_id}, email={invite_data.email} - {str(e)}")
raise raise
@@ -695,7 +700,8 @@ def accept_workspace_invite(
# 获取工作空间信息 # 获取工作空间信息
workspace = workspace_repository.get_workspace_by_id(db=db, workspace_id=invite.workspace_id) workspace = workspace_repository.get_workspace_by_id(db=db, workspace_id=invite.workspace_id)
business_logger.info(f"用户成功加入工作空间: user={user.username}, workspace={workspace.name}, role={workspace_role}") business_logger.info(
f"用户成功加入工作空间: user={user.username}, workspace={workspace.name}, role={workspace_role}")
return { return {
"message": "Successfully joined the workspace", "message": "Successfully joined the workspace",
@@ -716,7 +722,8 @@ def revoke_workspace_invite(
user: User user: User
) -> dict: ) -> dict:
"""撤销工作空间邀请""" """撤销工作空间邀请"""
business_logger.info(f"撤销工作空间邀请: workspace_id={workspace_id}, invite_id={invite_id}, 操作者: {user.username}") business_logger.info(
f"撤销工作空间邀请: workspace_id={workspace_id}, invite_id={invite_id}, 操作者: {user.username}")
try: try:
# 检查权限 # 检查权限
@@ -751,7 +758,8 @@ def update_workspace_member_roles(
user: User, user: User,
) -> List[WorkspaceMember]: ) -> List[WorkspaceMember]:
"""更新工作空间成员角色""" """更新工作空间成员角色"""
business_logger.info(f"更新工作空间成员角色: workspace_id={workspace_id}, 操作者: {user.username}, 更新数量: {len(updates)}") business_logger.info(
f"更新工作空间成员角色: workspace_id={workspace_id}, 操作者: {user.username}, 更新数量: {len(updates)}")
# 检查管理员权限 # 检查管理员权限
_check_workspace_admin_permission(db, workspace_id, user) _check_workspace_admin_permission(db, workspace_id, user)
@@ -765,7 +773,8 @@ def update_workspace_member_roles(
for upd in updates: for upd in updates:
# 检查成员是否存在 # 检查成员是否存在
if upd.id not in member_map: if upd.id not in member_map:
raise BusinessException(f"成员 {upd.id} 不存在于工作空间 {workspace_id}", BizCode.WORKSPACE_MEMBER_NOT_FOUND) raise BusinessException(f"成员 {upd.id} 不存在于工作空间 {workspace_id}",
BizCode.WORKSPACE_MEMBER_NOT_FOUND)
member = member_map[upd.id] member = member_map[upd.id]
@@ -1084,6 +1093,7 @@ def _create_default_memory_config(
} }
) )
# ==================== 检查配置相关服务 ==================== # ==================== 检查配置相关服务 ====================
def _ensure_default_memory_config(db: Session, workspace: Workspace) -> None: def _ensure_default_memory_config(db: Session, workspace: Workspace) -> None:
@@ -1209,4 +1219,3 @@ def _ensure_default_ontology_scenes(db: Session, workspace: Workspace) -> None:
business_logger.error( business_logger.error(
f"为工作空间 {workspace.id} 补建默认本体场景异常: {str(e)}" f"为工作空间 {workspace.id} 补建默认本体场景异常: {str(e)}"
) )