本地优先的浏览器插件:聚合、去重与摘要中文AI情报
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标签、[rolemain]少用那种带一长串哈希类名的选择器。第二控制请求频率同一个域名下的请求间隔至少两秒避免给人家服务器造成压力也避免自己被封。第三处理分页和增量更新记录上次抓取到的最新时间戳或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是来源本身的互动数据如果有的话比如点赞数、评论数。权重默认值是w10.5, w20.2, w30.2, w40.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.memoryChrome特有如果超过阈值就触发清理。清理逻辑包括清空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动态的人来说会很实用实现上也不复杂就是把过去七天的数据按相关度排序取前二十条让模型生成一段综述。我自己用这个插件大概三个月了最大的感受是信息焦虑明显降低。以前总怕错过什么重要消息现在知道插件在后台帮我盯着有值得看的会通知我没通知就说明今天没什么大事。这种“被动的主动”反而让我能更专注在手头的工作上。另外一个意外收获是因为摘要是在本地生成的我经常在等摘要出来的那一两秒里快速扫一眼标题判断这条要不要细看这个习惯帮我省了不少时间。如果你也想自己做一个类似的工具我的建议是先从一两个信息源开始把整个流程跑通再慢慢加源。一开始就追求大而全很容易在适配器编写上耗尽耐心。另外不要过度优化摘要质量差一点没关系关键是信息能到你眼前。先解决有无问题再解决好坏问题。