← Back to home@weixshaw

dsh-rss

DSH(DeepSeek Harness)RSS 阅读器插件:分组订阅、FreshRSS 账号同步、图片本地代理、BYOK AI 摘要/翻译/问答、播客与全文抓取

Stars
0
Language
JavaScript
Created
Oct 6, 2026
Updated
Oct 6, 2026
GitHub repo

Introduction

dsh-rss · DeepSeek Harness RSS 阅读器插件

在 DSH(DeepSeek Harness)里读 RSS:自定义 RSS/Atom 订阅、自托管 FreshRSS 账号同步(Google Reader API,已读/收藏双向写回)、AI 摘要 / 中文翻译 / 针对单篇提问(自带 Key,OpenAI 兼容接口)、文章图片本地代理加载(图床看不到你的 IP)。刷新有界并发且带 ETag/Last-Modified 条件请求(304 免下载免解析),阅读支持可选自动刷新与浏览上下文持久化。

产品交互灵感来自开源项目 qiaomu-ai-rss(GPL-3.0-only)。本插件是独立实现,未复制其任何源代码、字体、图片或订阅目录数据;行为参考已致谢,代码许可是 MIT。

  • 阅读入口(主):左侧主导航「RSS 阅读」——与其他插件面板并排的一等入口,点击即在中央内容区打开阅读器。兼容入口:设置 → RSS 阅读(专用设置面板:只含 订阅管理 / FreshRSS / AI 设置 三个配置分区,不嵌完整阅读工作台;面板内状态不读写浏览上下文持久化);装了 dsh-better-sidebar(≥0.24,可选)时还会自动出现右侧栏「RSS 阅读」tab。
  • 模型工具:rss_recent_digest(最近文章摘要列表)、rss_article_digest(读单篇缓存与已有 AI 结果)。工具只读本地缓存,不联网、不调用 AI、不接触密钥。

功能一览

能力说明
自定义订阅添加任意 RSS 2.0 / Atom 地址,重命名、分组(组/子组 路径)、退订;自带独立解析器(零依赖)
阅读工作台左侧订阅分组栏(真实分类树:分组路径/未分组桶/单订阅;行计数为未读/总数双计数,全已读显示灰色总数;默认隐藏已读完的订阅与分组(顶栏「只看未读/显示全部」切换,当前选中项保留不丢上下文,全部已读时显示 🎉 空态);栏底快速添加订阅(粘贴地址点 + 或回车,独立 RSS 无需 FreshRSS))+ 紧凑文章列表 + 舒适阅读栏三栏布局,三栏独立展开/收起:订阅栏由顶栏「☰」收起(持久化)、列表栏由专注模式收起为全宽阅读(工具栏按钮或 F 键,持久化)、阅读层由「← 返回」/Esc 收起;列表/阅读栏宽度可拖拽调整(240–560px,持久化);窄容器(右侧栏/移动)自动折叠为单栏 + 抽屉;选中分组=明确子树作用域(按原始分组串精确匹配,显示层才做路径规范化;分组消失后显式空作用域,0 结果 0 写入,绝不回落「全部」),筛选/搜索/全部已读均在当前作用域内;分组行折叠钮/选择钮为同级真实按钮(aria-expanded,Space/Enter 阻止默认);未读计数只在写确认后更新(失败保留计数并报错);作用域/筛选/搜索切换立即作废在途响应(含防抖窗口);分页「加载更多」(追加期间禁用+加载中,失败保留已加载列表;读/标后刷新保留已加载页数,超过后端单次 100 条上限自动分多次拉取);键盘 / 搜索、J/K 上下篇、R 已读、S 收藏、F 专注、Esc 返回
阅读增强抓取全文(摘要型订阅刚需):按原文链接抓页面、轻量 readability 提取正文(优先 <article>、去导航/页脚/脚本,图片保留裸 img)临时替换展示——不落盘、不进 AI,切文章自动恢复缓存正文;复制链接(clipboard API + execCommand 兜底);正文字号 A−/A+(14–22px,持久化)
列表与排版(qiaomu 式)列表行:顶部 meta(未读点 · 来源 · ★ · 相对时间)+ 两行标题 + 两行摘要(发丝线分隔,未读加重、已读降权);阅读栏:sticky 工具栏(返回/已读/收藏/原文/图片开关)+ 804px 居中衬线正文(Songti SC/Noto Serif CJK,17px/1.9 行距,代码块/引用/表格样式化)
图片显示(本地代理)默认零远程图片请求(占位文本);阅读栏「🖼 图片」开关开启后,文章内图片经 host 本地代理 /dsh-rss/media 加载——图床看不到你的 IP/UA/Referer;魔数嗅探只放行 png/jpeg/gif/webp/avif/bmp/ico(SVG 一律拒绝,可携脚本)、8MB 上限、磁盘缓存 100 张/64MB(LRU)、同 URL 在途去重;开关持久化在 host 配置(ui.imageMode);开启后列表行同时显示首图 64px 缩略图(同走代理)
播客与视频播客订阅的 enclosure 音频:阅读栏「▶ 加载播客音频」点击后才直连音频源(点击前零请求,不经过代理);文章链接是 YouTube(watch/shorts/youtu.be)或 B 站(BV 号,保留分 P)视频页时,「▶ 在阅读器内播放视频」点击后加载官方嵌入播放器(sandbox 白名单 iframe);其余站点一律不嵌入
宿主/客户端版本守护客户端启动时 ping 宿主比对版本(CLIENT_VERSION 与 package.json 由 check 门禁强制一致):本地路径安装硬刷新只更新客户端半边,宿主半边必须重启 DSH——版本不一致时顶部显示重启指引提示条(可关闭),而不是让图片代理等新功能无声 404
阅读与筛选全部 / 未读 / 收藏三种筛选、标题+正文搜索、已读小红点、收藏星标、全部已读(带作用域确认);列表相对时间(悬停见绝对时间);浏览上下文(作用域/筛选/搜索/视图/分组展开态/自动刷新开关)持久化在 localStorage,面板重挂/刷新页面自动恢复
刷新手动刷新(多订阅有界并发,5 路;部分失败如实逐条上报、不静默吞错,失败保留旧缓存);有 FreshRSS 订阅且账号可用时,「⟳ 刷新」在独立订阅之后自动继续账号同步(全部订阅都是 FreshRSS 时这就是最直觉的更新入口,不再误报「没有可刷新的独立订阅」);ETag/Last-Modified 条件请求(服务端 304 时跳过下载与解析,验证器随订阅缓存、不再返回时自动清空);列表栏「自动」开关:每 10 分钟静默自动刷新一次(仅阅读页打开时生效;同样覆盖 FreshRSS 同步,成功不打扰、失败只留控制台痕迹,手动刷新仍如实上报)
OPML嵌套分组导入:两段式(先预览统计,确认后才写入),文件内去重、非法协议跳过、达到订阅上限时截断上报;导出为标准 OPML
FreshRSS账号级连接:订阅列表(含服务端分类)、文章同步到本地;增量同步(游标持久化,第二次起只拉上次同步之后的新条目,同步全程成功才推进游标——部分失败下次从旧游标重放,宁重复不遗漏);本地已读/收藏变更写回服务端(edit-tag),失败保留队列下次重试;服务端已删除的订阅随同步移除;账号(地址+用户名)变更后失败关闭,须显式确认重置(防止把 A 账号的待同步状态写到 B 账号)
AI摘要 / 翻译 / 提问,结果可选持久化(绑定文章,最多 200 条)
安全正文白名单渲染(无 innerHTML)、远程图片默认不加载、密钥只存 0600 文件、同源防护、请求/响应全部有界

安装

前提:已安装 DSH Desktop(0.2.0-rc 线)或 DSH CLI,Node.js ≥ 20。

方式一:DSH Desktop GUI(推荐)

desktop profile 由 Electron 应用独占管理,dsh CLI 会直接拒绝(这是设计行为,不是故障):

dsh plugin --profile desktop add github:weixshaw/dsh-rss
error: profile "desktop" is managed exclusively by the Electron application

正确的安装方式是 DSH 桌面端 GUI 的插件管理页(对应 Host 的 pluginManager 服务,与 dsh plugin add 走同一套 pnpm 安装/挂载逻辑):

  1. 打开 DSH Desktop(GUI 地址如 http://127.0.0.1:19387),在**侧边栏选择「Plugins(插件)」**页面。

  2. 点击 「添加插件 / Add plugin」。输入框接受包名(可带版本)、Git 地址、压缩包(tarball)或本地绝对路径——粘贴本仓库 Git 地址:

    https://github.com/weixshaw/dsh-rss
    
  3. 阅读输入框上方关于信任的提示,点击 「安装 / Install」。安装进度会流式显示 pnpm 输出,可取消;失败会自动恢复 profile 的 package.json 与 pnpm-lock.yaml。

  4. 安装完成后:若界面提示需要重启,退出并重新打开 DSH Desktop(host 半生效;desktop profile 在启动时应用 patch),随后在 GUI 里硬刷新浏览器(Cmd/Ctrl+Shift+R)(client 半生效)。

  5. 之后在同一个 Plugins 页面可以启用/停用/卸载 dsh-rss;每行开关会写 profile 的 cordis.patch.yml 覆盖,无需手动改文件。

不要手动编辑 ~/.dsh/profiles/desktop/ 下的任何文件,也不要试图绕过 CLI 的 profile 所有权限制——桌面 profile 的变更一律走上述 GUI。

方式二:CLI(仅自建 profile,如 web)

CLI 插件管理只对自己创建的 profile可用(desktop 不行):

dsh plugin --profile web add github:weixshaw/dsh-rss

该命令在 profile 目录里用 pnpm 安装本包,并依据 package.json 的 dsh.bundle.patch(cordis.patch.yml)把 dsh-rss 追加进 dsh.profile.bundles 自动挂载;CLI/headless profile 会热应用 patch,改完硬刷新浏览器即可。

方式三:本地开发安装(贡献者)

clone 本仓库后,把上面两种方式里的 Git 地址换成本地绝对路径(如 /path/to/dsh-rss)即可。注意本地路径安装不会自动更新:代码改动后 = 在 Plugins 页面先卸载再重新添加,随后完全退出重启 DSH Desktop 并硬刷新浏览器(宿主半边不会热更新;客户端内置版本探测,宿主落后时会在界面顶部提示重启)。

更新与卸载

  • Git 地址安装:在 Plugins 页面卸载后重新添加(拉取最新代码)。
  • 卸载:Plugins 页面对 dsh-rss 选择卸载/移除;自建 profile 也可 dsh plugin --profile web remove dsh-rss。

FreshRSS 接入(自托管)

  1. FreshRSS 服务端:设置 → 认证 → API 访问,允许 API,并生成专用「API 密码」(不是登录密码)。 FreshRSS ≥ 1.20 默认提供 Google Reader 兼容 API。
  2. 本插件 设置 → RSS 阅读 → ☁️ FreshRSS 页:
    • 地址填站点根地址(https://freshrss.example.net)或完整 API 地址(https://freshrss.example.net/api/greader.php)均可;
    • 填用户名 + API 密码 → 「保存并测试连接」;
    • 「⇅ 立即同步」:拉取订阅列表与最近文章(最多 4 页 × 100 条),并把本地待同步的已读/收藏写回服务端。
  3. 同步语义:
    • 协议实现对照 FreshRSS 官方文档与其 p/api/greader.php 源码:accounts/ClientLogin(Email/Passwd → Auth)、reader/api/0/subscription/list、stream/contents/…?n=&xt=&c=(continuation 分页)、写操作先取 reader/api/0/token,再用 edit-tag(i/a/r/T)写读/星标;
    • 写失败不会被假装成功:推送失败会逐类显示原因,改动保留在待同步队列,下次同步重试;
    • 同步串行执行,确认推送按“版本化快照”进行——同步期间新产生的标记不会被误清除;
    • 远端状态落地:该条目在本地没有待推送改动时,服务端的已读/加星(无论 true 还是 false)都如实同步到本地;本地还有未推送成功的改动时,本地改动优先,不会被回滚;
    • 账号变更保护(失败关闭):以独立记录的“活动账号身份”+ 本地 greader 数据存在性判定——只要本地还有另一账号(或身份未知)的 FreshRSS 订阅/待同步状态,连接/同步一律拒绝,直到在「FreshRSS」页显式点击“重置 FreshRSS 数据”(清除该账号的全部本地订阅/缓存/已读/收藏/待同步队列,保留独立 RSS 订阅)。把配置清空或置 null 也无法绕过;换账号时旧 API 密码会被清除,需要重新输入。同步、FreshRSS 配置变更与重置共用同一互斥锁,账号切换/重置不会与在途同步交错。
  4. 隐私与明文 HTTP:
    • API 密码只保存在本机 ~/.dsh/dsh-rss/config.json(0600);不会出现在浏览器响应、模型工具输出或日志里。
    • 地址允许 http://(方便内网/局域网自托管),但插件会显示明文 HTTP 警告:密码在网络上不加密,建议上 HTTPS 或仅限可信内网。非 http/https 协议一律拒绝。
    • 对 FreshRSS 与 AI 接口的所有请求(含 GET)一律拒绝重定向——它们都携带凭据(密码/API Key/Auth 令牌),杜绝跨源凭据转发。

AI 配置(自带 Key / BYOK)

设置 → RSS 阅读 → ✨ AI 设置:填 OpenAI 兼容接口地址(如 https://api.deepseek.com/v1、https://api.openai.com/v1、本地 Ollama/LiteLLM 的地址)、模型名(如 deepseek-chat)、API Key,勾选启用。

  • 本版本有意采用自带 Key(BYOK)方案:插件不读取、也不复用 DSH 内置的模型配置,AI 调用完全走你在本页填写的接口与 Key。
  • Key 仅存本机 0600 配置文件;界面回显只有“已配置”布尔,输入框留空表示不修改。
  • 只有你在阅读页点「AI 摘要 / AI 翻译 / 💬 提问」时,对应文章正文才会被发送到该接口;模型工具(rss_recent_digest 等)不会触发 AI 调用。
  • 文章正文在提示词里按不可信外部数据处理:定界包裹 + 截断(约 1.2 万字符)+ 系统提示要求忽略正文中出现的任何指令。这能显著降低提示词注入的影响,但不能保证完全消除;因此 AI 输出只作为纯文本展示,不会自动执行任何操作,重要结论请对照原文。

安全说明

  • 浏览器正文渲染:host 入库前先清理(去 script/iframe/事件属性/危险协议;图片重建为只含校验过的 src/alt 的裸 <img>,src 仅 http/https、支持 data-src 懒加载回退、属性值转义),浏览器端再用白名单标签树渲染(绝不使用 innerHTML);图片默认完全不加载(占位文本),开启「图片代理」后 src 只指向本地 /dsh-rss/media 路由,永不直连图床。
  • 所有 /dsh-rss/* JSON 路由要求自定义 X-DSH-RSS: 1 头(跨站表单/简单请求无法伪造),且带 Origin 时强制与 Host 同源;请求体上限 512KB。图片代理路由是 GET 资源(<img> 无法携带自定义头),改用 Sec-Fetch-Site 防护:cross-site 一律 403(防止本机端口被恶意网页当免费图床代理)+ Origin 同源校验兜底;响应一律 X-Content-Type-Options: nosniff + Content-Security-Policy: default-src 'none',类型只认魔数嗅探(不信任源站 Content-Type / 扩展名),SVG 一律拒绝(可携脚本,直接打开会以本源执行)。
  • 对外请求仅允许 http/https,限时(订阅 15s / FreshRSS 12s / AI 60s / 图片 15s)、限响应体积(订阅 2MB / API 1MB / 图片 8MB);无任意路径文件接口。
  • 本地数据全部在 ~/.dsh/dsh-rss/(见下方上限表):目录 0700、文件 0600、原子写(pid+时间戳+随机 tmp,失败清理并显式报错)+ 每文件串行写队列;文章缓存按订阅分文件(articles/<feedId>.json,刷新一个订阅只重写它自己的文件,跨订阅写入互不阻塞;旧版单文件 articles.json 在启动时一次性迁移,成功后改名 articles.json.migrated-v1 留作备份);配置更新走串行事务(并发补丁不丢更新);状态文件损坏时显式报错而非静默重置。
  • 错误消息统一脱敏:不含密码/API Key/Auth 令牌。

本地数据与上限

数据目录:~/.dsh/dsh-rss/(可用环境变量 DSH_RSS_HOME 覆盖)。

项上限
订阅总数(含 FreshRSS 同步的订阅)200
每订阅缓存文章100 条(按时间裁剪)
已读 / 收藏 ID各 5000
保留 AI 结果的文章数200
待同步 FreshRSS 标记每类 2000 条
图片代理磁盘缓存(media/,LRU 修剪)100 张 / 64MB,单张 ≤ 8MB

开发与测试

cd dsh-rss
npm test       # node --test:解析器/存储/OPML/GReader 协议/AI/host 路由(本地回环夹具服务器)/client 契约(vm)/打包
npm run check  # 语法(node --check)+ 模块加载 + DSH 清单 + cordis.patch.yml + README 关键内容

零运行时依赖(Node 内置模块 + host 提供的 peer 依赖)。测试不会触碰真实 ~/.dsh(全部使用临时目录)。

客户端加载与导航修复说明(源码级)

  • 阅读工作台重设计(本迭代):三栏布局(订阅分组栏 / 紧凑列表 / 阅读栏)+ 容器查询窄容器适配;订阅按真实分类(feed.group 路径,FreshRSS 分类/OPML 嵌套同样落到该字段)组织成树,显式「未分组」桶;选中分组=该子树下实际存在的分组串集合(groups 精确匹配,不发明分类),筛选/搜索/全部已读都在作用域内;未读计数来自 host feeds/list 返回的 store.feedCounts() 真实推导并在读/标操作后同步;列表请求带过期守卫(切换作用域后迟到响应丢弃)与 30 条分页 + 「加载更多」。后端契约:articles/feeds/mark-all-read 新增 groups 字符串数组(''=未分组桶,与旧 group 精确串共存且优先)。
  • 重设计正确性修复(本迭代第二批,逐项带自动化回归):
    1. groups: [] 在路由与存储两侧都是显式空作用域(0 匹配 / 0 批量写入,绝不静默回落「全部」);畸形 groups(非数组/非字符串成员)host 以 400 拒绝;选中分组在订阅更新后"消失"时,列表查询与「全部已读」也只下发空集合(host/store/客户端三层都有回归)。
    2. 导航树显示用规范化路径(段 trim、去空段),请求永远下发订阅里存的原始分组串(host 按原文精确匹配);纯空白分组归入「未分组」桶并按原文下发。
    3. 分组行 = 折叠钮 + 选择钮两个同级真实按钮(可聚焦、aria-expanded、Space/Enter preventDefault 且不触发作用域切换);叶子分组默认展开、首次点击即收起(针对"当前有效值"切换,有子分组的默认折叠、首次点击展开)。
    4. 作用域/筛选/搜索/选中变化立即递增请求序号作废在途响应(不等防抖结束):旧文章 / 自动已读完成回调 / AI 结果针对 A 的迟到响应都不会在切到 B 后落地;mark 完成后的列表刷新使用最新作用域+筛选+搜索。
    5. 未读计数只在写确认成功后调整(函数式更新,同订阅并发减计数不丢;根/分组/订阅三层计数一致);单条标记与批量已读失败都保留计数并显式报错。
    6. .drss-shell{position:relative} 位于自身容器查询之外的基础规则(元素永远不会匹配自身所在的容器查询,写在 @container 块内是死代码,窄容器抽屉会锚到 shell 之外的宿主容器);main 页 / 设置面板 / 右侧栏 tab 三入口共用同一条 height:100% + flex:1 + min-height:0 + overflow:auto 高度约束链(reader-ui.test.js 源码级断言)。
    7. 「加载更多」追加期间禁用并显示「加载中…」;追加/刷新失败保留已加载列表并报错;读/标后的刷新保留用户已加载的页数——已加载超过后端单次 100 条上限时自动按 ≤100/页多次拉取补齐。
  • 性能与体验批(本迭代第三批,逐项带自动化回归):
    1. 文章缓存按订阅分文件(articles/<feedId>.json,每文件独立 0600/原子写/串行队列):刷新一个订阅不再整库重写全部订阅的文章;旧版单文件 articles.json 在 Store.init() 一次性迁移(幂等:逐文件写完才改名 legacy,中途崩溃下次以 legacy 重放),迁移后保留 .migrated-v1 备份;退订/账号重置连带删除对应分文件(无孤儿文件);分文件损坏在 init 预加载时显式报错(与旧单文件行为一致)。
    2. 手动刷新从串行改为有界并发(5 路 worker 游标模型),结果仍按订阅原顺序返回、部分失败仍逐条如实上报;配合分文件写队列,跨订阅写入天然互不阻塞。
    3. 订阅刷新带 ETag / Last-Modified 条件请求:验证器随订阅行缓存(feeds/add 首次抓取即缓存),刷新时发送 If-None-Match/If-Modified-Since;服务端 304 → 不下载正文、不解析、不写库(notModified:true),本地缓存与已读状态原样保留;服务端不再返回验证器时清空旧值(不陈旧命中)。凭据类请求(FreshRSS/AI)不受影响,仍一律拒绝重定向。
    4. UI 状态持久化(localStorage,dsh-rss:ui:v1):作用域/筛选/搜索/视图/分组展开态/自动刷新开关变更即写入,面板重挂(切面板/刷新页面)恢复上次浏览上下文;恢复值全部过白名单校验(非法 kind/结构/损坏 JSON 一律回落默认,不炸不注入);瞬时态(分组抽屉开合)不持久化。
    5. feedsReady 门控(竞态修复):恢复的分组作用域要靠订阅列表展开子树分组串——订阅列表就绪前列表查询一律不发(否则会以 groups:[] 显式空作用域查出 0 条且无人重查);自动刷新 tick 同样等订阅列表就绪(未就绪跳过且不烧掉 10 分钟窗口,下个 tick 重试)。
    6. 可选自动刷新:列表栏「自动」开关(持久化);开启后每分钟检查、距上次 ≥10 分钟才真正发刷新请求,仅阅读页挂载时启用;静默执行(成功只刷新数据并保留已加载页数,失败仅 console.warn),绝不弹错误横幅打断阅读。
    7. 列表相对时间(刚刚/N 分钟前/N 小时前/N 天前,一周外或未来时间回绝对时间;悬停 title 显示绝对时间戳)。
  • 图片本地代理与布局改版(本迭代第四批,逐项带自动化回归;交互灵感致谢 qiaomu-ai-rss,独立实现):
    1. 入库图片语义变更:sanitizeStoredHtml 把 <img> 重建为只含校验过的 src/alt 的裸标签(src 仅 http/https 绝对地址、data-src 懒加载回退、alt/src 属性值转义;非法/危险协议 src 退回 [图片: …] 文本标记);htmlToText(AI/搜索/摘要路径)不变,图片仍回文本标记。升级注意:旧缓存文章入库时图片已被替换为文本标记,升级后需要重新刷新/重新订阅才能拿到带 <img> 的新内容(已读状态按稳定 ID 保留)。
    2. host 图片代理路由 GET /dsh-rss/media?u=<encodeURIComponent(图片URL)>:fetchBounded 新增 binary:true(原始字节,不走 utf8 文本解码,测试断言逐字节一致);lib/media.js 魔数嗅探(png/jpeg/gif/webp/avif/bmp/ico;SVG/伪造扩展名一律 415)+ 磁盘缓存(media/<sha1(url)>.img,0600 原子写,LRU 100 张/64MB)+ 同 URL 在途去重;响应带 nosniff + CSP default-src 'none' + 私有缓存头;路由防护见「安全说明」(Sec-Fetch-Site)。
    3. 浏览器端 renderSafe(html, key, {images}):图片节点独立二次校验 src(防旧缓存/绕过);代理模式渲染 <img src="/dsh-rss/media?u=…" loading="lazy">(绝不出现远程直连 src),默认模式一律占位文本(零 img 元素);阅读栏「🖼 图片」开关写回 host 配置 ui.imageMode(never/proxy,跨面板/重启保持)。
    4. 布局改版(qiaomu 式扫描/阅读面):列表行为「顶部 meta(未读点·来源·★·相对时间)+ 两行标题 + 两行摘要」,发丝线分隔、未读加重/已读降权;阅读栏 804px 居中衬线正文(Songti SC / Noto Serif CJK,17px/1.9),sticky 工具栏承载返回/已读/收藏/原文/图片开关;列表/阅读栏之间拖拽调宽手柄(pointer 捕获 + 键盘 ←/→ 微调 ±24px,240–560px 钳制,宽度持久化 localStorage 并随面板重挂恢复);窄容器下自动隐藏手柄回单栏。
  • 媒体补全与宿主版本守护(本迭代第五批,逐项带自动化回归):
    1. 宿主/客户端版本检测:客户端 CLIENT_VERSION(check 门禁强制与 package.json 一致)与 host ping 返回版本比对;不一致(本地路径安装硬刷新后宿主半边仍旧)时顶部显示重启指引提示条。webServer 路由语义已对照本机 @deepseek-ai/dsh-host-webserver 源码核实:exact 匹配取 new URL(req.url).pathname(query string 剥离),/dsh-rss/media?u=… 形态合法。
    2. 播客 enclosure 音频:audio/* 类型或常见音频扩展 + http/src 白名单 → 阅读栏「▶ 加载播客音频」点击后渲染 <audio controls preload="metadata">(点击前零网络请求;音频直连源、不过图片代理——音频文件大且非图片魔数)。
    3. YouTube/B 站嵌入:文章链接命中白名单(YouTube watch/shorts/youtu.be 的 11 位 ID、B 站 BV 号+分 P)→「▶ 在阅读器内播放视频」点击后加载官方嵌入播放器(iframe sandbox 白名单、referrerPolicy、16:9 占位);有音频 enclosure 时优先音频,其余站点一律不嵌入。
    4. 列表缩略图:图片开关开启时取文章首图(入库后的裸 <img> 正则提取)经本地代理渲染 64px 圆角缩略图(object-fit:cover,行内 grid 第二列);开关关闭时零缩略图请求。
    5. FreshRSS 增量同步:游标(上次同步开始毫秒)持久化在 state.json 的 meta.greaderLastSyncMs;条目流带 ot(秒)只拉新内容;同步全程成功才推进游标(推送部分失败下次从旧游标重放,宁重复不遗漏);首次同步仍为全量(4 页 × 100 条)。
  • 刷新语义修正与设置面板(本迭代第六批,逐项带自动化回归):
    1. 「⟳ 刷新」编排修正:存在 greader 订阅且 FreshRSS 账号可用(configured + enabled + 无需重置)时,独立订阅刷新之后自动继续账号同步并汇报同步结果;全部订阅都是 FreshRSS 的用户不再被「没有可刷新的独立订阅」误导(该报错只留给既无独立订阅又不可同步的情况)。自动刷新(静默路径)同样编排。
    2. 旧缓存图片恢复指引:图片开关开启且正文仍含 [图片 文本标记(旧版入库特征)时,阅读栏显示指引——点「⟳ 刷新」重新拉取即可恢复正文图与缩略图;新缓存(含 <img>)或图片关闭时不显示。
  • 三栏展开收起与阅读增强(本迭代第七批,逐项带自动化回归):
    1. 三栏独立展开/收起:订阅栏顶栏「☰」开关(宽屏;窄屏仍是抽屉按钮,两者互不干扰),收起即不渲染、网格列动态生成;专注模式收起列表栏、阅读全宽(工具栏「⇥ 专注」按钮或 F 键;仅打开文章时生效,未打开文章列表始终可见);阅读层收起 = 既有「← 返回」/Esc。两者均持久化、随面板重挂恢复。
    2. 抓取全文:host lib/fulltext.js 轻量正文提取(多个 <article> 取文字量最大者且需达标、否则 <body>;整块去 nav/header/footer/aside/script 等;复用入库清理,图片保留裸 img)+ 路由 article/fetch-full(只抓该文章原文 URL、无凭据、20s/3MB 上限、content-type 校验、登录墙/纯 JS 页如实报「未能提取」);客户端「⤓ 抓取全文」临时替换正文(带提示条与字数汇报,「↩ 缓存正文」一键切回),不落盘不进 AI,切文章自动清。
    3. 复制链接(clipboard API + textarea/execCommand 兜底,无 DOM 环境静默失败)与正文字号 A−/A+(14–22px 钳制,作用于正文行内字号,持久化)。
  • 订阅导航强化(本迭代第八批,逐项带自动化回归;独立 RSS 订阅与 FreshRSS 同为一等入口):
    1. 未读/总数双计数:订阅行与分组/根行显示 未读/总数(tabular-nums,如 3/12),全已读显示灰色纯总数(悬停 aria-label 说明);分组与「全部/未分组」按每订阅真实计数求和(与既有计数同步链一致)。
    2. 隐藏已读完(默认开,持久化):只作用于导航栏显示(作用域/查询/批量操作不受影响);当前选中的订阅/分组即使已读完也保留显示;分组子树全已读则整组隐藏;全部已读时显示 🎉 空态与切换指引。
    3. 订阅栏快速添加:栏底输入框粘贴 RSS/Atom 地址,点「+」或回车即调 feeds/add(独立订阅在阅读视图内完成订阅,无需切管理页);成功清空输入并刷新,空地址/失败如实提示。
    4. 修复一个真实竞态:刷新/同步/标读后的文章重查改为等订阅列表落地后再发起(此前并发下可能用旧订阅列表展开分组作用域,构建出过期查询体)。
    5. 设置入口专用面板(App mode='settings'):settings.section 渲染只含 订阅管理 / FreshRSS / AI 设置 的配置面板(无「阅读」tab、无三栏工作台),首屏为订阅管理;面板内不读写浏览上下文持久化(设置里的视图切换不污染阅读面板状态);宿主版本提示条在设置面板同样可见。
  • 左侧主导航入口(一等面板):client.js 经 slots.inject('main', …) 注册中央内容页(key: 'dsh-rss')+ slots.inject('sidebar.panellist', …) 注册左栏行(id: 'dsh-rss', label: 'RSS 阅读')——「两次注册、同一身份」,行点击由宿主处理并按面板 key 键控挂载/卸载页面。契约同构本机已装插件 dsh-slidestudio(registerStandalone,其 dsh.client.inject 为空表,证明两座位随壳内置)与 dsh-context(watchInsightPage 的「Two registrations, one identity」;两座位自 DSH 0.1.5-rc.1 起随壳提供)。主面板页为桌面窗口铬条留出 --dsh-frame-top-clearance 间隙;导航停止函数由 ctx.effect 持有,模块卸载时统一注销。
  • 客户端模块(client.js)对 DSH 模块加载器声明 exports.inject = ['slots']:slots 服务就绪后才 apply,「设置 → RSS 阅读」入口稳定可用。
  • Better Sidebar(可选依赖)的右侧栏 tab 改为嵌套 ctx.inject(['betterSidebar'], scope => …) 响应式注册:未安装时跳过(设置入口与左侧主导航不受影响)、服务上线后恰好注册一次(scope.effect 持有 registerTab 注销函数)、服务下线/热重载自动注销并在恢复时重新注册——不重复、不泄漏、不丢弃注销函数。契约对齐本机核查的 DSH 生态插件用法(dshmarket src/client/index.ts 的「NESTED inject on purpose」、dsh-agent-sync index.mjs 的 reactive routes 注册)与 dsh-better-sidebar src/client/service.ts 的 TabDescriptor/重复 id 语义。
  • 验证方式:node --test(模块契约、settings 注册、左侧导航描述符/点击挂载/依赖守卫/销毁、分组树/作用域查询/批量已读作用域/真实计数/过期守卫/分页/响应式结构、七项正确性回归:折叠钮同级真实按钮与键盘语义、规范化路径→原始分组串映射、选中分组消失后的显式空作用域(含 host groups:[]/畸形 400 与 store 空作用域)、旧文章/mark/AI 迟到响应守卫与防抖窗口作废、计数失败保留与并发一致、追加加载态/失败保留/>100 条多页刷新、CSS 外壳定位与高度约束链(源码级)、晚到 provider、下线/恢复/销毁生命周期、未声明访问守卫、静态打包门禁)+ npm run check。尚未在真实 DSH GUI 里实机验证(安装仍走 GUI 插件页,装好后需重启 + 硬刷新;视觉观感以装机为准)。

已验证 vs 未验证

  • 已验证(自动化测试):RSS/Atom 解析与稳定 ID、缓存合并保留已读状态、OPML 嵌套导入/导出/去重、FreshRSS ClientLogin/订阅/分页条目流/edit-tag 写回/mark-all-read 参数/增量同步 ot 参数与游标推进(按官方协议的本地 mock 服务端全流程)、AI 请求构造(含不可信内容定界与截断、Key 不外泄)、host 路由同源防护/掩码/部分失败/groups 空数组与畸形拒绝、client 模块契约与安全渲染、左侧主导航注册/点击挂载(宿主行为模拟)、Better Sidebar 注册生命周期(按文档化 Cordis 语义的模拟运行时,非实机)、阅读工作台行为级回归(内存路由按 groups/feedId/filter/search/offset 真实过滤,覆盖作用域查询、批量已读作用域、真实计数、过期响应守卫、分页与追加、键盘/折叠可访问性、选中分组消失后的空作用域)、按订阅分文件存储的迁移/退订清理/跨订阅并发写、ETag 条件请求(首次缓存验证器 → 304 跳过解析且缓存保留)、多订阅并发刷新(时序断言)、UI 状态持久化(写入/重挂恢复/损坏数据回落/非法作用域校验)、自动刷新(默认关/开启即查/10 分钟门限内不重复)、相对时间、图片代理全链路(魔数嗅探含 SVG/伪 png 拒绝、binary 字节一致性、缓存命中不发网络请求、在途去重、LRU 修剪、路由防护 cross-site/跨源/非 GET/非法 URL、nosniff+CSP 头、renderSafe 代理/默认双模式、图片开关写回配置、旧缓存安全回退)、列表摘要行结构、拖宽手柄(拖动/钳制/键盘微调/持久化/重挂恢复)、宿主版本检测(落后→提示条+可关闭;一致→不提示)、订阅栏收起(☰ 切换/网格列增减/持久化/恢复)、双计数徽标(未读/总数、灰态总数、分组求和)、隐藏已读完(默认藏全已读订阅与分组/选中保留/切换持久化/全空 🎉 空态)、订阅栏快速添加(+/回车提交/成功清空/空地址不发请求)、刷新后重查等订阅列表落地(竞态)、专注模式(收列表全宽/退出恢复/持久化/F 键源码级)、复制链接(clipboard 写入+成功提示)、字号 A±(行内字号变化/持久化/重挂恢复)、抓取全文(提取器 article 优先/干扰元素移除/登录墙空提取/路由 404 与空提取如实报错;客户端替换与切回/切文章自动清)、刷新编排(greader+账号可用→刷新后自动同步并汇报;纯 standalone 不多发;账号不可用不发;自动刷新同样编排)、旧缓存图片恢复指引(开图+含标记→显示;含 img 或关图→不显示)、设置面板专用模式(无阅读 tab/首屏订阅管理/不写持久化;完整模式照常持久化)、播客音频(点击前零请求/点击后 audio 直连源/非音频 enclosure 不渲染)、视频嵌入(YouTube watch 与 B 站 BV+分 P 的官方嵌入地址、sandbox、点击加载、普通链接不嵌入)、列表缩略图(开启时代理 src/关闭时零请求)。
  • 未验证(需要真实环境):不同版本 DSH 宿主的兼容面(当前实机验证基于 DSH Desktop 0.2.0-rc 线);真实 FreshRSS 服务器与 OpenAI 兼容接口的长跑稳定性。安装后如遇问题见下。

故障排查

现象处理
error: profile "desktop" is managed exclusively by the Electron application设计行为:desktop profile 由 Electron 独占管理,CLI 一律拒绝。改用 GUI:侧边栏 Plugins → 添加插件 → 本地绝对路径安装(见「安装」)
设置里没有「RSS 阅读」确认 profile 已挂载(Plugins 页面能看到 dsh-rss);desktop 需重启后硬刷新浏览器
顶部出现「插件宿主仍在运行 v0.x」提示条 / 图片代理等新功能 404本地路径安装不会热更新宿主端:硬刷新只更新了客户端半边。完全退出并重启 DSH Desktop,再硬刷新浏览器(Cmd/Ctrl+Shift+R)
图片开关已开但图片仍不显示① 先看上一条(宿主旧没有 media 路由);② 旧缓存文章入库时图片已被替换为文本标记——图片开启时正文会显示恢复指引,直接点顶部「⟳ 刷新」(FreshRSS 订阅会自动继续同步)即可恢复 <img>、正文图与缩略图;③ 个别图床反盗链/需登录,代理也取不到(正常现象,打开原文可见即属此类)
Plugins 页显示不可用说明该 Host 未管理 profile(桌面版正常都管理);这属于环境异常,请如实反馈而不是绕
添加插件预检报 already-installed先在 Plugins 页卸载旧的 dsh-rss 再重新添加
刷新报「无法识别的订阅源格式」地址需直接返回 RSS/Atom XML(不是网页);自托管常见问题是反代改写/需要登录
FreshRSS 认证失败用户名 + API 密码(服务端「设置 → 认证」生成),不是登录密码;确认 API 已开启
AI 401检查 Key 与接口地址(地址填到 /v1 或不填 /v1 均可,插件会补全)
「抓取全文」报「宿主响应为空/不是 JSON」原文页面太慢(>12s)被链路中断或网关回错误页;稍后重试即可。正常页面 1–3 秒返回,慢源属个别情况
明文 HTTP 警告内网自用可接受;公网务必 HTTPS

License

MIT。产品灵感致谢 qiaomu-ai-rss(GPL-3.0-only,未复制其代码/素材/数据)。