VWED_server/routes/script_api.py

552 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本管理API路由
提供脚本项目和文件的CRUD操作接口
"""
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from fastapi.responses import StreamingResponse
from typing import Optional, Dict, Any
from pydantic import BaseModel
from io import BytesIO
from urllib.parse import quote
import os
from services.online_script.script_file_service import get_file_service
from services.online_script.script_engine_service import get_script_engine
from utils.api_response import success_response, error_response
from utils.logger import get_logger
logger = get_logger("routes.script_api")
router = APIRouter(prefix="/api/script", tags=["在线脚本模块"])
# 请求模型定义
class ProjectCreateRequest(BaseModel):
project_name: str
description: Optional[str] = ""
created_by: Optional[str] = None
class FileCreateRequest(BaseModel):
project_id: int
file_name: str
file_path: Optional[str] = None
content: Optional[str] = "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nasync def boot():\n \"\"\"脚本启动函数\"\"\"\n pass"
file_type: Optional[str] = "python"
created_by: Optional[str] = None
class FileUpdateRequest(BaseModel):
content: str
updated_by: Optional[str] = None
class ScriptStartRequest(BaseModel):
script_id: int
start_params: Optional[Dict[str, Any]] = None
class FunctionCallRequest(BaseModel):
script_id: str
function_name: str
function_args: Any = None
@router.post("/projects", summary="创建脚本项目")
async def create_project(request: ProjectCreateRequest):
"""创建新的脚本项目"""
try:
file_service = get_file_service()
result = await file_service.create_project(
project_name=request.project_name,
description=request.description,
created_by=request.created_by
)
if result["success"]:
# return result
return success_response(data=result["project"], message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"创建项目失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"创建项目失败: {str(e)}")
@router.get("/projects", summary="获取项目列表")
async def get_projects(status: Optional[str] = "active"):
"""获取所有脚本项目"""
try:
file_service = get_file_service()
result = await file_service.get_projects(status=status)
if result["success"]:
return success_response(data=result["projects"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"获取项目列表失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取项目列表失败: {str(e)}")
@router.get("/projects/{project_id}/files", summary="获取项目文件列表")
async def get_project_files(project_id: int, include_content: bool = False):
"""获取指定项目的所有文件"""
try:
file_service = get_file_service()
result = await file_service.get_project_files(
project_id=project_id,
include_content=include_content
)
if result["success"]:
return success_response(data=result)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"获取项目文件列表失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取项目文件列表失败: {str(e)}")
@router.post("/files", summary="创建脚本文件")
async def create_file(request: FileCreateRequest):
"""创建新的脚本文件"""
try:
file_service = get_file_service()
# 如果file_path为空使用file_name作为默认路径
file_path = request.file_path if request.file_path else request.file_name
result = await file_service.create_file(
project_id=request.project_id,
file_name=request.file_name,
file_path=file_path,
content=request.content,
file_type=request.file_type,
created_by=request.created_by
)
if result["success"]:
return success_response(data=result["file"], message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"创建文件失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"创建文件失败: {str(e)}")
@router.get("/files/search", summary="搜索文件")
async def search_files(
keyword: str,
project_id: Optional[int] = None,
file_type: Optional[str] = None,
has_boot: Optional[bool] = None
):
"""搜索脚本文件"""
try:
file_service = get_file_service()
result = await file_service.search_files(
keyword=keyword,
project_id=project_id,
file_type=file_type,
has_boot=has_boot
)
if result["success"]:
return success_response(data=result)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"搜索文件失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"搜索文件失败: {str(e)}")
@router.get("/files/{file_id}", summary="获取文件内容")
async def get_file_content(file_id: int):
"""获取脚本文件的内容"""
try:
file_service = get_file_service()
result = await file_service.get_file_content(file_id=file_id)
if result["success"]:
return success_response(data=result)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"获取文件内容失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取文件内容失败: {str(e)}")
@router.put("/files/{file_id}", summary="更新文件内容")
async def update_file_content(file_id: int, request: FileUpdateRequest):
"""更新脚本文件的内容"""
try:
file_service = get_file_service()
result = await file_service.update_file_content(
file_id=file_id,
content=request.content,
updated_by=request.updated_by
)
if result["success"]:
return success_response(data=result["file"], message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"更新文件内容失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"更新文件内容失败: {str(e)}")
@router.delete("/files/{file_id}", summary="删除文件")
async def delete_file(file_id: int):
"""删除脚本文件"""
try:
file_service = get_file_service()
result = await file_service.delete_file(file_id=file_id)
if result["success"]:
return success_response(message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"删除文件失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"删除文件失败: {str(e)}")
@router.post("/validate-syntax", summary="验证脚本语法")
async def validate_syntax(request: dict):
"""验证Python脚本语法"""
try:
content = request.get("content", "")
if not content:
return error_response(message="脚本内容不能为空")
file_service = get_file_service()
result = await file_service.validate_script_syntax(content)
if result["success"]:
return success_response(data=result)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"语法验证失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"语法验证失败: {str(e)}")
@router.post("/scripts/start", summary="启动脚本服务")
async def start_script_service(request: ScriptStartRequest):
"""启动脚本服务"""
try:
script_engine = get_script_engine()
result = await script_engine.start_script_service(
script_id=request.script_id,
start_params=request.start_params
)
if result["success"]:
return success_response(data=result, message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"启动脚本服务失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"启动脚本服务失败: {str(e)}")
@router.post("/scripts/{script_id}/stop", summary="停止脚本服务")
async def stop_script_service(script_id: str):
"""停止脚本服务"""
try:
script_engine = get_script_engine()
result = await script_engine.stop_script_service(script_id=script_id)
if result["success"]:
return success_response(message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"停止脚本服务失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"停止脚本服务失败: {str(e)}")
@router.get("/scripts/running", summary="获取运行中的脚本")
async def get_running_scripts():
"""获取所有运行中的脚本"""
try:
script_engine = get_script_engine()
running_scripts = await script_engine.get_running_scripts()
return success_response(data=running_scripts)
except Exception as e:
logger.error(f"获取运行中的脚本失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取运行中的脚本失败: {str(e)}")
@router.get("/scripts/{script_id}/history", summary="获取脚本历史记录")
async def get_script_history(script_id: str):
"""获取指定脚本的所有历史记录"""
try:
script_engine = get_script_engine()
history = await script_engine.get_script_history(script_id)
return success_response(data=history)
except Exception as e:
logger.error(f"获取脚本历史记录失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取脚本历史记录失败: {str(e)}")
@router.get("/scripts/{script_id}/active", summary="获取脚本活跃记录")
async def get_active_script_record(script_id: str):
"""获取指定脚本的活跃记录"""
try:
script_engine = get_script_engine()
active_record = await script_engine.get_active_script_record(script_id)
if active_record:
return success_response(data=active_record)
else:
return error_response(message=f"脚本 {script_id} 没有活跃记录")
except Exception as e:
logger.error(f"获取活跃脚本记录失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取活跃脚本记录失败: {str(e)}")
@router.post("/scripts/execute-function", summary="执行脚本函数")
async def execute_function(request: FunctionCallRequest):
"""执行脚本中的指定函数"""
try:
script_engine = get_script_engine()
result = await script_engine.execute_script_function(
script_id=request.script_id,
function_name=request.function_name,
function_args=request.function_args
)
if result["success"]:
return success_response(data=result)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"执行脚本函数失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"执行脚本函数失败: {str(e)}")
@router.get("/registry/status", summary="获取注册中心状态")
async def get_registry_status():
"""获取脚本注册中心状态"""
try:
from services.online_script.script_registry_service import get_global_registry
registry = get_global_registry()
registrations = registry.get_all_registrations()
return success_response(data=registrations)
except Exception as e:
logger.error(f"获取注册中心状态失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取注册中心状态失败: {str(e)}")
@router.delete("/projects/{project_id}", summary="删除项目")
async def delete_project(project_id: int):
"""删除脚本项目"""
try:
file_service = get_file_service()
result = await file_service.delete_project(project_id=project_id)
if result["success"]:
return success_response(message=result["message"])
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"删除项目失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"删除项目失败: {str(e)}")
@router.get("/files/{file_id}/export", summary="导出脚本文件")
async def export_file(file_id: int):
"""导出单个脚本文件"""
try:
file_service = get_file_service()
result = await file_service.export_file(file_id=file_id)
if result["success"]:
# 创建文件响应
content = result["content"]
file_name = result["file_name"]
# 确保文件名有正确的扩展名
if not file_name.endswith('.py'):
file_name = f"{file_name}.py"
# URL编码文件名以支持中文
encoded_filename = quote(file_name.encode('utf-8'))
return StreamingResponse(
BytesIO(content.encode('utf-8')),
media_type="text/plain; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"}
)
else:
return error_response(message=result["error"])
except Exception as e:
logger.error(f"导出文件失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"导出文件失败: {str(e)}")
@router.post("/upload", summary="上传脚本文件")
async def upload_script_file(
project_id: int = Form(...),
file: UploadFile = File(...)
):
"""上传脚本文件"""
try:
# 读取文件内容
content = await file.read()
content_str = content.decode('utf-8')
file_service = get_file_service()
result = await file_service.create_file(
project_id=project_id,
file_name=file.filename,
file_path=file.filename,
content=content_str,
file_type="python"
)
if result["success"]:
return success_response(data=result["file"], message=result["message"])
else:
return error_response(message=result["error"])
except UnicodeDecodeError:
return error_response(message="文件编码错误请确保文件为UTF-8编码")
except Exception as e:
logger.error(f"上传脚本文件失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"上传脚本文件失败: {str(e)}")
@router.get("/built-in-functions", summary="获取内置函数文档")
async def get_built_in_functions():
"""获取内置函数文档内容"""
try:
# 获取项目根目录下的文档文件路径
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
docs_file_path = os.path.join(current_dir, "docs", "built_in_functions.md")
# 检查文件是否存在
if not os.path.exists(docs_file_path):
return error_response(message="内置函数文档文件不存在")
# 读取文档内容
with open(docs_file_path, 'r', encoding='utf-8') as f:
content = f.read()
return success_response(data={"content": content}, message="获取内置函数文档成功")
except Exception as e:
logger.error(f"获取内置函数文档失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取内置函数文档失败: {str(e)}")
@router.get("/built-in-functions/categories", summary="获取内置函数分类列表")
async def get_built_in_function_categories():
"""获取内置函数分类列表"""
try:
categories = [
{
"key": "protocol_communication",
"name": "协议通信",
"description": "支持多种工业协议的通信功能",
"modules": [
{"key": "fins", "name": "Fins协议", "description": "欧姆龙PLC通信协议"},
{"key": "http_request", "name": "HTTP请求", "description": "HTTP网络请求功能"},
{"key": "mqtt", "name": "MQTT协议", "description": "物联网消息传输协议"},
{"key": "melsec", "name": "Melsec协议", "description": "三菱PLC通信协议"},
{"key": "modbus_tcp", "name": "Modbus TCP协议", "description": "Modbus TCP工业通信协议"},
{"key": "opc_ua", "name": "OPC UA协议", "description": "OPC Unified Architecture通信协议"},
{"key": "s7", "name": "S7协议", "description": "西门子S7系列PLC通信协议"},
{"key": "websocket", "name": "WebSocket协议", "description": "WebSocket实时通信功能"}
]
},
{
"key": "task_management",
"name": "任务管理",
"description": "VWED任务系统相关功能"
},
{
"key": "storage_management",
"name": "库位管理",
"description": "仓储库位管理功能"
},
{
"key": "robot_management",
"name": "机器人管理",
"description": "AMR机器人管理功能"
},
{
"key": "cache_management",
"name": "缓存数据管理",
"description": "全局缓存数据管理"
},
{
"key": "system_utilities",
"name": "系统工具",
"description": "系统级工具和实用功能"
},
{
"key": "database_operations",
"name": "数据库操作",
"description": "数据库查询和操作功能"
},
{
"key": "file_operations",
"name": "文件操作",
"description": "文件读写操作功能"
},
{
"key": "logging",
"name": "日志相关",
"description": "日志记录和输出功能"
},
{
"key": "registration",
"name": "注册方法",
"description": "脚本方法注册功能"
},
{
"key": "threading_time",
"name": "多线程及时间",
"description": "时间处理和异步操作"
},
{
"key": "handheld_terminal",
"name": "手持端相关",
"description": "手持终端设备控制功能"
}
]
return success_response(data=categories, message="获取分类列表成功")
except Exception as e:
logger.error(f"获取内置函数分类列表失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取内置函数分类列表失败: {str(e)}")