diff --git a/routes/skill_manager.py b/routes/skill_manager.py index 7d26276..30bb197 100644 --- a/routes/skill_manager.py +++ b/routes/skill_manager.py @@ -50,6 +50,18 @@ def validate_bot_id(bot_id: str) -> str: return bot_id +def validate_skill_name(skill_name: str) -> str: + """验证 skill_name 格式,防止路径遍历攻击""" + if not skill_name: + raise HTTPException(status_code=400, detail="skill_name 不能为空") + + # 检查路径遍历字符 + if '..' in skill_name or '/' in skill_name or '\\' in skill_name: + raise HTTPException(status_code=400, detail="skill_name 包含非法字符") + + return skill_name + + async def validate_upload_file_size(file: UploadFile) -> int: """验证上传文件大小,返回实际文件大小""" file_size = 0 @@ -411,3 +423,70 @@ async def upload_skill(file: UploadFile = File(...), bot_id: Optional[str] = For logger.error(f"Error uploading skill file: {str(e)}") # 不暴露详细错误信息给客户端(安全考虑) raise HTTPException(status_code=500, detail="Skill文件上传失败") + + +@router.delete("/api/v1/skill/remove") +async def remove_skill( + bot_id: str = Query(..., description="Bot ID"), + skill_name: str = Query(..., description="Skill name to remove") +): + """ + 删除用户上传的 skill + + Args: + bot_id: Bot ID + skill_name: 要删除的 skill 名称 + + Returns: + dict: 删除结果 + + Notes: + - 只能删除用户上传的 skills,不能删除官方 skills + - 删除路径: projects/uploads/{bot_id}/skills/{skill_name} + """ + try: + # 验证参数(防止路径遍历攻击) + bot_id = validate_bot_id(bot_id) + skill_name = validate_skill_name(skill_name) + + logger.info(f"Skill remove - bot_id: {bot_id}, skill_name: {skill_name}") + + # 构建删除目录路径 + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + skill_dir = os.path.join(base_dir, "projects", "uploads", bot_id, "skills", skill_name) + + # 规范化路径并确保在允许的目录内 + skill_dir_real = os.path.realpath(skill_dir) + allowed_base = os.path.realpath(os.path.join(base_dir, "projects", "uploads", bot_id, "skills")) + + if not skill_dir_real.startswith(allowed_base + os.sep): + raise HTTPException(status_code=403, detail="非法的删除路径") + + # 检查目录是否存在 + if not os.path.exists(skill_dir_real): + raise HTTPException(status_code=404, detail="Skill 不存在") + + # 检查是否为目录 + if not os.path.isdir(skill_dir_real): + raise HTTPException(status_code=400, detail="目标路径不是目录") + + # 使用线程池删除目录(避免阻塞事件循环) + await asyncio.to_thread(shutil.rmtree, skill_dir_real) + + logger.info(f"Successfully removed skill directory: {skill_dir_real}") + + return { + "success": True, + "message": f"Skill '{skill_name}' 删除成功", + "bot_id": bot_id, + "skill_name": skill_name + } + + except HTTPException: + raise + except Exception as e: + import traceback + error_details = traceback.format_exc() + logger.error(f"Error removing skill: {str(e)}") + logger.error(f"Full traceback: {error_details}") + raise HTTPException(status_code=500, detail="删除 Skill 失败")