<!-- Source: https://deepseek-browser-use.io.nexus/18 -->

# 源码教程：生命周期、导航与页签

本章从 `PlaywrightService.start`、`BrowserInstance` 和页签操作方法入手。共享浏览器策略详见[浏览器与登录态](<https://deepseek-browser-use.io.nexus/05>)。

## 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 入口 |
| --- | --- | --- |
| `start` | `headless`、`browser` | `browserInfo` / `start` |
| `close` | `closeBrowser` | `close` |
| `shutdown` | 无 | `shutdown` |
| `navigate` | `url`*、`waitUntil`、`timeoutMs` | `goToUrl` |
| `go_to_url` | `url`*、`waitUntil`、`timeoutMs` | `goToUrl` |
| `go_back` | 无 | `goBack` |
| `go_forward` | 无 | `goForward` |
| `reload` | 无 | `reload` |
| `get_url` | 无 | `getUrl` |
| `get_title` | 无 | `getTitle` |
| `get_tabs` | 无 | `getTabs` |
| `new_tab` | `url` | `newTab` |
| `switch_tab` | `pageIndex`* | `switchTab` |
| `switch_tab_by_url` | `url`* | `switchTabByUrl` |
| `close_tab` | `pageIndex`* | `closeTab` |
| `close_other_tabs` | `pageIndex` | `closeOtherTabs` |
| `bring_to_front` | `pageIndex` | `bringToFront` |

<!-- current-source-tutorial -->
## 当前源码：命令注册与执行

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

### `start`

::: details 展开参数注册

源码：`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);
    });
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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"))));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("shutdown", (svc, id, a) -> svc.shutdown());
```

:::

::: details 展开 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()));
  }
```

:::

### `navigate`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("go_back", (svc, id, a) -> svc.goBack(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("go_forward", (svc, id, a) -> svc.goForward(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("reload", (svc, id, a) -> svc.reload(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("get_url", (svc, id, a) -> svc.getUrl(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("get_title", (svc, id, a) -> svc.getTitle(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

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

```java
    put("get_tabs", (svc, id, a) -> svc.getTabs(id));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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()));
  }
```

:::
