Files
my-daily/ai-daily-main/scripts/_gen_units.py
T

114 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""根据 config.json 渲染 systemd 单元模板。
被 scripts/install.sh 调用。本身不直接读环境,需要的运行时参数(user/group/uv 路径)
通过 CLI 注入,便于 install.sh 处理 sudo 场景。
"""
import argparse
import json
import sys
from pathlib import Path
def cron_to_oncalendar(expr: str) -> str:
"""5 段 cron 表达式 → systemd OnCalendar 字符串。
本项目 push_cron 只用到 minute/hour,其他位必须为 `*`。
遇到不支持的语法直接报错,避免静默生成错误的 timer。
"""
parts = expr.split()
if len(parts) != 5:
raise ValueError(f"无效 cron 表达式(必须 5 段): {expr}")
minute, hour, dom, mon, dow = parts
for label, val in [("day-of-month", dom), ("month", mon), ("day-of-week", dow)]:
if val != "*":
raise ValueError(f"暂不支持非 * 的 {label} 字段: {expr}")
try:
m, h = int(minute), int(hour)
except ValueError as e:
raise ValueError(f"minute/hour 必须是整数: {expr}") from e
if not (0 <= m <= 59 and 0 <= h <= 23):
raise ValueError(f"minute/hour 越界: {expr}")
return f"*-*-* {h:02d}:{m:02d}:00"
def render(template_path: Path, variables: dict) -> str:
text = template_path.read_text(encoding="utf-8")
for key, val in variables.items():
text = text.replace(f"{{{{{key}}}}}", str(val))
return text
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--project-dir", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--group", required=True)
parser.add_argument("--uv-bin", required=True)
parser.add_argument("--output-dir", required=True)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
output_dir = Path(args.output_dir).resolve()
template_dir = project_dir / "systemd"
config_path = project_dir / "config.json"
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
fetch_min = config["schedule"]["fetch_interval_minutes"]
if not isinstance(fetch_min, int) or fetch_min <= 0:
raise ValueError(f"fetch_interval_minutes 必须是正整数: {fetch_min}")
push_crons = config["schedule"]["push_cron"]
if not push_crons:
raise ValueError("config.schedule.push_cron 不能为空")
push_lines = "\n".join(
f"OnCalendar={cron_to_oncalendar(c)}" for c in push_crons
)
log_retention = config.get("log", {}).get("retention_days", 7)
if not isinstance(log_retention, int) or log_retention <= 0:
raise ValueError(f"log.retention_days 必须是正整数: {log_retention}")
common = {
"PROJECT_DIR": str(project_dir),
"USER": args.user,
"GROUP": args.group,
"UV_BIN": args.uv_bin,
}
output_dir.mkdir(parents=True, exist_ok=True)
renderings = [
("dnews-fetch.service.tmpl", "dnews-fetch.service", {}),
("dnews-fetch.timer.tmpl", "dnews-fetch.timer",
{"FETCH_INTERVAL_MIN": fetch_min}),
("dnews-push.service.tmpl", "dnews-push.service", {}),
("dnews-push.timer.tmpl", "dnews-push.timer",
{"PUSH_ONCALENDAR_LINES": push_lines}),
("journald-dnews.conf.tmpl", "journald-dnews.conf",
{"LOG_RETENTION_DAYS": log_retention}),
]
print(f"📂 输出目录: {output_dir}")
for tpl_name, out_name, extra in renderings:
tpl_path = template_dir / tpl_name
if not tpl_path.exists():
print(f"❌ 模板不存在: {tpl_path}", file=sys.stderr)
return 2
rendered = render(tpl_path, {**common, **extra})
(output_dir / out_name).write_text(rendered, encoding="utf-8")
print(f" ✓ {out_name}")
print(f"\n生成参数:")
print(f" fetch 间隔 → 每 {fetch_min} 分钟(间隔触发,从上次完成开始计时)")
for c, line in zip(push_crons, push_lines.split("\n")):
print(f" push '{c}' → {line}")
print(f" 日志保留 → {log_retention} 天")
return 0
if __name__ == "__main__":
sys.exit(main())