三
三股水
三
三股水

Obsidian RSS Dashboard 二次开发初步尝试

本文从 Obsidian 撰写发布

Obsidian RSS Dashboard 二次开发完整教程

从零开始搭建开发环境、修复 bug、自定义功能,到管理你自己的开发版插件。适用于 Windows + Obsidian 桌面端。

📋 目录

  1. 环境准备与首次构建
  2. 解决 esbuild 安装脚本被拦截的问题
  3. 用 pnpm 节省磁盘空间(可选)
  4. 开发工作流与热重载
  5. 自定义功能:给列表加"原文"按钮
  6. 修复:Feed View 分组重复显示
  7. 自定义日期格式
  8. 解决 favicon 空容器残留
  9. 保护你的修改不被插件更新覆盖
  10. 理解插件的存储模式
  11. 常见问题排查

1. 环境准备与首次构建

需要安装的工具

  • Node.js(含 npm)—— 建议 LTS 版本
  • Git
  • 一个独立的测试库(Vault)——千万不要在主力库里调试

步骤

1. 把插件源码放到测试库的插件目录

推荐路径:

<你的测试库>\.obsidian\plugins\rss-dashboard\

把源码文件夹整个放进去(或复制一份)。不要放到主力库。

2. 安装依赖

在插件目录打开终端(CMD 或 PowerShell):

npm install

3. 启动开发编译

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 dev

4. 开发工作流与热重载

日常开发只需一条命令

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\*.ts

CSS 类名(rss-dashboard-container 之类)不用改。

方案 B:开发版和正式版并存

  1. 把整个项目复制一份,目录命名为 rss-dashboard-dev;
  2. 改新目录 manifest.json 的 id 为 rss-dashboard-dev,name 改为 RSS Dashboard (Dev);
  3. 在 Obsidian 里禁用正式版,启用 dev 版;
  4. 数据独立:两个版本的 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 的实际二次开发过程整理。

上一篇 宝塔 Nginx 上传文件 413 错误排查教程 下一篇 Obsidian阅读个人博客圈RSS绝爽!!

2条评论

Ctrl + Enter 发送
    1. 水拍石 Lv3

      你真能折腾。我就就修改了下wordpress主题,发现功能越多,到后面升级新功能兼容性越差,需要排的雷越多。你不断地折腾新功能,配置这么多环境依赖,不怕经常报错吗?这样的话耗在工具上的时间就太多了。

      回复 Windows · Chrome
      1. XIGE 站长 Lv5

        严格的做好备份和版本控制就行啦,现在我是每动一点都做备份,随时做好出错重来的准备,这样反倒很少出错,即使出错有AI也不怕了。

        回复 Windows · Chrome
© 2026 三股水