上游被删了,然后带着一个重写版回来

· 9 分钟 · devops dockerself-hostingupstreammaintenanceregistry

docker pull hectorqin/reader 返回 404。而这正是我线上阅读服务所依赖的项目:一个只发邀请码的小书库,给我家人和几位付费读者用,每天在跑,上面还搭了一条我自己做的语音朗读管线,让「朗读」按钮念出来的是正经的神经网络音色。

404 还不是最有意思的部分。2026-09-16,同一个仓库被从零重新初始化——提交信息是 chore: 初始化仓库——十六天后,它已经有了 233 个提交、另一门语言、另一个端口、另一种数据库、另一个镜像仓库。我依赖的项目没有死,它是被一个重写版替换了,名字没变、地址没变、那 11,036 颗星也没变。

所以问题是:怎么判断一个上游项目是死了,还是正在被重写?而当它以另一个应用的样子回来时,你该怎么做?

先说我这边的情况,因为细节才是这件事的关键

这个应用是一个自托管阅读服务器。浏览器端是 hash 路由的 Vue SPA,服务端是 Kotlin + Spring Boot,配置按用户存成磁盘上的 JSON。而我专门为它的一条能力搭了一整套管线:HTTP 语音合成。阅读器会把朗读请求代理到你配置的 URL,也就是说音色不是应用自带的——我的指向 n8n 的两个 webhook,由它们去换取短期 Azure / Google 凭据并合成音频。语音选择器里那 9 个引擎是这么来的,不是应用自带的。

此外,应用的镜像外面还有我自己的一层 nginx 网关容器:对外落地页、社交分享卡片、中英切换、以及一层英文界面补丁,全在那里,不在应用镜像里。当初做这个决定是为了品牌,而正是它让整件事从头到尾都很平静——后面我会回到这点。

我去找升级路径的时候,发现了什么

三个选项,没有一个叫「升级」:

选项它实际是什么我为什么没选
官方镜像Docker Hub 上的 hectorqin/reader没了。仓库返回 404,docker pull 直接失败
还在维护的 forkchangshengyu/reader,版本 2.5.4基于最后一个完全开源的快照。它的朗读只用浏览器自带的 speechSynthesis,没有 HTTP 语音。它的整个历史里没有任何 httpTTS 字样,/reader3/httpTts 也是 404。我那 9 个引擎会全部消失
别人重新打包的镜像liangnianzhi/reader.hectorqin:latest-20250525这就是我现在跑的。 唯一还能拉取、并且保留了 HTTP 语音的 3.x 构建。最后更新 2025-09-05,一共三个标签,发布者不是原作者

我把那个标签钉死,把数据目录打包备份,把源码镜像到自己的 Git 服务器上——至少我依赖的代码得存在于我能控制的地方。然后把那句决定未来任何镜像是否有资格的话写了下来:它是否还讲我这条管线所实现的 TTS 协议?

这就是当时的处境:一个跑在「官方仓库已被删除」的镜像上的服务,靠着某个陌生人重新打包的构建活着。

然后上游回来了

以下数据于 2026-10-06 直接取自 GitHub API 与镜像仓库:

指标数值
仓库hectorqin/reader —— 活着,AGPL-3.0,默认分支 main
Star / Fork11,036 / 5,471
最后推送2026-10-02
提交数233 —— 而最早的一条是 chore: 初始化仓库,日期 2026-09-16
release / tag0 / 0
Docker Hub仍然 404

2021 年那段历史还在,但已经断开了。main 上是 233 个提交,最早一条的日期是 2026-09-16;旧分支仍然存在,但只剩一个提交,日期 2026-06-22,提交信息就是 commit,而它的 README 全文只有一个词——deleted。两个分支没有共同祖先——这就是「先清空、再重来」在 git 里的样子。

而这 233 个提交里的代码,不是我原来那个代码库的延续。它是另一个应用,只是恰好沿用了同一个名字:

我现在跑的(3.x)上游现在的样子
运行时Kotlin,Spring Boot + Vert.xTypeScript,Node
端口80805888
数据/storage/data,按用户存 JSON/data,SQLite(reader.db)
配置环境变量 + 每用户 JSON 文件管理后台界面,存进数据库
镜像Docker Hub,命名空间已删除cnb.cool/hectorqin/reader:main
范围只有书书 和 电影、剧集、音乐、有声书、OPDS、OpenList、PWA、Android 客户端

表里有三个细节值得单独拎出来,因为它们各自都有一个后果:

镜像仓库搬去了一个大多数人从没拉过东西的平台。 官方 compose 里写的是 image: ${READER_IMAGE:-cnb.cool/hectorqin/reader:main},旁边一行注释解释了原因:CI 只发布分支和提交标签,刻意不发布 :latest。也就是说默认配置跟的是一个会动的分支,唯一稳定的引用是一个 commit SHA。

配置离开了环境变量。 业务设置——HTTP 语音接口、扫描行为、登录有效期、对外 URL、允许的浏览器来源、WebDAV——现在都在管理后台里,持久化进数据库。文档说明:首次升级时「旧版业务环境变量会被导入一次」,并且已存在的数据库值永远优先。这是一次性的导入加一条优先级规则,正是你希望在副本上验证、而不是在线上实例上发现的东西。

HTTP 语音回来了,只是换了个地方。 新版本重新提供了 HTTP 语音上游的配置:合成 URL、token、voices URL、超时、缓存上限,还有连接测试和试听,全部在管理后台里配。而我的 n8n webhook 实现的协议,是我照着旧版构建自己设计的。这两个形状是否匹配,现在是一个需要用实验回答的问题,不是查文档就能回答的问题。

没人提醒你的部分:重写版是一个新应用

本能反应是把「上游复活了,还多了影音支持和 Android 客户端」读成「我等的升级到了」。并不是。一次重写会同时改变数据格式、部署面和配置模型——这意味着要做的是一场带 schema 边界的迁移,而不是 docker pull 加一次重启。

让我决定慢下来的,是项目自己的进度记录。不要拿厂商的营销页当状态报告,去看它自己写的东西,你会看到作者在列没做完的事情:

这是一份诚实得罕见的文档,也是整个仓库里最有用的一个文件。重写的作者在告诉我:升级路径写好了,但没人走过。如果连他们都没走过,我当然不会在工作日下午、在我读者正在使用的实例上、在没有数据副本的情况下走去试。

我现在会把一个依赖交给线上服务之前做的事

  1. 仓库健康不等于产物健康。 pushed_at、star、fork 说明源码还活着;对你要跑的那个确切标签执行 docker pull,才能说明你部署的东西还在。两个都要查,并读一遍 compose 看它到底指向哪个镜像仓库——我这个现在指向一个我本周之前从没打开过的域名。
  2. 「没有 release、没有 tag」是一条部署事实。 零 tag 意味着每次升级要么写死一个提交哈希,要么跟一个会动的分支。把那个 SHA 记进自己的笔记,上游不会为你记。
  3. 你输不起的东西,就镜像一份——源码和镜像都要。 一个钉在别人命名空间里的标签,是一种「那个陌生人账号别出事」的依赖。我活下来的 3.x 源码现在在我自己的 Git 服务器上,重写版也是。
  4. 先比形状,再比功能。 端口、挂载路径、变量名、数据库引擎、配置存放位置。如果这些大多变了,就把它当成一个新产品,按迁移来规划。
  5. 你重建不了的部件,放在你控制不了的镜像之外。 我的落地页、分享卡片、语言切换和翻译层,是同一条 stack 里的另一个容器。正因为如此,上游这次重写没有打扰到任何一个读者——应用镜像怎么换,都碰不到前门。
  6. 读维护者的「还没做」清单,而不是功能清单。 功能清单是销售页,未完成清单才是风险登记表。
  7. 如果你把它当成对外服务来跑,读一下许可证。 这个是 AGPL-3.0,如果你拿它给付费用户提供服务、而不是自己用,值得认真读一遍。
  8. 「仓库被归档或被清空」不等于「项目被放弃」。 有好几个月,「这个项目死了吗」的诚实答案是「是」——README 就写着 deleted,而且它说的是实话。然后有人重新开始了,换了门语言,而诚实的答案在名字不变的情况下变了。

结果

书库在跑,跑在我钉住的那个构建上,读者完全无感——这本来就是目标,而且它一直成立。变的是:「我们被困在一个没人管的镜像上」从一句含糊的担忧,变成了一个有方案的具体问题:

真正有用的教训不是「要钉版本」——那个我早就做了。而是:「这个项目还活着吗」有三个答案,而它们可以互相矛盾。仓库可以活着,而镜像已被删除;README 可以写着 deleted,而重写版已经开工四周;版本号可以纹丝不动,而运行时、端口、数据库和配置模型在底下全换了一遍。这三件事要分别去查,否则你迟早会为一个「已经不再以你测试过的形态存在」的应用规划升级。


你在替别人跑自托管服务,但不确定底下的东西还有没有人管? 这正是我在做的审计:依赖与镜像健康度、升级可行性,以及随之而来的迁移方案。

WhatsApp +60 12-797 2969 · [email protected] · hoelee.com

Lee Teong Hoe

Lee Teong Hoe · Mr Hoelee

Full-stack developer & DevOps engineer based in Malaysia. I run my own production infrastructure and build web systems for small businesses — this blog is the living record.

Hiring? Profile & stack · Malaysian SME? Website design & development ↗ · All posts