"""定时任务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 }