npm.io
1.1.0 • Published 3d ago

dom-compact

Licence
MIT
Version
1.1.0
Deps
0
Size
514 kB
Vulns
0
Weekly
551

DOM Compact

English

让 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 result
ES 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 会自动补充语义信息:

补充来源

  1. 补充属性data-testiddata-actiondata-icondata-tooltiparia-roledescriptionaria-keyshortcuts — 仅当元素无文本时才追加
  2. 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. (郭玉峰, 吴琼)

Keywords