From 98fe210b6483df4dcf8f4d784d1794435d05d108 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Mar 2026 13:07:39 +0800 Subject: [PATCH] feat: separate agent projects from canvas storage --- app/api/agent-projects/[id]/route.ts | 179 +++++++++ app/api/agent-projects/route.ts | 99 +++++ app/api/canvas/route.ts | 9 +- app/canvas/[id]/page.tsx | 376 +++++++++++++++++- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 127 +++++- components/agents/AgentProjectListSheet.tsx | 195 +++++++++ .../agents/AgentWorkflowPanelDarkV2.tsx | 12 +- lib/agents/generation-project-state.ts | 159 ++++++++ lib/agents/workflow-composer.ts | 12 +- lib/agents/workflow-ui-config.ts | 41 ++ services/agent-projects/index.ts | 80 ++++ types/agent.ts | 37 ++ 12 files changed, 1285 insertions(+), 41 deletions(-) create mode 100644 app/api/agent-projects/[id]/route.ts create mode 100644 app/api/agent-projects/route.ts create mode 100644 components/agents/AgentProjectListSheet.tsx create mode 100644 lib/agents/generation-project-state.ts create mode 100644 services/agent-projects/index.ts diff --git a/app/api/agent-projects/[id]/route.ts b/app/api/agent-projects/[id]/route.ts new file mode 100644 index 000000000..1082fa90b --- /dev/null +++ b/app/api/agent-projects/[id]/route.ts @@ -0,0 +1,179 @@ +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 { + createEmptyAgentGenerationProjectPersistedState, + 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) + : {}; + + return { + id: row.id, + userId: row.userId, + title: row.title, + description: row.description || null, + thumbnail: row.thumbnail || null, + projectState: normalizeAgentGenerationProjectPersistedState( + canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState() + ), + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +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), + eq(canvasProject.workspaceType, "agent") + ) + ) + .limit(1); + + 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 }); + } +} + +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), + eq(canvasProject.workspaceType, "agent") + ) + ) + .limit(1); + + if (!existingProject) { + return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 }); + } + + const title = body.title !== undefined ? String(body.title || "").trim() || "未命名生图项目" : undefined; + const description = + body.description !== undefined ? String(body.description || "").trim() || null : undefined; + 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) + : {}; + + const nextProjectState = + body.projectState !== undefined + ? normalizeAgentGenerationProjectPersistedState(body.projectState) + : normalizeAgentGenerationProjectPersistedState( + existingCanvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState() + ); + + const [project] = await db + .update(canvasProject) + .set({ + title, + description, + thumbnail, + 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), + eq(canvasProject.workspaceType, "agent") + ) + ) + .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 [project] = await db + .delete(canvasProject) + .where( + and( + eq(canvasProject.id, id), + eq(canvasProject.userId, session.user.id), + eq(canvasProject.workspaceType, "agent") + ) + ) + .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 }); + } +} diff --git a/app/api/agent-projects/route.ts b/app/api/agent-projects/route.ts new file mode 100644 index 000000000..1c628ec85 --- /dev/null +++ b/app/api/agent-projects/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from "next/server"; +import { and, 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"; + +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) + : {}; + + return { + id: row.id, + userId: row.userId, + title: row.title, + description: row.description || null, + thumbnail: row.thumbnail || null, + projectState: normalizeAgentGenerationProjectPersistedState( + canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState() + ), + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export async function GET() { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); + } + + const projects = await db + .select() + .from(canvasProject) + .where( + and( + eq(canvasProject.userId, session.user.id), + eq(canvasProject.workspaceType, "agent") + ) + ) + .orderBy(desc(canvasProject.updatedAt)); + + return NextResponse.json({ + success: true, + data: projects.map(mapAgentProjectRow), + }); + } catch (error) { + console.error("[AgentProjects API] 获取项目列表失败:", error); + return NextResponse.json({ success: false, error: "获取生图项目列表失败" }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); + } + + const body = await request.json().catch(() => ({})); + const title = String(body.title || "").trim() || "未命名生图项目"; + const description = String(body.description || "").trim() || null; + const thumbnail = String(body.thumbnail || "").trim() || null; + const projectState = normalizeAgentGenerationProjectPersistedState(body.projectState); + + const [project] = await db + .insert(canvasProject) + .values({ + userId: session.user.id, + title, + description, + thumbnail, + workspaceType: "agent", + canvasData: { + nodes: [], + edges: [], + selectedNodeIds: [], + viewport: { x: 0, y: 0, zoom: 1 }, + tool: "select", + agentProjectState: projectState, + } as any, + }) + .returning(); + + return NextResponse.json({ success: true, data: mapAgentProjectRow(project) }); + } catch (error) { + console.error("[AgentProjects API] 创建项目失败:", error); + return NextResponse.json({ success: false, error: "创建生图项目失败" }, { status: 500 }); + } +} diff --git a/app/api/canvas/route.ts b/app/api/canvas/route.ts index d8d793ee7..ace961c7b 100644 --- a/app/api/canvas/route.ts +++ b/app/api/canvas/route.ts @@ -3,7 +3,7 @@ import { auth } from '@/app/(auth)/auth'; import { db } from '@/lib/db'; import { canvasProject } from '@/lib/db/schema'; import { syncCanvasNodesForProject } from '@/lib/db/queries'; -import { eq, desc } from 'drizzle-orm'; +import { and, eq, desc } from 'drizzle-orm'; // 获取画布列表 export async function GET(request: NextRequest) { @@ -16,7 +16,12 @@ export async function GET(request: NextRequest) { const projects = await db .select() .from(canvasProject) - .where(eq(canvasProject.userId, session.user.id)) + .where( + and( + eq(canvasProject.userId, session.user.id), + eq(canvasProject.workspaceType, "canvas") + ) + ) .orderBy(desc(canvasProject.updatedAt)); return NextResponse.json({ success: true, data: projects }); diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index c505510e3..61437ec8d 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -70,6 +70,7 @@ import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew"; import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2"; import { AgentWorkflowPanelDarkV2 } from "@/components/agents/AgentWorkflowPanelDarkV2"; import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; +import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet"; import { CanvasWorkspace } from "@/components/canvas"; import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel"; import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel"; @@ -94,7 +95,14 @@ import { createEmptyAgentModeWorkspaceState, normalizeAgentModeWorkspaceState, } from "@/lib/agents/mode-workspace-state"; +import { + createEmptyAgentGenerationProjectPersistedState, + createEmptyAgentGenerationProjectWorkspaceState, + normalizeAgentGenerationProjectPersistedState, +} from "@/lib/agents/generation-project-state"; +import { agentProjectService } from "@/services/agent-projects"; import type { + AgentGenerationProjectWorkspaceState, AgentModeWorkspaceState, AgentWorkflowComposerContext, AgentWorkflowMaterialSelections, @@ -142,6 +150,15 @@ function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") { ].join("-"); } +function buildDefaultAgentProjectTitle() { + const now = new Date(); + const month = `${now.getMonth() + 1}`.padStart(2, "0"); + const day = `${now.getDate()}`.padStart(2, "0"); + const hour = `${now.getHours()}`.padStart(2, "0"); + const minute = `${now.getMinutes()}`.padStart(2, "0"); + return `生图项目 ${month}-${day} ${hour}:${minute}`; +} + export default function CanvasEditorPage({ params }: PageProps) { // 使用 useParams 确保客户端导航时能正确获取更新的 id const routeParams = useParams(); @@ -212,10 +229,25 @@ export default function CanvasEditorPage({ params }: PageProps) { const [agentModeState, setAgentModeState] = useState( () => createEmptyAgentModeWorkspaceState() ); + const [agentWorkspaceState, setAgentWorkspaceState] = + useState( + () => createEmptyAgentGenerationProjectWorkspaceState() + ); + const [agentProjectTitle, setAgentProjectTitle] = useState("未命名生图项目"); + const [agentProjectThumbnail, setAgentProjectThumbnail] = useState(null); + const [agentProjectResultUrls, setAgentProjectResultUrls] = useState([]); + const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false); + const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false); const [workflowHydrationSource, setWorkflowHydrationSource] = useState< "pending" | "project" | "local" | "empty" >("pending"); - const agentModeChatId = useMemo(() => buildScopedCanvasChatId(id, "agent-mode"), [id]); + const requestedAgentProjectId = searchParams.get("agentProjectId"); + const agentProjectId = requestedAgentProjectId ? requestedAgentProjectId.trim() : ""; + const hasActiveAgentProject = Boolean(agentProjectId); + const agentModeChatId = useMemo( + () => buildScopedCanvasChatId(agentProjectId || id, "agent-mode"), + [agentProjectId, id] + ); const { data: siteSettingsData } = useSWR<{ siteName?: string; @@ -290,6 +322,7 @@ export default function CanvasEditorPage({ params }: PageProps) { updateCanvasSearchParams({ mode: null, agentPresetId: null, + agentProjectId: null, }); return; } @@ -297,10 +330,11 @@ export default function CanvasEditorPage({ params }: PageProps) { updateCanvasSearchParams({ mode: "agent", agentPresetId: null, + agentProjectId: agentProjectId || null, }); setIsPanelOpen(true); setPanelMode("chat"); - }, [isAgentMode, updateCanvasSearchParams]); + }, [agentProjectId, isAgentMode, updateCanvasSearchParams]); // 加载画布数据 useLayoutEffect(() => { @@ -320,6 +354,10 @@ export default function CanvasEditorPage({ params }: PageProps) { setSelectedBrandModel(null); setSelectedAgentPresetId(null); setAgentModeState(createEmptyAgentModeWorkspaceState()); + setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); + setAgentProjectTitle("未命名生图项目"); + setAgentProjectThumbnail(null); + setAgentProjectResultUrls([]); // 🆕 先清空当前画布状态,确保切换时不会显示旧数据 useCanvasStore.getState().clearCanvas(); @@ -331,7 +369,7 @@ export default function CanvasEditorPage({ params }: PageProps) { if (result.data.canvasData) { useCanvasStore.getState().loadCanvas(result.data.canvasData); const persistedWorkflow = result.data.canvasData.workflowState; - if (persistedWorkflow) { + if (!requestedAgentMode && persistedWorkflow) { setWorkflowInputs({ primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), @@ -388,20 +426,22 @@ export default function CanvasEditorPage({ params }: PageProps) { } else { // 画布不存在时,创建一个新画布 console.log('[Canvas] 画布不存在,创建新画布:', id); - useCanvasStore.getState().setProject(id, '未命名画布'); + useCanvasStore.getState().setProject(id, requestedAgentMode ? '智能体工作台' : '未命名画布'); setWorkflowHydrationSource("empty"); - // 保存新画布到数据库 - try { - await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' }); - } catch (error) { - console.error('[Canvas] 创建新画布失败:', error); + if (!requestedAgentMode) { + // 保存新画布到数据库 + try { + await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' }); + } catch (error) { + console.error('[Canvas] 创建新画布失败:', error); + } } } setIsLoading(false); }; loadProject(); - }, [id]); // 只依赖 id,使用 getState() 直接调用 store actions + }, [id, requestedAgentMode]); useEffect(() => { const updateViewportWidth = () => setViewportWidth(window.innerWidth); @@ -425,6 +465,14 @@ export default function CanvasEditorPage({ params }: PageProps) { const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]); const buildPersistedCanvasData = useCallback(() => { + return { + ...getCanvasState(), + }; + }, [ + getCanvasState, + ]); + + const buildPersistedAgentProjectState = useCallback(() => { const workflowState: AgentWorkflowPersistedState = { inputs: { primarySku: workflowInputs.primarySku.trim(), @@ -441,21 +489,24 @@ export default function CanvasEditorPage({ params }: PageProps) { }; return { - ...getCanvasState(), workflowState, + workspaceState: agentWorkspaceState, + resultImageUrls: agentProjectResultUrls, }; }, [ + agentModeState, + agentProjectResultUrls, + agentWorkspaceState, currentAgentStage, - getCanvasState, selectedAgentPresetId, selectedBrandModel, - agentModeState, workflowInputs, workflowMaterials, workflowSelections, ]); useEffect(() => { + if (isAgentMode) return; if (typeof window === "undefined") return; if (workflowHydrationSource !== "empty") return; @@ -499,9 +550,10 @@ export default function CanvasEditorPage({ params }: PageProps) { } finally { setWorkflowHydrationSource("local"); } - }, [workflowHydrationSource, workflowStorageKey]); + }, [isAgentMode, workflowHydrationSource, workflowStorageKey]); useEffect(() => { + if (isAgentMode) return; if (typeof window === "undefined" || workflowHydrationSource === "pending") return; try { @@ -521,6 +573,7 @@ export default function CanvasEditorPage({ params }: PageProps) { console.warn("[Canvas] Failed to persist workflow state:", error); } }, [ + isAgentMode, workflowHydrationSource, workflowInputs, workflowMaterials, @@ -532,6 +585,247 @@ export default function CanvasEditorPage({ params }: PageProps) { agentModeState, ]); + const persistAgentProject = useCallback( + async (reason: "autosave" | "manual" | "switch") => { + if (!isAgentMode || !agentProjectId) return false; + + const result = await agentProjectService.updateProject(agentProjectId, { + title: agentProjectTitle || buildDefaultAgentProjectTitle(), + thumbnail: agentProjectThumbnail, + projectState: buildPersistedAgentProjectState(), + }); + + if (!result.success) { + console.error(`[Agent Project] ${reason} save failed:`, result.error); + return false; + } + + if (result.data) { + setAgentProjectTitle(result.data.title || buildDefaultAgentProjectTitle()); + setAgentProjectThumbnail(result.data.thumbnail || null); + } + + return true; + }, + [ + agentProjectId, + agentProjectThumbnail, + agentProjectTitle, + buildPersistedAgentProjectState, + isAgentMode, + ] + ); + + useEffect(() => { + if (!requestedAgentMode || !isAgentFeatureEnabled || agentProjectId) return; + + let cancelled = false; + const createAgentProject = async () => { + setIsAgentProjectLoading(true); + try { + const result = await agentProjectService.createProject({ + title: buildDefaultAgentProjectTitle(), + projectState: createEmptyAgentGenerationProjectPersistedState(), + }); + if (!result.success || !result.data) { + throw new Error(result.error || "创建生图项目失败"); + } + if (cancelled) return; + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: result.data.id, + }); + } catch (error) { + if (!cancelled) { + toast.error(error instanceof Error ? error.message : "创建生图项目失败"); + } + } finally { + if (!cancelled) { + setIsAgentProjectLoading(false); + } + } + }; + + void createAgentProject(); + return () => { + cancelled = true; + }; + }, [agentProjectId, isAgentFeatureEnabled, requestedAgentMode, updateCanvasSearchParams]); + + useEffect(() => { + if (!requestedAgentMode || !agentProjectId) return; + + let cancelled = false; + const loadAgentProject = async () => { + setIsAgentProjectLoading(true); + try { + const result = await agentProjectService.getProject(agentProjectId); + if (!result.success || !result.data) { + throw new Error(result.error || "加载生图项目失败"); + } + if (cancelled) return; + + const persistedState = normalizeAgentGenerationProjectPersistedState( + result.data.projectState + ); + const persistedWorkflow = persistedState.workflowState; + + setWorkflowInputs({ + primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(), + secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(), + brand: String(persistedWorkflow.inputs?.brand || "").trim(), + store: String(persistedWorkflow.inputs?.store || "").trim(), + }); + setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)); + setWorkflowSelections(persistedWorkflow.selections || {}); + setSelectedBrandModel(persistedWorkflow.selectedBrandModel || null); + setSelectedAgentPresetId(String(persistedWorkflow.selectedAgentPresetId || "").trim() || null); + setCurrentAgentStage(persistedWorkflow.currentStage === "mode" ? "mode" : "workflow"); + setAgentModeState(normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState)); + setAgentWorkspaceState( + persistedState.workspaceState || createEmptyAgentGenerationProjectWorkspaceState() + ); + setAgentProjectTitle(result.data.title || "未命名生图项目"); + setAgentProjectThumbnail(result.data.thumbnail || null); + setAgentProjectResultUrls(persistedState.resultImageUrls || []); + } catch (error) { + if (!cancelled) { + toast.error(error instanceof Error ? error.message : "加载生图项目失败"); + } + } finally { + if (!cancelled) { + setIsAgentProjectLoading(false); + } + } + }; + + void loadAgentProject(); + return () => { + cancelled = true; + }; + }, [agentProjectId, requestedAgentMode]); + + useEffect(() => { + if (!isAgentMode || !agentProjectId || isAgentProjectLoading) return; + + const timer = window.setTimeout(async () => { + setIsAgentProjectSaving(true); + try { + await persistAgentProject("autosave"); + } finally { + setIsAgentProjectSaving(false); + } + }, 2500); + + return () => window.clearTimeout(timer); + }, [ + agentModeState, + agentProjectId, + agentProjectResultUrls, + agentProjectThumbnail, + agentProjectTitle, + agentWorkspaceState, + currentAgentStage, + isAgentMode, + isAgentProjectLoading, + persistAgentProject, + selectedAgentPresetId, + selectedBrandModel, + workflowInputs, + workflowMaterials, + workflowSelections, + ]); + + const handleManualSaveAgentProject = useCallback(async () => { + if (!agentProjectId) { + toast.error("当前还没有可保存的生图项目"); + return; + } + setIsAgentProjectSaving(true); + try { + const success = await persistAgentProject("manual"); + if (success) { + toast.success("生图项目已保存"); + } else { + toast.error("保存生图项目失败"); + } + } finally { + setIsAgentProjectSaving(false); + } + }, [agentProjectId, persistAgentProject]); + + const handleCreateAgentProject = useCallback(async () => { + if (agentProjectId) { + await persistAgentProject("switch"); + } + + const result = await agentProjectService.createProject({ + title: buildDefaultAgentProjectTitle(), + projectState: createEmptyAgentGenerationProjectPersistedState(), + }); + if (!result.success || !result.data) { + toast.error(result.error || "创建生图项目失败"); + return; + } + + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: result.data.id, + }); + }, [agentProjectId, persistAgentProject, updateCanvasSearchParams]); + + const handleSelectAgentProject = useCallback( + async (nextProjectId: string) => { + if (!nextProjectId || nextProjectId === agentProjectId) return; + if (agentProjectId) { + await persistAgentProject("switch"); + } + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: nextProjectId, + }); + }, + [agentProjectId, persistAgentProject, updateCanvasSearchParams] + ); + + const handleDeleteAgentProject = useCallback( + async (targetProjectId: string) => { + const result = await agentProjectService.deleteProject(targetProjectId); + if (!result.success) { + toast.error(result.error || "删除生图项目失败"); + return; + } + + if (targetProjectId === agentProjectId) { + const listResult = await agentProjectService.getProjects(); + const nextProject = listResult.success + ? listResult.data?.find((project) => project.id !== targetProjectId) || null + : null; + + if (nextProject) { + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: nextProject.id, + }); + } else { + const createResult = await agentProjectService.createProject({ + title: buildDefaultAgentProjectTitle(), + projectState: createEmptyAgentGenerationProjectPersistedState(), + }); + if (createResult.success && createResult.data) { + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: createResult.data.id, + }); + } + } + } + + toast.success("生图项目已删除"); + }, + [agentProjectId, updateCanvasSearchParams] + ); + useEffect(() => { if (!selectedBrandModel) return; const normalizedBrand = workflowInputs.brand.trim().toLowerCase(); @@ -558,7 +852,7 @@ export default function CanvasEditorPage({ params }: PageProps) { // 自动保存逻辑 useEffect(() => { - if (isLoading) return; + if (isLoading || isAgentMode) return; const saveTimer = setTimeout(async () => { setIsSaving(true); @@ -571,11 +865,11 @@ export default function CanvasEditorPage({ params }: PageProps) { }, 3000); // 3秒自动保存 return () => clearTimeout(saveTimer); - }, [nodes, workflowInputs, workflowSelections, isLoading, persistCanvasState]); + }, [isAgentMode, nodes, isLoading, persistCanvasState]); // 删除节点时立即保存(按节点ID差集检测),避免删除后快速刷新被回滚 useEffect(() => { - if (isLoading) { + if (isLoading || isAgentMode) { previousNodeIdsRef.current = new Set(nodes.map((node) => node.id)); return; } @@ -602,11 +896,11 @@ export default function CanvasEditorPage({ params }: PageProps) { }; void persistAfterDelete(); } - }, [nodes, isLoading, persistCanvasState]); + }, [isAgentMode, nodes, isLoading, persistCanvasState]); // 页面关闭/刷新前尝试做一次 keepalive 保存,降低“刚删就刷新”回滚概率 useEffect(() => { - if (isLoading) return; + if (isLoading || isAgentMode) return; const handleBeforeUnload = () => { try { @@ -628,10 +922,11 @@ export default function CanvasEditorPage({ params }: PageProps) { window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("pagehide", handleBeforeUnload); }; - }, [buildPersistedCanvasData, id, isLoading]); + }, [buildPersistedCanvasData, id, isAgentMode, isLoading]); // 智能命名与缩略图逻辑 useEffect(() => { + if (isAgentMode) return; const autoTitleAndThumbnail = async () => { // 如果标题是默认值,且有图片节点 if ((projectTitle === "未命名画布" || projectTitle === "新画布") && nodes.length > 0) { @@ -664,7 +959,7 @@ export default function CanvasEditorPage({ params }: PageProps) { // 使用防抖,避免频繁触发 const timer = setTimeout(autoTitleAndThumbnail, 5000); // 5秒后触发,给用户一点时间操作 return () => clearTimeout(timer); - }, [nodes, projectTitle, id, setProjectTitle]); + }, [id, isAgentMode, nodes, projectTitle, setProjectTitle]); // 保存画布 (手动触发保留作为备用,但UI上移除) const handleSave = useCallback(async () => { @@ -1039,6 +1334,34 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null; + const agentProjectNavSlot = isAgentMode ? ( +
+ + +
+ ) : null; + if (isAgentMode) { // 深色主题智能体模式 V2 - 完整功能版本 @@ -1057,6 +1380,7 @@ export default function CanvasEditorPage({ params }: PageProps) { siteLogo={siteSettingsData?.siteLogo} siteLogoDark={siteSettingsData?.siteLogoDark} showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig} + topNavRightSlot={agentProjectNavSlot} onNextStep={() => setCurrentAgentStage("mode")} onSkipStep={() => setCurrentAgentStage("mode")} onReset={() => { @@ -1066,6 +1390,7 @@ export default function CanvasEditorPage({ params }: PageProps) { setSelectedBrandModel(null); setSelectedAgentPresetId(null); setAgentModeState(createEmptyAgentModeWorkspaceState()); + setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); setCurrentAgentStage("workflow"); }} /> @@ -1082,9 +1407,18 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowContext={workflowContext} agentModeState={agentModeState} onAgentModeStateChange={setAgentModeState} + workspaceState={agentWorkspaceState} + onWorkspaceStateChange={setAgentWorkspaceState} + onResultPreviewUrlsChange={(urls) => { + setAgentProjectResultUrls(urls); + if (urls[0]) { + setAgentProjectThumbnail(urls[0]); + } + }} siteName={siteSettingsData?.siteName} siteLogo={siteSettingsData?.siteLogo} siteLogoDark={siteSettingsData?.siteLogoDark} + topNavRightSlot={agentProjectNavSlot} syncGeneratedResultsToCanvas={false} onBack={() => setCurrentAgentStage("workflow")} onOpenCanvasEditor={() => { diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 2577d15df..9f4a59dc1 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -42,6 +42,7 @@ import type { Attachment, ChatMessage } from "@/lib/types"; import { cn, fetchWithErrorHandlers, fetcher, generateUUID } from "@/lib/utils"; import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; import type { + AgentGenerationProjectWorkspaceState, AgentModeReferenceCandidate, AgentModeWorkspaceState, AgentPreset, @@ -52,6 +53,10 @@ import type { import { createEmptyAgentModeWorkspaceState, } from "@/lib/agents/mode-workspace-state"; +import { + createEmptyAgentGenerationProjectWorkspaceState, + normalizeAgentGenerationProjectPersistedState, +} from "@/lib/agents/generation-project-state"; interface AgentGenerationWorkspaceDarkV2Props { chatId: string; @@ -71,6 +76,10 @@ interface AgentGenerationWorkspaceDarkV2Props { agentModeState?: AgentModeWorkspaceState | null; onAgentModeStateChange?: (next: AgentModeWorkspaceState) => void; onOpenCanvasEditor?: () => void; + workspaceState?: AgentGenerationProjectWorkspaceState | null; + onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void; + onResultPreviewUrlsChange?: (urls: string[]) => void; + topNavRightSlot?: ReactNode; } interface PickedAttachment extends Attachment {} @@ -244,6 +253,10 @@ export function AgentGenerationWorkspaceDarkV2({ agentModeState = null, onAgentModeStateChange, onOpenCanvasEditor, + workspaceState = null, + onWorkspaceStateChange, + onResultPreviewUrlsChange, + topNavRightSlot, }: AgentGenerationWorkspaceDarkV2Props) { const rawChatId = useMemo( () => (chatId.startsWith("canvas-") ? chatId.replace("canvas-", "") : chatId), @@ -257,15 +270,38 @@ export function AgentGenerationWorkspaceDarkV2({ const hiddenPromptRef = useRef(null); const generatedImageKeysRef = useRef>(new Set()); const lastSyncedBrandModelUrlRef = useRef(null); + const workspaceStateSignatureRef = useRef(""); const [localAgentModeState, setLocalAgentModeState] = useState( () => createEmptyAgentModeWorkspaceState() ); - - const [input, setInput] = useState(""); - const [attachments, setAttachments] = useState>([]); + const normalizedIncomingWorkspaceState = useMemo( + () => + normalizeAgentGenerationProjectPersistedState({ + workflowState: {}, + workspaceState, + resultImageUrls: [], + }).workspaceState || createEmptyAgentGenerationProjectWorkspaceState(), + [workspaceState] + ); + + const [input, setInput] = useState(normalizedIncomingWorkspaceState.input); + const [attachments, setAttachments] = useState>( + normalizedIncomingWorkspaceState.attachments.map((item) => + item + ? { + url: item.url, + name: item.name || "参考图", + contentType: item.contentType || "image/png", + assetId: item.assetId || undefined, + } + : null + ) + ); const [activeAssignmentSlotIndex, setActiveAssignmentSlotIndex] = useState(null); const [slotUploading, setSlotUploading] = useState(null); - const [selectedModelId, setSelectedModelId] = useState("auto"); + const [selectedModelId, setSelectedModelId] = useState( + normalizedIncomingWorkspaceState.selectedModelId || "auto" + ); const [selectedResultUrl, setSelectedResultUrl] = useState(null); const [fullscreenPreview, setFullscreenPreview] = useState(null); const [batchDownloadState, setBatchDownloadState] = useState(null); @@ -284,9 +320,9 @@ export function AgentGenerationWorkspaceDarkV2({ lastQuery: null, }); const [imageParams, setImageParams] = useState({ - aspectRatio: "auto", - imageSize: "2K", - imageCount: 1, + aspectRatio: normalizedIncomingWorkspaceState.imageParams?.aspectRatio || "auto", + imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K", + imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1, }); const { data: historyData } = useSWR<{ messages: ChatMessage[] }>( @@ -514,6 +550,10 @@ export function AgentGenerationWorkspaceDarkV2({ }, [historyMessages, messages.length, setMessages]); useEffect(() => { + if (normalizedIncomingWorkspaceState.selectedModelId) { + setSelectedModelId(normalizedIncomingWorkspaceState.selectedModelId); + return; + } if (selectedAgentPreset?.defaultModelId) { setSelectedModelId(selectedAgentPreset.defaultModelId); return; @@ -521,7 +561,11 @@ export function AgentGenerationWorkspaceDarkV2({ if (modelsData?.defaultModelId) { setSelectedModelId(modelsData.defaultModelId); } - }, [modelsData?.defaultModelId, selectedAgentPreset?.defaultModelId]); + }, [ + modelsData?.defaultModelId, + normalizedIncomingWorkspaceState.selectedModelId, + selectedAgentPreset?.defaultModelId, + ]); // 初始化附件 useEffect(() => { @@ -761,6 +805,71 @@ export function AgentGenerationWorkspaceDarkV2({ const nextToolStep = activeToolStepIndex < toolSteps.length - 1 ? toolSteps[activeToolStepIndex + 1] : null; + useEffect(() => { + const incomingSignature = JSON.stringify(normalizedIncomingWorkspaceState); + if (incomingSignature === workspaceStateSignatureRef.current) { + return; + } + workspaceStateSignatureRef.current = incomingSignature; + setInput(normalizedIncomingWorkspaceState.input); + setAttachments( + normalizedIncomingWorkspaceState.attachments.map((item) => + item + ? { + url: item.url, + name: item.name || "参考图", + contentType: item.contentType || "image/png", + assetId: item.assetId || undefined, + } + : null + ) + ); + setSelectedModelId(normalizedIncomingWorkspaceState.selectedModelId || "auto"); + setImageParams({ + aspectRatio: normalizedIncomingWorkspaceState.imageParams?.aspectRatio || "auto", + imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K", + imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1, + }); + }, [normalizedIncomingWorkspaceState]); + + useEffect(() => { + const nextWorkspaceState = { + input, + attachments: attachments.map((item) => + item + ? { + url: item.url, + name: item.name || null, + contentType: item.contentType || null, + assetId: item.assetId || null, + } + : null + ), + selectedModelId, + imageParams: { + aspectRatio: imageParams.aspectRatio, + imageSize: imageParams.imageSize, + imageCount: imageParams.imageCount, + }, + }; + workspaceStateSignatureRef.current = JSON.stringify(nextWorkspaceState); + onWorkspaceStateChange?.(nextWorkspaceState); + }, [ + attachments, + imageParams.aspectRatio, + imageParams.imageCount, + imageParams.imageSize, + input, + onWorkspaceStateChange, + selectedModelId, + ]); + + useEffect(() => { + onResultPreviewUrlsChange?.( + Array.from(new Set(isReplacementPreset ? latestReplacementResultUrls : allGeneratedImages)) + ); + }, [allGeneratedImages, isReplacementPreset, latestReplacementResultUrls, onResultPreviewUrlsChange]); + useEffect(() => { if (!selectedResultUrl && latestGeneratedGroup?.imageUrls[0]) { setSelectedResultUrl(latestGeneratedGroup.imageUrls[0]); @@ -1323,7 +1432,7 @@ export function AgentGenerationWorkspaceDarkV2({ siteLogoDark={siteLogoDark} onBack={onBack} activeKey="agent" - rightSlot={
} + rightSlot={topNavRightSlot ||
} /> {/* Main Content */} diff --git a/components/agents/AgentProjectListSheet.tsx b/components/agents/AgentProjectListSheet.tsx new file mode 100644 index 000000000..b6c5b5606 --- /dev/null +++ b/components/agents/AgentProjectListSheet.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { formatDistanceToNow } from "date-fns"; +import { zhCN } from "date-fns/locale"; +import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { agentProjectService } from "@/services/agent-projects"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { cn } from "@/lib/utils"; +import { buildImageProxyUrl } from "@/lib/image-delivery"; +import type { AgentGenerationProject } from "@/types/agent"; + +interface AgentProjectListSheetProps { + currentProjectId?: string | null; + currentProjectTitle?: string | null; + onCreateProject: () => Promise | void; + onSelectProject: (projectId: string) => Promise | void; + onDeleteProject: (projectId: string) => Promise | void; +} + +export function AgentProjectListSheet({ + currentProjectId, + currentProjectTitle, + onCreateProject, + onSelectProject, + onDeleteProject, +}: AgentProjectListSheetProps) { + const [open, setOpen] = useState(false); + const [projects, setProjects] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [pendingDeleteId, setPendingDeleteId] = useState(null); + + const loadProjects = async () => { + setIsLoading(true); + try { + const result = await agentProjectService.getProjects(); + if (!result.success || !result.data) { + throw new Error(result.error || "加载失败"); + } + setProjects(result.data); + } catch (error) { + toast.error(error instanceof Error ? error.message : "加载生图项目失败"); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!open) return; + void loadProjects(); + }, [open, currentProjectId]); + + return ( + + + + + + + 生图项目列表 + + 这里保存独立于无限画布的生图项目,可随时继续生成。 + + + +
+ +
+ + +
+ {isLoading ? ( +
+ +
+ ) : projects.length > 0 ? ( + projects.map((project) => ( + + + )) + ) : ( +
+
还没有生图项目
+
点击上方按钮创建一个独立项目。
+
+ )} +
+
+
+
+ ); +} diff --git a/components/agents/AgentWorkflowPanelDarkV2.tsx b/components/agents/AgentWorkflowPanelDarkV2.tsx index fe351ee9b..de70b8d2d 100644 --- a/components/agents/AgentWorkflowPanelDarkV2.tsx +++ b/components/agents/AgentWorkflowPanelDarkV2.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, type ReactNode } from "react"; import { motion } from "framer-motion"; import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react"; import { toast } from "sonner"; @@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, flattenAgentWorkflowSteps, + flattenVisibleAgentWorkflowSteps, getEnabledAgentWorkflowStepTree, } from "@/lib/agents/workflow-ui-config"; import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; @@ -49,6 +50,7 @@ interface AgentWorkflowPanelDarkV2Props { siteLogo?: string | null; siteLogoDark?: string | null; showcaseConfig?: AgentWorkflowShowcaseConfig | null; + topNavRightSlot?: ReactNode; } function buildSelectionSummary(params: { @@ -56,7 +58,7 @@ function buildSelectionSummary(params: { selections: Partial>; }) { const result: AgentWorkflowComposerSelection[] = []; - for (const { step } of flattenAgentWorkflowSteps(params.config.steps)) { + for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) { const selectedOptionKey = params.selections[step.key]; if (!selectedOptionKey) continue; const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled); @@ -173,6 +175,7 @@ export function AgentWorkflowPanelDarkV2({ siteLogo, siteLogoDark, showcaseConfig, + topNavRightSlot, }: AgentWorkflowPanelDarkV2Props) { const brandOptions = useMemo( () => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], @@ -192,10 +195,10 @@ export function AgentWorkflowPanelDarkV2({ }, [config]); const flattenedResolvedSteps = useMemo( () => - flattenAgentWorkflowSteps(resolvedConfig.steps).filter( + flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter( ({ step }) => Array.isArray(step.options) && step.options.length > 0 ), - [resolvedConfig.steps] + [resolvedConfig.steps, selections] ); const summary = useMemo( @@ -239,6 +242,7 @@ export function AgentWorkflowPanelDarkV2({ siteLogo={siteLogo} siteLogoDark={siteLogoDark} activeKey="agent" + rightSlot={topNavRightSlot} />
diff --git a/lib/agents/generation-project-state.ts b/lib/agents/generation-project-state.ts new file mode 100644 index 000000000..bf13ba29b --- /dev/null +++ b/lib/agents/generation-project-state.ts @@ -0,0 +1,159 @@ +import { + createEmptyAgentModeWorkspaceState, + normalizeAgentModeWorkspaceState, +} from "@/lib/agents/mode-workspace-state"; +import { + createEmptyAgentWorkflowMaterialSelections, + normalizeAgentWorkflowMaterialSelections, +} from "@/lib/agents/workflow-material-config"; +import type { + AgentGenerationProjectPersistedState, + AgentGenerationProjectWorkspaceState, + AgentWorkflowPersistedState, +} from "@/types/agent"; + +export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerationProjectWorkspaceState { + return { + input: "", + attachments: [], + selectedModelId: "auto", + imageParams: { + aspectRatio: "auto", + imageSize: "2K", + imageCount: 1, + }, + }; +} + +export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersistedState { + return { + inputs: { + primarySku: "", + secondarySku: "", + brand: "", + store: "", + }, + materials: createEmptyAgentWorkflowMaterialSelections(), + selections: {}, + selectedBrandModel: null, + selectedAgentPresetId: null, + currentStage: "workflow", + agentModeState: createEmptyAgentModeWorkspaceState(), + }; +} + +export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerationProjectPersistedState { + return { + workflowState: createEmptyAgentWorkflowPersistedState(), + workspaceState: createEmptyAgentGenerationProjectWorkspaceState(), + resultImageUrls: [], + }; +} + +function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspaceState { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + + const attachments = Array.isArray(raw.attachments) + ? raw.attachments + .map((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return null; + } + const record = item as Record; + const url = String(record.url || "").trim(); + if (!url) return null; + return { + url, + name: String(record.name || "").trim() || null, + contentType: String(record.contentType || "").trim() || null, + assetId: String(record.assetId || "").trim() || null, + }; + }) + .slice(0, 16) + : []; + + const rawImageParams = + raw.imageParams && typeof raw.imageParams === "object" && !Array.isArray(raw.imageParams) + ? (raw.imageParams as Record) + : {}; + + const imageCount = Number(rawImageParams.imageCount); + + return { + input: String(raw.input || "").trim(), + attachments, + selectedModelId: String(raw.selectedModelId || "").trim() || "auto", + imageParams: { + aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto", + imageSize: String(rawImageParams.imageSize || "").trim() || "2K", + imageCount: Number.isFinite(imageCount) ? Math.max(1, Math.min(8, Math.round(imageCount))) : 1, + }, + }; +} + +function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + + return { + inputs: { + primarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).primarySku || "" : "").trim(), + secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).secondarySku || "" : "").trim(), + brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).brand || "" : "").trim(), + store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).store || "" : "").trim(), + }, + materials: normalizeAgentWorkflowMaterialSelections(raw.materials), + selections: + raw.selections && typeof raw.selections === "object" && !Array.isArray(raw.selections) + ? (raw.selections as Record) + : {}, + selectedBrandModel: + raw.selectedBrandModel && typeof raw.selectedBrandModel === "object" && !Array.isArray(raw.selectedBrandModel) + ? { + ...(raw.selectedBrandModel as Record), + brandName: String((raw.selectedBrandModel as Record).brandName || "").trim(), + id: String((raw.selectedBrandModel as Record).id || "").trim(), + imageUrl: String((raw.selectedBrandModel as Record).imageUrl || "").trim(), + assetId: String((raw.selectedBrandModel as Record).assetId || "").trim() || null, + displayName: String((raw.selectedBrandModel as Record).displayName || "").trim() || null, + mimeType: String((raw.selectedBrandModel as Record).mimeType || "").trim() || null, + sortOrder: Number.isFinite(Number((raw.selectedBrandModel as Record).sortOrder)) + ? Math.max(0, Math.min(999, Math.round(Number((raw.selectedBrandModel as Record).sortOrder)))) + : 0, + } + : null, + selectedAgentPresetId: String(raw.selectedAgentPresetId || "").trim() || null, + currentStage: raw.currentStage === "mode" ? "mode" : "workflow", + agentModeState: normalizeAgentModeWorkspaceState(raw.agentModeState), + }; +} + +export function normalizeAgentGenerationProjectPersistedState( + value: unknown +): AgentGenerationProjectPersistedState { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + + const resultImageUrls = Array.isArray(raw.resultImageUrls) + ? Array.from( + new Set( + raw.resultImageUrls + .map((item) => String(item || "").trim()) + .filter((item) => /^https?:\/\//i.test(item)) + ) + ).slice(0, 64) + : []; + + return { + workflowState: normalizeWorkflowState(raw.workflowState), + workspaceState: normalizeWorkspaceState(raw.workspaceState), + resultImageUrls, + }; +} diff --git a/lib/agents/workflow-composer.ts b/lib/agents/workflow-composer.ts index 1ded53ba3..7ea6f4b9c 100644 --- a/lib/agents/workflow-composer.ts +++ b/lib/agents/workflow-composer.ts @@ -1,6 +1,6 @@ import { defaultAgentWorkflowUiConfig, - flattenAgentWorkflowSteps, + flattenVisibleAgentWorkflowSteps, getEnabledAgentWorkflowStepTree, } from "@/lib/agents/workflow-ui-config"; import type { @@ -27,8 +27,9 @@ export function buildAgentWorkflowComposerContext(params: { selections: Partial>; }): AgentWorkflowComposerContext | null { const config = params.config || defaultAgentWorkflowUiConfig; - const steps = flattenAgentWorkflowSteps( - getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []) + const steps = flattenVisibleAgentWorkflowSteps( + getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []), + params.selections ) .map(({ step }) => step) .filter((step) => Array.isArray(step.options) && step.options.length > 0); @@ -223,8 +224,9 @@ export function getAgentWorkflowProgress(params: { selections: Partial>; }) { const config = params.config || defaultAgentWorkflowUiConfig; - const enabledSteps = flattenAgentWorkflowSteps( - getEnabledAgentWorkflowStepTree(config.steps || []) + const enabledSteps = flattenVisibleAgentWorkflowSteps( + getEnabledAgentWorkflowStepTree(config.steps || []), + params.selections ) .map(({ step }) => step) .filter((step) => Array.isArray(step.options) && step.options.length > 0); diff --git a/lib/agents/workflow-ui-config.ts b/lib/agents/workflow-ui-config.ts index 0df185726..536752bb8 100644 --- a/lib/agents/workflow-ui-config.ts +++ b/lib/agents/workflow-ui-config.ts @@ -153,6 +153,47 @@ export function flattenAgentWorkflowSteps( ]); } +export function flattenVisibleAgentWorkflowSteps( + steps: AgentWorkflowStepUiConfig[], + selections: Partial>, + depth = 1, + parentKeys: string[] = [], + ancestorsSatisfied = true +): FlattenedAgentWorkflowStep[] { + return [...steps] + .sort((left, right) => left.sortOrder - right.sortOrder) + .flatMap((step) => { + const normalizedStep = { + ...step, + options: [...(step.options || [])].sort((left, right) => left.sortOrder - right.sortOrder), + children: [...(step.children || [])].sort((left, right) => left.sortOrder - right.sortOrder), + }; + + if (!ancestorsSatisfied) { + return [] as FlattenedAgentWorkflowStep[]; + } + + const currentEntry: FlattenedAgentWorkflowStep = { + step: normalizedStep, + depth, + parentKeys, + }; + const currentSatisfied = + normalizedStep.options.length === 0 || Boolean(String(selections[normalizedStep.key] || "").trim()); + + return [ + currentEntry, + ...flattenVisibleAgentWorkflowSteps( + normalizedStep.children, + selections, + depth + 1, + [...parentKeys, normalizedStep.key], + currentSatisfied + ), + ]; + }); +} + export function findAgentWorkflowStepByKey( steps: AgentWorkflowStepUiConfig[], stepKey: string diff --git a/services/agent-projects/index.ts b/services/agent-projects/index.ts new file mode 100644 index 000000000..74d55335d --- /dev/null +++ b/services/agent-projects/index.ts @@ -0,0 +1,80 @@ +import type { + AgentGenerationProject, + AgentGenerationProjectPersistedState, +} from "@/types/agent"; + +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +export const agentProjectService = { + async getProjects(): Promise> { + try { + const response = await fetch("/api/agent-projects", { cache: "no-store" }); + return await response.json(); + } catch { + return { success: false, error: "获取生图项目列表失败" }; + } + }, + + async getProject(id: string): Promise> { + try { + const response = await fetch(`/api/agent-projects/${id}`, { cache: "no-store" }); + return await response.json(); + } catch { + return { success: false, error: "获取生图项目失败" }; + } + }, + + async createProject(payload: { + title?: string; + description?: string | null; + thumbnail?: string | null; + projectState?: AgentGenerationProjectPersistedState; + }): Promise> { + try { + const response = await fetch("/api/agent-projects", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + return await response.json(); + } catch { + return { success: false, error: "创建生图项目失败" }; + } + }, + + async updateProject( + id: string, + payload: { + title?: string; + description?: string | null; + thumbnail?: string | null; + projectState?: AgentGenerationProjectPersistedState; + } + ): Promise> { + try { + const response = await fetch(`/api/agent-projects/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + return await response.json(); + } catch { + return { success: false, error: "保存生图项目失败" }; + } + }, + + async deleteProject(id: string): Promise { + try { + const response = await fetch(`/api/agent-projects/${id}`, { + method: "DELETE", + }); + return await response.json(); + } catch { + return { success: false, error: "删除生图项目失败" }; + } + }, +}; diff --git a/types/agent.ts b/types/agent.ts index 0bec42cf6..67ec6af9a 100644 --- a/types/agent.ts +++ b/types/agent.ts @@ -238,6 +238,26 @@ export interface AgentModeWorkspaceState { skuReplacement: AgentSkuReplacementState; } +export interface AgentGenerationProjectWorkspaceAttachment { + url: string; + name?: string | null; + contentType?: string | null; + assetId?: string | null; +} + +export interface AgentGenerationProjectImageParams { + aspectRatio?: string; + imageSize?: string; + imageCount?: number; +} + +export interface AgentGenerationProjectWorkspaceState { + input: string; + attachments: Array; + selectedModelId?: string | null; + imageParams?: AgentGenerationProjectImageParams; +} + export interface AgentWorkflowPersistedState { inputs: { primarySku: string; @@ -253,6 +273,23 @@ export interface AgentWorkflowPersistedState { agentModeState?: AgentModeWorkspaceState; } +export interface AgentGenerationProjectPersistedState { + workflowState: AgentWorkflowPersistedState; + workspaceState?: AgentGenerationProjectWorkspaceState; + resultImageUrls?: string[]; +} + +export interface AgentGenerationProject { + id: string; + userId: string; + title: string; + description?: string | null; + thumbnail?: string | null; + projectState: AgentGenerationProjectPersistedState; + createdAt: Date | string; + updatedAt: Date | string; +} + export interface AgentCategoryTreeConfig { productCategories: string[]; audienceTags: string[];