dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
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,自动识别,无需手动指定。 |
| 时间范围筛选 | 顶栏一排胶囊 全部 / 今天 / 近 3 天 / 近 7 天(窄栏收成下拉),按本地自然日收窄条目流。默认仍是「全部」,不动老习惯。选什么就是什么:范围里没有内容就显示空,不会自己换一个范围。被时间范围排除的无日期条目会报出条数,绝不静默吞掉。 |
| 未读按时间拆分 | 顶栏不再只写一个「41 条未读」,而是 今天 5 条未读 · 更早 36 条未读——一个数字混着今天的新闻和几个月的积压,永远降不下去;拆开之后它才是"现在要看几件"。 |
| 阅读体验 | 未读计数与筛选、收藏(★)、标题/摘要/作者搜索、一键全部已读、相对时间显示。 |
| 增量抓取 | 记录 ETag / Last-Modified,未更新时服务端返回 304,不重复下载正文。 |
| 交给 Agent | 工具 rss_read 让模型按需读取订阅内容;命令 /rss 手动查看;两者与面板共享同一份数据。 |
| 可选后台刷新 | refreshMinutes 开启后按间隔自动刷新。 |
插件是标准的 DSH 组合包(Cordis 插件),以依赖的形式装进某个 profile,由该 profile 启动时挂载。下面以 web profile 为例($DSH_HOME/profiles/web,$DSH_HOME 默认 ~/.dsh)。
需要 DSH ≥ 0.2.0-rc.1,以及 pnpm 在 PATH 上(dsh plugin 只是 pnpm 的转发器,缺了会报 pnpm not found on PATH)。
为什么是 0.2.0:插件按 0.2.0 这一代的宿主契约校准(
defineTool的output.schema/render/presentCall、webServer.register、systemPrompt.section、commands.register、sessionController.modelCatalog(),以及slots/sidebar.panellist/settings.section/ 右侧边栏guide[].id)。这些在 0.2.0-rc.1 上都与 0.1.7 一致,所以下限直接对齐当前使用的运行时;更早的 0.1.5 / 0.1.6 缺guide[].id,安装会被dsh的版本校验拦下(peerDependencies 不满足),而不是装上以后行为诡异。
dsh plugin --profile web add github:FYKANG/dsh-rss-reader
这条命令会装下包、自动把 dsh-rss-reader 追加进 profile 的 dsh.profile.bundles(因为包内声明了 dsh.bundle.patch,不用手改 cordis.patch.yml),并让浏览器半体随 GUI 一起下发;它按已安装依赖的真实包名核对,所以 github: 这种写法不需要额外配置。
从本地副本安装(改代码时用这种,github: 跟随的是上游而不是你的本地改动):
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:.)会按你执行命令时所在的目录解析。另外file:装进去的是副本(pnpm 的nodeLinker: hoisted不做符号链接),改完源码要重新add一次才生效。
装完重启 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,卸载不会删除。
中间主区域(三栏)
https://www.ithome.com/rss/)或网站首页(例如 https://github.blog/),可选填分组,点「添加并获取」。右侧边栏(单栏)
○ 未读、★ 收藏、⟳ 刷新、+ 添加),鼠标悬停有完整说明。订阅源行上没有删除按钮。取消订阅走两步:
刷新此源 / 全部标为已读 / 取消订阅…;菜单和确认框都可以随时放弃,期间不会发出任何请求。
RSS 的设置不混在「通用」里,而是自己一页(导航里叫 RSS 阅读器,排在 通用 / 模型 / 插件 之后):
「左侧栏入口」
「正文图片直接展开」
▸ 图片:配图说明 / ▸ 图片 3 张),位置与原文一致,点开才加载;loading="lazy" 按需加载),最接近原文观感。「收起订阅源栏」
全部 / 各订阅源 + 未读数);只影响右侧边栏那个窄面板。中间主区域的三栏布局里,左栏订阅源列表同时也是重排顺序、右键管理订阅源的地方,折掉它会拿走能力而不只是腾地方,所以那里不跟着折;面板表头上的折叠按钮也只在窄栏出现。
「翻译模型」(仅当这个 profile 真的能翻译时出现)
三个下拉 + 恢复默认:模型服务、模型、思考强度(只有当所选模型声明了可选强度时才出现)。下面一行写清当前生效的是哪个路由(服务 / 模型,或「跟随 DSH 默认模型」)以及它是「自定义」还是「默认」;换模型服务会同时清掉模型选择,避免留下半套路由。
translateMaxTokens 更有效。「订阅源顺序」
带编号的订阅源列表,每行 ↑ / ↓ 两个按钮,首行的 ↑ 与末行的 ↓ 是禁用的(到顶到底了)。面板里拖出来的顺序也会实时反映在这里。
「RSSHub 实例」(仅当这个 profile 真的启用了 RSSHub 时出现)
一个地址输入框 + 保存 / 测试连接 / 恢复默认:
rsshubBase(没配置就是官方 https://rsshub.app)。这些开关都点下即生效,不需要重启,也不需要刷新页面(关掉入口后左侧栏那一行立刻消失,打开后立刻回来;切换图片偏好后正文立刻重排)。选择会写进订阅数据文件(prefs.showSidebarEntry / prefs.expandImages / prefs.collapseFeeds),重启后依然有效;写入失败时开关会弹回原位并就地报错,不会假装已保存。
窄栏表头的折叠按钮和这一页的开关写的是同一个偏好,所以从哪里折、从哪里开都一样,折叠状态也会跟着重启保留(这正是「右侧边栏面板重新打开时不该又弹回来」所要求的)。
之所以把这些开关放进 DSH 自己的设置、而不是放进 RSS 面板:入口那个开关决定的是面板有没有那个入口,如果只能从那个入口进去关它,就是个死循环。而图片偏好与排序都是阅读偏好,和「左侧栏长什么样」一样属于 DSH 层面的设置。
三种操作,写的是同一份顺序(存在宿主的状态文件里,所以换浏览器、换面板、Agent 看到的都是同一个顺序):
| 方式 | 位置 | 适合 |
|---|---|---|
| 拖动 | 面板左栏的订阅源行(窄栏是顶部胶囊) | 一眼看清、一次挪到位 |
| 右键 →「上移 / 下移」 | 订阅源行右键菜单的最上面 | 挪一位、不想拖;窄栏里尤其好用 |
| ↑ / ↓ 按钮 | 设置 → RSS 阅读器 → 订阅源顺序 | 精确调整、触屏、一次挪很多位 |
细节:
「🔍 查找」解决的是「我已经知道那个地址」;探索解决的是另一半问题:RSSHub 上别人整理好的那些订阅源,有什么值得我订?
点顶栏 🧭 探索(窄栏里只显示图标):
热搜 会列出「B 站热搜 / 百度热搜榜 / 微博热搜榜」;输入 bilibili 会把这些站点的路由都排前面。每条路由会带上它自己声明的事实,避免「订了却取不到」:
| 标记 | 含义 |
|---|---|
| 需配置 | 这条路由需要实例侧配置(Cookie / Token)。公共实例上通常取不到,需要自建实例。鼠标悬停会列出缺哪些变量名。 |
| 反爬 | 站点有反爬措施,公共实例可能被拦。 |
| 需浏览器 | 需要实例开启 Playwright / Puppeteer。 |
| 示例 … | RSSHub 文档里给出的可运行示例地址。 |
订下去之后,标题由订阅源自己决定:像 36kr 那条多用途路由(资讯 / 快讯 / 用户文章……)在参数为 newsflashes 时,最终订阅名是「36氪 - 快讯」而不是路由表里的长名字。
首次打开会慢几秒:宿主要先把实例的整张路由表(约 3 MB)拉下来并投影成紧凑结构(实测约 1 秒),之后 12 小时内都走内存缓存(实测 30 ms 左右)。缓存窗口可用
rsshubCatalogMinutes调整。
订阅源是窗口,不是档案。阮一峰的 atom.xml 里就只有 3 条(实测 44319 字节、3 个 <entry>),所以无论刷新多少次都到不了第 400 期——不是插件截断了,是源里没有。
往前的文章仍在他自己的归档页上,插件可以按需取回:
它的行为是刻意保守的:
http / https 两种写法视为同一条);historyDelayMs 可调),一篇失败不影响其余各篇,失败原因会在对话框里列出;maxItemsPerFeed,默认 100):抓回的旧文按日期排序,超限时丢的是最老的,不会挤掉订阅源自己给的那几条。配置项:
history: true # 设为 false 关闭这个功能(接口会返回 503)
historyMaxPerRun: 50 # 单次上限,无论对话框里填多少
historyDelayMs: 300 # 每篇之间的间隔
抓取的是别人服务器上的页面,一次 50 篇就是 50 个请求。默认值是按"够用且克制"选的;要一次拉完几百期,请把篇数调大并自己确认对方站点能接受,或者干脆分批抓。
顶栏那排胶囊就是为这件事准备的:
| 胶囊 | 含义 |
|---|---|
| 全部 | 不限时间(默认)——面板打开时看到的东西和以前完全一样 |
| 今天 | 今天 00:00(你本机时区) 之后发布的条目 |
| 近 3 天 | 含今天在内的 3 个自然日(不是"72 小时内") |
| 近 7 天 | 含今天在内的 7 个自然日 |
几条刻意的行为:
今天内暂无内容,可换一个时间范围。不会自动放宽到近 3 天——那会让胶囊写着"今天"却给你看三天,也就是让控件说谎。范围由你决定,面板只负责如实执行并说明为什么是空的。今天 + 只看未读 是最常用的组合,而它正是"今天该读什么"的答案。3 条内容没有发布日期,未计入当前时间范围,条目行上也有 无日期 徽标。一旦时间筛选会悄悄吞东西,你就不敢再用它了,所以这条是底线。时间范围 下拉,取值完全相同。今天 3 条未读,不再拆「更早」)。配套的还有未读计数按时间拆分:顶栏写的是 今天 5 条未读 · 更早 36 条未读,而不是笼统的 41 条未读。今天没有未读时只显示总数,不写"今天 0 条"。
时间范围是按条目的发布日期算的,不是"这条什么时候进到你的阅读器"。所以如果你三天没打开,这三天陆续发布的新闻会分散在「近 3 天」里,而不是全挤在「今天」。按"什么时候第一次见到"来筛(也就是真正的「新到的」视图)需要额外记录每个条目的首次出现时间,目前没做。
sticky,所以操作跟着它一直留在右上角。★ 收藏、译、↗ 打开原文、○ 未读),鼠标悬停有完整说明——和窄栏顶栏收图标是同一套做法。★ 高亮、当前是译文则显示「原文」、已读则显示「标为未读」)。▸ 图片 3 张),单张图显示它的配文(▸ 图片:配图说明)。
<img> 都不渲染);点一下展开缩略图,再点某张图放大看原图(Esc 或点背景关闭,可点「原图 ↗」在新标签打开)。文字  文字)折成一枚 🖼 说明 小标签留在句子中,点开就地显示,不把一句话拆成三段。提示悬浮在窗口右下角,而不是插在面板内容里——这样它在正文阅读中途出现时,不会把你正在看的那一列顶下去;同时出现的异常与状态提示会上下叠放,不会互相盖住。
很多站点自己根本不提供 RSS。在「+ 添加订阅源」对话框里粘贴任意网页地址,点 🔍 查找,插件会分两路找出可用订阅源:
<link rel="alternate"> —— 权威、不依赖第三方,排在最前;结果会带来源标签(站点 / 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
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/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/ 测试运行器与验证脚本
几处刻意的设计取舍:
undici 仅在检测到代理环境变量时按需 import,缺失也能正常工作。require 启动时提供的平台种子模块(react、react/jsx-runtime、react-dom、cordis、store、slots、primitives、dockkit),marked 不在其中——引用它会在运行时直接抛出「missed the module table」。因此渲染器是自带的,覆盖订阅源真正会用到的那部分语法。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 也被换成面板自己的对话框——它会阻塞整个页面,且说不清将要删掉什么。settings.section)。放进面板会造成死循环——入口没了,就再也进不去把它打开。面板自己不做这种自锁的门。saveOrder(ids),发同一个 PATCH /feeds/order。所以三种方式不可能各排各的——这也是为什么「拖动」值得做,而上移/下移仍然保留:拖动快,但需要指针和稳定手势,窄栏里是一排横向胶囊,触屏上更不友好。rsshubBase 既是配置项,也是可改的偏好。配置是默认值,用户改过就以用户为准,"" 等于删掉这个选择——这样既能「出厂指向某个自建实例」,又能让读者在设置里随时换一个,而不必动 profile。判断「这个值是谁的」不能靠比较字符串(等于默认值的偏好和没选过长得一样),所以 /prefs 额外回报 stored 键名,设置页据此说「自定义实例」还是「默认实例」。useBase() 在改地址的同时清空两个客户端的缓存——否则会拿着 A 实例的规则去读 B 实例,给出一条 B 上根本不存在的路由。normalizeBase),而不是等下一次启动。一个错字要么在设置页当场被拒绝并说明原因,要么就会变成「重启后 RSSHub 整体不可用」——后者看起来像插件坏了,其实是配置坏了。apply 时就已经用了。所以启动流程在 store.load() 之后会把存下来的实例地址再应用一次——否则「设置页显示 A 实例、实际请求打向 B 实例」,这种不一致最难查。PREF_KEYS 白名单内的布尔键,其余一律丢弃;而且只存用户显式选过的值,没选过的键根本不出现。这样 showSidebarEntry 这类配置项才能一直是「默认值」:改配置能影响所有没覆盖它的人,用户选过之后则以用户为准。把 prefs 段删掉就等于回到配置默认。apply 从偏好决定是否注册左侧栏那一行,并在偏好变化时撤销或重新注册(slots.register 的 disposer 是真的撤下那一行)。所以点开关不需要刷新页面,也不需要重启;启动时先读偏好再注册,避免「关掉了却在每次加载时闪一下」。