Skip to content

源码教程:生命周期、导航与页签 ​

本章从 PlaywrightService.start、BrowserInstance 和页签操作方法入手。共享浏览器策略详见浏览器与登录态。

1. 启动不是每个任务都创建一个浏览器 ​

start 先解析 browser、headless、profile 等配置,再复用或创建共享浏览器,将任务 ID 与 BrowserInstance 关联。任务持有自己的页签集合和当前 Page;多个任务可能共用同一个 BrowserContext,所以页签隔离不等于 Cookie 隔离。

注册层默认 headless=false,显示窗口,没有指定 ID 时由服务生成。返回不仅有 id,还有请求浏览器与实际浏览器信息、参数是否被采纳、profile 是否见过等诊断字段。选中某浏览器不能证明账号已经登录,仍要读实际页面。

json
{"method":"start","params":{"headless":true,"browser":"chrome"}}

先保存返回 ID,再调用后续命令。共享进程首次启动未完成时,先用 list_tasks 观察状态,不重复创建同一任务。

close 移除任务;若还有其他任务,只关闭该任务页签。最后一个任务保留浏览器与数据页,移除旧任务监听及路由。显式 closeBrowser:true 才释放浏览器,有其他任务时拒绝。shutdown 则关闭全部任务与共享浏览器,但不终止 Java 服务。二者不应混用。

2. 导航实现与结果校验 ​

waitUntil 支持 commit、domcontentloaded(默认)、load、networkidle;timeoutMs 为 1—120000 毫秒。NavigationValidator 使用 ParameterValidator 校验,返回实际 URL、status 和 waitUntil。

命令实现方式与注意点
navigate、go_to_url导向 goToUrl 的导航流程;调用 Page 导航并检查当前 URL,处理已跳转但底层异常的情形
go_back调用当前 Page 的历史后退,不能保证前一页是指定业务页面
go_forward调用历史前进,无可前进项时不等于应用故障
reload刷新当前 Page,可能丢失表单和一次性验证状态
get_url从当前 Page 取 URL
get_title从当前 Page 取标题,不根据地址猜标题

导航成功后应重取快照,旧 DOM 索引属于旧页面。页面 URL 相同也可能经过 SPA 重建,不能据此长期缓存 Locator 的索引映射。

json
{"id":"1001","method":"go_to_url","params":{"url":"https://example.com"}}

3. 页签是任务内的列表 ​

get_tabs 返回任务拥有的页签;new_tab 在共享上下文创建 Page,登记到当前任务,并按可选 URL 导航。由网页弹出的页签也需要被事件监听归属到任务。不能直接使用整个 Context 的 Page 列表替代任务列表,否则会操作其他任务的页面。

命令Java 层的关键行为
switch_tab校验任务内 pageIndex,切换当前 Page,并重置或更新相关快照
switch_tab_by_url在任务页签中匹配 URL,切到命中的页面;调用前先检查实际列表
close_tab关闭指定任务页签;当前页被关闭时需要选取剩余 Page
close_other_tabs保留指定或当前页,关闭该任务的其他页签
bring_to_front将指定或当前 Page 提到前台,便于人工操作;不是登录完成信号

pageIndex 从 0 开始。文本形式的 browser_state 展示序号可能从 1 开始,不可照抄为参数。新页签出现也不保证当前 Page 已切换,先 get_tabs 再决定。

4. 本地验证 ​

在本地测试页面加入一个 target="_blank" 链接,启动两个任务。验证新页签只属于触发它的任务;关闭其中一个任务后,另一个仍能读取标题。再验证最后一个任务结束后浏览器及页面保留,新任务不接管这些页面,以及显式关闭后资源释放,另检查不存在 ID、越界 pageIndex 的错误返回。

人工验证页面不要用 reload 作为常规排障手段。关闭或重启浏览器前,也要考虑其他任务是否仍在使用共享上下文。

注册参数与 Java 入口 ​

以下按 CommandTable 实际读取参数整理。* 表示注册层使用必填读取器;其余字段省略后由服务决定默认行为。带条件的入口仍需满足正文说明,例如上传文件来源、元素定位二选一。外层 id 不重复列出。

命令params 字段Java 入口
startheadless、browserbrowserInfo / start
closecloseBrowserclose
shutdown无shutdown
navigateurl*、waitUntil、timeoutMsgoToUrl
go_to_urlurl*、waitUntil、timeoutMsgoToUrl
go_back无goBack
go_forward无goForward
reload无reload
get_url无getUrl
get_title无getTitle
get_tabs无getTabs
new_taburlnewTab
switch_tabpageIndex*switchTab
switch_tab_by_urlurl*switchTabByUrl
close_tabpageIndex*closeTab
close_other_tabspageIndexcloseOtherTabs
bring_to_frontpageIndexbringToFront

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

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

start ​

展开参数注册

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

java
    put("start", (svc, id, a) -> {
      // Show the browser window unless headless mode is explicitly requested.
      Boolean headless = a.getBoolean("headless");
      // 不传 browser 时按配置里的默认类型(browser.type / browser.engine)处理,行为与以前一致;
      // 传了不认识的值会在 svc.start 里被拒掉,错误信息里带上可选值(见 BrowserType)
      String requested = a.getString("browser");
      // 网络记录模式可选:true = 从一开始就记请求/响应;false = 这个任务不记;不传 = 按全局配置
      // (browser.network.record,默认 lazy:第一次要读网络数据时才挂监听器)
      Boolean networkRecording = a.getBoolean("networkRecording");
      long newId = svc.start(id, Boolean.TRUE.equals(headless), requested, networkRecording,
          new ProfileSelection(a.getString("profile"), a.getString("userDataDir"),
              a.getString("profileDirectory")));
      Kv data = Kv.by("id", newId);
      data.set("networkRecording", networkRecording == null
          ? PlaywrightService.networkRecordMode() + "(按全局配置)"
          : (networkRecording ? "on" : "off"));
      // 这次任务实际用的浏览器与 profile:用的哪个浏览器、是不是本机 Chrome、有没有用上用户自己的 profile
      Kv browser = svc.browserInfo(newId);
      if (browser != null) {
        data.set("browser", browser);
        // 「我要的浏览器」与「实际用的浏览器」都要写清楚:老版本发布包会**静默忽略** browser 参数,
        // 回 ok:true 却用另一个浏览器打开页面,这种问题靠回执一眼看穿,不用再去翻日志
        String effective = browser.getStr("type");
        boolean honored = engineHonored(requested, effective);
        data.set("requestedBrowser", requested == null || requested.isBlank() ? null : requested)
            .set("effectiveBrowser", effective).set("engineHonored", honored);
        // engineHonored:true 只说明「参数被采纳了」,不说明「这份 profile 里有登录态」——这两件事必须分开说,
        // 否则换引擎之后「所有站点都退登录了」会完全没有线索(见技能手册里「换引擎等于换一套登录态」)
        if (browser.get("profileSeenBefore") != null) {
          data.set("profileSeenBefore", browser.get("profileSeenBefore"));
        }
        if (browser.getStr("profileNote") != null) {
          data.set("profileNote", browser.getStr("profileNote"));
        }
        if (!honored) {
          data.set("engineWarning", "请求的浏览器是 " + requested + ",实际用的是 " + effective
              + ":说明这个服务实例没有按 browser 参数切换浏览器(常见于旧版本发布包,它只认 headless)。"
              + "要按需切换浏览器请升级服务端,或先用 get_config 确认 engine/type");
        }
      }
      return RespBodyVo.ok(data);
    });
展开 start 实现

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

java
  public long start(Long id, boolean headless, String browser) {
    BrowserChoice requested = null;
    if (browser != null && !browser.isBlank()) {
      requested = BrowserChoice.parse(browser);
      if (requested == null) {
        throw new IllegalArgumentException("无法识别的浏览器类型:" + browser + ",可选值:" + BrowserChoice.choices());
      }
    }
    long taskId = id == null ? SnowflakeIdUtils.id() : id;
    if (INSTANCES.containsKey(taskId)) {
      throw new IllegalStateException("该 id 已经有正在运行的浏览器实例:" + taskId + ",请先调用 close,或换一个 id");
    }
    long startedAt = System.currentTimeMillis();
    // 「这份 profile 之前用过吗」必须在启动**之前**判断:启动之后目录一定存在,再问就永远是「用过」
    java.util.Set<String> existedBefore = profileDirsExistingBeforeLaunch(requested);
    SharedBrowser shared = sharedBrowser(headless, requested);
    applyProfileHistory(shared, existedBefore);
    BrowserInstance instance = newTaskInstance(taskId, shared);
    INSTANCES.put(taskId, instance);
    log.info("task {} 浏览器就绪(browser={}),耗时 {}ms", taskId, shared.resolvedType.id(),
        System.currentTimeMillis() - startedAt);
    return taskId;
  }

close ​

展开参数注册

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

java
    put("close", (svc, id, a) -> svc.close(id, Boolean.TRUE.equals(a.getBoolean("closeBrowser"))));
展开 close 实现

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

java
  public RespBodyVo close(Long browserId) {
    return close(browserId, false);
  }

  public RespBodyVo close(Long browserId, boolean closeBrowser) {
    synchronized (PlaywrightService.class) {
      if (closeBrowser && INSTANCES.keySet().stream().anyMatch(id -> !id.equals(browserId))) {
        return RespBodyVo.fail("Other tasks are active; closeBrowser cannot close their browser");
      }
      // A completed task may explicitly release its retained idle browser later.
      if (closeBrowser && INSTANCES.isEmpty()) {
        closeSharedBrowser();
        return RespBodyVo.ok(Kv.by("browserRetained", false).set("pagesRetained", false).set("remainingTasks", 0));
      }
      RespBodyVo result = closeTask(browserId);
      if (closeBrowser && result.isOk()) {
        closeSharedBrowser();
        result.setData(Kv.by("browserRetained", false).set("pagesRetained", false).set("remainingTasks", 0));
      }
      return result;
    }
  }

  private RespBodyVo closeTask(Long browserId) {
    BrowserInstance inst = INSTANCES.remove(browserId);
    if (inst == null) {
      return RespBodyVo.fail("没有找到对应的浏览器实例:" + browserId);
    }
    inst.detached = true;
    List<Page> taskPages = pagesOf(inst);
    boolean keepPages = INSTANCES.isEmpty() && sharedBrowser != null;
    if (keepPages) {
      // Hand completed pages to the user; future tasks must not claim them.
      sharedBrowser.foreignPages.removeIf(Page::isClosed);
      sharedBrowser.foreignPages.addAll(taskPages);
    }
    for (Page page : taskPages) {
      try {
        detachTaskPage(inst, page);
        if (!keepPages) {
          page.close();
        }
      } catch (RuntimeException e) {
        // 页签可能已经因为浏览器崩溃而失效,这里不该让 close 变成失败
        log.warn("关闭任务 {} 的页签失败:{}", browserId, e.getMessage());
      }
    }
    inst.pages.clear();
    // 注意:这里**不关** Playwright。它是全进程共享的 driver,关掉会把其它任务的浏览器一起搞死;
    // 它由 JVM 退出时的 shutdown hook 负责收尾。
    return RespBodyVo.ok(Kv.by("browserRetained", sharedBrowser != null).set("pagesRetained", keepPages)
        .set("remainingTasks", INSTANCES.size()));
  }

shutdown ​

展开参数注册

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

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

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

java
  public RespBodyVo shutdown() {
    List<Long> closed = new ArrayList<>();
    for (Long id : new ArrayList<>(INSTANCES.keySet())) {
      try {
        close(id);
        closed.add(id);
      } catch (RuntimeException e) {
        log.warn("关停任务 {} 失败:{}", id, briefMessage(e.getMessage()));
      }
    }
    closeSharedBrowser();
    return RespBodyVo.ok(Kv.by("closedTasks", closed).set("remainingTasks", INSTANCES.size()));
  }
展开参数注册

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

java
    put("navigate", (svc, id, a) -> svc.goToUrl(id, reqStr(a, "url"), a.getString("waitUntil"), a.getInteger("timeoutMs")));
展开 navigate 实现

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

java
  public RespBodyVo navigate(Long browserId, String url) {
    return goToUrl(browserId, url);
  }

go_to_url ​

展开参数注册

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

java
    put("go_to_url", (svc, id, a) -> svc.goToUrl(id, reqStr(a, "url"), a.getString("waitUntil"), a.getInteger("timeoutMs")));
展开 goToUrl 实现

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

java
  public RespBodyVo goToUrl(Long browserId, String url) {
    return goToUrl(browserId, url, null, null);
  }

  public RespBodyVo goToUrl(Long browserId, String url, String waitUntil, Integer timeoutMs) {
    WaitUntilState stage = NavigationValidator.stage(waitUntil);
    NavigationValidator.timeout(timeoutMs);
    return withInstance(browserId, inst -> {
      try {
        Page.NavigateOptions options = new Page.NavigateOptions().setWaitUntil(stage);
        if (timeoutMs != null) {
          options.setTimeout(timeoutMs);
        }
        Response rsp = inst.page.navigate(url, options);
        return RespBodyVo.ok(Kv.by("status", rsp == null ? 0 : rsp.status())
            .set("url", inst.page.url()).set("waitUntil", stage.name().toLowerCase(java.util.Locale.ROOT)));
      } catch (PlaywrightException e) {
        // 伪故障兜底:这条异常可能只是事件泵投递过来的(见 ActionError#SPURIOUS_DISPATCH),
        // 而导航**其实已经成功**。实测 go_to_url 连试 3 次都报 Object doesn't exist: response@…,
        // 随后 get_url 读到的正是目标地址 —— 报成失败会直接误导调用方去排查「为什么打不开」。
        // 幂等导航的答案在地址栏里,读一次就能定论,不必猜。
        if (ActionError.isSpuriousDispatch(e.getMessage()) && landedOn(inst, url)) {
          return RespBodyVo.ok(Kv.by("status", 200).set("url", safeUrl(inst))
              .set("warning", "这次导航的底部调用抛了 Playwright 事件分发的伪故障"
                  + "(Object doesn't exist,与本次命令无关),但地址栏**已经是目标地址**,按成功处理")
              .set("spuriousDispatch", true));
        }
        // 网络层失败:报成 ACTION_TIMEOUT 会把调用方带向「元素/选择器」这条完全错误的路。
        // 真正该说的是「Chrome 停在他自己的网络错误页上,这一页没有任何内容可读」(见 networkFailureReceipt)
        RespBodyVo networkFailure = networkFailureReceipt(inst, url, e);
        if (networkFailure != null) {
          return networkFailure;
        }
        return RespBodyVo.fail("go_to_url 失败:" + briefMessage(e.getMessage()));
      }
    });
  }

go_back ​

展开参数注册

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

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

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

java
  public RespBodyVo goBack(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    String before = inst.page.url();
    Response rsp;
    try {
      rsp = inst.page.goBack();
    } catch (PlaywrightException e) {
      return RespBodyVo.fail("go_back 失败:" + briefMessage(e.getMessage()));
    }
    String after = inst.page.url();
    if (after.equals(before)) {
      return RespBodyVo.fail("无法后退:没有可用历史记录");
    }
    return RespBodyVo.ok(Kv.by("status", rsp == null ? 0 : rsp.status()).set("url", after));
  }

go_forward ​

展开参数注册

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

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

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

java
  public RespBodyVo goForward(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    String before = inst.page.url();
    Response rsp;
    try {
      rsp = inst.page.goForward();
    } catch (PlaywrightException e) {
      return RespBodyVo.fail("go_forward 失败:" + briefMessage(e.getMessage()));
    }
    String after = inst.page.url();
    if (after.equals(before)) {
      return RespBodyVo.fail("无法前进:没有可用历史记录");
    }
    return RespBodyVo.ok(Kv.by("status", rsp == null ? 0 : rsp.status()).set("url", after));
  }

reload ​

展开参数注册

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

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

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

java
  public RespBodyVo reload(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    Response rsp;
    try {
      rsp = inst.page.reload();
    } catch (PlaywrightException e) {
      return RespBodyVo.fail("reload 失败:" + briefMessage(e.getMessage()));
    }
    return RespBodyVo.ok(Kv.by("status", rsp == null ? 0 : rsp.status()));
  }

get_url ​

展开参数注册

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

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

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

java
  public RespBodyVo getUrl(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    return RespBodyVo.ok(Kv.by("url", inst.page.url()));
  }

get_title ​

展开参数注册

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

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

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

java
  public RespBodyVo getTitle(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    return RespBodyVo.ok(Kv.by("title", safeTitle(inst.page)));
  }

get_tabs ​

展开参数注册

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

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

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

java
  public RespBodyVo getTabs(Long browserId) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    return RespBodyVo.ok(Kv.by("tabs", tabs(inst)));
  }

new_tab ​

展开参数注册

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

java
    put("new_tab", (svc, id, a) -> svc.newTab(id, optStr(a, "url")));
展开 newTab 实现

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

java
  public RespBodyVo newTab(Long browserId, String url) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    Page page = inst.context.newPage();
    claimPage(inst, page);
    if (url != null && !url.isEmpty()) {
      page.navigate(url);
    }
    inst.page = page;
    // 新建页签后同样带到最前,便于人工观察
    activate(page);
    List<Page> pages = pagesOf(inst);
    return RespBodyVo.ok(Kv.by("pageIndex", pages.indexOf(page)).set("url", page.url()));
  }

switch_tab ​

展开参数注册

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

java
    put("switch_tab", (svc, id, a) -> svc.switchTab(id, reqInt(a, "pageIndex")));
展开 switchTab 实现

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

java
  public RespBodyVo switchTab(Long browserId, int pageIndex) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    List<Page> pages = pagesOf(inst);
    if (pageIndex < 0 || pageIndex >= pages.size()) {
      return RespBodyVo.fail("switch_tab 页签索引越界: " + pageIndex);
    }
    inst.page = pages.get(pageIndex);
    // 有头模式下把页签带到最前,否则人工看到的还是原来那个页签
    activate(inst.page);
    return RespBodyVo.ok();
  }

switch_tab_by_url ​

展开参数注册

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

java
    put("switch_tab_by_url", (svc, id, a) -> svc.switchTabByUrl(id, reqStr(a, "url")));
展开 switchTabByUrl 实现

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

java
  public RespBodyVo switchTabByUrl(Long browserId, String url) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    List<Page> pages = pagesOf(inst);
    for (int i = 0; i < pages.size(); i++) {
      String current = pages.get(i).url();
      if (urlMatches(current, url)) {
        inst.page = pages.get(i);
        activate(inst.page);
        return RespBodyVo.ok(Kv.by("pageIndex", i).set("url", current).set("title", safeTitle(pages.get(i))));
      }
    }
    return RespBodyVo.fail("switch_tab_by_url 没匹配到页签: " + url + ",当前页签:" + tabs(inst));
  }

close_tab ​

展开参数注册

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

java
    put("close_tab", (svc, id, a) -> svc.closeTab(id, reqInt(a, "pageIndex")));
展开 closeTab 实现

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

java
  public RespBodyVo closeTab(Long browserId, int pageIndex) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    List<Page> pages = pagesOf(inst);
    if (pageIndex < 0 || pageIndex >= pages.size()) {
      return RespBodyVo.fail("close_tab 页签索引越界: " + pageIndex);
    }
    Page toClose = pages.get(pageIndex);
    boolean wasCurrent = inst.page == toClose;
    toClose.close();
    inst.pages.remove(toClose);
    if (wasCurrent) {
      // 关掉的是当前页:换一个还活着的页签,人工看到的窗口也跟着切
      recoverCurrentPage(inst);
    }
    return RespBodyVo.ok();
  }

close_other_tabs ​

展开参数注册

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

java
    put("close_other_tabs", (svc, id, a) -> svc.closeOtherTabs(id, a.getInteger("pageIndex")));
展开 closeOtherTabs 实现

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

java
  public RespBodyVo closeOtherTabs(Long browserId, Integer pageIndex) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    List<Page> pages = pagesOf(inst);
    Page keep;
    if (pageIndex == null) {
      keep = inst.page;
    } else {
      if (pageIndex < 0 || pageIndex >= pages.size()) {
        return RespBodyVo.fail("close_other_tabs 页签索引越界: " + pageIndex);
      }
      keep = pages.get(pageIndex);
    }
    int closed = 0;
    for (Page page : pages) {
      if (page == keep) {
        continue;
      }
      try {
        page.close();
        closed++;
      } catch (PlaywrightException e) {
        log.debug("关闭页签失败:{}", briefMessage(e.getMessage()));
      }
    }
    inst.page = keep;
    activate(keep);
    List<Page> remaining = pagesOf(inst);
    return RespBodyVo.ok(Kv.by("closed", closed).set("remaining", remaining.size())
        .set("pageIndex", remaining.indexOf(keep)).set("url", keep.url()));
  }

bring_to_front ​

展开参数注册

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

java
    put("bring_to_front", (svc, id, a) -> svc.bringToFront(id, a.getInteger("pageIndex")));
展开 bringToFront 实现

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

java
  public RespBodyVo bringToFront(Long browserId, Integer pageIndex) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    List<Page> pages = pagesOf(inst);
    if (pageIndex == null) {
      activate(inst.page);
      return RespBodyVo.ok(Kv.by("pageIndex", pages.indexOf(inst.page)).set("url", inst.page.url()));
    }
    if (pageIndex < 0 || pageIndex >= pages.size()) {
      return RespBodyVo.fail("bring_to_front 页签索引越界: " + pageIndex);
    }
    activate(pages.get(pageIndex));
    return RespBodyVo.ok(Kv.by("pageIndex", pageIndex).set("url", pages.get(pageIndex).url()));
  }