dom-compact
v1.1.0
Published
DOM Compact - Convert webpage DOM tree to compact text format for LLM
Maintainers
Readme
DOM Compact
让 LLM 看懂网页——将 DOM 树压缩为极简文本,索引化交互元素,一键操作
将网页 DOM 树转换为紧凑文本格式,供 LLM 理解和使用。源自阿里巴巴 PageAgent 的 DOM 脱水算法,独立提取为轻量级库。
为什么需要 DOM Compact?
想象一下:你在构建一个 Web Agent,让 LLM 自主浏览网页、填写表单、点击按钮。你把整个 HTML 喂给 LLM,但问题来了:
场景 1:HTML 太大,Token 爆了
一个普通电商页面的 HTML:150,000+ 字符
LLM 上下文窗口:128K tokens
结果:一个页面就占满了上下文,没有空间思考和回复场景 2:LLM 无法定位和操作元素
LLM:"我想点击登录按钮"
你: "哪个按钮?页面上有 47 个 <button>"
LLM: "呃...第三个?"
结果:操作失败,因为 LLM 无法精确引用 DOM 元素场景 3:页面动态变化,Agent 迷失了
LLM 点击"加载更多"后,页面新增了 20 个元素
但 LLM 不知道哪些是新的,无法高效处理增量变化
结果:重复处理已有内容,浪费 Token 和时间DOM Compact 完美解决了这些问题:
| 问题 | 直接喂 HTML | DOM Compact |
|------|-----------|-------------|
| Token 消耗 | 150K+ 字符原始 HTML | 压缩至 2K~5K 紧凑文本 |
| 元素定位 | "页面上第 3 个 button" | [4]<button >登录 /> — 精确索引 |
| 动态变化 | 无法感知 | *[5] 新增、~[1] 变化 — 自动标记 |
| 纯图标按钮 | LLM 看不到语义 | getNoTextElements() 返回选择器,视觉 LLM 补充 |
| DOM 安全 | 操作可能破坏页面 | 只读提取 + cloneNode,零副作用 |
特性
- DOM 树提取:从网页中提取扁平化的 DOM 树结构
- 紧凑输出:将 DOM 转换为针对 LLM 消费优化的文本格式
- 交互元素检测:自动识别并索引交互元素(按钮、链接、输入框等)
- 变更检测:追踪元素新增(
*[N])和属性/文本变化(~[N]),支持只读探测模式 - 无文本元素检测:识别没有文本内容的交互元素,返回 CSS 选择器用于视觉文本补充
- 页面信息:获取视口尺寸、滚动位置等页面信息
- 空元素过滤:过滤无意义的空元素,减少 Token 消耗
安装
npm install dom-compact快速开始
浏览器注入(推荐)
<script src="dom-compact.iife.js"></script>
<script>
// 检查是否已注入
if (window.DomCompact?.getFlatTree) {
const flatTree = DomCompact.getFlatTree({ viewportExpansion: -1 })
const text = DomCompact.flatTreeToString(flatTree)
console.log(text)
}
</script>Playwright 自动化
from playwright.async_api import async_playwright
async def get_compact_content(page):
"""获取页面紧凑内容"""
# 检查是否已注入
is_injected = await page.evaluate("""
() => typeof window.DomCompact?.getFlatTree === 'function'
""")
if not is_injected:
# 读取并注入 IIFE 脚本
with open('dom-compact.iife.js', 'r', encoding='utf-8') as f:
iife_code = f.read()
await page.add_script_tag(content=iife_code)
# 获取紧凑内容
result = await page.evaluate("""
() => {
const flatTree = DomCompact.getFlatTree({ viewportExpansion: -1 });
const text = DomCompact.flatTreeToString(flatTree);
const selectorMap = DomCompact.getSelectorMap(flatTree);
return {
text,
nodeCount: Object.keys(flatTree.map).length,
interactiveCount: selectorMap.size
};
}
""")
return resultES Module 方式
import { getFlatTree, flatTreeToString, getPageInfo } from 'dom-compact'
// 获取页面信息
const pageInfo = getPageInfo()
console.log(pageInfo)
// {
// viewport_width: 1920,
// viewport_height: 1080,
// page_width: 1920,
// page_height: 5000,
// scroll_x: 0,
// scroll_y: 500,
// pixels_above: 500,
// pixels_below: 3420,
// ...
// }
// 获取扁平化 DOM 树
const flatTree = getFlatTree({
viewportExpansion: -1, // -1 = 全页面, 0 = 仅视口
})
// 紧凑输出(供 LLM 阅读)
const compact = flatTreeToString(flatTree)
console.log(compact)
// [0]<a aria-label=Home />
// [1]<button >登录 />
// [2]<input placeholder=搜索 />
// ...
// 获取交互元素映射
const selectorMap = getSelectorMap(flatTree)
console.log(selectorMap.get(0)) // 第一个交互元素使用示例
示例 1:基础提取与紧凑输出
对于一个包含导航、登录表单和内容的页面:
<nav id="main-nav" aria-label="主导航">
<a href="/" aria-label="首页">首页</a>
<a href="/docs">文档</a>
</nav>
<main>
<form id="login-form">
<input type="text" name="username" placeholder="请输入用户名" />
<input type="password" name="password" placeholder="请输入密码" />
<button type="submit" id="login-btn">登录</button>
</form>
<p>这是一段普通文本内容。</p>
</main>提取紧凑文本:
const flatTree = DomCompact.getFlatTree({ viewportExpansion: -1 })
const text = DomCompact.flatTreeToString(flatTree)
console.log(text)输出结果:
[0]<a aria-label=首页 />
[1]<a >文档 />
[2]<input type=text name=username placeholder=请输入用户名 />
[3]<input type=password name=password placeholder=请输入密码 />
[4]<button type=submit >登录 />
这是一段普通文本内容。其中 [0]~[4] 为交互元素的索引,LLM 可通过索引操作对应元素。
示例 2:获取页面信息
const pageInfo = DomCompact.getPageInfo()
console.log(pageInfo)输出结果:
{
viewport_width: 1280,
viewport_height: 720,
page_width: 1280,
page_height: 3000,
scroll_x: 0,
scroll_y: 500,
pixels_above: 500, // 视口上方未显示的像素
pixels_below: 1780, // 视口下方未显示的像素
pixels_right: 0,
pixels_left: 0
}示例 3:变更检测
// 1. 首次调用,建立缓存基线
DomCompact.getFlatTree({ viewportExpansion: -1 })
// 2. 页面发生变化(如 JS 动态修改了按钮文字)
// document.getElementById('login-btn').textContent = '提交'
// 3. 只读探测:检查是否有变化(不更新缓存)
const changed = DomCompact.hasDomChanged()
console.log(changed) // true
// 4. 再次提取,输出中标记变化
const tree = DomCompact.getFlatTree({ viewportExpansion: -1 })
const text = DomCompact.flatTreeToString(tree)
// ~[4]<button type=submit ✎text >提交 />
// ↑ ~ 表示属性或文本变化,✎text 标记文本发生了改变示例 4:新增元素检测
// 首次提取
DomCompact.getFlatTree({ viewportExpansion: -1 })
// 页面动态添加了新按钮
// document.body.appendChild(Object.assign(document.createElement('button'), { textContent: '新按钮' }))
// 再次提取,新元素用 * 标记
const tree = DomCompact.getFlatTree({ viewportExpansion: -1 })
const text = DomCompact.flatTreeToString(tree)
// *[5]<button >新按钮 />
// ↑ * 表示新增元素示例 5:通过索引操作元素(Playwright)
# 注入库后,通过索引精确定位并操作元素
handle = await page.evaluate_handle("DomCompact.getElementByIndex(4)")
element = handle.as_element()
# 点击登录按钮
await element.click()
# 或在输入框中填写内容
input_handle = await page.evaluate_handle("DomCompact.getElementByIndex(2)")
await input_handle.as_element().fill("my_username")示例 6:无文本元素检测与自动补充
对于纯图标按钮、SVG 按钮等没有文本内容的交互元素,flatTreeToString 会自动补充语义信息:
补充来源:
- 补充属性:
data-testid、data-action、data-icon、data-tooltip、aria-roledescription、aria-keyshortcuts— 仅当元素无文本时才追加 - SVG 子文本:自动提取 SVG 内的
<title>、<desc>、<text>、<tspan>文本,以svg-text=xxx属性展示
<button data-testid="search-btn"><svg><circle r="10"/></svg></button>
<button data-action="close-modal"><svg><circle r="8"/></svg></button>
<button><svg><title>播放</title><circle r="10"/></svg></button>
<button>搜索</button>输出结果:
[0]<button data-testid=search-btn />
[1]<button data-action=close-modal />
[2]<button svg-text=播放 />
[3]<button >搜索 />注意:有文本的元素(如
[3])不受影响,不会追加补充属性。仍无法识别的纯图标元素留给上层视觉 LLM 兜底处理。
对于需要视觉 LLM 补全的场景,可使用 getNoTextElements 获取无文本元素列表:
const noTextElements = DomCompact.getNoTextElements({ viewportExpansion: -1 })
console.log(noTextElements)
// [ { index: 0, selector: 'button[data-testid="search-btn"]', tagName: 'button' } ]示例 7:生成稳定 CSS 选择器
const element = document.getElementById('login-btn')
const selector = DomCompact.generateUniqueSelector(element)
console.log(selector) // "#login-btn"
// 优先级:id > data-testid > aria-label > title > role > class > nth-of-type示例 8:交互元素黑名单/白名单
// 排除特定元素(如导航栏),减少无关交互元素
const tree = DomCompact.getFlatTree({
viewportExpansion: -1,
interactiveBlacklist: [document.getElementById('main-nav')]
})
// 或指定只关注某些元素
const tree2 = DomCompact.getFlatTree({
viewportExpansion: -1,
interactiveWhitelist: [document.getElementById('login-form')]
})示例 9:保留语义化标签
// 默认输出不保留 nav/header/footer 等非交互语义标签
const text1 = DomCompact.flatTreeToString(flatTree)
// 开启后保留语义标签,提供更多结构上下文
const text2 = DomCompact.flatTreeToString(flatTree, [], true)
// <nav>
// [0]<a aria-label=首页 />
// [1]<a >文档 />
// </nav>
// <header>
// ...
// </header>API
| 函数 | 说明 |
|------|------|
| getPageInfo() | 获取页面尺寸、滚动位置等 |
| getFlatTree(config) | 获取扁平化 DOM 树 |
| flatTreeToString(tree, includeAttributes?, keepSemanticTags?) | 将 DOM 树转换为紧凑文本 |
| getSelectorMap(tree) | 获取索引 → 元素映射 |
| getElementTextMap(html) | 获取索引 → 元素文本映射 |
| getElementByIndex(index) | 通过索引获取 DOM 元素 |
| hasDomChanged() | 只读探测:检查 DOM 是否发生变化(新增 *[N] 或变化 ~[N]),不更新缓存 |
| generateUniqueSelector(element) | 为元素生成稳定的 CSS 选择器(优先级:id > data-testid > aria-label > class > nth-of-type) |
| getNoTextElements(opts?) | 检测无文本交互元素,返回 {index, selector, tagName}[],用于视觉文本补充 |
通过索引操作元素
紧凑输出中的元素带有索引 [0]、[1]、[2]...,可用于操作元素。
索引与元素绑定机制
getFlatTree() 调用时会将 索引号 → DOM 元素引用 的映射缓存到 globalThis.__domCompactRefMap__。后续 getElementByIndex() 直接从缓存取元素引用,而非重新遍历 DOM 树。这保证了:
- 索引不错位:快照后即使 DOM 发生变化(插入/删除元素),操作仍指向快照时刻的元素
*~标记不被消费:getElementByIndex()不再内部调用getFlatTree(),不会影响变更检测缓存- 失效安全:如果元素已脱离 DOM(
isConnected === false),返回null,调用方应重新获取快照 - 降级兜底:如果缓存不存在(未调用过
getFlatTree),自动降级为重算模式
注意:
dryRun: true模式下不更新 refMap 缓存,确保只读探测不会影响后续操作。
JavaScript
// 先获取快照(建立 refMap 缓存)
const flatTree = DomCompact.getFlatTree({ viewportExpansion: -1 })
// 通过索引获取 DOM 元素(从缓存取,O(1))
const element = DomCompact.getElementByIndex(0)Playwright
# 获取快照
result = await page.evaluate("""
() => {
const tree = DomCompact.getFlatTree({ viewportExpansion: -1 });
return DomCompact.flatTreeToString(tree);
}
""")
# 获取 ElementHandle(从快照缓存定位)
handle = await page.evaluate_handle("DomCompact.getElementByIndex(0)")
element = handle.as_element()
# 使用 Playwright 原生 API
await element.click()
await element.fill("hello")配置
interface DomConfig {
// 视口扩展:-1=全页面, 0=仅视口, 正数=扩展像素数
viewportExpansion?: number
// 交互元素黑名单
interactiveBlacklist?: (Element | (() => Element))[]
// 交互元素白名单
interactiveWhitelist?: (Element | (() => Element))[]
// 输出中包含的属性
includeAttributes?: string[]
// 保留语义化标签(nav, main, header 等)
keepSemanticTags?: boolean
// 只读探测模式:检测变化但不更新缓存
// 适用于检查 DOM 是否变化,而不影响后续 getFlatTree() 的 * ~ 标记
dryRun?: boolean
}变更检测
getFlatTree() 自动追踪交互元素的变化,通过紧凑输出中的标记表示:
| 标记 | 含义 | 示例 |
|------|------|------|
| *[N] | 新增元素 | *[3]<button >提交 /> |
| ~[N] | 属性或文本变化 | ~[1]<input value=新值 ✎value /> |
| [N] | 无变化 | [0]<a aria-label=首页 /> |
变更检测使用内部缓存(WeakMap),仅在当前页面生命周期内有效。页面刷新后缓存重置。
hasDomChanged():只读探测,检查 DOM 是否变化但不更新缓存dryRun: true:在getFlatTree()中启用只读模式,检测变化但不更新缓存
无文本元素检测
getNoTextElements() 检测没有文本内容且没有文本描述属性(aria-label、title、alt、placeholder)的交互元素。返回其索引、CSS 选择器和标签名。主要用于视觉文本补充——通过视觉 LLM 识别纯图标按钮的语义。
const noTextElements = DomCompact.getNoTextElements({ viewportExpansion: -1 })
// 返回: [{ index: 5, selector: 'div.tooltip-wrapper > div > div', tagName: 'div' }, ...]参数指南
| 参数 | 场景 | 说明 |
|------|------|------|
| viewportExpansion: -1 | 全页面分析 | 提取所有交互元素 |
| viewportExpansion: 0 | 视口操作 | 仅提取当前可见元素 |
| viewportExpansion: 500 | 预加载 | 提取视口附近的元素 |
| keepSemanticTags: true | 结构分析 | 保留语义化标签以获取更多上下文 |
| includeAttributes: [...] | 精细控制 | 仅包含指定属性 |
最佳实践
注入检测
// 方法一:检查全局变量
function isInjected() {
return typeof window.DomCompact !== 'undefined'
}
// 方法二:检查 API 方法(推荐)
function isInjected() {
return typeof window.DomCompact?.getFlatTree === 'function'
}空元素过滤
const flatTree = DomCompact.getFlatTree({ viewportExpansion: -1 })
const selectorMap = DomCompact.getSelectorMap(flatTree)
// 过滤空元素
const nonEmptyElements = new Map()
selectorMap.forEach((node, index) => {
const hasText = (node.text || '').trim().length > 0
const hasAttrs = Object.keys(node.attributes || {}).length > 0
if (hasText || hasAttrs) {
nonEmptyElements.set(index, node)
}
})性能优化
// 大页面分批处理
const flatTree = DomCompact.getFlatTree({ viewportExpansion: 0 })
// 滚动后重新获取
window.scrollTo(0, 1000)
const flatTree2 = DomCompact.getFlatTree({ viewportExpansion: 0 })构建
# 安装依赖
npm install
# 构建(ESM + IIFE)
npm run build
# 仅构建 ESM
npm run build:lib
# 仅构建 IIFE
npm run build:iife输出文件
| 文件 | 格式 | 用途 |
|------|------|------|
| dist/lib/dom-compact.js | ESM | 模块导入 |
| dist/lib/index.d.ts | TypeScript | 类型声明 |
| dist/iife/dom-compact.iife.js | IIFE | 浏览器注入 |
项目结构
dom-compact/
├── src/
│ ├── dom/
│ │ ├── dom_tree/ # DOM 树提取核心
│ │ ├── index.ts # 紧凑 API、变更检测、选择器生成
│ │ └── getPageInfo.ts # 页面信息
│ ├── utils/ # 工具函数
│ └── index.ts # 包入口
├── dist/ # 构建输出
├── vite.config.js # ESM 构建配置
└── vite.iife.config.js # IIFE 构建配置来源
本项目衍生自 Alibaba PageAgent,将 DOM 脱水算法提取为独立库。由郭玉峰和吴琼(北京锋通科技有限公司)扩展和修改,以增强可用性并添加功能。
许可证
MIT
Copyright (c) 2026 SimonLuvRamen Copyright (c) 2026 Alibaba Group Holding Limited Copyright (c) 2026 Beijing Fengtong Technology Co., Ltd. (郭玉峰, 吴琼)
