Metadata-Version: 2.5
Name: farewell-rss
Version: 0.1.6
Summary: 我到现在终于没有见——大约 RSS 的确死了。
Project-URL: Repository, https://github.com/xuangeyouneihan/FarewellRSS
Author: 轩哥有内涵
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: aiosqlite>=0.22.1
Requires-Dist: bcrypt>=5.0.0
Requires-Dist: fastapi>=0.139.0
Requires-Dist: feedparser>=6.0.12
Requires-Dist: python-multipart>=0.0.32
Requires-Dist: sqlalchemy>=2.0.51
Requires-Dist: uvicorn[standard]>=0.51.0
Requires-Dist: winuvloop>=0.2.4
Description-Content-Type: text/markdown

# 告别 RSS

> 简体中文 | [English](i18n/docs-en/README.md)

> 鲁镇的信息集散地的格局，是和别处不同的：街头立着一块曲尺形的大屏，屏上滚动着各路消息，屏前设着几把椅子，供人歇脚。做工的人，傍午傍晚散了工，往往花几分钟，刷一刷屏上那些热热闹闹的推送，——这是前些年的事了，如今推送算法越发精明，连刷都不用刷，消息直接送到眼前，——或立或坐，随意看了休息；倘肯多花点心思，便可以点开“兴趣标签”，筛出些合胃口的碎片来，若愿意折腾到十几步开外，钻进那“深度阅读”的犄角旮旯，倒也能翻出几篇长文，但这些顾客，多是短衣帮，大抵没有这般闲情。只有穿长衫的，才踱进屏风后面的雅间里，慢条斯理地调教自己的订阅源。
>
> 我从十七岁起，便在镇口的“咸亨信息港”里当差，掌柜说我脑筋迟钝，怕伺候不了那些讲究的长衫主顾，便让我在外头管管热门的滚动栏。外头的短衣主顾，虽然随和，但被算法喂得嘴刁了，常常抱怨推送的尽是些重复嚼过的馍，要看新鲜事还得自己扒拉。他们往往要亲眼瞧着信息从源头吐出来，验过时间戳，又亲看他没被篡改，才肯放心：在这般严苛的监督下，掺水造假也难得很。所以过了几天，掌柜又嫌我做得不熨帖。幸而荐头的面子大，辞退不得，便改为专管调订阅的一种无聊职务了。
>
> 我从此便整日守着那板块，清冷得很。虽说没什么过错，但总觉得乏味，像对着一面空墙。掌柜是张冷脸，顾客也少有热乎气，教人活泼不得；只有那 RSS 到店时，才能生出些趣事，所以至今还记着。
>
> RSS 是那些站着看屏却偏要穿长衫的主顾里，最扎眼的一个。他身形庞大，青白的面皮上总带着几道抓痕，一部花白的胡须乱蓬蓬的；那件长衫又脏又破，仿佛十年没浆洗过。他对人说话，满口“源”“聚合”“元数据”之类，教人半懂不懂的。RSS 一到店，所有看屏的人都望着他笑，有人叫道：“RSS，你脸上又添新抓痕了！”他不理会，只对柜里说：“调两条订阅，要一碟热门标签。”便排出九个点击。他们又故意嚷道：“你一定又偷人家的内容了！”RSS 睁大眼说：“你怎么凭空污人清白……”“什么清白？我前天亲眼见你扒了何家的文章，人家追着你打。”RSS 便涨红了脸，青筋暴起，争辩道：“聚合不能算偷……聚合！……信息流通的事，能算偷么？”接着便是些难懂的话，什么“开放协议”，什么“去中心化”之类，引得众人哄笑起来：店内外充满了快活的空气。
>
> 听人背地里说，RSS 早年也风光过，在“博客黄金时代”颇受敬重，但终归没跟上浪潮，又不会迎合人；于是愈过愈穷，弄到快要没人用了。幸而写得一手好代码，便替人搭搭阅读器，换几口饭。可惜他偏有桩怪癖——好为人师，又懒于迁就。每每帮人搭好阅读架子没几天，便连人带那些订阅源一齐消失得无影无踪。如是几次，请他的人也绝迹了。RSS 没法子，便免不了做些偷偷聚合的勾当。但在我们店里，他品行倒比别人强，从不拖欠——虽然偶尔赊账，记在粉板上，但不出一月，定然还清，名字便被拭去。
>
> RSS 调好两条源，泛青的脸色渐渐复原，旁人又问道：“RSS，你当真认得‘真相’么？”他斜睨着问话的人，显出不屑置辩的神气。他们便追着问：“那你怎的连半个爆款都造不出呢？”RSS 立刻显出颓唐模样，脸上蒙了灰，嘴里嘟囔些“信息茧房”“过滤泡”之类，谁也听不懂。这时候，众人又哄笑起来：店内外充满了快活的空气。
>
> 逢着这些时候，我也可以附和着笑，掌柜从不责备。而且掌柜见了 RSS，也总这般打趣他，引大家乐。RSS 自己知道不能和他们谈天，便只好向我说。有一回他问我：“你用过订阅源么？”我懒懒点头。他说：“用过……那我考你一考。资讯的‘讯’字，怎样滤出干净版？”我想，快要被淘汰的东西，也配考我么？便别过脸去。RSS 等了半晌，恳切道：“不会罢？……我教你，记着！这些法子应当记着。将来做掌柜，筛信息用得着。”我暗想掌柜与我隔着十万八千里呢，且掌柜从不用那劳什子；又好笑，又不耐烦，没好气答他：“谁要你教？不就是去掉广告、留下正文么？”RSS 显出极高兴的样子，枯长的手指敲着屏沿，点头说：“对呀对呀！……‘讯’有四样筛法，你可知道？”我更烦了，努着嘴走开。RSS 刚要用指头蘸了茶水在柜上画图，见我毫不热心，便叹口气，显出极惋惜的模样。
>
> 有几回，隔壁孩子听见笑声，也围过来看热闹。RSS 便分他们几条“精选冷知识”，一人一条。孩子读完还不散，眼巴巴望着他的源列表。RSS 慌了，伸开五指罩住屏幕，弯腰说：“不多了，我已经不多了。”直起身又看看自己的源，自个儿摇头说：“不多不多！多乎哉？不多也。”孩子们便在笑声里散了。
>
> RSS 这样地使人快活，可是没有他，别人也便这么过。
>
> 有一天，大约是中秋前两三天，掌柜正慢慢算账，取下粉板，忽而说：“RSS 长久没来了。还欠十九个点击呢！”我这才觉着他的确久不来了。一个顾客说：“他怎么会来？……他腿被打折了。”掌柜问：“哦？”“他总仍旧是偷。这一回，是自己发昏，竟偷到丁大 V 家的付费墙里去了。他家的内容，偷得的么？”“后来呢？”“后来？先写认罪书，后来是打，打了大半夜，再打折了腿。”“再后来呢？”“再后来打折了腿了。”“打折了又怎样？”“怎样？……谁晓得？许是死了。”掌柜也不再问，低头慢慢拨他的算盘。
>
> 中秋过后，秋风一天凉比一天，看看将近年关；我整日靠供暖口坐着，也须裹上薄袄了。一天下午，店里没半个顾客，我正合眼打盹。忽听得一声：“调一条订阅。”声音极低，却耳熟。抬头看时，不见人影。站起来往外一望，那 RSS 便在屏下对着门槛坐着。他脸上黑而且瘦，不成样子；穿一件破夹袄，盘着两腿，下面垫个棉包，用草绳挂在肩上；见了我，又说：“调一条订阅。”掌柜也伸出头去，一面说：“RSS 么？你还欠十九个点击呢！”RSS 很颓唐地仰面道：“这……下回还清罢。这一回是现付，要干净订阅。”掌柜仍同往常一样，笑着说：“RSS，你又偷东西了！”但他这回却不十分分辩，单说一句：“不要取笑！”“取笑？要是不偷，怎会打断腿？”RSS 低声说：“跌断，跌，跌……”他的眼色，很像恳求掌柜莫再提。此时已聚了几个人，便都和掌柜笑。我调好一条订阅，递过去。他从破衣袋里摸出四个点击，放在我手心，见他满手是泥，原来他便是用这手走来的。不一会，他读完了那条订阅，便又在旁人的说笑声中，坐着用这手慢慢蹭去了。
>
> 自此以后，又长久没见 RSS。到了年关，掌柜取下粉板说：“RSS 还欠十九个点击呢！”到第二年端午，又说：“RSS 还欠十九个点击呢！”到中秋可没再说，再到年关也没见。
>
> 我到现在终于没有见——大约 RSS 的确死了。

## 数据结构

[![](https://img.plantuml.biz/plantuml/svg/hLPRJnD157vlqlymyb90QBnh211UD4OHZUXpC-rEsz7PYxkpGO8aceWfaQZn5ctKn4GJ30c4e8bGa3_JeUN9l-2PcUrbbzr23FlGTCyvyvtBTywPdV0uThblidJgXEFYaeaHPioUCUb3QfKGTtsIJgLJ6JIc7YHMlzHtteZLQdrpCP2bKmM6FG_TzOYBHaTlNHyVHtFf548d4JNG2A8Mbs_3o0S32vi4P1vt9No_Rz47FXdGUWTGPcpNo9MnLsuRQLtHfSGos6mk1A1rrCjXmwGMYFEspRJG88nmOkGm1x61EU7KX5JcPJOwtYk463tZBRkac1GhuRWK2SEq3Lgan8Y8EUKi6Z6Zrls8oFFpSPQEdsVKAolKJXQgcevyFoWl4bw8iyS-BzjkNFq2JIZTa8QOcB9eqGks5H6vj5JcNZHzNEg8448PT6twvbIWuqpN6pr2bi-OKa_P5a5ZgBwpstYxB1QMnVhZ_Tg7vkgt47cNBUxEzcIl2FJcMYAml0AMMbFogP74wn7PEZvQFDyqCQGIAj7LEpScG-gSmp2DrXiOuCJYIKS3TUpHOsw6ACaHdZtRTmiaIeOo4jllW6OrYrbgH2WkCDlpNTAJPY8R8Pxdc1mOiKgyt7sUpphT9PZNssvOFaWiR3IVl6sir_vkL-IVsflcr-M_sujAsmhpDi76D0TETNnwpSK7r2DMsH2mYaqdQLk12_SOtE1IBnecSPumu07o5I_N6Z-s8DcZnkVdlO9B7KvXBnmQOl9mnWrWmcwCcSjWA-LdEV42JpOp3aPnUD9TgIc3Gs-s-7HY9YpKSIgG_SMVeh8YFa9lLJCwu0oin0ombP5dvWpgtRC1UWY6sdJanJRVRjNMnQnw5NuR6xNsnIprfxFwPjvx_dtlqw9E1bAj_tuirZR5jwhALbdqdUw3naTzfxBPmJvjg27EPBj7eThkU3aMkr_sNdpN7iJh9RsNao3rqXv1BW00FPt96JRi6MiOzSipGzfiGBi-6tRTdeB_TIq_Ty1rPvgINP-BkEvgy1ZlSeCbELTV6YEoZIL2NhPcQ5p2CKZh88JpMTIeFHMlAk1VWOYb1V5gfPLgHMnlrVzyRlvwhpmh2-bvC9At1hlG1QRBqKOB5kq1H6rs038mrx0NiwZvfAfXLS_qomOVqsqyW4Jb6TGlj1xIAJrH4jVku8PA0kMQwy89UlrIl2-OauwZ5hD9NjhCjrt0SOK_0L-Owalu7m00)](https://editor.plantuml.com/uml/hLRFQnD15BulxFymvg9MirX_GgcbelQY1r4yXzdiT7TmTsUPcQK6ARHWfS4YHKKmQ4_s95HA4IoD-izaqtZgl-1CPfFTJAOfskQmaE-zdUzxttipi_ECGyhJABIjSmc5VWG134Eo5E7dEFO1focoBTiw3sxtVo3RsUhyV9Vjj3hxpHophNe86GDF6Q9WTlRHWxavyCAs0BW4KW75C49W1Z1EnO8Ihe94f2yHwjK2o08jnWZbDRTXGY6hezYJicQ0IuYGl2orAVO5XBnxa4DD0VP4CewvMYQWQ55R6N7eQr14FBo8aQV17FDGhoJ4yJDTQkgQCfFK3J4B-gjwa2EE8wGYQIB_Z-CmvG6XDOD_USHGJHv14SIX5i9r4ciGnNx0cLuczCSW7WvSB3b-F-QqSQBbYw8ljG4aCXT4fehuAVvFY__TIvP64QHZKwNGMX92h7iZtE98A3QYnZVEk5-6FZ2IqZeojg8U4fPIT68xa6oQkHy6UI6AVHwKt-UDf5nTISDZ1BqdNBYkYAlZct-4RO8GAbQgGJxIn1AH8A5dGoJFFeecSgKkgrESS2mspoHV9siuVXEO9lwO4yIm4V8zxJOuOdaIT54uJbKgwQ7CE4KjsdmlZruFQtkzrVVYURXtCBmUP7pQoME7kmT7xTPmM8xQwxtVMzqts-hwoDvk5EVAKRjfMtsuCbq1rIgeN76SgKgHO5l5q3aUMOeTS47cNovIBYhwgwFqmn4w9NqnYfFfhsdqfQaoA1XSPSS96CG5VtayHoKCad85rnrmUF0osrnNlH6iTqcK48QLXgPGDRn1iusrRFDhkSvXI0gPCbeXIMwSJ59S6aEMqj5bMqLyZ6eaJzBTT41ljLMck-Fx5Fcme5F3dgs_4iO3pVQilTlvzRdt_KFVPvKfIKb1Mdv3STvoGFVZpf-LLkV7IlPjF_lI4Ypz7dLVRtS_DTNsChOjZzZMlFYA4LzSVm40)

## API

后端完全基于 Google Reader API 与前端通信，同时扩展了注册、删号、TAG 分离等端点。详见 [docs/API.md](docs/API.md)。

## 配置

所有配置通过 `FAREWELL_RSS_*` 环境变量完成（数据目录、端口、注册开关、邀请码、抓取调度等），OS 环境变量优先于数据目录下的 `.env`。详见 [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md)。

## 构建

### 本地构建

```sh
cd frontend
pnpm install
pnpm build                               # 构建前端（首次或有改动时）
cd ..
uv sync                                  # 创建环境并安装后端及开发依赖
uv build                                  # 构建 wheel 和源码包
```

使用本地源码运行：

```sh
uv run farewell-rss                      # 一条命令同时起前后端（默认 3000 端口）
```

前端开发热更新用 `cd frontend && pnpm dev`（5173 端口，代理到 3000）。

### Docker 本地构建

```sh
docker build -t farewell-rss .
```

## 运行

### Docker Hub

```sh
docker run -d -p 3000:3000 -v farewell-rss-data:/data \
  xuangeyouneihan/farewell-rss:latest
```

也可以使用本地构建的镜像：

```sh
docker run -d -p 3000:3000 -v farewell-rss-data:/data farewell-rss
```

或 `docker compose up -d`。镜像内默认 `FAREWELL_RSS_DATA_DIR=/data`（挂卷持久化），其余配置用环境变量覆盖（见上文）。打开 `http://localhost:3000`。

### PyPI

使用 uv 安装为长期可用的命令（推荐）：

```sh
uv tool install farewell-rss
farewell-rss
```

也可以安装到当前 Python 环境：

```sh
pip install farewell-rss
farewell-rss
```

## 画饼

### 一期工程

> 基础的 RSS 服务。

- [X] ORM 模型与数据库基础设施
- [X] Feed 抓取引擎（feedparser 集成、ETag/Modified 增量更新）
- [X] Repository 层（Feed、Entry、Enclosure、User、Subscription）
- [X] Service 层（订阅管理、Feed 刷新调度）
- [X] FastAPI 后端（Google Reader API + 用户注册等扩展端点）
- [X] 网页前端（订阅管理、文章阅读）

### 二期工程

> 在我看来，RSS 并不能解决信息茧房的问题，因为用户只能看到已订阅的内容。如果订阅了不喜欢的源，用户也不一定愿意看。所以二期工程引入推荐与发现功能——通过标签匹配与社区热度信号，让用户在未订阅的源中发现可能感兴趣的内容，同时用可调权重的双滑条（个性化程度 α + 热度程度 β）把“看什么”的控制权还给用户。

- [ ] 用户参与贡献的订阅源池（新源自动入库 + Wilson 下限冷启动）
- [ ] 包含未订阅源的推荐页面，以及 Google Reader API 的扩展流 ID
- [ ] 允许用户自己调整的推荐算法（个性化权重 α + 热度权重 β）
- [ ] 网页前端做成 PWA

### 三期工程

> RSS 的一大优势就是去中心化，任何人都能写 RSS 源。但如果 RSS 源的发现机制掌握在中心化的实例里也无济于事，而且单个实例用户有限，订阅源池也不够大。所以三期工程通过 P2P 同步打破实例壁垒，把各实例各自积累的订阅源池与热度数据合并为社区共建的公共资源，同时用 Wilson 下限 + 实例信任权重防御 Sybil 攻击，确保数据不被少数节点污染。

- [ ] 使用 P2P 方式在不同实例间同步订阅源池与热度权重
- [ ] Wilson 下限 + 实例信任权重的 Sybil Attack 防御机制
