313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { and, eq } from "drizzle-orm";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { db } from "@/lib/db";
|
|
import { canvasProject } from "@/lib/db/schema";
|
|
import {
|
|
extractAgentProjectStateFromCanvasData,
|
|
isAgentProjectRow,
|
|
} from "@/lib/agents/agent-project-compat";
|
|
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
|
import type {
|
|
AgentGenerationProjectPersistedState,
|
|
AgentGenerationProjectReplacementResultState,
|
|
AgentGenerationProjectResultCollections,
|
|
} from "@/types/agent";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
export const revalidate = 0;
|
|
|
|
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
|
return {
|
|
id: row.id,
|
|
userId: row.userId,
|
|
title: row.title,
|
|
description: row.description || null,
|
|
thumbnail: row.thumbnail || null,
|
|
projectState: extractAgentProjectStateFromCanvasData(row.canvasData),
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
};
|
|
}
|
|
|
|
const RESULT_COLLECTION_KEYS = [
|
|
"scene_generation",
|
|
"detail_generation",
|
|
"detail_replacement",
|
|
"sku_replacement",
|
|
] as const;
|
|
|
|
const REPLACEMENT_RESULT_KEYS = [
|
|
"detail_replacement",
|
|
"sku_replacement",
|
|
] as const;
|
|
|
|
function normalizeProjectResultUrlList(urls: string[] | null | undefined) {
|
|
return Array.from(
|
|
new Set(
|
|
(urls || [])
|
|
.map((url) => String(url || "").trim())
|
|
.filter((url) => /^https?:\/\//i.test(url))
|
|
)
|
|
);
|
|
}
|
|
|
|
function mergeResultCollections(
|
|
existing: AgentGenerationProjectResultCollections | null | undefined,
|
|
incoming: AgentGenerationProjectResultCollections | null | undefined
|
|
): AgentGenerationProjectResultCollections {
|
|
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
|
|
(next, key) => {
|
|
next[key] = normalizeProjectResultUrlList([
|
|
...normalizeProjectResultUrlList(existing?.[key]),
|
|
...normalizeProjectResultUrlList(incoming?.[key]),
|
|
]);
|
|
return next;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
function getResultCollectionUrlSet(
|
|
collections: AgentGenerationProjectResultCollections | null | undefined
|
|
) {
|
|
return new Set(
|
|
RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key]))
|
|
);
|
|
}
|
|
|
|
function removeDeletedUrlsFromCollections(
|
|
collections: AgentGenerationProjectResultCollections,
|
|
deletedUrls: Set<string>
|
|
): AgentGenerationProjectResultCollections {
|
|
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
|
|
(next, key) => {
|
|
next[key] = normalizeProjectResultUrlList(collections[key]).filter(
|
|
(url) => !deletedUrls.has(url)
|
|
);
|
|
return next;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
function mergeReplacementResultState(
|
|
existing: AgentGenerationProjectReplacementResultState | null | undefined,
|
|
incoming: AgentGenerationProjectReplacementResultState | null | undefined,
|
|
deletedUrls: Set<string>
|
|
): AgentGenerationProjectReplacementResultState {
|
|
return REPLACEMENT_RESULT_KEYS.reduce<AgentGenerationProjectReplacementResultState>(
|
|
(next, key) => {
|
|
const existingModeState = existing?.[key] || {};
|
|
const incomingModeState = incoming?.[key] || {};
|
|
const mergedModeState = { ...existingModeState, ...incomingModeState };
|
|
next[key] = Object.fromEntries(
|
|
Object.entries(mergedModeState).filter(([, record]) => {
|
|
const url = String(record?.url || "").trim();
|
|
return !url || !deletedUrls.has(url);
|
|
})
|
|
);
|
|
return next;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
function getReplacementResultUrls(
|
|
state: AgentGenerationProjectReplacementResultState | null | undefined
|
|
) {
|
|
return REPLACEMENT_RESULT_KEYS.flatMap((key) =>
|
|
Object.values(state?.[key] || {})
|
|
.map((record) => String(record?.url || "").trim())
|
|
.filter((url) => /^https?:\/\//i.test(url))
|
|
);
|
|
}
|
|
|
|
function getAllResultUrlsFromState(state: AgentGenerationProjectPersistedState) {
|
|
return normalizeProjectResultUrlList([
|
|
...normalizeProjectResultUrlList(state.resultImageUrls),
|
|
...RESULT_COLLECTION_KEYS.flatMap((key) =>
|
|
normalizeProjectResultUrlList(state.resultCollections?.[key])
|
|
),
|
|
...getReplacementResultUrls(state.replacementResultState),
|
|
]);
|
|
}
|
|
|
|
function mergeAgentProjectStateForSave(params: {
|
|
existingState: AgentGenerationProjectPersistedState;
|
|
incomingState: unknown;
|
|
}) {
|
|
const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState);
|
|
const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState);
|
|
const deletedResultCollections = mergeResultCollections(
|
|
existingState.deletedResultCollections,
|
|
incomingState.deletedResultCollections
|
|
);
|
|
const deletedUrls = getResultCollectionUrlSet(deletedResultCollections);
|
|
const replacementResultState = mergeReplacementResultState(
|
|
existingState.replacementResultState,
|
|
incomingState.replacementResultState,
|
|
deletedUrls
|
|
);
|
|
const resultCollections = removeDeletedUrlsFromCollections(
|
|
mergeResultCollections(existingState.resultCollections, incomingState.resultCollections),
|
|
deletedUrls
|
|
);
|
|
|
|
return normalizeAgentGenerationProjectPersistedState({
|
|
...incomingState,
|
|
resultCollections,
|
|
deletedResultCollections,
|
|
replacementResultState,
|
|
resultImageUrls: normalizeProjectResultUrlList([
|
|
...getAllResultUrlsFromState(existingState),
|
|
...getAllResultUrlsFromState(incomingState),
|
|
...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []),
|
|
...getReplacementResultUrls(replacementResultState),
|
|
]).filter((url) => !deletedUrls.has(url)),
|
|
});
|
|
}
|
|
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { id } = await params;
|
|
const [project] = await db
|
|
.select()
|
|
.from(canvasProject)
|
|
.where(
|
|
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
|
)
|
|
.limit(1);
|
|
|
|
if (!project || !isAgentProjectRow(project)) {
|
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
|
} catch (error) {
|
|
console.error("[AgentProjects API] 获取项目失败:", error);
|
|
return NextResponse.json({ success: false, error: "获取生图项目失败" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function PUT(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { id } = await params;
|
|
const body = await request.json().catch(() => ({}));
|
|
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 title =
|
|
body.title !== undefined
|
|
? String(body.title || "").trim() || "未命名生图项目"
|
|
: existingProject.title;
|
|
const description =
|
|
body.description !== undefined
|
|
? String(body.description || "").trim() || null
|
|
: existingProject.description || null;
|
|
|
|
const existingProjectState = extractAgentProjectStateFromCanvasData(existingProject.canvasData);
|
|
const nextProjectState =
|
|
body.projectState !== undefined
|
|
? mergeAgentProjectStateForSave({
|
|
existingState: existingProjectState,
|
|
incomingState: body.projectState,
|
|
})
|
|
: existingProjectState;
|
|
const firstResultUrl = getAllResultUrlsFromState(nextProjectState)[0] || null;
|
|
const requestedThumbnail =
|
|
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
|
const thumbnail =
|
|
requestedThumbnail || existingProject.thumbnail || firstResultUrl || null;
|
|
|
|
const [project] = await db
|
|
.update(canvasProject)
|
|
.set({
|
|
title,
|
|
description,
|
|
thumbnail,
|
|
workspaceType: "agent",
|
|
updatedAt: new Date(),
|
|
canvasData: {
|
|
nodes: [],
|
|
edges: [],
|
|
selectedNodeIds: [],
|
|
viewport: { x: 0, y: 0, zoom: 1 },
|
|
tool: "select",
|
|
agentProjectState: nextProjectState,
|
|
} as any,
|
|
})
|
|
.where(
|
|
and(eq(canvasProject.id, id), eq(canvasProject.userId, session.user.id))
|
|
)
|
|
.returning();
|
|
|
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
|
} catch (error) {
|
|
console.error("[AgentProjects API] 保存项目失败:", error);
|
|
return NextResponse.json({ success: false, error: "保存生图项目失败" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
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)))
|
|
.returning();
|
|
|
|
if (!project) {
|
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
|
} catch (error) {
|
|
console.error("[AgentProjects API] 删除项目失败:", error);
|
|
return NextResponse.json({ success: false, error: "删除生图项目失败" }, { status: 500 });
|
|
}
|
|
}
|