在 AIStudio 镜像中心使用 Dockerfile 自助构建最新版 vLLM 镜像在 AIStudio 镜像中心使用 Dockerfile 自助构建最新版 vLLM 镜像 ,无需访问 DockerHub立即构建
Skip to content
回到全部文章

用 agent-browser 驱动 Chrome CDP:在平台容器中启用 Agent 浏览器自动化

Chrome DevTools Protocol(CDP)允许您通过 HTTP / WebSocket 远程控制 Chrome:打开网页、执行 JavaScript、模拟键盘鼠标、抓取 DOM、生成截图等。在「仅 SSH 可达」的平台容器环境里,CDP + SSH 端口转发可以让本地工具/Agent 安全地操作远端浏览器。如果您希望让 Agent 更快、更稳地完成真实网页交互,推荐搭配 agent-browser:它提供「snapshot(可访问性树)+ refs(稳定引用)」的工作流,让 Agent 几乎不需要手写选择器或 CDP 细节,就能执行点击、输入与截图。

您将完成什么

  • 在容器里安装 google-chrome-stable(Deb 包,不依赖 Snap)
  • 安装 CJK 字体,确保截图中文不变成「豆腐」方块
  • tmux 中启动 headless Chrome,并启用 CDP(默认端口 9222
  • 通过 SSH 端口转发,在本地访问 http://127.0.0.1:9222/json/version
  • 用 CDP 验证「可操作」:读标题、连接 WebSocket 执行 CDP 命令并截图
  • (可选)安装 Node.js 并使用 agent-browser 连接 CDP,完成 snapshot/点击/截图(更适合 Agent)

适用场景

  • Agent 需要可编程浏览器:表单提交、搜索、点击按钮、执行 JS、抓取页面内容
  • 远端网络/环境限制:浏览器必须运行在容器里,但控制端在本地
  • 需要稳定截图:对页面渲染(含中文)有要求,用于报告/回归测试/采集
  • 希望用 CLI 快速落地:用 agent-browser(snapshot + refs)减少选择器/脚本维护成本

开始之前(安全与约束)

警告

不要暴露 CDP 到公网/内网

CDP 等价于「远程控制浏览器」。请务必只监听 127.0.0.1,并通过 SSH 端口转发访问;不要让 9222 直接监听 0.0.0.0

警告

root 运行 Chrome 需要 --no-sandbox

平台容器中默认以 root 用户运行。Chrome 在 Linux 上会拒绝以 root + sandbox 模式启动,因此需要 --no-sandbox。 这会降低安全性:请避免访问不可信网站/打开不可信文件。若条件允许,优先使用非 root 用户运行 Chrome。

前提条件

  • 一个可 SSH 登录的 Ubuntu 容器(本文以 Ubuntu 22.04 为例)
  • 能访问外网下载 Chrome .deb
  • 容器内具备 tmux(建议),以及 python3(用于验证脚本)

Step 1 安装最小依赖与中文字体

注意

  • 您不需要安装桌面环境(XFCE4/Xorg/VNC)就能使用 CDP。本文只依赖 headless Chrome。
  • 考虑到截图可能包含中文,因此必须安装中文字体。

在容器中执行:

bash
apt-get update && apt-get install -y --no-install-recommends \
  ca-certificates \
  wget \
  fontconfig \
  fonts-liberation \
  fonts-noto-cjk \
  tmux \
  python3

# 刷新字体缓存(避免截图仍然缺字)
fc-cache -fv

验证字体(应能看到 Noto Sans CJK):

bash
fc-list | grep -i "Noto Sans CJK" | head -n 5

Step 2 安装 Google Chrome(Deb 包)

注意

Ubuntu 22.04 上 chromium-browser 常常来自 Snap;在受限容器里 Snap/squashfs 可能不可用。 因此这里直接安装官方 Deb 的 google-chrome-stable

bash
wget -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dpkg -i /tmp/chrome.deb || apt-get install -y -f
rm -f /tmp/chrome.deb

google-chrome-stable --version

Step 3 在 tmux 中启动 headless Chrome + CDP

CDP 默认端口使用 9222。建议把关键路径写到一个 info 文件,方便排查和清理。

bash
PORT=9222
INFO=/tmp/chrome-cdp-$PORT.info
LOG=/tmp/chrome-cdp-$PORT.log

# 进入 tmux(若已在 tmux 里,可跳过)
tmux new -s chrome-cdp-$PORT

# 在 tmux 里执行下面命令(建议复制整段)
DIR=$(mktemp -d /tmp/chrome-cdp-profile.$PORT.XXXXXX)
printf 'port=%s\nprofile_dir=%s\nlog=%s\n' "$PORT" "$DIR" "$LOG" | tee "$INFO"

google-chrome-stable \
  --headless=new \
  --no-sandbox \
  --disable-gpu \
  --disable-dev-shm-usage \
  --no-first-run \
  --no-default-browser-check \
  --remote-debugging-address=127.0.0.1 \
  --remote-debugging-port="$PORT" \
  --user-data-dir="$DIR" \
  about:blank \
  2>&1 | tee -a "$LOG"

提示

要让 Chrome 在后台继续运行:在 tmux 中按 Ctrl-b 再按 d(detach)。 以后随时用 tmux attach -t chrome-cdp-9222 回来查看日志/停止服务。

注意

Headless Chrome 不会在 VNC/桌面里出现窗口,这是预期行为。

Step 4 通过 SSH 端口转发访问 CDP(本地控制端)

如果您的 Agent/工具运行在本地电脑,需要先建立隧道:

bash
ssh -N -L 9222:localhost:9222 -p <SSH_PORT> root@guangdong-b-is.cloud.infini-ai.com

然后在本地打开:

  • http://127.0.0.1:9222/json/version

注意

http://127.0.0.1:9222/ 显示空白页面通常是正常的;关键是 json/versionjson/list 是否可访问。

Step 5 验证 CDP「可操作」

Chrome 新版本对 json/newjson/close 这类「有副作用」的接口要求使用 PUT。 如果您用 GET 访问并收到 HTTP 405 Method Not Allowed,请改用 PUT。

本步骤提供两个独立示例:

  • 示例 1:打开网页并读取标题(只用 HTTP 接口,最小依赖)
  • 示例 2:连接 DevTools WebSocket 执行 CDP 命令并截图(更接近 Agent 的真实用法)

示例 1:打开网页并读取标题

本地或容器内执行下面脚本(只要能访问 http://127.0.0.1:9222 即可):

bash
python3 - <<'PY'
import json, time, urllib.parse, urllib.request

BASE = "http://127.0.0.1:9222"
URL = "https://example.com/"

# 1) 新建一个 tab(PUT /json/new)
req = urllib.request.Request(
    BASE + "/json/new?" + urllib.parse.quote(URL, safe=":/?&=#"),
    method="PUT",
)
created = json.load(urllib.request.urlopen(req, timeout=10))
tid = created["id"]
print("created id:", tid)

# 2) 轮询 /json/list,直到页面 title 可用
for _ in range(80):
    targets = json.load(urllib.request.urlopen(BASE + "/json/list", timeout=10))
    t = next((x for x in targets if x.get("id") == tid), None)
    if t and t.get("title"):
        print("url:", t.get("url"))
        print("title:", t.get("title"))
        break
    time.sleep(0.25)
else:
    raise SystemExit("page did not finish loading in time")

# 3) 关闭 tab(PUT /json/close/<id>)
urllib.request.urlopen(
    urllib.request.Request(BASE + "/json/close/" + tid, method="PUT"),
    timeout=10,
)
print("closed")
PY

如果您看到 title: Example Domain,就说明 CDP 可用且能驱动浏览器访问网页

示例 2:用 CDP 打开网页并截图(保存到脚本所在机器)

注意

截图会保存在哪里?

CDP 的 Page.captureScreenshot 会把 PNG 以 base64 数据返回给客户端。因此 截图保存的位置取决于脚本运行的位置

  • 在本地运行脚本(通过 SSH 端口转发访问 CDP):PNG 保存在本地。
  • 在 SSH 主机/容器内运行脚本:PNG 保存在远端(例如 /tmp)。

注意

WebSocket 连接返回 403 Forbidden

新版 Chrome 会校验 DevTools WebSocket 的 Origin 头。如果您的 CDP 客户端默认发送 Origin,可能会收到 403 Forbidden

解决方法(二选一):

  • 推荐:让客户端不要发送 Origin。例如 Python websocket-client 可使用 suppress_origin=True
  • 可选:启动 Chrome 时增加 --remote-allow-origins=http://127.0.0.1(同时仍需确保只监听 127.0.0.1,不要暴露到公网/内网)。

安装依赖(Ubuntu):

bash
apt-get update && apt-get install -y --no-install-recommends python3-websocket

运行脚本(示例:访问 news.qq.com,截图首页,并随机打开一个链接再截图一次):

bash
python3 - <<'PY'
import base64
import json
import random
import time
import urllib.parse
import urllib.request

from websocket import create_connection

BASE_HTTP = "http://127.0.0.1:9222"
HOME_URL = "https://news.qq.com/"

DENY_HOSTS = {
    "v.qq.com",
    "passport.qq.com",
    "qzone.qq.com",
    "mail.qq.com",
}


def http_json(path: str, method: str = "GET"):
    req = urllib.request.Request(BASE_HTTP + path, method=method)
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.load(resp)


class CDP:
    def __init__(self, ws_url: str):
        # websocket-client 会默认发送 Origin;新版 Chrome 会拒绝未放行的 Origin。
        # suppress_origin=True 会移除 Origin 头,避免 403。
        self.ws = create_connection(ws_url, timeout=30, suppress_origin=True)
        self.next_id = 1

    def close(self):
        try:
            self.ws.close()
        except Exception:
            pass

    def send(self, method: str, params=None, timeout_s: float = 30.0):
        msg_id = self.next_id
        self.next_id += 1

        payload = {"id": msg_id, "method": method}
        if params is not None:
            payload["params"] = params

        self.ws.send(json.dumps(payload))

        deadline = time.time() + timeout_s
        while True:
            if time.time() > deadline:
                raise TimeoutError(f"timeout waiting for response: {method}")

            msg = json.loads(self.ws.recv())
            # 忽略事件与其他请求的返回
            if msg.get("id") != msg_id:
                continue

            if "error" in msg:
                raise RuntimeError(msg["error"].get("message", json.dumps(msg["error"])))
            return msg.get("result")

    def wait_event(self, event_method: str, timeout_s: float = 30.0):
        deadline = time.time() + timeout_s
        while True:
            if time.time() > deadline:
                raise TimeoutError(f"timeout waiting for event: {event_method}")
            msg = json.loads(self.ws.recv())
            if msg.get("method") == event_method:
                return msg.get("params")

    def eval(self, expression: str):
        res = self.send(
            "Runtime.evaluate",
            {
                "expression": expression,
                "returnByValue": True,
                "awaitPromise": True,
            },
        )
        return (res or {}).get("result", {}).get("value")

    def screenshot_png(self, out_path: str):
        res = self.send(
            "Page.captureScreenshot",
            {
                "format": "png",
                "fromSurface": True,
                "captureBeyondViewport": True,
            },
        )
        data = (res or {}).get("data", "")
        with open(out_path, "wb") as f:
            f.write(base64.b64decode(data))


def get_candidates(cdp: CDP):
    js = r"""(() => {
      const seen = new Set();
      const out = [];
      for (const a of document.querySelectorAll('a[href]')) {
        const href = a.href;
        let text = (a.innerText || a.textContent || '').trim();
        if (!href || !href.startsWith('http')) continue;
        if (!text) continue;
        text = text.replace(/\s+/g, ' ');
        if (text.length < 8) continue;
        if (seen.has(href)) continue;
        seen.add(href);
        out.push({ href, text });
        if (out.length >= 200) break;
      }
      return out;
    })()"""

    items = cdp.eval(js) or []
    cleaned = []
    for it in items:
        if not isinstance(it, dict):
            continue
        href = it.get("href")
        text = it.get("text")
        if not href or not text:
            continue
        host = urllib.parse.urlparse(href).hostname or ""
        if not host or host in DENY_HOSTS:
            continue
        # 只保留 qq.com 域名(包含 mp.weixin.qq.com)
        if not host.endswith("qq.com"):
            continue
        cleaned.append({"href": href, "text": text})
    return cleaned


def main():
    ts = time.strftime("%Y%m%d-%H%M%S")
    home_png = f"/tmp/cdp-qq-home-{ts}.png"
    article_png = f"/tmp/cdp-qq-article-{ts}.png"

    created = http_json(
        "/json/new?" + urllib.parse.quote(HOME_URL, safe=":/?&=#"),
        method="PUT",
    )
    tid = created["id"]
    ws_url = created["webSocketDebuggerUrl"]

    cdp = CDP(ws_url)
    picked = None

    try:
        cdp.send("Page.enable")
        cdp.send("Runtime.enable")
        cdp.send("Network.enable")
        cdp.send(
            "Emulation.setDeviceMetricsOverride",
            {"width": 1440, "height": 900, "deviceScaleFactor": 1, "mobile": False},
        )

        # 1) 首页截图
        cdp.send("Page.navigate", {"url": HOME_URL})
        try:
            cdp.wait_event("Page.loadEventFired", timeout_s=30)
        except Exception:
            pass
        time.sleep(2.5)
        cdp.screenshot_png(home_png)

        # 2) 随机挑选一个链接并截图
        candidates = get_candidates(cdp)
        if not candidates:
            raise SystemExit("no candidate links found on homepage")
        picked = random.choice(candidates)

        cdp.send("Page.navigate", {"url": picked["href"]})
        try:
            cdp.wait_event("Page.loadEventFired", timeout_s=30)
        except Exception:
            pass
        time.sleep(2.5)
        cdp.screenshot_png(article_png)

        print("HOME:", HOME_URL)
        print("PICKED_TITLE:", picked["text"])
        print("PICKED_URL:", picked["href"])
        print("SCREENSHOT_HOME:", home_png)
        print("SCREENSHOT_ARTICLE:", article_png)

    finally:
        try:
            http_json("/json/close/" + tid, method="PUT")
        except Exception:
            pass
        cdp.close()


if __name__ == "__main__":
    main()
PY

提示

如果目标网站触发验证码/安全验证,建议先用稳定测试页(例如 https://example.com/)验证流程,再切换到目标站点,或在脚本中增加「过滤域名/关键词」的逻辑。

Step 6 使用 agent-browser 连接 CDP(推荐给 Agent 的 CLI)

agent-browser 是一个面向 AI Agent 的无头浏览器自动化 CLI:它提供了「snapshot(可访问性树)+ refs(稳定引用)」的工作流,适合让 Agent 在不写 CDP/Playwright 代码的情况下完成点击、输入、截图等操作。

注意

本节将 agent-browser 作为 CDP 客户端 使用:直接连接您在 Step 3 启动的 Chrome(端口 9222)。因此不需要额外下载 Chromium。

安装 Node.js 24(Active LTS)

注意

如果您已经是 root,请移除命令中的 sudo

bash
# 下载并运行 Node.js 24 安装脚本
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -

# 安装 Node.js
sudo apt install -y nodejs

# 验证安装
node -v  # 应显示 v24.x.x

安装 agent-browser

bash
npm install -g agent-browser
agent-browser --help | head -n 8

连接 CDP 并验证(打开网页 → snapshot → 点击 → 截图)

注意

截图会保存在哪里?

agent-browser screenshot /path/to.png 会把文件保存到 运行命令的那台机器。 如果您在 SSH 主机上运行命令,图片就保存在 SSH 主机;如果您在本地运行命令(通过端口转发连接 CDP),图片就保存在本地。

在 SSH 主机/容器内执行(直接连本机 9222):

bash
# 打开页面
agent-browser --cdp 9222 open https://example.com

# 输出可交互元素(带 ref)
agent-browser --cdp 9222 snapshot -i

# 机器可读输出(推荐给 Agent)
agent-browser --cdp 9222 snapshot -i --json

# 使用 ref 点击(示例页通常会出现 link ref=e1)
agent-browser --cdp 9222 click @e1

# 截图保存到远端
agent-browser --cdp 9222 screenshot /tmp/agent-browser-example.png
ls -lh /tmp/agent-browser-example.png

如果您看到 snapshot 输出类似下面内容,并且截图文件成功生成,就说明 agent-browser 已经可以稳定连接 CDP 并驱动网页:

text
- link "Learn more" [ref=e1]

提示

如果 agent-browser --cdp 9222 ... 连接失败,请先回到 Step 5 验证 /json/version 是否可访问,并查看「常见问题排查」中的:

  • 连接 webSocketDebuggerUrl 返回 403 Forbidden

(可选)快速验证「截图中文渲染」是否正常

如果您只想快速确认「字体安装正确、渲染不缺字」,可以用 headless Chrome 的 --screenshot 生成一张 PNG(不需要 CDP 客户端):

bash
HTML=/tmp/cjk-font-test.html
PNG=/tmp/cjk-font-test.png
PROFILE=$(mktemp -d /tmp/chrome-screenshot-profile.XXXXXX)

cat > "$HTML" <<'EOF'
<!doctype html>
<meta charset="utf-8" />
<style>
  body { font-family: "Noto Sans CJK SC", "Noto Sans CJK", sans-serif; font-size: 48px; padding: 32px; }
</style>
中文渲染测试:您好,世界。ABC 123
EOF

google-chrome-stable \
  --headless=new \
  --no-sandbox \
  --disable-gpu \
  --disable-dev-shm-usage \
  --user-data-dir="$PROFILE" \
  --window-size=1200,400 \
  --screenshot="$PNG" \
  "file://$HTML"

ls -l "$PNG"

您可以用 scp 把图片拷到本地查看:

bash
scp -P <SSH_PORT> root@guangdong-b-is.cloud.infini-ai.com:/tmp/cjk-font-test.png .

停止与清理

停止 CDP Chrome(任选其一):

  • 回到 tmux:tmux attach -t chrome-cdp-9222,按 Ctrl+C
  • 直接杀掉会话:tmux kill-session -t chrome-cdp-9222

清理临时 profile:

bash
DIR=$(sed -n 's/^profile_dir=//p' /tmp/chrome-cdp-9222.info)
[ -n "$DIR" ] && rm -rf "$DIR"

常见问题

访问 /json/new 返回 405

新版 Chrome 要求 PUT。请用脚本或 curl -X PUT ...

启动 Chrome 报错 Running as root without --no-sandbox

这是 root + sandbox 的限制。请加 --no-sandbox,或改用非 root 用户运行(更推荐)。

http://127.0.0.1:9222/ 是白屏

正常现象。请看 /json/version/json/list

截图里中文变成方块

确认安装了 fonts-noto-cjk 并执行了 fc-cache -fv

连接 webSocketDebuggerUrl 返回 403 Forbidden

这是新版 Chrome 的 DevTools WebSocket「Origin 校验」导致的:如果您的客户端默认发送 Origin,Chrome 可能会拒绝连接并返回 403。

解决方法(二选一):

  • 推荐:让客户端不要发送 Origin。例如 Python websocket-client 可使用 suppress_origin=True
  • 可选:启动 Chrome 时增加 --remote-allow-origins=http://127.0.0.1(同时仍需确保只监听 127.0.0.1,不要暴露到公网/内网)。

既然 Chrome 命令行 --screenshot 能出图,为什么 Agent 还需要 CDP?

这里的 --screenshot 指的是 Chrome 的命令行参数。例如:

google-chrome-stable --headless=new --screenshot=/tmp/out.png https://example.com

在 headless 模式下打开一个 URL,并把渲染结果直接保存为 PNG。它适合「打开一个 URL → 截图 → 退出」的单步采集;但 Agent 通常需要 CDP 来完成更复杂、可控的自动化流程,例如:

  • 交互操作:点击、输入、滚动、选择、触发 JS 行为(--screenshot 无法完成)。
  • 多步骤流程:从首页挑链接、进入详情页、等待某个元素出现后再截图。
  • 可观测性与调试:读取 DOM/文本、获取网络请求、捕获 console 输出、精确控制等待条件。
  • 复用会话状态:复用同一份 profile/cookie/session,避免每一步都冷启动浏览器。

既然 Agent 可以直接用 CDP,为什么还要用 agent-browser

两者都能完成网页自动化,但分工不同:

  • 直接使用 CDP:更底层、更灵活。适合需要精细控制 DevTools 协议(事件、网络、性能、追踪等)或需要自定义复杂逻辑的场景,但需要您自己处理等待、元素定位、重试与错误恢复等「编排」工作。
  • 使用 agent-browser:更偏向「给 Agent 用的工作流」。它把常见操作封装成 CLI,并提供 snapshot -i可访问性树 + refs(例如 @e1,让 Agent 用稳定引用进行点击/输入,减少 CSS 选择器的脆弱性,也更容易用 --json 输出做机器可读集成。

如果您的目标是「让 Agent 快速稳定落地」,优先使用 agent-browser;当您需要更底层能力或超出 CLI 范围的功能时,再回到 CDP 脚本。

Chrome 和 Chromium 有什么区别?我应该选哪个?

  • Chromium:开源项目(上游代码库)。
  • Google Chrome:基于 Chromium 的发行版,包含品牌与部分专有组件。

对本文这种「容器里启用 CDP 做自动化」的场景,二者都支持 CDP,通常都能工作。主要差异常见于:

  • 发行与安装方式:在 Ubuntu 22.04 上,chromium-browser 常见来自 Snap;受限容器里 Snap/squashfs 可能不可用。本文选择安装官方 Deb 的 google-chrome-stable,更容易在容器环境落地。
  • 媒体/DRM/编解码能力:Chrome 往往包含更完整的专有组件(例如部分编解码/DRM 支持)。如果您的自动化涉及音视频播放或受 DRM 保护内容,Chrome 更可能「开箱即用」。

能否通过某个 HTTP 端口访问 Chrome 浏览器 UI?

**不能。**CDP 端口提供的是「调试与自动化接口」,不是「浏览器窗口 UI」(地址栏、标签页、菜单)。

  • 你能从 CDP 端口得到什么
    • 目标列表(例如 /json/version/json/list
    • 每个页面的 webSocketDebuggerUrl(Agent 连接后即可控制网页、执行脚本、截图)
  • 你不能从 CDP 端口得到什么
    • 一个“像本地浏览器一样可点击”的窗口 UI。--headless 模式下 Chrome 不会创建可见窗口,因此也不存在可转发的 UI。

人类如何观察或调试页面?

  • 只需要“看页面渲染结果”:用 CDP 的 Page.captureScreenshot(或本文的 --screenshot)产出图片,再下载查看。
  • 需要检查 DOM/Network/Console:通过 SSH 端口转发访问 http://127.0.0.1:9222/json/list,从返回的 devtoolsFrontendUrl 打开 DevTools(这是 DevTools,不是浏览器窗口)。
  • 需要完整可交互 UI(像本地 Chrome 一样):这属于“远程桌面”能力,通常需要桌面环境 + VNC/noVNC 等服务。可参考:在容器化 Ubuntu 上安装 XFCE4 并配置生产就绪 VNC 环境

延伸阅读:为什么本文不安装中文输入法(IME)?

本文面向的核心场景是:Agent 通过 CDP 自动化驱动浏览器。在这个场景下,通常不需要在容器里安装中文输入法(IME)。

先区分两个概念:

  • 字体(fonts):决定「能否正确渲染中文」(例如截图里是否出现「豆腐」方块)。本文已在 Step 1 安装了 CJK 字体。
  • 输入法(IME):决定「人工交互时如何把拼音/五笔变成中文」(候选框、组合输入、快捷键切换等)。

通常不需要 IME 的情况

  • 自动化填写表单:CDP/自动化工具可以直接把 Unicode 中文字符串写入输入框(例如设置 value 或通过 CDP 的输入事件)。不依赖 IME 的候选框流程。
  • 页面渲染/截图/抓取:这类任务只与字体相关,和 IME 无关。

提示

如果您的目标只是「网页里能输入中文」,优先使用自动化直接写入中文文本。这通常更稳定、成本更低。

确实需要 IME 的情况

只有当您满足下面任一条件时,才建议为容器补齐 IME:

  • 您需要在远程桌面中像本地一样进行人工输入(拼音/候选框/选词)。
  • 您需要测试 IME 的组合输入行为(composition events)、候选词选择、快捷键切换等「输入法流程」本身。

为什么在受限容器里加 IME 会更麻烦

在平台容器中,IME 往往不仅是「装个包」,还依赖一整套图形与会话基础设施:

  • 需要图形会话:Headless Chrome 没有可交互的桌面会话,候选框也无处显示。通常需要 Xorg/Wayland + 桌面环境/窗口管理器。
  • 需要输入法框架与集成:例如 IBus/Fcitx5,以及应用侧的 IM module(GTK/Qt/XMODIFIERS 等环境配置)。
  • 需要稳定的用户会话/DBus:受限容器常常没有 systemd 用户会话,输入法守护进程的启动与环境注入会更容易踩坑。
  • root 会话不友好:很多桌面/输入法组件更推荐在非 root 用户下运行(也更接近生产使用方式)。

需要「桌面 + 中文输入法」,该如何配置?

如果您的场景必须使用远程桌面进行人工中文输入,建议先搭建完整的桌面与 VNC 环境,再在该环境中配置 IME。您可以参考这篇教程完成桌面化与生产级排障配置:

找不到想要的答案?
让 AI 助手为您解答