zcbot/web/routers/schedules.py

106 lines
4.1 KiB
Python

"""定时任务路由(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
],
}