dsh-daily
Verified@kpeace/dsh-daily · v0.6.4 · MIT · Web UI
DSH 日报插件:每日主动推荐多类型新闻,可用自然语言调教推荐偏好,提供含网页 / YouTube / 哔哩哔哩视频的高价值索引,并在右侧栏以常驻网页呈现、点击即达。
Install
dsh plugin add @kpeace/dsh-daily Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-daily · 日报
DSH 插件:每日主动推荐多类型新闻,可用自然语言调教推荐偏好,提供含网页 / YouTube / 哔哩哔哩视频的高价值索引, 并在右侧栏以常驻网页呈现,点条目直接跳转。
它解决什么
| 需求 | 实现 |
|---|---|
| (a) 每日主动推荐不同类型的新闻 | 每天定时抓取 + 按主题分组生成日报;紧凑摘要注入 systemPrompt,模型会自己提起今天有什么值得看的 |
| (b) 可自定义调教 | 右侧栏一个输入框,或直接对模型说「以后每天多推 AI Agent 相关的,少推币圈」→ 落盘偏好并立即重排 |
| (c) 高价值索引(网页 + YouTube + 哔哩哔哩) | 11 个源、跨源评分(新鲜度 × 源权重 × 主题命中 × 关键词 × 热度信号)、两段式去重;视频条目带封面·时长·播放量 |
| 展示形式 | 右侧栏「日报」tab,常驻网页;分类筛选 / 搜索 / 仅未读 / 👍👎 / 已读 / 隐藏 / 日报 Markdown 预览 / 按天翻阅 / 页脚「订阅管理」与「本周回顾」 |
采集源(按本机实测可达性筛选)
| 源 | 类型 | 本机可达 |
|---|---|---|
| Hacker News(经 hnrss) | 网页 | ✅ |
| arXiv(cs.AI / cs.CL / cs.LG / cs.CV) | 网页·学术 | ✅ |
| GitHub Search API(近 30 天新建、star 最高) | 网页·开源 | ✅ |
| YouTube(RSSHub 镜像池) | 视频 | ⚠️ 仅镜像可用,会抖动 |
| 哔哩哔哩·热门 / 排行榜 | 视频 | ✅(排行榜偶发 -352 风控) |
| 少数派 / IT之家 / Solidot / 阮一峰 | 网页 | ✅ |
自定义 RSS(配置项 extraFeeds) |
网页 | 取决于地址 |
直连 YouTube 官方 feed、Invidious、Piped、Google News、HuggingFace、GitHub Trending HTML 在本机网络下全部超时, 所以 YouTube 走 RSSHub 镜像池,GitHub 走 Search API,英文社区热度走 HN。这不是偷懒,是实测结论。
安装
从 npm(推荐)
npm install @kpeace/dsh-daily
# 或在 DSH 里:dsh plugin add @kpeace/dsh-daily
已发布:@kpeace/[email protected](https://www.npmjs.com/package/@kpeace/dsh-daily)
—— 装完仍需重启 DSH(原因见下面「改完怎么验证」一节:客户端半体在启动时组装)。
关于名字:包名为什么带
@kpeace/前缀 —— 不带前缀的dsh-daily已被另一个 npm 账号 (dushaobindoudou,描述写着 name reserved; first release in development)预留。 如果那是你自己的账号,用那个账号在仓库里跑npm publish就能发到dsh-daily这个名字上; 本包已把package.json的name与cordis.patch.yml的插入名保持一致,改回不带前缀只需同步改这两个地方。
# 1. 编译(本包依赖已在本工作区备好,直接编译即可)
cd packages/dsh-daily
./node_modules/typescript/bin/tsc -p tsconfig.json && node scripts/copy-assets.mjs
# 2. 装进 profile(**真实目录**,不要用 link:,原因见下)
node scripts/install-to-profile.mjs desktop
# 3. 重启 DSH(或让插件管理器重建 include:daily 这个 entry)
为什么不用 dsh plugin add link:(重要,踩过的坑)
dsh 的插件解析根是 $DSH_HOME/profiles/node_modules —— 里面 @deepseek-ai/* 全部是指向 dsh 安装目录的符号链接,
插件里 import { defineTool } from '@deepseek-ai/dsh-tools' 就靠这条链解析。
而 Node 的模块解析默认取符号链接的真实路径:插件被 link: 进 profile 后,真实路径落在工作区,
向上查找永远走不到 $DSH_HOME/profiles/node_modules,导入直接失败:
Cannot find package '@deepseek-ai/dsh-tools'
所以必须装成真实目录(scripts/install-to-profile.mjs 已处理,且会幂等地补上 profile 用户层 patch 的那一行 insert)。
第二个坑:Node 会缓存失败的模块实例化。如果你先踩了第一个坑(导入失败),之后即使改对了,
同一个模块 URL 仍会一直报 failed to import —— 需要换目录名(改变 URL)或重启 DSH。
另:本机 dsh plugin add / 插件市场的 install_bundle 会因 profile 的 pnpm store 版本冲突失败
(node_modules 由 store v10 链接,当前 pnpm 想用 v11),要求整树重装 —— 代价太大,所以走上面的脚本。
配置
配置项走 profile 的插件配置(Config schema,src/config.ts):
| 字段 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关 |
dataDir |
~/.dsh/daily |
数据目录 |
dailyTime |
08:00 |
每天几点抓取并生成日报 |
announceToAgent |
true |
是否把日报摘要注入 systemPrompt(= 主动推荐) |
announceMaxItems |
6 |
注入几条标题(每轮都要付的 token,别贪) |
autoRefreshOnStart |
true |
启动时若今天没数据就补抓 |
refreshIntervalMinutes |
0 |
额外周期抓取,0 = 关 |
requestTimeoutMs / concurrency |
12000 / 6 |
单请求超时 / 并发上限 |
maxItemsPerSource / dailyQuota |
8 / 120 |
每源与全天的条数上限 |
dedupeWindowDays |
3 |
跨天去重窗口(日历天)。窗口内推过的条目重罚到 ×0.2 并标注「X 月 X 日已推过」——但仍留在索引里,daily_search 搜得到。0 = 关闭 |
rsshubMirrors |
3 个镜像 | 按顺序探测,成功优先 |
youtubeChannels |
5 个 AI/技术频道 | 格式 名称|频道ID或@handle|主题 |
extraFeeds |
[] |
额外 RSS,格式 名称|URL|主题 |
enabledSources |
[](全部) |
只启用哪些内置源 |
aiSummaryEnabled |
false |
可选 AI 摘要(默认关)。打开后给当天前 N 条生成一句中文摘要,侧边栏与日报都显示 |
aiSummaryProvider / aiSummaryModel |
空 | 模型路由。两者必须成对;都留空则跟随宿主默认模型;只填一个视为未配置(不猜) |
aiSummaryMaxItems / aiSummaryConcurrency |
20 / 3 |
摘要几条 / 并发几路 |
aiSummaryMaxTokens |
1024 |
单次上限。别调太小 —— 推理 token 与正文共享它 |
aiSummaryTimeoutMs |
20000 |
单条超时 |
aiSummaryCacheTtlDays / aiSummaryCacheMax |
30 / 500 |
摘要缓存有效期与条数上限(同一新闻跨天重复时不重复付钱) |
archiveCompactAfterDays |
60 |
超过这些天的归档压成索引(丢摘要/热度,保留标题/链接/主题/分数)。0 = 不压缩 |
archiveDeleteAfterDays |
0 |
超过这些天的归档整组删除。默认 0 = 永不删除;要开就设成比上一条更大的值 |
youtubeRotateChannels |
true |
是否按 LRU 轮转抓 YouTube 频道(对抗免费镜像的 429) |
youtubeChannelsPerRefresh |
3 |
每轮抓几个频道。>= 频道总数等价于全抓 |
youtubeFailureCooldownMinutes |
20 |
频道连续失败后的退避基准分钟(按 2 的幂递增,封顶 8 倍) |
notifyEnabled |
false |
是否在定时/启动采集完成后弹一条 macOS 系统通知(手动刷新不弹)。默认关 |
notifyMaxTitles |
3 |
通知正文里列几条标题 |
用户偏好(主题 / 关键词权重 / 屏蔽词 / 源权重 / 每日上限)落在 ~/.dsh/daily/prefs.json,
由右侧栏页面和 daily_tune 工具改写 —— 不走插件配置,因为改配置会触发 profile 重载。
模型工具
| 工具 | 副作用 | 说明 |
|---|---|---|
daily_brief |
只读 | 今日日报(按主题分组、带链接与推荐理由) |
daily_digest |
只读(缺则生成) | 完整 Markdown 日报 |
daily_search |
只读 | 在最近 N 天索引里检索 |
daily_sources |
只读 | 源清单与最近一轮状态(排查「为什么没有 YouTube 条目」先看它) |
daily_tune |
写盘 + 重排 | 自然语言调教 |
daily_opml |
写盘(import)/ 可选写文件(export) | 导入 / 导出 OPML 订阅列表 |
daily_weekly |
写盘 | 生成周报(跨天汇总:最值得看 / 主题分布 / 每天条数 / 源健康) |
daily_prune |
默认只读(apply:true 才写盘) |
看归档占用;压缩/清理旧归档 |
daily_project |
写注册表(use 时) | 列出 / 查看 / 切换项目(一个项目 = 一个工作目录) |
daily_profile |
写盘(profile.json) | 采集项目画像:技术栈 / 依赖 / 关键词 / README 摘录(绝不读敏感文件) |
daily_research |
写盘(research-plan.json) | 从画像生成调研检索词(AI 增强,失败自动降级为确定性种子) |
daily_feedback |
写盘 | like / dislike / read / hide |
daily_refresh |
联网 + 写盘 | 立刻抓一轮(5~20 秒) |
可选 AI 摘要(默认关闭)
只有标题的列表要一条条点进去才知道值不值得看;打开摘要后每条带一句中文概要,扫一眼就能筛。 侧边栏右上角会出现 ✨ 按钮,随时给已抓到的当天条目补摘要,不必等第二天。
打开方式(profile 的插件配置):
aiSummaryEnabled: true
aiSummaryProvider: '' # 留空则跟随宿主默认模型;要固定就两者都填
aiSummaryModel: ''
三条从实测里来的经验(都写进了代码注释与测试):
aiSummaryMaxTokens别调小。推理 token 与正文共享这个上限:设 200 时 20 条里只有 7 条成功, 1024 时 12 条,4096 时 20 条全成功。撞上限的条目不会静默变空摘要 —— 失败原因里会写明 「模型没写出正文就用完了 maxTokens=…(终止原因 max-tokens)」。- 单条失败必须隔离。20 条挂 1 条,另外 19 条照常出;失败条数与原因会出现在提示条里。
- 按条目缓存。跨天重复的新闻命中缓存直接复用,不重复花钱。
失败面都被测试钉住了:超时、空输出、max-tokens、error/aborted、流中途抛错、
没有 finish 事件、宿主没有 llm 服务、只填了一半的路由、缓存过期与超量淘汰。
导入自己的订阅(OPML)
你多半已经在别的阅读器里订阅了几十个源 —— 一条条抄 URL 是这个插件最可能被放弃的一步。 直接给它一个 OPML 文件:
「把这个文件里的订阅导进来:
~/Downloads/subscriptions.opml」
或者直接调用 daily_opml({ action: 'import', path: '…' })。行为:
- 按 URL 去重(
?utm_source=这类参数会先被规范化掉,绕不过去) - 内置源也参与判重:你的 OPML 里若有少数派 / IT之家 / Solidot / 阮一峰的源,会被跳过 —— 它们已经在抓了,重复加入只会让同一份内容被抓两次
- 已有条目不覆盖:你手动改过的名字比 OPML 里的更可信
- 即时生效:写进
prefs.extraFeeds,下一次刷新就抓,不需要改配置、不需要重载 profile - 返回可核对的明细:解析出几条、新增几条、重复几条、跳过几条
反向也支持:daily_opml({ action: 'export' }) 把内置源 + 自定义源导出成 OPML,
可以带回你自己的阅读器。
开发脚本的一个安全约定
npm run regression # 构建 + 全部单测 —— **不写盘、不装机**
npm run regression:install # 上面这些 + 装机 + 安装校验(会改 profile,显式才做)
为什么把装机从 regression 里拆出来:它曾经隐式包含 install-to-profile。
在"用户明确要求暂不安装"的时期,我跑一次 npm run regression 就把插件装回了他的 profile
—— 一个名为"回归测试"的命令不应该有安装这种副作用。所以默认那条做成纯只读,
装机必须显式写出来。
刷新语义:合并,不是覆盖
同一天多次刷新时,当天数据是合并的(按规范化 URL 去重后取并集再统一排序),不是覆盖。 这条语义有两个直接后果,都是能感觉到的:
- 定向刷新不会冲掉别的源:
daily_refresh({ sources: ['hn'] })之后, 当天别的源(arXiv / B站 / YouTube…)的条目仍然在 - 某个源临时失败也不会丢数据:上午抓到的东西不会因为下午那次刷新里该源挂了而从列表消失
合并时继承两个字段:firstSeenAt(首次见到它的时间)与已有的 aiSummary
(避免同一天刷新两次就重新调用模型一次)。
源状态同样合并:本轮跑过的用新状态,没跑的沿用上一次。
项目自动识别(默认开启)
插件会自己认出你正在哪个项目,而不是一直停在默认项目上:
| 来源(按可信度) | 怎么用 |
|---|---|
| 当前会话的 cwd | 每轮装配 prompt 时,宿主会把当前会话的上下文递进来 —— 这是最准的来源,随便切会话就跟着变 |
| 最近活跃的会话 | 启动时读 sessions(拿不到就跳过) |
| 工作区清单 | 兜底读 workspaceRegistry |
三条设计原则
- 显式优先于推断:你用
daily_project({action:'use'})或侧边栏下拉手动选过项目后, 自动识别不再覆盖它 —— 否则会出现「我明明切过,又被它自己换回去」。 想交回自动跟随:daily_project({action:'follow'})(界面上不提供手动入口)。 - 可见:界面上的项目下拉会标出「(自动)」,悬停能看到来源与数据目录 —— 否则用户不知道"为什么它自己换了"。
- 拿不到就不猜:探不到目录、或目录不存在时保持当前项目,绝不随机挑一个。
关闭:配置 autoDetectProject: false(行为回到旧版:完全手动)。
内部实现(为什么是"探"而不是直接读字段):Session/Agent/Workspace 是拿不到编译期
类型的宿主内部对象,硬写 session.header.cwd 在宿主换版本时会静默变成 undefined。
所以按已知键名集合去探,并有严格边界:深度 ≤3 层、最多 60 个节点、只认绝对路径、
不下探消息正文(路径出现在对话里是最常见的误报来源)。
项目(一个项目 = 一个工作目录)
这个插件正在从「日报」长成「调研插件」:日报回答"今天世界上发生了什么", 调研回答"这个项目缺的那块知识在哪"。所以每一份偏好、每一天的索引、每一条反馈都按项目隔离 —— 给 A 项目调的「多推 Rust」不会污染 B 项目。
# 模型侧
「切到 ~/DEV/my-app 这个项目」→ daily_project({ action: 'use', dir: '~/DEV/my-app' })
「有哪些项目」 → daily_project({ action: 'list' })
「现在在哪个项目」 → daily_project({ action: 'current' })
侧边栏顶部只有一行只读的项目状态:显示"现在用哪个项目、怎么定的(自动识别 / 未识别到项目)"。
为什么界面不给选:项目跟着当前会话/工作区自动走。做成下拉框会诱导手动指定,
而一次手动指定就会永久关掉自动识别(见下方"显式优先于推断"),界面上却看不出来 —— 那是个陷阱。
需要显式指定时走 daily_project 工具(高级用法,不在界面上暴露)。
| 事实 | 说明 |
|---|---|
| 项目身份 | 归一化后的绝对路径(~、相对路径、结尾斜杠、.. 都归一到同一个身份) |
| 数据落点 | 默认项目 = 数据根目录本身(老数据零迁移);其它项目 = <root>/projects/<slug>-<hash8>/ |
| 目录命名 | slug 给人看(dsh-plugins),hash8 保证唯一(不同父目录下的同名项目不会撞) |
| 注册表 | <root>/projects.json,记录当前项目与已知项目(跨重启保留) |
| 失效目录 | 项目目录被删/改名后,启动时安全回落默认项目,不会让插件打不开 |
| 隔离范围 | 偏好 / 反馈 / 每日索引 / 归档 / 轮转状态(各自的 meta.json)全部独立 |
为什么要按目录隔离而不是一个全局配置:调研的关键词高度依赖项目 —— 在写前端的仓库里想看到的是 UI 相关的进展,在写插件的仓库里想看到的是宿主 API 的变更。 一个全局配置只会在两个项目之间来回打架。
调研检索词(调研的入口)
画像回答"项目是什么",检索词回答"该去搜什么"。daily_research 从画像推出这批词:
「给当前项目生成调研检索词」→ daily_research() # force:true 可忽略缓存重算
真实输出(项目 = msim-front-react):
[ai ] react 19 迁移注意事项
[ai ] zustand v5 破坏性变更
[ai ] vite 6 环境变量约定
[keyword] <项目领域词> 最新进展
[dep ] @ant-design/icons changelog release notes
[tech ] TypeScript best practices
设计要点
| 决策 | 为什么 |
|---|---|
| 没有模型也要能用 | 检索词是整条链的入口;强依赖一次模型调用意味着超时/未配置/"点了没反应"就等于功能不可用。默认先给确定性种子(技术栈/依赖/领域词直接拼),AI 只做增强 |
| AI 失败 = 降级,不是失败 | 超时、抛错、输出跑偏、流以 reason.failure 结束 —— 四种情况全部降级到种子,并把真实原因写进 notes 让用户看见 |
| 只认结构化输出 | 提示词要求 JSON 数组;模型回散文时宁可降级也不硬猜(单行"这里没有任何可用检索词"曾被当成一条检索词 —— 用户会拿到垃圾还不自知) |
| 缓存键是画像指纹 | 画像变了才重新生成;只靠时间会导致"项目没变却反复花模型调用" |
| 种子按"贴项目程度"排序 | 关键词 → 依赖 → 技术栈。合并会按条数截断,排在最后的先被砍;早先把关键词排最后,真实联调时它们一条不剩 |
| 依赖按族去重 | 真实依赖表前几位常是 @ant-design/icons / plots / pro-components,取前 3 个等于用三次搜索问同一件事 |
| 过滤结构词 | 画像高频词里混着 src / pages / context / tech / task.md 这类任何项目都会高频的词;「context 最新进展」搜不出与项目有关的东西 |
当前的瓶颈已经不在检索词逻辑,而在画像的关键词提取质量: 真实项目抽出来的领域词仍偏弱(
experiment/portal/项目/目录)。 下一轮该做的是让 README 的正文词汇权重高于 manifest 与目录名 —— README 才携带领域词表。
项目画像(调研的输入)
调研要回答"这个项目缺的那块知识在哪",所以先得知道"这个项目是什么"。
daily_profile 从项目目录里抽出一小段画像 —— 它是「AI 生成检索词」的输入:
「给当前项目做个画像」→ daily_profile() # 也可以用 verbose:true 看读了什么/跳过了什么
画像内容(真实输出,本插件自己的目录):
项目:dsh-daily
工作目录:/Users/kp/DEV/dsh-plugins/packages/dsh-daily
技术栈:TypeScript(tsconfig.json)、Node.js(package.json)
主要依赖:@deepseek-ai/cordis, @deepseek-ai/dsh-tools, @deepseek-ai/schemastery, dsh-better-sidebar, typescript
关键词:dsh、node、项目、scripts、目录、youtube、日报、失败、插件、默认、deepseek-ai、数据、摘要、索引、频道…
顶层结构:CHANGELOG.md, PLAN.md, README.md, cordis.patch.yml, lib, package.json, scripts, src, test, tsconfig.json, web
README 摘录:…
(画像依据:读了 4 个文件 / 30.3KB;跳过 0 项,其中敏感文件 0 个)
三条硬约束(都来自「这份数据会被送进模型」这个事实)
| 约束 | 做法 |
|---|---|
| 绝不读敏感文件 | .env*、*.pem、id_rsa*、credentials*、.npmrc、.git-credentials… 按文件名先挡。不读才是真的安全——「读了再过滤」迟早会漏 |
| 有界 | 单文件 64KB / 总计 256KB / 最多 24 个文件;只读顶层的少量已知文件(README、manifest、CI 配置),不做递归遍历 |
| 可审计 | 返回里明确列出读了哪些文件、跳过什么、为什么。用户有权知道插件为了「了解项目」到底看了什么 |
daily_profile({ verbose: true }) 会把完整清单打出来。画像落盘在
<项目数据目录>/profile.json(只读顶层、不递归,也不会因为目录里有几 G 的构建产物而变慢)。
桌面通知(macOS,默认关闭)
「每日主动推荐」最字面的形态:采集完成后弹一条系统通知,不用打开任何东西也能知道今天有什么。
notifyEnabled: true
notifyMaxTitles: 3
通知长这样:
日报 · 2026-10-12
67 条索引 · 60 未读:DeepSeek Solved AI's Memory Problem · Passkeys are a trap · 派早报:…
三条设计原则:
- 默认关闭,且只在"定时/启动"时弹。手动刷新(点按钮、调工具)不弹 —— 那时你本来就在看着界面,再弹一条纯属噪音。这是"通知很烦"最常见的来源。
- 标题用
pickDiverse挑(主题轮转 + 每源上限),否则三条标题很可能全来自同一个源 —— 而"推荐不同类型"正是你最初的诉求。 - 文本走 argv,不拼进 AppleScript 源码。新闻标题里出现引号/反斜杠/换行是常态, 拼接字符串总会有漏网的(而那种 bug 的表现是"脚本报错、通知不出现",很难查)。
已知边界:宿主没有任何通知服务(@deepseek-ai/ 下没有 notifier/toast 包),
所以只能落到系统层(osascript)。实测 osascript 返回 0(调用有效),
但横幅是否真的显示取决于 macOS 的通知权限——这我无法从代码侧确认。
如果你开了开关却没看到通知,去「系统设置 → 通知」里找一个叫 Script Editor 的条目并允许通知。
非 macOS 平台会安全跳过(日志里写明原因)。
往回翻:归档与周报
「高价值索引」意味着能往回翻,而不是只看今天。两条路径:
- 按天翻:侧边栏标题下方有一条日期导航(
‹ 2026-10-12 ›+ 「回到今天」)。 只在存在多天归档时才出现。翻到历史那天时:统计行会显示「历史数据 · 刷新只抓今天」, 刷新按钮置灰(抓取总会写进当天),日报预览会取那一天的 Markdown。 - 按关键词找:直接问模型「上周有没有关于 xxx 的东西」,它会调
daily_search。
周报:对模型说「生成本周周报」,或 daily_weekly({ days: 7 })。落盘到
~/.dsh/daily/digest/week-<起>_<止>.md,包含四节:
| 小节 | 回答什么问题 |
|---|---|
| 本周最值得看 | 这周最好的东西是什么(跨天全局排序) |
| 按主题 | 我这周的注意力花在哪个方向(含每主题的代表条目与占比) |
| 每天 | 哪天信息量大、哪天断了(条数骤降通常意味着采集出问题) |
| 采集源 | 哪几个源失败了(避免"某周少了 YouTube"被悄悄吞掉) |
长期运行:归档容量
这个插件是要每天跑、长期跑的。days/*.json 一天约 50KB —— 一年 ~18MB,
而且 daily_search / 归档索引 / 周报都要读它们。所以有分级保留:
| 归档年龄 | 处理 | 保留什么 |
|---|---|---|
最近 60 天(archiveCompactAfterDays) |
完整保留 | 摘要、热度、推荐理由、封面 |
| 更早 | 压缩成索引 | 标题 / 链接 / 主题 / 来源 / 分数 / 已读状态 / AI 摘要 |
更早且显式开启删除(archiveDeleteAfterDays,默认 0) |
整天删除 | 数据文件与那天的日报一起删 |
实测压缩效果(本机真实数据,120 天 × 67 条):
未压缩:49.6 KB/天 → 压缩后:26.8 KB/天 (-46%)
全年外推:17.7 MB → 9.6 MB
为什么默认不删除:数据不可再生,一个「自动清理」的默认值不该由插件替用户决定。 压缩是安全的(只丢详情、不丢索引),删除必须由你显式开启。
整理自动进行(每天最多一次,挂在采集完成之后,你不需要管);也可以手动:
「看看日报数据占了多少地方」→
daily_prune(默认只预览、不写盘) 「把旧归档压缩一下」→daily_prune({ apply: true })
联调脚本(scripts/dev-server.mjs)
不进 dsh 也能把界面跑起来,且不会碰你的真实数据:
node scripts/dev-server.mjs \
--data-dir=/tmp/dsh-demo \ # 用临时数据目录(默认 ~/.dsh/daily)
--seed-history=4 \ # 把今天的数据复制成过去 4 天,用来验跨天界面
--ai-fake \ # 打开 AI 摘要并注入假 stream(不花 token)
--fail-api=state \ # 让指定接口返回 503,用来验错误态
--no-refresh # 启动时不抓取
--fail-api 这类故障注入开关是有意做的:错误态(红条文案、能否重试、会不会卡死)
是用户真会碰到的状态,但正常开发流程里几乎无法自然触发 —— 不注入就只能靠"看代码觉得没问题"。
数据与文件
~/.dsh/daily/
├── prefs.json # 用户偏好(调教的落点)
├── feedback.json # 反馈(跨天累积,用于学习)
├── meta.json # 最近刷新 / 镜像健康度
├── days/YYYY-MM-DD.json # 当天的全部条目与源状态
└── digest/YYYY-MM-DD.md # 生成的日报 Markdown
开发
node scripts/probe-sources.mjs # 真实采集探针(不落盘)
node scripts/probe-sources.mjs hn youtube
node scripts/dev-server.mjs --port 8799 # 不开 DSH 联调页面与接口
node scripts/tune-demo.mjs "以后多推开源" # 调教前/后 Top10 对照
npm test # tsc 构建 + node --test(基线 69 组全绿)
scripts/dev-server.mjs 把同一个 mountRoutes 挂到裸 http server 上,所以 UI 迭代不需要装进 profile。
改完怎么验证(重要,这条决定你多久能看见效果)
| 你改了什么 | 怎么验证 | 要重启 DSH 吗 |
|---|---|---|
src/collect/*、src/rank.ts、src/tune.ts、src/render.ts 等纯逻辑 |
npm test + node scripts/probe-sources.mjs / tune-demo.mjs |
不需要(新进程) |
web/*(页面) |
dev-server.mjs 起页面,浏览器刷新 |
不需要(路由直出静态文件,no-store) |
src/api.ts、src/tools.ts、src/prompt.ts、src/schedule.ts(host 运行时行为) |
只能 dev-server.mjs 验接口;工具与注入要看真实会话 |
需要 |
src/client.js(右侧栏 tab) |
只能在真实 GUI 里看 | 需要(客户端半体启动时组装) |
为什么 host 改动必须重启:Node 会缓存已成功导入的 ESM 模块,
plugin_manager set_plugin 的 enable 切换只是重建 fiber、不会重新读盘。
(顺带一个更具欺骗性的现象:如果某个模块 URL 第一次导入失败过,
那个失败也会被缓存,修好之后同名目录仍一直报 failed to import —— 换目录名或重启才能解开。)
完整回归清单(每次收口跑一遍)
cd packages/dsh-daily
npm run regression # 构建 → 单测(当前 156 组)→ 装机 → 逐文件校验安装(15 项)
npm run verify:install # 只跑安装校验;额外验证重复装机幂等(--idempotent)
npm run probe # 真实采集(联网,约 8~70s;不读也不写你的数据)
npm run regression 里的安装校验(scripts/verify-install.mjs)检查的是几类
**"装完看起来没事、跑起来才炸"**的错:
| 检查 | 不检查会怎样 |
|---|---|
| 装的是真实目录、目录内无符号链接 | 符号链接让真实路径落回工作区 → @deepseek-ai/* 解析不到 → 插件导入直接失败 |
$DSH_HOME/profiles/node_modules/@deepseek-ai/* 存在 |
同上,解析链的根 |
| profile 里的代码与工作区构建产物逐文件一致 | 改了代码忘了重装 —— 你会对着新代码查半天,而跑的一直是旧的(最容易犯、最难发现) |
lib/client.js 存在且含 __ModuleLoader__.load |
右侧栏 tab 消失,而 host 侧一切正常 |
patch 里 insert 行存在、名字与目录匹配、不重复 |
文件都在,但 profile 根本不加载它 |
index.html 引用的本地资源都在 |
页面白屏或样式丢失 |
| 安装副本能被 Node 独立导入 | 最贴近"dsh 真的会加载它"的验证 |
脚本会对每个场景分别报原因(例如解析失败时会说清是「符号链接绕回工作区」还是「共享 node_modules 缺失」, 因为这两者的修法完全不同)。
小贴士:验证脚本自身也会失效。H5 复验时我一度以为"点分类 chip 后列表永久卡住",
其实是我的脚本把 chip 节点存进了变量,而 renderChips 每次筛选都会重建节点 ——
点的是已脱离 DOM 的旧节点。先排除验证方法本身的错,再断言产品有 bug。
已知限制
- YouTube 封面在本机网络下不可达(
i.ytimg.com被墙):条目仍然正确(标题/链接/频道/时间), 封面显示为纯色占位块。实现上是「占位块常驻底层 + 图片进入视口才插入并盖在上面」, 所以不存在"超时误杀"——挂住的请求只是永远盖不上来而已(详见 CHANGELOG 里 R12 那一条)。 - YouTube 依赖第三方 RSSHub 镜像,镜像会 429/503。默认按 LRU 轮转抓取(每轮 3/5 个频道,
按 2 的幂退避连续失败的频道)—— 这是为了对抗镜像限流:改成轮转前实测出现
「一次 2/5 频道失败、一次整源 0 条」,改成轮转后连续两轮 0 失败。
不会漏内容:RSS 返回的是频道最近 ~15 条投稿而非"今天的投稿",
5 个频道每轮抓 3 个 ≈ 两天覆盖一轮。
本轮没抓的频道会写进源状态(例:
✅ YouTube · 24 条(本轮轮转抓取 3/5 个频道:…(按轮转下次优先))), 日报的「采集源状态」与侧边栏统计项的 tooltip 都能看到 —— 条数变少是轮转,不是故障。 要回到"每轮全抓"就设youtubeRotateChannels: false。B站的视频索引不受影响。 - 哔哩哔哩排行榜偶发
-352风控(已退避重试,仍可能失败)。 - 侧边栏 tab 需要 DSH 重启后才会出现:客户端半体在启动时组装,不像 host 路由可以实时挂载。
计划与进度
下一站:从「日报」变成「调研插件」(按用户 2026-10-12 的新要求)
—— 按项目切换目录,用 AI 搜索与当前项目有关的新闻与知识来补充项目。
项目 = 工作目录(与 DSH 的 workspace 对齐),每个项目一套独立状态。
六轮增量与"明确不做"的范围见 PLAN.md §2.5。
- 开发计划:
PLAN.md(按小时的第一波 6 小时节奏) - 进度台账:
STATUS.md(每轮必更新) - 竞品侦察:
../../docs/DAILY-PLUGIN-RECON.md
运维:改完为什么必须重启
DSH 的宿主半体是 Node ESM 模块,加载成功后会被缓存。所以:
- 改
src/(宿主半体)后,plugin_manager的 enable 开关不会重新读盘 → 必须重启 dsh client.js(客户端半体,右侧栏那个 tab)本来就在启动时被扫进模块图 → 也必须重启
验证改动的正确姿势(不重启也能验):
npm test—— tsc 构建 + 全量单测(当前 337 组)node scripts/dev-server.mjs --data-dir=/tmp/x --no-refresh --port 8851—— 起一个独立的假宿主, 页面在http://127.0.0.1:8851/dsh-daily/,可对照真实页面行为- 跑着的实例可以直接读接口(不需要 token):
GET /dsh-daily/api/state—— 状态、项目、识别诊断(project.detect)GET /dsh-daily/api/findings/POST—— 调研结果 / 立刻跑一轮
装机与幂等
node scripts/install-to-profile.mjs desktop # 装进 desktop profile(真实目录,非符号链接)
node scripts/verify-install.mjs desktop # 装机校验(17 项)
bash scripts/verify-idempotent.sh desktop # 幂等性验证(沙箱里跑两次,断言产物一致)
⚠️ 自己写脚本装到沙箱时,必须显式设
DSH_HOME,只改HOME挡不住。 装机脚本解析主目录的顺序是process.env.DSH_HOME ?? homedir()/.dsh, 而 DSH 会给子进程设好DSH_HOME—— 只改HOME仍会写进真实 profile。 (这个坑我是用真实事故换来的:一次"沙箱"试跑把 0.5.1 装进了真实 profile, 发现后已撤销。verify-idempotent.sh因此一上来就强制覆盖DSH_HOME。)