Skip to content

Einstieg und Start

源码版本v2026.6.11

Verantwortung

Nachdem man den openclaw-Befehl abgeschickt hat, gehört alles bis zum tatsächlichen Beginn des ersten Geschäftskommandos zum „Einstieg (entry)": Node-Versionsprüfung, Compile-Cache-Pfadauswahl, Help/Version-Schnellpfade, Signalweiterleitung und schließlich die Übergabe an dist/entry.js. Nachdem entry.js übernimmt, erfolgt eine erneute Environment-Normalisierung, Auflösung von Profile/Container-Optionen, Respawn-Entscheidung und dann der Aufruf von runMainOrRootHelp, der argv an die CLI-Verteilung übergibt.

Dass der Einstieg in zwei Schichten geteilt ist, hat einen Grund: openclaw.mjs ist reines Node-ESM, ohne TypeScript-Abhängigkeiten, eigenständig ausführbar; es löst die Dinge, „die entschieden werden müssen, bevor das TS-Kompilat geladen ist". src/entry.ts ist der Einstieg des TS-Kompilats und geht davon aus, dass dist/ bereits existiert; hier sind aufwendigere dynamische Imports und Traces möglich.

Designmotivation

Warum den Einstieg in zwei Schichten aufteilen und so viele Fast-Paths in die erste Schicht legen? Die Wurzel ist die Kaltstartlatenz (cold-start latency). OpenClaw ist ein CLI; die Latenztoleranz für openclaw --version liegt im Bereich hunderter Millisekunden, das vollständige Laden von Commander + Plugins + Konfiguration dauert aber mehrere hundert Millisekunden bis über eine Sekunde. Würde jeder --version-Aufruf die volle Startkette durchlaufen, wäre die Erfahrung miserabel.

Die Strategie der Einstiegsschicht: Was nicht geladen werden muss, wird nicht geladen. Die Versionsnummer wird direkt aus package.json gelesen, der Help-Text aus einem vorkompilierten cli-startup-metadata.json, erst wenn wirklich ein Kommando ausgeführt werden soll, erfolgt ein dynamischer Import von dist/entry.js. So kann openclaw --version in wenigen Dutzend Millisekunden antworten.

Die zweite Schicht entry.ts hat ebenfalls eigene Fast-Paths: Sie versucht zunächst tryHandleRootHelpFastPath und tryHandlePrecomputedCommandHelpFastPath; erst wenn beide nicht zuschlagen, wird ./cli/run-main.js dynamisch importiert. Jeder Schritt dieser Importkette ist awaited, sodass bei Fast-Path-Treffer der folgende Code gar nicht geladen wird.

Ein weiteres Motiv ist die Isolierung des Compile-Caches (compile cache isolation). module.enableCompileCache() in Node 22+ beschleunigt den ESM-Start erheblich, aber Source-Checkout und frühe Node-24-Versionen auf Windows haben einen Deadlock-Bug. Der Einstieg enthält deshalb eine Respawn-Logik: Zuerst wird geprüft, ob der aktuelle Prozess ein Source-Checkout (isSourceCheckoutLauncher) oder eine betroffene Node-24-Version ist; wenn ja, wird ein Kindprozess ohne Compile-Cache gestartet und NODE_DISABLE_COMPILE_CACHE=1 in die env gesetzt. Diese Entscheidung muss vor dem Laden des TS-Einstiegs getroffen werden, sonst ist der Cache bereits kontaminiert.

Schlüsseldateien

Datenfluss

Der Versionswächter ganz oben in openclaw.mjs (openclaw.mjs:L38):

typescript
const ensureSupportedNodeVersion = () => {
  if (isSupportedNodeVersion(parseNodeVersion(process.versions.node))) {
    return;
  }

  process.stderr.write(
    `openclaw: Node.js v${MIN_NODE_VERSION}+ is required (current: v${process.versions.node}).\n` +
      "If you use nvm, run:\n" +
      `  nvm install ${MIN_NODE_MAJOR}\n` +
      `  nvm use ${MIN_NODE_MAJOR}\n` +
      `  nvm alias default ${MIN_NODE_MAJOR}\n`,
  );
  process.exit(1);
};

MIN_NODE_MAJOR=22, MIN_NODE_MINOR=19 sind harte Schwellen; bei Unterschreitung sofort Exit. Nach dem Versionswächter folgt die Compile-Cache-Respawn-Entscheidung (openclaw.mjs:L207):

typescript
const respawnWithoutCompileCacheIfNeeded = () => {
  const needsDisabledCompileCacheRespawn =
    isSourceCheckoutLauncher() || shouldSkipCompileCacheForWindowsNode24();
  if (!needsDisabledCompileCacheRespawn) {
    return false;
  }
  if (process.env[COMPILE_CACHE_DISABLED_RESPAWNED_ENV] === "1") {
    return false;
  }
  // ...
  return runRespawnedChild(
    process.execPath,
    [...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
    env,
  );
};

Beachten Sie die Selbstschutz-Variable COMPILE_CACHE_DISABLED_RESPAWNED_ENV: Der respawnete Kindprozess durchläuft diese Logik erneut, aber die env-Variable ist bereits "1", also erfolgt maximal ein Respawn — keine endlose Rekursion.

Erste Schicht fällt auf dist/entry.js zurück (openclaw.mjs:L765): Erst wenn alle Fast-Paths verfehlt haben, erfolgt tryImport("./dist/entry.js"); bei Misserfolg wird .mjs versucht, und erst wenn beides scheitert, liefert buildMissingEntryErrorMessage den Hinweis „Dies ist ein ungebauter Source-Baum, führe pnpm install && pnpm build aus". tryImport schluckt nur „direkte module-not-found"-Fehler, andere Fehler werden weitergeworfen — so wird ein Fehler innerhalb von dist/entry.js nicht als „missing dist" verschleiert.

Kern der Einstiegsverteilung der zweiten Schicht entry.ts (entry.ts:L130):

typescript
if (!tryHandleRootVersionFastPath(process.argv)) {
  await runMainOrRootHelp(process.argv);
}

runMainOrRootHelp selbst probiert zuerst einen Help-Fast-Path (entry.ts:L271):

typescript
async function runMainOrRootHelp(argv: string[]): Promise<void> {
  if (await tryHandleRootHelpFastPath(argv)) {
    return;
  }
  if (await tryHandlePrecomputedCommandHelpFastPath(argv)) {
    return;
  }
  try {
    const { runCli } = await gatewayEntryStartupTrace.measure(
      "run-main-import",
      () => import("./cli/run-main.js"),
    );
    await runCli(argv);
  } catch (error) {
    const { formatCliFailureLines } = await import("./cli/failure-output.js");
    for (const line of formatCliFailureLines({
      title: "Could not start the CLI.",
      error,
      argv,
    })) {
      console.error(line);
    }
    process.exit(1);
  }
}

Beachten Sie gatewayEntryStartupTrace.measure("run-main-import", ...) — dieser eine dynamische Import von run-main.js wird explizit gemessen, da er der schwerste Schritt im Kaltstart ist (mehrere hundert Millisekunden). Wenn runCli wirft, wird nicht einfach console.error(error.stack) verwendet, sondern einheitlich über formatCliFailureLines ein benutzerfreundlicher Titel + Trace ausgegeben, dann process.exit(1).

Oben in entry.ts gibt es zudem einen zentralen isMainModule-Wächter (entry.ts:L56):

typescript
if (
  !isMainModule({
    currentFile: fileURLToPath(import.meta.url),
    wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
  })
) {
  // Imported as a dependency — skip all entry-point side effects.
} else {
  // ... vollständige Startlogik
}

Der Kommentar nennt den Grund: Ein Bundler könnte entry.js als shared Dependency von dist/index.js importieren; ohne diesen Wächter würde das Gateway zweimal starten, der zweite Instanz würde den Port streitig machen und abstürzen.

Grenzen und Fehler

  • Respawn-Signalweiterleitung: runRespawnedChild (openclaw.mjs:L102) registriert am Kindprozess SIGTERM/SIGINT/SIGHUP/SIGQUIT-Listener, leitet empfangene Signale an den Kindprozess weiter und hat nach 1 Sekunde einen Fallback process.exit. So wird verhindert, dass der Launcher hängen bleibt, wenn der Kindprozess Signale ignoriert. Windows hat eine andere Signalmenge (nur SIGTERM/SIGINT/SIGBREAK).
  • Compile-Cache-Pfadkontamination: Wenn die Umgebungsvariable NODE_COMPILE_CACHE auf ein nicht beschreibbares Verzeichnis zeigt, schlägt module.enableCompileCache stillschweigend fehl (openclaw.mjs:L267); try/catch schluckt den Fehler — bewusst, ein Cache-Fehler darf den Start nicht blockieren.
  • Source-Checkout-Erkennung: isSourceCheckoutLauncher() prüft das Vorhandensein von .git oder src/entry.ts; ist es ein Source-Baum, wird der Compile-Cache zwangsweise deaktiviert (Source kann sich ändern, gecachte Kompilate wären veraltet).
  • Missing-dist-Fehler: buildMissingEntryErrorMessage (openclaw.mjs:L354) prüft, ob src/entry.ts existiert; wenn ja, folgt der Hinweis „Dies ist ein ungebauter Source-Baum, führe pnpm install && pnpm build aus" statt eines schlichten „file not found" — diese Unterscheidung spart dem Nutzer Minuten der Fehlersuche.
  • isMainModule-Doppelstart: Der Kommentar des Wächters oben in entry.ts (entry.ts:L56) erklärt, dass ein Bundler ohne ihn entry.js als shared Dependency nutzen würde, sodass runCli zweimal aufgerufen würde und die zweite Instanz an der Gateway-Sperre/dem Port scheitert.
  • Respawn-Schleifenschutz: Die beiden Umgebungsvariablen COMPILE_CACHE_DISABLED_RESPAWNED_ENV und OPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED bewachen jeweils zwei Respawn-Arten und stellen sicher, dass maximal einmal respawned wird.

Zusammenfassung

Die Einstiegsschicht erledigt, „was vor dem Laden von TS gelöst werden kann": Versionswächter, Compile-Cache-Entscheidung, Help/Version-Fast-Path, Signalweiterleitung. openclaw.mjs ist ein reines Node-Skript ohne Abhängigkeit von Kompilaten; entry.ts ist der Einstieg des TS-Kompilats und ergänzt Respawn-Plan, Profile-Auflösung, Help-Fast-Path-Fallback und reicht argv schließlich an runCli für die eigentliche CLI-Befehlsverteilung. Wie die Verteilungskette argv an SubCLIs weiterreicht, den Gateway-Hot-Path nutzt und Befehle lazy registriert, siehe nächste Seite; die Startsequenz des Gateways selbst siehe Gateway-Kern.

Vergleich mit offiziellen Ressourcen: OpenClaw-Doku · README.