"""定时任务路由(DESIGN §8.5)。 前端只读展示 + 停用/删除两个便捷动作;建/改全走对话(schedule_* 工具)。 与对话工具共用 core.scheduler 服务层,两条路径不漂移。 """ from __future__ import annotations from uuid import UUID from fastapi import Depends, HTTPException from sqlalchemy import func, select from core.storage import session_scope from core.storage.models import Message, Task from core.storage.usage_report import task_usage_aggregates as usage_aggregates from ..common import task_dict from ..schemas import SchedulePatchRequest def register_schedule_routes(app, *, require_user) -> None: @app.get("/v1/schedules", tags=["schedules"]) def list_schedules(user_id: UUID = Depends(require_user)): """列当前用户的定时任务(只读)。前端「定时」面板一次拉满。""" from core import scheduler return {"results": scheduler.list_jobs(user_id)} @app.patch("/v1/schedules/{job_id}", tags=["schedules"]) def patch_schedule( job_id: str, body: SchedulePatchRequest, user_id: UUID = Depends(require_user), ): """改定时任务 —— 前端只用来停用/启用(enabled)。其余编辑走对话。""" from core import scheduler if body.enabled is None: raise HTTPException(400, "no fields to update") try: return scheduler.set_enabled(user_id, job_id, body.enabled) except scheduler.JobError as e: raise HTTPException(404, str(e)) @app.delete("/v1/schedules/{job_id}", status_code=204, tags=["schedules"]) def delete_schedule(job_id: str, user_id: UUID = Depends(require_user)): """删定时任务(软删,立即停止触发)。""" from core import scheduler try: scheduler.cancel_job(user_id, job_id) except scheduler.JobError as e: raise HTTPException(404, str(e)) @app.get("/v1/schedules/{job_id}/tasks", tags=["schedules"]) def list_schedule_tasks( job_id: str, page: int = 1, page_size: int = 20, user_id: UUID = Depends(require_user), ): """列某定时任务的历史执行 task(归属 scheduled_job_id),按 created_at 倒序分页。 isolated 模式每次触发新建一个 task,这些 task 不进普通 /v1/tasks 列表 (scheduled_job_id 过滤),只能经此端点回看;persistent 模式始终只有绑定的那一条。 返回标准分页壳 `{page, page_size, count, results}`(results 同 /v1/tasks,复用 task_dict)。 user_id 过滤天然隔离他人 job —— 非本人 / 非法 job_id 一律返回空列表。 """ page = max(1, page) page_size = max(1, min(page_size, 100)) try: jid = UUID(str(job_id).strip()) except (ValueError, AttributeError): return {"page": page, "page_size": page_size, "count": 0, "results": []} conditions = [ Task.user_id == user_id, Task.deleted_at.is_(None), Task.scheduled_job_id == jid, ] offset = (page - 1) * page_size with session_scope() as s: cnt = s.execute( select(func.count()).select_from(Task).where(*conditions) ).scalar_one() or 0 rows = s.execute( select(Task).where(*conditions) .order_by(Task.created_at.desc()) .limit(page_size).offset(offset) ).scalars().all() tids = [r.task_id for r in rows] msg_counts = ( dict(s.execute( select(Message.task_id, func.count()) .where(Message.task_id.in_(tids)) .group_by(Message.task_id) ).all()) if tids else {} ) usage = usage_aggregates(s, tids) return { "page": page, "page_size": page_size, "count": int(cnt), "results": [ task_dict(r, n_messages=msg_counts.get(r.task_id, 0), usage=usage.get(r.task_id)) for r in rows ], }