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 = {
|
type DetailPlanRequest = {
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
@ -1645,18 +1653,26 @@ export async function POST(request: Request) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const imageInputs =
|
let imageInputs: Array<{ type: "image"; image: string }> = [];
|
||||||
supportsVisionPlanning(plannerConfig.model) && referenceImages.length > 0
|
if (supportsVisionPlanning(plannerConfig.model) && referenceImages.length > 0) {
|
||||||
? (
|
if (isVolcanoResponsesPlanner(plannerConfig)) {
|
||||||
await inlineVisionImages(referenceImages, {
|
imageInputs = referenceImages
|
||||||
maxImages: 6,
|
.slice(0, 6)
|
||||||
maxSide: 900,
|
.map((url) => ({ type: "image" as const, image: url }));
|
||||||
timeoutMs: 12000,
|
} else {
|
||||||
retries: 2,
|
const inlined = await inlineVisionImages(referenceImages, {
|
||||||
loggerPrefix: "[Canvas Detail Plan]",
|
maxImages: 6,
|
||||||
})
|
maxSide: 900,
|
||||||
).inlineDataUrls.map((dataUrl) => ({ type: "image" as const, image: dataUrl }))
|
timeoutMs: 12000,
|
||||||
: [];
|
retries: 2,
|
||||||
|
loggerPrefix: "[Canvas Detail Plan]",
|
||||||
|
});
|
||||||
|
imageInputs = inlined.inlineDataUrls.map((dataUrl) => ({
|
||||||
|
type: "image" as const,
|
||||||
|
image: dataUrl,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let text = "";
|
let text = "";
|
||||||
try {
|
try {
|
||||||
@ -1714,16 +1730,20 @@ export async function POST(request: Request) {
|
|||||||
} as any);
|
} as any);
|
||||||
text = String(result?.text || "");
|
text = String(result?.text || "");
|
||||||
} catch (error) {
|
} 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);
|
console.error("[Canvas Detail Plan] Primary planning aborted, fallback applied:", error);
|
||||||
|
const fallbackReason = /invalid scheme/i.test(message)
|
||||||
|
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
|
||||||
|
: "规划模型调用超时,已自动回退到稳定分镜模板。";
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
...fallback,
|
...fallback,
|
||||||
summary: `${fallback.summary}\n规划模型超时,已回退到稳定分镜模板。`,
|
summary: `${fallback.summary}\n${fallbackReason}`,
|
||||||
qualityChecks: {
|
qualityChecks: {
|
||||||
passed: false,
|
passed: false,
|
||||||
issues: ["规划模型调用超时,已自动回退到稳定分镜模板。"],
|
issues: [fallbackReason],
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
id: plannerConfig.model.id,
|
id: plannerConfig.model.id,
|
||||||
@ -1734,7 +1754,7 @@ export async function POST(request: Request) {
|
|||||||
passed: false,
|
passed: false,
|
||||||
designDirectorScore: 0,
|
designDirectorScore: 0,
|
||||||
marketingDirectorScore: 0,
|
marketingDirectorScore: 0,
|
||||||
issues: ["规划阶段超时,已启用稳定模板回退。"],
|
issues: [fallbackReason],
|
||||||
rewriteApplied: false,
|
rewriteApplied: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
|
|||||||
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 180;
|
||||||
|
|
||||||
type DetailReviewRequest = {
|
type DetailReviewRequest = {
|
||||||
outputLanguage?: string;
|
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" {
|
function normalizeLanguage(value: unknown): "zh" | "en" {
|
||||||
const raw = String(value || "").toLowerCase();
|
const raw = String(value || "").toLowerCase();
|
||||||
if (raw.includes("en") || raw.includes("english")) return "en";
|
if (raw.includes("en") || raw.includes("english")) return "en";
|
||||||
@ -246,35 +273,68 @@ export async function POST(request: Request) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const { inlineDataUrls } = await inlineVisionImages([imageUrl, ...referenceImages], {
|
const reviewImageCandidates = [imageUrl, ...referenceImages].slice(0, 6);
|
||||||
maxImages: 6,
|
let effectiveReviewImages = reviewImageCandidates;
|
||||||
maxSide: 900,
|
if (!isVolcanoResponsesReviewer(reviewerConfig)) {
|
||||||
timeoutMs: 12000,
|
const { inlineDataUrls } = await inlineVisionImages(reviewImageCandidates, {
|
||||||
retries: 2,
|
maxImages: 6,
|
||||||
loggerPrefix: "[Canvas Detail Review]",
|
maxSide: 900,
|
||||||
});
|
timeoutMs: 12000,
|
||||||
const effectiveReviewImages = inlineDataUrls.length > 0 ? inlineDataUrls : [imageUrl, ...referenceImages];
|
retries: 2,
|
||||||
|
loggerPrefix: "[Canvas Detail Review]",
|
||||||
|
});
|
||||||
|
effectiveReviewImages = inlineDataUrls.length > 0 ? inlineDataUrls : reviewImageCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
const content: Array<{ type: "text"; text: string } | { type: "image"; image: string }> = [
|
const content: Array<{ type: "text"; text: string } | { type: "image"; image: string }> = [
|
||||||
{ type: "text", text: promptParts },
|
{ type: "text", text: promptParts },
|
||||||
...effectiveReviewImages.map((image) => ({ type: "image" as const, image })),
|
...effectiveReviewImages.map((image) => ({ type: "image" as const, image })),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { text } = await generateText({
|
let text = "";
|
||||||
model,
|
try {
|
||||||
messages: [
|
const result = await generateText({
|
||||||
{
|
model,
|
||||||
role: "user",
|
messages: [
|
||||||
content,
|
{
|
||||||
},
|
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."
|
system:
|
||||||
: "你同时是高转化电商详情页的平面设计总监与营销总监。按严格专业标准评估,仅输出JSON。",
|
language === "en"
|
||||||
temperature: 0.1,
|
? "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."
|
||||||
maxTokens: 800,
|
: "你同时是高转化电商详情页的平面设计总监与营销总监。按严格专业标准评估,仅输出JSON。",
|
||||||
} as any);
|
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<{
|
const parsed = extractJsonBlock<{
|
||||||
pass?: unknown;
|
pass?: unknown;
|
||||||
|
|||||||
@ -130,8 +130,12 @@ async function callResponsesApi(params: {
|
|||||||
imageUrls: string[];
|
imageUrls: string[];
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}) {
|
}) {
|
||||||
|
const remoteImageUrls = params.imageUrls
|
||||||
|
.map((url) => String(url || "").trim())
|
||||||
|
.filter((url) => /^https?:\/\//i.test(url));
|
||||||
|
|
||||||
const content = [
|
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 || "请根据图片内容进行分析。" },
|
{ type: "input_text", text: params.text || "请根据图片内容进行分析。" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -190,7 +194,7 @@ async function callResponsesApi(params: {
|
|||||||
|
|
||||||
export function createVolcanoMultimodal(config: VolcanoMultimodalConfig) {
|
export function createVolcanoMultimodal(config: VolcanoMultimodalConfig) {
|
||||||
const endpoint = normalizeResponsesEndpoint(config.baseURL);
|
const endpoint = normalizeResponsesEndpoint(config.baseURL);
|
||||||
const timeout = config.timeout ?? 60000;
|
const timeout = config.timeout ?? 120000;
|
||||||
const toV2Usage = (usage: { promptTokens?: number; completionTokens?: number }) => {
|
const toV2Usage = (usage: { promptTokens?: number; completionTokens?: number }) => {
|
||||||
const inputTokens = usage.promptTokens ?? 0;
|
const inputTokens = usage.promptTokens ?? 0;
|
||||||
const outputTokens = usage.completionTokens ?? 0;
|
const outputTokens = usage.completionTokens ?? 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user