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

@@ -28,8 +28,10 @@ class TaskCreate(BaseModel):
schedule_type: str = "simple" # simple | cron
time_points: Optional[List[str]] = None # ["09:00", "14:00"]
cron_expression: Optional[str] = None # "0 9,14 * * *"
webhook_url: str
execution_type: str = "webhook" # webhook | script
webhook_url: Optional[str] = None
input_params: Optional[dict] = None
script_content: Optional[str] = None
is_enabled: bool = True
@@ -39,8 +41,15 @@ class TaskUpdate(BaseModel):
schedule_type: Optional[str] = None
time_points: Optional[List[str]] = None
cron_expression: Optional[str] = None
execution_type: Optional[str] = None
webhook_url: Optional[str] = None
input_params: Optional[dict] = None
script_content: Optional[str] = None
class ScriptTestRequest(BaseModel):
tenant_id: str
script_content: str
# API Endpoints
@@ -110,6 +119,14 @@ async def create_task(
if not data.cron_expression:
raise HTTPException(status_code=400, detail="CRON模式需要提供表达式")
# 验证执行配置
if data.execution_type == "webhook":
if not data.webhook_url:
raise HTTPException(status_code=400, detail="Webhook模式需要提供URL")
elif data.execution_type == "script":
if not data.script_content:
raise HTTPException(status_code=400, detail="脚本模式需要提供脚本内容")
# 插入数据库
import json
time_points_json = json.dumps(data.time_points) if data.time_points else None
@@ -119,9 +136,9 @@ async def create_task(
text("""
INSERT INTO platform_scheduled_tasks
(tenant_id, task_name, task_desc, schedule_type, time_points,
cron_expression, webhook_url, input_params, is_enabled)
cron_expression, execution_type, webhook_url, input_params, script_content, is_enabled)
VALUES (:tenant_id, :task_name, :task_desc, :schedule_type, :time_points,
:cron_expression, :webhook_url, :input_params, :is_enabled)
:cron_expression, :execution_type, :webhook_url, :input_params, :script_content, :is_enabled)
"""),
{
"tenant_id": data.tenant_id,
@@ -130,8 +147,10 @@ async def create_task(
"schedule_type": data.schedule_type,
"time_points": time_points_json,
"cron_expression": data.cron_expression,
"execution_type": data.execution_type,
"webhook_url": data.webhook_url,
"input_params": input_params_json,
"script_content": data.script_content,
"is_enabled": 1 if data.is_enabled else 0
}
)
@@ -209,9 +228,15 @@ async def update_task(
if data.cron_expression is not None:
updates.append("cron_expression = :cron_expression")
params["cron_expression"] = data.cron_expression
if data.execution_type is not None:
updates.append("execution_type = :execution_type")
params["execution_type"] = data.execution_type
if data.webhook_url is not None:
updates.append("webhook_url = :webhook_url")
params["webhook_url"] = data.webhook_url
if data.script_content is not None:
updates.append("script_content = :script_content")
params["script_content"] = data.script_content
if data.input_params is not None:
updates.append("input_params = :input_params")
params["input_params"] = json.dumps(data.input_params)
@@ -354,3 +379,96 @@ async def get_task_logs(
"size": size,
"items": logs
}
@router.post("/test-script")
async def test_script(
data: ScriptTestRequest,
user: User = Depends(require_operator)
):
"""测试执行脚本(不记录日志)"""
from ..services.script_executor import test_script as run_test
if not data.script_content or not data.script_content.strip():
raise HTTPException(status_code=400, detail="脚本内容不能为空")
result = await run_test(
tenant_id=data.tenant_id,
script_content=data.script_content
)
return result.to_dict()
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取 SDK 文档"""
return {
"description": "脚本执行 SDK 文档",
"methods": [
{
"name": "ai(prompt, system=None, model='gemini-2.5-flash')",
"description": "调用大模型生成内容",
"example": "result = ai('帮我写一段营销文案')"
},
{
"name": "dingtalk(webhook, content, msg_type='text', at_mobiles=None)",
"description": "发送钉钉群消息",
"example": "dingtalk('https://oapi.dingtalk.com/robot/send?access_token=xxx', '消息内容')"
},
{
"name": "wecom(webhook, content, msg_type='text')",
"description": "发送企业微信群消息",
"example": "wecom('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx', '消息内容')"
},
{
"name": "db(sql, params=None)",
"description": "执行 SQL 查询(仅支持 SELECT",
"example": "rows = db('SELECT * FROM users WHERE id = :id', {'id': 1})"
},
{
"name": "http_get(url, headers=None, params=None)",
"description": "发送 HTTP GET 请求",
"example": "data = http_get('https://api.example.com/data')"
},
{
"name": "http_post(url, data=None, json_data=None, headers=None)",
"description": "发送 HTTP POST 请求",
"example": "data = http_post('https://api.example.com/submit', json_data={'key': 'value'})"
},
{
"name": "get_var(key, default=None)",
"description": "获取存储的变量(跨执行持久化)",
"example": "count = get_var('run_count', 0)"
},
{
"name": "set_var(key, value)",
"description": "存储变量(跨执行持久化)",
"example": "set_var('run_count', count + 1)"
},
{
"name": "log(message, level='INFO')",
"description": "记录日志",
"example": "log('任务执行完成')"
}
],
"example_script": '''# 示例:每日推送 AI 生成的内容到钉钉
import json
# 获取历史数据
history = get_var('history', [])
# 调用 AI 生成内容
prompt = f"根据以下信息生成今日营销文案:{json.dumps(history[-5:], ensure_ascii=False)}"
content = ai(prompt, system="你是一个专业的营销文案专家")
# 发送到钉钉
dingtalk(
webhook="你的钉钉机器人Webhook",
content=content
)
# 记录日志
log(f"已发送: {content[:50]}...")
'''
}