fix: optimize workflow prompt preview images
This commit is contained in:
parent
90e56b7dde
commit
591f462136
@ -579,6 +579,7 @@ function createWorkflowOptionDraft(
|
||||
buttonName: `选项${optionIndex + 1}`,
|
||||
description: "",
|
||||
previewImageUrl: "",
|
||||
previewImageSourceUrl: "",
|
||||
enabled: true,
|
||||
sortOrder: optionIndex,
|
||||
promptFragment: `请按选项${optionIndex + 1}执行。`,
|
||||
@ -3621,6 +3622,7 @@ export function AgentWorkflowSettingsClient({
|
||||
setUploadingKey(refKey);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "agent-workflow-preview");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
const result = await response.json();
|
||||
@ -3631,7 +3633,9 @@ export function AgentWorkflowSettingsClient({
|
||||
|
||||
updateOption(stepKey, optionKey, (option) => ({
|
||||
...option,
|
||||
previewImageUrl: result.url,
|
||||
previewImageUrl: result.previewUrl || result.thumbnailUrl || result.url,
|
||||
previewImageSourceUrl:
|
||||
result.previewImageSourceUrl || result.originalUrl || result.previewUrl || result.url,
|
||||
}));
|
||||
toast.success("预览图上传成功");
|
||||
} catch (error) {
|
||||
|
||||
@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { createAgentWorkflowPreviewImageBuffer } from "@/lib/agents/workflow-preview-images";
|
||||
import { upsertMediaAsset } from "@/lib/db/queries";
|
||||
import { isSiteAdminType } from "@/lib/middleware/admin";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
@ -83,6 +84,11 @@ async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer>
|
||||
}
|
||||
}
|
||||
|
||||
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
|
||||
const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||||
return view.slice().buffer;
|
||||
}
|
||||
|
||||
// Use Blob instead of File since File is not available in Node.js environment
|
||||
const FileSchema = z.object({
|
||||
file: z
|
||||
@ -165,6 +171,54 @@ export async function POST(request: Request) {
|
||||
console.log("[Upload API] File buffer size:", fileBuffer.byteLength);
|
||||
|
||||
try {
|
||||
if (uploadPurpose === "agent-workflow-preview" && file.type !== "image/svg+xml") {
|
||||
const preview = await createAgentWorkflowPreviewImageBuffer(Buffer.from(fileBuffer));
|
||||
const previewResult = await uploadFile(
|
||||
`${filename.replace(/\.[^.]+$/, "") || "agent-workflow-preview"}.webp`,
|
||||
toArrayBuffer(preview.buffer)
|
||||
);
|
||||
|
||||
let assetId: string | null = null;
|
||||
try {
|
||||
const asset = await upsertMediaAsset({
|
||||
userId: session.user.id,
|
||||
mediaType: "image",
|
||||
sourceType: "agent_workflow_preview_upload",
|
||||
status: "ready",
|
||||
originalUrl: previewResult.url,
|
||||
previewUrl: previewResult.url,
|
||||
thumbnailUrl: previewResult.url,
|
||||
mimeType: "image/webp",
|
||||
width: preview.width,
|
||||
height: preview.height,
|
||||
displayName: filename,
|
||||
sourceTable: "agent_workflow_preview_upload",
|
||||
metadata: {
|
||||
pathname: previewResult.pathname,
|
||||
originalSize: fileBuffer.byteLength,
|
||||
previewSize: preview.buffer.length,
|
||||
purpose: uploadPurpose,
|
||||
},
|
||||
});
|
||||
assetId = asset?.id ?? null;
|
||||
} catch (assetError) {
|
||||
console.warn("[Upload API] Failed to register workflow preview asset:", assetError);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...previewResult,
|
||||
url: previewResult.url,
|
||||
previewUrl: previewResult.url,
|
||||
thumbnailUrl: previewResult.url,
|
||||
originalUrl: previewResult.url,
|
||||
previewImageSourceUrl: previewResult.url,
|
||||
assetId,
|
||||
width: preview.width,
|
||||
height: preview.height,
|
||||
optimizedFor: uploadPurpose,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 使用Sharp优化图片(保持原格式)
|
||||
let optimizedBuffer: Buffer = Buffer.from(fileBuffer);
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
normalizeAgentWorkflowStepOrdering,
|
||||
normalizeAgentWorkflowUiConfig,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { optimizeAgentWorkflowUiConfigPreviewImages } from "@/lib/agents/workflow-preview-images";
|
||||
import {
|
||||
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
|
||||
defaultAgentWorkflowBrandModelConfig,
|
||||
@ -1340,6 +1341,7 @@ const agentWorkflowOptionSchema: z.ZodTypeAny = z.lazy(() =>
|
||||
buttonName: z.string().trim().min(1).max(64),
|
||||
description: z.string().trim().max(300).nullable().optional(),
|
||||
previewImageUrl: z.string().trim().max(1000).nullable().optional(),
|
||||
previewImageSourceUrl: z.string().trim().max(1000).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
promptFragment: z
|
||||
@ -2309,9 +2311,14 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
if (agentWorkflowUiConfig !== undefined) {
|
||||
const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig);
|
||||
const { config: optimized, summary } =
|
||||
await optimizeAgentWorkflowUiConfigPreviewImages(normalized);
|
||||
if (summary.optimized > 0 || summary.failed > 0) {
|
||||
console.log("[Site Settings API] Agent workflow preview optimization:", summary);
|
||||
}
|
||||
await persistSystemSettingWithHistory(
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
normalized,
|
||||
optimized,
|
||||
"agent-workflow"
|
||||
);
|
||||
}
|
||||
@ -2961,9 +2968,14 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
if (agentWorkflowUiConfig !== undefined) {
|
||||
const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig);
|
||||
const { config: optimized, summary } =
|
||||
await optimizeAgentWorkflowUiConfigPreviewImages(normalized);
|
||||
if (summary.optimized > 0 || summary.failed > 0) {
|
||||
console.log("[Site Settings API] Agent workflow preview optimization:", summary);
|
||||
}
|
||||
await persistSystemSettingWithHistory(
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
normalized,
|
||||
optimized,
|
||||
"agent-workflow"
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,14 +111,28 @@ const VIRTUAL_OPTION_GRID_GAP = 16;
|
||||
const VIRTUAL_OPTION_GRID_MAX_HEIGHT = 640;
|
||||
const VIRTUAL_OPTION_GRID_MIN_CARD_HEIGHT = 122;
|
||||
const VIRTUAL_OPTION_GRID_THRESHOLD = 12;
|
||||
const WORKFLOW_OPTION_PREVIEW_WIDTH = 360;
|
||||
const WORKFLOW_OPTION_PREVIEW_WIDTH = 512;
|
||||
const WORKFLOW_OPTION_PREVIEW_QUALITY = 62;
|
||||
const WORKFLOW_OPTION_PREVIEW_EAGER_COUNT = 10;
|
||||
const WORKFLOW_OPTION_PREVIEW_PREFETCH_COUNT = 12;
|
||||
|
||||
function buildWorkflowOptionPreviewUrl(url?: string | null) {
|
||||
function isLikelyStoredWorkflowPreview(url: string, sourceUrl?: string | null) {
|
||||
const source = String(sourceUrl || "").trim();
|
||||
if (source && source !== url) {
|
||||
return true;
|
||||
}
|
||||
if (source && source === url && /\.webp(?:[?#]|$)/i.test(url)) {
|
||||
return true;
|
||||
}
|
||||
return /[?&]x-oss-process=/i.test(url);
|
||||
}
|
||||
|
||||
function buildWorkflowOptionPreviewUrl(url?: string | null, sourceUrl?: string | null) {
|
||||
const source = String(url || "").trim();
|
||||
if (!source) return "";
|
||||
if (isLikelyStoredWorkflowPreview(source, sourceUrl)) {
|
||||
return source;
|
||||
}
|
||||
return buildImageProxyUrl(source, {
|
||||
width: WORKFLOW_OPTION_PREVIEW_WIDTH,
|
||||
quality: WORKFLOW_OPTION_PREVIEW_QUALITY,
|
||||
@ -291,6 +305,7 @@ const StepOptionCard = memo(function StepOptionCard({
|
||||
buttonName: string;
|
||||
description?: string | null;
|
||||
previewImageUrl?: string | null;
|
||||
previewImageSourceUrl?: string | null;
|
||||
};
|
||||
step: { title: string };
|
||||
selected: boolean;
|
||||
@ -303,8 +318,8 @@ const StepOptionCard = memo(function StepOptionCard({
|
||||
onSelect(option.key);
|
||||
}, [onSelect, option.key]);
|
||||
const previewUrl = useMemo(
|
||||
() => buildWorkflowOptionPreviewUrl(option.previewImageUrl),
|
||||
[option.previewImageUrl]
|
||||
() => buildWorkflowOptionPreviewUrl(option.previewImageUrl, option.previewImageSourceUrl),
|
||||
[option.previewImageSourceUrl, option.previewImageUrl]
|
||||
);
|
||||
const shouldLoadFast = selected || index < WORKFLOW_OPTION_PREVIEW_EAGER_COUNT;
|
||||
|
||||
@ -1133,7 +1148,9 @@ function AgentWorkflowPanelDarkV2Component({
|
||||
new Set(
|
||||
options
|
||||
.slice(0, WORKFLOW_OPTION_PREVIEW_PREFETCH_COUNT)
|
||||
.map((option) => buildWorkflowOptionPreviewUrl(option.previewImageUrl))
|
||||
.map((option) =>
|
||||
buildWorkflowOptionPreviewUrl(option.previewImageUrl, option.previewImageSourceUrl)
|
||||
)
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
306
lib/agents/workflow-preview-images.ts
Normal file
306
lib/agents/workflow-preview-images.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import "server-only";
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AgentWorkflowOptionUiConfig, AgentWorkflowUiConfig } from "@/types/agent";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
|
||||
const PREVIEW_WIDTH = 512;
|
||||
const PREVIEW_QUALITY = 68;
|
||||
const FETCH_TIMEOUT_MS = 15_000;
|
||||
const MAX_SOURCE_BYTES = 40 * 1024 * 1024;
|
||||
const DEFAULT_CONCURRENCY = 3;
|
||||
|
||||
export interface AgentWorkflowPreviewImageResult {
|
||||
url: string;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowPreviewOptimizeSummary {
|
||||
total: number;
|
||||
optimized: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
failures: Array<{ optionKey: string; url: string; error: string }>;
|
||||
}
|
||||
|
||||
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
|
||||
const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||||
return view.slice().buffer;
|
||||
}
|
||||
|
||||
function normalizeUrl(value: string | null | undefined): string {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function isAbsoluteHttpUrl(value: string): boolean {
|
||||
return /^https?:\/\//i.test(value);
|
||||
}
|
||||
|
||||
function isPrivateIpv4(hostname: string): boolean {
|
||||
const parts = hostname.split(".").map((part) => Number.parseInt(part, 10));
|
||||
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) return false;
|
||||
if (parts[0] === 10 || parts[0] === 127) return true;
|
||||
if (parts[0] === 169 && parts[1] === 254) return true;
|
||||
if (parts[0] === 192 && parts[1] === 168) return true;
|
||||
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isBlockedHost(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
if (!normalized) return true;
|
||||
if (
|
||||
normalized === "localhost" ||
|
||||
normalized === "::1" ||
|
||||
normalized.endsWith(".local") ||
|
||||
normalized.endsWith(".internal")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (isPrivateIpv4(normalized)) return true;
|
||||
if (normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUnsupportedSource(url: string): boolean {
|
||||
const lower = url.trim().toLowerCase();
|
||||
return (
|
||||
!lower ||
|
||||
lower.startsWith("data:") ||
|
||||
lower.startsWith("blob:") ||
|
||||
/\.(svg|gif|ico)(?:[?#]|$)/i.test(lower)
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyGeneratedPreview(url: string): boolean {
|
||||
try {
|
||||
const target = new URL(url);
|
||||
return /\.webp$/i.test(target.pathname);
|
||||
} catch {
|
||||
return /\.webp(?:[?#]|$)/i.test(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchImageBuffer(url: string): Promise<{ buffer: Buffer; contentType: string }> {
|
||||
let target: URL;
|
||||
try {
|
||||
target = new URL(url);
|
||||
} catch {
|
||||
throw new Error("Invalid preview image URL");
|
||||
}
|
||||
|
||||
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
||||
throw new Error("Only http/https preview images are supported");
|
||||
}
|
||||
|
||||
if (isBlockedHost(target.hostname)) {
|
||||
throw new Error("Blocked preview image host");
|
||||
}
|
||||
|
||||
const response = await fetch(target.toString(), {
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
headers: {
|
||||
Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Preview image fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
||||
if (!contentType.toLowerCase().startsWith("image/")) {
|
||||
throw new Error("Preview source is not an image");
|
||||
}
|
||||
|
||||
const contentLength = Number(response.headers.get("content-length") || 0);
|
||||
if (Number.isFinite(contentLength) && contentLength > MAX_SOURCE_BYTES) {
|
||||
throw new Error("Preview source is too large");
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
if (buffer.length > MAX_SOURCE_BYTES) {
|
||||
throw new Error("Preview source is too large");
|
||||
}
|
||||
|
||||
return { buffer, contentType };
|
||||
}
|
||||
|
||||
export async function createAgentWorkflowPreviewImageBuffer(
|
||||
input: Buffer
|
||||
): Promise<{ buffer: Buffer; width: number | null; height: number | null }> {
|
||||
const sharp = (await import("sharp")).default;
|
||||
const image = sharp(input, { failOn: "none" }).rotate();
|
||||
const resized = image.resize({
|
||||
width: PREVIEW_WIDTH,
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
const buffer = await resized
|
||||
.webp({
|
||||
quality: PREVIEW_QUALITY,
|
||||
effort: 5,
|
||||
smartSubsample: true,
|
||||
})
|
||||
.toBuffer();
|
||||
const metadata = await sharp(buffer, { failOn: "none" }).metadata();
|
||||
return {
|
||||
buffer,
|
||||
width: metadata.width ?? null,
|
||||
height: metadata.height ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAgentWorkflowPreviewImageFromUrl(
|
||||
sourceUrl: string
|
||||
): Promise<AgentWorkflowPreviewImageResult> {
|
||||
const normalizedSourceUrl = normalizeUrl(sourceUrl);
|
||||
if (!normalizedSourceUrl || isUnsupportedSource(normalizedSourceUrl)) {
|
||||
throw new Error("Unsupported preview source");
|
||||
}
|
||||
|
||||
const { buffer } = await fetchImageBuffer(normalizedSourceUrl);
|
||||
const preview = await createAgentWorkflowPreviewImageBuffer(buffer);
|
||||
const hash = createHash("sha1").update(normalizedSourceUrl).digest("hex").slice(0, 12);
|
||||
const uploaded = await uploadFile(`agent-workflow-preview-${hash}.webp`, toArrayBuffer(preview.buffer));
|
||||
|
||||
return {
|
||||
url: uploaded.url,
|
||||
width: preview.width,
|
||||
height: preview.height,
|
||||
size: preview.buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneOptionTree(options: AgentWorkflowOptionUiConfig[]): AgentWorkflowOptionUiConfig[] {
|
||||
return options.map((option) => ({
|
||||
...option,
|
||||
previewImageUrl: normalizeUrl(option.previewImageUrl),
|
||||
previewImageSourceUrl: normalizeUrl(option.previewImageSourceUrl),
|
||||
children: cloneOptionTree(option.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
function collectOptions(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
output: AgentWorkflowOptionUiConfig[]
|
||||
) {
|
||||
for (const option of options) {
|
||||
output.push(option);
|
||||
collectOptions(option.children || [], output);
|
||||
}
|
||||
}
|
||||
|
||||
function cloneStepTree(steps: AgentWorkflowUiConfig["steps"]): AgentWorkflowUiConfig["steps"] {
|
||||
return (steps || []).map((step) => ({
|
||||
...step,
|
||||
options: cloneOptionTree(step.options || []),
|
||||
children: cloneStepTree(step.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
async function runWithConcurrency<T>(
|
||||
items: T[],
|
||||
concurrency: number,
|
||||
worker: (item: T) => Promise<void>
|
||||
) {
|
||||
let cursor = 0;
|
||||
const workerCount = Math.max(1, Math.min(concurrency, items.length || 1));
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, async () => {
|
||||
for (;;) {
|
||||
const index = cursor;
|
||||
cursor += 1;
|
||||
if (index >= items.length) return;
|
||||
await worker(items[index]!);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function resolveOptimizationSource(
|
||||
option: AgentWorkflowOptionUiConfig,
|
||||
force: boolean
|
||||
): string | null {
|
||||
const previewUrl = normalizeUrl(option.previewImageUrl);
|
||||
const sourceUrl = normalizeUrl(option.previewImageSourceUrl);
|
||||
if (!previewUrl || !isAbsoluteHttpUrl(previewUrl) || isUnsupportedSource(previewUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (sourceUrl && sourceUrl !== previewUrl) {
|
||||
return null;
|
||||
}
|
||||
if (sourceUrl && sourceUrl === previewUrl && isLikelyGeneratedPreview(previewUrl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return sourceUrl || previewUrl;
|
||||
}
|
||||
|
||||
export async function optimizeAgentWorkflowUiConfigPreviewImages(
|
||||
config: AgentWorkflowUiConfig,
|
||||
options: { force?: boolean; concurrency?: number; dryRun?: boolean } = {}
|
||||
): Promise<{ config: AgentWorkflowUiConfig; summary: AgentWorkflowPreviewOptimizeSummary }> {
|
||||
const nextConfig: AgentWorkflowUiConfig = {
|
||||
...config,
|
||||
steps: cloneStepTree(config.steps || []),
|
||||
};
|
||||
|
||||
const allOptions: AgentWorkflowOptionUiConfig[] = [];
|
||||
const collectStepOptions = (steps: AgentWorkflowUiConfig["steps"]) => {
|
||||
for (const step of steps || []) {
|
||||
collectOptions(step.options || [], allOptions);
|
||||
collectStepOptions(step.children || []);
|
||||
}
|
||||
};
|
||||
collectStepOptions(nextConfig.steps);
|
||||
|
||||
const summary: AgentWorkflowPreviewOptimizeSummary = {
|
||||
total: allOptions.length,
|
||||
optimized: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
failures: [],
|
||||
};
|
||||
|
||||
await runWithConcurrency(
|
||||
allOptions,
|
||||
options.concurrency ?? DEFAULT_CONCURRENCY,
|
||||
async (option) => {
|
||||
const sourceUrl = resolveOptimizationSource(option, Boolean(options.force));
|
||||
if (!sourceUrl) {
|
||||
summary.skipped += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
summary.optimized += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const preview = await createAgentWorkflowPreviewImageFromUrl(sourceUrl);
|
||||
option.previewImageUrl = preview.url;
|
||||
option.previewImageSourceUrl = sourceUrl;
|
||||
summary.optimized += 1;
|
||||
} catch (error) {
|
||||
summary.failed += 1;
|
||||
summary.failures.push({
|
||||
optionKey: option.key,
|
||||
url: sourceUrl,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
summary.skipped = summary.total - summary.optimized - summary.failed;
|
||||
return { config: nextConfig, summary };
|
||||
}
|
||||
@ -149,6 +149,7 @@ function cloneOption(
|
||||
...option,
|
||||
description: option.description || "",
|
||||
previewImageUrl: option.previewImageUrl || "",
|
||||
previewImageSourceUrl: option.previewImageSourceUrl || "",
|
||||
promptFragment: option.promptFragment || "",
|
||||
blockedOptionKeys: Array.from(new Set((option.blockedOptionKeys || []).filter(Boolean))).slice(
|
||||
0,
|
||||
@ -205,6 +206,7 @@ function createOption(
|
||||
buttonName,
|
||||
description: "",
|
||||
previewImageUrl: "",
|
||||
previewImageSourceUrl: "",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
promptFragment,
|
||||
@ -999,6 +1001,7 @@ function createSpecialModuleRootOptionFromStep(
|
||||
buttonName: step.title || step.key,
|
||||
description: step.description || "",
|
||||
previewImageUrl: "",
|
||||
previewImageSourceUrl: "",
|
||||
enabled: step.enabled,
|
||||
sortOrder: step.sortOrder,
|
||||
promptFragment: "",
|
||||
@ -1459,6 +1462,10 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
description:
|
||||
normalizeText(optionRecord.description, 300) || defaultOption?.description || "",
|
||||
previewImageUrl: normalizeText(optionRecord.previewImageUrl, 1000),
|
||||
previewImageSourceUrl:
|
||||
normalizeText(optionRecord.previewImageSourceUrl, 1000) ||
|
||||
defaultOption?.previewImageSourceUrl ||
|
||||
"",
|
||||
enabled: normalizeBoolean(optionRecord.enabled, defaultOption?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(
|
||||
optionRecord.sortOrder,
|
||||
|
||||
86
scripts/backfill-agent-workflow-preview-images.ts
Normal file
86
scripts/backfill-agent-workflow-preview-images.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { config as loadEnv } from "dotenv";
|
||||
import { eq } from "drizzle-orm";
|
||||
import Module from "node:module";
|
||||
|
||||
loadEnv({ path: ".env.local" });
|
||||
|
||||
const moduleLoader = Module as typeof Module & {
|
||||
_load: (request: string, parent: NodeModule | null, isMain: boolean) => unknown;
|
||||
};
|
||||
const originalLoad = moduleLoader._load;
|
||||
moduleLoader._load = function patchedServerOnlyLoad(
|
||||
this: typeof moduleLoader,
|
||||
request,
|
||||
parent,
|
||||
isMain
|
||||
) {
|
||||
if (request === "server-only") {
|
||||
return {};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const force = process.argv.includes("--force");
|
||||
|
||||
async function main() {
|
||||
const dbModule = await import("../lib/db");
|
||||
const schemaModule = await import("../lib/db/schema");
|
||||
const workflowConfig = await import("../lib/agents/workflow-ui-config");
|
||||
const previewImages = await import("../lib/agents/workflow-preview-images");
|
||||
const { db } = dbModule;
|
||||
const { systemSettings } = schemaModule;
|
||||
|
||||
const [setting] = await db
|
||||
.select({ value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, workflowConfig.AGENT_WORKFLOW_UI_CONFIG_KEY))
|
||||
.limit(1);
|
||||
|
||||
if (!setting?.value) {
|
||||
console.log("No agent workflow UI config found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = workflowConfig.normalizeAgentWorkflowUiConfig(setting.value);
|
||||
const { config: optimized, summary } =
|
||||
await previewImages.optimizeAgentWorkflowUiConfigPreviewImages(normalized, {
|
||||
force,
|
||||
concurrency: 3,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
dryRun,
|
||||
force,
|
||||
summary,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.optimized > 0) {
|
||||
await db
|
||||
.update(systemSettings)
|
||||
.set({
|
||||
value: optimized,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(systemSettings.key, workflowConfig.AGENT_WORKFLOW_UI_CONFIG_KEY));
|
||||
console.log(`Updated ${summary.optimized} agent workflow preview images.`);
|
||||
} else {
|
||||
console.log("No preview image update needed.");
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -84,6 +84,7 @@ export interface AgentWorkflowOptionUiConfig {
|
||||
buttonName: string;
|
||||
description?: string | null;
|
||||
previewImageUrl?: string | null;
|
||||
previewImageSourceUrl?: string | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
promptFragment?: string | null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user