From 92c82c24a4faa6cd506eaddd48a1e266a5c3a7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Wed, 7 Jan 2026 14:47:25 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(skills):=20add=20skill=20file?= =?UTF-8?q?=20upload=20API=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new POST endpoint /api/v1/skills/upload for uploading skill zip files. The endpoint: - Accepts zip files with bot_id parameter - Validates file format (must be .zip) - Saves zip to projects/uploads/{bot_id}/skill_zip/ - Automatically extracts to projects/uploads/{bot_id}/skills/{skill_name}/ - Returns success response with file and extract paths This enables programmatic skill deployment for specific bots. --- routes/files.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/routes/files.py b/routes/files.py index e8f59a8..5da3c48 100644 --- a/routes/files.py +++ b/routes/files.py @@ -1,6 +1,7 @@ import os import uuid import shutil +import zipfile from datetime import datetime from typing import Optional, List from fastapi import APIRouter, HTTPException, Header, UploadFile, File, Form @@ -271,7 +272,7 @@ async def cleanup_project_async_endpoint(dataset_id: str, remove_all: bool = Fal async def upload_file(file: UploadFile = File(...), folder: Optional[str] = Form(None)): """ 文件上传API接口,上传文件到 ./projects/uploads/ 目录下 - + 可以指定自定义文件夹名,如果不指定则使用日期文件夹 指定文件夹时使用原始文件名并支持版本控制 @@ -286,18 +287,16 @@ async def upload_file(file: UploadFile = File(...), folder: Optional[str] = Form # 调试信息 logger.info(f"Received folder parameter: {folder}") logger.info(f"File received: {file.filename if file else 'None'}") - + # 确定上传文件夹 if folder: # 使用指定的自定义文件夹 target_folder = folder - # 安全性检查:防止路径遍历攻击 target_folder = os.path.basename(target_folder) else: # 获取当前日期并格式化为年月日 current_date = datetime.now() target_folder = current_date.strftime("%Y%m%d") - # 创建上传目录 upload_dir = os.path.join("projects", "uploads", target_folder) os.makedirs(upload_dir, exist_ok=True) @@ -312,7 +311,6 @@ async def upload_file(file: UploadFile = File(...), folder: Optional[str] = Form # 根据是否指定文件夹决定命名策略 if folder: - # 使用原始文件名,支持版本控制 final_filename, version = get_versioned_filename(upload_dir, name_without_ext, file_extension) file_path = os.path.join(upload_dir, final_filename) @@ -352,6 +350,82 @@ async def upload_file(file: UploadFile = File(...), folder: Optional[str] = Form raise HTTPException(status_code=500, detail=f"文件上传失败: {str(e)}") +@router.post("/api/v1/skills/upload") +async def upload_skills(file: UploadFile = File(...), bot_id: Optional[str] = Form(None)): + """ + Skill文件上传API接口,上传zip文件到 ./projects/uploads/ 目录下并自动解压 + + 如果folder参数包含"skills"且文件是.zip格式,保存后会自动解压到同名目录 + + Args: + file: 上传的zip文件 + folder: 可选的自定义文件夹名(如 "skills" 或 "projects/uploads/xxx/skills") + + Returns: + dict: 包含文件路径、解压信息的响应 + """ + try: + # 调试信息 + logger.info(f"Skill upload - bot_id parameter: {bot_id}") + logger.info(f"File received: {file.filename if file else 'None'}") + + # 验证文件名 + if not file.filename: + raise HTTPException(status_code=400, detail="文件名不能为空") + + # 验证是否为zip文件 + original_filename = file.filename + name_without_ext, file_extension = os.path.splitext(original_filename) + + if file_extension.lower() != '.zip': + raise HTTPException(status_code=400, detail="仅支持上传.zip格式的skill文件") + + + folder_name = name_without_ext + # 创建上传目录 + upload_dir = os.path.join("projects", "uploads", bot_id, "skill_zip") + extract_target = os.path.join("projects", "uploads", bot_id, "skills", folder_name) + os.makedirs(extract_target, exist_ok=True) + os.makedirs(upload_dir, exist_ok=True) + try: + # 保存zip文件(使用原始文件名) + file_path = os.path.join(upload_dir, original_filename) + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + logger.info(f"Saved zip file: {file_path}") + with zipfile.ZipFile(file_path, 'r') as zip_ref: + zip_ref.extractall(extract_target) + logger.info(f"Extracted to: {extract_target}") + + return { + "success": True, + "message": f"Skill文件上传并解压成功", + "file_path": file_path, + "extract_path": extract_target, + "original_filename": original_filename, + "skill_name": folder_name + } + + except zipfile.BadZipFile: + # 解压失败,删除已保存的zip文件 + if os.path.exists(file_path): + os.remove(file_path) + raise HTTPException(status_code=400, detail="上传的文件不是有效的zip文件") + except Exception as e: + # 解压失败,删除已保存的zip文件 + if os.path.exists(file_path): + os.remove(file_path) + logger.error(f"解压失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"解压失败: {str(e)}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error uploading skill file: {str(e)}") + raise HTTPException(status_code=500, detail=f"Skill文件上传失败: {str(e)}") + + # Task management routes that are related to file processing @router.get("/api/v1/task/{task_id}/status") async def get_task_status(task_id: str):