mirror of https://github.com/openclaw/openclaw.git
perf: precompute base config schema
This commit is contained in:
parent
593e333c10
commit
ca99163b98
|
|
@ -571,7 +571,8 @@
|
|||
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"check": "pnpm check:host-env-policy:swift && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check": "pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
|
|
@ -579,6 +580,8 @@
|
|||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
"config:docs:check": "node --import tsx scripts/generate-config-doc-baseline.ts --check",
|
||||
"config:docs:gen": "node --import tsx scripts/generate-config-doc-baseline.ts --write",
|
||||
"config:schema:check": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"config:schema:gen": "node --import tsx scripts/generate-base-config-schema.ts --write",
|
||||
"deadcode:ci": "pnpm deadcode:report:ci:knip",
|
||||
"deadcode:knip": "pnpm dlx knip --config knip.config.ts --isolate-workspaces --production --no-progress --reporter compact --files --dependencies",
|
||||
"deadcode:report": "pnpm deadcode:knip; pnpm deadcode:ts-prune; pnpm deadcode:ts-unused",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { computeBaseConfigSchemaResponse } from "../src/config/schema-base.js";
|
||||
|
||||
const GENERATED_BY = "scripts/generate-base-config-schema.ts";
|
||||
const DEFAULT_OUTPUT_PATH = "src/config/schema.base.generated.ts";
|
||||
|
||||
function readIfExists(filePath: string): string | null {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTypeScriptModule(source: string, outputPath: string): string {
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const formatter = spawnSync(
|
||||
process.platform === "win32" ? "pnpm.cmd" : "pnpm",
|
||||
["exec", "oxfmt", "--stdin-filepath", outputPath],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
input: source,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (formatter.status !== 0) {
|
||||
const details =
|
||||
formatter.stderr?.trim() || formatter.stdout?.trim() || "unknown formatter failure";
|
||||
throw new Error(`failed to format generated base config schema: ${details}`);
|
||||
}
|
||||
return formatter.stdout;
|
||||
}
|
||||
|
||||
export function renderBaseConfigSchemaModule(params?: { generatedAt?: string }): string {
|
||||
const payload = computeBaseConfigSchemaResponse({
|
||||
generatedAt: params?.generatedAt ?? new Date().toISOString(),
|
||||
});
|
||||
return formatTypeScriptModule(
|
||||
`// Auto-generated by ${GENERATED_BY}. Do not edit directly.
|
||||
|
||||
import type { BaseConfigSchemaResponse } from "./schema-base.js";
|
||||
|
||||
export const GENERATED_BASE_CONFIG_SCHEMA = ${JSON.stringify(payload, null, 2)} as const satisfies BaseConfigSchemaResponse;
|
||||
`,
|
||||
DEFAULT_OUTPUT_PATH,
|
||||
);
|
||||
}
|
||||
|
||||
export function writeBaseConfigSchemaModule(params?: {
|
||||
repoRoot?: string;
|
||||
outputPath?: string;
|
||||
check?: boolean;
|
||||
}): { changed: boolean; wrote: boolean; outputPath: string } {
|
||||
const repoRoot = path.resolve(
|
||||
params?.repoRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
|
||||
);
|
||||
const outputPath = path.resolve(repoRoot, params?.outputPath ?? DEFAULT_OUTPUT_PATH);
|
||||
const current = readIfExists(outputPath);
|
||||
const generatedAt =
|
||||
current?.match(/generatedAt:\s*"([^"]+)"/u)?.[1] ??
|
||||
current?.match(/"generatedAt":\s*"([^"]+)"/u)?.[1] ??
|
||||
new Date().toISOString();
|
||||
const next = renderBaseConfigSchemaModule({ generatedAt });
|
||||
const changed = current !== next;
|
||||
|
||||
if (params?.check) {
|
||||
return { changed, wrote: false, outputPath };
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(outputPath, next, "utf8");
|
||||
}
|
||||
return { changed, wrote: changed, outputPath };
|
||||
}
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
if (args.has("--check") && args.has("--write")) {
|
||||
throw new Error("Use either --check or --write, not both.");
|
||||
}
|
||||
|
||||
if (import.meta.url === new URL(process.argv[1] ?? "", "file://").href) {
|
||||
const result = writeBaseConfigSchemaModule({ check: args.has("--check") });
|
||||
if (result.changed) {
|
||||
if (args.has("--check")) {
|
||||
console.error(
|
||||
`[base-config-schema] stale generated output at ${path.relative(process.cwd(), result.outputPath)}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`[base-config-schema] wrote ${path.relative(process.cwd(), result.outputPath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { VERSION } from "../version.js";
|
||||
import type { ConfigUiHints } from "./schema.hints.js";
|
||||
import { buildBaseHints, mapSensitivePaths } from "./schema.hints.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
type ConfigSchema = Record<string, unknown>;
|
||||
|
||||
type JsonSchemaObject = Record<string, unknown> & {
|
||||
properties?: Record<string, JsonSchemaObject>;
|
||||
required?: string[];
|
||||
additionalProperties?: JsonSchemaObject | boolean;
|
||||
};
|
||||
|
||||
export type BaseConfigSchemaResponse = {
|
||||
schema: ConfigSchema;
|
||||
uiHints: ConfigUiHints;
|
||||
version: string;
|
||||
generatedAt: string;
|
||||
};
|
||||
|
||||
function cloneSchema<T>(value: T): T {
|
||||
if (typeof structuredClone === "function") {
|
||||
return structuredClone(value);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function asSchemaObject(value: unknown): JsonSchemaObject | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as JsonSchemaObject;
|
||||
}
|
||||
|
||||
function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
|
||||
const next = cloneSchema(schema);
|
||||
const root = asSchemaObject(next);
|
||||
if (!root || !root.properties) {
|
||||
return next;
|
||||
}
|
||||
// Allow `$schema` in config files for editor tooling, but hide it from the
|
||||
// Control UI form schema so it does not show up as a configurable section.
|
||||
delete root.properties.$schema;
|
||||
if (Array.isArray(root.required)) {
|
||||
root.required = root.required.filter((key) => key !== "$schema");
|
||||
}
|
||||
const channelsNode = asSchemaObject(root.properties.channels);
|
||||
if (channelsNode) {
|
||||
channelsNode.properties = {};
|
||||
channelsNode.required = [];
|
||||
channelsNode.additionalProperties = true;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function computeBaseConfigSchemaResponse(params?: {
|
||||
generatedAt?: string;
|
||||
}): BaseConfigSchemaResponse {
|
||||
const schema = OpenClawSchema.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
});
|
||||
schema.title = "OpenClawConfig";
|
||||
const hints = applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints()));
|
||||
return {
|
||||
schema: stripChannelSchema(schema),
|
||||
uiHints: hints,
|
||||
version: VERSION,
|
||||
generatedAt: params?.generatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { computeBaseConfigSchemaResponse } from "./schema-base.js";
|
||||
import { GENERATED_BASE_CONFIG_SCHEMA } from "./schema.base.generated.js";
|
||||
|
||||
describe("generated base config schema", () => {
|
||||
it("matches the computed base config schema payload", () => {
|
||||
expect(
|
||||
computeBaseConfigSchemaResponse({
|
||||
generatedAt: GENERATED_BASE_CONFIG_SCHEMA.generatedAt,
|
||||
}),
|
||||
).toEqual(GENERATED_BASE_CONFIG_SCHEMA);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,14 @@
|
|||
import crypto from "node:crypto";
|
||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { GENERATED_BASE_CONFIG_SCHEMA } from "./schema.base.generated.js";
|
||||
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
import { applySensitiveHints, buildBaseHints, mapSensitivePaths } from "./schema.hints.js";
|
||||
import { applySensitiveHints } from "./schema.hints.js";
|
||||
import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
|
||||
export type ConfigSchema = ReturnType<typeof OpenClawSchema.toJSONSchema>;
|
||||
export type ConfigSchema = Record<string, unknown>;
|
||||
|
||||
type JsonSchemaNode = Record<string, unknown>;
|
||||
|
||||
|
|
@ -406,43 +405,11 @@ function setMergedSchemaCache(key: string, value: ConfigSchemaResponse): void {
|
|||
mergedSchemaCache.set(key, value);
|
||||
}
|
||||
|
||||
function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
|
||||
const next = cloneSchema(schema);
|
||||
const root = asSchemaObject(next);
|
||||
if (!root || !root.properties) {
|
||||
return next;
|
||||
}
|
||||
// Allow `$schema` in config files for editor tooling, but hide it from the
|
||||
// Control UI form schema so it does not show up as a configurable section.
|
||||
delete root.properties.$schema;
|
||||
if (Array.isArray(root.required)) {
|
||||
root.required = root.required.filter((key) => key !== "$schema");
|
||||
}
|
||||
const channelsNode = asSchemaObject(root.properties.channels);
|
||||
if (channelsNode) {
|
||||
channelsNode.properties = {};
|
||||
channelsNode.required = [];
|
||||
channelsNode.additionalProperties = true;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||
if (cachedBase) {
|
||||
return cachedBase;
|
||||
}
|
||||
const schema = OpenClawSchema.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
});
|
||||
schema.title = "OpenClawConfig";
|
||||
const hints = applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints()));
|
||||
const next = {
|
||||
schema: stripChannelSchema(schema),
|
||||
uiHints: hints,
|
||||
version: VERSION,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
const next = GENERATED_BASE_CONFIG_SCHEMA as unknown as ConfigSchemaResponse;
|
||||
cachedBase = next;
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue