2026-08-21 13:00:00
昨天我把一块装着 [[Proxmox VE]] 系统的硬盘从之前的 SER8 拆下来,插到 4 盘位的 Me Pro 上,本以为开机就能继续用,结果卡在了最基础的一步:Me Pro 的物理网口和我原来的网口对不上,物理网口变更了名字导致无法上网。屏幕上 PVE 的控制台在闪,ip addr 里却无法找到两个物理网口,Web 管理界面自然也打不开。

在和 Claude 交流寻找解决方案的过程中,我看到 Claude 说「用手机 USB 共享网络」,先确保 PVE 有网络,然后利用网络更新 Kernel。我开始还有点疑惑,这样也可以,但是手边正好有一台 Pixel,顺手就用 Type-C 数据线链接了,然后在 Android 端切换到 USB tethering,在 PVE 查看,立马就能看到多出一张网卡,利用 dhclient enxxx 就可以获取 IP 地址,立马就能 ping 通网络。
这个功能是,当手机自己连着 Wi-Fi 的时候,它同样可以把这份 Wi-Fi 网络通过 USB 线转发出去。换句话说,一根数据线加一台安卓手机,就等价于一张即插即用的 USB 无线网卡。
这件事让我开始重新审视抽屉里那几台退役的安卓手机。它们其实是完整的 ARM 计算机:有 CPU、有内存、有存储、有电池(相当于自带 UPS)、有 Wi-Fi 和蓝牙、有摄像头和屏幕、有 USB OTG。我们只是习惯性地把它们当作「淘汰的手机」,而不是「一台便宜的、带屏幕的、待机功耗只有几瓦的小型服务器」。
要理解为什么这招好用,得先知道手机在 USB 那端到底扮演了什么角色。安卓设备通过 USB 连接电脑时,可以以不同的 USB Gadget 身份出现:MTP 模式下它是一个媒体设备,ADB 模式下它是一个调试设备,而打开 USB 网络共享之后,它把自己声明成了一个 USB 网络适配器。
具体用的协议一般是 RNDIS(Remote NDIS,微软定义的一套通过 USB 承载以太网帧的规范),近几年不少设备也改用了更标准、效率更高的 CDC-NCM。这两种协议在 Linux 内核里都有现成的驱动,分别是 rndis_host 和 cdc_ncm,属于早就编译进主流发行版内核的东西。所以主机侧不需要装任何厂商驱动,插上线之后内核就会枚举出一张新的以太网接口,传统命名是 usb0,在启用了 systemd 可预测网卡命名的系统上则是 enx 加上 MAC 地址的形式,比如 enx0a1b2c3d4e5f。
有意思的是手机这一侧。它并不是简单地做一个透明网桥,而是实实在在跑了一套小型路由:手机在这个 USB 网络接口上给自己分配 192.168.42.129 这个地址,同时启动一个 DHCP 服务,把 192.168.42.0/24 网段的地址发给通过 USB 连过来的主机,然后对出站流量做 NAT,把它们转发到当前的上游链路。这个上游链路可以是移动数据,也可以是手机正连着的 Wi-Fi。安卓的 tethering 框架并不关心上游是什么,它只负责把默认路由指向那条可用的链路。这正是「手机变无线网卡」的关键:Wi-Fi 进,USB 出。
顺带一提,Wi-Fi 热点用的是 192.168.43.0/24,蓝牙共享用的是 192.168.44.0/24,这三个网段是安卓 tethering 的固定约定。知道这个在排查问题时很有用,看到 192.168.42.129 这个网关地址,你就能确定链路走的是 USB 而不是别的。
PVE 基于 Debian,所以下面的步骤在绝大多数 Debian 系发行版上通用。先把线插上,在手机的设置里找到「网络和互联网 - 热点与网络共享 - USB 网络共享」并打开。注意这个开关往往是灰的,直到系统检测到 USB 数据连接才会变为可点,而且它要求你用的是数据线而不是只能充电的线,这个坑我踩过不止一次。
打开之后回到主机,先确认内核有没有认出设备:
lsusb
ip link
dmesg | tail -20
正常情况下 ip link 里会多出一张 usb0 或 enx 开头的接口,dmesg 里能看到类似 rndis_host ... register 'rndis_host' at usb-0000:00:14.0-2, RNDIS device, 0a:1b:2c:3d:4e:5f 的日志。如果什么都没出现,先手动加载一次驱动:
modprobe rndis_host
modprobe cdc_ether
modprobe cdc_ncm
接口出来之后,临时用的话一条命令就够了:
ip link set usb0 up
dhclient usb0
拿到地址后 ip route 里会出现指向 192.168.42.129 的默认路由,此时 PVE 的 Web 界面就能通过这个新地址访问了。如果你希望它持久生效,可以在 /etc/network/interfaces 里加一段:
allow-hotplug usb0
iface usb0 inet dhcp
allow-hotplug 而不是 auto 是有讲究的,手机不一定一直插着,用 auto 会让开机时因为等待这张不存在的网卡而拖慢启动。
有一点需要提醒:这套方案更适合救急和临时维护,不适合长期作为服务器的主链路。手机会不断被主机供电,长时间维持在满电状态对锂电池不友好,而且系统更新、电话来电、后台清理都可能中断这条链路。我的做法是把它当成「带外管理通道」,用它把机器救活、把正式网络配置好,然后拔掉。
PVE 的特殊之处在于它既是宿主机也是虚拟化平台,所以这张「手机网卡」有两种用法,要根据目的选择。
第一种是留在宿主机上,也就是我前面救急时用的方式。适合的场景是宿主机自己失联了,你需要一条通道进入 Web 管理界面。配置就是上面那几条命令,不需要动虚拟化相关的东西。如果你还希望虚拟机也能借道上网,不要试图把 usb0 桥接进 vmbr0,这条路走不通。原因在于 RNDIS 接口只会为它对面的那一个 MAC 地址学习和转发,安卓侧不接受来自多个 MAC 的帧,桥接之后虚拟机发出的包能出去但回不来。正确的做法是在宿主机上做 NAT:
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o usb0 -j MASQUERADE
这样挂在 vmbr0 上的虚拟机把网关指向宿主机,就能通过手机出网。要持久化的话把 net.ipv4.ip_forward=1 写进 /etc/sysctl.d/,NAT 规则用 iptables-persistent 保存。
第二种是把手机整个直通给某台虚拟机,比如你想让 OpenWrt 或者软路由虚拟机直接管理这条上行链路。在 PVE 的 Web 界面里,选中虚拟机进入硬件页,添加 USB 设备,这里会给出两种绑定方式:按厂商和设备 ID,或者按 USB 端口。
对安卓设备一定要选按端口。这是个容易忽略但很关键的细节:手机在切换 USB 模式时会重新枚举,Product ID 跟着变,比如 Pixel 在纯充电、MTP、ADB、网络共享几种状态下报的 ID 各不相同(Vendor ID 固定是 Google 的 18d1,Product ID 却会在 4ee1、4ee2、4ee7 之间跳)。如果你按 ID 绑定,用户在手机上一点「USB 网络共享」,设备就从虚拟机眼前消失了。按端口绑定则只认物理插口,无论手机怎么切换模式都能保持连接。
命令行的写法是先用 lsusb -t 找到总线和端口号,再挂给虚拟机:
lsusb -t
qm set 100 -usb0 host=1-4
其中 1-4 表示 1 号总线的 4 号端口,100 是虚拟机的 VMID。如果插的是 USB 3 口,可以加上 usb3=1。这个操作支持热插拔,虚拟机不需要重启就能看到新设备,虚拟机内部同样会出现一张 usb0,配置方法和前面完全一致。
两种方式不能同时使用。设备一旦直通给虚拟机,宿主机上的 usb0 就会消失,所以如果你的目的是抢救宿主机本身,老老实实用第一种。
救急之后我顺着这个思路想下去,安卓能做的远不止转发网络。最有想象空间的是 [[Termux]],它在不 root 的前提下提供了一个相当完整的 Linux 用户空间环境,有自己的包管理器 pkg,能装 Python、Node.js、Go、Rust、git、ffmpeg、nginx、openssh 这些常规工具。你可以在手机上跑一个 sshd,然后从笔记本 ssh 进去,体验和登录一台小型 VPS 没有本质区别。
实际能落地的场景不少:把 [[Syncthing]] 装在旧手机上,它就是一个永远在线的同步节点,比常开一台 NAS 省电得多;用 Termux 跑定时任务抓取数据、做备份;跑一个轻量的下载器,配合外接的 OTG 硬盘当离线下载盒子;甚至可以跑 [[copyparty]] 这类文件服务,把手机的存储变成局域网里的文件共享点。一台闲置的旧机器待机功耗大概在 2 到 5 瓦之间,比树莓派还低,而且自带屏幕和电池。
如果你需要的是更完整的发行版而不只是 Termux 的环境,[[Andronix]] 或者 Termux 自带的 proot-distro 可以在用户态跑起 Debian、Ubuntu、Arch 的根文件系统。性能会因为 proot 的系统调用拦截而打折扣,但对于跑一些依赖 glibc 的软件是够用的。
[[Scrcpy]] 是我用得最频繁的工具之一。它通过 ADB 把一个精简的 server 推到手机上,用 H.264 编码把屏幕流传回电脑,同时把电脑的键鼠事件注入回手机。延迟通常在 35 到 70 毫秒之间,实际体验接近于手机变成了电脑的一个窗口,可以直接用键盘打字、用鼠标操作,还能拖拽文件安装 APK。不需要 root,也不需要在手机上装任何应用。
配合 ADB 还能玩出更多花样。adb reverse tcp:8080 tcp:8080 可以做反向端口转发,让手机上的应用访问到你电脑本地的服务,调试移动端对接本地 API 的时候特别方便。adb shell 则是一个完整的命令行入口,很多在 UI 上被厂商藏起来的设置项,都能通过 settings put 直接改。
Android 14 开始,系统原生支持把手机作为 USB 摄像头使用,插上电脑就会被识别成一个标准 UVC 设备,不需要装任何驱动或者第三方软件。对于手上有旗舰旧机的人来说,这基本是白捡了一个画质远超普通笔记本内置摄像头的网络摄像头。如果你的系统版本较低,DroidCam 或者 IP Webcam 这类应用也能达到类似效果,只是要走 Wi-Fi 或者 USB 转发,多一层配置。
反过来,手机也可以当显示器。spacedesk 这类工具把手机变成 Windows 的扩展屏,出差时带一台平板或者旧手机,就多了一块放监控面板、放文档的副屏。
USB OTG 则打开了另一扇门。旧手机接上 OTG 线之后可以读取 U 盘、SD 读卡器、移动硬盘,也可以接键盘鼠标。我见过有人把旧手机加 OTG 键盘当成一个极简的写作机器,续航一整天。
这两个用法比较小众,但真到需要的时候特别救命。
DriveDroid 可以把手机模拟成一个 USB 存储设备或者光驱,直接把手机里存放的 ISO 镜像挂载给目标电脑启动。这意味着你不需要随身带 U 盘和 [[Ventoy]],手机里存几个常用的救援镜像就够了。代价是这个功能需要 root 权限,因为它要操作内核的 USB Gadget 配置接口。
另一个是串口控制台。很多服务器、交换机、软路由的管理口是 RJ45 转串口或者 USB 串口,配一根 USB 转 TTL 的线(CH340 或者 FTDI 芯片都行)和一个 OTG 转接头,再装一个 Serial USB Terminal 之类的应用,手机就变成了一台便携的串口终端。机房里不用再抱着笔记本蹲在机柜前,这个体验的差别是巨大的。
PCAPdroid 是一个不需要 root 的抓包工具,它的实现思路很巧妙:利用安卓的 VpnService API 把本机流量引到自己的虚拟接口上,然后落盘成 PCAP 文件。你可以按应用维度过滤,看某个 App 究竟往哪些域名发了请求,也可以把抓到的包导出用 Wireshark 分析。对于研究某个应用的网络行为、排查国内 App 的隐私问题,这是最低门槛的方案。
再加上各种 Wi-Fi 分析工具能看信道占用和信号强度,手机其实是一个相当称职的现场网络诊断设备。装修布网络、排查家里 Wi-Fi 死角的时候,比拿电脑方便太多。
数据线是第一个坑,也是最常见的一个。很多随手机附赠的线或者从充电宝里翻出来的线只有电源触点,没有数据线芯,插上去手机只会显示充电,USB 网络共享的开关始终点不亮。换一根确定能传数据的线,能省掉半小时的排查。
后台被杀是第二个坑。国内定制系统的省电策略非常激进,Termux 里跑着的服务可能在你锁屏几分钟后就被清掉。对策是在系统设置里把这个应用加入电池优化白名单,同时在 Termux 里执行 termux-wake-lock 持有唤醒锁。如果需要开机自启,装一个 Termux:Boot 插件,把启动脚本放到 ~/.termux/boot/ 下。
还有一个比较隐蔽的问题:从 Android 12 开始,系统引入了 phantom process 限制,会主动杀掉应用派生出的子进程,默认上限是 32 个。这对普通应用没影响,但 Termux 里跑多个服务很容易触发,表现为进程莫名其妙消失。可以通过 ADB 关掉这个监控:
adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put activity_manager max_phantom_processes 2147483647
需要注意这个设置在部分系统重启后会失效,需要重新执行。
最后是应用来源。Termux 的 Google Play 版本早已停止维护,功能残缺且不再更新,务必从 F-Droid 或者官方 GitHub Release 安装。这类工具类应用普遍存在类似情况,装之前多确认一下渠道。
这次 PVE 迁移带来的最大收获是让我意识到自己对手上工具的想象力实在有限。一台安卓手机被我们默认框定在「打电话、刷视频、拍照」的用途里,但它的底层是一个跑着 Linux 内核的通用计算设备,具备网络栈、USB Gadget、传感器、编解码硬件这些完整的能力。厂商的 UI 只是把这些能力包装成了消费级的形态,而这些能力本身一直都在。
抽屉里那几台旧手机,我打算挑一台出来常年插着电,跑 Termux 加 Syncthing 当同步节点,顺便当成随时可用的救急网卡。它的性能可能不如一台四五百块的迷你主机,但它已经在那里了,边际成本是零。在折腾家庭实验室这件事上,先把已有的东西用起来,往往比再买一件新的更有成就感。
2026-08-16 13:00:00
用 [[Obsidian]] 记笔记这么多年,有一个需求一直没有被很好地解决,那就是在浏览器里直接访问自己的笔记库。Obsidian 官方一直没有推出 Web 版本,过去想要在别人的电脑上、或者在不方便安装客户端的环境里查看和编辑笔记,要么依赖远程桌面这种笨重的方案,要么就只能把笔记渲染成静态网站,牺牲掉编辑能力,或者通过 SSH 登录我的 macOS 通过命令行方式访问。最近我发现了一个叫 Ignis 的开源项目,它的口号非常直接:Run Obsidian as a self-hosted web app. Not remote desktop, an actual web app。它不是远程桌面,而是让 Obsidian 真正跑在浏览器里,体验下来确实让我眼前一亮,这篇文章就来聊聊它。

Obsidian 是一个基于 Electron 的桌面应用,它的编辑器、插件系统、文件访问都建立在 Electron 提供的 Node.js 能力之上,而浏览器出于安全考虑并不提供这些 API,这是官方迟迟没有 Web 版的根本原因。在 Ignis 出现之前,想远程访问自己的笔记库大致有几类办法。
第一类是远程桌面方案,比如用 KasmVNC 或者类似 linuxserver 的 Obsidian 容器镜像,把整个桌面版 Obsidian 的画面通过 VNC 串流到浏览器。这类方案功能上最完整,但体验很差,字体渲染模糊、剪贴板不通、延迟明显,在手机上更是几乎不可用。
第二类是发布类方案,比如 Obsidian Publish、[[Quartz]]、Flowershow 这些工具,把笔记库渲染成静态网站。它们适合对外分享,但本质上是只读的,无法在浏览器里编辑笔记,也用不了任何插件。
第三类是换用天生就是 Web 应用的笔记工具,比如 SiYuan、AFFiNE 之类,但这意味着放弃 Obsidian 的整个插件生态和已经养成的工作流,迁移成本太高。
Ignis 走的是第四条路:它是一个兼容层(compatibility shim),为 Obsidian 所依赖的 Electron API 提供了浏览器端的实现,让原版 Obsidian 的代码直接在浏览器里运行,笔记库则保存在服务器上。值得一提的是,Ignis 本身不包含也不分发任何 Obsidian 的代码和资源,Docker 容器在首次启动时会从 Obsidian 官方源下载程序本体。项目采用 AGPL-3.0 协议开源,作者还专门写了一份 LEGAL.md,援引欧盟软件指令中关于互操作性的条款说明合法性,并明确表示无意损害 Obsidian 官方的商业利益,这种认真程度在同类项目里并不多见。
我最关心的当然是兼容性,毕竟一个残缺的 Obsidian 没有意义。实际情况比我预期的好很多,Obsidian 的核心功能基本都能用:编辑器、Canvas 白板、Bases 数据库视图、命令面板、右键菜单、主题和 CSS 片段都正常工作,绝大多数基于 Obsidian 插件 API 开发的社区插件也能直接加载。图谱视图、大纲这些功能在正确配置 HTTPS 之后也都可用,这一点后面讲部署时会展开。
在 Web 化之后,Ignis 还带来了一些桌面版没有的能力。它支持通过工具栏、右键菜单或者直接拖拽来上传文件到笔记库,也可以把单个文件或整个文件夹打包成 ZIP 下载下来。多仓库支持做得很完整,可以创建、切换、重命名、删除 vault,不同的浏览器标签页甚至可以打开不同的 vault。多个标签页之间通过 WebSocket 实时同步,在一个标签页里的编辑会在一秒内出现在另一个标签页中。另外还有两个很实用的 URL 参数:?workspace= 可以在独立标签页中打开某个保存好的工作区布局,?file= 可以通过 URL 直接打开某篇笔记,这让 Obsidian 的笔记第一次拥有了可以分享给自己其他设备的链接。小屏幕设备上 Ignis 会切换到移动端 UI,手机浏览器里的体验接近 Obsidian 移动客户端。
| 同步方面,官方的 Obsidian Sync 可以在登录的标签页里正常工作,Ignis 还提供了服务端的 Headless Sync,即使浏览器标签页全部关闭,服务器也能继续在后台同步,这个设计解决了 Web 应用”关掉页面就停止工作”的天然缺陷。对于我这种用第三方方案同步的用户,obsidian-livesync 这类走 WebSocket 或 HTTP 的插件也能配置成功,只是要注意一些网络上的细节,后面避坑部分会提到。我在 [[2020-11-23-obsidian-sync-acrose-devices-solution | 我的 Obsidian 笔记跨设备同步方案]] 里梳理过各种同步方式,Ignis 相当于给这些方案又加了一个随时可用的 Web 入口。 |
性能上作者也下了功夫。Ignis 用一次预压缩的 bootstrap 请求就把 vault 信息、元数据树、插件列表全部交付给浏览器,配合索引器预取(indexer pre-fetch)预热内容缓存,让 Obsidian 启动时的索引过程命中缓存而不是反复走网络。服务端用 LRU 缓存控制内存占用,默认 50MB,不会把整个笔记库都加载进内存,这些参数都可以在设置面板里调整。我的笔记库有几千个文件,加载速度完全在可接受范围内。
Ignis 的部署非常简单,官方提供了 Docker 镜像,一个 docker-compose 文件就能跑起来:
services:
ignis:
image: nobbe/ignis:latest
ports:
- "8080:8080"
environment:
# 运行 id 命令查看自己的 uid/gid 并填入
- PUID=1000
- PGID=1000
volumes:
- ./vaults:/vaults
- ./data:/app/data
- obsidian-app:/app/obsidian-app
restart: unless-stopped
volumes:
obsidian-app:
保存为 docker-compose.yml 之后执行 docker compose up -d,首次启动时容器会从官方源下载 Obsidian 和 obsidian-headless CLI,大概需要一两分钟,可以用 docker compose logs -f 观察进度。之后访问 http://localhost:8080,如果 vaults 目录下已经有笔记库会自动加载,否则会打开 vault 管理器引导创建第一个。
有几个部署细节值得注意。PUID 和 PGID 要和宿主机用户匹配,用 id 命令查一下自己的 uid 和 gid 填进去,否则 Ignis 写入的文件归属会出问题。如果笔记库放在 NAS 挂载或者 NFS 上,可以直接把外部目录挂载到 /vaults 下面的子目录;对于 rclone mount、FUSE、NFS、SMB 这类较慢的文件系统,还可以设置 WRITE_COALESCE_MS 环境变量开启写入合并去抖,减少频繁的小写入。
我自己的做法是把 Ignis 指向已有的同步目录,这样桌面版 Obsidian、手机客户端和 Ignis 操作的是同一份数据,Ignis 只是多出来的一个访问入口,不需要改变原有的同步链路。
这一部分是使用 Ignis 之前必须搞清楚的,官方文档也用了醒目的警告来强调。核心有两点:Ignis 没有内置任何身份验证,以及浏览器的安全上下文(secure context)要求。
先说安全上下文。Obsidian 依赖的一些浏览器 API,比如加密和剪贴板相关的接口,只在 HTTPS 或者 localhost 环境下可用。所以如果你通过 http://192.168.1.10:8080 这样的局域网地址裸访问 Ignis,会发现图谱视图、大纲、Sync 等一系列功能默默失效,这不是 bug,而是浏览器的安全策略。解决办法有两类:正经的做法是在前面加一层 TLS,用 Caddy、nginx 或 Traefik 做反向代理(官方 examples 目录里有现成配置),或者用 tailscale serve、Cloudflare Tunnel 这类免证书管理的方案;偷懒的做法是在每个客户端浏览器里把 Ignis 的地址加入安全源白名单,Chromium 系浏览器在 chrome://flags/#unsafely-treat-insecure-origin-as-secure 设置,但这种方式只适合局域网,Safari 没有对应选项只能上 TLS。
再说身份验证。Ignis 默认监听纯 HTTP 且没有登录机制,任何能访问到这个端口的人都可以读写你的整个笔记库。所以绝对不要把 Ignis 直接暴露到公网。如果需要在外网访问,务必在前面加一层认证:反向代理的 Basic Auth 是最简单的,Authelia、Authentik、OAuth2 Proxy 这类 SSO 方案更完善,也可以用 Cloudflare Access 配合 Tunnel,或者干脆走 Tailscale、WireGuard 这样的 VPN 只在私有网络里访问。官方 examples 里提供了两套完整的 Caddy 配置,分别对应 Basic Auth 和 Authelia,可以直接拿来用。路线图里提到未来会支持内置认证和多用户 OIDC,但在那之前,认证完全是自己的责任。
我个人的建议是家庭网络内用 tailscale serve 一条命令解决 HTTPS 和访问控制两个问题,既不用管证书,也天然只有自己的设备能访问,是最省心的组合。
还有一个容易踩的坑是第三方同步插件的连通性。出于防止恶意网络扫描的考虑,Ignis 服务端默认拒绝中继指向私有地址、回环地址的 HTTP 请求,所以如果你的 CouchDB 或者其他同步服务器跑在局域网或同一台 Docker 主机上,需要通过 PROXY_ALLOW_PRIVATE_HOSTS 环境变量显式放行对应的 IP 或 CIDR(注意只接受 IP 不接受主机名),或者在设置里配置 direct-fetch 让浏览器直连(这要求同步服务器开启 CORS)。走 WebSocket 的同步插件则是浏览器直连,当 Ignis 本身是 HTTPS 时,浏览器会拒绝明文的 ws:// 连接,同步服务器也需要提供 wss://,用受信任的证书或者同样套一层 tailscale serve 就能解决。
把 Electron 应用塞进浏览器不可能没有代价,有些限制需要提前知晓。最主要的是需要 Node 原生模块或 child_process 的插件无法加载,比如依赖本地执行命令的插件(典型如调用本地 Git 二进制、执行 shell 脚本的那一类)在 Ignis 里是跑不起来的,官方文档维护了一个插件兼容性页面,重度依赖某个插件的话建议先去查一下。
另一个需要留意的是密钥存储。桌面版 Obsidian 的插件可以用 Electron 的 safeStorage 借助操作系统加密敏感数据,浏览器没有等价能力,所以 Ignis 里插件存储的 API key 之类的秘密目前是明文保存的,服务端加密在计划中但尚未实现。在共享或安全性存疑的服务器上部署时,这一点要纳入考虑。
还有一些小的差异:浏览器无法弹出真正的本地文件选择器,所以像 Importer 这类插件导入文件要分两步操作,先选择文件暂存再重新执行动作;拼写检查语言跟随浏览器设置而不是应用内设置;依赖 Electron 菜单 API 的原生菜单选项被禁用。这些都属于可以接受的妥协。
最后要提醒的是,Ignis 还是一个很年轻的项目,虽然作者自己已经把它当作日常笔记工具在用,GitHub 上也已经收获了超过 1200 个 star,但处于活跃开发阶段意味着可能遇到未记录的问题。好在它只是数据的一个访问层,笔记本体始终是磁盘上的 Markdown 文件,就算 Ignis 出问题,数据本身也不会受影响,这也是我敢直接把它指向主力笔记库的原因。当然,任何时候都不要忘了备份。
Ignis 解决的是一个存在了很多年的真实痛点:Obsidian 的本地优先哲学和随时随地访问之间的矛盾。此前的答案要么是体验糟糕的远程桌面,要么是丧失编辑能力的静态发布,而 Ignis 用兼容层的思路给出了第三种答案,让你在浏览器里得到一个接近原生的、插件可用的、可编辑的 Obsidian,而数据依然完整地躺在自己服务器的文件系统里。
对我来说,它最大的价值是让 Obsidian 的访问入口从”装了客户端的设备”扩展到了”任何一个有浏览器的地方”,配合 Tailscale 之后,在公司的电脑、朋友的电脑、甚至 iPad 的浏览器里打开自己的笔记库都只是一个 URL 的事情。如果你也是 Obsidian 的自托管爱好者,手边有一台跑着 Docker 的小主机或 NAS,非常值得花十几分钟把 Ignis 跑起来体验一下。项目的 GitHub 仓库 和官方文档都写得相当清楚,部署前把安全章节读一遍,就可以放心使用了。
2026-08-11 13:00:00
2026 年山之日这个假期外面下着雨,没有出行的计划,索性看起了放到了待看列表的《卧底厨神》,因为之前看过《黑白大厨》知道了权主厨,郑主厨,本来没有对这部综艺抱有太大的期待,只是想在一个休息日放松心情看一篇轻松的综艺,但没想到完全被这三位主厨圈粉了。
看完 [[卧底厨神]],我发现自己记住的居然不是任何一道菜,而是几个人在陌生后厨里手忙脚乱的样子。对一档美食综艺来说,这挺反常的,但我仔细想了想卧底厨神或许更适合将其归类为真人观察类综艺。
自从 [[黑白大厨]] 火了之后,美食综艺几乎被”竞技”二字绑架了:切磋、对决、淘汰、排名,厨师们像格斗选手一样被推上擂台。但卧底厨神聪明就聪明在,在这里我们看不到对决,淘汰,而是真实的餐厅后厨,真实的把每一道菜端到客人面前:三位在韩国功成名就的主厨,隐藏姓名和头衔,伪装成完全不会做菜的菜鸟,潜入意大利帕尔马、那不勒斯和中国成都的餐厅后厨,从洗碗、择菜、擦灶台、切菜做起。
他们的任务只有一个:在 5 天之内,不暴露自己厨师的身份,却要赢得当地主厨的认可,把自己的招牌菜写进餐厅菜单。
这个设定微妙在它的双重矛盾:会做菜的人要装作不会做菜,本身就是高难度表演;而装作不会的同时,又得”恰到好处”地露出潜力,否则永远轮不到你碰灶台。知道的是卧底综艺,不知道的还以为在看演技考核。
三位主厨各自带着不同的故事线。Sam Kim 是韩国意大利菜的代表人物,去了启发他料理生涯的帕尔马,伪装成转行的农夫;权圣晙顶着黑白大厨首季冠军”那不勒斯黑手党”的名号,回到了绰号的来源地那不勒斯;[[郑智善]] 作为韩国首位女性中餐主厨,伪装成拳击手、去了川菜的大本营成都。这三位主厨的人物刻画,每一个都非常生动。我们可以从节目组叙事的娓娓道来中去熟悉每一个主厨的人物生平。
权圣晙主厨是傲娇的,有点拽,但拽得很可爱。日常生活里不拘小节,演播室里主持人问”想改变第一天的什么内容”,权主厨的回答是想改一下后采时的发型,此时再去看权主厨的头发时,我当场笑死。但反差也恰恰在这里:日常呆萌又安静的一个人,一进厨房,那种属于厨师的专注立刻就出来了。
郑智善主厨的反差则在演播室和后厨之间。演播室里的她是强势果断的领导者气质——毕竟是拥有多家餐厅的女老板;但在成都当菜鸟的时候,你能看到她细腻的一面:观察力很强,会照顾周围人的情绪,外刚内柔。她对自己的要求也高到近乎苛刻:厨房里大家都坐着休息,但从来没看到郑主厨坐下过,她不允许自己的餐厅里听歌。从她的自述里也能感受到一路走来的艰辛——作为一名女性主厨,长期得不到行业内的认可,但她依然靠着实力、经验和努力获得了今天的成就。也正因为这样,当她在成都被地道川菜和沉得多的中式炒锅接连挫败、肉眼可见地陷入自我怀疑时(据 PD 事后采访说,制作组当时甚至担心第二天还能不能拍下去),这条线才格外有张力。后面怎么走,就不剧透了,留给你自己看。
而我最喜欢的,是节目中出现的第一位主厨 Sam Kim(샘킴)的故事。明明已经是多家餐厅的大老板,管理着 20 多名员工,但到了异国他乡,依然是那个胆小、内向、不会主动开口说话的人。性格温和,但专业能力藏不住。经典的搞笑段落也大多是他贡献的——意大利饺子的形状连着做错了 3 次。虽然犯了这么多新人的错,但依然能看到 Sam Kim 主厨的认真,上班前学好意大利语,背下来再上班,看他这条线的时候,最容易让我想起初入社会的菜鸟。这样一位温柔害羞的大叔,谁能不喜欢。
导演 [[洪镇珠]] 在采访里说过一句我很认同的话:这个节目从来不是料理综艺,更接近一部”人类纪录片”。节目组没有为了效果放水,餐厅也没有配合演出——于是镜头里留下的是货真价实的挫败、鸡同鸭讲的语言障碍、深夜才结束的营业,以及在这一切之上依然生长出来的感情。正因为这 5 天的关系不是演出来的,后面每一个情绪节点才都立得住。
观众也用遥控器投了票:播出期间收视率一路涨到 6.2%,是首播的 3 倍,连续 10 周同时段第一。大家早就看腻了被剧本保护起来的成功。
另一个值得说的点是”重走来时路”这个母题。三位主厨都不是空降的天才,都当过厨房最底层的小工。节目让他们在职业生涯的顶点重新体验一次底层——Sam Kim 每天早上背意大利语,琢磨怎么跟前辈搭话;权圣晙营业到半夜也一声不吭地干活。这种东西,综艺剧本写不出来。
好节目不是偶然。写这篇观后感的时候,我顺手翻了 [[洪镇珠]] PD 和尹阿尔音编剧的收官采访,几个幕后细节很值得记下来。
企划始于 2025 年末制作团队构思新节目的阶段。洪镇珠是入行 11 年的 PD,此前一直做 [[帐篷外是欧洲]] 系列,这是其第一次从零企划一档新节目。出发点是一个判断:观众的眼光越来越高,必须做”真的”才能让人满足;而且要做一档能让人”过度沉浸”的节目——这种沉浸不只属于观众,也属于出演者。用洪镇珠的话说,出演者要沉浸到忘记自己在拍摄,才会讲出真正的故事。卧底潜入的设定就是从这个想法里长出来的。
真正难的是落地。给主厨们编的掩护故事是”拍摄一部隐退名人挑战第二人生的纪录片”;而选餐厅要同时考虑味道、员工的性格倾向、顾客构成、厨房的运作体系,制作组形容那个阶段是”跑断腿、磨破嘴”。餐厅方面起初并不友好——陌生人跑来说要拍摄,换谁都会起疑,成都餐厅的家人和帕尔马餐厅都疑虑重重,信任是一家一家磨出来的。
选角的标准也有意思:最看重的不是名气,而是”对这个节目有多大兴趣”。制作组在企划初期从隐世高手到新人厨师广泛请教,最终选中的三位,都是对企划本身充满好奇、并且能把当年做厨房小辈的经历讲得生动有趣的人。事后看,这个标准选出来的人确实撑起了节目——对企划没有热情的人,很难心甘情愿地在异国后厨打 5 天零工。
编剧尹阿尔音的说法则从另一个角度印证了节目的定位:从一开始就没把它当料理节目做,关注的是主厨们的工作方式,以及在升级过程中与当地员工建立的关系。所以那种”人类纪录片”的质感不是剪出来的,是策划阶段就定下来的。
看 [[黑白大厨]] 的时候,作为观众我只是一味地觉得比赛、竞技精彩;但在这档综艺里,虽然讲的是厨神们如何从底层做起,却无比”真实”。语言不通、反复犯错、在厨房里不知所措——这些画面更让我想起自己初入社会时的情形:看眼色,讲人情,还会在一些重要的时刻犯错。但身边的所有人都非常友好、善良,可以包容菜鸟的犯错;也可以看到当他们被主厨认可之后的那种成就感。
我想,这三位主厨以他们在韩国的位置,本可能不会参加这么辛苦的节目——因为我们在节目里是能看到那份辛苦的。从他们自己的回忆中,也可以感受到过去为了学习料理付出的汗水。但我恰恰是被这些真实存在的人感动了。短短几天时间内建立起来的情感,综艺中所表现出来的辛酸与情义,以至于让我都忘记了他们最终的目标,更愿意去看他们和这群普通人最后的结局。
如果你被上面的描述勾起了兴趣,几个实用信息:
卧底厨神给我的最大启发其实跟做菜无关:一个领域的顶尖高手,把头衔摘掉之后还剩下什么?节目给出的答案是——剩下的是习惯。是每天提前到岗的习惯,是一遍一遍反复擦拭的灶台,是被否定之后第二天照样出现的习惯。头衔证明不了这些,只有把人扔回底层才能看见。
至于三个人最后到底有没有把菜写进菜单,这里就不说了。反正看到后来,连我自己都忘了这档节目原本的任务是什么——能让观众忘掉规则、只记得人的综艺,不多见。期待衍生篇。
2026-08-04 13:00:00

今天在终端里跑 mise upgrade 想要升级 herdr 的时候,突然看到一条以前没见过的 WARN 提示,「mise WARN 1 newer herdr release hidden by minimum_release_age」,大意是有一个更新的版本存在,但因为发布时间太短,被 minimum_release_age 设置过滤掉了,暂时不会被安装。最后去看了 [[mise]] 的更新日志才发现,这是 mise 在 2026 年新引入的一个供应链安全机制,而且从 v2026.6.2 开始默认对所有人生效。这篇文章就把这个机制的来龙去脉、警告的含义以及怎么配置讲清楚。
简单说,minimum_release_age 的作用是:一个新版本发布之后,必须先等待一段时间(默认 24 小时),mise 才会认为它”可用”。在这个时间窗口内,即使上游已经发布了新版本,mise install、mise upgrade、mise latest 这些命令也会当它不存在,继续解析到上一个满足时间要求的版本。
这个设计针对的是近几年愈演愈烈的软件供应链攻击。典型的攻击场景是这样的:攻击者拿到某个流行包的发布权限(钓鱼拿到 maintainer 的 npm token、CI 配置泄露等等),发布一个带恶意代码的新版本,然后等着全世界的自动更新工具在几小时内把它拉下来。这类被投毒的版本通常存活时间很短,社区、安全厂商和 registry 官方往往在几小时到一两天内就会发现并下架。所以”等一等”本身就是一种非常朴素但有效的防御——只要你不做第一批吃螃蟹的人,绝大多数投毒版本在到达你机器之前就已经被清理掉了。
这个思路并不是 mise 首创。[[Renovate]] 很早就有 minimumReleaseAge 配置,用来推迟自动升级 PR 的创建;[[pnpm]] 也在 10.16 之后加入了同名的 minimumReleaseAge 设置,安装依赖时跳过太新的版本。mise 做的事情是把同样的理念搬到了开发工具版本管理这一层——你通过 mise 安装的 node、go、terraform,以及各种通过 aqua、npm、pipx 后端装的 CLI 工具,统一套上这道时间闸门。
回到开头那条警告。当你运行 mise upgrade 或者 mise outdated 之类的命令时,mise 会去查询各个工具的远程版本列表。如果它发现存在比当前已安装版本更新的版本,但那个版本的发布时间还没超过 minimum_release_age 设定的时长,就会打印一条 WARN,告诉你有 N 个更新版本因为太新而被暂时过滤掉了。
这里要强调的是,这不是错误,也不需要你做任何事。mise 会继续使用满足时间要求的最新版本,被过滤的那个版本会在时间窗口过去之后(默认发布满 24 小时)自动变为可用,下次再跑 mise upgrade 就会正常升级上去。这条警告存在的意义只是告知,避免你困惑”明明上游发新版了为什么 mise 装不到”。
从 v2026.6.2 开始,mise 为所有能提供发布时间戳的后端内置了这个 24 小时的默认延迟,包括 core(node、go 这些核心工具)、aqua、github、cargo、go、npm、pipx 等。也就是说即使你从来没在配置里写过 minimum_release_age,这个机制也已经在保护你了,这也是为什么很多人像我一样是先看到警告、再反过来查文档的。
在翻 mise 文档和源码的过程中,我发现这个机制有几个行为细节值得单独说清楚,不然很容易产生错误的预期。
第一,它只影响模糊版本解析,不影响显式 pin 的版本。所谓模糊版本,就是 latest、node@20、terraform@1 这种需要 mise 去解析”到底是哪个具体版本”的写法。如果你在 mise.toml 里明确写死了 node = "22.14.0",那不管这个版本是不是一小时前刚发布的,mise 都会照装不误。这个设计是合理的——显式 pin 意味着你明确知道自己要什么,工具不应该替你做主;而模糊解析场景下你把选择权交给了 mise,它就有责任帮你过滤掉风险窗口内的版本。
第二,过滤能力取决于后端能不能提供发布时间戳。core、aqua、github、cargo、go、npm、pipx 这些后端能拿到每个版本的发布时间,过滤就能生效;拿不到时间戳的版本会被默认放行,不会因为”无法判断”而被误伤。
第三,传递依赖的覆盖范围目前还有限。对于 npm: 和 pipx: 后端安装的工具,mise 会把时间窗口透传给底层的包管理器,让传递依赖也遵守同样的规则;其他后端目前只过滤顶层工具本身的版本。如果你的威胁模型主要担心 npm 生态的依赖投毒,这一点算是个不小的加分项。
默认的 24 小时对大多数人来说是个不错的平衡点,但 mise 提供了完整的配置手段,可以按自己的风险偏好调整。
全局调整时间窗口,在 ~/.config/mise/config.toml 或项目的 mise.toml 里设置:
[settings]
minimum_release_age = "7d" # 只安装发布超过 7 天的版本
时长支持 24h、7d、1y 这种相对写法。如果你所在的团队对安全要求比较高,把这个值调到 3 到 7 天是常见做法。
有些工具的更新是时间敏感的,比如漏洞扫描器 trivy,它的新版本往往携带最新的漏洞库,晚装一天反而降低安全性。这种情况可以按工具覆盖全局设置:
[settings]
minimum_release_age = "7d"
[tools.trivy]
version = "latest"
minimum_release_age = "1d"
也可以用排除列表把特定工具或整个后端排除在全局策略之外,支持通配符:
[settings]
minimum_release_age = "7d"
minimum_release_age_excludes = ["trivy", "npm:*"]
需要注意优先级顺序:命令行的 --minimum-release-age 参数最高,其次是按工具的设置,最后才是全局设置。被排除的工具仍然会尊重它自己的 per-tool 设置和命令行参数。
如果某次你确实需要立刻装上一个刚发布的版本(比如上游刚修了一个影响你的 bug),不用改配置文件,临时用命令行参数绕过即可:
mise upgrade node --minimum-release-age 0
mise latest node --minimum-release-age 2024-01-01 # 也支持绝对日期
反过来,如果你完全不想要这个机制,在全局配置里把 minimum_release_age 设为 0 就可以彻底关掉。不过在关掉之前建议想清楚,这个默认值的存在几乎没有日常成本——你感知到的无非就是新版本晚一天到手——换来的却是躲开绝大多数投毒版本存活窗口的保护。
另外值得一提的是它和 mise.lock 的配合。lockfile 保证的是”团队所有人装到的是同一个被验证过的版本”,minimum_release_age 保证的是”解析新版本时不会撞上刚出炉的风险版本”,两者是互补关系而不是替代关系。对于有 CI 环境的项目,lockfile 加上默认的时间窗口,基本就把工具链这一层的供应链风险控制在了一个比较舒服的水平。
minimum_release_age 是那种典型的”好的默认值”设计:机制本身极其简单,就是给新版本加一道冷静期,但它选择了默认开启,让所有 mise 用户在无感知的情况下获得了对供应链投毒攻击的基础免疫。从 npm 的 event-stream 到近几年层出不穷的 maintainer 账号劫持事件,这类攻击的共同特点就是投毒版本存活时间短、传播依赖自动更新,而”等 24 小时”恰好精准打在这两个特点上。
2026-07-29 13:00:00
chezmoi 是一个用 [[Go]] 编写的开源 dotfiles 管理工具,帮助开发者在多台机器上统一管理个人配置文件(如 ~/.gitconfig、~/.zshrc、~/.vimrc 等)。
发音为 /ʃeɪ mwa/ (shay-moi),是法语”在我家”的意思,名称寓意让每台机器都拥有”家的感觉”。
区别于 [[GNU Stow]] 这类基于符号链接的工具,chezmoi 将配置文件复制到目标位置(而非创建 symlink),并通过 Go 模板语言实现机器之间的差异化配置,同时内置密钥管理和加密能力。
传统 dotfiles 管理方式(直接用 git 仓库 + symlink)有几个痛点:
一是密钥安全问题。git 不适合存储密码、API 密钥等敏感内容,一旦误提交到公开仓库就可能立即造成泄露。
二是多机差异问题。工作机和个人机的 git 邮箱不同、macOS 和 Linux 的配置路径不同,维护多套配置文件繁琐。
三是跨平台问题。[[GNU Stow]] 仅支持 Unix 系统,且依赖 symlink,在 Windows 上无法正常工作。
chezmoi 通过模板化、加密和声明式管理解决了上述问题。
模板化配置是 chezmoi 的核心能力。基于 [[Go]] 标准库的 text/template,同一份配置文件可以根据主机名、操作系统、环境变量等条件动态渲染出不同内容。例如:
[user]
name =
email = [email protected]@gmail.com
加密与密钥管理方面,chezmoi 原生支持 age、gpg、git-crypt 和 transcrypt 对文件加密,也支持从 1Password、Bitwarden、Vault、KeePassXC 等密码管理器中动态读取密钥,敏感内容永远不会以明文形式出现在 git 仓库中。
差异化忽略支持对不同机器忽略不同文件,例如 VPN 配置只在工作机上管理,而不出现在个人机上。
脚本执行支持在 apply 时运行安装脚本,并通过 onchange_ 前缀实现”只在配置变更时重新执行”,避免每次 apply 都重跑 Homebrew 安装等耗时操作。
干运行与 diff 模式支持 chezmoi diff 查看将要应用的变更,chezmoi apply --dry-run 模拟执行,对新机器上线特别有用。
chezmoi 维护一个”源目录”(默认在 ~/.local/share/chezmoi),其中的文件名通过前缀约定来描述意图:
dot_ 前缀:对应目标路径中的 .,例如 dot_zshrc → ~/.zshrc
private_ 前缀:文件应设置为仅所有者可读(权限 0600)executable_ 前缀:文件应具有可执行权限encrypted_ 前缀:文件内容已加密存储once_ 前缀:脚本只执行一次onchange_ 前缀:脚本在内容变更时才重新执行执行 chezmoi apply 时,chezmoi 读取源目录中的文件,渲染模板,解密加密文件,最后将结果写入 Home 目录对应位置。
一行命令安装并初始化(将 GITHUB_USERNAME 替换为你的 GitHub 用户名):
sh -c "$(curl -fsLS chezmoi.io/get)" -- init --apply GITHUB_USERNAME
macOS 用 Homebrew 安装:
brew install chezmoi
Linux 各发行版也可通过包管理器安装,或直接下载二进制文件。
初始化一个新的 chezmoi 配置(不从 git 仓库拉取):
chezmoi init
添加一个文件到 chezmoi 管理:
chezmoi add ~/.zshrc
chezmoi add ~/.gitconfig
进入 chezmoi 源目录(方便直接编辑):
chezmoi cd
查看将要应用的变更:
chezmoi diff
将源目录中的配置应用到 Home 目录:
chezmoi apply
从 git 仓库拉取最新配置并应用:
chezmoi update
编辑某个已托管的文件后直接应用:
chezmoi edit ~/.zshrc --apply
| 特性 | chezmoi | [[GNU Stow]] | YADM |
|---|---|---|---|
| 实现方式 | 文件复制 + 模板 | 符号链接 | git 包装器 |
| 模板支持 | 内置 Go 模板 | 无 | 依赖外部工具(已停维) |
| 密钥管理 | 内置多种加密 | 无 | 有限 |
| 多 OS 支持 | 优秀 | 需手动处理 | 基础 |
| Windows 支持 | 支持 | 不支持 | 不支持 |
| 停止使用成本 | 低(文件即原文件) | 需删除 symlink | 低 |
| 学习曲线 | 中等 | 极低 | 低 |
与 [[dotbot]] 相比,chezmoi 的模板能力更强,密钥管理更完善,但 dotbot 的 YAML 配置风格对习惯声明式配置的用户更直观。
同时使用多台机器(工作机、家用机、云服务器)的开发者最能受益于 chezmoi。
配置文件中含有敏感信息(API 密钥、私有地址等)需要安全管理的场景,chezmoi 的加密集成是最佳选择。
在 macOS、Linux 乃至 Windows 跨平台工作的开发者,可以用同一套源文件生成各平台的差异化配置。
对于想要在新机器上实现”一键还原工作环境”的工程师,chezmoi 配合 Homebrew Bundle 或 Nix 可以做到完整的环境复现。
优势方面,chezmoi 文档详细、社区活跃,内置功能几乎涵盖所有 dotfiles 管理场景,且单一二进制无外部依赖。由于使用文件复制而非 symlink,随时停止使用都不需要额外清理工作。
挑战方面,源目录中的文件名加了 dot_ 等前缀,与实际路径不完全对应,初期会有一定认知成本。对于只管理少量配置文件、无跨机需求的用户,chezmoi 的功能可能显得过重,简单的 git bare repo 方式或许更合适。
chezmoi 是目前功能最完整、维护最积极的 dotfiles 管理工具之一,尤其适合有跨设备、跨平台需求或需要在配置中处理敏感信息的开发者。从 [[dotbot]] 或 [[GNU Stow]] 迁移到 chezmoi 有一定学习成本,但长期来看模板化和密钥管理带来的收益十分显著。
2026-07-07 13:00:00

前几天例行 brew upgrade 之后,[[Syncthing]] 其中的某一个高频使用的同步文件夹就一直卡在 Preparing to Sync 状态,进度条纹丝不动,CPU 占用却莫名升高。我最初以为是网络问题或者对端设备没启动,结果检查一遍发现所有的其他 Syncthing 节点都正常,就是本机 macOS 这端不动弹。这个状态持续了将近一个小时,才让我意识到不对劲,开始认真排查。
Syncthing 是一款开源的点对点文件同步工具,不依赖任何中心服务器,数据直接在你自己的设备之间流转。我用它同步多台设备上的工作目录和重要文件,已经稳定运行了6,7年,这次升级前一切都好好的。
这次触发问题的,是 [[Homebrew]] 将 Syncthing 从 1.x 版本升级到了 2.x 版本。Syncthing 2.0 是一次幅度相当大的版本跨越,包含了几项可能让用户措手不及的破坏性变更(breaking changes)。
Syncthing 2.0 最核心的变化是将底层的索引数据库引擎从 Google 的 [[LevelDB]] 切换到了 SQLite。Syncthing 用这个数据库存储所有文件的元数据、校验值和同步状态,这个库一旦发生格式变更,就意味着首次启动时必须完成一次完整的数据格式迁移。
官方文档对此的表述是:”数据库后端已从 LevelDB 切换到 SQLite。首次启动时会进行一次迁移,对于较大的数据集这个过程可能需要较长时间。”对于文件数量众多的用户,这个迁移甚至可以持续数小时乃至整夜。而在迁移完成之前,Syncthing 对所有文件夹展示的状态正是 “Preparing to Sync”——它在准备,只是用户看不到进度。
问题在于,通过 brew services start syncthing 作为系统服务运行时,Syncthing 的控制台输出被完全隐藏。用户打开 Web UI 只能看到一个没有进度提示的状态标签,却无从判断迁移是否还在进行、还是真的卡死了。这种信息缺失,是让人以为出了大问题的直接原因。
在采取任何激进操作之前,先确认 Syncthing 到底是真的卡住了,还是还在默默工作。
停止 Homebrew 服务,改为手动从终端启动 Syncthing,这样就能实时看到日志输出:
brew services stop syncthing
syncthing
观察终端输出。以下这类日志都属于正常现象,说明 Syncthing 仍在工作中,不需要干预:
INF GC was interrupted due to exceeding time limit (processed=3 runtime=5m34s folder=default fdb=folder.0001-xxx.db table=blocks)
INF Completed initial scan (folder.label="Default Folder" folder.id=default folder.type=sendreceive)
第一行是 SQLite 数据库在做垃圾回收(GC),运行超过时间限制后被中断,这是 Syncthing 2.0 新引入的行为,中断不代表出错,GC 会在后续继续执行。第二行是初始扫描完成的确认,出现这行后文件夹就会脱离 “Preparing to Sync” 状态。
反之,如果日志几分钟内没有任何新内容,或者出现了 “database disk image is malformed” 这样的错误,才需要进行下一步处理。
如果手动运行后看到迁移活动,最好的做法就是什么都不做,等它跑完。文件越多,等待时间越长。根据社区反馈,拥有十几万文件的用户,迁移时间可能超过两个小时。迁移完成后,Syncthing 会自动恢复正常同步,之后再用 brew services start syncthing 挂回后台服务即可。
如果迁移确实卡死,或者日志中出现了数据库损坏相关的错误,就需要删除旧的索引数据库,让 Syncthing 从零开始重新扫描。
首先,找到 Syncthing 的数据目录:
# macOS 上默认路径
ls ~/Library/Application\ Support/Syncthing/
Syncthing 2.0 的 SQLite 索引数据库位于 index-v2/ 目录下,每个同步文件夹对应一个独立的 .db 文件(如 folder.0001-abcd1234.db),以及配套的 .db-shm 和 .db-wal 文件。如果迁移卡死或数据库损坏,可以删除整个 index-v2/ 目录让 Syncthing 重建:
brew services stop syncthing
rm -rf ~/Library/Application\ Support/Syncthing/index-v2/
如果你仍有旧版 LevelDB 格式的 index-v0.14.0.db 残留(升级异常时可能存在),也可以一并清理:
rm -rf ~/Library/Application\ Support/Syncthing/index-v0.14.0.db
删除后重新启动 Syncthing,它会从头对所有文件夹执行完整扫描。文件本身不会丢失,只是 Syncthing 需要重新建立对所有文件的认知,这个过程同样需要一些时间,但通常比数据库迁移要快得多。
如果问题只是同步状态不一致,而数据库本身没有损坏,可以尝试使用 --reset-deltas 参数启动 Syncthing,让它重置增量同步状态:
brew services stop syncthing
syncthing --reset-deltas
这个方法比删除整个数据库要温和,只清除增量同步的记录,不会触发全量重扫,适合作为第一道修复手段。
如果只有某一两个文件夹卡住,其他文件夹正常,可以通过 Syncthing 的 REST API 只重置问题文件夹的索引,避免影响已经工作正常的文件夹。
先在 Web UI 的设置里找到你的 API Key,然后执行:
# 重置特定文件夹(将 your-folder-id 替换为实际的文件夹 ID)
curl -X POST -H "X-API-Key: your-api-key" \
"http://127.0.0.1:8384/rest/system/reset?folder=your-folder-id"
如果不带 folder 参数,则会重置所有文件夹的数据库,效果等同于方案二。
如果以上方案都不奏效,可以做一次干净的重装。这个方法对 macOS + Homebrew 环境效果最好:
brew services stop syncthing
brew uninstall syncthing
brew install syncthing
重装完成后,先从终端手动启动一次,观察迁移日志,确认迁移正常完成后再切换回服务模式:
syncthing
# 等待迁移完成,Ctrl+C 退出
brew services start syncthing
除了数据库迁移,Syncthing 2.0 还有一些其他变化值得留意。日志格式改为结构化日志,旧的 --verbose 和 --logflags 命令行参数已被移除,改用 --log-level 参数控制日志级别。如果你的 Syncthing Web UI 不是绑定在默认的 127.0.0.1:8384,需要注意升级机制存在一个已知 bug,它硬编码了默认地址,非默认配置下可能导致自动升级失败。
另外,Syncthing 2.0 不再为部分平台提供预编译二进制文件,包括 DragonFlyBSD、Illumos、Linux on PowerPC64、NetBSD 和部分 OpenBSD 架构,这些平台的用户需要自行从源码编译。
这次 Syncthing 升级卡住的经历,再次提醒我在做系统级工具的大版本升级之前,最好先读一读 release notes。Syncthing 2.0 将 LevelDB 换成 SQLite 是一次从根基上的重构,好处是数据库更易于维护和调试,坏处是首次启动的迁移成本对数据量大的用户来说相当可观。
如果你遇到了同样的问题,按照本文的顺序依次尝试,大概率能在方案一或方案二这里就解决。关键是不要慌,不要在迁移过程中强行重启或删除数据,那样才是真的会造成问题。耐心等一等,或者删掉旧索引让它重建,[[Syncthing]] 本身的可靠性依然值得信任。