feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# daily-news - Daily News 系统服务管理包装脚本
|
||||
# 由 scripts/install.sh 自动生成;项目路径:{{PROJECT_DIR}}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TIMERS=(dnews-fetch.timer dnews-push.timer)
|
||||
SERVICES=(dnews-fetch.service dnews-push.service)
|
||||
PROJECT_DIR="{{PROJECT_DIR}}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
用法: daily-news <command>
|
||||
|
||||
命令:
|
||||
start 启动 fetch 和 push timer
|
||||
stop 停止两个 timer
|
||||
restart 重启两个 timer(不会立即触发 service,仅重置调度)
|
||||
status [N] 查看 timer / service 状态 + 最近 N 行日志(默认 15)
|
||||
logs 实时跟随日志(Ctrl+C 退出)
|
||||
|
||||
其他操作:
|
||||
$PROJECT_DIR/scripts/install.sh 重新生成单元并安装(改 config.json 后用)
|
||||
$PROJECT_DIR/scripts/uninstall.sh 完全卸载(不删数据)
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
start)
|
||||
sudo systemctl start "${TIMERS[@]}"
|
||||
echo "✓ Daily News 已启动"
|
||||
;;
|
||||
stop)
|
||||
sudo systemctl stop "${TIMERS[@]}"
|
||||
echo "✓ Daily News 已停止"
|
||||
;;
|
||||
restart)
|
||||
sudo systemctl restart "${TIMERS[@]}"
|
||||
echo "✓ Daily News 已重启"
|
||||
;;
|
||||
status)
|
||||
lines="${2:-15}"
|
||||
echo "═══ Timer ═══"
|
||||
systemctl list-timers 'dnews-*' --no-pager || true
|
||||
echo ""
|
||||
echo "═══ Service 状态 ═══"
|
||||
systemctl status "${SERVICES[@]}" --no-pager --lines=0 || true
|
||||
echo ""
|
||||
echo "═══ 最近 $lines 行日志 ═══"
|
||||
journalctl --namespace=dnews -u dnews-fetch -u dnews-push -n "$lines" --no-pager
|
||||
echo ""
|
||||
echo "💡 实时跟随:daily-news logs"
|
||||
;;
|
||||
logs)
|
||||
journalctl --namespace=dnews -u dnews-fetch -u dnews-push -f
|
||||
;;
|
||||
-h|--help|help|"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "❌ 未知命令: $cmd" >&2
|
||||
echo ""
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# Daily News - 系统服务一键安装
|
||||
# 用法:./scripts/install.sh
|
||||
# 不要用 sudo 直接调用本脚本;脚本会在需要时自行 sudo 提权。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# ─── 用户检测 ────────────────────────────────────────────────
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
if [[ -z "${SUDO_USER:-}" ]]; then
|
||||
echo "❌ 请勿直接用 root 运行本脚本,请用普通用户(脚本按需自动提权)"
|
||||
exit 1
|
||||
fi
|
||||
RUN_USER="$SUDO_USER"
|
||||
else
|
||||
RUN_USER="$USER"
|
||||
fi
|
||||
RUN_GROUP="$(id -gn "$RUN_USER")"
|
||||
|
||||
# ─── uv 检测 ─────────────────────────────────────────────────
|
||||
UV_BIN="$(command -v uv || true)"
|
||||
if [[ -z "$UV_BIN" ]]; then
|
||||
echo "❌ 找不到 uv 命令,请先安装:https://docs.astral.sh/uv/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── systemd 检测 ────────────────────────────────────────────
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo "❌ 系统未检测到 systemctl(非 systemd 系统),本脚本仅支持 systemd Linux"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "🚀 Daily News - 系统服务安装"
|
||||
echo "────────────────────────────────────────"
|
||||
echo " 项目目录: $PROJECT_DIR"
|
||||
echo " 运行身份: $RUN_USER:$RUN_GROUP"
|
||||
echo " uv 路径: $UV_BIN"
|
||||
echo "────────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# 提前提权一次,后续 sudo 命令直接复用凭据缓存
|
||||
echo "🔐 需要 sudo 权限来写入 /etc/systemd/system/"
|
||||
sudo -v
|
||||
echo ""
|
||||
|
||||
# ─── [1/6] 同步依赖 ──────────────────────────────────────────
|
||||
echo "📦 [1/6] 同步依赖 (uv sync)..."
|
||||
uv sync
|
||||
echo "✓ 依赖已就绪"
|
||||
echo ""
|
||||
|
||||
# ─── [2/6] 验证 .env ────────────────────────────────────────
|
||||
echo "🔐 [2/6] 验证 .env..."
|
||||
if [[ ! -f "$PROJECT_DIR/.env" ]]; then
|
||||
echo "❌ .env 不存在,请先复制 .env.example 并填入 API Key"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ .env 存在"
|
||||
echo ""
|
||||
|
||||
# ─── [3/6] LLM 健康检查 ─────────────────────────────────────
|
||||
echo "🔍 [3/6] 校验 LLM 接口(仅在此处校验,运行时不再校验)..."
|
||||
if ! uv run python -m src.main check; then
|
||||
echo ""
|
||||
echo "❌ LLM 校验失败,已中止安装"
|
||||
echo "请检查 config.json 中的 llm.baseUrl/model 以及 .env 中的 API Key"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ─── [4/6] 生成单元文件 ─────────────────────────────────────
|
||||
echo "📝 [4/6] 渲染 systemd 单元模板..."
|
||||
STAGE_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE_DIR"' EXIT
|
||||
|
||||
uv run python "$PROJECT_DIR/scripts/_gen_units.py" \
|
||||
--project-dir "$PROJECT_DIR" \
|
||||
--user "$RUN_USER" \
|
||||
--group "$RUN_GROUP" \
|
||||
--uv-bin "$UV_BIN" \
|
||||
--output-dir "$STAGE_DIR"
|
||||
|
||||
# 同时渲染 daily-news 包装脚本(/usr/local/bin/daily-news)
|
||||
sed "s|{{PROJECT_DIR}}|$PROJECT_DIR|g" \
|
||||
"$PROJECT_DIR/scripts/daily-news.tmpl" > "$STAGE_DIR/daily-news"
|
||||
echo " ✓ daily-news"
|
||||
echo ""
|
||||
|
||||
# ─── [5/6] 安装到 systemd ───────────────────────────────────
|
||||
echo "📥 [5/6] 安装到系统..."
|
||||
sudo install -m 644 "$STAGE_DIR/dnews-fetch.service" /etc/systemd/system/
|
||||
sudo install -m 644 "$STAGE_DIR/dnews-fetch.timer" /etc/systemd/system/
|
||||
sudo install -m 644 "$STAGE_DIR/dnews-push.service" /etc/systemd/system/
|
||||
sudo install -m 644 "$STAGE_DIR/dnews-push.timer" /etc/systemd/system/
|
||||
|
||||
# 日志保留策略(journald 命名空间 drop-in)
|
||||
sudo mkdir -p /etc/systemd/journald@dnews.conf.d
|
||||
sudo install -m 644 "$STAGE_DIR/journald-dnews.conf" \
|
||||
/etc/systemd/journald@dnews.conf.d/retention.conf
|
||||
|
||||
# daily-news 包装脚本到系统 PATH
|
||||
sudo install -m 755 "$STAGE_DIR/daily-news" /usr/local/bin/daily-news
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 命名空间 journald 实例首次会在 fetch/push 首次产出日志时自动拉起;
|
||||
# 若已存在(重装场景),重启以加载新保留策略。
|
||||
sudo systemctl restart systemd-journald@dnews.service 2>/dev/null || true
|
||||
|
||||
echo "✓ 单元文件 + daily-news 包装脚本已安装"
|
||||
echo ""
|
||||
|
||||
# ─── [6/6] 启用并启动 timer ─────────────────────────────────
|
||||
echo "🚦 [6/6] 启用并(重)启动 timer..."
|
||||
# 先 enable 注册开机自启
|
||||
sudo systemctl enable dnews-fetch.timer dnews-push.timer
|
||||
# 再 restart 强制按新单元文件重置内存中的 timer 状态
|
||||
# (enable --now 对已运行的 timer 是 no-op,无法应用新 OnCalendar/OnActiveSec 等改动)
|
||||
sudo systemctl restart dnews-fetch.timer dnews-push.timer
|
||||
echo ""
|
||||
|
||||
# ─── 完成 ───────────────────────────────────────────────────
|
||||
echo "✅ 安装完成!"
|
||||
echo ""
|
||||
echo "下次触发时间:"
|
||||
systemctl list-timers 'dnews-*' --no-pager || true
|
||||
echo ""
|
||||
echo "常用命令(系统 PATH 中可直接调用):"
|
||||
echo " daily-news status 查看状态 + 最近日志"
|
||||
echo " daily-news logs 实时跟随日志"
|
||||
echo " daily-news stop 停止"
|
||||
echo " daily-news start 启动"
|
||||
echo " daily-news restart 重启 timer"
|
||||
echo ""
|
||||
echo "手动立即触发一次任务(不影响下次调度):"
|
||||
echo " sudo systemctl start dnews-fetch.service"
|
||||
echo " sudo systemctl start dnews-push.service"
|
||||
echo ""
|
||||
echo "卸载:$PROJECT_DIR/scripts/uninstall.sh"
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Daily News - 状态查看
|
||||
# 显示:timer 下次触发时间、service 上次结果、最近日志
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LINES="${1:-15}"
|
||||
|
||||
echo "═══ Timer ═══"
|
||||
systemctl list-timers 'dnews-*' --no-pager || true
|
||||
echo ""
|
||||
|
||||
echo "═══ Service 状态 ═══"
|
||||
systemctl status dnews-fetch.service --no-pager --lines=0 || true
|
||||
echo "──────────────────────────────"
|
||||
systemctl status dnews-push.service --no-pager --lines=0 || true
|
||||
echo ""
|
||||
|
||||
echo "═══ 最近 $LINES 行日志 ═══"
|
||||
journalctl --namespace=dnews \
|
||||
-u dnews-fetch.service \
|
||||
-u dnews-push.service \
|
||||
-n "$LINES" \
|
||||
--no-pager
|
||||
echo ""
|
||||
echo "💡 实时跟随:journalctl --namespace=dnews -f"
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Daily News - 卸载系统服务(不删数据)
|
||||
set -euo pipefail
|
||||
|
||||
echo "🗑 Daily News - 卸载"
|
||||
echo ""
|
||||
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo "❌ 系统未检测到 systemctl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🛑 停止并禁用 timer..."
|
||||
sudo systemctl disable --now dnews-fetch.timer dnews-push.timer 2>/dev/null || true
|
||||
|
||||
echo "🧹 清理单元文件..."
|
||||
sudo rm -f /etc/systemd/system/dnews-fetch.service \
|
||||
/etc/systemd/system/dnews-fetch.timer \
|
||||
/etc/systemd/system/dnews-push.service \
|
||||
/etc/systemd/system/dnews-push.timer
|
||||
|
||||
echo "🧹 清理日志保留策略 drop-in..."
|
||||
sudo rm -f /etc/systemd/journald@dnews.conf.d/retention.conf
|
||||
sudo rmdir /etc/systemd/journald@dnews.conf.d 2>/dev/null || true
|
||||
|
||||
echo "🧹 清理 daily-news 包装脚本..."
|
||||
sudo rm -f /usr/local/bin/daily-news
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl stop systemd-journald@dnews.service 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "✅ 已卸载(news-data/ 与项目代码未删除)"
|
||||
echo ""
|
||||
echo "如需清理历史日志:sudo journalctl --namespace=dnews --vacuum-time=1s"
|
||||
Reference in New Issue
Block a user