552 lines
20 KiB
Python
552 lines
20 KiB
Python
#!/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)}") |