这是提交至Hacktoberfest 开源 AI 挑战赛第 4 周:机器中的幽灵


TL;DR:我在 DEV 首页信息流的大多数标签页里反复看到同样的帖子:Discover,Following以及Curated。

想通过实现一个简单的标记为已读浏览器扩展来修复它。然后事情一件接一件,最后我做出了三个项目:一个已被放弃,以及两个仍在进行中(Feedgrab和Ghostify)。

给用户的说明:这仍在开发中,所以如果你看到错误,请查找更新:https://github.com/SweaDev/ghostify/releases

给评委的说明: v1.0.0是在提交期内完成的;之后的版本是更新。

背景故事

这些天,我在 DEV 上花了太多时间。

  • 删掉了我的旧 X/Twitter 账号,因为它让我头疼!只新建了一个作为占位。
  • 停用了我的 Facebook 账号,因为它……嗯,那是另一个故事了!

所以我上网的大部分时间都花在 DEV 和 YouTube 上。

但 DEV(以及 YouTube)的问题是,我在信息流里总是看到同样的帖子!

早上打开 DEV:又是那个帖子。午饭时打开:它又在那儿。晚上打开:猜猜谁回来了!我读过了。我互动过了。我甚至可能评论过了。它还在那儿,盯着我,好像从没被介绍过一样。

于是在 10 月 26 日,我做了任何理智的开发者都会做的事:我打开编辑器,而不是关掉标签页。

尝试 #1:myDEV(安息 🪦)

计划很简单:

  • 在每张帖子卡片上、紧挨着 Bookmark 放一个 ✓ 按钮。
  • 点击它,帖子就从信息流中消失,并进入一个新的myDEV标签页。
  • 搞定。回去继续阅读。

它奏效了!大约一个小时。然后我打开 YouTube,这周第 14 次看到同一个视频,才意识到 myDEV 只会读取一个网站,而且选择器是我手工硬编码的。一旦 DEV 改了类名,它就死了。而对于 YouTube,我得从头再来。

我花了几天才承认,但今天我最终把它归档了。安息吧,myDEV。你对这个世界来说太具体了。

尝试 #2:Feedgrab

我真正需要的不是一个 DEV 插件。我需要的是能查看任何页面并说:“这是一个信息流,这些是条目,而这一个是你之前见过的同一个条目,即使无限滚动重新渲染过它。”

那变成了Feedgrab,一个 TypeScript 库,它可以:

  • 检测页面上的信息流(结构启发式,可选由 LLM 精炼),或采用你输入的 CSS/XPath 选择器;
  • 让你确认或修正它的猜测,在 Shadow DOM 覆盖层中;
  • 把信息流变成一个去重后的流,其中的条目能跟上无限滚动;
  • 除非你要求,否则绝不删除任何内容。

它运行在浏览器扩展、无头浏览器和服务器上,使用相同的 API。

尝试 #3:Ghostify

然后,终于,我最初想要的东西。在 Feedgrab 之上。带着一个幽灵。👻

我构建了什么

Ghostify是一个浏览器扩展(Chrome + Firefox),用于那些不断向你展示相同帖子的信息流。

悬停在一个帖子上,点击幽灵,它就从信息流中消失了。永久消失。刷新、滚动、明天再来:它依然消失。

但它不是被删除。它变成了一个幽灵。Ghostify 保留一份副本,这样你可以再次找到它、稍后阅读,或把它推迟到周末。

Hover a post on the DEV feed and click the ghost

这是我对机器中的幽灵的理解:你已经看过的帖子不会消亡,它们只是不再纠缠你的信息流,而是移入机器中。安静地。礼貌地。直到你召唤它们回来。

它的功能:

  • 幽灵按钮会出现在你指针下的帖子上。点击它,帖子消失,一个撤销提示条给你 5 秒的后悔时间。
  • Ghostify 页面把所有内容保存在三个标签页中:已幽灵化、稍后和已推迟。
  • 推迟一个帖子到稍后今天、明天、周末、下周或任何日期。时间到了,它会出现在从推迟中返回中。
  • 搜索你的幽灵,按关键词,或开启语义并直接描述你记得的内容(“那篇关于 JavaScript 添加对象和数组的帖子”)。
  • 自动幽灵化规则用平实的英语,按站点设置:“我已经读过的每周重复讨论帖”。一个本地决策模型检查新的信息流条目是否匹配它们,并幽灵化匹配项。如果它弄错了一个,不是这个会把帖子带回来,并且该规则再也不会碰它。
  • 添加任何站点。在任何有信息流的页面上,点击添加此站点。Feedgrab 会提议信息流,你在它的覆盖层中确认它们,并选择它们是用于已登录还是未登录视图。
  • 新标签页风味,如果你想让幽灵出现在每个新标签页上。

Ghosted, Later and Snoozed tabs with search, tags and summaries

演示

还没有商店上架(暂时!),所以演示就是:安装它,打开 DEV,开始幽灵化。大约需要一分钟:

  1. 从以下位置下载 zipReleases:ghostify-<version>-chrome.zip或ghostify-<version>-firefox.zip。
  2. Chrome:解压,打开chrome://extensions,开启开发者模式,点击加载已解压的扩展程序。Firefox:打开about:debugging#/runtime/this-firefox,点击加载临时附加组件…并选择该 zip。
  3. 打开https://dev.to/处于登出状态,悬停在一篇帖子上,点击幽灵。

这里有一个快速导览。(截图展示的是用户会看到的内容。)

工具栏弹窗列出了你最近的幽灵。点击 👁 取消某个幽灵,它就会重新出现在信息流中:

The toolbar popup over the DEV feed

点击一个幽灵查看其详情、AI 摘要和标签:

Item details with an AI summary and tags, in dark mode

用通俗英语按站点编写自动幽灵化规则:

Auto-ghostify rules in Settings → Sites

并为每项任务选择模型:

Settings → AI with Ollama models for embeddings, general and decision

代码

幽灵化

Ghostify 是一个浏览器扩展(Chrome 和 Firefox),用于那些总是向你展示相同帖子的信息流。悬停在一篇帖子上,点击幽灵,它就会从该站点的信息流中永久消失。Ghostify 会保留一份副本,以便你之后再次找到它。

v1.0.0。Ghostify 开箱即用于 dev.to 首页信息流,你也可以添加任何站点。它有一个完整的 Ghostify 页面,支持暂停和搜索,以及可选的 AI,可通过 Ollama 在本地运行,或使用你自己的 OpenRouter、Gemini 或 OpenAI 密钥。

功能

  • dev.to 登出状态的首页信息流(相关/、最新/latest、热门/top/*)会被自动识别,通过feedgrab。

  • 幽灵按钮会显示在你指针下的帖子上。点击它会隐藏该帖子并保存,一个撤销提示条可让你撤回。

  • 被幽灵化的帖子会保持隐藏当你重新加载时,以及当它们再次出现时……

Feedgrab

一个 TypeScript 库,可查找并描述内容信息流在网页中(帖子时间线、搜索结果、产品网格、文章列表),并将它们转换为一个去重后的单一流,包含带有排除报告的

  • 检测自动检测信息流(结构启发式,可选由 LLM 优化),通过检查元素,或通过你输入的 CSS/XPath 选择器。
  • 确认/编辑在 Shadow-DOM 覆盖层中查看提案,或通过无头辅助工具以编程方式操作。
  • 迭代一个统一的信息流,可跟上无限滚动(MutationObserver)或分页ingest),并带有每项报告:kept,excluded(以及原因),或pending。
  • 排除按特异性排序的选择器规则、内置广告规则或自然语言 LLM 条件来筛选条目。除非你调用移除辅助函数,否则不会移除任何内容。

它运行在浏览器扩展(Chromium + Firefox,MV3)、无头浏览器(Playwright/Puppeteer 注入的 bundle)以及服务端(Bun/Node 配合解析后的 DOM)中,各处使用同一套 API。

安装

…

我是如何构建它的

开源 AI 部分

AI 是可选的,且默认关闭。Ghostify 完全可以只是一个简单的“隐藏这条帖子”按钮。开启 AI 后,它会承担三项工作,每项都有自己的模型:

任务 默认(本地,通过 Ollama) 用于
嵌入 embeddinggemma-2 对你的 ghost 进行语义搜索
通用 gemma4:e2b 为每条被 ghost 的帖子生成摘要和标签
决策 d3:2b 用你的自动 ghostify 规则检查新的信息流条目

这三者都是开源权重模型,小到可以在笔记本电脑上运行。启动 Ollama 使其接受来自扩展的请求,就这些:

ollama pull embeddinggemma-2
ollama pull gemma4:e2b
ollama pull d3:2b
OLLAMA_ORIGINS="chrome-extension://*,moz-extension://*" ollama serve

如果你更想用托管模型,每项工作都可以切换到OpenRouter、Gemini或OpenAI,使用你自己的密钥。底层管道是AI SDK,所以切换提供商只是一个下拉菜单,而不是重写。

有几件事让我很满意:

  • 处处都有回退。如果决策模型不可用,通用模型会处理规则检查。如果嵌入失败,搜索会回退到关键词。不会因为 Ollama 没运行就出问题。
  • 嵌入会标记生成它们的模型。更换嵌入模型后,Ghostify 会忽略旧向量,而不是把它们混在一起。重建索引会填补空缺。
  • 一个坦诚的缺口: d3的响应格式(还)没有文档。Ghostify 会读取它认识的形状,把其他任何内容视为“不匹配”,并记录下来。宁可漏掉一个 ghost,也不隐藏你想看的内容!

扩展部分

  • WXT用于一套代码库构建 Chrome MV3 和 Firefox,外加新标签页版本。
  • React 19 + Tailwind v4用于弹窗、Ghostify 页面和设置。
  • Dexie(IndexedDB)用于存储 ghost、嵌入和密钥。一切都在你的浏览器里。没有账户,没有同步,没有服务器。
  • Feedgrab用于查找信息流和条目,以及隐藏它们。Ghostify 从不直接触碰信息流节点;它请求 Feedgrab 的removeItem来隐藏它,而它自己的 UI 位于单独的 Shadow DOM 中。

安全性方面有点像个无底洞:

  • 内容脚本永远看不到 API 密钥。它们甚至不打开 IndexedDB。一切都通过类型化消息传给后台脚本,由后台与模型通信。
  • 密钥也不在storage.local里,因为内容脚本可以读取它。它们存放在扩展自身源上的 Dexie 表中。
  • 在内容脚本中,Feedgrab 拿到的是一个代理而不是模型,这也让打包体积从380 KB 缩小到 71 KB。不错的副作用!

“你怎么在一周内做出这一切”的部分

大部分代码不是我手写的。我用了一个编码智能体。

但我也没有只说一句“给我做个扩展”。我一直在用一个小型的规范驱动流程,我称之为specx:

  1. 规范:我手写了规范。它只有 17 行。字面上就是一段话加一个功能列表。
  2. 计划:智能体把它变成了一个计划,包含技术栈、架构和一份决策日志。
  3. 任务:然后拆成23 个任务,分布在 4 个阶段,每个任务都标注了它需要的规范/计划行范围、它涉及的文件,以及一个完成检查。
  4. 构建循环:取第一个未被阻塞的任务,只读它引用的内容,实现它,运行它的完成检查加上 lint + 类型检查 + 测试,勾选复选框,提交。重复。
  5. 阶段检查:每个阶段结束时,从干净状态按照 README 操作,构建,在浏览器中加载,修复坏掉的地方,打一个发布标签。

规则放在AGENTS.md里,所以任何智能体都能接手。每个判断决定都写进计划的决策日志,这样我能看到为什么某件事要那样做。而且每个任务一个提交,所以git log读起来就像变更日志。

阶段 1 是“在 DEV 上幽灵化一篇帖子”。阶段 4 是“用本地 AI 自动幽灵化”。每个阶段结束时都有我能真正加载和使用的东西,而不是一堆半接线的模块。

为什么开放创新很重要?

因为Ghostify 读取的东西是我的浏览记录。

想想一个信息流扩展里的 AI 功能实际能看到什么:你滚过的每个信息流里的每篇帖子、你隐藏了什么、你稍后保存了什么、你搜索了什么。那是一个相当详细的人物画像。我不想它默认流向任何人的 API。连我自己的都不想。

有了开源权重模型,就不必如此:

  • Gemma 4 E2B、EmbeddingGemma 2 和 d3:2b 在我的笔记本电脑上运行。摘要、标签、语义搜索和规则检查全都在本地完成。用 Ollama 的话,什么都不会离开这台机器。
  • 每条帖子零成本。自动幽灵化检查信息流中的每一个新条目。用付费 API 的话,我每滚动一次就是在烧表。本地运行,只是我的风扇多转一会儿。
  • 它一直能用。没有速率限制,没有弃用邮件,没有“此模型将于……退役”。
  • 一个小型专用模型就够了。我不需要一个巨型模型来回答“这是不是每周讨论帖?”一个 2B 的决策模型对这个任务来说大小正合适,而我能选它,只因为它是开放的。

而且不只是模型。Ghostify 自下而上都构建在开源之上:WXT、React、Dexie、AI SDK、Biome、Vitest……还有 Feedgrab,我把它拆分出来是因为我希望它能被复用。如果有人想为 YouTube 做一个“隐藏重播”扩展,或者为一个没有 RSS 的网站做信息流阅读器,最难的部分已经做好了。MIT 许可。放手去做吧。

闭源 API 也许能给我更聪明的摘要。开源模型给了我一个我真正愿意用在我读的所有内容上的工具。

我的智能体会话

整个项目都是在智能体会话中构建的,遵循上面的 specx 流程。查看它们的最好方式是提交历史:每个任务一个提交,从Scaffold WXT React project到Add auto-ghostify rules with rescue。

它的精神

我一直在想这个名字。

大多数“隐藏这个”功能实际上是“删除这个”。你一点,它没了,你再也找不到它。这一直让我觉得有点不对劲。我确实读过那篇帖子。其中有些还不错!我只是不需要每次打开主页都看到它们。

所以在 Ghostify 里,没有东西会死。它会变成幽灵。它留在机器里,带着标题、作者、一小段摘要和几个标签,等你把它召唤回来。有点像帖子的亡灵节:不在这里了,但也没被遗忘。

而在某个归档的仓库里,myDEV 正安息着,知道这一切都是由它开始的。🕯️

奖项类别

  • Gemma 最佳应用:Ghostify 使用Gemma 4 E2B(gemma4:e2b)做摘要和标签,使用EmbeddingGemma 2(embeddinggemma-2)做语义搜索,两者默认都通过 Ollama 在本地运行。

感谢阅读!如果你的一篇帖子从我的信息流里消失了,别往心里去。这只说明我可能已经读过它、看过它,或者以后会读它。👻