# 无 GUI Linux 服务器浏览器能力解锁 SOP（Xvfb + Web VNC + Chrome + 手动安装 CDP Bridge）

**适用场景**：云服务器、容器、SSH 远程主机等**不带桌面 GUI 的 Linux**。目标是在服务器上提供一个可通过浏览器访问的虚拟桌面，在里面运行有界面的 Chrome，手动安装 `tmwd_cdp_bridge` 扩展，再通过 Chrome DevTools Protocol（CDP）/ TMWebDriver 解锁完整浏览器自动化能力。

**核心理念**：无 GUI Linux → Xvfb 虚拟显示器 → 轻量窗口管理器 → noVNC/Websockify 提供 Web 访问 → GUI Chrome → 手动安装 CDP Bridge 扩展 → CDP/TMWebDriver 自动化。

> 重点：这里不是 Chrome headless 模式。因为 CDP Bridge 扩展需要在 GUI Chrome 的扩展页中手动安装/确认，所以必须先把“看得见、点得到”的 Chrome GUI 暴露到 Web。

---

## 0. 架构与已验证结论

### 架构
```
Xvfb虚拟屏幕 (:99)
  ↓
Chrome (--remote-debugging-port=9222, --load-extension=tmwd_cdp_bridge)
  ↓
CDP HTTP接口 (127.0.0.1:9222/json/*)
  ↓
tmwd_cdp_bridge扩展 (WS监听 127.0.0.1:18765)
  ↓
TMWebDriver Python类 (会话管理 + JS注入)
```

### 已验证坑点
- `TMWebDriver.get_all_sessions()` 可能返回空，但 Chrome 与扩展仍可能已通过 WS 连上；**不能只凭会话表为空判失败**。
- Chrome DevTools `/json/new?...` 创建新标签页时，当前环境已验证 **GET 会返回 405**，应使用 **PUT**。
- `chrome://extensions/?errors=...` 里的 `ERR_CONNECTION_REFUSED` 可能是历史残留；需与端口监听、连接状态、service worker 状态交叉确认。

---

## 1. 安装系统组件

Ubuntu/Debian 示例：

```bash
sudo apt-get update
sudo apt-get install -y \
  xvfb x11vnc openbox websockify novnc \
  dbus-x11 x11-utils curl wget unzip ca-certificates
```

Chrome 二选一：

```bash
# 优先：Google Chrome
wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt-get install -y ./google-chrome-stable_current_amd64.deb

# 或：Chromium
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium
```

检查：

```bash
which google-chrome || which chromium || which chromium-browser
which Xvfb
which websockify
```

---

## 2. 启动虚拟桌面 DISPLAY=:99

```bash
export DISPLAY=:99

# 若 :99 已占用，改成 :100 并同步修改后续 DISPLAY
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &

# 启动窗口管理器，否则 Chrome 窗口可能无法正常移动/聚焦
openbox >/tmp/openbox.log 2>&1 &

# 可选：设置背景，帮助确认桌面已启动
xsetroot -solid '#202020' || true
```

验证：

```bash
echo $DISPLAY                  # :99
xdpyinfo -display :99 | grep dimensions
```

---

## 3. 提供 Web 访问虚拟桌面（noVNC）

### 3.1 启动 VNC 到 Xvfb

```bash
export DISPLAY=:99
x11vnc -display :99 -forever -shared -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 &
```

> 安全提醒：`-nopw` 只适合本机隧道或临时内网环境。公网服务器请加 `-localhost`，再用 SSH 隧道访问，或为 noVNC 加反向代理鉴权。

### 3.2 启动 noVNC/Websockify

不同发行版 noVNC 路径不同，常见路径：

```bash
# Debian/Ubuntu 常见
websockify --web=/usr/share/novnc 6080 localhost:5900 >/tmp/novnc.log 2>&1 &

# 若路径不存在，查找：
dpkg -L novnc | grep 'vnc.html$'
```

访问：

```text
http://<server-ip>:6080/vnc.html
```

更安全的 SSH 隧道方式：

```bash
ssh -L 6080:127.0.0.1:6080 user@server
# 本地浏览器打开 http://127.0.0.1:6080/vnc.html
```

验收：浏览器里能看到一个空桌面，后续启动 Chrome 后能看到 Chrome 窗口，并可鼠标键盘操作。

---

## 4. 启动 GUI Chrome（不是 headless）

准备独立用户目录，便于保存扩展和登录态：

```bash
mkdir -p /tmp/chrome_profile
export DISPLAY=:99
```

启动：

```bash
google-chrome \
  --remote-debugging-address=127.0.0.1 \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/chrome_profile \
  --no-first-run \
  --no-default-browser-check \
  --disable-dev-shm-usage \
  --disable-gpu \
  --no-sandbox \
  about:blank >/tmp/chrome.log 2>&1 &
```

如果命令是 Chromium，请替换成实际二进制：`chromium` 或 `chromium-browser`。

验证：

```bash
curl -s http://127.0.0.1:9222/json/version | python3 -m json.tool
curl -s http://127.0.0.1:9222/json/list | python3 -m json.tool
```

在 noVNC 页面中应能看到 Chrome GUI。

---

## 5. 准备 CDP Bridge 扩展目录

扩展必须是 Chrome 可加载的**解压目录**，目录内应有 `manifest.json`。

```bash
# 示例：把扩展放到 /opt/tmwd_cdp_bridge
sudo mkdir -p /opt/tmwd_cdp_bridge
sudo chown -R $USER:$USER /opt/tmwd_cdp_bridge

# 如果已有 zip 包：
unzip tmwd_cdp_bridge.zip -d /opt/tmwd_cdp_bridge

# 验证：
test -f /opt/tmwd_cdp_bridge/manifest.json && echo OK
```

如果解压后多了一层目录，例如 `/opt/tmwd_cdp_bridge/tmwd_cdp_bridge/manifest.json`，加载时要选择包含 `manifest.json` 的那一层。

---

## 6. 在 Web GUI Chrome 中手动安装 CDP Bridge

这是本 SOP 的关键步骤：**通过 noVNC 打开的 Chrome GUI 手动安装扩展**。

1. 在 noVNC 页面里操作 Chrome。
2. 地址栏打开：
   ```text
   chrome://extensions/
   ```
3. 右上角打开 **Developer mode / 开发者模式**。
4. 点击 **Load unpacked / 加载已解压的扩展程序**。
5. 在文件选择器中选择 CDP Bridge 扩展目录，例如：
   ```text
   /opt/tmwd_cdp_bridge
   ```
   必须选择含 `manifest.json` 的目录。
6. 扩展出现后，确认没有红色错误。必要时点 **Details / 详细信息**，允许所需权限。
7. 如扩展需要保持 service worker 活跃，至少保留一个普通页面标签页，例如 `about:blank` 或目标网页。

安装后验证：

```bash
# CDP 列表里应能看到扩展 service_worker/background
curl -s http://127.0.0.1:9222/json/list | grep -E 'chrome-extension://|service_worker|background' || true

# CDP Bridge 常见监听端口
ss -ltnp | grep 18765 || true
ss -tnp  | grep 18765 || true
```

判定：
- `/json/list` 有 `type: service_worker` 且 URL 为 `chrome-extension://.../background...`：扩展已被 Chrome 加载。
- `127.0.0.1:18765` 有监听：bridge 后端/扩展通信端口就绪。
- 有 `ESTAB` 连接：Chrome 与 bridge 通信通常已通。

---

## 7. 可选：用命令行预加载扩展，但仍以手动确认为准

有时可在启动 Chrome 时加：

```bash
--load-extension=/opt/tmwd_cdp_bridge
```

完整示例：

```bash
google-chrome \
  --remote-debugging-address=127.0.0.1 \
  --remote-debugging-port=9222 \
  --load-extension=/opt/tmwd_cdp_bridge \
  --user-data-dir=/tmp/chrome_profile \
  --no-first-run --no-default-browser-check \
  --disable-dev-shm-usage --disable-gpu --no-sandbox \
  about:blank &
```

但不要只依赖它。实际验收仍应通过 Web GUI 打开 `chrome://extensions/`，确认扩展已加载、无错误、权限已允许。

---

## 8. CDP/TMWebDriver 能力验证

```bash
curl -s http://127.0.0.1:9222/json/list | python3 -m json.tool
```

Python 验证：
```python
from TMWebDriver import TMWebDriver
wd = TMWebDriver()
sessions = wd.get_all_sessions()
print("Sessions:", sessions)

# 注意：sessions 为空不代表桥断开；继续用 CDP /json/list、service_worker、18765 端口交叉确认
```

---

## 9. 浏览器能力解锁

### A. 搜索任务（优先 Google）

```python
import urllib.request, urllib.parse, json, time

# 方案1：TMWebDriver 会话可用时
from TMWebDriver import TMWebDriver
wd = TMWebDriver()
sessions = wd.get_all_sessions()
if sessions:
    wd.jump("https://www.google.com/search?q=" + urllib.parse.quote("广州天气"))
else:
    # 方案2：CDP 兜底（会话为空时）
    target = "https://www.google.com/search?q=" + urllib.parse.quote("广州天气")
    req = urllib.request.Request(
        "http://127.0.0.1:9222/json/new?" + urllib.parse.quote(target, safe=":/?=&"),
        method="PUT",  # 必须用 PUT，GET 会 405
    )
    result = urllib.request.urlopen(req, timeout=10).read().decode()
    print("Created tab:", result)
    
    # 验收：检查标签页是否已打开
    time.sleep(2)
    tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json/list", timeout=10).read())
    for t in tabs:
        if t.get("type") == "page" and "广州天气" in t.get("title", ""):
            print("✓ 搜索页已打开:", t.get("title"))
```

### B. 文件上传（CDP batch）

```python
# 通过 web_execute_js 调用 tmwd_cdp_bridge 扩展
script = {
    "cmd": "batch",
    "commands": [
        {"cmd": "cdp", "method": "DOM.getDocument", "params": {"depth": 1}},
        {"cmd": "cdp", "method": "DOM.querySelector", "params": {
            "nodeId": "$0.root.nodeId",
            "selector": "input[type=file]"
        }},
        {"cmd": "cdp", "method": "DOM.setFileInputFiles", "params": {
            "nodeId": "$1.nodeId",
            "files": ["/absolute/path/to/file.pdf"]
        }}
    ]
}

# 使用 web_execute_js 工具执行
# web_execute_js(script=json.dumps(script))
```

### C. 验证码识别（CDP 截图）

```python
script = {
    "cmd": "cdp",
    "method": "Page.captureScreenshot",
    "params": {"format": "png"}
}

# 返回 base64 编码的 PNG 图片
# result = web_execute_js(script=json.dumps(script))
# image_data = result['data']  # base64 字符串
```

### D. 跨域 iframe 操作

```python
script = {
    "cmd": "batch",
    "commands": [
        {"cmd": "cdp", "method": "Page.getFrameTree"},
        {"cmd": "cdp", "method": "Page.createIsolatedWorld", "params": {
            "frameId": "$0.frameTree.childFrames[0].frame.id"
        }},
        {"cmd": "cdp", "method": "Runtime.evaluate", "params": {
            "expression": "document.querySelector('button').click()",
            "contextId": "$1.executionContextId"
        }}
    ]
}
```

---

## 10. 扩展报错排查顺序

当遇到 `ERR_CONNECTION_REFUSED` 或其他错误时：

1. **查监听**：`ss -ltnp | grep 18765`
2. **查连接**：`ss -tnp | grep 18765`
3. **查 Chrome 调试页**：`curl http://127.0.0.1:9222/json/list`
4. **查 service worker**：列表中应有 `chrome-extension://.../background.js`
5. **只有上述均异常**，才考虑重启扩展或 Chrome

---

## 11. 禁忌

- ❌ 禁止把 `/json/new` 的 405 当作网络错误；先换 PUT
- ❌ 禁止只凭 `get_all_sessions()==[]` 判定桥断开
- ❌ 禁止只凭 `chrome://extensions/?errors=...` 的旧错误页判定当前失败
- ❌ 禁止无新信息重复重启；每次重启前至少确认端口/连接/worker 三者之一异常
- ❌ 禁止在 Windows 环境使用 Xvfb（Windows 用 `--headless=new` 或物理显示器）

---

## 12. 验收标准

- ✅ Xvfb 进程存在，`$DISPLAY` 正确
- ✅ Chrome 进程存在，`--remote-debugging-port=9222` 参数在
- ✅ `/json/list` 能看到 page 和 service_worker
- ✅ `127.0.0.1:18765` 有监听和 ESTAB 连接
- ✅ 搜索任务：标题形如 `关键词 - Google 搜索`
- ✅ 文件上传：input 元素 `files` 属性已填充
- ✅ 验证码识别：返回有效的 base64 图片数据

---

## 13. 常见问题

### Q1: Chrome 启动后立即退出
A: 检查 `--user-data-dir` 是否有写权限，或改用 `/tmp/chrome_profile_$(date +%s)`

### Q2: 扩展未加载
A: 确认 `--load-extension` 路径是绝对路径，且目录下有 `manifest.json`

### Q3: CDP 连不上
A: 检查防火墙是否拦截 9222 端口，或 Chrome 是否用了 `--remote-debugging-pipe`（与 `--remote-debugging-port` 冲突）

### Q4: TMWebDriver 会话一直为空
A: 正常现象，改用 CDP `/json/new` + PUT 兜底即可

### Q5: Xvfb 显示 "Fatal server error"
A: 检查 `:99` 显示号是否被占用，改用 `:100` 或 `Xvfb :99 -nolisten tcp`