fix: harden psd text workflow compatibility
This commit is contained in:
parent
3e935fae37
commit
01748fe501
@ -63,6 +63,11 @@ import {
|
||||
defaultAgentWorkflowGoodsInfoApiConfig,
|
||||
normalizeAgentWorkflowGoodsInfoApiConfig,
|
||||
} from "@/lib/agents/workflow-goods-info-config";
|
||||
import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@ -84,6 +89,7 @@ import type {
|
||||
AgentWorkflowGoodsInfoApiConfig,
|
||||
AgentWorkflowMaterialApiConfig,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentPsdTextColorConfig,
|
||||
AgentWorkflowShowcaseConfig,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
@ -467,6 +473,7 @@ export function AgentWorkflowSettingsClient({
|
||||
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
|
||||
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
|
||||
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
||||
agentPsdTextColorConfig?: AgentPsdTextColorConfig;
|
||||
}>("/api/site-settings", fetcher);
|
||||
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
|
||||
defaultAgentPresetLibraryConfig
|
||||
@ -481,6 +488,9 @@ export function AgentWorkflowSettingsClient({
|
||||
const [showcaseConfig, setShowcaseConfig] = useState<AgentWorkflowShowcaseConfig>(
|
||||
defaultAgentWorkflowShowcaseConfig
|
||||
);
|
||||
const [psdTextColorConfig, setPsdTextColorConfig] = useState<AgentPsdTextColorConfig>(
|
||||
defaultAgentPsdTextColorConfig
|
||||
);
|
||||
const [brandModelConfig, setBrandModelConfig] = useState<AgentWorkflowBrandModelConfig>(
|
||||
defaultAgentWorkflowBrandModelConfig
|
||||
);
|
||||
@ -523,6 +533,9 @@ export function AgentWorkflowSettingsClient({
|
||||
setShowcaseConfig(
|
||||
normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig)
|
||||
);
|
||||
setPsdTextColorConfig(
|
||||
normalizeAgentPsdTextColorConfig(data.agentPsdTextColorConfig)
|
||||
);
|
||||
setBrandModelConfig(
|
||||
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
||||
);
|
||||
@ -1023,6 +1036,18 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
};
|
||||
|
||||
const updatePsdTextColor = (index: 0 | 1, value: string) => {
|
||||
const normalized = normalizeHexColor(value, psdTextColorConfig.colors[index] || "#111111");
|
||||
setPsdTextColorConfig((prev) => {
|
||||
const nextColors = [...prev.colors] as [string, string];
|
||||
nextColors[index] = normalized;
|
||||
return {
|
||||
...prev,
|
||||
colors: nextColors,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
@ -1036,6 +1061,7 @@ export function AgentWorkflowSettingsClient({
|
||||
agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig,
|
||||
agentWorkflowBrandModelConfig: brandModelConfig,
|
||||
agentWorkflowShowcaseConfig: showcaseConfig,
|
||||
agentPsdTextColorConfig: psdTextColorConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -2618,6 +2644,80 @@ export function AgentWorkflowSettingsClient({
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500/10">
|
||||
<Settings2 className="h-4 w-4 text-violet-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">PSD 文字颜色选项</CardTitle>
|
||||
<CardDescription>
|
||||
控制套用 PSD 模板时,前台是否允许用户在两种预设文字颜色之间切换。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用 PSD 文字双色切换</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后,用户在 PSD 模板编辑弹窗里可为文字图层选择两种系统预设颜色。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={psdTextColorConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setPsdTextColorConfig((prev) => ({ ...prev, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{psdTextColorConfig.colors.map((color, index) => (
|
||||
<div
|
||||
key={`psd-text-color-${index + 1}`}
|
||||
className="rounded-xl border bg-muted/20 p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">{`颜色 ${index + 1}`}</Label>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
用户前台可直接选择这组文字颜色。
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="h-10 w-10 rounded-full border shadow-sm"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(event) =>
|
||||
updatePsdTextColor(index as 0 | 1, event.target.value)
|
||||
}
|
||||
className="h-11 w-14 cursor-pointer rounded-lg border bg-transparent p-1"
|
||||
/>
|
||||
<Input
|
||||
value={color}
|
||||
onChange={(event) =>
|
||||
updatePsdTextColor(index as 0 | 1, event.target.value)
|
||||
}
|
||||
placeholder="#FFFFFF"
|
||||
className="font-mono uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@ -4,28 +4,22 @@ import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { canvasProject } from "@/lib/db/schema";
|
||||
import {
|
||||
createEmptyAgentGenerationProjectPersistedState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
extractAgentProjectStateFromCanvasData,
|
||||
isAgentProjectRow,
|
||||
} from "@/lib/agents/agent-project-compat";
|
||||
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||
const canvasData =
|
||||
row.canvasData && typeof row.canvasData === "object" && !Array.isArray(row.canvasData)
|
||||
? (row.canvasData as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.userId,
|
||||
title: row.title,
|
||||
description: row.description || null,
|
||||
thumbnail: row.thumbnail || null,
|
||||
projectState: normalizeAgentGenerationProjectPersistedState(
|
||||
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||
),
|
||||
projectState: extractAgentProjectStateFromCanvasData(row.canvasData),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
@ -46,15 +40,11 @@ export async function GET(
|
||||
.select()
|
||||
.from(canvasProject)
|
||||
.where(
|
||||
and(
|
||||
eq(canvasProject.id, id),
|
||||
eq(canvasProject.userId, session.user.id),
|
||||
eq(canvasProject.workspaceType, "agent")
|
||||
)
|
||||
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
if (!project || !isAgentProjectRow(project)) {
|
||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
@ -81,15 +71,11 @@ export async function PUT(
|
||||
.select()
|
||||
.from(canvasProject)
|
||||
.where(
|
||||
and(
|
||||
eq(canvasProject.id, id),
|
||||
eq(canvasProject.userId, session.user.id),
|
||||
eq(canvasProject.workspaceType, "agent")
|
||||
)
|
||||
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingProject) {
|
||||
if (!existingProject || !isAgentProjectRow(existingProject)) {
|
||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
@ -99,19 +85,10 @@ export async function PUT(
|
||||
const thumbnail =
|
||||
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
||||
|
||||
const existingCanvasData =
|
||||
existingProject.canvasData &&
|
||||
typeof existingProject.canvasData === "object" &&
|
||||
!Array.isArray(existingProject.canvasData)
|
||||
? (existingProject.canvasData as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const nextProjectState =
|
||||
body.projectState !== undefined
|
||||
? normalizeAgentGenerationProjectPersistedState(body.projectState)
|
||||
: normalizeAgentGenerationProjectPersistedState(
|
||||
existingCanvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||
);
|
||||
: extractAgentProjectStateFromCanvasData(existingProject.canvasData);
|
||||
|
||||
const [project] = await db
|
||||
.update(canvasProject)
|
||||
@ -119,6 +96,7 @@ export async function PUT(
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
workspaceType: "agent",
|
||||
updatedAt: new Date(),
|
||||
canvasData: {
|
||||
nodes: [],
|
||||
@ -130,11 +108,7 @@ export async function PUT(
|
||||
} as any,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(canvasProject.id, id),
|
||||
eq(canvasProject.userId, session.user.id),
|
||||
eq(canvasProject.workspaceType, "agent")
|
||||
)
|
||||
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||
)
|
||||
.returning();
|
||||
|
||||
@ -156,15 +130,19 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const [existingProject] = await db
|
||||
.select()
|
||||
.from(canvasProject)
|
||||
.where(and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingProject || !isAgentProjectRow(existingProject)) {
|
||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const [project] = await db
|
||||
.delete(canvasProject)
|
||||
.where(
|
||||
and(
|
||||
eq(canvasProject.id, id),
|
||||
eq(canvasProject.userId, session.user.id),
|
||||
eq(canvasProject.workspaceType, "agent")
|
||||
)
|
||||
)
|
||||
.where(and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id)))
|
||||
.returning();
|
||||
|
||||
if (!project) {
|
||||
|
||||
@ -1,31 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { canvasProject } from "@/lib/db/schema";
|
||||
import {
|
||||
createEmptyAgentGenerationProjectPersistedState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
extractAgentProjectStateFromCanvasData,
|
||||
isAgentProjectRow,
|
||||
} from "@/lib/agents/agent-project-compat";
|
||||
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||
const canvasData =
|
||||
row.canvasData && typeof row.canvasData === "object" && !Array.isArray(row.canvasData)
|
||||
? (row.canvasData as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.userId,
|
||||
title: row.title,
|
||||
description: row.description || null,
|
||||
thumbnail: row.thumbnail || null,
|
||||
projectState: normalizeAgentGenerationProjectPersistedState(
|
||||
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||
),
|
||||
projectState: extractAgentProjectStateFromCanvasData(row.canvasData),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
@ -41,17 +35,12 @@ export async function GET() {
|
||||
const projects = await db
|
||||
.select()
|
||||
.from(canvasProject)
|
||||
.where(
|
||||
and(
|
||||
eq(canvasProject.userId, session.user.id),
|
||||
eq(canvasProject.workspaceType, "agent")
|
||||
)
|
||||
)
|
||||
.where(eq(canvasProject.userId, session.user.id))
|
||||
.orderBy(desc(canvasProject.updatedAt));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: projects.map(mapAgentProjectRow),
|
||||
data: projects.filter(isAgentProjectRow).map(mapAgentProjectRow),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AgentProjects API] 获取项目列表失败:", error);
|
||||
|
||||
@ -17,6 +17,14 @@ const payloadSchema = z.object({
|
||||
productModel: z.string().trim().max(128).optional(),
|
||||
});
|
||||
|
||||
function isUploadLikeFile(value: FormDataEntryValue | null) {
|
||||
return (
|
||||
Boolean(value) &&
|
||||
typeof value !== "string" &&
|
||||
typeof (value as Blob).arrayBuffer === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeFilenameStem(value: string) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
@ -28,6 +36,23 @@ function sanitizeFilenameStem(value: string) {
|
||||
.slice(0, 80);
|
||||
}
|
||||
|
||||
function normalizeUploadedImageMimeType(file: Blob & { name?: string; type?: string }) {
|
||||
const type = String(file.type || "").trim().toLowerCase();
|
||||
if (type.startsWith("image/")) {
|
||||
return type;
|
||||
}
|
||||
|
||||
const fileName = String(file.name || "").trim().toLowerCase();
|
||||
if (/\.(png)$/i.test(fileName)) return "image/png";
|
||||
if (/\.(jpe?g)$/i.test(fileName)) return "image/jpeg";
|
||||
if (/\.(webp)$/i.test(fileName)) return "image/webp";
|
||||
if (/\.(gif)$/i.test(fileName)) return "image/gif";
|
||||
if (/\.(bmp)$/i.test(fileName)) return "image/bmp";
|
||||
if (/\.(heic)$/i.test(fileName)) return "image/heic";
|
||||
if (/\.(heif)$/i.test(fileName)) return "image/heif";
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
@ -36,12 +61,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
const fileEntry = formData.get("file");
|
||||
if (!isUploadLikeFile(fileEntry)) {
|
||||
return NextResponse.json({ error: "缺少图片文件" }, { status: 400 });
|
||||
}
|
||||
const file = fileEntry as Blob & { name?: string; type?: string };
|
||||
|
||||
if (!String(file.type || "").startsWith("image/")) {
|
||||
const mimeType = normalizeUploadedImageMimeType(file);
|
||||
if (!mimeType) {
|
||||
return NextResponse.json({ error: "请上传图片文件" }, { status: 400 });
|
||||
}
|
||||
|
||||
@ -56,6 +83,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const imageBuffer = Buffer.from(arrayBuffer);
|
||||
if (imageBuffer.byteLength <= 0) {
|
||||
return NextResponse.json({ error: "图片文件为空,请重新上传" }, { status: 400 });
|
||||
}
|
||||
|
||||
const promptPrefix = payload.source === "secondary" ? "搭配SKU" : "主SPU";
|
||||
const prompt = `${promptPrefix}素材抠图`;
|
||||
|
||||
@ -66,7 +97,8 @@ export async function POST(request: NextRequest) {
|
||||
skipGallerySave: true,
|
||||
});
|
||||
|
||||
const originalStem = sanitizeFilenameStem(file.name) || `workflow-upload-${Date.now()}`;
|
||||
const originalFileName = String(file.name || "").trim() || `workflow-upload-${Date.now()}.png`;
|
||||
const originalStem = sanitizeFilenameStem(originalFileName) || `workflow-upload-${Date.now()}`;
|
||||
const displayName = `${originalStem}-rmbg.png`;
|
||||
const asset = await upsertMediaAsset({
|
||||
userId: session.user.id,
|
||||
@ -88,7 +120,8 @@ export async function POST(request: NextRequest) {
|
||||
matchedSku: payload.matchedSku || null,
|
||||
matchedSpu: payload.matchedSpu || null,
|
||||
productModel: payload.productModel || null,
|
||||
originalFileName: file.name,
|
||||
originalFileName,
|
||||
originalMimeType: mimeType,
|
||||
provider: result.provider,
|
||||
},
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placemen
|
||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@ -34,6 +35,7 @@ const renderSchema = z.object({
|
||||
)
|
||||
.default({}),
|
||||
textAssignments: z.record(z.string(), z.string()).default({}),
|
||||
textColorAssignments: z.record(z.string(), z.string()).default({}),
|
||||
});
|
||||
|
||||
function jsonError(status: number, error: string) {
|
||||
@ -107,6 +109,7 @@ export async function POST(request: NextRequest) {
|
||||
const imageAssignments: Record<string, string> = {};
|
||||
const imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>> = {};
|
||||
const textAssignments: Record<string, string> = {};
|
||||
const textColorAssignments: Record<string, string> = {};
|
||||
for (const layer of editableLayers) {
|
||||
if (isAgentPsdImageLayerSlot(layer)) {
|
||||
const explicitUrl = String(body.imageAssignments[layer.id] || "").trim();
|
||||
@ -127,6 +130,10 @@ export async function POST(request: NextRequest) {
|
||||
if (nextText) {
|
||||
textAssignments[layer.id] = nextText;
|
||||
}
|
||||
textColorAssignments[layer.id] = normalizeHexColor(
|
||||
body.textColorAssignments[layer.id] || layer.textColor,
|
||||
layer.textColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,6 +153,7 @@ export async function POST(request: NextRequest) {
|
||||
imageAssignments,
|
||||
imagePlacements,
|
||||
textAssignments,
|
||||
textColorAssignments,
|
||||
fonts,
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { systemSettings } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATES_KEY,
|
||||
getEditableAgentPsdTemplateLayers,
|
||||
@ -8,6 +11,11 @@ import {
|
||||
isAgentPsdTemplateReady,
|
||||
type AgentPsdTemplateListResponse,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import {
|
||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||
@ -33,7 +41,16 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
|
||||
const allTemplates = await readAgentPsdTemplates();
|
||||
const [allTemplates, textColorSetting] = await Promise.all([
|
||||
readAgentPsdTemplates(),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, AGENT_PSD_TEXT_COLOR_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
.catch(() => null),
|
||||
]);
|
||||
const repairedTemplateMap = new Map<
|
||||
string,
|
||||
Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]
|
||||
@ -112,6 +129,9 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
templates,
|
||||
textColorConfig: textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
} satisfies AgentPsdTemplateListResponse);
|
||||
} catch (error) {
|
||||
console.error("[PSD Templates] list failed:", error);
|
||||
|
||||
@ -57,6 +57,11 @@ import {
|
||||
defaultAgentWorkflowShowcaseConfig,
|
||||
normalizeAgentWorkflowShowcaseConfig,
|
||||
} from "@/lib/agents/workflow-showcase-config";
|
||||
import {
|
||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
defaultAgentPresetLibraryConfig,
|
||||
@ -266,6 +271,7 @@ export async function GET() {
|
||||
agentWorkflowGoodsInfoApiConfigSetting,
|
||||
agentWorkflowBrandModelConfigSetting,
|
||||
agentWorkflowShowcaseConfigSetting,
|
||||
agentPsdTextColorConfigSetting,
|
||||
] = await Promise.all([
|
||||
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
||||
db
|
||||
@ -418,6 +424,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, AGENT_PSD_TEXT_COLOR_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
]);
|
||||
|
||||
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||
@ -484,6 +496,9 @@ export async function GET() {
|
||||
const agentWorkflowShowcaseConfig = normalizeAgentWorkflowShowcaseConfig(
|
||||
agentWorkflowShowcaseConfigSetting?.value
|
||||
);
|
||||
const agentPsdTextColorConfig = normalizeAgentPsdTextColorConfig(
|
||||
agentPsdTextColorConfigSetting?.value
|
||||
);
|
||||
|
||||
// 如果没有设置记录,返回默认值
|
||||
const baseSettings = settings || {
|
||||
@ -519,6 +534,7 @@ export async function GET() {
|
||||
agentWorkflowGoodsInfoApiConfig: defaultAgentWorkflowGoodsInfoApiConfig,
|
||||
agentWorkflowBrandModelConfig: defaultAgentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig: defaultAgentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
||||
realtimeCanvasEnabled: true,
|
||||
realtimeCanvasServiceEnabled: true,
|
||||
storageFailoverConfig,
|
||||
@ -569,6 +585,7 @@ export async function GET() {
|
||||
agentWorkflowGoodsInfoApiConfig,
|
||||
agentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -790,6 +807,12 @@ const updateSchema = z.object({
|
||||
footerText: z.string().max(64).optional(),
|
||||
})
|
||||
.optional(),
|
||||
agentPsdTextColorConfig: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
colors: z.array(z.string().trim().max(32)).length(2).optional(),
|
||||
})
|
||||
.optional(),
|
||||
realtimeCanvasEnabled: z.boolean().optional(),
|
||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||
canvasDefaultModelId: z.string().nullable().optional(),
|
||||
@ -877,6 +900,7 @@ export async function PUT(request: NextRequest) {
|
||||
"agentWorkflowGoodsInfoApiConfig",
|
||||
"agentWorkflowBrandModelConfig",
|
||||
"agentWorkflowShowcaseConfig",
|
||||
"agentPsdTextColorConfig",
|
||||
]);
|
||||
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
||||
if (hasRestrictedKeys) {
|
||||
@ -900,6 +924,7 @@ export async function PUT(request: NextRequest) {
|
||||
agentWorkflowGoodsInfoApiConfig,
|
||||
agentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -1292,6 +1317,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (agentPsdTextColorConfig !== undefined) {
|
||||
const normalized = normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -1490,6 +1535,10 @@ export async function PUT(request: NextRequest) {
|
||||
agentWorkflowShowcaseConfig !== undefined
|
||||
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
||||
: defaultAgentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig:
|
||||
agentPsdTextColorConfig !== undefined
|
||||
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
@ -1870,6 +1919,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (agentPsdTextColorConfig !== undefined) {
|
||||
const normalized = normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -2066,6 +2135,10 @@ export async function PUT(request: NextRequest) {
|
||||
agentWorkflowShowcaseConfig !== undefined
|
||||
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
||||
: defaultAgentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig:
|
||||
agentPsdTextColorConfig !== undefined
|
||||
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
|
||||
@ -39,6 +39,10 @@ import {
|
||||
type AgentPsdImagePlacement,
|
||||
type AgentPsdImageSize,
|
||||
} from "@/lib/agents/psd-image-placement";
|
||||
import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdTextLayerSlot,
|
||||
@ -50,7 +54,7 @@ import {
|
||||
type AgentPsdTemplateRenderResponse,
|
||||
type AgentPsdTemplateTextLayerSlot,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import { resolveTextLayout } from "@/lib/agents/psd-text-layout";
|
||||
import { normalizeHexColor, resolveTextLayout } from "@/lib/agents/psd-text-layout";
|
||||
|
||||
const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
try {
|
||||
@ -63,6 +67,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
return {
|
||||
success: false,
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
error: typeof payload?.error === "string" ? payload.error : "PSD 模板加载失败",
|
||||
};
|
||||
}
|
||||
@ -70,11 +75,13 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
templates: parseAgentPsdTemplates(payload.templates),
|
||||
textColorConfig: normalizeAgentPsdTextColorConfig(payload.textColorConfig),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
error: "PSD 模板加载失败",
|
||||
};
|
||||
}
|
||||
@ -231,6 +238,13 @@ function getResolvedTextPreview(layer: AgentPsdTemplateTextLayerSlot, value: str
|
||||
return value || layer.placeholder || layer.layerName;
|
||||
}
|
||||
|
||||
function getResolvedTextLayerColor(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
value?: string
|
||||
) {
|
||||
return normalizeHexColor(value || layer.textColor, layer.textColor);
|
||||
}
|
||||
|
||||
function getTextLayerRenderFrame(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
value: string
|
||||
@ -262,13 +276,14 @@ function getTextLayerRenderFrame(
|
||||
width,
|
||||
height,
|
||||
offsetX,
|
||||
offsetY: 0,
|
||||
offsetY: -resolvedLayout.visualOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextLayerContentStyle(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
value: string
|
||||
value: string,
|
||||
color: string
|
||||
): CSSProperties {
|
||||
const resolvedLayout = getTextLayerRenderFrame(layer, value);
|
||||
|
||||
@ -279,7 +294,7 @@ function getTextLayerContentStyle(
|
||||
fontSize: resolvedLayout.fontSize,
|
||||
lineHeight: layer.lineHeight,
|
||||
letterSpacing: `${layer.letterSpacing}px`,
|
||||
color: layer.textColor,
|
||||
color,
|
||||
fontWeight: layer.fontWeight,
|
||||
textAlign: layer.textAlign,
|
||||
paddingLeft: layer.paddingX,
|
||||
@ -335,20 +350,32 @@ function hasEditedTextValue(layer: AgentPsdTemplateTextLayerSlot, value: string)
|
||||
return String(value || "").replace(/\r\n/g, "\n") !== String(layer.defaultText || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function hasCustomizedTextLayerAppearance(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
value: string,
|
||||
color: string
|
||||
) {
|
||||
return (
|
||||
hasEditedTextValue(layer, value) ||
|
||||
getResolvedTextLayerColor(layer, color) !== getResolvedTextLayerColor(layer)
|
||||
);
|
||||
}
|
||||
|
||||
function getInlineTextEditorStyle(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
preserveSourceAppearance: boolean,
|
||||
value: string
|
||||
value: string,
|
||||
color: string
|
||||
): CSSProperties {
|
||||
const baseStyle = getTextLayerContentStyle(layer, value);
|
||||
const baseStyle = getTextLayerContentStyle(layer, value, color);
|
||||
return {
|
||||
...baseStyle,
|
||||
background: "transparent",
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
color: preserveSourceAppearance ? "transparent" : layer.textColor,
|
||||
caretColor: layer.textColor,
|
||||
WebkitTextFillColor: preserveSourceAppearance ? "transparent" : layer.textColor,
|
||||
overflow: "visible",
|
||||
color: preserveSourceAppearance ? "transparent" : color,
|
||||
caretColor: color,
|
||||
WebkitTextFillColor: preserveSourceAppearance ? "transparent" : color,
|
||||
};
|
||||
}
|
||||
|
||||
@ -379,6 +406,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
fetcher
|
||||
);
|
||||
const templates = useMemo(() => (data?.success ? data.templates : []), [data]);
|
||||
const textColorConfig = useMemo(
|
||||
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
||||
[data?.textColorConfig]
|
||||
);
|
||||
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||
@ -386,6 +417,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const [imagePlacements, setImagePlacements] = useState<Record<string, AgentPsdImagePlacement>>({});
|
||||
const [imageSizes, setImageSizes] = useState<Record<string, AgentPsdImageSize>>({});
|
||||
const [textAssignments, setTextAssignments] = useState<Record<string, string>>({});
|
||||
const [textColorAssignments, setTextColorAssignments] = useState<Record<string, string>>({});
|
||||
const [uploadingSlotId, setUploadingSlotId] = useState<string | null>(null);
|
||||
const [isRendering, setIsRendering] = useState(false);
|
||||
const [renderedResultUrl, setRenderedResultUrl] = useState<string>("");
|
||||
@ -495,6 +527,21 @@ export function AgentPsdTemplateEditorDialog({
|
||||
|
||||
return nextAssignments;
|
||||
}, [editableTextLayers, selectedTemplate, textAssignments]);
|
||||
const effectiveTextColorAssignments = useMemo(() => {
|
||||
const nextAssignments: Record<string, string> = {};
|
||||
if (!selectedTemplate) {
|
||||
return nextAssignments;
|
||||
}
|
||||
|
||||
for (const layer of editableTextLayers) {
|
||||
nextAssignments[layer.id] = getResolvedTextLayerColor(
|
||||
layer,
|
||||
textColorAssignments[layer.id]
|
||||
);
|
||||
}
|
||||
|
||||
return nextAssignments;
|
||||
}, [editableTextLayers, selectedTemplate, textColorAssignments]);
|
||||
|
||||
const effectiveImagePlacements = useMemo(() => {
|
||||
const nextPlacements: Record<string, AgentPsdImagePlacement> = {};
|
||||
@ -521,6 +568,9 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const selectedImagePlacement = selectedImageLayer
|
||||
? normalizeAgentPsdImagePlacement(effectiveImagePlacements[selectedImageLayer.id])
|
||||
: getDefaultAgentPsdImagePlacement();
|
||||
const selectedTextColor = selectedTextLayer
|
||||
? effectiveTextColorAssignments[selectedTextLayer.id] || selectedTextLayer.textColor
|
||||
: "#111111";
|
||||
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
|
||||
const selectedTemplateSelectValue = selectedTemplate?.id;
|
||||
const hasTemplateOptions = templates.length > 0;
|
||||
@ -548,6 +598,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
setImageAssignments(initialState.imageAssignments);
|
||||
setImagePlacements(initialState.imagePlacements);
|
||||
setTextAssignments(initialState.textAssignments);
|
||||
setTextColorAssignments({});
|
||||
setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate));
|
||||
|
||||
if (!selectedTemplate || typeof window === "undefined") {
|
||||
@ -872,6 +923,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
imageAssignments: effectiveImageAssignments,
|
||||
imagePlacements: effectiveImagePlacements,
|
||||
textAssignments: effectiveTextAssignments,
|
||||
textColorAssignments: effectiveTextColorAssignments,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -1113,7 +1165,12 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}
|
||||
|
||||
const value = effectiveTextAssignments[layer.id] || "";
|
||||
const shouldUseEditedPreview = hasEditedTextValue(layer, value);
|
||||
const resolvedTextColor = effectiveTextColorAssignments[layer.id] || layer.textColor;
|
||||
const shouldUseEditedPreview = hasCustomizedTextLayerAppearance(
|
||||
layer,
|
||||
value,
|
||||
resolvedTextColor
|
||||
);
|
||||
|
||||
if (!shouldUseEditedPreview && layer.sourceUrl) {
|
||||
return (
|
||||
@ -1153,7 +1210,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
>
|
||||
<div
|
||||
className="whitespace-pre"
|
||||
style={getTextLayerContentStyle(layer, value || layer.placeholder || layer.layerName)}
|
||||
style={getTextLayerContentStyle(
|
||||
layer,
|
||||
value || layer.placeholder || layer.layerName,
|
||||
resolvedTextColor
|
||||
)}
|
||||
>
|
||||
{value || layer.placeholder || layer.layerName}
|
||||
</div>
|
||||
@ -1277,8 +1338,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const isSelected = selectedLayerId === layer.id;
|
||||
const frameStyle = getLayerFrameStyle(layer, isSelected);
|
||||
const currentValue = effectiveTextAssignments[layer.id] || "";
|
||||
const resolvedTextColor =
|
||||
effectiveTextColorAssignments[layer.id] || layer.textColor;
|
||||
const preserveSourceAppearance =
|
||||
!!layer.sourceUrl && !hasEditedTextValue(layer, currentValue);
|
||||
!!layer.sourceUrl &&
|
||||
!hasCustomizedTextLayerAppearance(layer, currentValue, resolvedTextColor);
|
||||
const editedTextFrame = getTextLayerRenderFrame(layer, currentValue);
|
||||
|
||||
if (isSelected) {
|
||||
@ -1325,7 +1389,12 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}))
|
||||
}
|
||||
className="resize-none border-none bg-transparent outline-none"
|
||||
style={getInlineTextEditorStyle(layer, preserveSourceAppearance, currentValue)}
|
||||
style={getInlineTextEditorStyle(
|
||||
layer,
|
||||
preserveSourceAppearance,
|
||||
currentValue,
|
||||
resolvedTextColor
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={getTextLayerNameBadgeClassName()}>
|
||||
@ -1585,6 +1654,57 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<span>行高:{selectedTextLayer.lineHeight}</span>
|
||||
<span>字距:{selectedTextLayer.letterSpacing}px</span>
|
||||
</div>
|
||||
{textColorConfig.enabled ? (
|
||||
<div className="space-y-3 rounded-xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">文字颜色</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
当前模板允许在两种系统预设颜色之间切换。
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="h-8 w-8 rounded-full border border-white/15"
|
||||
style={{ backgroundColor: selectedTextColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{textColorConfig.colors.map((color, index) => {
|
||||
const isActive =
|
||||
getResolvedTextLayerColor(selectedTextLayer, color) ===
|
||||
getResolvedTextLayerColor(selectedTextLayer, selectedTextColor);
|
||||
return (
|
||||
<button
|
||||
key={`text-color-option-${index + 1}`}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setTextColorAssignments((current) => ({
|
||||
...current,
|
||||
[selectedTextLayer.id]: color,
|
||||
}))
|
||||
}
|
||||
className={`flex items-center gap-3 rounded-xl border px-3 py-3 text-left transition ${
|
||||
isActive
|
||||
? "border-emerald-400/55 bg-emerald-500/[0.12] text-white"
|
||||
: "border-white/10 bg-white/[0.02] text-white/80 hover:bg-white/[0.06]"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="h-6 w-6 rounded-full border border-white/20"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="text-xs font-medium">{`颜色 ${index + 1}`}</span>
|
||||
<span className="font-mono text-[11px] uppercase text-white/55">
|
||||
{color}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center text-center">
|
||||
|
||||
@ -405,7 +405,7 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok || !payload?.success || !payload?.data?.imageUrl) {
|
||||
throw new Error(payload?.error || payload?.detail || "图片上传失败");
|
||||
throw new Error(payload?.detail || payload?.error || "图片上传失败");
|
||||
}
|
||||
|
||||
const uploadedImage = payload.data as AgentWorkflowMaterialImage;
|
||||
|
||||
69
lib/agents/agent-project-compat.ts
Normal file
69
lib/agents/agent-project-compat.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import {
|
||||
createEmptyAgentGenerationProjectPersistedState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
import type { canvasProject } from "@/lib/db/schema";
|
||||
|
||||
function getCanvasDataRecord(value: unknown) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function hasLegacyWorkflowState(value: unknown) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const raw = value as Record<string, unknown>;
|
||||
return (
|
||||
"inputs" in raw ||
|
||||
"materials" in raw ||
|
||||
"selectedAgentPresetId" in raw ||
|
||||
"currentStage" in raw ||
|
||||
"agentModeState" in raw
|
||||
);
|
||||
}
|
||||
|
||||
export function isLegacyAgentProjectCanvasData(value: unknown) {
|
||||
const raw = getCanvasDataRecord(value);
|
||||
return (
|
||||
hasLegacyWorkflowState(raw.workflowState) ||
|
||||
hasLegacyWorkflowState(raw.agentProjectState) ||
|
||||
hasLegacyWorkflowState(raw.projectState) ||
|
||||
Array.isArray(raw.resultImageUrls) ||
|
||||
(raw.resultCollections && typeof raw.resultCollections === "object") ||
|
||||
(raw.deletedResultCollections && typeof raw.deletedResultCollections === "object") ||
|
||||
(raw.workspaceState && typeof raw.workspaceState === "object")
|
||||
);
|
||||
}
|
||||
|
||||
export function isAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||
return row.workspaceType === "agent" || isLegacyAgentProjectCanvasData(row.canvasData);
|
||||
}
|
||||
|
||||
export function extractAgentProjectStateFromCanvasData(value: unknown) {
|
||||
const raw = getCanvasDataRecord(value);
|
||||
const explicitStateCandidate =
|
||||
raw.agentProjectState && typeof raw.agentProjectState === "object"
|
||||
? raw.agentProjectState
|
||||
: raw.projectState && typeof raw.projectState === "object"
|
||||
? raw.projectState
|
||||
: null;
|
||||
|
||||
if (explicitStateCandidate) {
|
||||
return normalizeAgentGenerationProjectPersistedState(explicitStateCandidate);
|
||||
}
|
||||
|
||||
if (isLegacyAgentProjectCanvasData(raw)) {
|
||||
return normalizeAgentGenerationProjectPersistedState({
|
||||
workflowState: raw.workflowState,
|
||||
workspaceState: raw.workspaceState,
|
||||
resultImageUrls: raw.resultImageUrls,
|
||||
resultCollections: raw.resultCollections,
|
||||
deletedResultCollections: raw.deletedResultCollections,
|
||||
});
|
||||
}
|
||||
|
||||
return createEmptyAgentGenerationProjectPersistedState();
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||
|
||||
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
||||
|
||||
@ -98,6 +100,7 @@ export interface AgentPsdTemplate {
|
||||
export interface AgentPsdTemplateListResponse {
|
||||
success: boolean;
|
||||
templates: AgentPsdTemplate[];
|
||||
textColorConfig?: AgentPsdTextColorConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
|
||||
import { ensurePsdFontAvailableForRender } from "@/lib/agents/psd-font-runtime";
|
||||
import { normalizeAgentPsdImagePlacement, resolveAgentPsdImagePlacementFrame } from "@/lib/agents/psd-image-placement";
|
||||
import { resolveTextLayout } from "@/lib/agents/psd-text-layout";
|
||||
import { normalizeHexColor, resolveTextLayout } from "@/lib/agents/psd-text-layout";
|
||||
|
||||
async function getSharp() {
|
||||
const sharp = await import("sharp");
|
||||
@ -125,6 +125,7 @@ function resolveTextOverlayFrame(params: {
|
||||
contentWidth: number;
|
||||
contentHeight: number;
|
||||
textAlign: "left" | "center" | "right";
|
||||
topOffset?: number;
|
||||
}) {
|
||||
const width = Math.max(params.layerWidth, params.contentWidth);
|
||||
const height = Math.max(params.layerHeight, params.contentHeight);
|
||||
@ -139,7 +140,7 @@ function resolveTextOverlayFrame(params: {
|
||||
width,
|
||||
height,
|
||||
leftOffset,
|
||||
topOffset: 0,
|
||||
topOffset: Number.isFinite(Number(params.topOffset)) ? Number(params.topOffset) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -326,9 +327,17 @@ export async function renderAgentPsdTemplate(params: {
|
||||
imageAssignments: Record<string, string>;
|
||||
imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>>;
|
||||
textAssignments: Record<string, string>;
|
||||
textColorAssignments?: Record<string, string>;
|
||||
fonts: AgentPsdFont[];
|
||||
}) {
|
||||
const { template, imageAssignments, imagePlacements, textAssignments, fonts } = params;
|
||||
const {
|
||||
template,
|
||||
imageAssignments,
|
||||
imagePlacements,
|
||||
textAssignments,
|
||||
textColorAssignments = {},
|
||||
fonts,
|
||||
} = params;
|
||||
const sharp = await getSharp();
|
||||
const canvasWidth = Math.max(1, Math.round(Number(template.width || 1)));
|
||||
const canvasHeight = Math.max(1, Math.round(Number(template.height || 1)));
|
||||
@ -511,7 +520,12 @@ export async function renderAgentPsdTemplate(params: {
|
||||
if (isAgentPsdTextLayerSlot(layer)) {
|
||||
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
||||
const defaultText = String(layer.defaultText || "").trim();
|
||||
if (!text || text === defaultText) {
|
||||
const textColor = normalizeHexColor(
|
||||
textColorAssignments[layer.id] || layer.textColor,
|
||||
layer.textColor
|
||||
);
|
||||
const hasColorChange = normalizeHexColor(textColor, layer.textColor) !== normalizeHexColor(layer.textColor);
|
||||
if (!text || (text === defaultText && !hasColorChange)) {
|
||||
await appendStoredLayer(layer);
|
||||
continue;
|
||||
}
|
||||
@ -563,7 +577,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
fontWeight: layer.fontWeight,
|
||||
lineHeight: layer.lineHeight,
|
||||
letterSpacing: layer.letterSpacing,
|
||||
textColor: layer.textColor,
|
||||
textColor,
|
||||
textAlign: layer.textAlign,
|
||||
paddingX: layer.paddingX,
|
||||
paddingY: layer.paddingY,
|
||||
@ -590,6 +604,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
contentWidth: overlayLayout.contentWidth,
|
||||
contentHeight: overlayLayout.contentHeight,
|
||||
textAlign: layer.textAlign,
|
||||
topOffset: -overlayLayout.visualOffsetY,
|
||||
});
|
||||
|
||||
const didAppend = await appendBufferOverlay({
|
||||
|
||||
35
lib/agents/psd-text-color-config.ts
Normal file
35
lib/agents/psd-text-color-config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
|
||||
export const AGENT_PSD_TEXT_COLOR_CONFIG_KEY = "agent_psd_text_color_config";
|
||||
|
||||
export const defaultAgentPsdTextColorConfig: AgentPsdTextColorConfig = {
|
||||
enabled: false,
|
||||
colors: ["#ffffff", "#111111"],
|
||||
};
|
||||
|
||||
export function normalizeAgentPsdTextColorConfig(
|
||||
value: unknown
|
||||
): AgentPsdTextColorConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const colors = Array.isArray(raw.colors)
|
||||
? raw.colors
|
||||
.map((item) => normalizeHexColor(String(item || ""), ""))
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
: [];
|
||||
|
||||
const [firstColor, secondColor] = [
|
||||
colors[0] || defaultAgentPsdTextColorConfig.colors[0],
|
||||
colors[1] || colors[0] || defaultAgentPsdTextColorConfig.colors[1],
|
||||
];
|
||||
|
||||
return {
|
||||
enabled: raw.enabled === true,
|
||||
colors: [firstColor, secondColor],
|
||||
};
|
||||
}
|
||||
@ -124,6 +124,13 @@ export function resolveTextLayout(params: {
|
||||
const availableHeight = Math.max(8, height - paddingY * 2);
|
||||
const resolvedLineHeight = Math.max(1, Number(lineHeight || 1));
|
||||
const resolvedMaxLines = Math.max(1, Number(maxLines || 1));
|
||||
const visualOffsetY = Number(
|
||||
Math.max(2, Math.min(20, Math.round(Math.max(10, Number(fontSize || 10)) * 0.16))).toFixed(2)
|
||||
);
|
||||
const visualBottomPadding = Number(
|
||||
Math.max(3, Math.min(24, Math.round(Math.max(10, Number(fontSize || 10)) * 0.24))).toFixed(2)
|
||||
);
|
||||
const minContentHeight = height + visualOffsetY + visualBottomPadding;
|
||||
|
||||
let fittedFontSize = Math.max(10, Number(fontSize || 10));
|
||||
let bestLines: string[] = [];
|
||||
@ -153,8 +160,13 @@ export function resolveTextLayout(params: {
|
||||
maxLineWidth: Number(bestMaxLineWidth.toFixed(2)),
|
||||
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
||||
contentHeight: Number(
|
||||
Math.max(height, Math.max(1, bestLines.length) * bestLineHeightPx + paddingY * 2).toFixed(2)
|
||||
Math.max(
|
||||
minContentHeight,
|
||||
Math.max(1, bestLines.length) * bestLineHeightPx + paddingY * 2 + visualOffsetY + visualBottomPadding
|
||||
).toFixed(2)
|
||||
),
|
||||
visualOffsetY,
|
||||
visualBottomPadding,
|
||||
};
|
||||
}
|
||||
|
||||
@ -202,8 +214,13 @@ export function resolveTextLayout(params: {
|
||||
maxLineWidth: Number(bestMaxLineWidth.toFixed(2)),
|
||||
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
||||
contentHeight: Number(
|
||||
Math.max(height, Math.max(1, bestLines.length) * bestLineHeightPx + paddingY * 2).toFixed(2)
|
||||
Math.max(
|
||||
minContentHeight,
|
||||
Math.max(1, bestLines.length) * bestLineHeightPx + paddingY * 2 + visualOffsetY + visualBottomPadding
|
||||
).toFixed(2)
|
||||
),
|
||||
visualOffsetY,
|
||||
visualBottomPadding,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -411,6 +411,11 @@ export interface AgentGenerationProject {
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface AgentPsdTextColorConfig {
|
||||
enabled: boolean;
|
||||
colors: [string, string];
|
||||
}
|
||||
|
||||
export interface AgentCategoryTreeConfig {
|
||||
productCategories: string[];
|
||||
audienceTags: string[];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user