MoreRSS

site iconmikusa | 初之音修改

生物专业。在粤打工。博客主要内容是开箱 / 推荐 / 动画评鉴 / 教程。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

mikusa | 初之音的 RSS 预览

Typecho 后台夜间模式插件:DarkMode

2026-08-09 02:01:00

半夜糊 PandaBangumi 的时候被 Typecho 的后台亮得晃眼,遂让 Codex 编写了一个夜间模式插件。

效果对比图
效果对比图

项目地址:Typecho-Plugin-DarkMode

安装

先进入Typecho 的插件文件夹:

usr/plugins/

然后克隆并重命名仓库:

git clone https://github.com/mikusaa/Typecho-Plugin-DarkMode.git DarkMode

进入 Typecho 后台的「控制台 → 插件」,找到 DarkMode 并启用即可。

插件安装要求:

  • Typecho 1.3.0 及以上(其他版本未测试);
  • PHP 8.2 及以上;
  • 支持 prefers-color-scheme 的现代浏览器;
  • 在线更新需要 PHP cURL 扩展,以及 DarkMode 插件目录的写入权限。

功能

目前插件支持:

  • 全后台夜间外观
  • 跟随设备、浅色和深色三种模式(默认「跟随设备」)
  • 一键更新

实现方式没什么特别的,JavaScript 根据浏览器外观偏好给页面打上对应标记,再由 CSS 覆盖 Typecho 原有的后台样式,并不会修改 Typecho 核心文件。

更新功能则是参考了 APlayer。我看总共也没几个文件,直接拉取 GitHub 仓库,更新起来比较方便。

使用

按钮我放在了右上角,点击即可切换。

想不到什么比较契合 Typecho 的样式,就设计成这样了。

演示

大多数页面效果都差不太多,这里拎几个重点给大家看看。

后台主页

撰写文章

网站外观

基本设置

官方的提示横幅会额外闪一下,在夜间模式下特别扎眼。我让 Codex 一并干掉了。

最后

有什么 bug 可以在这里留言,或是 GitHub 仓库发 issue,我会让 AI 加班加点修掉!

PandaBangumi for Typecho 更新!

2026-08-06 15:06:00

本次 PandaBangumi 4.0.0 带来了以下更新:

  • 追番日历、单部条目卡片回归
  • 支持动画、书籍、游戏、三次元和音乐收藏
  • 条目卡片支持动画、书籍、游戏、三次元和音乐
  • 重新设计的响应式样式
  • Bangumi API 镜像支持
  • 封面可缓存到本站
  • 收藏列表按需分页
  • 后台代码速查与一键复制
  • 编辑器快捷插入条目卡片
  • 更完善的 PJAX 适配

以及缓存、请求与错误处理等后端改进。

展示页面 | GitHub 仓库 | 插件下载

本项目由 AlanDecode 创建,现基于 Izumiko 维护的版本 继续开发。4.0 的重构过程中使用了 AI 辅助编程。

安装

安装前请确认运行环境满足以下要求:

  • PHP 8.0 或更高版本
  • PHP 启用 curl 和 openssl
  • 插件目录允许 PHP 写入

随后:

  1. Releases 下载发布包并解压。
  2. 将插件文件夹重命名为 PandaBangumi
  3. 上传到 Typecho 的 usr/plugins 目录。
  4. 在 Typecho 后台启用插件。
  5. 填写 Bangumi 用户 ID,并按需设置 API 镜像、封面加载方式等选项。

PandaBangumi 插件设置页面
PandaBangumi 插件设置页面

启用后,可在插件设置页生成收藏列表、追番日历和条目卡片代码;文章编辑器工具栏也会出现条目卡片按钮,可直接输入 Subject ID,或直接粘贴 Bangumi 条目链接。

编辑器中的 Bangumi 条目卡片按钮
编辑器中的 Bangumi 条目卡片按钮

支持解析 bangumi.tv、bgm.tv、chii.in 三种域名的 HTTPS 条目链接。

设置

  • 用户 ID 是 Bangumi 主页 /user/ 后面的用户名或数字 ID。
  • Bangumi API 镜像为可选项,留空时使用官方 https://api.bgm.tv
  • 封面可以由访客浏览器直接加载,也可以缓存到本站后,通过本站地址加载。
  • 数据刷新间隔默认 86400 秒,最低 300 秒。
  • 收藏数量上限默认 30,可设为 0 至 300;日历的「仅在看」不受这个数量限制。

更新

PandaBangumi 4.0 重做了缓存目录、图片配置和前端入口,不再读取或迁移旧版缓存,因此不能直接覆盖升级。请先在 Typecho 后台停用插件,删除整个 usr/plugins/PandaBangumi 文件夹,再上传新版、重新启用并检查插件设置。

使用

下面这些代码都可以由插件设置页的「代码速查」直接生成;收藏列表、日历和条目卡片都可以出现在普通文章或独立页面中,不需要在插件设置中分别开启。

日历

「仅在看」会根据插件设置中的用户 ID 筛选自己的收藏,「全部番剧」则会显示完整的每日放送列表。

只显示自己正在看的动画:

<div data-filter="watching" class="bgm-calendar"></div>

Bangumi 每日放送:

<div class="bgm-calendar"></div>
每日放送需要展示的条目太多,这里就不具体演示了。

收藏

在看动画:

<div data-type="watching" data-cate="anime" class="bgm-collection"></div>

看过动画:

<div data-type="watched" data-cate="anime" class="bgm-collection"></div>

在看或看过的三次元:

<div data-type="watching" data-cate="real" class="bgm-collection"></div>
<div data-type="watched" data-cate="real" class="bgm-collection"></div>

在读书籍:

<div data-type="reading" data-cate="book" class="bgm-collection"></div>

读过书籍:

<div data-type="read" data-cate="book" class="bgm-collection"></div>

在玩游戏:

<div data-type="playing" data-cate="game" class="bgm-collection"></div>

玩过游戏:

<div data-type="played" data-cate="game" class="bgm-collection"></div>

在听或听过的音乐:

<div data-type="listening" data-cate="music" class="bgm-collection"></div>
<div data-type="listened" data-cate="music" class="bgm-collection"></div>

卡片

推荐直接使用文章编辑器中的 Bangumi 按钮,手动插入时只需要 Subject ID。Subject ID 就是 Bangumi 条目地址 /subject/ 后面的数字,例如 https://bgm.tv/subject/364450 中的 364450。或手动插入:

<div class="bgm-card" data-id="364450"></div>

动画:

三次元:

游戏:

书籍:

音乐:

注意

关于镜像

Bangumi API 镜像只用于服务端数据请求,镜像地址不会直接暴露给访客。封面仍然使用 API 返回的图片地址;如果你计划自建 API 和图片镜像,不希望访客直接请求外部封面,可以将封面加载方式设置为「缓存到本站后加载」。

使用本站缓存时,请确保插件目录允许 PHP 写入。缓存文件保存在 usr/plugins/PandaBangumi/.cache/ 中。Apache 会使用插件附带的 .htaccess 阻止直接访问;Nginx 和 Caddy 不会读取 .htaccess,需要在站点配置中额外禁止访问该目录。

Nginx:

location ~ (^|/)\.cache(/|$) {
    return 404;
}

Caddy:

@hiddenCache path_regexp hiddenCache (^|/)\.cache(/|$)
respond @hiddenCache 404
如果站点已经配置了全局隐藏目录保护,无需重复添加上述规则。配置完成后,可以尝试访问插件目录下的 .cache 路径,确认服务器返回 403 或 404。

关于数据刷新

PandaBangumi 会按照设置的刷新间隔缓存 JSON 数据。在 Bangumi 修改收藏状态后,博客页面不一定会立即更新;默认刷新间隔为 86400 秒,即 24 小时。

选择「缓存到本站后加载」时,封面不会按照上述间隔重复下载。插件会根据 JSON 数据中的封面地址复用本站缓存;数据刷新后,如果封面地址发生变化,会在下次访问时下载新封面,地址没有变化则继续使用已有文件。不再被当前数据引用的旧封面会由定期维护逐步清理。

关于自建镜像

目前公开的镜像有;

这两个 API 镜像在反代官方 API 的同时也镜像了图片,可以直接使用,并适当考虑在插件中启用封面缓存功能。

而自行搭建的具体方法,可以参考《猫猫博客:自建 Bangumi API 和图片反代》。文章给出了 nginx 和 cloudflare worker 的反代方法,caddy 反代的配置在他的仓库中。这里照搬过来:


查看 Caddy 配置详情

# =====================================================================
# Bangumi 反向代理 - Caddy 版 (Caddyfile)
# ---------------------------------------------------------------------
# 代理:
#   api.example.com  ->  api.bgm.tv   (会改写 JSON 里的 lain.bgm.tv)
#   img.example.com  ->  lain.bgm.tv  (图片)
#
# Caddy 的好处是自动签发/续期 HTTPS,省事。
#
# ⚠️ 重要:改写响应体需要 replace-response 插件,标准版 Caddy 没有内置。
#    用以下任一方式获得带该插件的 Caddy:
#       方式A(推荐): xcaddy build --with github.com/caddyserver/replace-response
#       方式B: 官方下载页自定义构建 https://caddyserver.com/download 勾选 replace-response
#
# 使用步骤:
#   1. 把所有 example.com 改成你自己的域名。
#   2. 确保 api./img. 两个子域名的 DNS 已解析到本机公网 IP(Caddy 要靠它签证书)。
#   3. caddy run --config ./bangumi-proxy.Caddyfile
#      或放到 /etc/caddy/Caddyfile 后 systemctl reload caddy
# =====================================================================

# ---- API: api.example.com -> api.bgm.tv ----
api.example.com {
    # 把 JSON 里写死的图片域名换成你自己的图片域名(需要 replace-response 插件)
    replace "lain.bgm.tv" "img.example.com"

    reverse_proxy https://api.bgm.tv {
        header_up Host api.bgm.tv
        # 关掉压缩,确保 replace 能改到响应体
        header_up Accept-Encoding ""
        transport http {
            tls
            tls_server_name api.bgm.tv
        }
    }

    # CORS(浏览器端调用时放开;纯服务端调用可删掉 header 与 @opts 这两段)
    header Access-Control-Allow-Origin "*"
    header Access-Control-Allow-Methods "GET,POST,PUT,PATCH,DELETE,OPTIONS"
    header Access-Control-Allow-Headers "Authorization,Content-Type"
    @opts method OPTIONS
    respond @opts 204
}

# ---- Images: img.example.com -> lain.bgm.tv ----
img.example.com {
    reverse_proxy https://lain.bgm.tv {
        header_up Host lain.bgm.tv
        transport http {
            tls
            tls_server_name lain.bgm.tv
        }
    }

    # 浏览器侧缓存(Caddy 标准版无服务端缓存;如需边缘缓存可用 cache-handler 插件)
    header Cache-Control "public, max-age=2592000"
    header Access-Control-Allow-Origin "*"
}

关于 PJAX

插件已经适配常见的 PJAX、Turbo 和 Turbolinks 事件。只有主题使用自定义页面切换逻辑,并且切换后组件没有加载时,才需要在主题回调中调用:

window.PandaBangumi.init();

旧版本使用的 window.initCollection() 已经移除。

就到这里了,关于更详细的插件重构记录,我会在之后整理出来。快来试试看吧~

金华日记

2026-07-31 10:45:00

家住金华的酚酞先生喊我去他那儿转转已经有很长一阵子了,可我一直找不到什么合适的时机出趟远门。我确实很想出去走走,生活总归需要些调料压压丧气,可平日被工作牵制住了手脚,假期又少得可怜。无奈,这事就这样被无限搁置,直到我准备提桶跑路。

由于种种原因,我决定离开上海,回家休息一阵,再另谋出路。如果是请假去金华玩,交通上的费用不是个小数目,回家就不一样了。虽然上海回家有直达列车,改道经过金华,行程上的开销相差无几。我和酚酞约定,我会在三月底四月初这段时间内,前去他家拜访。

最初,我是打算在工作交接完毕、看完《巫师 3》十周年的音乐会后,就向公司辞行,在 3 月的最后一周去金华玩玩,也看看有没有机会去游览其他城市。毕竟新来的后辈上手很快,几乎没有我可以插手的余地;我再怎么老油条,也无法觍着脸继续在老板眼皮子底下摸鱼。可一场突如其来的雨,打乱了我的出行计划。这场连绵不绝的春雨持续下了一周,直到三月的最后一天,才稍微放晴。

想着不能再这么放酚酞鸽子了,我决定即刻背上行囊,离开上海。

3月31日

从上海到金华的票源非常充足,也可能是两地相距不远的缘故。我慢悠悠地坐地铁到达虹桥站,在安检机前买了最近班次的车票。列车一路向南,我于中午十二点半抵达了金华。

由于这回也算是一次「说走就走的旅行」,不出意外地,我又迷了路。我一边在车站里打转,一边和酚酞保持联系。不愧是土生土长的金华人,在酚酞的指引下,我顺利地在南出站口看到了等候多时的酚酞。

我走近了些,向他挥手。虽然我们素未谋面,只是平日里通过网络闲聊,偶尔打个语音电话,这还是我们头一回真真切切地见到对方。不过,我还是一眼就认出了他。也可能是站在出站口等人的就他一个。

后来听酚酞说,在见到我时,他的第一反应是我的体型「意外地和声音不符」。哼哼,毕竟作为肥宅,自然得在网络上好好隐藏自己的属性。

我们一同打车前往他的住处。刚放下行李,窗外倏地落下了一阵雨。时针已过一点,我们稍作歇息。待雨渐稀,酚酞带我下楼,钻进了附近一家叫「老傅」的面馆。他对这家面馆评价颇高,而我正好喜欢吃面。于是,一碗热腾腾的牛肉拉面下肚,温度与滋味皆是正好。

汤足面饱后,我们一同步行回到家中。天色尚早,我开始着手预订酒店。因为基本不出门,我对酒店这方面的认知约等于零;长夜漫漫,而我和酚酞都玩守望先锋。于是,带电脑的电竞酒店便成了首选项。我货比三家,订了个价格适中、带洗衣烘干设备的电竞酒店。

当晚,酚酞亲自下厨做了几道家常菜。饭桌上,他大概暗自观察了我的饭量,事后吐槽我「光顾着扒白米饭,菜却吃得少」。饭后,我们步行前往预订的电竞酒店。

这酒店离酚酞家不远,可按照美团上显示的地址,我们只找到了一家网吧。正当我以为,「既然是电竞酒店,那藏身于网吧之中也算合情合理」时,网管否定了我的猜想。我们只好与酒店再三确认,最终在网吧旁的玻璃门后找到了接待人。这酒店连个像样的招牌都没有,怎么看都不像是我印象中的酒店:既没有前台,也没有大堂,简陋得像个出租屋。连身为本地人的酚酞,都不知道家附近还藏着这样的酒店。

夜里,酚酞向我推荐了缙云烧饼,我们分着吃了一个。味道不错,就是略咸。随后,如同无数个线上的夜晚一样,我们打起了守望先锋。并肩打游戏和隔着网线语音还是不太一样的——许久没碰枪,我更菜了,无法借口设备差掩盖我的弱鸡枪法。

4月1日

一觉睡到了中午,我们热了前一晚的剩菜简单对付了一顿。按照计划,今天是在酚酞家附近逛逛。

快递站的流浪猫
快递站的流浪猫
免费的城市自行车
免费的城市自行车

酚酞领着我在江北尝了尝蔬菜薄饼。我对昨晚烧饼的咸味还心有余悸,于是点了碗甜豆花。结果这薄饼也是咸得我头皮发麻,吃了一半就放弃了,豆花倒还不错。看来对我这个口味清淡的福建人来说,金华的本土饮食着实有些重口了。

我们继续朝着婺州公园前进。经过小码头,再穿过马路,从金华渡牌坊下走到了婺江边。

后来我才知道,婺城区是金华的老城区,「小码头」则是承载婺江千年记忆的古渡口,在上世纪 90 年代还是十分繁荣的集市。不远处的「金华渡」,则与对岸的「五百滩」一并成为城市公园的一部分。李白有诗云:「闻说金华渡,东连五百滩」,说的就是这两处。

途中经过一处亭廊,长椅上躺着午睡的行人。可酚酞却说,这都是些无家可归的流浪汉。旁边有一面墙上贴满了「金华交友」的相亲广告,远远看过去的时候,我还以为是寻子启事……

上海连日阴雨,久未见晴,我人都快要发霉了。难得碰上这样的好天气,自然要好好走动走动。我们沿着江边走走停停,途经婺州公园、彩虹桥和燕尾洲,又走过三江里和浮桥,一路晃到了古子城和万佛塔下。

如今回想起来,这一下午已经只剩下几个零散的画面:酚酞向我讲起彩虹桥刚开通时的热闹景象、浮桥对岸的万佛塔,还有古子城中的八咏楼。那段时间,我嘴上说着要好好放松一下,心里却总忍不住琢磨离职以后该怎么办。不过,沿着江边走的那几个小时里,我还是尽量将这些事情抛在脑后。晒晒太阳,呼吸新鲜空气,看看江水和路边的树。

什么都不用想,确实很舒服啊。

婺州公园
婺州公园
树杈子
树杈子
彩虹桥
彩虹桥
彩虹桥上
彩虹桥上
浮桥
浮桥
对岸的万佛塔
对岸的万佛塔
古子城
古子城
鱼
万佛塔
万佛塔

不过,由于前一晚我没睡好,这一路又走得很累。大概下午三点左右,我们便回去休息了。

我补了一觉,醒来也差不多到了晚饭时间。晚餐去的是酚酞强烈推荐的「小女当家」,这店在大众点评必吃榜上的排名非常高。我们打车到了目的地,店里几乎坐满了人,我们稍微等了一会儿才有座位。至于「吃什么」,我其实没啥特别的喜好,随手点了一道红烧肉,其余就全权交由酚酞决定了。

不得不说,这红烧肉确实不错,酱红色的汤汁浓郁醇厚,咸香中带着微甜,非常下饭!就是吃了两块便有些腻了,和其他剩菜一块打包带了回去,明天热热配饭吃。

今晚的车队颇为坎坷,最后连输了五局,才拿下一场胜利。连跪得我俩都有些郁闷,我便让酚酞早点回去休息,明天的事明天再商量。

我这么菜实在是抱歉。毕竟暴雪退出中国前,我就没怎么好好推过车,更甭提国服回归后了。

4月2日

最后一日的早晨,以外卖的福建羹开场。这是一种类似锅边的小吃,味道依然偏咸,但配上好吃的煎饺,也算瑕不掩瑜。饭后,我们骑车去看了电影《挽救计划》。从影院出来,骑车折返,临近中午。酚酞煮了锅新鲜的米饭,我俩就着昨日打包的剩菜,又叫了饭后奶茶,填饱了肚皮。差不多到了该出发的点,酚酞将我送上了网约车。

这次金华之行多少有些不够尽兴,这几天虽然过得轻松,但一想到回家以后还得重新找工作,心里终究没法完全放松下来。等之后生活安定一些,再找机会约酚酞好好玩一趟吧。

记一种 Chrome 绿色化方案

2026-06-10 23:59:00

所谓软件的「绿色化」,或称「便携化」,是指将原本需要通过安装程序部署的软件,使用其他方式处理成「解压即用」的便携形式,使其尽量减少对系统目录和注册表的依赖,以便让配置、缓存等数据能直接保存在软件自身目录中。

不过实际情况是,大多数软件都采用「程序与数据分离」的设计方式。例如 Google Chrome。

我们从 Google Chrome 官网 下载的 ChromeSetup.exe 程序,实际上是一个在线安装器。

双击运行后,它会自动将 Chrome 本体安装到 C:\Program Files\Google\Chrome 路径下。

用户数据则保存在 %LOCALAPPDATA%\Google\Chrome\User Data(对应 C:\Users\用户名\AppData\Local\Google\Chrome\User Data)目录中。

这种形式虽说符合 Windows 的标准设计理念1,却不便备份或转移。如果在重装系统时没有及时备份这些数据,那么即便后续能依赖账号同步恢复部分浏览器配置,扩展程序的本地配置、网站登录状态(如 Cookie、会话信息)以及其他无法跟随账号同步的数据,仍会因本地数据缺失而无法恢复。

绿色化要做的,就是在不改变软件功能的前提下,将程序本体与用户数据尽可能收拢到同一目录中,使其摆脱对系统固定路径的依赖,从而获得更强的便携性与可迁移性。

因此,接下来就让我用最直白、最直接、最不绕弯的方式告诉你,如何快速绿色化 Chrome。

说明

我所了解的绿色化 Chrome 的方法,源自 Chrome++ Next 项目。该项目提供的 version.dll 注入文件,除了增强标签页操作、快捷键映射、启动行为控制等功能外,还支持将浏览器数据目录重定向到指定位置,从而实现便携化运行。本文主要使用的,便是它提供的这一能力。

为避免出现不可预测的问题,建议在全新安装的前提下制作便携版,并从零开始使用。对于已在使用中的 Chrome 配置文件,本文不讨论迁移方案。

另:需要拥有科学上网环境。

准备

注入文件

前往 Chrome++ Next Releases 页面,下载注入文件压缩包。

解压后,选择适合你系统架构(现在一般是 X64)的注入文件,备用。

App 文件夹内是便携化需要用到的注入文件。

离线安装包

由于从官网下载的是在线安装包,为了制作便携化版本,我们需要下载离线安装包。你可以在 这里这里这里 下载最新版本的 EXE 安装包。

一般选择正式版64位
一般选择正式版64位

下载后,不要双击打开!不要双击打开!不要双击打开!

安装任意解压缩软件,右键程序当作压缩包打开。

解压后得到 Chrome-bin 文件夹,其中包含 Chrome 的全部程序文件。

制作

制作便携版就非常方便了。开两个窗口,分别打开注入文件所在文件夹和离线包文件夹,把 Chrome 程序文件全部移动到 App 文件夹即可。

直接把 X64 这个文件夹重命名为 Chrome,移动到你心仪的路径下,例如 D:\Chrome,双击 chrome.exe 即可启动浏览器。

数据会自动保存在 Data 文件夹内。

其他

另有安装器可直接安装 Chrome 和注入文件,详见 chrome_updater 项目。

下载压缩包解压后,得到 chrome_updater.exe 程序。

一般是amd64
一般是amd64

将该程序移动至如 D:\Chrome\App 文件夹内(App 区分大小写!),双击打开。

程序会自动检测安装路径。点击检测 Chrome 最新版本,如果曾安装过 Chrome,也会检测当前版本。下载安装会自动覆盖当前旧版本文件。

Chrome 安装完成后,切换至 Chrome++ 窗口,下载安装最新版本注入文件。

后续更新时,只需定期运行 chrome_updater 更新 Chrome 主程序,并同步更新 Chrome++ 注入文件即可,无需重新制作便携版。

最后

你可以在 这里 下载到本文提到的所有文件。(不包括 Chrome 离线安装包)


  1. 有利于多用户隔离、权限管理以及系统升级时的数据维护。

Mikusa Annual Issue 6

2026-04-15 23:02:00

我对 AI 的使用一直相当朴素,无非是把它当成一个更聪明一点的搜索引擎:遇到不会的东西,就在网页上来回车轮战,让 AI 猜解决方案,再把答案手动搬回编辑器里慢慢调试,最后一点点把功能做出来。例如之前提到的《给博客代码框增加复制按钮》,就是这样一点一点折腾出来的。

彼时的 AI 于我而言,更像一个“贴心的老师”——它愿意告诉我答案,却不能直接帮我写作业。因此,我对博客的修改大多只能停留在小修小补的层面。涉及稍微复杂一些的东西,我就无能为力了。

直到最近,我接触到了支持 AI Agent 的 IDE。第一次尝试借助 Google Antigravity 内置的 Claude Opus 模型编写小工具,亲眼看着代码从零开始一点点搭起来,我才真正意识到,如今的 AI 编程工具和前几年已经不是一回事了。它不再只是帮你补全几行代码、修正几处语法错误,而是可以直接阅读、理解,甚至参与修改整个项目。对于我这种完全不懂编程的小白来说,这种体验多少有点魔幻。

于是,我也终于决定开始实施一项蓄谋已久的计划:翻新 VOID 主题。

更新 VOID 主题

如果你对这个 AI 适配的 VOID 主题感兴趣,可以访问 这里 下载主题压缩包并安装。

距离 VOID 上一个 3.5.1 版本发布,已经过去了五年多。其间,Typecho 先后经历了 1.2.1 的小版本更新和 1.3.0 的大版本更新,而旧版 VOID 早在 1.2.0 时期就已经有启用后白屏的问题。

所以这一次,我没有一上来就急着做兼容适配,而是先借助 Claude Opus 4.6,把这些年我自己陆续加在 VOID 上的小改动一点点迁了过去。 毕竟对我来说,这些东西比适配新版 Typecho 还要更紧迫一些。

而后,我才让 Claude Code 针对 Typecho 1.3 及 PHP 8.2 的升级作专项适配,解决主题白屏等一系列兼容性问题。这中间我充分利用了上班摸鱼的时间,断断续续修改了大约一周。

功能增强

在我使用 VOID 的这些年,基于个人需求添加或改进了许多功能。我一直在思考这些小东西的必要性,是否只有我自己才用得到。最终还是决定,既然这是我个人修改的主题,那么就应该优先考虑自己的需求。在不破坏原主题整体设计的前提下,我完全可以将这些功能整合进主题源码,并公之于众。

代码复制

代码框的复制按钮其实早就合并到官方 VOID 仓库中了,这次是让 AI 根据原生剪切板 API 做了优化。

原本是想让它抄一下 GitHub 的剪切板方案,但没能抄成功。

GitHub 的复制按钮
GitHub 的复制按钮

最后改成鼠标划过代码框才浮出复制按钮,复制成功的提示则还是放在弹窗上。

OwO 表情包

这一块我曾让 Z 酱帮忙提交了我一些新增的米哈游表情包。这次则是针对我自己真实的使用情况,对这些表情包做了一些增删。

米哈游表情包
米哈游表情包

其实常用的还是不多。

写到这我才想起来,如果新增的表情包太多,超出数量的分组会因为溢出而被隐藏。于是火速让 Codex 修了这个 bug,现在的表情包选框是这样的:

支持横向滚动
支持横向滚动

但测试的时候又发现,这个表情包框只会往下弹,如果是没有评论的文章,就会把底部都撑开。于是就让 AI 糊成了这样:

感觉实际效果还是有些不太对,等后续有想法了再作修改吧。

归档页增强

这就完全属于我自娱自乐的功能了。去年写总结的时候想看看今年总共写了多少字、哪些主题,觉得一篇篇手数好麻烦,就让 AI 糊了这么个功能。

现在,归档页支持年度文章发布统计,只需将鼠标划过年份,即可展示年度总字数与分类聚合信息;同时,在文章列表右侧增加了其关联的分类标签,实测超多分类也可以正常显示。

该功能不可关闭

文章过时提示开关

VOID 会默认在文章发布 90 天后,提示其信息可能过时。

例如这个提示
例如这个提示

对于教程类或是强时效类的文章,这个提示很有必要。但若是日常类或其他不受时间影响的文章,就有些多余了。

于是,我在文章编辑页添加了一个开关,可按需展示内容是否过时。

新文章默认关闭过时提示。

主图来源

我的博客使用了大量来自 Pixiv、Twitter 和其他网站的图片。考虑到可能存在的风险,我曾使用 Copyright 插件将主图来源展示在文末。

但 VOID 的一大特色便是其主图,主图在主题的整体布局中具有十分重要的地位。那么,主图来源理应受到同等重视。

可我对这方面的设计毫无头绪。在询问 Gemini 后,它给出了下面 4 种方案。

文章封面图源样式演示
作者名称 · 2026-02-23
📷 题图:Pixiv @某画师
题图来自 Pixiv 📷
封面图片由 某画师 提供

(正文演示区域)前些时间提到,我的群晖 NAS 数据被套件误删了。由于是第三方套件造成的...

🎨 点击切换图源展示方案预览

如你所见,我选择的是最后一种。

设置也很简单。使用 markdown 或 HTML 格式填写主图来源,即可展示在文章顶部。

主图预览

严格来说,这个功能也有些鸡肋。但对我而言,它解决了一个很具体的问题:在正式发布前,确认主图在首页的展示效果。

Typecho 并不像 WordPress 那样提供完整的文章预览功能,所以我以前只能在本地另开一个测试博客,发布之后再查看实际呈现。这样的流程不仅低效,也很折腾。于是,我索性花了几天时间,借助 AI 把这个功能做了出来。

现在,VOID 支持在编辑页面预览首页封面卡片的实际效果,支持原 VOID 主题预设的三种首页主图样式,支持预览卡片的摘要随实际摘要变动。

卡片上方的文字描述可能还需要再稍微改改,但预览效果大体上是符合实际首页的。

评论交互增强

以前的评论其实也没什么大问题,只是层级一多,就看不出来回复顺序。

中间考虑过这么设计。但这样子的话,后续的回复会随着评论量增加越来越往右边挤。

我把目光移到了隔壁 B 站。

最后还是抄了一把叔叔的设计,为 VOID 的评论区做了这几点改动:

  • 不再纠结层级问题,不管是谁回复谁,都按回复时间顺序排列
  • 第一条回复、自己回复自己,均隐去 回复@xxx 的字样
  • 将评论时间、赞踩按钮移动到了评论主体下方,回复按钮转移到踩按钮的右侧
  • 默认折叠超过两条以上的评论

折叠效果如下图所示:

折叠时会有轻微的回弹,应该是个 bug,我问不出原因来,暂且搁置……

同时,我还调整了回复按钮的交互。点击 回复按钮 展开评论框时,回复按钮会自动变成 取消回复 按钮。

我本来想干掉右侧的 取消回复 按钮,但 AI 说应当保留,避免访客找不到回复按钮,就留着了。

字数统计

又一个鸡肋的功能,用于实时预估当前编辑内容的字数、图片数和标签数。

该功能依赖 VOID 插件,随插件启用生效。

兼容适配

关于 VOID 在 Typecho 1.2.0 等后续版本白屏的问题,AI 给出了几点主要原因:

  • 继承签名对齐 (致命修复):修复了 libs/Comments.php。Typecho 1.3 核心类引入了 PHP 强类型提示,主题原有的 ___children() 等重写方法因缺少 : array 或类型不匹配导致 Fatal Error (白屏)
  • Null 传参防御:PHP 8.1+ 严禁向系统函数(如 preg_match)传递 null。我们在libs/Utils.php (UA 处理) 和 libs/Contents.php (图片尺寸) 处添加了严谨的 null 保护。
  • 调试开关:发现主题 functions.phperror_reporting(0) 会掩盖报错堆栈,适配阶段已建议通过控制台或手动调整显隐。

这一部分就太过硬核了,没有我能插嘴的余地,AI 说啥就是啥。

后台优化

这个想法,源于我在添加字数统计功能时的一次临时起意。

原本我并没有打算细化这一部分的样式,没想到 AI 在生成功能时,顺带把字数统计区域的视觉样式也设计了出来。实际效果意外地不错,于是我索性以此为起点,将整个 VOID 中涉及到的字段样式重新梳理并统一设计了一遍。

整体设计基本由 Gemini 主导。它建议我将 选项 改为滑块、将 是否 改为开关;而我则根据自己的理解,把文章类型中的 landscape 译作 封面文章,以便与旁边的 一般文章 在字面上形成对应。

我还调整了部分字段的展示逻辑。例如,只有在填写文章主图后,才会展开与主图相关的其他设置。

首页卡片预览也默认隐藏,只能手动展开查看。展开后还可以根据首页主图样式自由调整并预览实际效果。

体验优化

在把我需要的功能全部补齐之后,我又问 AI:VOID 还有哪些地方值得继续升级?它首先指出的是 PJAX 的问题。

PJAX 上一次更新是 2017 年,而今天的现代浏览器已经具备实现这类局部刷新的核心能力。于是,我让 AI 把主题里原本依赖 jquery.pjax 的插件式方案,重构成了一个基于浏览器原生能力实现的 VoidPjax。新的实现直接使用 fetchDOMParserhistory.pushState/popstate 和原生事件代理来完成页面局部跳转、容器替换与历史管理,同时保留 pjax:* 事件兼容层。这样不仅减少了对老旧插件的依赖,也让后续在脚本重跑、一次性脚本去重、Safari 兼容,以及评论区局部刷新这类细节场景上的控制更加从容。

也许应该直接干掉 PJAX,但我没什么好的想法让 AI 执行,改天再问问看。

此外,bigfoot 已经十几年没更新过了,我用 littlefoot 完整替换了 bigfoot,优化了脚注在暗色模式、触屏设备和 PJAX 场景下的体验;pangu.js 也一并更新到最新版本,并切换使用新版 API,减少脚注、数学公式附近的错误加空格问题;MathJax 也从 2.7.4 迁移到 4.1.1,并精简运行时使用的资源包。

数学公式我用得不多,实际使用中可能会存在其他未知的 bug。

jQuery 更新到了 3.7.1;Prism高亮插件更新到了 1.30.0;fancybox 大版本改动太大,更到了最后一个 3.5.7;Headroom 导航栏自动隐藏、tocbot 目录树等也一并更新到了最新版本。

其他优化则是在主题构建方面,例如:

  • 移除 node-sass,切换到 Dart sass。
  • 升级 Gulp 到 5.x,并同步更新相关依赖锁文件。
  • 迁移 Sass 模块写法,转向 meta.load-css。
  • 升级 gulp-sass、sass、del、gulp-autoprefixer、gulp-rev 等构建依赖。
  • 升级 GitHub Actions 工作流与 CI 环境到 Node 20。
  • 修复构建过程中错误扫描 node_modules 并将其内容误带入产物的问题。
  • 调整 clean / move / md5 等构建任务实现,使新工具链下构建结果稳定。
  • 迁移 ESLint 到新配置格式。

至此,VOID 主题的适配与升级,就告一段落了。完整的修改记录可以查看从 这个 开始的 commit。

更新 VOID 插件

如果你对这个 AI 适配的 VOID 插件感兴趣,可以访问 这里 下载插件压缩包并安装。

接下来是 VOID 主题配套插件的适配问题。改起来倒是方便,AI 三下五除二就糊好了。

但主要的问题是,旧版本解析评论 IP 与归属地使用的 17monipdb.dat IP库太过古老,我想更换一个支持 IPv6 的、数据更新更及时一些的数据库。AI 给出的推荐是 ip2region

因此,现在 VOID 插件使用的 IP 数据库为 ip2region xdb,支持 IPv4 与 IPv6;UA 识别范围也更完整,后台互动页可以更准确地显示访客浏览器与系统信息。

修改前
修改前
修改后
修改后

当然,还包括后台编辑器新增的实时统计面板。可在文章及独立页面实时显示字数、图片数与标签数。

更新 ExSearch 插件

如果你对这个 AI 适配的 ExSearch 插件感兴趣,可以访问 这里 下载插件压缩包并安装。

既然 VOID 插件都适配了,索性把 ExSearch 插件也糊了一遍。

当前 ExSearch 插件已适配 Typecho 1.3,并将搜索前端改为原生实现,去除了运行时对 jQuery 的依赖。另优化了搜索界面的样式、适配了夜间模式,提升桌面与移动端的一致性。

日间
日间
夜间
夜间

更新 APlayer 插件

如果你对这个 AI 适配的 APlayer 插件感兴趣,可以访问 这里 下载插件压缩包并安装。

APlayer 在 Typecho 1.3 下直接失效了。我有时需要在文章内插入音乐,就也让 ChatGPT 完整适配了下。

顺便更新了 MetingJS。在新版本 MetingJS 的加持下,Aplayer 可以默认使用 <meting-js> 中 Custom Element 来渲染播放器。因此,在常见的 PJAX 场景中,新页面内容插入后会自动初始化播放器,旧节点移除时也会自动销毁,一般不需要再额外添加 loadMeting() 一类的重载函数。

最后

本来还想再写一写关于 Typecho-Plugin-PandaBangumiTypecho-Plugin-Copyright 这两个插件的修改,前者用于展示番剧日历、卡片,后者用于在文末展示版权声明。可尝试让 AI 总结 PandaBangumi 的改动情况时(因为这个插件还是我一点点用网页糊出来的,改得比较乱),它说:

我顺手看到的几个后续注意点

  • 番剧卡片样式被限制在 .yue .bgm-card 下,如果主题没有 .yue 这个容器类,卡片会功能正常但样式不完整。
  • loadCalendar() 只取了第一个 .bgm-calendar,如果同一页放多个日历实例,后面的不会渲染 PandaBangumi.js
  • 卡片标题做了转义,但列表里的 item.name / item.name_cn 仍然直接拼进 HTML,卡片标签 tag.name 也没有转义;数据源通常可信,但严格说这块还可以再补一层 PandaBangumi.js
  • cURL 关闭了 SSL 证书校验,能减少环境问题,但安全性偏保守。

所以,这部分还得等后续继续让 AI 把这些问题修完,才能正式发布。

Copyright 插件则是我计划塞一堆声明样式,但方向没设计好,越糊越乱,也不能发出来……

所以,对主题与插件的改动记录,便到此为止了。写完这篇日志,我也该回头去填别的坑了,不然时间一长,我就要忘掉它们怎么写了。

下篇文章见!

Backrest:自建备份方案的一种可能

2026-02-23 21:25:05

前些时间提到,我的群晖 NAS 数据被套件误删了。由于是第三方套件造成的,即便我之前使用的是正版群晖硬件,也不可能就此向官方讨个说法。不过,我仍庆幸群晖有着强大的应用生态,能借助多种工具的协同配合完成重要数据的备份。例如,我通过 Hyper Backup 对 Docker 数据文件夹做了增量备份,又用 CloudSync 将这些备份同步到了云端。当本地数据意外丢失时,这些云端的备份全部得以幸免于难。

可换用飞牛的 fnOS 作为 NAS 系统之后,这套备份组合也随之失效。

fnOS 无论是界面、应用、生态还是移动端 APP ,都很符合我现阶段对一个 NAS 系统的基本需求。唯一美中不足的地方,就是它的备份功能。

作为 NAS 系统,其备份功能仍显不足。即便是迭代至正式版,它支持的备份方式也依旧有限。而且在实际使用过程中,备份到百度网盘的数据还会因为体积太大而经常上传失败。

我本想着,既然现在的 fnOS 备份不方便,说不定后续的更新迭代会优化这一块,要不就先放一边,等下次想起来再说。可谁曾想,飞牛于近日被爆出存在超高危的 0day 漏洞

这可把我吓了一跳。好在我一直通过 VPN 访问家庭网络,从未将 NAS 公开至公网,仔细检查后也没有发现病毒。

但要命的是,虽然该问题在系统更新后被修复(?),但是飞牛官方对此次事件采取的却是近乎冷处理的公关方式:既没有第一时间向用户发出明确的风险预警和紧急防护指引,也未做任何醒目的升级提醒,更是使用「异常访问风险」、「自身代码疏漏」这类模糊表述淡化问题本质,迟迟不肯正面承认此次漏洞产生的严重危害。

软件存在问题在所难免,承认错误及时处理便是,可飞牛对待此类安全问题却是这般敷衍的态度。我开始重新评估自己对该系统安全性的信任边界。这次的攻击者只是利用漏洞将 NAS 当作肉鸡对外攻击,更现实的担忧在于,若未来攻击直接针对数据本身,后果可能远比此次事件严重。我不希望遇上数据被病毒加密勒索的糟心事,不希望暴露 XP,更不想重新整理一遍动画片……

眼下 NAS 不在身边,我没法立刻更换系统,只能先决定即刻采取备份措施,避免后续出现任何数据安全意外。

但离了群晖的 Hyper Backup 和 Cloudsync 的助阵,有什么简单的方法,可以实现定时的增量备份,并将备份数据保存到云端呢?

这就需要请 Backrest 登场了。

关于

在介绍 Backrest 之前,有必要先引入一个几乎被所有备份方案反复提及的原则——「3-2-1 备份原则」。

所谓「3-2-1 原则」,是指在进行文件备份时,需要做到:

  • 3:至少保留 3 份完整的数据副本,防误删。例如:

    • 一份原始数据
    • 一份本地备份(同地 / 同机器)
    • 一份异地备份(云存储 / 异地 NAS)
  • 2:存储在 2 种不同的介质上,防硬盘损坏。例如:

    • 本地磁盘(机械硬盘、固态硬盘、光盘、磁带)
    • 云存储
  • 1:存储 1 份备份在异地,防勒索病毒、极端物理灾害。例如:

    • 异地 NAS
    • 云存储

在合理执行的前提下,可以最大限度降低数据丢失的风险。

可是,即便我们有意愿按「3-2-1 原则」执行备份,却需要面对另一个残酷的现实:如今机械硬盘的价格,已不是常人所能轻松负担的了。

8T机械硬盘
8T机械硬盘

我 2022 年 3 月购买的一块全新国行 8T 西数 HC320 的价格为 995 元。

但是!细心的小伙伴们应该已经注意到了,在「3-2-1 原则」的示例中,几乎每一步都可以将数据备份到「云存储」上。而云存储的价格,远比同等容量的硬盘便宜。

除了网盘,还有 COS、OSS、S3、B2 等一众云存储服务可供选择,价格都比购买实体硬盘要实惠,我们便可以考虑直接将数据备份到一个乃至多个不同的云端,以较低的成本满足「3-2-1 备份原则」的要求。

最终,我决定使用 Restic 作为我备份体系的核心。

Restic 是一款优秀的命令行备份工具,以快速、安全、高效著称。它支持:

  • 增量备份、数据去重和端到端加密,兼顾高效与安全
  • 多种主流存储后端,包括本地磁盘、网络共享目录(SMB/NFS)、远程服务器(SFTP/SSH)及各类对象存储(AWS S3、阿里云OSS、腾讯云COS等)、云存储服务(OpenStack Swift、Azure Blob Storage等)
  • 断点续传,备份中断后再次执行可从断点继续,无需重新开始
  • 任意快照点恢复(可恢复至某一次备份完整状态),也支持文件级精准恢复,恢复速度快且无需复杂配置,通过命令即可完成
  • 提供仓库校验命令,可定期检查备份数据块的完整性、修复损坏或丢失问题
  • 采用不可变仓库结构,备份过程中不修改已存储的历史数据块,避免历史备份损坏

但由于缺乏图形界面与调度管理功能,仍需配合脚本与 cron 实现自动化。对不想折腾这些东西的我来说,使用起来非常痛苦。

Backrest 正是在这一背景下登场的。作为一款专为简化 Restic 备份管理而设计的 Web UI 和编排工具,Backrest 通过直观的图形界面包装了 Restic 的命令行功能,并补齐了任务调度、状态监控与集中管理等关键功能,让复杂的 Restic 备份操作变得简单易行。

借助 Backrest,我们便能使用 Restic 实现一套可以长期稳定运行、也更容易被维护的备份方案。

准备

在正式开始之前,我们需要先思考一个很严肃的问题:

我们应该备份哪些数据?

备份并不是简单地把所有文件原封不动地复制一份。操作系统、软件包,甚至容器和镜像,本质上都属于可重建资源,只要环境还在,花一点时间就能重新部署。真正值得被长期保存的,是那些随着使用不断产生、积累,且一旦丢失就很难恢复的数据。

从实际使用场景来看,以下几类内容通常应当作为备份的重点:

  • 用户数据与业务数据
    例如文档、照片、媒体文件、项目资料,以及服务运行过程中产生的数据文件。这些内容往往具有唯一性,一旦丢失,几乎无法通过其他方式找回。
  • 服务与系统配置
    包括应用配置文件、服务参数、自定义脚本等。这些文件体积不大,却高度个性化。合理备份可以大幅降低系统重建时的时间成本。
  • 容器的持久化数据
    在使用 Docker 等容器化方案时,需要被备份的并不是容器本身,而是 Volume 或挂载目录中的数据。例如数据库文件、上传内容、状态数据等。
  • 高恢复成本数据

    例如游戏资源、音乐等。严格意义上说,这些文件并非完全不可再获取,但一旦涉及到命名规范、目录结构、封面整理和元数据获取,它们的恢复成本就不再只是「重新下载」那么简单。

相对应地,也有一些内容并不适合作为常规备份对象:

  • 操作系统本身
    系统可以重装,软件可以重新安装,将其纳入备份往往只会徒增体积和复杂度。
  • 缓存、临时文件和可再生成数据
    如构建产物、下载缓存、日志缓存等,这类数据即便丢失,恢复成本也相对较低。
  • 镜像、安装包等可从官方渠道重新获取的资源
  • 可下载的影音视频资源

通过这种取舍,将备份的重点从「尽可能多地保存数据」,转变为「只保存真正重要的那一部分」。不仅能有效控制备份体积和成本,也可以让后续的备份与恢复过程更加清晰、可控。

安装

这里以 Docker Compose 安装 Backrest 为例。

首先,需要在宿主机上创建好必要的数据文件夹,用于存放程序数据、配置、缓存和临时文件:

mkdir -p data config cache tmp rclone

随后新建 compose.yml 文件,填入以下内容:

services:
  backrest:
    image: garethgeorge/backrest:latest
    container_name: backrest
    hostname: backrest
    volumes:
    ## 程序数据文件夹
      - ./data:/data
      - ./config:/config
      - ./cache:/cache
      - ./tmp:/tmp
      - ./rclone:/root/.config/rclone # 挂载 rclone 配置(使用 rclone 远程时需要)
    ## 挂载本地备份路径
      - /vol1/1000/media:/vol1/1000/media
      - /volume2/docker:/volume2/docker:ro
    environment:
      - BACKREST_DATA=/data
      - BACKREST_CONFIG=/config/config.json
      - XDG_CACHE_HOME=/cache
      - TMPDIR=/tmp
      - TZ=Asia/Shanghai
    ports:
      - "9898:9898"
    restart: always

有几点需要注意:

  • 为避免后续误操作,建议挂载本地备份路径时直接使用宿主机原始路径
  • 对于只需备份、不需写入的目录,可适当设置只读 ro 权限(如上例 /volume2/docker)。
  • 如果使用远程存储(如 rclone 对接 S3 / COS / B2 等),记得提前在宿主机生成 rclone 配置文件,并挂载到容器内。
  • 端口 9898 用于访问 Backrest Web UI,如果通过反代访问,也可以不映射到宿主机。

设置

访问 IP:9898 启动 Backrest。Backrest 会根据浏览器语言自动切换显示语言,并弹出设置。你需要先为这台机器上启动的 Backrest 服务设置一个唯一的 实例ID,例如 my-nas ,用以区分多个 Backrest 服务。

接着,设置身份验证。如果你的 Backrest 只计划在内网中使用,那么可以不设置用户,并禁用身份验证。但如果是公开的服务,请务必设置好登录用户和高强度密码,并确保未勾选 禁用身份验证 选框,以启用身份验证功能。

鉴于公开服务可能带来的不可预测因素,建议即便是内网,也使用强密码认证身份,并在配置完毕后关闭 Backrest 服务的端口。

仓库

你应当尽可能自行阅读 backrest 官方指南 (英文) 以了解如何配置仓库,或者查看 Restic 文档 (英文) 了解更多有关仓库的信息。不过我也没看过,诶嘿

但本文演示时所使用的 Backrest 1.11.2 版本已原生支持中文界面,因此在实际配置过程中,基本可以做到无需额外查阅文档即可完成操作。

点击左侧的 「添加仓库」 按钮,新建一个 Restic 仓库。该仓库即为备份数据的最终存放位置,后续所有备份任务都可以将其作为统一的备份目的地。

仓库详情

首先,为其指定一个「仓库名称」。该名称仅用于 Backrest 内部识别不同存储库,例如区分本地仓库与云端仓库,本身并不影响实际的存储路径或数据结构。仓库名称创建后无法修改,因此建议在命名时保持清晰与可区分性。

而最关键的 「仓库 URI」,需要严格按照所选存储方式对应的 URI 格式填写。无论是本地路径、S3 对象存储,还是通过 rclone 访问的远程存储,只有格式正确,Backrest 才能成功连接并初始化仓库。

例如:

  • 本地存储库:/vol1/1000/backups/Restic
    使用本地磁盘或 NAS 上的目录作为仓库,适合作为第一层本地备份1
  • S3 / S3 兼容对象存储:s3:my-backup-bucket/Restic
    适用于 AWS S3 以及各类 S3 兼容服务(如 COS、OSS、MinIO 等)。
  • Backblaze B2:b2:my-b2-bucket/Restic
    以 B2 存储桶作为备份仓库,常见于低成本云端备份方案。
  • SFTP:sftp:[email protected]:/data/Restic-repo
    通过 SFTP 将备份数据存放到远程服务器的指定目录。
  • rclone 远程存储:rclone:remote-name:Restic
    通过 rclone 访问远程存储,其中 remote-namerclone config 中定义的远程名称,Restic 为仓库存放的子目录。

「仓库密码」 用于对 Restic 仓库中的所有数据进行加密。Restic 采用端到端加密设计,所有备份内容在写入存储库之前就已经完成加密,云端或远程存储只会保存加密后的数据本身。可以直接使用 Backrest 自动生成一段强度较高的随机密码,通常已满足安全需求。但如果有统一的密码管理策略,也可以自行指定符合习惯的密码。此外,Restic 也支持通过环境变量(如 Restic_PASSWORDRestic_PASSWORD_FILE 等)提供密码,方便在自动化或无交互环境中使用。

需要特别注意的是:该密码无法被找回或重置。一旦密码遗失,即使仓库文件仍然完整存在,也将无法再解密其中的任何数据。因此,应将仓库密码视为与数据本身同等重要,并妥善保存。建议使用密码管理器进行存储。

下方的 「自动解锁」 选项用于在执行清理(forget)和修剪(prune)操作前,自动处理仓库锁定文件。在单实例、单设备使用的情况下通常不会用到,但如果同一个仓库被多个设备或实例同时访问,开启该选项反而可能带来安全隐患,因此默认保持关闭即可。

参数变量

环境变量和命令参数(Environment & Flags)一节,本地仓库或通过 rclone 访问的常见云存储,大多数情况下可以留空。只有在使用对象存储、需要自定义认证方式,或对 Restic 行为有特殊需求时,才需要进行配置。

这里粘贴一段 AI 的见解,不一定准确。你可以在有需要时详尽地向 AI 发问、或阅读官方文档,并在正式将仓库投入使用前大量测试。

环境变量

用于向 Restic 传递环境变量,最常见的用途是:

  • 提供 对象存储的认证信息
    比如 S3、B2、Wasabi、MinIO 等
  • 设置 rclone 的运行环境变量
  • 或者传递一些 Restic 本身支持的高级参数

Backrest 会在执行 Restic 命令时,把这里填写的变量一并注入到运行环境中。常见示例包括:

  • S3 / 兼容对象存储

    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
    • AWS_DEFAULT_REGION
  • Backblaze B2

    • B2_ACCOUNT_ID
    • B2_ACCOUNT_KEY
  • Restic 通用

    • Restic_PASSWORD(仓库密码,不想写在仓库配置里的话)

另外一个比较实用的小细节是,这里 支持引用父进程中的环境变量,例如:

AWS_SECRET_ACCESS_KEY=${MY_AWS_KEY}

这在 Docker / systemd 环境中非常好用,可以把敏感信息只放在宿主环境里,而不是写死在 Backrest 配置中。

命令参数

对应的是 直接追加到 Restic 命令后的 flags,属于更偏高级/进阶的用法。适合以下场景:

  • 调整 Restic 的性能或行为
  • 解决网络不稳定、对象存储兼容性问题
  • 临时启用调试或特殊特性

常见例子有:

  • 限制并发、降低 IO 压力

    --limit-upload 20480
  • 指定缓存目录

    --cache-dir /data/Restic-cache
  • 某些 S3 兼容存储需要关闭 HTTP/2

    --option s3.disable-http2=true

这些参数 不会影响 Backrest 本身,只会在它调用 Restic 时生效。

定期修剪

日常执行备份时,Restic 的工作机制是:

  • backup:只往仓库里追加新数据块,从不删旧数据
  • snapshots:只是这些数据块的「索引」,方便按时间点恢复
  • forget:删除的是快照记录本身,不会立刻删除底层数据

还需配合 prune 来完成最后的空间回收工作。prune 意为修剪、精简,在 Restic 中,prune 用于删除不再使用的数据块。具体流程可以概括为:

  • 扫描整个仓库
  • 找出 不再被任何快照引用 的数据块
  • 删除这些数据块,并在必要时重新打包剩余数据以优化空间利用率

但由于 prune 需要遍历仓库索引,并对大量数据块进行读写、校验和重组,这一过程相对耗时且对存储 I/O 压力较大,尤其是在仓库规模已经不小、或使用对象存储的情况下更是如此。因此,Restic 官方文档并不建议在每次备份完成后都立刻执行 prune,而是推荐将其作为一项低频的维护任务,定期运行即可——例如每周一次,或每月执行一次清理。既能有效回收空间,又不会对日常备份造成额外负担。

Backrest 自然是将 prune 也做成了一个可视化的定时任务。

你只需要在初始化仓库时,完成三件事:

  1. 设置最大未使用比

    对应 Restic 的 --max-unused 参数,单位是百分比。

    顾名思义,这个数字代表允许仓库中未使用内容所占的最大百分比:

    • 数字越小:仓库残余无用数据块越少,但检索过程会更费时间、IO 占用也更高
    • 数字越大:清理动作更温和、更快,但仓库里会保留更多暂时用不到的数据块

    对个人 NAS 来说,保持默认的 10 一般已经足够。

  2. 选择 Schedule Type(何时执行)

    这决定 prune 自动运行的方式,有以下几种:

    • Disabled:禁用自动执行,只在手动点击「运行」时才会清理
    • Interval (Hours):每隔 N 小时跑一次
    • Interval (Days):每隔 N 天跑一次
    • Cron:使用标准 cron 表达式自定义时间点

    对个人 NAS 来说,更推荐选择 Interval (Days),每 7 天运行一次;或选择 Cron,设置每周/每月指定时间段(比如无人使用 NAS 的凌晨)运行。

  3. Cron Expression 与 Reference Clock

    当选择 Cron 时,会出现:

    • Cron Expression(Cron 表达式):

      • 界面里常见的 0 0 1 * *:表示「每月 1 日 0 点 0 分」
      • 想要「每周日凌晨 3 点」可以写 0 3 * * 0

      表达式可以直接询问 AI 获取。

    • Reference Clock(参考时间):

      • Local:按服务器本地时区
      • UTC:按 UTC
      • Last Run Time:多用于「间隔」调度,基于上一次实际运行的时间继续往后算

综合起来,一个相对通用、比较稳妥的配置是:

  • 最大未使用比:10(默认)
  • Schedule Type:Cron
  • Cron Expression:0 3 * * 0(每周日凌晨 3 点自动清理)
  • Reference Clock:Local

这样,Restic 就会在通常不怎么用机器的时候自动执行 prune,对仓库进行「垃圾回收 + 瘦身」。既不影响日常使用,又能长期把磁盘空间控制在一个相对合理的范围内。

数据验证

这一项对应的是 Restic 的 check 操作,用于验证仓库中数据的完整性

  • 验证数据占比
    这里填写的是一个百分比,用来控制每次检查时抽查多少数据

    • 100%:每次检查都会完整扫描并校验整个仓库,最安全,但耗时和带宽开销最大
    • 较小的数值(如 5%10%):只随机抽查一部分数据块,能在较低成本下发现大多数潜在问题

    对于已经稳定运行的仓库,一般不需要每次都全量校验,定期抽查即可;而在刚迁移仓库、存储后端不太可靠(比如部分对象存储)时,可以适当调高比例。

  • 调度方式(Schedule Type)
    可根据需求设置为 Interval 或 Cron。
  • Reference Clock
    用于指定调度时间的参考基准,也同上一环节的使用方法一致,一般保持默认。

高级设置

在大多数使用场景下,这一部分完全可以保持默认。Backrest 已经为常见环境选择了相对保守、稳定的配置,除非你对系统资源调度或自动化流程有明确需求,否则无需在这里折腾。

这里同样粘贴一段 AI 的见解,仅供参考。

高级设置可以为备份任务指定运行时的 IO 与 CPU 优先级,用于控制 Restic 在系统中的「存在感」。

硬件优先级

  • IO 优先级
    决定备份任务在磁盘读写层面的优先程度。
    默认的 IO_DEFAULT 表示不做额外干预,由操作系统自行调度。
  • CPU 优先级
    决定备份任务在 CPU 调度中的权重。
    CPU_DEFAULT 同样表示使用系统默认策略。

在以下场景中,这些选项才可能派上用场:

  • 备份任务与数据库、转码、下载等高 IO 负载服务共存
  • 希望备份「慢一点跑」,但不影响前台服务响应
  • 服务器性能有限,需要人为限制备份任务的资源占用

否则,保持默认,Restic 本身已经足够克制。

钩子

Hooks 用于在备份生命周期的特定阶段执行自定义脚本,例如:

  • 备份开始前 / 结束后执行脚本
  • 备份成功或失败时发送通知
  • 备份前暂停服务,结束后再恢复服务

典型使用场景包括:

  • 备份数据库前执行 dump
  • 备份完成后通过 Telegram / 邮件推送结果
  • prunecheck 前后执行清理或校验操作

对于只想安稳备份数据的用户来说,这一功能并非必需;但如果你有自动化流程需求,Hooks 提供了非常大的扩展空间。

配置示例

以创建一个本地仓库为例。

  • 仓库详情

    • 仓库名称:local
    • 仓库 URL:/vol1/1000/backup/mikusa
    • 密码:********(自动生成强密码)
    • 自动解锁:关闭(不选中选框)
  • Environment & Flags(参数变量)

    • 默认
  • Prune(定期修剪)

    • 最大未使用比:10(默认)
    • Schedule Type:Cron
    • Cron Expression:0 3 * * 0(每周日凌晨 3 点自动清理)
    • Reference Clock:Local
  • Check(数据校验)

    • 验证数据占比:10(视数据重要程度和体积而定)
    • Schedule Type:Cron
    • Cron Expression:0 2 1 * *(每月 1 日凌晨 2 点校验)
    • Reference Clock:Local
  • Advanced(高级设置)

    • 默认

计划

点击左侧的 「添加调度计划」 按钮,新建一个 Restic 计划。必须先配置好仓库,才能新建调度计划。

计划详情

这里需要注意的事项与创建 Restic 仓库时基本一致。例如,计划名称必须唯一,且创建后无法修改,主要用于区分不同的备份任务。此外,虽然可以直接选择已创建的仓库作为备份目标,但单个计划只能指定一个仓库;若需要同时备份至多个目标,则需分别创建多个计划。

不过,一个仓库是可以供多个备份计划使用的。在 Restic 中,多个备份计划共用一个仓库时,去重是以「数据内容」为单位进行的,而不是以「计划」或「路径」为单位。即使多个计划备份了相同的文件,这些文件的数据块在仓库中也只会存储一份。因此,即使不同计划备份了大量重叠数据,仓库体积也不会线性增长。

我一开始忽视了「数据去重」的含义,于是在同一存储库里按路径创建了很多仓库……

备份范围

你可以参考 Restic 文档 (英文) 了解更多信息。

  • 路径

    至少添加一个要备份的路径,可以是目录,也可以是文件。Restic 会把这些路径当作备份入口,在快照里保留原本的目录结构。路径也可以在界面中直接选择,避免手动输入错误。

  • 排除规则

    用来排除不需要备份的路径或文件模式,语法与 Restic 的 --exclude 相同。可以使用排除规则把缓存、临时文件、日志等剔出去,减小仓库体积、提升备份速度。

    例如:

    • *.tmp :排除所有临时文件
    • /home/*/.cache :排除用户缓存目录
    • /var/log :排除大量滚动日志

    这一栏是区分大小写的,在类 Unix 系统中需要特别注意这类问题。

  • 排除规则(不区分大小写)

    与上面类似,但匹配时不区分大小写,对 Windows 等环境更友好。比如填写 *.log 时,会同时匹配 .log, .LOG 等。

备份调度

视数据的重要程度,可手动、按每时/每天、或 Cron 设置备份频率,基本上与创建仓库 prune 时的原理一致,就不再赘述了。

如果没有特别复杂的需求,比较常见的配置是:

  • Schedule Type:Cron
  • Cron Expression:0 2 * * *(每天凌晨 2 点)
  • Reference Clock:Local
但这块的 UI 是不是有点问题?强迫症受不了。

保留策略

保留策略(Retention Policy)对应的是 Restic 的 forget 规则,用于控制旧快照的保留。

可选项有:

  • By Count:按数量保留,比如只保留最近 N 个快照。

  • By Time Period:按时间维度保留,是最常用、也最直观的一种。

    该模式下每个字段的意思是:

    • Hourly:每小时最多保留多少个快照
    • Daily:每天最多保留多少个快照
    • Weekly:每周最多保留多少个快照
    • Monthly:每月最多保留多少个快照
    • Yearly:每年最多保留多少个快照
    • Latest (Count):无论时间如何,始终至少保留最近 N 个快照

    因此,截图中示例的配置意为:

    • 小时级:最近 24 小时内,每个小时保留 1 个快照
    • 天级:最近 7 天内,每天至少保留 1 个快照
    • 周级:最近 4 周内,每周至少保留 1 个快照
    • 月级:最近 3 个月内,每月至少保留 1 个快照
    • 年级:不额外保留按年的快照
    • 最近:不额外强制保留「最近 N 个快照」,完全按上面的时间分组来决定
  • None:不做自动清理,所有快照都保留。适合短期测试,不推荐长期使用。

应该根据数据重要程度、存储空间和恢复需求调整保留策略,例如保留 7 天的日快照 + 6 个月的月快照就已足够日常使用。如果数据本就不是频繁备份,那么也就没必要设置太精细的策略。

高级设置

计划的「高级设置」也同仓库处一样,没有特殊需求的话可以保持默认

备份参数对应 Restic backup 命令的额外参数,用于调整备份行为或性能,例如:

  • --one-file-system:只备份当前文件系统,避免跨挂载点
  • --exclude-larger-than 500M:跳过超大文件
  • --compression max(Restic 0.16+):提高压缩等级,换取更小体积

脚本(Hooks)则可以在备份生命周期的不同阶段执行自定义脚本或通知,支持多种 hook 触发点(如 before-backup, after-backup, on-error 等),具体用法可以参考官方 hook 文档。例如:

  • 备份前暂停某个服务、锁库
  • 备份后重启服务
  • 发送任务状态通知

现版本的 Backrest 已原生支持 TG 通知,不用自行编写脚本。只需在 Hooks 处选择 Telegram、确定需要通知的环节,填入 Bot Token 和 Chat ID,再参考官方模板设置一下通知文本,就能知道备份有没有正常执行了。

我的模板如下:

📦 备份任务通知

任务名称:  .Task }}
执行时间( {{ .FormatTime .CurTime )
事件类型:  .EventName .Event }}
仓库 ID( {{ .Repo.Id )
计划 ID:  .Plan.Id }}

{{ if .Error -}}
❌ 任务执行失败

错误信息(
{{ .Error ) else -}}
✅ 任务执行成功

{{ if .SnapshotStats -}}
📊 备份统计

• 新增数据( {{ .FormatSizeBytes .SnapshotStats.DataAdded )
• 处理文件数:  .SnapshotStats.TotalFilesProcessed }}
• 处理数据量( {{ .FormatSizeBytes .SnapshotStats.TotalBytesProcessed )
• 执行耗时: {{ printf "%.1f" .SnapshotStats.TotalDuration }} 秒

{{ end -}}
{{ end }}

From Backrest 自动通知

效果大概是这样:

📦 备份任务通知

任务名称: backup for plan "book"
执行时间: 2026-02-18T23:40:39+08:00
事件类型: snapshot end
仓库 ID: baidupan
计划 ID: book

✅ 任务执行成功

📊 备份统计

• 新增数据: 0.000 B
• 处理文件数: 4865
• 处理数据量: 533.452 GB
• 执行耗时: 13.3 秒

From Backrest 自动通知

栗子

让我再举个更实际的例子:使用 openlist 挂载百度网盘,并用 rclone 连接 openlist 提供的 webdav 服务。

先获取个人用户 UID/GID:

# mikusa @ truenas in ~ [11:45:35]
$ id
uid=1000(mikusa) gid=3000(mikusa)

准备 .env 环境变量文件,置于 compose.yml 同级路径,填入基础的公共变量:

PUID=1000
PGID=3000
TZ=Asia/Shanghai

安装 openlist:

services:

  openlist:
    image: openlistteam/openlist:latest
    container_name: openlist
    user: ${PUID}:${PGID}
    ports:
      - 5244:5244
    volumes:
      - ./openlist:/opt/openlist/data
    environment:
      - UMASK=022
      - TZ=${TZ}
    restart: always

启动后,打开 openlist 添加百度网盘存储,设置挂载路径,如 /baidupan ,并填写刷新令牌。

刷新令牌需在官方的 OpenList Token 获取工具 获取,找到「百度网盘 (OAuth2) 验证登录」并勾选「使用 OpenList 提供的参数」,点击「获取 Token」,将底下的刷新令牌粘贴至 openlist 对应位置即可。

确认当前用户具备 Webdav 相应权限。

在终端中连接 NAS,进入 Backrest 容器内部命令环境:

docker exec -it backrest /bin/sh

使用 rclone config 新建 rclone 配置, 添加 WebDav 存储,所需的信息有:

  • name 名称: openlist
  • url 地址:http://openlist:5244/dav
  • vendor 提供商:other
  • 用户名 user:mikusa
  • password 密码:mikusa

具体流程这里不做演示。

创建完成后,rclone 对应映射文件夹内的 rclone.conf 文件应当有如下内容:

[openlist]
type = webdav
url = http://openlist:5244/dav
vendor = other
user = mikusa
pass = mikusamikusamikusamikusamikusamikusa
密码部分是自动加密的,因此会与真实密码看起来不太一样。

使用 rclone ls openlist:// 测试配置是否有误,如有输出即表示正常。例如:

/ # rclone ls openlist://baidupan/mikusa
8753641238 零售机_游戏性能大横评_2026.mp4
842538736 琉璃的宝石/AICL-4796~7.rar
829036840 琉璃的宝石/VVCL-2788~9.rar

接着,打开 backrest 创建 Restic 仓库。

假设网盘挂载路径为/baidupan,计划备份到 mikusa/Backup/truenas 内,那就在仓库 URI 一栏填入:

rclone:openlist://baidupan/mikusa/Backup/truenas

如果是多个计划使用同一个仓库,可以考虑勾选自动解锁,否则修剪任务可能会失败。

保存仓库、测试连接成功后,即可新建计划任务使用这个仓库。

你可以先备份一些小文件进行测试,并尝试恢复云端备份文件到本地。待确定一切正常后,再执行大批量备份。

由于上传的都是些经过加密的文件块,体积不大。实测备份约 2T 数据,暂未遇到上传限制问题(具体情况可能因账户与网络环境而异)。唯一可能需要担心的,便是宽带运营商是否会因持续大量的上传而限速了。

最后

以上便是我关于 Backrest 的全部心得了。不同于 Restic 只是一个纯粹的命令行工具,Backrest 本质上是一个常驻运行的 Web 服务,拥有完整的 HTTP 端口监听、用户认证与任务调度能力。为了读取所有需要备份的目录,它在 Docker 中往往需要较高权限,甚至直接挂载宿主机的大量路径。这意味着,一旦 Backrest 本身存在安全漏洞——无论是认证绕过、未授权访问,还是远程代码执行——攻击者能够触及的范围,便远不止备份数据本身。

当然,这并不意味着 Backrest 不值得使用。对于大多数家庭 NAS 场景而言,只要运行在内网环境中,做好基本的隔离与访问控制,风险通常是可以接受的。若你对安全边界有更严格的要求,那么直接使用 Restic + cron 的纯命令行方案,会是更简洁、也更克制的选择。

参考

本文在摸索时期参考了少数派对「3-2-1 原则」的《不想被勒索软件毁掉数据,就按照「3-2-1 原则」来备份文件》,刘一树的《服务器备份 - backrest + rclone + oss》,试验成功、再加上官方推出了中文界面后,便大量依赖 ChatGPT、Gemini、Doubao 等一系列人工智能的解释了。

因而本文 AI 含量较高,如有顾虑,酌情阅读。


  1. 第一层备份,是「多层备份架构」中的术语。指的是将数据备份到本地或同一网络中的存储设备,作为最基础的备份层级。这一层的核心特点是:备份速度最快、恢复时间最短、成本最低。它通常是备份计划中最频繁执行的一部分,主要应对日常误操作、硬盘故障、系统崩溃等常见风险。尽管第一层备份提供了较高的恢复速度,但并不具备防范物理灾害、勒索病毒或盗窃的能力,因此需要配合其他层级的备份(如异地备份)来确保数据的长期安全。