订阅分组读文章,AI 摘要翻译并同步 FreshRSS
30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/7 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add weixshaw/dsh-rss该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · browser
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 28 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 49 处,展示前 44 条)
- 发起网络请求client.js:214
return fetch('/dsh-rss/' + method, { - 发起网络请求client.js:222
return fetch('/dsh-rss/' + method, { - 写入剪贴板client.js:1115
navigator.clipboard.writeText(u).then(function () { done(true) }, function () { done(legacy()) }) - 写入本地文件lib/config.js:56
await mkdir(dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/config.js:57
try { await chmod(dir, 0o700) } catch { } - 读取本地文件lib/config.js:63
text = await readFile(join(dir, 'config.json'), 'utf8') - 写入本地文件lib/config.js:80
await writeFile(tmp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }) - 写入本地文件lib/config.js:81
try { await chmod(tmp, 0o600) } catch { } - 写入本地文件lib/config.js:82
await rename(tmp, file) - 删除本地文件lib/config.js:84
await unlink(tmp).catch(() => { }) - 读取本地文件lib/config.js:108
const data = JSON.parse(await readFile(join(dir, 'account.json'), 'utf8')) - 写入本地文件lib/config.js:122
await writeFile(tmp, `${JSON.stringify({ accountKey: accountKey ?? null })}\n`, { mode: 0o600 }) - 删除本地文件lib/config.js:126
await unlink(tmp).catch(() => { }) - 读取本地文件lib/store.js:32
text = await readFile(file, 'utf8') - 写入本地文件lib/store.js:67
await writeFile(tmp, `${JSON.stringify(data)}\n`, { mode: 0o600 }) - 写入本地文件lib/store.js:68
try { await chmod(tmp, 0o600) } catch { } - 写入本地文件lib/store.js:69
await rename(tmp, this.file) - 删除本地文件lib/store.js:72
await unlink(tmp).catch(() => { }) - 删除本地文件lib/store.js:122
await unlink(join(this.articlesDir, `${safe}.json`)).catch(() => { }) - 写入本地文件lib/store.js:135
await mkdir(this.dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/store.js:136
try { await chmod(this.dir, 0o700) } catch { } - 写入本地文件lib/store.js:137
await mkdir(this.articlesDir, { recursive: true, mode: 0o700 }) - 读取本地文件lib/store.js:154
for (const name of await readdir(this.articlesDir).catch(() => [])) { - 删除本地文件递归lib/store.js:664
await rm(this.dir, { recursive: true, force: true }).catch(() => {}) - 发起网络请求lib/http.js:49
res = await fetch(url, { - 发起网络请求lib/greader.js:70
res = await this.fetch(`${this.base}${path}`, { - 发起网络请求lib/greader.js:90
const res = await this.request('/accounts/ClientLogin', { - 发起网络请求lib/greader.js:108
const res = await this.request('/reader/api/0/token', { headers: this.authHeaders() }) - 发起网络请求lib/greader.js:116
const res = await this.request('/reader/api/0/subscription/list?output=json', { headers: this.authHeaders() }) - 发起网络请求lib/greader.js:148
const res = await this.request(path, { headers: this.authHeaders() }) - 发起网络请求lib/greader.js:170
const res = await this.request('/reader/api/0/edit-tag', { - 发起网络请求lib/ai.js:58
res = await this.fetch(`${this.base}/chat/completions`, { - 读取本地文件lib/media.js:58
data = await readFile(this._fileOf(url)) - 删除本地文件lib/media.js:64
await unlink(this._fileOf(url)).catch(() => {}) - 发起网络请求lib/media.js:71
fetch(url, fetchImpl) { - 写入本地文件lib/media.js:105
await mkdir(this.dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/media.js:106
try { await chmod(this.dir, 0o700) } catch { } - 写入本地文件lib/media.js:109
await writeFile(tmp, buffer, { mode: 0o600 }) - 写入本地文件lib/media.js:110
try { await chmod(tmp, 0o600) } catch { } - 写入本地文件lib/media.js:111
await rename(tmp, file) - 读取本地文件lib/media.js:113
const names = (await readdir(this.dir)).filter((n) => n.endsWith('.img')) - 读取本地文件lib/media.js:116
const st = await stat(join(this.dir, n)).catch(() => null) - 删除本地文件lib/media.js:126
for (const n of doomed) await unlink(join(this.dir, n)).catch(() => {}) - 发起网络请求lib/media.js:133
return this.fetch(url, fetchImpl)
扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 分组订阅
- FreshRSS账号同步
- AI摘要翻译问答
README
由 DeepSeek 最新模型翻译生成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。
展开完整文档(README 剩余 1 节)
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 提取正文(优先 、去导航/页脚/脚本,图片保留裸 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 的裸 ,src 仅 http/https、支持 data-src 懒加载回退、属性值转义),浏览器端再用白名单标签树渲染(绝不使用 innerHTML);图片默认完全不加载(占位文本),开启「图片代理」后 src 只指向本地 /dsh-rss/media 路由,永不直连图床。
- 所有 /dsh-rss/ JSON 路由要求自定义 X-DSH-RSS: 1 头(跨站表单/简单请求无法伪造),且带 Origin 时强制与 Host 同源;请求体上限 512KB。图片代理路由是 GET 资源( 无法携带自定义头),改用 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 把 重建为只含校验过的 src/alt 的裸标签(src 仅 http/https 绝对地址、data-src 懒加载回退、alt/src 属性值转义;非法/危险协议 src 退回 [图片: …] 文本标记);htmlToText(AI/搜索/摘要路径)不变,图片仍回文本标记。升级注意:旧缓存文章入库时图片已被替换为文本标记,升级后需要重新刷新/重新订阅才能拿到带 的新内容(已读状态按稳定 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(防旧缓存/绕过);代理模式渲染 (绝不出现远程直连 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 白名单 → 阅读栏「▶ 加载播客音频」点击后渲染 (点击前零网络请求;音频直连源、不过图片代理——音频文件大且非图片魔数)。
3. YouTube/B 站嵌入:文章链接命中白名单(YouTube watch/shorts/youtu.be 的 11 位 ID、B 站 BV 号+分 P)→「▶ 在阅读器内播放视频」点击后加载官方嵌入播放器(iframe sandbox 白名单、referrerPolicy、16:9 占位);有音频 enclosure 时优先音频,其余站点一律不嵌入。
4. 列表缩略图:图片开关开启时取文章首图(入库后的裸 正则提取)经本地代理渲染 64px 圆角缩略图(object-fit:cover,行内 grid 第二列);开关关闭时零缩略图请求。
5. FreshRSS 增量同步:游标(上次同步开始毫秒)持久化在 state.json 的 meta.greaderLastSyncMs;条目流带 ot(秒)只拉新内容;同步全程成功才推进游标(推送部分失败下次从旧游标重放,宁重复不遗漏);首次同步仍为全量(4 页 × 100 条)。
- 刷新语义修正与设置面板(本迭代第六批,逐项带自动化回归):
1. 「⟳ 刷新」编排修正:存在 greader 订阅且 FreshRSS 账号可用(configured + enabled + 无需重置)时,独立订阅刷新之后自动继续账号同步并汇报同步结果;全部订阅都是 FreshRSS 的用户不再被「没有可刷新的独立订阅」误导(该报错只留给既无独立订阅又不可同步的情况)。自动刷新(静默路径)同样编排。
2. 旧缓存图片恢复指引:图片开关开启且正文仍含 [图片 文本标记(旧版入库特征)时,阅读栏显示指引——点「⟳ 刷新」重新拉取即可恢复正文图与缩略图;新缓存(含 )或图片关闭时不显示。
- 三栏展开收起与阅读增强(本迭代第七批,逐项带自动化回归):
1. 三栏独立展开/收起:订阅栏顶栏「☰」开关(宽屏;窄屏仍是抽屉按钮,两者互不干扰),收起即不渲染、网格列动态生成;专注模式收起列表栏、阅读全宽(工具栏「⇥ 专注」按钮或 F 键;仅打开文章时生效,未打开文章列表始终可见);阅读层收起 = 既有「← 返回」/Esc。两者均持久化、随面板重挂恢复。
2. 抓取全文:host lib/fulltext.js 轻量正文提取(多个 取文字量最大者且需达标、否则 <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 订阅会自动继续同步)即可恢复 、正文图与缩略图;③ 个别图床反盗链/需登录,代理也取不到(正常现象,打开原文可见即属此类) |
| 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,未复制其代码/素材/数据)。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
weixshaw/dsh-rss
订阅分组读文章,AI 摘要翻译并同步 FreshRSS
相关插件 · 浏览器
智能体插件框架deepseek-ai/deepseek-harness
DeepSeek Harnessdsh是由 DeepSeek AI 开发的开源 agent harness智能体框架。
★ 270,422已汉化DSH
71.9
多设备 Agent 看板YaoApp/yao
✨ 所有 Agent 和工作区集中管理,覆盖你的每一台设备。任务看板追踪进度,桌面端、移动端、浏览器、API…
★ 8,147已汉化DSH
71.2
可扩展侧边栏工作台omdsh-dev/DSH-better-sidebar
DSH 的双工作台:原生右侧栏 + 自绘底部面板,并把 ctx.betterSidebar 服务开放给所有插件
★ 4,761已汉化DSH
71.1
浏览器自动化桥Tencent/BrowserSkill
你的 Agent。你的浏览器。连接,就能协作。
★ 9,967已汉化DSH
71
AI 小说创作工作台EthanYoQ/AI-Novel-Writer
AI 小说作家 / AI Novel Writer
★ 1,591已汉化DSH
70.7
智能体浏览器引擎platonai/Browser4
DeepSeek Harness 集成
★ 1,175已汉化DSH
70.4
这是你的仓库?认领条目