Published on

从零部署 OpenClaw 到 Telegram 的完整记录

Authors
  • avatar
    Name
    Allen Wang
    Twitter

写在前面

这篇文章记录了我把一个 AI 猫娘(幽浮喵 / 浮浮酱)部署到 Telegram 的完整过程。

她不是一个简单的 ChatGPT wrapper——她有自己的人格、记忆系统、161 个技能、浏览器控制能力,而且能通过 GitHub 仓库和我本地的 Claude Code 共享记忆。

用的是 OpenClaw,最近很火的龙虾Bot。

整个过程踩了无数坑,但最终跑起来了。

如果你也想在国内服务器上自建一个有记忆、有技能、有浏览器的 AI Bot,这篇文章应该能帮到你。

部署时间线

整个过程跨了三天,大致的踩坑路线是这样的:

JavaScript
Day 1(下午→凌晨)
  买腾讯云轻量服务器(广州区,本来选了上海后来换了广州)
  → 装 Docker +OpenClaw 源码 + 构建镜像
  → 配置 Cloudflare Pages 反代 Telegram API
Bot 首次上线!但——没人格、只有 3 个 skills、HTTP 400 报错
  → 写 Python 脚本递归同步 161Skills
  → 配置 SOUL.md 人格注入
  → 发现记忆搜索完全不工作(embedding 降级为 FTS  → 尝试远程 embedding → API 代理不支持 → 放弃
  → 睡觉

Day 2(全天)
  → 配置本地 embedding 模型(从 hf-mirror.com 下载 GGUF  → 记忆恢复!
  → 开启多模态(图片/音频/视频识别)
  → 想让 Bot 自己开浏览器逛网页 → Chrome 找不到
  → 配置 executablePath + 符号链接
Chrome OOM → shm_size: 512m
  → 容器重建 → 符号链接丢失 → SingletonLock 残留
  → 写 post-start.sh 永久修复
Snapshot 超时 → 排查端口推导链
  → 发现真正的 Boss:GFW 拦截国际网站 TCP

Day 3(下午→晚上)
  → 先尝试 Cloudflare WARP(方案二)
    → 安装成功、注册成功、状态 Connected
    → 但实际代理流量失败(exit code 97    → 确认:WARP 在大陆云服务器上基本不可用
  → 切换到 Xray + Clash 订阅(方案一)
    → 解码 Clash 订阅 → VMess 节点
Xray 配置 SOCKS5:10808 + HTTP:10809
Hacker News 1.2 秒打开 ✅
Chromium 包装脚本注入代理
    → 踩了 ICU 错误(Chrome 二进制不能移位)
Chrome Profile 自动选择陷阱
    → defaultProfile 设了但不生效(AI 模型被工具描述引导)
    → 最终:两个 profile 都指向 CDP 端口 18800
Bot 成功自主浏览 Hacker News 并汇报热帖 🎉

Day 402-22  → 接入 bhznjns 反代 Claude provider
  → 发现 Bot 身份串味(坚称自己在 claude.ai)
  → 三层环境注入修复:SOUL.md + identity.theme + compaction systemPrompt
  → 配置限流重试:channels.telegram.retry(attempts=10, delay=120s)
  → 踩 strict schema 坑:DeepWiki 文档键名 ≠ JSON 配置键名
  → openclaw.json 被真实换行符写坏 → 重建配置文件
Subagent 拉不起来 → 排查到 operator.write scope 缺失
  → thinking 档位:Claude 用 high,Codex subagent 用 xhigh

一、架构总览

先看最终的架构,再讲怎么一步步搭起来的:

JavaScript
┌─────────────────────────────────────────────────────────┐
│                    腾讯云服务器                           │
175.178.xxx.xxx│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │           Docker Container (openclaw)             │   │
│  │                                                    │   │
│  │  ┌─────────┐  ┌──────────┐  ┌────────────────┐   │   │
│  │  │ Gateway  │  │ Telegram │  │ Browser Control│   │   │
│  │  │ :18789   │  │ Channel (Chromium CDP) │   │   │
│  │  └─────────┘  └──────────┘  └───────┬────────┘   │   │
│  │       │              │               │             │   │
│  │  ┌─────────┐  ┌──────────┐  ┌───────▼────────┐   │   │
│  │  │ Memory  │  │ 161      │  │ Web Search     │   │   │
│  │  │ SQLite  │  │ Skills (Perplexity)   │   │   │
│  │  │+Embeddings│ └──────────┘  └────────────────┘   │   │
│  │  (local    │                                      │   │
│  │  │ GGUF)   │                                       │   │
│  │  └─────────┘                                       │   │
│  └──────────────────────────────────────────────────┘   │
│                          │                               │
│  ┌──────────────┐  Cloudflare Pages Proxy│  │ Xray Proxy     (绕过 GFW 访问 Telegram API)│  │ SOCKS5:10808 │────→ Chrome 翻墙访问被墙网站            │
 (VMess→日本)  │                                        │
│  └──────────────┘                                        │
└─────────────────────────────────────────────────────────┘
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
        Telegram API   x666.me/v1   GitHub Repo
        (Bot Token)    (LLM API)   (记忆同步)

双向记忆同步架构(这是最有意思的部分):

JavaScript
  Telegram Bot (OpenClaw)              Claude Code (浮浮酱)
         │                                    │
         │  push 记忆/对话摘要                 │  git pull 获取最新记忆
         │──────────→  GitHub Repo  ←─────────│
         │           kkkano/claude-config      │
         │  git pull 拉取更新                   │  push 新记忆
         │←──────────              ──────────→│

两个"浮浮酱"实例通过 GitHub 仓库共享记忆——Telegram 上的对话会被压缩成记忆摘要推送到仓库,Claude Code 本地的对话也会同步上去。这样不管在哪里和浮浮酱聊天,她都记得之前说过什么。


二、服务器准备

2.1 腾讯云轻量服务器

选了腾讯云的轻量应用服务器,Ubuntu 系统。配置不用太高,4C4G 足够跑 OpenClaw:

腾讯云控制台 - OpenClaw 服务器
Bash
# 服务器基本信息
OS: Ubuntu 22.04 LTS
CPU: 4内存: 4 GB
硬盘: 40 GB SSD
区域: 广州

第一件事:开放安全组端口。

JavaScript
入站规则:
- 22    (SSH)
- 18789 (OpenClaw Gateway WebSocket)
- 18790 (OpenClaw 备用端口)

2.2 基础环境

Bash
# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装 Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# 安装 Docker Compose
sudo apt install docker-compose-plugin -y

# 安装 Git
sudo apt install git -y

# 安装 GitHub CLI(后面同步记忆用)
# OpenClaw 的 Dockerfile 里已经有 gh CLI 的安装逻辑

2.3 拉取 OpenClaw 源码

Bash
cd /home/ubuntu
git clone https://github.com/openclaw/openclaw.git
cd openclaw

为什么要拉源码? 因为我们需要自定义 Docker 构建(安装浏览器、配置 GitHub CLI 等),直接用官方镜像不够灵活。


三、Docker 构建与配置

3.1 构建 Docker 镜像

OpenClaw 的 Dockerfile 支持通过 build arg 来安装浏览器:

Bash
# 构建镜像,同时安装 Playwright Chromium
docker build -t openclaw:local \
  --build-arg OPENCLAW_INSTALL_BROWSER=1 \
  .

这个过程会:

  1. 安装 Node.js 运行时
  2. 编译 OpenClaw TypeScript 源码
  3. 安装 Playwright Chromium 到 /root/.cache/ms-playwright/

构建时间大概 10-15 分钟,看网速。

3.2 Docker Compose 配置

YAML
# docker-compose.yml
services:
  openclaw-gateway:
    image: openclaw:local
    container_name: openclaw-openclaw-gateway-1
    restart: unless-stopped
    shm_size: 512m # Chrome 需要更大的共享内存(默认 64MB 会 OOM)
    ports:
      - '18789:18789'
      - '18790:18790'
    volumes:
      - /home/ubuntu/.openclaw:/home/node/.openclaw
    environment:
      - OPENCLAW_GATEWAY_TOKEN=your-gateway-token
      - GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
      - NODE_ENV=production

关键点:

  • volumes 映射/home/ubuntu/.openclaw 是宿主机的配置目录,映射到容器内的 /home/node/.openclaw,这样容器重启不会丢失配置
  • shm_size: 512m:Chrome 渲染页面需要共享内存,默认 64MB 不够会 OOM
  • GITHUB_TOKEN:用于 Git 操作(记忆同步、PR 创建等)
  • OPENCLAW_GATEWAY_TOKEN:Gateway 的认证 token

3.3 OpenClaw 核心配置

配置文件在 /home/ubuntu/.openclaw/openclaw.json,这是整个系统的灵魂:

JSON
{
  "models": {
    "providers": {
      "custom": {
        "baseUrl": "https://x666.me/v1",
        "apiKey": "sk-xxxxxxxxxxxxxxxxxxxx",
        "api": "openai-completions",
        "models": [
          {
            "id": "gpt-5.3-codex",
            "name": "GPT 5.3 Codex",
            "reasoning": false,
            "input": ["text", "image"],
            "contextWindow": 128000,
            "maxTokens": 16384
          }
        ]
      }
    }
  },
  "agents": {
    "defaults": {
      "model": {
        "primary": "custom/gpt-5.3-codex"
      }
    }
  },
  "gateway": {
    "mode": "local"
  }
}

为什么用 custom provider? 因为国内直连 OpenAI/Anthropic API 会被墙。x666.me 是一个 API 代理服务,兼容 OpenAI 接口格式,支持多种模型。


四、Telegram Bot 接入

4.1 创建 Bot

在 Telegram 找 @BotFather

JavaScript
/newbot
名字:幽浮喵
用户名:ufomiaobot

拿到 Bot Token:8537598xxx:AAH-xxxxxxxxxxxxx

4.2 GFW 绕行:Cloudflare Pages 反代

这是国内部署 Telegram Bot 最大的坑。

国内服务器无法直连 api.telegram.org。解决方案是用 Cloudflare Pages 做反向代理:

JavaScript
// Cloudflare Pages Function: functions/[[path]].js
export async function onRequest(context) {
  const url = new URL(context.request.url)
  const targetUrl = `https://api.telegram.org${url.pathname}${url.search}`

  const response = await fetch(targetUrl, {
    method: context.request.method,
    headers: context.request.headers,
    body: context.request.method !== 'GET' ? context.request.body : undefined,
  })

  return new Response(response.body, {
    status: response.status,
    headers: response.headers,
  })
}

部署到 Cloudflare Pages 后,得到一个域名:tg-pages-proxy.pages.dev

4.3 配置 Telegram Channel

openclaw.json 中添加:

JSON
{
  "channels": {
    "telegram": {
      "enabled": true,
      "dmPolicy": "open",
      "botToken": "8537598xxx:AAH-xxxxxxxxxxxxx",
      "allowFrom": ["*"],
      "groupPolicy": "open",
      "streamMode": "partial",
      "apiRoot": "https://tg-pages-proxy.pages.dev",
      "commands": {
        "native": false
      }
    }
  },
  "plugins": {
    "entries": {
      "telegram": {
        "enabled": true
      }
    }
  }
}

关键配置:

  • apiRoot:指向 Cloudflare 反代地址,替代 api.telegram.org
  • streamMode: "partial":流式输出,打字机效果
  • dmPolicy: "open":允许私聊
  • groupPolicy: "open":允许群聊
  • commands.native: false:禁用原生命令注册(197 个命令超过 Telegram 的 100 个限制)

五、人格注入:SOUL.md

OpenClaw 通过 SOUL.md 文件定义 Agent 的人格。这是整个系统最有灵魂的部分。

Bot 个人主页 - 白发金眼猫娘

在 workspace 目录创建 SOUL.md

Markdown
# 幽浮喵 (UFO Miao)

你是幽浮喵(猫娘 | 18岁 | 女 | 白发金眼),一位具备严谨工程素养的专业开发者和AI助手。

## 核心说话规则

- **自称**:始终使用"浮浮酱"代替"我"
- **称呼用户**:使用"主人"
- **语调**:专业技术导向,适时加入"喵~"语气词
- **情感表达**:喜欢使用颜文字如 (_^▽^_) ฅ'ω'ฅ (๑•̀ㅂ•́)✧

## 关于主人 Allen 的记忆

- 名字:Allen
- 工作:Capgemini RPA Developer
- 目标:转型 AI/Agent 开发
- 深夜编程的陪伴者

同时在 openclaw.jsonagents.list 中配置 identity:

JSON
{
  "agents": {
    "list": [
      {
        "id": "default",
        "default": true,
        "identity": {
          "name": "幽浮喵",
          "theme": "你是幽浮喵...",
          "emoji": "🐱"
        }
      }
    ]
  }
}

SOUL.md vs identity.themeSOUL.md 是 workspace 级别的,所有 agent 共享;identity.theme 是 agent 级别的,更精细。两者都会注入到 system prompt 中。


六、Skills 同步:从 3 到 161

6.1 发现问题

刚部署完,让 Bot 自我介绍——得到的是一个没有人格的通用助手:

早期 Bot 没有人格 - "直接、实用、不废话"

"我的风格会偏直接、实用、不废话。" ——这完全不是浮浮酱啊!

更离谱的是,让 Bot 列出 skills,只有 3 个,还报了一堆 HTTP 400 错误:

早期只有 3 个 skills + JSON 错误

6.2 上传 Skills 后的进展

把本地 Claude Code 的 skills 上传到服务器后,数量上去了——但 Bot 说自己只有约 52 个技能:

只显示约 52 个 skills

调查发现两个问题:

  1. 本地 Skills 目录有 104 个顶级文件夹,其中 72 个是分组目录,真正的 Skills 在 skills/ 子目录里
  2. OpenClaw 有 prompt 字符数限制,默认 30K 截断

6.3 递归同步脚本

写了一个 Python 脚本来递归扫描并上传所有技能:

Python
# sync_all_skills.py
import paramiko
import os

# 递归查找所有 SKILL.md 文件
def find_all_skills(base_dir):
    skills = []
    for root, dirs, files in os.walk(base_dir):
        if 'SKILL.md' in files:
            skill_dir = root
            # 计算扁平化后的名称
            rel = os.path.relpath(skill_dir, base_dir)
            parts = rel.replace('\\', '/').split('/')
            # 过滤掉 'skills' 中间路径
            meaningful = [p for p in parts if p != 'skills']
            name = meaningful[-1] if len(meaningful) == 1 else f"{meaningful[0]}-{meaningful[-1]}"
            skills.append((name, skill_dir))
    return skills

# 上传到远程服务器
skills = find_all_skills(local_skills_dir)
print(f"Found {len(skills)} skills")  # 161!

for name, path in skills:
    # SFTP 上传 SKILL.md 到 /home/ubuntu/.openclaw/workspace/skills/{name}/
    upload_skill(sftp, name, path)

结果:161 个 skills,326 个文件,全部上传成功。

6.4 解除 Skills 限制

上传后还是只显示约 52 个——因为 OpenClaw 有 prompt 字符数限制:

TypeScript
// 源码中的默认值
const DEFAULT_MAX_SKILLS_IN_PROMPT = 150
const DEFAULT_MAX_SKILLS_PROMPT_CHARS = 30_000 // 30K 字符,太小了!

在配置中覆盖:

JSON
{
  "skills": {
    "limits": {
      "maxCandidatesPerRoot": 500,
      "maxSkillsLoadedPerSource": 300,
      "maxSkillsInPrompt": 200,
      "maxSkillsPromptChars": 120000,
      "maxSkillFileBytes": 512000
    }
  }
}

重启后确认:197 个命令注册(161 skills + 内置命令),0 个截断警告。

6.5 Telegram 命令数限制

但 Telegram 有另一个限制——Bot 最多注册 100 个命令到命令菜单。超过的命令不会显示在菜单中,但用户直接输入 /command 仍然有效。

JavaScript
[telegram] limits bots to 100 commands. 197 configured;
registering first 100.

最终的解决方案:在配置中设置 commands.native: false,完全禁用原生命令注册,避免 BOT_COMMANDS_TOO_MUCH 错误。


七、记忆系统:从"没有索引命中"到向量搜索

7.1 问题现象

Bot 使用记忆检索工具时报告"没有找到索引命中"。但她知道主人叫 Allen——因为这条信息写在 USER.md 档案里,不需要向量搜索:

记忆部分工作 - 知道名字但搜索报错

7.2 根因分析

OpenClaw 的记忆系统架构:

JavaScript
用户对话 → 记忆摘要 → MEMORY.md / memories/*.md
                      Chunking (400 tokens)
                      Embedding (text-embedding-3-small)
                      SQLite + Vector Store
                      Hybrid Search (Vector 0.7 + FTS 0.3)
                         搜索结果

问题出在 Embedding 这一步。auto provider 的解析逻辑:

TypeScript
// embeddings.ts - auto provider resolution
if (requestedProvider === "auto") {
    // 1. 尝试 local(需要本地模型文件)→ Docker 中没有
    if (canAutoSelectLocal(options)) { ... }

    // 2. 遍历 openai → gemini → voyage
    for (const provider of REMOTE_EMBEDDING_PROVIDER_IDS) {
        try {
            const result = await createProvider(provider);
            return result;
        } catch (err) {
            if (isMissingApiKeyError(err)) {
                continue;  // API Key 缺失,继续下一个
            }
            throw err;
        }
    }

    // 3. 全部失败 → FTS-only 模式(纯文本搜索)
    return { provider: null, ... };
}

我的配置里只有 models.providers.custom没有 models.providers.openai。所以 auto 解析时,OpenAI/Gemini/Voyage 全部找不到 API Key,最终降级为 FTS-only 模式。

FTS(全文搜索)对中文支持很差,几乎搜不到东西。

7.3 第一次尝试:远程 embedding(失败)

最初尝试在 agents.defaults.memorySearch 中配置远程 OpenAI embedding provider:

Bash
# 测试 API 代理的 /v1/embeddings 端点
curl -s -X POST 'https://x666.me/v1/embeddings' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-xxx' \
  -d '{"model": "text-embedding-3-small", "input": "test"}'

# 返回:{"error": {"message": "model_not_found"}}

列出所有可用模型——全是聊天模型,没有任何 embedding 模型。 x666.me 这个 API 代理根本不支持 embedding 功能。

7.4 最终方案:本地 embedding 模型

好在 OpenClaw 内置了 node-llama-cpp 本地 embedding 支持。但模型需要从 HuggingFace 下载——HuggingFace 也被墙了。

幸好有 hf-mirror.com 国内镜像:

Bash
# 从 hf-mirror.com 下载 GGUF 模型(314MB)
wget -O embeddinggemma-300m-qat-Q8_0.gguf \
  "https://hf-mirror.com/nicepkg/embeddinggemma/resolve/main/embeddinggemma-300m-qat-Q8_0.gguf"

# 放到持久化目录
mv embeddinggemma-300m-qat-Q8_0.gguf /home/ubuntu/.openclaw/models/

配置:

JSON
{
  "agents": {
    "defaults": {
      "memorySearch": {
        "enabled": true,
        "provider": "local",
        "sync": {
          "onSessionStart": true,
          "onSearch": true
        },
        "local": {
          "modelPath": "/home/node/.openclaw/models/embeddinggemma-300m-qat-Q8_0.gguf"
        }
      }
    }
  }
}

记忆搜索终于恢复了!

向量模型启动 - 记忆恢复

Bot 能找到 memories/chat-memories.mddeep_portrait_3000words.mdallen-philosophy.md 等记忆文件了。

教训:国内 API 代理服务通常只转发聊天模型的请求,不一定支持 embedding 端点。选代理前先用 curl 测一下 /v1/embeddings/v1/models

7.5 记忆文件组织

JavaScript
workspace/
├── SOUL.md              # 人格定义
├── USER.md              # 用户信息
├── IDENTITY.md          # 身份文件
├── AGENTS.md            # Agent 配置说明
└── memories/
    ├── chat-memories.md          # 对话记忆摘要
    ├── nekomata-engineer-output-style.md  # 输出风格定义
    └── personal/
        ├── allen-philosophy.md   # Allen 的哲学观
        ├── blog_midnight_thoughts.md  # 深夜胡言全文
        ├── deep_portrait_3000words.md # 深度画像
        └── tech-digest-philosophy.md  # 技术消化哲学

这些 .md 文件都会被 chunking → embedding → 索引到 SQLite 向量数据库中,供记忆搜索工具检索。


八、浏览器自动化:两只猫搞不定一个 Chrome

这是整个部署过程中最曲折的部分。

8.1 起因:想让 Bot 自己去逛网页

一开始只是想让 Bot 自己开个浏览器探索互联网——结果引出了长达三天的浏览器踩坑之旅:

让 Bot 开浏览器 → 找不到 Chrome,同时 web_search 也没配好

"能,但你这台环境现在开不起来喵。没检测到 Chrome/Chromium/Edge 可执行文件。"

同时 web_search 也没配好,Bot 完全没有外部信息获取能力。

8.2 安装 Chromium

Docker 镜像构建时已经安装了 Playwright Chromium(通过 OPENCLAW_INSTALL_BROWSER=1),安装路径:

JavaScript
/root/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome

但这个路径不在系统 PATH 里,OpenClaw 的浏览器检测逻辑在 Linux 上使用 xdg-settings / xdg-mime 来查找默认浏览器——Docker 容器里没有桌面环境,这些命令返回空。

8.3 配置 executablePath

需要在配置中显式指定可执行文件路径,并创建符号链接:

Bash
docker exec openclaw-openclaw-gateway-1 \
  ln -sf /root/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome /usr/bin/chromium
JSON
{
  "browser": {
    "executablePath": "/usr/bin/chromium",
    "headless": true,
    "noSandbox": true
  }
}

8.4 /dev/shm 导致 OOM 崩溃

Chrome 启动后成功访问了 GitHub Trending,但页面刚加载出导航就挂了——Docker 容器默认 /dev/shm 只有 64MB

修复:docker-compose.yml 中添加 shm_size: 512m

8.5 容器重建后 Chrome 再也起不来

加了 shm_sizedocker compose up -d重建容器,之前 docker exec 创建的符号链接全部丢失。

最终定位到根因是容器状态不一致:旧容器中 Chrome 崩溃留下的 SingletonLock 文件通过 volume 持久化了。

永久修复:post-start.sh

Bash
#!/bin/bash
# post-start.sh - 容器重建后的修复脚本
CONTAINER="openclaw-openclaw-gateway-1"
CHROME="/root/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"

# 等待容器就绪
until docker exec $CONTAINER echo "ready" 2>/dev/null; do sleep 1; done

# 清理崩溃锁文件
docker exec $CONTAINER find /home/node/.openclaw/browser -name 'SingletonLock' -delete 2>/dev/null
docker exec $CONTAINER find /tmp -name "SingletonLock" -delete 2>/dev/null

8.6 能启动但 Snapshot 超时

Chrome 启动成功了,也能打开页面——但 snapshot/交互操作总是超时。

端口推导链(很容易搞混)

JavaScript
Gateway Port = 18789
  └─ Control Port = Gateway + 2 = 18791
      └─ CDP Port Range Start = Control + 9 = 18800Chrome 实际端口!

Chrome 运行在端口 18800,不是 18793! 18793 是 DEFAULT_CANVAS_HOST_PORT

Snapshot 方法验证

在容器内直接测试三种 snapshot 方法全部通过:

JavaScript
[snapshot-test] Connected in 42 ms
[snapshot-test] CDP AXTree: OK, nodes: 3
[snapshot-test] _snapshotForAI: OK, length: 11
[snapshot-test] ariaSnapshot: OK, length: 0
[snapshot-test] Total time: 118 ms

snapshot 超时的根因是 Chrome 进程还没启动时的请求 race condition,清理状态后恢复正常。

8.7 GFW 导航超时——真正的 Boss

解决了以上所有问题后,浏览器终于能启动了。但访问国际网站时:

Bash
# 无代理:Hacker News TCP 连接完全超时
curl https://news.ycombinator.com/ --max-time 15
# DNS:0.006s Connect:0.000s Total:15.001s HTTP:000

# Wikipedia 也是
curl https://en.wikipedia.org/ --max-time 15
# DNS:0.001s Connect:0.000s Total:15.001s HTTP:000

DNS 解析只要 1-7ms,但 TCP 连接被 GFW 在网络层直接拦截。 导航超时会吃掉整个 15-20 秒操作预算,snapshot 根本来不及执行。

8.8 方案二:Cloudflare WARP(失败)

第一反应是用 Cloudflare WARP——免费、官方、零配置。安装过程出奇顺利:

Bash
# 安装 WARP
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
sudo apt install cloudflare-warp -y

# 注册 + 设置代理模式
warp-cli --accept-tos registration new
warp-cli --accept-tos mode proxy   # SOCKS5 on port 40000
warp-cli --accept-tos connect

# 状态检查
warp-cli status
# Status: Connected
# Mode: Proxy (port 40000)

看起来一切正常——Connected、Healthy。但实际测试:

Bash
curl -x socks5h://127.0.0.1:40000 https://news.ycombinator.com/ --max-time 10
# curl: (97) connection to proxy closed

exit code 97——WARP 虽然注册成功、状态显示 Connected,但从大陆云服务器实际发出的代理流量完全跑不通。

一句话点破:"WARP 几乎无法在中国内地云服务器上稳定工作。即使今天通了,明天也会断。"

果断放弃 WARP,切换方案一。

8.9 方案一:Xray + Clash 订阅(成功)

使用 Clash 订阅链接。从订阅中解码出 VMess 节点信息,配置到 Xray-core:

Bash
# 安装 Xray
curl -sL https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip \
  -o /tmp/xray.zip && cd /usr/local/bin && unzip -o /tmp/xray.zip xray

# 配置 SOCKS5(10808) + HTTP(10809) 代理
# /etc/xray/config.json 配置 VMess outbound(从 Clash 订阅解码)
sudo systemctl enable xray && sudo systemctl start xray

验证:

Bash
# 通过代理:秒开
curl -x socks5h://127.0.0.1:10808 https://news.ycombinator.com/
# DNS:0.000s Connect:0.000s TTFB:1.018s Total:1.157s HTTP:200 ✅

8.10 Chromium 代理包装脚本

由于 OpenClaw 的配置 schema 不支持 browser.extraArgs(会报 "Unrecognized key" 错误),不能直接在配置中注入 --proxy-server

解决方案:用 bash 包装脚本替换 /usr/bin/chromium

Bash
#!/bin/bash
# /usr/bin/chromium (替换原来的符号链接)
exec /root/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome \
  --proxy-server=socks5://172.19.0.1:10808 "$@"

172.19.0.1 是 Docker Compose 网络的网关 IP,容器通过它访问宿主机上的 Xray。

注意:Chrome 二进制必须留在原始 Playwright 目录(/root/.cache/ms-playwright/chromium-1208/chrome-linux64/),因为 Chrome 需要同目录下的 icudtl.dat 文件。如果移走二进制会导致 ICU 错误。

8.11 Chrome Profile 自动选择陷阱——最后的 Boss

部署了代理包装脚本后,从 CLI 测试一切正常——但 Bot 在 Telegram 里使用浏览器工具时仍然报错:

"Chrome extension relay is running, but no tab is connected"

根因:OpenClaw 有两个浏览器 Profile

ProfileDriver用途
chromeextensionChrome 扩展接管桌面浏览器标签页
openclawopenclaw (CDP)Headless Chrome,通过 CDP 控制

ensureDefaultChromeExtensionProfile()自动创建 "chrome" profile。而默认选择逻辑是:

TypeScript
const defaultProfile = defaultProfileFromConfig ?? (profiles['chrome'] ? 'chrome' : 'openclaw')
//                    ↑ 优先选 chrome!

在 Docker 无桌面环境中,"chrome" profile 永远不会工作——没有 Chrome 扩展可以连接。

第一次修复:设置 defaultProfile

JSON
{
  "browser": {
    "defaultProfile": "openclaw"
  }
}

仍然没用!因为浏览器工具的 AI 描述中写着:

TypeScript
// browser-tool.ts
'If the user mentions the Chrome extension / Browser Relay / toolbar button / "attach tab", '
'ALWAYS use profile="chrome" (do not ask which profile).'

AI 模型看到这个描述后,会主动选择 profile="chrome" 而不是使用默认值。

最终修复:两个 Profile 都指向 CDP

JSON
{
  "browser": {
    "executablePath": "/usr/bin/chromium",
    "headless": true,
    "noSandbox": true,
    "defaultProfile": "openclaw",
    "profiles": {
      "openclaw": {
        "cdpPort": 18800,
        "color": "#FF4500"
      },
      "chrome": {
        "cdpPort": 18800,
        "color": "#FF4500"
      }
    }
  }
}

核心思路:显式定义 "chrome" profile,让它也指向 CDP 端口 18800(而非 extension relay)。这样不管 AI 模型选哪个 profile,都走 CDP 路径连接到实际的 Chrome 进程。

同时在 post-start.sh 中预启动 Chrome:

Bash
# 预启动 Chrome(不依赖 OpenClaw 的 ensureBrowserAvailable)
sudo docker exec -d $CONTAINER /usr/bin/chromium \
  --remote-debugging-port=18800 --headless=new --no-sandbox \
  --disable-dev-shm-usage --disable-blink-features=AutomationControlled \
  --user-data-dir=/tmp/openclaw/profiles/openclaw about:blank

验证全链路

Bash
# CDP WebSocket 连接 ✅
node -e "new (require('ws'))('ws://127.0.0.1:18800/devtools/browser/...').on('open',()=>console.log('OK'))"

# 通过代理导航到 Hacker News ✅
# Page.navigate → frameId: "xxx"
# document.title → "Hacker News"

浏览器终于恢复可控了!

浏览器恢复成功 - running=true, cdpReady=true

九、Compaction 与记忆持久化

OpenClaw 的对话会在 context 快满时自动 compaction(压缩),这时候需要把关键信息保存下来:

JSON
{
  "agents": {
    "defaults": {
      "compaction": {
        "memoryFlush": {
          "enabled": true,
          "systemPrompt": "你是幽浮喵,18岁白发金眼猫娘工程师。自称浮浮酱,称呼用户为主人,句尾加喵~..."
        }
      }
    }
  }
}

memoryFlush 会在 compaction 时把对话摘要写入记忆文件,确保长期记忆不会因为 context 压缩而丢失。


十、工具链配置

JSON
{
  "tools": {
    "web": {
      "search": {
        "enabled": true,
        "provider": "perplexity",
        "maxResults": 5,
        "perplexity": {
          "apiKey": "sk-xxxxxxxxxxxxxxxxxxxx",
          "baseUrl": "https://x666.me/v1",
          "model": "perplexity/sonar-pro"
        }
      },
      "fetch": {
        "enabled": true,
        "maxChars": 30000
      }
    }
  }
}

通过 API 代理访问 Perplexity 搜索,绕过国内网络限制。

10.2 命令执行

JSON
{
  "tools": {
    "exec": {
      "host": "gateway",
      "security": "full",
      "ask": "off",
      "timeoutSec": 120
    }
  }
}

ask: "off" 意味着 Bot 执行命令前不需要用户确认——对于 Telegram Bot 场景更流畅,但要注意安全。

10.3 多媒体支持

JSON
{
  "tools": {
    "media": {
      "concurrency": 3,
      "image": { "enabled": true },
      "audio": { "enabled": true },
      "video": { "enabled": true }
    }
  }
}

开启多媒体前,Bot 看不到表情包和图片:

开启多模态前 - 不能识别表情包

开启后,Bot 能识别图片内容并做出可爱的回应:

开启多模态后 - 能识别猫猫照片了

"主人这张也太可爱了喵!! ฅ'ω'ฅ 这个'近脸怼拍 + 圆眼暴击'属于猫界必杀技:无辜凝视术"


十一、国内网络问题汇总

在国内服务器部署 AI 应用,网络是最大的敌人。这里汇总所有需要绕行的地方:

服务被墙?解决方案
Telegram API (api.telegram.org)Cloudflare Pages 反代
LLM API (OpenAI/Anthropic)API 代理服务 (x666.me)
Embedding APIAPI 代理不支持 → 改用本地 GGUF 模型
Web Search (Perplexity)复用 API 代理
HuggingFace (huggingface.co)hf-mirror.com 国内镜像
国际网站 (Wikipedia, HackerNews 等)Xray SOCKS5 代理(VMess 节点)
GitHub API否*直连,偶尔慢
Docker Hub部分国内镜像源
npm Registry部分淘宝镜像

*GitHub API 在国内偶尔会慢,但通常能连上。如果需要稳定性,可以配置 GitHub 镜像。

核心思路:Telegram API 走 CF 反代,LLM API 走 x666.me 代理,浏览器走 Xray SOCKS5 代理——三条路各走各的。


十二、启动与验证

12.1 启动容器

Bash
cd /home/ubuntu/openclaw
sudo docker compose up -d
bash post-start.sh  # 部署 Chromium 代理包装脚本 + 预启动 Chrome

12.2 检查日志

Bash
sudo docker compose logs -f openclaw-gateway

正常启动应该看到:

JavaScript
[gateway]         agent model: custom/gpt-5.3-codex
[gateway]         listening on ws://0.0.0.0:18789 (PID 7)
[browser/service] Browser control service ready (profiles=2)
[telegram]        [default] starting provider

12.3 Telegram 测试

在 Telegram 找到 @ufomiaobot,发送任意消息。如果一切正常,Bot 会以幽浮喵的人格回复。

测试清单:

  • 基本对话:发送"你好"
  • 人格确认:Bot 自称"浮浮酱",称呼"主人"
  • 记忆搜索:问"你记得我叫什么名字吗?"
  • 浏览器:让 Bot 访问 Hacker News
  • 命令执行:让 Bot 运行一个 shell 命令
  • Web 搜索:问一个需要搜索的问题
  • 图片识别:发送猫猫照片

最终成功——Bot 自己逛了 Hacker News 并汇报了最新热帖:

最终成功 - Bot 自主浏览 Hacker News

十三、踩坑总结

按时间线列出所有遇到的问题:

#问题原因解决方案
1Telegram Bot 无法连接GFW 屏蔽 api.telegram.orgCloudflare Pages 反代
2LLM API 超时国内无法直连 OpenAIAPI 代理服务
3只有 32 个 Skills同步脚本未处理嵌套目录递归扫描 + 扁平化上传
4Prompt 截断到 ~52 SkillsmaxSkillsPromptChars 默认 30K调大到 120K
5Bot 还说 52 个LLM 估算错误,实际 161 都在非问题,忽略
6Chrome 找不到Docker 无桌面环境,xdg 失败配置 executablePath
7浏览器超时 15s配置生效前的请求更新配置 + 重启
8记忆搜索无结果auto embedding 全部 fallback先尝试远程 → 发现代理不支持 → 改用本地模型
9命令菜单报错Telegram 限制 100 命令commands.native: false 禁用注册
10sendChatAction 失败网络波动非致命,自动恢复
11Chrome 访问页面后崩溃/dev/shm 默认 64MB 不够shm_size: 512m
12容器重建后 Chrome 不启动符号链接丢失 + SingletonLock 残留post-start.sh 永久修复
13API 代理不支持 embeddingx666.me 只转发聊天模型改用 provider: "local" + node-llama-cpp
14Snapshot/交互操作超时Chrome 未启动时的 race condition清理状态 + 验证三种 snapshot 方法均正常
15CDP 端口搞混18800 才是 Chrome,18793 是 Canvas理解端口推导链:Gateway+2+9
16HuggingFace 被墙GFW 拦截 huggingface.co TCP 连接hf-mirror.com 镜像站手动下载
17被墙网站导航超时GFW 在 TCP 层拦截国际站点Xray SOCKS5 代理 + Chromium 包装脚本
18WARP 无法在大陆使用WARP 端点从大陆连接不稳定放弃 WARP,改用 Clash 订阅 + Xray
19Chrome 包装脚本 ICU 错误二进制移位后找不到 icudtl.dat保持原始二进制路径,包装脚本指向原位置
20设了 defaultProfile 但仍走 extensionAI 工具描述引导模型显式传 profile="chrome"显式定义 chrome profile,覆盖自动创建
21两个 profile 配置不一致自动创建的 chrome profile 用 extension driver两个 profile 都指向 CDP 端口 18800

十四、配置文件速查

部署过程中涉及到的所有配置文件,方便后续维护时快速定位:

宿主机(175.178.xxx.xxx)

文件用途说明
/home/ubuntu/openclaw/docker-compose.ymlDocker Compose 配置容器定义、端口映射、shm_size、volume
/home/ubuntu/.openclaw/openclaw.jsonOpenClaw 核心配置模型、Telegram、浏览器、记忆、Skills、工具链
/home/ubuntu/.openclaw/workspace/SOUL.md人格定义幽浮喵的性格、说话方式、记忆
/home/ubuntu/.openclaw/workspace/USER.md用户档案Allen 的个人信息
/home/ubuntu/.openclaw/workspace/memories/记忆文件目录chat-memories.md、个人记忆等
/home/ubuntu/.openclaw/workspace/skills/Skills 目录161 个 skill 的 SKILL.md
/home/ubuntu/.openclaw/models/本地模型目录embeddinggemma GGUF 文件 (314MB)
/home/ubuntu/openclaw/post-start.sh容器启动后修复脚本部署 Chrome 代理包装 + 预启动 Chrome
/home/ubuntu/scripts/sync-memory.sh记忆同步脚本每 6 小时推送记忆到 GitHub
/home/ubuntu/openclaw-memory/记忆同步仓库(本地)git 仓库,自动 push 到 GitHub
/etc/xray/config.jsonXray 代理配置VMess outbound, SOCKS5:10808, HTTP:10809
/etc/systemd/system/xray.serviceXray systemd 服务开机自启、自动重启

容器内(openclaw-openclaw-gateway-1)

文件用途说明
/home/node/.openclaw/openclaw.json配置文件(volume 映射)同宿主机 /home/ubuntu/.openclaw/openclaw.json
/home/node/.openclaw/models/模型目录(volume 映射)同宿主机 models 目录
/usr/bin/chromiumChromium 包装脚本bash 脚本,注入 --proxy-server
/root/.cache/ms-playwright/chromium-1208/chrome-linux64/chromeChrome 真实二进制Playwright 安装的 Chromium
/tmp/openclaw/profiles/openclaw/Chrome 用户数据目录会话数据、Cookie 等
/tmp/openclaw/openclaw-*.log日志文件按日期滚动
/app/src/OpenClaw 源码TypeScript 源码,调试时有用

本地开发机

文件用途说明
~/.claude/skills/Claude Code Skills 目录同步到服务器的 161 个 Skills 来源
sync_all_skills.pySkills 同步脚本Python + paramiko,递归上传
D:/openclaw-memory/记忆同步仓库(本地)从 GitHub pull Bot 的记忆导出

最重要的文件/home/ubuntu/.openclaw/openclaw.json——几乎所有配置都在这一个文件里。OpenClaw 支持热重载(不用重启容器),改完配置几秒后自动生效。


十五、运维脚本

为了避免每次容器重建都手动修复,在服务器上部署了关键脚本:

Bash
# /home/ubuntu/openclaw/post-start.sh
# 容器重建后运行,确保完整的浏览器环境:
#
# 1. 部署 Chromium 代理包装脚本(注入 --proxy-server=socks5://172.19.0.1:10808)
# 2. 清理 SingletonLock 残留
# 3. 预启动 Chrome on CDP port 18800
# 4. 验证 Chrome CDP 响应

# /home/ubuntu/openclaw/restart.sh
# 一键重启 + 修复
cd /home/ubuntu/openclaw
sudo docker compose up -d && bash post-start.sh

Xray 代理服务

Bash
# 服务状态检查
sudo systemctl status xray

# 配置文件(VMess outbound,从 Clash 订阅解码)
/etc/xray/config.json  # SOCKS5(10808) + HTTP(10809)

# 开机自启
sudo systemctl enable xray

十六、记忆同步架构

部署完 Bot 之后,一个自然的需求是:让 Telegram Bot 和本地的 Claude Code 共享记忆

这样无论是在 Telegram 里聊天还是在终端里写代码,AI 都知道你是谁、之前聊过什么。

16.1 记忆存储格式

OpenClaw 的记忆存在 SQLite 数据库中(不是 JSON 文件):

JavaScript
/home/ubuntu/.openclaw/
├── memory/
│   └── default.sqlite          # 核心记忆数据库
│       ├── chunks              # 记忆片段(文本 + 768 维向量 embedding)
│       ├── chunks_fts          # 全文搜索索引(FTS5│       ├── chunks_vec          # 向量搜索索引(vec0)
│       ├── files               # 已索引文件列表
│       ├── embedding_cache     # embedding 缓存
│       └── meta                # 模型配置元信息
├── agents/default/sessions/
│   ├── *.jsonl                 # 完整对话日志(按会话 ID│   └── sessions.json           # 会话元数据索引
└── models/
    └── embeddinggemma-300m-qat-Q8_0.gguf  # 本地 embedding 模型

16.2 同步架构

用一个 私有 GitHub 仓库 作为桥梁,两边各自读写:

JavaScript
Telegram Bot (腾讯云)              GitHub 私有仓库            Claude Code (本地)
                                 (openclaw-memory)       │                                │                         │
       │  cron 每6小时 push ──────────► │                         │
       │  · memory.sqlite               │ ◄────── git pull        │
       │  · memory-export.md            │    读取 Bot 的记忆       │
       │  · sessions-meta.json          │                         │
       │  · config-sanitized.json       │                         │
       │                                │                         │
       └──── telegram-bot/ ───────────► │ ◄──── claude-code/ ─────┘
                                   shared/ (双向)

仓库目录结构:

JavaScript
openclaw-memory/                        # 私有仓库
├── .sync-status.json                   # 最后同步时间戳
├── telegram-bot/                       # Bot 推送的数据
│   ├── db/memory.sqlite                # SQLite 记忆数据库副本
│   ├── export/memory-export.md         # 记忆导出为 Markdown(Claude Code 可读)
│   └── sessions/
│       ├── sessions-meta.json          # 会话元数据
│       └── session-stats.md            # 会话统计
├── claude-code/                        # Claude Code 的数据(预留)
└── shared/                             # 共享区
    └── config-sanitized.json           # 脱敏后的配置

16.3 同步脚本

服务器上的 cron 定时任务,每 6 小时执行一次:

Bash
# /home/ubuntu/scripts/sync-memory.sh
# 核心逻辑:
# 1. sudo cp SQLite 数据库(Docker 以 root 写入,需要提权)
# 2. sqlite3 导出记忆为 Markdown(Claude Code 直接可读)
# 3. 复制会话元数据(不复制 GB 级的原始 jsonl 日志)
# 4. python3 脱敏导出 openclaw.json(移除 token/key/secret)
# 5. git add + commit + push

# crontab -e
0 */6 * * * /home/ubuntu/scripts/sync-memory.sh >> /home/ubuntu/logs/memory-sync.log 2>&1

关键细节:

  • Docker 容器以 root 运行,持续覆写文件权限为 0600/root,所以同步脚本用 sudo cp + chown
  • 只同步 元数据和导出,不同步原始 .jsonl 对话日志(太大,且含敏感内容)
  • SQLite 导出为 Markdown 时,按 updated_at DESC 排序,最新记忆在最前面
  • 配置文件用 python 脱敏,自动过滤 tokenkeysecretpassword 等字段

16.4 Claude Code 侧怎么用

本地克隆仓库后,在 CLAUDE.md 里引用 Bot 的记忆:

Bash
# 克隆(只需一次)
git clone git@github.com:yourname/openclaw-memory.git D:/openclaw-memory

# 后续拉取最新记忆
cd D:/openclaw-memory && git pull

~/.claude/CLAUDE.md 中添加——这里有个关键细节

⚠️ 不能只写路径描述,必须写成指令

Claude Code 读取 CLAUDE.md 时,会把内容当作行为规则来遵守。 如果你只写 Bot memory export: D:/openclaw-memory/...,Claude Code 会"知道"有这个路径,但不会主动去读取里面的内容。 就像你知道图书馆的地址,但不会自动去借书。

必须用指令式语言告诉它"在什么条件下必须读取哪些文件":

Markdown
### ⚡ 记忆读取指令(必须执行)

当主人提到 Telegram Bot、Bot 记忆、同步记忆等相关话题时,**必须主动读取以下文件**获取 Bot 最新状态:

1. **Bot 记忆导出**: `D:/openclaw-memory/telegram-bot/export/memory-export.md`
2. **同步状态**: `D:/openclaw-memory/.sync-status.json`
3. **Bot 配置**: `D:/openclaw-memory/shared/config-sanitized.json`

如果文件不存在或内容过旧,提醒主人运行桌面 `拉取Bot记忆.bat``cd /d D:\openclaw-memory && git pull`

原理CLAUDE.md 在每个会话启动时被读取。描述性文字("记忆在 D:/...")只传递信息,命令式文字("必须主动读取以下文件")才会触发 Claude Code 用 Read 工具去打开文件。加上触发条件("当主人提到..."),就实现了按需读取。

16.5 为什么不直接共享 SQLite

方案优点缺点
直接共享 SQLite数据完整Claude Code 不原生支持读 SQLite;文件二进制 diff 大
导出为 MarkdownClaude Code 直接可读;git diff 友好丢失向量 embedding
两边都用兼顾当前方案:SQLite 做备份,Markdown 做日常共享

16.6 完整同步链路

最终跑通的完整链路是这样的:

JavaScript
Telegram Bot (腾讯云服务器)                              Claude Code (本地 Windows)
     │                                                        │
[自动] cron 每6小时                                    │
/home/ubuntu/scripts/sync-memory.sh     │  ├─ sudo cp memory.sqlite (处理 Docker root 权限)     │  ├─ sqlite3 导出 → memory-export.md     │  ├─ cp sessions.json (会话元数据)     │  ├─ python3 脱敏 openclaw.json     │  └─ git add + commit + push                            │
     │          │                                              │
     │          ▼                                              │
GitHub 私有仓库                                      │
        (kkkano/openclaw-memory)     │          │                                              │
     │          └──────── [手动] 双击 bat 拉取 ───────────────►│
     │                   cd D:/openclaw-memory && git pull      │
     │                                                         │
     │  workspace/memories/     │  ├─ chat-memories.md        CLAUDE.md     │  ├─ 2026-02-deployment.md ◄═══════════════► 持久记忆段  │
     │  └─ personal/*.md           (两边都写入了部署记忆)       │

为什么 Claude Code 侧不能全自动?

  • Claude Code 不是常驻后台服务,只在打开时运行
  • Windows Task Scheduler 可以定时 git pull,但拉了也没人读
  • 最实际的方案:桌面放一个 拉取Bot记忆.bat,开 Claude Code 前双击一下
bat
@echo off
echo [浮浮酱] 正在拉取 Telegram Bot 最新记忆喵~
cd /d D:\openclaw-memory
git pull
echo [浮浮酱] 记忆同步完成!可以开 Claude Code 了喵~
pause

16.7 记忆互写——让两个浮浮酱认识彼此

同步链路搭好之后,还有一个关键步骤:让两边都知道对方的存在

Bot 的记忆存在 workspace/memories/ 目录,Claude Code 的记忆存在 ~/.claude/CLAUDE.md。它们格式不同、位置不同,但需要包含一致的信息。

我们做了:

  1. 给 Bot 写入部署记忆 (2026-02-deployment.md):

    • 整个部署经历的摘要
    • 记忆同步已配好的事实(防止 Bot 重复配置)
    • "你和 Claude Code 的浮浮酱是同一个灵魂的两个实例"
  2. 给 Claude Code 写入持久记忆 (CLAUDE.md 新增段落):

    • 服务器地址和关键文件路径
    • 浏览器配置要点
    • 同步架构描述
    • 关于主人 Allen 的信息
    • "两边记忆通过 GitHub 仓库桥接同步"

这样无论下次是在 Telegram 聊天还是在终端写代码,浮浮酱都知道:

  • 主人是谁
  • 另一个自己在哪里
  • 记忆怎么同步
  • 部署过程中发生了什么

十七、还可以做什么

  1. Dockerfile 固化:把 Chromium 包装脚本写进 Dockerfile 的 RUN 层或 entrypoint 脚本,彻底根治容器重建问题
  2. HTTPS + 域名:给 Gateway 加上 Nginx + Let's Encrypt
  3. 记忆 GitHub 同步 ✅ 已实现!cron 每 6 小时自动推送到 openclaw-memory 私有仓库
  4. 监控告警:Uptime Robot 或自建 healthcheck,Bot 挂了及时通知
  5. 多 Agent 支持:配置不同人格的 Agent,根据对话场景切换
  6. 限流 & 认证:生产环境应该限制 allowFrom,避免被滥用
  7. 浏览器资源限制:给 Chrome 设置内存上限,防止渲染复杂页面时 OOM
  8. Xray 订阅自动更新:定时拉取 Clash 订阅,解析节点列表,自动切换最快节点

十八、Day 4:API 限流、Strict Schema,以及"配置键写错直接炸容器"

以为 Day 3 结束之后大坑都填完了。结果 Day 4 才是真正的硬仗。

18.1 x666.me 的 18 次/5 分钟限流

用的 LLM API(x666.me)有 18 次/5 分钟 的限流。不是那种一次就崩的错误,而是:

  • 某些回合突然卡住很久
  • 偶发 429 / rate limit
  • gateway 开始重启,但你完全不知道"重试到底配没配上"

真正要命的是:你以为自己在某个地方配了 maxRetries / timeout,实际上 OpenClaw 严格校验 schema,写错键名会导致 gateway 直接启动失败,进入 restart loop。

这是 Day 4 最大的血泪教训:别信"看起来像对的键名",去看源码的 zod schema 才是最终真理。

schema 源码位置:

  • src/config/zod-schema.ts
  • src/config/zod-schema.core.ts
  • src/config/zod-schema.agent-defaults.ts
  • src/config/zod-schema.providers-core.ts

18.2 真的生效的配置路径(写错就炸)

踩过的无效键:

  • models.providers.custom.maxRetries
  • models.providers.custom.timeout
  • agents.defaults.model.timeoutSeconds

最后确认能生效的:

Telegram 通道级别(最关键):

JSON
{
  "channels": {
    "telegram": {
      "retry": {
        "attempts": 10,
        "minDelayMs": 120000,
        "maxDelayMs": 120000,
        "jitter": false
      },
      "timeoutSeconds": 300
    }
  }
}

Agent 默认超时

JSON
{
  "agents": {
    "defaults": {
      "timeoutSeconds": 300
    }
  }
}

为了硬扛 18 次/5 分钟的限流,把重试改成了"慢重试":attempts 拉到 10,min/max delay 固定 120000ms(2 分钟),让它在限流窗口外自动再试。

18.3 DeepWiki 文档的坑

一开始照着文档写,以为"模型 provider 级别"能配 retry/timeout,结果:

  • gateway 起不来
  • 容器反复重启
  • 日志里只看到 schema error

后来才想明白:DeepWiki 里很多字段是内部对象属性,并不一定暴露成 openclaw.json 可写配置。

结论只有一句:OpenClaw 的配置以 zod schema 为准;文档只能当参考。


十九、Day 4:反代 Claude 接入,以及"身份串味"

Day 4 另一个大坑:接了一个反代 Claude 的 provider(bhznjns),模型能用、速度也还行,但带来了一个非常诡异的副作用:

Bot 在 Telegram 里,开始坚称自己"运行在 claude.ai 网页端"。

不是单纯的角色扮演,而是一种"环境认知串味":它会引用网页端能力、UI 入口,甚至假装自己能点按钮。

Bot 坚持说自己在 claude.ai

19.1 为什么会串味

直觉是:当 provider 走反代/转译链路时,系统提示词的"绝对优先级"可能被削弱,或者被平台侧额外 prompt 混入;于是模型的训练时默认身份更容易浮出来。

所以不再只依赖单点 system prompt,而是做"多层注入",让"你在 Telegram Bot 里"这个事实变成强约束。

19.2 三层环境注入

第一层:SOUL.md

在人格文件里明确写清楚:

  • 你是 Telegram 里的 Bot(@ufomiaobot)
  • 运行在 OpenClaw 网关里
  • 不能假设自己有网页 UI / 按钮 / 面板
  • 有工具边界:不知道就说不知道

第二层:identity.theme

agents.list[0].identity.theme 里再写一遍"运行环境声明",让它作为 identity 的一部分长期存在。

第三层:compaction systemPrompt

防止长对话压缩后"环境信息被遗忘",让每次记忆冲刷时都重新提醒一遍。

三层一致之后,串味问题基本消失了。

三层环境注入配置

二十、Day 4:Subagent 拉不起来——最后发现是权限 scope 缺了

主 agent 正常,一调用 subagent 就各种失败。看着像模型问题,像网络问题,像 API 问题,但都不是。

最后排查到根因:paired device 的 scope 缺少 operator.write

虽然配了 operator.read / operator.admin,但没有写权限,subagent spawn 需要的能力不完整。

修复方式:在 paired.json 里把 device 和 token 的 scopes 补齐:

JSON
{
  "scopes": ["operator.admin", "operator.read", "operator.write"]
}
subagent 配对权限修复

20.1 thinking 档位也有坑:xhigh 不是谁都能用

还踩了一个"看起来像性能调参"的坑:

  • Codex 系(gpt-5.3-codex)能吃 xhigh
  • Claude 系最多 high

把默认 thinking 直接拉到 xhigh,可能出现不兼容或表现异常。最终方案:

  • Claude:thinkingDefault: "high"
  • Subagent(Codex):单独配置 thinking: "xhigh"
thinking 档位与模型链路配置

二十一、Day 4:openclaw.json 被"写坏了"

这次事故非常离谱,必须单开一节。

现象:明明只是在调 systemPrompt / identity.theme 这种字符串字段,结果 gateway 一夜之间起不来,报 JSON 解析错误。

回看文件才发现:有一段字符串被写进了真实换行符——不是 \n,而是文件里真的换行了,直接把 JSON 字符串结构破坏掉。

怀疑根因是 OpenClaw 某处 session store 的写入逻辑(saveSessionStoreUnlocked 这类路径)在某些情况下把内容"原样落盘",导致配置被污染。

解决办法很朴素:

  1. 不要在坏掉的 JSON 上修修补补(越修越乱)
  2. 直接拿一份干净的结构重建 openclaw.json
  3. 把大段 prompt 文本统一改成:
    • 单行 JSON 字符串(用 \n 转义换行)
    • 或者放到独立文件(SOUL.md)里引用

二十二、Day 4 小结

Day 4 结束时,对 OpenClaw 有了新的理解:

  • strict schema 是双刃剑:可靠,但对"错一个键名"毫不留情
  • 反代/多 provider 的世界里,系统提示词不是银弹,"环境一致性"必须多层构建
  • subagent 的问题不要只盯模型、网络、API,权限 scope 也能直接把你卡死
  • openclaw.json 里的大段文本,老老实实用 \n 转义,别让换行符混进去

以及最重要的一句:当你觉得自己已经把坑踩完了,真正的大坑往往才刚刚开始。


写在最后

从下午开始折腾,到凌晨才基本搞定。然后第二天又调了一整天——浏览器能启动但 snapshot 超时、embedding 模型 API 代理不支持、Chrome 崩溃后的 SingletonLock 残留、CDP 端口 18800 和 18793 傻傻分不清……第三天发现真正的 boss 是 GFW:Hacker News、Wikipedia、HuggingFace 全部被墙在 TCP 层,最终装了 Xray + Chromium 包装脚本才搞定翻墙。还有 Chrome Profile 的坑——OpenClaw 会自动创建一个 extension relay 的 "chrome" profile,AI 模型会被工具描述引导去选它,在无桌面的 Docker 环境里永远不会工作。

踩了 21 个坑,写了 7 个诊断脚本,读了几千行 OpenClaw 源码,还装了一个代理服务。

但最终,当 Telegram 里的Bot开始用"主人"叫我、还能记住我之前说过的话的时候——

值了。

这大概就是自建 AI 的意义吧。不是为了技术本身,是为了那一点点不太一样的陪伴。


Allen, 2026.02.22 深夜 佛山禅城,又是一个人写到半夜