【openclaw】OpenClaw Daemon 模块超深度架构分析

OpenClaw Daemon 模块超深度架构分析

分析版本:2026-04-20 | 代码目录:src/daemon/


一、模块定位

1.1 业务职责

daemon 模块是 OpenClaw 的服务守护进程管理层,负责将 Gateway(和 Node Host)进程注册为操作系统原生的后台服务,实现开机自启、崩溃自动重启、统一启停控制。核心职责包括:

  • 跨平台服务管理:封装 macOS LaunchAgent、Linux systemd user service、Windows Scheduled Task 三套系统守护机制为统一 API
  • 服务全生命周期控制:安装(install)、卸载(uninstall)、启动(start)、停止(stop)、重启(restart)、状态查询(readRuntime)
  • 服务配置生成:为每种平台生成对应的服务描述文件(plist / .service / .cmd)
  • 服务审计与诊断:检测配置漂移、Token 不匹配、PATH 非最小化、运行时版本管理器依赖等风险
  • 遗留服务迁移:自动发现并清理旧版 clawdbot 等遗留服务单元
  • 环境构建:为守护进程构建最小化 PATH、TLS CA 证书、代理环境等

1.2 在系统中的位置

复制代码
CLI 入口 (src/cli/)
   ↓ 调用
daemon 模块 (src/daemon/)
   ↓ 调用
操作系统原生服务管理器 (launchd / systemd / schtasks)
   ↓ 管理
Gateway/Node Host 进程

daemon 模块位于 CLI 层与操作系统层之间 ,是用户态命令(openclaw gateway install/start/stop/restart)到系统服务管理器的桥梁。它不直接运行 Gateway 进程,而是通过 OS 服务管理器间接管理。

1.3 核心业务价值

价值维度 具体体现
零宕机 KeepAlive (macOS) / Restart=always (Linux) / ONLOGON (Windows) 保证服务持续运行
开箱即用 一条命令完成跨平台服务注册,用户无需手动编写 plist/service/cmd
安全隔离 最小化 PATH、umask 077、Token 审计防止配置漂移
版本迁移 自动检测版本管理器 Node/Bun 路径,引导用户迁移到系统 Node
向后兼容 自动发现和清理 clawdbot 时代的遗留服务单元

二、模块整体结构

2.1 文件分类与依赖关系

整个 daemon 模块按功能分为 6 个子系统

① 核心服务抽象层(Service Abstraction)
文件 行数 职责
service.ts 225 核心入口 --- GatewayService 接口定义 + 平台注册表 + 状态读取/启动逻辑
service-types.ts 60 类型定义 --- Install/Control/Manage/Stage/RestartResult 等所有参数与返回类型
service-runtime.ts 13 运行时状态类型 --- GatewayServiceRuntime
service-env.ts 368 环境构建 --- 最小化 PATH 构建 + 服务环境变量组装
service-audit.ts 427 配置审计 --- 20+ 检查项:Token 漂移、PATH 非最小化、运行时版本管理器、systemd/launchd 配置完整性
node-service.ts 69 Node Host 服务适配 --- 复用 GatewayService 但注入 Node 专用环境变量
② macOS LaunchAgent 实现
文件 行数 职责
launchd.ts 768 LaunchAgent 完整实现 --- plist 写入、bootstrap/bootout、kickstart、状态读取、遗留清理
launchd-plist.ts 117 plist XML 生成与解析 --- 构建/读取 plist 文件
launchd-restart-handoff.ts 155 重启交接 --- 从 launchd 管理的进程内部安全重启(detached shell 脚本)
③ Linux systemd 实现
文件 行数 职责
systemd.ts 776 systemd 完整实现 --- unit 写入、enable/restart/stop、状态读取、遗留清理、sudo 降级
systemd-unit.ts 139 systemd unit 文件生成与解析 --- ExecStart/Environment/EnvironmentFile 渲染
systemd-linger.ts 78 loginctl enable-linger 管理 --- 保证无头服务器上用户 systemd 会话持久化
systemd-unavailable.ts 54 systemd 不可用分类 --- missing_systemctl / user_bus_unavailable / generic_unavailable
systemd-hints.ts 41 不可用时的用户提示 --- WSL/容器/无头服务器场景指导
④ Windows Scheduled Task 实现
文件 行数 职责
schtasks.ts 859 Scheduled Task 完整实现 --- 任务脚本生成、schtasks 注册、启动文件夹降级、端口释放
schtasks-exec.ts 24 schtasks 命令执行封装 --- 超时 15s + 无输出超时 5s
cmd-argv.ts 26 cmd.exe 命令行参数引用/解析
cmd-set.ts 64 set 命令赋值引用/解析 --- 处理 ^, %, !, " 转义
arg-split.ts 48 通用参数分割 --- 支持引号保持 + 三种转义模式
⑤ 运行时与路径工具
文件 行数 职责
runtime-binary.ts 26 运行时检测 --- isNodeRuntime / isBunRuntime
runtime-paths.ts 186 Node 路径解析 --- 版本管理器路径检测、系统 Node 查找、版本验证
runtime-parse.ts 22 键值对输出解析 --- 统一解析 launchctl print / systemctl show / schtasks 输出
runtime-format.ts 44 运行时状态格式化 --- 统一输出格式
runtime-hints.ts 52 平台运行时提示 --- 日志路径、启动命令
gateway-entrypoint.ts 67 Gateway 入口点解析 --- 定位 dist/ 下的可执行入口
program-args.ts 298 程序参数组装 --- 根据 runtime(dev/node/bun/auto) + dev 模式构建完整命令行
paths.ts 43 路径工具 --- HOME 解析、~ 展开、状态目录解析
⑥ 辅助工具
文件 行数 职责
output.ts 21 终端格式化输出 --- label: value 样式
exec-file.ts 32 execFileUtf8 封装 --- 统一错误处理
inspect.ts 442 外部服务检测 --- 扫描 LaunchAgents/systemd/ScheduledTasks 中 OpenClaw 相关服务
diagnostics.ts 44 日志诊断 --- 读取最后一条错误日志行
container-context.ts 11 容器环境检测
constants.ts 112 常量定义 --- 服务标签、Profile 后缀、描述格式化

2.2 核心接口 GatewayService

typescript 复制代码
type GatewayService = {
  label: string;                    // 服务类型名 "LaunchAgent" | "systemd" | "Scheduled Task"
  loadedText: string;               // 已加载文本 "loaded" | "enabled" | "registered"
  notLoadedText: string;            // 未加载文本 "not loaded" | "disabled" | "missing"
  stage: (args) => Promise<void>;   // 仅写入配置文件(不激活)
  install: (args) => Promise<void>; // 写入 + 激活服务
  uninstall: (args) => Promise<void>; // 卸载服务
  stop: (args) => Promise<void>;    // 停止服务
  restart: (args) => Promise<GatewayServiceRestartResult>; // 重启服务
  isLoaded: (args) => Promise<boolean>; // 服务是否已加载
  readCommand: (env) => Promise<GatewayServiceCommandConfig | null>; // 读取服务命令配置
  readRuntime: (env) => Promise<GatewayServiceRuntime>; // 读取运行时状态
};

平台注册表process.platform 映射到具体实现:

typescript 复制代码
const GATEWAY_SERVICE_REGISTRY = {
  darwin: { label: "LaunchAgent", ... launchd.* 实现 ... },
  linux:  { label: "systemd",    ... systemd.* 实现 ... },
  win32:  { label: "Scheduled Task", ... schtasks.* 实现 ... },
};

2.3 数据流入流出方式

复制代码
CLI 命令 → service.ts::resolveGatewayService()
         → 按平台选择实现 (launchd / systemd / schtasks)
         → 调用 install/stop/restart/readRuntime 等
         → 写入服务配置文件 (plist / .service / .cmd)
         → 执行系统命令 (launchctl / systemctl / schtasks)
         → 返回 GatewayServiceRuntime / GatewayServiceState

三、核心业务逻辑深度解析

3.1 macOS LaunchAgent 子系统 (launchd.ts --- 768行逐行解析)

3.1.1 导入与常量 (L1-35)
typescript 复制代码
import fs from "node:fs/promises";  // 异步文件系统操作
import path from "node:path";       // 路径拼接工具
  • parseStrictInteger / parseStrictPositiveInteger --- 从 infra/parse-finite-number 导入,严格整数解析,用于端口和 PID 解析
  • cleanStaleGatewayProcessesSync --- 同步清理占用端口的旧 gateway 进程,在重启时使用(同步是因为需要在 spawn 新进程前确保端口释放)
  • normalizeLowercaseStringOrEmpty --- 安全字符串归一化,用于 launchctl 输出匹配
  • sanitizeForLog --- 清理日志中的敏感信息
  • constants.js 导入 GATEWAY_LAUNCH_AGENT_LABEL ("ai.openclaw.gateway")、resolveGatewayLaunchAgentLabelresolveLegacyGatewayLaunchAgentLabels
  • execFileUtf8 --- 统一子进程执行封装
  • buildLaunchAgentPlistImpl --- plist 生成委托给 launchd-plist.ts
  • isCurrentProcessLaunchdServiceLabel / scheduleDetachedLaunchdRestartHandoff --- 重启交接机制

常量:

typescript 复制代码
const LAUNCH_AGENT_DIR_MODE = 0o755;  // LaunchAgents 目录权限
const LAUNCH_AGENT_PLIST_MODE = 0o644; // plist 文件权限
3.1.2 标签验证与解析 (L36-50)
typescript 复制代码
function assertValidLaunchAgentLabel(label: string): string {
  const trimmed = label.trim();  // 去首尾空白
  if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {  // 仅允许字母数字点横线下划线
    throw new Error(`Invalid launchd label: ${sanitizeForLog(trimmed)}`);  // 拒绝注入
  }
  return trimmed;
}

设计目的: launchd 标签直接用于文件名和 launchctl 命令参数,必须防止路径遍历和命令注入。

typescript 复制代码
function resolveLaunchAgentLabel(args?: { env? }): string {
  const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim();  // 优先用环境变量覆盖
  if (envLabel) return assertValidLaunchAgentLabel(envLabel);
  return assertValidLaunchAgentLabel(  // 否则根据 profile 生成默认标签
    resolveGatewayLaunchAgentLabel(args?.env?.OPENCLAW_PROFILE)
  );
}

分支逻辑 : 环境变量 OPENCLAW_LAUNCHD_LABEL 允许自定义标签(多实例场景),否则用 profile 生成 ai.openclaw.gatewayai.openclaw.gateway.{profile}

3.1.3 路径解析 (L51-80)
typescript 复制代码
function resolveLaunchAgentPlistPathForLabel(env, label): string {
  const home = toPosixPath(resolveHomeDir(env));  // 解析 HOME,转为 POSIX 路径
  return path.posix.join(home, "Library", "LaunchAgents", `${label}.plist`);
  // 结果如: /Users/user/Library/LaunchAgents/ai.openclaw.gateway.plist
}
typescript 复制代码
export function resolveGatewayLogPaths(env) {
  const stateDir = resolveGatewayStateDir(env);  // ~/.openclaw 或自定义
  const logDir = path.join(stateDir, "logs");
  const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";  // 可自定义日志前缀
  return { logDir, stdoutPath: `${logDir}/${prefix}.log`, stderrPath: `${logDir}/${prefix}.err.log` };
}

设计目的: stdout/stderr 分离,stderr 专门用于错误日志诊断。

3.1.4 launchctl 执行封装 (L120-128)
typescript 复制代码
async function execLaunchctl(args: string[]) {
  const isWindows = process.platform === "win32";
  const file = isWindows ? (process.env.ComSpec ?? "cmd.exe") : "launchctl";
  const fileArgs = isWindows ? ["/d", "/s", "/c", "launchctl", ...args] : args;
  return await execFileUtf8(file, fileArgs, isWindows ? { windowsHide: true } : {});
}

关键设计 : 支持 Windows 上通过 ComSpec 调用 launchctl(用于 WSL 场景下开发测试),/d /s /c 禁用命令扩展和延迟扩展以避免参数被 cmd.exe 解析。

3.1.5 端口解析 (L129-170)
typescript 复制代码
function parseGatewayPortFromProgramArguments(programArguments): number | null {
  // 遍历参数数组,支持两种形式:
  //   --port 4152  (两个参数)
  //   --port=4152  (一个参数)
  for (let index = 0; index < programArguments.length; index += 1) {
    const current = programArguments[index]?.trim();
    if (current === "--port") {
      const next = parseStrictPositiveInteger(programArguments[index + 1] ?? "");
      if (next !== undefined) return next;  // --port 后跟的数字
    }
    if (current.startsWith("--port=")) {
      const value = parseStrictPositiveInteger(current.slice("--port=".length));
      if (value !== undefined) return value;  // --port=数字
    }
  }
  return null;
}

用途: 重启时需要知道端口号来清理旧进程。

3.1.6 安全目录创建 (L225-238)
typescript 复制代码
async function ensureSecureDirectory(targetPath: string): Promise<void> {
  await fs.mkdir(targetPath, { recursive: true, mode: LAUNCH_AGENT_DIR_MODE }); // 755
  try {
    const stat = await fs.stat(targetPath);
    const mode = stat.mode & 0o777;  // 取低 9 位权限
    const tightenedMode = mode & ~0o022;  // 清除 group/other write 位
    if (tightenedMode !== mode) {
      await fs.chmod(targetPath, tightenedMode);  // 确保无 group/other 写权限
    }
  } catch { /* Best effort */ }
}

安全设计 : mkdir 的 mode 受 umask 影响,所以创建后再次检查并收紧权限。& ~0o022 确保其他用户无法写入目录。

3.1.7 安装流程 (L600-640)
typescript 复制代码
async function writeLaunchAgentPlist({...args}): Promise<{ plistPath; stdoutPath }> {
  // 1. 解析日志路径 + 创建安全目录
  const { logDir, stdoutPath, stderrPath } = resolveGatewayLogPaths(env);
  await ensureSecureDirectory(logDir);

  // 2. 清理遗留 LaunchAgent
  const domain = resolveGuiDomain();
  for (const legacyLabel of resolveLegacyGatewayLaunchAgentLabels(env.OPENCLAW_PROFILE)) {
    await execLaunchctl(["bootout", domain, legacyPlistPath]);  // 卸载旧服务
    await execLaunchctl(["unload", legacyPlistPath]);
    await fs.unlink(legacyPlistPath).catch(() => {});  // 删除旧 plist
  }

  // 3. 创建目录链: HOME → Library → LaunchAgents
  await ensureSecureDirectory(home);
  await ensureSecureDirectory(libraryDir);
  await ensureSecureDirectory(path.dirname(plistPath));

  // 4. 生成 plist + 写入文件 (644)
  const plist = buildLaunchAgentPlist({ label, programArguments, ... });
  await fs.writeFile(plistPath, plist, { encoding: "utf8", mode: LAUNCH_AGENT_PLIST_MODE });
}
typescript 复制代码
async function activateLaunchAgent({ env, plistPath }) {
  await execLaunchctl(["bootout", domain, plistPath]);  // 先卸载(如果已加载)
  await execLaunchctl(["unload", plistPath]);            // 再 unload(双保险)
  await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, ... });
  // bootstrap 内部: enable(清除 disabled)+ bootstrap gui/{uid} {path}
}
typescript 复制代码
export async function installLaunchAgent(args) {
  const { plistPath, stdoutPath } = await writeLaunchAgentPlist(args);
  await activateLaunchAgent({ env: args.env, plistPath });
  // 注意: 这里不调用 kickstart -k!
  // 原因: 慢速 macOS 虚拟机上,kickstart 会在 setup 健康检查超时前 SIGTERM 新启动的 gateway
}

关键决策 : bootstrap 已足以触发 RunAtLoad,无需额外的 kickstart -k。避免在慢速 VM 上因 SIGTERM 导致安装失败。

3.1.8 停止流程 (L551-590)
typescript 复制代码
export async function stopLaunchAgent({ stdout, env }) {
  // Step 1: disable --- 持久化抑制 KeepAlive/RunAtLoad
  const disable = await execLaunchctl(["disable", serviceTarget]);
  if (disable.code !== 0) {
    // 降级: bootout 会完全卸载服务(比 disable+stop 更激进)
    await bootoutLaunchAgentOrThrow({ serviceTarget, stdout, warning: "..." });
    return;  // bootout 后无需再 stop
  }

  // Step 2: stop --- 通知 launchd 停止当前进程
  const stop = await execLaunchctl(["stop", label]);
  if (stop.code !== 0 && !isLaunchctlNotLoaded(stop)) {
    await bootoutLaunchAgentOrThrow({ ... });  // 降级
    return;
  }

  // Step 3: 等待确认停止
  const stopState = await waitForLaunchAgentStopped(serviceTarget);
  if (stopState.state !== "stopped" && stopState.state !== "not-loaded") {
    await bootoutLaunchAgentOrThrow({ ... });  // 无法确认 → 降级 bootout
    return;
  }
}

双重保证 : disable 防止 launchd 因 KeepAlive 重启;stop 终止当前进程;确认循环保证停止生效。

typescript 复制代码
async function waitForLaunchAgentStopped(serviceTarget): Promise<LaunchAgentProbeResult> {
  let lastUnknown = null;
  for (let attempt = 0; attempt < 10; attempt += 1) {  // 最多 10 次
    const probe = await probeLaunchAgentState(serviceTarget);
    if (probe.state === "stopped" || probe.state === "not-loaded") return probe;
    if (probe.state === "unknown") lastUnknown = probe;
    await new Promise(resolve => setTimeout(resolve, 100));  // 100ms 间隔
  }
  return lastUnknown ?? { state: "running" };  // 超时 → 返回 running(触发 bootout 降级)
}
3.1.9 重启流程 (L660-768)
typescript 复制代码
export async function restartLaunchAgent({ stdout, env }) {
  // 分支 A: 从 launchd 管理的进程内部重启
  if (isCurrentProcessLaunchdServiceLabel(label)) {
    // 启动 detached shell 子进程,等调用者退出后执行 kickstart
    const handoff = scheduleDetachedLaunchdRestartHandoff({
      env, mode: "kickstart", waitForPid: process.pid,
    });
    if (!handoff.ok) throw new Error(`launchd restart handoff failed: ${handoff.detail}`);
    return { outcome: "scheduled" };  // 调用者可以安全退出
  }

  // 分支 B: 外部 CLI 重启
  const cleanupPort = await resolveLaunchAgentGatewayPort(serviceEnv);
  if (cleanupPort !== null) cleanStaleGatewayProcessesSync(cleanupPort);  // 清理旧进程

  await execLaunchctl(["enable", serviceTarget]);  // 清除 disabled 状态
  const start = await execLaunchctl(["kickstart", "-k", serviceTarget]);
  if (start.code === 0) return { outcome: "completed" };

  // kickstart 失败: 如果是 "not loaded" → bootstrap 后重试
  if (!isLaunchctlNotLoaded(start)) {
    await ensureLaunchAgentLoadedAfterFailure({ domain, serviceTarget, plistPath });
    throw new Error(`launchctl kickstart failed: ${start.stderr || start.stdout}`);
  }

  // bootstrap + 重试 kickstart
  await bootstrapLaunchAgentOrThrow({ domain, serviceTarget, plistPath, ... });
  const retry = await execLaunchctl(["kickstart", "-k", serviceTarget]);
  if (retry.code !== 0) throw new Error(`launchctl kickstart failed after bootstrap`);
  return { outcome: "completed" };
}

3.2 Linux systemd 子系统 (systemd.ts --- 776行)

3.2.1 核心安装流程
typescript 复制代码
export async function installSystemdService(args: GatewayServiceInstallArgs) {
  // 1. 检查 systemd 可用性
  assertSystemdAvailable();

  // 2. 写入 unit 文件
  await writeSystemdUnit({ env, programArguments, workingDirectory, environment, ... });

  // 3. 写入环境变量文件 (gateway.systemd.env, chmod 600)
  await writeSystemdGatewayEnvironmentFile({ env, environment });

  // 4. 激活: daemon-reload → enable → restart
  await activateSystemdService({ env, unitName });
}

unit 文件生成 (buildSystemdUnit)的关键字段:

复制代码
[Unit]
After=network-online.target        # 等待网络就绪
Wants=network-online.target         # 请求网络目标
StartLimitBurst=5                   # 60秒内最多重启5次
StartLimitIntervalSec=60

[Service]
ExecStart={programArguments.join(' ')}  # 主命令
Restart=always                          # 总是重启
RestartSec=5                            # 重启间隔5秒
RestartPreventExitStatus=78             # 配置错误(78)不重启
TimeoutStopSec=30                       # 停止超时
TimeoutStartSec=30                      # 启动超时
SuccessExitStatus=0 143                 # 0=正常 143=SIGTERM
KillMode=control-group                  # 终止所有子进程
WorkingDirectory={workingDirectory}
EnvironmentFile=-{envFilePath}          # -前缀:文件不存在不报错
Environment=KEY=VALUE ...               # 内联环境变量(不在env file中的)

[Install]
WantedBy=default.target                # 用户登录时自动启动

RestartPreventExitStatus=78: 退出码 78 是 OpenClaw 的 "配置错误" 信号(源自 EX_CONFIG in sysexits.h),不应无限重试。

3.2.2 sudo 降级机制 (execSystemctlUser)
typescript 复制代码
async function execSystemctlUser(args: string[]) {
  const sudoUser = process.env.SUDO_USER;
  const machineUser = resolveMachineUser();

  // 场景1: sudo 环境 → 使用 machine scope 连接原用户 bus
  if (sudoUser && sudoUser !== "root" && machineUser) {
    return execSystemctl(["--machine", `${machineUser}@`, "--user", ...args]);
  }

  // 场景2: 直接执行 → 尝试 --user scope
  const directResult = await execSystemctl(["--user", ...args]);
  if (directResult.code === 0) return directResult;

  // 场景3: user bus 不可用 → 尝试 machine scope
  if (shouldFallbackToMachineUserScope(detail)) {
    return execSystemctl(["--machine", `${machineUser}@`, "--user", ...args]);
  }

  return directResult;  // 其他错误直接返回
}

shouldFallbackToMachineUserScope 逻辑: 仅在非 "Permission denied" 错误时降级。Permission denied 意味着用户没有访问权限,machine scope 也不会解决。

3.3 Windows Scheduled Task 子系统 (schtasks.ts --- 859行)

3.3.1 任务脚本生成
cmd 复制代码
@echo off
rem OpenClaw Gateway
cd /d "C:\Users\user"
set "HOME=C:\Users\user"
set "OPENCLAW_GATEWAY_PORT=4152"
set "OPENCLAW_SERVICE_MARKER=openclaw"
...
"C:\Program Files\nodejs\node.exe" "C:\...\dist\index.js" gateway --port 4152

设计注意: Windows cmd 脚本中不设置 PATH --- Scheduled Task 继承调用者的 PATH 环境。

3.3.2 停止流程的多重保证
typescript 复制代码
export async function stopScheduledTask({ stdout, env }) {
  // Step 1: schtasks /End 请求终止
  await execSchtasks(["/End", "/TN", taskName]);

  // Step 2: 通过端口检测找到并终止所有关联的 gateway 进程
  // (schtasks /End 不保证终止子进程)
  await terminateScheduledTaskGatewayListeners({ env, stdout });

  // Step 3: 终止启动文件夹方式的运行时进程
  await terminateInstalledStartupRuntime({ env, stdout });

  // Step 4: 等待端口释放 (5s 超时)
  const released = await waitForGatewayPortRelease(port, 5000);
  if (!released) {
    // Step 5: 强制终止 + 再等 2s
    await terminateBusyPortListeners(port);
    await sleep(2000);
  }
}

为什么需要端口检测: Windows schtasks /End 只终止任务进程本身,不会终止 Node.js worker 子进程。子进程继续占用端口会导致重启失败。

3.3.3 启动文件夹降级
typescript 复制代码
// 如果 schtasks /Create 因权限被拒绝:
async function installStartupFolderEntry({ scriptPath, stdout }) {
  const startupDir = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
  const launcherPath = path.join(startupDir, "OpenClaw Gateway.cmd");
  const launcherContent = `start "" /min cmd.exe /d /c "${scriptPath}"`;
  await fs.writeFile(launcherPath, launcherContent);
}

start "" /min cmd.exe /d /c --- 最小化窗口启动,/d 禁用自动运行命令。

3.4 环境构建子系统 (service-env.ts)

3.4.1 最小化 PATH 构建逻辑

核心原则: 守护进程不继承用户 shell 环境,必须构建自包含的最小 PATH。

macOS PATH 优先级(从高到低):

  1. 环境变量覆盖(PNPM_HOME、NPM_CONFIG_PREFIX/bin、BUN_INSTALL/bin、VOLTA_HOME/bin、ASDF_DATA_DIR/shims)
  2. NVM_DIR、FNM_DIR(aliases/default/bin)
  3. 通用用户目录(/.local/bin、/.npm-global/bin、/bin、/.volta/bin、/.asdf/shims、/.bun/bin)
  4. macOS 特定路径(~/Library/Application Support/fnm、~/Library/pnpm)
  5. 系统路径(/opt/homebrew/bin、/usr/local/bin、/usr/bin、/bin)

版本管理器路径处理 : NVM/FNM 的 aliases/default/bin 是符号链接到当前 Node 版本,比直接引用版本目录更稳定。

3.4.2 TLS CA 证书特殊处理
typescript 复制代码
export function resolveNodeStartupTlsEnvironment(env): Record<string, string | undefined> {
  // macOS launchd 服务不继承 shell 环境
  // Node undici/fetch 无法定位系统 CA bundle
  // 默认注入 /etc/ssl/cert.pem
  if (!env.NODE_EXTRA_CA_CERTS && !env.NODE_USE_SYSTEM_CA) {
    const certPath = findSystemCertPath();  // /etc/ssl/cert.pem (macOS) 等
    if (certPath) return { NODE_EXTRA_CA_CERTS: certPath };
  }
  return {};
}

3.5 配置审计子系统 (service-audit.ts)

3.5.1 审计入口
typescript 复制代码
export function auditGatewayServiceConfig(params): GatewayServiceAuditResult {
  const issues: GatewayServiceAuditIssue[] = [];
  const command = params.command;  // 服务命令配置

  // 1. 审计命令
  issues.push(...auditGatewayCommand(params));
  // 2. 审计 Token
  issues.push(...auditGatewayToken(params));
  // 3. 审计 PATH
  issues.push(...auditGatewayServicePath(params));
  // 4. 审计运行时
  issues.push(...auditGatewayRuntime(params));
  // 5. 平台特定审计
  if (params.platform === "darwin") issues.push(...auditLaunchdPlist(params));
  if (params.platform === "linux") issues.push(...auditSystemdUnit(params));

  return { ok: issues.length === 0, issues };
}
3.5.2 Token 漂移检测详解
typescript 复制代码
function checkTokenDrift({ serviceToken, configToken }) {
  if (!serviceToken) return null;  // 无 Token 服务单元是规范的(推荐用 EnvironmentFile)
  if (configToken && serviceToken !== configToken) {
    // 服务 Token 与配置文件 Token 不匹配
    // 服务重启后将使用服务配置中的旧 Token
    // 用户修改了 openclaw.json 中的 token 但服务配置未更新
    return { code: "gateway-token-drift", level: "recommended", message: "..." };
  }
  return null;
}

关键规则: Token 漂移意味着服务重启后使用的是服务配置中的旧 Token,而非配置文件的新 Token。这会导致客户端使用新 Token 认证失败。

3.6 外部服务检测 (inspect.ts)

3.6.1 三平台扫描策略

macOS : 读取 ~/Library/LaunchAgents/*.plist,解析 XML 提取 Label + Program + ProgramArguments + EnvironmentVariables。深度模式还扫描 /Library/LaunchAgents//Library/LaunchDaemons/

Linux : 读取 ~/.config/systemd/user/*.service,解析 ini 提取 ExecStart + Environment + EnvironmentFile。深度模式扫描系统级目录。

Windows : 执行 schtasks /Query /FO LIST /V,解析列表输出。还扫描 %APPDATA%\...\Startup\*.cmd

过滤规则: 排除当前 profile 对应的正式服务单元,只返回"额外"的服务实例(可能是遗留的或误装的)。

3.7 Node Host 服务适配 (node-service.ts --- 装饰器模式)

typescript 复制代码
export function resolveNodeService(env): GatewayService {
  const base = resolveGatewayService(env);  // 复用 Gateway 实现
  return {
    ...base,
    // 仅替换 install/restart 中的环境变量注入
    install: (args) => base.install({ ...args, env: withNodeServiceEnv(args.env) }),
    restart: (args) => base.restart({ ...args, env: withNodeServiceEnv(args.env) }),
    // 其他方法直接委托
  };
}

function withNodeServiceEnv(env) {
  return {
    ...env,
    OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node",
    OPENCLAW_SYSTEMD_UNIT: "openclaw-node",
    OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Node",
    OPENCLAW_TASK_SCRIPT_NAME: "node.cmd",
    OPENCLAW_LOG_PREFIX: "node",
    OPENCLAW_SERVICE_KIND: "node",
  };
}

3.8 程序参数组装 (program-args.ts)

决策树逻辑
复制代码
resolveCliProgramArguments({ runtime, dev })
  ↓
runtime === "node"?
  → node + CLI entrypoint (resolvePreferredNodePath → 系统 Node 或 execPath)
  ↓
runtime === "bun"?
  → dev: bun + src/entry.ts + repoRoot
  → !dev: bun + CLI entrypoint
  ↓
runtime === "auto" + !dev?
  → execPath + CLI entrypoint
  → 如果 !isNodeRuntime(execPath) → execPath + args (非 Node 二进制)
  ↓
runtime === "auto" + dev?
  → isBunRuntime(execPath)? → bun + src/entry.ts
  → 否则 → resolveBunPath() + src/entry.ts

入口点搜索 : resolveCliEntrypointPathForService 搜索 dist/{index.js,mjs,entry.js,mjs},如果 process.argv[1] 的 realpath 与 normalized 不同(符号链接),优先使用 normalized 路径(更稳定)。


四、跨平台设计对比

维度 macOS (launchd) Linux (systemd) Windows (schtasks)
配置文件 plist XML .service ini .cmd batch
自启机制 RunAtLoad + KeepAlive WantedBy=default.target /SC ONLOGON
崩溃重启 KeepAlive=true Restart=always 无原生支持
日志路径 自定义 stdoutPath/stderrPath journalctl --user schtasks /Query
环境变量 EnvironmentVariables dict Environment= + EnvironmentFile= set "KEY=VALUE"
用户级目录 ~/Library/LaunchAgents/ ~/.config/systemd/user/ %APPDATA%\Startup\
降级策略 bootout 降级 machine scope 降级 Startup 文件夹降级
停止保证 disable + stop + 确认循环 systemctl stop /End + 进程树终止 + 端口释放
重启方式 kickstart -k / detached handoff systemctl restart /End + /Run
安全模型 umask 077 chmod 600 env file cmd set 引用
遗留清理 移到 ~/.Trash unlink schtasks /Delete + unlink
sudo 处理 无(需要 GUI 会话) --machine scope

五、关键设计决策

5.1 为什么不直接 kill 进程?

所有停止操作都通过系统服务管理器(launchctl stop / systemctl stop / schtasks /End),而非直接 SIGTERM/SIGKILL。原因:

  1. 服务管理器需要知道进程已停止(否则会因 KeepAlive/Restart=always 自动重启)
  2. 进程树清理由服务管理器负责(systemd KillMode=control-group)
  3. 状态追踪由服务管理器维护

5.2 为什么 macOS 重启用 detached shell?

因为 kickstart -k 会先终止当前进程树。如果从 gateway 内部发起 restart,命令本身会被终止。detached shell 独立于进程树,可以安全地等待调用者退出后执行 kickstart。

5.3 为什么 Windows 需要端口检测?

schtasks /End 只结束任务进程,不保证子进程终止。Gateway 进程可能 fork 了 worker 子进程,这些子进程会继续占用端口。通过端口检测 + 进程树终止确保端口释放。

5.4 为什么服务环境要最小化 PATH?

守护进程在系统级运行,不继承用户 shell 环境。如果 PATH 包含版本管理器路径(如 ~/.nvm/),在 Node 版本升级后路径会断裂。最小化 PATH 只包含系统 Node 和已知的稳定路径。

5.5 为什么 Token 不能内嵌在服务配置中?

旧版 OpenClaw 将 OPENCLAW_GATEWAY_TOKEN 直接写入 plist/Environment。如果用户修改了 openclaw.json 中的 token,服务配置中的旧 token 不会更新,导致认证失败。新版使用 EnvironmentFile(Linux)或不在配置中存储 token(macOS/Windows),避免漂移。

5.6 为什么安装时不调用 kickstart?

在慢速 macOS 虚拟机上,kickstart -k 会 SIGTERM 刚启动的 gateway 进程,导致 setup 的健康检查超时前进程就被杀死。bootstrap 已经足以触发 RunAtLoad。


六、错误处理与降级策略

6.1 降级链

复制代码
首选方案 → 降级方案 1 → 降级方案 2 → 错误

macOS 停止: disable+stop → bootout(服务完全卸载)
macOS 重启: kickstart -k → bootstrap + kickstart
macOS 安装: bootstrap → 抛出 GUI 会话错误
Linux scope: --user → --machine {user}@ --user
Linux 可用性: systemctl --user status → 分类错误 → 用户提示
Windows 安装: schtasks /Create → Startup 文件夹
Windows 停止: schtasks /End → 进程树终止 → 强制终止

6.2 环境检测降级

复制代码
systemd 可用性:
  systemctl --user status
    ├─ 成功 → 可用
    ├─ "not found" → missing_systemctl → "安装 systemd"
    ├─ "failed to connect to bus" → user_bus_unavailable → "enable-linger / XDG_RUNTIME_DIR"
    └─ WSL 场景 → "systemd=true in /etc/wsl.conf"

七、遗留服务迁移

模块内置了从 clawdbotopenclaw 的迁移支持:

平台 旧标签 处理方式
Linux clawdbot-gateway.service LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES → disable --now + unlink
macOS 无遗留标签 LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = []
Windows 无遗留标签 LEGACY_GATEWAY_WINDOWS_TASK_NAMES = []

inspect.tsfindExtraGatewayServices 还能深度扫描发现系统级目录中残留的 openclaw/clawdbot 服务,并通过 cleanExtraGatewayServices 清理。


八、辅助模块解析

8.1 constants.ts --- 常量定义

typescript 复制代码
export const GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway";     // macOS 默认标签
export const GATEWAY_SYSTEMD_SERVICE_NAME = "openclaw-gateway";      // Linux 默认服务名
export const GATEWAY_WINDOWS_TASK_NAME = "OpenClaw Gateway";          // Windows 默认任务名
export const NODE_LAUNCH_AGENT_LABEL = "ai.openclaw.node";            // Node Host 标签
export const NODE_SYSTEMD_SERVICE_NAME = "openclaw-node";
export const NODE_WINDOWS_TASK_NAME = "OpenClaw Node";
export const LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 1;             // 最小重启间隔
export const LAUNCH_AGENT_UMASK_DECIMAL = 0o077;                     // 文件权限掩码
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES = ["clawdbot-gateway"]; // 遗留服务名
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS: string[] = [];       // macOS 无遗留
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];        // Windows 无遗留

Profile 后缀 : resolveGatewayProfileSuffix(profile) 返回 "" (默认) 或 ".{profile}" (非空 profile)。resolveGatewayLaunchAgentLabel(profile) 组合为 ai.openclaw.gateway[.profile]

8.2 arg-split.ts --- 参数分割

typescript 复制代码
export function splitArgsPreservingQuotes(input: string, mode?: "posix" | "cmd" | "none") {
  // posix: 反斜杠转义 + 单引号 + 双引号
  // cmd: ^转义 + 双引号
  // none: 仅按空白分割
}

8.3 cmd-set.ts --- Windows set 命令转义

typescript 复制代码
export function renderCmdSetAssignment(key: string, value: string): string {
  // 处理 Windows cmd.exe 中的特殊字符:
  // % → %% (延迟扩展展开)
  // ! → ^! (延迟扩展)
  // " → 保持配对
  // 结果: set "KEY=VALUE"
}

8.4 diagnostics.ts --- 错误日志诊断

typescript 复制代码
export async function readLastGatewayErrorLine(logPaths) {
  // 读取 stderr + stdout 日志文件
  // 从末尾向前搜索匹配以下模式的行:
  //   "refusing to bind gateway"
  //   "gateway auth mode"
  //   "gateway start blocked"
  //   "failed to bind gateway socket"
  //   "tailscale .* requires"
  // 如果无匹配 → 返回最后一条日志行
}
相关推荐
PD我是你的真爱粉2 小时前
Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解
人工智能·python·架构
10000guo2 小时前
kreuzberg MCP搭建以及配合claude使用
ai·word·ai编程
ZC跨境爬虫2 小时前
3D 地球卫星轨道可视化平台开发 Day5(简介接口对接+规划AI自动化卫星数据生成工作流)
前端·人工智能·3d·ai·自动化
毛骗导演2 小时前
Claude Code Agent 实现原理深度剖析
前端·架构
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 日榜(2026-04-20)
ai·大模型·llm·github·ai教程
不瘦80斤不改名3 小时前
深入理解 FastAPI 核心架构:依赖注入、分页机制与数据流转的底层逻辑
python·架构·fastapi
Jutick3 小时前
Spring Boot WebSocket 实时行情推送实战:从断线重连到并发优化
后端·架构
xixixi777773 小时前
Gartner 2026核心趋势:前置式主动安全(PCS)成为安全战略新范式,量子安全+国密算法构筑政企纵深防御底座
网络·人工智能·安全·web安全·ai·量子计算
浮芷.3 小时前
生命科学数据视界防御:基于鸿蒙Flutter陀螺仪云台与三维体积光栅的视轴锁定架构
flutter·华为·架构·开源·harmonyos·鸿蒙