mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +00:00
refactor(config): share schema lookup helpers
This commit is contained in:
@@ -7,6 +7,7 @@ import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { buildConfigSchema, type ConfigSchemaResponse } from "./schema.js";
|
||||
import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js";
|
||||
|
||||
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
@@ -132,24 +133,6 @@ function asSchemaObject(value: unknown): JsonSchemaObject | null {
|
||||
return value as JsonSchemaObject;
|
||||
}
|
||||
|
||||
function schemaHasChildren(schema: JsonSchemaObject): boolean {
|
||||
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(schema.items)) {
|
||||
return schema.items.some((entry) => typeof entry === "object" && entry !== null);
|
||||
}
|
||||
for (const branch of [schema.oneOf, schema.anyOf, schema.allOf]) {
|
||||
if (branch?.some((entry) => entry && typeof entry === "object" && schemaHasChildren(entry))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Boolean(schema.items && typeof schema.items === "object");
|
||||
}
|
||||
|
||||
function splitHintLookupPath(path: string): string[] {
|
||||
const normalized = normalizeBaselinePath(path);
|
||||
return normalized ? normalized.split(".").filter(Boolean) : [];
|
||||
@@ -159,45 +142,11 @@ function resolveUiHintMatch(
|
||||
uiHints: ConfigSchemaResponse["uiHints"],
|
||||
path: string,
|
||||
): ConfigSchemaResponse["uiHints"][string] | undefined {
|
||||
const targetParts = splitHintLookupPath(path);
|
||||
let bestMatch:
|
||||
| {
|
||||
hint: ConfigSchemaResponse["uiHints"][string];
|
||||
wildcardCount: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
for (const [hintPath, hint] of Object.entries(uiHints)) {
|
||||
const hintParts = splitHintLookupPath(hintPath);
|
||||
if (hintParts.length !== targetParts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wildcardCount = 0;
|
||||
let matches = true;
|
||||
for (let index = 0; index < hintParts.length; index += 1) {
|
||||
const hintPart = hintParts[index];
|
||||
const targetPart = targetParts[index];
|
||||
if (hintPart === targetPart) {
|
||||
continue;
|
||||
}
|
||||
if (hintPart === "*") {
|
||||
wildcardCount += 1;
|
||||
continue;
|
||||
}
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (!bestMatch || wildcardCount < bestMatch.wildcardCount) {
|
||||
bestMatch = { hint, wildcardCount };
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch?.hint;
|
||||
return findWildcardHintMatch({
|
||||
uiHints,
|
||||
path,
|
||||
splitPath: splitHintLookupPath,
|
||||
})?.hint;
|
||||
}
|
||||
|
||||
function normalizeTypeValue(value: string | string[] | undefined): string | string[] | undefined {
|
||||
|
||||
28
src/config/schema.shared.test.ts
Normal file
28
src/config/schema.shared.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js";
|
||||
|
||||
describe("schema.shared", () => {
|
||||
it("prefers the most specific wildcard hint match", () => {
|
||||
const match = findWildcardHintMatch({
|
||||
uiHints: {
|
||||
"channels.*.token": { label: "wildcard" },
|
||||
"channels.telegram.token": { label: "telegram" },
|
||||
},
|
||||
path: "channels.telegram.token",
|
||||
splitPath: (value) => value.split("."),
|
||||
});
|
||||
|
||||
expect(match).toEqual({
|
||||
path: "channels.telegram.token",
|
||||
hint: { label: "telegram" },
|
||||
});
|
||||
});
|
||||
|
||||
it("treats branch schemas as having children", () => {
|
||||
expect(
|
||||
schemaHasChildren({
|
||||
oneOf: [{ type: "string" }, { properties: { token: { type: "string" } } }],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
73
src/config/schema.shared.ts
Normal file
73
src/config/schema.shared.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
type JsonSchemaObject = {
|
||||
properties?: Record<string, JsonSchemaObject>;
|
||||
additionalProperties?: JsonSchemaObject | boolean;
|
||||
items?: JsonSchemaObject | JsonSchemaObject[];
|
||||
anyOf?: JsonSchemaObject[];
|
||||
allOf?: JsonSchemaObject[];
|
||||
oneOf?: JsonSchemaObject[];
|
||||
};
|
||||
|
||||
export function schemaHasChildren(schema: JsonSchemaObject): boolean {
|
||||
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(schema.items)) {
|
||||
return schema.items.some((entry) => typeof entry === "object" && entry !== null);
|
||||
}
|
||||
for (const branch of [schema.oneOf, schema.anyOf, schema.allOf]) {
|
||||
if (branch?.some((entry) => entry && typeof entry === "object" && schemaHasChildren(entry))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Boolean(schema.items && typeof schema.items === "object");
|
||||
}
|
||||
|
||||
export function findWildcardHintMatch<T>(params: {
|
||||
uiHints: Record<string, T>;
|
||||
path: string;
|
||||
splitPath: (path: string) => string[];
|
||||
}): { path: string; hint: T } | null {
|
||||
const targetParts = params.splitPath(params.path);
|
||||
let bestMatch:
|
||||
| {
|
||||
path: string;
|
||||
hint: T;
|
||||
wildcardCount: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
for (const [hintPath, hint] of Object.entries(params.uiHints)) {
|
||||
const hintParts = params.splitPath(hintPath);
|
||||
if (hintParts.length !== targetParts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wildcardCount = 0;
|
||||
let matches = true;
|
||||
for (let index = 0; index < hintParts.length; index += 1) {
|
||||
const hintPart = hintParts[index];
|
||||
const targetPart = targetParts[index];
|
||||
if (hintPart === targetPart) {
|
||||
continue;
|
||||
}
|
||||
if (hintPart === "*") {
|
||||
wildcardCount += 1;
|
||||
continue;
|
||||
}
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (!bestMatch || wildcardCount < bestMatch.wildcardCount) {
|
||||
bestMatch = { path: hintPath, hint, wildcardCount };
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch ? { path: bestMatch.path, hint: bestMatch.hint } : null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
import { applySensitiveHints, buildBaseHints, mapSensitivePaths } from "./schema.hints.js";
|
||||
import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
@@ -500,52 +501,11 @@ function resolveUiHintMatch(
|
||||
uiHints: ConfigUiHints,
|
||||
path: string,
|
||||
): { path: string; hint: ConfigUiHint } | null {
|
||||
const targetParts = splitLookupPath(path);
|
||||
let best: { path: string; hint: ConfigUiHint; wildcardCount: number } | null = null;
|
||||
|
||||
for (const [hintPath, hint] of Object.entries(uiHints)) {
|
||||
const hintParts = splitLookupPath(hintPath);
|
||||
if (hintParts.length !== targetParts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wildcardCount = 0;
|
||||
let matches = true;
|
||||
for (let index = 0; index < hintParts.length; index += 1) {
|
||||
const hintPart = hintParts[index];
|
||||
const targetPart = targetParts[index];
|
||||
if (hintPart === targetPart) {
|
||||
continue;
|
||||
}
|
||||
if (hintPart === "*") {
|
||||
wildcardCount += 1;
|
||||
continue;
|
||||
}
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (!best || wildcardCount < best.wildcardCount) {
|
||||
best = { path: hintPath, hint, wildcardCount };
|
||||
}
|
||||
}
|
||||
|
||||
return best ? { path: best.path, hint: best.hint } : null;
|
||||
}
|
||||
|
||||
function schemaHasChildren(schema: JsonSchemaObject): boolean {
|
||||
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(schema.items)) {
|
||||
return schema.items.some((entry) => typeof entry === "object" && entry !== null);
|
||||
}
|
||||
return Boolean(schema.items && typeof schema.items === "object");
|
||||
return findWildcardHintMatch({
|
||||
uiHints,
|
||||
path,
|
||||
splitPath: splitLookupPath,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveItemsSchema(schema: JsonSchemaObject, index?: number): JsonSchemaObject | null {
|
||||
|
||||
Reference in New Issue
Block a user