fix: harden psd text workflow compatibility
This commit is contained in:
parent
3e935fae37
commit
01748fe501
@ -63,6 +63,11 @@ import {
|
|||||||
defaultAgentWorkflowGoodsInfoApiConfig,
|
defaultAgentWorkflowGoodsInfoApiConfig,
|
||||||
normalizeAgentWorkflowGoodsInfoApiConfig,
|
normalizeAgentWorkflowGoodsInfoApiConfig,
|
||||||
} from "@/lib/agents/workflow-goods-info-config";
|
} 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 { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@ -84,6 +89,7 @@ import type {
|
|||||||
AgentWorkflowGoodsInfoApiConfig,
|
AgentWorkflowGoodsInfoApiConfig,
|
||||||
AgentWorkflowMaterialApiConfig,
|
AgentWorkflowMaterialApiConfig,
|
||||||
AgentWorkflowOptionUiConfig,
|
AgentWorkflowOptionUiConfig,
|
||||||
|
AgentPsdTextColorConfig,
|
||||||
AgentWorkflowShowcaseConfig,
|
AgentWorkflowShowcaseConfig,
|
||||||
AgentWorkflowStepUiConfig,
|
AgentWorkflowStepUiConfig,
|
||||||
AgentWorkflowUiConfig,
|
AgentWorkflowUiConfig,
|
||||||
@ -467,6 +473,7 @@ export function AgentWorkflowSettingsClient({
|
|||||||
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
|
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
|
||||||
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
|
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
|
||||||
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
||||||
|
agentPsdTextColorConfig?: AgentPsdTextColorConfig;
|
||||||
}>("/api/site-settings", fetcher);
|
}>("/api/site-settings", fetcher);
|
||||||
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
|
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
|
||||||
defaultAgentPresetLibraryConfig
|
defaultAgentPresetLibraryConfig
|
||||||
@ -481,6 +488,9 @@ export function AgentWorkflowSettingsClient({
|
|||||||
const [showcaseConfig, setShowcaseConfig] = useState<AgentWorkflowShowcaseConfig>(
|
const [showcaseConfig, setShowcaseConfig] = useState<AgentWorkflowShowcaseConfig>(
|
||||||
defaultAgentWorkflowShowcaseConfig
|
defaultAgentWorkflowShowcaseConfig
|
||||||
);
|
);
|
||||||
|
const [psdTextColorConfig, setPsdTextColorConfig] = useState<AgentPsdTextColorConfig>(
|
||||||
|
defaultAgentPsdTextColorConfig
|
||||||
|
);
|
||||||
const [brandModelConfig, setBrandModelConfig] = useState<AgentWorkflowBrandModelConfig>(
|
const [brandModelConfig, setBrandModelConfig] = useState<AgentWorkflowBrandModelConfig>(
|
||||||
defaultAgentWorkflowBrandModelConfig
|
defaultAgentWorkflowBrandModelConfig
|
||||||
);
|
);
|
||||||
@ -523,6 +533,9 @@ export function AgentWorkflowSettingsClient({
|
|||||||
setShowcaseConfig(
|
setShowcaseConfig(
|
||||||
normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig)
|
normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig)
|
||||||
);
|
);
|
||||||
|
setPsdTextColorConfig(
|
||||||
|
normalizeAgentPsdTextColorConfig(data.agentPsdTextColorConfig)
|
||||||
|
);
|
||||||
setBrandModelConfig(
|
setBrandModelConfig(
|
||||||
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
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 () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
@ -1036,6 +1061,7 @@ export function AgentWorkflowSettingsClient({
|
|||||||
agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig,
|
agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig,
|
||||||
agentWorkflowBrandModelConfig: brandModelConfig,
|
agentWorkflowBrandModelConfig: brandModelConfig,
|
||||||
agentWorkflowShowcaseConfig: showcaseConfig,
|
agentWorkflowShowcaseConfig: showcaseConfig,
|
||||||
|
agentPsdTextColorConfig: psdTextColorConfig,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2618,6 +2644,80 @@ export function AgentWorkflowSettingsClient({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</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>
|
</motion.div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@ -4,28 +4,22 @@ import { auth } from "@/app/(auth)/auth";
|
|||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { canvasProject } from "@/lib/db/schema";
|
import { canvasProject } from "@/lib/db/schema";
|
||||||
import {
|
import {
|
||||||
createEmptyAgentGenerationProjectPersistedState,
|
extractAgentProjectStateFromCanvasData,
|
||||||
normalizeAgentGenerationProjectPersistedState,
|
isAgentProjectRow,
|
||||||
} from "@/lib/agents/generation-project-state";
|
} from "@/lib/agents/agent-project-compat";
|
||||||
|
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
userId: row.userId,
|
userId: row.userId,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description || null,
|
description: row.description || null,
|
||||||
thumbnail: row.thumbnail || null,
|
thumbnail: row.thumbnail || null,
|
||||||
projectState: normalizeAgentGenerationProjectPersistedState(
|
projectState: extractAgentProjectStateFromCanvasData(row.canvasData),
|
||||||
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
|
||||||
),
|
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
};
|
};
|
||||||
@ -46,15 +40,11 @@ export async function GET(
|
|||||||
.select()
|
.select()
|
||||||
.from(canvasProject)
|
.from(canvasProject)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||||
eq(canvasProject.id, id),
|
|
||||||
eq(canvasProject.userId, session.user.id),
|
|
||||||
eq(canvasProject.workspaceType, "agent")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!project) {
|
if (!project || !isAgentProjectRow(project)) {
|
||||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,15 +71,11 @@ export async function PUT(
|
|||||||
.select()
|
.select()
|
||||||
.from(canvasProject)
|
.from(canvasProject)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||||
eq(canvasProject.id, id),
|
|
||||||
eq(canvasProject.userId, session.user.id),
|
|
||||||
eq(canvasProject.workspaceType, "agent")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!existingProject) {
|
if (!existingProject || !isAgentProjectRow(existingProject)) {
|
||||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,19 +85,10 @@ export async function PUT(
|
|||||||
const thumbnail =
|
const thumbnail =
|
||||||
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
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 =
|
const nextProjectState =
|
||||||
body.projectState !== undefined
|
body.projectState !== undefined
|
||||||
? normalizeAgentGenerationProjectPersistedState(body.projectState)
|
? normalizeAgentGenerationProjectPersistedState(body.projectState)
|
||||||
: normalizeAgentGenerationProjectPersistedState(
|
: extractAgentProjectStateFromCanvasData(existingProject.canvasData);
|
||||||
existingCanvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
|
||||||
);
|
|
||||||
|
|
||||||
const [project] = await db
|
const [project] = await db
|
||||||
.update(canvasProject)
|
.update(canvasProject)
|
||||||
@ -119,6 +96,7 @@ export async function PUT(
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
workspaceType: "agent",
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
canvasData: {
|
canvasData: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@ -130,11 +108,7 @@ export async function PUT(
|
|||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
||||||
eq(canvasProject.id, id),
|
|
||||||
eq(canvasProject.userId, session.user.id),
|
|
||||||
eq(canvasProject.workspaceType, "agent")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@ -156,15 +130,19 @@ export async function DELETE(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
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
|
const [project] = await db
|
||||||
.delete(canvasProject)
|
.delete(canvasProject)
|
||||||
.where(
|
.where(and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id)))
|
||||||
and(
|
|
||||||
eq(canvasProject.id, id),
|
|
||||||
eq(canvasProject.userId, session.user.id),
|
|
||||||
eq(canvasProject.workspaceType, "agent")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|||||||
@ -1,31 +1,25 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
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 { auth } from "@/app/(auth)/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { canvasProject } from "@/lib/db/schema";
|
import { canvasProject } from "@/lib/db/schema";
|
||||||
import {
|
import {
|
||||||
createEmptyAgentGenerationProjectPersistedState,
|
extractAgentProjectStateFromCanvasData,
|
||||||
normalizeAgentGenerationProjectPersistedState,
|
isAgentProjectRow,
|
||||||
} from "@/lib/agents/generation-project-state";
|
} from "@/lib/agents/agent-project-compat";
|
||||||
|
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
userId: row.userId,
|
userId: row.userId,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description || null,
|
description: row.description || null,
|
||||||
thumbnail: row.thumbnail || null,
|
thumbnail: row.thumbnail || null,
|
||||||
projectState: normalizeAgentGenerationProjectPersistedState(
|
projectState: extractAgentProjectStateFromCanvasData(row.canvasData),
|
||||||
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
|
||||||
),
|
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
};
|
};
|
||||||
@ -41,17 +35,12 @@ export async function GET() {
|
|||||||
const projects = await db
|
const projects = await db
|
||||||
.select()
|
.select()
|
||||||
.from(canvasProject)
|
.from(canvasProject)
|
||||||
.where(
|
.where(eq(canvasProject.userId, session.user.id))
|
||||||
and(
|
|
||||||
eq(canvasProject.userId, session.user.id),
|
|
||||||
eq(canvasProject.workspaceType, "agent")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(desc(canvasProject.updatedAt));
|
.orderBy(desc(canvasProject.updatedAt));
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: projects.map(mapAgentProjectRow),
|
data: projects.filter(isAgentProjectRow).map(mapAgentProjectRow),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentProjects API] 获取项目列表失败:", error);
|
console.error("[AgentProjects API] 获取项目列表失败:", error);
|
||||||
|
|||||||
@ -17,6 +17,14 @@ const payloadSchema = z.object({
|
|||||||
productModel: z.string().trim().max(128).optional(),
|
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) {
|
function sanitizeFilenameStem(value: string) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.trim()
|
.trim()
|
||||||
@ -28,6 +36,23 @@ function sanitizeFilenameStem(value: string) {
|
|||||||
.slice(0, 80);
|
.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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -36,12 +61,14 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const file = formData.get("file");
|
const fileEntry = formData.get("file");
|
||||||
if (!(file instanceof File)) {
|
if (!isUploadLikeFile(fileEntry)) {
|
||||||
return NextResponse.json({ error: "缺少图片文件" }, { status: 400 });
|
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 });
|
return NextResponse.json({ error: "请上传图片文件" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +83,10 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const imageBuffer = Buffer.from(arrayBuffer);
|
const imageBuffer = Buffer.from(arrayBuffer);
|
||||||
|
if (imageBuffer.byteLength <= 0) {
|
||||||
|
return NextResponse.json({ error: "图片文件为空,请重新上传" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const promptPrefix = payload.source === "secondary" ? "搭配SKU" : "主SPU";
|
const promptPrefix = payload.source === "secondary" ? "搭配SKU" : "主SPU";
|
||||||
const prompt = `${promptPrefix}素材抠图`;
|
const prompt = `${promptPrefix}素材抠图`;
|
||||||
|
|
||||||
@ -66,7 +97,8 @@ export async function POST(request: NextRequest) {
|
|||||||
skipGallerySave: true,
|
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 displayName = `${originalStem}-rmbg.png`;
|
||||||
const asset = await upsertMediaAsset({
|
const asset = await upsertMediaAsset({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
@ -88,7 +120,8 @@ export async function POST(request: NextRequest) {
|
|||||||
matchedSku: payload.matchedSku || null,
|
matchedSku: payload.matchedSku || null,
|
||||||
matchedSpu: payload.matchedSpu || null,
|
matchedSpu: payload.matchedSpu || null,
|
||||||
productModel: payload.productModel || null,
|
productModel: payload.productModel || null,
|
||||||
originalFileName: file.name,
|
originalFileName,
|
||||||
|
originalMimeType: mimeType,
|
||||||
provider: result.provider,
|
provider: result.provider,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placemen
|
|||||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
||||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
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";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@ -34,6 +35,7 @@ const renderSchema = z.object({
|
|||||||
)
|
)
|
||||||
.default({}),
|
.default({}),
|
||||||
textAssignments: z.record(z.string(), z.string()).default({}),
|
textAssignments: z.record(z.string(), z.string()).default({}),
|
||||||
|
textColorAssignments: z.record(z.string(), z.string()).default({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function jsonError(status: number, error: string) {
|
function jsonError(status: number, error: string) {
|
||||||
@ -107,6 +109,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const imageAssignments: Record<string, string> = {};
|
const imageAssignments: Record<string, string> = {};
|
||||||
const imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>> = {};
|
const imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>> = {};
|
||||||
const textAssignments: Record<string, string> = {};
|
const textAssignments: Record<string, string> = {};
|
||||||
|
const textColorAssignments: Record<string, string> = {};
|
||||||
for (const layer of editableLayers) {
|
for (const layer of editableLayers) {
|
||||||
if (isAgentPsdImageLayerSlot(layer)) {
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
const explicitUrl = String(body.imageAssignments[layer.id] || "").trim();
|
const explicitUrl = String(body.imageAssignments[layer.id] || "").trim();
|
||||||
@ -127,6 +130,10 @@ export async function POST(request: NextRequest) {
|
|||||||
if (nextText) {
|
if (nextText) {
|
||||||
textAssignments[layer.id] = 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,
|
imageAssignments,
|
||||||
imagePlacements,
|
imagePlacements,
|
||||||
textAssignments,
|
textAssignments,
|
||||||
|
textColorAssignments,
|
||||||
fonts,
|
fonts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { upsertSystemSetting } from "@/lib/db/queries";
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { systemSettings } from "@/lib/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
getEditableAgentPsdTemplateLayers,
|
getEditableAgentPsdTemplateLayers,
|
||||||
@ -8,6 +11,11 @@ import {
|
|||||||
isAgentPsdTemplateReady,
|
isAgentPsdTemplateReady,
|
||||||
type AgentPsdTemplateListResponse,
|
type AgentPsdTemplateListResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} 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 { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||||
@ -33,7 +41,16 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
|
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<
|
const repairedTemplateMap = new Map<
|
||||||
string,
|
string,
|
||||||
Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]
|
Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]
|
||||||
@ -112,6 +129,9 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
templates,
|
templates,
|
||||||
|
textColorConfig: textColorSetting
|
||||||
|
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||||
|
: defaultAgentPsdTextColorConfig,
|
||||||
} satisfies AgentPsdTemplateListResponse);
|
} satisfies AgentPsdTemplateListResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[PSD Templates] list failed:", error);
|
console.error("[PSD Templates] list failed:", error);
|
||||||
|
|||||||
@ -57,6 +57,11 @@ import {
|
|||||||
defaultAgentWorkflowShowcaseConfig,
|
defaultAgentWorkflowShowcaseConfig,
|
||||||
normalizeAgentWorkflowShowcaseConfig,
|
normalizeAgentWorkflowShowcaseConfig,
|
||||||
} from "@/lib/agents/workflow-showcase-config";
|
} from "@/lib/agents/workflow-showcase-config";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||||
|
defaultAgentPsdTextColorConfig,
|
||||||
|
normalizeAgentPsdTextColorConfig,
|
||||||
|
} from "@/lib/agents/psd-text-color-config";
|
||||||
import {
|
import {
|
||||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||||
defaultAgentPresetLibraryConfig,
|
defaultAgentPresetLibraryConfig,
|
||||||
@ -266,6 +271,7 @@ export async function GET() {
|
|||||||
agentWorkflowGoodsInfoApiConfigSetting,
|
agentWorkflowGoodsInfoApiConfigSetting,
|
||||||
agentWorkflowBrandModelConfigSetting,
|
agentWorkflowBrandModelConfigSetting,
|
||||||
agentWorkflowShowcaseConfigSetting,
|
agentWorkflowShowcaseConfigSetting,
|
||||||
|
agentPsdTextColorConfigSetting,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
||||||
db
|
db
|
||||||
@ -418,6 +424,12 @@ export async function GET() {
|
|||||||
.where(eq(systemSettings.key, AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY))
|
.where(eq(systemSettings.key, AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then(rows => rows[0]),
|
.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: "" };
|
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||||
@ -484,6 +496,9 @@ export async function GET() {
|
|||||||
const agentWorkflowShowcaseConfig = normalizeAgentWorkflowShowcaseConfig(
|
const agentWorkflowShowcaseConfig = normalizeAgentWorkflowShowcaseConfig(
|
||||||
agentWorkflowShowcaseConfigSetting?.value
|
agentWorkflowShowcaseConfigSetting?.value
|
||||||
);
|
);
|
||||||
|
const agentPsdTextColorConfig = normalizeAgentPsdTextColorConfig(
|
||||||
|
agentPsdTextColorConfigSetting?.value
|
||||||
|
);
|
||||||
|
|
||||||
// 如果没有设置记录,返回默认值
|
// 如果没有设置记录,返回默认值
|
||||||
const baseSettings = settings || {
|
const baseSettings = settings || {
|
||||||
@ -519,6 +534,7 @@ export async function GET() {
|
|||||||
agentWorkflowGoodsInfoApiConfig: defaultAgentWorkflowGoodsInfoApiConfig,
|
agentWorkflowGoodsInfoApiConfig: defaultAgentWorkflowGoodsInfoApiConfig,
|
||||||
agentWorkflowBrandModelConfig: defaultAgentWorkflowBrandModelConfig,
|
agentWorkflowBrandModelConfig: defaultAgentWorkflowBrandModelConfig,
|
||||||
agentWorkflowShowcaseConfig: defaultAgentWorkflowShowcaseConfig,
|
agentWorkflowShowcaseConfig: defaultAgentWorkflowShowcaseConfig,
|
||||||
|
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
realtimeCanvasEnabled: true,
|
realtimeCanvasEnabled: true,
|
||||||
realtimeCanvasServiceEnabled: true,
|
realtimeCanvasServiceEnabled: true,
|
||||||
storageFailoverConfig,
|
storageFailoverConfig,
|
||||||
@ -569,6 +585,7 @@ export async function GET() {
|
|||||||
agentWorkflowGoodsInfoApiConfig,
|
agentWorkflowGoodsInfoApiConfig,
|
||||||
agentWorkflowBrandModelConfig,
|
agentWorkflowBrandModelConfig,
|
||||||
agentWorkflowShowcaseConfig,
|
agentWorkflowShowcaseConfig,
|
||||||
|
agentPsdTextColorConfig,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
pointsSystemConfig,
|
pointsSystemConfig,
|
||||||
@ -790,6 +807,12 @@ const updateSchema = z.object({
|
|||||||
footerText: z.string().max(64).optional(),
|
footerText: z.string().max(64).optional(),
|
||||||
})
|
})
|
||||||
.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(),
|
realtimeCanvasEnabled: z.boolean().optional(),
|
||||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||||
canvasDefaultModelId: z.string().nullable().optional(),
|
canvasDefaultModelId: z.string().nullable().optional(),
|
||||||
@ -877,6 +900,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
"agentWorkflowGoodsInfoApiConfig",
|
"agentWorkflowGoodsInfoApiConfig",
|
||||||
"agentWorkflowBrandModelConfig",
|
"agentWorkflowBrandModelConfig",
|
||||||
"agentWorkflowShowcaseConfig",
|
"agentWorkflowShowcaseConfig",
|
||||||
|
"agentPsdTextColorConfig",
|
||||||
]);
|
]);
|
||||||
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
||||||
if (hasRestrictedKeys) {
|
if (hasRestrictedKeys) {
|
||||||
@ -900,6 +924,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentWorkflowGoodsInfoApiConfig,
|
agentWorkflowGoodsInfoApiConfig,
|
||||||
agentWorkflowBrandModelConfig,
|
agentWorkflowBrandModelConfig,
|
||||||
agentWorkflowShowcaseConfig,
|
agentWorkflowShowcaseConfig,
|
||||||
|
agentPsdTextColorConfig,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
pointsSystemConfig,
|
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) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -1490,6 +1535,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentWorkflowShowcaseConfig !== undefined
|
agentWorkflowShowcaseConfig !== undefined
|
||||||
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
||||||
: defaultAgentWorkflowShowcaseConfig,
|
: defaultAgentWorkflowShowcaseConfig,
|
||||||
|
agentPsdTextColorConfig:
|
||||||
|
agentPsdTextColorConfig !== undefined
|
||||||
|
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||||
|
: defaultAgentPsdTextColorConfig,
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? 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) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -2066,6 +2135,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentWorkflowShowcaseConfig !== undefined
|
agentWorkflowShowcaseConfig !== undefined
|
||||||
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
? normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig)
|
||||||
: defaultAgentWorkflowShowcaseConfig,
|
: defaultAgentWorkflowShowcaseConfig,
|
||||||
|
agentPsdTextColorConfig:
|
||||||
|
agentPsdTextColorConfig !== undefined
|
||||||
|
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||||
|
: defaultAgentPsdTextColorConfig,
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? normalizeMembershipConfig(membershipConfig)
|
||||||
|
|||||||
@ -39,6 +39,10 @@ import {
|
|||||||
type AgentPsdImagePlacement,
|
type AgentPsdImagePlacement,
|
||||||
type AgentPsdImageSize,
|
type AgentPsdImageSize,
|
||||||
} from "@/lib/agents/psd-image-placement";
|
} from "@/lib/agents/psd-image-placement";
|
||||||
|
import {
|
||||||
|
defaultAgentPsdTextColorConfig,
|
||||||
|
normalizeAgentPsdTextColorConfig,
|
||||||
|
} from "@/lib/agents/psd-text-color-config";
|
||||||
import {
|
import {
|
||||||
isAgentPsdImageLayerSlot,
|
isAgentPsdImageLayerSlot,
|
||||||
isAgentPsdTextLayerSlot,
|
isAgentPsdTextLayerSlot,
|
||||||
@ -50,7 +54,7 @@ import {
|
|||||||
type AgentPsdTemplateRenderResponse,
|
type AgentPsdTemplateRenderResponse,
|
||||||
type AgentPsdTemplateTextLayerSlot,
|
type AgentPsdTemplateTextLayerSlot,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} 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> => {
|
const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||||
try {
|
try {
|
||||||
@ -63,6 +67,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
templates: [],
|
templates: [],
|
||||||
|
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
error: typeof payload?.error === "string" ? payload.error : "PSD 模板加载失败",
|
error: typeof payload?.error === "string" ? payload.error : "PSD 模板加载失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -70,11 +75,13 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
templates: parseAgentPsdTemplates(payload.templates),
|
templates: parseAgentPsdTemplates(payload.templates),
|
||||||
|
textColorConfig: normalizeAgentPsdTextColorConfig(payload.textColorConfig),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
templates: [],
|
templates: [],
|
||||||
|
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
error: "PSD 模板加载失败",
|
error: "PSD 模板加载失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -231,6 +238,13 @@ function getResolvedTextPreview(layer: AgentPsdTemplateTextLayerSlot, value: str
|
|||||||
return value || layer.placeholder || layer.layerName;
|
return value || layer.placeholder || layer.layerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResolvedTextLayerColor(
|
||||||
|
layer: AgentPsdTemplateTextLayerSlot,
|
||||||
|
value?: string
|
||||||
|
) {
|
||||||
|
return normalizeHexColor(value || layer.textColor, layer.textColor);
|
||||||
|
}
|
||||||
|
|
||||||
function getTextLayerRenderFrame(
|
function getTextLayerRenderFrame(
|
||||||
layer: AgentPsdTemplateTextLayerSlot,
|
layer: AgentPsdTemplateTextLayerSlot,
|
||||||
value: string
|
value: string
|
||||||
@ -262,13 +276,14 @@ function getTextLayerRenderFrame(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY: 0,
|
offsetY: -resolvedLayout.visualOffsetY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextLayerContentStyle(
|
function getTextLayerContentStyle(
|
||||||
layer: AgentPsdTemplateTextLayerSlot,
|
layer: AgentPsdTemplateTextLayerSlot,
|
||||||
value: string
|
value: string,
|
||||||
|
color: string
|
||||||
): CSSProperties {
|
): CSSProperties {
|
||||||
const resolvedLayout = getTextLayerRenderFrame(layer, value);
|
const resolvedLayout = getTextLayerRenderFrame(layer, value);
|
||||||
|
|
||||||
@ -279,7 +294,7 @@ function getTextLayerContentStyle(
|
|||||||
fontSize: resolvedLayout.fontSize,
|
fontSize: resolvedLayout.fontSize,
|
||||||
lineHeight: layer.lineHeight,
|
lineHeight: layer.lineHeight,
|
||||||
letterSpacing: `${layer.letterSpacing}px`,
|
letterSpacing: `${layer.letterSpacing}px`,
|
||||||
color: layer.textColor,
|
color,
|
||||||
fontWeight: layer.fontWeight,
|
fontWeight: layer.fontWeight,
|
||||||
textAlign: layer.textAlign,
|
textAlign: layer.textAlign,
|
||||||
paddingLeft: layer.paddingX,
|
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");
|
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(
|
function getInlineTextEditorStyle(
|
||||||
layer: AgentPsdTemplateTextLayerSlot,
|
layer: AgentPsdTemplateTextLayerSlot,
|
||||||
preserveSourceAppearance: boolean,
|
preserveSourceAppearance: boolean,
|
||||||
value: string
|
value: string,
|
||||||
|
color: string
|
||||||
): CSSProperties {
|
): CSSProperties {
|
||||||
const baseStyle = getTextLayerContentStyle(layer, value);
|
const baseStyle = getTextLayerContentStyle(layer, value, color);
|
||||||
return {
|
return {
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
overflow: "hidden",
|
overflow: "visible",
|
||||||
color: preserveSourceAppearance ? "transparent" : layer.textColor,
|
color: preserveSourceAppearance ? "transparent" : color,
|
||||||
caretColor: layer.textColor,
|
caretColor: color,
|
||||||
WebkitTextFillColor: preserveSourceAppearance ? "transparent" : layer.textColor,
|
WebkitTextFillColor: preserveSourceAppearance ? "transparent" : color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,6 +406,10 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
const templates = useMemo(() => (data?.success ? data.templates : []), [data]);
|
const templates = useMemo(() => (data?.success ? data.templates : []), [data]);
|
||||||
|
const textColorConfig = useMemo(
|
||||||
|
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
||||||
|
[data?.textColorConfig]
|
||||||
|
);
|
||||||
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||||
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||||
@ -386,6 +417,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
const [imagePlacements, setImagePlacements] = useState<Record<string, AgentPsdImagePlacement>>({});
|
const [imagePlacements, setImagePlacements] = useState<Record<string, AgentPsdImagePlacement>>({});
|
||||||
const [imageSizes, setImageSizes] = useState<Record<string, AgentPsdImageSize>>({});
|
const [imageSizes, setImageSizes] = useState<Record<string, AgentPsdImageSize>>({});
|
||||||
const [textAssignments, setTextAssignments] = useState<Record<string, string>>({});
|
const [textAssignments, setTextAssignments] = useState<Record<string, string>>({});
|
||||||
|
const [textColorAssignments, setTextColorAssignments] = useState<Record<string, string>>({});
|
||||||
const [uploadingSlotId, setUploadingSlotId] = useState<string | null>(null);
|
const [uploadingSlotId, setUploadingSlotId] = useState<string | null>(null);
|
||||||
const [isRendering, setIsRendering] = useState(false);
|
const [isRendering, setIsRendering] = useState(false);
|
||||||
const [renderedResultUrl, setRenderedResultUrl] = useState<string>("");
|
const [renderedResultUrl, setRenderedResultUrl] = useState<string>("");
|
||||||
@ -495,6 +527,21 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
|
|
||||||
return nextAssignments;
|
return nextAssignments;
|
||||||
}, [editableTextLayers, selectedTemplate, textAssignments]);
|
}, [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 effectiveImagePlacements = useMemo(() => {
|
||||||
const nextPlacements: Record<string, AgentPsdImagePlacement> = {};
|
const nextPlacements: Record<string, AgentPsdImagePlacement> = {};
|
||||||
@ -521,6 +568,9 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
const selectedImagePlacement = selectedImageLayer
|
const selectedImagePlacement = selectedImageLayer
|
||||||
? normalizeAgentPsdImagePlacement(effectiveImagePlacements[selectedImageLayer.id])
|
? normalizeAgentPsdImagePlacement(effectiveImagePlacements[selectedImageLayer.id])
|
||||||
: getDefaultAgentPsdImagePlacement();
|
: getDefaultAgentPsdImagePlacement();
|
||||||
|
const selectedTextColor = selectedTextLayer
|
||||||
|
? effectiveTextColorAssignments[selectedTextLayer.id] || selectedTextLayer.textColor
|
||||||
|
: "#111111";
|
||||||
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
|
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
|
||||||
const selectedTemplateSelectValue = selectedTemplate?.id;
|
const selectedTemplateSelectValue = selectedTemplate?.id;
|
||||||
const hasTemplateOptions = templates.length > 0;
|
const hasTemplateOptions = templates.length > 0;
|
||||||
@ -548,6 +598,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
setImageAssignments(initialState.imageAssignments);
|
setImageAssignments(initialState.imageAssignments);
|
||||||
setImagePlacements(initialState.imagePlacements);
|
setImagePlacements(initialState.imagePlacements);
|
||||||
setTextAssignments(initialState.textAssignments);
|
setTextAssignments(initialState.textAssignments);
|
||||||
|
setTextColorAssignments({});
|
||||||
setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate));
|
setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate));
|
||||||
|
|
||||||
if (!selectedTemplate || typeof window === "undefined") {
|
if (!selectedTemplate || typeof window === "undefined") {
|
||||||
@ -872,6 +923,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
imageAssignments: effectiveImageAssignments,
|
imageAssignments: effectiveImageAssignments,
|
||||||
imagePlacements: effectiveImagePlacements,
|
imagePlacements: effectiveImagePlacements,
|
||||||
textAssignments: effectiveTextAssignments,
|
textAssignments: effectiveTextAssignments,
|
||||||
|
textColorAssignments: effectiveTextColorAssignments,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1113,7 +1165,12 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const value = effectiveTextAssignments[layer.id] || "";
|
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) {
|
if (!shouldUseEditedPreview && layer.sourceUrl) {
|
||||||
return (
|
return (
|
||||||
@ -1153,7 +1210,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="whitespace-pre"
|
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}
|
{value || layer.placeholder || layer.layerName}
|
||||||
</div>
|
</div>
|
||||||
@ -1277,8 +1338,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
const isSelected = selectedLayerId === layer.id;
|
const isSelected = selectedLayerId === layer.id;
|
||||||
const frameStyle = getLayerFrameStyle(layer, isSelected);
|
const frameStyle = getLayerFrameStyle(layer, isSelected);
|
||||||
const currentValue = effectiveTextAssignments[layer.id] || "";
|
const currentValue = effectiveTextAssignments[layer.id] || "";
|
||||||
|
const resolvedTextColor =
|
||||||
|
effectiveTextColorAssignments[layer.id] || layer.textColor;
|
||||||
const preserveSourceAppearance =
|
const preserveSourceAppearance =
|
||||||
!!layer.sourceUrl && !hasEditedTextValue(layer, currentValue);
|
!!layer.sourceUrl &&
|
||||||
|
!hasCustomizedTextLayerAppearance(layer, currentValue, resolvedTextColor);
|
||||||
const editedTextFrame = getTextLayerRenderFrame(layer, currentValue);
|
const editedTextFrame = getTextLayerRenderFrame(layer, currentValue);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@ -1325,7 +1389,12 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="resize-none border-none bg-transparent outline-none"
|
className="resize-none border-none bg-transparent outline-none"
|
||||||
style={getInlineTextEditorStyle(layer, preserveSourceAppearance, currentValue)}
|
style={getInlineTextEditorStyle(
|
||||||
|
layer,
|
||||||
|
preserveSourceAppearance,
|
||||||
|
currentValue,
|
||||||
|
resolvedTextColor
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={getTextLayerNameBadgeClassName()}>
|
<div className={getTextLayerNameBadgeClassName()}>
|
||||||
@ -1585,6 +1654,57 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
<span>行高:{selectedTextLayer.lineHeight}</span>
|
<span>行高:{selectedTextLayer.lineHeight}</span>
|
||||||
<span>字距:{selectedTextLayer.letterSpacing}px</span>
|
<span>字距:{selectedTextLayer.letterSpacing}px</span>
|
||||||
</div>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-[200px] flex-col items-center justify-center text-center">
|
<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(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
if (!response.ok || !payload?.success || !payload?.data?.imageUrl) {
|
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;
|
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_TEMPLATES_KEY = "agent_psd_templates";
|
||||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
||||||
|
|
||||||
@ -98,6 +100,7 @@ export interface AgentPsdTemplate {
|
|||||||
export interface AgentPsdTemplateListResponse {
|
export interface AgentPsdTemplateListResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
templates: AgentPsdTemplate[];
|
templates: AgentPsdTemplate[];
|
||||||
|
textColorConfig?: AgentPsdTextColorConfig;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
|
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
|
||||||
import { ensurePsdFontAvailableForRender } from "@/lib/agents/psd-font-runtime";
|
import { ensurePsdFontAvailableForRender } from "@/lib/agents/psd-font-runtime";
|
||||||
import { normalizeAgentPsdImagePlacement, resolveAgentPsdImagePlacementFrame } from "@/lib/agents/psd-image-placement";
|
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() {
|
async function getSharp() {
|
||||||
const sharp = await import("sharp");
|
const sharp = await import("sharp");
|
||||||
@ -125,6 +125,7 @@ function resolveTextOverlayFrame(params: {
|
|||||||
contentWidth: number;
|
contentWidth: number;
|
||||||
contentHeight: number;
|
contentHeight: number;
|
||||||
textAlign: "left" | "center" | "right";
|
textAlign: "left" | "center" | "right";
|
||||||
|
topOffset?: number;
|
||||||
}) {
|
}) {
|
||||||
const width = Math.max(params.layerWidth, params.contentWidth);
|
const width = Math.max(params.layerWidth, params.contentWidth);
|
||||||
const height = Math.max(params.layerHeight, params.contentHeight);
|
const height = Math.max(params.layerHeight, params.contentHeight);
|
||||||
@ -139,7 +140,7 @@ function resolveTextOverlayFrame(params: {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
leftOffset,
|
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>;
|
imageAssignments: Record<string, string>;
|
||||||
imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>>;
|
imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>>;
|
||||||
textAssignments: Record<string, string>;
|
textAssignments: Record<string, string>;
|
||||||
|
textColorAssignments?: Record<string, string>;
|
||||||
fonts: AgentPsdFont[];
|
fonts: AgentPsdFont[];
|
||||||
}) {
|
}) {
|
||||||
const { template, imageAssignments, imagePlacements, textAssignments, fonts } = params;
|
const {
|
||||||
|
template,
|
||||||
|
imageAssignments,
|
||||||
|
imagePlacements,
|
||||||
|
textAssignments,
|
||||||
|
textColorAssignments = {},
|
||||||
|
fonts,
|
||||||
|
} = params;
|
||||||
const sharp = await getSharp();
|
const sharp = await getSharp();
|
||||||
const canvasWidth = Math.max(1, Math.round(Number(template.width || 1)));
|
const canvasWidth = Math.max(1, Math.round(Number(template.width || 1)));
|
||||||
const canvasHeight = Math.max(1, Math.round(Number(template.height || 1)));
|
const canvasHeight = Math.max(1, Math.round(Number(template.height || 1)));
|
||||||
@ -511,7 +520,12 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
if (isAgentPsdTextLayerSlot(layer)) {
|
if (isAgentPsdTextLayerSlot(layer)) {
|
||||||
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
||||||
const defaultText = String(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);
|
await appendStoredLayer(layer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -563,7 +577,7 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
fontWeight: layer.fontWeight,
|
fontWeight: layer.fontWeight,
|
||||||
lineHeight: layer.lineHeight,
|
lineHeight: layer.lineHeight,
|
||||||
letterSpacing: layer.letterSpacing,
|
letterSpacing: layer.letterSpacing,
|
||||||
textColor: layer.textColor,
|
textColor,
|
||||||
textAlign: layer.textAlign,
|
textAlign: layer.textAlign,
|
||||||
paddingX: layer.paddingX,
|
paddingX: layer.paddingX,
|
||||||
paddingY: layer.paddingY,
|
paddingY: layer.paddingY,
|
||||||
@ -590,6 +604,7 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
contentWidth: overlayLayout.contentWidth,
|
contentWidth: overlayLayout.contentWidth,
|
||||||
contentHeight: overlayLayout.contentHeight,
|
contentHeight: overlayLayout.contentHeight,
|
||||||
textAlign: layer.textAlign,
|
textAlign: layer.textAlign,
|
||||||
|
topOffset: -overlayLayout.visualOffsetY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const didAppend = await appendBufferOverlay({
|
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 availableHeight = Math.max(8, height - paddingY * 2);
|
||||||
const resolvedLineHeight = Math.max(1, Number(lineHeight || 1));
|
const resolvedLineHeight = Math.max(1, Number(lineHeight || 1));
|
||||||
const resolvedMaxLines = Math.max(1, Number(maxLines || 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 fittedFontSize = Math.max(10, Number(fontSize || 10));
|
||||||
let bestLines: string[] = [];
|
let bestLines: string[] = [];
|
||||||
@ -153,8 +160,13 @@ export function resolveTextLayout(params: {
|
|||||||
maxLineWidth: Number(bestMaxLineWidth.toFixed(2)),
|
maxLineWidth: Number(bestMaxLineWidth.toFixed(2)),
|
||||||
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
||||||
contentHeight: Number(
|
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)),
|
maxLineWidth: Number(bestMaxLineWidth.toFixed(2)),
|
||||||
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
contentWidth: Number(Math.max(width, bestMaxLineWidth + paddingX * 2).toFixed(2)),
|
||||||
contentHeight: Number(
|
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;
|
updatedAt: Date | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentPsdTextColorConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
colors: [string, string];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentCategoryTreeConfig {
|
export interface AgentCategoryTreeConfig {
|
||||||
productCategories: string[];
|
productCategories: string[];
|
||||||
audienceTags: string[];
|
audienceTags: string[];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user