✨ feat(skills): add skill file upload API endpoint
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.
This commit is contained in:
parent
1233bdda0c
commit
92c82c24a4
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user