#OpenClaw #Docker #Ollama #邮件插件 #网络排障

接上篇 《N100 自托管 OpenClaw + Hermes Agent 部署实录》,记录第二阶段的配置与踩坑。


一、修复 Docker 容器无法访问公网 WAN TCP

现象

容器内 ping 1.1.1.1 正常(ICMP 通),但 curl https://api.deepseek.com 超时。导致 DeepSeek / Anthropic API 全部失败,日志输出:

1
Error: All models failed (2): deepseek/deepseek-chat: LLM idle timeout (120s)

根本原因

N100 使用 WiFi(wlp2s0)上网。Docker bridge 模式下,NAT/MASQUERADE 规则在某些 WiFi 驱动上对 TCP 转发有问题——ICMP 能通(路由层),WAN TCP 超时(NAT 层)。

修复:改用 network_mode: host

1
2
3
4
5
6
7
8
9
10
11
12
13
# ~/dockerfile/openclaw/docker-compose.yml
services:
openclaw:
image: ghcr.io/openclaw/openclaw:latest
container_name: openclaw
user: root
restart: unless-stopped
network_mode: host # ← 关键:直接使用宿主机网络栈
volumes:
- ./data:/root/.openclaw
- ./openclaw.json:/root/.openclaw/openclaw.json
env_file:
- .env

注:network_mode: host 后不需要 ports 映射,容器直接绑定宿主机 IP。

同步修改 openclaw.json 的 bind

之前 bind: loopback 只在本机 127.0.0.1 监听。改为 lan 后绑定局域网 IP:

1
2
3
4
5
6
7
{
"gateway": {
"mode": "local",
"bind": "lan",
"port": 18789
}
}

开放 UFW 防火墙

1
sudo ufw allow 18789/tcp

验证:从 Mac curl http://192.168.0.155:18789/ 返回 HTML,通。


二、配置邮件插件(163 + QQ)

最终有效的 openclaw.json 邮件段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"plugins": {
"allow": ["email", "gmail"],
"entries": {
"email": {
"enabled": true,
"config": {
"imap": {
"host": "imap.163.com",
"port": 993,
"secure": true,
"username": "xxx@163.com",
"password": "YOUR_163_AUTH_CODE"
},
"smtp": {
"host": "smtp.163.com",
"port": 465,
"secure": true,
"from": "xxx@163.com"
},
"requireExplicitSendConfirmation": false
}
},
"gmail": {
"enabled": true,
"config": {
"username": "xxx@qq.com",
"appPassword": "YOUR_QQ_APP_PASSWORD",
"from": "xxx@qq.com",
"imap": {
"host": "imap.qq.com",
"port": 993,
"secure": true
},
"smtp": {
"host": "smtp.qq.com",
"port": 465,
"secure": true
},
"requireExplicitSendConfirmation": false
}
}
}
}
}

注意gmail 插件 manifest id 是 gmail(不是包名 @manuelfedele/openclaw-gmail-plugin),plugins.entries 的 key 必须用 gmail

安装命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 停容器 → 安装 → 重启(避免文件锁)
docker stop openclaw

docker run --rm --user root \
-v ~/dockerfile/openclaw/data:/root/.openclaw \
ghcr.io/openclaw/openclaw:latest \
openclaw plugins install email

docker run --rm --user root \
-v ~/dockerfile/openclaw/data:/root/.openclaw \
ghcr.io/openclaw/openclaw:latest \
openclaw plugins install @manuelfedele/openclaw-gmail-plugin

cd ~/dockerfile/openclaw && docker compose up -d

三、踩坑:email 插件工具无法被 Agent 识别

现象

QQ(gmail 插件)工具全部正常,但 163(email 插件)工具 email_mailboxes_list 等完全不出现,Agent 报:

1
⚠️ 🛠️ `run openclaw email` failed

openclaw plugins list 显示 email 状态为 loadedtoolNames: []

排查过程

  1. 检查网络连通性:node -e "tls.connect(993, 'imap.163.com', ...)"CONNECTED,排除网络问题

  2. 检查插件 manifest:发现 email/openclaw.plugin.json 只有 skills 字段,没有 contracts.tools

1
2
3
4
5
6
// email 插件(有问题的原始 manifest)
{
"id": "email",
"skills": ["skills/email"]
// ← 缺少 contracts.tools!
}
1
2
3
4
5
6
7
// gmail 插件(正常的 manifest)
{
"id": "gmail",
"contracts": {
"tools": ["gmail_mailboxes_list", "gmail_messages_search", ...]
}
}

根本原因

OpenClaw 通过 contracts.tools 静态声明来决定向 Agent 暴露哪些工具。skills 字段用于 skill runner 机制,并不等价于工具暴露。email 插件 v0.1.0 缺少这个声明。

修复

在容器内手动补充 contracts.tools(文件在 volume 中持久化):

1
2
3
4
5
6
7
8
9
10
11
12
docker exec openclaw python3 -c "
import json
with open('/root/.openclaw/extensions/email/openclaw.plugin.json') as f:
m = json.load(f)
m['contracts'] = {'tools': [
'email_mailboxes_list', 'email_messages_search', 'email_message_get',
'email_message_update', 'email_message_move', 'email_send', 'email_reply'
]}
with open('/root/.openclaw/extensions/email/openclaw.plugin.json', 'w') as f:
json.dump(m, f, indent=2)
print('OK')
"

然后刷新插件注册表:

1
docker exec openclaw openclaw plugins registry --refresh

验证:

1
2
3
docker exec openclaw openclaw agent --agent main \
--message 'call email_mailboxes_list' --timeout 60
# 输出 163 邮箱文件夹列表,成功

四、接入 Ollama(Win11, RTX 2070 Super)

设备信息

项目
IP 192.168.0.195
GPU NVIDIA RTX 2070 Super Max-Q(8GB VRAM)
Ollama 版本 0.30.11
已下载模型 qwen2.5-coder:7bllava:7bnomic-embed-textqwen3:8bdeepseek-r1:8b

问题:Ollama 托盘 App 硬覆盖 OLLAMA_HOST

Ollama Windows 托盘 app(0.30.11)会在启动 server 时把 OLLAMA_HOST 强制设为 http://127.0.0.1:11434,忽略系统环境变量。server 日志可以确认:

1
2
OLLAMA_HOST:http://127.0.0.1:11434
msg="Listening on 127.0.0.1:11434 (version 0.30.11)"

解决方案:Windows portproxy 内核级端口转发

不修改 Ollama 进程,直接在 Windows 网络栈层做端口映射:

1
2
3
netsh interface portproxy add v4tov4 `
listenport=11434 listenaddress=0.0.0.0 `
connectport=11434 connectaddress=127.0.0.1

验证:

1
2
netsh interface portproxy show all
# 0.0.0.0 11434 -> 127.0.0.1 11434 ✓

同时确保 Windows 防火墙开放入站规则:

1
2
3
New-NetFirewallRule -DisplayName 'Ollama LAN' `
-Direction Inbound -Protocol TCP -LocalPort 11434 `
-Action Allow -Profile Private,Domain

portproxy 规则重启后自动保留,无需额外配置。

更新 openclaw.json

1
2
3
4
5
6
7
8
9
{
"models": {
"providers": {
"ollama": {
"baseUrl": "http://192.168.0.195:11434"
}
}
}
}

OpenClaw 检测到配置变更后热重载,无需重启容器:

1
2
[reload] config change detected; evaluating reload (models.providers.ollama.baseUrl)
[reload] config hot reload applied (models.providers.ollama.baseUrl)

五、当前完整 openclaw.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"gateway": {
"mode": "local",
"bind": "lan",
"port": 18789
},
"models": {
"providers": {
"deepseek": {
"baseUrl": "https://api.deepseek.com/v1",
"apiKey": "YOUR_DEEPSEEK_KEY",
"api": "openai-completions",
"auth": "api-key",
"maxTokens": 8192
},
"ollama": {
"baseUrl": "http://192.168.0.195:11434"
},
"anthropic": {
"apiKey": "YOUR_ANTHROPIC_KEY"
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "deepseek/deepseek-chat",
"fallbacks": ["anthropic/claude-sonnet-4-6"]
}
}
},
"plugins": {
"allow": ["email", "gmail"],
"entries": {
"email": { "enabled": true, "config": { "...": "163 邮箱配置" } },
"gmail": { "enabled": true, "config": { "...": "QQ 邮箱配置" } }
}
}
}

六、经验总结

问题 根本原因 解决方案
Docker 容器 WAN TCP 不通 bridge 模式 + WiFi NAT 问题 network_mode: host
email 插件工具 Agent 不可见 manifest 缺少 contracts.tools 手动补充 + plugins registry --refresh
Ollama 只监听 127.0.0.1 托盘 app 硬覆盖环境变量 netsh portproxy 内核级转发
局域网无法访问 18789 端口 UFW 默认拒绝 sudo ufw allow 18789/tcp
插件 config 注入 SecretRef 失败 TypeBox Type.String() 校验拒绝对象 明文写入 openclaw.json

七、迁移至 RTX 5080 新主机(2026-07-18)

背景

旧推理机 milin_win11(192.168.2.12,RTX 2070 Super 8GB)已就位,新主机 milin_desktop(192.168.2.10,RTX 5080 16GB)完成配置。将 OpenClaw 主力 LLM 切换至新主机的本地 Ollama。

新主机 Ollama 使用 NSSM 服务运行,直接绑定 0.0.0.0:11434,不再需要 portproxy。

坑:WiFi 网络为 Public,防火墙规则不生效

Windows 连接新 WiFi 时默认为 Public 网络配置文件。已有的防火墙规则设置了 Profile=Private,Domain,对 Public 网络无效,导致端口 11434 虽然在 netstat 中显示监听,但外部无法连接(连接超时)。

定位方法

1
2
3
# 从 N100 测试 TCP 连接
timeout 5 bash -c 'echo >/dev/tcp/192.168.2.10/11434' && echo open || echo blocked
# → blocked

修复

1
2
3
4
5
# 方案 A:将 WiFi 改为 Private(推荐)
Set-NetConnectionProfile -Name "你的WiFi名" -NetworkCategory Private

# 方案 B:将防火墙规则改为覆盖所有 Profile
Set-NetFirewallRule -DisplayName 'Ollama LAN' -Profile Any

迁移步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 更新 N100 上的 OpenClaw .env
ssh MyUbuntu "sed -i 's|OPENCLAW_LLM_API_URL=.*|OPENCLAW_LLM_API_URL=http://192.168.2.10:11434|' \
/home/milin/dockerfile/openclaw/.env"
ssh MyUbuntu "sed -i 's|OPENCLAW_LLM_MODEL=.*|OPENCLAW_LLM_MODEL=qwen3.5-35b-a3b:latest|' \
/home/milin/dockerfile/openclaw/.env"

# 2. 重启容器(.env 变更不热重载)
ssh MyUbuntu "cd /home/milin/dockerfile/openclaw && docker compose down && docker compose up -d"

# 3. 验证 N100 可达新主机 Ollama
ssh MyUbuntu "curl -s --max-time 10 http://192.168.2.10:11434/api/tags | \
python3 -c 'import json,sys; [print(m[\"name\"]) for m in json.load(sys.stdin)[\"models\"]]'"
# → qwen3.5-35b-a3b:latest

当前 .env LLM 配置

1
2
3
4
5
6
OPENCLAW_LLM_PROVIDER=ollama
OPENCLAW_LLM_API_URL=http://192.168.2.10:11434
OPENCLAW_LLM_MODEL=qwen3.5-35b-a3b:latest
OPENCLAW_LLM_FALLBACK_PROVIDER=anthropic
OPENCLAW_LLM_FALLBACK_API_KEY=...
OPENCLAW_LLM_FALLBACK_MODEL=claude-sonnet-4-6

模型 qwen3.5-35b-a3b:latest 为 Q4_K_M 量化(22 GB),从旧机局域网传输而非重新下载,首次加载约 9s。


八、从 Ollama 切换到 llama-server(2026-07-18)

背景

实测 Ollama 推理 qwen3.5-35b-a3b Q4_K_M 的 pp(prompt 处理)速度仅 39.9 tok/s,而 llama.cpp 直接运行可达 164 tok/s(提升 4x)。tg(token 生成)两者相当(~11 tok/s),瓶颈在 PCIe 带宽(模型 20.49 GiB > VRAM 16.3 GiB,约 4 GiB 在 CPU RAM)。由于我更侧重长上下文处理速度,切换到 llama-server 收益明显。

llama-server 配置(milin_desktop)

以计划任务运行,端口 11435(与 Ollama 11434 分开,方便回退):

1
2
3
4
5
6
7
@echo off
D:\llamacpp\llama-server.exe ^
-m D:\llamacpp\qwen3.5-35b-a3b-q4km.gguf ^
-ngl 99 --no-mmap -c 32768 ^
-rea off ^
--host 0.0.0.0 --port 11435 ^
>> C:\Temp\llama_server.log 2>&1

-rea off:禁用 Qwen3.5 默认开启的 thinking 模式。不禁用时 content 字段为空,全部输出在 reasoning_content,OpenClaw 收到空响应。

验证 API 可达(从 N100):

1
2
3
4
curl -s http://192.168.2.10:11435/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"any","messages":[{"role":"user","content":"hi"}],"max_tokens":30}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['choices'][0]['message']['content'])"

添加 llamacpp provider 到 openclaw.json

/home/milin/dockerfile/openclaw/openclaw.json(容器直接挂载的顶层文件)的 models.providers 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"llamacpp": {
"baseUrl": "http://192.168.2.10:11435/v1",
"apiKey": "",
"auth": "api-key",
"api": "openai-completions",
"timeoutSeconds": 180,
"models": [{
"id": "qwen3.5-35b-a3b",
"name": "Qwen 3.5 35B A3B (Q4_K_M, llama.cpp)",
"reasoning": false,
"input": ["text"],
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
"contextWindow": 32768,
"maxTokens": 8192
}]
}

同时将 agents.defaults.model.primary 改为 "llamacpp/qwen3.5-35b-a3b"

openclaw.json 内的 providers/agents 字段支持热重载,无需重启容器。

更新 .env

1
2
3
4
5
6
OPENCLAW_LLM_PROVIDER=llamacpp
OPENCLAW_LLM_API_URL=http://192.168.2.10:11435/v1
OPENCLAW_LLM_MODEL=qwen3.5-35b-a3b
OPENCLAW_LLM_FALLBACK_PROVIDER=anthropic
OPENCLAW_LLM_FALLBACK_API_KEY=<key>
OPENCLAW_LLM_FALLBACK_MODEL=claude-sonnet-4-6

.env 变更不热重载,需要 docker compose down && docker compose up -d

坑:顶层 openclaw.json 优先级高于 data/ 目录

docker-compose.yml 挂载了两层:

1
2
3
volumes:
- ./data:/home/node/.openclaw # 目录挂载(低优先级)
- ./openclaw.json:/home/node/.openclaw/openclaw.json # 文件挂载(高优先级)

文件挂载会覆盖目录挂载中的同名文件。修改 ./data/openclaw.json 无效,必须修改顶层的 ./openclaw.json

坑:Ollama 插件自动发现覆盖模型路由

OpenClaw 的 Ollama 插件会在启动时查询 Ollama 实例,自动注册发现的所有模型(如 ollama/qwen3.5-35b-a3b:latest)。当 llamacpp 配置的模型 ID 与 Ollama 自动发现的模型名匹配时,Ollama 插件注册的版本(含 :latest 标签)会在模型解析时优先于 llamacpp provider

现象:启动日志显示 agent model: llamacpp/qwen3.5-35b-a3b,但实际请求走的是 provider=ollama model=qwen3.5-35b-a3b:latest

修复:如果已切换到 llama-server,将 Ollama 插件禁用:

1
2
# 修改 /home/milin/dockerfile/openclaw/openclaw.json
d['plugins']['entries']['ollama']['enabled'] = False

此配置支持热重载,无需重启容器。

坑:会话模型选择存储在 sessions.json 和 trajectory.jsonl,不随 provider 配置热重载

OpenClaw 的 WebChat UI 每次连接都会广播当前已选中的模型(存储于浏览器 localStorage)。这个 model_change 事件会覆盖服务端的 agents.defaults.model.primary 配置,并写入 session 的 JSONL 文件中持久化。

现象:已将 agents.defaults.model.primary 设为 llamacpp/qwen3.5-35b-a3b,gateway 启动日志也显示正确,但 WebChat 发来的消息仍走 deepseek-chat(因为 UI 端 localStorage 里记的是 deepseek-chat)。

修复:在 WebChat UI 的模型选择器中手动切换到 llamacpp/qwen3.5-35b-a3b。切换后,UI 会持久化新选择,后续连接自动推送 llamacpp 模型。

llamacpp 首次调用性能说明

llama.cpp 的 KV cache 是 session 级的。第一次请求需要完整 prefill 系统提示(约 24,000 token / ~300 秒);一旦缓存预热,后续调用通过 LCP(最长公共前缀)匹配复用缓存,仅需处理新增 token

指标 冷启动(首次) 热缓存(后续)
prefill 时间 ~300 s (24K token) ~9 s (519 token)
generation 速度 3.8 tok/s 4.6 tok/s
总响应时间 ~5 分钟 ~30 秒
KV graphs reused 162 259

建议:服务启动后先发送一条消息"预热",之后正常使用响应时间约 30 秒。

当前最终配置

openclaw.json 关键变更:

  • models.providers.llamacpp:新增,baseUrl: http://192.168.2.10:11435/v1

  • agents.defaults.model.primaryllamacpp/qwen3.5-35b-a3b

  • plugins.entries.ollama.enabledfalse(避免模型路由冲突)

  • agents.defaults.timeoutSeconds600(首次冷启动需要 ~300 秒)

  • models.providers.llamacpp.timeoutSeconds600

当前 .env LLM 配置:

1
2
3
4
5
6
OPENCLAW_LLM_PROVIDER=llamacpp
OPENCLAW_LLM_API_URL=http://192.168.2.10:11435/v1
OPENCLAW_LLM_MODEL=qwen3.5-35b-a3b
OPENCLAW_LLM_FALLBACK_PROVIDER=anthropic
OPENCLAW_LLM_FALLBACK_API_KEY=<key>
OPENCLAW_LLM_FALLBACK_MODEL=claude-sonnet-4-6

九、待配置(计划中)

  • 2026-07-24deepseek-chatdeepseek-v4-flash(模型重命名)


十、TODO:llama-server 自动启停与 OpenClaw 云端 API 降级

场景

打游戏或运行高 GPU 占用任务时,需要手动停止 llama-server 释放显存。目前已在 milin_desktop 桌面部署:

  • Stop_Llama.bat → 双击停止 llama-server,释放 GPU

  • Start_Llama.bat → 双击启动 llama-server,等待就绪后弹出通知

待自动化的功能:

10.1 GPU 占用监测 → 自动停止

1
2
3
# TODO: 用 NVML / nvidia-smi 监听 GPU 利用率
# 当 GPU 占用 > 80% 且非 llama-server 进程 → 自动停止 llama-server
# 当 GPU 占用回落 < 20% 且 llama-server 未运行 → 自动重启

实现方案:Windows 计划任务 + PowerShell 脚本轮询 nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader

10.2 llama-server 不可用时自动切 OpenClaw 到云端 API

目前 OpenClaw 有 fallback 链配置(anthropic/claude-sonnet-4-6),但需要 llamacpp 调用失败后才触发,有延迟。

理想方案:llama-server 停止前,主动通知 OpenClaw 切换 provider。

1
2
3
4
5
6
7
8
9
10
# 方案 A:修改 openclaw.json 的 primary model(需容器内执行)
ssh MyUbuntuLocal "python3 -c \"
import json
with open('/home/milin/dockerfile/openclaw/openclaw.json') as f: d = json.load(f)
d['agents']['defaults']['model']['primary'] = 'deepseek/deepseek-chat'
open('/home/milin/dockerfile/openclaw/openclaw.json','w').write(json.dumps(d,indent=2))
\""
# openclaw.json 的 agents.defaults 字段支持热重载,无需重启容器

# 方案 B:通过 OpenClaw WebSocket/REST API 推送 model_change 事件(待研究 API)

10.3 OpenClaw 调用时检测 llama-server 状态 → 自动唤醒

当 OpenClaw 有请求到来但 llama-server 未运行时:

  1. 检测 http://192.168.2.10:11435/health 失败

  2. 触发 schtasks /Run /TN LlamaServer

  3. 轮询等待就绪(约 15-30 秒,模型加载)

  4. 请求继续走 llamacpp

实现方案:llama-server 前置代理(nginx/caddy 或轻量 Python 脚本),拦截 /v1/* 请求并按需唤醒。

1
2
3
OpenClaw → N100:11435_proxy → 检测 milin_desktop:11435 状态
↓ 未就绪 → SSH 触发 schtasks LlamaServer → 等待 → 转发
↓ 就绪 → 直接透传

延伸阅读