All checks were successful
continuous-integration/drone/push Build is passing
- 扩展 ToolConfig 配置类型,新增 external_api 类型
- 实现接口注册表,包含 90+ 睿美云开放接口定义
- 实现 TPOS SHA256WithRSA 签名鉴权
- 实现睿美云 API 客户端,支持多租户配置
- 新增代理路由 /api/ruimeiyun/call/{api_name}
- 支持接口权限控制和健康检查
560 lines
19 KiB
Python
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
|
|
}
|