post: two reader/gateway posts — the upstream rewrite, and the front-door pattern (EN+ZH) + og/banner images
Deploy / build (push) Successful in 22s
Deploy / build (push) Successful in 22s
1) the-upstream-was-deleted-then-came-back-rewritten (devops, pubDate 2026-10-06) docker pull hectorqin/reader -> 404; the repo was re-initialised 2026-09-16 and now carries 233 commits, a TypeScript/Node rewrite, port 5888, SQLite /data, config moved into the admin UI, and an image on cnb.cool with no tags or releases. Covers telling dead from rewriting, the migration boundary, and keeping the front door outside the app image. Facts re-verified live 2026-10-06. 2) putting-a-front-door-on-an-app-you-cant-modify (devops, pubDate 2025-06-24 backdated into the empty 2025-06 window, updatedDate 2026-10-06 so lastmod stays honest) The reader-gateway pattern: a second nginx container owning the public port, the request-time resolver, why a proxy body rewrite cannot touch a client-rendered SPA, the gate-bounce injection with its pass token, the app's own CSS hook for branding, and the silent-failure check to run after every app upgrade. Both: EN + ZH twins, TERMINALS/BANNERS entries appended via append_generator_entries.py (additive, deletions 0, node --check OK), og/banner generated and measured PASS (og rows=1 overflow=0 missingHash=0; banner 8 rows, delta 2, overflow 0).
This commit is contained in:
1 parent
67b844e3a0
commit
378d55c3c8
10 files changed
+732
No files matched your search
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "上游被删了,然后带着一个重写版回来"
|
||||
description: "我线上跑的阅读服务,依赖的镜像官方仓库已经 404。几个月后上游回来了——233 个提交、换了语言、零 release。怎么分辨「项目死了」和「项目在重写」。"
|
||||
pubDate: 2026-10-06
|
||||
category: devops
|
||||
tags: ["docker", "self-hosting", "upstream", "maintenance", "registry"]
|
||||
ogImage: "/og/the-upstream-was-deleted-then-came-back-rewritten.png"
|
||||
banner: "/banners/the-upstream-was-deleted-then-came-back-rewritten.png"
|
||||
draft: false
|
||||
---
|
||||
|
||||
`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` 直接失败 |
|
||||
| 还在维护的 fork | `changshengyu/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 / Fork | 11,036 / 5,471 |
|
||||
| 最后推送 | 2026-10-02 |
|
||||
| 提交数 | **233** —— 而最早的一条是 `chore: 初始化仓库`,日期 **2026-09-16** |
|
||||
| release / tag | **0 / 0** |
|
||||
| Docker Hub | 仍然 **404** |
|
||||
|
||||
2021 年那段历史没了。`created_at` 还显示 2021-08-13,只是因为 GitHub 在仓库被重新填充后保留了这个字段,但 git 历史是从 2026-09-16 一条空仓库提交开始的。被删除的痕迹还留在旧分支上:去取 `master` 的 README,全文只有两个字——`deleted`。
|
||||
|
||||
而这 233 个提交里的代码,不是我原来那个代码库的延续。它是另一个应用,只是恰好沿用了同一个名字:
|
||||
|
||||
| | 我现在跑的(3.x) | 上游现在的样子 |
|
||||
|---|---|---|
|
||||
| 运行时 | Kotlin,Spring Boot + Vert.x | TypeScript,Node |
|
||||
| 端口 | 8080 | 5888 |
|
||||
| 数据 | `/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` 加一次重启。
|
||||
|
||||
让我决定慢下来的,是项目自己的进度记录。不要拿厂商的营销页当状态报告,去看它自己写的东西,你会看到作者在列**没做完**的事情:
|
||||
|
||||
- 旧数据**自动迁移和首次接入向导都还没实现**;
|
||||
- **镜像拉取与真实容器升级演练从未执行过**,因为构建机上没有跑 Docker 引擎。
|
||||
|
||||
这是一份诚实得罕见的文档,也是整个仓库里最有用的一个文件。重写的作者在告诉我:升级路径写好了,但没人走过。如果连他们都没走过,我当然不会在工作日下午、在我读者正在使用的实例上、在没有数据副本的情况下走去试。
|
||||
|
||||
## 我现在会把一个依赖交给线上服务之前做的事
|
||||
|
||||
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`,而且它说的是实话。然后有人重新开始了,换了门语言,而诚实的答案在名字不变的情况下变了。
|
||||
|
||||
## 结果
|
||||
|
||||
书库在跑,跑在我钉住的那个构建上,读者完全无感——这本来就是目标,而且它一直成立。变的是:「我们被困在一个没人管的镜像上」从一句含糊的担忧,变成了一个有方案的具体问题:
|
||||
|
||||
- 数据目录已复制,新镜像自带的 `backup` / `verify` / `restore` 命令先**对着副本**跑,确认没问题才碰线上卷;
|
||||
- TTS 协议要拿我现有的 webhook 实测,因为一旦对不上,我丢掉的就是朗读这个功能;
|
||||
- 旧镜像标签留着做回滚,而且无论走哪条路,源码现在都在我自己的服务器上;
|
||||
- 前门不在讨论范围内,因为它在另一个容器里,换镜像够不着它。
|
||||
|
||||
真正有用的教训不是「要钉版本」——那个我早就做了。而是:**「这个项目还活着吗」有三个答案,而它们可以互相矛盾**。仓库可以活着,而镜像已被删除;README 可以写着 `deleted`,而重写版已经开工四周;版本号可以纹丝不动,而运行时、端口、数据库和配置模型在底下全换了一遍。这三件事要分别去查,否则你迟早会为一个「已经不再以你测试过的形态存在」的应用规划升级。
|
||||
|
||||
---
|
||||
|
||||
**你在替别人跑自托管服务,但不确定底下的东西还有没有人管?** 这正是我在做的审计:依赖与镜像健康度、升级可行性,以及随之而来的迁移方案。
|
||||
|
||||
[WhatsApp +60 12-797 2969](https://wa.me/60127972969) ·
|
||||
[[email protected]](mailto:[email protected]?subject=Self-hosted%20dependency%20audit) ·
|
||||
[hoelee.com](https://hoelee.com)
|
||||
Reference in new issue
Block a user