1. 信息过载时代的本地优先思路
每天打开浏览器,光是各种技术社区、行业资讯站、社交平台推送的AI相关内容就能刷出上百条。更麻烦的是,其中大量内容要么是英文原文需要额外花时间消化,要么是标题党点进去发现毫无营养,真正值得看的中文AI情报反而被淹没在信息洪流里。我粗略统计过,如果要把当天所有渠道的AI资讯过一遍,至少需要四十分钟以上,而且大部分时间都花在了筛选和跳转上。
这个项目要解决的核心问题很明确:让中文AI情报主动来找你,而不是你去找它。具体做法是做一个浏览器插件,在本地完成信息聚合、去重、摘要和推送,不依赖任何远程服务,所有数据都在你自己的机器上处理。适合的人群包括每天需要跟踪AI动态的开发者、产品经理、技术博主,以及任何不想被碎片化信息牵着走但又怕错过重要进展的人。
为什么强调“本地优先”?这里有两层考虑。第一层是隐私,你的阅读偏好、浏览记录、关注列表这些数据如果上传到某个服务器,总归是不太放心。第二层是可控性,远程服务一旦挂掉或者改规则,你的信息流就断了,而本地插件只要浏览器能打开,它就能工作。这个思路贯穿了整个项目的设计决策,后面会反复提到。
2. 整体架构与核心模块拆解
2.1 为什么选择浏览器插件而不是独立应用
一开始我考虑过做桌面应用或者命令行工具,但很快就否掉了。桌面应用需要单独安装、单独更新,跨平台适配也是麻烦事;命令行工具对非技术用户门槛太高。浏览器插件的好处在于:你本来就在浏览器里看资讯,插件可以直接在同一个环境里完成抓取、处理和展示,不需要切换窗口。而且现代浏览器插件体系已经非常成熟,内容脚本、后台脚本、存储API、通知API这些能力都现成的,开发成本比想象中低很多。
另一个关键考量是权限边界。浏览器插件可以精确控制它能访问哪些网站,用户装的时候心里有数。相比之下,一个独立应用要抓取网页内容,要么用系统级网络权限(用户根本不知道它读了什么),要么让用户手动导入数据(体验太差)。插件模式下,用户可以在扩展管理页面清楚看到这个插件申请了哪些域名的访问权限,这种透明度是本地优先理念的重要体现。
2.2 三层架构:采集层、处理层、呈现层
整个插件在逻辑上分成三层,每层职责清晰,方便单独调试和替换。
采集层负责从各个信息源拉取原始内容。这里不依赖任何第三方聚合服务,而是直接对接各个站点的公开接口或者解析页面结构。每个信息源写成一个独立的适配器模块,新增或删除源只需要改一个配置文件。采集频率默认是每三十分钟一次,可以在设置里调整,最短支持五分钟。
处理层是核心,包含去重、语言过滤、摘要生成、关键词打分四个步骤。去重用的是标题的SimHash加上正文前两百字的余弦相似度,双重校验避免误杀。语言过滤主要判断是否为中文内容,非中文的直接丢弃或者标记为待翻译。摘要生成用的是本地运行的小型语言模型,后面会详细讲怎么选型和部署。关键词打分是根据用户配置的关注词列表,给每条资讯算一个相关度分数,分数低的直接不展示。
呈现层就是插件的弹窗界面和通知系统。弹窗里按时间线展示已处理的资讯,支持按相关度排序、按来源筛选、标记已读/收藏。通知系统用的是浏览器原生通知API,只有相关度超过阈值的资讯才会弹通知,避免打扰。
2.3 数据存储方案:IndexedDB加本地文件备份
存储这块我试过几种方案。最开始用chrome.storage.local,简单是简单,但容量有限(通常只有10MB左右),存个几百条资讯就满了。后来换成IndexedDB,容量理论上只受硬盘限制,而且支持索引和事务,查询效率高很多。具体建了三个对象仓库:raw_items存原始抓取内容,processed_items存处理后的结果,user_prefs存用户配置和阅读状态。
为了防止浏览器数据被清理导致丢失,我还加了一个可选的本地文件备份功能。每天定时把IndexedDB里的数据导出成JSON文件,通过浏览器的下载API存到用户指定的目录。这个功能默认关闭,因为有些用户可能觉得多余,但对于需要长期积累情报库的人来说很实用。
3. 核心细节解析与实操要点
3.1 信息源适配器的编写规范
每个信息源适配器就是一个JavaScript模块,导出一个标准接口。这个接口定义如下:
// source-adapter-example.js export default { id: 'source-unique-id', name: '来源显示名称', icon: '图标URL或base64', // 抓取函数,返回原始条目数组 async fetch() { // 实现具体的抓取逻辑 // 返回格式:[{ title, url, content, publishTime, sourceId }] }, // 可选:自定义解析规则 parse(rawHtml) { // 如果fetch返回的是HTML,这里做解析 } };写适配器的时候有几个坑要注意。第一,不要假设页面结构永远不变,尽量用语义化的选择器(比如article标签、[role="main"]),少用那种带一长串哈希类名的选择器。第二,控制请求频率,同一个域名下的请求间隔至少两秒,避免给人家服务器造成压力,也避免自己被封。第三,处理分页和增量更新,记录上次抓取到的最新时间戳或ID,下次只抓新的,不要每次全量拉。
提示:写适配器之前先手动抓几次包,看看实际返回的数据结构。有些站点虽然页面看起来一样,但移动端和桌面端返回的JSON结构可能不同,选一个稳定的用。
3.2 中文内容识别与过滤策略
判断一条内容是不是中文,听起来简单,做起来有不少细节。最粗糙的做法是看字符编码或者统计中文字符占比,但这样会把中英混合的技术文章误判。我的做法是综合三个指标:
- 中文字符(CJK统一表意文字)占总字符数的比例,超过15%就算候选
- 标题中是否包含中文标点(,。!?等)
- 正文前一百字中是否出现常见中文停用词(的、了、是、在等)
三个指标满足两个就判定为中文内容。实测下来准确率在95%以上,误判的主要是一些纯代码片段或者英文术语堆砌的标题。对于误判的情况,用户可以在设置里手动调整阈值,或者把某个来源直接标记为“始终视为中文”或“始终视为非中文”。
还有一个特殊情况是机器翻译内容。有些站点会把英文文章自动翻译成中文发布,这类内容质量参差不齐。我的处理方式是给这类来源单独打标签,在展示时可以折叠或者降低权重,让用户自己决定要不要看。
3.3 本地摘要生成的模型选型与量化
摘要生成是整个项目里最吃资源的环节。我试过三种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 调用在线API | 效果好,无需本地资源 | 需要网络,有隐私顾虑,可能收费 | 不推荐,违背本地优先原则 |
| 规则式抽取 | 速度快,零依赖 | 摘要生硬,经常断章取义 | 对质量要求不高的场景 |
| 本地小模型 | 质量不错,完全离线 | 需要下载模型文件,占用内存 | 推荐方案 |
最终选了本地小模型方案。具体用的是经过量化处理的Transformer架构模型,参数量在1亿到3亿之间。量化到INT8之后,模型文件大概在100MB到300MB,推理时内存占用在500MB左右,现代电脑完全扛得住。推理速度方面,一段五百字的文章生成摘要大概需要0.5到1.5秒,可以接受。
模型文件不打包在插件里,而是首次使用时从用户指定的本地路径加载,或者通过插件界面引导用户下载。这样做的好处是插件安装包保持轻量,用户也可以自己替换成其他兼容的模型。
注意:模型推理最好放在Web Worker里跑,不要阻塞主线程。否则生成摘要的时候整个浏览器界面会卡住,体验很差。
3.4 相关度打分的权重设计
相关度打分决定了哪些资讯会出现在你的时间线顶部,哪些会被折叠甚至隐藏。打分公式不复杂,但权重的设计需要根据实际使用反馈反复调整。我目前的方案是:
score = w1 * keyword_match + w2 * source_weight + w3 * freshness + w4 * engagement其中keyword_match是标题和正文中命中用户关注词的程度,source_weight是用户给每个来源设置的权重(比如官方博客权重高,聚合站权重低),freshness是时间衰减因子(越新越高),engagement是来源本身的互动数据(如果有的话,比如点赞数、评论数)。
权重默认值是w1=0.5, w2=0.2, w3=0.2, w4=0.1。这个比例是我自己用了一个月之后调出来的,关键词匹配最重要,但也不能完全忽略来源质量和时效性。用户可以在高级设置里自己改这些权重,不过大部分人都用默认值就够了。
4. 实操过程与核心环节实现
4.1 开发环境搭建与插件骨架生成
先确保你机器上有Node.js 18以上版本和npm。然后创建一个项目目录,初始化package.json,安装必要的构建工具。我用的构建方案是Vite加上一个轻量的插件打包工具,配置起来比Webpack简单很多。
mkdir ai-info-radar && cd ai-info-radar npm init -y npm install -D vite @crxjs/vite-plugin目录结构大概是这样:
src/ background/ # 后台脚本 content/ # 内容脚本 popup/ # 弹窗界面 options/ # 设置页面 core/ # 核心逻辑 sources/ # 信息源适配器 processor/ # 处理管道 storage/ # 存储封装 assets/ # 图标等静态资源 manifest.json # 插件清单manifest.json里需要声明几个关键权限:storage用于本地存储,notifications用于推送通知,alarms用于定时任务。主机权限不要一股脑写<all_urls>,而是根据你实际要抓取的来源逐个列出,这样用户安装时看到权限列表也放心。
4.2 后台脚本的定时抓取与消息通信
后台脚本(Service Worker)负责调度抓取任务和协调各模块。核心逻辑是一个定时器,每隔设定时间触发一轮抓取。这里有个细节:Manifest V3的Service Worker会被浏览器随时挂起,所以不能用setInterval,必须用chrome.alarmsAPI。
// background/scheduler.js const ALARM_NAME = 'fetch-ai-news'; async function setupAlarm(intervalMinutes) { await chrome.alarms.clear(ALARM_NAME); await chrome.alarms.create(ALARM_NAME, { periodInMinutes: intervalMinutes, delayInMinutes: 1 }); } chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name !== ALARM_NAME) return; const sources = await loadEnabledSources(); for (const source of sources) { try { const items = await source.fetch(); await processAndStore(items, source.id); } catch (err) { console.error(`[${source.id}] fetch failed:`, err); await recordSourceError(source.id, err.message); } } await updateBadge(); });消息通信方面,弹窗界面和后台脚本之间用chrome.runtime.sendMessage和onMessage来传递数据。注意消息传递是异步的,而且如果接收方没有及时响应,发送方会收到undefined。我的做法是每个消息都返回一个Promise,并且设置超时,避免界面一直转圈。
4.3 处理管道的实现与性能优化
处理管道按顺序执行去重、语言过滤、摘要生成、打分四个步骤。每个步骤都是纯函数,输入输出明确,方便单独测试。
去重这一步,我建了一个内存中的SimHash索引,每次新条目进来先算SimHash,然后跟索引里的值比对汉明距离。距离小于3的视为重复,直接丢弃。索引定期持久化到IndexedDB,重启后恢复。
摘要生成是性能瓶颈,所以做了两个优化。第一,只对相关度分数超过阈值的条目生成摘要,低分条目只存标题和链接。第二,摘要生成放在Web Worker里,主线程通过postMessage发送文本,Worker返回摘要结果。Worker内部维护一个模型实例,避免每次重新加载。
// core/processor/summarizer-worker.js let model = null; self.onmessage = async (e) => { const { id, text } = e.data; if (!model) { model = await loadModel(); } const summary = await model.summarize(text, { maxLength: 120 }); self.postMessage({ id, summary }); };实测下来,一轮抓取两百条资讯,去重后剩一百二十条左右,其中需要生成摘要的大概四十条,整个处理流程耗时在八到十二秒之间。这个延迟对于后台任务来说完全可以接受。
4.4 弹窗界面的交互设计与状态管理
弹窗界面用的是原生JavaScript加少量CSS,没有引入前端框架。原因很简单:弹窗的DOM结构不复杂,用框架反而增加打包体积和启动时间。状态管理用一个简单的发布订阅模式,数据变化时通知视图更新。
界面分三个区域:顶部是筛选栏(按来源、按时间、按相关度),中间是资讯列表,底部是操作栏(标记全部已读、打开设置、手动刷新)。每条资讯显示标题、来源图标、发布时间、摘要片段和相关度分数。点击标题在新标签页打开原文,点击摘要区域展开完整摘要。
一个细节是滚动位置记忆。用户滚动到一半关掉弹窗,下次打开应该回到上次的位置。这个用sessionStorage存一下滚动容器的scrollTop就行,很简单但体验提升明显。
5. 常见问题与排查技巧实录
5.1 抓取失败与反爬应对
抓取失败是最常见的问题,原因五花八门。我整理了一个排查清单:
| 现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 返回空数组 | 页面结构变了 | 手动打开页面检查DOM | 更新适配器选择器 |
| 返回403/429 | 请求频率过高 | 看控制台网络面板 | 增加请求间隔,加User-Agent |
| 返回乱码 | 编码识别错误 | 检查响应头Content-Type | 显式指定UTF-8解码 |
| 超时 | 网络问题或服务器慢 | 用curl测试响应时间 | 增加超时时间,加重试机制 |
| 部分字段缺失 | 接口字段名变了 | 对比新旧响应JSON | 更新字段映射 |
反爬这块,我的原则是温和抓取。不伪造Referer,不模拟登录态,不绕过任何访问限制。如果某个来源明确禁止自动化访问,那就直接放弃这个源,不去硬碰。毕竟这个工具是给自己用的,没必要为了多抓一个源惹麻烦。
5.2 摘要质量不稳定的调优
本地小模型生成的摘要有时候会跑偏,比如把不重要的细节当成重点,或者摘要比原文还长。调优主要从三个方向入手:
第一,输入预处理。把原文里的广告、导航栏、评论区这些噪音去掉,只保留正文。我写了一个简单的正文提取函数,基于文本密度和标签权重来判断哪些段落是正文。
第二,提示词工程。虽然是小模型,但给一个好的提示词还是能明显提升效果。我的提示词模板是:“用一句话总结以下内容的核心信息,不超过80字,不要包含原文中没有的信息。”这个模板经过多次调整,目前效果比较稳定。
第三,后处理过滤。生成摘要后检查长度,如果超过原文的30%或者少于10个字,就回退到规则式抽取的结果。这样至少保证不会出现太离谱的摘要。
5.3 内存占用与性能监控
插件跑久了内存占用会慢慢涨,主要是IndexedDB缓存和模型推理的中间结果没有及时释放。我的做法是加一个内存监控模块,每隔十分钟检查一次performance.memory(Chrome特有),如果超过阈值就触发清理。
清理逻辑包括:清空SimHash索引的内存缓存(下次用时从IndexedDB重建)、释放Worker里模型推理的中间张量、清理超过七天的已读条目(保留收藏的)。这套机制跑下来,连续运行一周内存占用稳定在300MB到500MB之间,没有明显泄漏。
提示:开发阶段可以在后台脚本里加一个
chrome.runtime.onSuspend监听,在Service Worker被挂起前把关键状态持久化,避免数据丢失。
5.4 多浏览器兼容性注意事项
这个插件主要在Chrome和Edge上测试,Firefox和Safari的兼容性有一些差异。主要问题在Manifest V3的支持程度上:Firefox对Service Worker的支持还在完善中,Safari的插件体系跟Chrome差别较大。如果要做全平台兼容,建议用webextension-polyfill这个库来抹平API差异,然后针对每个浏览器单独打包。
不过说实话,如果你只是自己用,选一个主力浏览器就够了,没必要追求全平台。我自己的主力是Chrome,所以代码里用了一些Chrome特有的API(比如chrome.alarms的某些参数),在别的浏览器上可能需要调整。
6. 扩展方向与个人实践体会
这个插件目前的功能已经能满足我日常的情报跟踪需求,但还有几个方向可以继续折腾。一个是多设备同步,目前数据只存在本地,换电脑就没了。可以考虑用浏览器的账号同步功能,或者让用户自己配置一个WebDAV目录来同步数据。另一个是自动分类,现在只有相关度打分,没有按主题分类。如果能自动把资讯归到“大模型”、“AI绘画”、“行业动态”这些类别里,浏览效率会更高。
还有一个想法是周报生成。每周日自动把这一周的高相关度资讯汇总成一份摘要报告,导出成Markdown或者PDF。这个功能对于需要定期汇报AI动态的人来说会很实用,实现上也不复杂,就是把过去七天的数据按相关度排序,取前二十条,让模型生成一段综述。
我自己用这个插件大概三个月了,最大的感受是信息焦虑明显降低。以前总怕错过什么重要消息,现在知道插件在后台帮我盯着,有值得看的会通知我,没通知就说明今天没什么大事。这种“被动的主动”反而让我能更专注在手头的工作上。另外一个意外收获是,因为摘要是在本地生成的,我经常在等摘要出来的那一两秒里快速扫一眼标题,判断这条要不要细看,这个习惯帮我省了不少时间。
如果你也想自己做一个类似的工具,我的建议是先从一两个信息源开始,把整个流程跑通,再慢慢加源。一开始就追求大而全,很容易在适配器编写上耗尽耐心。另外,不要过度优化,摘要质量差一点没关系,关键是信息能到你眼前。先解决有无问题,再解决好坏问题。