fix: fallback ai detail plan on planner abort timeout
This commit is contained in:
parent
18149083d4
commit
153efc8f96
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user