在 Windows 上把 OpenClaw跑起来:一份带血的实战记录

12 阅读7分钟

在 Windows 上把 OpenClaw跑起来:一份带血的实战记录

虽然正常情况下我们一般在mac或者linux上安装openclaw,但是有时候受限于特定条件,需要安装到windows上,我花了 3 个小时远程把 OpenClaw 装到一台 Win10 机器上,全程 SSH 操作。中间踩了 19 个坑:WSL 卡死、Bonjour 崩进程、portproxy 回环死锁、浏览器拒绝 secure context、device pairing... 这篇把全套链路、每个坑和修法都写下来,帮你少走弯路。


推荐我的几本源码精讲书

写完 OpenClaw 部署,顺手安利下我自己写的几本源码深度解析专著(全都有试读章节,《LangChain 设计与实现》全本免费):

完整书单和试读:yangyitao.com


一、OpenClaw 是什么、为什么要装它

OpenClaw(github.com/openclaw/openclaw)是一个 36 万 stars 的开源 AI 助手运行时,作者是 steipete(业界知名的 iOS 工程师,PSPDFKit 创始人)。它支持 Claude/OpenAI/各种国内大模型作为后端,可以像桌面助手一样常驻、装 channel 接消息平台、跑 cron 任务、共享 Canvas。

我的需求很简单:找一台 Windows 机器把它装上常驻跑,本机和远程都能开面板。后端用火山引擎方舟(doubao/glm/kimi 都能调)。

听起来 5 分钟的事,对吧?官方 README 说"WSL2 strongly recommended on Windows",看起来跟个普通 Node.js 项目一样。

实际跑了 3 小时。

二、关键决策(少做错路,少返工)

每一项都是后面踩坑后重新选的,给你直接抄:

决策原因
WSL2 而非原生 WinOpenClaw 大量 POSIX 依赖(fs.watch / signal / symlink),原生跑会缺东西
wsl --import 而非 Microsoft Store1) 可指定安装目录到 E 盘;2) 不触发交互建用户向导;3) root 直登适合脚本化
GitHub MSI 装新版 WSL(内核 6.6+)Win10 内置的 5.10 内核 + systemd 在 idle 后会让 LxssManager 卡 STOP_PENDING
--gateway-bind=lanbind=tailnet 会因为 WSL 内没 Tailscale 接口而 fallback 到 127.0.0.1,Windows 都访问不到
netsh portproxy 桥接WSL2 NAT 模式下 Windows host 不会自动转发到 WSL 服务
Tailscale Serve 提供 HTTPS浏览器对 100.x.x.x(Tailscale CGNAT IP)不当 secure context,Control UI 会拒绝
Sysinternals Autologon 配自动登录不登录桌面 Tailscale 续约会慢 4-5 分钟
VBS 隐身启动器 + 计划任务 持久化wsl.exe 是 console app,schtasks 直接跑会弹窗

三、踩坑全记录

我把整个过程按踩坑顺序串起来,每个坑标编号方便你定位。

坑 1:cmd 不识别 ; 当分隔符

第一发现:

sshpass -p '...' ssh Administrator@host 'echo alive; powershell -NoProfile -Command "..."'

cmd 把 ; 当成 echo 的字面参数:alive; powershell -NoProfile ... 全部被打印出来。

修法:cmd 用 & 分隔,或直接用 PowerShell。

sshpass -p '...' ssh Administrator@host 'echo alive & powershell -NoProfile -Command "..."'

坑 2:PowerShell over SSH 的 escape 是噩梦

跨 SSH 发 Set-Content -Value "[network]\ngenerateResolvConf = false`n" ...` 这种命令,反斜杠/反引号/$ / " 经过 zsh → ssh → cmd → powershell 四层 shell,escape 经常对不上,一会儿 path not found,一会儿语法错误。

修法:用 PowerShell 的 -EncodedCommand,把整个脚本 base64 化:

PS_CMD='你的 powershell 脚本,里面随便用 ` $ " \'
ENC=$(echo -n "$PS_CMD" | iconv -t UTF-16LE | base64)
sshpass -p "$PASS" ssh user@host "powershell -NoProfile -EncodedCommand $ENC"

后面所有 PowerShell 都走这条管道,一劳永逸。

坑 3:第一次启用 VMP 后开机 3-5 分钟

dism /enable-feature 装 Hyper-V/VMP 后第一次开机要做一堆初始化,耐心等。我从 SSH 端用 polling:

until sshpass -p "$PASS" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
        "Administrator@$HOST" hostname 2>/dev/null; do
    echo "$(date +%T) waiting..."
    sleep 8
done

这次重启等了 5 分钟才回来。

坑 4:旧 Ubuntu rootfs URL 已 404

老文档里:

https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-amd64-wsl.rootfs.tar.gz

→ 404。Canonical 改名了,最新是:

https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz

直接 curl 列目录验证:

curl -sSL "https://cloud-images.ubuntu.com/wsl/jammy/current/" | grep -oE 'href="[^"]*"'

坑 5:不要启用 systemd(致命)

第一次我在 wsl.conf 里加了:

[boot]
systemd = true

结果是 LxssManager 服务卡 STOP_PENDING。从那一刻起所有 wsl ... 命令全 timeout,不能再操作 WSL。试过 Stop-Service -Forcetaskkill /F、SYSTEM 级 schtasks 强杀 —— 都不行,svchost 是 SYSTEM-protected。只能重启

sc queryex LxssManager
# SERVICE_NAME: LxssManager 
#         STATE              : 3  STOP_PENDING 
#                                 (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
#         PID                : 6496
# Stop-Process: Access denied (SYSTEM-protected)

修法:不要启用 systemd。openclaw 不需要 systemd。Win10 5.10 内核 + systemd 是死亡组合,新版 6.6 内核也不需要它。

坑 6:Tailscale 抢 DNS 让 WSL 解析失败

在装了 Tailscale 的 Windows 上,WSL 自动生成的 resolv.conf 会指向 Windows host gateway(172.x.x.1),这个 gateway 不会转发 DNS —— Tailscale MagicDNS 抢走了。

$ wsl -d Ubuntu --user root -- apt-get update -qq
W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/jammy/InRelease  Temporary failure resolving 'archive.ubuntu.com'

修法 三步

  1. wsl.conf 关掉自动生成

    [network]
    generateResolvConf = false
    
  2. 写静态 resolv.conf(用国内 DNS 优先)

    nameserver 223.5.5.5
    nameserver 119.29.29.29
    nameserver 8.8.8.8
    
  3. chattr +i 标记不可变(关键!)

    WSL 2.6+ 默认把 /etc/resolv.conf 做成符号链接到 /mnt/wsl/resolv.conf,每次 distro 重启会重建链接吞掉你写的内容。chattr +i 阻止它。

    rm -f /etc/resolv.conf
    cp /mnt/e/Apps/resolv.conf /etc/resolv.conf
    chattr +i /etc/resolv.conf
    

坑 7:nvm installnode 还在 PATH 外

# 这样写,Node 装上了但下一行命令找不到
. /root/.nvm/nvm.sh && nvm install 24 && nvm alias default 24
node --version    # bash: node: command not found

nvm install 24 会内部 nvm use,但 PATH 设置在 subshell context 里,subshell 退了 PATH 没了。

修法:所有用到 node 的命令都包一层完整初始化:

bash -lc '
export NVM_DIR=/root/.nvm
. $NVM_DIR/nvm.sh
nvm use default >/dev/null
node --version    # 这下能用
...
'

坑 8:onboard 用错 bind 模式

openclaw onboard --gateway-bind tailnet ...

WSL 内没 Tailscale 接口,OpenClaw fallback 到 127.0.0.1(WSL 的 loopback)。Windows 无法跨 WSL2 NAT 边界访问 WSL 的 loopback。结果:怎么都连不上。

修法:用 --gateway-bind lan,让 gateway 绑 0.0.0.0(WSL 内全部接口),然后 Windows 通过 portproxy 桥接。

坑 9:Bonjour 插件让 gateway 进 30 秒重启循环

这个最坑。Gateway 起来 ~12 秒后会崩:

[plugins] bonjour: watchdog detected non-announced service; attempting re-advertise (state=probing)
[plugins] bonjour: restarting advertiser (service stuck in probing for 12320ms ...)
[openclaw] Unhandled promise rejection: CIAO PROBING CANCELLED
[openclaw] wrote stability bundle: ...unhandled_rejection.json
[gateway] loading configuration…   ← 重启了!

每 30 秒重启一次,永远到不了"稳定 ready"状态。

根因:Bonjour 用 mDNS 在多播域里宣告自己。WSL2/Hyper-V vSwitch 下 mDNS 多播经常出问题,会卡在 probing 状态,然后超时取消,取消触发的 promise 没处理就把整个进程崩掉。

修法:禁掉 bonjour 插件。

import json
p='/root/.openclaw/openclaw.json'
d=json.load(open(p))
d.setdefault('plugins',{}).setdefault('entries',{})['bonjour']={'enabled':False}
json.dump(d, open(p,'w'), indent=2)

修完后 gateway 终于 stable:

[gateway] ready (5 plugins: acpx, browser, device-pair, phone-control, talk-voice; 5.3s)

坑 10:模型 ID 不在 openclaw 目录里

火山控制台你创建的"endpoint name"叫 doubao-seed-2-0-pro-260215,但这是火山自己的内部 endpoint ID,不是 openclaw 模型目录里的 ID。warmup 报错:

startup model warmup failed for volcengine-plan/doubao-seed-2-0-pro-260215: Error: Unknown model

修法:先列出 openclaw 知道的火山模型:

openclaw infer model list | grep volcengine-plan

输出(截至本文写时):

ark-code-latest         (Ark Coding Plan 默认别名, 256k ctx)
doubao-seed-code        (256k)
doubao-seed-code-preview-251028 (256k)
glm-4.7                 (GLM 4.7 Coding, 195k ctx)
kimi-k2-thinking        (256k 推理)
kimi-k2.5               (256k 编码)

挑一个能用的:

openclaw models set volcengine-plan/glm-4.7

坑 11:SSH 退出后 gateway 跟着死

# 这种起法在 SSH 一断就完蛋
nohup openclaw gateway run >/var/log/openclaw/gateway.log 2>&1 < /dev/null &
disown

WSL2 distro 在没有任何 wsl.exe 会话连接它时会 idle 关停(vmIdleTimeout 默认 60s)。disown 在 SSH 退出后没救 —— 整个 WSL VM 都被 Hyper-V 回收了。

修法:从 Windows 端用 VBS 启动器 + 计划任务(坑 13 详细写)。

坑 12:netsh portproxy 0.0.0.0 监听导致回环死锁

第一次配端口转发:

netsh interface portproxy add v4tov4 listenport=18789 listenaddress=0.0.0.0 connectport=18789 connectaddress=127.0.0.1

测试:从 Windows curl http://127.0.0.1:18789/ → timeout。

为什么?listenaddress=0.0.0.0 意思是"监听所有接口",包括 127.0.0.1 本身。当连接进 127.0.0.1:18789,被 portproxy 的 0.0.0.0 监听器拿走,转发到 127.0.0.1:18789 —— 又被自己抓住。回环死锁

修法:listenaddress 写具体地址。

# Tailscale IP 的入口
$tsip = "<你的 Tailscale IP,比如 100.x.x.x>"
netsh interface portproxy add v4tov4 listenport=18789 listenaddress=$tsip connectport=18789 connectaddress=$wslip

# 本机 localhost 的入口(给 Tailscale Serve 当上游)
netsh interface portproxy add v4tov4 listenport=18789 listenaddress=127.0.0.1 connectport=18789 connectaddress=$wslip

坑 13:wsl.exe 是 console app,schtasks 直接跑会弹窗

$action = New-ScheduledTaskAction -Execute "wsl.exe" -Argument "-d Ubuntu --user root -- /mnt/e/Apps/start-gateway.sh"

这样起,Administrator 桌面上会弹一个 PowerShell 控制台窗口(wsl.exe 是 console application,conhost 给它分配了窗口)。挡视线 + 不能关(关了 gateway 也死)。

修法:包一层 VBS 隐身启动:

' E:\Apps\hidden.vbs
Set WshShell = WScript.CreateObject("WScript.Shell")
WshShell.Run "wsl.exe -d Ubuntu --user root -- /mnt/e/Apps/start-gateway.sh", 0, false

Run "...", 0, false0=隐藏窗口,false=不等待。然后任务执行 wscript.exe E:\Apps\hidden.vbs,wscript 自己就是无窗口的,启动子进程也是隐藏的。

$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "E:\Apps\hidden.vbs"
$trigger = New-ScheduledTaskTrigger -AtLogon -User "Administrator"
$settings = New-ScheduledTaskSettingsSet -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) ...
$principal = New-ScheduledTaskPrincipal -UserId "Administrator" -LogonType Interactive -RunLevel Highest
Register-ScheduledTask -TaskName "OpenClawGateway" ...

-LogonType Interactive 是必须的,否则 task 在 Session 0 跑,wsl.exe 起不来 Hyper-V VM

坑 14:浏览器对 100.x.x.x 不当 secure context

面板首次加载就报:

control ui requires device identity (use HTTPS or localhost secure context)

100.x.x.x 是 Tailscale CGNAT 段,浏览器(Chrome/Safari/Firefox 一致行为)不把它当 secure context,WebCrypto / Storage Access API / 一些 Permissions Policy 都会被禁。OpenClaw 检测到非 secure 就拒。

修法两选一:

A. SSH tunnel(一次性)

ssh -L 18789:<你的 Tailscale IP>:18789 Administrator@host
# 然后浏览器开 http://localhost:18789/?token=...

localhost 浏览器无条件视为 secure context,问题消失。

B. Tailscale Serve 提供 HTTPS(长期)

前提:Tailscale 控制台 login.tailscale.com/admin/dns 启用 HTTPS Certificates。

& "C:\Program Files\Tailscale\tailscale.exe" serve --bg --https=443 http://localhost:18789

这条让 Tailscale 在 <machine>.<tailnet>.ts.net:443 上提供 HTTPS(真 Let's Encrypt 证书),转发到 Windows localhost:18789(再经 portproxy 进 WSL gateway)。

$ tailscale serve status
https://<machine>.<tailnet>.ts.net (tailnet only)
|-- / proxy http://localhost:18789

之后浏览器开 https://<machine>.<tailnet>.ts.net/?token=...*.ts.net 是真域名 + 真证书,secure context 满足。

坑 15:origin not allowed

换 HTTPS 域名后又一道关:

origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)

OpenClaw 默认只信 localhost。把所有访问入口都加进白名单:

import json
p='/root/.openclaw/openclaw.json'
d=json.load(open(p))
d['gateway'].setdefault('controlUi',{})['allowedOrigins']=[
    "http://localhost:18789",
    "http://127.0.0.1:18789",
    "http://<你的 Tailscale IP>:18789",
    "https://<machine>.<tailnet>.ts.net",
    "https://<machine>.<tailnet>.ts.net:443",
]
json.dump(d, open(p,'w'), indent=2)

每次改 openclaw.json 必须 wsl --shutdown + 重启 task 才生效。

坑 16:device pairing required

刷新后又报:

device pairing required (requestId: b82a1cf6-2a83-4287-9f8f-e087e7336119)

OpenClaw 对非 localhost 访问要求 device pairing。服务端批准:

openclaw devices list                              # 看到 pending
openclaw devices approve <requestId>               # 批准

⚠️ 每次开新无痕窗口都要重新 pairing(device token 存在 localStorage,私密模式不持久化)。

坑 17:Chrome ERR_CONNECTION_CLOSED 但 curl 正常

修完上面所有还报:

<machine>.<tailnet>.ts.net 意外终止了连接
ERR_CONNECTION_CLOSED

curl -v https://<machine>.<tailnet>.ts.net/ TLS 握手成功 + HTTP 200 拿到完整 HTML。

是 Chrome 缓存了之前失败请求的 socket pool / HTTP2 流状态。

修法:

  • 无痕窗口(最快)
  • chrome://net-internals/#sockets → Flush socket pools
  • 或 完全退出 Chrome(Cmd+Q)再重开

坑 18:Tailscale 不登录桌面起得来,但慢 4-5 分钟

测试重启时发现:第一次重启后 5 分钟 SSH 才通。SSH 服务自启正常,问题出在 Tailscale —— 没人登录桌面时,Tailscale 续约协商显著变慢。

修法:装 Sysinternals Autologon,密码加密存 LSA:

& E:\Apps\autologon\Autologon64.exe /accepteula Administrator . "你的密码"

之后重启 ~70 秒就 SSH 通,省掉那 4 分钟尾巴。

坑 19:远程截屏时自己的 PowerShell 窗口入镜

调试要看桌面状态,从 SSH 跑截屏脚本时自己的 PowerShell 窗口被截进去了,闹鬼一样。

修法:截屏脚本也包一层 VBS 隐身:

' shot.vbs
Set WshShell = WScript.CreateObject("WScript.Shell")
WScript.Sleep 800     ' 留一点时间让其它窗口稳定
WshShell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Apps\shot.ps1", 0, true

shot.ps1 标准 .NET 截屏:

Add-Type -AssemblyName System.Windows.Forms
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen(0,0,0,0,$b.Size)
$bmp.Save("E:\Apps\screenshot.png")
$g.Dispose(); $bmp.Dispose()

触发 + 拉回:

schtasks /Create /TN ShotTask /SC ONCE /ST 23:59 \
    /TR "wscript.exe E:\Apps\shot.vbs" /RU Administrator /RL HIGHEST /IT /F
schtasks /Run /TN ShotTask
sleep 6
schtasks /Delete /TN ShotTask /F
scp Administrator@host:'E:/Apps/screenshot.png' /tmp/shot.png

/IT 是关键,让任务在 Administrator 当前交互会话里跑,截到的就是物理屏幕。SSH 自己的 session 是不可见的 Session 0/1。

四、最终架构图

                        Mac/iOS 浏览器
                             │
               https://<machine>.<tailnet>.ts.net
                             │
                       Tailscale tailnet
                             │
                     ┌───────▼───────┐
                     │  Win 机   │
                     │  (Win10)      │
                     │               │
                     │ Tailscale Serve (:443, Let's Encrypt 证书)
                     │       │       │
                     │       ▼       │
                     │ localhost:18789
                     │       │       │
                     │ netsh portproxy
                     │       │       │
                     │  WSL 172.x:18789
                     │       │       │
                     │  ┌────▼────┐  │
                     │  │ Ubuntu  │  │
                     │  │ openclaw│  │
                     │  │ gateway │  │
                     │  └─────────┘  │
                     └───────────────┘
                             │
                             ▼
                     火山方舟 coding plan
                     volcengine-plan/glm-4.7

五、快速验收清单

# 1. WSL 内 gateway 监听
wsl -d Ubuntu --user root -- ss -tlnp | grep 18789
# LISTEN 0 511 0.0.0.0:18789 ... openclaw-gatewa

# 2. Windows localhost 通
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:18789/?token=$TOKEN"
# 200

# 3. Tailscale HTTPS 通
curl -s -o /dev/null -w "%{http_code}\n" "https://<machine>.<tailnet>.ts.net/?token=$TOKEN"
# 200

# 4. 持久化任务存在
schtasks /Query /TN OpenClawGateway /V /FO LIST | findstr "状态"

# 5. 模型对得上
wsl -d Ubuntu --user root -- bash -lc 'openclaw models status' | grep Default
# Default: volcengine-plan/glm-4.7

六、总结

预期 30 分钟,实际 3 小时。复盘下来,时间主要花在:

  1. WSL2 老内核 + systemd 引发的卡死,重启了 3 次
  2. Bonjour 插件 30 秒重启循环,定位用了 30 分钟
  3. portproxy 回环死锁,搞清楚 listenaddress 语义又花 20 分钟
  4. Control UI 的三道安全闸:secure context → allowedOrigins → device pairing,每一道都要新东西修
  5. 远程截屏的 Session 隔离 + 自己窗口入镜的怪圈

如果你也要做类似的事,抄上面那个"关键决策"表 + 19 个坑就够了。

更深入想了解 OpenClaw 内部到底怎么转的(Gateway 引擎、Provider 热切换、Agent 编排、工具沙箱),看我那本 《OpenClaw 设计与实现》


联系作者yangyitao.com 完整书单yangyitao.com