DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Rss Reader — DSH Plugin for DeepSeek Harness
← Plugins
R

dsh-rss-reader

Rss Reader

DSH plugin: an in-harness RSS/Atom reader — subscribe to feeds, browse items in a dedicated panel, refresh on demand, read with the agent

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:FYKANG/dsh-rss-reader#53550e1190fde41d8033e23482a1bb13d34fa916
READMECompatibilityVersions

Compatibility and provenance

Rss Reader is published as dsh-rss-reader and currently resolves to version 0.1.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
9/24/2026

Versions

0.1.0stable
9/24/2026

Related plugins

Loading related plugins…

Latest
0.1.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/24/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in

Related plugins

More verified plugins in search-research.

Browser Skill Dsh Plugin@wxg-prc-cpg/browser-skill-dsh-pluginDeepSeek Harness tool plugin that exposes BrowserSkill browser automation (browser_* tools) to the modelWeknora@wxg-prc-cpg/dsh-weknoraWeKnora knowledge retrieval tools for DeepSeek Harness (dsh): semantic search, document reading and RAG/agent answers over your own knowledge bases.Find Plugindsh-find-pluginFind DeepSeek Harness plugins inside the agent — live GitHub dsh-plugin topic search, ranked by stars.Dejadsh-dejadeja-vu memory for DeepSeek Harness: the session history of thirty-three other coding agents, searchable and recalled before each step — a local index, no LLM.

README

dsh-rss-reader

DeepSeek Harness(DSH)插件:内嵌在 DSH 里的 RSS / Atom 阅读器。

订阅源、手动刷新、未读管理、收藏、搜索、Markdown 正文排版、一键翻译、RSSHub 订阅源发现——全部在 DSH 界面内完成,不需要外部阅读器,也不需要单独的服务进程。

面板有两种呈现:中间主区域的三栏阅读界面,以及右侧边栏里的单栏面板(订阅源横条 · 条目列表 · 详情就地展开并可返回列表)。

功能

能力说明
内嵌阅读面板注册为一个 main 面板(左侧栏出现「RSS」入口),点击即在中间主区域打开三栏阅读界面:订阅源列表 · 条目流 · 正文阅读区。
右侧边栏面板同时注册为一个原生右侧边栏 tab 类型(页面类型,kind rss-reader)。入口有两个:左侧栏底部设置旁的 RSS 阅读器 按钮,或右侧边栏自己的 + 面板选择器里的「RSS 阅读器」。该栏很窄,因此这里是单栏形态:订阅源横条 → 条目列表 → 详情就地替换列表。
详情在当前页面右侧边栏里点条目不会跳新标签页:详情直接占用列表的位置,← 返回列表 固定在阅读区顶部(position: sticky),读到长文末尾也不用滚回顶部就能返回;返回时筛选条件、搜索词与滚动位置都还在。悬浮条本身做得很扁、下方不留缝隙,正文从它底边开始,不会从缝里露出来。
记住上次读到哪关掉面板(或切走)再打开,会回到上次那篇、并停在文章内的同一位置。记忆存在宿主上(跟着订阅列表走,不属于某个浏览器),记录三件事:当时筛的源、打开的那条、正文滚动位置。滚动位置由滚动本身触发延迟写入(约 0.4 秒),所以既不会每个滚动帧都发请求,也不会漏掉真正读到的地方。回到列表不会清掉它——那是"下次回来接着读"的凭据。引用失效时各自降级:源被删了就回到「全部」,条目没了就回到列表。
切换源直接回列表详情打开时点另一个订阅源(或「全部」)会立即回到列表,不需要先手动返回;因为换源的意思本就是要看那个源的条目,而当前打开的那条甚至可能不属于新选的源。删除当前选中的源也一样。
探索 RSSHub 订阅源「🧭 探索」打开 RSSHub 社区维护的全部路由表(实测该实例:1589 个站点 / 3288 条路由,其中 3026 条带官方示例):按分类筛、按站点钻取、或直接搜站点名 / 路由名 / 路径。每条路由都带参数说明,并用官方示例预填,所以多数情况下点「添加 → 订阅」即可,不必去读文档。
RSSHub 订阅源发现粘贴任意网页(如 space.bilibili.com/2267573),点「🔍 查找」即可列出可用订阅源:页面自身声明的 + RSSHub 生成的。这让本来没有 RSS 的站点也能订阅。
Markdown 排版正文以 Markdown 渲染:标题、列表、引用、表格、代码块、链接、粗斜体都还原为真实排版,而不是一堆标签。HTML→Markdown 的转换在宿主机完成,浏览器端只负责渲染。
图片留在原位正文里的图片待在文章给它的位置上,不再被统一搬到结尾:段与段之间的图就是段与段之间的一个折叠条,连续的图合成一组。默认折叠(折叠时连 <img> 都不渲染,因此不产生任何网络请求),点开才加载,再点图放大看原图(Esc 关闭)。句中的小图折成一枚可点开的 🖼 小标签,不打断句子。想直接看图,在 设置 → 通用 → 「正文图片直接展开」 里打开即可。
一键翻译顶栏「译」按钮把当前条目翻译成目标语言,复用你在 DSH 里配置的模型(模型与思考强度可在 设置 → RSS 阅读器 → 翻译模型 里选);译文同样以 Markdown 渲染、保留链接与代码,可在原文 / 译文 / 对照之间切换——对照把原文与译文逐段穿插,每段原文下面紧跟它自己的译文。翻译结果(含分段)会缓存,第二次打开不再调用模型。
订阅源管理面板内「+ 添加订阅源」即可添加 / 删除订阅源,支持分组。取消订阅要经过右键菜单 + 二次确认(见下),不会误触。
订阅源自由排序三种排法,都写同一份顺序:在面板里拖动订阅源到目标位置;或在订阅源上右键选「上移 / 下移」;或在 设置 → RSS 阅读器 里用 ↑ / ↓ 按钮精确调整(窄栏、触屏、想一次挪多位时最顺手)。新订阅的源排在末尾,不会插进你已经排好的顺序中间。
订阅源栏可收起右侧边栏那个窄面板顶部的订阅源一行(全部 / 各源 + 未读数)可以折起来,把纵向空间让给条目。折起来后表头的 ▸ 会写清当前筛的是哪个源,点它展开。折叠状态是偏好,重启后仍然折着。
独立 RSS 设置页RSS 自己的设置集中在 设置 → RSS 阅读器 一页里:左侧栏入口开关、正文图片是否直接展开、订阅源栏是否收起、订阅源顺序,以及 RSSHub 实例地址(可直接改成自建或更近的实例,还带「测试连接」),不再混在「通用」里。
入口可隐藏 / 阅读偏好都在 设置 → RSS 阅读器:「左侧栏入口」 关掉左侧栏那个 RSS 图标(点下即生效,不必重启或刷新页面;关掉后仍可从左侧栏底部的「RSS 阅读器」按钮或右侧边栏打开面板,也可以把配置项 showSidebarEntry 设为 false 作为出厂默认);「正文图片直接展开」 决定正文里的图片是折成提示条还是直接显示在原位;「收起订阅源栏」 决定右侧边栏面板顶部那一行订阅源胶囊是显示还是折起来。
智能发现粘贴网站首页也能订阅:插件会读取页面的 <link rel="alternate"> 声明并自动跳到真正的订阅源地址。
手动刷新顶栏「⟳ 刷新」(全部或当前源,强制重新拉取)、单个源旁的「⟳」、以及打开面板时若缓存超过 30 分钟会自动刷新一次。
抓取更早的文章订阅源只是一个窗口:很多博客的 feed 只给最近几篇,再怎么刷新也回不到去年。选中一个源后点顶栏 ⇤ 更早,填入该站的归档页地址与篇数,插件就会从归档页里挑出与已订阅地址同形态的链接、按最新在前逐篇抓回正文。一次最多 50 篇,请求之间会停顿,已订阅过的不会重复抓。
格式支持RSS 2.0、RSS 1.0 / RDF、Atom 1.0,自动识别,无需手动指定。
阅读体验未读计数与筛选、收藏(★)、标题/摘要/作者搜索、一键全部已读、相对时间显示。
增量抓取记录 ETag / Last-Modified,未更新时服务端返回 304,不重复下载正文。
交给 Agent工具 rss_read 让模型按需读取订阅内容;命令 /rss 手动查看;两者与面板共享同一份数据。
可选后台刷新refreshMinutes 开启后按间隔自动刷新。

安装

插件是标准的 DSH 组合包(Cordis 插件),以依赖的形式装进某个 profile,由该 profile 启动时挂载。下面以 web profile 为例($DSH_HOME/profiles/web,$DSH_HOME 默认 ~/.dsh)。

需要 DSH ≥ 0.1.5-rc.1,以及 pnpm 在 PATH 上(dsh plugin 只是 pnpm 的转发器,缺了会报 pnpm not found on PATH)。

dsh plugin --profile web add file:<本仓库的绝对路径>

例如本仓库在 /path/to/dsh-rss-reader:

dsh plugin --profile web add file:/path/to/dsh-rss-reader

用绝对路径:dsh plugin 在 profile 目录里执行 pnpm,相对路径(file:.)会按你执行命令时所在的目录解析。

这条命令会装下包、自动把 dsh-rss-reader 追加进 profile 的 dsh.profile.bundles(因为包内声明了 dsh.bundle.patch,不用手改 cordis.patch.yml),并让浏览器半体随 GUI 一起下发。

装完重启 dsh web(会中断当前会话与 GUI),然后刷新页面。启动后:左侧栏出现 RSS 图标(在中间主区域打开三栏面板),左侧栏底部出现 RSS 阅读器 按钮(在右侧边栏打开单栏面板)。

想确认装好了,看依赖与挂载层:

dsh plugin --profile web why dsh-rss-reader

手动挂载(备选):不想让它进 bundles 时,把包放进 profile 的 node_modules,再在 profile 的 cordis.patch.yml 追加:

- insert:
    - id: rss-reader
      name: 'dsh-rss-reader'

两种方式二选一:同时用会在启动时重复注册 /api/rss-reader 路由而挂载失败(刻意的响亮失败)。

卸载:dsh plugin --profile web remove dsh-rss-reader(会自动从 bundles 撤掉),然后重启。订阅数据在 $DSH_HOME/rss-reader/feeds.json,卸载不会删除。

使用

界面

中间主区域(三栏)

  1. 点击左侧栏 RSS 图标打开阅读面板。
  2. 首次使用点 + 添加订阅源,粘贴订阅源地址(例如 https://www.ithome.com/rss/)或网站首页(例如 https://github.blog/),可选填分组,点「添加并获取」。
  3. 左栏选择订阅源或「全部订阅源」,中间点选条目,右侧阅读正文(Markdown 排版)。
  4. 顶栏可搜索、筛选未读 / 收藏,或「⟳ 刷新」。

右侧边栏(单栏)

  1. 点左侧栏底部的 RSS 阅读器(或右侧边栏 + 里的同名项)打开。
  2. 顶部一排是订阅源 横条,每个胶囊显示源名与未读数;下面就是条目列表。
  3. 点某条 → 列表就地换成该条详情,← 返回列表 固定在顶部随读随在(不跟随滚动条跑掉);返回后筛选与搜索都保持原样,没有跳转、没有新标签页。
  4. 读的时候想换个源?直接点顶部的订阅源胶囊——会立刻回到列表(不会停在上一条的详情里让你再点一次返回)。「全部」同理。
  5. 悬浮条贴住阅读区顶边、下方没有空隙:阅读区本身没有上内边距(有的话会落在悬浮条上方,正文会从缝里透出来),正文的首个元素上外边距也用一条只作用于该面板的规则清掉了——外边距塌陷没法用行内样式表达,这是唯一一处注入的 CSS。
  6. 顶栏按钮在窄栏里收成图标(○ 未读、★ 收藏、⟳ 刷新、+ 添加),鼠标悬停有完整说明。

取消订阅(右键 + 二次确认)

订阅源行上没有删除按钮。取消订阅走两步:

  1. 在订阅源上右键(或点行尾的 ⋯)→ 弹出操作菜单:刷新此源 / 全部标为已读 / 取消订阅…;
  2. 选「取消订阅…」后弹出确认框,写清将要删除的条目数,点 确定取消订阅 才真正执行。

菜单和确认框都可以随时放弃,期间不会发出任何请求。

设置页(设置 → RSS 阅读器)

RSS 的设置不混在「通用」里,而是自己一页(导航里叫 RSS 阅读器,排在 通用 / 模型 / 插件 之后):

「左侧栏入口」

  • 打开(默认):左侧栏显示 RSS 图标,点击在中间主区域打开三栏面板;
  • 关闭:左侧栏不再显示该图标。面板本身没有消失——仍可从左侧栏底部的「RSS 阅读器」按钮或右侧边栏打开。

「正文图片直接展开」

  • 关闭(默认):正文里的图片折成一条可展开的提示(▸ 图片:配图说明 / ▸ 图片 3 张),位置与原文一致,点开才加载;
  • 打开:图片直接显示在原位(仍然 loading="lazy" 按需加载),最接近原文观感。

「收起订阅源栏」

  • 关闭(默认):右侧边栏面板顶部保留订阅源那一行胶囊(全部 / 各订阅源 + 未读数);
  • 打开:那一行折起来,把纵向空间让给条目。折叠后图钉位置改由表头的 ▸ 承担——它会写清当前筛的是哪个源(没筛就是「订阅源 N」),点它即可展开。

只影响右侧边栏那个窄面板。中间主区域的三栏布局里,左栏订阅源列表同时也是重排顺序、右键管理订阅源的地方,折掉它会拿走能力而不只是腾地方,所以那里不跟着折;面板表头上的折叠按钮也只在窄栏出现。

「翻译模型」(仅当这个 profile 真的能翻译时出现)

三个下拉 + 恢复默认:模型服务、模型、思考强度(只有当所选模型声明了可选强度时才出现)。下面一行写清当前生效的是哪个路由(服务 / 模型,或「跟随 DSH 默认模型」)以及它是「自定义」还是「默认」;换模型服务会同时清掉模型选择,避免留下半套路由。

  • 清单来自宿主自己的模型目录,所以每个模型能选的思考强度也会一并列出;
  • 选了当前 profile 没挂载的服务,这里会直接写明「没有挂载,翻译会失败」,而不是等你点了「译」才发现;
  • 恢复默认一次清掉三项,回到插件配置 / 会话默认模型;
  • 长文翻译失败最常见的原因就在这里:推理型模型会把输出额度先花在「思考」上,换一个不做长推理的模型,或把思考强度调到最低,比调大 translateMaxTokens 更有效。

「订阅源顺序」

带编号的订阅源列表,每行 ↑ / ↓ 两个按钮,首行的 ↑ 与末行的 ↓ 是禁用的(到顶到底了)。面板里拖出来的顺序也会实时反映在这里。

「RSSHub 实例」(仅当这个 profile 真的启用了 RSSHub 时出现)

一个地址输入框 + 保存 / 测试连接 / 恢复默认:

  • 框里显示的是当前生效的实例,下面一行写清它是「自定义实例」还是「默认实例」;
  • 保存后立即生效(不用重启),并自动探一次可达性,直接告诉你「已保存。实例可达(HTTP 200)」或「已保存。实例没有响应:…」;
  • 测试连接不改地址,只探当前这个实例;
  • 地址填错会被拒绝保存(不是存下来等下次启动才炸),错误就显示在框下面;
  • 恢复默认清掉自定义地址,回到插件配置里的 rsshubBase(没配置就是官方 https://rsshub.app)。

这些开关都点下即生效,不需要重启,也不需要刷新页面(关掉入口后左侧栏那一行立刻消失,打开后立刻回来;切换图片偏好后正文立刻重排)。选择会写进订阅数据文件(prefs.showSidebarEntry / prefs.expandImages / prefs.collapseFeeds),重启后依然有效;写入失败时开关会弹回原位并就地报错,不会假装已保存。

窄栏表头的折叠按钮和这一页的开关写的是同一个偏好,所以从哪里折、从哪里开都一样,折叠状态也会跟着重启保留(这正是「右侧边栏面板重新打开时不该又弹回来」所要求的)。

之所以把这些开关放进 DSH 自己的设置、而不是放进 RSS 面板:入口那个开关决定的是面板有没有那个入口,如果只能从那个入口进去关它,就是个死循环。而图片偏好与排序都是阅读偏好,和「左侧栏长什么样」一样属于 DSH 层面的设置。

给订阅源排序

三种操作,写的是同一份顺序(存在宿主的状态文件里,所以换浏览器、换面板、Agent 看到的都是同一个顺序):

方式位置适合
拖动面板左栏的订阅源行(窄栏是顶部胶囊)一眼看清、一次挪到位
右键 →「上移 / 下移」订阅源行右键菜单的最上面挪一位、不想拖;窄栏里尤其好用
↑ / ↓ 按钮设置 → RSS 阅读器 → 订阅源顺序精确调整、触屏、一次挪很多位

细节:

  • 行首有 ⠿ 记号、鼠标是抓手,提示这一行可以拖。
  • 拖到某一行的位置上就占它的位,中间的行依次让位——不需要瞄准缝隙。
  • 菜单里没有「上移」就是已经在最上面(而不是给一个点了没反应的灰项)。
  • 新订阅的源排在末尾,不会插进你已经排好的顺序中间;删掉的源会从顺序里消失,不留空洞。
  • 排序立即生效(先动屏幕、再落盘),宿主拒绝时会把顺序退回并提示,不会留一个「屏幕上是这样、文件里不是」的状态。

探索 RSSHub 订阅源

「🔍 查找」解决的是「我已经知道那个地址」;探索解决的是另一半问题:RSSHub 上别人整理好的那些订阅源,有什么值得我订?

点顶栏 🧭 探索(窄栏里只显示图标):

  1. 搜索:一个输入框同时搜站点名、路由名、路径。输入 热搜 会列出「B 站热搜 / 百度热搜榜 / 微博热搜榜」;输入 bilibili 会把这些站点的路由都排前面。
  2. 分类:一排分类胶囊(编程、社交媒体、大学、传统媒体、财经、政务……),每个后面是它覆盖的站点数。选中即过滤,再点一次取消。
  3. 站点:默认列出站点(名称 + 域名 + 路由条数),按路由条数从多到少。点进去就是该站点的所有路由,左上角有 ← 返回。
  4. 订阅:在一条路由上点 添加,展开参数表单——参数已经用 RSSHub 官方示例填好,可以直接改,也可以留空可选项(留空的可选参数会从地址里省略)。点 订阅 即完成,对话框不会关闭,方便接着订第二条。

每条路由会带上它自己声明的事实,避免「订了却取不到」:

标记含义
需配置这条路由需要实例侧配置(Cookie / Token)。公共实例上通常取不到,需要自建实例。鼠标悬停会列出缺哪些变量名。
反爬站点有反爬措施,公共实例可能被拦。
需浏览器需要实例开启 Playwright / Puppeteer。
示例 …RSSHub 文档里给出的可运行示例地址。

订下去之后,标题由订阅源自己决定:像 36kr 那条多用途路由(资讯 / 快讯 / 用户文章……)在参数为 newsflashes 时,最终订阅名是「36氪 - 快讯」而不是路由表里的长名字。

首次打开会慢几秒:宿主要先把实例的整张路由表(约 3 MB)拉下来并投影成紧凑结构(实测约 1 秒),之后 12 小时内都走内存缓存(实测 30 ms 左右)。缓存窗口可用 rsshubCatalogMinutes 调整。

抓取更早的文章

订阅源是窗口,不是档案。阮一峰的 atom.xml 里就只有 3 条(实测 44319 字节、3 个 <entry>),所以无论刷新多少次都到不了第 400 期——不是插件截断了,是源里没有。

往前的文章仍在他自己的归档页上,插件可以按需取回:

  1. 在订阅源横条上选中一个源(「全部」时这个入口不出现,一次抓所有源对人家服务器不礼貌);
  2. 点顶栏 ⇤ 更早,对话框里填归档页地址(默认按该源已有条目的目录猜一个,通常直接确认即可)和篇数(默认 20,上限 50);
  3. 抓完列表里就多了这些文章,按日期插在正确的位置,可以正常阅读、翻译、收藏。

它的行为是刻意保守的:

  • 只挑"像文章"的链接:拿已订阅条目的地址推出形态(目录形态 + 扩展名,或整条路径形态),归档页里的导航、标签页、月度索引都会被滤掉;
  • 不重复抓:已订阅过的地址会跳过,所以连点两次不会重复下载(http / https 两种写法视为同一条);
  • 请求之间有停顿(默认 300ms,historyDelayMs 可调),一篇失败不影响其余各篇,失败原因会在对话框里列出;
  • 每源保留上限仍然生效(maxItemsPerFeed,默认 100):抓回的旧文按日期排序,超限时丢的是最老的,不会挤掉订阅源自己给的那几条。

配置项:

history: true          # 设为 false 关闭这个功能(接口会返回 503)
historyMaxPerRun: 50   # 单次上限,无论对话框里填多少
historyDelayMs: 300    # 每篇之间的间隔

抓取的是别人服务器上的页面,一次 50 篇就是 50 个请求。默认值是按"够用且克制"选的;要一次拉完几百期,请把篇数调大并自己确认对方站点能接受,或者干脆分批抓。

阅读正文

  • 图片留在原位:图片不再被搬到文章结尾,而是留在原文给它的位置——段与段之间的图就是段与段之间的一个折叠条,连续的图合成一组(▸ 图片 3 张),单张图显示它的配文(▸ 图片:配图说明)。
    • 折叠时不会加载任何图片(连 <img> 都不渲染);点一下展开缩略图,再点某张图放大看原图(Esc 或点背景关闭,可点「原图 ↗」在新标签打开)。
    • 展开后每张图先显示「载入中…」;加载成功即显示图片,加载失败则在该位置显示「图片加载失败」,不会一直停在载入中。
    • 句子里的图(文字 ![说明](图) 文字)折成一枚 🖼 说明 小标签留在句子中,点开就地显示,不把一句话拆成三段。
    • 想一进来就看到图,打开 设置 → 通用 → 「正文图片直接展开」;那之后图片仍在原位,只是不再折叠(依旧按需加载),而且单独点一条仍可以只把那条折回去。
  • 一键翻译:点顶栏 译 按钮,把当前条目翻译成左下角「翻译为」所选的语言。
    • 首次翻译会调用模型(通常几秒到二十几秒,按钮显示「翻译中…」)。
    • 翻好后按钮变为 显示译文 / 显示原文,可随时切换;译文同样按 Markdown 排版,链接与代码原样保留。
    • 对照:把原文与译文逐段穿插显示——每一段原文下面紧跟它自己的译文(译文左侧有一条竖线区分)。这是默认推荐的读法:既能核对译法,也不会丢掉原文的措辞。
      • 逐段对照靠"按段翻译"实现:插件把正文切成段落(代码块内的空行不算切分),逐段编号交给模型,要求一一对应地返回,再按编号配对。
      • 只有完全对齐时才显示对照;模型少给、多给或编号重复时,宁可退回整篇译文,也不会把某段的译文错配到另一段下面(那时页面会说明并提示点 ↻ 重译)。
      • 只有图片或只有代码的段落没有可译内容,会原样保留在原文一侧、不重复显示。
      • 旧版本缓存下来的译文没有分段信息,点 对照 会自动重译一次以获得对齐结果。
    • 旁边的 ↻ 强制重译,✕ 删除已缓存的译文。
    • 换一个目标语言会自动重新翻译(不会拿中文译文冒充日文)。
    • 若当前 profile 没有可用模型,按钮会直接不显示。

提示条怎么消失

  • 成功 / 状态提示(「已刷新 3/4 个源,新增 12 条」「订阅成功」「翻译完成」「已显示缓存的翻译」)出现 2 秒后自动消失,不必手动点关闭;连续两条提示时,计时从最新那条重新开始。
  • 异常提示(刷新失败、翻译失败、宿主返回错误等)不会自动消失,必须点「关闭」——这类信息你可能还要照着处理,不该在读完之前自己跑掉。
  • 设置页里的输入校验提示(实例地址、翻译模型)是常驻说明,不是一次性提示,不参与这个规则。

RSSHub 订阅源发现

很多站点自己根本不提供 RSS。在「+ 添加订阅源」对话框里粘贴任意网页地址,点 🔍 查找,插件会分两路找出可用订阅源:

  1. 页面自身声明的 <link rel="alternate"> —— 权威、不依赖第三方,排在最前;
  2. RSSHub 的 Radar 规则 —— 把页面 URL 匹配成一条 RSSHub 路由,为没有 RSS 的站点生成订阅源。

结果会带来源标签(站点 / RSSHub),点任意一条即可直接订阅,订阅名用路由标题而不是裸 URL。

粘贴 https://space.bilibili.com/2267573  →
  [RSSHub] UP 主图文      → rsshub…/bilibili/user/article/2267573
  [RSSHub] UP 主投币视频  → rsshub…/bilibili/user/coin/2267573
  [RSSHub] UP 主动态      → rsshub…/bilibili/user/dynamic/2267573

如果只粘贴了首页(如 github.com)而没有具体路径,通常匹配不到路由。此时插件不会留下一片空白,而是提示该站点在 RSSHub 中共有多少条路由、并举例说明——让你知道该换成哪种页面地址(github.com/作者/仓库/issues 之类)再去查找。

关于 RSSHub 实例:默认使用官方公共实例 https://rsshub.app。公共实例常有速率限制或需要 key,自建实例通常更稳定,可在配置里指定:

config:
  rsshubBase: 'https://rsshub.example.com'   # 自建或偏好的公共实例
  rsshub: true                                # 设为 false 可关闭该功能
  rsshubTimeoutMs: 15000
  rsshubCacheMinutes: 360
  • 若实例地址填错,插件不会启动失败,只是关闭该功能并在日志里说明原因。
  • 规则按域名缓存(默认 6 小时)。若某条路由取不到内容,多半是实例限流或该路由需要配置 key/Cookie,与插件无关——可换实例试试。

注意:rsshub.app 在部分网络环境下不可达。发现功能依赖能否访问你配置的实例;实例不可用时插件仍可正常订阅页面自身声明的 RSS。

命令

/rss                 # 列出订阅源与最近条目
/rss refresh         # 先刷新再列出
/rss refresh 掘金     # 只刷新并查看指定源

工具

Agent 可调用 rss_read:

参数说明
feedId只看某个源(id 或标题),省略为全部
limit最多列出多少条,默认 30
unreadOnly只看未读
refresh先联网刷新再读取

配置

在插入条目里写 config(或使用 dsh plugin 安装后编辑 profile 的 cordis.patch.yml):

- insert:
    - id: rss-reader
      name: 'dsh-rss-reader'
      config:
        refreshMinutes: 30      # 后台自动刷新间隔(分钟),0 = 关闭(默认)
        refreshOnStart: true    # 启动时刷新一次,默认 false
        timeoutMs: 20000        # 单次请求超时
        maxBytes: 8388608       # 单个响应大小上限(8 MiB)
        concurrency: 4          # 并发抓取的源数量
        maxItemsPerFeed: 100    # 每个源保留的条目数
        maxFeeds: 200           # 订阅源数量上限
        storeFile: ''           # 状态文件路径,默认 $DSH_HOME/rss-reader/feeds.json
        tool: true              # 注册 rss_read 工具与提示词说明
        webApi: true            # 提供 Web 面板与 HTTP API
        showSidebarEntry: true  # 左侧栏是否显示 RSS 入口(用户可在 DSH 设置里覆盖,见上)
        # ── 翻译 ──────────────────────────────────────────────
        translate: true         # 开启「译」按钮
        translateTarget: zh-CN  # 默认目标语言
        translateProvider: ''   # 留空 = 复用当前会话的默认模型;用户可在 设置 → RSS 阅读器 里覆盖
        translateModel: ''
        translateEffort: ''     # 思考强度(如 low / high);留空 = 模型自己的默认
        # ── 抓取更早的文章 ──────────────────────────────────────
        history: true           # 允许从站点归档页按需抓取更早的文章
        historyMaxPerRun: 50    # 单次抓取上限(对话框里填再多也不会超过)
        historyDelayMs: 300     # 每篇之间的间隔,对别人服务器客气一点
        translateTimeoutMs: 90000
        translateMaxTokens: 4000  # 输出预算的**下限**,长文会自动提高(见下)
        # ── RSSHub 订阅源发现 ───────────────────────────────────
        rsshub: true                            # 开启「🔍 查找」发现功能
        rsshubExplore: true                     # 开启「🧭 探索」路由表浏览
        rsshubBase: ''                          # 默认实例;留空 = https://rsshub.app。用户可在 设置 → RSS 阅读器 里覆盖
        rsshubTimeoutMs: 15000
        rsshubCacheMinutes: 360                 # Radar 规则缓存(按域名)
        rsshubCatalogMinutes: 720               # 路由表缓存(整张表,默认 12 小时)

关于翻译用的模型:translateProvider / translateModel / translateEffort 是默认值,用户可以在 设置 → RSS 阅读器 → 翻译模型 里改(三个下拉:模型服务、模型、思考强度),改完立即生效、不用重启。三层优先级是:用户在设置里选的 → 插件配置 → 会话默认模型(agentDefaultModel,也就是你在 DSH 里选的那个),所以什么都不配也能用,不需要额外 API Key。如果当前 profile 没有挂载 LLM 服务或没有可用模型,插件仍能正常阅读,只是隐藏「译」按钮(而不是给你一个点了必失败的按钮)。

模型清单来自宿主自己的模型目录(sessionController.modelCatalog()),因此每个模型可选的思考强度也一并列出;选了一个当前 profile 没有挂载的服务时,设置页会直接说它没有挂载、翻译会失败,而不是让你点了才发现。

关于 translateMaxTokens:它是输出预算的下限,不是硬上限。正文越长,插件要求的预算越高(按正文字符数估算,上限 64000),因为一篇长文的译文本身就和原文差不多长,而推理型模型还会先花掉同一份额度去思考——额度在思考阶段就用光时,模型一个文字块都没吐出来。这也是长文翻译失败最常见的原因:与其调大额度,不如在设置里换一个不做长推理的模型,或把思考强度调到最低。真的被截断时报错会说明「模型在输出 N tokens 时被截断」并指向设置页,而不是含糊地说「模型没有返回文本」。要控成本就把这个值设小(长文可能因此失败);translate: false 则整体关掉翻译(面板上不会出现「译」按钮)。

关于 showSidebarEntry:它是默认值,不是开关锁。用户在 DSH 设置里选过之后,以用户的选择为准;没选过的人才跟着这个配置走。所以把它设成 false 可以得到「出厂即隐藏左侧栏入口」的效果,而用户仍能在设置里把它打开。要回到配置默认值,删掉数据文件里的 prefs 段即可。

关于 rsshubBase:同理,它是默认实例,不是锁。官方实例在部分网络下不可达,而找到可用实例不该意味着改配置 + 重启——所以 设置 → RSS 阅读器 → RSSHub 实例 可以随时改,改完立即生效(两个客户端一起换,并丢掉旧实例的缓存)。用户没改过就跟着这个配置走;「恢复默认」会删掉用户的选择,回到这里配置的值。

HTTP API

面板与宿主通过同源 HTTP 通信,全部挂在 /api/rss-reader/*:

方法路径说明
GET/state当前订阅状态(?items=0 可省略条目以减小响应)
GET/item?feedId=&itemId=单条正文(Markdown)与已缓存的翻译
POST/feeds添加订阅源;{url, group?, title?, refresh?},默认立即抓取
PATCH/feeds修改订阅源({id, title?, group?, url?})
PATCH/feeds/order重排订阅源({ids: [...]});未知 id 忽略、未提到的 id 排在末尾,{ids} 不是数组 → 400
DELETE/feeds?id=取消订阅
POST/refresh刷新({ids?, force?, concurrency?})
PATCH/items标记已读 / 收藏({feedId, itemId?, all?, read?, starred?})
GET/translate翻译是否可用、目标语言清单、默认目标
POST/translate翻译({feedId, itemId, target?, force?});命中缓存时不调用模型
DELETE/translate?feedId=&itemId=删除已缓存的翻译
GET/discover(已扩展)同时返回页面自身声明的订阅源与 RSSHub 路由候选
POST/discover查找;{url, page?, rsshub?, limit?} 可分别关掉某一路
GET/rsshubRSSHub 是否可用、探索是否可用、当前实例地址;?check=1 额外向该实例发一个请求报告可达性
GET/models翻译可选用的模型目录(来自宿主 sessionController.modelCatalog(),含每个模型的思考强度),外加 stored:哪些翻译选择确实是用户做的;没有 LLM 服务 → 503
POST

列表接口不下发正文:一篇正文可达数十 KB,把每个源每一条都塞进列表会让响应膨胀到几 MB,而列表只渲染标题与摘要。正文由 /item 按需取一条。

状态码语义:调用方输入有误(URL 非法等)→ 400;上游不可达 / 解析失败 / 模型失败 → 502;功能不可用 → 503;其余 → 500。

请求除依赖回环绑定外,还要求浏览器同源标记(Origin 或 Sec-Fetch-*),因此本机其他程序无法直接驱动该 API;这只是一道绊线,真正的边界仍是 DSH 自身的鉴权。

架构

dsh-rss-reader/
├── lib/
│   ├── index.js    宿主半体:装配 store / 定时器 / 工具 / 命令 / 路由 / 翻译
│   ├── api.js      HTTP API 与「仅本机 UI」围栏
│   ├── store.js    订阅状态、视图偏好、翻译缓存与原子持久化
│   ├── refresh.js  并发刷新编排与去重
│   ├── fetch.js    抓取:超时、体积上限、条件 GET、代理、发现
│   ├── feed.js     RSS 2.0 / RSS 1.0 / Atom 解析与归一化
│   ├── xml.js      容错 XML/HTML 读取器
│   ├── markdown.js HTML → Markdown 转换
│   ├── translate.js 一键翻译(提示词、调用、结果归一化)
│   ├── rsshub.js    RSSHub Radar 规则发现(匹配、缓存、实例容错)
│   ├── explore.js   RSSHub 路由表(投影、检索、路由地址拼装)
│   └── client.js    浏览器半体:阅读面板(三栏 / 单栏两种呈现)+ Markdown 渲染 + 发现/探索对话框 + 设置行
├── test/           347 个测试
└── scripts/        测试运行器与验证脚本

几处刻意的设计取舍:

  • 零运行时依赖:XML / 订阅解析 / HTML→Markdown 全部自研(Node 无内置 XML 解析),因此安装体积小、不受上游 CVE 影响;undici 仅在检测到代理环境变量时按需 import,缺失也能正常工作。
  • Markdown 在宿主侧转换:订阅源的正文是 HTML,转换一次并存入条目,浏览器端就只需一个纯 Markdown 渲染器。这也让翻译的输入输出都是 Markdown——模型对 Markdown 的处理远好于标签汤,且译文能走同一条渲染路径。
  • 不引入 Markdown 库:客户端 bundle 只能 require 启动时提供的平台种子模块(react、react/jsx-runtime、react-dom、cordis、store、slots、primitives、dockkit),marked 不在其中——引用它会在运行时直接抛出「missed the module table」。因此渲染器是自带的,覆盖订阅源真正会用到的那部分语法。
  • 渲染为 React 元素,绝不拼 HTML:不使用 dangerouslySetInnerHTML,也不生成 HTML 字符串,因此订阅源里的 <script> 只会变成惰性文本,无法注入页面。
  • 图片留在原位,而不是被搬到结尾:早期版本把整篇的图片抽出来、统一放到正文末尾的一条折叠条里。那实现起来最省事(一个列表、一次渲染),代价是把文章的排版毁了——图文的顺序是作者排版的一部分。现在解析器把「只由图片组成的段落」作为一种块(images),图片因此按文档顺序留在原地;相邻的图片块合并成一组,因为它们本来就是一排图。折叠与「不点不下载」照旧:默认只渲染那条提示,<img> 根本不进 DOM。句中图片是唯一的例外——它折成一枚小标签留在句子里,因为把一句话拆成三段更不像原文。默认值可由 设置 → 通用 → 「正文图片直接展开」 反转,那是阅读偏好,不是渲染规则。
  • 惰性加载的图片绝不能先被隐藏:早期版本在图片载入前给它 display: none(想避免撑开空白),结果自锁——隐藏元素不进视口 → loading="lazy" 不触发抓取 → onLoad 永不触发 → 一直隐藏,于是永远停在「载入中…」。现在 <img> 始终在文档流里,占位文字只是盖在预留高度的盒子上的一层;另外若图片命中缓存、在 React 挂上监听前就已就绪,会由元素自身的 complete / naturalWidth 判定,避免错过事件。
  • 右侧边栏是可选服务:右侧边栏的两个服务(sidebarRightTabs、sidebarRight)通过 ctx.inject([...]) 获取;没有它的构建(或它尚未装好时)插件照常加载,只是没有那个 tab,而不会整棵插件树启动失败。注册的是一个页面类型(不声明 patterns,因此不认领任何资源地址),tab 正文注册在 keyed 坑位 sidebar.right.pane.tab 下、键就是该类型自己的 id。所有注册都包在 ctx.effect 里:该注册表对同一 id 的二次注册是抛错的,所以服务重建时旧注册必须先随之销毁。
  • 一个面板,两种呈现:RssPanel 用 variant 区分——中间主区域是三栏(列表与正文并排),右侧边栏传 "sidebar" 走单栏(订阅源横条 + 列表,详情就地替换列表并带返回)。二者共用同一份视图状态与同一套 API,不复制业务逻辑。
  • 破坏性操作要两步:取消订阅不再放在行上,而是收进右键菜单,再经一层确认框。原先的 window.confirm 也被换成面板自己的对话框——它会阻塞整个页面,且说不清将要删掉什么。
  • 设置页放 DSH 自己的设置里,不放面板里:RSS 的设置是「设置 → RSS 阅读器」这一整页(settings.section)。放进面板会造成死循环——入口没了,就再也进不去把它打开。面板自己不做这种自锁的门。
  • 顺序是一张 id 列表,不是每条记录上的一个位置:「把这条上移」同时改变了它的邻居,列表让这次编辑只写一次,而每条带位置号需要写两条、两条还可能互相矛盾。列表还自带两条好性质:删掉的源在读取时被过滤,不留空洞;新订阅的源因为不在列表里,自然排在末尾,不会插进用户已经排好的顺序中间。

开发

npm test        # 347 个测试

测试覆盖:XML 容错与编码(含解析循环必须前进的回归)、三种订阅源格式、URL / 日期归一化、HTML→Markdown 转换(结构、转义、危险协议、图片、void 元素)、存储与原子持久化(含启动竞态、带类型的偏好白名单与写穿、空字符串 = 清除选择、订阅源顺序:新源排末尾、未知 id 忽略、部分重排、删除后不留空洞、快照跟随顺序)、翻译(提示词、JSON 提取、超时与取消、缓存)、RSSHub 发现(规则匹配、目标填充、子域分桶、空 200 / 404 / 非 JSON / 网络故障的区分、缓存与回退、路由列举)、RSSHub 路由表(模板解析、示例反推、地址拼装、可选参数规则、参数归一化、投影与分类计数、排序检索与分页、缓存与失败不缓存、/explore 与 /explore/url 的拒绝路径)、并发刷新与去重、路由与请求围栏、错误码映射、/prefs 的默认值 / 覆盖 / 清除 / 拒绝非法输入、/feeds/order 的重排与拒绝、/rsshub?check=1 的可达性、换实例后两个客户端一起移动、客户端 bundle 契约与渲染(Markdown 结构、图片载入中 / 成功 / 失败三态、图片留在原位 / 相邻合并 / 句中图小标签、apply 的右侧边栏与 RSS 设置页注册、设置页与左侧栏入口的注册 / 撤销、实例地址的显示 / 保存 / 测试 / 恢复默认 / 被拒、拖动排序 / 右键上移下移 / 设置页 ↑↓ 三个入口、右键菜单 + 二次确认的取消订阅、单栏详情 ⇄ 列表、探索对话框的浏览 / 分类 / 预填 / 订阅与拒绝、图片折叠、翻译交互、发现对话框),以及真实 HTTP 集成测试(用 DSH 自带的 WebServer 起真实回环服务,跑完整订阅 → 抓取 → 阅读流程,以及偏好落盘)。

验证已安装的实例(可选)

仓库内的测试用替代实现(例如自制 React shim)驱动,因此还需要在真实 DSH 引导下验证一次:

# 1. 用一份禁用单例插件的叠加层,另起一个实例(避免与日常实例争抢锁与端口)
dsh --profile web --patch ./scripts/verify-profile.patch.yml --port 3199 --no-open

# 2. 校验该实例实际下发给浏览器的客户端 bundle
#    路径带 rev 查询串,从实例页面里取出(例如 /plugins/??dsh-rss-reader/client.js&rev=…)
curl -s -o served-client.js "http://127.0.0.1:3199/plugins/??dsh-rss-reader/client.js&rev=<rev>"
node scripts/verify-served-bundle.mjs served-client.js

verify-served-bundle.mjs 除了 bundle 契约,还会驱动 apply(ctx) 检查槽位注册:main 面板、sidebar.panellist 行、RSS 设置页(settings.section)且「通用」里不再有 RSS 行、右侧边栏 tab 类型 / 正文坑位 / 启动器,以及「只注册进已知槽位」。它会给 /prefs 打一个桩,因此看到的是「用户没动过设置」时的那套布局。

scripts/verify-profile.patch.yml 会临时禁用 ui-task-board、better-sidebar 等会抢占全局锁或固定端口的插件,只保留 dsh-base、dsh-web-app 与待验证插件。它只用于验证,不要在日常启动中使用。

验证「探索」时多半要换实例:官方 rsshub.app 在部分网络下不可达(本机就不可达),而 rsshubBase 一旦连不上,「探索」只会如实报错。验证时用一份叠加层按 id 覆盖配置即可,不必改 profile:

# 追加到叠加层末尾
- id: rss-reader
  config:
    rsshubBase: 'https://rsshub.rssforever.com'

然后 curl 'http://127.0.0.1:3199/api/rss-reader/explore' 应返回 totals(实测 1589 个站点 / 3288 条路由),并且 POST /explore/url 拼出的地址里带着 namespace(https://<instance>/github/trending/daily/any)。

换实例也可以完全不走配置文件——现在用接口就行,效果和用户在设置页里改一样:

curl -X PATCH -H 'content-type: application/json' -H 'Origin: http://127.0.0.1:3199' \
  -d '{"rsshubBase":"https://rsshub.rssforever.com"}' \
  http://127.0.0.1:3199/api/rss-reader/prefs

改完 GET /rsshub 会立刻报新地址;GET /rsshub?check=1 会告诉你它是否真的响应。

该叠加层不会禁用 llm-codebuddy-cli:它注册的是当前会话默认模型所用的 provider,禁用后翻译必然失败(报「模型没有返回文本」),看起来像插件 bug,实为该叠加层的副作用。

注意:lib/client.js 是手写的、不经打包的 __ModuleLoader__ 半体。改动后必须重启 dsh web 才生效(客户端 bundle 在启动时组合下发)。

改了代码却"没生效",先查这一条:dsh plugin add file:<本仓库> 装的是一个目录依赖,而 pnpm 在本 profile 上用的是 hoisted linker,于是它在 node_modules 里落的是一份拷贝,不是指回本仓库的链接。改本仓库的文件,宿主读到的仍是那份旧拷贝——而且 pnpm install 也救不了:锁文件已满足,pnpm 认为这个目录依赖没变,会把同一份旧拷贝再铺一遍。表现极具误导性:界面还是旧行为,新加的偏好键会被 400 拒绝,看起来像功能坏了。

一条命令即可,之后重启 DSH:

node scripts/sync-install.mjs          # 把本仓库的 files 拷进每个已安装的 profile
node scripts/sync-install.mjs --check  # 只报告有没有漂移,不写文件

已知限制

  • BBC 等部分站点:若网络环境无法直连,会报网络错误(属环境问题,非解析问题)。
  • Markdown 是自带渲染器:因为客户端 bundle 不允许引入 marked 等库,渲染器覆盖的是订阅源真正会用到的语法子集(标题、列表、引用、表格、代码、链接、图片、粗斜体、分隔线)。极冷门的语法(脚注、定义列表、HTML 内联)会退化为纯文本。
  • 图片只显示不下载:图片按远程 URL 直接引用(带 no-referrer),插件不会代理或缓存图片,因此需要能直连图床。
  • 图片位置靠 Markdown 结构还原,不理解 HTML 版式:插件把订阅源正文转成 Markdown 再渲染,所以「图在它原来待的段落之间」是准的,但原文里靠 CSS 实现的复杂版式(多图并排、图文环绕、图注在侧)不会被还原——那些信息在转 Markdown 的那一步就没了。连续的图片会合成一组,因此「并排三张」与「上下三张」在插件里长得一样。
  • 翻译质量取决于所配置的模型:插件只负责把 Markdown 结构和 URL 原样交给模型并要求保留;模型仍可能改动人名或术语。
  • RSSHub 覆盖度随实例而异:路由由社区维护,实例之间版本不同;某条路由在一个实例上 503(限流)不代表不可用,换实例或自建通常更稳。部分路由(如微博、B 站)在公共实例上需要配置 key 或 Cookie。
  • 「探索」看到的是实例自己的路由表:分类与路由都来自你配置的那个实例。官方实例不可达时,在 设置 → RSS 阅读器 → RSSHub 实例 里换成可达的(公共镜像或自建)即可,不用改配置、不用重启;换完两个功能一起跟着走,旧实例的缓存会被丢掉。
  • 首次打开探索要等一次大请求:整张表约 3.3 MB(压缩后),实测约 1 秒;之后走宿主内存缓存(默认 12 小时)。缓存是进程内的,所以重启 dsh web 后第一次打开又要拉一次。
  • 探索不判断「哪条真的能取到」:需配置 / 反爬 / 需浏览器 是路由自己声明的,插件照实显示。真正失败会像任何订阅源一样记在 lastError 里(例如实例对该路由返回 503),不会因为来自探索页就特殊对待。
  • 参数只支持写在路径里的:路由表偶尔会给出路径模板里没有的参数键(例如某些路由的 routeParams)。插件不猜它们的位置——猜错会拼出一个语义不同的地址。这类路由仍可订阅,但那些参数需要你在「+ 添加订阅源」里手改地址。
  • 发现只推荐"可直接订阅"的路由:需要参数才能生成的路由只作提示列出,不会当成可订阅项。
  • 无全文抓取:只读取订阅源自身提供的内容,不会去抓取原文页面补全正文。
  • 无 OPML 导入 / 导出:目前只能逐条添加(状态文件为 JSON,可直接编辑)。
  • 右侧边栏面板依赖该功能存在:tab 注册在 dsh-web-app 自带的右侧边栏(sidebarRightTabs / sidebarRight)上。若某个 profile 没有它,插件照常加载,只有中间主区域的三栏面板可用。
  • 左侧栏入口可以关,中间面板本身不能关:设置里的开关控制的是「左侧栏那一行」,main 面板始终注册着——只是关掉那一行后就只能从右侧边栏或底部按钮打开面板。这与「隐藏入口」的字面意思一致,但值得知道:关掉后中间的三栏面板不会消失,只是少了一个入口。
  • 偏好是每个 DSH 实例一份:存在宿主的状态文件里,不是浏览器本地存储。所以同一个人换浏览器、或用别的机器访问同一个 DSH,看到的是同一个选择;反过来,两个不同的 DSH 实例互不影响。

License

MIT

/history
按需抓取更早的文章;{feedId, archiveUrl, limit}。归档页不可达 → 502,地址不是 http(s) 或源不存在 → 400 / 404,功能关闭 → 503;成功时连同新 state 一起返回
GET/explore浏览 / 搜索路由表;?q=&category=&namespace=&limit=&offset=&refresh=1
POST/explore/url把路由 + 参数拼成订阅地址({namespace, path, values});缺参数 → 400(并说明缺哪个),路由表里没有 → 404
GET/prefs当前生效的视图偏好(用户选过的值,否则是插件配置的默认值),外加 stored:哪些键确实是用户选的
PATCH/prefs修改偏好({showSidebarEntry?, expandImages?, collapseFeeds?} 为布尔,{rsshubBase?, translateProvider?, translateModel?, translateEffort?, lastFeedId?, lastItemKey?, lastScrollTop?} 为字符串);未知键、类型不对、或实例地址不是 http(s) → 400,且不写入任何一项;字符串设为 "" = 删掉这个选择(回到配置默认)。后三个是阅读位置记忆,由面板自己读写
GET/health健康检查
  • 三种排序入口,一份实现:拖动、右键菜单、设置页的 ↑/↓ 最终都调用同一个 saveOrder(ids),发同一个 PATCH /feeds/order。所以三种方式不可能各排各的——这也是为什么「拖动」值得做,而上移/下移仍然保留:拖动快,但需要指针和稳定手势,窄栏里是一排横向胶囊,触屏上更不友好。
  • 排序先动屏幕再落盘:宿主是本地进程,但「拖完等一个来回才动」仍然会让人觉得拖空了。所以先本地重排,再发请求;宿主拒绝就重新读一遍状态(而不是自己回滚,因为权威版本在宿主那里)。
  • 实例地址是「配置 + 偏好」两层,而不是只有一层:rsshubBase 既是配置项,也是可改的偏好。配置是默认值,用户改过就以用户为准,"" 等于删掉这个选择——这样既能「出厂指向某个自建实例」,又能让读者在设置里随时换一个,而不必动 profile。判断「这个值是谁的」不能靠比较字符串(等于默认值的偏好和没选过长得一样),所以 /prefs 额外回报 stored 键名,设置页据此说「自定义实例」还是「默认实例」。
  • 换实例要连缓存一起换:Radar 规则按域名缓存、路由表整张缓存,都是从某个实例取来的。所以 useBase() 在改地址的同时清空两个客户端的缓存——否则会拿着 A 实例的规则去读 B 实例,给出一条 B 上根本不存在的路由。
  • 存进文件之前先确认它是个地址:实例地址的校验放在写入端(normalizeBase),而不是等下一次启动。一个错字要么在设置页当场被拒绝并说明原因,要么就会变成「重启后 RSSHub 整体不可用」——后者看起来像插件坏了,其实是配置坏了。
  • 偏好被读取之前也要追上:状态文件是启动后异步读的,而配置里的默认值在 apply 时就已经用了。所以启动流程在 store.load() 之后会把存下来的实例地址再应用一次——否则「设置页显示 A 实例、实际请求打向 B 实例」,这种不一致最难查。
  • 偏好是「白名单 + 用户选择优先」:状态文件里只接受 PREF_KEYS 白名单内的布尔键,其余一律丢弃;而且只存用户显式选过的值,没选过的键根本不出现。这样 showSidebarEntry 这类配置项才能一直是「默认值」:改配置能影响所有没覆盖它的人,用户选过之后则以用户为准。把 prefs 段删掉就等于回到配置默认。
  • 开关立即生效:apply 从偏好决定是否注册左侧栏那一行,并在偏好变化时撤销或重新注册(slots.register 的 disposer 是真的撤下那一行)。所以点开关不需要刷新页面,也不需要重启;启动时先读偏好再注册,避免「关掉了却在每次加载时闪一下」。
  • 写入失败要回滚:开关是乐观更新的(点下即动),但宿主拒绝时会把值退回并就地显示错误。一个默默保留「宿主根本没存下」的值的控件,比一个会弹回的控件更糟。
  • 路由表在宿主侧投影,不下发给浏览器:实例的整张路由表约 3.3 MB(gzip 后,解压后 3.0 MB 的 JSON,实测 2.3 秒)。把它交给面板是不可行的,所以宿主机只拉一次,投影成紧凑结构(约 830 KB JSON、含搜索用的小写索引)留在内存里,接口按页下发。宿主还顺带做了两件浏览器做不了的事:跨全表检索(3288 条路由上按「命中位置」排序,而不是按出现次数),以及只在内存里保留可渲染的字段(Markdown 文档表格、代码块、::: warning 这类排版噪声在投影时就被丢掉,因为列表行只需要一句话)。
  • 路由地址由宿主拼装,不交给浏览器:模板里哪些参数可省(:x?)、哪些能吃掉斜杠(:x{.+})、「可选参数夹在必填参数之前时不能省」——这些规则只实现一份,在 explore.js 里。浏览器把 {namespace, path, values} 发过来,宿主先确认这条路由在路由表里真的存在,再拼地址;因此一个过期的客户端也拼不出一个指向别的路由的地址。
  • namespace 必须拼进地址:路由模板是「相对 namespace」的(/trending/:since),而订阅地址不是(/github/trending/daily/any)。这条在单元测试里看着天经地义,写起来却真的漏过一次——漏掉时实例返回 503,看起来像「实例不支持这条路由」,实际是自己拼错了地址。真实引导下的端到端验证抓到了它。
  • 用官方示例反推参数:每条路由都带一个可运行的 example。把示例按模板逐段对齐,就能把每个参数的值读回来——这让 3026 条路由可以「点一下就能订阅」,而不是把用户丢进参数表。对不齐时(示例与模板段数不匹配)宁可不给任何预填,也不猜一个可能订错的地址。
  • 路由表只报事实,不排「热门」:注册表里唯一的排序信号是文档页的 view 计数,而它只覆盖 410/3288 条、最大值是 5——那是文档的浏览徽章,不是流行度。所以探索页不编造排行榜:默认按「路由条数」列站点(信息量最直接),其余交给分类与搜索。每条路由只显示它自己声明的 requireConfig / antiCrawler / requirePuppeteer,让「公共实例取不到」在点订阅之前就可见。
  • 翻译结果落盘:翻译是一次模型调用,代价不低;结果按条目缓存并写入状态文件,且在刷新时保留(与已读/收藏标记同样对待),重新打开即秒开。
  • 发现分两路,页面优先:先读页面自己的 <link rel="alternate">(权威、不依赖第三方),再问 RSSHub。两者合并去重后由用户选择,不会因为站点有 RSSHub 路由就擅自替你订阅某一条。
  • 未覆盖 ≠ 故障:RSSHub 对没有规则的域名返回 200 + 空响应体(不是 404)。早期版本直接 response.json() 会把「该站点没有规则」误报成「实例连不上」;现在按文本读取并区分「无规则」「实例不可用」「响应非 JSON」三种情况。
  • 规则按域名缓存,失败只短暂缓存:规则变化很慢,命中缓存 6 小时;但网络故障只缓存约 1 分钟,避免一次抖动把功能"钉死"在整个 TTL 内。
  • 域名逐级回退:规则按注册域(github.com)索引,而用户常粘贴子域(show.bilibili.com)。插件会依次尝试主机后缀直到命中;多段后缀(co.uk、com.cn)做了处理,不会把 bbc.co.uk 截成 co.uk。
  • 不可用路由不推荐:目标里参数填不满的路由(例如需要 :user 而页面没给)直接跳过,绝不会给出带 :user 占位符的"订阅源"。
  • 容错优先:真实世界的订阅源经常有未转义的 &、缺失的命名空间、错配的标签,甚至正文里混进 JavaScript。lib/xml.js 是宽容扫描器而非严格解析器——遇到畸形输入宁可尽力读出内容,也不整体失败。解析循环的每一步都保证前进(见下)。
  • 宿主持有状态:浏览器半体不保存订阅数据,只保存视图状态(选中项、筛选条件)。数据全部经 HTTP API 从宿主读取,因此重启页面不丢状态,Agent 与面板看到的是同一份数据。
  • 原子写入:状态先写临时文件再 rename,中途崩溃不会留下被截断的订阅列表;写入按 400 ms 去抖合并。
  • 防御性读取:状态文件是用户可编辑的,读取时会逐条校验并丢弃不可用记录;文件损坏时移到 .corrupt-<时间戳> 旁备份,而不是静默覆盖。
  • 解析循环必须前进:parseTag 的每个分支都保证推进游标。这不是洁癖——早期版本在遇到「没有属性名却出现 / 或 =」时(例如正文里漏出来的 plotW / (n - 1))会原地死循环,同步阻塞整个 DSH 进程(不只是这一个请求)。现在有回归测试守着这一点。