---
name: shared-chrome
description: 在 Docker 容器中快速部署一个 Agent（CDP）与人类用户（网页远程桌面）可实时共享、共同操作的持久化 Google Chrome 浏览器。当用户提到"部署共享浏览器"、"容器里跑带界面的 Chrome"、"Agent 和人同时操作浏览器"、"需要人工登录/过验证码/2FA 的浏览器自动化"、"CDP + 远程桌面 Chrome"、"持久化登录态浏览器容器"时使用。
---

# Shared Chrome — 人机共享持久化容器浏览器

在 Docker 中部署一个带完整 Wayland 桌面（Selkies HTML5 远程桌面）与 CDP 调试接口的持久化 Chrome 实例：

- **人类用户**：通过浏览器打开网页远程桌面（HTTPS `:3001` / HTTP `:3000`），实时看到画面并操作键鼠、登录账号、处理验证码或 2FA
- **Agent**：通过宿主机 `http://127.0.0.1:9222`（CDP）用 `agent-browser` / Playwright / Puppeteer 操作同一浏览器会话、共享全部 Cookie 与登录态
- **持久化**：Profile、Cookie、缓存落盘到 `./config`，容器重建后登录态不丢失

---

## 第一步：检查环境与确认参数

动手前先快速检查以下信息，缺失或冲突时向用户确认：

1. **宿主机架构**：运行 `uname -m`
   - `x86_64`（amd64）：可构建官方 Google Chrome 镜像（支持 Google 账号同步、风控与兼容性更好）
   - `aarch64`（arm64）：Google 官方无 Linux ARM64 `.deb` 包，跳过构建，直接用 `linuxserver/chromium:latest` 自带的 `chromium`
2. **部署目录与端口**：
   - 默认目录：`./browser`（若用户指定了路径则用用户路径）
   - 默认端口：Web 远程桌面 `127.0.0.1:3000`（HTTP，供反代）+ `3001:3001`（HTTPS 自签证书，局域网/直连），CDP `127.0.0.1:9222`
   - 先用 `ss -tln` 或 `lsof` 检查 `3000`、`3001`、`9222` 是否被占用，冲突则递增端口
3. **登录凭据与代理**：
   - 网页远程桌面默认开启 Basic Auth（`BROWSER_USER` / `BROWSER_PASSWORD`），未指定时生成强随机密码写入 `.env`
   - 若宿主机访问 Google / 下载 `google-chrome.deb` 需要代理，询问或读取代理地址（供构建下载及 `CHROME_CLI --proxy-server=...` 使用）
   - 运行用户：`PUID`/`PGID` 默认 `1000:1000`（若当前非 root 用户则用 `id -u` / `id -g`）

---

## 第二步：创建目录与核心文件

按下面结构在目标目录（如 `./browser`）生成文件：

```text
browser/
├── .env                          # Basic Auth 账号密码 (chmod 600)
├── docker-compose.yml            # 容器编排（bridge 网络 + shm_size 2gb）
├── chrome/                       # [仅 x86_64 官方 Chrome]
│   ├── Dockerfile
│   └── google-chrome.deb
├── services/
│   └── cdp-forward               # [必须 +x] 0.0.0.0:9223 -> 127.0.0.1:9222 转发
└── config/.config/labwc/
    └── autostart                 # [必须 +x] 独立 profile + 崩溃/关闭 3s 自动重启
```

### 1. 下载官方 Chrome 安装包（仅 `x86_64`）与 `chrome/Dockerfile`

若 Docker Hub 拉取慢可改用镜像前缀（如 `docker.1panel.live/linuxserver/chromium:latest`），避免用经常超时的 `lscr.io`。

```bash
mkdir -p chrome services config/.config/labwc
curl -fSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o chrome/google-chrome.deb
```

`chrome/Dockerfile`：

```dockerfile
FROM linuxserver/chromium:latest
COPY google-chrome.deb /tmp/
RUN rm -f /etc/apt/sources.list.d/* \
    && sed -i "s#deb.debian.org#mirrors.aliyun.com#g" /etc/apt/sources.list \
    && sed -i "/^deb-src/d; /security/d" /etc/apt/sources.list \
    && apt-get update \
    && apt-get install -y --no-install-recommends /tmp/google-chrome.deb \
    && rm -rf /tmp/google-chrome.deb /var/lib/apt/lists/* /etc/apt/sources.list.d/google-chrome.*
```

> 若服务器在海外无需阿里云源，可去掉两行 `sed`。`rm -f /etc/apt/sources.list.d/*` 是为了清理基础镜像里可能卡住 `apt-get update` 的第三方源。若为 `arm64`，跳过此步。

### 2. `services/cdp-forward`（必须 `chmod +x`）

**关键机制**：Chrome 有界面模式下 CDP 固定只监听容器内 `127.0.0.1:9222`，Docker 端口映射无法直接转出。利用基础镜像自带的 `/lsiopy/bin/python3` 起一个 s6 自定义常驻服务，将 `0.0.0.0:9223` 转发到 `127.0.0.1:9222`：

```python
#!/lsiopy/bin/python3
import asyncio

async def pipe(r, w):
    try:
        while data := await r.read(65536):
            w.write(data)
            await w.drain()
    finally:
        w.close()

async def handle(cr, cw):
    try:
        ur, uw = await asyncio.open_connection("127.0.0.1", 9222)
    except OSError:
        cw.close()
        return
    await asyncio.gather(pipe(cr, uw), pipe(ur, cw), return_exceptions=True)

async def main():
    server = await asyncio.start_server(handle, "0.0.0.0", 9223)
    await server.serve_forever()

asyncio.run(main())
```

### 3. `config/.config/labwc/autostart`（必须 `chmod +x`）

**关键机制**：
1. Chrome 136+ 禁止对默认配置目录启用 `--remote-debugging-port`，必须显式指定独立 `--user-data-dir=$HOME/.config/chrome-profile`。
2. 镜像默认只在启动时拉起一次浏览器，用户误关窗口后桌面会变空；`while true` 循环在退出 3 秒后自动重开并清理残留 `Singleton*` 锁。

```bash
#!/bin/bash
PROFILE=$HOME/.config/chrome-profile
BIN=google-chrome   # 若 arm64 直接使用基础镜像，改为 chromium
while true; do
  pgrep -x chrome >/dev/null || pgrep -x chromium >/dev/null || rm -f "$PROFILE"/Singleton*
  "$BIN" \
    --no-sandbox \
    --test-type \
    --password-store=basic \
    --start-maximized \
    --ozone-platform=wayland \
    --user-data-dir="$PROFILE" \
    ${CHROME_CLI} > /dev/null 2>&1
  sleep 3
done &
```

### 4. `.env`（`chmod 600`）与 `docker-compose.yml`

`.env`：

```env
BROWSER_USER=admin
BROWSER_PASSWORD=<生成强随机密码>
```

`docker-compose.yml`（`x86_64` 用 `build: ./chrome` + `image: shared-chrome:latest`；`arm64` 直接 `image: linuxserver/chromium:latest`）：

```yaml
services:
  chromium:
    build: ./chrome
    image: shared-chrome:latest
    container_name: shared-chrome
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - LC_ALL=zh_CN.UTF-8
      - CUSTOM_USER=${BROWSER_USER}
      - PASSWORD=${BROWSER_PASSWORD}
      - CHROME_CLI=--remote-debugging-port=9222 --remote-allow-origins=* --lang=zh-CN
      # 若需浏览器走代理，追加：
      # --proxy-server=http://<proxy-host>:<port> --proxy-bypass-list=<local>;127.*;10.*;172.16.*;192.168.*
    volumes:
      - ./config:/config
      - ./services:/custom-services.d:ro
    ports:
      - 127.0.0.1:3000:3000
      - 3001:3001
      - 127.0.0.1:9222:9223
    shm_size: "2gb"
    restart: unless-stopped
```

**安全与架构红线**：
- **严禁 `network_mode: host`**：容器内 Wayland/X `:1` 抽象 unix socket 会与宿主机桌面环境冲突，导致 PulseAudio/Selkies 卡死。
- **CDP 端口严禁暴露到 `0.0.0.0`**：必须绑定 `127.0.0.1:9222:9223`（CDP 无鉴权，可读写 Cookie、执行任意 JS）。
- **`shm_size: "2gb"` 不可省**：Docker 默认 64MB `/dev/shm` 会导致 Chrome 标签页频繁崩溃。

---

## 第三步：设权限、启动与验收

```bash
chmod +x services/cdp-forward config/.config/labwc/autostart
chmod 600 .env
# 确保挂载目录属主与 PUID:PGID 一致，否则容器内写 profile 会报权限错误
chown -R 1000:1000 config

docker compose up -d --build
```

等待约 5~10 秒后验收：

```bash
# 1. 验证 CDP 就绪
curl -fsS http://127.0.0.1:9222/json/version

# 2. 验证远程桌面端口监听
curl -skI https://127.0.0.1:3001 | head -n 3
```

验收成功后向用户输出：
- 远程桌面控制地址：`https://<服务器IP>:3001`（及只读观看地址 `https://<服务器IP>:3001/#shared`）
- Basic Auth 账号密码（来自 `.env`）
- 宿主机 CDP 地址：`http://127.0.0.1:9222`

---

## 第四步：Agent 与用户协同操作规范

### 人类用户端（Selkies 远程桌面）
- **控制端**：直接打开 URL（不带 hash），同一时刻只允许一个控制端，新接入控制端会接管并踢掉上一个控制端
- **只读观众（多人围观）**：打开 `URL/#shared`，可无限多人同时观看，输入事件会被丢弃，不抢控制权
- **人工登录落盘**：人工登录或过完验证码后，**等待约 30 秒再重启容器**，确保 Chrome 把 Cookie 刷回磁盘

### Agent 端（CDP 连接）
- **agent-browser**：在 `~/.agent-browser/config.json` 写入 `{"cdp": "9222"}`（或每次传 `--cdp 9222`）；结束会话用 `agent-browser close` 只断开连接，不会关闭 Chrome
- **Playwright**：
  ```js
  const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
  const context = browser.contexts()[0]; // 必须取 contexts()[0] 才能复用持久化登录态！
  ```
- **不要固定/修改共享实例的视口大小**：禁止在共享实例上执行 `agent-browser set viewport` 或 `setViewportSize`，否则页面尺寸与远程桌面窗口不一致会导致底部/右侧按钮被裁切；保持随窗口自适应。若误设，执行 `agent-browser close` 断开该会话即可清除模拟，无需重启容器
- **只保留单个 Profile**：不要在 Chrome 里新建第二个用户配置（Profile 2），否则 CDP 新开标签页会落到未登录的 Profile 上
- **人机接力流程**：自动化遇到登录、扫码、短信 2FA 或强风控验证码时，Agent 停留在当前页并提示用户打开远程桌面操作，待用户确认完成后直接在同一页面/上下文继续执行
