OpenClaw 设计与实现

第15章 部署与运维

作者 杨艺韬 · 16,966 字 · 发布于 · 更新于

第15章 部署与运维

造就"笔记本上跑得通"与"生产环境中可靠"之间差异的,不是某个大功能,而是一百个小细节——日志脱敏、优雅重启、非 root 执行、供应链验证。每个细节看起来都微不足道,直到你在凌晨三点需要它们。

本章要点

  • 掌握三种部署路径:裸机、Docker 多阶段构建、云平台
  • 深入日志系统:结构化日志、敏感信息脱敏、滚动策略
  • 理解重启管理与可观测性设计的工程实践
  • 掌握 Dockerfile 构建优化的关键技巧

上一章我们掌握了与 OpenClaw 交互的方式——CLI 和 TUI。但无论界面多么精美,如果系统跑不起来、跑不稳定,一切都是空谈。本章从开发环境走向生产环境,解决"最后一公里"的工程问题。

在第 2 章的消息旅程中,我们追踪了一条消息从通道进入到响应返回的完整路径。那条路径假设 Gateway 已经在稳定运行。本章关注的是"旅程之前的旅程"——Gateway 如何被部署、如何保持运行、如何在出错时恢复、如何在不停机的情况下更新。

15.1 从演示到生产

15.1.1 鸿沟有多大

你的 Agent 在笔记本上完美运行。现在挑战来了:把它部署到 24/7 运行的云服务器上,服务跨通道用户,经受重启考验,产生干净日志而不泄露 API 密钥。

从"能用"到"稳定运行",就像从搭一座积木桥到建一座真正的桥——材料可能一样,但工程标准完全不同。积木桥倒了重搭就好,真正的桥上面跑着几千辆车。

15.1.2 生产环境的六个隐性需求

开发环境不需要但生产环境必须有的:

  1. 进程管理:崩溃后自动重启,优雅关闭不丢数据。
  2. 日志系统:结构化日志、自动脱敏、大小限制、滚动归档。
  3. 安全加固:非 root 运行、文件权限收紧、供应链验证。
  4. 资源控制:内存限制、磁盘配额、网络安全组。
  5. 可观测性:健康检查端点、状态监控、告警集成。
  6. 可重现性:相同的代码 + 相同的配置 = 相同的行为。

每一个需求看起来都"微小",但忽略其中任何一个都可能招来凌晨三点的告警电话。

🔥 深度洞察:部署是免疫系统,不是包装纸

大多数开发者把部署当作"把代码包装好放到服务器上"——一个后期附加步骤。这是根本性的误解。正确的类比来自医学:部署不是包装纸,而是免疫系统。人体的免疫系统不是在皮肤之外运作的,它渗透在每一个器官、每一条血管中——白细胞在血液中巡逻,淋巴结分布在全身,皮肤本身就是第一道屏障。同样,OpenClaw 的"生产就绪"不是最后一步涂上的外壳,而是从第一行代码就开始渗透的品质:日志脱敏与配置里的 SecretRef 从两端守住密钥,重启的延迟窗口要照顾在途的 LLM 调用,Docker 的非 root 执行对应着 /home/node/.openclaw 这样的状态路径。生产环境不会原谅"先跑通再加固"的思维——因为加固不是附加层,而是骨骼结构本身。

15.1.3 部署架构的演进

OpenClaw 的部署模型经历了三个阶段的演进,每个阶段对应了用户群体和使用场景的扩大:

第一阶段:裸机直接运行。开发者在自己的机器上 npm install -g openclaw,用 openclaw onboard 写好配置后,在前台运行 openclaw gateway(即 openclaw gateway run)。简单直接,但没有进程管理——终端关了,Agent 就停了。(注意 openclaw gateway start 不是"在前台启动网关",它只启动已安装的系统服务,属于下一阶段;想一步到位,直接 openclaw onboard --install-daemon。)

第二阶段:系统服务化。引入 src/daemon/ 模块,支持 systemd/launchd/schtasks 三平台服务管理。Agent 变成了真正的守护进程——开机自启、崩溃重启、后台运行。这是大多数个人用户和小团队的最佳选择。

第三阶段:容器化部署。Docker 镜像 + Compose 编排。适合需要环境隔离、可复现部署或云平台部署的场景。代价是额外的容器管理开销。

为什么三个阶段共存而不是只保留最新的? 因为不同用户有不同的运维能力和部署需求:

  • 个人开发者用裸机模式就够了——简单、直接、零开销
  • 小团队用系统服务——可靠但不需要 Docker 知识
  • 企业部署用容器——环境隔离、CI/CD 集成、基础设施即代码

OpenClaw 不强制用户接受不需要的复杂性。

关键概念:渐进式部署(Progressive Deployment) OpenClaw 的部署模型遵循渐进式复杂度原则——从最简单的裸机全局安装(零 Docker 知识),到系统服务化(Daemon 管理),再到容器化部署(Docker + Compose)。每一层都是可选的,用户只需承担其实际需求对应的复杂度。这与"全部容器化"的一刀切方案形成鲜明对比。

15.2 裸机部署:最简路径

15.2.1 全局安装

npm install -g openclaw
# 或
bun install -g openclaw

package.json 的 engines 写着 node: ">=22.14.0",官方文档进一步推荐 Node 24(Docker 镜像和 CI 都用 24),Node 22.14+ 作为受支持的 LTS 路径保留。OpenClaw 也能跑在 Bun 上——src/daemon/runtime-binary.ts 的 isBunRuntime() / isNodeRuntime() 就是为此存在:服务管理器需要知道该用哪个二进制来拉起守护进程。守护进程的默认运行时是 node(DEFAULT_GATEWAY_DAEMON_RUNTIME),Bun 只有在 openclaw gateway install --runtime bun 显式指定时才会写进服务的启动命令;即便如此,服务审计(src/daemon/service-audit.ts)也会对绑到 Bun 的服务报 gateway-runtime-bun,建议迁回 node。官方文档把 Bun 标为实验性,明确不推荐用作网关运行时(WhatsApp、Telegram 有已知问题)。

15.2.2 首次配置

openclaw onboard  # 交互式引导(安装脚本装完会自动拉起它)
# 或
openclaw setup    # 只初始化配置与 Agent 工作区

配置向导(第 14 章 14.8 节)引导用户完成:

  1. Provider 选择和认证配置
  2. 通道启用和 token 设置
  3. 安全策略确认
  4. 测试连接

配置文件生成在 ~/.openclaw/openclaw.json(文件名是 .json,但按 JSON5 解析,允许注释和尾逗号;可用 OPENCLAW_CONFIG_PATH 覆盖路径,用 OPENCLAW_STATE_DIR 或 --profile 整体挪窝)。

15.2.3 系统服务注册

openclaw gateway install   # 生成并注册系统服务,装完即启动
openclaw gateway start     # 服务被停掉后用它重新拉起(只对已安装的服务有效)

(openclaw onboard --install-daemon 会在引导流程里一并做掉这一步。)gateway install 做了以下事情:

  1. 检测当前操作系统和可用的服务管理器
  2. 生成对应的服务配置(systemd unit / launchd plist / schtasks XML)
  3. 注册服务并立即启动(systemd 上是 enable 之后再 restart)

Linux systemd 的具体实现:

src/daemon/systemd-unit.ts 生成的 unit 文件:

[Unit]
Description=OpenClaw Gateway
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/node /path/to/openclaw/dist/index.js gateway --port 18789
Restart=always
RestartSec=5
TimeoutStopSec=30
TimeoutStartSec=30
SuccessExitStatus=0 143
KillMode=control-group
Environment=OPENCLAW_GATEWAY_PORT=18789
# ……其余 Environment= 行:HOME、PATH、OPENCLAW_SYSTEMD_UNIT 等服务变量,
# 以及安装时收集到的 ~/.openclaw/.env 与 SecretRef 引用的环境变量

[Install]
WantedBy=default.target

WorkingDirectory= 只在开发模式(从源码树运行)下才会写入;正常安装的服务没有这一行。几个关键配置决策(对照 buildSystemdUnit):

  • After= / Wants=network-online.target:Gateway 需要网络来连接通道和 LLM API,必须在网络就绪后才启动
  • Restart=always:任何退出都拉起来。Gateway 的正常"退出"路径本身就包含重启(见 15.6 节),交给 systemd 兜底最省事
  • RestartSec=5:5 秒重启延迟,防止快速失败循环(crash loop)耗尽系统资源
  • SuccessExitStatus=0 143:143 = 128+15(SIGTERM),把"被优雅终止"也算作成功退出,避免日志里刷出无意义的失败记录
  • KillMode=control-group:把 ACP、运行时 worker 等子进程一并带走,避免重启后留下孤儿进程
  • TimeoutStopSec=30:与 run-loop 里的 SUPERVISOR_STOP_TIMEOUT_MS = 30_000 对齐——进程内的看门狗(25 秒)必须先于 systemd 的硬杀生效

生成的单元文件路径由 resolveSystemdUserUnitPath() 决定,服务名是 openclaw-gateway(带 profile 时是 openclaw-gateway-<profile>)。

systemd-linger.ts 的必要性。默认情况下,systemd 会在用户注销时停止其用户服务。loginctl enable-linger 让服务在用户注销后继续运行——这对无人值守的服务器至关重要。OpenClaw 会检测 linger 状态,如果未启用则提示用户。

15.2.4 裸机部署的局限

裸机部署的主要风险是环境依赖:

  • Node.js 版本不匹配(OpenClaw 要求 22.14+,推荐 24,而发行版仓库里可能只有 18)
  • 系统库缺失(Chromium 需要特定的 X11/Wayland 库)
  • 权限问题(npm 全局安装可能需要 sudo)

这些问题正是容器化要解决的。

⚠️ 注意:裸机部署时,务必使用 loginctl enable-linger 确保 systemd 用户服务在 SSH 断开后继续运行。否则关闭 SSH 会话后,Gateway 守护进程会被 systemd 自动终止——这是新部署中最常见的"Agent 突然失联"问题。

💡 最佳实践:生产环境部署推荐使用非 root 用户运行 OpenClaw。创建专用用户 useradd -m openclaw,将配置文件权限设为 600(仅 owner 可读写),避免 API 密钥被其他用户读取。

15.3 Docker:多阶段构建工程

15.3.1 四阶段构建

OpenClaw 的 Dockerfile(264 行)把构建拆成四个有实质工作的阶段——ext-deps → build → runtime-assets → 运行时(此外还有 base-default / base-slim 两个只负责选基础镜像的别名阶段,不做任何构建)。这不是"从简单到复杂"的演进,而是对构建效率和镜像安全的精细工程。注意包管理器是 pnpm(packageManager: "pnpm@10.32.1"),不是 npm。

图 15-1:四阶段 Docker 构建流程

下图展示了 OpenClaw Docker 镜像的构建流程:ext-deps 只抽出可选扩展的 package.json,build 安装依赖并编译,runtime-assets 剪裁 devDependencies 与类型/映射文件,最终阶段基于按摘要锁定的运行时基础镜像组装出镜像。

flowchart LR
    S1["ext-deps\n扩展依赖抽取"] --> S2["build\n安装 + 编译"]
    S2 --> S3["runtime-assets\n生产剪裁"]
    S3 --> S4["base-default / base-slim\n最终镜像"]
    S1 -.- D1["只取扩展的\npackage.json"]
    S2 -.- D2["pnpm install\n--frozen-lockfile"]
    S3 -.- D3["pnpm prune --prod\n删 .d.ts / .map"]
    S4 -.- D4["非 root 用户\n+ 健康检查"]
    style S4 fill:#2e7d32,color:#fff

阶段 1:ext-deps。只从可选扩展目录里挑出被 OPENCLAW_EXTENSIONS 点名的那些 package.json。目的很明确——让主构建层不会因为无关插件的源码改动而失效。

阶段 2:build。先装 Bun(构建脚本需要),整个安装流程最多尝试 5 次、失败后间隔 attempt * 2 秒(线性退避,容忍 bun.sh/GitHub 偶发 5xx);然后 corepack enable,只复制依赖描述文件再安装:

# 节选:中间省略了 ui/patches/扩展清单的 COPY、权限规整、A2UI 打包与 UI 构建等步骤
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
    NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
COPY . .
RUN pnpm build:docker

三个细节:--frozen-lockfile 严格按 lockfile 安装、不更新它,保证构建可重现;BuildKit 的 cache mount 让 pnpm store 跨构建复用;--max-old-space-size=2048 是为了小内存 VM 上装依赖不被 OOM Killer 打死(否则会得到一个语焉不详的 exit 137)。

阶段 3:runtime-assets。剪裁生产不需要的一切:

FROM build AS runtime-assets
RUN CI=true pnpm prune --prod && \
    find dist -type f \( -name '*.d.ts' -o -name '*.map' ... \) -delete

移除全部开发依赖(TypeScript 编译器、测试框架、lint 工具),再顺手删掉 dist 里的类型声明和 sourcemap——它们在生产环境完全无用,却实打实地增加体积和攻击面。

阶段 4:运行时镜像。基础镜像按摘要锁定(node:24-bookworm@sha256:…),非 root 执行(USER node,uid 1000),内建健康检查:

ENV NODE_ENV=production
USER node
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]

健康检查用 node -e 而不是 curl:node 是镜像里必然存在的运行时,不依赖额外安装的系统工具(当前 Dockerfile 其实给两个变体都装了 curl,但健康检查不必押在这上面)。

15.3.2 两个镜像变体

变体 基础镜像 构建方式
default node:24-bookworm docker build .
slim node:24-bookworm-slim docker build --build-arg OPENCLAW_VARIANT=slim .

两者的差别是 Debian 基础镜像的完整度:bookworm 自带一堆系统工具,bookworm-slim 则要靠 Dockerfile 里那句 apt-get install procps hostname curl git lsof openssl 补齐 OpenClaw 运行时真正依赖的几个。

浏览器是可选的,不是变体的一部分。Chromium 与 Xvfb 由构建参数 OPENCLAW_INSTALL_BROWSER=1 决定装不装,Dockerfile 里的注释写着"增加约 300MB,但省掉每次容器启动时 60–90 秒的 Playwright 安装"。同理,沙箱要用的 Docker CLI 由 OPENCLAW_INSTALL_DOCKER_CLI=1 决定(注释标注约 50MB,且只装 CLI、不装 daemon)。

选择建议:

  • 你的 Agent 需要 browser 工具(截图、网页浏览、表单填写)→ 加 --build-arg OPENCLAW_INSTALL_BROWSER=1
  • 你要用 agents.defaults.sandbox 的容器隔离 → 加 --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1,并把宿主的 /var/run/docker.sock 挂进来(docker-compose.yml 里有现成的注释行,还要用 group_add 加上宿主 docker 组的 GID)
  • 都不需要 → 用 slim,镜像最小

15.3.3 供应链安全加固

基础镜像按摘要固定:node:24-bookworm@sha256:3a09aa63… 而不是浮动的 tag。Dockerfile 里明确写了这笔交易的代价——"上游 tag 移动时必须手工更新摘要",并给出了更新方法(docker buildx imagetools inspect node:24-bookworm)。可重现性从来不是免费的。

GPG 指纹验证:安装可选的 Docker CLI 时,先把 Docker 的 apt 签名密钥下载下来,算出指纹,和硬编码的期望值逐字比对,不一致就直接 exit 1:

ARG OPENCLAW_DOCKER_GPG_FINGERPRINT="9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
...
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc \
  | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
  echo "ERROR: Docker apt key fingerprint mismatch ..." >&2; exit 1; \
fi

关键在于这个密钥是要被当作 apt 的根信任来用的——不验证指纹就等于把镜像的软件源信任交给了网络。

--frozen-lockfile:确保 pnpm install 使用与开发环境完全相同的依赖版本。没有这个标志,安装过程可能静默更新 lockfile,导致"在我机器上好好的"问题。

非 root 执行:容器内进程以 node 用户运行,即使攻击者突破容器,也没有 root 权限。这是纵深防御的一部分(呼应第 13 章的安全哲学)——容器隔离是一道防线,非 root 是另一道,两者独立生效。

15.3.4 Docker Compose 双容器编排

# 节选自仓库的 docker-compose.yml,省略了大部分 environment 条目与注释
services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-openclaw:local}
    environment:
      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
      # ……HOME、TZ 等
    volumes:
      - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
      - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
    ports:
      - "${OPENCLAW_GATEWAY_PORT:-18789}:18789"
      - "${OPENCLAW_BRIDGE_PORT:-18790}:18790"
    init: true
    restart: unless-stopped
    command: ["node", "dist/index.js", "gateway",
              "--bind", "${OPENCLAW_GATEWAY_BIND:-lan}", "--port", "18789"]
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:18789/healthz')..."]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  openclaw-cli:
    image: ${OPENCLAW_IMAGE:-openclaw:local}
    network_mode: "service:openclaw-gateway"
    cap_drop: [NET_RAW, NET_ADMIN]
    security_opt: ["no-new-privileges:true"]
    # environment / volumes 与网关相同,此处省略
    stdin_open: true
    tty: true
    init: true
    entrypoint: ["node", "dist/index.js"]
    depends_on: [openclaw-gateway]

为什么双容器而不是单容器? 延续了第 14 章的"守护进程 + 客户端"架构——Gateway 容器持久运行,CLI 容器按需启动(docker compose run --rm openclaw-cli status)。

三个值得注意的编排细节:其一,两个服务绑定挂载同一对宿主目录(配置与工作区),而不是用命名卷——这样宿主上就能直接 cat ~/.openclaw/openclaw.json,运维体验好得多;其二,CLI 用 network_mode: "service:openclaw-gateway" 直接共享网关的网络命名空间,因此它连 127.0.0.1:18789 就能打到网关,不需要额外端口;其三,两个服务都开了 init: true,让容器里有个真正的 PID 1 来回收僵尸进程——Agent 会 fork 出大量子进程,这一条不是可选项。

命令行里的 --bind lan 也值得停一下:网关默认绑回环,而在 Docker 桥接网络下绑回环意味着宿主根本连不上。Dockerfile 的注释专门为此写了一段提醒——要么 --network host,要么把 bind 改成 lan(0.0.0.0)并且配上认证凭据。后半句才是重点:compose 通过 OPENCLAW_GATEWAY_TOKEN 把令牌交给两个容器(仓库的 scripts/docker/setup.sh 会生成它并写进 .env)。网关自己也不会在非回环地址上裸奔:token 模式下没配令牌会自动生成一个,其他缺凭据的情况直接拒绝启动(src/cli/gateway-cli/run.ts)。

安全配置详解:

配置 含义 防护目标
cap_drop: [NET_RAW] 移除原始网络访问能力 防止端口扫描和网络嗅探
cap_drop: [NET_ADMIN] 移除网络配置能力 防止修改路由表、防火墙规则
no-new-privileges 禁止通过 setuid 提权 防止利用 suid 二进制提升权限
restart: unless-stopped 崩溃自动重启,手动停止不重启 确保服务可用性

cap_drop 移除了不需要的 Linux 能力——不需要原始网络访问(NET_RAW,ping/嗅探)或网络配置(NET_ADMIN,改路由表和防火墙)。这遵循最小权限原则——进程只应该拥有完成工作所需的最小权限集。

注意仓库自带的 docker-compose.yml 把 cap_drop 和 no-new-privileges 加在了 CLI 服务上;网关服务只设了 restart: unless-stopped 与健康检查。如果你的网关不需要 NET_RAW/NET_ADMIN(绝大多数部署都不需要),把这两行照抄到网关服务上是一个成本为零的加固。

15.3.5 容器化 vs 裸机的深度对比

维度 裸机部署 容器化部署
启动速度 进程直接启动 多一层容器启动开销
环境隔离 无——共享主机环境 完全隔离——独立文件系统和网络
资源占用 最小——无容器运行时开销 多一份容器运行时的常驻开销
可重现性 依赖主机环境配置 完全可重现——镜像即环境
升级回滚 手动——需要备份和恢复 简单——切换镜像版本即可
调试便利性 高——直接 strace、gdb 中——需要 docker exec 进入容器
多实例 需要手动管理端口和配置 简单——Compose 原生支持
日志管理 OpenClaw 自己按天滚动 + 24 小时清理 同左,另可叠加 Docker 日志驱动

什么时候选裸机?

  • 个人开发机——简单直接,调试方便
  • 资源受限的设备(如树莓派)——Docker 开销不可忽视
  • 需要直接访问 GPU——Docker GPU 透传配置复杂

什么时候选容器?

  • 生产部署——环境一致性和可重现性是刚需
  • CI/CD 集成——容器是标准构建产物
  • 多实例部署——需要在同一主机运行多个 Gateway
  • 团队协作——"在我机器上好好的"不应该是问题

15.4 云平台部署

15.4.1 Fly.io 部署

仓库根目录直接带了一份 fly.toml(还有一份 fly.private.toml)。

关键配置(节选自仓库的 fly.toml,省略了 app、[build] 等字段,行尾注释为本专栏所加):

[env]
OPENCLAW_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"

[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false   # ⚠️ 关键!
auto_start_machines = true
min_machines_running = 1

[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"

[mounts]
source = "openclaw_data"
destination = "/data"

auto_stop_machines = false 至关重要——网关维护与消息平台的持久 WebSocket 连接。Fly.io 的自动停止功能会切断所有通道连接,导致 Agent 变成"聋子"——看不到任何消息。

持久存储:OPENCLAW_STATE_DIR = "/data" 把状态目录整体指向挂载的卷,配置、会话、Cron 定义、审计记录全在里面。Fly volume 绑在单台机器上——这限制了水平扩展,但对于 OpenClaw 的单运营者假设是可接受的。

NODE_OPTIONS = "--max-old-space-size=1536" 也不是随手写的:VM 内存是 2048MB,给 V8 堆留 1536MB,剩下的留给原生内存和子进程。

其他 PaaS。仓库里还有 render.yaml(Render,healthCheckPath: /health、OPENCLAW_GATEWAY_TOKEN: generateValue: true 自动生成令牌),docs/install/ 下则备有 Railway、Northflank、Hetzner、DigitalOcean、Oracle、GCP、Azure、Kubernetes、Raspberry Pi 等十余份部署说明。

15.4.2 VPS 部署(通用方案)

对于不使用 PaaS 的场景(AWS EC2、DigitalOcean Droplet、Hetzner VPS),部署流程:

# 0. 最省事:官方安装脚本(自动装 Node、装 OpenClaw、拉起引导)
curl -fsSL https://openclaw.ai/install.sh | bash

# 或者手工来:
# 1. 安装 Node.js(推荐 24)
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs

# 2. 安装 OpenClaw
npm install -g openclaw@latest

# 3. 引导配置 + 安装守护进程(非交互)
#    --secret-input-mode ref 从环境变量读密钥,配置里只存引用;
#    安装服务时会把这个变量一并写进服务环境
export OPENAI_API_KEY="sk-..."
openclaw onboard --non-interactive --install-daemon \
  --auth-choice openai-api-key \
  --secret-input-mode ref \
  --accept-risk

# 4. 验证
openclaw gateway status
openclaw doctor

网络安全考量:

Gateway 默认绑定回环(127.0.0.1),要对外提供服务必须显式 --bind lan。在生产环境中,你应该:

  • 保持默认的回环绑定,通过反向代理(Nginx/Caddy)加 TLS 后暴露
  • 如果确实要 --bind lan,务必同时配置认证(token 或 password),并用防火墙(ufw / iptables)限制 18789 端口的来源
  • 通道连接(Telegram、Discord 等)是出站连接,不需要入站端口

15.4.3 移动端伴侣应用部署

OpenClaw 还支持通过移动端 companion app(iOS/Android/macOS)扩展 Agent 的感知能力(第 11 章)。这些 app 通过 WebSocket 连接到 Gateway,需要 Gateway 的地址可达。部署方案的选择会影响移动端连接:

  • 本地网络:Gateway 和 app 在同一局域网——直接通过 LAN IP 连接
  • Tailscale/ZeroTier:跨网络部署——通过 VPN 打通 Gateway 和 app。OpenClaw 内建了 Tailscale 集成(gateway.tailscale.mode):serve 只在 tailnet 内可达,funnel 对公网开放且要求 password 认证;两者都要求网关保持回环绑定(src/gateway/server-runtime-config.ts)
  • 公网部署:Gateway 有公网 IP——app 直接连接(需要 TLS)

还要注意节点上的高风险命令:camera.snap / camera.clip、screen.record,以及 contacts.add、calendar.add、reminders.add、sms.send / sms.search,默认不在各平台的放行清单里,要显式加进 gateway.nodes.allowCommands 才能用;交互式向导在全新的网关配置上还会把它们写进 gateway.nodes.denyCommands(src/gateway/node-command-policy.ts、src/wizard/setup.gateway-config.ts)。

15.5 日志系统:结构化、脱敏、滚动

15.5.1 日志系统的设计哲学

日志不是"调试完就删除的 console.log"——它是系统的飞行记录仪。OpenClaw 的日志系统设计遵循三个原则:

  1. 结构化优先:日志是数据,不是文本。可查询、可聚合、可告警
  2. 安全默认:敏感信息自动脱敏,不需要开发者记得"这里不能打印 API Key"
  3. 永不崩溃:日志系统的故障不应该影响核心功能

15.5.2 三个目的

日志系统同时服务三个目的,每个目的对日志格式和内容有不同需求:

目的 需求 OpenClaw 的实现
调试 详细上下文、堆栈追踪、请求/响应对 分级日志(debug/info/warn/error)
审计 谁做了什么、什么时候、结果如何 结构化字段 + 时间戳
监控 结构化字段、可查询、可告警 JSON Lines + 外部 transport

15.5.3 日志基础设施

src/logging/logger.ts 是日志系统的核心,基于 tslog 构建(import { Logger as TsLogger } from "tslog"),但添加了大量 OpenClaw 特有的功能:

// src/logging/logger.ts:43(简化)
export const DEFAULT_LOG_DIR = resolveDefaultLogDir();
export const DEFAULT_LOG_FILE = resolveDefaultLogFile(DEFAULT_LOG_DIR);

const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h
const DEFAULT_MAX_LOG_FILE_BYTES = 500 * 1024 * 1024; // 500 MB

双输出架构。每条日志同时写入两个目标:

  1. 文件:JSON Lines 格式,完整的结构化数据
  2. 控制台:人类可读的着色文本

两个输出有独立的级别控制——logging.level 只管文件,logging.consoleLevel(默认 info)只管控制台,logging.consoleStyle 可选 pretty / compact / json。这里有个容易踩的坑:--verbose 只提高控制台的啰嗦程度,不会提升文件日志级别;想在文件里留下 debug 细节,必须把 logging.level 设成 debug 或 trace。

子系统着色。每个子系统(gateway、cron、browser、security)有独立的日志器(createSubsystemLogger(name)),颜色由子系统名的字符串哈希在一个 6 色调色板(cyan / green / yellow / blue / magenta / red)里确定性挑选:

// src/logging/subsystem.ts:117
let hash = 0;
for (let i = 0; i < subsystem.length; i += 1) {
  hash = (hash * 31 + subsystem.charCodeAt(i)) | 0;
}
const idx = Math.abs(hash) % SUBSYSTEM_COLORS.length;
[12:34:56] [gateway] ■ Server listening on 0.0.0.0:18789
[12:34:57] [cron]    ■ Loaded 3 cron jobs
[12:34:57] [browser] ■ Chromium path: /usr/bin/chromium
[12:34:58] [security]■ Audit: 0 critical, 2 warnings

哈希着色保证同一个子系统的颜色在每次运行、每台机器上都一致——gateway 永远是青色,cron 永远是黄色。这让运维人员在快速扫描日志时可以用颜色过滤信息。只有 6 种颜色,撞色在所难免(discord 就和 gateway 同为青色);个别名字撞色或不好看时,SUBSYSTEM_COLOR_OVERRIDES 提供了逃生舱(目前只有 gmail-watcher 用了)。子系统名还会被规整:gateway/channels/providers 这几个前缀会被剥掉,层级最多保留两段——否则日志前缀会长到把正文挤出屏幕。

15.5.4 JSON Lines 格式

每行一个完整 JSON 对象——支持 jq 过滤和日志平台(Datadog、ELK Stack)摄入。行的形状就是 tslog 的日志对象再加一个 time 字段:日志参数按位置落在 "0"、"1"…… 这些数字键上,级别在 _meta.logLevelName,子系统则以 JSON 字符串的形式放在 _meta.name 里(src/logging/logger.ts 的 buildLogger;解析逻辑见 src/logging/parse-log-line.ts):

{"0":"{\"subsystem\":\"gateway/channels/discord\"}","1":"connected","_meta":{"name":"{\"subsystem\":\"gateway/channels/discord\"}","logLevelName":"INFO"},"time":"2026-03-21T18:15:31.000+08:00"}

(示意,_meta 里的其他字段从略。)另有一个容易混淆的格式:logging.consoleStyle 设为 json 时,控制台输出的是扁平的 {time, level, subsystem, message} 对象,和文件日志不是一回事。

为什么用 JSON Lines 而不是多行 JSON?因为 JSON Lines 是可追加的——新日志直接 append 到文件末尾,不需要解析和重写整个文件。这对于高频写入的日志文件至关重要。而且 jq 原生支持 JSON Lines,运维友好。

15.5.5 自动脱敏

src/logging/redact.ts 的 DEFAULT_REDACT_PATTERNS 有 17 条正则,分三类:

结构性模式(不认识具体服务也能命中)——KEY/TOKEN/SECRET/PASSWORD 结尾的环境变量赋值、"apiKey"/"token"/"accessToken" 等 JSON 字段、--api-key 之类的 CLI 标志、Authorization: Bearer … 头、以及整段 PEM 私钥块。

已知前缀——sk-(OpenAI 系)、ghp_ / github_pat_(GitHub)、xox[baprs]- / xapp-(Slack)、gsk_(Groq)、AIza(Google)、pplx-(Perplexity)、npm_。

Telegram Bot Token——两条:一条匹配 URL 里的 /bot<id>:<token>/,一条匹配裸的 <数字>:<token>。

掩码算法(maskToken)保留前 6 个和后 4 个字符,中间用 … 连接——足以识别是哪个密钥(当你有多个时),但不足以重建完整密钥。短于 18 字符的整体替换为 ***。PEM 块则只留首尾两行,中间换成 …redacted…。

为什么不用一条通用规则? "替换所有看起来像密钥的字符串"误报率极高——正常的 base64 数据、哈希、UUID 全会被误掩。OpenClaw 的做法是结构性模式打底、已知前缀补刀,在准确性与覆盖率之间走钢丝。

范围要说清楚:默认模式是 tools(logging.redactSensitive: off | tools),它作用于工具摘要与控制台流,官方文档明确写着 "This is tools-only and does not alter file logs"。要扩展规则用 logging.redactPatterns(数组,覆盖默认值,支持裸正则或 /pattern/flags)。

脱敏与第 13 章的 SecretRef 的关系。SecretRef(第 13 章)确保密钥不以明文出现在配置文件中。日志脱敏确保密钥不出现在工具摘要和控制台输出中(这是最容易被截图、被粘进工单、被 Agent 自己读回上下文的地方)。两者是互补的——SecretRef 防止静态泄露,脱敏防止运行时泄露。

15.5.6 日志失败永不崩溃

// src/logging/logger.ts:232
function appendLogLine(file: string, line: string): boolean {
  try {
    fs.appendFileSync(file, line, { encoding: "utf8" });
    return true;
  } catch {
    return false;
  }
}

每次日志写入都包在 try-catch 里,失败只返回 false。磁盘满时静默丢弃消息,而非崩溃网关。外部 transport 也是同样待遇——attachExternalTransport 里那句注释直白得可爱:// never block on logging failures。

为什么日志失败不应该崩溃? 日志是辅助功能——Agent 的核心功能(响应消息、执行工具、管理会话)不依赖日志。因为日志写入失败而停止 Agent 服务,就像因为行车记录仪坏了而熄火停车——你的乘客可不会觉得这是"安全设计"。

更深层的原因是:日志写入失败最常见的原因是磁盘满。此时系统已经处于异常状态。如果因此崩溃,重启后磁盘仍然是满的,服务会陷入 crash loop。静默丢弃让服务继续运行,运维人员有时间处理磁盘问题。

15.5.7 滚动与大小限制

// src/logging/logger.ts:48
const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24 小时
const DEFAULT_MAX_LOG_FILE_BYTES = 500 * 1024 * 1024; // 500 MB
  • 文件名按日期:openclaw-2026-03-21.log——每天一个新文件,日期取网关宿主的本地时区
  • 默认目录是 /tmp/openclaw/(resolvePreferredOpenClawTmpDir(),可用 logging.file 覆盖)——注意不是 ~/.openclaw/
  • 超过 24 小时的滚动日志被清理(pruneOldRollingLogs 按 mtime 比对 MAX_LOG_AGE_MS)
  • 单文件 500MB 上限——防止失控日志填满磁盘

放在 /tmp 下是一个有意的取舍:日志天然是短期的、可丢弃的,把它和长期状态(~/.openclaw/)分开,意味着磁盘紧张时清理日志绝不会误伤会话或配置。代价是重启机器后历史日志就没了——需要长期留存的部署应当显式设置 logging.file,或者把日志转发到外部系统(见下)。

与外部日志系统的集成。src/logging/logger.ts 支持外部 transport:

// 外部 transport 注册
const externalTransports = new Set<LogTransport>();

function attachExternalTransport(logger: TsLogger<LogObj>, transport: LogTransport): void {
  logger.attachTransport((logObj: LogObj) => {
    if (!externalTransports.has(transport)) return;
    try {
      transport(logObj as LogTransportRecord);
    } catch {
      // never block on logging failures
    }
  });
}

这让插件可以将日志转发到 Datadog、Grafana Loki、ELK Stack 等外部系统,实现集中式日志管理。transport 的错误不会影响日志系统本身——又一次"永不崩溃"原则的体现。

15.5.8 环境变量日志级别覆盖

src/logging/env-log-level.ts 支持通过环境变量覆盖日志级别:

OPENCLAW_LOG_LEVEL=debug openclaw gateway run

resolveEnvLogLevelOverride() 解析这个变量;值非法时不会静默忽略,而是往 stderr 打一行提示并列出允许的取值。这对调试生产问题至关重要——不需要修改配置文件并重启,只需设置环境变量并在前台起一个网关即可。同样的能力也有 CLI 形态:根级 --log-level <level> 会写进 process.env.OPENCLAW_LOG_LEVEL。

特殊的 Vitest 快速路径。在测试环境中(VITEST === "true" 且未设 OPENCLAW_TEST_FILE_LOG=1),文件日志级别直接短路成 silent,连配置读取都跳过——canUseSilentVitestFileLogFastPath 的注释写得很直白:"避免在启动时拉起沉重的 config/schema 栈"。这个细节体现了"日志系统不应该拖慢任何东西"的设计原则,也顺带说明了为什么测试里看不到文件日志。

15.6 重启管理

15.6.1 为什么重启不是"停止 + 启动"

天真的重启实现是 stop && start。但在 Agent 系统中,这有两个严重问题:

  1. LLM 调用中断:一次 LLM 调用可能持续 30 秒到 3 分钟。如果在调用进行中重启,用户会丢失这次(可能已经消耗了大量 token 的)响应。
  2. 通道连接重建:WhatsApp、Discord 等通道的连接重建需要时间(3-30 秒),在此期间消息会丢失。

OpenClaw 的重启管理系统解决了这两个问题。

15.6.2 SIGUSR1 信号

为什么选择 SIGUSR1 而非其他信号?

信号 问题
SIGHUP 终端断开时自动发送——SSH 断开会意外重启
SIGTERM 默认终止进程——系统可能在重启逻辑执行前杀掉进程
HTTP API 需要自己的认证层,重启期间 API 不可用
SIGUSR1 ✅ 用户定义信号,无默认行为,完全由应用控制

实际的时序分两段(src/infra/restart.ts + src/cli/gateway-cli/run-loop.ts):

信号之前——deferGatewayRestartUntilIdle 每 500ms 轮询一次待处理工作量,全部排空后才发 SIGUSR1;最长等 DEFAULT_DEFERRAL_MAX_WAIT_MS = 300_000(5 分钟,可用 gateway.reload.deferralTimeoutMs 调),超时则强行发信号。发信号本身还有去重保护:一个未被消费的重启信号在途时,新的请求直接被丢弃。

信号之后——run-loop 的 SIGUSR1 处理器:

  1. markGatewayDraining():立刻拒绝新入队的任务,让调用方拿到明确的"正在重启"错误,而不是静默丢失
  2. 对正在压缩上下文的运行做尽力而为的中止(免得它跨重启边界持有会话写锁)
  3. 等待活跃任务与嵌入式运行排空,超时 DRAIN_TIMEOUT_MS = 90_000
  4. server.close(),并把 restartExpectedMs: 1500 告知客户端,让 TUI/App 知道该等多久再重连
  5. 优先整进程重启(restartGatewayProcessWithFreshPid(),拿到新 PID);不可行时回退到进程内重启——重新获取网关锁、重新起服务,PID 不变

整个过程还有一个看门狗:超过 DRAIN_TIMEOUT_MS + 25s 仍未收尾就以非零码退出,让被 systemd/launchd 托管的实例干净地被拉起。这个 25 秒是从 SUPERVISOR_STOP_TIMEOUT_MS(30 秒)里减掉 5 秒得来的——必须抢在服务管理器的硬杀之前生效。

15.6.3 延迟重启

重启不会贸然中断进行中的 LLM 调用——发信号前系统最多等 5 分钟让它们完成。用户不会因为一次配置变更而失去一个耗时几分钟的 Agent 回复。

5 分钟这个数字的出处在源码注释里写得很朴素:// Default to 5 minutes to avoid aborting in-flight subagent LLM calls. 它不是某个基准测试的结论,而是一个"够长到覆盖绝大多数带工具循环的回合、又短到不至于让运维干等"的工程取值——所以它是可配置的(gateway.reload.deferralTimeoutMs)。超过这个时长仍未排空?大概率是某个工具卡死了,强行重启反而是正解。

15.6.4 请求合并

密集的重启请求不会各自触发一次重启,合并发生在几个层面。配置文件监听器先做 300ms 防抖(gateway.reload.debounceMs,src/gateway/config-reload.ts),一次保存引起的多个文件事件只处理一次;并非所有配置变更都需要重启,能热加载的直接热加载。真要重启时,emitGatewayRestart() 用 emittedRestartToken/consumedRestartToken 这一对计数器保证"在途的重启信号只有一个",已经在等待排空的重启也不会再起第二个轮询。经 scheduleGatewaySigusr1Restart() 发起的请求(Agent 的 gateway 工具、/restart 命令等)还多一层:默认延迟 2 秒执行,期间再来的请求合并进已排定的那一次;距上一次发出重启信号不足 RESTART_COOLDOWN_MS = 30_000(30 秒)时,新的重启顺延到冷却期满。

图 15-2:重启调度器

sequenceDiagram
    participant Caller as 调用方(gateway 工具 / /restart)
    participant RS as 重启调度器
    participant GW as Gateway
    Caller->>RS: 请求重启
    RS->>RS: 排定定时器(默认 2 秒;距上次重启不足 30 秒则顺延)
    Caller->>RS: 再次请求重启
    RS->>RS: 合并到已排定的那一次
    RS->>GW: 发送 SIGUSR1(仅一次)
    GW->>GW: 排空并重启

15.6.5 跨平台服务管理

Linux  → systemctl --user restart openclaw-gateway.service(失败则退回系统级 systemctl restart)
macOS  → launchctl(kickstart / bootstrap 等多套语法逐一尝试)
Windows → schtasks(计划任务立即执行)

triggerOpenClawRestart() 依次尝试这些命令,每次 spawnSync 限时 2 秒(SPAWN_TIMEOUT_MS),并把试过的命令记进 tried 数组一并返回——排障时你能直接看到"它到底试了什么、各自报了什么错"。Linux 上先试 --user 再试系统级,是因为 OpenClaw 默认装成 systemd 用户服务。统一的 openclaw gateway restart 命令在所有平台上工作,底层适配不同的服务管理器。

macOS launchctl 的历史包袱。macOS 的 launchctl 有新旧两套子命令语法——老的 launchctl load/unload,新的 launchctl bootstrap/bootout。src/daemon/launchd.ts 注册服务一律走 bootstrap,卸载和清理旧服务时则 bootout 与 unload 各跑一遍,确保两种方式注册的服务都能被清掉;重启时 kickstart 失败会回退到 bootstrap 再 kickstart(src/infra/restart.ts)。

15.7 可观测性设计

15.7.1 健康检查端点

Gateway 在 HTTP 层暴露四个探针路径,分成存活和就绪两组(src/gateway/server-http.ts 的 GATEWAY_PROBE_STATUS_BY_PATH):

路径 语义 响应
/healthz、/health liveness(存活) 恒 200,{"ok":true,"status":"live"}
/readyz、/ready readiness(就绪) 就绪 200,未就绪 503

为什么存活探针要"恒 200"? 因为这两组探针回答的是不同的问题。存活探针回答"这个进程还需要被杀掉重启吗"——通道断开不是杀进程的理由,杀了也修不好。就绪探针才回答"现在能不能把流量交给它"。源码里甚至有一条专门的测试:"即使就绪检查报告通道故障,/healthz 也保持浅层"。把两者混为一谈,是容器编排里最常见的自伤方式——一个 Telegram token 过期就把整个网关卷进无尽重启循环。

就绪响应的详细程度还取决于调用方身份:通过认证(或来自可信来源)才返回 failing 列表与 uptimeMs,否则只回一个 {"ready":false}。健康端点是无认证可达的,不能顺手变成信息泄露面。

丰富的通道级状态另有出口——openclaw status、openclaw health 和网关 RPC 的 health 方法(见 15.7.2)。

15.7.2 通道健康监控

src/gateway/channel-health-monitor.ts(第 3 章详述)守护通道级健康。它每 5 分钟巡检一次(DEFAULT_CHECK_INTERVAL_MS),启动后有 60 秒宽限期不打扰(DEFAULT_MONITOR_STARTUP_GRACE_MS),重启同一通道之间要隔 2 个巡检周期(DEFAULT_COOLDOWN_CYCLES),且每小时最多重启 10 次(DEFAULT_MAX_RESTARTS_PER_HOUR)——最后这条是熔断:一个反复重启也修不好的通道,继续重启只会掩盖真正的问题。判定"不健康"的依据包括超过 30 分钟没有任何事件(DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS)。通道自身的重连退避则走 src/gateway/server-channels.ts 的 CHANNEL_RESTART_POLICY(基于 src/infra/backoff.ts)。

这与 Docker 的容器健康检查形成互补:

  • Docker 健康检查回答:"Gateway 进程还活着吗?"
  • 通道健康监控回答:"Gateway 的 Telegram 通道还工作吗?"

两者缺一不可——Gateway 进程可能活着但 Telegram 通道已断开(API token 过期),或者 Telegram 通道正常但 Gateway 即将耗尽内存。

15.7.3 配置变更审计

系统记录每次配置变更——不只是"配置发生了修改",还包括"哪个字段从什么值变成了什么值"。src/gateway/config-reload.ts 的 diffConfigPaths 函数计算精确的配置差异。

diffConfigPaths(prev, next) 递归比较两棵配置树,返回所有发生变化的点分路径。这些路径还会进入重启审计:src/infra/restart.ts 的 formatRestartAudit 把 actor、deviceId、clientIp 和 changedPaths(最多列 6 条,其余折叠成 +N more)拼成一行审计记录。于是事后分析能问出这样的问题:"Agent 在 10:15 开始行为异常——日志显示 10:12 有一次重启,changedPaths=agents.defaults.model,发起者是某个设备的 RPC 调用。"

15.8 基础设施模块

15.8.1 SSRF 防护

阻止 Agent 控制的工具(如网页抓取)访问内部网络资源。src/infra/net/ssrf.ts 的 isBlockedHostnameOrIp() 由两半组成。

主机名黑名单:localhost、localhost.localdomain、metadata.google.internal,以及任何以 .localhost、.local、.internal 结尾的名字。

IP 分类:不自己手写 CIDR,而是用 ipaddr.js 的 range() 分类,拒绝整类地址——IPv4 拒 loopback、private、linkLocal、carrierGradeNat、unspecified、broadcast、multicast、reserved,IPv6 拒 loopback、linkLocal、uniqueLocal、multicast、reserved、benchmarking 等。linkLocal 这一类涵盖了 169.254.0.0/16——AWS 实例元数据服务(169.254.169.254)就在里面,它握着 IAM 角色凭证;Agent 能读到它,就等于拿到了这台实例的 AWS 权限。

真正见功力的是三条防绕过措施:

  1. DNS 重绑定防护:不只在请求前检查主机名,还在 DNS 解析后用同一个分类函数检查每一个解析出来的地址(源码注释:Reuse the exact same host/IP classifier as the pre-DNS check to avoid drift.)。否则一个域名完全可以先解析到公网 IP 通过检查、再解析到 127.0.0.1。
  2. 畸形字面量一律拒绝:无法严格解析的 IPv6 字面量、非标准点分十进制的"传统" IPv4 写法(0177.0.0.1、2130706433 这类),直接判定为被阻止——fail closed。
  3. IPv6 内嵌 IPv4 拆包:IPv4-mapped、RFC 6052/6145、以及 6to4/ISATAP 这类哨兵格式里内嵌的 IPv4 地址会被抽出来单独判定,堵住"把 169.254.169.254 包进 IPv6 地址里"的路子。

例外由 SsrFPolicy 控制:allowedHostnames 与名字就在劝退的 dangerouslyAllowPrivateNetwork 用来放行,hostnameAllowlist 则反过来收紧——设了它,不在名单里的主机名一律拦截。要特别留意浏览器工具:它默认是 trusted-network 模式,没有显式配置 browser.ssrfPolicy 时允许访问私网(extensions/browser/src/browser/config.ts),云上部署应当显式关掉。SSRF 防护是部署在云环境中的 Agent 系统的必须项,不是可选项。

15.8.2 文件锁

跨进程顾问锁防止并发网关实例损坏共享状态。如果运营者意外起了两个网关(比如后台服务已在运行,又在终端里敲了一次 openclaw gateway),文件锁确保第二个实例检测到冲突并退出,而非两个实例竞争同一个配置和状态目录。

$ openclaw gateway
Gateway failed to start: gateway already running (pid 12345); lock timeout after 5000ms
If the gateway is supervised, stop it with: openclaw gateway stop

实现比"操作系统锁"更朴素也更可控:fs.open(lockPath, "wx") 依赖 O_EXCL 的原子创建语义,锁文件里写着持有者的 pid、createdAt、configPath(Linux 上还会记 /proc 里的进程启动时间)。之所以要记启动时间,是因为单看 PID 会认错人——进程死后 PID 可能被复用,启动时间才能确认"这个 PID 还是当初那个进程"。取不到锁时它会读出持有者信息判定其死活:确认已死就清掉陈旧锁文件重试,还活着就继续轮询等待,默认 5 秒后仍拿不到锁就带着对方的 PID 报错退出。

15.8.3 状态迁移

OpenClaw 升级后若状态目录的布局变了,由 openclaw doctor 检测并迁移遗留状态(实现在 src/infra/state-migrations.ts,src/commands/doctor-state-migrations.ts 只是转出口;非交互模式下直接执行)。推荐的升级入口 openclaw update 会识别安装方式(npm 或 git)、拉取新版本、跑一遍 openclaw doctor --non-interactive 再重启网关,所以正常升级不需要手动触发迁移。普通 CLI 启动只做轻量的配置校验,不跑这批状态迁移(src/cli/program/config-guard.ts 里显式传了 migrateState: false)。

迁移的是目录与文件布局,不是带版本号的数据格式:旧的状态目录整体改名到新位置,并在旧路径留一个符号链接;旧会话目录、旧 Agent 目录里的文件搬进新的按 Agent 划分的目录,搬不走的连同旧目录改名为 .legacy-<时间戳> 备份留存;配对白名单之类按计划复制。这与数据库迁移(如 Rails 的 db:migrate)思路相近,但更简单——状态存储是文件系统(JSON/JSONL),迁移基本就是 rename、copy 和符号链接。迁移没有配套的降级路径:升级前想留后路,先用下一节的备份。

15.8.4 备份系统

src/infra/backup-create.ts 和 src/infra/archive-*.ts 驱动状态目录的备份(手动执行,或由你自己的定时任务调用):

openclaw backup create                      # 在当前目录写一个带时间戳的归档
openclaw backup create --output ~/Backups   # 写到指定目录
openclaw backup create --dry-run --json     # 只打印备份计划,不落盘
openclaw backup create --verify             # 写完立刻校验清单与载荷布局
openclaw backup verify <archive>            # 单独校验一个归档

备份范围是配置、凭证、会话与工作区,产物是 <ISO 时间戳>-openclaw-backup.tar.gz,内含一份 manifest 供校验。两个开关值得知道:--only-config 只备份当前生效的 JSON 配置文件,--no-include-workspace 排除体积最大的工作区目录。

要注意的是 CLI 只提供 create 和 verify——没有 restore 子命令。恢复是手工解包回状态目录的过程,--verify 的存在正是为了让你在真需要恢复的那天不至于发现归档是坏的。日志不在备份范围内(默认就在 /tmp 下,体积大且时效性强)。

15.8.5 重试基础设施

src/infra/backoff.ts 封装了带抖动的指数退避,用于配置加载重试、通道重启等场景。整个模块只有两个导出,核心就 4 行:

// src/infra/backoff.ts:3-14(类型定义压成一行)
export type BackoffPolicy = { initialMs: number; maxMs: number; factor: number; jitter: number };

export function computeBackoff(policy: BackoffPolicy, attempt: number) {
  const base = policy.initialMs * policy.factor ** Math.max(attempt - 1, 0);
  const jitter = base * policy.jitter * Math.random();
  return Math.min(policy.maxMs, Math.round(base + jitter));
}

抖动是加性的:延迟落在 [base, base × (1 + jitter)] 区间内,再被 maxMs 截顶。这不是 AWS 那篇《Exponential Backoff and Jitter》里的 "Full Jitter"(random(0, base)),而是一个更保守的变体——下界保住了 base,不会出现"退避了却几乎立刻重试"的情况;上界的随机抖动则用来打散同时到期的重试。

为什么这个取舍对 OpenClaw 合适?因为它的并发规模不是"十万个客户端撞同一个 API",而是"一个网关的若干条通道和若干个 Provider 调用"。惊群效应确实存在,但更现实的风险是退避太激进导致恢复变慢。把下界钉在 base 上、只用抖动做打散,是对这个规模的合理回应。

另一个导出 sleepWithAbort(ms, signal) 让退避等待可被中断——重启或关闭时不必干等一个 5 分钟的退避走完。

各处的策略参数各自定义,例如通道重启用 src/gateway/server-channels.ts 的 CHANNEL_RESTART_POLICY,配置加载重试用 src/agents/context.ts 的 CONFIG_LOAD_RETRY_POLICY;出站投递队列另有一套 computeBackoffMs(src/infra/outbound/delivery-queue-recovery.ts)。

15.8.6 性能优化实践

部署到生产环境后,性能调优是持续运维的核心关注点。OpenClaw 涉及的性能维度与传统 Web 服务有显著不同——Token 消耗是首要成本项,延迟敏感度极高,而并发模型受限于 LLM API 的速率限制。

Token 优化策略

Token 是 LLM 调用的"货币",每一个多余的 Token 都直接转化为成本和延迟。以下是经过验证的优化实践:

策略 实现方式 作用机理
系统提示精简 审查工作区里的 AGENTS.md 等常驻文本,移除冗余指令 系统提示每轮都重发,与工具定义一样属于"每次对话都要付一遍"的固定开销
技能按需加载 依赖第 16 章的按需加载,只注入技能的元数据,正文按需读取 技能数量的增长不再线性推高每轮上下文
上下文压缩 配置 agents.defaults.contextTokens 上限,触发第 5 章的自动压缩 把线性增长的历史折成常数量级的摘要
工具输出截断 利用工具策略管线(第 10 章)的输出限制 一次 cat 大文件就可能顶满窗口,截断是止损而非优化
模型分级使用 轻量任务(分类、路由)用小模型,复杂推理用大模型 单价差异常有数量级之别,而路由类任务对模型能力并不敏感

具体能省多少完全取决于你的工作负载(提示体积、工具输出量、对话长度、模型单价),本专栏不给百分比——请用下面的命令在你自己的日志上量。

# 网关自带的用量成本汇总(数据来自会话记录,而不是滚动日志)
openclaw gateway usage-cost --days 30
openclaw gateway usage-cost --days 7 --json

流式响应延迟优化

用户感知的延迟由两部分组成:首 Token 延迟(Time-to-First-Token, TTFT) 和 Token 间延迟(Inter-Token Latency, ITL)。

优化首 Token 延迟的关键措施:

  • 选择地理位置接近的 API 端点:网络往返时间直接叠加到 TTFT
  • 启用 Provider 缓存:命中 prompt cache 的前缀无需重新计算,TTFT 与成本同时下降;OpenClaw 在用量统计里单列 cacheRead / cacheWrite(src/agents/stream-message-shared.ts),可以直接看命中情况
  • 减少系统提示体积:更短的 prompt 意味着更快的首 Token 到达
  • 利用流式输出:模型调用本身走流式接口(StreamFn),TUI、Web 控制台收到增量即显示;聊天通道的分块流式回复(agents.defaults.blockStreamingDefault)默认是 "off",想让 Telegram 等通道边生成边发送需显式打开

优化 Token 间延迟:

  • 思维链呈现方式:ReasoningLevel 有 off / on / stream 三档(src/auto-reply/thinking.shared.ts),stream 边推理边显示,用户的感知等待最短——注意这不改变实际耗时,只改变等待的体感
  • 避免阻塞式工具调用:第 4.4.4 节的压缩与流式协调机制确保压缩操作在流式间隙执行

大规模并发处理

OpenClaw 面对的并发挑战与传统 Web 服务不同——瓶颈不在 CPU 或内存,而在 LLM API 的速率限制(RPM/TPM) 和 每次调用的高延迟(秒级):

  • Provider 级别速率控制:不同 Provider 有独立的速率限制。配置多个 Provider 并利用第 4 章的故障转移机制,可以突破单一供应商的限制
  • 限流与过载处理:遇到限流或过载错误时,嵌入式运行器(src/agents/pi-embedded-runner/run.ts)会轮换认证档案、必要时降级到备用模型(第 4 章;过载时还会先按配置退避片刻),而不是在本地排队无限重试
  • 会话隔离:第 5 章的会话管理确保并发对话的上下文互不干扰——100 个用户同时对话,每个都有独立的上下文窗口
  • 子 Agent 并发:第 18 章的扇出编排模式允许多个子 Agent 并行处理独立子任务,但需注意总 RPM 消耗

缓存策略

OpenClaw 在多个层面实施缓存:

  1. 模型目录缓存:loadModelCatalog 用 modelCatalogPromise(第 4 章)的 Promise 单例避免重复发现,useCache: false 可强制刷新
  2. LLM Prompt 缓存:利用 Provider 原生的 prompt caching——相同前缀的请求命中缓存后,这部分输入 token 按显著更低的价格计费(具体折扣以各家定价页为准)
  3. 文件系统缓存:models.json 文件作为本地缓存,原子写入避免竞态
  4. 连接复用:HTTP Keep-Alive 和连接池减少 TLS 握手开销

💡 运维建议:Prompt caching 的命中率是成本优化的关键指标,而它对前缀稳定性极其敏感——系统提示里但凡有一个时间戳、一个随机 ID 或一段每轮都在变的动态内容,后面所有内容的缓存都会失效。如果 cacheRead 持续为零,先去检查系统提示的前缀是不是每轮都在变。

15.8.7 故障排查指南

生产环境中的故障排查需要系统化的方法。以下是 OpenClaw 运维中最常见的问题、诊断路径和解决方案。

常见错误消息速查表

错误消息 含义 解决方案
Gateway not running.(openclaw doctor 输出) Gateway 未在运行 已装服务就 openclaw gateway start;没装过则 openclaw gateway install,或在前台跑 openclaw gateway
ECONNREFUSED 127.0.0.1:18789 Gateway 端口未监听 检查是否有端口冲突:openclaw gateway status --deep、lsof -i :18789
401 Unauthorized (Provider) API 密钥无效或过期 重跑 openclaw onboard 或 openclaw doctor 修复凭证
429 Too Many Requests 超过 Provider 速率限制 等待冷却期或切换到备用 Provider
Provider 报 context window / context overflow 类错误 上下文超过模型窗口限制 调整 agents.defaults.contextTokens 或切换到更大窗口的模型
Blocked hostname or private/internal/special-use IP address 出站请求被 SSRF 防护拦截 检查目标地址是否为私有 IP 或受限域名
pairing required 设备尚未获批配对 openclaw devices list 找到请求 ID,openclaw devices approve <requestId> 后重连
Channel ERROR 通道连接断开 检查对应通道的 Token/Webhook 配置
ENOSPC 磁盘空间不足 清理 /tmp/openclaw/ 下的滚动日志(超过 24 小时的会被自动清理,但突发写入可能来不及)
signal SIGTERM received 进程被系统终止 检查 OOM Killer 或 systemd 超时设置

诊断分层方法

按照从外到内的顺序排查:

第 1 层 — 进程与服务
  └─ Gateway 是否在运行?
  └─ systemd 状态是否正常?
  └─ 端口是否在监听?

第 2 层 — 网络与通道
  └─ 各通道连接状态?
  └─ Provider API 是否可达?
  └─ DNS 解析是否正常?

第 3 层 — 配置与认证
  └─ API 密钥是否有效?
  └─ 配置文件语法是否正确?
  └─ 环境变量是否已设置?

第 4 层 — 逻辑与行为
  └─ Agent 指令是否正确?
  └─ 技能是否正确加载?
  └─ 工具权限是否配置?

日志分析实用命令

# 日志默认在 /tmp/openclaw/ 下按天滚动;也可以直接用 openclaw logs
openclaw logs --limit 200            # 通过 RPC 拉取(网关在跑时最方便)
openclaw logs --follow --json        # 跟随输出,JSON 行

LOG=/tmp/openclaw/openclaw-$(date +%F).log   # 若配置了 logging.file 则以其为准

# 查看最近的错误日志
cat "$LOG" | jq 'select(._meta.logLevelName == "ERROR")' | tail -20

# 按子系统过滤日志(子系统以 JSON 字符串存在 _meta.name 里)
cat "$LOG" | jq 'select((._meta.name // "") | contains("discord"))' | tail -20

# 查看特定时间范围的日志(time 是带本地时区偏移的 ISO 字符串)
cat "$LOG" | jq \
  'select(.time >= "2026-03-21T10:00" and .time < "2026-03-21T11:00")'

# 统计各级别日志数量
cat "$LOG" | jq -r '._meta.logLevelName' | sort | uniq -c | sort -rn

# 追踪特定会话(会话标识没有固定字段,直接按字符串搜)
grep -F '<会话 key>' "$LOG" | jq '.'

# 实时监控日志(类似 tail -f)
tail -f "$LOG" | jq '.'

调试技巧

  1. 提升日志级别:通过环境变量临时开启详细日志

    OPENCLAW_LOG_LEVEL=debug openclaw gateway run
    # 或用根级选项:openclaw --log-level debug gateway run

    注意 --verbose 只影响控制台,不会提升文件日志级别。

  2. TUI 内调试:用 /verbose on 和 /reasoning on 实时观察 Agent 推理过程(TUI 里这两个命令只提供 on/off)

  3. Bang 模式本地诊断:在 TUI 中用 !curl 或 !ping 直接诊断网络问题,不消耗 Token

  4. 健康检查端点:访问 Gateway 的健康检查端点获取结构化诊断信息

    curl -s http://127.0.0.1:18789/healthz   # {"ok":true,"status":"live"}
    curl -s http://127.0.0.1:18789/readyz    # 就绪则 200,未就绪 503
  5. 配置验证:使用 openclaw doctor 自动检查常见配置问题

⚠️ 生产环境调试注意:debug/trace 级别会大幅增加日志量,可能快速吃满磁盘(默认日志在 /tmp 下,单文件上限 500MB)。建议 OPENCLAW_LOG_LEVEL=debug 只在排查期间临时使用,排查完毕即恢复默认。

15.9 Dockerfile 构建优化细节

15.9.1 BuildKit 缓存挂载

RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
    NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile

BuildKit 的缓存挂载让 pnpm 的内容寻址 store 在多次构建之间持久化——即使 lockfile 变化触发了重新安装,已下载的包仍在 store 里,不必再从 registry 拉一遍。sharing=locked 保证并行构建不会同时写坏这个 store。同样的手法也用在 apt 上(openclaw-bookworm-apt-cache / openclaw-bookworm-apt-lists)。

15.9.2 Bun 安装的重试机制

OpenClaw 的 Docker 构建给 Bun 安装留了最多 5 次尝试,退避是线性的(attempt * 2 秒):

RUN set -eux; \
    for attempt in 1 2 3 4 5; do \
      if curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL https://bun.sh/install | bash; then \
        break; \
      fi; \
      if [ "$attempt" -eq 5 ]; then exit 1; fi; \
      sleep $((attempt * 2)); \
    done

注意这里是两层重试:curl 自己重试 5 次(--retry-all-errors --retry-delay 2),外层最多把整个安装流程跑 5 遍。Dockerfile 的注释说明了原因——"容忍 CI 镜像构建期间来自 bun.sh/GitHub 的瞬时 5xx"。同一模式还用在 corepack prepare 上。构建期的网络依赖是最容易让 CI 变红的一环,而它的失败几乎总是瞬时的。

15.9.3 .dockerignore 的重要性

一个常常遭到忽略的优化点——仓库根目录的 .dockerignore 排除 node_modules、.git、构建产物等不需要进入构建上下文的内容。构建上下文是 docker build 开始前完整打包发给守护进程的,一个装满了 node_modules 和 .git 历史的上下文能把"改一行代码重建"从几十秒拖成几分钟。

15.10 实战推演:从零到生产的完整部署

让我们走一遍从裸机到生产级部署的完整流程,在每一步中标注"为什么这样做"。

15.10.1 场景

你有一台 Hetzner VPS(4核 8GB,Ubuntu 22.04),要部署一个连接 Telegram 和 Discord 的个人 Agent。

15.10.2 部署脚本(带注释)

脚本分成两段:前一段以 root 身份执行,后一段必须以 openclaw 用户在它自己的登录会话里执行(例如 ssh openclaw@<服务器>)。不能在同一个脚本里用 su - openclaw 切过去——su 拿不到 systemd 用户会话,--install-daemon 检测到用户服务不可用就会跳过服务安装。

#!/bin/bash
# 第一段:以 root 执行
set -euo pipefail  # 任何错误立即停止——部署脚本不能"差不多行"

# 1. 创建非 root 用户 —— 最小权限原则;同时开启 linger,用户服务不随登录会话结束而停
useradd -m -s /bin/bash openclaw
loginctl enable-linger openclaw

# 2. 安装 Node.js(推荐 24)和 OpenClaw(全局安装需要 root)
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
npm install -g openclaw@latest
# 注意:如果用 nvm 之类的版本管理器装 node,服务审计会报
# gateway-runtime-node-version-manager —— 服务重启后路径可能失效

# 3. 浏览器自动化需要真正的 Chrome:Ubuntu 的 apt chromium 只是 snap 的壳,
#    官方排障文档推荐装 Google Chrome 的 .deb(/usr/bin/google-chrome-stable 会被自动发现;
#    这个包只有 amd64 版)
wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt-get install -y ./google-chrome-stable_current_amd64.deb

# 4. 配置防火墙
ufw allow 22/tcp       # SSH
ufw allow 443/tcp      # Caddy HTTPS
ufw default deny incoming
ufw --force enable     # 脚本里要 --force 跳过交互确认
#!/bin/bash
# 第二段:以 openclaw 用户、在它自己的登录会话里执行
set -euo pipefail

# 5. 非交互式引导 + 安装守护进程 —— CI/CD 友好
#    密钥写进 ~/.openclaw/.env(网关启动时会加载它),配置里只存 SecretRef 引用
mkdir -p ~/.openclaw && touch ~/.openclaw/.env && chmod 600 ~/.openclaw/.env
cat >> ~/.openclaw/.env <<ENV
OPENAI_API_KEY=${OPENAI_API_KEY}
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
ENV
openclaw onboard --non-interactive --install-daemon \
  --auth-choice openai-api-key \
  --secret-input-mode ref \
  --accept-risk

# 6. 配置通道(令牌走环境变量引用,不以明文写进配置文件)
openclaw config set channels.telegram.botToken \
  --ref-provider default --ref-source env --ref-id TELEGRAM_BOT_TOKEN
openclaw config set channels.discord.token \
  --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
openclaw config set browser.headless true        # 服务器没有图形界面

# 7. 安全加固
openclaw config set gateway.bind "loopback"       # 只监听本地——通过 Caddy 反代暴露
                                                  # 取值:auto|lan|loopback|custom|tailnet
openclaw config set tools.exec.security "allowlist"  # 不配时按主机分流;显式写死才不随主机变
openclaw security audit --fix                     # 自动修复权限问题

# 8. 重启服务让第 6、7 步生效(第 5 步的 --install-daemon 已经注册并启动了它)
openclaw gateway restart

# 9. 验证
openclaw gateway status      # 应该显示 running
openclaw doctor              # 应该全绿

15.10.3 每个决策的安全影响

步骤 如果省略会怎样
非 root 用户 攻击者突破 Agent 后获得 root 权限
bind: loopback 改成 lan 后网关端口直接面向网络,安全全押在认证凭据上
显式写 deny / 谨慎使用 allowlist 放到 full 则 Agent 可以执行任意命令(包括 rm -rf /)
security audit --fix 配置文件可能 world-readable(密钥泄露)
enable-linger SSH 断开后服务停止
防火墙 Gateway 端口直接暴露在公网

部署不是把代码放到服务器上就完事了——它是把代码放到一个敌意环境中并确保它能自我保护。互联网上的每台服务器每天都在被扫描、被探测、被尝试入侵。你的部署脚本不仅是在安装软件,更是在构筑防线。每一行配置都在回答一个问题:"如果有人试图攻击这里,会发生什么?"

15.11 生产就绪清单

与其对着一张随时会过期的横向对比表打勾,不如把 OpenClaw 在"生产就绪"这件事上真正做了什么列成一张可核对的清单——你可以拿它去检查任何一个自建的 Agent 系统:

生产就绪项 OpenClaw 的做法 出处
多阶段构建 4 个实质构建阶段(另有 2 个只选基础镜像的别名阶段),基础镜像按 SHA256 摘要锁定 Dockerfile
非 root 执行 USER node(uid 1000) Dockerfile
存活/就绪分离 /healthz 恒 200,/readyz 反映真实就绪 src/gateway/server-http.ts
结构化日志 JSON Lines + 子系统哈希着色 + 按天滚动 src/logging/logger.ts
日志脱敏 17 条默认模式,可扩展 src/logging/redact.ts
优雅重启 5 分钟延迟窗口 + 90 秒排空 + 在途去重与 30 秒冷却 src/infra/restart.ts、src/cli/gateway-cli/run-loop.ts
跨平台服务 systemd / launchd / schtasks 统一接口 src/daemon/service.ts
服务配置审计 入口、运行时、PATH、令牌、单元字段 src/daemon/service-audit.ts
SSRF 防护 IPv4/IPv6 分类 + DNS 重绑定防护 + fail closed src/infra/net/ssrf.ts
单实例保证 O_EXCL 锁文件 + PID/启动时间校验 src/infra/gateway-lock.ts
状态迁移 openclaw doctor(openclaw update 会自动调用)执行的遗留状态迁移 src/infra/state-migrations.ts
备份 backup create / backup verify,带 manifest src/infra/backup-create.ts
配置变更审计 字段级 diff + actor/device/ip 记录 src/gateway/config-reload.ts

15.12 关键源码文件

文件 用途
Dockerfile 多阶段构建
docker-compose.yml 双容器编排
src/logging/logger.ts 日志核心——双输出、滚动、大小限制
src/logging/redact.ts 自动脱敏(17 种模式)
src/logging/config.ts 日志配置加载
src/logging/levels.ts 日志级别定义和标准化
src/logging/env-log-level.ts 环境变量级别覆盖
src/logging/console.ts 控制台输出样式
src/daemon/service.ts 统一服务管理接口
src/daemon/systemd.ts Linux systemd 适配
src/daemon/systemd-unit.ts systemd unit 文件生成
src/daemon/systemd-linger.ts systemd linger 管理
src/daemon/launchd.ts macOS launchd 适配
src/daemon/launchd-plist.ts launchd plist 文件生成
src/daemon/schtasks.ts Windows 计划任务适配
src/daemon/constants.ts 跨平台服务常量
src/daemon/diagnostics.ts 服务诊断
src/infra/net/ssrf.ts SSRF 防护
src/infra/backoff.ts 指数退避重试
src/infra/backup-create.ts 备份创建
src/infra/archive-staging.ts 归档暂存
src/gateway/channel-health-monitor.ts 通道健康监控
src/gateway/config-reload.ts 配置热重载和变更审计
src/daemon/service-audit.ts 服务配置审计(入口/运行时/PATH/令牌/单元字段)
src/infra/restart.ts 重启调度:延迟窗口、去重、冷却、跨平台触发
src/cli/gateway-cli/run-loop.ts SIGUSR1 排空与整进程/进程内重启
src/cli/daemon-cli/restart-health.ts 重启后的端口健康轮询
src/infra/gateway-lock.ts 单实例锁(O_EXCL + PID/启动时间校验)
src/gateway/server-http.ts /healthz、/readyz 探针

15.13 本章小结

OpenClaw 的部署与运维架构将生产就绪视为一等关注点。让我们回顾核心设计决策和它们之间的内在联系:

  1. 三种部署模型共存。裸机、系统服务、容器化——不是演进替代关系,而是适应不同场景的并行方案。仓库的 docs/install/ 下备着从 Raspberry Pi 到 Kubernetes 的十余种部署路径,同一套代码适应所有环境。

  2. 日志系统的安全默认。17 条默认脱敏模式 + SecretRef(第 13 章)形成双重防线。JSON Lines 格式兼顾人类可读和机器可查。永不崩溃的写入策略确保日志问题不会扩散为服务故障。

  3. 优雅重启的工程细节。SIGUSR1 避免被 SSH 断开之类的事件误触发;发信号前最多等 5 分钟让在途 LLM 调用收工;信号之后再给 90 秒排空活跃任务;在途去重与 30 秒冷却合并密集的重启请求;跨平台服务管理统一操作体验。

  4. 可观测性的多层设计。存活/就绪探针(进程级)+ 通道健康监控(通道级)+ 配置变更审计(操作级)——三个层面共同提供"系统现在怎么样?"的完整答案。存活与就绪必须分开,否则一个过期的通道 token 就能把整个网关拖进重启循环。

  5. Docker 的安全工程。四阶段构建(ext-deps / build / runtime-assets / 运行时)优化镜像体积,基础镜像按摘要锁定保证可重现,非 root + cap_drop + no-new-privileges 落实最小权限,apt 密钥指纹校验保障供应链安全。

核心洞察:生产就绪不是最后添加的功能——它是从一开始就渗透每个设计决策的品质。日志脱敏与配置里的 SecretRef 从两端守住密钥。重启的延迟窗口照顾在途的 LLM 调用。Docker 的非 root 执行对应着 /home/node/.openclaw 而非 /root/.openclaw 这样的状态路径。SSRF 防护守在 Agent 工具的出站抓取上(isBlockedHostnameOrIp 在 DNS 解析前后各查一次)。这些横切关注点不是独立的"运维功能"——它们是系统架构的有机组成部分。

造就"开发中可用"与"生产中可靠"之间差异的,不是某一个大功能,而是一百个小细节。 谁会在乎日志着色用的是字符串哈希还是固定映射?——凌晨三点 SSH 到远程服务器的你会在乎。颜色一致、结构清晰、敏感信息已脱敏——那一刻,你会感谢每一个"微不足道"的决定。

本章解决了"Agent 在哪里跑"。下一章回答"Agent 怎么变得更聪明"——技能系统。基础设施给了 Agent 生存的土壤,技能给了它进化的阶梯。

思考题

  1. 概念理解:OpenClaw 的跨平台 Daemon 管理为什么要统一封装 systemd/launchd/schtasks,而不是让用户直接使用各平台原生工具?这种抽象的成本是什么?
  2. 实践应用:设计一个 OpenClaw 的高可用部署方案——当主节点宕机时,备份节点能在 30 秒内接管所有通道连接。你会面临哪些技术挑战(会话状态迁移、通道重连、配置同步)?
  3. 开放讨论:对于个人开发者和小团队,"在自己的机器上跑 Agent"与"使用云端 Agent 服务"各有什么利弊?OpenClaw 的本地部署定位在未来会如何演变?

📚 推荐阅读