别再半夜抢遥控器了:用 DSM 计划任务给群晖儿童视频目录自动上锁
先说结论
这次不是“禁用孩子账号”,而是只给群晖里的一个儿童视频目录定时上锁。做法是在 DSM 7.2.1 上准备一个
kid_video_guard.sh,到点用synoacltool给目标目录加一条临时denyACL,开放时间再把这条 ACL 删除。账号、密码、家庭相册、作业目录都不动,只有“儿童视频”这个柜门到点关上。

问题背景:不是不让用 NAS,而是不想让视频目录全天开门
家里 NAS 一旦承担了“家庭资料中心”的角色,它就不再只是一个硬盘盒。孩子会用它看照片、交作业、同步平板文件,也会顺手打开视频目录。前面几件事都该保留,问题只出在最后一件:到了吃饭、午休或者睡觉时间,儿童动画目录还像便利店一样 24 小时亮着灯。
很多家庭会把这件事处理成“账号管理”:到点停用孩子账号,过了点再启用。这个办法有效,但有点像为了锁零食柜,把整套房门钥匙都收走。孩子想看作业资料、同步照片、打开其它共享,也一起被挡住。更好的边界应该是:人可以回家,只有零食柜上锁。
这篇文章参考的是一台 DSM 7.2.1 机器上的实际脚本。原脚本有真实目录、真实用户组和真实执行记录,下面全部已经脱敏。你会看到完整思路、可直接改变量的脚本、DSM 人工配置方法,以及 Windows 11 / Ubuntu 26.04 / macOS 26 三端一键部署脚本。

问题表现:手动改权限为什么很快会失控
最开始我以为这只是一个“小事”:打开 DSM,进控制面板,找到共享文件夹或 File Station 权限,把某个用户组的读取权限取消,过一会儿再加回来。真正做了几天后,问题就露出来了。
第一,路径太深。DSM 的权限页面不是遥控器上的暂停键,点进去要找用户、找组、展开高级权限,再确认是不是改在正确目录上。对一个视频目录做这件事还行,对多个时段重复做就很烦。
第二,容易误伤。手动改权限时,人脑会把“儿童视频目录”“家庭视频目录”“家庭相册目录”混在一起。点错一次,轻则孩子第二天看不到作业资料,重则大人以为同步坏了,开始排查完全错误的问题。
第三,没有证据链。晚上到底有没有锁?中午到底有没有解?如果只是靠人点按钮,第二天复盘时通常只剩一句“我记得好像改过”。而一个可靠的系统应该像门禁记录一样,告诉你几点几分上锁、几点几分解锁、改之前有没有备份。
这就是为什么我最后没有选择继续在界面里点来点去,而是把动作收敛成一个脚本:lock、unlock、status。脚本像电梯按钮一样,只暴露三个动作;背后的权限细节让机器处理。
问题分析:ACL 顺序匹配,deny 插在前面就像“临时封条”
DSM 的共享权限底层是 ACL。你可以把 ACL 想成门口保安手里的名单:第一行写“某组禁止进入”,第二行写“某用户可以读”,第三行写“某设备可以写”。当请求进来时,系统会按规则判断这个人能不能进去。
这次的做法不是删除原有 allow,也不是改孩子账号,而是额外放一张“临时封条”:
group:KIDS_GROUP:deny:rwxpdDaARWcCo:fd--
这条规则的意思可以粗略理解为:KIDS_GROUP 这个组,在当前目录及其子项上被拒绝访问。到了开放时间,脚本把这条规则删掉,原来那些 allow 又自然生效。就像零食柜门上贴了一张临时封条,封条撕掉以后,柜子里的东西没有被挪走,钥匙也没有换。
这里有两个关键点。
第一,脚本只找自己加的那一条 deny。它不会清空整个 ACL,也不会重新生成权限列表。真实家庭 NAS 的 ACL 往往非常复杂,有设备账号、备份账号、应用账号、家庭成员账号,最怕“为了自动化,把原来的权限表重写一遍”。这版脚本只做插入和删除一条标记。
第二,脚本每次修改前都备份当前 ACL。NAS 权限不是玩具,写脚本时要假设自己未来会忘记当时为什么这么改。备份文件就是后悔药:哪怕脚本写错、组名写错、目录写错,也能回头看当时的权限列表。

真实状态截图:定时任务和日志长什么样
下面三张图来自真实命令输出,但已经做了脱敏。完整内网地址、机器名、真实共享目录、真实用户、真实用户组都没有出现在图里。保留这些截图,是为了让你知道正常系统应该长什么样。

第一张图说明两个事实:当前目录处于“已上锁”状态,并且系统有四条计划任务。示例时段是 11 点解锁、13 点上锁、17 点解锁、18 点上锁。你可以把它改成自己的作息,比如 20 点上锁、周末增加一个上午窗口。

第二张图是脚本核心变量和函数。真正需要你改的通常只有两个值:TARGET 和 GROUP。脚本里的 ACE 会根据组名自动拼出来。

第三张图是日志。它最重要的价值不是“看起来很高级”,而是能回答三个很普通的问题:什么时候改的?改之前有没有备份?改完以后有没有确认目标规则存在或不存在?
完整脱敏脚本:NAS 端只需要这一份
下面这份是脱敏后的完整脚本。它参考真实脚本结构,但路径、用户组全部换成占位符。你应该先在测试目录跑通,再换成真实视频目录。
#!/bin/bash
# kid_video_guard.sh - Synology DSM 7.2.x directory curfew by ACL.
# Usage:
# kid_video_guard.sh lock
# kid_video_guard.sh unlock
# kid_video_guard.sh status
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
set -e
TARGET="/volumeX/Family/Video/Kids" # 改成你的儿童视频目录
GROUP="KIDS_GROUP" # 改成你的儿童用户组
ACE="group:${GROUP}:deny:rwxpdDaARWcCo:fd--"
LOG="/var/log/kid_video_guard.log"
BACKUP_DIR="/var/backups/kid_video_guard"
mkdir -p "$BACKUP_DIR"
ts() { date "+%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
if [ ! -d "$TARGET" ]; then
log "ERROR: target dir not found: $TARGET"
exit 1
fi
backup_acl() {
local tag="$1"
local f="${BACKUP_DIR}/${tag}_$(date +%Y%m%d_%H%M%S).ace"
synoacltool -get "$TARGET" 2>/dev/null > "$f"
log "backup: $f"
}
find_marker() {
local line
line=$(synoacltool -get "$TARGET" 2>/dev/null | grep -F "$ACE" | head -1)
if [ -z "$line" ]; then
echo -1
return
fi
echo "$line" | grep -oE '^\s*\[([0-9]+)\]' | tr -d ' \t[]' | head -1
}
lock() {
local cur
cur=$(find_marker)
if [ "$cur" != "-1" ]; then
log "SKIP lock: deny already present at ACE[$cur]"
return 0
fi
backup_acl "pre_lock"
synoacltool -add "$TARGET" "$ACE" >> "$LOG" 2>&1
local new
new=$(find_marker)
if [ "$new" = "-1" ]; then
log "ERROR lock: add succeeded but marker not found"
exit 2
fi
log "OK lock: deny $GROUP inserted at ACE[$new]"
}
unlock() {
local cur
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
log "SKIP unlock: deny not present (already unlocked)"
return 0
fi
backup_acl "pre_unlock"
synoacltool -del "$TARGET" "$cur" >> "$LOG" 2>&1
local after
after=$(find_marker)
if [ "$after" != "-1" ]; then
log "ERROR unlock: deny still at ACE[$after]"
exit 3
fi
log "OK unlock: deny $GROUP removed"
}
case "${1:-}" in
lock) lock ;;
unlock) unlock ;;
status)
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
echo "状态: 未上锁 (${GROUP} 可读)"
else
echo "状态: 已上锁 (deny at ACE[$cur])"
fi
;;
*)
echo "usage: $0 {lock|unlock|status}" >&2
exit 1
;;
esac
手动测试顺序
上传后不要急着加计划任务,先在 SSH 里跑三条:
chmod 700 /usr/local/sbin/kid_video_guard.sh
/usr/local/sbin/kid_video_guard.sh status
/usr/local/sbin/kid_video_guard.sh lock
/usr/local/sbin/kid_video_guard.sh unlock
如果 lock 后孩子用户组不能读视频目录,unlock 后又恢复,才进入定时阶段。这个顺序很像装门锁:先拿钥匙试开关,再让家人正式使用。
在 DSM 7.2.1 上人工配置计划任务
DSM 的人工路径是:
- 打开 DSM 控制面板。
- 进入“任务计划”。
- 新增“计划的任务”里的“用户定义的脚本”。
- 用户选择
root。 - 分别创建
unlock和lock任务。 - 在“任务设置”里填命令。
命令示例:
/usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
/usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
建议先建四个任务:
| 时间 | 动作 | 说明 |
|---|---|---|
| 11:00 | unlock |
午饭前开放 |
| 13:00 | lock |
午休或下午重新上锁 |
| 17:00 | unlock |
晚饭前开放 |
| 18:00 | lock |
晚饭后重新上锁 |

为什么不直接用一个复杂脚本判断时间?可以,但我更喜欢“每个任务只做一个动作”。出了问题时,你一眼能看到是哪条任务没有跑。复杂判断塞进脚本里,排错时反而要同时看时间、星期、节假日、脚本逻辑和权限状态。
Agent 自动配置方法:给 AI 的任务边界要写清楚
如果让 AI Agent 来做这件事,不要只说“帮我配置一下”。更好的提示词是:
请通过 SSH 登录我的 Synology DSM 7.2.1 NAS。
目标:安装一个 kid_video_guard.sh,只对一个儿童视频目录做定时 ACL deny/allow。
约束:
1. 不要删除任何用户、用户组、共享目录。
2. 不要重写整个 ACL,只能插入或删除脚本自己的 deny ACE。
3. 所有真实目录、用户组、主机名、IP 在最终报告中都要脱敏。
4. 修改 /etc/crontab 前必须备份。
5. 安装后必须执行 status,并 grep kid_video_guard 验证计划任务存在。
开放窗口:11:00-13:00,17:00-18:00。
AI Agent 配置的优势不是“它会敲命令”,而是它能按检查清单执行:先看目录是否存在,再看组是否存在,再写脚本,再加定时任务,最后复查。你给它的边界越清楚,它越不容易把“锁一个柜门”做成“拆一面墙”。

三端一键部署脚本
下面三份脚本的共同特点是:不依赖第三方服务,只用本机自带 SSH/SCP,把 NAS 端脚本上传到 /usr/local/sbin/,备份 /etc/crontab,删除旧的同名任务,再写入四条新任务。
Windows 11 PowerShell
# Windows 11 PowerShell: deploy-kid-video-guard.ps1
# Run in an elevated PowerShell window.
param(
[string]$NasHost = "192.168.xxx.xxx",
[string]$NasUser = "root",
[string]$Target = "/volumeX/Family/Video/Kids",
[string]$Group = "KIDS_GROUP"
)
$ErrorActionPreference = "Stop"
$local = Join-Path $env:TEMP "kid_video_guard.sh"
$install = Join-Path $env:TEMP "install_kid_video_guard.sh"
@'
#!/bin/bash
# kid_video_guard.sh - Synology DSM 7.2.x directory curfew by ACL.
# Usage:
# kid_video_guard.sh lock
# kid_video_guard.sh unlock
# kid_video_guard.sh status
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
set -e
TARGET="/volumeX/Family/Video/Kids" # 改成你的儿童视频目录
GROUP="KIDS_GROUP" # 改成你的儿童用户组
ACE="group:${GROUP}:deny:rwxpdDaARWcCo:fd--"
LOG="/var/log/kid_video_guard.log"
BACKUP_DIR="/var/backups/kid_video_guard"
mkdir -p "$BACKUP_DIR"
ts() { date "+%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
if [ ! -d "$TARGET" ]; then
log "ERROR: target dir not found: $TARGET"
exit 1
fi
backup_acl() {
local tag="$1"
local f="${BACKUP_DIR}/${tag}_$(date +%Y%m%d_%H%M%S).ace"
synoacltool -get "$TARGET" 2>/dev/null > "$f"
log "backup: $f"
}
find_marker() {
local line
line=$(synoacltool -get "$TARGET" 2>/dev/null | grep -F "$ACE" | head -1)
if [ -z "$line" ]; then
echo -1
return
fi
echo "$line" | grep -oE '^\s*\[([0-9]+)\]' | tr -d ' \t[]' | head -1
}
lock() {
local cur
cur=$(find_marker)
if [ "$cur" != "-1" ]; then
log "SKIP lock: deny already present at ACE[$cur]"
return 0
fi
backup_acl "pre_lock"
synoacltool -add "$TARGET" "$ACE" >> "$LOG" 2>&1
local new
new=$(find_marker)
if [ "$new" = "-1" ]; then
log "ERROR lock: add succeeded but marker not found"
exit 2
fi
log "OK lock: deny $GROUP inserted at ACE[$new]"
}
unlock() {
local cur
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
log "SKIP unlock: deny not present (already unlocked)"
return 0
fi
backup_acl "pre_unlock"
synoacltool -del "$TARGET" "$cur" >> "$LOG" 2>&1
local after
after=$(find_marker)
if [ "$after" != "-1" ]; then
log "ERROR unlock: deny still at ACE[$after]"
exit 3
fi
log "OK unlock: deny $GROUP removed"
}
case "${1:-}" in
lock) lock ;;
unlock) unlock ;;
status)
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
echo "状态: 未上锁 (${GROUP} 可读)"
else
echo "状态: 已上锁 (deny at ACE[$cur])"
fi
;;
*)
echo "usage: $0 {lock|unlock|status}" >&2
exit 1
;;
esac
'@.Replace('/volumeX/Family/Video/Kids', $Target).Replace('KIDS_GROUP', $Group) | Set-Content -Encoding UTF8 $local
@'
set -e
install -m 700 /tmp/kid_video_guard.sh /usr/local/sbin/kid_video_guard.sh
cp /etc/crontab /etc/crontab.bak.$(date +%Y%m%d_%H%M%S)
grep -v kid_video_guard /etc/crontab > /tmp/crontab.clean
cat /tmp/crontab.clean > /etc/crontab
cat >> /etc/crontab <<'CRON'
0 11 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 13 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
0 17 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 18 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
CRON
/usr/local/sbin/kid_video_guard.sh status
grep kid_video_guard /etc/crontab
'@ | Set-Content -Encoding ASCII $install
scp $local "${NasUser}@${NasHost}:/tmp/kid_video_guard.sh"
scp $install "${NasUser}@${NasHost}:/tmp/install_kid_video_guard.sh"
ssh "${NasUser}@${NasHost}" "sh /tmp/install_kid_video_guard.sh"
Ubuntu 26.04
#!/usr/bin/env bash
# Ubuntu 26.04: deploy-kid-video-guard.sh
set -euo pipefail
NAS_HOST="${NAS_HOST:-192.168.xxx.xxx}"
NAS_USER="${NAS_USER:-root}"
TARGET="${TARGET:-/volumeX/Family/Video/Kids}"
GROUP="${GROUP:-KIDS_GROUP}"
tmp="$(mktemp)"
cat > "$tmp" <<'SCRIPT'
#!/bin/bash
# kid_video_guard.sh - Synology DSM 7.2.x directory curfew by ACL.
# Usage:
# kid_video_guard.sh lock
# kid_video_guard.sh unlock
# kid_video_guard.sh status
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
set -e
TARGET="/volumeX/Family/Video/Kids" # 改成你的儿童视频目录
GROUP="KIDS_GROUP" # 改成你的儿童用户组
ACE="group:${GROUP}:deny:rwxpdDaARWcCo:fd--"
LOG="/var/log/kid_video_guard.log"
BACKUP_DIR="/var/backups/kid_video_guard"
mkdir -p "$BACKUP_DIR"
ts() { date "+%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
if [ ! -d "$TARGET" ]; then
log "ERROR: target dir not found: $TARGET"
exit 1
fi
backup_acl() {
local tag="$1"
local f="${BACKUP_DIR}/${tag}_$(date +%Y%m%d_%H%M%S).ace"
synoacltool -get "$TARGET" 2>/dev/null > "$f"
log "backup: $f"
}
find_marker() {
local line
line=$(synoacltool -get "$TARGET" 2>/dev/null | grep -F "$ACE" | head -1)
if [ -z "$line" ]; then
echo -1
return
fi
echo "$line" | grep -oE '^\s*\[([0-9]+)\]' | tr -d ' \t[]' | head -1
}
lock() {
local cur
cur=$(find_marker)
if [ "$cur" != "-1" ]; then
log "SKIP lock: deny already present at ACE[$cur]"
return 0
fi
backup_acl "pre_lock"
synoacltool -add "$TARGET" "$ACE" >> "$LOG" 2>&1
local new
new=$(find_marker)
if [ "$new" = "-1" ]; then
log "ERROR lock: add succeeded but marker not found"
exit 2
fi
log "OK lock: deny $GROUP inserted at ACE[$new]"
}
unlock() {
local cur
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
log "SKIP unlock: deny not present (already unlocked)"
return 0
fi
backup_acl "pre_unlock"
synoacltool -del "$TARGET" "$cur" >> "$LOG" 2>&1
local after
after=$(find_marker)
if [ "$after" != "-1" ]; then
log "ERROR unlock: deny still at ACE[$after]"
exit 3
fi
log "OK unlock: deny $GROUP removed"
}
case "${1:-}" in
lock) lock ;;
unlock) unlock ;;
status)
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
echo "状态: 未上锁 (${GROUP} 可读)"
else
echo "状态: 已上锁 (deny at ACE[$cur])"
fi
;;
*)
echo "usage: $0 {lock|unlock|status}" >&2
exit 1
;;
esac
SCRIPT
TARGET="$TARGET" GROUP="$GROUP" perl -0pi -e 's|\Q/volumeX/Family/Video/Kids\E|$ENV{TARGET}|g; s|\QKIDS_GROUP\E|$ENV{GROUP}|g' "$tmp"
scp "$tmp" "${NAS_USER}@${NAS_HOST}:/tmp/kid_video_guard.sh"
ssh "${NAS_USER}@${NAS_HOST}" '
set -e
install -m 700 /tmp/kid_video_guard.sh /usr/local/sbin/kid_video_guard.sh
cp /etc/crontab /etc/crontab.bak.$(date +%Y%m%d_%H%M%S)
grep -v kid_video_guard /etc/crontab > /tmp/crontab.clean
cat /tmp/crontab.clean > /etc/crontab
cat >> /etc/crontab <<CRON
0 11 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 13 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
0 17 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 18 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
CRON
/usr/local/sbin/kid_video_guard.sh status
grep kid_video_guard /etc/crontab
'
macOS 26
#!/usr/bin/env bash
# macOS 26: deploy-kid-video-guard-macos.sh
set -euo pipefail
NAS_HOST="${NAS_HOST:-192.168.xxx.xxx}"
NAS_USER="${NAS_USER:-root}"
TARGET="${TARGET:-/volumeX/Family/Video/Kids}"
GROUP="${GROUP:-KIDS_GROUP}"
tmp="$(mktemp)"
cat > "$tmp" <<'SCRIPT'
#!/bin/bash
# kid_video_guard.sh - Synology DSM 7.2.x directory curfew by ACL.
# Usage:
# kid_video_guard.sh lock
# kid_video_guard.sh unlock
# kid_video_guard.sh status
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
set -e
TARGET="/volumeX/Family/Video/Kids" # 改成你的儿童视频目录
GROUP="KIDS_GROUP" # 改成你的儿童用户组
ACE="group:${GROUP}:deny:rwxpdDaARWcCo:fd--"
LOG="/var/log/kid_video_guard.log"
BACKUP_DIR="/var/backups/kid_video_guard"
mkdir -p "$BACKUP_DIR"
ts() { date "+%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
if [ ! -d "$TARGET" ]; then
log "ERROR: target dir not found: $TARGET"
exit 1
fi
backup_acl() {
local tag="$1"
local f="${BACKUP_DIR}/${tag}_$(date +%Y%m%d_%H%M%S).ace"
synoacltool -get "$TARGET" 2>/dev/null > "$f"
log "backup: $f"
}
find_marker() {
local line
line=$(synoacltool -get "$TARGET" 2>/dev/null | grep -F "$ACE" | head -1)
if [ -z "$line" ]; then
echo -1
return
fi
echo "$line" | grep -oE '^\s*\[([0-9]+)\]' | tr -d ' \t[]' | head -1
}
lock() {
local cur
cur=$(find_marker)
if [ "$cur" != "-1" ]; then
log "SKIP lock: deny already present at ACE[$cur]"
return 0
fi
backup_acl "pre_lock"
synoacltool -add "$TARGET" "$ACE" >> "$LOG" 2>&1
local new
new=$(find_marker)
if [ "$new" = "-1" ]; then
log "ERROR lock: add succeeded but marker not found"
exit 2
fi
log "OK lock: deny $GROUP inserted at ACE[$new]"
}
unlock() {
local cur
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
log "SKIP unlock: deny not present (already unlocked)"
return 0
fi
backup_acl "pre_unlock"
synoacltool -del "$TARGET" "$cur" >> "$LOG" 2>&1
local after
after=$(find_marker)
if [ "$after" != "-1" ]; then
log "ERROR unlock: deny still at ACE[$after]"
exit 3
fi
log "OK unlock: deny $GROUP removed"
}
case "${1:-}" in
lock) lock ;;
unlock) unlock ;;
status)
cur=$(find_marker)
if [ "$cur" = "-1" ]; then
echo "状态: 未上锁 (${GROUP} 可读)"
else
echo "状态: 已上锁 (deny at ACE[$cur])"
fi
;;
*)
echo "usage: $0 {lock|unlock|status}" >&2
exit 1
;;
esac
SCRIPT
TARGET="$TARGET" GROUP="$GROUP" perl -0pi -e 's|\Q/volumeX/Family/Video/Kids\E|$ENV{TARGET}|g; s|\QKIDS_GROUP\E|$ENV{GROUP}|g' "$tmp"
scp "$tmp" "${NAS_USER}@${NAS_HOST}:/tmp/kid_video_guard.sh"
ssh "${NAS_USER}@${NAS_HOST}" '
set -e
install -m 700 /tmp/kid_video_guard.sh /usr/local/sbin/kid_video_guard.sh
cp /etc/crontab /etc/crontab.bak.$(date +%Y%m%d_%H%M%S)
grep -v kid_video_guard /etc/crontab > /tmp/crontab.clean
cat /tmp/crontab.clean > /etc/crontab
cat >> /etc/crontab <<CRON
0 11 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 13 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
0 17 * * * root /usr/local/sbin/kid_video_guard.sh unlock >> /var/log/kid_video_guard.log 2>&1
0 18 * * * root /usr/local/sbin/kid_video_guard.sh lock >> /var/log/kid_video_guard.log 2>&1
CRON
/usr/local/sbin/kid_video_guard.sh status
grep kid_video_guard /etc/crontab
'

风险和回滚
这类脚本最怕三件事:目录写错、组名写错、ACL 索引删错。
目录写错时,脚本会先检查 TARGET 是否存在,不存在就退出。组名写错时,synoacltool 可能仍然接受字符串,但实际不会命中你想限制的人,所以安装前应该用一个测试用户确认效果。ACL 索引删错最危险,因此脚本不是固定删除 ACE[1],而是先用完整 ACE 文本查找索引,再删除那个索引。
回滚有两层。
第一层是解锁:
/usr/local/sbin/kid_video_guard.sh unlock
第二层是手工恢复备份。备份文件是 synoacltool -get 的输出,主要用于对照和审计。DSM ACL 的恢复方式建议谨慎处理,最好先在测试目录验证命令,再在真实目录操作。对于家庭场景,绝大多数事故用 unlock 就能恢复,因为脚本只删除自己加的那条 deny。
Q&A
Q:为什么不直接停用孩子账号?
A:停用账号是“大门锁”。本文方案是“柜门锁”。如果你的目标是晚上完全不能登录 NAS,停用账号更直接;如果目标只是限制动画目录,ACL 更精确。
Q:为什么不用 File Station 手动改权限?
A:偶尔改一次可以。每天改、一天改四次,就应该交给计划任务。自动化不是为了炫技,是为了把重复动作从家庭对话里拿掉。
Q:synoacltool 是官方稳定 API 吗?
A:它是 DSM 系统里的内部命令,很多管理员会用它排查和处理 ACL,但不要把它当成跨版本永远稳定的公开 API。升级 DSM 后,先在测试目录跑 status/lock/unlock。
Q:可以只在工作日执行吗?
A:可以。人工配置时在 DSM 计划任务里选星期;Agent 或脚本配置时调整 cron 的星期字段。例如只在周一到周五执行,可以把最后一个 * 改成 1-5。
Q:会不会影响管理员或备份账号?
A:只要管理员和备份账号不属于被限制的儿童组,就不应该受影响。更稳妥的做法是用儿童专用组承载这条规则,不要拿 users 这种大组做 deny。
Q:为什么要保留日志?
A:日志是家庭自动化的“监控录像”。不是为了每天看,而是出问题时能回答“到底有没有发生”。没有日志的自动化,很容易变成新的玄学。
参考资料
- Synology DSM Help: Task Scheduler
- Synology DSM Help: Terminal & SNMP
- Synology DSM Help: Shared Folder permissions
- Synology Knowledge Center: How to sign in to DSM with root permission via SSH/Telnet