02-启动流程

第二章 · 启动流程

OpenClaw 的启动流程分为三个关键阶段 :Node.js 启动器 (openclaw.mjs) → TypeScript 入口 (src/entry.ts) → CLI 运行时 (src/cli/run-main.ts)。

2.1 整体流程

复制代码
用户敲下命令
      │
      ▼
┌─────────────────────────────────┐
│  openclaw.mjs (Node.js 启动器)   │
│  ① 检查 Node 版本 ≥ 22.14       │
│  ② 智能编译缓存处理               │
│  ③ 加载 dist/entry.js           │
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  src/entry.ts (TypeScript 入口) │
│  ① 主模块守卫                    │
│  ② CLI 重生 (respawn) 检查       │
│  ③ 环境初始化                    │
│  ④ 命令行参数解析                 │
│  ⑤ Profile/Container 处理        │
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  run-main.ts (CLI 运行时)       │
│  ① Commander 命令注册            │
│  ② 子命令分发                    │
│  ③ Gateway / Agent 启动          │
└─────────────────────────────────┘

2.2 第一阶段:openclaw.mjs 启动器

openclaw.mjs 是 npm 包中定义的 bin 入口,它是用户敲下 openclaw 命令后第一个执行的脚本。

2.2.1 Node 版本检查

javascript 复制代码
// openclaw.mjs:11-42
const MIN_NODE_MAJOR = 22;
const MIN_NODE_MINOR = 12;

const parseNodeVersion = (rawVersion) => {
  const [majorRaw = "0", minorRaw = "0"] = rawVersion.split(".");
  return { major: Number(majorRaw), minor: Number(minorRaw) };
};

const isSupportedNodeVersion = (version) =>
  version.major > MIN_NODE_MAJOR ||
  (version.major === MIN_NODE_MAJOR && version.minor >= MIN_NODE_MINOR);

如果 Node 版本过低,会输出友好的错误信息并提示使用 nvm 升级。

2.2.2 智能编译缓存策略

这是 OpenClaw 启动器最精妙的设计之一:两级编译缓存重生策略

javascript 复制代码
// openclaw.mjs:87-153

// 阶段一:源码检出时禁用缓存
const respawnWithoutCompileCacheIfNeeded = () => {
  if (!isSourceCheckoutLauncher()) return false;    // 非源码环境不需要
  if (OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED) return false; // 已重生过
  // ... 设置 NODE_DISABLE_COMPILE_CACHE=1,重新 spawn 进程
};

// 阶段二:打包安装时使用版本化缓存
const respawnWithPackagedCompileCacheIfNeeded = () => {
  if (isSourceCheckoutLauncher()) return false;     // 源码环境跳过
  if (OPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED) return false;
  // ... 计算缓存目录路径,设置 NODE_COMPILE_CACHE,重新 spawn
};

缓存目录路径计算逻辑:

复制代码
缓存目录 = {tmpdir}/node-compile-cache/openclaw/{version}/{installMarker}

其中 installMarker = {package.json 修改时间}-{package.json 大小}

这样设计的优势:

  • 源码开发环境:禁用缓存,确保每次运行最新代码
  • 生产安装环境:启用版本化缓存,大幅提升启动速度(避免重复编译 TypeScript)
  • 升级安全:版本号和安装标记共同构成缓存键,升级后自动使用新缓存

2.2.3 编译缓存启用时机

javascript 复制代码
// openclaw.mjs:155-162
if (module.enableCompileCache &&
    !isNodeCompileCacheDisabled() &&
    !isSourceCheckoutLauncher()) {
  try {
    module.enableCompileCache(resolvePackagedCompileCacheDirectory());
  } catch {
    // Ignore errors
  }
}

注意,这里的 module.enableCompileCache 是 Node.js 22+ 的实验性 API:

  • 在两次重生尝试之后才启用缓存,避免无效缓存污染
  • 仅在打包安装环境启用(非源码检出)

2.2.4 快速帮助路径

启动器为 --help-h 提供了快速路径,无需加载完整运行时:

javascript 复制代码
// openclaw.mjs:242-313
const isBareRootHelpInvocation = (argv) =>
  argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");

// 优先从预计算的 JSON 文件读取帮助文本
const precomputed = loadPrecomputedHelpText("rootHelpText");
if (precomputed) {
  process.stdout.write(precomputed);
  return true;
}

// 回退到加载专用模块
const mod = await import("./dist/cli/program/root-help.js");

2.2.5 入口文件加载

如果快速路径不匹配,启动器尝试加载运行时代码:

javascript 复制代码
// openclaw.mjs:304-313
if (await tryImport("./dist/entry.js")) {
  // OK - 标准构建产物
} else if (await tryImport("./dist/entry.mjs")) {
  // OK - 备用构建产物
} else {
  throw new Error(await buildMissingEntryErrorMessage());
  // 提示用户需要先运行 pnpm install && pnpm build
}

2.3 第二阶段:src/entry.ts TypeScript 入口

entry.ts 是 TypeScript 侧的真正入口,负责运行时初始化和 CLI 分发。

2.3.1 主模块守卫

typescript 复制代码
// src/entry.ts:77-84
if (!isMainModule({
  currentFile: fileURLToPath(import.meta.url),
  wrapperEntryPairs: [
    { wrapperBasename: "openclaw.mjs", entryBasename: "entry.js" },
    { wrapperBasename: "openclaw.js", entryBasename: "entry.js" },
  ],
})) {
  // 作为依赖被导入 ------ 跳过入口副作用
} else {
  // 作为主模块 ------ 执行启动逻辑
}

这个守卫设计是因为:

  • dist/index.js 是 npm 包的主导出,也会 import entry.js
  • 没有这个守卫,entry.ts 的顶层代码会被执行两次,导致端口冲突和锁文件竞争

2.3.2 环境初始化序列

typescript 复制代码
// src/entry.ts:85-98
process.title = "openclaw";                           // 设置进程名
ensureOpenClawExecMarkerOnProcess();                   // 标记当前为 OpenClaw 进程
installProcessWarningFilter();                         // 安装警告过滤器
normalizeEnv();                                        // 标准化环境变量
enableOpenClawCompileCache({ installRoot });           // 启用编译缓存
gatewayEntryStartupTrace.mark("bootstrap");            // 启动追踪标记

2.3.3 启动追踪(Gateway Startup Trace)

entry.ts 内置了一个可选的 Gateway 启动性能追踪器:

typescript 复制代码
// src/entry.ts:37-68
function createGatewayEntryStartupTrace(argv: string[]) {
  const enabled = isTruthyEnvValue(OPENCLAW_GATEWAY_STARTUP_TRACE)
                  && argv.slice(2).includes("gateway");
  // ...
  return {
    mark(name: string) { /* 记录时间戳 */ },
    async measure<T>(name: string, run: () => Promise<T>): Promise<T> { /* 测量异步操作 */ }
  };
}

设置 OPENCLAW_GATEWAY_STARTUP_TRACE=1 后,启动时的每个关键阶段都会被标记和输出。

2.3.4 CLI 重生逻辑

typescript 复制代码
// src/entry.ts:109-140
function ensureCliRespawnReady(): boolean {
  const plan = buildCliRespawnPlan();  // 检查是否需要重生
  if (!plan) return false;
  // 用新的 Node 进程 spawn 自身
  const child = spawn(plan.command, plan.argv, {
    stdio: "inherit",
    env: plan.env,
  });
  // 父进程等待子进程结束
  child.once("exit", (code, signal) => process.exit(code ?? 1));
  return true;
}

重生逻辑主要处理跨平台兼容性问题,比如 Windows 上的 node vs node.exe 差异。

2.3.5 Profile 和 Container 参数解析

typescript 复制代码
// src/entry.ts:145-169
const parsedContainer = parseCliContainerArgs(process.argv);   // --container <name>
const parsed = parseCliProfileArgs(parsedContainer.argv);       // --profile <name> / --dev

// 冲突检测:--container 和 --profile 不能同时使用
if (containerTargetName && parsed.profile) {
  console.error("[openclaw] --container cannot be combined with --profile/--dev");
  process.exit(2);
}

if (parsed.profile) {
  applyCliProfileEnv({ profile: parsed.profile });  // 应用 Profile 环境变量
  process.argv = parsed.argv;
}

2.3.6 最终分发

typescript 复制代码
// src/entry.ts:220-237
async function runMainOrRootHelp(argv: string[]): Promise<void> {
  if (await tryHandleRootHelpFastPath(argv)) return;  // --help 快速路径
  const { runCli } = await import("./cli/run-main.js");  // 动态导入 CLI 运行时
  await runCli(argv);  // 委托给 Commander
}

2.4 总结:启动路径决策树

复制代码
openclaw <args>
    │
    ├── Node < 22.14? → 报错退出
    │
    ├── 源码环境? → 禁用缓存重生 → 重新 spawn
    │
    ├── 打包环境? → 版本化缓存重生 → 重新 spawn
    │
    ├── --help / -h? → 快速帮助路径 → 直接输出
    │
    ├── 加载 dist/entry.js 失败? → 提示 build
    │
    └── 正常加载 →
        ├── 主模块检查
        ├── 环境初始化
        ├── CLI 重生检查
        ├── Profile/Container 解析
        └── runCli() → Commander 命令分发

关键设计亮点

  1. 两级编译缓存:源码头和打包头使用不同策略,兼顾开发体验和生产性能
  2. 快速帮助路径--help 不需要完整启动,毫秒级响应
  3. 主模块守卫:防止作为依赖导入时副作用重复执行
  4. 懒加载src/cli/run-main.ts 通过动态 import() 加载,减少启动时的初始化开销

下章预告:命令行系统 ------ Commander.js 框架、子命令注册、Profile 机制。

相关推荐
kcuwu.2 小时前
机器学习入门:线性回归完全指南(含波士顿房价预测案例)
人工智能·机器学习·线性回归
幸运的大号暖贴2 小时前
解决Vibe Coding时Idea经常不自动git add问题
java·人工智能·git·intellij-idea·claudecode·opencode
MonkeyKing_sunyuhua2 小时前
什么是服务端 VAD 端点检测
人工智能·语音识别
ascarl20102 小时前
Linux.do 帖子整理:AI 调用 Chrome DevTools 调试前端页面
linux·前端·人工智能
qxq_sunshine2 小时前
从 CNN 到 Agent:给 DL 工程师的“智能体”入门黑话指南(概念篇)
人工智能·神经网络·cnn
郝学胜-神的一滴2 小时前
反向传播:神经网络的「灵魂」修炼法则
人工智能·pytorch·深度学习·神经网络·机器学习·数据挖掘
Tutankaaa2 小时前
知识竞赛软件SaaS版 vs 本地部署
人工智能·经验分享·笔记·学习
DanCheOo2 小时前
开源 | 我是怎么用 ai-memory 让 Cursor 每次开新对话都自动知道项目背景的
前端·人工智能·ai·ai编程
丝雨_xrc2 小时前
AIGC 时代,面向开发者的内容营销正在被重新定义
人工智能