fix(detail): stabilize volcano multimodal plan/review image handling
This commit is contained in:
parent
ac76f31878
commit
5301c3bd68
@ -48,6 +48,14 @@ function supportsVisionPlanning(model: {
|
||||
);
|
||||
}
|
||||
|
||||
function isVolcanoResponsesPlanner(config: {
|
||||
provider?: { type?: string | null; baseUrl?: string | null } | null;
|
||||
}) {
|
||||
if (!config?.provider || config.provider.type !== "custom") return false;
|
||||
const baseUrl = String(config.provider.baseUrl || "").toLowerCase();
|
||||
return baseUrl.includes("ark.cn-beijing.volces.com") || /\/api\/v3(\/responses)?\/?$/.test(baseUrl);
|
||||
}
|
||||
|
||||
type DetailPlanRequest = {
|
||||
prompt?: string;
|
||||
count?: number;
|
||||
@ -1645,18 +1653,26 @@ export async function POST(request: Request) {
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const imageInputs =
|
||||
supportsVisionPlanning(plannerConfig.model) && referenceImages.length > 0
|
||||
? (
|
||||
await inlineVisionImages(referenceImages, {
|
||||
maxImages: 6,
|
||||
maxSide: 900,
|
||||
timeoutMs: 12000,
|
||||
retries: 2,
|
||||
loggerPrefix: "[Canvas Detail Plan]",
|
||||
})
|
||||
).inlineDataUrls.map((dataUrl) => ({ type: "image" as const, image: dataUrl }))
|
||||
: [];
|
||||
let imageInputs: Array<{ type: "image"; image: string }> = [];
|
||||
if (supportsVisionPlanning(plannerConfig.model) && referenceImages.length > 0) {
|
||||
if (isVolcanoResponsesPlanner(plannerConfig)) {
|
||||
imageInputs = referenceImages
|
||||
.slice(0, 6)
|
||||
.map((url) => ({ type: "image" as const, image: url }));
|
||||
} else {
|
||||
const inlined = await inlineVisionImages(referenceImages, {
|
||||
maxImages: 6,
|
||||
maxSide: 900,
|
||||
timeoutMs: 12000,
|
||||
retries: 2,
|
||||
loggerPrefix: "[Canvas Detail Plan]",
|
||||
});
|
||||
imageInputs = inlined.inlineDataUrls.map((dataUrl) => ({
|
||||
type: "image" as const,
|
||||
image: dataUrl,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let text = "";
|
||||
try {
|
||||
@ -1714,16 +1730,20 @@ export async function POST(request: Request) {
|
||||
} as any);
|
||||
text = String(result?.text || "");
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
const message = String((error as Error)?.message || "");
|
||||
if (isAbortError(error) || /invalid scheme/i.test(message)) {
|
||||
console.error("[Canvas Detail Plan] Primary planning aborted, fallback applied:", error);
|
||||
const fallbackReason = /invalid scheme/i.test(message)
|
||||
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
|
||||
: "规划模型调用超时,已自动回退到稳定分镜模板。";
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
...fallback,
|
||||
summary: `${fallback.summary}\n规划模型超时,已回退到稳定分镜模板。`,
|
||||
summary: `${fallback.summary}\n${fallbackReason}`,
|
||||
qualityChecks: {
|
||||
passed: false,
|
||||
issues: ["规划模型调用超时,已自动回退到稳定分镜模板。"],
|
||||
issues: [fallbackReason],
|
||||
},
|
||||
model: {
|
||||
id: plannerConfig.model.id,
|
||||
@ -1734,7 +1754,7 @@ export async function POST(request: Request) {
|
||||
passed: false,
|
||||
designDirectorScore: 0,
|
||||
marketingDirectorScore: 0,
|
||||
issues: ["规划阶段超时,已启用稳定模板回退。"],
|
||||
issues: [fallbackReason],
|
||||
rewriteApplied: false,
|
||||
},
|
||||
},
|
||||
|
||||
@ -13,7 +13,7 @@ import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
|
||||
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 60;
|
||||
export const maxDuration = 180;
|
||||
|
||||
type DetailReviewRequest = {
|
||||
outputLanguage?: string;
|
||||
@ -66,6 +66,33 @@ function supportsVisionReview(model: {
|
||||
);
|
||||
}
|
||||
|
||||
function isVolcanoResponsesReviewer(config: {
|
||||
provider?: { type?: string | null; baseUrl?: string | null } | null;
|
||||
}) {
|
||||
if (!config?.provider || config.provider.type !== "custom") return false;
|
||||
const baseUrl = String(config.provider.baseUrl || "").toLowerCase();
|
||||
return baseUrl.includes("ark.cn-beijing.volces.com") || /\/api\/v3(\/responses)?\/?$/.test(baseUrl);
|
||||
}
|
||||
|
||||
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 normalizeLanguage(value: unknown): "zh" | "en" {
|
||||
const raw = String(value || "").toLowerCase();
|
||||
if (raw.includes("en") || raw.includes("english")) return "en";
|
||||
@ -246,35 +273,68 @@ export async function POST(request: Request) {
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const { inlineDataUrls } = await inlineVisionImages([imageUrl, ...referenceImages], {
|
||||
maxImages: 6,
|
||||
maxSide: 900,
|
||||
timeoutMs: 12000,
|
||||
retries: 2,
|
||||
loggerPrefix: "[Canvas Detail Review]",
|
||||
});
|
||||
const effectiveReviewImages = inlineDataUrls.length > 0 ? inlineDataUrls : [imageUrl, ...referenceImages];
|
||||
const reviewImageCandidates = [imageUrl, ...referenceImages].slice(0, 6);
|
||||
let effectiveReviewImages = reviewImageCandidates;
|
||||
if (!isVolcanoResponsesReviewer(reviewerConfig)) {
|
||||
const { inlineDataUrls } = await inlineVisionImages(reviewImageCandidates, {
|
||||
maxImages: 6,
|
||||
maxSide: 900,
|
||||
timeoutMs: 12000,
|
||||
retries: 2,
|
||||
loggerPrefix: "[Canvas Detail Review]",
|
||||
});
|
||||
effectiveReviewImages = inlineDataUrls.length > 0 ? inlineDataUrls : reviewImageCandidates;
|
||||
}
|
||||
|
||||
const content: Array<{ type: "text"; text: string } | { type: "image"; image: string }> = [
|
||||
{ type: "text", text: promptParts },
|
||||
...effectiveReviewImages.map((image) => ({ type: "image" as const, image })),
|
||||
];
|
||||
|
||||
const { text } = await generateText({
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
},
|
||||
],
|
||||
system:
|
||||
language === "en"
|
||||
? "You are both an Art Director and a Marketing Director for high-conversion e-commerce detail pages. Evaluate with strict professional standards and output JSON only."
|
||||
: "你同时是高转化电商详情页的平面设计总监与营销总监。按严格专业标准评估,仅输出JSON。",
|
||||
temperature: 0.1,
|
||||
maxTokens: 800,
|
||||
} as any);
|
||||
let text = "";
|
||||
try {
|
||||
const result = await generateText({
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
},
|
||||
],
|
||||
system:
|
||||
language === "en"
|
||||
? "You are both an Art Director and a Marketing Director for high-conversion e-commerce detail pages. Evaluate with strict professional standards and output JSON only."
|
||||
: "你同时是高转化电商详情页的平面设计总监与营销总监。按严格专业标准评估,仅输出JSON。",
|
||||
temperature: 0.1,
|
||||
maxTokens: 800,
|
||||
} as any);
|
||||
text = String(result?.text || "");
|
||||
} catch (error) {
|
||||
const message = String((error as Error)?.message || "");
|
||||
if (isAbortError(error) || /invalid scheme/i.test(message)) {
|
||||
const fallbackIssues = isAbortError(error)
|
||||
? ["质检模型超时,已跳过二次审稿并保留本屏结果。"]
|
||||
: ["质检模型图片输入协议不兼容,已跳过二次审稿并保留本屏结果。"];
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
pass: true,
|
||||
score: 78,
|
||||
designDirectorScore: 78,
|
||||
marketingDirectorScore: 78,
|
||||
issues: fallbackIssues,
|
||||
layoutFixes: [],
|
||||
revisedPrompt: fallbackPrompt,
|
||||
model: {
|
||||
id: reviewerConfig.model.id,
|
||||
displayName: reviewerConfig.model.displayName,
|
||||
providerType: reviewerConfig.provider.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const parsed = extractJsonBlock<{
|
||||
pass?: unknown;
|
||||
|
||||
@ -130,8 +130,12 @@ async function callResponsesApi(params: {
|
||||
imageUrls: string[];
|
||||
timeout: number;
|
||||
}) {
|
||||
const remoteImageUrls = params.imageUrls
|
||||
.map((url) => String(url || "").trim())
|
||||
.filter((url) => /^https?:\/\//i.test(url));
|
||||
|
||||
const content = [
|
||||
...params.imageUrls.map((url) => ({ type: "input_image", image_url: url })),
|
||||
...remoteImageUrls.map((url) => ({ type: "input_image", image_url: url })),
|
||||
{ type: "input_text", text: params.text || "请根据图片内容进行分析。" },
|
||||
];
|
||||
|
||||
@ -190,7 +194,7 @@ async function callResponsesApi(params: {
|
||||
|
||||
export function createVolcanoMultimodal(config: VolcanoMultimodalConfig) {
|
||||
const endpoint = normalizeResponsesEndpoint(config.baseURL);
|
||||
const timeout = config.timeout ?? 60000;
|
||||
const timeout = config.timeout ?? 120000;
|
||||
const toV2Usage = (usage: { promptTokens?: number; completionTokens?: number }) => {
|
||||
const inputTokens = usage.promptTokens ?? 0;
|
||||
const outputTokens = usage.completionTokens ?? 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user