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.