源码教程:网络记录、请求拦截与控制台
网络功能用于解释“点击后发生了什么”。核心状态位于 BrowserInstance,通过页面事件监听记录请求、响应和失败,再由查询命令过滤和返回。
1. 从事件到请求记录
任务认领 Page 时安装监听器,为请求生成 requestId,保存 URL、方法、类型、时间及必要的正文摘要。响应和失败事件继续更新同一记录,在途计数则提供给等待逻辑。
记录有容量和截断限制,不是永久网络审计数据库。监听器挂载之前的请求不会凭空补齐;空列表可能只是还没有发生新请求,或过滤条件过严。
| 命令 | 实现及主要结果 |
|---|---|
get_requests | 按 URL 子串、resourceType、since 毫秒时间过滤,再取最新 limit 条;返回 requests/count/total、recordedSince、inflight |
get_response_body | 优先按 requestId 定位,也支持过滤或索引;从保留的响应记录读取正文,按 maxChars 裁剪 |
wait_for_response | 按 URL 条件等待响应,可回看近期记录;返回匹配响应及可取得的正文信息 |
同一 URL 可能连续请求多次。可靠关联应该使用 requestId,不能总取“最后一个相同 URL”就断言是某次按钮的结果。正文暂不可得与请求失败也不是同一个概念,应检查 bodyAvailable、truncated 和失败说明。
{"id":"1001","method":"get_requests","params":{"filter":"/api/demo","resourceType":"fetch","limit":10}}
获取实际 requestId 后再读取正文;不在示例中编造某个真实接口响应。页面跳转后仍可能读取缓存正文,但缓存不是无限期保存,不能作为长期持久化依据。
2. route 与 unroute
route 将 urlPattern 对应规则保存在任务状态中,注册到任务拥有的各个 Page;新页签认领后补上已有规则。这样不会为了拦截当前任务而直接修改其他任务的 Page。
规则包含 action、body、status、contentType。默认 action 是 abort;mock 用给定内容构造响应。允许通过的处理走浏览器继续请求流程。它不是完整的任意请求重写 API,命令表没有暴露的参数不能自行添加后期待生效。
{"id":"1001","method":"route","params":{"urlPattern":"**/api/demo","action":"mock","status":200,"contentType":"application/json","body":"{\"ok\":true,\"source\":\"fixture\"}"}}
unroute 传 pattern 时移除对应规则;不传时清除该任务页签上的规则和内存规则表。测试结束应解除拦截,否则后续读取到的可能仍是模拟响应。
3. 控制台记录
get_console_logs 返回任务捕获的浏览器控制台记录;clear_console_logs 清空这些记录。它们不是 Java 服务日志,也不等同于网络请求记录。console 输出、网络 body 和脚本异常可能含敏感资料,应分别限制展示与留档。
4. 本地验证
准备一个返回自增编号的本地接口,连续调用两次,验证 requestId 不同且各自正文对应正确编号。再测试超长响应的截断、网络失败、导航后的正文读取和 filter 无匹配。给接口挂 mock 后验证服务端计数没有增长,解除后再验证恢复真实请求。控制台测试使用虚构消息,不输出真实 token。
注册参数与 Java 入口
以下按 CommandTable 实际读取参数整理。* 表示注册层使用必填读取器;其余字段省略后由服务决定默认行为。带条件的入口仍需满足正文说明,例如上传文件来源、元素定位二选一。外层 id 不重复列出。
| 命令 | params 字段 | Java 入口 |
|---|---|---|
route | urlPattern*、action、body、status、contentType | route |
unroute | urlPattern | unroute |
get_requests | filter、resourceType、limit、since | getRequests |
wait_for_response | urlPattern*、timeoutSeconds、maxChars、lookBackSeconds | waitForResponse |
get_response_body | filter、index、maxChars、requestId | getResponseBody |
get_console_logs | 无 | getConsoleLogs |
clear_console_logs | 无 | clearConsoleLogs |
当前源码:命令注册与执行
先在本章上半部分理解行为,再按命令展开实现。注册代码说明 JSON 参数如何传给 Java;服务方法展示实际浏览器操作。方法依赖共享类中的字段和辅助函数,不应脱离原类直接粘贴编译。
route
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("route", (svc, id, a) -> svc.route(id, reqStr(a, "urlPattern"), optStr(a, "action"), optStr(a, "body"),
a.getInteger("status"), optStr(a, "contentType")));
展开 route 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo route(Long browserId, String urlPattern, String action, String body, Integer status,
String contentType) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
Kv rule = Kv.by("action", action == null || action.isEmpty() ? "abort" : action);
rule.set("body", body);
rule.set("status", status == null ? 200 : status);
rule.set("contentType", contentType == null ? "application/json" : contentType);
inst.routes.put(urlPattern, rule);
if (inst.routedPatterns.add(urlPattern)) {
for (Page page : pagesOf(inst)) {
registerRoute(inst, page, urlPattern);
}
}
return RespBodyVo.ok(Kv.by("urlPattern", urlPattern).set("action", rule.getStr("action")));
}
unroute
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("unroute", (svc, id, a) -> svc.unroute(id, optStr(a, "urlPattern")));
展开 unroute 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo unroute(Long browserId, String urlPattern) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
if (urlPattern == null || urlPattern.isEmpty()) {
for (Page page : pagesOf(inst)) {
page.unrouteAll();
}
inst.routes.clear();
inst.routedPatterns.clear();
} else {
inst.routes.remove(urlPattern);
if (inst.routedPatterns.remove(urlPattern)) {
for (Page page : pagesOf(inst)) {
page.unroute(urlPattern);
}
}
}
return RespBodyVo.ok();
}
get_requests
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_requests", (svc, id, a) -> svc.getRequests(id, optStr(a, "filter"), optStr(a, "resourceType"),
a.getInteger("limit"), a.getLong("since")));
展开 getRequests 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getRequests(Long browserId, String filter, String resourceType, Integer limit, Long since) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
List<Kv> matched = filterRequests(inst, filter, resourceType, since);
if (limit != null && limit > 0 && matched.size() > limit) {
matched = new ArrayList<>(matched.subList(matched.size() - limit, matched.size()));
}
Kv data = Kv.by("requests", matched).set("count", matched.size()).set("total", inst.requests.size())
.set("recordedSince", inst.recorderAttachedAt).set("inflight", inst.inflight.get())
.set("filter", filter).set("resourceType", resourceType);
if (matched.isEmpty()) {
data.set("note", inst.recorderAttachedAt == 0
? "这个任务还没有认领页签,所以什么都没记到"
: "没有匹配的请求。记录挂在页签上、且从认领那一刻开始:页面在 start 之前发出的请求、"
+ "以及发生在别的页签里的请求都不会出现在这里。先做一次动作(导航/点击)再读,"
+ "或检查 filter/resourceType 是否把结果过滤掉了");
}
return RespBodyVo.ok(data);
}
wait_for_response
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("wait_for_response", (svc, id, a) -> svc.waitForResponse(id, reqStr(a, "urlPattern"),
a.getDouble("timeoutSeconds"), a.getInteger("maxChars"), a.getInteger("lookBackSeconds")));
展开 waitForResponse 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo waitForResponse(Long browserId, String urlPattern, Double timeoutSeconds, Integer maxChars,
Integer lookBackSeconds) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
int lookBack = lookBackSeconds == null ? DEFAULT_RESPONSE_LOOKBACK_SECONDS : lookBackSeconds;
if (lookBack > 0) {
long earliest = System.currentTimeMillis() - lookBack * 1_000L;
BrowserInstance.RecordedResponse hit = null;
for (BrowserInstance.RecordedResponse recorded : snapshotResponses(inst)) {
if (recorded.at >= earliest && urlMatches(recorded.response.url(), urlPattern)) {
hit = recorded;
}
}
if (hit != null) {
Kv kv = responseInfo(hit, maxChars);
kv.set("ageMs", System.currentTimeMillis() - hit.at).set("fromLookBack", true);
return RespBodyVo.ok(kv);
}
}
Response response;
try {
// 用谓词而不是字符串重载:回看和等待共用同一套匹配,避免"回看命中但等待等不到"
response = inst.page.waitForResponse(candidate -> urlMatches(candidate.url(), urlPattern),
new Page.WaitForResponseOptions().setTimeout(timeoutMillis(timeoutSeconds)), () -> {
});
} catch (PlaywrightException e) {
return RespBodyVo.fail(waitFailure("wait_for_response", e));
}
BrowserInstance.RecordedResponse recorded = snapshotResponses(inst).stream()
.filter(item -> item.response == response).findFirst()
.orElseGet(() -> new BrowserInstance.RecordedResponse(response, System.currentTimeMillis(),
requestInfo(response.request())));
Kv kv = responseInfo(recorded, maxChars);
kv.set("ageMs", 0).set("fromLookBack", false);
return RespBodyVo.ok(kv);
}
get_response_body
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_response_body", (svc, id, a) -> svc.getResponseBody(id, optStr(a, "filter"), a.getInteger("index"),
a.getInteger("maxChars"), optStr(a, "requestId")));
展开 getResponseBody 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getResponseBody(Long browserId, String filter, Integer index, Integer maxChars, String requestId) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
List<BrowserInstance.RecordedResponse> matched = new ArrayList<>();
for (BrowserInstance.RecordedResponse recorded : snapshotResponses(inst)) {
if ((filter == null || filter.isEmpty() || recorded.response.url().contains(filter))
&& (requestId == null || requestId.equals(recorded.request.getStr("requestId")))) {
matched.add(recorded);
}
}
if (matched.isEmpty()) {
return RespBodyVo.fail(noResponseMatchMessage(inst, filter, requestId));
}
int position = index == null ? matched.size() - 1 : index;
if (position < 0 || position >= matched.size()) {
return RespBodyVo.fail("get_response_body 索引越界: " + position + ",匹配到 " + matched.size() + " 个响应");
}
return RespBodyVo.ok(responseInfo(matched.get(position), maxChars));
}
get_console_logs
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("get_console_logs", (svc, id, a) -> svc.getConsoleLogs(id));
展开 getConsoleLogs 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo getConsoleLogs(Long browserId) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
return RespBodyVo
.ok(Kv.by("logs", new ArrayList<>(inst.consoleLogs)).set("errors", new ArrayList<>(inst.pageErrors)));
}
clear_console_logs
展开参数注册
源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
put("clear_console_logs", (svc, id, a) -> svc.clearConsoleLogs(id));
展开 clearConsoleLogs 实现
源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。
public RespBodyVo clearConsoleLogs(Long browserId) {
BrowserInstance inst = INSTANCES.get(browserId);
if (inst == null) {
return notFound(browserId);
}
inst.consoleLogs.clear();
inst.pageErrors.clear();
return RespBodyVo.ok();
}
