mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): handle SecretInput union in config form analyzer
The config form marks models.providers as unsupported because SecretInputSchema creates a oneOf union that the form analyzer cannot handle. Add detection for secret-ref union variants and normalize them to plain string inputs for form display. Closes #31490
This commit is contained in:
committed by
Peter Steinberger
parent
1727279598
commit
9c1312b5e4
@@ -304,6 +304,83 @@ describe("config form renderer", () => {
|
||||
expect(noMatchContainer.textContent).toContain('No settings match "mode tag:security"');
|
||||
});
|
||||
|
||||
it("supports SecretInput unions in additionalProperties maps", () => {
|
||||
const onPatch = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
models: {
|
||||
type: "object",
|
||||
properties: {
|
||||
providers: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: {
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: { type: "string", const: "env" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: { type: "string", const: "file" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const analysis = analyzeConfigSchema(schema);
|
||||
expect(analysis.unsupportedPaths).not.toContain("models.providers");
|
||||
expect(analysis.unsupportedPaths).not.toContain("models.providers.*.apiKey");
|
||||
|
||||
render(
|
||||
renderConfigForm({
|
||||
schema: analysis.schema,
|
||||
uiHints: {
|
||||
"models.providers.*.apiKey": { sensitive: true },
|
||||
},
|
||||
unsupportedPaths: analysis.unsupportedPaths,
|
||||
value: { models: { providers: { openai: { apiKey: "old" } } } },
|
||||
onPatch,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const apiKeyInput: HTMLInputElement | null = container.querySelector("input[type='password']");
|
||||
expect(apiKeyInput).not.toBeNull();
|
||||
if (!apiKeyInput) {
|
||||
return;
|
||||
}
|
||||
apiKeyInput.value = "new-key";
|
||||
apiKeyInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["models", "providers", "openai", "apiKey"], "new-key");
|
||||
});
|
||||
|
||||
it("flags unsupported unions", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
|
||||
@@ -118,6 +118,58 @@ function normalizeSchemaNode(
|
||||
};
|
||||
}
|
||||
|
||||
function isSecretRefVariant(entry: JsonSchema): boolean {
|
||||
if (schemaType(entry) !== "object") {
|
||||
return false;
|
||||
}
|
||||
const source = entry.properties?.source;
|
||||
const provider = entry.properties?.provider;
|
||||
const id = entry.properties?.id;
|
||||
if (!source || !provider || !id) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof source.const === "string" &&
|
||||
schemaType(provider) === "string" &&
|
||||
schemaType(id) === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isSecretRefUnion(entry: JsonSchema): boolean {
|
||||
const variants = entry.oneOf ?? entry.anyOf;
|
||||
if (!variants || variants.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return variants.every((variant) => isSecretRefVariant(variant));
|
||||
}
|
||||
|
||||
function normalizeSecretInputUnion(
|
||||
schema: JsonSchema,
|
||||
path: Array<string | number>,
|
||||
remaining: JsonSchema[],
|
||||
nullable: boolean,
|
||||
): ConfigSchemaAnalysis | null {
|
||||
const stringIndex = remaining.findIndex((entry) => schemaType(entry) === "string");
|
||||
if (stringIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
const nonString = remaining.filter((_, index) => index !== stringIndex);
|
||||
if (nonString.length !== 1 || !isSecretRefUnion(nonString[0])) {
|
||||
return null;
|
||||
}
|
||||
return normalizeSchemaNode(
|
||||
{
|
||||
...schema,
|
||||
...remaining[stringIndex],
|
||||
nullable,
|
||||
anyOf: undefined,
|
||||
oneOf: undefined,
|
||||
allOf: undefined,
|
||||
},
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeUnion(
|
||||
schema: JsonSchema,
|
||||
path: Array<string | number>,
|
||||
@@ -161,6 +213,13 @@ function normalizeUnion(
|
||||
remaining.push(entry);
|
||||
}
|
||||
|
||||
// Config secrets accept either a raw key string or a structured secret ref object.
|
||||
// The form only supports editing the string path for now.
|
||||
const secretInput = normalizeSecretInputUnion(schema, path, remaining, nullable);
|
||||
if (secretInput) {
|
||||
return secretInput;
|
||||
}
|
||||
|
||||
if (literals.length > 0 && remaining.length === 0) {
|
||||
const unique: unknown[] = [];
|
||||
for (const value of literals) {
|
||||
|
||||
Reference in New Issue
Block a user