mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
175 lines
7.0 KiB
Python
175 lines
7.0 KiB
Python
import traceback
|
|
from datetime import datetime
|
|
|
|
from quart import jsonify, request
|
|
|
|
from astrbot.core import logger
|
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
|
|
from .route import Response, Route, RouteContext
|
|
|
|
|
|
class CronRoute(Route):
|
|
def __init__(
|
|
self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle
|
|
) -> None:
|
|
super().__init__(context)
|
|
self.core_lifecycle = core_lifecycle
|
|
self.routes = [
|
|
("/cron/jobs", ("GET", self.list_jobs)),
|
|
("/cron/jobs", ("POST", self.create_job)),
|
|
("/cron/jobs/<job_id>", ("PATCH", self.update_job)),
|
|
("/cron/jobs/<job_id>", ("DELETE", self.delete_job)),
|
|
]
|
|
self.register_routes()
|
|
|
|
def _serialize_job(self, job) -> dict:
|
|
data = job.model_dump() if hasattr(job, "model_dump") else job.__dict__
|
|
for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]:
|
|
if isinstance(data.get(k), datetime):
|
|
data[k] = data[k].isoformat()
|
|
# expose note explicitly for UI (prefer payload.note then description)
|
|
payload = data.get("payload") or {}
|
|
data["note"] = payload.get("note") or data.get("description") or ""
|
|
data["run_at"] = payload.get("run_at")
|
|
data["run_once"] = data.get("run_once", False)
|
|
# status is internal; hide to avoid implying one-time completion for recurring jobs
|
|
data.pop("status", None)
|
|
return data
|
|
|
|
async def list_jobs(self):
|
|
try:
|
|
cron_mgr = self.core_lifecycle.cron_manager
|
|
if cron_mgr is None:
|
|
return jsonify(
|
|
Response().error("Cron manager not initialized").__dict__
|
|
)
|
|
job_type = request.args.get("type")
|
|
jobs = await cron_mgr.list_jobs(job_type)
|
|
data = [self._serialize_job(j) for j in jobs]
|
|
return jsonify(Response().ok(data=data).__dict__)
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return jsonify(Response().error(f"Failed to list jobs: {e!s}").__dict__)
|
|
|
|
async def create_job(self):
|
|
try:
|
|
cron_mgr = self.core_lifecycle.cron_manager
|
|
if cron_mgr is None:
|
|
return jsonify(
|
|
Response().error("Cron manager not initialized").__dict__
|
|
)
|
|
|
|
payload = await request.json
|
|
if not isinstance(payload, dict):
|
|
return jsonify(Response().error("Invalid payload").__dict__)
|
|
|
|
name = payload.get("name") or "active_agent_task"
|
|
cron_expression = payload.get("cron_expression")
|
|
note = payload.get("note") or payload.get("description") or name
|
|
session = payload.get("session")
|
|
persona_id = payload.get("persona_id")
|
|
provider_id = payload.get("provider_id")
|
|
timezone = payload.get("timezone")
|
|
enabled = bool(payload.get("enabled", True))
|
|
run_once = bool(payload.get("run_once", False))
|
|
run_at = payload.get("run_at")
|
|
|
|
if not session:
|
|
return jsonify(Response().error("session is required").__dict__)
|
|
if run_once and not run_at:
|
|
return jsonify(
|
|
Response().error("run_at is required when run_once=true").__dict__
|
|
)
|
|
if (not run_once) and not cron_expression:
|
|
return jsonify(
|
|
Response()
|
|
.error("cron_expression is required when run_once=false")
|
|
.__dict__
|
|
)
|
|
if run_once and cron_expression:
|
|
cron_expression = None # ignore cron when run_once specified
|
|
run_at_dt = None
|
|
if run_at:
|
|
try:
|
|
run_at_dt = datetime.fromisoformat(str(run_at))
|
|
except Exception:
|
|
return jsonify(
|
|
Response().error("run_at must be ISO datetime").__dict__
|
|
)
|
|
|
|
job_payload = {
|
|
"session": session,
|
|
"note": note,
|
|
"persona_id": persona_id,
|
|
"provider_id": provider_id,
|
|
"run_at": run_at,
|
|
"origin": "api",
|
|
}
|
|
|
|
job = await cron_mgr.add_active_job(
|
|
name=name,
|
|
cron_expression=cron_expression,
|
|
payload=job_payload,
|
|
description=note,
|
|
timezone=timezone,
|
|
enabled=enabled,
|
|
run_once=run_once,
|
|
run_at=run_at_dt,
|
|
)
|
|
|
|
return jsonify(Response().ok(data=self._serialize_job(job)).__dict__)
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return jsonify(Response().error(f"Failed to create job: {e!s}").__dict__)
|
|
|
|
async def update_job(self, job_id: str):
|
|
try:
|
|
cron_mgr = self.core_lifecycle.cron_manager
|
|
if cron_mgr is None:
|
|
return jsonify(
|
|
Response().error("Cron manager not initialized").__dict__
|
|
)
|
|
|
|
payload = await request.json
|
|
if not isinstance(payload, dict):
|
|
return jsonify(Response().error("Invalid payload").__dict__)
|
|
|
|
updates = {
|
|
"name": payload.get("name"),
|
|
"cron_expression": payload.get("cron_expression"),
|
|
"description": payload.get("description"),
|
|
"enabled": payload.get("enabled"),
|
|
"timezone": payload.get("timezone"),
|
|
"run_once": payload.get("run_once"),
|
|
"payload": payload.get("payload"),
|
|
}
|
|
# remove None values to avoid unwanted resets
|
|
updates = {k: v for k, v in updates.items() if v is not None}
|
|
if "run_at" in payload:
|
|
updates.setdefault("payload", {})
|
|
if updates["payload"] is None:
|
|
updates["payload"] = {}
|
|
updates["payload"]["run_at"] = payload.get("run_at")
|
|
|
|
job = await cron_mgr.update_job(job_id, **updates)
|
|
if not job:
|
|
return jsonify(Response().error("Job not found").__dict__)
|
|
return jsonify(Response().ok(data=self._serialize_job(job)).__dict__)
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return jsonify(Response().error(f"Failed to update job: {e!s}").__dict__)
|
|
|
|
async def delete_job(self, job_id: str):
|
|
try:
|
|
cron_mgr = self.core_lifecycle.cron_manager
|
|
if cron_mgr is None:
|
|
return jsonify(
|
|
Response().error("Cron manager not initialized").__dict__
|
|
)
|
|
await cron_mgr.delete_job(job_id)
|
|
return jsonify(Response().ok(message="deleted").__dict__)
|
|
except Exception as e:
|
|
logger.error(traceback.format_exc())
|
|
return jsonify(Response().error(f"Failed to delete job: {e!s}").__dict__)
|