diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index 87cc96d7540..49f1e511113 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -1,8 +1,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { formatPluginConfigIssue } from "openclaw/plugin-sdk/extension-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; -import type { z } from "openclaw/plugin-sdk/zod"; import { AcpxPluginConfigSchema, DEFAULT_ACPX_TIMEOUT_SECONDS } from "./config-schema.js"; import type { AcpxPluginConfig, @@ -112,26 +112,13 @@ type ParseResult = | { ok: true; value: AcpxPluginConfig | undefined } | { ok: false; message: string }; -function formatAcpxConfigIssue(issue: z.ZodIssue | undefined): string { - if (!issue) { - return "invalid config"; - } - if (issue.code === "unrecognized_keys" && issue.keys.length > 0) { - return `unknown config key: ${issue.keys[0]}`; - } - if (issue.code === "invalid_type" && issue.path.length === 0) { - return "expected config object"; - } - return issue.message; -} - function parseAcpxPluginConfig(value: unknown): ParseResult { if (value === undefined) { return { ok: true, value: undefined }; } const parsed = AcpxPluginConfigSchema.safeParse(value); if (!parsed.success) { - return { ok: false, message: formatAcpxConfigIssue(parsed.error.issues[0]) }; + return { ok: false, message: formatPluginConfigIssue(parsed.error.issues[0]) }; } return { ok: true, diff --git a/extensions/diffs/src/config.ts b/extensions/diffs/src/config.ts index 8fe2fd1f2b9..a238b8d0f2d 100644 --- a/extensions/diffs/src/config.ts +++ b/extensions/diffs/src/config.ts @@ -1,3 +1,4 @@ +import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared"; import { buildPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry"; import { z } from "openclaw/plugin-sdk/zod"; import type { OpenClawPluginConfigSchema } from "../api.js"; @@ -180,13 +181,7 @@ const diffsPluginConfigSchemaBase = buildPluginConfigSchema(DiffsPluginJsonSchem return { success: false, error: { - issues: result.error.issues.map((issue) => ({ - path: issue.path.filter((segment): segment is string | number => { - const kind = typeof segment; - return kind === "string" || kind === "number"; - }), - message: issue.message, - })), + issues: mapPluginConfigIssues(result.error.issues), }, }; }, diff --git a/extensions/memory-wiki/src/config.ts b/extensions/memory-wiki/src/config.ts index a15dcf8c6f4..3d5caf5ee0f 100644 --- a/extensions/memory-wiki/src/config.ts +++ b/extensions/memory-wiki/src/config.ts @@ -1,5 +1,6 @@ import os from "node:os"; import path from "node:path"; +import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared"; import { buildPluginConfigSchema, z, type OpenClawPluginConfigSchema } from "../api.js"; export const WIKI_VAULT_MODES = ["isolated", "bridge", "unsafe-local"] as const; @@ -174,13 +175,7 @@ const memoryWikiConfigSchemaBase = buildPluginConfigSchema(MemoryWikiConfigSourc return { success: false, error: { - issues: result.error.issues.map((issue) => ({ - path: issue.path.filter((segment): segment is string | number => { - const kind = typeof segment; - return kind === "string" || kind === "number"; - }), - message: issue.message, - })), + issues: mapPluginConfigIssues(result.error.issues), }, }; }, diff --git a/extensions/openshell/src/config.ts b/extensions/openshell/src/config.ts index 8366574d897..c946b0f0823 100644 --- a/extensions/openshell/src/config.ts +++ b/extensions/openshell/src/config.ts @@ -1,5 +1,9 @@ import path from "node:path"; import { buildPluginConfigSchema, type OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { + formatPluginConfigIssue, + mapPluginConfigIssues, +} from "openclaw/plugin-sdk/extension-shared"; import { z } from "openclaw/plugin-sdk/zod"; export type OpenShellPluginConfig = { @@ -91,19 +95,6 @@ const OpenShellPluginConfigSchema = z.strictObject({ .optional(), }); -function formatOpenShellConfigIssue(issue: z.ZodIssue | undefined): string { - if (!issue) { - return "invalid config"; - } - if (issue.code === "unrecognized_keys" && issue.keys.length > 0) { - return `unknown config key: ${issue.keys[0]}`; - } - if (issue.code === "invalid_type" && issue.path.length === 0) { - return "expected config object"; - } - return issue.message; -} - function isManagedOpenShellRemotePath(value: string): boolean { return OPEN_SHELL_MANAGED_REMOTE_ROOTS.some( (root) => value === root || value.startsWith(`${root}/`), @@ -141,13 +132,7 @@ export function createOpenShellPluginConfigSchema(): OpenClawPluginConfigSchema return { success: false, error: { - issues: parsed.error.issues.map((issue) => ({ - path: issue.path.filter((segment): segment is string | number => { - const kind = typeof segment; - return kind === "string" || kind === "number"; - }), - message: formatOpenShellConfigIssue(issue), - })), + issues: mapPluginConfigIssues(parsed.error.issues), }, }; }, @@ -176,7 +161,7 @@ export function resolveOpenShellPluginConfig(value: unknown): ResolvedOpenShellP const parsed = OpenShellPluginConfigSchema.safeParse(value); if (!parsed.success) { - const message = formatOpenShellConfigIssue(parsed.error.issues[0]); + const message = formatPluginConfigIssue(parsed.error.issues[0]); throw new Error(`Invalid openshell plugin config: ${message}`); } const cfg = parsed.data as OpenShellPluginConfig; diff --git a/src/plugin-sdk/extension-shared.ts b/src/plugin-sdk/extension-shared.ts index 98eec401b8d..f21dad06395 100644 --- a/src/plugin-sdk/extension-shared.ts +++ b/src/plugin-sdk/extension-shared.ts @@ -143,6 +143,54 @@ const DEFAULT_PACKAGE_JSON_VERSION_CANDIDATES = [ type PackageJsonRequire = (id: string) => unknown; +type PluginConfigIssuePathSegment = string | number; + +type PluginConfigIssue = { + path: PluginConfigIssuePathSegment[]; + message: string; +}; + +type PluginConfigIssueMessageOptions = { + invalidConfigMessage?: string; + unknownKeyMessage?: (key: string) => string; + rootInvalidTypeMessage?: string; +}; + +export function formatPluginConfigIssue( + issue: z.ZodIssue | undefined, + options?: PluginConfigIssueMessageOptions, +): string { + if (!issue) { + return options?.invalidConfigMessage ?? "invalid config"; + } + if (issue.code === "unrecognized_keys" && issue.keys.length > 0) { + return options?.unknownKeyMessage?.(issue.keys[0]) ?? `unknown config key: ${issue.keys[0]}`; + } + if (issue.code === "invalid_type" && issue.path.length === 0) { + return options?.rootInvalidTypeMessage ?? "expected config object"; + } + return issue.message; +} + +export function normalizePluginConfigIssuePath( + path: readonly unknown[], +): PluginConfigIssuePathSegment[] { + return path.filter((segment): segment is PluginConfigIssuePathSegment => { + const kind = typeof segment; + return kind === "string" || kind === "number"; + }); +} + +export function mapPluginConfigIssues( + issues: readonly z.ZodIssue[], + options?: PluginConfigIssueMessageOptions, +): PluginConfigIssue[] { + return issues.map((issue) => ({ + path: normalizePluginConfigIssuePath(issue.path), + message: formatPluginConfigIssue(issue, options), + })); +} + export function readPluginPackageVersion(params: { require: PackageJsonRequire; candidates?: readonly string[];