Files
Nexus/server/utils/schedule_cycle.py
2026-07-08 22:31:31 +08:00

151 lines
5.1 KiB
Python

"""Schedule cycle ↔ 5-field cron (Baota-compatible)."""
from __future__ import annotations
from typing import Any, TypedDict
class ScheduleCycleConfig(TypedDict, total=False):
type: str
minute: int
hour: int
interval: int
week: int
monthDay: int
customCron: str
def _clamp_int(n: int | float, lo: int, hi: int) -> int:
try:
v = int(n)
except (TypeError, ValueError):
return lo
return max(lo, min(hi, v))
SCHEDULE_MIN_INTERVAL_MINUTES = 1
def build_cron_from_cycle(config: dict[str, Any]) -> str:
kind = config.get("type", "day")
if kind == "custom":
raw = str(config.get("customCron") or "").strip()
if not raw:
raise ValueError("自定义 cron 不能为空")
parts = raw.split()
if len(parts) != 5:
raise ValueError("cron 表达式必须为 5 个字段")
return raw
minute = _clamp_int(config.get("minute", 0), 0, 59)
hour = _clamp_int(config.get("hour", 0), 0, 23)
if kind == "day":
return f"{minute} {hour} * * *"
if kind == "day-n":
n = _clamp_int(config.get("interval", 1), 1, 365)
return f"{minute} {hour} */{n} * *"
if kind == "hour":
return f"{minute} * * * *"
if kind == "hour-n":
n = _clamp_int(config.get("interval", 1), 1, 23)
return f"{minute} */{n} * * *"
if kind == "minute-n":
n = _clamp_int(config.get("interval", 1), SCHEDULE_MIN_INTERVAL_MINUTES, 59)
return f"*/{n} * * * *"
if kind == "week":
w = _clamp_int(config.get("week", 0), 0, 6)
return f"{minute} {hour} * * {w}"
if kind == "month":
dom = _clamp_int(config.get("monthDay", 1), 1, 31)
return f"{minute} {hour} {dom} * *"
raise ValueError(f"未知的周期类型: {kind}")
def _parse_step_or_fixed(field: str) -> tuple[str, int | None]:
if field == "*":
return "star", None
if field.startswith("*/"):
step = int(field[2:])
return "step", step
return "fixed", int(field)
def parse_cron_to_cycle(cron: str) -> dict[str, Any]:
base: dict[str, Any] = {
"type": "day",
"minute": 0,
"hour": 2,
"interval": 2,
"week": 1,
"monthDay": 1,
"customCron": "",
}
expr = (cron or "").strip()
if not expr:
return base
parts = expr.split()
if len(parts) != 5:
return {**base, "type": "custom", "customCron": expr}
min_f, hour_f, dom_f, mon_f, dow_f = parts
if mon_f != "*":
return {**base, "type": "custom", "customCron": expr}
min_k, min_v = _parse_step_or_fixed(min_f)
hour_k, hour_v = _parse_step_or_fixed(hour_f)
dom_k, dom_v = _parse_step_or_fixed(dom_f)
dow_k, dow_v = _parse_step_or_fixed(dow_f)
if min_k == "step" and hour_k == "star" and dom_k == "star" and dow_k == "star":
return {**base, "type": "minute-n", "interval": min_v}
if min_k == "fixed" and hour_k == "star" and dom_k == "star" and dow_k == "star":
return {**base, "type": "hour", "minute": min_v}
if min_k == "fixed" and hour_k == "step" and dom_k == "star" and dow_k == "star":
return {**base, "type": "hour-n", "minute": min_v, "interval": hour_v}
if min_k == "fixed" and hour_k == "fixed" and dom_k == "star" and dow_k == "fixed":
w = 0 if dow_f == "7" else dow_v
return {**base, "type": "week", "minute": min_v, "hour": hour_v, "week": w}
if min_k == "fixed" and hour_k == "fixed" and dom_k == "fixed" and dow_k == "star":
return {**base, "type": "month", "minute": min_v, "hour": hour_v, "monthDay": dom_v}
if min_k == "fixed" and hour_k == "fixed" and dom_k == "step" and dow_k == "star":
return {**base, "type": "day-n", "minute": min_v, "hour": hour_v, "interval": dom_v}
if min_k == "fixed" and hour_k == "fixed" and dom_k == "star" and dow_k == "star":
return {**base, "type": "day", "minute": min_v, "hour": hour_v}
return {**base, "type": "custom", "customCron": expr}
_WEEK_LABELS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
def format_cycle_label(cron_or_config: str | dict[str, Any]) -> str:
config = parse_cron_to_cycle(cron_or_config) if isinstance(cron_or_config, str) else cron_or_config
kind = config.get("type", "day")
if kind == "custom":
cc = config.get("customCron") or ""
return f"自定义: {cc}" if cc else "—"
minute = int(config.get("minute", 0))
hour = int(config.get("hour", 0))
time_s = f"{hour:02d}:{minute:02d}"
if kind == "day":
return f"当天 {time_s}"
if kind == "day-n":
return f"每 {config.get('interval', 1)}{time_s}"
if kind == "hour":
return f"每小时 第 {minute} 分"
if kind == "hour-n":
return f"每 {config.get('interval', 1)} 小时 第 {minute} 分"
if kind == "minute-n":
return f"每 {config.get('interval', 1)} 分钟"
if kind == "week":
w = int(config.get("week", 0))
wd = _WEEK_LABELS[w] if 0 <= w < 7 else f"周{w}"
return f"每周{wd} {time_s}"
if kind == "month":
return f"每月 {config.get('monthDay', 1)}{time_s}"
return "—"