# Browser

> 用异步 Python 调用 QwenPaw 内置 Browser SDK 驱动真实浏览器。完整参考在下方；上下文压缩后请重新加载此 browser skill。

- Skill: `agentscope-ai/browser-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add agentscope-ai/browser-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/agentscope-ai/browser-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: agentscope-ai (https://skillmd.com/u/agentscope-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/agentscope-ai/browser-2

---


# 浏览器

保持工作纪律：先感知当前页面，再通过已列出的 API 动作，最后重新感知后才可声明成功。
只能陈述本轮实际观察到的事实。卡住时的合格交付 = 说明卡在哪一步 + 已亲眼验证的部分结果；
不要为了给出完整答案而补全你没有看到的内容。

尊重人工边界：登录、验证码、2FA 或任何必须由人完成的步骤，调用
`await browser.handoff(...)` 后停止，绝不自动化这些流程。
浏览器完不成时，不要拿其他渠道（如 web_search）的数据顶替并仍说成浏览器结果——
如实写明每个数据的来源。

这是 QwenPaw 内置的 Browser SDK，不是 Playwright。它是封闭的 API 表面：
未列出的方法不存在。完整参考在下方；若上下文中不再保留，请用 Skill 工具重新加载
此 browser skill。

<!-- BEGIN GENERATED: browser-manual -->
QwenPaw Browser SDK — complete reference. This is QwenPaw's OWN internal
SDK and this is the ENTIRE API; these are all the entrypoints. The SDK
is already in scope as Browser — call the methods below directly. Write
async Python. Work in a loop: perceive → act → verify.

# Copy this shape:
browser = await Browser.connect()                  # connect once; reused all session
page = await browser.open("https://example.com")   # open a page
obs = await page.snapshot()                         # PERCEIVE — page text is obs.text
if len(obs.text) < 6000:
    print(obs.text)
else:
    # Large page: read selectively instead of dumping everything.
    lines = [line for line in obs.text.splitlines()
             if "keyword" in line]
    print(f"{len(obs.text)} chars total; {len(lines)} matching lines:")
    print("\n".join(lines[:80]))
# For a focused count, use: await page.snapshot(query="keyword")
await page.get_by_role("textbox", name="Search").fill("laptop")   # ACT
await page.get_by_role("button", name="Search").click()            # ACT
obs = await page.snapshot()                         # VERIFY — re-perceive to confirm
print("Verified; inspect obs.text with the selective pattern above.")

Session state: this is a stateful session — variables you assign (browser,
page) persist across calls, so connect once and reuse them. If a call
reports the session was reset, re-run await Browser.connect().

Chrome backend caveat: with backend=chrome you operate inside the user's
real browser. A session is a tab-ownership group — tabs are isolated per
session, but identity (cookies, logins, storage) is shared with the user's
profile and with every other session. Do not rely on session-level identity
isolation on this backend.

browser (orchestration):
await Browser.connect(*, identity: "auto"|"user"|"avatar"|"guest" = "auto") -> browser
    Connect as an identity: user, avatar, guest, or auto.

    ``auto`` picks ``user`` when Chrome is connected, otherwise ``guest``.
    An unavailable explicit identity raises instead of substituting.
await browser.open(url: str | None = None) -> page
    Open a page at ``url`` and return it.

    Reuses this session's active page when one exists; otherwise a
    new page is created. Pages are released when the response cycle ends;
    start each cycle by calling ``open(url)`` again.
await browser.pages() -> list of page ref (.id, .url, .title, .active)
    List open pages with URL, title, and active-state details.
await browser.switch_page(page: page ref (.id, .url, .title, .active)) -> none
    Make the given page ref active for later operations.
await browser.close_page(page: page ref (.id, .url, .title, .active)) -> none
    Close the given page ref in this session.
await browser.session_status() -> session status (.owner, .variant, .context, .connected)
    Report the owner, variant, context, and connected state.
await browser.handoff(reason: str, instructions: str = "") -> a result dict
    Hand a step back to a human (captcha, login, 2FA).

    Pass a short reason and instructions; the run stops on this signal —
    never automate these flows. The active cycle-scoped page is retained
    for one extra response cycle after the handoff.
await browser.present(url: str | None = None) -> page
    Open a page retained for the chat lifetime.
await browser.close() -> none
    Close this session's browser and release its context.

page (operation):
await page.goto(url: str) -> a result dict
    Navigate this page to ``url`` and return raw navigation facts.
await page.go_back() -> a result dict
    Navigate back to the previous page in history.
await page.go_forward() -> a result dict
    Navigate forward to the next page in history.
await page.reload() -> a result dict
    Reload the current page.
await page.keep() -> none
    Retain this page across response cycles for the current chat.
await page.wait_for_load_state(state: str = "load", *, timeout: float | None = None) -> none
    Wait until the page reaches the requested load state.

    ``networkidle`` semantics depend on the backend: the Playwright
    backend waits for true network quiescence, while CDP-based
    backends (cdp, chrome) degrade to ``document.readyState ==
    "complete"`` plus a fixed 500 ms quiet delay and do NOT track
    in-flight requests — content loaded by late XHR may still be
    missing when this returns.
await page.wait_for_timeout(timeout: float) -> none
    Sleep unconditionally for *timeout* milliseconds (capped at 30 000).

    Prefer :py:meth:`locator.wait_for(state, timeout)
    <LocatorView.wait_for>` when waiting for a specific DOM condition
    — it returns as soon as the condition is met and is both faster
    and more reliable than an unconditional sleep.
await page.screenshot() -> a result dict
    Capture this page to a PNG file in the active workspace.
page.get_by_role(role: str, *, name: str | None = None) -> locator
    Locate elements by accessible role and optional name.
page.get_by_text(text: str) -> locator
    Locate elements by their visible text.
page.get_by_label(text: str) -> locator
    Locate a form control by its associated label text.
page.get_by_placeholder(text: str) -> locator
    Locate an input by its placeholder text.
page.locator(selector: str) -> locator
    Locate elements by a CSS selector when no semantic locator fits.
page.frame_locator(selector: str) -> locator
    Scope subsequent locators to the iframe matching ``selector``.
await page.snapshot(query: str | None = None) -> observation (read .text; .match_count when you pass a query)
    Perceive the page and return readable content in ``.text``.

    Pass ``query`` to also report ``.match_count``.
await page.current_surface() -> surface facts (.url, .title, .load_state)
    Return this page's current URL, title, and load facts.
page.mouse -> coordinate/keyboard input surface (see methods below)
    Viewport-coordinate input surface.

    click(x, y) -> a result dict; verify the effect with snapshot().
page.keyboard -> coordinate/keyboard input surface (see methods below)
    Keyboard input surface.

    press(key) -> a result dict; verify the effect with snapshot().

page.get_by_* / page.locator(...) return a locator that mirrors a SUBSET
of Playwright's Python locator API — the Playwright-shaped part of this
SDK:
  compose/scope (chainable): get_by_role/get_by_text/get_by_label/
      get_by_placeholder, locator(sel), filter(...), nth(i), first, last (properties)
  iframe scope: page.frame_locator(sel).locator(...) (one frame; no nested frames)
  read (await): count()->int, inner_text()->str, text_content()->str|None,
      all_text_contents()->list, get_attribute(name)->str|None,
      input_value()->str, is_visible()->bool, is_enabled()->bool
  act (await; returns a short evidence line — read .evidence): click(), fill(v),
      type(t), press(key), check(), uncheck(), set_checked(b),
      select_option(*v), hover(), dblclick(), scroll(),
      focus(), blur(), clear(), wait_for(state), screenshot(),
      bounding_box()->dict|None (viewport ["x"] ["y"] ["width"] ["height"]; None when the element is not visible)
  strict-mode
      uniqueness is enforced — act only when the locator resolves to exactly
      one element (use count() to check). element_handle / raw CDP unavailable.

Backend differences (chrome/cdp vs playwright): on chrome/cdp the
accessible name is a heuristic (aria-labelledby > aria-label > alt >
title > text content) - container elements may match
get_by_role(name=) more broadly than under playwright, so
strict-mode errors are more likely there; narrow with
filter(has_text=) or a more specific role. is_enabled() reflects
only the disabled property, not aria-disabled. press() supports a
fixed key set: printable characters, Enter, Tab, Escape, Backspace,
Delete, Arrow keys, Home/End/PageUp/PageDown, and
Control/Shift/Alt/Meta combos - anything else fails with guidance.
type() sets the value directly and fires an input event; editors that
need real per-key events may not react - prefer fill() where possible.

Reading results (read these fields; the type names don't matter):
  snapshot()        -> .text (page text), .match_count (when you pass query)
  current_surface() -> .url, .title, .load_state
  page refs         -> .id, .url, .title, .active
  screenshot()      -> result dict; read ["path"]
  bounding_box()    -> viewport ["x"] ["y"] ["width"] ["height"]; None when the element is not visible
  mouse.click()/keyboard.press() -> result dict; fields depend on the backend, so verify with snapshot()
  actions           -> .evidence (a short line saying what happened)
  locator reads return plain str/int/bool/list directly.

If a locator fails, step DOWN one rung (don't jump):
  1. semantic  page.get_by_role/label/text            first choice
  2. css       page.locator(css)                       role missing/unstable
  3. coordinates use locator.bounding_box() first for an exact, low-cost viewport rectangle; use a screenshot to explore only when the element is absent from snapshot()
For captcha/login/2FA or any human-only step: await browser.handoff(reason,
instructions) and stop — never automate them.
<!-- END GENERATED: browser-manual -->

