diff --git a/app/api/psd-templates/render/route.ts b/app/api/psd-templates/render/route.ts index 234ebaa16..3578ae8ec 100644 --- a/app/api/psd-templates/render/route.ts +++ b/app/api/psd-templates/render/route.ts @@ -1,22 +1,19 @@ import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; -import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; -import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement"; +import { + AGENT_PSD_RENDER_TASK_POLL_AFTER_MS, + agentPsdTemplateRenderRequestSchema, +} from "@/lib/agents/psd-render-task-contract"; +import { + createAgentPsdRenderTask, + enqueueAgentPsdRenderTask, + getAgentPsdRenderTaskForUser, +} from "@/lib/agents/psd-render-task-queue"; import { describeRemoteAssetLocation } from "@/lib/agents/psd-remote-asset"; import { type AgentPsdTemplateRenderResponse, - getAgentPsdTemplateRuntimeIssue, - getEditableAgentPsdTemplateLayers, - isAgentPsdImageLayerSlot, - isAgentPsdTemplateReady, - isAgentPsdTemplateRuntimeReady, - isAgentPsdTextLayerSlot, + type AgentPsdTemplateRenderStatus, } from "@/lib/agents/psd-template-config"; -import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer"; -import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair"; -import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; -import { normalizeHexColor } from "@/lib/agents/psd-text-layout"; import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage, @@ -25,24 +22,6 @@ import { export const runtime = "nodejs"; -const renderSchema = z.object({ - templateId: z.string().trim().min(1), - sourceImageUrl: z.string().trim().optional().nullable(), - imageAssignments: z.record(z.string(), z.string()).default({}), - imagePlacements: z - .record( - z.string(), - z.object({ - scale: z.number().positive().max(8).optional(), - offsetX: z.number().optional(), - offsetY: z.number().optional(), - }) - ) - .default({}), - textAssignments: z.record(z.string(), z.string()).default({}), - textColorAssignments: z.record(z.string(), z.string()).default({}), -}); - function jsonError(status: number, error: string) { const payload: AgentPsdTemplateRenderResponse = { success: false, @@ -51,113 +30,102 @@ function jsonError(status: number, error: string) { return NextResponse.json(payload, { status }); } +function normalizeTaskStatus( + status: string +): Extract { + return status === "running" ? "running" : "pending"; +} + +export async function GET(request: NextRequest) { + const session = await auth().catch(() => null); + if (!session?.user?.id) { + return jsonError(401, "未登录"); + } + + const { searchParams } = new URL(request.url); + const taskId = String(searchParams.get("taskId") || "").trim(); + if (!taskId) { + return jsonError(400, "缺少 taskId 参数"); + } + + try { + const task = await getAgentPsdRenderTaskForUser({ + taskId, + userId: session.user.id, + }); + if (!task) { + return jsonError(404, "导出任务不存在"); + } + + if (task.status === "succeeded") { + const result = task.result as AgentPsdTemplateRenderResponse["result"] | null; + if (!result?.url) { + return NextResponse.json( + { + success: false, + status: "failed", + taskId: task.id, + error: "导出任务已完成,但未返回结果图", + } satisfies AgentPsdTemplateRenderResponse + ); + } + + return NextResponse.json({ + success: true, + status: "succeeded", + taskId: task.id, + result, + } satisfies AgentPsdTemplateRenderResponse); + } + + if (task.status === "failed") { + return NextResponse.json({ + success: false, + status: "failed", + taskId: task.id, + error: task.error || "PSD 模板渲染失败", + } satisfies AgentPsdTemplateRenderResponse); + } + + return NextResponse.json({ + success: true, + processing: true, + status: normalizeTaskStatus(task.status), + taskId: task.id, + pollAfterMs: AGENT_PSD_RENDER_TASK_POLL_AFTER_MS, + } satisfies AgentPsdTemplateRenderResponse); + } catch (error) { + console.error("[PSD Templates] render status failed:", { taskId, error }); + return jsonError(500, "查询 PSD 导出任务失败"); + } +} + export async function POST(request: NextRequest) { const session = await auth().catch(() => null); if (!session?.user?.id) { return jsonError(401, "未登录"); } - let body: z.infer; + let body: ReturnType; try { - body = renderSchema.parse(await request.json()); + body = agentPsdTemplateRenderRequestSchema.parse(await request.json()); } catch { return jsonError(400, "渲染参数不合法"); } try { - const templates = await readAgentPsdTemplates(); - const template = templates.find( - (entry) => - entry.id === body.templateId && - entry.status === "active" && - isAgentPsdTemplateReady(entry) - ); - - if (!template) { - return jsonError(404, "PSD 模板不存在或未启用"); - } - - const fonts = await readAgentPsdFonts(); - const resolvedTemplate = repairAgentPsdTemplateFontBindings(template, fonts); - if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) { - return jsonError( - 409, - getAgentPsdTemplateRuntimeIssue(resolvedTemplate) || - "PSD 模板资源不完整,请在后台重新解析后再导出" - ); - } - - const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate); - if (editableLayers.length === 0) { - return jsonError(400, "当前模板未配置可编辑图层"); - } - - const fontMap = new Map(fonts.map((font) => [font.id, font])); - const missingFontLayer = editableLayers.find( - (layer) => isAgentPsdTextLayerSlot(layer) && layer.fontId && !fontMap.has(layer.fontId) - ); - if (missingFontLayer) { - return jsonError(400, `文字图层“${missingFontLayer.layerName}”引用的字体已不存在`); - } - - const imageAssignments: Record = {}; - const imagePlacements: Record> = {}; - const textAssignments: Record = {}; - const textColorAssignments: Record = {}; - for (const layer of editableLayers) { - if (isAgentPsdImageLayerSlot(layer)) { - const explicitUrl = String(body.imageAssignments[layer.id] || "").trim(); - if (explicitUrl) { - imageAssignments[layer.id] = explicitUrl; - imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(body.imagePlacements[layer.id]); - continue; - } - - if (layer.isDefaultResultSource && body.sourceImageUrl?.trim()) { - imageAssignments[layer.id] = body.sourceImageUrl.trim(); - imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(body.imagePlacements[layer.id]); - } - } - - if (isAgentPsdTextLayerSlot(layer)) { - const nextText = String(body.textAssignments[layer.id] || layer.defaultText || "").trim(); - if (nextText) { - textAssignments[layer.id] = nextText; - } - textColorAssignments[layer.id] = normalizeHexColor( - body.textColorAssignments[layer.id] || layer.textColor, - layer.textColor - ); - } - } - - const missingRequiredLayer = editableLayers.find( - (layer) => - layer.isRequired && - (isAgentPsdImageLayerSlot(layer) - ? !imageAssignments[layer.id] - : !textAssignments[layer.id]) - ); - if (missingRequiredLayer) { - return jsonError(400, `缺少必传素材:${missingRequiredLayer.layerName}`); - } - - const uploaded = await renderAgentPsdTemplate({ - template: resolvedTemplate, - imageAssignments, - imagePlacements, - textAssignments, - textColorAssignments, - fonts, + const task = await createAgentPsdRenderTask({ + userId: session.user.id, + request: body, }); + await enqueueAgentPsdRenderTask(task.id); return NextResponse.json({ success: true, - result: { - url: uploaded.url, - pathname: uploaded.pathname, - contentType: uploaded.contentType, - }, + processing: true, + status: normalizeTaskStatus(task.status), + taskId: task.id, + pollAfterMs: AGENT_PSD_RENDER_TASK_POLL_AFTER_MS, } satisfies AgentPsdTemplateRenderResponse); } catch (error) { console.error("[PSD Templates] render failed:", { diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 2574bc7f8..3c489c48e 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -80,6 +80,7 @@ import { normalizeHexColor, resolveTextLayout, } from "@/lib/agents/psd-text-layout"; +import { requestLongTaskResult } from "@/lib/client/long-task"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from "@/lib/request-error"; @@ -2203,26 +2204,19 @@ export function AgentPsdTemplateEditorDialog({ try { setIsRendering(true); - const response = await fetch("/api/psd-templates/render", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ + setRenderedResultUrl(""); + const payload = (await requestLongTaskResult({ + endpoint: "/api/psd-templates/render", + timeoutMs: 8 * 60 * 1000, + payload: { templateId: selectedTemplate.id, sourceImageUrl: sourceImageUrl || "", imageAssignments: effectiveImageAssignments, imagePlacements: effectiveImagePlacements, textAssignments: effectiveTextAssignments, textColorAssignments: effectiveTextColorAssignments, - }), - }); - - const payload = await parseActionJsonResponse( - response, - "PSD 模板渲染失败", - getFriendlyRateLimitMessage("当前 PSD 渲染请求过于频繁,请稍后再试") - ); + }, + })) as AgentPsdTemplateRenderResponse; if (!payload?.success || !payload.result?.url) { throw new Error(payload?.error || "PSD 模板渲染失败"); } diff --git a/lib/agents/psd-render-service.ts b/lib/agents/psd-render-service.ts new file mode 100644 index 000000000..dc91021df --- /dev/null +++ b/lib/agents/psd-render-service.ts @@ -0,0 +1,123 @@ +import "server-only"; + +import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; +import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement"; +import { + type AgentPsdTemplateRenderRequest, +} from "@/lib/agents/psd-render-task-contract"; +import { + type AgentPsdTemplateRenderResult, + getAgentPsdTemplateRuntimeIssue, + getEditableAgentPsdTemplateLayers, + isAgentPsdImageLayerSlot, + isAgentPsdTemplateReady, + isAgentPsdTemplateRuntimeReady, + isAgentPsdTextLayerSlot, +} from "@/lib/agents/psd-template-config"; +import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer"; +import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair"; +import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; +import { normalizeHexColor } from "@/lib/agents/psd-text-layout"; + +export async function executeAgentPsdTemplateRenderRequest( + body: AgentPsdTemplateRenderRequest +): Promise { + const templates = await readAgentPsdTemplates(); + const template = templates.find( + (entry) => + entry.id === body.templateId && + entry.status === "active" && + isAgentPsdTemplateReady(entry) + ); + + if (!template) { + throw new Error("PSD 模板不存在或未启用"); + } + + const fonts = await readAgentPsdFonts(); + const resolvedTemplate = repairAgentPsdTemplateFontBindings(template, fonts); + if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) { + throw new Error( + getAgentPsdTemplateRuntimeIssue(resolvedTemplate) || + "PSD 模板资源不完整,请在后台重新解析后再导出" + ); + } + + const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate); + if (editableLayers.length === 0) { + throw new Error("当前模板未配置可编辑图层"); + } + + const fontMap = new Map(fonts.map((font) => [font.id, font])); + const missingFontLayer = editableLayers.find( + (layer) => isAgentPsdTextLayerSlot(layer) && layer.fontId && !fontMap.has(layer.fontId) + ); + if (missingFontLayer) { + throw new Error(`文字图层“${missingFontLayer.layerName}”引用的字体已不存在`); + } + + const imageAssignments: Record = {}; + const imagePlacements: Record< + string, + ReturnType + > = {}; + const textAssignments: Record = {}; + const textColorAssignments: Record = {}; + + for (const layer of editableLayers) { + if (isAgentPsdImageLayerSlot(layer)) { + const explicitUrl = String(body.imageAssignments[layer.id] || "").trim(); + if (explicitUrl) { + imageAssignments[layer.id] = explicitUrl; + imagePlacements[layer.id] = normalizeAgentPsdImagePlacement( + body.imagePlacements[layer.id] + ); + continue; + } + + if (layer.isDefaultResultSource && body.sourceImageUrl?.trim()) { + imageAssignments[layer.id] = body.sourceImageUrl.trim(); + imagePlacements[layer.id] = normalizeAgentPsdImagePlacement( + body.imagePlacements[layer.id] + ); + } + } + + if (isAgentPsdTextLayerSlot(layer)) { + const nextText = String(body.textAssignments[layer.id] || layer.defaultText || "").trim(); + if (nextText) { + textAssignments[layer.id] = nextText; + } + textColorAssignments[layer.id] = normalizeHexColor( + body.textColorAssignments[layer.id] || layer.textColor, + layer.textColor + ); + } + } + + const missingRequiredLayer = editableLayers.find( + (layer) => + layer.isRequired && + (isAgentPsdImageLayerSlot(layer) + ? !imageAssignments[layer.id] + : !textAssignments[layer.id]) + ); + if (missingRequiredLayer) { + throw new Error(`缺少必传素材:${missingRequiredLayer.layerName}`); + } + + const uploaded = await renderAgentPsdTemplate({ + template: resolvedTemplate, + imageAssignments, + imagePlacements, + textAssignments, + textColorAssignments, + fonts, + }); + + return { + url: uploaded.url, + pathname: uploaded.pathname, + contentType: uploaded.contentType, + }; +} diff --git a/lib/agents/psd-render-task-contract.ts b/lib/agents/psd-render-task-contract.ts new file mode 100644 index 000000000..2e633b4cb --- /dev/null +++ b/lib/agents/psd-render-task-contract.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const AGENT_PSD_RENDER_TASK_POLL_AFTER_MS = 1500; + +export const agentPsdTemplateRenderRequestSchema = z.object({ + templateId: z.string().trim().min(1), + sourceImageUrl: z.string().trim().optional().nullable(), + imageAssignments: z.record(z.string(), z.string()).default({}), + imagePlacements: z + .record( + z.string(), + z.object({ + scale: z.number().positive().max(8).optional(), + offsetX: z.number().optional(), + offsetY: z.number().optional(), + }) + ) + .default({}), + textAssignments: z.record(z.string(), z.string()).default({}), + textColorAssignments: z.record(z.string(), z.string()).default({}), +}); + +export type AgentPsdTemplateRenderRequest = z.infer< + typeof agentPsdTemplateRenderRequestSchema +>; diff --git a/lib/agents/psd-render-task-queue.ts b/lib/agents/psd-render-task-queue.ts new file mode 100644 index 000000000..d9df1498d --- /dev/null +++ b/lib/agents/psd-render-task-queue.ts @@ -0,0 +1,243 @@ +import "server-only"; + +import { and, asc, eq, lt, notInArray, sql } from "drizzle-orm"; +import { executeAgentPsdTemplateRenderRequest } from "@/lib/agents/psd-render-service"; +import type { AgentPsdTemplateRenderRequest } from "@/lib/agents/psd-render-task-contract"; +import { db } from "@/lib/db"; +import { agentPsdRenderTask } from "@/lib/db/schema"; + +type AgentPsdRenderTaskStatus = "pending" | "running" | "succeeded" | "failed"; + +interface QueueState { + pending: string[]; + pendingSet: Set; + running: Set; + draining: boolean; +} + +const DEFAULT_CONCURRENCY = 1; +const STALE_RUNNING_MS = 15 * 60 * 1000; +const queueStateKey = "__agent_psd_render_task_queue_state__"; + +function getQueueConcurrency(): number { + const raw = Number.parseInt(process.env.AGENT_PSD_RENDER_TASK_CONCURRENCY || "", 10); + if (Number.isFinite(raw) && raw > 0) { + return Math.min(raw, 3); + } + return DEFAULT_CONCURRENCY; +} + +function getQueueState(): QueueState { + const globalObj = globalThis as typeof globalThis & { + [queueStateKey]?: QueueState; + }; + + if (!globalObj[queueStateKey]) { + globalObj[queueStateKey] = { + pending: [], + pendingSet: new Set(), + running: new Set(), + draining: false, + }; + } + + return globalObj[queueStateKey] as QueueState; +} + +function enqueueLocalTask(taskId: string) { + const state = getQueueState(); + if (!taskId || state.running.has(taskId) || state.pendingSet.has(taskId)) { + return; + } + state.pending.push(taskId); + state.pendingSet.add(taskId); +} + +async function claimNextPendingTask(): Promise { + const state = getQueueState(); + while (state.pending.length > 0) { + const taskId = state.pending.shift(); + if (!taskId) continue; + state.pendingSet.delete(taskId); + if (!state.running.has(taskId)) { + return taskId; + } + } + + const runningIds = Array.from(state.running); + const whereCondition = + runningIds.length > 0 + ? and(eq(agentPsdRenderTask.status, "pending"), notInArray(agentPsdRenderTask.id, runningIds)) + : eq(agentPsdRenderTask.status, "pending"); + + const rows = await db + .select({ id: agentPsdRenderTask.id }) + .from(agentPsdRenderTask) + .where(whereCondition) + .orderBy(asc(agentPsdRenderTask.createdAt)) + .limit(1); + + return rows[0]?.id ?? null; +} + +async function runSingleTask(taskId: string) { + const state = getQueueState(); + state.running.add(taskId); + + try { + const [task] = await db + .update(agentPsdRenderTask) + .set({ + status: "running", + startedAt: new Date(), + completedAt: null, + error: null, + updatedAt: new Date(), + attempts: sql`${agentPsdRenderTask.attempts} + 1`, + }) + .where(and(eq(agentPsdRenderTask.id, taskId), eq(agentPsdRenderTask.status, "pending"))) + .returning({ + id: agentPsdRenderTask.id, + request: agentPsdRenderTask.request, + }); + + if (!task) { + return; + } + + const result = await executeAgentPsdTemplateRenderRequest( + task.request as AgentPsdTemplateRenderRequest + ); + await db + .update(agentPsdRenderTask) + .set({ + status: "succeeded", + result: result as unknown as Record, + error: null, + completedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(agentPsdRenderTask.id, taskId)); + } catch (error) { + await db + .update(agentPsdRenderTask) + .set({ + status: "failed", + error: error instanceof Error ? error.message : String(error), + completedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(agentPsdRenderTask.id, taskId)); + } finally { + state.running.delete(taskId); + void drainAgentPsdRenderTaskQueue(); + } +} + +export async function drainAgentPsdRenderTaskQueue() { + const state = getQueueState(); + if (state.draining) return; + + state.draining = true; + try { + const maxConcurrent = getQueueConcurrency(); + while (state.running.size < maxConcurrent) { + const taskId = await claimNextPendingTask(); + if (!taskId) break; + if (state.running.has(taskId)) continue; + void runSingleTask(taskId); + } + } finally { + state.draining = false; + } +} + +export async function enqueueAgentPsdRenderTask(taskId: string) { + enqueueLocalTask(taskId); + await drainAgentPsdRenderTaskQueue(); +} + +export async function createAgentPsdRenderTask(params: { + userId: string; + request: AgentPsdTemplateRenderRequest; +}) { + const [task] = await db + .insert(agentPsdRenderTask) + .values({ + userId: params.userId, + status: "pending", + request: params.request as unknown as Record, + attempts: 0, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning({ + id: agentPsdRenderTask.id, + status: agentPsdRenderTask.status, + createdAt: agentPsdRenderTask.createdAt, + }); + + return task; +} + +export async function getAgentPsdRenderTaskForUser(params: { + taskId: string; + userId: string; +}) { + const rows = await db + .select({ + id: agentPsdRenderTask.id, + status: agentPsdRenderTask.status, + request: agentPsdRenderTask.request, + result: agentPsdRenderTask.result, + error: agentPsdRenderTask.error, + attempts: agentPsdRenderTask.attempts, + startedAt: agentPsdRenderTask.startedAt, + completedAt: agentPsdRenderTask.completedAt, + createdAt: agentPsdRenderTask.createdAt, + updatedAt: agentPsdRenderTask.updatedAt, + }) + .from(agentPsdRenderTask) + .where( + and(eq(agentPsdRenderTask.id, params.taskId), eq(agentPsdRenderTask.userId, params.userId)) + ) + .limit(1); + + const task = rows[0]; + if (!task) { + return null; + } + + if (task.status === "running" && task.startedAt) { + const staleBefore = new Date(Date.now() - STALE_RUNNING_MS); + if (task.startedAt < staleBefore) { + await db + .update(agentPsdRenderTask) + .set({ + status: "pending", + startedAt: null, + completedAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(agentPsdRenderTask.id, task.id), + eq(agentPsdRenderTask.status, "running"), + lt(agentPsdRenderTask.startedAt, staleBefore) + ) + ); + enqueueLocalTask(task.id); + await drainAgentPsdRenderTaskQueue(); + task.status = "pending" as AgentPsdRenderTaskStatus; + task.startedAt = null; + task.completedAt = null; + } + } + + if (task.status === "pending") { + enqueueLocalTask(task.id); + await drainAgentPsdRenderTaskQueue(); + } + + return task; +} diff --git a/lib/agents/psd-template-config.ts b/lib/agents/psd-template-config.ts index e73db32dc..0a590b9d8 100644 --- a/lib/agents/psd-template-config.ts +++ b/lib/agents/psd-template-config.ts @@ -133,8 +133,18 @@ export interface AgentPsdTemplateRenderResult { contentType?: string; } +export type AgentPsdTemplateRenderStatus = + | "pending" + | "running" + | "succeeded" + | "failed"; + export interface AgentPsdTemplateRenderResponse { success: boolean; + processing?: boolean; + status?: AgentPsdTemplateRenderStatus; + taskId?: string; + pollAfterMs?: number; result?: AgentPsdTemplateRenderResult; error?: string; } diff --git a/lib/db/migrations/0068_add_agent_psd_render_tasks.sql b/lib/db/migrations/0068_add_agent_psd_render_tasks.sql new file mode 100644 index 000000000..a3a305595 --- /dev/null +++ b/lib/db/migrations/0068_add_agent_psd_render_tasks.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "AgentPsdRenderTask" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "userId" uuid NOT NULL REFERENCES "User"("id") ON DELETE cascade, + "status" varchar(16) NOT NULL DEFAULT 'pending', + "request" jsonb NOT NULL, + "result" jsonb, + "error" text, + "attempts" integer NOT NULL DEFAULT 0, + "startedAt" timestamp, + "completedAt" timestamp, + "createdAt" timestamp NOT NULL DEFAULT now(), + "updatedAt" timestamp NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS "AgentPsdRenderTask_user_status_created_idx" + ON "AgentPsdRenderTask"("userId", "status", "createdAt"); + +CREATE INDEX IF NOT EXISTS "AgentPsdRenderTask_status_created_idx" + ON "AgentPsdRenderTask"("status", "createdAt"); diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index e08e7e32a..db3eaf4af 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -379,6 +379,13 @@ "when": 1775462400000, "tag": "0067_add_ai_cad_multi_workspace", "breakpoints": true + }, + { + "idx": 54, + "version": "7", + "when": 1775782800000, + "tag": "0068_add_agent_psd_render_tasks", + "breakpoints": true } ] } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index e2a714dbf..a1b395b56 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -739,6 +739,26 @@ export const canvasImageTask = pgTable("CanvasImageTask", { export type CanvasImageTask = InferSelectModel; +export const agentPsdRenderTask = pgTable("AgentPsdRenderTask", { + id: uuid("id").primaryKey().notNull().defaultRandom(), + userId: uuid("userId") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + status: varchar("status", { length: 16 }) + .notNull() + .default("pending"), // pending | running | succeeded | failed + request: jsonb("request").notNull(), + result: jsonb("result"), + error: text("error"), + attempts: integer("attempts").notNull().default(0), + startedAt: timestamp("startedAt"), + completedAt: timestamp("completedAt"), + createdAt: timestamp("createdAt").notNull().defaultNow(), + updatedAt: timestamp("updatedAt").notNull().defaultNow(), +}); + +export type AgentPsdRenderTask = InferSelectModel; + // 画布节点表(可选,用于更细粒度的查询) export const canvasNode = pgTable("CanvasNode", { id: uuid("id").primaryKey().notNull().defaultRandom(),