import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core"; import { z } from "openclaw/plugin-sdk/zod"; import type { OpenClawPluginConfigSchema } from "../api.js"; import { DIFF_IMAGE_QUALITY_PRESETS, DIFF_INDICATORS, DIFF_LAYOUTS, DIFF_MODES, DIFF_OUTPUT_FORMATS, DIFF_THEMES, type DiffFileDefaults, type DiffImageQualityPreset, type DiffIndicators, type DiffLayout, type DiffMode, type DiffOutputFormat, type DiffPresentationDefaults, type DiffTheme, type DiffToolDefaults, } from "./types.js"; type DiffsPluginConfig = { defaults?: { fontFamily?: string; fontSize?: number; lineSpacing?: number; layout?: DiffLayout; showLineNumbers?: boolean; diffIndicators?: DiffIndicators; wordWrap?: boolean; background?: boolean; theme?: DiffTheme; fileFormat?: DiffOutputFormat; fileQuality?: DiffImageQualityPreset; fileScale?: number; fileMaxWidth?: number; format?: DiffOutputFormat; // Backward-compatible aliases retained for existing configs. imageFormat?: DiffOutputFormat; imageQuality?: DiffImageQualityPreset; imageScale?: number; imageMaxWidth?: number; mode?: DiffMode; }; security?: { allowRemoteViewer?: boolean; }; }; const DEFAULT_IMAGE_QUALITY_PROFILES = { standard: { scale: 2, maxWidth: 960, maxPixels: 8_000_000, }, hq: { scale: 2.5, maxWidth: 1200, maxPixels: 14_000_000, }, print: { scale: 3, maxWidth: 1400, maxPixels: 24_000_000, }, } as const satisfies Record< DiffImageQualityPreset, { scale: number; maxWidth: number; maxPixels: number } >; export const DEFAULT_DIFFS_TOOL_DEFAULTS: DiffToolDefaults = { fontFamily: "Fira Code", fontSize: 15, lineSpacing: 1.6, layout: "unified", showLineNumbers: true, diffIndicators: "bars", wordWrap: true, background: true, theme: "dark", fileFormat: "png", fileQuality: "standard", fileScale: DEFAULT_IMAGE_QUALITY_PROFILES.standard.scale, fileMaxWidth: DEFAULT_IMAGE_QUALITY_PROFILES.standard.maxWidth, mode: "both", }; export type DiffsPluginSecurityConfig = { allowRemoteViewer: boolean; }; export const DEFAULT_DIFFS_PLUGIN_SECURITY: DiffsPluginSecurityConfig = { allowRemoteViewer: false, }; const DiffsPluginJsonSchemaSource = z.strictObject({ defaults: z .strictObject({ fontFamily: z.string().default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily).optional(), fontSize: z.number().min(10).max(24).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize).optional(), lineSpacing: z .number() .min(1) .max(3) .default(DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing) .optional(), layout: z.enum(DIFF_LAYOUTS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.layout).optional(), showLineNumbers: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.showLineNumbers).optional(), diffIndicators: z .enum(DIFF_INDICATORS) .default(DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators) .optional(), wordWrap: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap).optional(), background: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.background).optional(), theme: z.enum(DIFF_THEMES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.theme).optional(), fileFormat: z .enum(DIFF_OUTPUT_FORMATS) .default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat) .optional(), format: z.enum(DIFF_OUTPUT_FORMATS).optional(), fileQuality: z .enum(DIFF_IMAGE_QUALITY_PRESETS) .default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality) .optional(), fileScale: z.number().min(1).max(4).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileScale).optional(), fileMaxWidth: z .number() .min(640) .max(2400) .default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileMaxWidth) .optional(), imageFormat: z.enum(DIFF_OUTPUT_FORMATS).optional(), imageQuality: z.enum(DIFF_IMAGE_QUALITY_PRESETS).optional(), imageScale: z.number().min(1).max(4).optional(), imageMaxWidth: z.number().min(640).max(2400).optional(), mode: z.enum(DIFF_MODES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.mode).optional(), }) .optional(), security: z .strictObject({ allowRemoteViewer: z .boolean() .default(DEFAULT_DIFFS_PLUGIN_SECURITY.allowRemoteViewer) .optional(), }) .optional(), }); export const diffsPluginConfigSchema: OpenClawPluginConfigSchema = buildPluginConfigSchema( DiffsPluginJsonSchemaSource, { safeParse(value: unknown) { if (value === undefined) { return { success: true, data: undefined }; } try { return { success: true, data: resolveDiffsPluginDefaults(value) }; } catch (error) { return { success: false, error: { issues: [{ path: [], message: error instanceof Error ? error.message : String(error) }], }, }; } }, }, ); export function resolveDiffsPluginDefaults(config: unknown): DiffToolDefaults { if (!config || typeof config !== "object" || Array.isArray(config)) { return { ...DEFAULT_DIFFS_TOOL_DEFAULTS }; } const defaults = (config as DiffsPluginConfig).defaults; if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) { return { ...DEFAULT_DIFFS_TOOL_DEFAULTS }; } const fileQuality = normalizeFileQuality(defaults.fileQuality ?? defaults.imageQuality); const profile = DEFAULT_IMAGE_QUALITY_PROFILES[fileQuality]; return { fontFamily: normalizeFontFamily(defaults.fontFamily), fontSize: normalizeFontSize(defaults.fontSize), lineSpacing: normalizeLineSpacing(defaults.lineSpacing), layout: normalizeLayout(defaults.layout), showLineNumbers: defaults.showLineNumbers !== false, diffIndicators: normalizeDiffIndicators(defaults.diffIndicators), wordWrap: defaults.wordWrap !== false, background: defaults.background !== false, theme: normalizeTheme(defaults.theme), fileFormat: normalizeFileFormat(defaults.fileFormat ?? defaults.imageFormat ?? defaults.format), fileQuality, fileScale: normalizeFileScale(defaults.fileScale ?? defaults.imageScale, profile.scale), fileMaxWidth: normalizeFileMaxWidth( defaults.fileMaxWidth ?? defaults.imageMaxWidth, profile.maxWidth, ), mode: normalizeMode(defaults.mode), }; } export function resolveDiffsPluginSecurity(config: unknown): DiffsPluginSecurityConfig { if (!config || typeof config !== "object" || Array.isArray(config)) { return { ...DEFAULT_DIFFS_PLUGIN_SECURITY }; } const security = (config as DiffsPluginConfig).security; if (!security || typeof security !== "object" || Array.isArray(security)) { return { ...DEFAULT_DIFFS_PLUGIN_SECURITY }; } return { allowRemoteViewer: security.allowRemoteViewer === true, }; } export function toPresentationDefaults(defaults: DiffToolDefaults): DiffPresentationDefaults { const { fontFamily, fontSize, lineSpacing, layout, showLineNumbers, diffIndicators, wordWrap, background, theme, } = defaults; return { fontFamily, fontSize, lineSpacing, layout, showLineNumbers, diffIndicators, wordWrap, background, theme, }; } function normalizeFontFamily(fontFamily?: string): string { const normalized = fontFamily?.trim(); return normalized || DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily; } function normalizeFontSize(fontSize?: number): number { if (fontSize === undefined || !Number.isFinite(fontSize)) { return DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize; } const rounded = Math.floor(fontSize); return Math.min(Math.max(rounded, 10), 24); } function normalizeLineSpacing(lineSpacing?: number): number { if (lineSpacing === undefined || !Number.isFinite(lineSpacing)) { return DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing; } return Math.min(Math.max(lineSpacing, 1), 3); } function normalizeLayout(layout?: DiffLayout): DiffLayout { return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout; } function normalizeDiffIndicators(diffIndicators?: DiffIndicators): DiffIndicators { return diffIndicators && DIFF_INDICATORS.includes(diffIndicators) ? diffIndicators : DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators; } function normalizeTheme(theme?: DiffTheme): DiffTheme { return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme; } function normalizeFileFormat(fileFormat?: DiffOutputFormat): DiffOutputFormat { return fileFormat && DIFF_OUTPUT_FORMATS.includes(fileFormat) ? fileFormat : DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat; } function normalizeFileQuality(fileQuality?: DiffImageQualityPreset): DiffImageQualityPreset { return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality; } function normalizeFileScale(fileScale: number | undefined, fallback: number): number { if (fileScale === undefined || !Number.isFinite(fileScale)) { return fallback; } const rounded = Math.round(fileScale * 100) / 100; return Math.min(Math.max(rounded, 1), 4); } function normalizeFileMaxWidth(fileMaxWidth: number | undefined, fallback: number): number { if (fileMaxWidth === undefined || !Number.isFinite(fileMaxWidth)) { return fallback; } const rounded = Math.round(fileMaxWidth); return Math.min(Math.max(rounded, 640), 2400); } function normalizeMode(mode?: DiffMode): DiffMode { return mode && DIFF_MODES.includes(mode) ? mode : DEFAULT_DIFFS_TOOL_DEFAULTS.mode; } export function resolveDiffImageRenderOptions(params: { defaults: DiffFileDefaults; fileFormat?: DiffOutputFormat; format?: DiffOutputFormat; fileQuality?: DiffImageQualityPreset; fileScale?: number; fileMaxWidth?: number; imageFormat?: DiffOutputFormat; imageQuality?: DiffImageQualityPreset; imageScale?: number; imageMaxWidth?: number; }): { format: DiffOutputFormat; qualityPreset: DiffImageQualityPreset; scale: number; maxWidth: number; maxPixels: number; } { const format = normalizeFileFormat( params.fileFormat ?? params.imageFormat ?? params.format ?? params.defaults.fileFormat, ); const qualityOverrideProvided = params.fileQuality !== undefined || params.imageQuality !== undefined; const qualityPreset = normalizeFileQuality( params.fileQuality ?? params.imageQuality ?? params.defaults.fileQuality, ); const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset]; const scale = normalizeFileScale( params.fileScale ?? params.imageScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale, ); const maxWidth = normalizeFileMaxWidth( params.fileMaxWidth ?? params.imageMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth, ); return { format, qualityPreset, scale, maxWidth, maxPixels: profile.maxPixels, }; }