"""定时任务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_type: str = 'script' schedule_type: str = 'simple' time_points: Optional[List[str]] = None cron_expression: Optional[str] = None webhook_url: Optional[str] = None webhook_method: Optional[str] = 'POST' webhook_headers: Optional[dict] = None script_content: Optional[str] = None script_timeout: Optional[int] = 300 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 class TaskUpdate(BaseModel): tenant_id: Optional[str] = None task_name: Optional[str] = None task_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 webhook_method: Optional[str] = None webhook_headers: Optional[dict] = None script_content: Optional[str] = None script_timeout: Optional[int] = 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 status: 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 # ==================== 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: query = query.filter(ScheduledTask.status == status) 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_type=data.task_type, schedule_type=data.schedule_type, time_points=json.dumps(data.time_points) if data.time_points else None, cron_expression=data.cron_expression, webhook_url=data.webhook_url, webhook_method=data.webhook_method, webhook_headers=json.dumps(data.webhook_headers) if data.webhook_headers else None, script_content=data.script_content, script_timeout=data.script_timeout, input_params=json.dumps(data.input_params) if data.input_params else None, retry_count=data.retry_count, retry_interval=data.retry_interval, alert_on_failure=1 if data.alert_on_failure else 0, alert_webhook=data.alert_webhook, status=1 ) 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_type is not None: task.task_type = data.task_type if data.schedule_type is not None: task.schedule_type = data.schedule_type if data.time_points is not None: task.time_points = json.dumps(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.webhook_method is not None: task.webhook_method = data.webhook_method if data.webhook_headers is not None: task.webhook_headers = json.dumps(data.webhook_headers) if data.script_content is not None: task.script_content = data.script_content if data.script_timeout is not None: task.script_timeout = data.script_timeout if data.input_params is not None: task.input_params = json.dumps(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 = 1 if data.alert_on_failure else 0 if data.alert_webhook is not None: task.alert_webhook = data.alert_webhook if data.status is not None: task.status = data.status db.commit() # 更新调度器 if task.status == 1: 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.status = 0 if task.status == 1 else 1 db.commit() if task.status == 1: scheduler_service.add_task(task.id) else: scheduler_service.remove_task(task.id) return {"success": True, "status": task.status} @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] } # ==================== Script Testing ==================== @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 # ==================== SDK Documentation ==================== @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编码"} ] } # ==================== Secrets ==================== @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} @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: """格式化任务数据""" data = { "id": task.id, "tenant_id": task.tenant_id, "task_name": task.task_name, "task_type": task.task_type, "schedule_type": task.schedule_type, "time_points": json.loads(task.time_points) if task.time_points else [], "cron_expression": task.cron_expression, "status": task.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, "created_at": task.created_at, "updated_at": task.updated_at } if include_content: data["webhook_url"] = task.webhook_url data["webhook_method"] = task.webhook_method data["webhook_headers"] = json.loads(task.webhook_headers) if task.webhook_headers else None data["script_content"] = task.script_content data["script_timeout"] = task.script_timeout data["input_params"] = json.loads(task.input_params) if task.input_params else None 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 }