Obsidian RSS Dashboard 二次开发完整教程
从零开始搭建开发环境、修复 bug、自定义功能,到管理你自己的开发版插件。适用于 Windows + Obsidian 桌面端。
📋 目录
- 环境准备与首次构建
- 解决 esbuild 安装脚本被拦截的问题
- 用 pnpm 节省磁盘空间(可选)
- 开发工作流与热重载
- 自定义功能:给列表加"原文"按钮
- 修复:Feed View 分组重复显示
- 自定义日期格式
- 解决 favicon 空容器残留
- 保护你的修改不被插件更新覆盖
- 理解插件的存储模式
- 常见问题排查
1. 环境准备与首次构建
需要安装的工具
- Node.js(含 npm)—— 建议 LTS 版本
- Git
- 一个独立的测试库(Vault)——千万不要在主力库里调试
步骤
1. 把插件源码放到测试库的插件目录
推荐路径:
<你的测试库>\.obsidian\plugins\rss-dashboard\把源码文件夹整个放进去(或复制一份)。不要放到主力库。
2. 安装依赖
在插件目录打开终端(CMD 或 PowerShell):
npm install3. 启动开发编译
npm run dev看到下面这两行就说明成功了:
Watch mode active: main.js and styles.css are being watched.
[watch] build finished, watching for changes...然后终端会停在那里不动——这是正常的,它在监听你的代码改动。改一次代码,它自动重编译一次。不要关这个终端。
4. 在 Obsidian 里加载
- 打开测试库 → 设置 → 第三方插件 → 关闭安全模式
- 启用 RSS Dashboard
2. 解决 esbuild 安装脚本被拦截的问题
新版 npm(11.17+)默认阻止安装脚本自动执行。esbuild 装完后需要跑 postinstall 下载原生可执行文件,被拦了就会导致后面编译报错。
现象
npm install 后出现:
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts esbuild@0.28.2 (postinstall: node install.js)解决
依次执行:
npm approve-scripts esbuild如果它弹出 allow/deny 选项,选择 allow。
然后强制重装:
npm install esbuild --force验证
npx esbuild --version能输出版本号(如 0.28.2)就成功了。
3. 用 pnpm 节省磁盘空间(可选)
如果你要二开多个插件,npm 会为每个插件存一份重复的 node_modules,很快占满磁盘。pnpm 用硬链接共享同一份依赖,省空间且更快。
npm install -g pnpm之后在插件目录用 pnpm 代替 npm:
pnpm install
pnpm run dev4. 开发工作流与热重载
日常开发只需一条命令
npm run dev它只跑 esbuild,秒级编译,不跑 lint、不跑类型检查、不跑合规检查。改完保存,Obsidian 自动生效(配合 Hot-Reload 插件)。
强烈建议安装 Hot-Reload 插件
装完后,你每次保存代码,Obsidian 会自动重新加载插件,不用手动禁用/启用。
关于 npm run build
npm run build 是发版用的完整流水线,包含合规检查、lint、类型检查、生产打包,又慢又严格。日常开发不要用它。
如果 npm run build 卡住,通常是 ESLint 卡在某个文件上。用 --debug 单独跑:
npx eslint . --max-warnings=0 --debug它会实时打印正在处理的文件,最后停在哪个文件,就是元凶。
5. 自定义功能:给列表加"原文"按钮
目标
在文章列表的每条信息里,加一个"在浏览器打开原文"的按钮。目前这个按钮只在阅读页有。
定位
列表里所有按钮(已读、保存、星标、标签)都在这个文件里统一创建:
src/components/article-list/utils/article-actions.ts改这一个文件,list / card / feed 三种视图同时生效。
改动
在文件里新增一个函数,并在 createActionButtons 里调用。
新增函数(放在 createTagsToggle 后面):
export function createOpenInBrowserButton(
arg: Pick<CreateActionButtonArgs, "article" | "actionToolbar" | "callbacks">,
): HTMLElement {
const openButton = arg.actionToolbar.createDiv({
cls: "rss-dashboard-open-in-browser clickable-icon",
attr: {
title: "Open original article in browser",
role: "button",
tabindex: "0",
"aria-label": "Open original article in browser",
},
});
setIcon(openButton, "external-link");
if (!openButton.querySelector("svg")) {
openButton.textContent = "↗";
}
const handleOpen = (e: Event) => {
e.stopPropagation();
e.preventDefault();
if (arg.callbacks.onOpenInBrowser) {
arg.callbacks.onOpenInBrowser(arg.article);
return;
}
// Fallback: if the host did not provide a callback, open the raw link.
if (arg.article.link) {
window.open(arg.article.link, "_blank");
} else {
new Notice("This article has no original link.");
}
};
toggleClickableIcon(openButton, handleOpen);
return openButton;
}修改 createActionButtons:
export function createActionButtons(arg: CreateActionButtonArgs): void {
createReadToggle(arg);
createOpenInBrowserButton(arg); // ← 新增这一行
if (arg.mode === "minimal-read") {
return;
}
createSaveButton(arg);
createStarToggle(arg);
createTagsToggle(arg);
}要点
e.stopPropagation()必须加,否则点按钮会触发"进入阅读视图";- 优先用
callbacks.onOpenInBrowser,复用插件已有的"打开原文"逻辑; external-link是 Obsidian 内置图标名。
6. 修复:Feed View 分组重复显示
现象
Feed View(大卡片流)里,点击作者展开后,列表重复显示两遍作者/网站。List View 和 Card View 正常。
根因
article-list.ts外层按articleGroupBy分了一次组,渲染了分组头;feed-view.ts内部又写死按"feed"分了一次组,又渲染一次分组头。
两层分组叠加,就出现了重复标题。
修复
让 feed view 自己承担全部分组职责,不再被外层包一层。
第 1 步:view-types.ts 加两个字段
export interface ViewDeps {
// ... 原有字段
/** Resolve a feed URL to its folder path (used by "folder" grouping). */
getFeedFolder?(feedUrl: string): string | undefined;
}
export interface BaseViewContext {
// ... 原有字段
/** Grouping mode; feed view uses this to decide how to split sections. */
articleGroupBy?: "none" | "feed" | "date" | "folder";
}第 2 步:feed-view.ts 的分组改为读配置
把 renderFeedView 里的写死分组改成:
export function renderFeedView(
container: HTMLElement,
articles: FeedItem[],
ctx: BaseViewContext,
deps: ViewDeps,
): void {
const groupBy = ctx.articleGroupBy ?? "feed";
// "none" grouping: no collapsible sections, just flat feed cards.
if (groupBy === "none") {
for (const article of articles) {
renderArticleCard(container, article, ctx, deps);
}
return;
}
// Group articles by the configured grouping mode.
const groupedArticles = groupArticles(articles, groupBy, (feedUrl: string) =>
deps.getFeedFolder?.(feedUrl),
);
// ... 后面的 section 渲染逻辑保持不变
}第 3 步:article-list.ts 里 feed view 不套外层分组
找到 renderArticles() 里的分组渲染块,把开头改成:
if (this.settings.viewStyle === "feed") {
// Feed view owns its own grouping and collapsible sections.
// Do NOT wrap it in the outer grouping layer, or section headers duplicate.
this.renderFeedView(articlesList, this.articles);
} else if (this.settings.articleGroupBy === "none") {
if (this.settings.viewStyle === "list") {
this.renderListView(articlesList, this.articles);
} else {
this.renderCardView(articlesList, this.articles);
this.scheduleCardTagLayout(articlesList);
}
} else {
// ... 外层分组逻辑保持不变(只给 list / card 用)
}第 4 步:article-list.ts 传参
getBaseViewContext() 加:
articleGroupBy: this.settings.articleGroupBy,getViewDeps() 加:
getFeedFolder: (feedUrl) => this.getFeedFolder(feedUrl),效果
- Feed View 保留折叠 section,同时支持
none / feed / date / folder四种分组; - 不再重复显示作者/网站;
- List / Card 行为不变。
7. 自定义日期格式
需求
列表里的日期:最近显示相对时间("3 小时前"),超过 48 小时显示 2026.09.13 11:20(24 小时制)。
改 src/utils/platform-utils.ts
只改 formatArticleDate,其他函数不动(formatDateWithRelative 被日期分组使用,动了会影响分组)。
新增一个私有辅助函数:
/**
* Format a date as "YYYY.MM.DD HH:mm" using 24-hour time.
*/
function formatAbsoluteDateTime(date: Date): string {
const pad = (n: number): string => String(n).padStart(2, "0");
return (
`${date.getFullYear()}.${pad(date.getMonth() + 1)}.${pad(date.getDate())}` +
` ${pad(date.getHours())}:${pad(date.getMinutes())}`
);
}替换 formatArticleDate:
/**
* Returns { text, title } for an article date.
*
* Display rule:
* - < 48 hours ago: relative text ("刚刚", "N 分钟前", "N 小时前", "1 天前")
* - >= 48 hours ago: absolute text "2026.09.13 11:20"
*
* The `title` is always the full absolute timestamp for hover tooltips.
*
* NOTE: Grouping still uses formatDateWithRelative() — this function is for
* display only, so changing it will not affect "group by date" keys.
*/
export function formatArticleDate(
date: Date | string,
_style: "relative" | "absolute" = "relative",
): { text: string; title: string } {
const targetDate = typeof date === "string" ? new Date(date) : date;
if (isNaN(targetDate.getTime())) {
return { text: "Invalid date", title: "Invalid date" };
}
const absoluteText = formatAbsoluteDateTime(targetDate);
const diffMs = Date.now() - targetDate.getTime();
// Future dates: treat as "just now"
if (diffMs < 0) {
return { text: "刚刚", title: absoluteText };
}
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
if (diffMins < 1) {
return { text: "刚刚", title: absoluteText };
}
if (diffMins < 60) {
return { text: `${diffMins} 分钟前`, title: absoluteText };
}
if (diffHours < 24) {
return { text: `${diffHours} 小时前`, title: absoluteText };
}
if (diffHours < 48) {
return { text: "1 天前", title: absoluteText };
}
// 48 hours or older: absolute format
return { text: absoluteText, title: absoluteText };
}要点
getHours()本身就是 24 小时制(0–23);padStart(2, "0")补齐月/日/时/分;- 保留
_style参数避免调用方报错。
8. 解决 favicon 空容器残留
现象
关闭 favicon 后,部分旧文章的 feed 标题左侧仍有一个黑色空方块。新文章没有。
根因
renderFeedIcon 会无条件创建 .rss-dashboard-article-feed-icon 容器,然后:
- 有些 feed 请求 favicon → 网络超时 →
onerror回调empty()容器 → 容器变空但还在; - CSS 给
.rss-dashboard-feed-favicon设了background-color和16px宽度 → 露出黑方块。
旧文章因为之前缓存过 favicon,走的是请求/缓存路径,所以才有黑方块。
修复
第 1 步:feed-icon.ts 的 renderFeedIcon 末尾加移除逻辑
// If nothing was rendered into the icon container, remove it entirely so it
// does not reserve horizontal space in the source row.
if (iconContainer.childElementCount === 0 && iconContainer.textContent === "") {
iconContainer.remove();
}第 2 步:favicon-utils.ts 的 createSafeIconImage 支持异步失败后移除容器
export function createSafeIconImage(
container: HTMLElement,
src: string,
alt: string,
onErrorFallback: () => void,
cssClass?: string,
options?: { removeContainerOnFailure?: boolean },
): HTMLImageElement {
const img = container.createEl("img", {
attr: { src, alt },
cls: cssClass ?? "rss-dashboard-feed-icon-img",
});
img.onerror = () => {
failedFeedIconUrls.add(src);
img.onerror = null;
img.src = TRANSPARENT_PIXEL;
onErrorFallback();
// If the caller asked us to, remove the container entirely when no icon
// could be rendered, so it does not leave an empty box with a background.
if (
options?.removeContainerOnFailure &&
container.childElementCount === 0 &&
container.textContent === ""
) {
container.remove();
}
};
return img;
}第 3 步:在 feed-icon.ts 里给 renderFeedIcon 路径的调用传 { removeContainerOnFailure: true }
注意:renderHeaderFeedIcon 的容器是共享的,不要传这个参数,否则会删掉头部容器。
补充
- favicon 用的是
google.com/s2/favicons,国内网络常超时; - 如果不想每次等超时,在设置里关闭 favicon 相关开关,让代码不请求;
- 插件的 favicon 缓存可能存在 vault 里,导致旧文章表现和新文章不同。清缓存后再看。
9. 保护你的修改不被插件更新覆盖
核心原则
Obsidian 判断"是不是同一个插件"靠的是 manifest.json 里的 id,不是版本号。改版本号没用,市场更新照样覆盖你的 main.js。
方案 A:只留开发版(最简单)
如果你已经删掉了市场版、只留自己这一份:
- 不需要改 id;
- 但市场仍会认为这是"官方 RSS Dashboard",可能提示更新,一旦点了就覆盖你的改动;
- 建议还是把
manifest.json的id改成rss-dashboard-dev,一劳永逸。
改了 id 后,代码里硬编码的 "rss-dashboard" 字符串也要同步改(比如 plugins.getPlugin("rss-dashboard")),否则打开标签设置等功能会失效。搜一下:
findstr /s /i /n "rss-dashboard" src\*.tsCSS 类名(rss-dashboard-container 之类)不用改。
方案 B:开发版和正式版并存
- 把整个项目复制一份,目录命名为
rss-dashboard-dev; - 改新目录
manifest.json的id为rss-dashboard-dev,name改为RSS Dashboard (Dev); - 在 Obsidian 里禁用正式版,启用 dev 版;
- 数据独立:两个版本的
data.json各走各的。
用软链接避免手动复制(可选)
mklink /D "<你的库>\.obsidian\plugins\rss-dashboard-dev" "C:\...\GI-RSS-DASHBOARD"需管理员权限 CMD 或开启 Windows 开发者模式。
强烈建议:Git 管理源码
cd C:\...\GI-RSS-DASHBOARD
git init
git add .
git commit -m "my custom changes".gitignore:
node_modules/
main.js以后每改一次提交一次,被覆盖也能一键恢复。
10. 理解插件的存储模式
RSS Dashboard 2.3.0+ 引入了 Vault Shards 存储,2.4.0+ 的 Shard Storage v2 把元数据强制放在 vault 里。
所以你会看到仓库根目录出现 .rss-dashboard-data 文件夹——这是默认行为,不是 bug。
在哪里改这个目录
设置 → RSS Dashboard → General(通用)→ Storage(存储),里面有 Storage folder(存储文件夹) 输入框,可以改成任意仓库内路径。
注意
- v2 模式下只能改文件夹名,不能移回插件目录;
- 改完通常需要点 Migrate to vault storage / Repair storage 执行迁移;
- 迁移前备份 vault 和插件数据。
11. 常见问题排查
npm run build 卡住不动
大概率是 ESLint 卡在某个文件。单独跑:
npx eslint . --max-warnings=0 --debug看它最后停在哪个文件。日常开发不需要 npm run build,用 npm run dev 即可。
编译报 esbuild 平台错误
npm approve-scripts esbuild
npm install esbuild --force
npx esbuild --version列表日期显示不对
检查 formatArticleDate 是否被你改动过,以及 formatDateWithRelative 是否被误改(它影响日期分组)。
文章读完就"消失"
不是丢失,是视图过滤:
- 侧边栏选中了"未读"视图;
- 或顶部过滤器勾选了 "Unread"。
切到"所有文章",或取消 Unread 过滤即可。
favicon 显示黑色空方块
见第 8 节。核心是网络超时 + 空容器未移除。
展开作者后列表重复
见第 6 节。Feed View 被双层分组。
附录:关键文件速查
| 功能 | 文件 |
|---|---|
| 列表/卡片/feed 视图调度 | src/components/article-list.ts |
| 各视图渲染 | src/components/article-list/views/ |
| 文章按钮 | src/components/article-list/utils/article-actions.ts |
| Feed 图标 | src/components/article-list/utils/feed-icon.ts |
| favicon 工具 | src/utils/favicon-utils.ts |
| 日期格式化 | src/utils/platform-utils.ts |
| 阅读视图 | src/views/reader-view.ts |
| 主视图 | src/views/dashboard-view.ts |
| 分组逻辑 | src/components/article-list/utils/article-grouping.ts |
| 样式 | styles.css |
本教程基于 Obsidian RSS Dashboard v2.6.0 的实际二次开发过程整理。
你真能折腾。我就就修改了下wordpress主题,发现功能越多,到后面升级新功能兼容性越差,需要排的雷越多。你不断地折腾新功能,配置这么多环境依赖,不怕经常报错吗?这样的话耗在工具上的时间就太多了。
严格的做好备份和版本控制就行啦,现在我是每动一点都做备份,随时做好出错重来的准备,这样反倒很少出错,即使出错有AI也不怕了。