Skip to content

CLI-Befehlsverteilung ​

源码版本v2026.6.11

Verantwortung ​

runCli(argv) ist die Stelle, an der OpenClaw-Befehle tatsächlich verteilt werden. Von Einstieg übernimmt es argv und erledigt: Profile/Container-Optionen parsen, .env laden, Proxy-Start entscheiden, Help-Fast-Path-Fallback, dann Route-First-Fast-Path probieren, bevor auf das volle Commander-Programm zurückgegriffen wird — primary commands dynamisch registrieren, Plugin-Befehle registrieren, program.parseAsync(argv) Commander verteilen lassen.

Verteilung (dispatch) ist in OpenClaw keine einzelne Aktion, sondern ein dreistufiger Trichter:

  1. Fast-Path: --help/--version/browser --help/secrets --help/nodes --help und reine Help-Anfragen lesen vorkompilierten Text und kehren zurück, ohne Commander zu laden.
  2. Route-First: tryRouteCli leitet argv über eine vorgeparste routedCommands-Tabelle direkt an die Befehlsimplementierung weiter und umgeht die vollständige Commander-Registrierung.
  3. Voll-Commander: buildProgram() baut das Commander-Wurzelprogramm auf, registriert primary commands und Plugin-Befehle lazy und ruft zuletzt program.parseAsync auf.

Jede Stufe return bei Treffer, sonst tiefere Stufe. So kann openclaw gateway run als heißer Pfad (tryRunGatewayRunFastPath:L143) den Großteil der Registrierungskosten überspringen, während openclaw plugins install foo als kalter Pfad den vollständigen Commander durchläuft.

Designmotivation ​

Warum ein dreistufiger Trichter statt einfach program.parseAsync(argv)? Weil vor parseAsync zunächst buildProgram aufgerufen werden muss und buildProgram über registerProgramCommands alle Kernbefehle und SubCLIs registriert. OpenClaw hat 40+ SubCLIs (gateway/agent/cron/models/devices/plugins/channels/security/secrets/skills/...), jeder SubCLI hat wieder einen eigenen Unterbefehlsbaum; eine vollständige eager-Registrierung würde den Kaltstart auf mehrere hundert Millisekunden treiben.

Mehrere Schlüsselentscheidungen:

  • Primary-Only-Registrierung: Das erste Nicht-Option-Token im argv ist der Primary-Befehl (in openclaw gateway run ist gateway der Primary). Ist shouldRegisterPrimaryCommandOnly(argv) wahr, wird nur dieser eine registriert, alle anderen SubCLIs übersprungen.
  • Lazy-Import: Jeder SubCLI-Registrar ist () => import("../xxx-cli.js") dynamisch; erst bei tatsächlichem Treffer wird er ausgeführt.
  • Gateway-Hot-Path: openclaw gateway run hat die höchste Nutzungsfrequenz und einen eigenen tryRunGatewayRunFastPath, der buildProgram umgeht und ein minimales Commander-Programm konstruiert.
  • Route-First: Manche Befehle brauchen nicht einmal Commanders Parsing, sondern werden über eine vorgeparste argv-Tabelle direkt verteilt.

Der Preis: Mehr Codepfade, höhere Verstehenskosten. Der Gewinn: Der Kaltstart von openclaw gateway run sinkt von Sekunden auf wenige hundert Millisekunden.

Schlüsseldateien ​

Datenfluss ​

runCli führt oben argv-Normalisierung und Environment-Laden durch (run-main.ts:L610):

typescript
export async function runCli(argv: string[] = process.argv) {
  const originalArgv = normalizeWindowsArgv(argv);
  const startupTrace = createGatewayStartupTrace(originalArgv, "cli.main");
  const parsedContainer = parseCliContainerArgs(originalArgv);
  if (!parsedContainer.ok) {
    throw new Error(parsedContainer.error);
  }
  // ...
  const containerTarget = maybeRunCliInContainer(originalArgv);
  if (containerTarget.handled) {
    if (containerTarget.exitCode !== 0) {
      process.exitCode = containerTarget.exitCode;
    }
    return;
  }

Beachten Sie maybeRunCliInContainer — wenn argv --container <name> angibt, wird dieser Prozess durch eine In-Container-Ausführung ersetzt und kehrt zurück; die eigentliche Verteilungslogik läuft nicht weiter.

Der Gateway-Hot-Path ist der zentrale Knoten des Verteilungstrichters (run-main.ts:L905):

typescript
if (!bootstrapProxyBeforeFastPath &&
    (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))) {
  return;
}
// Nach dem Proxy-Bootstrap erneut den Hot-Path probieren
if (bootstrapProxyBeforeFastPath &&
    (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))) {
  return;
}
const { tryRouteCli } = await startupTrace.measure("route-import", () => import("./route.js"));
if (await startupTrace.measure("route", () => tryRouteCli(normalizedArgv))) {
  return;
}

tryRunGatewayRunFastPath wird vor dem Proxy-Start einmal ausgeführt, weil gateway run den Proxy selbst verwaltet (über installGatewayRunRuntimeHooks). Trifft der Hot-Path nicht und wird ein Proxy benötigt, wird zuerst der Proxy gebootstrappt und dann erneut der Hot-Path probiert. Erst dann folgt tryRouteCli. Innerhalb von tryRunGatewayRunFastPath werden mit Promise.all parallel 8 Module dynamisch importiert (run-main.ts:L159), dann ein minimales Commander-Programm konstruiert, das nur den gateway-run-Befehl enthält. Die vollständige Startsequenz von gateway run siehe Gateway-Kern.

Route-First-Fast-Path (route.ts:L82) verwendet, wenn OPENCLAW_DISABLE_ROUTE_FIRST nicht gesetzt ist und argv ohne Help/Version ist, resolveCliArgvInvocation, um den commandPath aufzulösen, ruft findRoutedCommand (routes.ts:L8) für eine lineare Suche der routedCommands-Tabelle nach dem ersten matches(path) und canRun(argv) Treffer, führt prepareRoutedCommand aus (Plugins werden nach route.loadPlugins-Strategie geladen — text-only bedeutet, dass nur Nicht---json-Aufrufe vorab laden) und dann route.run(argv), ohne den vollständigen Commander zu bemühen.

Der vollständige Commander-Pfad führt eine Primary-only-Registrierung durch (run-main.ts:L1009):

typescript
const { primary } = invocation;
if (primary && shouldRegisterPrimaryCommandOnly(parseArgv)) {
  await startupTrace.measure("register-primary", async () => {
    const ctx = getProgramContext(program);
    if (ctx) {
      const { registerCoreCliByName } = await import("./program/command-registry.js");
      await registerCoreCliByName(program, ctx, primary, parseArgv);
    }
    const { registerSubCliByName } = await import("./program/register.subclis.js");
    await registerSubCliByName(program, primary, parseArgv);
  });
}

Die SubCLI-Registry ist deklarativ (register.subclis-core.ts:L92): defineImportedProgramCommandGroupSpecs behandelt einfache Fälle „Modul exportiert feste Funktionsnamen" (rein deklarativ — commandNames + loadModule: () => import(...) + exportName), Fälle mit zusätzlichen Parametern (z. B. plugins muss vor/nach Plugin-Befehle anhängen) verwenden Inline-Objekte mit registerSubCliWithPluginCommands und steuern über pluginCliPosition: "before" | "after", ob Plugin-Befehle vor oder nach dem SubCLI eingehängt werden.

registerSubCliByName hat einen Gateway-Sonderfall (register.subclis-core.ts:L45) — selbst beim Fallback auf den vollständigen Commander-Pfad durchläuft openclaw gateway run immer noch eine schlanke „nur run-Unterbefehl"-Registrierung; removeCommandByName entfernt einen eventuell bereits eingehängten gateway-Befehl und hängt ihn neu ein. Das ist die dritte Hot-Path-Fallback-Stufe.

Grenzen und Fehler ​

  • unowned primary-Abfang: openclaw <tippfehler> wird nach dem Help-Fast-Path, aber vor der Route, durch eine unowned-primary-Prüfung (run-main.ts:L826) abgefangen und wirft einen Unknown command-Fehler statt stillschweigend den Top-Level-Help anzuzeigen. Das behebt #81077 — sonst würde openclaw gatway --help still erfolgreich sein.
  • Plugin-Befehl fehlt-Diagnose: Nach der Primary-only-Registrierung ist der Primary weder Kernbefehl noch in der SubCLI-Tabelle (run-main.ts:L1043), dann ruft resolveMissingPluginCommandMessageFromPolicy den Hinweis „Ist vielleicht Plugin X zu installieren?" statt eines leeren Help.
  • Proxy-Start-Reihenfolge: tryRunGatewayRunFastPath wird vor dem Proxy-Start einmal ausgeführt (run-main.ts:L905), weil gateway run den Proxy selbst verwaltet. Andere Befehle bootstrapen zuerst den Proxy und versuchen es dann — falsche Reihenfolge führt dazu, dass der Gateway-Prozess von einem externen Proxy übernommen wird.
  • Commander-Parsing-Exit: Wenn program.parseAsync einen commander.*-Fehler wirft (run-main.ts:L324), erkennt isCommanderParseExit das und setzt nur process.exitCode ohne Re-Throw — Commander exitOverride behandelt Help/Version als Exit.
  • Ressourcenbereinigung: Der finally-Block von runCli (run-main.ts:L1089) ruft closeCliMemoryManagers, disposeCliAgentHarnesses, pauseNonTtyStdinForCliExit auf — ein kurzlebiges CLI muss vor dem Exit Speicher-Runtimes und Agent-Harnesses aufräumen, sonst lecken Kindprozesse.
  • gateway run dreistufiger Fallback: shouldRegisterGatewayRunOnly (register.subclis-core.ts:L45) ist die dritte Hot-Path-Stufe — selbst wenn die vorherigen Fast-Paths verfehlt wurden und der vollständige Commander durchlaufen wird, registriert gateway run weiterhin nur den run-Unterbefehl und lädt nicht den vollständigen Gateway-Befehlsbaum.

Zusammenfassung ​

CLI-Verteilung ist ein dreistufiger Trichter plus eine Fallback-Stufe: Help-Fast-Path liest vorkompilierten Text, Route-First nutzt eine vorgeparste Routentabelle, der vollständige Commander macht eine Primary-only-Registrierung. gateway run ist ein heißer Pfad und hat in allen drei Stufen Spezialoptimierungen (tryRunGatewayRunFastPath, shouldRegisterGatewayRunOnly, registerGatewayRunOnly). Wie die Agent-Hauptschleife nach dem tatsächlichen Beginn der Ausführung läuft, siehe Agent-Hauptschleife; die Gateway-Startsequenz siehe Gateway-Kern; wie der Einstieg argv an runCli übergibt, siehe Einstieg und Start.

Vergleich mit offiziellen Ressourcen: OpenClaw-Doku · README.