Files
000-platform/backend/app/routers/tasks.py
Admin afcf30b519
All checks were successful
continuous-integration/drone/push Build is passing
feat: 新增睿美云对接模块
- 扩展 ToolConfig 配置类型,新增 external_api 类型
- 实现接口注册表,包含 90+ 睿美云开放接口定义
- 实现 TPOS SHA256WithRSA 签名鉴权
- 实现睿美云 API 客户端,支持多租户配置
- 新增代理路由 /api/ruimeiyun/call/{api_name}
- 支持接口权限控制和健康检查
2026-01-30 17:27:58 +08:00

560 lines
19 KiB
Python

"""定时任务API路由"""
import json
from datetime import datetime
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import desc
from ..database import get_db
from ..models.scheduled_task import ScheduledTask, TaskLog, Secret
from ..services.scheduler import scheduler_service
from ..services.script_executor import ScriptExecutor
router = APIRouter(prefix="/api/scheduled-tasks", tags=["scheduled-tasks"])
# ==================== Schemas ====================
class TaskCreate(BaseModel):
tenant_id: Optional[str] = None
task_name: str
task_desc: Optional[str] = None
execution_type: str = 'script'
schedule_type: str = 'simple'
time_points: Optional[List[str]] = None
cron_expression: Optional[str] = None
webhook_url: Optional[str] = None
script_content: Optional[str] = None
input_params: Optional[dict] = None
retry_count: Optional[int] = 0
retry_interval: Optional[int] = 60
alert_on_failure: Optional[bool] = False
alert_webhook: Optional[str] = None
notify_channels: Optional[List[int]] = None # 通知渠道ID列表
notify_wecom_app_id: Optional[int] = None # 企微应用ID
class TaskUpdate(BaseModel):
tenant_id: Optional[str] = None
task_name: Optional[str] = None
task_desc: Optional[str] = None
execution_type: Optional[str] = None
schedule_type: Optional[str] = None
time_points: Optional[List[str]] = None
cron_expression: Optional[str] = None
webhook_url: Optional[str] = None
script_content: Optional[str] = None
input_params: Optional[dict] = None
retry_count: Optional[int] = None
retry_interval: Optional[int] = None
alert_on_failure: Optional[bool] = None
alert_webhook: Optional[str] = None
is_enabled: Optional[bool] = None
notify_channels: Optional[List[int]] = None
notify_wecom_app_id: Optional[int] = None
class SecretCreate(BaseModel):
tenant_id: Optional[str] = None
secret_key: str
secret_value: str
description: Optional[str] = None
class SecretUpdate(BaseModel):
secret_value: Optional[str] = None
description: Optional[str] = None
class TestScriptRequest(BaseModel):
script_content: str
tenant_id: Optional[str] = None
params: Optional[dict] = None
# ==================== Static Routes (must be before dynamic routes) ====================
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取SDK文档"""
return {
"functions": [
{
"name": "log",
"signature": "log(message: str, level: str = 'INFO')",
"description": "记录日志",
"example": "log('处理完成', 'INFO')"
},
{
"name": "print",
"signature": "print(*args)",
"description": "打印输出",
"example": "print('Hello', 'World')"
},
{
"name": "ai",
"signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)",
"description": "调用AI模型",
"example": "result = ai('生成一段问候语', system='你是友善的助手')"
},
{
"name": "dingtalk",
"signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)",
"description": "发送钉钉消息",
"example": "dingtalk(webhook_url, '# 标题\\n内容')"
},
{
"name": "wecom",
"signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')",
"description": "发送企微消息",
"example": "wecom(webhook_url, '消息内容')"
},
{
"name": "http_get",
"signature": "http_get(url: str, headers: dict = None, params: dict = None)",
"description": "发起GET请求",
"example": "resp = http_get('https://api.example.com/data')"
},
{
"name": "http_post",
"signature": "http_post(url: str, data: any = None, headers: dict = None)",
"description": "发起POST请求",
"example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})"
},
{
"name": "db_query",
"signature": "db_query(sql: str, params: dict = None)",
"description": "执行只读SQL查询",
"example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})"
},
{
"name": "get_var",
"signature": "get_var(key: str, default: any = None)",
"description": "获取持久化变量",
"example": "counter = get_var('counter', 0)"
},
{
"name": "set_var",
"signature": "set_var(key: str, value: any)",
"description": "设置持久化变量",
"example": "set_var('counter', counter + 1)"
},
{
"name": "del_var",
"signature": "del_var(key: str)",
"description": "删除持久化变量",
"example": "del_var('temp_data')"
},
{
"name": "get_param",
"signature": "get_param(key: str, default: any = None)",
"description": "获取任务参数",
"example": "prompt = get_param('prompt', '默认提示词')"
},
{
"name": "get_params",
"signature": "get_params()",
"description": "获取所有任务参数",
"example": "params = get_params()"
},
{
"name": "get_tenants",
"signature": "get_tenants(app_code: str = None)",
"description": "获取租户列表",
"example": "tenants = get_tenants('notification-service')"
},
{
"name": "get_tenant_config",
"signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)",
"description": "获取租户的应用配置",
"example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')"
},
{
"name": "get_all_tenant_configs",
"signature": "get_all_tenant_configs(app_code: str)",
"description": "获取所有租户的应用配置",
"example": "configs = get_all_tenant_configs('notification-service')"
},
{
"name": "get_secret",
"signature": "get_secret(key: str)",
"description": "获取密钥(优先租户级)",
"example": "api_key = get_secret('api_key')"
}
],
"variables": [
{"name": "task_id", "description": "当前任务ID"},
{"name": "tenant_id", "description": "当前租户ID"},
{"name": "trace_id", "description": "当前执行追踪ID"}
],
"libraries": [
{"name": "json", "description": "JSON处理"},
{"name": "re", "description": "正则表达式"},
{"name": "math", "description": "数学函数"},
{"name": "random", "description": "随机数"},
{"name": "hashlib", "description": "哈希函数"},
{"name": "base64", "description": "Base64编解码"},
{"name": "datetime", "description": "日期时间处理"},
{"name": "timedelta", "description": "时间差"},
{"name": "urlencode/quote/unquote", "description": "URL编码"}
]
}
@router.post("/test-script")
async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)):
"""测试脚本执行"""
executor = ScriptExecutor(db)
result = executor.test_script(
script_content=data.script_content,
task_id=0,
tenant_id=data.tenant_id,
params=data.params
)
return result
@router.get("/secrets")
async def list_secrets(
tenant_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""获取密钥列表"""
query = db.query(Secret)
if tenant_id:
query = query.filter(Secret.tenant_id == tenant_id)
items = query.order_by(desc(Secret.created_at)).all()
return {
"items": [
{
"id": s.id,
"tenant_id": s.tenant_id,
"secret_key": s.secret_key,
"description": s.description,
"created_at": s.created_at,
"updated_at": s.updated_at
}
for s in items
]
}
@router.post("/secrets")
async def create_secret(data: SecretCreate, db: Session = Depends(get_db)):
"""创建密钥"""
secret = Secret(
tenant_id=data.tenant_id,
secret_key=data.secret_key,
secret_value=data.secret_value,
description=data.description
)
db.add(secret)
db.commit()
db.refresh(secret)
return {"success": True, "id": secret.id}
# ==================== Task CRUD ====================
@router.get("")
async def list_tasks(
tenant_id: Optional[str] = None,
status: Optional[int] = None,
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""获取任务列表"""
query = db.query(ScheduledTask)
if tenant_id:
query = query.filter(ScheduledTask.tenant_id == tenant_id)
if status is not None:
is_enabled = status == 1
query = query.filter(ScheduledTask.is_enabled == is_enabled)
total = query.count()
items = query.order_by(desc(ScheduledTask.created_at)).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"items": [format_task(t) for t in items]
}
@router.get("/{task_id}")
async def get_task(task_id: int, db: Session = Depends(get_db)):
"""获取任务详情"""
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return format_task(task, include_content=True)
@router.post("")
async def create_task(data: TaskCreate, db: Session = Depends(get_db)):
"""创建任务"""
task = ScheduledTask(
tenant_id=data.tenant_id,
task_name=data.task_name,
task_desc=data.task_desc,
execution_type=data.execution_type,
schedule_type=data.schedule_type,
time_points=data.time_points,
cron_expression=data.cron_expression,
webhook_url=data.webhook_url,
script_content=data.script_content,
input_params=data.input_params,
retry_count=data.retry_count,
retry_interval=data.retry_interval,
alert_on_failure=data.alert_on_failure,
alert_webhook=data.alert_webhook,
notify_channels=data.notify_channels,
notify_wecom_app_id=data.notify_wecom_app_id,
is_enabled=True
)
db.add(task)
db.commit()
db.refresh(task)
# 添加到调度器
scheduler_service.add_task(task.id)
return {"success": True, "id": task.id}
@router.put("/{task_id}")
async def update_task(task_id: int, data: TaskUpdate, db: Session = Depends(get_db)):
"""更新任务"""
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
# 更新字段
if data.tenant_id is not None:
task.tenant_id = data.tenant_id
if data.task_name is not None:
task.task_name = data.task_name
if data.task_desc is not None:
task.task_desc = data.task_desc
if data.execution_type is not None:
task.execution_type = data.execution_type
if data.schedule_type is not None:
task.schedule_type = data.schedule_type
if data.time_points is not None:
task.time_points = data.time_points
if data.cron_expression is not None:
task.cron_expression = data.cron_expression
if data.webhook_url is not None:
task.webhook_url = data.webhook_url
if data.script_content is not None:
task.script_content = data.script_content
if data.input_params is not None:
task.input_params = data.input_params
if data.retry_count is not None:
task.retry_count = data.retry_count
if data.retry_interval is not None:
task.retry_interval = data.retry_interval
if data.alert_on_failure is not None:
task.alert_on_failure = data.alert_on_failure
if data.alert_webhook is not None:
task.alert_webhook = data.alert_webhook
if data.notify_channels is not None:
task.notify_channels = data.notify_channels
if data.notify_wecom_app_id is not None:
task.notify_wecom_app_id = data.notify_wecom_app_id
if data.is_enabled is not None:
task.is_enabled = data.is_enabled
db.commit()
# 更新调度器
if task.is_enabled:
scheduler_service.add_task(task.id)
else:
scheduler_service.remove_task(task.id)
return {"success": True}
@router.delete("/{task_id}")
async def delete_task(task_id: int, db: Session = Depends(get_db)):
"""删除任务"""
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
# 从调度器移除
scheduler_service.remove_task(task_id)
# 删除相关日志
db.query(TaskLog).filter(TaskLog.task_id == task_id).delete()
db.delete(task)
db.commit()
return {"success": True}
@router.post("/{task_id}/toggle")
async def toggle_task(task_id: int, db: Session = Depends(get_db)):
"""启用/禁用任务"""
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
task.is_enabled = not task.is_enabled
db.commit()
if task.is_enabled:
scheduler_service.add_task(task.id)
else:
scheduler_service.remove_task(task.id)
return {"success": True, "status": 1 if task.is_enabled else 0}
@router.post("/{task_id}/run")
async def run_task(task_id: int, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
"""立即执行任务"""
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
result = await scheduler_service.run_task_now(task_id)
return result
# ==================== Task Logs ====================
@router.get("/{task_id}/logs")
async def get_task_logs(
task_id: int,
status: Optional[str] = None,
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""获取任务执行日志"""
query = db.query(TaskLog).filter(TaskLog.task_id == task_id)
if status:
query = query.filter(TaskLog.status == status)
total = query.count()
items = query.order_by(desc(TaskLog.started_at)).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"items": [format_log(log) for log in items]
}
# ==================== Secrets (dynamic routes) ====================
@router.put("/secrets/{secret_id}")
async def update_secret(secret_id: int, data: SecretUpdate, db: Session = Depends(get_db)):
"""更新密钥"""
secret = db.query(Secret).filter(Secret.id == secret_id).first()
if not secret:
raise HTTPException(status_code=404, detail="密钥不存在")
if data.secret_value is not None:
secret.secret_value = data.secret_value
if data.description is not None:
secret.description = data.description
db.commit()
return {"success": True}
@router.delete("/secrets/{secret_id}")
async def delete_secret(secret_id: int, db: Session = Depends(get_db)):
"""删除密钥"""
secret = db.query(Secret).filter(Secret.id == secret_id).first()
if not secret:
raise HTTPException(status_code=404, detail="密钥不存在")
db.delete(secret)
db.commit()
return {"success": True}
# ==================== Helpers ====================
def format_task(task: ScheduledTask, include_content: bool = False) -> dict:
"""格式化任务数据"""
time_points = task.time_points
if isinstance(time_points, str):
try:
time_points = json.loads(time_points)
except:
time_points = []
# 处理 notify_channels
notify_channels = task.notify_channels
if isinstance(notify_channels, str):
try:
notify_channels = json.loads(notify_channels)
except:
notify_channels = []
data = {
"id": task.id,
"tenant_id": task.tenant_id,
"task_name": task.task_name,
"task_type": task.execution_type, # 前端使用 task_type
"schedule_type": task.schedule_type,
"time_points": time_points or [],
"cron_expression": task.cron_expression,
"status": 1 if task.is_enabled else 0, # 前端使用 status
"last_run_at": task.last_run_at,
"last_run_status": task.last_run_status,
"retry_count": task.retry_count,
"retry_interval": task.retry_interval,
"alert_on_failure": bool(task.alert_on_failure),
"alert_webhook": task.alert_webhook,
"notify_channels": notify_channels or [],
"notify_wecom_app_id": task.notify_wecom_app_id,
"created_at": task.created_at,
"updated_at": task.updated_at
}
if include_content:
data["webhook_url"] = task.webhook_url
data["script_content"] = task.script_content
input_params = task.input_params
if isinstance(input_params, str):
try:
input_params = json.loads(input_params)
except:
input_params = None
data["input_params"] = input_params
return data
def format_log(log: TaskLog) -> dict:
"""格式化日志数据"""
return {
"id": log.id,
"task_id": log.task_id,
"tenant_id": log.tenant_id,
"trace_id": log.trace_id,
"status": log.status,
"started_at": log.started_at,
"finished_at": log.finished_at,
"duration_ms": log.duration_ms,
"output": log.output,
"error": log.error,
"retry_count": log.retry_count,
"created_at": log.created_at
}