PVE 9关闭“无有效订阅”弹窗

PVE 9关闭“无有效订阅”弹窗
狂犬主子PVE 免费使用时会显示“没有有效订阅”的提醒弹窗。社区里常见的做法,是修改 proxmox-widget-toolkit 里的前端文件:
1 | /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js |
具体来说,是在 checked_command 函数开头插入一句:
1 | orig_cmd(); return; |
让订阅检查直接返回,不再弹提醒。
问题在于,这个文件不是你自己维护的文件,它属于 proxmox-widget-toolkit 软件包。只要 APT 升级时更新了这个包,dpkg 就会把新版文件装回去,你之前的手工修改也就被覆盖了。结果就是:补丁失效,订阅弹窗又回来。
所以真正要解决的不是“怎么改一次文件”,而是“怎么在软件包升级把文件覆盖后,自动再补一次”。
为什么不用 Cron 定时任务?
Cron 也能做这件事,比如每天检查一次 proxmoxlib.js 里还有没有补丁,没有就补上。但它不太贴合问题的触发方式。
补丁失效的时机很明确:APT 安装或升级了 proxmox-widget-toolkit,文件被覆盖。Cron 是按时间轮询的,它不知道刚才有没有升级软件包。于是会出现几个尴尬情况:
-
升级完成后补丁已经失效,但 Cron 还没到点,弹窗会先回来;
-
如果把 Cron 设得很频繁,大多数时候只是空跑;
-
如果设得不频繁,升级后就会有一段空窗期;
-
还要额外维护一个定时任务。
Cron 不是不能用,而是它解决的是“定时检查”,不是“升级后立刻修复”。
为什么用 APT 钩子更合适?
APT 本身支持在 dpkg 安装完成后执行钩子,也就是 DPkg::Post-Invoke。这个时机正好卡在软件包文件已经被安装、可能覆盖原文件之后。
用它来做这件事有几个好处:
-
只在 APT 安装或升级软件包后触发,不需要每天空跑;
-
proxmox-widget-toolkit更新覆盖文件后,能马上检查并重新打补丁; -
脚本可以做幂等判断,已经打过补丁就跳过,不会重复插入;
-
不需要等下一次定时任务,触发时机和问题原因是对应的。
这也是我认为它比 Cron 更适合这个场景的原因。
下面是把这件事自动化的配置。整个方案拆成两个文件:
| 文件 | 作用 |
|---|---|
/usr/local/sbin/pve-no-nag-patch |
检查并自动应用补丁 |
/etc/apt/apt.conf.d/99-pve-no-nag |
APT 安装完成后自动调用脚本 |
操作方法
1. 创建自动修复脚本
创建 /usr/local/sbin/pve-no-nag-patch:
1 | cat > /usr/local/sbin/pve-no-nag-patch <<'PY' #!/usr/bin/env python3 from pathlib import Path from datetime import datetime import shutil import subprocess JS = Path( "/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" ) LOG = Path("/var/log/pve-no-nag-patch.log") ANCHOR = "checked_command: function (orig_cmd) {" MARKER = "orig_cmd(); return;" def log(message): with LOG.open("a") as f: f.write( f"[{datetime.now():%Y-%m-%d %H:%M:%S}] " f"{message}\n" ) def main(): if not JS.is_file(): log("JS file not found; skipped") return content = JS.read_text(encoding="utf-8") if MARKER in content: log("Already patched; skipped") return if content.count(ANCHOR) != 1: log("Unknown JS structure; patch not applied") return backup = JS.with_name( JS.name + ".pre-no-nag-" + datetime.now().strftime("%Y%m%d-%H%M%S") ) shutil.copy2(JS, backup) updated = content.replace( ANCHOR, ANCHOR + " orig_cmd(); return;", 1 ) JS.write_text(updated, encoding="utf-8") if MARKER not in JS.read_text(encoding="utf-8"): shutil.copy2(backup, JS) log("Verification failed; original restored") return result = subprocess.run( ["systemctl", "restart", "pveproxy"], capture_output=True, text=True ) if result.returncode == 0: log(f"Patch applied successfully; backup: {backup}") else: log(f"Patch applied, but pveproxy restart failed: {result.stderr}") # Keep only the latest three backups backups = sorted( JS.parent.glob("proxmoxlib.js.pre-no-nag-*"), key=lambda p: p.stat().st_mtime, reverse=True ) for old in backups[3:]: old.unlink(missing_ok=True) if __name__ == "__main__": try: main() except Exception as e: log(f"ERROR: {e}") PY |
设置执行权限:
1 | chmod 755 /usr/local/sbin/pve-no-nag-patch |
这个脚本不是无脑替换。它会先判断文件是否存在,再判断是否已经打过补丁。已经打过就跳过;找不到唯一目标锚点,就记录“结构不认识”然后不动文件。真正修改前会备份,修改后会再验证一次,验证失败就恢复原文件。最后重启 pveproxy,并且只保留最近三个备份。
2. 配置 APT 自动钩子
创建 APT 钩子文件:
1 | cat > /etc/apt/apt.conf.d/99-pve-no-nag <<'EOF' DPkg::Post-Invoke { "/usr/local/sbin/pve-no-nag-patch"; }; EOF |
这样以后通过 APT 安装或升级软件包,dpkg 完成安装后就会自动调用这个脚本。
3. 手动测试一次
先别等下一次升级,手动跑一遍:
1 | /usr/local/sbin/pve-no-nag-patch |
检查补丁是否存在:
1 | grep -c 'orig_cmd(); return;' \ /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js |
正常输出:
1 | 1 |
确认 APT 是否读取了钩子:
1 | apt-config dump | grep -F 'pve-no-nag-patch' |
应该能看到:
1 | DPkg::Post-Invoke:: "/usr/local/sbin/pve-no-nag-patch"; |
查看日志:
1 | tail -20 /var/log/pve-no-nag-patch.log |
如果日志显示已经打过补丁并跳过,或者补丁应用成功,就说明流程正常。
触发时机说明
这个钩子是在 dpkg 安装软件包之后触发的。正常的软件包升级,比如:
1 | apt update apt full-upgrade |
其中 apt full-upgrade 调用 dpkg 安装软件包后,会自动检查和修复补丁。
单独执行 apt update 不会触发,因为它只更新软件包索引,不安装软件包。这一点要分清楚。
以后不想用了怎么恢复?
如果以后不想屏蔽订阅提醒,先删掉钩子和脚本:
1 | rm -f /etc/apt/apt.conf.d/99-pve-no-nag rm -f /usr/local/sbin/pve-no-nag-patch |
然后重装官方前端包:
1 | apt install --reinstall proxmox-widget-toolkit |
最后重启:
1 | systemctl restart pveproxy |
这样就会恢复官方原版前端文件,订阅提醒也会回来。
总结一下
这个方案的核心不是“又写了一个补丁脚本”,而是把修复时机放在正确的位置。补丁失效是因为 APT 升级覆盖文件,那就让 APT 在安装完成后自动检查一次。比起 Cron 定时轮询,DPkg::Post-Invoke 更直接,也更省心。



