fix: fallback ai detail plan on planner abort timeout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-22 03:19:18 +08:00
parent 18149083d4
commit 153efc8f96

View File

@ -9,7 +9,26 @@ import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
export const runtime = "nodejs";
export const maxDuration = 60;
export const maxDuration = 180;
function isAbortError(error: unknown): boolean {
const target = error as {
name?: unknown;
message?: unknown;
code?: unknown;
ABORT_ERR?: unknown;
};
const name = String(target?.name || "");
const message = String(target?.message || "");
const code = Number(target?.code);
const abortCode = Number(target?.ABORT_ERR);
return (
name === "AbortError" ||
code === 20 ||
abortCode === 20 ||
/aborted|abort/i.test(message)
);
}
function supportsVisionPlanning(model: {
supportsVision?: boolean | null;
@ -1054,45 +1073,77 @@ export async function POST(request: Request) {
? referenceImages.map((url) => ({ type: "image" as const, image: url }))
: [];
const { text } = await generateText({
model,
messages: [
{
role: "user",
content: [
{
type: "text",
text: userInput,
let text = "";
try {
const result = await generateText({
model,
messages: [
{
role: "user",
content: [
{
type: "text",
text: userInput,
},
...imageInputs,
],
},
],
system:
`你是资深电商视觉策划与提示词工程师。请为“AI详情生成”产出可直接用于生图的分镜提示词和文案。\n` +
`硬性要求:\n` +
`1) 输出严格 JSON,不要其他文本。\n` +
`2) 保持同一产品一致性(结构、颜色、材质、Logo、比例)。\n` +
`3) 所有分镜风格统一、构图统一、文案排版风格统一。主标题/副标题/正文的字体家族、字重、字号层级、行距必须全套统一。\n` +
`4) 每屏尺寸固定 790x1300 像素,输出提示词必须显式包含该尺寸约束。\n` +
`5) 结构遵循“黄金五屏原则”:屏1-2核心卖点,屏3-4差异强化,屏5证据背书;更多屏作为延展。\n` +
`6) 每屏都要有 FABE 闭环(B利益/F特征/A优势/E证据),且文案符合电商转化表达。\n` +
`7) 文案用词需贴近用户语言,不写空泛形容词,不泄露系统流程。\n` +
`8) 对每屏输出 useModel 与 modelReason:useModel=true 表示该屏必须融入固定模特图人物;false 表示该屏禁止出现人物。\n` +
`9) 对每屏输出 screenType(A/B/C):A=技术公信,B=场景共鸣,C=细节佐证。\n` +
`10) 主标题必须是卖点语义,不得出现“第1屏/Screen 1”等编号标识,建议4-10字。\n` +
`11) 在 globalStyle.typography 中写明明确字号与字重(示例:主标题64px/700,副标题34px/600,正文24px/500),并在每屏 layoutNote 里落实排版层级。\n` +
(hasModelImage
? `12) 已提供模特图:涉及人物场景时必须复用同一模特(人脸、发型、体型、肤色与服装风格一致),不得替换其他人物。\n`
: `12) 未提供模特图:所有分镜均设置 useModel=false。\n`) +
(hasBundleImages
? `13) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
: `13) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
`输出格式:\n` +
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"4-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
temperature: 0.25,
maxTokens: 1800,
} as any);
text = String(result?.text || "");
} catch (error) {
if (isAbortError(error)) {
console.error("[Canvas Detail Plan] Primary planning aborted, fallback applied:", error);
return NextResponse.json({
success: true,
data: {
...fallback,
summary: `${fallback.summary}\n规划模型超时,已回退到稳定分镜模板。`,
qualityChecks: {
passed: false,
issues: ["规划模型调用超时,已自动回退到稳定分镜模板。"],
},
...imageInputs,
],
},
],
system:
`你是资深电商视觉策划与提示词工程师。请为“AI详情生成”产出可直接用于生图的分镜提示词和文案。\n` +
`硬性要求:\n` +
`1) 输出严格 JSON,不要其他文本。\n` +
`2) 保持同一产品一致性(结构、颜色、材质、Logo、比例)。\n` +
`3) 所有分镜风格统一、构图统一、文案排版风格统一。主标题/副标题/正文的字体家族、字重、字号层级、行距必须全套统一。\n` +
`4) 每屏尺寸固定 790x1300 像素,输出提示词必须显式包含该尺寸约束。\n` +
`5) 结构遵循“黄金五屏原则”:屏1-2核心卖点,屏3-4差异强化,屏5证据背书;更多屏作为延展。\n` +
`6) 每屏都要有 FABE 闭环(B利益/F特征/A优势/E证据),且文案符合电商转化表达。\n` +
`7) 文案用词需贴近用户语言,不写空泛形容词,不泄露系统流程。\n` +
`8) 对每屏输出 useModel 与 modelReason:useModel=true 表示该屏必须融入固定模特图人物;false 表示该屏禁止出现人物。\n` +
`9) 对每屏输出 screenType(A/B/C):A=技术公信,B=场景共鸣,C=细节佐证。\n` +
`10) 主标题必须是卖点语义,不得出现“第1屏/Screen 1”等编号标识,建议4-10字。\n` +
`11) 在 globalStyle.typography 中写明明确字号与字重(示例:主标题64px/700,副标题34px/600,正文24px/500),并在每屏 layoutNote 里落实排版层级。\n` +
(hasModelImage
? `12) 已提供模特图:涉及人物场景时必须复用同一模特(人脸、发型、体型、肤色与服装风格一致),不得替换其他人物。\n`
: `12) 未提供模特图:所有分镜均设置 useModel=false。\n`) +
(hasBundleImages
? `13) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
: `13) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
`输出格式:\n` +
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"4-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
temperature: 0.25,
maxTokens: 1800,
} as any);
model: {
id: plannerConfig.model.id,
displayName: plannerConfig.model.displayName,
providerType: plannerConfig.provider.type,
},
preflight: {
passed: false,
designDirectorScore: 0,
marketingDirectorScore: 0,
issues: ["规划阶段超时,已启用稳定模板回退。"],
rewriteApplied: false,
},
},
});
}
throw error;
}
const parsed = extractJsonBlock<{ summary?: unknown; globalStyle?: unknown; screens?: unknown }>(text || "");
const globalStyleInitial = sanitizeGlobalStyle(parsed?.globalStyle, fallback.globalStyle);