import fs from "node:fs/promises"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { Static, Type } from "typebox"; import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js"; import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js"; import { resolveDiffImageRenderOptions } from "./config.js"; import { renderDiffDocument } from "./render.js"; import type { DiffArtifactStore } from "./store.js"; import type {
DiffArtifactContext,
DiffRenderOptions,
DiffRenderTarget,
DiffToolDefaults,
} from "./types.js"; import {
DIFF_IMAGE_QUALITY_PRESETS,
DIFF_LAYOUTS,
DIFF_MODES,
DIFF_OUTPUT_FORMATS,
DIFF_THEMES,
type DiffInput,
type DiffImageQualityPreset,
type DiffLayout,
type DiffMode,
type DiffOutputFormat,
type DiffTheme,
} from "./types.js"; import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
const DiffsToolSchema = Type.Object(
{
before: Type.Optional(Type.String({ description: "Original text content." })),
after: Type.Optional(Type.String({ description: "Updated text content." })),
patch: Type.Optional(
Type.String({
description: "Unified diff or patch text.",
maxLength: MAX_PATCH_BYTES,
}),
),
path: Type.Optional(
Type.String({
description: "Display path for before/after input.",
maxLength: MAX_PATH_BYTES,
}),
),
lang: Type.Optional(
Type.String({
description: "Optional language override for before/after input.",
maxLength: MAX_LANG_BYTES,
}),
),
title: Type.Optional(
Type.String({
description: "Optional title for the rendered diff.",
maxLength: MAX_TITLE_BYTES,
}),
),
mode: Type.Optional(
stringEnum(
DIFF_MODES, "Output mode: view, file, image (deprecated alias for file), or both. Default: both.",
),
),
theme: Type.Optional(stringEnum(DIFF_THEMES, "Viewer theme. Default: dark.")),
layout: Type.Optional(stringEnum(DIFF_LAYOUTS, "Diff layout. Default: unified.")),
fileQuality: Type.Optional(
stringEnum(DIFF_IMAGE_QUALITY_PRESETS, "File quality preset: standard, hq, or print."),
),
fileFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, "Rendered file format: png or pdf.")),
fileScale: Type.Optional(
Type.Number({
description: "Optional rendered-file device scale factor override (1-4).",
minimum: 1,
maximum: 4,
}),
),
fileMaxWidth: Type.Optional(
Type.Number({
description: "Optional rendered-file max width in CSS pixels (640-2400).",
minimum: 640,
maximum: 2400,
}),
),
imageQuality: Type.Optional(
stringEnum(DIFF_IMAGE_QUALITY_PRESETS, "Deprecated alias for fileQuality."),
),
imageFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, "Deprecated alias for fileFormat.")),
imageScale: Type.Optional(
Type.Number({
description: "Deprecated alias for fileScale.",
minimum: 1,
maximum: 4,
}),
),
imageMaxWidth: Type.Optional(
Type.Number({
description: "Deprecated alias for fileMaxWidth.",
minimum: 640,
maximum: 2400,
}),
),
expandUnchanged: Type.Optional(
Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." }),
),
ttlSeconds: Type.Optional(
Type.Number({
description: "Artifact lifetime in seconds. Default: 1800. Maximum: 21600.",
minimum: 1,
maximum: 21_600,
}),
),
baseUrl: Type.Optional(
Type.String({
description: "Optional gateway base URL override used when building the viewer URL. Overrides configured viewerBaseUrl, for example https://gateway.example.com.",
}),
),
},
{ additionalProperties: false },
);
type DiffsToolParams = Static<typeof DiffsToolSchema>;
type DiffsToolRawParams = DiffsToolParams & { // Keep backward compatibility for direct calls that still pass `format`.
format?: DiffOutputFormat;
};
export function createDiffsTool(params: {
api: OpenClawPluginApi;
store: DiffArtifactStore;
defaults: DiffToolDefaults;
viewerBaseUrl?: string;
screenshotter?: DiffScreenshotter;
context?: OpenClawPluginToolContext;
}): AnyAgentTool { return {
name: "diffs",
label: "Diffs",
description: "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for canvas use and can also render the same diff to a PNG or PDF.",
parameters: DiffsToolSchema,
execute: async (_toolCallId, rawParams) => { const toolParams = rawParams as DiffsToolRawParams; const artifactContext = buildArtifactContext(params.context); const input = normalizeDiffInput(toolParams); const mode = normalizeMode(toolParams.mode, params.defaults.mode); const theme = normalizeTheme(toolParams.theme, params.defaults.theme); const layout = normalizeLayout(toolParams.layout, params.defaults.layout); const expandUnchanged = toolParams.expandUnchanged === true; const ttlMs = normalizeTtlMs(toolParams.ttlSeconds); const image = resolveDiffImageRenderOptions({
defaults: params.defaults,
fileFormat: normalizeOutputFormat(
toolParams.fileFormat ?? toolParams.imageFormat ?? toolParams.format,
),
fileQuality: normalizeFileQuality(toolParams.fileQuality ?? toolParams.imageQuality),
fileScale: toolParams.fileScale ?? toolParams.imageScale,
fileMaxWidth: toolParams.fileMaxWidth ?? toolParams.imageMaxWidth,
}); const renderTarget = resolveRenderTarget(mode);
function normalizeDiffInput(params: DiffsToolParams): DiffInput { const patch = params.patch?.trim(); const before = params.before; const after = params.after;
if (patch) {
assertMaxBytes(patch, "patch", MAX_PATCH_BYTES); if (before !== undefined || after !== undefined) { thrownew PluginToolInputError("Provide either patch or before/after input, not both.");
} const title = params.title?.trim(); if (title) {
assertMaxBytes(title, "title", MAX_TITLE_BYTES);
} return {
kind: "patch",
patch,
title,
};
}
if (before === undefined || after === undefined) { thrownew PluginToolInputError("Provide patch or both before and after text.");
}
assertMaxBytes(before, "before", MAX_BEFORE_AFTER_BYTES);
assertMaxBytes(after, "after", MAX_BEFORE_AFTER_BYTES); const path = normalizeOptionalString(params.path); const lang = normalizeOptionalString(params.lang); const title = normalizeOptionalString(params.title); if (path) {
assertMaxBytes(path, "path", MAX_PATH_BYTES);
} if (lang) {
assertMaxBytes(lang, "lang", MAX_LANG_BYTES);
} if (title) {
assertMaxBytes(title, "title", MAX_TITLE_BYTES);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.