From 42de2dd8024db86c125f722909d9be80657f7add Mon Sep 17 00:00:00 2001 From: bestmountain Date: Thu, 10 Sep 2026 18:36:04 +0800 Subject: [PATCH 1/4] perf: optimize attach probing and frame decoding --- attach.go | 11 +++++ attach_test.go | 37 ++++++++++++++- react/src/client.test.ts | 12 ++++- react/src/client.ts | 13 +++--- session.go | 99 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 9 deletions(-) diff --git a/attach.go b/attach.go index 5182caf..9e4d3b4 100644 --- a/attach.go +++ b/attach.go @@ -68,6 +68,17 @@ func attachPage(ctx context.Context, browser Session) (Page, error) { if err != nil { return nil, err } + if detector, ok := browser.(ActiveTabSession); ok { + targetID, err := detector.ActiveTab(ctx, listed) + if err != nil { + return nil, err + } + page, err := tabs.PageForTab(ctx, targetID) + if err != nil { + return nil, fmt.Errorf("绑定激活 Tab: %w", err) + } + return page, nil + } return activePage(ctx, tabs, listed) } diff --git a/attach_test.go b/attach_test.go index 01a733a..ffc6642 100644 --- a/attach_test.go +++ b/attach_test.go @@ -3,6 +3,7 @@ package browserkit import ( "context" "encoding/json" + "errors" "testing" ) @@ -103,12 +104,46 @@ func (p *focusPage) Evaluate(context.Context, string, EvaluateOptions) (Evaluate type focusSession struct { *fakeViewportSession - pages map[string]*focusPage + pages map[string]*focusPage + pageCalls []string } func (s *focusSession) PageForTab(_ context.Context, id string) (Page, error) { + s.pageCalls = append(s.pageCalls, id) return s.pages[id], nil } + +type activeTargetSession struct { + *focusSession + activeTarget string + probeCalls int +} + +func (s *activeTargetSession) ActiveTab(_ context.Context, listed []TabInfo) (string, error) { + s.probeCalls++ + if len(listed) == 0 { + return "", errors.New("missing tabs") + } + return s.activeTarget, nil +} + +func TestAttachBindsOnlyProbedActiveTab(t *testing.T) { + a := &focusPage{fakePage: &fakePage{info: PageInfo{URL: "https://example.com/a"}}} + b := &focusPage{fakePage: &fakePage{info: PageInfo{URL: "https://example.com/b"}}} + legacy := &focusSession{ + fakeViewportSession: &fakeViewportSession{fakeSession: &fakeSession{}, tabs: []TabInfo{{TargetID: "a", Type: "page"}, {TargetID: "b", Type: "page"}}}, + pages: map[string]*focusPage{"a": a, "b": b}, + } + session := &activeTargetSession{focusSession: legacy, activeTarget: "b"} + page, err := attachPage(context.Background(), session) + if err != nil { + t.Fatal(err) + } + if page != b || session.probeCalls != 1 || len(session.pageCalls) != 1 || session.pageCalls[0] != "b" { + t.Fatalf("page=%p probes=%d bindings=%v", page, session.probeCalls, session.pageCalls) + } +} + func TestAttachActiveTab(t *testing.T) { for _, tc := range []struct { name string diff --git a/react/src/client.test.ts b/react/src/client.test.ts index 60a9d16..230a3af 100644 --- a/react/src/client.test.ts +++ b/react/src/client.test.ts @@ -36,9 +36,17 @@ describe('browser client',()=>{ it('decodes frame identity separately from image bytes and rejects invalid headers',async()=>{ const header=new TextEncoder().encode(JSON.stringify({page_id:'page-a',generation:7,mime:'image/jpeg'})) const bytes=new Uint8Array(4+header.length+3);new DataView(bytes.buffer).setUint32(0,header.length);bytes.set(header,4);bytes.set([1,2,3],4+header.length) - const frame=await decodeFrame({arrayBuffer:async()=>bytes.buffer} as Blob) + const slices:Array<[number,number, string | undefined]>=[] + const data={size:bytes.byteLength,slice:(start=0,end=bytes.byteLength,type?:string)=>{ + slices.push([start,end,type]) + const part=bytes.slice(start,end) + return {size:part.byteLength,type:type||'',arrayBuffer:async()=>part.buffer} as Blob + }} as Blob + const frame=await decodeFrame(data) expect(frame.pageID).toBe('page-a');expect(frame.generation).toBe(7);expect(frame.blob.size).toBe(3) - await expect(decodeFrame({arrayBuffer:async()=>new ArrayBuffer(3)} as Blob)).rejects.toThrow('图帧头缺失') + expect(frame.blob.type).toBe('image/jpeg') + expect(slices).toEqual([[0,4,undefined],[4,4+header.length,undefined],[4+header.length,bytes.byteLength,'image/jpeg']]) + await expect(decodeFrame({size:3} as Blob)).rejects.toThrow('图帧头缺失') }) }) diff --git a/react/src/client.ts b/react/src/client.ts index 07d2397..c04651a 100644 --- a/react/src/client.ts +++ b/react/src/client.ts @@ -191,14 +191,15 @@ export interface BrowserFrame { generation: number; } export async function decodeFrame(data: Blob): Promise { - const bytes = await data.arrayBuffer(); - if (bytes.byteLength < 4) + if (data.size < 4) throw new Error('浏览器图帧头缺失'); - const length = new DataView(bytes).getUint32(0); - if (length > 65536 || length + 4 > bytes.byteLength) + const prefix = await data.slice(0, 4).arrayBuffer(); + const length = new DataView(prefix).getUint32(0); + if (length > 65536 || length + 4 > data.size) throw new Error('浏览器图帧头无效'); - const header = JSON.parse(new TextDecoder().decode(bytes.slice(4, 4 + length))); + const headerBytes = await data.slice(4, 4 + length).arrayBuffer(); + const header = JSON.parse(new TextDecoder().decode(headerBytes)); if (typeof header.page_id !== 'string' || !header.page_id || !Number.isSafeInteger(header.generation) || typeof header.mime !== 'string') throw new Error('浏览器图帧身份无效'); - return { blob: new Blob([bytes.slice(4 + length)], { type: header.mime }), pageID: header.page_id, generation: header.generation }; + return { blob: data.slice(4 + length, data.size, header.mime), pageID: header.page_id, generation: header.generation }; } diff --git a/session.go b/session.go index 1904650..1118f61 100644 --- a/session.go +++ b/session.go @@ -68,6 +68,13 @@ type TabSession interface { CloseTab(context.Context, string) error } +// ActiveTabSession 可在不为每个 target 创建完整 Page 绑定的情况下探测激活 Tab。 +// Attach 会优先使用该可选能力,且只为最终选中的 target 调用 PageForTab。 +type ActiveTabSession interface { + TabSession + ActiveTab(context.Context, []TabInfo) (string, error) +} + // ViewportTabSession 是可为已有 Tab 设置页面视口的可选能力。 type ViewportTabSession interface { TabSession @@ -409,6 +416,98 @@ func (session *rodSession) ListTabs(ctx context.Context) ([]TabInfo, error) { return tabs, nil } +// ActiveTab 使用短生命周期 CDP session 探测页面焦点;探测完成后立即 detach, +// 避免 PageForTab 为所有候选页安装观察器、初始化输入设备并长期缓存 Page。 +func (session *rodSession) ActiveTab(ctx context.Context, listed []TabInfo) (string, error) { + if session == nil || session.closed.Load() || session.browser == nil { + return "", newError("session_closed", "浏览器 session 已关闭", "重新调用 browserOpen 创建 session", true) + } + if err := contextErr(ctx); err != nil { + return "", err + } + operationCtx, operationCancel, err := session.ctx(ctx) + if err != nil { + return "", err + } + defer operationCancel() + if err := lockInteraction(operationCtx, &session.attachMu); err != nil { + return "", err + } + defer session.attachMu.Unlock() + + visibleTarget := "" + visibleCount := 0 + for _, tab := range listed { + if tab.Type != "page" { + continue + } + state, err := session.probeTabState(operationCtx, proto.TargetTargetID(tab.TargetID)) + if err != nil { + return "", fmt.Errorf("读取 Tab 状态: %w", err) + } + if state.Focused { + return tab.TargetID, nil + } + if state.Visible { + visibleTarget = tab.TargetID + visibleCount++ + } + } + if visibleCount == 1 { + return visibleTarget, nil + } + return "", errors.New("无法唯一确定激活 Tab;请切回目标网页并保持浏览器焦点后重试") +} + +type tabFocusState struct { + Focused bool `json:"focused"` + Visible bool `json:"visible"` +} + +type targetSessionClient struct { + ctx context.Context + browser *rod.Browser + sessionID proto.TargetSessionID +} + +func (client targetSessionClient) GetContext() context.Context { return client.ctx } +func (client targetSessionClient) GetSessionID() proto.TargetSessionID { return client.sessionID } +func (client targetSessionClient) Call(ctx context.Context, sessionID, method string, params any) ([]byte, error) { + return client.browser.Call(ctx, sessionID, method, params) +} + +func (session *rodSession) probeTabState(ctx context.Context, targetID proto.TargetTargetID) (state tabFocusState, err error) { + attached, err := (proto.TargetAttachToTarget{TargetID: targetID, Flatten: true}).Call(session.browser.Context(ctx)) + if err != nil { + return tabFocusState{}, err + } + defer func() { + cleanupCtx, cleanupCancel, cleanupErr := session.controlCtx(session.browser.GetContext(), 5*time.Second) + if cleanupErr == nil { + cleanupErr = (proto.TargetDetachFromTarget{SessionID: attached.SessionID}).Call(session.browser.Context(cleanupCtx)) + cleanupCancel() + } + if err == nil && cleanupErr != nil { + err = fmt.Errorf("释放 Tab 探测 session: %w", cleanupErr) + } + }() + probe := targetSessionClient{ctx: ctx, browser: session.browser, sessionID: attached.SessionID} + result, err := (proto.RuntimeEvaluate{ + Expression: `({focused: document.hasFocus(), visible: document.visibilityState === "visible"})`, + ReturnByValue: true, + }).Call(probe) + if err != nil { + return tabFocusState{}, err + } + if result.ExceptionDetails != nil || result.Result == nil { + return tabFocusState{}, errors.New("页面未返回激活状态") + } + if err := result.Result.Value.Unmarshal(&state); err != nil { + return tabFocusState{}, fmt.Errorf("解析激活页状态: %w", err) + } + return state, nil +} + // PageForTab 绑定一个已经存在的网页 target,并安装与 NewPage 相同的页面状态观察器。 func (session *rodSession) PageForTab(ctx context.Context, targetID string) (Page, error) { if session == nil || session.closed.Load() || session.browser == nil { From 12403ee08bf423be426d5aaf4e77969d62e1c611 Mon Sep 17 00:00:00 2001 From: bestmountain Date: Tue, 22 Sep 2026 14:23:35 +0800 Subject: [PATCH 2/4] feat: support Pi Agent skill installation --- README.md | 14 +++-- README.zh-CN.md | 12 ++++- install.sh | 69 ++++++++++++++++++++++-- install_test.go | 95 ++++++++++++++++++++++++++++++++++ skills/browserkit-cli/SKILL.md | 2 +- 5 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 install_test.go diff --git a/README.md b/README.md index f6a9907..aad8ba5 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Run build commands from the repository root and dependency installation commands ## 1. CLI and Skill -The installer installs both the client and the Agent Skill: +The installer builds the client and configures its Agent Skill. Codex is the default target: ```sh ./install.sh @@ -128,8 +128,16 @@ browserkit --version ``` The default client directory is `~/.local/bin`. -The default Skill directory is `${CODEX_HOME:-~/.codex}/skills/browserkit-cli`. -Override them with `BROWSERKIT_BIN_DIR` and `BROWSERKIT_SKILL_DIR`. +The default Codex Skill directory is `${CODEX_HOME:-~/.codex}/skills/browserkit-cli`. + +Configure the same Skill for Pi Agent, or for both agents: + +```sh +./install.sh --agent pi +./install.sh --agent all +``` + +Pi installs the Skill under `${PI_CODING_AGENT_DIR:-~/.pi/agent}/skills/browserkit-cli`; start a new Pi session and use `/skill:browserkit-cli` to invoke it explicitly. Override destinations with `BROWSERKIT_BIN_DIR`, `BROWSERKIT_CODEX_SKILL_DIR`, or `BROWSERKIT_PI_SKILL_DIR`. The legacy `BROWSERKIT_SKILL_DIR` override remains supported for a selected target. Agent usage instructions are in the [Skill](skills/browserkit-cli/SKILL.md). ### Attach to an existing browser diff --git a/README.zh-CN.md b/README.zh-CN.md index 363cc39..da75729 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -118,7 +118,7 @@ go mod download ## 1. CLI 与 Skill -安装脚本同时安装 client 和 Agent Skill: +安装脚本会构建 client 并配置 Agent Skill,默认目标为 Codex: ```sh ./install.sh @@ -127,7 +127,15 @@ browserkit --version ``` 默认 client 目录为 `~/.local/bin`,Skill 目录为 `${CODEX_HOME:-~/.codex}/skills/browserkit-cli`。 -用 `BROWSERKIT_BIN_DIR` 和 `BROWSERKIT_SKILL_DIR` 修改安装位置。 + +配置给 Pi Agent,或同时配置给两个 Agent: + +```sh +./install.sh --agent pi +./install.sh --agent all +``` + +Pi Skill 默认安装到 `${PI_CODING_AGENT_DIR:-~/.pi/agent}/skills/browserkit-cli`;新建 Pi 会话后可用 `/skill:browserkit-cli` 显式调用。使用 `BROWSERKIT_BIN_DIR`、`BROWSERKIT_CODEX_SKILL_DIR` 和 `BROWSERKIT_PI_SKILL_DIR` 修改对应位置;旧的 `BROWSERKIT_SKILL_DIR` 仍可覆盖所选目标的 Skill 目录。 Agent 操作约定见 [Skill](skills/browserkit-cli/SKILL.md)。 ### 接管已有浏览器 diff --git a/install.sh b/install.sh index 1563264..c972acd 100755 --- a/install.sh +++ b/install.sh @@ -1,16 +1,77 @@ #!/bin/sh set -eu +usage() { + cat <<'EOF' +Usage: ./install.sh [--agent codex|pi|all] + +Build browserkit and install its Agent Skill. The default agent is codex. + +Environment: + BROWSERKIT_AGENT Default agent when --agent is omitted. + BROWSERKIT_BIN_DIR Binary destination (default: ~/.local/bin). + BROWSERKIT_SKILL_DIR Legacy override for one selected agent. + BROWSERKIT_CODEX_SKILL_DIR Codex Skill destination. + BROWSERKIT_PI_SKILL_DIR Pi Agent Skill destination. + CODEX_HOME Codex data directory (default: ~/.codex). + PI_CODING_AGENT_DIR Pi Agent data directory (default: ~/.pi/agent). +EOF +} + +agent=${BROWSERKIT_AGENT:-codex} +while [ "$#" -gt 0 ]; do + case "$1" in + --agent) + [ "$#" -ge 2 ] || { printf '%s\n' '缺少 --agent 参数。' >&2; usage >&2; exit 2; } + agent=$2 + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + printf '未知参数: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +case "$agent" in + codex|pi|all) ;; + *) + printf '不支持的 Agent: %s(可选 codex、pi、all)\n' "$agent" >&2 + exit 2 + ;; +esac + repo_root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) bin_dir=${BROWSERKIT_BIN_DIR:-"${HOME}/.local/bin"} -skill_root=${BROWSERKIT_SKILL_DIR:-"${CODEX_HOME:-${HOME}/.codex}/skills/browserkit-cli"} -mkdir -p "$bin_dir" "$skill_root" +mkdir -p "$bin_dir" bin_dir=$(CDPATH= cd -- "$bin_dir" && pwd) (cd "$repo_root" && go build -o "$bin_dir/browserkit" ./cmd/browserkit) -cp "$repo_root/skills/browserkit-cli/SKILL.md" "$skill_root/SKILL.md" + +install_skill() { + skill_root=$1 + mkdir -p "$skill_root" + skill_root=$(CDPATH= cd -- "$skill_root" && pwd) + cp "$repo_root/skills/browserkit-cli/SKILL.md" "$skill_root/SKILL.md" + printf '已为 %s 安装 Skill: %s\n' "$2" "$skill_root/SKILL.md" +} + +legacy_skill_root=${BROWSERKIT_SKILL_DIR:-} +if [ "$agent" = codex ] || [ "$agent" = all ]; then + codex_skill_root=${BROWSERKIT_CODEX_SKILL_DIR:-${legacy_skill_root:-"${CODEX_HOME:-${HOME}/.codex}/skills/browserkit-cli"}} + install_skill "$codex_skill_root" Codex +fi +if [ "$agent" = pi ] || [ "$agent" = all ]; then + pi_skill_root=${BROWSERKIT_PI_SKILL_DIR:-${legacy_skill_root:-"${PI_CODING_AGENT_DIR:-${HOME}/.pi/agent}/skills/browserkit-cli"}} + install_skill "$pi_skill_root" "Pi Agent" +fi + printf '已安装 browserkit: %s\n' "$bin_dir/browserkit" -printf '已安装 Skill: %s\n' "$skill_root/SKILL.md" case ":${PATH}:" in *:"$bin_dir":*) ;; *) printf '请将 %s 加入 PATH。\n' "$bin_dir" ;; diff --git a/install_test.go b/install_test.go new file mode 100644 index 0000000..8d72ad6 --- /dev/null +++ b/install_test.go @@ -0,0 +1,95 @@ +package browserkit + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestInstallScriptAgentTargets(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("install.sh requires a POSIX shell") + } + repo, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + name string + args []string + env []string + paths []string + }{ + {name: "default codex", paths: []string{"codex/skills/browserkit-cli/SKILL.md"}}, + {name: "pi", args: []string{"--agent", "pi"}, paths: []string{"pi/skills/browserkit-cli/SKILL.md"}}, + {name: "all", args: []string{"--agent", "all"}, paths: []string{"codex/skills/browserkit-cli/SKILL.md", "pi/skills/browserkit-cli/SKILL.md"}}, + {name: "environment default", env: []string{"BROWSERKIT_AGENT=pi"}, paths: []string{"pi/skills/browserkit-cli/SKILL.md"}}, + } { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + fakeBin := filepath.Join(root, "fake-bin") + if err := os.MkdirAll(fakeBin, 0o755); err != nil { + t.Fatal(err) + } + fakeGo := filepath.Join(fakeBin, "go") + if err := os.WriteFile(fakeGo, []byte("#!/bin/sh\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = -o ]; then mkdir -p \"$(dirname \"$2\")\"; : > \"$2\"; exit 0; fi\n shift\ndone\nexit 1\n"), 0o755); err != nil { + t.Fatal(err) + } + cmd := exec.Command("/bin/sh", append([]string{filepath.Join(repo, "install.sh")}, test.args...)...) + cmd.Env = append(os.Environ(), + "HOME="+root, + "PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"), + "BROWSERKIT_AGENT=", + "BROWSERKIT_BIN_DIR="+filepath.Join(root, "bin"), + "BROWSERKIT_SKILL_DIR=", + "BROWSERKIT_CODEX_SKILL_DIR=", + "BROWSERKIT_PI_SKILL_DIR=", + "CODEX_HOME="+filepath.Join(root, "codex"), + "PI_CODING_AGENT_DIR="+filepath.Join(root, "pi"), + ) + cmd.Env = append(cmd.Env, test.env...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("install failed: %v\n%s", err, output) + } + for _, path := range test.paths { + contents, err := os.ReadFile(filepath.Join(root, path)) + if err != nil { + t.Fatalf("missing %s: %v\n%s", path, err, output) + } + if !strings.Contains(string(contents), "name: browserkit-cli") { + t.Fatalf("invalid Skill at %s", path) + } + } + if _, err := os.Stat(filepath.Join(root, "bin", "browserkit")); err != nil { + t.Fatalf("missing binary: %v", err) + } + }) + } +} + +func TestInstallScriptRejectsUnknownAgent(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("install.sh requires a POSIX shell") + } + cmd := exec.Command("/bin/sh", "./install.sh", "--agent", "unknown") + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), "不支持的 Agent") { + t.Fatalf("expected agent validation error, err=%v output=%s", err, output) + } +} + +func TestInstallScriptRejectsRemovedBothAlias(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("install.sh requires a POSIX shell") + } + cmd := exec.Command("/bin/sh", "./install.sh", "--agent", "both") + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), "不支持的 Agent") { + t.Fatalf("expected removed alias to be rejected, err=%v output=%s", err, output) + } +} diff --git a/skills/browserkit-cli/SKILL.md b/skills/browserkit-cli/SKILL.md index cf99eb6..4f0d79b 100644 --- a/skills/browserkit-cli/SKILL.md +++ b/skills/browserkit-cli/SKILL.md @@ -5,7 +5,7 @@ description: 通过 BrowserKit CLI 操作 Chrome 网页,支持连接已有浏 # BrowserKit -仓库根目录的 `install.sh` 同时安装 client 与本 Skill。 +仓库根目录的 `install.sh` 同时安装 client 与本 Skill。Codex 使用默认安装;Pi Agent 使用 `./install.sh --agent pi`,并可通过 `/skill:browserkit-cli` 显式加载。 根据用户指定的浏览器选择模式;连接失败时检查 CDP 地址和目标页,不自动切换到另一个浏览器。 ## 启动 From 661dc50da4b8122a89be69f8cce2217b0275eeab Mon Sep 17 00:00:00 2001 From: bestmountain Date: Tue, 22 Sep 2026 14:29:39 +0800 Subject: [PATCH 3/4] test: wait for stable popup tab state --- manager_browser_test.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/manager_browser_test.go b/manager_browser_test.go index 28b9961..d7bfa62 100644 --- a/manager_browser_test.go +++ b/manager_browser_test.go @@ -1104,13 +1104,34 @@ func TestBrowserSnapshotReportsStableTabRevisionAndNewActiveTab(t *testing.T) { if third.TabsRevision <= second.TabsRevision || !third.TabsChanged || third.ActivePageID == first.ActivePageID || third.PageID != third.ActivePageID || len(third.Tabs) != 2 { t.Fatalf("新 Tab 应在 Snapshot 中被发现并成为 active: %+v", third) } + stable := third + deadline := time.Now().Add(time.Second) + for activeTabStatus(stable) != "ready" && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + stable, err = manager.Snapshot(ctx, SnapshotRequest{SessionID: opened.SessionID}) + if err != nil { + t.Fatal(err) + } + } + if activeTabStatus(stable) != "ready" { + t.Fatalf("新 Tab 没有进入 ready 状态: %+v", stable) + } fourth, err := manager.Snapshot(ctx, SnapshotRequest{SessionID: opened.SessionID}) if err != nil { t.Fatal(err) } - if fourth.TabsRevision != third.TabsRevision || fourth.TabsChanged { - t.Fatalf("重复 Snapshot 不应重复报告 Tab 变化: third=%+v fourth=%+v", third, fourth) + if fourth.TabsRevision != stable.TabsRevision || fourth.TabsChanged { + t.Fatalf("Tab ready 后重复 Snapshot 不应重复报告变化: stable=%+v fourth=%+v", stable, fourth) + } +} + +func activeTabStatus(snapshot SnapshotResponse) string { + for _, tab := range snapshot.Tabs { + if tab.PageID == snapshot.ActivePageID { + return tab.Status + } } + return "" } func TestBrowserInteractionReleasesLockBeforeTabReconcile(t *testing.T) { From 5bb9dc29d61b7756ebf7ae6b41a63f66563dc401 Mon Sep 17 00:00:00 2001 From: bestmountain Date: Tue, 22 Sep 2026 14:42:56 +0800 Subject: [PATCH 4/4] docs: show realistic embedded browser preview --- README.md | 33 +----- README.zh-CN.md | 33 +----- docs/images/browser-embedded.svg | 166 +++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 62 deletions(-) create mode 100644 docs/images/browser-embedded.svg diff --git a/README.md b/README.md index aad8ba5..7206739 100644 --- a/README.md +++ b/README.md @@ -22,39 +22,10 @@ The browser can run locally, on a remote server, or as an existing browser with ## Embedded experience
- - - - - - - - - - - - - - - - - - -
◉ BrowserKit Agent Workspace● LIVE
← ↻https://example.com/workspaceTake control
-

Live browser surface

-

Rendered by Chrome / Chromium and streamed over WebSocket.

-
The agent is inspecting the page and interacting with a target…
-

Mouse Touch Keyboard IME

-
- Diagnostics

- Console Network

- GET /api/items 200
- DOMContentLoaded
- 0 errors -
Tab: Workspace1920 × 1080 Controller connected
+ BrowserView embedded in an application, with tabs, address bar, live page, and the F12 Network panel
-This repository-native HTML mockup represents the embedded layout. The actual `BrowserView` renders the live browser stream, tabs, address bar, input controls, and diagnostics directly where it is mounted. +`BrowserView` renders the live browser stream, tabs, address bar, input controls, and diagnostics directly inside the host application's container. diff --git a/README.zh-CN.md b/README.zh-CN.md index da75729..524dbca 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,39 +21,10 @@ Agent 可通过 CLI 或 Go 函数读取页面、操作元素和管理 Tab;用 ## 嵌入效果
- - - - - - - - - - - - - - - - - - -
◉ BrowserKit Agent Workspace● LIVE
← ↻https://example.com/workspace接管操作
-

浏览器实时画面

-

由 Chrome / Chromium 渲染,通过 WebSocket 实时传输。

-
Agent 正在检查页面内容并操作目标元素……
-

鼠标 触摸 键盘 中文输入法

-
- 开发诊断

- Console Network

- GET /api/items 200
- DOMContentLoaded
- 0 errors -
Tab:Workspace1920 × 1080 控制方已连接
+ BrowserView 嵌入应用后的效果,包含 Tab、地址栏、实时页面与 F12 Network 面板
-这是基于 README 原生 HTML 的界面示意;实际 `BrowserView` 会在调用位置嵌入浏览器图流、Tab、地址栏、输入控制与诊断面板。 +`BrowserView` 会直接在宿主应用的容器内渲染浏览器图流、Tab、地址栏、输入控制与诊断面板。 diff --git a/docs/images/browser-embedded.svg b/docs/images/browser-embedded.svg new file mode 100644 index 0000000..364fc06 --- /dev/null +++ b/docs/images/browser-embedded.svg @@ -0,0 +1,166 @@ + + BrowserKit embedded browser interface + A BrowserView styled like the real component, with tabs, an address bar, a live page, and the developer tools panel below it. + + + + + + + + + + + + + + + + + + + + + + Fieldnotes · Workspace + + + + Project details + + + + + + + + + + http://127.0.0.1:8877/site/ + + + + + F12 + + + + + + + + ◈ Fieldnotes + Workspace + Projects / 2026 + + INPUT AND NAVIGATION + Work notes + Add a note, send a request, or open project details. + + + New note + Note + + Enter a note + + Save note + + Test request + Request succeeded + + + Projects + + Website redesign + Done + + + Mobile layout + Done + + + API testing + In progress + + Open project details ↗ + + + + + + + + + + + Developer tools + Console + Network + + Resources + Collecting + + Stop + + + + + + All + + Fetch/XHR + + Document + + Script + + Filter requests + + + + Name + Status + Method + Type + Time + + + + status + 200 + GET + fetch + 18 ms + + + + index.html + 200 + GET + document + 11 ms + + + + favicon.ico + 200 + GET + image + 3 ms + + + + 3 requests + 0 errors + + + +