

























@@ -459,7 +459,157 @@ function getAtPath(root: unknown, path: PathSegment[]): { found: boolean; value?
459459return { found: true, value: current };
460460}
461461462-type SetAtPathOptions = { numericObjectKeys?: boolean };
462+type JsonSchemaRecord = {
463+type?: unknown;
464+properties?: unknown;
465+additionalProperties?: unknown;
466+items?: unknown;
467+anyOf?: unknown;
468+oneOf?: unknown;
469+allOf?: unknown;
470+};
471+472+type SetAtPathOptions = {
473+numericObjectKeys?: boolean;
474+schema?: JsonSchemaRecord;
475+};
476+477+function isSchemaRecord(value: unknown): value is JsonSchemaRecord {
478+return Boolean(value && typeof value === "object" && !Array.isArray(value));
479+}
480+481+function schemaTypes(schema: JsonSchemaRecord): Set<string> {
482+if (typeof schema.type === "string") {
483+return new Set([schema.type]);
484+}
485+if (Array.isArray(schema.type)) {
486+return new Set(schema.type.filter((entry): entry is string => typeof entry === "string"));
487+}
488+return new Set();
489+}
490+491+function schemaAlternatives(
492+schema: JsonSchemaRecord,
493+seen = new Set<JsonSchemaRecord>(),
494+): JsonSchemaRecord[] {
495+if (seen.has(schema)) {
496+return [];
497+}
498+seen.add(schema);
499+const alternatives: JsonSchemaRecord[] = [schema];
500+for (const key of ["anyOf", "oneOf", "allOf"] as const) {
501+const entries = schema[key];
502+if (!Array.isArray(entries)) {
503+continue;
504+}
505+for (const entry of entries) {
506+if (isSchemaRecord(entry)) {
507+alternatives.push(...schemaAlternatives(entry, seen));
508+}
509+}
510+}
511+return alternatives;
512+}
513+514+function schemaLooksArray(schema: JsonSchemaRecord): boolean {
515+return (
516+schemaTypes(schema).has("array") ||
517+isSchemaRecord(schema.items) ||
518+Array.isArray(schema.items)
519+);
520+}
521+522+function schemaLooksObject(schema: JsonSchemaRecord): boolean {
523+const types = schemaTypes(schema);
524+return (
525+types.has("object") ||
526+isSchemaRecord(schema.properties) ||
527+schema.additionalProperties === true ||
528+isSchemaRecord(schema.additionalProperties)
529+);
530+}
531+532+function propertySchema(schema: JsonSchemaRecord, segment: PathSegment): JsonSchemaRecord[] {
533+const schemas: JsonSchemaRecord[] = [];
534+for (const alternative of schemaAlternatives(schema)) {
535+if (schemaLooksArray(alternative)) {
536+if (isIndexSegment(segment)) {
537+const index = Number.parseInt(segment, 10);
538+const indexedItem = Array.isArray(alternative.items)
539+ ? alternative.items[index]
540+ : alternative.items;
541+if (isSchemaRecord(indexedItem)) {
542+schemas.push(indexedItem);
543+}
544+}
545+continue;
546+}
547+const properties = isSchemaRecord(alternative.properties)
548+ ? (alternative.properties as Record<string, unknown>)
549+ : undefined;
550+const explicit = properties?.[segment];
551+if (isSchemaRecord(explicit)) {
552+schemas.push(explicit);
553+continue;
554+}
555+if (isSchemaRecord(alternative.additionalProperties)) {
556+schemas.push(alternative.additionalProperties);
557+}
558+}
559+return schemas;
560+}
561+562+function schemasAtPath(schema: JsonSchemaRecord | undefined, path: readonly PathSegment[]) {
563+if (!schema) {
564+return [];
565+}
566+let schemas = [schema];
567+for (const segment of path) {
568+schemas = schemas.flatMap((candidate) => propertySchema(candidate, segment));
569+if (schemas.length === 0) {
570+return [];
571+}
572+}
573+return schemas;
574+}
575+576+function schemaPrefersArrayAtPath(
577+schema: JsonSchemaRecord | undefined,
578+path: readonly PathSegment[],
579+): boolean | undefined {
580+const candidates = schemasAtPath(schema, path).flatMap((candidate) =>
581+schemaAlternatives(candidate),
582+);
583+if (candidates.length === 0) {
584+return undefined;
585+}
586+const hasArray = candidates.some((candidate) => schemaLooksArray(candidate));
587+const hasObject = candidates.some((candidate) => schemaLooksObject(candidate));
588+if (hasArray && !hasObject) {
589+return true;
590+}
591+if (hasObject && !hasArray) {
592+return false;
593+}
594+return undefined;
595+}
596+597+function shouldCreateArrayForMissingPathSegment(params: {
598+path: readonly PathSegment[];
599+segmentIndex: number;
600+next?: PathSegment;
601+options?: SetAtPathOptions;
602+}): boolean {
603+if (!params.next || params.options?.numericObjectKeys || !isIndexSegment(params.next)) {
604+return false;
605+}
606+const parentPath = params.path.slice(0, params.segmentIndex + 1);
607+const schemaPreference = schemaPrefersArrayAtPath(params.options?.schema, parentPath);
608+if (schemaPreference !== undefined) {
609+return schemaPreference;
610+}
611+return true;
612+}
463613464614function setAtPath(
465615root: Record<string, unknown>,
@@ -471,7 +621,12 @@ function setAtPath(
471621for (let i = 0; i < path.length - 1; i += 1) {
472622const segment = path[i];
473623const next = path[i + 1];
474-const nextIsIndex = !options?.numericObjectKeys && Boolean(next && isIndexSegment(next));
624+const nextIsIndex = shouldCreateArrayForMissingPathSegment({
625+ path,
626+segmentIndex: i,
627+ next,
628+ options,
629+});
475630if (Array.isArray(current)) {
476631if (!isIndexSegment(segment)) {
477632throw new Error(`Expected numeric index for array segment "${segment}"`);
@@ -1425,7 +1580,11 @@ function collectDryRunRefs(params: {
14251580if (!ref) {
14261581continue;
14271582}
1428-if (includeAllDiscoveredRefs || targetPaths.has(target.path) || providerAliases.has(ref.provider)) {
1583+if (
1584+includeAllDiscoveredRefs ||
1585+targetPaths.has(target.path) ||
1586+providerAliases.has(ref.provider)
1587+) {
14291588refsByKey.set(secretRefKey(ref), ref);
14301589}
14311590}
@@ -1629,6 +1788,14 @@ function formatAutoManagedMetaError(paths: readonly PathSegment[][]): string {
16291788].join("\n");
16301789}
163117901791+async function loadConfigMutationSchema(): Promise<JsonSchemaRecord | undefined> {
1792+try {
1793+return structuredClone((await readBestEffortRuntimeConfigSchema()).schema) as JsonSchemaRecord;
1794+} catch {
1795+return undefined;
1796+}
1797+}
1798+16321799function collectDryRunSchemaErrors(params: { config: OpenClawConfig }): ConfigSetDryRunError[] {
16331800const validated = validateConfigObjectRawWithPlugins(params.config);
16341801if (validated.ok) {
@@ -1719,6 +1886,7 @@ async function runConfigOperations(params: {
17191886// instead of snapshot.config (runtime-merged with defaults).
17201887// This prevents runtime defaults from leaking into the written config file (issue #6070)
17211888const next = structuredClone(snapshot.resolved) as Record<string, unknown>;
1889+const mutationSchema = await loadConfigMutationSchema();
17221890const unsetPaths: PathSegment[][] = [];
17231891const explicitSetPaths: PathSegment[][] = [];
17241892for (const operation of operations) {
@@ -1731,6 +1899,7 @@ async function runConfigOperations(params: {
17311899if (operation.mutation === "merge" || (options.merge && operation.mutation !== "replace")) {
17321900mergeAtPath(next, operation.setPath, operation.value, {
17331901numericObjectKeys: params.successMode === "patch",
1902+schema: mutationSchema,
17341903});
17351904} else {
17361905assertNonDestructiveReplacement({
@@ -1741,6 +1910,7 @@ async function runConfigOperations(params: {
17411910});
17421911setAtPath(next, operation.setPath, operation.value, {
17431912numericObjectKeys: params.successMode === "patch",
1913+schema: mutationSchema,
17441914});
17451915}
17461916}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。