保留 Folo 完整 Web 阅读体验,在本地离线运行 RSS 订阅与阅读。
Local Folo 是从 Folo 单体仓库中裁剪出的 本地 RSS 阅读器。它沿用官方桌面端 Web 渲染层(apps/desktop/layer/renderer)作为 UI 基线,并内置与 api.folo.is 兼容的 Local Folo API,使订阅、时间线、列表、设置等核心能力在无需账号与云端的情况下即可运行。
| 维度 | 取值 |
|---|---|
| 应用版本 | 1.7.0(见 apps/desktop/package.json) |
| 包管理器 | pnpm 10.17.0 |
| 运行时 | Node.js ≥ 20(推荐与 pnpm 版本匹配) |
| UI 框架 | React 19 + Vite 7 + Tailwind CSS 3 |
| 上游项目 | RSSNext/Folo |
- 本地 RSS 阅读:订阅 HTTP(S) / Atom / RSS 源,在 Folo 原生时间线 UI 中阅读,数据保存在本机。
- RSSHub 路由消费:通过
rsshub://协议订阅 RSSHub 路由,需自行部署或指向可用的 RSSHub 实例(默认http://localhost:1200)。 - 对外暴露 Entries API:通过
folo-entries-gateway在8787端口提供与https://api.folo.is/entries兼容的 HTTP 接口,供外部客户端拉取条目。 - 定时刷新:内置 cron 调度,按 15 / 30 / 60 / 120 / 360 分钟间隔自动刷新全部订阅。
flowchart TB
subgraph Browser[浏览器 / PWA]
UI[Folo Web Renderer<br/>React 19 + Jotai/Zustand]
end
subgraph Desktop[apps/desktop]
Vite[Vite Dev Server<br/>开发模式]
Prod[production.ts<br/>生产静态服务]
API[local-folo-api<br/>Local Folo API Handler]
end
subgraph Gateway[folo-entries-gateway :8787]
Proxy[proxy 模式 → 转发主服务]
Synthetic[synthetic 模式 → 内置样例]
end
subgraph External[外部依赖]
RSSHub[RSSHub 实例<br/>rsshub:// 路由解析]
Curl[curl<br/>Feed 抓取]
end
subgraph Storage[本地持久化]
Data[".local-rss/folo.db<br/>订阅 · 条目 · 设置"]
end
UI -->|同源 API| Vite
UI -->|同源 API| Prod
Vite --> API
Prod --> API
API --> Data
API --> Curl
API --> RSSHub
Gateway -->|FOLO_UPSTREAM_URL| Prod
Client[外部 HTTP 客户端] --> Gateway
请求路径说明
| 模式 | UI 服务 | API 挂载点 |
|---|---|---|
开发 (pnpm dev:web) |
Vite :2240 |
Vite 插件 localFoloApiPlugin 中间件 |
生产 (pnpm start) |
Node HTTP :2240 |
server/production.ts 内嵌同一 Handler |
开发与生产共用 createLocalFoloRequestHandler,避免 build:web 后仍依赖 Vite configureServer。
| 路径 | 说明 |
|---|---|
apps/desktop/layer/renderer |
Folo 桌面 Web UI(时间线、订阅、设置、阅读器等) |
apps/desktop/plugins/vite/local-folo-api.ts |
Local Folo API 核心实现 |
apps/desktop/server/production.ts |
生产环境静态资源 + API 服务 |
packages/internal/* |
共享包:components、store、models、database 等 |
packages/configs |
跨包 TypeScript / ESLint 配置 |
packages/internal/folo-entries-gateway |
Entries HTTP 网关 |
locales/ |
多语言资源(en、zh-CN、zh-TW、ja、fr-FR 等) |
apps/desktop/changelog/ |
升级日志(含 next.md,Vite 构建时注入 CHANGELOG_CONTENT) |
patches/ |
pnpm 依赖补丁 |
以下模块未包含在本仓库,以减小体积并聚焦本地 Web 场景:
- 移动端(mobile)、SSR、Landing 页
- CLI、OTA 更新、Electron 打包与发布流水线
- E2E 测试与 CI 发布脚本
| 层级 | 技术 |
|---|---|
| 构建 | Vite 7、TypeScript 5.9、pnpm workspace |
| 前端 | React 19、Tailwind CSS、Jotai、Zustand、React Query |
| 本地 API | Node.js 原生 HTTP、node-cron、curl 抓取 Feed |
| 数据 | PostgreSQL(DATABASE_URL)或 SQLite 回退(.local-rss/folo.db) |
| 网关 | @follow/folo-entries-gateway(独立 HTTP 服务) |
| PWA | vite-plugin-pwa(Service Worker 离线缓存) |
- Node.js 与 pnpm@10.17.0(见根目录
package.json的packageManager字段) curl必须在PATH中;Feed 抓取与 RSSHub 连通性测试均通过curl完成- (可选)RSSHub 实例:解析
rsshub://路由时需要,默认基址为http://localhost:1200
启用 corepack(推荐):
corepack enable
corepack prepare pnpm@10.17.0 --activatepnpm installpnpm dev:web浏览器访问 **http://localhost:2240**。Local RSS 模式默认开启(apps/desktop/.env 中 VITE_LOCAL_RSS_MODE=1),API 与 UI 同源,无需额外代理。
pnpm build:web # 输出到 apps/desktop/out/web
pnpm start # 启动 production.ts,默认 :2240或一步完成:
pnpm prod # build:web && start:all当外部客户端需要通过 8787 端口访问 /entries 时:
# 开发
pnpm dev:all
# 生产
pnpm start:all| 服务 | 默认端口 | 说明 |
|---|---|---|
| Folo 主服务 | 2240 |
UI + Local Folo API |
| Entries Gateway | 8787 |
兼容 api.folo.is/entries |
自带 PostgreSQL 16 + LocalFolo,数据库 schema 在应用首次启动时自动创建。
前置:Docker Engine + Compose 插件。
cp .env.example .env # 按需改 PUBLIC_ORIGIN、POSTGRES_PASSWORD
./deploy/docker-deploy.sh # 或 pnpm run deploy:docker默认访问 **http://localhost:2240**。常用子命令:
| 命令 | 说明 |
|---|---|
./deploy/docker-deploy.sh --no-build |
跳过镜像构建 |
./deploy/docker-deploy.sh --logs |
查看日志 |
./deploy/docker-deploy.sh --down |
停止(保留 pgdata) |
./deploy/docker-deploy.sh --reset-db |
清空 Postgres 卷 |
Docker 相关环境变量见根目录 .env.example(PUBLIC_ORIGIN、HOST_PUBLISH、POSTGRES_*)。容器内 RSSHub 默认 http://host.docker.internal:1200。
主配置文件:
- 本机开发:
apps/desktop/.env(见apps/desktop/.env.example) - Docker 部署:项目根目录
.env(见.env.example)
| 变量 | 默认值 | 说明 |
|---|---|---|
VITE_DEV_PORT |
2240 |
开发服务器端口 |
VITE_WEB_URL |
http://localhost:2240 |
Web 应用基址 |
VITE_API_URL |
http://localhost:2240 |
API 基址;Local RSS 模式须指向 localhost |
VITE_LOCAL_RSS_MODE |
1 |
启用本地 RSS 模式(关闭云端请求) |
VITE_LOCAL_RSSHUB_BASE_URL |
http://localhost:1200 |
rsshub:// 路由解析的默认 RSSHub 基址 |
VITE_BUILD_TYPE |
production |
构建类型标识 |
VITE_INBOXES_EMAIL |
@local.folo |
本地模式占位邮箱 |
PORT |
同 VITE_DEV_PORT |
生产服务器端口覆盖 |
Local RSS 模式下,
VITE_API_URL的 hostname 必须为localhost或127.0.0.1,否则应用启动时会抛出校验错误(见apps/desktop/layer/renderer/src/lib/local-mode.ts)。
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
8787 |
网关监听端口 |
FOLO_UPSTREAM_URL |
http://127.0.0.1:2240 |
proxy 模式下转发目标 |
FOLO_ENTRIES_GATEWAY_MODE |
proxy |
proxy 转发主服务;synthetic 使用内置样例数据 |
RSSHub 基址与 Cron 间隔可在 设置 → 集成 中图形化配置,运行时写入 .local-rss/folo.db 的 meta 表,优先级高于 .env 中的 VITE_LOCAL_RSSHUB_BASE_URL。
核心实现:apps/desktop/plugins/vite/local-folo-api.ts
| 路径 | 方法 | 功能 |
|---|---|---|
/subscriptions |
GET / POST / PATCH / DELETE | 订阅管理 |
/feeds |
GET | Feed 元数据 |
/feeds/refresh |
POST | 刷新指定 Feed |
/entries |
GET / POST | 条目查询(与官方 API 兼容) |
/entries/preview |
POST | 订阅前预览 |
/entries/readability |
POST | 正文提取 |
/lists |
GET / POST / PATCH / DELETE | 本地列表管理 |
/categories |
GET / PATCH / DELETE | 分类管理 |
/settings |
GET / PATCH | 设置读写 |
/discover |
GET / POST | 发现(本地模式返回空结果) |
/better-auth/* |
* | 认证占位(本地单用户) |
未实现或无需云端的能力(通知、收件箱、钱包、AI、计费等)返回 安全空响应(createSafeFallback),避免 UI 报错。
| 路径 | 方法 | 功能 |
|---|---|---|
/__local/config |
GET / POST | 读取/保存 RSSHub 基址、Cron 配置 |
/__local/refresh-all |
GET / POST | 手动刷新全部订阅 |
/__local/test-rsshub |
POST | 测试 RSSHub 连通性(curl 探针) |
/__local/rss |
GET | 导出本地 RSS(调试) |
对应 UI 组件:apps/desktop/layer/renderer/src/modules/settings/LocalRsshubInstancePanel.tsx、LocalCronRefreshPanel.tsx。
Local Folo 服务端数据默认写入 PostgreSQL(推荐与 LinkLoom 共用实例,独立 folo 库)。未配置 DATABASE_URL 时回退到 SQLite 文件。
在 apps/desktop/.env 中配置:
DATABASE_URL=postgres://linkloom:linkloom@localhost:5432/folo首次使用前在 Postgres 实例上创建库(LinkLoom Docker 示例):
docker exec linkloom-postgres-1 psql -U linkloom -d linkloom \
-c "CREATE DATABASE folo OWNER linkloom;"应用启动时 local-folo-api 会自动执行 schema 迁移(schema_migrations + 全部表)。
从 SQLite 迁移数据(若已有 .local-rss/folo.db):
DATABASE_URL=postgres://linkloom:linkloom@localhost:5432/folo \
node apps/desktop/scripts/migrate-folo-sqlite-to-postgres.mjs备份:pg_dump "postgres://linkloom:linkloom@localhost:5432/folo" > folo-backup.sql
架构说明:PostgreSQL 是唯一持久化源(RSS、已读、收藏、未读、设置)。浏览器在 VITE_LOCAL_RSS_MODE=1 下仅保留 Zustand 内存态,不再向 IndexedDB 写入上述数据。
未设置 DATABASE_URL / POSTGRES_* 时,数据写入:
.local-rss/folo.db # SQLite 主库(WAL 模式)
.local-rss/folo.db-wal # SQLite WAL 日志(运行期)
.local-rss/folo.db-shm # SQLite 共享内存(运行期)
该目录已在
.gitignore中排除。备份或迁移时请整体复制.local-rss/。
| 表 | 主键 | 说明 |
|---|---|---|
feeds |
id |
Feed 元数据;url 上有 UNIQUE 索引,等价于旧版 feedIdByUrl |
entries |
id;UNIQUE(feed_id, guid) |
条目;按 (feed_id, published_at_ms) 建索引,无条数上限 |
subscriptions |
id |
用户订阅;按 feed_id / view / category 建索引 |
lists |
id |
用户创建的列表(feed_ids 存 JSON 数组) |
settings |
tab |
每个设置 Tab 一行,payload 为 JSON |
meta |
key |
扁平 KV 配置:localRsshubBaseUrl、localCronRefreshMinutes 等 |
collections |
entry_id |
收藏条目 |
unread |
subscription_id |
各订阅未读计数 |
DDL 与 Drizzle schema 见:
apps/desktop/plugins/vite/local-db/schema.ts(typed Drizzle 定义)apps/desktop/plugins/vite/local-db/schema.sql(外部sqlite3CLI 可读的同步副本)
- 抓取入口
ensureFeedByUrl执行INSERT INTO entries ... ON CONFLICT(feed_id, guid) DO UPDATE:内容会被新数据覆盖,但inserted_at与read状态保留。 - 抓回 0 条不会删除任何已存条目(解决 Twitter 等源间歇空 RSS 覆盖历史的旧 bug)。
- 时间线
/entries直接走 SQL 索引分页,不再触发同步抓取;只在打开"第一页"且某个 feed 的updated_at距今超过 15 分钟时启动一次后台异步刷新(不阻塞响应)。 - 主动刷新仍由内置 cron(
/__local/config中配置)与手动触发的/feeds/refresh、/__local/refresh-all负责。
- PostgreSQL 备份:
pg_dump "$DATABASE_URL" > folo-backup.sql - SQLite 备份:复制整个
.local-rss/目录 - 完全重置(PG):
dropdb/createdb后重启应用自动建表 - 完全重置(SQLite):删除
.local-rss/folo.db*(含-wal/-shm)
启用 VITE_LOCAL_RSS_MODE=1 后,前端会安装 网络守卫(apps/desktop/layer/renderer/src/lib/local-mode.ts):
- 拦截对
api.folo.is、app.folo.is、PostHog、Sentry、Firebase 等远程域名的fetch请求 - 允许
localhost、127.0.0.1及当前页面同源请求 - 确保阅读数据不意外发往云端
本地用户使用固定 ID(createStableId("local-folo-user")),无需注册或登录。
.
├── apps/desktop/ # 桌面 Web 运行时
│ ├── layer/renderer/ # Folo Web UI(页面、模块、组件)
│ ├── plugins/vite/
│ │ └── local-folo-api.ts # Local Folo API 实现
│ ├── server/production.ts # 生产 HTTP 服务
│ ├── changelog/ # 版本更新日志
│ ├── .env / .env.example # 环境配置
│ └── vite.config.ts
├── packages/
│ ├── configs/ # 共享构建配置
│ └── internal/
│ ├── atoms/ # Jotai 原子状态
│ ├── components/ # 共享 UI 组件
│ ├── constants/ # 应用常量
│ ├── database/ # Drizzle ORM
│ ├── hooks/ # 共享 React Hooks
│ ├── models/ # 数据模型
│ ├── shared/ # 跨平台工具与环境变量
│ ├── store/ # Zustand 状态
│ ├── utils/ # 工具函数
│ └── folo-entries-gateway/ # Entries HTTP 网关
├── locales/ # i18n 翻译文件
├── patches/ # pnpm 依赖补丁
├── .local-rss/folo.db # 本地 RSS 数据(运行时生成)
├── package.json # Workspace 根脚本
├── pnpm-workspace.yaml
└── turbo.json
内部包开发约定见 packages/internal/AGENTS.md。
根目录 package.json:
| 命令 | 说明 |
|---|---|
pnpm dev:web |
启动 Vite 开发服务器(:2240) |
pnpm dev:all |
并行启动 dev:web + Entries Gateway |
pnpm build:web |
构建 Web 产物到 apps/desktop/out/web |
pnpm start |
启动生产服务器 |
pnpm start:all |
并行启动 start + Entries Gateway |
pnpm prod |
build:web 后执行 start:all |
pnpm entries:gateway |
单独启动 Entries Gateway |
pnpm typecheck |
TypeScript 类型检查 |
- 确认
curl已安装且在 PATH 中 - 检查 Feed URL 是否可访问;
rsshub://路由需 RSSHub 实例在线 - 在 设置 → 集成 中执行 RSSHub 连通性测试
- 查看终端
[local-folo-api]日志
网关处于 proxy 模式但主服务未启动。先运行 pnpm dev:web 或 pnpm start,确认 2240 端口可达。
确认已执行 pnpm build:web,且 apps/desktop/out/web/index.html 存在。pnpm start 会在缺少构建产物时返回 404 提示。
- 在 设置 → 集成 → 定时刷新 中选择非零间隔
- 确认
localCronPublicBaseUrl可从运行 cron 的机器访问(默认使用浏览器 origin) - 仅开发/生产 Node 进程运行时 cron 有效;纯静态托管无法调度
- UI 与交互设计源自 RSSNext/Folo(Folo Team)
- 本仓库为官方 Folo 的本地裁剪版本,保留完整 Web UI,移除 Electron 打包与云端依赖,专注于离线 Web 阅读场景