OUR CHOICE / DOCS

把注意力,还给
自己选择的内容。

“自选”是一个本地优先的内容订阅与整理工具。今日页只放你主动订阅的来源,发现页则把探索放在一个边界清晰的独立空间。

01
主动订阅不把推荐混进收件箱
02
本地保存选择与记录留在浏览器
03
开放来源RSS、Atom、RSSHub 与链接模式
01

CAPABILITY MAP

能力与边界

当前版本专注于“本机上的可控阅读”,不会把尚未实现的云端能力包装成已有功能。

现已支持

选择、阅读、整理

  • RSS / Atom / 播客预览与订阅
  • 网站公开 Feed 自动发现
  • 可选的 RSSHub + Radar 路由发现
  • 十二个主流中文内容平台的链接模式
  • 频道与具体内容在站内主区域加载
  • 订阅基线、查看记录与准确的新增判定
  • 在浏览器与平台允许时复用外部登录会话
  • 筛选、搜索、稍后看与安静阅读
  • 本地合集与人工精选发现
  • JSON 导入导出、跨标签页同步
明确边界

保持诚实的限制

  • 没有账号和跨设备云同步
  • 不会绕过付费墙或来源访问限制
  • 未配置或无法匹配 RSSHub 时,平台链接不自动拉取更新
  • 来源可通过安全响应头禁止被第三方页面内嵌
  • 发现偏好仅在当前浏览器生效
  • 清除站点数据前需要先导出备份
02

QUICK START

三分钟运行起来

需要 Node.js 22.13.0 或更高版本。克隆仓库后安装依赖并启动开发服务器。

Terminal
git clone https://github.com/BillShiyaoZhang/our-choice.git
cd our-choice
npm install
npm run dev

浏览器打开 http://localhost:3000。文档位于 /docs/

生产模式npm run build
npm run start
Dockerdocker compose up --build
Dev Container

在兼容编辑器中打开仓库,依赖会自动安装、RSSHub sidecar 会一起启动,并转发应用的 3000 端口。

02+

macOS 本地应用与单包安装

Mac 版用原生 Swift/WKWebView 包装同一套网站,并内置 Node 22、Vinext 生产产物和浏览器助手;运行应用不要求预装 Node.js 或 Docker。

开发构建

Terminal
npm run mac:build
open "build/macos/Our Choice.app"

应用只监听 loopback,并在退出时关闭自己启动的服务。它优先使用 3000;端口被 Docker 或 Dev Container 占用时自动尝试 3001...3031,浏览器助手会验证健康端点并自动记住实际地址,不需要用户同步端口。专项开发仍可用 OUR_CHOICE_PORT 显式覆盖。

无证书本机安装测试

Terminal
DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer npm run mac:package-local
open build/macos/Our-Choice-local-unsigned.pkg

产物为 build/macos/Our-Choice-local-unsigned.pkg。它是明确未签名、未公证且只支持当前 Mac 架构的测试包,会安装或覆盖 /Applications/Our Choice.app,不得上传或对外分发。

发行安装程序

Terminal
npm run mac:package

该命令需要完整 Xcode、Apple Developer Program、双架构 Node、Developer ID ApplicationDeveloper ID Installer 身份,以及 App Store Connect API key 或已保存的公证 profile。它把 Safari .appex 嵌入主应用,再签名、公证并生成安装到 /Applications 的 PKG;显式传入 --skip-notarization 时只生成固定名为 Our-Choice-signed-unnotarized.pkg 的本地预检包。

  1. 从 Apple Developer 创建或下载两类 Developer ID 证书,并连同私钥导入构建机钥匙串。
  2. 记录 Team ID,创建公证用 App Store Connect API key 或保存 notarytool profile。
  3. 把证书、密码与公证 key 配置到受保护的 macos-release environment,再手动触发正式 workflow。

正式 workflow 默认使用 Chrome manual 模式,不需要 Chrome Web Store 账号;商店公开发布后才可选择 store 模式。

浏览器助手通过安装版 Mac 应用每次启动重新生成的短期会话保护本地持久队列,自动发现端口后把内容交给 Mac 应用;Docker / 网页模式仍可使用手工配对。这样避免 Chrome、Safari 与 WKWebView 各自隔离的 localStorage 造成错投。完整产品数据仍使用设置中的 JSON 导入导出显式迁移。架构、签名和双架构验收见 docs/spec/macos-local-app.md

USER GUIDE
03

A CALM READING LOOP

从订阅到读完

1

添加你信任的来源

直接粘贴主页、分享文案、RSS 或网站地址,系统会自动识别平台;一个页面有多种订阅范围时,可以同时勾选多项。

2

在“今日”集中处理

按文章、视频、播客或新增筛选;点击后在主显示区打开,并用固定返回按钮回到自选。

3

把值得保留的内容放进合集

创建自己的主题合集,或从发现页明确订阅一个人工精选合集。

4

定期导出本地数据

在设置中下载 JSON 备份,换浏览器或清理数据前可随时恢复。

04

添加与管理订阅

添加来源

  1. 点击右上角“添加订阅”。
  2. 在唯一的“链接或 RSS”输入中粘贴公开 URL 或包含 URL 的分享文案,系统会自动识别平台。
  3. 界面会列出当前支持的中文平台;微信公众号 ID/Biz 使用单独的高级设置。
  4. 如果出现多个候选,可同时勾选投稿、动态、图文、回答、专栏等订阅范围,再预览组合内容。
  5. 检查来源名称、模式和最近内容预览。
  6. 选择是否保留最近 3 条作为频道历史,然后确认。

管理来源

在“订阅”中点击来源名称,会进入该来源的内容详情;视频、文章和播客分区展示,各分区都按发布时间从新到旧排列。列表中还可打开来源主页、单独刷新、暂停、设置或移除来源。

来源设置允许修改名称与说明;由同一链接发现多个范围时,还可重新识别并增加或移除范围。Bilibili 来源还能选择视频默认在站内查看,或在新窗口打开 Bilibili 网页以使用平台登录与大会员播放能力。

Bilibili、微信公众号、知乎、小红书、抖音、快手、微博、小宇宙、今日头条、百家号、豆瓣和喜马拉雅由同一个输入自动识别。部署者配置 RSSHub 后,自选会列出 Radar 候选供多选;同一链接的全部已选范围始终视为一个来源,内容也合并在该来源下。喜马拉雅专辑使用显式映射,微信公众号高级设置可填写优读 ID、Biz/HID 或 Wechat2RSS ID。无规则或转换失败时仍保留链接模式。

无法识别网页?

优先粘贴网站公开的 RSS/Atom 地址。普通网页只有声明了公开 Feed 时才能自动发现。

04+

自选浏览器助手

Chrome、Edge 与 Safari 共用同一套 Manifest V3 源码,可以在用户浏览时收藏当前内容、订阅当前来源,并把 B站关注页中的 UP 主批量送回自选确认。平台登录始终留在原网站,扩展不会读取或保存密码、Cookie 或访问令牌。

安装与自动连接

  1. chrome://extensionsedge://extensions 启用开发者模式。
  2. 选择“加载已解压的扩展程序”:已安装 Mac 应用时指向 /Applications/Our Choice.app/Contents/Resources/browser-extension/chrome;源码开发时先运行 npm run extensions:build,再指向 build/browser-extensions/chrome/
  3. Safari 版本随 Mac 应用安装;正式签名版到“Safari → 设置 → 扩展”启用并授权。本机 unsigned 版还要先在“高级”显示开发者功能,再从“开发”菜单允许未签名扩展;退出 Safari 后需重新允许。
  4. 打开安装版 Mac 应用;扩展会自动发现 3000...3031 中的实际端口并建立短期会话。
  5. 点击“检查待处理内容”验证连接;无需复制配对码。Docker / 网页模式才使用高级设置中的手工配对。

收藏与订阅

“稍后看”直接进入系统合集;“收藏到合集”会在自选中要求选择目标合集。扩展只保存标题、规范 URL、摘要、封面和用户主动选中的文字,不保存整页快照。

“订阅这个来源”优先使用页面声明的 RSS/Atom,否则交给现有来源预览接口识别网站、作者主页、RSSHub 或链接模式。首次导入建立当前基线,历史内容不会被标成新增。

B站关注导入

  1. 打开个人空间中的“全部关注”页,在扩展中选择“自动扫描全部关注”。
  2. 扩展逐页读取已经渲染的公开 MID、昵称和头像;页面内会显示进度,也可以随时取消并保留已扫描结果。
  3. 到达最后一页后自动发送,再从 UP 主主页可发现的 9 类固定来源中多选本次范围,随后预览、去重并确认;同一组选择会应用到本次导入的所有 UP 主。仍可使用“开始新一轮 / 扫描本页”手动扫描。

可选范围包括:UP 主图文、投币视频、动态、粉丝、关注用户、点赞视频、用户追番列表、默认收藏夹和投稿;默认只选择投稿。普通添加订阅、浏览器助手单个来源、批量导入及来源设置都使用同一套多选项。确认批量导入后,任务会在独立的右上角进度窗口中继续,可收起、暂停、继续或取消;最多六个账号并行处理,并省去每个 B站账号重复发现内容范围的请求。粉丝与关注用户依赖登录 UID 及自建 RSSHub 的 B站 Cookie 配置;投币、点赞、追番与收藏夹也受目标用户公开设置影响。

扩展处理的数据、保存期限和权限用途见独立的自选助手隐私政策

05

阅读与整理

今日

有限的内容收件箱

组合内容类型与新增筛选。点击内容会记录查看时间并在站内打开;全局搜索会检查标题、摘要与来源。

新增

从订阅时刻建立基线

刚加入来源时看到的条目属于频道历史,不会全部变成新增;后续首次发现且尚未查看的内容才计入新增。

稍后

延后决定,不让页面变乱

把当前不处理的条目加入稍后,再从合集区域集中回看。

合集

建立长期主题

创建本地合集并收录内容,也可以明确订阅发现页展示的精选合集。

发现

探索,但不污染订阅

“附近一步”“跨出一步”“随机看看”提供不同探索距离,并展示推荐原因。

06

数据与隐私

订阅、查看时间、新增基线、合集和发现偏好保存在当前浏览器的 localStorage,键名为 our-choice:state:v1;旧版数据会自动迁移到当前数据模型。

  • 备份:设置 → 数据与隐私 → 导出,下载 JSON 文件。
  • 恢复:在同一区域导入此前导出的 JSON;导入前请确认文件来源。
  • 跨标签页:同一站点下的其他标签页会收到存储变化。
  • 平台会话:内嵌页面只有在浏览器与平台允许第三方 Cookie 或 Storage Access 时才能沿用登录状态;自选只记录平台地址和最近打开时间,不接触密码或 Cookie 内容。
  • 助手连接:安装版使用应用进程短期会话并在重启后自动续连;our-choice:assistant:v1 只为 Docker / 网页模式保存本地配对码,不进入应用状态或 JSON 备份;扩展待处理队列保存在扩展自己的本地存储中。
  • 清理提醒:清除浏览器站点数据会删除本地内容,操作前请先导出。
07

常见问题

订阅刷新失败会丢掉之前的内容吗?

不会。来源请求或解析失败时会保留已有内容,并给出可重试提示。

为什么中文内容平台没有内容预览?

先确认已经选择具体订阅范围。未配置 RSSHub、Radar 没有适用规则、源站触发反爬,或该路由需要部署者配置 Cookie/Token 时,会安全降级为公开链接并显示原因;网站自己的 Feed 和可用的 RSSHub 路由仍可正常预览。

为什么分享短链或末尾带斜杠的地址识别失败?

自选会规范化末尾斜杠,并只跟随 B站、小红书、抖音等明确允许域名的有限次公开短链跳转;其他跳转继续按 SSRF 安全规则拒绝。分享文案中只应包含一个要订阅的公开 URL。

为什么有些页面无法在主显示区打开?

部分平台通过 X-Frame-Options 或 CSP 禁止任何第三方站点内嵌。此时可在当前页打开来源,使用浏览器返回后,查看记录仍会保留。

为什么我在内嵌的 B站页面登录后仍显示未登录?

浏览器可能隔离或阻止了 iframe 的第三方 Cookie。自选已经为支持 Storage Access API 的来源开放所需权限,但该权限必须由 B站页面自己请求,父页面无法代为调用。请使用查看器中的“在当前页打开并登录 B站”进入第一方登录环境;浏览器返回后,本地阅读记录仍会保留。

自选会保存我的平台密码吗?

不会。登录表单、Cookie 与会话都由来源平台和浏览器处理;自选只保存非敏感的平台地址与最近打开时间。

换一台设备后为什么没有数据?

当前没有账号或云同步。请在原设备导出 JSON,再在新设备导入。

为什么本地网络地址不能订阅?

来源预览接口会拒绝 localhost、私网 IP、非标准端口和危险重定向,防止服务端请求伪造(SSRF)。

DEVELOPER GUIDE
08

BUILD WITH INTENT

开发环境

项目采用 React 19、Next.js 16 接口约定与 vinext 构建,使用 TypeScript。没有独立数据库,产品状态由浏览器本地存储承载。

命令用途
npm run dev同步文档并启动开发服务器
npm run build同步文档并生成生产构建
npm test运行生产构建与 Node 测试
npm run lint执行 ESLint 静态检查
npm run docs:syncdocs/ 复制到 public/docs/
npm run extensions:build从共用源码生成 Chrome 与 Safari WebExtension 资源
npm run extensions:package:chrome生成并复验 Chrome Web Store 版本化上传 ZIP
npm run mac:build构建当前架构的本地 .app
npm run mac:package-local生成仅供当前 Mac 测试的未签名 PKG
npm run mac:verify-package-local从最终 Payload 独立复验本机 PKG
npm run mac:package嵌入 Safari 扩展并生成签名、公证用 PKG
09

项目结构

our-choice/
├── app/
│   ├── api/source-preview/route.ts  # 来源获取、发现与解析
│   ├── lib/model.ts                 # 产品类型和种子数据
│   ├── our-choice-app.tsx           # 客户端状态与界面
│   └── globals.css                  # 产品视觉系统
├── docs/
│   ├── assets/                      # 文档样式与渐进增强脚本
│   ├── spec/                        # 文档驱动的功能规格
│   └── index.html                   # 文档站入口
├── browser-extension/               # Chrome / Safari 共用 WebExtension 源码
├── macos/                            # Swift 外壳与本地 Node 代理
├── scripts/                          # 文档、扩展、Mac App 与 PKG 构建
├── tests/                            # 行为和渲染测试
└── .github/workflows/               # GitHub Pages 发布

public/docs/ 是构建前生成的副本,不应直接编辑或提交。

10

架构与数据流

CLIENTReact 应用视图、交互、状态
读取 / 写入
DEVICElocalStorageour-choice:state:v1
POST 预览
SERVER ROUTE来源预览校验、抓取、解析
公开 Feed / 内部配置
UPSTREAMRSS / Atom / RSSHub最多 1 MB

客户端拥有产品状态与来源基线;服务端路由只在预览和刷新时请求来源,不持久化用户订阅。可选浏览器助手在安装版中使用短期原生会话、在 Docker / 网页模式中使用手工配对,把本地扩展队列送入客户端确认;网页收藏直接写入本地合集,来源候选仍必须经过预览接口。配置 RSSHub 后,服务端通过 /api/radar/rules/{domain} 读取同源 Radar 规则并请求匹配的 Feed;实例地址和访问密钥不进入浏览器数据。外部频道和内容通过受限 iframe 在主区域加载,来源平台的会话仍由浏览器管理。响应设置 Cache-Control: no-store

11

来源预览 API

POST/api/source-preview
Request
{
  "url": "https://example.com/feed.xml",
  "limit": 12
}

成功响应

mode: "live" 返回规范化的来源、条目与抓取时间。由 RSSHub 转换的来源还会返回 provider: "rsshub"、原始 refreshUrl 与不含实例地址或密钥的 rsshubRoute。未配置、无规则或转换失败的中文内容平台返回 mode: "link-only"、平台标识、原始链接、空条目列表和明确警告;Bilibili 保留兼容的 BILIBILI_LINK_ONLY 警告。

失败响应

错误码含义可重试
INVALID_URL地址、协议、端口或网络范围不允许
UNSUPPORTED_FORMAT不是可识别的 Feed,且网页未声明 Feed
UPSTREAM_TIMEOUT来源在 7 秒内没有响应
UPSTREAM_HTTP连接、状态码或重定向失败视情况
FEED_TOO_LARGE响应超过 1 MB
PARSE_FAILEDXML 声明或内容无法安全解析

RSSHub 配置

Docker Compose 默认连接同编排中的独立 RSSHub 服务。直接运行应用时设置 RSSHUB_BASE_URL;实例启用访问控制时再设置仅服务端可见的 RSSHUB_ACCESS_KEY。自选不会默认使用公共实例,也不会执行远程 Radar JavaScript。

12

测试与质量

仓库遵循文档驱动开发和 Red–Green–Refactor:先在 docs/spec/ 记录可观察行为与边界,再添加会因缺失功能而失败的测试,最后实现并重构。

Quality checks
node --test tests/docs-site.test.mjs
npm run lint
npm test

新增测试必须确定、隔离且不依赖真实上游网络。API 测试应使用注入或模拟响应覆盖成功、超时、格式错误与 SSRF 防护。

13

文档发布

文档由 GitHub Pages 托管。推送到 main 且改动涉及 docs/** 或工作流时,GitHub Actions 会直接发布 docs/

  1. 在仓库 Settings → Pages 中将 Source 设为 GitHub Actions
  2. 更新 docs/;如行为发生变化,同步更新对应规格和测试。
  3. 提交并推送,等待 Deploy documentation to GitHub Pages 完成。
  4. 访问 billshiyaozhang.github.io/our-choice/ 验证。

文档站没有外部运行时依赖;.nojekyll 确保文件按原样发布。本地应用则由同步脚本在 /docs/ 提供同一份内容。