Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
  • 01_tio-boot 简介

    • 01_tio-boot 简介
    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 02_部署
    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FatJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 使用Systemctl启动项目
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
    • 历史部署页与替代方案
    • 胖包与瘦包的打包与部署
  • 03_配置

    • 03_配置
    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
    • 开启虚拟线程(Virtual Thread)
    • 框架级错误通知
  • 04_原理

    • 04_原理
    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • 05_json
    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 06_web
    • 概述
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件上传
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • 重定向和转发
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • Controller拦截器
    • 请求拦截器
    • LoggingInterceptor
    • 全局异常处理器
    • 异步处理
    • 动态 返回 CSS 实现
    • 返回图片
    • 跨域
    • 添加 Controller
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • handler入门
    • 返回 multipart
    • Handler 的请求方法与错误响应
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 常用工具类
    • HTTP Basic 认证
    • Http响应加密
    • 使用零拷贝发送大文件
    • 分片上传
    • 接口访问统计
    • 接口请求和响应数据记录
    • WebJars
    • JProtobuf
    • 测速
    • Gzip Bomb:使用压缩炸弹防御恶意爬虫
  • 07_validate

    • 07_validate
    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 08_websocket
    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • 09_java-db
    • Db 工具类
    • java‑db
    • 操作数据库入门示例
    • SQL 模板 (SqlTemplates)
    • 数据源配置与使用
    • ActiveRecord
    • Db 工具类
    • 批量操作
    • Model
    • Model生成器
    • 注解
    • 异常处理
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
    • Db 与 PostgreSQL 业务实践
  • 10_api-table

    • 10_api-table
    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • ApiTable 的权限与业务边界
    • ApiTable 联调与故障定位
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
    • TQL(Table SQL)前端输入规范
  • 11_aop

    • 11_aop
    • JFinal-aop
    • Aop 工具类
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 自定义注解拦截器
    • 原理解析
  • 12_cache

    • 12_cache
    • Caffeine
    • CacheUtils 工具类
    • 使用 java-db 整合 ehcache
  • 13_认证和权限

    • 13_认证
    • FixedTokenInterceptor
    • TokenManager
    • 数据表
    • 匿名登录
    • 个人中心
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
  • 14_i18n

    • 14_i18n
    • i18n
  • 15_enjoy

    • 15_enjoy
    • tio-boot 整合 Enjoy 模版引擎文档
    • Tio-Boot 整合 Java-DB 与 Enjoy 模板引擎示例
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Tio Boot + Enjoy:分页与 SEO 实战指南
    • TioBoot + Enjoy 生成 robots.txt 与 sitemap.xml:实战与SEO指南
    • Enjoy 使用示例
  • 16_定时任务

    • 16_定时任务
    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • 17_tests
    • TioBootTest:环境与 AOP 初始化
    • 真实 HTTP 集成测试
    • 数据库集成测试与隔离
  • 18_mybatis

    • 18_mybatis
    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 19_redis

    • 19_redis
    • 使用 Docker 安装 Redis
    • 使用 java-db 整合 Redis
    • Java DB Redis 相关 Api
    • redis 使用示例
    • 和 RedisTemplate 协作
    • 使用 Jedis 连接池接入 Redis
    • hutool RedisDS
    • Redisson
    • Caffeine 与 Redis 两级缓存
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
  • 20_mongodb

    • 20_mongodb
    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 21_elastic-search

    • 21_elastic-search
    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 22_mq

    • 22_mq
    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_kafka

    • 23_kafka
    • Kafka
    • AWS MSK
  • 24_postgresql

    • 24_postgresql
    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • 索引
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 全文检索
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 25_mysql

    • 25_mysql
    • 使用 Docker 运行 MySQL
    • 常见问题
  • 26_oceanbase

    • 26_oceanbase
    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • 待定
  • 27_oracle

    • 27_oracle
    • Oracle
  • 28_sqlserver

    • 28_sqlserver
    • SQL Server
  • 29_sqlite

    • 29_sqlite
    • SQLite
  • 30_integration_thirty_party

    • 30_integration_thirty_party
    • 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
    • 集成datadog
  • 31_tio

    • 31_tio
    • TioBootServer
    • 独立端口启动 TCP 服务器
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • SSL
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
    • Https建立连接过程
  • 32_tio-utils

    • 32_tio-utils
    • tio-utils
    • EnvUtils 配置工具
    • Notification
    • Email
    • JSON
    • File
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • HttpUtils
    • ByteBufferUtils
    • 系统监控
    • 线程
    • 虚拟线程
    • 毫秒并发 ID (MCID) 生成方案
  • 33_tio-http-server

    • 33_tio-http-server
    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
    • tio-http-server 与 tio-boot 的使用边界
  • 34_tio-websocket

    • 34_tio-websocket
    • WebSocket 服务器
    • WebSocket Client
    • TCP数据转发
  • 35_magic-script

    • 35_magic-script
    • tio-boot 与 magic-script 集成指南
  • 36_groovy

    • 36_groovy
    • tio-boot 整合 Groovy
    • 调试常用脚本
  • 37_spider

    • 37_spider
    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 38_dubbo

    • 38_dubbo
    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 39_spring

    • 39_spring
    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • tio-boot 整合 spring-boot-starter
    • Tio Boot 整合 Spring Boot Starter db
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 40_spring-cloud

    • 40_spring-cloud
    • tio-boot spring-cloud
  • 41_aio

    • 41_aio
    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
    • Java 21 中的虚拟线程与 AIO
  • 42_netty

    • 42_netty
    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 43_netty-boot

    • 43_netty-boot
    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 44_quarkus

    • 44_quarkus
    • Quarkus(无 HTTP)整合 tio-boot(有 HTTP)
    • tio-boot + Quarkus + Hibernate ORM Panache
    • tio-boot + Quarkus + Hibernate ORM Panache + jOOQ 整合方案
  • 45_jooq

    • 45_jooq
    • 使用配置类方式整合 jOOQ
    • tio-boot + jOOQ 事务管理
    • 批量操作与性能优化
    • 整合agroal
    • 代码生成与类型安全
    • 基于 Record / POJO 增删改查
    • UPSERT、批量更新、返回主键与高级 SQL
    • 的多表关联查询、DTO 投影、聚合统计与视图封装
    • 的窗口函数、CTE、JSON 查询与 PostgreSQL 高级 SQL 实战
    • tio-boot + jOOQ 的审计字段、乐观锁、数据权限与企业级 Repository 设计
    • 测试策略、SQL 日志、性能诊断与生产排障
    • 多租户、读写分离与多数据源设计
    • 代码生成治理、数据库迁移与团队协作规范实战
  • 46_media

    • 46_media
    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • 待定
  • 47_asr

    • 47_asr
    • Whisper-JNI
  • 48_tts

    • 48_tts
  • 49_ocr

    • 49_ocr
  • 50_native-media

    • 50_native-media
    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
    • 获取视频长度
    • 保存视频的最后一帧
    • 添加水印
    • linux版本
  • 51_cv

    • 51_cv
    • 使用 Java 运行 YOLOv8 ONNX 模型进行目标检测
    • tio-boot整合yolo
    • ONNX Runtime 推理说明
    • Paddle Structure
    • tio-boot 整合 Paddle Structure
    • tio-boot整合Paddle Structure 提取图片
    • U2Net 图片去背景原理
    • tio-boot 整合 U2Net 实现图片去背景
  • 52_telegram4j

    • 52_telegram4j
    • 数据库设计
    • 基于 HTTP 协议开发 Telegram 翻译机器人
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
    • 处理回调查询
    • Reactor
    • 文档翻译
    • 使用 Tio-Boot 整合 tdlight
    • tio-boot 整合 TelegramBots
    • tio-boot 整合 Telegram-Bot-Utils
    • Telegram-Bot-Utils 使用指南
  • 53_telegram-bots

    • 53_telegram-bots
    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 54_java-openai

    • 54_java-openai
    • 简介
    • 流式生成
    • 图片多模态输入
    • Google Gemini接入
    • google Vertex AI 接入
    • Perplexity API
    • WhisperClient 语音识别
    • SupadataClient 获取视频字幕
    • GiteeClient 文档解析与图片 OCR
    • DeepSeekClient 官方模型查询
    • BailianTTSClient 语音合成
  • 55_ai_agent

    • 55_ai_agent
    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • 意图识别
    • 智能问答
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 使用 OpenAI ASR 实现语音识别接口(Java 后端示例)
    • 定向搜索
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 接口文档
    • 自定义 ChatAskService
    • 请求记录
    • 限流和错误处理
    • 增强检索(RAG)
    • 结构化数据检索
    • AI 问答
    • 连接代码执行器
    • 待定
    • 模型编程能力评测
    • 音频会话 SDP 示例
  • 56_knowlege_base

    • 56_knowlege_base
    • 学术论文
    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 豫自然资办发〔2021〕18号文档解析实测
    • 离线运行向量模型
    • 爬取网页数据
  • 57_voice-agent

    • 57_voice-agent
    • 整合Gemini realtime模型
    • Voice Agent 前端接入接口文档
    • 整合千问realtime模型
    • 打断支持
    • 主动介入
    • eleven labs
    • 基于 tio-boot + ElevenLabs 构建实时语音 Agent(支持打断与主动介入)
  • 58_ai-search

    • 58_ai-search
    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 59_ai-coding

    • 59_ai-coding
    • Cline 提示词
    • Cline 提示词-中文版本
  • 60_ai-browser

    • deepseek-browser-use:从入门到源码
    • deepseek-browser-use:概念与学习路线
    • 安装、启动与健康检查
    • 第一个任务:打开页面、读取结果与关闭
    • 客户端:dsb 命令行、Python 与 PowerShell
    • 统一命令接口与人机协作
    • 浏览器、profile 与登录态
    • 窗口尺寸与页面视口
    • 接入智能体:观察、执行与验证
    • 表单与多层弹窗排障:防止重复提交
    • 调用追踪、页面留档与文件上传
    • 站点配方、技能与异步作业
    • Windows OCR:本地图片与页面文字识别
    • 配置项与运维自省
    • 命令清单
    • 源码教程:从 HTTP 请求到命令执行
    • DOM 原理:前端如何生成可交互快照
    • DOM 原理:Java 模型与跨 Frame 索引
    • 正文提取与上层结构化处理
    • 源码教程:生命周期、导航与页签
    • 源码教程:DOM、页面状态与元素读取
    • 源码教程:点击、输入、键盘与鼠标
    • 源码教程:等待条件与 JavaScript 执行
    • 源码教程:文件上传、截图、PDF 与 OCR
    • 源码教程:Cookie、存储与页面设置
    • 源码教程:网络记录、请求拦截与控制台
    • 源码教程:原生对话框、DOM 弹窗与人机协作
    • 源码教程:批量、配方、后台作业与维护
  • 61_java-uni-ai-server

    • 61_java-uni-ai-server
    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 待定
  • 62_java-llm-proxy

    • 62_java-llm-proxy
    • 使用tio-boot搭建多模型LLM代理服务
  • 63_java-kit-server

    • 63_java-kit-server
    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • 执行 Python (Manim) 代码
    • 待定
    • 待定
    • 待定
    • 视频下载增加水印说明文档
  • 64_文件存储

    • 64_文件存储
    • 文件上传数据表
    • 本地存储
    • 存储到 亚马逊 S3
    • 存储到 Cloudflare R2
    • 存储到 腾讯 COS
    • 上传文件到阿里云 OSS
  • 65_tio-boot-admin

    • 65_tio-boot-admin
    • 入门指南:使用框架内置配置
    • 手动初始化数据库
    • 配置职责、生效条件与扩展边界
    • 整合数据库
    • 与前端集成
    • 文件上传
    • 网络请求
    • 单图片管理(只读模式)
    • 多图片管理
    • 布尔值管理
    • 字段联动
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
    • 整合 Enjoy 模版引擎
    • 历史可选方案:Token 存储与 Sa-Token
    • 业务 API 与 H5 / 小程序联调
    • 方法路由与业务鉴权
    • 整合 Redis
    • 整合 Elasticsearch
    • 后端开发规范:tio-boot、java-db 与 Kv
    • 多表实现文件数据存储
  • 66_第三方登录注册

    • 66_第三方登录注册
    • 邮箱登录和注册
    • 邮箱重置密码
    • 腾讯云短信登录注册
    • 腾讯云短信重置密码
    • 阿里云短信登录和注册
    • 阿里云短信重置密码
    • 微信登录与绑定手机号
    • 支付宝登录与绑定手机号
    • 微信小程序手机号快捷登录
    • Google登录
    • 阿里云邮件推送验证邮箱
  • 67_pay

    • 支付集成
    • 微信小程序支付:普通支付
    • 微信支付:Native 扫码支付(PC 网页扫码)
    • 支付宝:电脑网站支付接入指南
  • 68_firebase

    • 68_firebase
    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 登录注册
    • 注册回调
  • 69_clerk

    • 69_clerk
    • Clerk
  • 70_tio-im

    • 70_tio-im
    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 71_tio-mail-wing

    • 71_tio-mail-wing
    • tio-mail-wing简介
    • 任务1:实现POP3系统
    • 使用 getmail 验证 tio-mail-wing POP3 服务
    • 任务2:实现 SMTP 服务
    • 数据库初始化文档
    • 用户管理
    • 邮件管理
    • 任务3:实现 SMTP 服务 数据库版本
    • 任务4:实现 POP3 服务(数据库版本)
    • IMAP 协议
    • 拉取多封邮件
    • 任务5:实现 IMAP 服务(数据库版本)
    • IMAP实现讲解
    • IMAP 手动测试脚本
    • IMAP 认证机制
    • 主动推送
    • namesapce
    • CONDSTORE and QRESYNC
  • 72_tio-mcp-server

    • 72_tio-mcp-server
    • 实现 MCP Server 开发指南
    • MCP 协议
    • /zh/72_tio-mcp-server/11.html
  • 73_tio-sip

    • 73_tio-sip
    • SIP Server 第一版原理说明
    • SIP Server 第一版实战
    • 一、Windows 平台测试
    • SIP Server 第二版实战
    • SIP Server 第三版实战
    • 性能优化
    • 基于 MediaProcessor 对接 Realtime 模型说明
    • 对接大语言模型
    • 支持 G722 宽带语音
    • G722编码和解码
    • 会话级采样率转换
    • 增加 9196 回声测试分机
    • 语音系统链路说明
    • 一、Gemini Realtime 的打断机制
  • 74_tio-log-server

    • 74_tio-log-server
    • 简介
    • 收集 docker 日志
    • 入库
  • 75_性能测试

    • 75_性能测试
    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
    • HTTP/1.1 Pipelining 性能测试报告
    • tio-boot vs Quarkus 性能对比测试报告
  • 76_tio-boot

    • 76_tio-boot
    • 源码阅读入口
    • Swagger 整合到 Tio-Boot 中的指南
    • 启动与关闭生命周期
    • HTTP 请求分发与路由优先级
    • 高性能网络编程中的 ByteBuffer 分配与回收策略
    • TioBootServerHandler 源码解析
  • 77_案例

    • 77_案例
    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令
    • /zh/77_%E6%A1%88%E4%BE%8B/11.html
    • /zh/77_%E6%A1%88%E4%BE%8B/12.html
    • /zh/77_%E6%A1%88%E4%BE%8B/13.html

DOM 原理:前端如何生成可交互快照

本章解释当前源码如何把网页 DOM 转成模型可读的结构化文本。先掌握 页面状态命令,再看实现。

源码入口

文件位于 playwright-server/src/main/resources/dom/dom_tree/:

文件用途
index.jsDOM 遍历、可见性与交互性判断、节点序列化和高亮
buildDomTextExp.js同目录的 DOM 构建脚本;改动时需要核对两个入口的一致性
run.js执行构建函数,并附加滚动距离、视口高度和页面高度

DomService.buildExpression() 加载构建脚本并填入包装脚本;Java 通过 Frame 的 evaluate 执行。不要把原始 HTML 字符串直接当成当前页面状态。

从 DOM 到索引的步骤

  1. 遍历元素和文本节点,收集标签、属性与层级信息。
  2. 根据样式、边界和视口等条件判断可见性。
  3. 根据元素语义和交互特征判断是否分配可操作索引。
  4. 收集表单的实时状态,并序列化节点。
  5. 按配置绘制高亮,将节点映射和根节点 ID 返回 Java。

索引的起点由 highlightIndexStart 指定。构建多个 Frame 的快照时,后续 Frame 接续已有索引,从而避免每个 Frame 都从零编号。

参数为什么影响结果

参数含义
doHighlightElements是否绘制元素高亮
focusHighlightIndex聚焦指定高亮索引
viewportExpansion视口外的采集范围,页面状态命令将相应参数传入
highlightIndexStart本次 Frame 索引的起点
descendIframes是否由本次遍历递归进入 iframe
debugMode是否启用调试信息

这些是内部脚本参数,不能全部原样放到 HTTP 请求。对外参数由 CommandTable 读取,名称与默认值以 DOM 命令教程 为准。

表单值必须读实时属性

输入后,HTML 的原始 value attribute 不一定更新。当前脚本会读控件的实时值和状态,涵盖 value、checked、selected、selected-text、只读与禁用等信息。密码值替换为 [redacted]。

可见的只读或禁用控件也可能需要展示状态,即使没有交互索引。没有编号不等于页面没有这个控件;定位与可操作性需要分别判断。

iframe 与高亮

Java 在多 Frame 采集时逐个执行脚本,并将 descendIframes 设为 false,避免同源子 Frame 被重复采集。每个索引还必须关联 Frame,才能把点击路由到正确文档,详见 Java DOM 模型。

高亮层是辅助观察信息,不属于网页业务正文。正文提取与截图实现会按需要处理高亮层,避免索引标记混入内容。

修改源码时如何验证

选择包含普通文本、输入框、只读字段、复选框和 iframe 的本地测试页:

  • 改变字段值后重新取状态,确认输出是最新值。
  • 检查密码值被隐藏、只读字段可读且不会误判成可操作控件。
  • 检查主文档与 Frame 的索引没有重复。
  • 导航或更新 DOM 后重新采集,确认调用方没有复用旧索引。

下一步:Java 如何组装 DOMState。

代码阅读:DOM 构建算法

遍历的入口是 buildDomTree。它区分文本节点和元素节点,过滤不需要的元素,对元素记录可见性、交互性、定位和属性,再递归收集子节点。下面先看三个短函数,再展开完整脚本查看高亮、iframe 和实时表单状态之间的连接。

1. 可见性判断

isElementVisible 检查元素尺寸和样式。它只回答是否可见,点击前还需要判断顶层遮挡和可操作性。

源码:playwright-server/src/main/resources/dom/dom_tree/index.js。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

  function isElementVisible(element) {
    const style = getCachedComputedStyle(element);
    return (
      element.offsetWidth > 0 &&
      element.offsetHeight > 0 &&
      style?.visibility !== "hidden" &&
      style?.display !== "none"
    );
  }

2. 元素过滤

先过滤不需要进入快照的标签,减少后续计算与模型输入。

源码:playwright-server/src/main/resources/dom/dom_tree/index.js。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

  function isElementAccepted(element) {
    if (!element || !element.tagName) return false;

    // Always accept body and common container elements
    const alwaysAccept = new Set([
      "body", "div", "main", "article", "section", "nav", "header", "footer"
    ]);
    const tagName = element.tagName.toLowerCase();

    if (alwaysAccept.has(tagName)) return true;

    const leafElementDenyList = new Set([
      "svg",
      "script",
      "style",
      "link",
      "meta",
      "noscript",
      "template",
    ]);

    return !leafElementDenyList.has(tagName);
  }

3. 几何信息缓存

同一个元素会被可见性、视口和高亮逻辑反复读取,缓存避免重复计算布局。

源码:playwright-server/src/main/resources/dom/dom_tree/index.js。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

  function getCachedBoundingRect(element) {
    if (!element) return null;

    if (DOM_CACHE.boundingRects.has(element)) {
      return DOM_CACHE.boundingRects.get(element);
    }

    const rect = element.getBoundingClientRect();

    if (rect) {
      DOM_CACHE.boundingRects.set(element, rect);
    }
    return rect;
  }

4. 完整 DOM 构建脚本

重点顺序:buildDomTree → isInteractiveElement → handleHighlighting → 子节点递归 → 节点映射返回。检查实时属性时,查找 checked、selected-text、readonly 和 [redacted]。

展开 index.js 当前完整实现

源码:playwright-server/src/main/resources/dom/dom_tree/index.js。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

(
  args = {
    doHighlightElements: true,
    focusHighlightIndex: -1,
    viewportExpansion: 0,
    debugMode: false,
    // 多 frame 快照时,每个 frame 单独求值,但索引必须**全局唯一**:调用方按 frame 顺序依次求值,
    // 每次把「前面几个 frame 已经用掉了多少个索引」当起点传进来(见 DomService.getClickableElements)。
    // 单 frame 时保持 0,行为与以前完全一致。
    highlightIndexStart: 0,
    // 要不要顺着 iframe 递归下去(同源 iframe 才能进)。默认 true,与以前完全一致;
    // 多 frame 快照会把它设成 false,由调用方**逐个 frame** 求值 —— 否则同源 iframe 的元素会被数两遍
    // (父 frame 递归一遍、iframe 自己再来一遍),索引也会撞车。
    descendIframes: true,
  }
) => {
  const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode, highlightIndexStart,
    descendIframes } = args;
  let highlightIndex = highlightIndexStart > 0 ? highlightIndexStart : 0; // 从调用方给的起点开始编号

  // Add caching mechanisms at the top level
  const DOM_CACHE = {
    boundingRects: new WeakMap(),
    clientRects: new WeakMap(),
    computedStyles: new WeakMap(),
    clearCache: () => {
      DOM_CACHE.boundingRects = new WeakMap();
      DOM_CACHE.clientRects = new WeakMap();
      DOM_CACHE.computedStyles = new WeakMap();
    }
  };

  /**
   * Gets the cached bounding rect for an element.
   *
   * @param {HTMLElement} element - The element to get the bounding rect for.
   * @returns {DOMRect | null} The cached bounding rect, or null if the element is not found.
   */
  function getCachedBoundingRect(element) {
    if (!element) return null;

    if (DOM_CACHE.boundingRects.has(element)) {
      return DOM_CACHE.boundingRects.get(element);
    }

    const rect = element.getBoundingClientRect();

    if (rect) {
      DOM_CACHE.boundingRects.set(element, rect);
    }
    return rect;
  }

  /**
   * Gets the cached computed style for an element.
   *
   * @param {HTMLElement} element - The element to get the computed style for.
   * @returns {CSSStyleDeclaration | null} The cached computed style, or null if the element is not found.
   */
  function getCachedComputedStyle(element) {
    if (!element) return null;

    if (DOM_CACHE.computedStyles.has(element)) {
      return DOM_CACHE.computedStyles.get(element);
    }

    const style = window.getComputedStyle(element);

    if (style) {
      DOM_CACHE.computedStyles.set(element, style);
    }
    return style;
  }

  /**
   * Gets the cached client rects for an element.
   *
   * @param {HTMLElement} element - The element to get the client rects for.
   * @returns {DOMRectList | null} The cached client rects, or null if the element is not found.
   */
  function getCachedClientRects(element) {
    if (!element) return null;

    if (DOM_CACHE.clientRects.has(element)) {
      return DOM_CACHE.clientRects.get(element);
    }

    const rects = element.getClientRects();

    if (rects) {
      DOM_CACHE.clientRects.set(element, rects);
    }
    return rects;
  }

  /**
   * Hash map of DOM nodes indexed by their highlight index.
   *
   * @type {Object<string, any>}
   */
  const DOM_HASH_MAP = {};

  const ID = { current: 0 };

  const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";

  // Add a WeakMap cache for XPath strings
  const xpathCache = new WeakMap();

  // // Initialize once and reuse
  // const viewportObserver = new IntersectionObserver(
  //   (entries) => {
  //     entries.forEach(entry => {
  //       elementVisibilityMap.set(entry.target, entry.isIntersecting);
  //     });
  //   },
  //   { rootMargin: `${viewportExpansion}px` }
  // );

  /**
   * Highlights an element in the DOM and returns the index of the next element.
   *
   * @param {HTMLElement} element - The element to highlight.
   * @param {number} index - The index of the element.
   * @param {HTMLElement | null} parentIframe - The parent iframe node.
   * @returns {number} The index of the next element.
   */
  function highlightElement(element, index, parentIframe = null) {
    if (!element) return index;

    const overlays = [];
    /**
     * @type {HTMLElement | null}
     */
    let label = null;
    let labelWidth = 20;
    let labelHeight = 16;
    let cleanupFn = null;

    try {
      // Create or get highlight container
      let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
      if (!container) {
        container = document.createElement("div");
        container.id = HIGHLIGHT_CONTAINER_ID;
        container.style.position = "fixed";
        container.style.pointerEvents = "none";
        container.style.top = "0";
        container.style.left = "0";
        container.style.width = "100%";
        container.style.height = "100%";
        // Use the maximum valid value in zIndex to ensure the element is not blocked by overlapping elements.
        container.style.zIndex = "2147483647";
        container.style.backgroundColor = 'transparent';
        document.body.appendChild(container);
      }

      // Get element client rects
      const rects = element.getClientRects(); // Use getClientRects()

      if (!rects || rects.length === 0) return index; // Exit if no rects

      // Generate a color based on the index
      const colors = [
        "#FF0000",
        "#00FF00",
        "#0000FF",
        "#FFA500",
        "#800080",
        "#008080",
        "#FF69B4",
        "#4B0082",
        "#FF4500",
        "#2E8B57",
        "#DC143C",
        "#4682B4",
      ];
      const colorIndex = index % colors.length;
      const baseColor = colors[colorIndex];
      const backgroundColor = baseColor + "1A"; // 10% opacity version of the color

      // Get iframe offset if necessary
      let iframeOffset = { x: 0, y: 0 };
      if (parentIframe) {
        const iframeRect = parentIframe.getBoundingClientRect(); // Keep getBoundingClientRect for iframe offset
        iframeOffset.x = iframeRect.left;
        iframeOffset.y = iframeRect.top;
      }

      // Create fragment to hold overlay elements
      const fragment = document.createDocumentFragment();

      // Create highlight overlays for each client rect
      for (const rect of rects) {
        if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects

        const overlay = document.createElement("div");
        overlay.style.position = "fixed";
        overlay.style.border = `2px solid ${baseColor}`;
        overlay.style.backgroundColor = backgroundColor;
        overlay.style.pointerEvents = "none";
        overlay.style.boxSizing = "border-box";

        const top = rect.top + iframeOffset.y;
        const left = rect.left + iframeOffset.x;

        overlay.style.top = `${top}px`;
        overlay.style.left = `${left}px`;
        overlay.style.width = `${rect.width}px`;
        overlay.style.height = `${rect.height}px`;

        fragment.appendChild(overlay);
        overlays.push({ element: overlay, initialRect: rect }); // Store overlay and its rect
      }

      // Create and position a single label relative to the first rect
      const firstRect = rects[0];
      label = document.createElement("div");
      label.className = "playwright-highlight-label";
      label.style.position = "fixed";
      label.style.background = baseColor;
      label.style.color = "white";
      label.style.padding = "1px 4px";
      label.style.borderRadius = "4px";
      label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
      label.textContent = index.toString();

      labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth; // Update actual width if possible
      labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight; // Update actual height if possible

      const firstRectTop = firstRect.top + iframeOffset.y;
      const firstRectLeft = firstRect.left + iframeOffset.x;

      let labelTop = firstRectTop + 2;
      let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;

      // Adjust label position if first rect is too small
      if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
        labelTop = firstRectTop - labelHeight - 2;
        labelLeft = firstRectLeft + firstRect.width - labelWidth; // Align with right edge
        if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft; // Prevent going off-left
      }

      // Ensure label stays within viewport bounds slightly better
      labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
      labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));


      label.style.top = `${labelTop}px`;
      label.style.left = `${labelLeft}px`;

      fragment.appendChild(label);

      // Update positions on scroll/resize
      const updatePositions = () => {
        const newRects = element.getClientRects(); // Get fresh rects
        let newIframeOffset = { x: 0, y: 0 };

        if (parentIframe) {
          const iframeRect = parentIframe.getBoundingClientRect(); // Keep getBoundingClientRect for iframe
          newIframeOffset.x = iframeRect.left;
          newIframeOffset.y = iframeRect.top;
        }

        // Update each overlay
        overlays.forEach((overlayData, i) => {
          if (i < newRects.length) { // Check if rect still exists
            const newRect = newRects[i];
            const newTop = newRect.top + newIframeOffset.y;
            const newLeft = newRect.left + newIframeOffset.x;

            overlayData.element.style.top = `${newTop}px`;
            overlayData.element.style.left = `${newLeft}px`;
            overlayData.element.style.width = `${newRect.width}px`;
            overlayData.element.style.height = `${newRect.height}px`;
            overlayData.element.style.display = (newRect.width === 0 || newRect.height === 0) ? 'none' : 'block';
          } else {
            // If fewer rects now, hide extra overlays
            overlayData.element.style.display = 'none';
          }
        });

        // If there are fewer new rects than overlays, hide the extras
        if (newRects.length < overlays.length) {
          for (let i = newRects.length; i < overlays.length; i++) {
            overlays[i].element.style.display = 'none';
          }
        }

        // Update label position based on the first new rect
        if (label && newRects.length > 0) {
          const firstNewRect = newRects[0];
          const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
          const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;

          let newLabelTop = firstNewRectTop + 2;
          let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;

          if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
            newLabelTop = firstNewRectTop - labelHeight - 2;
            newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
            if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
          }

          // Ensure label stays within viewport bounds
          newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
          newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));

          label.style.top = `${newLabelTop}px`;
          label.style.left = `${newLabelLeft}px`;
          label.style.display = 'block';
        } else if (label) {
          // Hide label if element has no rects anymore
          label.style.display = 'none';
        }
      };


      const throttleFunction = (func, delay) => {
        let lastCall = 0;
        return (...args) => {
          const now = performance.now();
          if (now - lastCall < delay) return;
          lastCall = now;
          return func(...args);
        };
      };

      const throttledUpdatePositions = throttleFunction(updatePositions, 16); // ~60fps
      window.addEventListener('scroll', throttledUpdatePositions, true);
      window.addEventListener('resize', throttledUpdatePositions);

      // Add cleanup function
      cleanupFn = () => {
        window.removeEventListener('scroll', throttledUpdatePositions, true);
        window.removeEventListener('resize', throttledUpdatePositions);
        // Remove overlay elements if needed
        overlays.forEach(overlay => overlay.element.remove());
        if (label) label.remove();
      };

      // Then add fragment to container in one operation
      container.appendChild(fragment);

      return index + 1;
    } finally {
      // Store cleanup function for later use
      if (cleanupFn) {
        // Keep a reference to cleanup functions in a global array
        (window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(cleanupFn);
      }
    }
  }

  // // Add this function to perform cleanup when needed
  // function cleanupHighlights() {
  //   if (window._highlightCleanupFunctions && window._highlightCleanupFunctions.length) {
  //     window._highlightCleanupFunctions.forEach(fn => fn());
  //     window._highlightCleanupFunctions = [];
  //   }

  //   // Also remove the container
  //   const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
  //   if (container) container.remove();
  // }

  /**
   * Gets the position of an element in its parent.
   *
   * @param {HTMLElement} currentElement - The element to get the position for.
   * @returns {number} The position of the element in its parent.
   */
  function getElementPosition(currentElement) {
    if (!currentElement.parentElement) {
      return 0; // No parent means no siblings
    }

    const tagName = currentElement.nodeName.toLowerCase();

    const siblings = Array.from(currentElement.parentElement.children)
      .filter((sib) => sib.nodeName.toLowerCase() === tagName);

    if (siblings.length === 1) {
      return 0; // Only element of its type
    }

    const index = siblings.indexOf(currentElement) + 1; // 1-based index
    return index;
  }


  function getXPathTree(element, stopAtBoundary = true) {
    if (xpathCache.has(element)) return xpathCache.get(element);

    const segments = [];
    let currentElement = element;

    while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
      // Stop if we hit a shadow root or iframe
      if (
        stopAtBoundary &&
        (currentElement.parentNode instanceof ShadowRoot ||
          currentElement.parentNode instanceof HTMLIFrameElement)
      ) {
        break;
      }

      const position = getElementPosition(currentElement);
      const tagName = currentElement.nodeName.toLowerCase();
      const xpathIndex = position > 0 ? `[${position}]` : "";
      segments.unshift(`${tagName}${xpathIndex}`);

      currentElement = currentElement.parentNode;
    }

    const result = segments.join("/");
    xpathCache.set(element, result);
    return result;
  }

  /**
   * Checks if a text node is visible.
   *
   * @param {Text} textNode - The text node to check.
   * @returns {boolean} Whether the text node is visible.
   */
  function isTextNodeVisible(textNode) {
    try {
      // Special case: when viewportExpansion is -1, consider all text nodes as visible
      if (viewportExpansion === -1) {
        // Still check parent visibility for basic filtering
        const parentElement = textNode.parentElement;
        if (!parentElement) return false;

        try {
          return parentElement.checkVisibility({
            checkOpacity: true,
            checkVisibilityCSS: true,
          });
        } catch (e) {
          // Fallback if checkVisibility is not supported
          const style = window.getComputedStyle(parentElement);
          return style.display !== 'none' &&
            style.visibility !== 'hidden' &&
            style.opacity !== '0';
        }
      }

      const range = document.createRange();
      range.selectNodeContents(textNode);
      const rects = range.getClientRects(); // Use getClientRects for Range

      if (!rects || rects.length === 0) {
        return false;
      }

      let isAnyRectVisible = false;
      let isAnyRectInViewport = false;

      for (const rect of rects) {
        // Check size
        if (rect.width > 0 && rect.height > 0) {
          isAnyRectVisible = true;

          // Viewport check for this rect
          if (!(
            rect.bottom < -viewportExpansion ||
            rect.top > window.innerHeight + viewportExpansion ||
            rect.right < -viewportExpansion ||
            rect.left > window.innerWidth + viewportExpansion
          )) {
            isAnyRectInViewport = true;
            break; // Found a visible rect in viewport, no need to check others
          }
        }
      }

      if (!isAnyRectVisible || !isAnyRectInViewport) {
        return false;
      }

      // Check parent visibility
      const parentElement = textNode.parentElement;
      if (!parentElement) return false;

      try {
        return parentElement.checkVisibility({
          checkOpacity: true,
          checkVisibilityCSS: true,
        });
      } catch (e) {
        // Fallback if checkVisibility is not supported
        const style = window.getComputedStyle(parentElement);
        return style.display !== 'none' &&
          style.visibility !== 'hidden' &&
          style.opacity !== '0';
      }
    } catch (e) {
      console.warn('Error checking text node visibility:', e);
      return false;
    }
  }

  /**
   * Checks if an element is accepted.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is accepted.
   */
  function isElementAccepted(element) {
    if (!element || !element.tagName) return false;

    // Always accept body and common container elements
    const alwaysAccept = new Set([
      "body", "div", "main", "article", "section", "nav", "header", "footer"
    ]);
    const tagName = element.tagName.toLowerCase();

    if (alwaysAccept.has(tagName)) return true;

    const leafElementDenyList = new Set([
      "svg",
      "script",
      "style",
      "link",
      "meta",
      "noscript",
      "template",
    ]);

    return !leafElementDenyList.has(tagName);
  }

  /**
   * Checks if an element is visible.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is visible.
   */
  function isElementVisible(element) {
    const style = getCachedComputedStyle(element);
    return (
      element.offsetWidth > 0 &&
      element.offsetHeight > 0 &&
      style?.visibility !== "hidden" &&
      style?.display !== "none"
    );
  }

  /**
   * Checks if an element is interactive.
   * 
   * lots of comments, and uncommented code - to show the logic of what we already tried
   * 
   * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)
   * 
   * @param {HTMLElement} element - The element to check.
   */
  function isInteractiveElement(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
      return false;
    }

    // Cache the tagName and style lookups
    const tagName = element.tagName.toLowerCase();
    const style = getCachedComputedStyle(element);

    // Define interactive cursors
    const interactiveCursors = new Set([
      'pointer',    // Link/clickable elements
      'move',       // Movable elements
      'text',       // Text selection
      'grab',       // Grabbable elements
      'grabbing',   // Currently grabbing
      'cell',       // Table cell selection
      'copy',       // Copy operation
      'alias',      // Alias creation
      'all-scroll', // Scrollable content
      'col-resize', // Column resize
      'context-menu', // Context menu available
      'crosshair',  // Precise selection
      'e-resize',   // East resize
      'ew-resize',  // East-west resize
      'help',       // Help available
      'n-resize',   // North resize
      'ne-resize',  // Northeast resize
      'nesw-resize', // Northeast-southwest resize
      'ns-resize',  // North-south resize
      'nw-resize',  // Northwest resize
      'nwse-resize', // Northwest-southeast resize
      'row-resize', // Row resize
      's-resize',   // South resize
      'se-resize',  // Southeast resize
      'sw-resize',  // Southwest resize
      'vertical-text', // Vertical text selection
      'w-resize',   // West resize
      'zoom-in',    // Zoom in
      'zoom-out'    // Zoom out
    ]);

    // Define non-interactive cursors
    const nonInteractiveCursors = new Set([
      'not-allowed', // Action not allowed
      'no-drop',     // Drop not allowed
      'wait',        // Processing
      'progress',    // In progress
      'initial',     // Initial value
      'inherit'      // Inherited value
      //? Let's just include all potentially clickable elements that are not specifically blocked
      // 'none',        // No cursor
      // 'default',     // Default cursor 
      // 'auto',        // Browser default
    ]);

    /**
     * Checks if an element has an interactive pointer.
     *
     * @param {HTMLElement} element - The element to check.
     * @returns {boolean} Whether the element has an interactive pointer.
     */
    function doesElementHaveInteractivePointer(element) {
      if (element.tagName.toLowerCase() === "html") return false;

      if (style?.cursor && interactiveCursors.has(style.cursor)) return true;

      return false;
    }

    let isInteractiveCursor = doesElementHaveInteractivePointer(element);

    // Genius fix for almost all interactive elements
    if (isInteractiveCursor) {
      return true;
    }

    const interactiveElements = new Set([
      "a",          // Links
      "button",     // Buttons
      "input",      // All input types (text, checkbox, radio, etc.)
      "select",     // Dropdown menus
      "textarea",   // Text areas
      "details",    // Expandable details
      "summary",    // Summary element (clickable part of details)
      "label",      // Form labels (often clickable)
      "option",     // Select options
      "optgroup",   // Option groups
      "fieldset",   // Form fieldsets (can be interactive with legend)
      "legend",     // Fieldset legends
    ]);

    // Define explicit disable attributes and properties
    const explicitDisableTags = new Set([
      'disabled',           // Standard disabled attribute
      // 'aria-disabled',      // ARIA disabled state
      'readonly',          // Read-only state
      // 'aria-readonly',     // ARIA read-only state
      // 'aria-hidden',       // Hidden from accessibility
      // 'hidden',            // Hidden attribute
      // 'inert',             // Inert attribute
      // 'aria-inert',        // ARIA inert state
      // 'tabindex="-1"',     // Removed from tab order
      // 'aria-hidden="true"' // Hidden from screen readers
    ]);

    // handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed
    if (interactiveElements.has(tagName)) {
      // Check for non-interactive cursor
      if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {
        return false;
      }

      // Check for explicit disable attributes
      for (const disableTag of explicitDisableTags) {
        if (element.hasAttribute(disableTag) ||
          element.getAttribute(disableTag) === 'true' ||
          element.getAttribute(disableTag) === '') {
          return false;
        }
      }

      // Check for disabled property on form elements
      if (element.disabled) {
        return false;
      }

      // Check for readonly property on form elements
      if (element.readOnly) {
        return false;
      }

      // Check for inert property
      if (element.inert) {
        return false;
      }

      return true;
    }

    const role = element.getAttribute("role");
    const ariaRole = element.getAttribute("aria-role");

    // Check for contenteditable attribute
    if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
      return true;
    }

    // Added enhancement to capture dropdown interactive elements
    if (element.classList && (
      element.classList.contains("button") ||
      element.classList.contains('dropdown-toggle') ||
      element.getAttribute('data-index') ||
      element.getAttribute('data-toggle') === 'dropdown' ||
      element.getAttribute('aria-haspopup') === 'true'
    )) {
      return true;
    }


    const interactiveRoles = new Set([
      'button',           // Directly clickable element
      // 'link',            // Clickable link
      // 'menuitem',        // Clickable menu item
      'menuitemradio',   // Radio-style menu item (selectable)
      'menuitemcheckbox', // Checkbox-style menu item (toggleable)
      'radio',           // Radio button (selectable)
      'checkbox',        // Checkbox (toggleable)
      'tab',             // Tab (clickable to switch content)
      'switch',          // Toggle switch (clickable to change state)
      'slider',          // Slider control (draggable)
      'spinbutton',      // Number input with up/down controls
      'combobox',        // Dropdown with text input
      'searchbox',       // Search input field
      'textbox',         // Text input field
      // 'listbox',         // Selectable list
      'option',          // Selectable option in a list
      'scrollbar'        // Scrollable control
    ]);


    // Basic role/attribute checks
    const hasInteractiveRole =
      interactiveElements.has(tagName) ||
      (role && interactiveRoles.has(role)) ||
      (ariaRole && interactiveRoles.has(ariaRole));

    if (hasInteractiveRole) return true;


    // check whether element has event listeners by window.getEventListeners
    try {
      if (typeof getEventListeners === 'function') {
        const listeners = getEventListeners(element);
        const mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];
        for (const eventType of mouseEvents) {
          if (listeners[eventType] && listeners[eventType].length > 0) {
            return true; // Found a mouse interaction listener
          }
        }
      }

      const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
      if (typeof getEventListenersForNode === 'function') {
        const listeners = getEventListenersForNode(element);
        const interactionEvents = ['click', 'mousedown', 'mouseup', 'keydown', 'keyup', 'submit', 'change', 'input', 'focus', 'blur'];
        for (const eventType of interactionEvents) {
          for (const listener of listeners) {
            if (listener.type === eventType) {
              return true; // Found a common interaction listener
            }
          }
        }
      }
      // Fallback: Check common event attributes if getEventListeners is not available (getEventListeners doesn't work in page.evaluate context)
      const commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick'];
      for (const attr of commonMouseAttrs) {
        if (element.hasAttribute(attr) || typeof element[attr] === 'function') {
          return true;
        }
      }
    } catch (e) {
      // console.warn(`Could not check event listeners for ${element.tagName}:`, e);
      // If checking listeners fails, rely on other checks
    }

    return false
  }


  /**
   * Checks if an element is the topmost element at its position.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is the topmost element at its position.
   */
  function isTopElement(element) {
    // Special case: when viewportExpansion is -1, consider all elements as "top" elements
    if (viewportExpansion === -1) {
      return true;
    }

    const rects = getCachedClientRects(element); // Replace element.getClientRects()

    if (!rects || rects.length === 0) {
      return false; // No geometry, cannot be top
    }

    let isAnyRectInViewport = false;
    for (const rect of rects) {
      // Use the same logic as isInExpandedViewport check
      if (rect.width > 0 && rect.height > 0 && !( // Only check non-empty rects
        rect.bottom < -viewportExpansion ||
        rect.top > window.innerHeight + viewportExpansion ||
        rect.right < -viewportExpansion ||
        rect.left > window.innerWidth + viewportExpansion
      )) {
        isAnyRectInViewport = true;
        break;
      }
    }

    if (!isAnyRectInViewport) {
      return false; // All rects are outside the viewport area
    }


    // Find the correct document context and root element
    let doc = element.ownerDocument;

    // If we're in an iframe, elements are considered top by default
    if (doc !== window.document) {
      return true;
    }

    // For shadow DOM, we need to check within its own root context
    const shadowRoot = element.getRootNode();
    if (shadowRoot instanceof ShadowRoot) {
      const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;
      const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;

      try {
        const topEl = shadowRoot.elementFromPoint(centerX, centerY);
        if (!topEl) return false;

        let current = topEl;
        while (current && current !== shadowRoot) {
          if (current === element) return true;
          current = current.parentElement;
        }
        return false;
      } catch (e) {
        return true;
      }
    }

    // For elements in viewport, check if they're topmost
    const centerX = rects[Math.floor(rects.length / 2)].left + rects[Math.floor(rects.length / 2)].width / 2;
    const centerY = rects[Math.floor(rects.length / 2)].top + rects[Math.floor(rects.length / 2)].height / 2;

    try {
      const topEl = document.elementFromPoint(centerX, centerY);
      if (!topEl) return false;

      let current = topEl;
      while (current && current !== document.documentElement) {
        if (current === element) return true;
        current = current.parentElement;
      }
      return false;
    } catch (e) {
      return true;
    }
  }

  /**
   * Checks if an element is within the expanded viewport.
   *
   * @param {HTMLElement} element - The element to check.
   * @param {number} viewportExpansion - The viewport expansion.
   * @returns {boolean} Whether the element is within the expanded viewport.
   */
  function isInExpandedViewport(element, viewportExpansion) {
    if (viewportExpansion === -1) {
      return true;
    }

    const rects = element.getClientRects(); // Use getClientRects

    if (!rects || rects.length === 0) {
      // Fallback to getBoundingClientRect if getClientRects is empty,
      // useful for elements like <svg> that might not have client rects but have a bounding box.
      const boundingRect = getCachedBoundingRect(element);
      if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
        return false;
      }
      return !(
        boundingRect.bottom < -viewportExpansion ||
        boundingRect.top > window.innerHeight + viewportExpansion ||
        boundingRect.right < -viewportExpansion ||
        boundingRect.left > window.innerWidth + viewportExpansion
      );
    }

    // Check if *any* client rect is within the viewport
    for (const rect of rects) {
      if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects

      if (!(
        rect.bottom < -viewportExpansion ||
        rect.top > window.innerHeight + viewportExpansion ||
        rect.right < -viewportExpansion ||
        rect.left > window.innerWidth + viewportExpansion
      )) {
        return true; // Found at least one rect in the viewport
      }
    }

    return false; // No rects were found in the viewport
  }

  // /**
  //  * Gets the effective scroll of an element.
  //  *
  //  * @param {HTMLElement} element - The element to get the effective scroll for.
  //  * @returns {Object} The effective scroll of the element.
  //  */
  // function getEffectiveScroll(element) {
  //   let currentEl = element;
  //   let scrollX = 0;
  //   let scrollY = 0;

  //   while (currentEl && currentEl !== document.documentElement) {
  //     if (currentEl.scrollLeft || currentEl.scrollTop) {
  //       scrollX += currentEl.scrollLeft;
  //       scrollY += currentEl.scrollTop;
  //     }
  //     currentEl = currentEl.parentElement;
  //   }

  //   scrollX += window.scrollX;
  //   scrollY += window.scrollY;

  //   return { scrollX, scrollY };
  // }

  /**
   * Checks if an element is an interactive candidate.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is an interactive candidate.
   */
  function isInteractiveCandidate(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;

    const tagName = element.tagName.toLowerCase();

    // Fast-path for common interactive elements
    const interactiveElements = new Set([
      "a", "button", "input", "select", "textarea", "details", "summary", "label"
    ]);

    if (interactiveElements.has(tagName)) return true;

    // Quick attribute checks without getting full lists
    const hasQuickInteractiveAttr = element.hasAttribute("onclick") ||
      element.hasAttribute("role") ||
      element.hasAttribute("tabindex") ||
      element.hasAttribute("aria-") ||
      element.hasAttribute("data-action") ||
      element.getAttribute("contenteditable") === "true";

    return hasQuickInteractiveAttr;
  }

  // --- Define constants for distinct interaction check ---
  const DISTINCT_INTERACTIVE_TAGS = new Set([
    'a', 'button', 'input', 'select', 'textarea', 'summary', 'details', 'label', 'option'
  ]);
  const INTERACTIVE_ROLES = new Set([
    'button', 'link', 'menuitem', 'menuitemradio', 'menuitemcheckbox',
    'radio', 'checkbox', 'tab', 'switch', 'slider', 'spinbutton',
    'combobox', 'searchbox', 'textbox', 'listbox', 'option', 'scrollbar'
  ]);


  /**
   * Heuristically determines if an element should be considered as independently interactive,
   * even if it's nested inside another interactive container.
   *
   * This function helps detect deeply nested actionable elements (e.g., menu items within a button)
   * that may not be picked up by strict interactivity checks.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is heuristically interactive.
   */
  function isHeuristicallyInteractive(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;

    // Skip non-visible elements early for performance
    if (!isElementVisible(element)) return false;

    // Check for common attributes that often indicate interactivity
    const hasInteractiveAttributes =
      element.hasAttribute('role') ||
      element.hasAttribute('tabindex') ||
      element.hasAttribute('onclick') ||
      typeof element.onclick === 'function';

    // Check for semantic class names suggesting interactivity
    const hasInteractiveClass = /\b(btn|clickable|menu|item|entry|link)\b/i.test(element.className || '');

    // Determine whether the element is inside a known interactive container
    const isInKnownContainer = Boolean(
      element.closest('button,a,[role="button"],.menu,.dropdown,.list,.toolbar')
    );

    // Ensure the element has at least one visible child (to avoid marking empty wrappers)
    const hasVisibleChildren = [...element.children].some(isElementVisible);

    // Avoid highlighting elements whose parent is <body> (top-level wrappers)
    const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);

    return (
      (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) &&
      hasVisibleChildren &&
      isInKnownContainer &&
      !isParentBody
    );
  }


  /**
   * Checks if an element likely represents a distinct interaction
   * separate from its parent (if the parent is also interactive).
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is a distinct interaction.
   */
  function isElementDistinctInteraction(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
      return false;
    }

    const tagName = element.tagName.toLowerCase();
    const role = element.getAttribute('role');

    // Check if it's an iframe - always distinct boundary
    if (tagName === 'iframe') {
      return true;
    }

    // Check tag name
    if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
      return true;
    }
    // Check interactive roles
    if (role && INTERACTIVE_ROLES.has(role)) {
      return true;
    }
    // Check contenteditable
    if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {
      return true;
    }
    // Check for common testing/automation attributes
    if (element.hasAttribute('data-testid') || element.hasAttribute('data-cy') || element.hasAttribute('data-test')) {
      return true;
    }
    // Check for explicit onclick handler (attribute or property)
    if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
      return true;
    }

    // return false

    // Check for other common interaction event listeners
    try {
      const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
      if (typeof getEventListenersForNode === 'function') {
        const listeners = getEventListenersForNode(element);
        const interactionEvents = ['click', 'mousedown', 'mouseup', 'keydown', 'keyup', 'submit', 'change', 'input', 'focus', 'blur'];
        for (const eventType of interactionEvents) {
          for (const listener of listeners) {
            if (listener.type === eventType) {
              return true; // Found a common interaction listener
            }
          }
        }
      }
      // Fallback: Check common event attributes if getEventListeners is not available (getEventListenersForNode doesn't work in page.evaluate context)
      const commonEventAttrs = ['onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onsubmit', 'onchange', 'oninput', 'onfocus', 'onblur'];
      if (commonEventAttrs.some(attr => element.hasAttribute(attr))) {
        return true;
      }
    } catch (e) {
      // console.warn(`Could not check event listeners for ${element.tagName}:`, e);
      // If checking listeners fails, rely on other checks
    }



    // if the element is not strictly interactive but appears clickable based on heuristic signals
    if (isHeuristicallyInteractive(element)) {
      return true;
    }

    // Default to false: if it's interactive but doesn't match above,
    // assume it triggers the same action as the parent.
    return false;
  }
  // --- End distinct interaction check ---

  /**
   * Handles the logic for deciding whether to highlight an element and performing the highlight.
   * @param {
    {
        tagName: string;
        attributes: Record<string, string>;
        xpath: any;
        children: never[];
        isVisible?: boolean;
        isTopElement?: boolean;
        isInteractive?: boolean;
        isInViewport?: boolean;
        highlightIndex?: number;
        shadowRoot?: boolean;
   }} nodeData - The node data object.
   * @param {HTMLElement} node - The node to highlight.
   * @param {HTMLElement | null} parentIframe - The parent iframe node.
   * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.
   * @returns {boolean} Whether the element was highlighted.
   */
  function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
    if (!nodeData.isInteractive) return false; // Not interactive, definitely don't highlight

    let shouldHighlight = false;
    if (!isParentHighlighted) {
      // Parent wasn't highlighted, this interactive node can be highlighted.
      shouldHighlight = true;
    } else {
      // Parent *was* highlighted. Only highlight this node if it represents a distinct interaction.
      if (isElementDistinctInteraction(node)) {
        shouldHighlight = true;
      } else {
        // console.log(`Skipping highlight for ${nodeData.tagName} (parent highlighted)`);
        shouldHighlight = false;
      }
    }

    if (shouldHighlight) {
      // Check viewport status before assigning index and highlighting
      nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);

      // When viewportExpansion is -1, all interactive elements should get a highlight index
      // regardless of viewport status
      if (nodeData.isInViewport || viewportExpansion === -1) {
        nodeData.highlightIndex = highlightIndex++;

        if (doHighlightElements) {
          if (focusHighlightIndex >= 0) {
            if (focusHighlightIndex === nodeData.highlightIndex) {
              highlightElement(node, nodeData.highlightIndex, parentIframe);
            }
          } else {
            highlightElement(node, nodeData.highlightIndex, parentIframe);
          }
          return true; // Successfully highlighted
        }
      } else {
        // console.log(`Skipping highlight for ${nodeData.tagName} (outside viewport)`);
      }
    }

    return false; // Did not highlight
  }

  /**
   * Creates a node data object for a given node and its descendants.
   *
   * @param {HTMLElement} node - The node to process.
   * @param {HTMLElement | null} parentIframe - The parent iframe node.
   * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.
   * @returns {string | null} The ID of the node data object, or null if the node is not processed.
   */
  function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
    // Fast rejection checks first
    if (!node || node.id === HIGHLIGHT_CONTAINER_ID ||
      (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE)) {
      return null;
    }

    if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
      return null;
    }

    // Special handling for root node (body)
    if (node === document.body) {
      const nodeData = {
        tagName: 'body',
        attributes: {},
        xpath: '/body',
        children: [],
      };

      // Process children of body
      for (const child of node.childNodes) {
        const domElement = buildDomTree(child, parentIframe, false); // Body's children have no highlighted parent initially
        if (domElement) nodeData.children.push(domElement);
      }

      const id = `${ID.current++}`;
      DOM_HASH_MAP[id] = nodeData;
      return id;
    }

    // Early bailout for non-element nodes except text
    if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
      return null;
    }

    // Process text nodes
    if (node.nodeType === Node.TEXT_NODE) {
      const textContent = node.textContent?.trim();
      if (!textContent) {
        return null;
      }

      // Only check visibility for text nodes that might be visible
      const parentElement = node.parentElement;
      if (!parentElement || parentElement.tagName.toLowerCase() === 'script') {
        return null;
      }

      const id = `${ID.current++}`;
      DOM_HASH_MAP[id] = {
        type: "TEXT_NODE",
        text: textContent,
        isVisible: isTextNodeVisible(node),
      };
      return id;
    }

    // Quick checks for element nodes
    if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
      return null;
    }

    // Early viewport check - only filter out elements clearly outside viewport
    // The getBoundingClientRect() of the Shadow DOM host element may return width/height = 0
    if (viewportExpansion !== -1 && !node.shadowRoot) {
      const rect = getCachedBoundingRect(node); // Keep for initial quick check
      const style = getCachedComputedStyle(node);

      // Skip viewport check for fixed/sticky elements as they may appear anywhere
      const isFixedOrSticky = style && (style.position === 'fixed' || style.position === 'sticky');

      // Check if element has actual dimensions using offsetWidth/Height (quick check)
      const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;

      // Use getBoundingClientRect for the quick OUTSIDE check.
      // isInExpandedViewport will do the more accurate check later if needed.
      if (!rect || (!isFixedOrSticky && !hasSize && (
        rect.bottom < -viewportExpansion ||
        rect.top > window.innerHeight + viewportExpansion ||
        rect.right < -viewportExpansion ||
        rect.left > window.innerWidth + viewportExpansion
      ))) {
        // console.log("Skipping node outside viewport (quick check):", node.tagName, rect);
        return null;
      }
    }

    /**
     * @type {
      {
          tagName: string;
          attributes: Record<string, string | null>;
          xpath: any;
          children: never[];
          isVisible?: boolean;
          isTopElement?: boolean;
          isInteractive?: boolean;
          isInViewport?: boolean;
          highlightIndex?: number;
          shadowRoot?: boolean;
      }
    } nodeData - The node data object.
     */
    const nodeData = {
      tagName: node.tagName.toLowerCase(),
      attributes: {},
      xpath: getXPathTree(node, true),
      children: [],
    };

    // Get attributes for interactive elements or potential text containers
    if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === 'iframe' || node.tagName.toLowerCase() === 'body') {
      const attributeNames = node.getAttributeNames?.() || [];
      for (const name of attributeNames) {
        const value = node.getAttribute(name);
        nodeData.attributes[name] = value;
      }
    }

    // HTML attributes are defaults; properties contain the current form state.
    if (['input', 'textarea', 'select', 'option'].includes(nodeData.tagName)) {
      const attrs = nodeData.attributes;
      attrs.value = node.type === 'password' ? '[redacted]' : String(node.value ?? '');
      attrs.disabled = String(node.matches(':disabled') || node.getAttribute('aria-disabled') === 'true');
      attrs.readonly = String(!!node.readOnly || node.getAttribute('aria-readonly') === 'true');
      attrs.editable = String(attrs.readonly !== 'true' && attrs.disabled !== 'true' &&
        (nodeData.tagName === 'textarea' || (nodeData.tagName === 'input' &&
        !['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'hidden'].includes(node.type))));
      if (['checkbox', 'radio'].includes(node.type)) attrs.checked = String(node.checked);
      if (nodeData.tagName === 'option') attrs.selected = String(node.selected);
      if (nodeData.tagName === 'select') {
        attrs['selected-text'] = Array.from(node.selectedOptions).map(o => o.text).join(', ');
        if (node.multiple) attrs.value = JSON.stringify(Array.from(node.selectedOptions).map(o => o.value));
      }
    } else if (node.isContentEditable) {
      nodeData.attributes.editable = 'true';
    }

    let nodeWasHighlighted = false;
    // Perform visibility, interactivity, and highlighting checks
    if (node.nodeType === Node.ELEMENT_NODE) {
      nodeData.isVisible = isElementVisible(node); // isElementVisible uses offsetWidth/Height, which is fine
      if (nodeData.isVisible) {
        nodeData.isTopElement = isTopElement(node);
        if (nodeData.isTopElement) {
          nodeData.isInteractive = isInteractiveElement(node);
          // Call the dedicated highlighting function
          nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
        }
      }
    }

    // Process children, with special handling for iframes and rich text editors
    if (node.tagName) {
      const tagName = node.tagName.toLowerCase();

      // Handle iframes
      if (tagName === "iframe") {
        // descendIframes=false 时只记下 iframe 本身,不递归进去(同源 iframe 的元素由调用方
        // 按 frame 逐个求值,避免同一批元素被数两遍、索引撞车)
        if (descendIframes === false) {
          // 不递归
        } else {
          try {
            const iframeDoc = node.contentDocument || node.contentWindow?.document;
            if (iframeDoc) {
              for (const child of iframeDoc.childNodes) {
                const domElement = buildDomTree(child, node, false);
                if (domElement) nodeData.children.push(domElement);
              }
            }
          } catch (e) {
            console.warn("Unable to access iframe:", e);
          }
        }
      }
      // Handle rich text editors and contenteditable elements
      else if (
        node.isContentEditable ||
        node.getAttribute("contenteditable") === "true" ||
        node.id === "tinymce" ||
        node.classList.contains("mce-content-body") ||
        (tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_"))
      ) {
        // Process all child nodes to capture formatted text
        for (const child of node.childNodes) {
          const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
          if (domElement) nodeData.children.push(domElement);
        }
      }
      else {
        // Handle shadow DOM
        if (node.shadowRoot) {
          nodeData.shadowRoot = true;
          for (const child of node.shadowRoot.childNodes) {
            const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
            if (domElement) nodeData.children.push(domElement);
          }
        }
        // Handle regular elements
        for (const child of node.childNodes) {
          // Pass the highlighted status of the *current* node to its children
          const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;
          const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);
          if (domElement) nodeData.children.push(domElement);
        }
      }
    }

    // Skip empty anchor tags only if they have no dimensions and no children
    if (nodeData.tagName === 'a' && nodeData.children.length === 0 && !nodeData.attributes.href) {
      // Check if the anchor has actual dimensions
      const rect = getCachedBoundingRect(node);
      const hasSize = (rect && rect.width > 0 && rect.height > 0) || (node.offsetWidth > 0 || node.offsetHeight > 0);

      if (!hasSize) {
        return null;
      }
    }

    const id = `${ID.current++}`;
    DOM_HASH_MAP[id] = nodeData;
    return id;
  }

  const rootId = buildDomTree(document.body);

  // Clear the cache before starting
  DOM_CACHE.clearCache();

  return { rootId, map: DOM_HASH_MAP };
};

5. Java 调用前的包装

%s 由 Java 的 String.format 替换成构建函数源码,随后采集滚动位置和页面尺寸。

源码:playwright-server/src/main/resources/dom/dom_tree/run.js。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

(args)=>{
  const buildDomTextExp = %s
  let result = buildDomTextExp(args);
  
  // 下面这段 JS 抓当前滚动 & 页面尺寸
  const scrollY        = window.scrollY;
  const viewportH      = window.innerHeight;
  const pageH          = document.documentElement.scrollHeight;
  const pixels_above   = scrollY;
  const pixels_below   = pageH - scrollY - viewportH;

  // 把滚动信息塞到返回值里
  result.pixels_above = pixels_above;
  result.pixels_below = pixels_below;
  result.viewport_height = viewportH;
  result.page_height     = pageH;  
  
  return result; 
}
Edit this page
Last Updated: 9/26/26, 2:46 PM
Contributors: Tong Li
Prev
源码教程:从 HTTP 请求到命令执行
Next
DOM 原理:Java 模型与跨 Frame 索引