VWED_server/services/online_script/script_engine_service.py

786 lines
35 KiB
Python
Raw Normal View History

2025-09-12 16:15:13 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本引擎服务
核心脚本执行引擎支持多脚本并发运行和生命周期管理
"""
import os
import sys
import time
import uuid
2025-09-25 10:52:52 +08:00
import asyncio
2025-09-12 16:15:13 +08:00
import traceback
from datetime import datetime
from typing import Dict, Any, Optional, List
from pathlib import Path
2025-09-25 10:52:52 +08:00
from data.session import get_async_session
2025-09-20 16:50:45 +08:00
from data.models.script_file import VWEDScriptFile
2025-09-12 16:15:13 +08:00
from data.models.script_registry import VWEDScriptRegistry
from data.models.script_execution_log import VWEDScriptExecutionLog
2025-09-20 16:50:45 +08:00
from .script_registry_service import get_global_registry
from .device_handler_service import get_device_service
2025-09-12 16:15:13 +08:00
from utils.logger import get_logger
2025-09-25 10:52:52 +08:00
from config.settings import settings
2025-09-12 16:15:13 +08:00
logger = get_logger("services.script_engine")
class ScriptExecutionContext:
"""脚本执行上下文"""
2025-09-20 16:50:45 +08:00
def __init__(self, script_id: str, file_path: str, registry_service, file_id: int = None):
2025-09-12 16:15:13 +08:00
self.script_id = script_id
self.file_path = file_path
2025-09-20 16:50:45 +08:00
self.file_id = file_id
2025-09-12 16:15:13 +08:00
self.registry = registry_service
self.start_time = datetime.now()
self.variables = {}
self.logs = []
def log(self, level: str, message: str, **kwargs):
"""记录日志"""
log_entry = {
"timestamp": datetime.now().isoformat(),
"level": level,
"message": message,
"script_id": self.script_id,
**kwargs
}
self.logs.append(log_entry)
logger.log(getattr(logger, level.lower(), logger.info), f"[{self.script_id}] {message}")
class MultiScriptEngine:
"""多脚本服务引擎"""
def __init__(self):
self.registry = get_global_registry()
self.running_scripts: Dict[str, Dict[str, Any]] = {} # {script_id: script_info}
self.script_contexts: Dict[str, ScriptExecutionContext] = {}
2025-09-25 10:52:52 +08:00
self.base_path = Path(settings.SCRIPT_SAVE_PATH)
self._initialized = False
2025-09-12 16:15:13 +08:00
2025-09-25 10:52:52 +08:00
async def _ensure_initialized(self):
"""确保引擎已初始化(延迟初始化)"""
if not self._initialized:
await self._initialize_cleanup_active_scripts()
self._initialized = True
2025-09-20 16:50:45 +08:00
async def start_script_service(self, script_path: str = None, script_id: int = None, start_params: Dict = None) -> Dict[str, Any]:
2025-09-12 16:15:13 +08:00
"""启动脚本服务 - 支持多脚本并发"""
2025-09-25 10:52:52 +08:00
import asyncio
# 确保引擎已初始化
await self._ensure_initialized()
2025-09-12 16:15:13 +08:00
try:
2025-09-20 16:50:45 +08:00
script_file = None
# 1. 根据参数类型获取脚本信息
if script_id is not None:
# 通过script_id查找脚本文件
async with get_async_session() as session:
from sqlalchemy import select
from sqlalchemy.orm import selectinload
result = await session.execute(
select(VWEDScriptFile)
.options(selectinload(VWEDScriptFile.project))
.where(VWEDScriptFile.id == script_id)
)
script_file = result.scalar_one_or_none()
if not script_file:
return {"success": False, "error": f"脚本文件不存在: script_id={script_id}"}
script_path = script_file.file_path
2025-09-25 10:52:52 +08:00
# 使用正确的文件路径拼接逻辑
full_script_path = Path(os.path.join(self.base_path, script_file.project.project_path, script_file.file_path))
2025-09-20 16:50:45 +08:00
elif script_path is not None:
# 传统方式通过script_path查找
full_script_path = Path(script_path)
else:
return {"success": False, "error": "必须提供script_id或script_path参数"}
# 2. 验证脚本文件存在
2025-09-12 16:15:13 +08:00
if not os.path.exists(full_script_path):
return {"success": False, "error": f"脚本文件不存在: {script_path}"}
2025-09-25 10:52:52 +08:00
# 3. 预检查脚本是否有boot函数
boot_check_result = await self._check_boot_function(full_script_path)
if not boot_check_result["success"]:
return boot_check_result
# 4. 确定脚本服务ID
if script_id is not None:
# 使用数据库中的script_id作为全局唯一标识
script_service_id = str(script_id)
else:
# 对于通过script_path启动的脚本保持原有生成逻辑
script_service_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
# 5. 检查脚本是否已在运行
if script_service_id in self.running_scripts:
# 拒绝启动已运行的脚本
running_info = self.running_scripts[script_service_id]
2025-09-12 16:15:13 +08:00
return {
"success": False,
2025-09-25 10:52:52 +08:00
"error": f"脚本 {script_service_id} 已在运行中",
"running_info": {
"script_id": script_service_id,
"script_path": running_info["script_path"],
"started_at": running_info["started_at"],
"status": running_info["status"]
},
"message": "请先停止正在运行的实例"
2025-09-12 16:15:13 +08:00
}
2025-09-25 10:52:52 +08:00
# 6. 清理可能存在的数据库旧记录和注册项(清理残留的非活跃记录)
await self._cleanup_script_resources(script_service_id)
2025-09-12 16:15:13 +08:00
2025-09-25 10:52:52 +08:00
# 7. 异步启动脚本(不阻塞当前请求)
asyncio.create_task(self._async_start_script(
script_service_id, script_path, full_script_path, script_file, start_params
))
2025-09-12 16:15:13 +08:00
2025-09-25 10:52:52 +08:00
# 立即返回启动状态,不等待完成
return {
"success": True,
"script_id": script_service_id,
"message": f"脚本服务启动中: {script_path}",
"status": "starting"
}
2025-09-12 16:15:13 +08:00
except Exception as e:
logger.error(f"启动脚本服务失败: {e}", exc_info=True)
return {"success": False, "error": f"脚本服务启动失败: {str(e)}"}
async def stop_script_service(self, script_id: str) -> Dict[str, Any]:
"""停止特定脚本服务 - 关键功能"""
2025-09-25 10:52:52 +08:00
# 确保引擎已初始化
await self._ensure_initialized()
2025-09-12 16:15:13 +08:00
try:
if script_id not in self.running_scripts:
return {"success": False, "error": f"脚本 {script_id} 未运行"}
# 获取注册统计
registrations = self.registry.get_script_registrations(script_id)
# 清理该脚本的所有注册项 - 这是关键步骤
self.registry.clear_script_registrations(script_id)
2025-09-20 16:50:45 +08:00
# 清理设备处理器
device_service = get_device_service()
device_service.registry.clear_script_device_registrations(script_id)
2025-09-12 16:15:13 +08:00
# 从运行列表移除
script_info = self.running_scripts.pop(script_id)
# 清理上下文
if script_id in self.script_contexts:
del self.script_contexts[script_id]
# 更新数据库状态
await self._update_script_status(script_id, "stopped")
logger.info(f"脚本服务已停止: {script_id}")
return {
"success": True,
2025-09-20 16:50:45 +08:00
"message": f"脚本服务已停止 ({script_info['script_path']}),已清理 {registrations['apis']} 个接口、{registrations['functions']} 个函数、{registrations['events']} 个事件监听器、{registrations.get('devices', 0)} 个设备处理器",
2025-09-12 16:15:13 +08:00
"registrations": registrations
}
except Exception as e:
logger.error(f"停止脚本服务失败: {e}", exc_info=True)
return {"success": False, "error": f"停止脚本服务失败: {str(e)}"}
2025-09-25 10:52:52 +08:00
async def get_running_scripts(self) -> List[Dict[str, Any]]:
2025-09-12 16:15:13 +08:00
"""获取所有运行中的脚本"""
2025-09-25 10:52:52 +08:00
# 确保引擎已初始化
await self._ensure_initialized()
2025-09-12 16:15:13 +08:00
result = []
for script_id, script_info in self.running_scripts.items():
registrations = self.registry.get_script_registrations(script_id)
result.append({
"script_id": script_id,
"script_path": script_info["script_path"],
"started_at": script_info["started_at"],
"status": script_info["status"],
"registrations": registrations
})
return result
2025-09-25 10:52:52 +08:00
async def get_script_history(self, script_id: str) -> List[Dict[str, Any]]:
"""获取脚本的所有历史记录"""
try:
async with get_async_session() as session:
from sqlalchemy import select
result = await session.execute(
select(VWEDScriptRegistry)
.where(VWEDScriptRegistry.script_id == script_id)
.order_by(VWEDScriptRegistry.created_at.desc())
)
records = result.scalars().all()
return [record.to_dict() for record in records]
except Exception as e:
logger.error(f"获取脚本历史记录失败: {e}")
return []
async def get_active_script_record(self, script_id: str) -> Optional[Dict[str, Any]]:
"""获取脚本的活跃记录"""
try:
async with get_async_session() as session:
from sqlalchemy import select
result = await session.execute(
select(VWEDScriptRegistry)
.where(VWEDScriptRegistry.script_id == script_id)
.where(VWEDScriptRegistry.is_active == True)
)
record = result.scalar_one_or_none()
return record.to_dict() if record else None
except Exception as e:
logger.error(f"获取活跃脚本记录失败: {e}")
return None
2025-09-12 16:15:13 +08:00
def find_running_script_by_path(self, script_path: str) -> Optional[Dict[str, Any]]:
"""根据脚本路径查找运行中的脚本"""
for script_id, script_info in self.running_scripts.items():
if script_info["script_path"] == script_path:
return {"script_id": script_id, **script_info}
return None
2025-09-25 10:52:52 +08:00
def find_running_script_by_id(self, script_id: str) -> Optional[Dict[str, Any]]:
"""根据脚本ID查找运行中的脚本"""
if script_id in self.running_scripts:
return {"script_id": script_id, **self.running_scripts[script_id]}
return None
2025-09-12 16:15:13 +08:00
async def execute_script_function(self, script_id: str, function_name: str,
function_args: Any) -> Dict[str, Any]:
"""执行脚本中的指定函数"""
2025-09-25 10:52:52 +08:00
from .script_output_capture import capture_script_output, push_script_error
2025-09-12 16:15:13 +08:00
try:
if script_id not in self.running_scripts:
return {"success": False, "error": f"脚本 {script_id} 未运行"}
# 从注册中心获取函数
func_info = self.registry.get_function_info(function_name)
if not func_info:
return {"success": False, "error": f"函数 {function_name} 未注册"}
if func_info["script_id"] != script_id:
return {"success": False, "error": f"函数 {function_name} 不属于脚本 {script_id}"}
start_time = time.time()
2025-09-25 10:52:52 +08:00
# 使用输出捕获上下文管理器
with capture_script_output(script_id):
try:
handler = func_info["handler"]
# 处理参数
if func_info["is_async"]:
result = await handler(**function_args)
else:
result = handler(**function_args)
execution_time_ms = int((time.time() - start_time) * 1000)
# 更新统计
self.registry.update_function_call_stats(function_name, execution_time_ms, True)
# 记录执行日志
await self._log_execution(script_id, "function_call", function_name,
function_args, result, "success", execution_time_ms)
return {
"success": True,
"result": result,
"execution_time_ms": execution_time_ms
}
except Exception as e:
execution_time_ms = int((time.time() - start_time) * 1000)
error_message = str(e)
error_traceback = traceback.format_exc()
# 更新统计
self.registry.update_function_call_stats(function_name, execution_time_ms, False)
# 记录执行日志
await self._log_execution(script_id, "function_call", function_name,
function_args, None, "failed", execution_time_ms,
error_message, error_traceback)
# 推送错误信息到WebSocket
await push_script_error(script_id, f"函数 {function_name} 执行失败: {error_message}")
return {
"success": False,
"error": error_message,
"traceback": error_traceback,
"execution_time_ms": execution_time_ms
}
2025-09-12 16:15:13 +08:00
except Exception as e:
logger.error(f"执行脚本函数失败: {e}", exc_info=True)
return {"success": False, "error": f"执行脚本函数失败: {str(e)}"}
async def _load_and_execute_script(self, script_id: str, script_path: str,
context: ScriptExecutionContext) -> Dict[str, Any]:
"""加载并执行脚本"""
2025-09-25 10:52:52 +08:00
from .script_output_capture import capture_script_output, push_script_error
2025-09-12 16:15:13 +08:00
try:
# 读取脚本内容
with open(script_path, 'r', encoding='utf-8') as f:
script_code = f.read()
2025-09-25 10:52:52 +08:00
2025-09-12 16:15:13 +08:00
# 准备脚本全局环境
2025-09-20 16:50:45 +08:00
from .script_vwed_objects import create_vwed_object
2025-09-12 16:15:13 +08:00
script_globals = {
'__name__': '__main__',
'__file__': script_path,
'VWED': create_vwed_object(script_id), # 注入VWED统一对象
'context': context
}
2025-09-25 10:52:52 +08:00
# 使用输出捕获上下文管理器
with capture_script_output(script_id):
# 执行脚本
exec(script_code, script_globals)
# 保存脚本全局环境以供后续使用
context.globals = script_globals
return {"success": True, "message": "脚本加载成功"}
2025-09-12 16:15:13 +08:00
except Exception as e:
error_message = str(e)
error_traceback = traceback.format_exc()
logger.error(f"加载脚本失败: {error_message}")
await self._log_execution(script_id, "load_script", None, None, None,
"failed", 0, error_message, error_traceback)
2025-09-25 10:52:52 +08:00
# 将错误信息也推送到WebSocket
await push_script_error(script_id, f"脚本加载失败: {error_message}")
2025-09-12 16:15:13 +08:00
return {"success": False, "error": error_message, "traceback": error_traceback}
async def _execute_boot_function(self, script_id: str,
context: ScriptExecutionContext) -> Dict[str, Any]:
"""执行boot函数注册服务"""
2025-09-25 10:52:52 +08:00
import asyncio
import inspect
from .script_output_capture import capture_script_output, push_script_error
2025-09-12 16:15:13 +08:00
try:
if 'boot' not in context.globals:
return {"success": False, "error": "脚本缺少boot()入口函数"}
boot_function = context.globals['boot']
start_time = time.time()
2025-09-25 10:52:52 +08:00
# 使用输出捕获上下文管理器
with capture_script_output(script_id):
# 执行boot函数
if callable(boot_function):
# 检查boot函数是否是异步函数
if inspect.iscoroutinefunction(boot_function):
# 如果是异步函数直接await
await boot_function()
else:
# 如果是同步函数,在线程池中执行,避免阻塞事件循环
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, boot_function)
else:
return {"success": False, "error": "boot不是一个可调用函数"}
2025-09-12 16:15:13 +08:00
execution_time_ms = int((time.time() - start_time) * 1000)
# 记录执行日志
await self._log_execution(script_id, "boot_function", "boot", None, None,
"success", execution_time_ms)
return {"success": True, "execution_time_ms": execution_time_ms}
except Exception as e:
error_message = str(e)
error_traceback = traceback.format_exc()
logger.error(f"执行boot函数失败: {error_message}")
await self._log_execution(script_id, "boot_function", "boot", None, None,
"failed", 0, error_message, error_traceback)
2025-09-25 10:52:52 +08:00
# 推送错误信息到WebSocket
await push_script_error(script_id, f"boot函数执行失败: {error_message}")
2025-09-12 16:15:13 +08:00
return {"success": False, "error": error_message, "traceback": error_traceback}
2025-09-25 10:52:52 +08:00
async def _initialize_cleanup_active_scripts(self):
"""初始化时清理所有活跃状态的脚本记录"""
try:
logger.info("开始初始化清理所有活跃状态的脚本记录")
async with get_async_session() as session:
from sqlalchemy import update
try:
# 标记所有活跃的函数注册记录为非活跃
from data.models.script_function_registration import VWEDScriptFunctionRegistration
await session.execute(
update(VWEDScriptFunctionRegistration)
.where(VWEDScriptFunctionRegistration.is_active == True)
.values(is_active=False)
)
except ImportError:
logger.warning("VWEDScriptFunctionRegistration 模型未找到")
try:
# 标记所有活跃的API注册记录为非活跃
from data.models.script_api_registration import VWEDScriptAPIRegistration
await session.execute(
update(VWEDScriptAPIRegistration)
.where(VWEDScriptAPIRegistration.is_active == True)
.values(is_active=False)
)
except ImportError:
logger.warning("VWEDScriptAPIRegistration 模型未找到")
# 标记所有活跃的脚本注册中心记录为非活跃状态
result = await session.execute(
update(VWEDScriptRegistry)
.where(VWEDScriptRegistry.is_active == True)
.values(
is_active=False,
status='stopped',
stopped_at=datetime.now()
)
)
await session.commit()
logger.info(f"初始化清理完成,共标记 {result.rowcount} 个活跃脚本记录为非活跃状态")
except Exception as e:
logger.error(f"初始化清理活跃脚本记录失败: {e}", exc_info=True)
async def _cleanup_script_resources(self, script_id: str):
"""清理脚本相关资源并标记历史记录为非活跃状态"""
try:
# 1. 清理内存中的注册项
self.registry.clear_script_registrations(script_id)
# 2. 清理设备处理器
device_service = get_device_service()
device_service.registry.clear_script_device_registrations(script_id)
# 3. 清理运行记录和上下文
if script_id in self.running_scripts:
del self.running_scripts[script_id]
if script_id in self.script_contexts:
del self.script_contexts[script_id]
# 4. 标记历史记录为非活跃状态(保留历史记录,不删除)
async with get_async_session() as session:
from sqlalchemy import update
try:
from data.models.script_function_registration import VWEDScriptFunctionRegistration
# 标记函数注册记录为非活跃
await session.execute(
update(VWEDScriptFunctionRegistration)
.where(VWEDScriptFunctionRegistration.script_id == script_id)
.values(is_active=False)
)
except ImportError:
logger.warning("VWEDScriptFunctionRegistration 模型未找到")
try:
from data.models.script_api_registration import VWEDScriptAPIRegistration
# 标记API注册记录为非活跃
await session.execute(
update(VWEDScriptAPIRegistration)
.where(VWEDScriptAPIRegistration.script_id == script_id)
.values(is_active=False)
)
except ImportError:
logger.warning("VWEDScriptAPIRegistration 模型未找到")
# 标记脚本注册中心记录为非活跃状态
await session.execute(
update(VWEDScriptRegistry)
.where(VWEDScriptRegistry.script_id == script_id)
.where(VWEDScriptRegistry.is_active == True)
.values(
is_active=False,
status='stopped',
stopped_at=datetime.now()
)
)
await session.commit()
logger.info(f"已将脚本 {script_id} 的历史记录标记为非活跃状态")
except Exception as e:
logger.error(f"清理脚本资源失败: {script_id}, 错误: {e}", exc_info=True)
2025-09-12 16:15:13 +08:00
async def _cleanup_failed_script(self, script_id: str):
"""清理失败的脚本"""
try:
# 清理注册项
self.registry.clear_script_registrations(script_id)
# 清理运行记录
if script_id in self.running_scripts:
del self.running_scripts[script_id]
# 清理上下文
if script_id in self.script_contexts:
del self.script_contexts[script_id]
# 更新数据库状态
await self._update_script_status(script_id, "error")
except Exception as e:
logger.error(f"清理失败脚本出错: {e}")
async def _update_script_status(self, script_id: str, status: str, error_message: str = None):
"""更新脚本状态"""
try:
async with get_async_session() as session:
2025-09-25 10:52:52 +08:00
from sqlalchemy import select
# 查找活跃的脚本记录
result = await session.execute(
select(VWEDScriptRegistry)
.where(VWEDScriptRegistry.script_id == script_id)
.where(VWEDScriptRegistry.is_active == True)
)
registry_record = result.scalar_one_or_none()
2025-09-12 16:15:13 +08:00
if registry_record:
registry_record.status = status
if error_message:
registry_record.error_message = error_message
if status in ['stopped', 'error']:
registry_record.stopped_at = datetime.now()
2025-09-25 10:52:52 +08:00
registry_record.is_active = False # 停止时标记为非活跃
2025-09-12 16:15:13 +08:00
# 更新注册统计
registrations = self.registry.get_script_registrations(script_id)
2025-09-25 10:52:52 +08:00
# 传递所有支持的参数
supported_params = {
'apis': registrations.get('apis', 0),
'functions': registrations.get('functions', 0),
'events': registrations.get('events', 0),
'timers': registrations.get('timers', 0),
'devices': registrations.get('devices', 0)
}
registry_record.update_registration_counts(**supported_params)
2025-09-12 16:15:13 +08:00
await session.commit()
2025-09-25 10:52:52 +08:00
else:
logger.warning(f"未找到活跃的脚本记录: {script_id}")
2025-09-12 16:15:13 +08:00
except Exception as e:
logger.error(f"更新脚本状态失败: {e}")
2025-09-20 16:50:45 +08:00
2025-09-25 10:52:52 +08:00
async def _check_boot_function(self, script_path: str) -> Dict[str, Any]:
"""检查脚本是否包含boot函数且没有其他入口点"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
script_content = f.read()
import re
# 1. 检查是否有boot函数定义支持同步和异步函数
boot_pattern = r'^\s*(async\s+)?def\s+boot\s*\('
has_boot = bool(re.search(boot_pattern, script_content, re.MULTILINE))
if not has_boot:
return {
"success": False,
"error": "脚本缺少boot()入口函数。请在脚本中定义boot()函数作为服务启动入口。"
}
# 2. 检查是否存在其他入口点(如 if __name__ == "__main__"
main_check_patterns = [
r'if\s+__name__\s*==\s*["\']__main__["\']\s*:', # if __name__ == "__main__":
r'if\s+__name__\s*==\s*["\']__main__["\']\s*\:', # 变体
r'if\s+__name__\s*==\s*"__main__"\s*:', # 双引号
r'if\s+__name__\s*==\s*\'__main__\'\s*:', # 单引号
]
for pattern in main_check_patterns:
if re.search(pattern, script_content, re.IGNORECASE):
return {
"success": False,
"error": "脚本中不能包含 'if __name__ == \"__main__\":' 等其他入口点。请仅使用boot()函数作为唯一入口点。"
}
# 3. 检查潜在的阻塞调用
blocking_check_result = self._check_for_blocking_calls(script_content)
if not blocking_check_result["success"]:
return blocking_check_result
return {"success": True}
except Exception as e:
return {"success": False, "error": f"检查boot函数失败: {str(e)}"}
def _check_for_blocking_calls(self, script_content: str) -> Dict[str, Any]:
"""检查脚本中潜在的阻塞调用"""
import re
# 定义潜在的阻塞函数调用模式
blocking_patterns = [
(r'time\.sleep\s*\(', 'time.sleep()', '建议在异步函数中使用 await asyncio.sleep() 代替'),
(r'input\s*\(', 'input()', '不建议在脚本中使用阻塞式输入'),
(r'\.join\s*\(\s*\)', '.join()', '线程join调用可能导致阻塞建议使用异步方式'),
(r'requests\.(get|post|put|delete|patch)', 'requests同步调用', '建议使用 aiohttp 或其他异步HTTP库'),
(r'\.wait\s*\(\s*\)', '.wait()', 'wait调用可能导致阻塞'),
]
warnings = []
for pattern, description, suggestion in blocking_patterns:
if re.search(pattern, script_content, re.IGNORECASE):
warnings.append(f"检测到潜在阻塞调用: {description} - {suggestion}")
# 检查是否在异步函数中使用了time.sleep
async_boot_with_time_sleep = re.search(
r'async\s+def\s+boot\s*\([^)]*\):[^}]*time\.sleep\s*\(',
script_content,
re.DOTALL | re.MULTILINE
)
if async_boot_with_time_sleep:
return {
"success": False,
"error": "在异步boot()函数中检测到 time.sleep() 调用,这会阻塞事件循环。请使用 'await asyncio.sleep()' 代替。"
}
# 如果有警告但不是致命错误,记录警告但允许继续
if warnings:
return {
"success": False,
"error": "在boot()函数中检测到 time.sleep() 调用,这会阻塞事件循环。请使用 'await asyncio.sleep()' 代替。"
}
return {"success": True, "warnings": warnings}
async def _async_start_script(self, script_service_id: str, script_path: str,
full_script_path: Path, script_file, start_params: Dict = None):
"""异步启动脚本的实际逻辑"""
try:
# 创建执行上下文
context = ScriptExecutionContext(script_service_id, script_path, self.registry,
script_file.id if script_file else None)
self.script_contexts[script_service_id] = context
# 设置全局注册中心上下文
self.registry.set_current_script(script_service_id)
self.registry.add_active_script(script_service_id, script_path)
# 创建数据库记录(新记录默认为活跃状态)
async with get_async_session() as session:
registry_record = VWEDScriptRegistry(
script_id=script_service_id,
file_id=script_file.id if script_file else None,
script_path=script_path,
status='starting',
is_active=True, # 新记录为活跃状态
start_params=start_params or {},
host_info={
"hostname": os.environ.get('COMPUTERNAME', os.environ.get('HOSTNAME', 'unknown')),
"platform": sys.platform,
"python_version": sys.version
}
)
session.add(registry_record)
await session.commit()
# 加载并执行脚本
result = await self._load_and_execute_script(script_service_id, full_script_path, context)
if result["success"]:
# 调用boot函数注册服务
boot_result = await self._execute_boot_function(script_service_id, context)
if not boot_result["success"]:
await self._cleanup_failed_script(script_service_id)
logger.error(f"脚本 {script_service_id} boot函数执行失败: {boot_result['error']}")
return
# 更新运行状态
self.running_scripts[script_service_id] = {
"script_path": script_path,
"started_at": context.start_time.isoformat(),
"status": "running",
"context": context
}
# 更新数据库状态
await self._update_script_status(script_service_id, "running")
# 获取注册统计
registrations = self.registry.get_script_registrations(script_service_id)
logger.info(f"脚本服务启动成功: {script_service_id}, 已注册 {registrations['apis']} 个接口,{registrations['functions']} 个函数")
else:
await self._cleanup_failed_script(script_service_id)
logger.error(f"脚本 {script_service_id} 加载失败: {result['error']}")
except Exception as e:
logger.error(f"异步启动脚本失败: {e}", exc_info=True)
await self._cleanup_failed_script(script_service_id)
2025-09-12 16:15:13 +08:00
async def _log_execution(self, script_id: str, execution_type: str, function_name: str,
input_params: Any, output_result: Any, status: str,
duration_ms: int, error_message: str = None,
error_traceback: str = None):
"""记录执行日志"""
try:
2025-09-20 16:50:45 +08:00
# 从执行上下文获取file_id
context = self.script_contexts.get(script_id)
file_id = context.file_id if context else None
2025-09-12 16:15:13 +08:00
async with get_async_session() as session:
log_entry = VWEDScriptExecutionLog(
script_id=script_id,
2025-09-20 16:50:45 +08:00
file_id=file_id,
2025-09-12 16:15:13 +08:00
execution_type=execution_type,
function_name=function_name,
input_params=input_params,
output_result=output_result,
status=status,
error_message=error_message,
error_traceback=error_traceback,
duration_ms=duration_ms
)
session.add(log_entry)
await session.commit()
except Exception as e:
logger.error(f"记录执行日志失败: {e}")
# 全局脚本引擎实例
_script_engine = MultiScriptEngine()
def get_script_engine() -> MultiScriptEngine:
"""获取脚本引擎实例"""
return _script_engine