Some checks failed
continuous-integration/drone/push Build is failing
- 支持 Python 脚本定时执行(类似青龙面板) - 内置 SDK:AI大模型、钉钉/企微通知、数据库查询、HTTP请求、变量存储 - 安全沙箱执行,禁用危险模块 - 前端脚本编辑器,支持测试执行 - SDK 文档查看 - 日志通过 TraceID 与 platform_logs 关联
263 lines
8.7 KiB
Python
263 lines
8.7 KiB
Python
"""脚本执行器 - 安全执行 Python 脚本"""
|
|
import asyncio
|
|
import io
|
|
import logging
|
|
import sys
|
|
import traceback
|
|
from contextlib import redirect_stdout, redirect_stderr
|
|
from datetime import datetime
|
|
from typing import Any, Dict
|
|
|
|
from .script_sdk import ScriptSDK
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 执行超时时间(秒)
|
|
SCRIPT_TIMEOUT = 300 # 5 分钟
|
|
|
|
# 禁止导入的模块
|
|
FORBIDDEN_MODULES = {
|
|
'os', 'subprocess', 'sys', 'builtins', '__builtins__',
|
|
'importlib', 'eval', 'exec', 'compile',
|
|
'open', 'file', 'input',
|
|
'socket', 'multiprocessing', 'threading',
|
|
'pickle', 'marshal', 'ctypes',
|
|
'code', 'codeop', 'pty', 'tty',
|
|
}
|
|
|
|
|
|
class ScriptExecutionResult:
|
|
"""脚本执行结果"""
|
|
|
|
def __init__(
|
|
self,
|
|
success: bool,
|
|
output: str = "",
|
|
error: str = None,
|
|
logs: list = None,
|
|
execution_time_ms: int = 0
|
|
):
|
|
self.success = success
|
|
self.output = output
|
|
self.error = error
|
|
self.logs = logs or []
|
|
self.execution_time_ms = execution_time_ms
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"success": self.success,
|
|
"output": self.output,
|
|
"error": self.error,
|
|
"logs": self.logs,
|
|
"execution_time_ms": self.execution_time_ms
|
|
}
|
|
|
|
|
|
def create_safe_builtins() -> Dict[str, Any]:
|
|
"""创建安全的内置函数集"""
|
|
import builtins
|
|
|
|
# 允许的内置函数
|
|
allowed = [
|
|
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes',
|
|
'callable', 'chr', 'complex', 'dict', 'divmod', 'enumerate',
|
|
'filter', 'float', 'format', 'frozenset', 'getattr', 'hasattr',
|
|
'hash', 'hex', 'id', 'int', 'isinstance', 'issubclass', 'iter',
|
|
'len', 'list', 'map', 'max', 'min', 'next', 'object', 'oct',
|
|
'ord', 'pow', 'print', 'range', 'repr', 'reversed', 'round',
|
|
'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'type', 'zip',
|
|
'True', 'False', 'None',
|
|
]
|
|
|
|
safe_builtins = {}
|
|
for name in allowed:
|
|
if hasattr(builtins, name):
|
|
safe_builtins[name] = getattr(builtins, name)
|
|
|
|
# 添加安全的 import 函数
|
|
def safe_import(name, *args, **kwargs):
|
|
"""安全的 import 函数,只允许特定模块"""
|
|
allowed_modules = {
|
|
'json', 'datetime', 'time', 're', 'math', 'random',
|
|
'collections', 'itertools', 'functools', 'operator',
|
|
'string', 'textwrap', 'unicodedata',
|
|
'hashlib', 'base64', 'urllib.parse',
|
|
}
|
|
|
|
if name in FORBIDDEN_MODULES:
|
|
raise ImportError(f"禁止导入模块: {name}")
|
|
|
|
if name not in allowed_modules and not name.startswith('urllib.parse'):
|
|
raise ImportError(f"不允许导入模块: {name},允许的模块: {', '.join(sorted(allowed_modules))}")
|
|
|
|
return __builtins__['__import__'](name, *args, **kwargs)
|
|
|
|
safe_builtins['__import__'] = safe_import
|
|
|
|
return safe_builtins
|
|
|
|
|
|
async def execute_script(
|
|
task_id: int,
|
|
tenant_id: str,
|
|
script_content: str,
|
|
trace_id: str = None
|
|
) -> ScriptExecutionResult:
|
|
"""
|
|
执行 Python 脚本
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
tenant_id: 租户 ID
|
|
script_content: 脚本内容
|
|
trace_id: 追踪 ID
|
|
|
|
Returns:
|
|
ScriptExecutionResult: 执行结果
|
|
"""
|
|
start_time = datetime.now()
|
|
sdk = None
|
|
|
|
try:
|
|
# 创建 SDK 实例
|
|
sdk = ScriptSDK(tenant_id, task_id, trace_id)
|
|
|
|
# 准备执行环境
|
|
script_globals = {
|
|
'__builtins__': create_safe_builtins(),
|
|
'__name__': '__script__',
|
|
|
|
# SDK 实例
|
|
'sdk': sdk,
|
|
|
|
# 快捷方法(同步包装)
|
|
'ai': lambda *args, **kwargs: asyncio.get_event_loop().run_until_complete(sdk.ai_chat(*args, **kwargs)),
|
|
'dingtalk': lambda *args, **kwargs: asyncio.get_event_loop().run_until_complete(sdk.send_dingtalk(*args, **kwargs)),
|
|
'wecom': lambda *args, **kwargs: asyncio.get_event_loop().run_until_complete(sdk.send_wecom(*args, **kwargs)),
|
|
'http_get': lambda *args, **kwargs: asyncio.get_event_loop().run_until_complete(sdk.http_get(*args, **kwargs)),
|
|
'http_post': lambda *args, **kwargs: asyncio.get_event_loop().run_until_complete(sdk.http_post(*args, **kwargs)),
|
|
|
|
# 同步方法
|
|
'db': sdk.db_query,
|
|
'get_var': sdk.get_var,
|
|
'set_var': sdk.set_var,
|
|
'delete_var': sdk.delete_var,
|
|
'log': sdk.log,
|
|
|
|
# 常用模块
|
|
'json': __import__('json'),
|
|
'datetime': __import__('datetime'),
|
|
're': __import__('re'),
|
|
'math': __import__('math'),
|
|
'random': __import__('random'),
|
|
}
|
|
|
|
# 捕获输出
|
|
stdout = io.StringIO()
|
|
stderr = io.StringIO()
|
|
|
|
sdk.log("脚本开始执行")
|
|
|
|
# 编译并执行脚本
|
|
try:
|
|
# 编译脚本
|
|
code = compile(script_content, '<script>', 'exec')
|
|
|
|
# 执行(带超时)
|
|
async def run_script():
|
|
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
exec(code, script_globals)
|
|
|
|
await asyncio.wait_for(run_script(), timeout=SCRIPT_TIMEOUT)
|
|
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
sdk.log(f"脚本执行完成,耗时 {execution_time}ms")
|
|
|
|
return ScriptExecutionResult(
|
|
success=True,
|
|
output=stdout.getvalue(),
|
|
logs=sdk.get_logs(),
|
|
execution_time_ms=execution_time
|
|
)
|
|
|
|
except asyncio.TimeoutError:
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
error_msg = f"脚本执行超时(超过 {SCRIPT_TIMEOUT} 秒)"
|
|
sdk.log(error_msg, level="ERROR")
|
|
|
|
return ScriptExecutionResult(
|
|
success=False,
|
|
output=stdout.getvalue(),
|
|
error=error_msg,
|
|
logs=sdk.get_logs(),
|
|
execution_time_ms=execution_time
|
|
)
|
|
|
|
except SyntaxError as e:
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
error_msg = f"语法错误: 第 {e.lineno} 行 - {e.msg}"
|
|
sdk.log(error_msg, level="ERROR")
|
|
|
|
return ScriptExecutionResult(
|
|
success=False,
|
|
output=stdout.getvalue(),
|
|
error=error_msg,
|
|
logs=sdk.get_logs(),
|
|
execution_time_ms=execution_time
|
|
)
|
|
|
|
except Exception as e:
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
|
|
# 获取详细的错误堆栈
|
|
tb = traceback.format_exc()
|
|
sdk.log(f"执行错误: {error_msg}\n{tb}", level="ERROR")
|
|
|
|
return ScriptExecutionResult(
|
|
success=False,
|
|
output=stdout.getvalue(),
|
|
error=error_msg,
|
|
logs=sdk.get_logs(),
|
|
execution_time_ms=execution_time
|
|
)
|
|
|
|
except Exception as e:
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
error_msg = f"执行器错误: {str(e)}"
|
|
logger.error(f"Script executor error: {e}", exc_info=True)
|
|
|
|
return ScriptExecutionResult(
|
|
success=False,
|
|
error=error_msg,
|
|
logs=sdk.get_logs() if sdk else [],
|
|
execution_time_ms=execution_time
|
|
)
|
|
|
|
finally:
|
|
# 清理资源
|
|
if sdk:
|
|
sdk.cleanup()
|
|
|
|
|
|
async def test_script(
|
|
tenant_id: str,
|
|
script_content: str
|
|
) -> ScriptExecutionResult:
|
|
"""
|
|
测试执行脚本(不记录日志到数据库)
|
|
|
|
Args:
|
|
tenant_id: 租户 ID
|
|
script_content: 脚本内容
|
|
|
|
Returns:
|
|
ScriptExecutionResult: 执行结果
|
|
"""
|
|
return await execute_script(
|
|
task_id=0, # 测试用 ID
|
|
tenant_id=tenant_id,
|
|
script_content=script_content,
|
|
trace_id=f"test_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
)
|