Files
server-deploy/docs/superpowers/plans/2026-08-06-xray-restart-schedule.md
2026-08-07 11:47:52 +08:00

42 KiB
Raw Permalink Blame History

Xray 定时重启计划 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal:vps-xray/deploy.sh 支持「每 N 天在固定时间点重启 Xray」,并提供一个不触碰 vless 链接的轻量命令行入口 --restart

Architecture: systemd timer 每天到点唤醒一个 oneshot serviceservice 调用守卫脚本 xray-restart-guard;守卫读取状态文件里的上次重启时间戳,满 N 天才真正执行 systemctl restart xray。这样绕开了 OnCalendar 无法表达任意 N 天间隔(*-*-1/7 会跨月重置)的限制。timer / service / guard 三个文件的生成逻辑收敛在一个函数里,完整部署与 --restart 共用。

Tech Stack: Bash 4+、systemd、Debian/Ubuntu。无测试框架——本计划新增一个自包含的 bash 断言脚本 vps-xray/tests/test-restart-schedule.sh,可在 Windows 的 Git Bash 或 Linux 上直接跑,不需要 root、不接触真实 systemd。

Global Constraints

  • 规格文档:docs/superpowers/specs/2026-08-06-xray-restart-schedule-design.md,本计划的所有行为以它为准
  • 默认关闭:首次部署 XRAY_RESTART_EVERY_DAYS=0,与当前行为一致
  • 旧字段 XRAY_DAILY_RESTART 仅在新字段完全未设置时才回退读取:true|yes|on|1(大小写不敏感)→ 1,其它 → 0
  • 时间接受 4:0004:00,落盘前一律归一化为两位小时 HH:MM
  • 参数不合法一律报错退出,不得静默回退默认值
  • 守卫脚本必须先写时间戳再重启
  • 守卫脚本间隔判断使用 300 秒容差(now - last) < DAYS*86400 - 300 才跳过
  • 状态文件 /var/lib/xray/last-restart;关闭定时重启时保留该文件
  • --restart 路径禁止:装依赖、升级 Xray、生成密钥、选伪装目标、重写 config.json、改防火墙与 sysctl、重启 xray
  • 所有可写路径通过可覆盖变量暴露,便于测试:SYSTEMD_DIRGUARD_BINRESTART_STATE_FILE
  • 提交信息用中文,与仓库现有风格一致
  • 每个中间提交都必须是可部署的。 本工作直接在 master 上进行,而 master 随时可能被 upload.sh 推到生产服务器。deploy.sh 开头是 set -euo pipefail,删除某个变量的赋值处而下游仍在引用,会让部署在中途崩溃(Task 1 已踩过一次)。拆任务时若某处的清理要延后,必须留过渡兼容代码,并在做清理的那个任务里显式删除。
  • set -u 推论:任何返回非 0 的函数都必须在条件位置调用if f; then / f || x / f && x),裸调用会让整个脚本退出

File Structure

文件 职责
vps-xray/deploy.sh 配置解析与迁移、单元文件生成、--restart 入口。改动集中在三处:顶部参数与配置解析、新增的一组 *_restart_* 函数、main() 早退分支
vps-xray/tests/test-restart-schedule.sh 新建。抽取 deploy.sh 的函数定义做断言,覆盖归一化、迁移、校验、.env 改写、单元文件渲染、守卫脚本行为
vps-xray/.env.example 三个新字段与说明
vps-xray/uninstall.sh 补充清理 guard 脚本与状态文件
vps-xray/README.md 定时重启一节

Task 1: 时间归一化与配置解析

Files:

  • Create: vps-xray/tests/test-restart-schedule.sh
  • Modify: vps-xray/deploy.sh(替换第 78~82 行附近的 XRAY_DAILY_RESTART case 块)

Interfaces:

  • Produces: normalize_restart_time <str> → stdout 输出 HH:MM,不合法返回 1

  • Produces: resolve_restart_config → 读环境变量 XRAY_RESTART_EVERY_DAYS / XRAY_RESTART_TIME / XRAY_RESTART_TIMEZONE / XRAY_DAILY_RESTART,设置全局 RESTART_EVERY_DAYS(整数)、RESTART_TIMEHH:MM)、RESTART_TZ(字符串);任一不合法则 error 后返回 1

  • Step 1: 写测试脚手架与失败测试

创建 vps-xray/tests/test-restart-schedule.sh

#!/usr/bin/env bash
# deploy.sh 中重启计划相关逻辑的单元测试
#
# 用法: bash vps-xray/tests/test-restart-schedule.sh
#
# 不需要 root,不接触真实 systemd:所有写入路径都被重定向到临时目录,
# systemctl 用 PATH 打桩拦截。
#
# 加载方式说明:deploy.sh 结尾会直接调用 main,且顶层有 root 检查与参数解析,
# 不能整份 source。这里只抽取「多行函数定义」(形如 `name() {` 开头、`}` 结尾)。
# 单行函数(log/warn/error/step)抽不到,正好由下面的桩覆盖。

set -u

TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY="${TEST_DIR}/../deploy.sh"

if [ ! -f "$DEPLOY" ]; then
    echo "找不到 $DEPLOY" >&2
    exit 1
fi

eval "$(sed -n '/^[a-z_][a-z_]*() {$/,/^}$/p' "$DEPLOY")"

# 日志桩:error 保留输出便于断言,其余静音
log()   { :; }
warn()  { :; }
step()  { :; }
error() { echo "ERROR: $*" >&2; }

PASS=0
FAIL=0
ok()  { PASS=$((PASS + 1)); echo "  ok   - $1"; }
bad() { FAIL=$((FAIL + 1)); echo "  FAIL - $1"; echo "         期望: $2"; echo "         实际: $3"; }
# is <描述> <实际值> <期望值>
is()  { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "$3" "$2"; fi; }

echo "== normalize_restart_time =="
is "04:00 原样通过"      "$(normalize_restart_time 04:00)" "04:00"
is "4:00 补零"           "$(normalize_restart_time 4:00)"  "04:00"
is "08:30 不被当八进制"  "$(normalize_restart_time 08:30)" "08:30"
is "23:59 边界"          "$(normalize_restart_time 23:59)" "23:59"
is "00:00 边界"          "$(normalize_restart_time 00:00)" "00:00"

normalize_restart_time "24:00" >/dev/null 2>&1
is "24:00 被拒" "$?" "1"
normalize_restart_time "04:60" >/dev/null 2>&1
is "04:60 被拒" "$?" "1"
normalize_restart_time "abc" >/dev/null 2>&1
is "abc 被拒" "$?" "1"
normalize_restart_time "" >/dev/null 2>&1
is "空串被拒" "$?" "1"

echo "== resolve_restart_config =="

reset_env() {
    unset XRAY_RESTART_EVERY_DAYS XRAY_RESTART_TIME XRAY_RESTART_TIMEZONE XRAY_DAILY_RESTART
    RESTART_EVERY_DAYS=""; RESTART_TIME=""; RESTART_TZ=""
}

reset_env
resolve_restart_config
is "全空时天数默认 0"   "$RESTART_EVERY_DAYS" "0"
is "全空时时间默认"     "$RESTART_TIME"       "04:00"
is "全空时时区默认"     "$RESTART_TZ"         "Asia/Shanghai"

reset_env; XRAY_RESTART_EVERY_DAYS=7; XRAY_RESTART_TIME=5:30
resolve_restart_config
is "读取新字段天数"     "$RESTART_EVERY_DAYS" "7"
is "读取新字段时间并补零" "$RESTART_TIME"     "05:30"

reset_env; XRAY_DAILY_RESTART=true
resolve_restart_config
is "迁移 true → 1"      "$RESTART_EVERY_DAYS" "1"

reset_env; XRAY_DAILY_RESTART=TRUE
resolve_restart_config
is "迁移 TRUE → 1"      "$RESTART_EVERY_DAYS" "1"

reset_env; XRAY_DAILY_RESTART=false
resolve_restart_config
is "迁移 false → 0"     "$RESTART_EVERY_DAYS" "0"

reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=3
resolve_restart_config
is "新字段优先于旧字段" "$RESTART_EVERY_DAYS" "3"

reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=0
resolve_restart_config
is "新字段显式 0 覆盖旧 true" "$RESTART_EVERY_DAYS" "0"

reset_env; XRAY_RESTART_EVERY_DAYS=abc
resolve_restart_config 2>/dev/null
is "天数非数字报错" "$?" "1"

reset_env; XRAY_RESTART_EVERY_DAYS=-1
resolve_restart_config 2>/dev/null
is "天数负数报错" "$?" "1"

reset_env; XRAY_RESTART_TIME=25:00
resolve_restart_config 2>/dev/null
is "时间非法报错" "$?" "1"

reset_env; XRAY_RESTART_TIMEZONE=""
resolve_restart_config 2>/dev/null
is "时区空串报错" "$?" "1"

echo ""
echo "通过 ${PASS},失败 ${FAIL}"
[ "$FAIL" -eq 0 ]
  • Step 2: 运行测试确认失败

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 大量 FAIL,并伴随 normalize_restart_time: command not found —— 因为函数尚未存在。

  • Step 3: 实现两个函数

vps-xray/deploy.sh 中,把现有的这段:

# 布尔开关归一化:容忍 true/TRUE/True/yes/on/1 等写法,
# 避免用户按直觉写了 TRUE 却被静默当成关闭
case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
    true|yes|on|1) DAILY_RESTART=1 ;;
    *)             DAILY_RESTART=0 ;;
esac

整段替换为:

# ===== 定时重启配置 =====
# 可覆盖路径,测试时指向临时目录
SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
GUARD_BIN="${GUARD_BIN:-/usr/local/bin/xray-restart-guard}"
RESTART_STATE_FILE="${RESTART_STATE_FILE:-/var/lib/xray/last-restart}"

RESTART_EVERY_DAYS=0
RESTART_TIME="04:00"
RESTART_TZ="Asia/Shanghai"

# 把 4:00 / 04:00 统一成两位小时的 HH:MM,不合法返回 1。
# 归一化的意义:OnCalendar 字符串格式统一,.env 内容可预测。
normalize_restart_time() {
    local t="${1:-}"
    [[ "$t" =~ ^([01]?[0-9]|2[0-3]):([0-5][0-9])$ ]] || return 1
    # 10# 前缀避免 08/09 被当成八进制
    printf '%02d:%s' "$((10#${BASH_REMATCH[1]}))" "${BASH_REMATCH[2]}"
}

resolve_restart_config() {
    local days="${XRAY_RESTART_EVERY_DAYS:-}"

    # 旧字段迁移:仅当新字段完全未设置时才回退去读,
    # 这样 .env 里显式写 0 能覆盖遗留的 XRAY_DAILY_RESTART=true
    if [ -z "$days" ]; then
        case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
            true|yes|on|1) days=1 ;;
            *)             days=0 ;;
        esac
    fi

    if ! [[ "$days" =~ ^[0-9]+$ ]]; then
        error "XRAY_RESTART_EVERY_DAYS 必须是非负整数,当前为 '${days}'"
        return 1
    fi
    RESTART_EVERY_DAYS="$days"

    local t
    if ! t=$(normalize_restart_time "${XRAY_RESTART_TIME:-04:00}"); then
        error "XRAY_RESTART_TIME 必须是 HH:MM00:00~23:59),当前为 '${XRAY_RESTART_TIME:-}'"
        return 1
    fi
    RESTART_TIME="$t"

    RESTART_TZ="${XRAY_RESTART_TIMEZONE-Asia/Shanghai}"
    if [ -z "$RESTART_TZ" ]; then
        error "XRAY_RESTART_TIMEZONE 不能为空"
        return 1
    fi

    return 0
}

resolve_restart_config || exit 1

# 过渡兼容:harden_system / start_service / save_env 仍引用 DAILY_RESTART
# 要到 Task 5 才清理。在那之前必须保证 master 上的脚本能完整跑完——
# deploy.sh 开头是 set -euo pipefail,缺这个变量会在部署中途崩溃。
DAILY_RESTART=$([ "$RESTART_EVERY_DAYS" -ne 0 ] && echo 1 || echo 0)

注意 ${XRAY_RESTART_TIMEZONE-Asia/Shanghai} 用的是 - 而非 :-——显式设成空串时要报错,不能被默认值悄悄补上。

为什么需要那行过渡兼容赋值: 被替换掉的 case 块是 DAILY_RESTART 的唯一赋值处,而下游三处引用要到 Task 5 才删。set -u 下变量缺失会让 harden_system 直接 unbound variable 退出——那时 Xray 已装好、config.json 已写入,但服务不会启动、.env 不会更新。每个中间提交都必须是可部署的。

  • Step 4: 运行测试确认通过

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 23,失败 0normalize 9 项 + resolve 14 项)

Run: bash -n vps-xray/deploy.sh Expected: 无输出

  • Step 5: 提交
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 重启计划配置解析与旧字段迁移

新增 normalize_restart_time / resolve_restart_config
XRAY_RESTART_EVERY_DAYS 缺失时才回退读 XRAY_DAILY_RESTART。
附单元测试,不需要 root 与 systemd。"

Task 2: .env 字段就地更新

Files:

  • Modify: vps-xray/deploy.sh(在 resolve_restart_config 之后新增函数)
  • Modify: vps-xray/tests/test-restart-schedule.sh

Interfaces:

  • Consumes: 无

  • Produces: update_env_field <file> <key> <value> → 文件中存在 ^key= 则替换该行,否则追加一行;文件不存在返回 1

  • Step 1: 追加失败测试

vps-xray/tests/test-restart-schedule.shecho "" 汇总行之前插入:

echo "== update_env_field =="

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

cat > "${TMP}/.env" << 'ENVEOF'
# 这是用户自己加的注释
XRAY_PORT=443
XRAY_RESTART_EVERY_DAYS=0
# 尾部注释
ENVEOF

update_env_field "${TMP}/.env" XRAY_RESTART_EVERY_DAYS 7
is "替换已有字段" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${TMP}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
is "保留用户注释" "$(grep -c '这是用户自己加的注释' "${TMP}/.env")" "1"
is "保留尾部注释" "$(grep -c '尾部注释' "${TMP}/.env")" "1"
is "不影响其它字段" "$(grep '^XRAY_PORT=' "${TMP}/.env")" "XRAY_PORT=443"

update_env_field "${TMP}/.env" XRAY_RESTART_TIME 05:30
is "追加缺失字段" "$(grep '^XRAY_RESTART_TIME=' "${TMP}/.env")" "XRAY_RESTART_TIME=05:30"

update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Asia/Shanghai
is "含斜杠的值不被 sed 分隔符干扰" \
   "$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Asia/Shanghai"

update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Europe/Berlin
is "含斜杠的值可被再次替换" \
   "$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Europe/Berlin"

is "字段总行数正确" "$(grep -c '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "1"

update_env_field "${TMP}/nonexistent" FOO bar 2>/dev/null
is "文件不存在返回 1" "$?" "1"
  • Step 2: 运行测试确认失败

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: update_env_field: command not found,新增断言全部 FAIL

  • Step 3: 实现

vps-xray/deploy.shresolve_restart_config 函数定义之后、resolve_restart_config || exit 1 调用之前,插入:

# 就地更新 .env 中的单个字段:存在则替换,不存在则追加。
# 刻意不复用 save_env——后者是 cat > 全量重写,会抹掉用户自己加的注释与字段;
# 轻量入口只该改自己那一行。
update_env_field() {
    local file="$1" key="$2" value="$3" esc
    [ -f "$file" ] || return 1

    if grep -q "^${key}=" "$file"; then
        # 用 | 作分隔符,并转义 value 中的 | & \,避免值里的 / 破坏 sed
        esc=$(printf '%s' "$value" | sed 's/[|&\\]/\\&/g')
        sed -i "s|^${key}=.*|${key}=${esc}|" "$file"
    else
        printf '%s=%s\n' "$key" "$value" >> "$file"
    fi
    return 0
}
  • Step 4: 运行测试确认通过

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 34,失败 0(新增 9 项 + 复审补的 2 项缺尾换行断言)

  • Step 5: 提交
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 新增 update_env_field 就地更新 .env 单个字段"

Task 3: 渲染 timer / service / guard 三个文件

Files:

  • Modify: vps-xray/deploy.sh
  • Modify: vps-xray/tests/test-restart-schedule.sh

Interfaces:

  • Consumes: 全局 SYSTEMD_DIRGUARD_BINRESTART_STATE_FILE

  • Produces: render_restart_units <days> <time> <tz> → 写出 ${SYSTEMD_DIR}/xray-restart.timer${SYSTEMD_DIR}/xray-restart.service${GUARD_BIN}(可执行)

  • Produces: remove_restart_units → 删除上述三个文件并 disable timer;原本存在返回 0,本就不存在返回 1

  • Step 1: 追加失败测试

在汇总行之前插入:

echo "== render_restart_units =="

SYSTEMD_DIR="${TMP}/systemd"
GUARD_BIN="${TMP}/bin/xray-restart-guard"
RESTART_STATE_FILE="${TMP}/state/last-restart"
mkdir -p "${TMP}/systemd" "${TMP}/bin" "${TMP}/state"

# systemctl 打桩:记录被调用的参数,避免碰真实 systemd
STUB="${TMP}/stub"
mkdir -p "$STUB"
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
STUBEOF
chmod 755 "${STUB}/systemctl"
export SYSTEMCTL_LOG="${TMP}/systemctl.log"
: > "$SYSTEMCTL_LOG"
export PATH="${STUB}:${PATH}"

render_restart_units 7 04:00 Asia/Shanghai

is "timer 文件存在"   "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes)"   "yes"
is "service 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes)" "yes"
is "guard 可执行"     "$([ -x "$GUARD_BIN" ] && echo yes)"                          "yes"

is "OnCalendar 每天触发" \
   "$(grep -c '^OnCalendar=\*-\*-\* 04:00:00$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
is "TimeZone 写入" \
   "$(grep -c '^TimeZone=Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
is "Persistent 开启" \
   "$(grep -c '^Persistent=true$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
is "service 指向 guard" \
   "$(grep -c "^ExecStart=${GUARD_BIN}\$" "${SYSTEMD_DIR}/xray-restart.service")" "1"
is "guard 里天数正确" \
   "$(grep -c '^DAYS=7$' "$GUARD_BIN")" "1"
is "guard 里状态文件路径正确" \
   "$(grep -c "^STATE=\"${RESTART_STATE_FILE}\"\$" "$GUARD_BIN")" "1"
is "guard 用 PATH 查找 systemctl 而非绝对路径" \
   "$(grep -c '^exec systemctl restart xray$' "$GUARD_BIN")" "1"

render_restart_units 3 23:05 Europe/Berlin
is "重复渲染覆盖天数" "$(grep -c '^DAYS=3$' "$GUARD_BIN")" "1"
is "重复渲染覆盖时间" \
   "$(grep -c '^OnCalendar=\*-\*-\* 23:05:00$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"

remove_restart_units
is "remove 后 timer 消失"   "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
is "remove 后 service 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
is "remove 后 guard 消失"   "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"

remove_restart_units
is "重复 remove 返回 1" "$?" "1"
  • Step 2: 运行测试确认失败

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: render_restart_units: command not found,新增断言 FAIL

  • Step 3: 实现

vps-xray/deploy.shupdate_env_field 之后插入:

# 生成 timer / service / guard 三个文件。
# 完整部署与 --restart 共用本函数,避免两条路径各写一份实现后漂移。
render_restart_units() {
    local days="$1" time="$2" tz="$3"

    mkdir -p "$SYSTEMD_DIR" "$(dirname "$GUARD_BIN")"

    # 守卫脚本:systemd 的 OnCalendar 表达不了「每 N 天」——
    # *-*-1/7 是「每月 1/8/15/22/29 号」,跨月会重置成 2~3 天。
    # 所以 timer 只负责每天到点唤醒,间隔判断放到这里。
    cat > "$GUARD_BIN" << EOF
#!/bin/bash
# 由 deploy.sh 生成,请勿手工编辑
STATE="${RESTART_STATE_FILE}"
DAYS=${days}

now=\$(date +%s)
last=0
if [ -f "\$STATE" ]; then
    last=\$(cat "\$STATE" 2>/dev/null || echo 0)
    # 内容被写坏时按 0 处理,即允许立即重启
    [[ "\$last" =~ ^[0-9]+\$ ]] || last=0
fi

# 300 秒容差:systemd timer 默认 AccuracySec=1min,第 N 次触发的实际间隔
# 可能是 N*86400-30s。严格比较会判「不满」从而顺延整整一天,且误差会累积。
if [ "\$last" -gt 0 ] && [ \$(( now - last )) -lt \$(( DAYS * 86400 - 300 )) ]; then
    echo "距上次重启不足 \${DAYS} 天(已过 \$(( (now - last) / 86400 )) 天),跳过"
    exit 0
fi

mkdir -p "\$(dirname "\$STATE")"
# 必须先写时间戳:restart 会中断本脚本所在的 systemd 事务,
# 顺序反了会丢记录,退化成每天都重启。
# 代价是重启失败时本轮仍被记为已执行——这比陷入每日重启循环可接受。
echo "\$now" > "\$STATE"
echo "距上次重启已满 \${DAYS} 天,执行重启"
exec systemctl restart xray
EOF
    chmod 755 "$GUARD_BIN"

    cat > "${SYSTEMD_DIR}/xray-restart.service" << EOF
[Unit]
Description=Scheduled restart of Xray (every ${days} day(s))
After=network.target

[Service]
Type=oneshot
Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=${GUARD_BIN}
EOF

    cat > "${SYSTEMD_DIR}/xray-restart.timer" << EOF
[Unit]
Description=Wake xray-restart daily at ${time} ${tz}; the guard enforces the ${days}-day interval

[Timer]
OnCalendar=*-*-* ${time}:00
TimeZone=${tz}
Persistent=true

[Install]
WantedBy=timers.target
EOF
}

# 删除 timer / service / guard。状态文件刻意保留:
# 以后重新启用时,距上次已超 N 天则首次触发即重启,符合直觉。
remove_restart_units() {
    if [ ! -f "${SYSTEMD_DIR}/xray-restart.timer" ] && [ ! -f "$GUARD_BIN" ]; then
        return 1
    fi
    systemctl disable --now xray-restart.timer >/dev/null 2>&1 || true
    rm -f "${SYSTEMD_DIR}/xray-restart.timer" \
          "${SYSTEMD_DIR}/xray-restart.service" \
          "$GUARD_BIN"
    return 0
}
  • Step 4: 运行测试确认通过

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 50,失败 034 基线 + 新增 16

Run: bash -n vps-xray/deploy.sh Expected: 无输出

  • Step 5: 提交
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 生成 timer/service/guard 三个单元文件

timer 每天唤醒,N 天间隔判断交给 guard 脚本,
绕开 OnCalendar 无法表达任意 N 天间隔的限制。"

Task 4: 守卫脚本行为

Files:

  • Modify: vps-xray/tests/test-restart-schedule.sh

Interfaces:

  • Consumes: Task 3 的 render_restart_units

本任务只加测试——被测对象是 Task 3 已生成的 guard 脚本。若测试暴露缺陷,回到 Task 3 的实现修改。

  • Step 1: 追加行为测试

在汇总行之前插入:

echo "== guard 脚本行为 =="

render_restart_units 7 04:00 Asia/Shanghai
rm -f "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"

# 无状态文件 → 首次必须执行
out=$("$GUARD_BIN" 2>&1)
is "首次执行重启"     "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "1"
is "首次写入时间戳"   "$([ -f "$RESTART_STATE_FILE" ] && echo yes)" "yes"

# 刚重启过 → 必须跳过
: > "$SYSTEMCTL_LOG"
out=$("$GUARD_BIN" 2>&1)
is "刚重启过则跳过"   "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "0"
is "跳过时有提示"     "$(echo "$out" | grep -c '跳过')" "1"

# 距上次 6 天 → 仍未满 7 天,跳过
echo "$(( $(date +%s) - 6 * 86400 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "6 天不足 7 天则跳过" "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "0"

# 距上次 7 天差 100 秒 → 300 秒容差内,必须执行
echo "$(( $(date +%s) - 7 * 86400 + 100 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "容差内视为已满 7 天" "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "1"

# 距上次 7 天差 600 秒 → 超出容差,跳过
echo "$(( $(date +%s) - 7 * 86400 + 600 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "超出容差仍判不足" "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "0"

# 状态文件内容被写坏 → 按 0 处理,立即执行
echo "garbage" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "状态文件非法时立即执行" "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "1"
is "非法内容被覆盖为数字" \
   "$(grep -cE '^[0-9]+$' "$RESTART_STATE_FILE")" "1"

# 每日模式:距上次 1 天即执行
render_restart_units 1 04:00 Asia/Shanghai
echo "$(( $(date +%s) - 86400 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "N=1 时满一天即执行" "$(grep -c 'systemctl restart xray' "$SYSTEMCTL_LOG")" "1"

# 时间戳先于重启写入:用一个必定失败的 systemctl 桩验证
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
exit 1
STUBEOF
chmod 755 "${STUB}/systemctl"
rm -f "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "重启失败时时间戳仍已写入" "$([ -s "$RESTART_STATE_FILE" ] && echo yes)" "yes"

# 恢复正常桩,避免影响后续
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
STUBEOF
chmod 755 "${STUB}/systemctl"
  • Step 2: 运行测试

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 61,失败 0(50 基线 + 新增 11)。若有 FAIL,说明 Task 3 生成的 guard 逻辑有缺陷——回到 Task 3 的 render_restart_units 修改后重跑。

  • Step 3: 提交
git add vps-xray/tests/test-restart-schedule.sh
git commit -m "test(vps-xray): 覆盖 guard 脚本的间隔判断、容差与状态文件容错"

Task 5: 接线进完整部署流程

Files:

  • Modify: vps-xray/deploy.shharden_systemstart_servicesave_env

Interfaces:

  • Consumes: resolve_restart_configrender_restart_unitsremove_restart_units

  • Produces: configure_restart_timer → 依据全局 RESTART_EVERY_DAYS / RESTART_TIME / RESTART_TZ 建立或移除定时重启,并完成 daemon-reload 与 enable/disable

  • Step 1: 新增 configure_restart_timer

vps-xray/deploy.shremove_restart_units 之后插入:

configure_restart_timer() {
    if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
        if remove_restart_units; then
            warn "已移除定时重启(XRAY_RESTART_EVERY_DAYS=0"
        fi
        systemctl daemon-reload
        log "定时重启: 关闭(崩溃恢复已由 Restart=always 秒级覆盖)"
        return
    fi

    render_restart_units "$RESTART_EVERY_DAYS" "$RESTART_TIME" "$RESTART_TZ"
    systemctl daemon-reload
    systemctl enable --now xray-restart.timer >/dev/null 2>&1
    log "定时重启: 每 ${RESTART_EVERY_DAYS}${RESTART_TIME} (${RESTART_TZ})"
    warn "重启会切断所有活动连接;崩溃恢复本已由 Restart=always 覆盖,按需启用"
}
  • Step 2: 替换 harden_system 里的旧逻辑

harden_system() 中,把从 # ===== 每日重启 timer:默认关闭 ===== 注释开始、到函数结尾 warn "该操作会切断所有活动连接,如无必要建议设 XRAY_DAILY_RESTART=false" 为止的全部内容(含 if [ "$DAILY_RESTART" -ne 1 ] 分支、两处 cat > /etc/systemd/system/xray-restart.* 生成块)删除,替换为单行:

    configure_restart_timer

保留其上方的文件描述符上限与 xray.service.d/override.conf 生成逻辑,不要动。

  • Step 3: 清理 start_service 中的重复 enable

start_service() 中删除这三行——timer 的 enable 已由 configure_restart_timer 负责,且 harden_systemstart_service 之前执行:

    if [ "$DAILY_RESTART" -eq 1 ]; then
        systemctl enable --now xray-restart.timer
    fi
  • Step 4: 更新 save_env

save_env() 中,把这一段:

# 每日 04:00 自动重启 Xray。默认 false。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
XRAY_DAILY_RESTART=$([ "$DAILY_RESTART" -eq 1 ] && echo true || echo false)

替换为:

# 定时重启。0 = 关闭(默认);N = 每 N 天在 XRAY_RESTART_TIME 重启一次。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而硬重启会切断
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
# 改这两项无需完整重新部署: bash deploy.sh --restart 7 04:00
XRAY_RESTART_EVERY_DAYS=${RESTART_EVERY_DAYS}
XRAY_RESTART_TIME=${RESTART_TIME}
XRAY_RESTART_TIMEZONE=${RESTART_TZ}
  • Step 5: 删除 Task 1 留下的过渡兼容赋值

下游三处引用已在 Step 2~4 全部清理,Task 1 加的这行兼容赋值到此完成使命,删掉:

# 过渡兼容:harden_system / start_service / save_env 仍引用 DAILY_RESTART
# 要到 Task 5 才清理。在那之前必须保证 master 上的脚本能完整跑完——
# deploy.sh 开头是 set -euo pipefail,缺这个变量会在部署中途崩溃。
DAILY_RESTART=$([ "$RESTART_EVERY_DAYS" -ne 0 ] && echo 1 || echo 0)

留着它就是僵尸代码——没有任何读取方,却会让下一个读代码的人以为还有别处在用。

  • Step 6: 确认没有遗留引用

Run: grep -n "DAILY_RESTART" vps-xray/deploy.sh Expected: 只剩 resolve_restart_config 里那处向后兼容的 ${XRAY_DAILY_RESTART:-false},共 1 行。

Run: bash -n vps-xray/deploy.sh Expected: 无输出

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 61,失败 0

  • Step 7: 提交
git add vps-xray/deploy.sh
git commit -m "refactor(vps-xray): 完整部署改用 configure_restart_timer

harden_system 与 start_service 里的旧 DAILY_RESTART 分支删除,
save_env 改写三个新字段。"

Task 6: --restart 轻量入口

Files:

  • Modify: vps-xray/deploy.shusage、参数解析、main,新增 apply_restart_only

Interfaces:

  • Consumes: resolve_restart_configupdate_env_fieldconfigure_restart_timer

  • Produces: 全局 RESTART_ONLYRESTART_ARG_DAYSRESTART_ARG_TIME;函数 apply_restart_only

  • Step 1: 更新 usage

usage()--redetect 那两行之后、-h, --help 之前插入:

    echo "  --restart [N] [HH:MM]"
    echo "                    只修改定时重启计划并立即生效。N=0 关闭。"
    echo "                    不重装 Xray、不重写配置、不重启服务,"
    echo "                    现有 vless 链接完全不受影响。"
    echo "                    例: bash deploy.sh --restart 7 04:00"
  • Step 2: 扩展参数解析

在参数解析区,把:

MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0

改为:

MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0
RESTART_ONLY=0
RESTART_ARG_DAYS=""
RESTART_ARG_TIME=""

while 循环的 case 中,--redetect) REDETECT=1; shift ;; 之后插入:

        --restart)
            RESTART_ONLY=1
            shift
            # 后面可跟 0~2 个位置参数,按形态判断,不会误吞后续选项
            if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
                RESTART_ARG_DAYS="$1"
                shift
                if [[ "${1:-}" =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]]; then
                    RESTART_ARG_TIME="$1"
                    shift
                fi
            fi
            ;;

while 循环结束后、root 检查之前插入互斥校验:

# --restart 是「只改重启计划」的轻量入口,与会改动服务的选项互斥,
# 避免用户以为只调了 timer、实际却顺带重装了 Xray
if [ "$RESTART_ONLY" -eq 1 ] \
   && { [ "$MODE_EXPLICIT" -eq 1 ] || [ "$SHOW_ONLY" -eq 1 ] || [ "$REDETECT" -eq 1 ]; }; then
    error "--restart 不能与 --mode / --show / --redetect 同时使用"
    exit 1
fi
  • Step 3: 实现 apply_restart_only

configure_restart_timer 之后插入:

# ===== --restart:只改重启计划 =====
# 明确不做:装依赖、升级 Xray、生成密钥、选伪装目标、重写 config.json、
# 改防火墙与 sysctl、重启 xray。因此 vless 链接与现有连接均不受影响。
apply_restart_only() {
    local env_file="$SCRIPT_DIR/.env"

    if [ ! -f "$env_file" ]; then
        error "未找到 ${env_file},无法确认这是一台已部署的机器"
        error "请先执行完整部署: bash deploy.sh"
        exit 1
    fi
    if ! systemctl cat xray >/dev/null 2>&1; then
        error "未找到 xray.service,请先执行完整部署: bash deploy.sh"
        exit 1
    fi

    # 命令行给了值就覆盖 .env 载入的值,没给则沿用
    if [ -n "$RESTART_ARG_DAYS" ]; then
        XRAY_RESTART_EVERY_DAYS="$RESTART_ARG_DAYS"
    fi
    if [ -n "$RESTART_ARG_TIME" ]; then
        XRAY_RESTART_TIME="$RESTART_ARG_TIME"
    fi
    resolve_restart_config || exit 1

    # 只把命令行显式给过的字段落盘,没给的不动 .env
    if [ -n "$RESTART_ARG_DAYS" ]; then
        update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"
    fi
    if [ -n "$RESTART_ARG_TIME" ]; then
        update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"
    fi

    configure_restart_timer

    echo ""
    echo "=========================================================="
    if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
        echo "  定时重启: 已关闭"
    else
        echo "  定时重启: 每 ${RESTART_EVERY_DAYS}${RESTART_TIME} (${RESTART_TZ})"
    fi
    echo "  xray 服务未重启,vless 链接与现有连接均未受影响。"
    echo "=========================================================="
    if [ "$RESTART_EVERY_DAYS" -ne 0 ]; then
        echo ""
        systemctl list-timers xray-restart.timer --no-pager 2>/dev/null || true
    fi
}

注意 resolve_restart_config 在脚本顶层已经跑过一次,这里再跑一次是为了让命令行传入的值参与校验与归一化。

  • Step 4: 在 main 中早退

main() 里,紧跟现有的 --show 分支之后插入:

    if [ "$RESTART_ONLY" -eq 1 ]; then
        apply_restart_only
        exit 0
    fi
  • Step 5: 修正非法参数的退出码

现状:usage() 结尾是 exit 0,而参数解析的兜底分支是 *) error "未知参数: $1"; usage ;;——非法参数会以退出码 0 结束。--restart 7 25:00 中的 25:00 不匹配时间形态、不被 --restart 消费,正会落进这个分支,于是「时间写错」表现为「成功退出」。这违反本计划的全局约束。

usage() 的结尾:

    exit 0
}

改为:

    exit "${1:-0}"
}

并把参数解析的兜底分支:

        *) error "未知参数: $1"; usage ;;

改为:

        *) error "未知参数: $1"; usage 1 ;;

-h|--help) usage ;; 保持不变,仍以 0 退出。

  • Step 6: 本地校验

Run: bash -n vps-xray/deploy.sh Expected: 无输出

Run: bash vps-xray/deploy.sh --badarg > /dev/null 2>&1; echo $? Expected: 1

Run: bash vps-xray/deploy.sh --help > /dev/null 2>&1; echo $? Expected: 0

Run: bash vps-xray/tests/test-restart-schedule.sh Expected: 通过 61,失败 0

Run: bash vps-xray/deploy.sh --help Expected: 帮助里出现 --restart [N] [HH:MM] 说明,退出码 0

  • Step 7: 提交
git add vps-xray/deploy.sh
git commit -m "feat(vps-xray): 新增 --restart 轻量入口

只改定时重启计划并立即生效,不重装 Xray、不重写配置、不重启服务,
与 --mode/--show/--redetect 互斥。
顺带修正非法参数以退出码 0 结束的问题。"

Task 7: 文档与卸载清理

Files:

  • Modify: vps-xray/.env.example

  • Modify: vps-xray/uninstall.sh

  • Modify: vps-xray/README.md

  • Step 1: 更新 .env.example

把现有这段:

# 每日 04:00 自动重启 Xray。默认 false。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
# XRAY_DAILY_RESTART=false

替换为:

# ===== 定时重启 =====
# 0 = 关闭(默认);N = 每 N 天重启一次 Xray。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级响应),而硬重启会切断
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
# XRAY_RESTART_EVERY_DAYS=0
# XRAY_RESTART_TIME=04:00
# XRAY_RESTART_TIMEZONE=Asia/Shanghai
#
# 改这几项无需完整重新部署,用轻量入口即可(不影响 vless 链接):
#   bash deploy.sh --restart 7 04:00
#   bash deploy.sh --restart 0
#
# 旧字段 XRAY_DAILY_RESTART 已废弃,仅在上面三项都没设时作为兼容回退:
# true → 每 1 天,其它 → 关闭。重跑部署后会自动换写成新字段。
  • Step 2: 更新 uninstall.sh

把现有这段:

if [ -f /etc/systemd/system/xray-restart.timer ]; then
    systemctl disable --now xray-restart.timer 2>/dev/null || true
    rm -f /etc/systemd/system/xray-restart.timer \
          /etc/systemd/system/xray-restart.service

替换为:

if [ -f /etc/systemd/system/xray-restart.timer ] \
   || [ -f /usr/local/bin/xray-restart-guard ]; then
    systemctl disable --now xray-restart.timer 2>/dev/null || true
    rm -f /etc/systemd/system/xray-restart.timer \
          /etc/systemd/system/xray-restart.service \
          /usr/local/bin/xray-restart-guard \
          /var/lib/xray/last-restart

卸载与「关闭定时重启」不同,卸载要连状态文件一起清掉。

  • Step 3: 更新 README

README.md 的「查看状态 / 日志」一节之前插入:

### 定时重启

默认**关闭**。崩溃恢复已由 systemd 的 `Restart=always` + `RestartSec=3` 覆盖(秒级响应),而硬重启会切断全部活动连接——跨夜下载、备份、CI 都会中断。仅在确实观察到长期运行后性能退化时才启用。

需要时用轻量入口调整,**不会重装 Xray、不会重写配置、不会重启服务,现有 vless 链接完全不受影响**

```bash
bash deploy.sh --restart 7 04:00   # 每 7 天 04:00 重启
bash deploy.sh --restart 3         # 每 3 天,时间沿用 .env
bash deploy.sh --restart 0         # 关闭
bash deploy.sh --restart           # 按 .env 现有值重建 timer

对应 .env 三个字段:

字段 含义 默认
XRAY_RESTART_EVERY_DAYS 0 = 关闭;N = 每 N 天 0
XRAY_RESTART_TIME 24 小时制 HH:MM 04:00
XRAY_RESTART_TIMEZONE 时区 Asia/Shanghai

为什么不是纯 systemd timer

OnCalendar 表达不了任意 N 天:*-*-1/7 04:00:00 是「每月 1、8、15、22、29 号」,跨月会重置——29 号到下月 1 号只隔 2~3 天。

所以实现是 timer 每天到点唤醒,由 /usr/local/bin/xray-restart-guard/var/lib/xray/last-restart,满 N 天才真正重启。间隔因此是「距上次实际重启」的真实天数,N=3N=7 同样准确。

查看下次触发时间与执行记录:

systemctl list-timers xray-restart.timer
journalctl -u xray-restart.service -n 20

旧字段 XRAY_DAILY_RESTART 已废弃。三个新字段都未设置时它仍作为兼容回退(true → 每 1 天,其它 → 关闭),重跑部署后 .env 会自动换写成新字段。


- [ ] **Step 4: 校验**

Run: `bash -n vps-xray/uninstall.sh`
Expected: 无输出

Run: `grep -c "XRAY_DAILY_RESTART" vps-xray/README.md`
Expected: `1`(只剩兼容说明那处)

- [ ] **Step 5: 提交**

```bash
git add vps-xray/.env.example vps-xray/uninstall.sh vps-xray/README.md
git commit -m "docs(vps-xray): 定时重启配置说明;uninstall 清理 guard 与状态文件"

Task 8: VPS 端到端验证

Files: 无改动。本任务在真实服务器上执行 spec 的验证方案。

前置: 目标机器已用旧版脚本部署过,.env 存在。先记下当前 vless 链接以便比对。

  • Step 1: 上传
bash vps-xray/upload.sh <VPS_IP>
  • Step 2: 启用并确认服务未被重启

在 VPS 上:

cd /opt/vps-xray
systemctl show xray -p ActiveEnterTimestamp    # 记下改动前的值
bash deploy.sh --restart 7 04:00
systemctl show xray -p ActiveEnterTimestamp    # 必须与上面完全相同
grep -E '^XRAY_RESTART_' .env
systemctl list-timers xray-restart.timer --no-pager

ExpectedActiveEnterTimestamp 前后一致(证明没重启 xray);.envXRAY_RESTART_EVERY_DAYS=7XRAY_RESTART_TIME=04:00list-timers 显示 NEXT 为次日 04:00。

  • Step 3: 守卫行为
rm -f /var/lib/xray/last-restart
/usr/local/bin/xray-restart-guard      # 应输出「执行重启」
/usr/local/bin/xray-restart-guard      # 应输出「跳过」
cat /var/lib/xray/last-restart

Expected:第一次重启并写入时间戳,第二次跳过。

  • Step 4: 关闭
bash deploy.sh --restart 0
ls -l /etc/systemd/system/xray-restart.* /usr/local/bin/xray-restart-guard 2>&1
ls -l /var/lib/xray/last-restart

Expected:三个单元/脚本文件均报 No such file;状态文件仍在。

  • Step 5: 参数校验与互斥
bash deploy.sh --restart 7 25:00      ; echo "exit=$?"
bash deploy.sh --restart abc          ; echo "exit=$?"
bash deploy.sh --restart 7 --redetect ; echo "exit=$?"
bash deploy.sh --restart 4:00         ; echo "exit=$?"
ls /etc/systemd/system/xray-restart.timer 2>&1

逐条预期(形态解析的既定行为,都不是缺陷):

命令 行为 退出码
--restart 7 25:00 25:00 不匹配时间形态、不被消费,落到兜底分支报「未知参数: 25:00」并打印帮助 1
--restart abc 同上,报「未知参数: abc」 1
--restart 7 --redetect --redetect 不被消费,正常置位,循环后互斥校验拦下,报「--restart 不能与 --mode / --show / --redetect 同时使用」 1
--restart 4:00 4:00 不匹配数字形态,不作 N 用;落到兜底分支报「未知参数: 4:00」。时间必须跟在天数之后,这是位置参数的固有约束,帮助文本已写明 --restart [N] [HH:MM] 1

最后 ls 应报 No such file——四条命令都不该留下任何 timer 文件。

  • Step 6: 旧字段迁移
cp .env /tmp/env.bak
sed -i '/^XRAY_RESTART_/d' .env
echo 'XRAY_DAILY_RESTART=true' >> .env
bash deploy.sh
grep -E '^XRAY_RESTART_|^XRAY_DAILY' .env
systemctl list-timers xray-restart.timer --no-pager

Expected.env 中出现 XRAY_RESTART_EVERY_DAYS=1,不再有 XRAY_DAILY_RESTARTtimer 已建立。

  • Step 7: 恢复并确认链接未变
cp /tmp/env.bak .env
bash deploy.sh --restart 0
bash deploy.sh --show

Expected--show 输出的 vless 链接与 Step 1 之前记录的完全一致。

  • Step 8: 记录结果并提交

把实测输出补进 docs/superpowers/plans/2026-08-06-xray-restart-schedule.md 末尾的「验证记录」小节,然后:

git add docs/superpowers/plans/2026-08-06-xray-restart-schedule.md
git commit -m "docs: 补充定时重启功能的 VPS 端到端验证记录"

验证记录

Task 8 执行后填写实测输出)