
1. 什么是 ponytail 插件我为什么要折腾它如果你经常需要“把一篇文章快速提炼成要点”或者“把网页正文干净地抓下来丢给 AI 整理”你多半经历过同样的一连串麻烦先手动复制再去掉广告、推荐位、评论区最后还要应付各种乱七八糟的格式。时间长了你就会发现真正有信息量的内容可能只有三分之一剩下全是噪音。我做的这个小工具叫 ponytail定位就是一个“网页内容整理插件 技能包”。它的名字很直白——马尾辫。散落的头发收拢成一条干净利落的马尾就像把网页里散乱的各种噪音收拢成一段清爽的正文。不管你是普通用户、内容创作者还是经常折腾个人 AI 工作流的人只要日常需要大量阅读网页并提炼信息这个工具都能帮你省下不少重复劳动。我把它拆成了两层一层是可安装的浏览器插件负责在页面里抓正文、清理杂质另一层是 skill也就是技能包负责把清理完的内容进一步“上脑”——比如调用大模型做摘要、生成日报、翻译成本地语言。两个东西搭配使用才能发挥最大价值。如果你只需要其中某一个部分也可以单独拿着用互不干扰。这篇文章我会从设计思路、安装配置、核心逻辑、踩坑记录一直讲到后续扩展方向全部基于我自己的实际使用经验。里面涉及代码的部分我会尽量贴出可运行的片段不是那种只给个伪代码就把你打发的教程。你甚至可以照着抄一段就立刻跑起来。2. 整体设计与核心思路拆解2.1 为什么要把“抓取”和“理解”拆成两层动手之前我参考过不少“一键摘要”类工具发现它们的通病是把页面抓取、文本提取、模型调用全部揉在一个脚本里。看着省事可一旦哪一层出了问题排查起来特别痛苦。比如页面改版了你可能要跑到很深的代码里去改 CSS 选择器想换一个模型又要动编辑器逻辑。两个不相干的事被绑在一起非常不利于维护。所以我干脆把流程拆成两个独立模块插件层collect只负责“把乱七八糟的网页变成干净的正文”。它不关心你后面用谁来总结、总结成什么样。技能层skill只负责“把干净的正文交给模型处理”。它不关心正文是从哪个网站、哪种页面结构里来的。这样做的好处很直接页面抓取变差了我就只修插件层模型换成新的版本或者想换摘要风格我就只调 skill 层。两边互不牵连出问题的范围一下就缩小了。而且抓下来的 Markdown 纯文本可以直接粘贴到任何笔记软件、聊天窗口或者本地 AI 工具里哪怕后面不想用模型也完全不浪费。2.2 为什么选浏览器插件而不是后端爬虫最早的原型我其实是用 Python 写的爬虫直接 HTTP 请求页面再解析 HTML。听起来技术感很强但实际用起来有致命伤很多页面是动态渲染的正文内容靠 JavaScript 加载你直接抓到的只是一堆空壳和初始化代码。登录态、个性化推荐也需要处理光 cookie 和会话维护这一块就足以让人头大。浏览器插件没有任何这个问题。它跑在真实浏览器环境里DOM 已经渲染完毕登录态天然就有浏览器自己也处理了编码和跳转。插件的 content script 可以拿到你当前看到的那个页面内容所见即所得这是任何后端爬虫都不可能替代的优势。代价是插件不能用 Node.js也没法直接跑 Python 的 BeautifulSoup。但好在浏览器的 DOM API 已经足够强大配合一些简单的文档密度算法实现一个轻量版正文提取器并不难。2.3 核心组件的技术选型逻辑整个项目实际上只有三块核心我做的技术选型也尽量遵循“能不引入依赖就不引入”的原则第一个是正文提取算法。我没有用商业方案也没有引那种动辄几百 KB 的第三方库而是自己实现了一个简化版 Readability先遍历页面主要区域的段落容器计算候选节点的文本密度再把分低的和明显是导航、广告的节点剔除。大概两百行 JavaScript效果已经能覆盖绝大多数资讯类、博客类页面。第二个是文本转换。从 DOM 里取出来的是 HTML要变成对 AI 友好的纯文本或 Markdown。这一块不需要复杂库核心就是递归遍历节点遇到标题、段落、列表、引用就按规则加对应的 Markdown 符号。注意不要直接拿 innerHTML 去正则替换因为嵌套标签和转义会让你的输出变成灾难。第三个是模型调用。技能层我用的是对命令行和脚本都比较友好的方式直接通过 HTTP 接口把清理好的文本送给模型服务返回结果再落回本地文件。接口地址、模型名、token 上限全部做成配置项。这样今天你用一个模型明天换另一个模型改配置就行代码不用碰。3. 插件安装与核心实战3.1 准备工作先把环境弄干净安装 ponytail 之前建议先把浏览器里的其他“阅读模式”“广告拦截”类扩展暂时停用。我踩过一次大坑开了几个广告拦截插件之后正文区域有大量元素被动态替换导致我的正文提取器总是抓到半截文章。排查了一个小时才发现是插件冲突。其次如果你要在 skill 层调大模型接口注意确认网络环境和接口配置。这一步属于常规准备工作。日常使用不需要额外配置账号等复杂内容插件本身的安装和加载流程非常简单。3.2 浏览器插件安装的三种方式Chrome 和 Edge 都支持“开发者模式加载已解压的扩展程序”。具体路径是地址栏输入chrome://extensions或edge://extensions打开右上角“开发者模式”开关然后选择“加载已解压的扩展程序”指向项目里plugin/目录即可。如果你偏好从命令行安装可以用# 先把插件目录压缩成 zip zip -r ponytail-plugin.zip plugin/ # 在支持的命令行浏览器环境下通过策略安装或手动导入 # 这一步不同系统差别较大实际使用还是建议直接拖拽加载还有一种更简单的用法在浏览器地址栏直接打开chrome://extensions后把plugin/文件夹拖进窗口也会自动触发安装。整个过程五秒钟。安装完成后地址栏旁边会出现一个小图标。点开之后你会看到两个主要功能区一个负责“立即整理当前页面”另一个负责“整理后交给 skill 继续处理”。如果当前页面的正文提取成功它会直接预览在弹窗里方便你随时检查抓取质量。3.3 核心配置项说明我的理念是“默认配置能跑进阶配置保手”。首次安装不需要任何配置就能用因为插件内置的默认值适用于大多数资讯页面。下面是几个比较关键的配置项它们都在插件的设置面板里可以改。配置项默认值作用说明max_length3000整理后的正文最长字符数超过则从段落末尾截断防止给模型塞太多无用内容keep_linksfalse是否保留 Markdown 链接。总结场景建议关闭原文存档场景建议打开selector留空手动指定正文区域选择器比如article.main。留空时自动检测output_formatmarkdown输出格式支持markdown、plaintextmodel_url留空skill 层调用的模型接口地址model_name留空模型名称用于请求体携带参数temperature0.3模型生成温度摘要任务越低越好防止自由发挥手动指定selector是一个容易被忽略但是非常实用的功能。某些网站结构特殊自动检测算法找不到正文你只要打开开发者工具找到正文对应的 CSS 路径然后在配置里填上这个选择器ponytail 就会优先使用你指定的区域不再盲猜。3.4 skill 层怎么接入你的个人流程skill 层本质上是一个可以被命令行或自动化流程调用的模块。最简单的用法是把它注册成一个本地的可执行命令export PONYTAIL_URLhttps://example.com/article export PONYTAIL_MODELqwen-plus python pony_tail_skill.py --input $PONYTAIL_URL --output summary.md运行之后它会先调用浏览器插件把网页整理成 Markdown 文件然后拿着这个文件请求你配置的模型接口最后把摘要写入summary.md。如果你用的是带函数调用的模型也可以把 skill 封装成一个函数。调用时模型会自己决定“这个请求需要用到 ponytail”然后传入网页地址拿到整理结果后再基于它回答用户问题。这一套非常适合接进个人知识库、文档助手或者简单的阅读代理里。4. 实操过程与关键代码逻辑4.1 页面主内容的提取逻辑前面说了我采用了一个简化版 Readability 算法。核心思路是先找到页面里“文字密度”最高的容器再围绕这个容器向上向下合并区块。下面是一段可以直接放进 content script 用的简化实现function getMainContent(root document) { const candidates root.querySelectorAll( article, [rolemain], .post-content, .entry-content, .article-content ); let best null; let bestScore -Infinity; candidates.forEach((node) { const text node.innerText || ; const length text.trim().length; // 基本密度分文本越长分数越高同时惩罚带有大量链接的节点 const linkDensity node.querySelectorAll(a).length / Math.max(node.querySelectorAll(p, div).length, 1); const score length - linkDensity * length * 0.2; if (score bestScore) { bestScore score; best node; } }); return best || root.body; }这段代码的思路很简单优先从语义化标签里找候选区然后用“纯文字长度减掉链接噪音”作为打分标准。实际用起来你会发现博客、新闻、帮助文档这类页面基本都能被准确命中速度也很快。拿到最佳候选节点之后还要做一层清理把藏在内部的script、style、noscript、svg和广告相关节点直接移除。注意移除前先存一份原始 HTML万一操作失误还能还原。4.2 HTML 转 Markdown 的细节处理HTML 转 Markdown 是很容易被低估的一步。很多人用正则一替换就完事结果列表全变成一团乱麻。稳定起见我建议用递归遍历的方式逐个节点判断类型再拼接文本下面是核心片段function nodeToMarkdown(node) { if (node.nodeType Node.TEXT_NODE) return node.textContent; const tag node.tagName?.toLowerCase(); let text ; node.childNodes.forEach((child) { text nodeToMarkdown(child); }); if (tag h1) return \n# ${text.trim()}\n; if (tag h2) return \n## ${text.trim()}\n; if (tag h3) return \n### ${text.trim()}\n; if (tag p) return ${text.trim()}\n\n; if (tag li) return - ${text.trim()}\n; if (tag blockquote) return ${text.trim()}\n; if (tag a) { const href node.getAttribute(href) || ; return keepLinksEnabled ? [${text.trim()}](${href}) : text.trim(); } return text; }这里你必须留意两点。第一标题下面我记得加空行不然渲染的时候会粘连成一行第二列表嵌套的情况要比我想象中多如果遇到二级无序列表最好根据父级ul的层级自动缩进两格。这个可以在递归参数里加一个 depth然后生成前缀空格。清理完的文本我会统一做一次空行合并把连续三个以上的换行压缩成两个。如果输出是纯文本还要顺带把 Markdown 符号全部剥掉只留内容本身。4.3 调用模型生成摘要的参数选择技能层调用模型时很多人上来就问“温度调多少max_tokens 调多少”其实这取决于你的输入长度和需要的摘要精度。我一般先统计整理后正文的字符数再按这个数据推算输出长度content_len len(markdown_content) # 目标摘要长度 输入长度的 30% 左右取整百 target_tokens max(200, int(content_len * 0.3 / 100) * 100) # 再留点余量给 prompt 和返回格式 max_tokens min(8000, target_tokens 200)温度建议直接按 0.3 固定。摘要任务本质是压缩信息不是创作。如果调高温度模型会忍不住“润色”内容一不小心就把原文里没有的意思加进去这在事实核查场景里是致命的。如果你要做的是内容改写而不是摘要才考虑把温度调到 0.7 左右。请求体注意携带stream: false实现更简单。响应返回 JSON 之后直接解析 content 字段落盘。如果你用的是新模型记得确认一下它的上下文窗口能不能装下你整篇正文装不下就先在插件层把max_length调小。4.4 输出与导出怎么接笔记软件和日报流程我不想让 ponytail 只停留在控制台输出所以多写了一个导出模块。它支持三种输出目标本地 Markdown 文件最通用适合丢进 Obsidian、Notion 或者其他笔记工具。剪贴板适合快速粘贴到聊天窗口或文档里省去文件管理。追加写入指定日志适合做“每日阅读摘要”流程。每天固定把当天整理过的内容追加到同一个文件里晚上直接拿出来归档。导出模块的配置同样放在同一个配置文件里切换目标只需要改一个参数。我个人最常用的组合是“清理后导出到剪贴板”因为很多时候我并不会立刻让模型参与而是先把原文存下来等有空再统一处理。5. 常见问题与排查技巧实录5.1 为什么抓不到正文或者抓到了侧边栏这个问题我修过的次数最多原因基本逃不开下面几个。第一页面是动态加载的正文区域在滚动之后才渲染出来。我的解决办法是在开始提取前先自动滚动页面到底部再回顶部强制触发懒加载。这个动作在提取逻辑里加一个await new Promise(r setTimeout(r, 800))就行。第二网站是单页应用SPA路由切换过程中 DOM 会大换血。插件需要监听history.pushState事件在路由变化后重新触发提取否则你只会拿到上个页面的残留内容。第三正文标题和正文内容分离两个容器。这点容易漏很多页面把标题放在header里正文在section里如果你只取一个候选节点标题就丢了。我后来在提取结束之后会额外补一次在当前页面里查找h1如果标题文本没有包含在正文里就手动拼到正文最前面。5.2 中文内容乱码或输出字数对不上现代浏览器基本上不会真的乱码除非页面本身用的是meta charsetgb2312这种老编码。不过插件拿到的是渲染后的 DOM编码已经被浏览器解码过了所以插件层通常没有乱码问题。乱码更容易出现在你“手动复制到某些老终端工具”这一步。字数对不上主要有两个原因一个是文本节点里包含大量空白字符和换行统计时没过滤干净另一个是遇到 emoji 和特殊符号JavaScript 的length按 UTF-16 码元计算一个 emoji 可能是两个码元。统一处理办法是统计前做text.replace(/\s/g, ).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, _)先把不可见字符清理掉再算长度。5.3 模型接口报错或返回超时技能层调模型接口时最常见的报错是超时和 JSON 解析失败。超时多半不是网络本身的问题而是你输入文本太长模型处理时间超过了接口默认的 60 秒阈值。解决办法有两个把max_length调小或者在插件配置里把超时时间从 60 秒提到 180 秒。JSON 解析失败通常是因为模型返回的内容里夹带了 Markdown 代码块标记。有的模型会习惯性在摘要外面包一层 json导致你直接json.loads失败。解析前先做一次清理import re text re.sub(r^json\s*|\s*$, , response_text.strip()) data json.loads(text)这个坑非常隐蔽因为大多数时候不会报错只有在你换了某个特定模型之后才频繁出现。如果你不想依赖这个清理逻辑也可以在 prompt 里明确强调“不要输出 Markdown 代码块标记直接输出纯 JSON”能显著降低概率。5.4 浏览器更新或版本差异导致插件失效Chrome 更新之后插件偶尔会失效尤其是每次大版本升级Manifest V3 的一些 API 行为会有细微调整。最典型的例子是scripting.executeScript的权限要求比 Manifest V2 严格得多如果你把 content script 改成动态注入的方式必须确认host_permissions里包含了目标站点。我建议每次浏览器提示升级时先不要急着更新 ponytail。等插件失效了你再升级或者升级后第一时间遍历一遍核心页面测试“整理当前页面”功能。浏览器升级带来的兼容性问题90% 和扩展权限声明有关重新检查manifest.json基本就能定位到问题。另外同一个插件在 Chrome 和 Edge 上虽然可以共用但如果你开了严格隐私模式第三方 cookie 和存储权限会不一样可能导致你的配置项在其中一个浏览器上“消失了”。解决方法是把配置文件放在插件自身的chrome.storage.local里而不是依赖页面级的 localStorage。5.5 把 ponytail 接进 AI 工作流时的注意事项很多人拿到工具第一反应是“我要拿它做一个全自动阅读机器人”我劝你不要一开始就全自动。建议先手动跑通三个场景单个网址整理、单页摘要、批量文件导出。在这三个场景都稳定之后再接进自动化流程。因为自动化的难点不在于调用而在于异常处理网页结构变了怎么办、模型接口限流怎么办、输出文件冲突了怎么办。每个问题都要有一套预案否则自动化跑起来十分钟就断你还要半夜爬起来看日志。另一个容易被忽视的点是 token 成本。跑批量摘要时不控制输入长度费用会非常夸张。我的经验是批量任务里把正文裁剪到 1000 到 1500 字摘要长度控制在 200 字以内信息保全率已经能到八成。为了多保那两成信息去付出三四倍的 token 开销不划算。6. 一些让我印象深刻的小技巧和后续扩展想法6.1 先说几个常规文档里不会写的细节第一个技巧是“摘要质量不好先别急着换模型先看正文清理干不干净”。我遇到过很多次模型发挥不稳定排查到最后发现是正文里混了一堆推荐位文字模型被带偏了。把清理阈值调严之后同样的模型、同样的 prompt效果立刻变好。数据干净比模型强重要得多。第二个技巧是“正文提取时优先选短路径选择器”。如果自动提取总是有问题你手动指定的选择器也不要写太长。选择器越长页面改版的概率就越大。比如#main .article-content就比body div#wrapper main section div.article-content稳定得多。能用类名解决就不要用层级路径。第三个技巧是“让 skill 层默认附带原文标题和来源链接”。不要只把正文丢给模型而是在开头拼上来源{url}和标题{title}。这样模型在总结时会自动带上来源意识不会凭空默认所有信息都来自同一个人。对于做信息聚合的场景这个细节能帮你省很多后面核对的功夫。6.2 这个工具还能往哪些方向继续折腾目前我计划里排在第一位的扩展是给它加一个“整站配置记忆”能力。大概思路是这样每当你手动指定过某个网站的正文选择器插件就把这个配置记下来下次访问同一个域名时自动应用。跑一段时间之后你常用网站的正确选择器会被逐渐积累出来以后基本就不需要手动干预了这个思路在信息流整理场景里特别实用。第二个方向是接入本地模型。跟远程接口相比本地模型的好处是隐私和零成本缺点是速度和效果参差不齐。你完全可以保留现在这套远程接口只是把model_url从云端地址改成本地地址剩下的逻辑几乎不用动。接口协议只要保持 OpenAI 风格兼容性就能拉到最高。第三个方向是多页面合并整理。现在的逻辑是“每页一条摘要”但实际场景里你经常需要搜集某个话题的多篇文章然后合并成一篇综述。我想要的是先批量整理二十个相关页面然后把二十份正文拼成一个临时文档再一次性让模型输出一篇综述。这个功能非常适合做行业调研和竞品分析省下来的时间非常可观。坦白说当初做 ponytail 只是被“手动复制粘贴网页内容”这件事烦得够呛。做到现在反而发现真正让效率起飞的不是哪一段特别神奇的代码而是你愿意为“整理与理解”这条链路多花一点心思。如果你也在被类似的问题困扰完全可以依照我上面写的那几个核心逻辑从最简陋的版本开始搭起。工具这东西自己动手改过一遍用起来才真正顺手。