ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenClaw Web Readability 插件参考:从本地 HTML 响应中抽取可读正文的实现与原理

OpenClaw Web Readability 插件参考:从本地 HTML 响应中抽取可读正文的实现与原理 OpenClaw Web Readability 插件参考从本地 HTML 响应中抽取可读正文的实现与原理【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw本文基于 OpenClaw 仓库中的插件参考文档 web-readability.md 及其对应实现 extensions/web-readability系统讲解openclaw/web-readability-plugin的契约webContentExtractors、运行链路与安全守卫HTML 大小与嵌套深度上限。读完后你能理解该插件如何在 web-fetch 流程中把原始 HTML 转成 Markdown 或纯文本正文并能依据源码评估其边界条件与测试覆盖。1. 插件定位与分发形态参考文档给出的核心定义是从本地 HTML 的 web fetch 响应中提取可读的文章正文Extract readable article content from local HTML web fetch responses。也就是说它处理的是已经在本地、抓取回来的 HTML 字符串而不是直接发起网络请求。插件的分发信息与文档 Distribution 小节一致包名openclaw/web-readability-plugin见 package.json当前版本为2026.9.3且标记为private: true说明它不通过 npm 独立发布而是随 OpenClaw 仓库内置分发。安装路径文档标注 included in OpenClaw内置在 OpenClaw 中与插件清单 openclaw.plugin.json 中的enabledByDefault: true相互印证——默认启用无需额外安装步骤。运行时依赖仅两个mozilla/readabilityMozilla 的正文抽取算法与linkedom纯 JS 的 DOM 实现。从 package.json 的依赖声明可以看到版本被固定为0.6.0和0.18.13。插件入口 index.ts 非常克制只注册了一个插件 ID 和元信息export default definePluginEntry({ id: web-readability, name: Web Readability Extraction, description: Extract readable article content from local HTML web fetch responses., register() { // Runtime is exposed through web-content-extractor.ts so hot web-fetch paths can // load only the narrow extractor artifact instead of the full plugin entrypoint. }, });注意register()是空的源码注释解释了这个设计真正的运行时代码暴露在一个更窄的模块web-content-extractor.ts中这样热路径上的 web-fetch 只需要加载抽取器本身而不必加载完整的插件入口。这是一个面向启动/加载开销的工程取舍下文第 4 节会看到运行时如何消费这个产物。2. 契约声明webContentExtractors参考文档的 Surface 小节指出该插件贡献的契约是webContentExtractors。在清单文件 openclaw.plugin.json 中可以看到完整声明{ id: web-readability, activation: { onStartup: false }, enabledByDefault: true, name: Web Readability Extraction, description: Extract readable article content from local HTML web fetch responses., contracts: { webContentExtractors: [readability] }, configSchema: { type: object, additionalProperties: false, properties: {} } }几个值得注意的点契约值为[readability]即该插件向运行时注册的抽取器 ID 是readability与实现文件 web-content-extractor.ts 中导出的工厂函数返回的id: readability完全对应。onStartup: false插件不在进程启动时主动激活而是按需由 web-fetch 流程惰性加载这与第 1 节的窄产物设计一致。configSchema为空对象该插件没有任何可配置项。它是一个行为固定、随默认启用的能力插件用户没有需要调参的配置面。3. 抽取器接口的类型契约抽取器插件需要实现的接口定义在 web-content-extractor-types.ts 中并通过公共 SDK 子路径 web-content-extractor.ts 对外暴露/** Web content extraction mode requested from extractor plugin. */ export type WebContentExtractMode markdown | text; /** Request passed to a web content extractor plugin. */ export type WebContentExtractionRequest { html: string; url: string; extractMode: WebContentExtractMode; }; /** Result returned by a web content extractor plugin. */ export type WebContentExtractionResult { text: string; title?: string; }; /** Web content extractor plugin contract. */ export type WebContentExtractorPlugin { id: string; label: string; autoDetectOrder?: number; extract: (request: WebContentExtractionRequest) PromiseWebContentExtractionResult | null; };接口要点请求输入是{ html, url, extractMode }原始 HTML 字符串、来源 URL用于解析相对链接与定位以及抽取模式markdown或text。返回值可以是null——这是一个允许放弃的契约任何抽取器都可以在无法可靠提取时返回空由上层做回退。autoDetectOrder是自动探测排序号web-readability 插件将其设为10见下文数值越小越先被尝试。除了类型SDK 还导出了几个 HTML 清理工具函数sanitizeHtml、htmlToMarkdown、normalizeWhitespace、stripInvisibleUnicode等见 src/plugin-sdk/web-content-extractor.ts抽取器实现直接复用这些共享助手保证与 web-fetch 主链路的 HTML 清洗行为一致。4. 核心实现安全守卫 Readability 解析实现全部集中在 extensions/web-readability/web-content-extractor.ts。整个抽取流程可以拆为四步清洗 HTML → 安全前置检查 → Readability 解析 → 按模式渲染输出。4.1 两个硬性的安全阈值const READABILITY_MAX_HTML_CHARS 1_000_000; const READABILITY_MAX_ESTIMATED_NESTING_DEPTH 3_000;大小阈值 1,000,000 字符清洗后的 HTML 超过一百万字符直接放弃抽取返回null避免对超大页面做昂贵的 DOM 构建。嵌套深度阈值 3,000即使总长度不超标若标签嵌套过深也直接拒绝。这一点由一个自研的轻量扫描函数exceedsEstimatedHtmlNestingDepth实现——它用charCodeAt逐字节扫描起始位置识别开/闭标签、跳过!/?声明、跳过自结束标签/并维护一个 void 标签集合area、br、img、meta等 14 种无闭合标签避免把br、img这类本就不产生嵌套深度的标签计入统计。为什么需要嵌套深度检查从源码结构看深度上万的病态 HTML 会让纯 JS 的linkedomDOM 构建退化成极深的树操作甚至逼近栈/堆压力。这是一个典型的在昂贵的第三方解析之前做廉价预检的防护设计。4.2 惰性加载第三方依赖const READABILITY_MODULE mozilla/readability; // The public worker bundle avoids per-module DOM loading; sanitized HTML excludes canvas. const LINKEDOM_MODULE linkedom/worker; const loadReadabilityDeps createLazyRuntimeModule(() Promise.all([ import(READABILITY_MODULE) as Promisetypeof import(mozilla/readability), import(LINKEDOM_MODULE) as Promisetypeof import(linkedom/worker), ]), );两个细节使用createLazyRuntimeModule来自openclaw/plugin-sdk/lazy-runtime包装动态 import只有真正调用extract时才首次加载之后复用缓存的模块实例。特意选择linkedom/worker子入口注释说明公共 worker bundle 避免逐模块加载 DOM即一次性拿到打包好的 DOM 实现减少模块解析开销。4.3 抽取主函数extractWithReadabilityasync function extractWithReadability(request: WebContentExtractionRequest) { const cleanHtml await sanitizeHtml(request.html); if ( cleanHtml.length READABILITY_MAX_HTML_CHARS || exceedsEstimatedHtmlNestingDepth(cleanHtml, READABILITY_MAX_ESTIMATED_NESTING_DEPTH) ) { return null; } try { const [{ Readability }, { parseHTML }] await loadReadabilityDeps(); const { document } parseHTML(cleanHtml, { location: { href: request.url } }); const textMode request.extractMode text; // Text mode consumes textContent; skip serializing the HTML it would discard. const reader new Readability(document, textMode ? { serializer: () } : undefined); const parsed reader.parse(); if (!parsed) { return null; } const title parsed.title || undefined; const rendered textMode ? { text: normalizeWhitespace(parsed.textContent ?? ), title } : htmlToMarkdown(parsed.content ?? ); const text stripInvisibleUnicode(rendered.text); return text ? { text, title: title ?? rendered.title } : null; } catch { return null; } }逐步解读sanitizeHtml先行先做可见性/安全清洗该助手定义在 web-fetch 共享工具链中后续所有阈值检查都基于清洗后的 HTML。parseHTML(cleanHtml, { location: { href: request.url } })把请求中的url作为文档位置传给 linkedom这使得 Readability 在解析页面中的相对链接如../next时能还原成绝对 URL——测试用例 4.4 中的断言[Continue reading](https://example.com/next)正是验证了这一点。text 模式的性能优化text 模式下Readability只需要textContent不需要序列化正文 HTML所以传入{ serializer: () }让 Readability 跳过 HTML 序列化这一步注释原文是 skip serializing the HTML it would discard。两种输出渲染路径text模式取parsed.textContent并做normalizeWhitespace归一化markdown模式把parsed.content正文 HTML交给htmlToMarkdown转成 Markdown。 两条路径最后都过一遍stripInvisibleUnicode剔除零宽字符等不可见 Unicode。空结果语义reader.parse()返回空、或最终text为空都返回null任何异常也被try/catch兜住返回null。整个函数永不抛出失败一律以无结果表达交由上层回退。标题来源优先用 Readability 的parsed.titlemarkdown 模式下若没有则回退到htmlToMarkdown产出的rendered.title。4.4 工厂函数与注册顺序export function createReadabilityWebContentExtractor(): WebContentExtractorPlugin { return { id: readability, label: Readability, autoDetectOrder: 10, extract: extractWithReadability, }; }autoDetectOrder: 10表明在多抽取器共存时它属于高优先级候选。这个数值如何参与排序由运行时桥接代码决定见下一节。5. 运行时如何发现并调用该插件参考文档只写了一行 Contracts:webContentExtractors而完整的发现链路可以从运行时桥接 web-content-extractors.runtime.ts 得到印证export function resolvePluginWebContentExtractors(params?: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; onlyPluginIds?: readonly string[]; }): PluginWebContentExtractorEntry[] { const extractors: PluginWebContentExtractorEntry[] []; for (const plugin of resolveEnabledBundledManifestContractPlugins({ config: params?.config, workspaceDir: params?.workspaceDir, env: params?.env, onlyPluginIds: params?.onlyPluginIds, contract: webContentExtractors, })) { const loaded loadBundledWebContentExtractorEntriesFromDir({ dirName: plugin.id, pluginId: plugin.id, }); if (loaded) { extractors.push(...loaded); } } return sortPluginEntriesForAutoDetect(extractors); }从源码结构看调用链是resolveEnabledBundledManifestContractPlugins({ contract: webContentExtractors })扫描启用状态的内置插件清单即各扩展目录下的openclaw.plugin.json筛出声明了webContentExtractors契约的插件loadBundledWebContentExtractorEntriesFromDir按插件 ID 定位目录加载其窄产物也就是web-content-extractor.ts这类入口而非完整插件入口拿到抽取器实例并打上pluginId元数据类型见PluginWebContentExtractorEntryweb-content-extractor-types.tssortPluginEntriesForAutoDetect按autoDetectOrder排序web-readability 的10在此生效。消费方则是 web-fetch 工具链src/agents/tools/web-fetch.ts 中存在对抽取器的引用同目录测试文件web-fetch.test-mocks.ts、web-fetch.signal.test.ts中也有相关 mock即抓取到的页面 HTML 会先经过这套插件化抽取器失败或返回null时再回退到基础 HTML 内容提取SDK 中同样导出了extractBasicHtmlContent作为兜底助手。6. 测试用例印证的关键行为web-content-extractor.test.ts 用一个包含导航、文章、页脚的SAMPLE_HTML固定样本验证了四条与文档提取可读内容承诺直接对应的行为测试请求参数断言印证的行为extracts readable textextractMode: text文本含正文、title Example Articletext 模式产出纯文本并保留标题extracts readable markdownextractMode: markdown文本含[Continue reading](https://example.com/next)markdown 模式将相对链接解析为绝对 URLdoes not count void tags toward the nesting limit在article内插入 3100 个BR仍成功抽取出正文void 标签不触发嵌套深度拒绝4.1 节设计rejects excessively nested HTML before extraction用 3001 层section包裹样本返回null嵌套深度超 3000 时在解析前直接拒绝最后一个用例值得强调3001 层section的样本没有走到 linkedom/Readability而是在exceedsEstimatedHtmlNestingDepth预检阶段被拒——这正是前置廉价检查保护昂贵解析的设计目标测试把它钉死成了回归约束。7. 小结能力边界与适用前提结合文档与源码该插件的边界可以归纳为输入本地已抓取的 HTML 字符串 来源 URL输出{ text, title? }或null。模式text归一化纯文本与markdown正文转 Markdown含相对链接绝对化两种由调用方按extractMode指定。无配置面configSchema为空对象行为由 openclaw.plugin.json 中的固定契约声明与默认启用状态决定。拒绝条件清洗后 HTML 超过 1,000,000 字符、估算嵌套深度超过 3,000、Readability 无法解析出正文、或最终文本为空——任一条件满足即返回null由上层web-fetch 链路决定回退策略。适用前提它面向文章型页面效果最佳正文/导航/页脚可分离的文档结构对于 JS 重度渲染的 SPA从源码结构看它只处理抓取到的静态 HTML不负责额外渲染。相关入口文件汇总插件参考文档 docs/plugins/reference/web-readability.md、插件入口 extensions/web-readability/index.ts、抽取器实现 extensions/web-readability/web-content-extractor.ts、SDK 子路径 src/plugin-sdk/web-content-extractor.ts、运行时桥接 src/plugins/web-content-extractors.runtime.ts、消费方工具 src/agents/tools/web-fetch.ts。【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表