Skip to content

源码教程:网络记录、请求拦截与控制台 ​

网络功能用于解释“点击后发生了什么”。核心状态位于 BrowserInstance,通过页面事件监听记录请求、响应和失败,再由查询命令过滤和返回。

1. 从事件到请求记录 ​

任务认领 Page 时安装监听器,为请求生成 requestId,保存 URL、方法、类型、时间及必要的正文摘要。响应和失败事件继续更新同一记录,在途计数则提供给等待逻辑。

记录有容量和截断限制,不是永久网络审计数据库。监听器挂载之前的请求不会凭空补齐;若只捕获到响应而没有匹配的请求记录,会生成明确标记为 correlationMissed: true、correlatedBy: "none" 的记录。空列表也可能只是还没有发生新请求,或过滤条件过严。

命令实现及主要结果
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 和失败说明。

json
{"id":"1001","method":"get_requests","params":{"filter":"/api/demo","resourceType":"fetch","limit":10}}

获取实际 requestId 后再读取正文;不在示例中编造某个真实接口响应。页面跳转后仍可能读取缓存正文,但缓存不是无限期保存,不能作为长期持久化依据。

正常响应会原地回填请求的 status、respondedAt,并返回 correlationMissed: false、correlatedBy: "identity"。这些关联字段也出现在读取响应的顶层数据和 request 元数据中。缺失关联时不按 URL 猜配,避免把并发请求的正文交给错误的 requestId。

响应头到达时只登记响应;下载完成的 requestfinished 回调在当前 Playwright 调用线程缓存 xhr/fetch 正文。缓存最多保留最近 100 个响应,每条最多 100000 字符;请求元数据最多 200 条。命中正文缓存时返回 bodyFromCache: true 和 bodyCapturedAt;可缓存的响应尚未完成缓存时可出现 bodyCapturePending: true。未缓存的正文仍尝试按需读取,读取失败才返回 bodyAvailable: false 和错误说明。详细原因与验证方法见请求响应关联与线程约束。

2. route 与 unroute ​

route 将 urlPattern 对应规则保存在任务状态中,注册到任务拥有的各个 Page;新页签认领后补上已有规则。这样不会为了拦截当前任务而直接修改其他任务的 Page。

规则包含 action、body、status、contentType。默认 action 是 abort;mock 用给定内容构造响应。允许通过的处理走浏览器继续请求流程。它不是完整的任意请求重写 API,命令表没有暴露的参数不能自行添加后期待生效。

json
{"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 入口
routeurlPattern*、action、body、status、contentTyperoute
unrouteurlPatternunroute
get_requestsfilter、resourceType、limit、sincegetRequests
wait_for_responseurlPattern*、timeoutSeconds、maxChars、lookBackSecondswaitForResponse
get_response_bodyfilter、index、maxChars、requestIdgetResponseBody
get_console_logs无getConsoleLogs
clear_console_logs无clearConsoleLogs

当前源码:命令注册与执行 ​

先在本章上半部分理解行为,再按命令展开实现。注册代码说明 JSON 参数如何传给 Java;服务方法展示实际浏览器操作。方法依赖共享类中的字段和辅助函数,不应脱离原类直接粘贴编译。

route ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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(() -> recordResponse(inst, response));
    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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("get_console_logs", (svc, id, a) -> svc.getConsoleLogs(id));
展开 getConsoleLogs 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("clear_console_logs", (svc, id, a) -> svc.clearConsoleLogs(id));
展开 clearConsoleLogs 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

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();
  }