fix: cap detail-plan latency and add LLM timeouts
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-24 12:15:25 +08:00
parent 598cdfd736
commit b6a12ced9f

View File

@ -14,6 +14,11 @@ import { inlineVisionImages } from "@/lib/ai/vision-inline";
export const runtime = "nodejs";
export const maxDuration = 180;
const DETAIL_PLAN_INLINE_MAX_IMAGES = 4;
const DETAIL_PLAN_INLINE_TIMEOUT_MS = 4000;
const DETAIL_PLAN_INLINE_RETRIES = 1;
const DETAIL_PLAN_MAIN_TIMEOUT_MS = 55000;
const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 35000;
function isAbortError(error: unknown): boolean {
const target = error as {
@ -56,6 +61,19 @@ function isVolcanoResponsesPlanner(config: {
return baseUrl.includes("ark.cn-beijing.volces.com") || /\/api\/v3(\/responses)?\/?$/.test(baseUrl);
}
async function generateTextWithTimeout(params: any, timeoutMs: number) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await generateText({
...params,
abortSignal: controller.signal,
} as any);
} finally {
clearTimeout(timer);
}
}
type DetailPlanRequest = {
prompt?: string;
count?: number;
@ -1465,7 +1483,7 @@ async function runDualDirectorPlanPreflight(params: {
briefText,
];
const { text } = await generateText({
const { text } = await generateTextWithTimeout({
model: params.model,
messages: [
{
@ -1479,7 +1497,7 @@ async function runDualDirectorPlanPreflight(params: {
: "你同时扮演高转化电商详情页的平面设计总监与营销总监。仅输出JSON。",
temperature: 0.1,
maxTokens: 2000,
} as any);
}, DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS);
const parsed = extractJsonBlock<{
pass?: unknown;
@ -1661,10 +1679,10 @@ export async function POST(request: Request) {
.map((url) => ({ type: "image" as const, image: url }));
} else {
const inlined = await inlineVisionImages(referenceImages, {
maxImages: 6,
maxImages: DETAIL_PLAN_INLINE_MAX_IMAGES,
maxSide: 900,
timeoutMs: 12000,
retries: 2,
timeoutMs: DETAIL_PLAN_INLINE_TIMEOUT_MS,
retries: DETAIL_PLAN_INLINE_RETRIES,
loggerPrefix: "[Canvas Detail Plan]",
});
imageInputs = inlined.inlineDataUrls.map((dataUrl) => ({
@ -1676,7 +1694,7 @@ export async function POST(request: Request) {
let text = "";
try {
const result = await generateText({
const result = await generateTextWithTimeout({
model,
messages: [
{
@ -1727,7 +1745,7 @@ export async function POST(request: Request) {
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"首屏示例:甩甩杆,自拍不手抖(6-20字)","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
temperature: 0.25,
maxTokens: 1800,
} as any);
}, DETAIL_PLAN_MAIN_TIMEOUT_MS);
text = String(result?.text || "");
} catch (error) {
const message = String((error as Error)?.message || "");