22d5d21c1f
单次执行不再出现「重复方式」;「每天」改为「当天」;循环模式下拉宽度收窄。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
5.1 KiB
Python
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("custom cron empty")
|
|
parts = raw.split()
|
|
if len(parts) != 5:
|
|
raise ValueError("cron must be 5 fields")
|
|
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"unknown cycle type: {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 "—"
|