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:
朱潮 2026-01-07 14:47:25 +08:00
parent 1233bdda0c
commit 92c82c24a4

View File

@ -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):