无 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 示例:
sudo apt-get update
sudo apt-get install -y \
xvfb x11vnc openbox websockify novnc \
dbus-x11 x11-utils curl wget unzip ca-certificates
Chrome 二选一:
# 优先: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
检查:
which google-chrome || which chromium || which chromium-browser
which Xvfb
which websockify
2. 启动虚拟桌面 DISPLAY=:99
export DISPLAY=:99
# 若 :99 已占用,改成 :100 并同步修改后续 DISPLAY
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
# 启动窗口管理器,否则 Chrome 窗口可能无法正常移动/聚焦
openbox >/tmp/openbox.log 2>&1 &
# 可选:设置背景,帮助确认桌面已启动
xsetroot -solid '#202020' || true
验证:
echo $DISPLAY # :99
xdpyinfo -display :99 | grep dimensions
3. 提供 Web 访问虚拟桌面(noVNC)
3.1 启动 VNC 到 Xvfb
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 路径不同,常见路径:
# Debian/Ubuntu 常见
websockify --web=/usr/share/novnc 6080 localhost:5900 >/tmp/novnc.log 2>&1 &
# 若路径不存在,查找:
dpkg -L novnc | grep 'vnc.html$'
访问:
http://<server-ip>:6080/vnc.html
更安全的 SSH 隧道方式:
ssh -L 6080:127.0.0.1:6080 user@server
# 本地浏览器打开 http://127.0.0.1:6080/vnc.html
验收:浏览器里能看到一个空桌面,后续启动 Chrome 后能看到 Chrome 窗口,并可鼠标键盘操作。
4. 启动 GUI Chrome(不是 headless)
准备独立用户目录,便于保存扩展和登录态:
mkdir -p /tmp/chrome_profile
export DISPLAY=:99
启动:
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。
验证:
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。
# 示例:把扩展放到 /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 手动安装扩展。
- 在 noVNC 页面里操作 Chrome。
- 地址栏打开:
chrome://extensions/ - 右上角打开 Developer mode / 开发者模式。
- 点击 Load unpacked / 加载已解压的扩展程序。
- 在文件选择器中选择 CDP Bridge 扩展目录,例如:
必须选择含/opt/tmwd_cdp_bridgemanifest.json的目录。 - 扩展出现后,确认没有红色错误。必要时点 Details / 详细信息,允许所需权限。
- 如扩展需要保持 service worker 活跃,至少保留一个普通页面标签页,例如
about:blank或目标网页。
安装后验证:
# 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 时加:
--load-extension=/opt/tmwd_cdp_bridge
完整示例:
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 能力验证
curl -s http://127.0.0.1:9222/json/list | python3 -m json.tool
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)
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)
# 通过 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 截图)
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 操作
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 或其他错误时:
- 查监听:
ss -ltnp | grep 18765 - 查连接:
ss -tnp | grep 18765 - 查 Chrome 调试页:
curl http://127.0.0.1:9222/json/list - 查 service worker:列表中应有
chrome-extension://.../background.js - 只有上述均异常,才考虑重启扩展或 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