"""租户工具配置路由""" import json from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from typing import Optional, List, Dict, Any from sqlalchemy.orm import Session from sqlalchemy import or_ from ..database import get_db from ..models.tool_config import ToolConfig from .auth import get_current_user from ..models.user import User router = APIRouter(prefix="/tool-configs", tags=["工具配置"]) # ======================================== # Schemas # ======================================== class ToolConfigCreate(BaseModel): """创建配置""" tenant_id: str tool_code: Optional[str] = None # NULL 表示租户级共享配置 config_type: str # datasource / jssdk / webhook / params config_key: str config_value: Optional[str] = None is_encrypted: int = 0 description: Optional[str] = None class ToolConfigUpdate(BaseModel): """更新配置""" config_value: Optional[str] = None is_encrypted: Optional[int] = None description: Optional[str] = None status: Optional[int] = None class ToolConfigBatchCreate(BaseModel): """批量创建配置""" tenant_id: str tool_code: Optional[str] = None configs: List[Dict[str, Any]] # [{config_type, config_key, config_value, description}] # ======================================== # 工具函数 # ======================================== def format_config(config: ToolConfig, mask_secret: bool = True) -> dict: """格式化配置输出""" value = config.config_value # 如果需要掩码且是加密字段 if mask_secret and config.is_encrypted and value: value = value[:4] + "****" + value[-4:] if len(value) > 8 else "****" return { "id": config.id, "tenant_id": config.tenant_id, "tool_code": config.tool_code, "config_type": config.config_type, "config_key": config.config_key, "config_value": value, "is_encrypted": config.is_encrypted, "description": config.description, "status": config.status, "created_at": config.created_at.isoformat() if config.created_at else None, "updated_at": config.updated_at.isoformat() if config.updated_at else None } # ======================================== # API Endpoints # ======================================== @router.get("") async def list_tool_configs( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), tenant_id: Optional[str] = None, tool_code: Optional[str] = None, config_type: Optional[str] = None, keyword: Optional[str] = None, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """获取配置列表""" query = db.query(ToolConfig).filter(ToolConfig.status == 1) if tenant_id: query = query.filter(ToolConfig.tenant_id == tenant_id) if tool_code: if tool_code == "__shared__": # 特殊标记:只查租户级共享配置 query = query.filter(ToolConfig.tool_code.is_(None)) else: query = query.filter(ToolConfig.tool_code == tool_code) if config_type: query = query.filter(ToolConfig.config_type == config_type) if keyword: query = query.filter( or_( ToolConfig.config_key.like(f"%{keyword}%"), ToolConfig.description.like(f"%{keyword}%") ) ) total = query.count() configs = query.order_by(ToolConfig.tool_code, ToolConfig.config_type, ToolConfig.config_key)\ .offset((page - 1) * size).limit(size).all() return { "total": total, "page": page, "size": size, "items": [format_config(c) for c in configs] } @router.get("/tenant/{tenant_id}") async def get_tenant_configs( tenant_id: str, tool_code: Optional[str] = None, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 获取租户完整配置(合并层级) 返回结构: { "__shared__": {"config_key": "value", ...}, # 租户级共享配置 "customer-profile": {"config_key": "value", ...}, # 工具级配置 ... } """ query = db.query(ToolConfig).filter( ToolConfig.tenant_id == tenant_id, ToolConfig.status == 1 ) if tool_code: # 只查指定工具 + 共享配置 query = query.filter( or_( ToolConfig.tool_code == tool_code, ToolConfig.tool_code.is_(None) ) ) configs = query.all() result = {"__shared__": {}} for config in configs: tool = config.tool_code or "__shared__" if tool not in result: result[tool] = {} result[tool][config.config_key] = { "value": config.config_value, "type": config.config_type, "encrypted": config.is_encrypted == 1, "description": config.description } return result @router.get("/merged/{tenant_id}/{tool_code}") async def get_merged_config( tenant_id: str, tool_code: str, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 获取合并后的工具配置(工具级 > 租户级 > 默认值) 返回扁平化的配置字典: {"config_key": "value", ...} """ # 查询租户级配置 shared_configs = db.query(ToolConfig).filter( ToolConfig.tenant_id == tenant_id, ToolConfig.tool_code.is_(None), ToolConfig.status == 1 ).all() # 查询工具级配置 tool_configs = db.query(ToolConfig).filter( ToolConfig.tenant_id == tenant_id, ToolConfig.tool_code == tool_code, ToolConfig.status == 1 ).all() # 合并:工具级覆盖租户级 result = {} for config in shared_configs: result[config.config_key] = config.config_value for config in tool_configs: result[config.config_key] = config.config_value return result @router.get("/{config_id}") async def get_tool_config( config_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """获取配置详情""" config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() if not config: raise HTTPException(status_code=404, detail="配置不存在") return format_config(config, mask_secret=False) @router.post("") async def create_tool_config( data: ToolConfigCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """创建配置""" # 检查是否已存在 existing = db.query(ToolConfig).filter( ToolConfig.tenant_id == data.tenant_id, ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None), ToolConfig.config_key == data.config_key ).first() if existing: raise HTTPException(status_code=400, detail="配置已存在") config = ToolConfig( tenant_id=data.tenant_id, tool_code=data.tool_code, config_type=data.config_type, config_key=data.config_key, config_value=data.config_value, is_encrypted=data.is_encrypted, description=data.description ) db.add(config) db.commit() db.refresh(config) return format_config(config) @router.post("/batch") async def batch_create_configs( data: ToolConfigBatchCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """批量创建配置""" created = [] skipped = [] for item in data.configs: config_key = item.get("config_key") if not config_key: continue # 检查是否已存在 existing = db.query(ToolConfig).filter( ToolConfig.tenant_id == data.tenant_id, ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None), ToolConfig.config_key == config_key ).first() if existing: skipped.append(config_key) continue config = ToolConfig( tenant_id=data.tenant_id, tool_code=data.tool_code, config_type=item.get("config_type", "params"), config_key=config_key, config_value=item.get("config_value"), is_encrypted=item.get("is_encrypted", 0), description=item.get("description") ) db.add(config) created.append(config_key) db.commit() return { "created": created, "skipped": skipped, "message": f"成功创建 {len(created)} 条配置,跳过 {len(skipped)} 条已存在配置" } @router.put("/{config_id}") async def update_tool_config( config_id: int, data: ToolConfigUpdate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """更新配置""" config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() if not config: raise HTTPException(status_code=404, detail="配置不存在") if data.config_value is not None: config.config_value = data.config_value if data.is_encrypted is not None: config.is_encrypted = data.is_encrypted if data.description is not None: config.description = data.description if data.status is not None: config.status = data.status db.commit() db.refresh(config) return format_config(config) @router.delete("/{config_id}") async def delete_tool_config( config_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """删除配置(软删除)""" config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() if not config: raise HTTPException(status_code=404, detail="配置不存在") config.status = 0 db.commit() return {"message": "删除成功"} # ======================================== # 配置类型和键名定义(供前端使用) # ======================================== @router.get("/schema/types") async def get_config_types(): """获取支持的配置类型""" return { "types": [ {"code": "datasource", "name": "数据源配置", "description": "数据库连接等"}, {"code": "jssdk", "name": "JS-SDK 配置", "description": "企微侧边栏等"}, {"code": "webhook", "name": "Webhook 配置", "description": "n8n 工作流地址等"}, {"code": "params", "name": "工具参数", "description": "各工具的自定义参数"} ] } @router.get("/schema/keys") async def get_config_keys(): """获取预定义的配置键(供前端下拉选择)""" return { "datasource": [ {"key": "scrm_db_host", "name": "SCRM 数据库地址", "encrypted": False}, {"key": "scrm_db_port", "name": "SCRM 数据库端口", "encrypted": False}, {"key": "scrm_db_user", "name": "SCRM 数据库用户", "encrypted": False}, {"key": "scrm_db_password", "name": "SCRM 数据库密码", "encrypted": True}, {"key": "scrm_db_name", "name": "SCRM 数据库名", "encrypted": False} ], "jssdk": [ {"key": "corp_id", "name": "企业ID", "encrypted": False}, {"key": "agent_id", "name": "应用ID", "encrypted": False}, {"key": "secret", "name": "应用密钥", "encrypted": True} ], "webhook": [ {"key": "n8n_base_url", "name": "n8n 基础地址", "encrypted": False}, {"key": "webhook_brainstorm", "name": "头脑风暴 Webhook", "encrypted": False}, {"key": "webhook_high_eq", "name": "高情商回复 Webhook", "encrypted": False}, {"key": "webhook_customer_profile", "name": "客户画像 Webhook", "encrypted": False}, {"key": "webhook_consultation", "name": "面诊方案 Webhook", "encrypted": False}, {"key": "webhook_medical_compliance", "name": "医疗合规 Webhook", "encrypted": False} ], "params": [ {"key": "default_data_tenant_id", "name": "默认数据租户ID", "encrypted": False}, {"key": "enable_deep_thinking", "name": "启用深度思考", "encrypted": False}, {"key": "max_history_rounds", "name": "最大历史轮数", "encrypted": False} ] }