源码教程:DOM、页面状态与元素读取
浏览器服务把 DOM 变为可供模型阅读的文本,同时保存索引与元素、frame 的对应关系。索引是一次快照的定位线索,不是网站自己的元素 ID。
1. DOM 采集链
DomService.buildExpression 读取 resources/dom/dom_tree/index.js 与 run.js,组合并缓存执行脚本。getFrameState 将脚本分别放到对应 Frame 执行,再把结果解析为 DOMState、DOMElementNode、DOMTextNode 和 FrameSnapshot。
Page 的 frame 列表
→ 每个 Frame 执行采集脚本
→ 可见性、交互性、实时表单属性、文本及定位信息
→ 分配不重叠的索引范围
→ Java DOM 模型和 frame 映射
→ 文本渲染、交互元素表、截图与快照存储
可见性、是否可交互以及可否接收点击是不同概念。采集需要考虑布局、视口及扩展范围;点击时仍需检查遮挡。密码属性输出为脱敏标记,但这不意味着截图、URL 和普通输入框都被脱敏。
get_browser_state 默认读取主文档及可处理的同源 frame,includeFrames:true 将跨域 frame 也纳入。各 frame 共享一个全局索引空间,按索引操作时通过映射回到正确 Frame;不能拿子 frame 内的 XPath 在主文档执行。
{"id":"1001","method":"get_browser_state","params":{"includeFrames":true,"highlight":false,"viewportExpansion":800,"includeElements":true,"maxElements":100}}
maxElements 控制响应内联元素数量,不应被当作整个 DOM 的元素总数。视口外找不到控件时,先检查滚动距离和视口扩展,再决定滚动或使用选择器。
快照一致性与索引有效性
当前实现比较采集前后的页面标记,返回 snapshotConsistent、snapshotAttempts、indicesUsable 和 pageAppearsBlank。发现文档或 frame 身份变化、部分读取失败或索引重校验不通过时会清除任务快照、作废索引,并有限重取;仍不一致时补充 snapshotIssues、snapshotHint 等诊断信息。
判定一份索引还算不算数,看的是结构变更 + 逐元素重校验,而不是「页面总共变过多少次」:
- 索引最终解析成的是位置型 xpath(形如
html/body/div[2]/form/input[1],只有同级序号、没有 class 谓词),所以文字改写、class/style变化这类内容类变更不会挪动任何同级序号,不影响索引; - 只有结构类变更(
childList里增删了元素节点)才会挪动同级序号,此时索引区间不再可信,一律作废; - 除此之外,采集到的每个索引会按它的 xpath 再查一次,比对标签与
id/name,对不上才作废。
采集期间的变更次数按增量返回,便于判断页面到底动的是什么:snapshotMutations、snapshotStructuralMutations、snapshotContentMutations。只有内容变动、结构未变且重校验通过时,回执另给 snapshotNote 说明「为什么这次没有作废」。传 strictSnapshot:true 可以恢复旧的「读取期间任何 DOM 变更即作废」判据,用于排查。
调用方必须检查 indicesUsable,不能仅看 ok:true 就使用索引。遇到 false 时等待目标内容后重新获取状态;pageAppearsBlank 只是此次文本为空的提示,不能直接认定网站没有内容或业务已经失败。
2. 页面级读取各有用途
| 命令 | 实现与结果 |
|---|---|
get_browser_state | 生成 DOM 快照,保存索引,返回页面文本、页签和元素信息,并留档 |
list_frames | 列出当前页面 frame 的序号、地址等信息;refresh 控制相关刷新行为 |
get_page_snapshot | 汇总页面快照,可附控制台和请求信息,便于一次获取诊断上下文 |
diff_dom_text | 重新生成快照,与之前的文本按行比较;它会更新快照,不是单纯读取旧缓存 |
get_interactive_map | 返回当前可交互元素映射,适合检查完整定位信息 |
get_form_state | 遍历表单控件,读取实时 value、checked、disabled、readonly、错误提示等;可限制 selector、隐藏项和数量 |
extract_structured_data | 当前实现读取 document.body.innerText,按选项收集链接;query 被保留为结果信息,不在此命令中调用大模型抽取 |
extract_markdown | 取页面(或选择器命中的第一个元素)的 HTML 并转成 Markdown,表格按 GFM 表格输出;同样不调用大模型 |
两条命令都不调用大模型:extract_structured_data 返回纯文本,extract_markdown 返回 Markdown,仅此区别。不要把命令名中的 structured 当成“已经按 query 生成业务 JSON”。如需特定结构,应在上层明确增加解析或模型步骤,见 正文提取。
页面上的表格不要用 extract_structured_data 读:它的正文取自 innerText,会把单元格逐行摊平、列对不上、colspan 丢失,改用 extract_markdown。
3. 元素读取共用 read 包装
多数读取方法调用 read(browserId, index, action, resultKey, reader):先找任务与索引,再执行读取函数,最后将结果写到指定 data 字段。下面是当前源码的调用形式:
return read(browserId, index, "get_element_value", "value",
locator -> locator.inputValue());
| 命令 | 底层读取 | 主要结果字段 |
|---|---|---|
get_element_text | Locator.innerText() | text |
get_element_html | Locator.innerHTML(),不是 outerHTML | html |
get_element_value | Locator.inputValue() | value |
get_element_attribute | Locator.getAttribute(name) | value |
is_visible | Locator.isVisible() | visible |
is_enabled | Locator.isEnabled() | enabled |
is_checked | Locator.isChecked() | checked |
get_element_count | 指定 Frame 内按 selector 统计 | 数量结果;无匹配与调用失败要区分 |
get_element_box | 获取元素 bounding box | 坐标与尺寸;无布局信息时不能虚构坐标 |
get_element_listeners | ListenerProbe 尝试协议探测和框架线索 | found、监听信息及检测说明;未知不等于没有监听 |
inputValue 与 HTML attribute 不同:用户修改输入框后,初始 value 属性可能没变。状态读取必须使用适合控件的实时属性。对非表单元素调用 inputValue、对非复选控件调用 isChecked,也可能因类型不匹配失败。
4. 索引生命周期与验证
在页面更新、导航或页签切换后重新取快照。即使新页面也有索引 8,它也不一定是上次的索引 8。实现中的旧快照提示和 frame 映射旨在帮助排障,不提供跨任意页面变更的稳定句柄。
本地 fixture 应包含输入框、复选框、隐藏字段、只读控件和 iframe。修改 property 后验证文本和 get_form_state 显示新值;验证密码脱敏、子 frame 索引不冲突,以及 get_element_html 只返回内部 HTML。另用两段等长文本测试差异检测,避免只比较字符串长度。
注册参数与 Java 入口
以下按 CommandTable 实际读取参数整理。* 表示注册层使用必填读取器;其余字段省略后由服务决定默认行为。带条件的入口仍需满足正文说明,例如上传文件来源、元素定位二选一。外层 id 不重复列出。
| 命令 | params 字段 | Java 入口 |
|---|---|---|
get_browser_state | highlight、viewportExpansion、includeElements、maxElements、includeFrames、strictSnapshot | getBrowserState |
list_frames | refresh | listFrames |
get_page_snapshot | includeConsole、includeRequests、requestFilter | getPageSnapshot |
diff_dom_text | highlight、viewportExpansion | diffDomText |
get_interactive_map | 无 | getInteractiveMap |
get_form_state | selector、includeHidden、max | getFormState |
extract_structured_data | query、extractLinks | extractStructuredData |
extract_markdown | selector、frame、includeLinks、maxChars | extractMarkdown |
get_element_text | index、selector、canvasOnly、frame | getElementText |
get_element_html | index* | getElementHtml |
get_element_value | index* | getElementValue |
get_element_attribute | index、name | getElementAttribute |
get_element_listeners | index、selector、frame | getElementListeners |
get_element_count | selector*、frame | getElementCount |
get_element_box | index* | getElementBox |
is_visible | index* | isVisible |
is_enabled | index* | isEnabled |
is_checked | index* | isChecked |
当前源码:命令注册与执行
先在本章上半部分理解行为,再按命令展开实现。注册代码说明 JSON 参数如何传给 Java;服务方法展示实际浏览器操作。方法依赖共享类中的字段和辅助函数,不应脱离原类直接粘贴编译。
get_browser_state
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_browser_state",
(svc, id, a) -> svc.getBrowserState(id, a.getBoolean("highlight"), a.getInteger("viewportExpansion"),
a.getBoolean("includeElements"), a.getInteger("maxElements"), a.getBoolean("includeFrames"),
a.getBoolean("strictSnapshot")));
展开 getBrowserState 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getBrowserState(Long browserId, Boolean highlight, Integer viewportExpansion,
Boolean includeElements, Integer maxElements, Boolean includeFrames, Boolean strictSnapshot) {
return getBrowserStateAttempt(browserId, highlight, viewportExpansion, includeElements, maxElements,
includeFrames, Boolean.TRUE.equals(strictSnapshot), 1);
}
list_frames
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("list_frames", (svc, id, a) -> svc.listFrames(id, a.getBoolean("refresh")));
展开 listFrames 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo listFrames(Long browserId, Boolean refresh) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
boolean rebuild = refresh == null || refresh;
if (rebuild) {
try {
recordSnapshot(inst, DomService.getFrameState(inst.page, false, 0));
} catch (PlaywrightException e) {
return RespBodyVo.fail("list_frames 失败:读取页面 frame 失败(" + briefMessage(e.getMessage()) + ")");
}
}
List<Kv> frames = frameList(inst.domState);
if (frames.isEmpty()) {
// 快照不是带 frame 的那种时,至少把 frame 的 URL/名称给出来,别让调用方以为「一个 frame 都没有」
for (DomService.FrameNode node : DomService.frameTree(inst.page)) {
Kv item = Kv.by("index", frames.size()).set("url", safeFrameUrl(node.frame))
.set("name", safeFrameName(node.frame)).set("isMain", node.parentIndex < 0).set("depth", node.depth);
if (node.parentIndex >= 0) {
item.set("parentIndex", node.parentIndex);
}
frames.add(item);
}
}
Kv data = Kv.by("count", frames.size()).set("frames", frames)
.set("note", "index 0 是主 frame;跨域 iframe 也能读到 URL(这是识别它的主要依据)。"
+ "要读 iframe 里的元素用 get_browser_state 的 includeFrames:true");
return RespBodyVo.ok(data);
}
get_page_snapshot
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_page_snapshot", (svc, id, a) -> svc.getPageSnapshot(id, a.getBoolean("includeConsole"),
a.getBoolean("includeRequests"), optStr(a, "requestFilter")));
展开 getPageSnapshot 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getPageSnapshot(Long browserId, Boolean includeConsole, Boolean includeRequests,
String requestFilter) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
Kv kv = Kv.by("url", inst.page.url()).set("title", safeTitle(inst.page)).set("tabs", tabs(inst))
.set("dialog", inst.lastDialog).set("loading", isPageLoading(inst));
if (includeConsole != null && includeConsole) {
kv.set("logs", new ArrayList<>(inst.consoleLogs));
kv.set("errors", new ArrayList<>(inst.pageErrors));
}
if (includeRequests != null && includeRequests) {
kv.set("requests", filterRequests(inst, requestFilter));
}
return RespBodyVo.ok(kv);
}
diff_dom_text
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("diff_dom_text",
(svc, id, a) -> svc.diffDomText(id, a.getBoolean("highlight"), a.getInteger("viewportExpansion")));
展开 diffDomText 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo diffDomText(Long browserId, Boolean highlight, Integer viewportExpansion) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
String previous = inst.lastDomText;
RespBodyVo current = buildState(inst, highlight, viewportExpansion);
if (!current.isOk() || !(current.getData() instanceof Kv)) {
return current;
}
Kv data = (Kv) current.getData();
String text = data.getStr("text");
Kv diff = Kv.by("url", data.get("url")).set("title", data.get("title")).set("tabs", data.get("tabs"));
if (previous == null) {
diff.set("first", true).set("changed", true).set("added", firstLines(text)).set("removed", new ArrayList<>());
return RespBodyVo.ok(diff);
}
Map<String, Integer> before = countLines(previous);
Map<String, Integer> after = countLines(text);
List<String> added = new ArrayList<>();
List<String> removed = new ArrayList<>();
for (Map.Entry<String, Integer> entry : after.entrySet()) {
int delta = entry.getValue() - before.getOrDefault(entry.getKey(), 0);
for (int i = 0; i < delta && added.size() < MAX_DIFF_LINES; i++) {
added.add(entry.getKey());
}
}
for (Map.Entry<String, Integer> entry : before.entrySet()) {
int delta = entry.getValue() - after.getOrDefault(entry.getKey(), 0);
for (int i = 0; i < delta && removed.size() < MAX_DIFF_LINES; i++) {
removed.add(entry.getKey());
}
}
diff.set("first", false).set("changed", !added.isEmpty() || !removed.isEmpty());
diff.set("added", added).set("removed", removed);
return RespBodyVo.ok(diff);
}
get_interactive_map
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_interactive_map", (svc, id, a) -> svc.getInteractiveMap(id));
展开 getInteractiveMap 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getInteractiveMap(Long browserId) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
if (inst.domState == null) {
// 以前这里直接失败,要求调用方「先调 get_browser_state」——多一次往返,而且很容易漏。
// 索引本来就是本命令的产物,自己建一次快照即可(不落盘,不产生截图/文本垃圾文件)。
RespBodyVo built = buildState(inst, false, 0);
if (!built.isOk()) {
return built;
}
}
List<Kv> items = interactiveElements(inst);
return RespBodyVo.ok(Kv.by("count", items.size()).set("elements", items)
.set("source", "get_browser_state 快照(索引与 click_element_by_index / input_text / "
+ "check_element_by_index 等按索引命令共用)"));
}
get_form_state
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_form_state", (svc, id, a) -> svc.getFormState(id, optStr(a, "selector"), a.getBoolean("includeHidden"),
a.getInteger("max")));
展开 getFormState 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getFormState(Long browserId, String selector, Boolean includeHidden, Integer max) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
JSONObject args = new JSONObject();
args.put("selector", selector);
args.put("includeHidden", includeHidden != null && includeHidden);
args.put("max", max == null || max <= 0 ? 200 : max);
Object result;
try {
result = inst.page.evaluate(FORM_STATE, args);
} catch (PlaywrightException e) {
return RespBodyVo.fail("get_form_state 失败:" + briefMessage(e.getMessage()));
}
if (!(result instanceof Map)) {
return RespBodyVo.fail("get_form_state 失败:页面没有返回表单信息");
}
Kv data = new Kv();
data.putAll((Map) result);
data.set("url", safeUrl(inst.page));
data.set("scope", selector == null || selector.isBlank() ? "document" : selector);
return RespBodyVo.ok(data);
}
extract_structured_data
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("extract_structured_data",
(svc, id, a) -> svc.extractStructuredData(id, optStr(a, "query"), optBool(a, "extractLinks")));
展开 extractStructuredData 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo extractStructuredData(Long browserId, String query, boolean extractLinks) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
try {
Object text = inst.page
.evaluate("() => {" + " const c = document.getElementById('playwright-highlight-container');"
+ " const prev = c ? c.style.display : null;" + " if (c) c.style.display = 'none';"
+ " const t = document.body ? document.body.innerText.slice(0, 20000) : '';"
+ " if (c) c.style.display = prev || '';" + " return t; }");
Kv kv = Kv.by("query", query).set("text", text);
if (extractLinks) {
kv.set("links", linksIn(inst.page.mainFrame()));
}
return RespBodyVo.ok(kv);
} catch (PlaywrightException e) {
return RespBodyVo.fail("extract_structured_data 失败:" + briefMessage(e.getMessage()));
}
}
/** 页面链接清单:文本截到 80 字符,`href` 用解析后的绝对地址 */
private static Object linksIn(Frame frame) {
return frame.evaluate("() => Array.from(document.querySelectorAll('a[href]'))"
+ ".map(a => ({text: (a.innerText || '').trim().slice(0, 80), href: a.href}))");
}
extract_markdown
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("extract_markdown", (svc, id, a) -> svc.extractMarkdown(id, optStr(a, "selector"), optStr(a, "frame"),
optBool(a, "includeLinks"), a.getInteger("maxChars")));
展开 extractMarkdown 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo extractMarkdown(Long browserId, String selector, String frame, boolean includeLinks,
Integer maxChars) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
int limit = maxChars == null || maxChars <= 0 ? DEFAULT_MARKDOWN_MAX_CHARS : maxChars;
try {
Frame target = frameOf(inst, frame);
String html;
String source;
if (selector == null || selector.trim().isEmpty()) {
Object body = target.evaluate("() => document.body ? document.body.innerHTML : ''");
html = body == null ? "" : String.valueOf(body);
source = "body";
} else {
Locator locator = target.locator(selector);
if (locator.count() == 0) {
return RespBodyVo.fail("extract_markdown 失败:选择器没有匹配到元素: " + selector);
}
Object outer = locator.first().evaluate("el => el.outerHTML");
html = outer == null ? "" : String.valueOf(outer);
source = selector;
}
String markdown = HtmlMarkdown.toMarkdown(html);
int length = markdown.length();
boolean truncated = length > limit;
Kv kv = Kv.by("markdown", truncated ? markdown.substring(0, limit) : markdown).set("length", length)
.set("truncated", truncated).set("source", source).set("url", inst.page.url())
.set("title", inst.page.title());
if (includeLinks) {
kv.set("links", linksIn(target));
}
return RespBodyVo.ok(kv);
} catch (IllegalArgumentException e) {
return RespBodyVo.fail("extract_markdown 失败:" + e.getMessage());
} catch (PlaywrightException e) {
return RespBodyVo.fail("extract_markdown 失败:" + briefMessage(e.getMessage()));
}
}
转换本身与浏览器无关,单独成类、可独立测试:先用 jsoup 摘掉不该进正文的节点,再交给转换器。
public class HtmlMarkdown {
private static final String HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
private static final String NON_CONTENT_SELECTOR = "script, style, noscript, template, head";
public static String toMarkdown(String html) {
if (html == null || html.isEmpty()) {
return "";
}
Document doc = Jsoup.parse(html);
doc.select(NON_CONTENT_SELECTOR).remove();
doc.select("#" + HIGHLIGHT_CONTAINER_ID).remove();
Element body = doc.body();
String source = body != null && !body.html().isEmpty() ? body.html() : doc.html();
FlexmarkHtmlConverter converter = FlexmarkHtmlConverter.builder().build();
return tidy(converter.convert(source));
}
}
get_element_text
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_text", (svc, id, a) -> svc.getElementText(id, a.getInteger("index"), optStr(a, "selector"),
Boolean.valueOf(optBool(a, "canvasOnly")), optStr(a, "frame")));
展开 getElementText 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementText(Long browserId, Integer index, String selector, Boolean canvasOnly,
String frame) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
if ((selector == null || selector.isBlank()) && index == null) {
return RespBodyVo.fail("get_element_text 需要 index 或 selector 之一");
}
Locator locator;
String target;
if (selector != null && !selector.isBlank()) {
try {
locator = locatorIn(inst, frame, selector);
} catch (IllegalArgumentException e) {
return RespBodyVo.fail("get_element_text 失败:" + e.getMessage());
}
int count = locator.count();
if (count == 0) {
return RespBodyVo.fail("get_element_text 没匹配到元素: 选择器 " + selector + frameSuffix(frame));
}
target = "选择器 " + selector + frameSuffix(frame);
} else {
locator = locatorOf(inst, index);
if (locator == null) {
return RespBodyVo.fail("get_element_text 索引越界: " + index + indexHint(inst));
}
target = "index=" + index;
}
Kv data = new Kv();
data.set("target", target);
String text;
try {
text = locator.innerText();
data.set("text", text);
} catch (PlaywrightException e) {
return RespBodyVo.fail(actionFailure("get_element_text", e));
}
if (Boolean.TRUE.equals(canvasOnly) && (text == null || text.isBlank())) {
Boolean onlyCanvas = null;
try {
Object raw = locator.evaluate("el => { if (!el.querySelector) return null;"
+ " const canvases = el.tagName === 'CANVAS' ? [el] : Array.from(el.querySelectorAll('canvas'));"
+ " if (!canvases.length) return false;"
+ " const textish = Array.from(el.querySelectorAll('*')).filter(n => n.children.length === 0"
+ " && (n.textContent || '').trim().length > 0).length;"
+ " return textish === 0; }");
onlyCanvas = raw instanceof Boolean ? (Boolean) raw : null;
} catch (PlaywrightException e) {
// 诊断取不到不算失败:文字那一段已经如实返回了
onlyCanvas = null;
}
if (onlyCanvas != null) {
data.set("canvasOnly", onlyCanvas);
if (Boolean.TRUE.equals(onlyCanvas)) {
data.set("hint", "这个元素里的内容画在 <canvas> 上(图表 / 地图 / 看板),没有文字节点:"
+ "innerText 必然是空的。改走像素手段(截图后用 OCR),"
+ "或去读同一页面上**同时是文字**的地方(图例、数据面板、十字线浮标对应的 DOM 文本)");
}
}
}
return RespBodyVo.ok(data);
}
这个命令有两个地方与同类「按索引读取」的命令不同,都是为了把「读不到」说清楚:
selector与index二选一。canvas通常不进快照(不可交互的元素拿不到索引),所以只能按选择器够到它; 找不到匹配时直接报「没匹配到元素」,不会等满可操作性超时。canvasOnly是诊断开关。innerText只认真正的文字节点,而图表 / 地图 / 看板上的刻度与浮标是画在<canvas>上的像素,这里必然是空。开关打开且确实读不到文字时,回执追加canvasOnly(这里是不是基本只有 canvas)与hint(给出路:截图像素 + OCR,或去读旁边同时是 DOM 文字的地方)。诊断取不到不算失败, 文字那一段照常返回。
回执里的 target 说明这次定位到的是谁(index=… 或 选择器 …),便于排查「读到的是不是我以为的那个元素」。
get_element_html
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_html", (svc, id, a) -> svc.getElementHtml(id, reqInt(a, "index")));
展开 getElementHtml 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementHtml(Long browserId, int index) {
return read(browserId, index, "get_element_html", "html", (locator) -> locator.innerHTML());
}
get_element_value
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_value", (svc, id, a) -> svc.getElementValue(id, reqInt(a, "index")));
展开 getElementValue 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementValue(Long browserId, int index) {
return read(browserId, index, "get_element_value", "value", (locator) -> locator.inputValue());
}
get_element_attribute
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_attribute", (svc, id, a) -> svc.getElementAttribute(id, reqInt(a, "index"), reqStr(a, "name")));
展开 getElementAttribute 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementAttribute(Long browserId, int index, String name) {
return read(browserId, index, "get_element_attribute", "value", (locator) -> locator.getAttribute(name));
}
get_element_listeners
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_listeners", (svc, id, a) -> svc.getElementListeners(id, a.getInteger("index"),
optStr(a, "selector"), optStr(a, "frame")));
展开 getElementListeners 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementListeners(Long browserId, Integer index, String selector, String frame) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
if ((selector == null || selector.isBlank()) && index == null) {
return RespBodyVo.fail("get_element_listeners 需要 index 或 selector 之一");
}
Locator locator;
String target;
if (selector != null && !selector.isBlank()) {
try {
locator = locatorIn(inst, frame, selector);
} catch (IllegalArgumentException e) {
return RespBodyVo.fail("get_element_listeners 失败:" + e.getMessage());
}
target = "selector=" + selector + frameSuffix(frame);
} else {
locator = locatorOf(inst, index);
target = "index=" + index;
if (locator == null) {
return RespBodyVo.fail("get_element_listeners 索引越界: " + index + indexHint(inst));
}
}
Kv probe;
Frame probeFrame = null;
String elementScript;
try {
if (locator.count() == 0) {
return RespBodyVo.ok(Kv.by("found", false).set("target", target)
.set("note", "没找到这个元素:选择器/索引可能已经过期,重新 get_browser_state 再试"));
}
// CDP 的权威探测需要 frame 与本元素的解析表达式;拿不到就退回启发式,并把结论标成「未知」
if (selector != null && !selector.isBlank()) {
probeFrame = frame == null || frame.isBlank() ? inst.page.mainFrame() : frameOf(inst, frame);
} else {
probeFrame = frameForIndex(inst, inst.domState, index);
if (probeFrame == null) {
probeFrame = inst.page.mainFrame();
}
}
elementScript = elementResolveScript(inst, index, selector);
probe = ListenerProbe.probe(locator, probeFrame, elementScript);
} catch (IllegalArgumentException e) {
return RespBodyVo.fail("get_element_listeners 失败:" + e.getMessage());
} catch (PlaywrightException e) {
return RespBodyVo.fail("get_element_listeners 失败:" + briefMessage(e.getMessage()));
}
if (!Boolean.TRUE.equals(probe.getBoolean("found"))) {
return RespBodyVo.ok(Kv.by("found", false).set("target", target)
.set("note", "没找到这个元素:选择器/索引可能已经过期,重新 get_browser_state 再试"));
}
Kv data = Kv.by("found", true).set("target", target).set(probe);
String note = ListenerProbe.note(probe);
if (note != null) {
data.set("note", note);
}
return RespBodyVo.ok(data);
}
get_element_count
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_count", (svc, id, a) -> svc.getElementCount(id, reqStr(a, "selector"), optStr(a, "frame")));
展开 getElementCount 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementCount(Long browserId, String selector, String frame) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
try {
return RespBodyVo.ok(Kv.by("count", frameOf(inst, frame).locator(selector).count()));
} catch (IllegalArgumentException e) {
return RespBodyVo.fail("get_element_count 失败:" + e.getMessage());
} catch (PlaywrightException e) {
return RespBodyVo.fail("get_element_count 失败:" + briefMessage(e.getMessage()));
}
}
get_element_box
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_element_box", (svc, id, a) -> svc.getElementBox(id, reqInt(a, "index")));
展开 getElementBox 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getElementBox(Long browserId, int index) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
Locator locator = locatorOf(inst, index);
if (locator == null) {
return RespBodyVo.fail("get_element_box 索引越界: " + index + indexHint(inst));
}
try {
BoundingBox box = locator.boundingBox();
if (box == null) {
return RespBodyVo.fail("get_element_box 失败:元素不可见或没有布局信息");
}
return RespBodyVo.ok(Kv.by("x", box.x).set("y", box.y).set("width", box.width).set("height", box.height));
} catch (PlaywrightException e) {
return RespBodyVo.fail(actionFailure("get_element_box", e));
}
}
is_visible
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("is_visible", (svc, id, a) -> svc.isVisible(id, reqInt(a, "index")));
展开 isVisible 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo isVisible(Long browserId, int index) {
return read(browserId, index, "is_visible", "visible", (locator) -> locator.isVisible());
}
is_enabled
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("is_enabled", (svc, id, a) -> svc.isEnabled(id, reqInt(a, "index")));
展开 isEnabled 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo isEnabled(Long browserId, int index) {
return read(browserId, index, "is_enabled", "enabled", (locator) -> locator.isEnabled());
}
is_checked
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("is_checked", (svc, id, a) -> svc.isChecked(id, reqInt(a, "index")));
展开 isChecked 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo isChecked(Long browserId, int index) {
return read(browserId, index, "is_checked", "checked", (locator) -> locator.isChecked());
}
