在 WSLg 中运行 Claude Desktop,并配置 Fcitx5 + Rime 中文输入法

在 WSL2 Ubuntu 24.04 的 WSLg 中让 Claude Desktop 输入中文:Fcitx5 只走 X11、kimpanel 常驻候选面板,以及 WSLg 幽灵窗口的取舍。

本文目录

一句话结论:WSLg 不提供可用的 Wayland 输入法协议,Fcitx5 在 WSLg 里需要退回 X11,并让 Claude Desktop 通过 D-Bus 前端连接输入法。中文输入本身能正常工作;真正麻烦的是 Fcitx5 原生候选窗被 unmap 后,Windows 一侧会残留约 2 秒的“幽灵窗口”。最终做法是写一个永不被 unmap 的 kimpanel 候选面板,把“隐藏”变成缩成 1×1。

这是单台机器上的实测记录,不是对所有 WSLg、桌面环境和输入法版本的通用保证。文中的“已验证”“推断”“未解决”会尽量分开标注。

验证环境

项 值
宿主 Windows 11 家庭版 10.0.26200
发行版 WSL2 Ubuntu 24.04.5 LTS,发行版名 Ubuntu-24.04-Claude
内核 6.18.54.1-microsoft-standard-WSL2
图形栈 WSLg / Weston 9.0.0,RDP backend + RAIL;Xwayland 提供 :0
Fcitx5 5.1.7-1build3;fcitx5-rime 5.1.4-2build2;librime 1.10.0
Rime 方案 雾凇拼音 rime-ice,rime_ice.schema.yaml 版本 2026-03-08
Claude Desktop deb 4.4212.0,Electron 44.9.3
Python 工具链 uv 0.13.1;uv 管理的 CPython 3.12.15,带 Tk 9.0

主体实验在 2026-11-02 完成,补充观察持续到 2026-11-08。WSL 内的时区是 JST;WSLg 的 weston.log 时间戳比它慢 1 小时,排查跨层问题时不要直接对齐两边的时间。

先给结论

如果你的目标只是“在 WSLg 的 Claude Desktop 里输入中文”,需要同时满足四个条件:

  1. Fcitx5 不启动 Wayland 前端。WSLg 会拒绝 zwp_input_method_v1 的 bind,Fcitx5 会因此退出;改成只走 X11 后才能稳定运行。
  2. Claude Desktop 通过 GTK_IM_MODULE=fcitx 和 Fcitx5 的 D-Bus 前端连接,而不是 XIM。
  3. 从 Windows 开始菜单启动 Claude Desktop 时,环境变量要在 /usr/local/bin/claude-desktop 包装脚本里注入。这个启动路径是非登录、非交互 shell,不读 ~/.profile 或 ~/.bashrc。
  4. 如果无法接受原生候选窗消失后约 2 秒的残影,就不要再使用 Fcitx5 的 classicui,改用 kimpanel 常驻面板。面板永远不 unmap,只缩成 1×1。

剪贴板图片粘贴是另一条独立的问题线,涉及 Xwayland 的类型桥接和 RDP 剪贴板的单请求死锁,已经拆到单独一篇文章。

安装 Fcitx5 与雾凇拼音

先安装输入法本体、Rime 引擎、各 GUI 框架前端和配置工具:

sudo apt-get install -y fcitx5 fcitx5-rime fcitx5-frontend-gtk3 fcitx5-frontend-gtk4 \
     fcitx5-frontend-qt5 fcitx5-config-qt fcitx5-module-lua librime-plugin-lua

然后用 Plum 安装雾凇。这里的 rime_frontend 和 rime_dir 两个变量不能省:

mkdir -p ~/.local/share/fcitx5/rime ~/.local/src
cd ~/.local/src
git clone --depth 1 https://github.com/rime/plum.git
cd plum
rime_frontend=fcitx5-rime rime_dir=$HOME/.local/share/fcitx5/rime \
  bash rime-install iDvel/rime-ice:others/recipes/full

第一次加载 Rime 时 Fcitx5 会完成部署,build/rime_ice.table.bin 约 64.8 MB。安装目录最终约 134 MB,加上用户词库和备份后约 151 MB。系统里只有 Droid Sans Fallback 一个可用的 CJK 字体,所以候选窗能显示中文,但生僻 CJK 仍可能缺字;这一点后面还会提到。

迁移 Windows 端 Rime 配置

如果 Windows 端已经有一套小狼毫/Weasel 配置,可以把个人配置和词库迁到 WSL,但不要直接复制整个目录:

  • 可以迁移:default.custom.yaml、rime_ice.custom.yaml、custom_phrase.txt、个人词库和自定义短语;
  • 不要迁移:build/、installation.yaml、user.yaml、weasel.custom.yaml。这些要么是平台专用的编译缓存,要么是 Windows 界面配置。

迁移前先备份 Linux 端的 Rime 目录。复制 Windows 文件时去掉 CRLF;词库用“追加/挂载”的方式接入上游 schema,不要直接覆盖上游文件。这样以后重新运行 rime-install 升级雾凇时,个人内容还能保留。

用户词频库是 LevelDB。复制前必须先关掉 Windows 端的 Weasel,否则 LOCK、MANIFEST 和 .log 会被占用,得到的是缺 MANIFEST 的残缺库:

cp: error reading '.../rime_ice.userdb/LOCK': Input/output error
cp: cannot open '.../MANIFEST-000571' for reading: Permission denied

关掉 Weasel 后重新复制,并删掉从 Windows 带来的空 LOCK 文件。Fcitx5 重启后会合并日志和旧 manifest,yin 的首选词才会回到预期结果。

为什么 Fcitx5 一启动就退出

第一版 systemd 服务让 Fcitx5 继承 WAYLAND_DISPLAY=wayland-0。服务状态显示 inactive,进程是 exited, status=0,并不是崩溃。前台运行一次就能看到原因:

X11 display: :0 is xwayland.
Created classicui for x11 display::0
Created classicui for wayland display:
zwp_input_method_v1@10: error 0: permission to bind input_method denied
Wayland connection got error: 71
Connection removed

Weston 直接拒绝了 zwp_input_method_v1 的 bind,连接以协议错误(errno 71,EPROTO)断开。Fcitx5 的 Wayland 模块随之退出,整个进程也结束。

修复方式是禁用 Wayland 相关前端和剪贴板插件,只保留 X11:

# ~/.config/systemd/user/fcitx5.service
[Unit]
Description=Fcitx5 input method (WSLg, X11 only)
Wants=fcitx5-panel.service
After=fcitx5-panel.service

[Service]
Type=simple
Environment=DISPLAY=:0
UnsetEnvironment=WAYLAND_DISPLAY
ExecStart=/usr/bin/fcitx5 --replace --disable wayland,waylandim,clipboard
Restart=on-failure
RestartSec=2

[Install]
WantedBy=default.target

clipboard 也要一起禁用,原因不是输入法功能,而是它会成为 WSLg 剪贴板桥最频繁的并发读取者,具体见剪贴板专文。

重新加载并启动:

systemctl --user daemon-reload
systemctl --user enable --now fcitx5.service
systemctl --user status fcitx5.service --no-pager

Claude Desktop 是 X11 下的 Electron 应用,窗口类和输入上下文都能在 Fcitx5 的调试信息里看到:

program:claude-desktop frontend:dbus cap:6000000032 focus:1

这说明它走的是 D-Bus 前端,不是 XIM。GTK_IM_MODULE=fcitx 对这条路径是有效的。

从开始菜单启动时,环境变量该放哪

Windows 开始菜单里的 Claude (Ubuntu-24.04-Claude) 快捷方式,本质上是这样一条命令:

wslg.exe -d Ubuntu-24.04-Claude --cd "~" -- claude-desktop

这条路径启动的是非登录、非交互 shell。用 bash -c 'echo $0; shopt login_shell' 检查会得到 /bin/bash 和 login_shell off,因此它不会读 ~/.profile、~/.bashrc,也不会读用户自己加在里面的 GTK_IM_MODULE。

~/.config/environment.d/90-fcitx5.conf 也只对由 systemd 用户管理器启动的进程可靠;WSLg 从 wslg.exe 直接拉起的 GUI 进程不一定经过它。在这台机器上,唯一稳定的注入点是 PATH 里排在 /usr/bin 前面的 /usr/local/bin/claude-desktop 包装脚本。

包装脚本不改 Claude Desktop 的安装文件,只负责导环境变量、确保 Fcitx5 已启动,再 exec 原程序:

#!/bin/bash
# /usr/local/bin/claude-desktop
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export SDL_IM_MODULE=fcitx

systemctl --user start fcitx5.service >/dev/null 2>&1 || true

# WSLg 不把 Windows 的显示缩放暴露给 Linux,先固定 125%。
exec /usr/lib/claude-desktop/claude-desktop --force-device-scale-factor=1.25 "$@"

创建这个文件需要 sudo。先确认 /usr/local/bin/claude-desktop 不存在,再把原启动器保留为 /usr/bin/claude-desktop;如果要回滚,删掉包装脚本即可。--force-device-scale-factor 不是输入法必需的,只是解决 WSLg 下界面过小的问题;不需要缩放时去掉这个参数。

验证输入法是否真的工作

fcitx5-remote 的退出码在脚本里很好用:

fcitx5-remote; echo $?
# 0 = 未运行 / 关闭
# 1 = 已运行但未激活
# 2 = 已激活

查看输入上下文和当前前端:

busctl --user call org.fcitx.Fcitx5 /controller \
  org.fcitx.Fcitx.Controller1 DebugInfo

没有 rime_api_console 时,可以临时用 D-Bus 伪装一个输入上下文:调用 CreateInputContext 拿到 IC 路径,再依次 FocusIn()、ProcessKeyEvent(),读取 CommitString。这套方法只用于自动化验证,不是运行时所必需的组件。

实测上屏结果:

输入 上屏 来源
nihao 你好 基本拼音
bangduoli 邦多利 个人词库
mengxianda 夢限大 个人词库(繁体)
nktl Nekotail 自定义短语
ciallo Ciallo~ (∠・ω< )⌒★ 自定义短语
nihao, 你好, 中文标点

能打出这些词,说明 Rime 的词库、个人短语和标点配置都已经进入 Fcitx5,而不是只完成了“进程启动”。个人邮箱等私有短语没有放进公开文章。

另外,Windows 的 Weasel 如果还开着,Ctrl+Space 可能先被 Windows 的全局热键/输入法钩子截获。WSLg 窗口获得焦点并不等于 Windows 的热键让位;关掉 Weasel 后,Fcitx5 的触发键才稳定。

候选窗残影:Fcitx5 没有问题,WSLg 有问题

中文能打之后,真正影响体验的现象出现了:候选窗该消失时不消失。我看到的不是“旧候选残留一瞬”,而是提交文字后原位置还留着一条候选栏,约 2 秒后才消失。

先看 Fcitx5 一侧。它的 classicui 候选窗是 X11 窗口 Fcitx5 Input Window,类型为 _NET_WM_WINDOW_TYPE_POPUP_MENU。用 xwininfo 连续采样:

16:37:29  X:943   Y:889   W:491 H:31  IsViewable
16:37:31  ...     W:517 / 611 / 517 / 611 / 530 ...  IsViewable
16:37:34  ...     W:364                         IsViewable
16:37:34  ...     W:364                         IsUnMapped
16:37:34  ...     X:1018 ...                   下一次候选的新位置

每次提交后,X11 窗口都在 1 秒内变成 IsUnMapped,没有泄漏,也没有卡住。Fcitx5 这一侧的行为是正确的;残影出在 WSLg 把 X11 窗口映射成 Windows 窗口的那一层。 X11 的 unmap 已经完成,Windows 侧收掉窗口要晚约 2 秒,画面停在最后一帧。

Fcitx5 的 classicui 没有“隐藏时缩成 1 像素”的配置,Wayland 路径又被 WSLg 拒绝,所以只改输入法配置绕不过去。

四种“隐藏窗口”实验

为了确认哪种方式能让 WSLg 立刻收掉画面,我在同一个位置依次测试了四种隐藏方式。实验时先把窗口文字改成“已隐藏 看到=残影”,避免把“没看见”误判成“隐藏成功”。

方式 做法 观察
A withdraw(unmap,等同于 Fcitx5 原生行为) 残影约 2 秒
B 用 X Shape 把窗口形状收成空区域 窗口没有隐藏,右半边变成黑块,更糟
C 缩成 1×1 放在原位置 干净消失,不留残影
D 移到屏幕外 (-3000, -3000) 残影仍然存在

结论很明确:不要 unmap,也不要试图把窗口移出屏幕;缩成 1×1 才有效。 WSLg 仍然认为窗口存在,所以不会走“关闭窗口”的延迟路径,而 1 个像素在视觉上等于不存在。

用 kimpanel 做一个常驻候选面板

既然 Fcitx5 的候选窗不能 unmap,就让它把候选界面交给一个自己控制的进程。Fcitx5 提供了 kimpanel 协议:

Claude Desktop (X11 / Electron)
        │ GTK_IM_MODULE=fcitx,D-Bus 前端
        ▼
Fcitx5 5.1.7(只启用 X11 前端)
        │ org.kde.impanel2.SetLookupTable / SetSpotRect
        ▼
fcitx5-wsl-panel(Tk 无边框窗口,永不 unmap)

面板占用 D-Bus 名 org.kde.impanel。Fcitx5 的 kimpanel 插件发现 owner 后,会把候选表、光标矩形和显示/隐藏信号发过来:

  • 方法:SetLookupTable、SetSpotRect、SetSpotLocation;
  • 信号:ShowLookupTable、ShowAux、ShowPreedit、UpdateAux、UpdatePreeditText。

面板需要实现这些方法并回复 method return;Introspect 之类的探测调用也要正常应答。

隐藏逻辑

候选面板的核心不是“画得漂亮”,而是永不 unmap:

def hide(self):
    # 不要 withdraw / unmap:WSLg 会留下约 2 秒的幽灵窗口。
    self.visible = False
    self.label.pack_forget()
    self.root.geometry("1x1")  # 保持当前位置,下次显示不从 (0,0) 跳

def place(self, x, y, w, h):
    # 先在 1x1 状态下移动到目标位置,再放大,避免旧位置闪一帧。
    if not self.visible:
        self.root.geometry(f"1x1+{x}+{y}")
        self.root.update_idletasks()
    self.root.geometry(f"{w}x{h}+{x}+{y}")
    self.visible = True

面板用 Tk 创建无边框、置顶窗口;D-Bus 线程用 jeepney 接收消息,主线程用 queue.Queue 取事件。这里有一个容易忽略的坑:uv 管理的 Python 3.12 自带 Tk 9.0,但只有核心 X 字体,没有 CJK 字体。 用 Tk 直接渲染中文会全部变成方块,所以候选文字实际是用 Pillow 画成 PNG,再贴到 Tk Label 上。

字体选择也不能按 Unicode 区间猜。Droid Sans Fallback 有 CJK 但没有 ASCII,DejaVu Sans 有拉丁字母和数字,Noto Color Emoji 是 CBDT 位图字体。面板用 fontTools 读取各字体的 cmap,谁真正包含这个字形就用谁;emoji 需要特殊处理(Pillow 只能以固定尺寸加载,渲染后再缩放),❤ 这类字符还要注意不能先被单色字体截走。

服务启动顺序

面板和 Fcitx5 的启动顺序会影响第一轮输入是否走面板。现在的单元关系是:

# fcitx5-panel.service
[Unit]
Description=Persistent candidate panel for fcitx5 (WSLg ghost-window workaround)
Before=fcitx5.service
PartOf=fcitx5.service

[Service]
Environment=DISPLAY=:0
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=%h/.local/bin/fcitx5-wsl-panel
Restart=on-failure
SuccessExitStatus=143
RestartSec=1

[Install]
WantedBy=fcitx5.service
systemctl --user daemon-reload
systemctl --user enable --now fcitx5-panel.service

让面板先于 Fcitx5 拿到 org.kde.impanel,Fcitx5 启动时才更容易在 classicui 初始化阶段认到它。这个顺序是根据日志推断的:故障发生时没有抓到现场,只在模拟输入里看到过 Kimpanel new owner 晚于 classicui 初始化约 90 ms。这是推断,不是严格证明。

模式切换提示与 Shift 的局限

按 Ctrl+Space 切换输入法时,Fcitx5 会发 UpdateAux('雾','') 或 UpdateAux('en','');面板据此弹一个 1200 ms 的“中文/EN”提示条。提示条存在期间要忽略紧随其后的隐藏候选信号,否则刚出现就被盖掉。

Shift 切换 Rime 的 ascii_mode 则没有对应的 kimpanel 信号,Fcitx5 没有把这个状态变化告诉面板。所以提示条对 Ctrl+Space 有效,对 Shift 无效;我确认 Shift 本身能切换,只是没有提示。

代价

面板进程稳定运行后,平均占用约 13% 单核 CPU、RSS 约 64 MB。原因是主线程每 8 ms 轮询一次事件队列,布局自愈线程每 3 秒启动一次 xrandr。这不是不可接受,但绝不算轻量;更合理的做法是用 after_idle/事件唤醒替代高频轮询,本次没有改。

显示缩放与双屏

WSLg 知道 Windows 的缩放比例,但不通过 X11 暴露给 Linux:xdpyinfo 始终是 96 DPI,也没有 Xft.dpi。因此 Claude Desktop 的缩放只能在包装脚本里用 --force-device-scale-factor=1.25 固定;这不是自动适配每块屏幕。

双屏下,X11 看到的是一个归一化后的虚拟屏。WSLg 的 Weston 坐标可以是负的,例如副屏在 x:-2560;X11 里则变成从 0 开始的宽屏,两个显示器并列。面板必须用当前 xrandr --current 的尺寸做边缘钳位,不能相信 Tk 在连接时缓存的 winfo_screenwidth()。

我遇到过一次面板把屏幕宽度记成 640,导致候选窗的 x 坐标被钳在 434。根因是面板启动时 X 屏幕还没变成真实分辨率,Tk 缓存了旧值。修复方式是每次显示候选窗时用 xrandr --current 解析 current W x H,失败才回退 Tk;重新验证后,坐标能正确落到 1500、2154 等位置。

已知边界

  • 面板 CPU 约 13%:8 ms 轮询造成,尚未改成事件驱动。
  • 用户词库警告:迁移后的某段时间里,Fcitx5 日志出现过 3204 条 failed in parsing key-value from userdb entry,值是极小的浮点数;重启后不再出现,是否有词频丢失没有查清。
  • 启动顺序与偶发不可见:有一次开机后候选窗不出现,重启面板后恢复;后来加了“面板先启动”和显示器布局自愈,但没有在真实故障现场验证。
  • Shift 无提示:如上,kimpanel 没有暴露 Rime 的 ascii_mode 变化。
  • 缺字:Droid Sans Fallback 没有部分扩展区 CJK;装 fonts-noto-cjk 并配置后备字体可以补,但本次没装。
  • 剪贴板:与输入法独立,见下一节链接。Fcitx5 的 clipboard 插件已禁用,不要重新打开。
  • 环境依赖:这套方案针对 X11/Electron 客户端和 WSLg 的 Weston 9.0.0。Wayland 原生应用不适用,换发行版或 WSLg 版本后需要重新验证。

复现顺序

  1. 确认 WSLg 在运行,DISPLAY=:0 可用,systemd 用户服务可用。
  2. 安装 Fcitx5、Rime 前端和雾凇,完成首次部署。
  3. 关闭 Wayland 前端:--disable wayland,waylandim,clipboard,并 UnsetEnvironment=WAYLAND_DISPLAY。
  4. 在 /usr/local/bin/claude-desktop 包装脚本里注入 IM 环境变量,保留原启动器以便回滚。
  5. 用 fcitx5-remote、DebugInfo 或 D-Bus 模拟输入确认中文上屏。
  6. 如果出现候选窗残影,先接受 2 秒延迟,或实现 kimpanel 面板;改 Fcitx5 配置本身无效。
  7. 面板需要固定显示器变化、启动顺序和 1×1 隐藏逻辑;不要用 unmap、Shape 空区域或移出屏幕。
  8. 最后处理缩放;不要把 WSLg 的显示设置和 Windows 全局设置混在一起改。

相关阅读