























1-import { spawnSync } from "node:child_process";
1+import { spawn, spawnSync } from "node:child_process";
22import { createHash } from "node:crypto";
33import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
44import path from "node:path";
@@ -28,6 +28,7 @@ const extensionsDir = path.join(rootDir, "extensions");
2828const ROOT_HELP_RENDER_TIMEOUT_MS = 120_000;
2929const BROWSER_HELP_RENDER_TIMEOUT_MS = 120_000;
3030const COMMAND_HELP_RENDER_TIMEOUT_MS = 120_000;
31+const COMMAND_HELP_RENDER_CONCURRENCY = 2;
3132const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = ["doctor", "gateway", "models", "plugins"] as const;
3233const CORE_CHANNEL_ORDER = [
3334"telegram",
@@ -55,6 +56,9 @@ type BundledChannelCatalog = {
5556type PrecomputedSubcommandHelpCommand = (typeof PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS)[number];
5657type PrecomputedSubcommandHelpText = Record<PrecomputedSubcommandHelpCommand, string>;
5758type RootHelpRenderContext = Pick<RootHelpRenderOptions, "config" | "env">;
59+type Awaitable<T> = T | Promise<T>;
60+type SourceCommandHelpCommand = "nodes" | "secrets" | PrecomputedSubcommandHelpCommand;
61+type SourceCommandHelpText = Record<SourceCommandHelpCommand, string>;
58625963function resolveRootHelpBundleIdentity(
6064distDirOverride: string = distDir,
@@ -257,6 +261,98 @@ function createIsolatedRootHelpRenderContext(
257261return { config, env };
258262}
259263264+async function mapWithConcurrency<T, R>(
265+values: readonly T[],
266+limit: number,
267+run: (value: T) => Promise<R>,
268+): Promise<R[]> {
269+const results: R[] = [];
270+results.length = values.length;
271+let nextIndex = 0;
272+const workerCount = Math.min(Math.max(1, limit), values.length);
273+await Promise.all(
274+Array.from({ length: workerCount }, async () => {
275+for (;;) {
276+const index = nextIndex;
277+nextIndex += 1;
278+if (index >= values.length) {
279+return;
280+}
281+results[index] = await run(values[index]);
282+}
283+}),
284+);
285+return results;
286+}
287+288+async function spawnText(
289+args: string[],
290+options: {
291+cwd: string;
292+env: NodeJS.ProcessEnv;
293+failureMessage: string;
294+timeoutMs: number;
295+},
296+): Promise<string> {
297+return await new Promise((resolve, reject) => {
298+const child = spawn(process.execPath, args, {
299+cwd: options.cwd,
300+env: options.env,
301+stdio: ["ignore", "pipe", "pipe"],
302+});
303+let stdout = "";
304+let stderr = "";
305+let settled = false;
306+let timedOut = false;
307+const settle = (callback: () => void) => {
308+if (settled) {
309+return;
310+}
311+settled = true;
312+clearTimeout(timeout);
313+callback();
314+};
315+const timeout = setTimeout(() => {
316+timedOut = true;
317+child.kill("SIGKILL");
318+}, options.timeoutMs);
319+child.stdout.setEncoding("utf8");
320+child.stdout.on("data", (chunk: string) => {
321+stdout += chunk;
322+});
323+child.stderr.setEncoding("utf8");
324+child.stderr.on("data", (chunk: string) => {
325+stderr += chunk;
326+});
327+child.once("error", (error) => {
328+settle(() => {
329+reject(error);
330+});
331+});
332+child.once("close", (code, signal) => {
333+settle(() => {
334+if (code === 0) {
335+resolve(stdout);
336+return;
337+}
338+const detail = stderr.trim();
339+reject(
340+new Error(
341+options.failureMessage +
342+(detail
343+ ? `: ${detail}`
344+ : timedOut
345+ ? ": timed out"
346+ : signal
347+ ? `: terminated by ${signal}`
348+ : ""),
349+),
350+);
351+});
352+});
353+});
354+}
355+260356export async function renderBundledRootHelpText(
261357_distDirOverride: string = distDir,
262358renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(
@@ -389,52 +485,60 @@ function renderSourceBrowserHelpText(
389485return result.stdout ?? "";
390486}
391487392-function renderSourceCommandHelpText(
393-command: "nodes" | "secrets" | PrecomputedSubcommandHelpCommand,
488+async function renderSourceCommandHelpText(
489+command: SourceCommandHelpCommand,
394490renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
395-): string {
396-const result = spawnSync(process.execPath, ["openclaw.mjs", command, "--help"], {
491+): Promise<string> {
492+return await spawnText(["openclaw.mjs", command, "--help"], {
397493cwd: rootDir,
398-encoding: "utf8",
399494env: {
400495 ...renderContext.env,
401496OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1",
402497},
403-timeout: COMMAND_HELP_RENDER_TIMEOUT_MS,
498+failureMessage: `Failed to render source ${command} help`,
499+timeoutMs: COMMAND_HELP_RENDER_TIMEOUT_MS,
404500});
405-if (result.error) {
406-throw result.error;
407-}
408-if (result.status !== 0) {
409-const stderr = result.stderr?.trim();
410-throw new Error(
411-`Failed to render source ${command} help` +
412-(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
413-);
414-}
415-return result.stdout ?? "";
416501}
417502418-function renderSourceSecretsHelpText(
503+async function renderSourceSecretsHelpText(
419504renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
420-): string {
421-return renderSourceCommandHelpText("secrets", renderContext);
505+): Promise<string> {
506+return await renderSourceCommandHelpText("secrets", renderContext);
422507}
423508424-function renderSourceNodesHelpText(
509+async function renderSourceNodesHelpText(
425510renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
426-): string {
427-return renderSourceCommandHelpText("nodes", renderContext);
511+): Promise<string> {
512+return await renderSourceCommandHelpText("nodes", renderContext);
428513}
429514430-function renderSourceSubcommandHelpTextRecord(
515+async function renderSourceCommandHelpTextRecord(
516+commands: readonly SourceCommandHelpCommand[],
431517renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
432-): PrecomputedSubcommandHelpText {
433-const entries = PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS.map((commandName) => [
434-commandName,
435-renderSourceCommandHelpText(commandName, renderContext),
436-]);
437-return Object.fromEntries(entries) as PrecomputedSubcommandHelpText;
518+): Promise<SourceCommandHelpText> {
519+const helpTexts = await mapWithConcurrency(
520+commands,
521+COMMAND_HELP_RENDER_CONCURRENCY,
522+async (commandName) => await renderSourceCommandHelpText(commandName, renderContext),
523+);
524+return Object.fromEntries(
525+commands.map((commandName, index) => [commandName, helpTexts[index]]),
526+) as SourceCommandHelpText;
527+}
528+529+async function renderSourceSubcommandHelpTextRecord(
530+renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
531+): Promise<PrecomputedSubcommandHelpText> {
532+const commandHelpText = await renderSourceCommandHelpTextRecord(
533+PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS,
534+renderContext,
535+);
536+return Object.fromEntries(
537+PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS.map((commandName) => [
538+commandName,
539+commandHelpText[commandName],
540+]),
541+) as PrecomputedSubcommandHelpText;
438542}
439543440544export async function writeCliStartupMetadata(options?: {
@@ -445,9 +549,11 @@ export async function writeCliStartupMetadata(options?: {
445549renderBundledRootHelpText?: typeof renderBundledRootHelpText;
446550renderSourceRootHelpText?: typeof renderSourceRootHelpText;
447551renderSourceBrowserHelpText?: typeof renderSourceBrowserHelpText;
448-renderSourceSecretsHelpText?: typeof renderSourceSecretsHelpText;
449-renderSourceNodesHelpText?: typeof renderSourceNodesHelpText;
450-renderSourceSubcommandHelpTextRecord?: typeof renderSourceSubcommandHelpTextRecord;
552+renderSourceSecretsHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
553+renderSourceNodesHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
554+renderSourceSubcommandHelpTextRecord?: (
555+renderContext: RootHelpRenderContext,
556+) => Awaitable<PrecomputedSubcommandHelpText>;
451557}): Promise<void> {
452558const resolvedDistDir = options?.distDir ?? distDir;
453559const resolvedOutputPath = options?.outputPath ?? outputPath;
@@ -514,15 +620,31 @@ export async function writeCliStartupMetadata(options?: {
514620const browserHelpText = (options?.renderSourceBrowserHelpText ?? renderSourceBrowserHelpText)(
515621renderContext,
516622);
517-const secretsHelpText = (options?.renderSourceSecretsHelpText ?? renderSourceSecretsHelpText)(
518-renderContext,
519-);
520-const nodesHelpText = (options?.renderSourceNodesHelpText ?? renderSourceNodesHelpText)(
521-renderContext,
522-);
523-const subcommandHelpText = (
524-options?.renderSourceSubcommandHelpTextRecord ?? renderSourceSubcommandHelpTextRecord
525-)(renderContext);
623+const commandHelpText =
624+options?.renderSourceSecretsHelpText ||
625+options?.renderSourceNodesHelpText ||
626+options?.renderSourceSubcommandHelpTextRecord
627+ ? null
628+ : await renderSourceCommandHelpTextRecord(
629+["secrets", "nodes", ...PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS],
630+renderContext,
631+);
632+const secretsHelpText = commandHelpText
633+ ? commandHelpText.secrets
634+ : await (options?.renderSourceSecretsHelpText ?? renderSourceSecretsHelpText)(renderContext);
635+const nodesHelpText = commandHelpText
636+ ? commandHelpText.nodes
637+ : await (options?.renderSourceNodesHelpText ?? renderSourceNodesHelpText)(renderContext);
638+const subcommandHelpText = commandHelpText
639+ ? (Object.fromEntries(
640+PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS.map((commandName) => [
641+commandName,
642+commandHelpText[commandName],
643+]),
644+) as PrecomputedSubcommandHelpText)
645+ : await (options?.renderSourceSubcommandHelpTextRecord ?? renderSourceSubcommandHelpTextRecord)(
646+renderContext,
647+);
526648527649mkdirSync(resolvedDistDir, { recursive: true });
528650writeFileSync(
@@ -561,6 +683,12 @@ function hasAllPrecomputedSubcommandHelpText(value: unknown): boolean {
561683);
562684}
563685686+export const testing = {
687+ mapWithConcurrency,
688+};
689+690+export { testing as __testing };
691+564692if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
565693await writeCliStartupMetadata();
566694process.exit(0);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。