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,卸载不会删除。
使用
界面
中间主区域(三栏)
- 点击左侧栏 RSS 图标打开阅读面板。
- 首次使用点 + 添加订阅源,粘贴订阅源地址(例如
https://www.ithome.com/rss/)或网站首页(例如https://github.blog/),可选填分组,点「添加并获取」。 - 左栏选择订阅源或「全部订阅源」,中间点选条目,右侧阅读正文(Markdown 排版)。
- 顶栏可搜索、筛选未读 / 收藏,或「⟳ 刷新」。
右侧边栏(单栏)
- 点左侧栏底部的 RSS 阅读器(或右侧边栏 + 里的同名项)打开。
- 顶部一排是订阅源 横条,每个胶囊显示源名与未读数;下面就是条目列表。
- 点某条 → 列表就地换成该条详情,← 返回列表 固定在顶部随读随在(不跟随滚动条跑掉);返回后筛选与搜索都保持原样,没有跳转、没有新标签页。
- 读的时候想换个源?直接点顶部的订阅源胶囊——会立刻回到列表(不会停在上一条的详情里让你再点一次返回)。「全部」同理。
- 悬浮条贴住阅读区顶边、下方没有空隙:阅读区本身没有上内边距(有的话会落在悬浮条上方,正文会从缝里透出来),正文的首个元素上外边距也用一条只作用于该面板的规则清掉了——外边距塌陷没法用行内样式表达,这是唯一一处注入的 CSS。
- 顶栏按钮在窄栏里收成图标(
○未读、★收藏、⟳刷新、+添加),鼠标悬停有完整说明。
取消订阅(右键 + 二次确认)
订阅源行上没有删除按钮。取消订阅走两步:
- 在订阅源上右键(或点行尾的 ⋯)→ 弹出操作菜单:
刷新此源/全部标为已读/取消订阅…; - 选「取消订阅…」后弹出确认框,写清将要删除的条目数,点 确定取消订阅 才真正执行。
菜单和确认框都可以随时放弃,期间不会发出任何请求。
设置页(设置 → 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 上别人整理好的那些订阅源,有什么值得我订?
点顶栏 🧭 探索(窄栏里只显示图标):
- 搜索:一个输入框同时搜站点名、路由名、路径。输入
热搜会列出「B 站热搜 / 百度热搜榜 / 微博热搜榜」;输入bilibili会把这些站点的路由都排前面。 - 分类:一排分类胶囊(编程、社交媒体、大学、传统媒体、财经、政务……),每个后面是它覆盖的站点数。选中即过滤,再点一次取消。
- 站点:默认列出站点(名称 + 域名 + 路由条数),按路由条数从多到少。点进去就是该站点的所有路由,左上角有 ← 返回。
- 订阅:在一条路由上点 添加,展开参数表单——参数已经用 RSSHub 官方示例填好,可以直接改,也可以留空可选项(留空的可选参数会从地址里省略)。点 订阅 即完成,对话框不会关闭,方便接着订第二条。
每条路由会带上它自己声明的事实,避免「订了却取不到」:
| 标记 | 含义 |
|---|---|
| 需配置 | 这条路由需要实例侧配置(Cookie / Token)。公共实例上通常取不到,需要自建实例。鼠标悬停会列出缺哪些变量名。 |
| 反爬 | 站点有反爬措施,公共实例可能被拦。 |
| 需浏览器 | 需要实例开启 Playwright / Puppeteer。 |
| 示例 … | RSSHub 文档里给出的可运行示例地址。 |
订下去之后,标题由订阅源自己决定:像 36kr 那条多用途路由(资讯 / 快讯 / 用户文章……)在参数为 newsflashes 时,最终订阅名是「36氪 - 快讯」而不是路由表里的长名字。
首次打开会慢几秒:宿主要先把实例的整张路由表(约 3 MB)拉下来并投影成紧凑结构(实测约 1 秒),之后 12 小时内都走内存缓存(实测 30 ms 左右)。缓存窗口可用
rsshubCatalogMinutes调整。
抓取更早的文章
订阅源是窗口,不是档案。阮一峰的 atom.xml 里就只有 3 条(实测 44319 字节、3 个 <entry>),所以无论刷新多少次都到不了第 400 期——不是插件截断了,是源里没有。
往前的文章仍在他自己的归档页上,插件可以按需取回:
- 在订阅源横条上选中一个源(「全部」时这个入口不出现,一次抓所有源对人家服务器不礼貌);
- 点顶栏 ⇤ 更早,对话框里填归档页地址(默认按该源已有条目的目录猜一个,通常直接确认即可)和篇数(默认 20,上限 50);
- 抓完列表里就多了这些文章,按日期插在正确的位置,可以正常阅读、翻译、收藏。
它的行为是刻意保守的:
- 只挑"像文章"的链接:拿已订阅条目的地址推出形态(目录形态 + 扩展名,或整条路径形态),归档页里的导航、标签页、月度索引都会被滤掉;
- 不重复抓:已订阅过的地址会跳过,所以连点两次不会重复下载(
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。在「+ 添加订阅源」对话框里粘贴任意网页地址,点 🔍 查找,插件会分两路找出可用订阅源:
- 页面自身声明的
<link rel="alternate">—— 权威、不依赖第三方,排在最前; - 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 |
/rsshub |
RSSHub 是否可用、探索是否可用、当前实例地址;?check=1 额外向该实例发一个请求报告可达性 |
GET |
/models |
翻译可选用的模型目录(来自宿主 sessionController.modelCatalog(),含每个模型的思考强度),外加 stored:哪些翻译选择确实是用户做的;没有 LLM 服务 → 503 |
POST |
/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 |
健康检查 |
列表接口不下发正文:一篇正文可达数十 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 列表,不是每条记录上的一个位置:「把这条上移」同时改变了它的邻居,列表让这次编辑只写一次,而每条带位置号需要写两条、两条还可能互相矛盾。列表还自带两条好性质:删掉的源在读取时被过滤,不留空洞;新订阅的源因为不在列表里,自然排在末尾,不会插进用户已经排好的顺序中间。
- 三种排序入口,一份实现:拖动、右键菜单、设置页的 ↑/↓ 最终都调用同一个
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 进程(不只是这一个请求)。现在有回归测试守着这一点。
开发
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
No comments yet. Be the first to write one.