自由翱翔：十年学习成长之路 - 技术专业
type: channel
link: https://mcetf.cn/index.php/category/comport/
items: iPhone 网络定位测试：用 Quantumult X 配置 WLOC 的一次记录
items: 折腾记：大疆4G模块刷出新玩法
items: 折腾记：自建 peer relay 与 DERP，把 Tailscale 内网延迟从 300ms 打到 30ms
items: Immich 3.0 升级实录：DS218+ 从 2.7 升级到 3.0
items: Podsync 的一次"空时长"崩溃排障：从 bad format string 到 PO Token 补全
items: 从 VPS 被入侵，到建立网站备份体系
items: 一次 VPS 安全事件后的反思：从问题发现到建立完整监控体系
items: 从 Dropbox 到 Seafile：让 NAS 上的文件逐渐长成知识库
items: 从 Google Photos、iCloud 到 Immich：构建属于自己的照片管理中心
items: 一个面向 NAS 的影视文件自动整理工具

iPhone 网络定位测试：用 Quantumult X 配置 WLOC 的一次记录
type: item
date: 2026-07-16T18:00:10+00:00
description: 今天尝试了一个 iPhone 网络定位测试方案：通过 Quantumult X 拦截苹果的网络定位请求，替换 Wi‑Fi/基站定位返回的坐标。它不是修改 GPS 硬件信号，而是影响苹果网络定位服务的结果。因此在室内、GPS 信号较弱时更可能有效；如果在室外，真实 GPS 信号很强，地图 App 可能仍优先使用真实位置。配置过程使用的工具是 Quantumult X，并导入 WLOC 的重写资源：https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.conf导入时需要放在 Quantumult X 的“重写资源（Rewrite Resources）”中，而不是节点订阅。导入成功后，MITM 主机名中会自动出现：gs-loc.apple.com gs-loc-cn.apple.com这两项配置正确，且应避免添加 *.apple.com 或 * 之类的全域规则，以免扩大 HTTPS 解密范围。证书配置由于定位响应使用 HTTPS，Quantumult X 必须生成并安装自己的 CA 证书。流程如下：在 Quantumult X 的 设置 → MitM 中生成 CA。选择安装证书。在 iPhone 的 设置 → 通用 → VPN 与设备管理 中安装描述文件。到 设置 → 通用 → 关于本机 → 证书信任设置，开启 Quantumult X CA 的“完全信任”。回到 Quantumult X，打开 MitM、Rewrite 和 WLOC 重写资源，并启动连接。如果 Quantumult X 已有 CA，不必重复生成；重新生成会让旧证书失效，需要重新安装和信任。设置测试坐标项目提供了“wloc 设置地理位置”快捷指令。安装后，需要让它出现在 iOS 共享菜单中：打开“快捷指令”App，进入该快捷指令的详情。开启“在共享表单中显示”。接收类型选择 URL/链接。之后可以在苹果地图或高德地图中选点，点击“共享”，选择“wloc 设置地理位置”。快捷指令提示“已更新位置”，只表示目标坐标已写入 Quantumult X 的持久化存储，并不代表地图一定已经显示新位置。快捷指令跳转到定位服务页面执行快捷指令后，系统会停留在“定位服务”设置页面。这不是报错，而是为了刷新定位权限和系统缓存。可以按以下顺序操作：保持 Quantumult X 已连接，确认 Rewrite、MitM、WLOC 都已开启。关闭“定位服务”总开关。完全退出高德地图和苹果地图。重新打开“定位服务”。若 App 再次请求定位权限，可先选择“下次询问或在我共享时”。优先用苹果地图验证，再测试高德地图。如果系统版本较新、地图仍显示旧位置，重启手机往往更有效。重启后，应先启动 Quantumult X 并确认 VPN 图标出现，再开启定位服务和地图。恢复真实位置测试完成后，应运行“清理恢复位置”快捷指令，关闭或删除 WLOC 重写资源。若地图仍显示旧位置，重启手机后再确认。不再测试时，建议关闭 MitM，并撤销 Quantumult X CA 的完全信任或删除证书。因为 HTTPS 解密证书属于高权限配置，应只保留在自有设备、授权测试场景中。

折腾记：大疆4G模块刷出新玩法
type: item
date: 2026-07-09T23:55:28+00:00
description: 最近跟风买了一个大疆的 4G 模块。一开始我只是想看看它能不能接到 NAS 上，用来做一个短信收发管理工具。结果一路折腾下来，发现这东西确实有点意思，但也不是“插上就能用”。一开始的目标我的初始目标很简单：把这个大疆 4G 模块接到自己的设备上，实现短信集中收发管理。最好可以放在 NAS 上，因为 NAS 长期开机、管理方便，也适合跑一些 Web 服务。后来又看到网上有人把这个模块识别成 Quectel EC25 后，配合 VoHive 使用，可以管理短信、eSIM、设备状态，甚至通过 Telegram 做通知，于是就开始评估是否要刷机或改设备信息。要不要刷机？刚开始比较担心“刷机”这个词，因为涉及固件、NV 参数之类的东西，风险不低。后来确认下来，这次并不是传统意义上的刷固件，而是通过 AT 命令修改 USB 识别信息。这个大疆 4G 模块本质上是移远 Quectel EG25-G 的变体，默认 USB ID 是大疆私有的：2ca3:4006 修改后可以让系统把它识别成更通用的 Quectel EC25：2c7c:0125 修改之后，Linux 内核就能比较自然地加载 option、qmi_wwan、cdc_wdm 等驱动，设备会出现：/dev/ttyUSB* /dev/cdc-wdm0 wwan0 这一步完成后，可玩性大幅增加。为什么最后没有放在 NAS 上？一开始我确实更想把它插在 NAS 上。NAS 稳定、安静、长期在线，看起来是最合适的宿主。但实际测试后发现，我的 NAS 虽然能看到 USB 设备本身，却缺少关键内核模块，比如：option qmi_wwan cdc_wdm usb_wwan 也就是说，NAS 能看到“有个 USB 设备插进来了”，但不能把它识别成可用的蜂窝模块、串口设备或 QMI 网络设备。这就很尴尬了。VoHive 即使跑在 NAS 上，也无法真正接管这个模块。所以最后确定方案是：模块接在 OpenWrt 路由器上，VoHive 也部署在 OpenWrt 上；NAS 如有需要，只做反向代理或访问入口。这个方案反而更合理。路由器本来就是网络设备，OpenWrt 对 USB 网卡、QMI、串口这些支持也更直接。部署到 OpenWrtOpenWrt 上识别成功后，模块状态基本正常：2c7c:0125 /dev/cdc-wdm0 /dev/ttyUSB0 - /dev/ttyUSB3 wwan0 之后在 OpenWrt 上部署 VoHive。VoHive 提供了一个 Web 管理界面，可以看到模块状态、SIM 信息、短信中心、eSIM 页面、通知配置等功能。最后访问地址是：[http://192.168.5.1:7575](http://192.168.5.1:7575) 部署完成后，VoHive 可以正常启动，也能识别模块。修改设备信息后的变化把设备从大疆私有 ID 改成 Quectel EC25 兼容身份后，最大的变化是系统识别能力明显增强。原来它更像是一个“只服务于大疆生态的模块”；修改后，它变成了一个可以被 Linux 正常管理的蜂窝模块。可以实现的东西包括：识别 SIM 卡信息查看信号强度、网络状态、注册状态管理短信收发通过 QMI 管理蜂窝模块配合 VoHive 做 Web 化管理配置 Telegram、Webhook、Email 等通知理论上支持 eSIM/eUICC 卡管理使用支持蜂窝注册的 SIM 时，还可以进一步配置数据联网这一步算是整个折腾里最关键的分水岭。目前实现的使用场景目前我已经实现的场景是：把一张能正常注册蜂窝网络的 SIM 卡插在大疆 4G 模块里，通过 OpenWrt 上的 VoHive 实现短信集中收发管理。现在可以在 Web 页面里查看短信，也可以配置 Telegram Bot，把收到的短信推送到 Telegram。这样一来，一些验证码、通知短信、备用号码短信，就不用再依赖某一台手机常年开机插卡了。模块插在路由器上，VoHive 负责管理，Telegram 负责提醒，整体体验还挺顺手。

折腾记：自建 peer relay 与 DERP，把 Tailscale 内网延迟从 300ms 打到 30ms
type: item
date: 2026-07-09T23:54:07+00:00
description: 起因手机上通过某第三方客户端内嵌的 Tailscale 模块连回家里内网，延迟高得离谱——tailscale ping 动辄 300ms 起步，还会抖到 1.6 秒，速度慢到基本不可用。表面是"网络慢"，深挖下去是一整条链路的问题。这篇记录完整的排查与解决过程，也算给自己留个底。第一步：定位根因——中继落在境外高延迟在 Tailscale 场景里几乎都指向同一个根因：流量走了 DERP 中继，而不是 P2P 直连。先在 NAS 上定位：tailscale ping 100.x.x.x # pong from node via DERP(hkg) in 381ms ... direct connection not establishedvia DERP(hkg)——中继落在境外节点。再看 netcheck：Nearest DERP: Hong Kong hkg: 145.6msNAS 到官方境外 DERP 就要 145ms，手机（蜂窝网络）到该节点又是一段，两段叠加加抖动，300ms+ 就是这么来的。P2P 直连没打通，全程依赖这个远距离中继。结论：得把中继点从境外换到境内。第二步：尝试 peer relayTailscale 的 peer relay（对等中继）走 UDP，延迟特性接近直连，比 DERP（TCP/TLS）更优，是"打洞失败但还没沦落到 DERP"的中间层。我在一台境内云服务器上部署它。踩坑记录一开始以为打了 tag:relay 标签就算部署好了，实际差得远：tailscale debug prefs 里 grep 到的 tag:relay 只是身份标签，不是 relay 服务本身；peer relay 真正的启用是写入 RelayServerPort：tailscale set --relay-server-port=41641启用后确认：tailscale debug prefs | grep -i relay # "RelayServerPort": 41641 ss -lunp | grep 41641 # tailscaled 在 41641/udp 上 LISTEN（v4+v6 双栈）ACL 里也补上了 grant："grants": [ { "src": ["*"], "dst": ["tag:relay"], "app": { "tailscale.com/cap/relay": [] } } ]云服务器安全组放行 41641/UDP（这台没装 ufw，系统层不拦，但云平台安全组是独立一层，必须在控制台放行）。验证服务端本身没问题关键的一步——从 NAS 直接 ping 这台 relay：tailscale ping 100.x.x.x # pong from relay-vps via <公网IP>:41641 in 17msdirect UDP 直连，17ms，走的正是 41641。 说明服务端、端口、安全组、端点宣告全部正常。再 ping 另一台海外机器（官方客户端）也是 direct。但手机就是走不通配置全齐了，手机的 tailscale ping 依然死守 via DERP(hkg)。做了对照实验后规律清晰：链路结果性质NAS ↔ 境内 VPS17msdirect ✅NAS ↔ 海外机器186msdirect ✅NAS ↔ 手机(第三方客户端)300ms+via DERP(hkg) ❌所有官方 Tailscale 客户端之间都能 direct，唯独跟手机（第三方客户端内嵌模块）之间只能走 DERP。 问题不在服务端，在那个第三方客户端的内嵌 Tailscale 实现——它不支持 peer relay 协商。这个变量我控制不了。第三步：自建 DERP（derper）——不挑客户端的兜底方案peer relay 依赖对端支持，而对端是第三方客户端，这条路对我的实际场景走不通。转向 derper：它是 DERP 协议，所有 Tailscale 客户端天生都会用，包括那个内嵌模块，配好即用、必然生效。而且实测这台境内 VPS 到我家公网 IP 只有 14ms、到 NAS 17ms（netcheck 显示"最近 DERP 在境外"是被国际出口策略绕路误导，真实 UDP 路径是境内直达的），位置其实极佳，正是理想的 derper 载体。部署要点（derper 独立跑 8443，与 Web 服务的 443 共存）443 端口被这台上的 Caddy 占用（跑网站），所以让 derper 跑 8443，两者井水不犯河水。证书：让 Caddy 负责签发和续期，derper 用 manual 模式读现成证书。在 Caddyfile 加一个块为你的 DERP 子域名签证书。这里踩了个低级坑：一开始把子域名的后…

Immich 3.0 升级实录：DS218+ 从 2.7 升级到 3.0
type: item
date: 2026-07-09T10:00:12+00:00
description: 最近看到 Immich 正式发布了 3.0 版本。作为我 NAS 上最重要的应用之一，自然第一时间关注了一下这次的大版本更新。在升级之前，我先对比了一下 3.0 与我目前使用的 2.7 版本有哪些值得升级的新功能。为什么升级？官方这次 3.0 并不是一次简单的版本更新，而是一次比较大的功能升级。比较吸引我的几个新特性包括：内置 AI 修图（Image Editing），可以直接在 Immich 中完成简单的照片编辑，而不用再导出到其它软件。更完善的照片管理能力，包括更好的浏览体验和细节优化。搜索和 AI 能力持续增强，后续还有更多基于视觉模型的扩展空间。底层架构升级，数据库向量引擎等组件进行了优化，为后续功能提供基础。对于我来说，最有价值的还是修图功能。平时大量照片都是手机自动同步到 NAS，偶尔需要裁剪、调整亮度或者做一些简单编辑，以前都需要下载到手机或电脑处理，再重新上传。现在如果能够直接在 Immich 中完成这些操作，整个照片管理流程会方便很多。综合评估之后，我决定将家里的群晖 DS218+ 升级到 Immich 3.0。我的环境升级前的环境如下：NAS：Synology DS218+DSM 7Docker Compose 部署Immich 2.7.xPostgreSQL 官方数据库镜像十万余张照片AI 人脸识别已完成由于图库已经比较大，所以升级之前还是决定按照官方建议，先做好完整备份。升级前检查首先确认当前的 Docker Compose 配置。检查发现，我使用的已经是官方新版 Compose 文件，数据库也已经采用了新的 VectorChord 镜像，因此官方升级文档中关于数据库迁移的步骤并不适用于我的环境，不需要再修改数据库配置。随后备份了几个关键文件：PostgreSQL 数据库docker-compose.yml.env这样即使升级过程中出现问题，也能够快速恢复。开始升级升级过程实际上非常简单，基本按照官方文档执行即可。首先拉取最新镜像：docker compose pull随后重新启动容器：docker compose up -d整个升级过程中唯一遇到的问题，是下载镜像速度比较慢。Immich 镜像托管在 GitHub Container Registry（GHCR），其中 immich-server 镜像有一个三百多 MB 的 Layer 下载速度非常慢，在我的网络环境下等待了相当长时间才完成。这一点需要一点耐心，或者提前为 NAS 配置代理。升级后的第一次启动镜像更新完成后，我立即访问：http://NAS_IP:2283结果浏览器一直打不开。第一反应还以为升级失败了。随后开始逐步排查。首先检查容器状态：docker compose ps发现：PostgreSQL 正常Redis 正常Machine Learning 正常Immich Server 已经启动，但状态仍然是 health: starting继续查看日志：docker logs immich_server日志显示：Running migrations同时可以看到系统正在处理十万多张照片对应的人脸索引和向量索引。这才意识到，并不是升级失败，而是 Immich 3.0 第一次启动正在执行数据库迁移和索引检查。对于只有两核 CPU 的 DS218+ 来说，这一步需要一定时间。只要耐心等待，服务完成初始化后，健康状态会从 starting 变成 healthy，网页也会恢复正常访问。总结整个升级过程比想象中顺利，没有遇到兼容性问题，也不需要重新导入照片或重新识别人脸。目前 Immich 已经逐渐从一个优秀的开源相册，发展成了一套功能越来越完整的个人照片管理平台。对于我来说，这次升级最大的期待，就是能够在 NAS 上直接完成一些日常修图操作，进一步减少照片在不同设备之间来回导出的流程。

Podsync 的一次"空时长"崩溃排障：从 bad format string 到 PO Token 补全
type: item
date: 2026-07-09T06:08:34+00:00
description: 家里 NAS 上跑着三个 podsync 容器，把一个频道转成播客音频喂给我的播客客户端。这套东西平时很省心，直到某天日志里开始反复刷同一条错误。这篇是完整的排障记录——之所以写下来，是因为过程里有三次方向判断被推翻，而每一次被推翻，都比一开始"猜对"更能说明这个系统到底是怎么运转的。一、现象日志里，某一个 feed 每轮更新都失败，其它 feed 正常：ERRO ... failed to update feed: https://www.youtube.com/channel/UC.../videos error="update failed: failed to parse duration : bad format string"注意报错里 duration 后面那个值是空的。而且它只影响 A01——后面的 A02、A03 照常下载，说明 podsync 是逐个 feed 更新的，一个挂了不拖累其余。损失面很小，但这条错误会一直刷，且只要触发条件在，就不会自愈。二、定位：三次推翻第一次判断：加个过滤把它挡掉 —— 失败第一反应是：列表顶部大概有一个"未开播的首播/直播预告"，它没有时长，把解析撑爆了。那就用 yt-dlp 的 match-filter 把这类视频过滤掉。于是往该 feed 的 youtube_dl_args 里加了：youtube_dl_args = [..., "--match-filter", "live_status!=is_upcoming"]重启后，报错依旧。第二次判断：过滤发生的阶段不对 —— 部分正确，但仍不完整查文档后明白了：youtube_dl_args 只在下载阶段传给 yt-dlp，而崩溃发生在更早的列表构建阶段。过滤加在了下载侧，够不到列表侧的解析，自然没用。同理，podsync 自带的 filters（标题、时长过滤）也是解析完之后才执行的，一样挡不住。这一步方向对了，但我当时以为列表也是 yt-dlp 抽的。直到看清报错本质，才有了第三次、也是最终的定位。第三次判断（最终）：这是 Go 二进制里的 bug，配置无解关键线索是 bad format string 这个措辞——它来自 podsync 解析 ISO8601 时长（PT#H#M#S 那种格式）时对空值的报错。而 ISO8601 时长只有 YouTube Data API 才会返回；yt-dlp 给的时长是浮点秒数，不长这样。于是链路彻底清楚了：拉列表 = podsync 调 YouTube Data API，逐个解析视频的 contentDetails.duration（ISO8601）；下载 = yt-dlp（cookies / node / GetPOT 都在这一侧）。那个频道当时挂了多个长周期的预约首播（首播时间排到第二天），这些未开播视频的 API 时长是空字符串，podsync 老版 Go 二进制拿到空值直接抛 bad format string、整条 feed 崩。结论：崩溃在 podsync 的 Go 代码里，任何配置层面的过滤都救不了它。 一个讽刺的旁证是——那两条卡住我的预告标题里都带"XX分钟"，而我配置里本就写了 not_title = "XX分钟" 想排除它们；但解析崩在过滤之前，连执行这条排除规则的机会都没有。查到官方 issue #638 正是这个问题，且已 Closed 但没有关联主线 PR——最合理的解读是：提出者（fqx，同时是 podsync-with-yt-dlp fork 的维护者）在自己 fork 里修好后关掉了上游 issue，主线从未合并这个补丁。所以升级官方镜像大概率没用，补丁只在 fqx 的 fork 里。三、方案：换一个"能容忍空时长"的构建确认我跑的是三年前的官方 mxpv/podsync:latest（Docker Hub），且 :latest tag 不会自动刷新运行中容器的 Go 二进制——yt-dlp 靠 self_update 一直是新的，但 podsync 本体一直是老的。这解释了为什么它一边下载正常、一边解析崩溃：下载能力和崩溃点是两条独立的线。走弯路：把新二进制塞进老镜像 —— 失败第一个想法是外科手术式的：用多阶段构建，只把 fqx 修好的 podsync 二进制 COPY 进我现在这个能正常下载的老镜像，其余不动。FROM ghcr.io/fqx/podsync-with-yt-dlp:latest AS patched FROM mxpv/podsync:latest COPY --from=patched /app/podsync /app/podsync构建成功，但一启动：exec /app/podsync: no such file o…

从 VPS 被入侵，到建立网站备份体系
type: item
date: 2026-07-03T02:00:10+00:00
description: 最近我的私有 VPS 被部署恶意程序的事件，对我最大的触动其实不是如何清除病毒，而是意识到另一个经常被忽略的问题——数据安全。以前更多关注的是服务器安全：关闭端口、最小权限、防火墙、监控告警……这些都很重要。但真正静下心来思考后发现，即使安全措施做得再完善，也没有任何人敢保证服务器永远不会出现问题。真正能够降低风险的，不仅是防御，更是可恢复能力（Recoverability）。因此，在完成 Sentinel 安全监控体系之后，我又开始补齐另一块短板——应用数据备份体系。为什么要做备份对于 我的 网站来说，真正有价值的并不是代码。代码可以通过 GitHub 恢复，可以重新部署；真正难以恢复的是：SQLite 数据库中的业务数据配置文件网站运行过程中不断积累的数据如果服务器被删除、磁盘损坏，甚至再次遭遇攻击，没有备份意味着这些数据可能永久丢失。所以这次工作的目标很明确：让任何一台服务器出现问题，都能够在最短时间内恢复业务。第一层：服务器本地自动备份首先为项目新增了独立备份脚本。备份策略分为两类：数据备份每天自动执行使用 SQLite Backup API 导出数据库避免直接复制正在运行中的 SQLite 文件自动校验数据库完整性（PRAGMA integrity_check = ok）默认保留 60 天历史版本代码备份每周自动打包自动排除 .git、.venv、缓存目录、运行数据、备份目录等无关内容默认保留 12 周历史版本这样，每一次备份都会生成带时间戳的压缩包，可以随时回滚到任意历史版本。第二层：Systemd 定时自动执行备份不能依赖手工执行。因此，又新增了 systemd 定时任务，让整个流程完全自动化。目前的计划为：数据备份：每天执行代码备份：每周执行所有 Timer 已安装到系统，并验证能够正常触发。以后服务器即使长期无人登录，也会持续生成新的备份。第三层：NAS 异地备份本地备份解决不了服务器本身损坏的问题。如果 VPS 被删除、硬盘故障，或者整台机器不可恢复，本地所有备份仍然会一起丢失。因此又增加了异地备份。整个方案采用 NAS 主动拉取（Pull）的方式：VPS │ SSH + rsync │ Synology NAS（Docker）NAS 通过 Docker 容器定时连接 VPS，将服务器上的备份目录同步到本地。同步完成后，备份会保存在 NAS 独立的数据分区中，不占用系统分区空间，也方便后续继续扩展更多项目。这样即使 VPS 整体损坏，也仍然保留着完整的数据和代码备份。第四层：恢复文档备份真正发挥作用，是在恢复的时候。因此，又补充整理了完整的备份文档，包括：本地备份方式自动备份部署方法NAS Docker 配置数据恢复流程代码恢复流程同时同步更新了项目状态文档和 GitHub 配置，使整个备份体系能够长期维护，而不是依赖记忆。

一次 VPS 安全事件后的反思：从问题发现到建立完整监控体系
type: item
date: 2026-07-02T04:37:21+00:00
description: 最近这几天，几乎把所有业余时间都投入到了 VPS 安全排查中。事情的起因，是一台 Oracle Cloud VPS 被发现运行了疑似恶意程序。虽然最终没有造成明显损失，但整个事件却让我意识到一个问题：真正危险的不是服务器被攻击，而是服务器被攻击了很久，而自己却毫不知情。这次事件，也促使我重新设计了整个 VPS 安全监控体系。从最初的发现异常，到紧急处置，再到监控体系建设和持续能力升级，对自己的运维理念产生了很大的影响。一、问题的发现：一次持续多天的安全隐患最初收到的并不是入侵告警，而是一条来自自己编写的巡检脚本的提示：可疑高 CPU 进程。登录服务器后，很快发现了异常。进程运行在 /tmp 目录，文件名随机，长期占用大量 CPU，父进程指向 LiteLLM，随后又发现多个 Python Loader 以及已经退出的僵尸进程。继续分析时，所有程序已经全部删除，只留下运行痕迹。整个行为非常符合目前 Linux 挖矿木马常见的模式：下载恶意程序临时运行删除自身不留下文件虽然最终没有找到矿池地址和程序样本，无法做到百分之百确认，但综合各种行为特征来看，基本可以判断是一类典型的 CPU 挖矿木马。更让我后怕的是：从日志推算，这个程序已经运行了接近二十天。如果不是偶然触发巡检，我甚至不知道服务器曾经执行过恶意程序。二、紧急处置：先控制风险，再寻找原因确认异常后，并没有急于寻找攻击入口，而是先进行了风险控制。首先检查了系统是否已经被进一步控制：SSH 登录记录authorized_keysCron 定时任务Systemd 服务网络连接新监听端口Docker 容器临时目录幸运的是，没有发现持久化后门，也没有发现异常网络连接。随后又检查了 LiteLLM 保存的各种 API Key。由于服务器已经运行过未知程序，虽然没有发现盗刷迹象，但为了避免风险扩大，还是全部进行了轮换，包括：Gemini API KeyLiteLLM Master Key与此同时，也重新评估了 Oracle Cloud Free Tier 的风险。结合官方 Abuse 策略来看，只要没有持续挖矿、DDoS、垃圾邮件等行为，被封禁账号的概率较低。最后还是决定：这台服务器不再继续使用，重装恢复可信环境。对于安全来说，“怀疑已经失陷”和”已经失陷”并没有本质区别。三、重新认识监控：没有监控，就没有安全整个排查过程中，一个问题不断出现：很多异常，其实已经发生了。但是没有任何证据留下。例如：/tmp 程序已经删除Python Loader 已删除临时端口已经关闭高 CPU 已恢复正常等人登录服务器的时候，现场已经消失。这让我意识到：传统的”发现问题再登录服务器分析”已经越来越难应对现在的恶意程序。因此，我开始重新设计整个监控体系。四、构建多层监控体系目前已经初步形成四层监控架构。第一层：Healthchecks负责监控所有定时任务是否仍然正常执行。任何一个定时任务停止运行，都能够第一时间收到通知。第二层：Sentinel 自检每台服务器都会定时向 Telegram 发送自检消息。例如：✅ Sentinel 自检：Telegram 通道正常如果某台服务器突然没有发送自检信息，即使 Healthchecks 正常，也说明监控程序自身可能已经失效。这次排查过程中，就发现了三台服务器没有发送 Sentinel 自检消息。虽然 VPS 本身正常，Healthchecks 也正常，但最终定位到 Sentinel 部署存在遗漏。这也验证了：监控程序本身，也需要被监控。第三层：安全巡检目前已经能够自动检测：新监听端口高 CPU 可疑进程SSH 登录变化Root 登录新增用户Systemd 服务变化Cron 变化Docker 异常API Key 风险临时目录可执行文件所有异常统一推送 Telegram。第四层：人工复核自动监控负责第一时间发现问题。真正的判断仍然需要人工分析。因为：安全最大的敌人，不一定是漏报。还有误报。五、监控上线后的第一次”考试”监控刚上线，很快连续收到几条安全告警。例如：发现新的 UDP 监听端口。第一时间登录服务器检查。结果发现：只是 WireGuard、rpcbind 等系统组件。还有一次：可疑高 CPU。最后发现：居然是巡检脚本自己执行 ps 命令时，把自己识别成了高 CPU 进程。这些误报虽然没有真正的安全风险，却让我认识到：监控不仅要能够发现异常，更要尽可能减少误报。否则时间久了，真正的重要告警也容易被忽略。六、监控能力持续升级结合这几次排查，目前已经规划了 Sentinel 下一阶段升级方向。包括：新端口二次确认，过滤瞬时监听进程白名单机制自动保存现场证据SHA256 校验可执行文件 Hash 监控Docker 配置变化检测Caddy、Xray 配置变更监控API K…

从 Dropbox 到 Seafile：让 NAS 上的文件逐渐长成知识库
type: item
date: 2026-06-15T00:00:48+00:00
description: 前言多年来，我一直将 Dropbox 作为最重要的资料管理和同步平台。工作资料、学习笔记、家庭文档以及各种项目文件，几乎都依赖 Dropbox 在不同设备之间同步。这个周末，我刚刚完成了一项重要工作——利用 Immich 替代 Google Photos，实现了照片的统一管理和自托管存储。当照片完成迁移之后，一个新的问题自然摆在面前：既然照片已经回到了自己的 NAS，那么文件是否也应该回到自己的 NAS？于是我开始寻找 Dropbox 的替代方案。经过一段时间调研和测试，最终选择了 Seafile。而更有意思的是，在使用过程中，我发现 Seafile 不仅仅可以替代 Dropbox 的同步功能，还可以与 AI 能力结合，逐步构建属于自己的知识库体系。为什么选择 Seafile在 NAS 生态中，可选的文件同步方案很多：Synology DriveNextcloudOwnCloudResilio SyncSeafile最终选择 Seafile，主要基于三个考虑。同步能力优秀Dropbox 最大的优势一直都是同步。而 Seafile 是少数能够在同步体验上接近 Dropbox 的开源产品。它采用块级同步技术：文件仅上传变化部分大文件同步效率高网络带宽利用率更好多设备同步稳定实际使用下来，同步体验完全达到预期。数据掌握在自己手中Dropbox 本质上仍然是 SaaS 服务。虽然方便，但数据最终存放在第三方平台。而 Seafile 部署在自己的 NAS 上：数据自主掌控不受订阅空间限制可以结合 NAS 实现多重备份不受平台政策变化影响这一点与我选择 Immich 替代 Google Photos 的逻辑完全一致。资源占用较低我的设备是一台：群晖 DS218+（10GB内存）相比 Nextcloud 这种“大而全”的平台，Seafile 更专注于文件同步本身。因此：部署简单资源占用低长期运行稳定对于老款 NAS 非常友好。从文件同步到知识管理使用一段时间后，我发现一个问题：Seafile 可以很好地解决文件同步问题。但它并没有解决另一个问题：如何快速找到文件中的知识？随着资料越来越多：项目方案会议纪要学习笔记技术文档博客素材虽然都保存在 Seafile 中，但真正需要某个内容时，依然需要：回忆文件名回忆目录结构手工搜索而近年来 Dropbox 已经逐渐向智能内容平台发展。例如：AI 搜索内容摘要智能问答文档关联这些能力恰恰是 Seafile 社区版所欠缺的。于是我开始思考：能否让 Seafile 专注存储，而把 AI 能力外挂出来？最终形成了下面的架构。Seafile + Open WebUI + RAG整体架构如下：Seafile │ ▼ Seaf-CLI 实时同步 │ ▼ NAS 本地目录 │ ▼ oikb 自动同步 │ ▼ Open WebUI Knowledge │ ▼ LiteLLM │ ├── Gemini Flash（问答） └── Gemini Embedding（向量化）整体思路非常简单：第一层：Seafile负责：文件存储文件同步版本管理这一层不承担任何 AI 功能。第二层：Seaf-CLI这是整个方案最关键的一步。Open WebUI 并不能直接读取 Seafile。因此需要利用官方客户端：seaf-cli将 Seafile 库实时同步到 NAS 本地目录。同步后的目录就是普通文件夹：/work /home /study /private所有文件变化都会自动同步下来。第三层：Open WebUIOpen WebUI 负责：文档解析向量存储RAG 检索AI 对话这里我没有使用本地模型。而是接入自己已经部署好的 LiteLLM 网关。第四层：LiteLLMLiteLLM 统一管理各种模型。当前配置：问答模型gemini-2.5-flash负责最终生成回答。向量模型gemini-embedding-001负责：文档向量化问题向量化相似度检索这是构建知识库必不可少的一层。自动增量同步知识库如果每次新增文件都手工上传知识库，这套体系就失去了意义。因此我引入了：oikb（Open WebUI Knowledge Base Sync）它负责：自动扫描目录新文件自动入库修改文件自动更新删除文件自动清理定时增量同步最终形成完整闭环：文件放入 Seafile │ ▼ 自动同步到 NAS │ ▼ 自动更新 Open WebUI │ ▼ 自动生成向量 │ ▼ 知识库实时更新整个过程无需人工干预。实际效果部署完成后，最大的变化是：我不再依赖目录结构寻找资料。而是直接向自己的知识库提问。例如：工作场景总结一下近三个月客服智能体建设进展生活场景我之前关于 Immich 迁移记录了哪些内容当前感受截至目前，Seafile 已经基本满足了我对 Dr…

从 Google Photos、iCloud 到 Immich：构建属于自己的照片管理中心
type: item
date: 2026-06-15T00:00:36+00:00
description: 前言照片记录着生活中的点点滴滴。过去十几年里，我一直习惯使用 Google Photos 作为主要的照片存储和管理平台。Google Photos 刚推出时，可以说是最好的照片管理工具之一。不限量存储、强大的搜索能力、优秀的人脸识别和时间轴管理，让我几乎把所有照片都上传到了 Google Photos。然而随着 Google 取消免费无限容量政策，照片存储逐渐变成了一项持续支出。随着照片和视频数量不断增加，每个月都需要为额外空间支付费用。同时，由于网络环境原因，Google Photos 的访问体验也越来越不方便。更大的问题在于，随着设备的增加，我的照片开始散落在多个平台。Google Photos：主要照片库iCloud Photos：部分 iPhone 照片华为手机本地照片DJI 无人机照片和视频运动相机视频NAS 本地存储由于不同设备和平台之间的同步机制不同，同一张照片经常会出现在多个地方。结果就是：照片越来越多，但真正想找到一张照片却越来越困难。因此，我决定借助 Immich 构建一个统一的照片管理中心，实现所有照片的集中管理和自动去重。为什么选择 Immich在选择照片管理平台时，我对比过多个方案：Synology PhotosPhotoPrismNextcloud PhotosImmich最终选择 Immich，原因主要有三个。第一，体验最接近 Google PhotosImmich 提供了：时间轴浏览地图浏览人脸识别智能搜索手机自动备份多用户管理对于 NAS 用户来说，已经具备替代 Google Photos 的能力。第二，完全掌握数据所有照片存储在自己的 NAS 中。不再依赖第三方平台，也不再担心未来政策变化导致额外费用增加。第三，强大的排重能力这也是我此次迁移最看重的一点。我的照片来源复杂：Google PhotosiCloud华为手机DJI 设备本地目录这些来源之间存在大量重复照片。Immich 本身具备较好的重复照片识别能力，而 immich-go 工具又进一步增强了导入过程中的排重能力。这意味着：无论照片来自哪里，最终在 Immich 中都只保留一份。这正是我想要的结果。第一步：升级 Immich开始迁移之前，我首先对 Immich 进行了升级。我的版本已经比较老：v1.135.3升级目标版本：v2.7.5由于跨越多个大版本，因此先进行了完整备份：PostgreSQL 数据库Immich Librarydocker-compose 配置.env 配置确认备份完成后执行升级。升级完成后重新索引数据库和媒体文件，为后续的大规模导入做好准备。第二步：导入 Google PhotosGoogle Photos 是我的核心照片库。经过多年积累，导出的 Google Takeout 数据达到数百 GB。Google Takeout 导出通过 Google Takeout 导出 Google Photos：Google Takeout ↓ Google Photos ↓ ZIP格式 ↓ 10GB分卷最终生成了数十个压缩包。批量下载问题由于文件数量巨大，手工逐个下载非常麻烦。因此采用浏览器 Cookie 认证的方式编写自动下载脚本。脚本实现：自动读取Cookie ↓ 自动下载压缩包 ↓ 失败自动重试 ↓ 断点续传最终实现无人值守下载。使用 immich-go 导入Google Photos 最大的问题是元数据。很多拍摄时间、相册信息并不在照片文件本身，而是在对应的 JSON 文件中。因此不能简单解压后直接上传。最终采用：immich-go upload from-google-photos进行导入。导入过程中：自动恢复拍摄时间自动恢复相册自动关联元数据自动检测重复照片整个过程虽然耗时较长，但最终成功保留了 Google Photos 的完整历史记录。第三步：导入 iCloud Photos相比 Google Photos，iCloud 的导出过程反而更加折腾。Apple 数据导出Apple 提供数据导出功能。申请后会生成下载链接。但这里有一个问题：下载链接有效时间非常短实际测试大约十几分钟后就会失效。暴力并发下载面对这个限制，我采用了最直接的方法：获取所有下载链接 ↓ 同时启动全部下载任务 ↓ 最大化利用有效时间虽然简单粗暴，但效果很好。最终成功获取全部照片和视频。使用 immich-go 导入导入方式与普通目录类似：immich-go upload导入过程中：自动读取 EXIF自动识别拍摄时间自动排重大量已经存在于 Google Photos 中的照片被自动识别为重复资产。因此最终并没有出现照片数量翻倍的问题。第四步：导入 DJI、运动相机和无人机素材除了手机照片之外，我还有大量设备产生的视频文件。包括：DJI 无人…

一个面向 NAS 的影视文件自动整理工具
type: item
date: 2026-05-24T14:00:08+00:00
description: 最近整理 NAS 上的电影和剧集文件时，遇到一个很实际的问题：下载目录里的文件名经常非常混乱，包含资源站水印、清晰度、压制组、音轨、字幕信息，甚至还有拼音缩写、别名、错别字。如果全部手工整理，不仅耗时，也很容易出错。伴随Ai编码的使用，我现在已经比较少的使用现有的代码了，主要还是为了避免一些安全上的问题，毕竟要部署在我的装着各种资料的nas上。于是做了一个小工具：AutoReel。它的目标很简单： 监听一个输入目录，自动识别电影和剧集，然后按 Emby / Jellyfin / Plex 友好的目录结构移动并重命名文件。项目地址：https://github.com/walker22026/AutoReel代码的编写过程远远的超出的我的开始预估，框架搭建起来容易，但是真正的运行，能用就有一些问题，当然更谈不上好用。最主要的问题就是我的节目源主要来自于阿里云或者百度云，这个共享资源为了避免被“和谐”采取各种名称变型，导致猜测也猜不出来，这种情况下，我尝试使用llm来进行节目真实名称的理解，效果并不好。最终还是选择简化处理的模式，能够自动处理的自动处理，无法识别的放入特定目录中，等待使用人接入。避免llm自动处理导致的节目的整理混乱。项目定位AutoReel 不是一个复杂的媒体库管理系统，也不替代 Emby、Jellyfin、Plex。它只做一件事：把下载目录里的影视文件，整理到标准媒体库目录中。例如：输入目录： /emby/source/毒液：最后·一舞.2160p.DV.HDR (2024)/xxx.mp4 整理后： /emby/电影/毒液：最后一舞 (2024)/毒液：最后一舞 (2024).mp4剧集则整理为：/emby/剧集/低智商犯罪 (2026)/Season 01/低智商犯罪 - S01E01.mp4当前执行策略AutoReel 启动后会监听输入目录。当前设计中有三个核心目录：输入目录：/host/emby/source 电影目录：/host/emby/电影 剧集目录：/host/emby/剧集输入目录下还会自动生成：_unrecognized 未识别目录 _duplicates 重复文件目录 _pending_delete 无媒体文件待清理目录整体流程是：输入目录 ├── 单个视频文件 │ ├── 识别成功 -> 移动到电影/剧集目录 │ └── 识别失败 -> 移入 _unrecognized │ ├── 子目录 │ ├── 作为一个批次处理 │ ├── 全部视频识别成功 -> 整个目录整理 │ ├── 任一视频识别失败 -> 整个目录移入 _unrecognized │ └── 目标文件已存在 -> 整个目录移入 _duplicates │ └── _unrecognized / _pending_delete └── 扫描时跳过为什么不用硬链接最初考虑过硬链接，这样可以让下载目录和媒体库目录同时存在一份“看起来独立”的文件，但实际只占一份空间。不过最终我选择了直接 move 模式，也就是：直接移动并重命名原始文件。原因是我的使用场景更偏向媒体库整理，不需要继续做种，也不希望一个文件在多个位置看起来同时存在。这样逻辑更简单，也更容易理解：整理完成后，源目录里就不再保留原文件。电影处理规则电影分两种情况。单视频目录如果一个目录中只有一个视频文件，优先使用目录名识别电影。例如：毒液：最后·一舞.2160p.DV.HDR (2024)/ xxx.mp4清洗后会识别为：毒液：最后·一舞 2024然后通过 TMDB 查询，整理为：电影/毒液：最后一舞 (2024)/毒液：最后一舞 (2024).mp4多视频目录如果一个目录下有多个视频文件，则更像电影合集或系列电影。例如：夺宝奇兵/ Raiders.of.the.Lost.Ark.1981.mkv Indiana.Jones.and.the.Last.Crusade.1989.mkv Indiana.Jones.and.the.Dial.of.Destiny.2023.mkv这种情况下不会只用目录名“夺宝奇兵”去匹配一部电影，而是逐个视频文件识别。只有全部识别成功后，才会整体移动。如果其中任何一个文件无法识别，整个目录会被移动到 _unrecognized，避免整理一半导致目录结构被破坏。剧集处理规则剧集采用“目录优先”的原则。原因是很多剧集文件名非常简单，例如：低智商犯罪（2026）/ 01 4K.mp4 02 4K.mp4 03 4K.mp4如果直接把 01 4K.mp4 拿去 TMDB 搜索，很可能会误识别成电影，比如：01 -> 某部电影 02 -> 另一部电影 03 -> 又一部电影这是非常危险的。所以现在的策略是：目录名负责识别剧名 文件名只负责提取集数也就…