feat: 脚本执行平台功能
Some checks failed
continuous-integration/drone/push Build is failing

- 支持 Python 脚本定时执行(类似青龙面板)
- 内置 SDK:AI大模型、钉钉/企微通知、数据库查询、HTTP请求、变量存储
- 安全沙箱执行,禁用危险模块
- 前端脚本编辑器,支持测试执行
- SDK 文档查看
- 日志通过 TraceID 与 platform_logs 关联
This commit is contained in:
2026-01-28 11:45:02 +08:00
parent ed88099cf0
commit 644255891e
6 changed files with 1153 additions and 35 deletions

View File

@@ -0,0 +1,262 @@
"""脚本执行器 - 安全执行 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')}"
)