fix: streamline extend image payload flow
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-05-08 08:48:07 +08:00
parent 2814669881
commit c4c0bd8f64
6 changed files with 211 additions and 119 deletions

View File

@ -12,6 +12,22 @@ import {
export const maxDuration = 180;
function parseIncludeBase64(value: unknown): boolean {
if (typeof value === "boolean") return value;
const normalized = String(value ?? "").trim().toLowerCase();
return normalized === "true" || normalized === "1" || normalized === "yes";
}
function buildExtendSuccessPayload(result: Awaited<ReturnType<typeof executeExtendJob>>) {
return {
success: true,
original_url: result.original_url,
thumbnail_url: result.thumbnail_url,
...(result.image_base64 ? { base64: result.image_base64 } : {}),
processing_time: result.processing_time,
};
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
@ -26,6 +42,8 @@ export async function POST(request: NextRequest) {
let prompt = "图像拓展处理";
let chatId: string | null = null;
let messageId: string | null = null;
let includeBase64 = false;
const requestId = `extend-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
if (contentType.includes("application/json")) {
const body = await request.json();
@ -34,6 +52,7 @@ export async function POST(request: NextRequest) {
prompt = body.prompt || prompt;
chatId = body.chatId || null;
messageId = body.messageId || null;
includeBase64 = parseIncludeBase64(body.includeBase64);
if (!imageUrl && !imageBase64) {
return NextResponse.json(
@ -42,6 +61,16 @@ export async function POST(request: NextRequest) {
);
}
console.log("[Extend API] Request received:", {
requestId,
source: imageUrl ? "url" : "base64",
includeBase64,
chatId,
messageId,
xScale: body.xScale,
yScale: body.yScale,
});
const pointsConfig = await getPointsSystemConfig();
const pointsCharge = await prepareFeaturePointsCharge({
userId: session.user.id,
@ -64,6 +93,8 @@ export async function POST(request: NextRequest) {
messageId,
xScale: body.xScale ? Number(body.xScale) : undefined,
yScale: body.yScale ? Number(body.yScale) : undefined,
includeBase64,
requestId,
});
if (pointsCharge.chargeable) {
@ -74,13 +105,14 @@ export async function POST(request: NextRequest) {
}
}
return NextResponse.json({
success: true,
original_url: result.original_url,
thumbnail_url: result.thumbnail_url,
base64: result.image_base64,
processing_time: result.processing_time,
console.log("[Extend API] Request completed:", {
requestId,
includeBase64,
processingTime: result.processing_time,
originalUrl: result.original_url.slice(0, 120),
});
return NextResponse.json(buildExtendSuccessPayload(result));
}
if (contentType.includes("multipart/form-data")) {
@ -92,6 +124,17 @@ export async function POST(request: NextRequest) {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const multipartChatId = String(formData.get("chatId") || "") || null;
const multipartMessageId = String(formData.get("messageId") || "") || null;
includeBase64 = parseIncludeBase64(formData.get("includeBase64"));
console.log("[Extend API] Multipart request received:", {
requestId,
includeBase64,
chatId: multipartChatId,
messageId: multipartMessageId,
bytes: buffer.length,
});
const pointsConfig = await getPointsSystemConfig();
const pointsCharge = await prepareFeaturePointsCharge({
userId: session.user.id,
@ -109,8 +152,10 @@ export async function POST(request: NextRequest) {
userId: session.user.id,
imageBase64: buffer.toString("base64"),
prompt: String(formData.get("prompt") || prompt),
chatId: String(formData.get("chatId") || "") || null,
messageId: String(formData.get("messageId") || "") || null,
chatId: multipartChatId,
messageId: multipartMessageId,
includeBase64,
requestId,
});
if (pointsCharge.chargeable) {
@ -121,13 +166,14 @@ export async function POST(request: NextRequest) {
}
}
return NextResponse.json({
success: true,
original_url: result.original_url,
thumbnail_url: result.thumbnail_url,
base64: result.image_base64,
processing_time: result.processing_time,
console.log("[Extend API] Multipart request completed:", {
requestId,
includeBase64,
processingTime: result.processing_time,
originalUrl: result.original_url.slice(0, 120),
});
return NextResponse.json(buildExtendSuccessPayload(result));
}
return NextResponse.json(

View File

@ -2583,6 +2583,7 @@ export function AgentPsdTemplateEditorDialog({
body: JSON.stringify({
image_url: normalizedImageUrl,
prompt: "PSD 套版图片拓展处理",
includeBase64: false,
}),
});
const payload = await parseActionJsonResponse<{

View File

@ -49,6 +49,23 @@ import type { VideoToolCapabilityContract } from "@/types/video-tool-capability"
type ImageGenerationPhase = "idle" | "queued" | "processing" | "finalizing" | "failed";
function getImageFileExtension(mimeType: string | null | undefined): string {
switch (String(mimeType || "").toLowerCase()) {
case "image/jpeg":
case "image/jpg":
return "jpg";
case "image/webp":
return "webp";
case "image/gif":
return "gif";
case "image/svg+xml":
return "svg";
case "image/png":
default:
return "png";
}
}
function normalizeImageParamsForProvider(
params: ImageGenerationParams,
providerType?: string
@ -1821,122 +1838,139 @@ export function Chat({
}, 100);
try {
// 获取图片并转换为Base64
const response = await fetch(imageUrl);
const blob = await response.blob();
const reader = new FileReader();
reader.onloadend = async () => {
let extendSourceUrl = String(imageUrl || "").trim();
if (!extendSourceUrl) {
throw new Error("缺少可拓展的图片地址");
}
if (extendSourceUrl.startsWith("data:") || extendSourceUrl.startsWith("blob:")) {
const sourceResponse = await fetch(extendSourceUrl);
if (!sourceResponse.ok) {
throw new Error("读取待拓展图片失败");
}
const blob = await sourceResponse.blob();
const mimeType =
blob.type || sourceResponse.headers.get("content-type") || "image/png";
const file = new File(
[blob],
`extend-source-${Date.now()}.${getImageFileExtension(mimeType)}`,
{ type: mimeType },
);
const formData = new FormData();
formData.append("file", file);
const uploadResponse = await fetchUploadWithRetry(formData);
const uploadPayload = await uploadResponse
.json()
.catch(() => null as { url?: string; error?: string } | null);
if (!uploadResponse.ok) {
throw new Error(uploadPayload?.error || "上传待拓展图片失败");
}
const uploadedUrl = String(uploadPayload?.url || "").trim();
if (!uploadedUrl) {
throw new Error("上传待拓展图片成功,但未返回图片地址");
}
extendSourceUrl = uploadedUrl;
} else {
extendSourceUrl = new URL(extendSourceUrl, window.location.origin).toString();
}
const extendResponse = await fetch("/api/extend", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image_url: extendSourceUrl,
prompt: "图片图像拓展处理",
chatId: id,
messageId,
includeBase64: false,
}),
});
const result = await extendResponse.json().catch(
() =>
null as {
success?: boolean;
error?: string;
original_url?: string;
thumbnail_url?: string;
processing_time?: number;
} | null,
);
if (!extendResponse.ok) {
throw new Error(result?.error || "图像拓展处理失败");
}
if (result?.success && result.original_url) {
// 构造图片Markdown(使用与Gemini生成相同的格式)
const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`;
// 创建最终消息(前端显示用)
const finalMessage: ChatMessage = {
id: messageId,
role: "assistant",
parts: [{ type: "text", text: imageMarkdown }],
metadata: {
createdAt: new Date().toISOString(),
},
} as ChatMessage;
// 更新消息
setMessages((prev) =>
prev.map((msg) => (msg.id === messageId ? finalMessage : msg)),
);
// 滚动到底部,显示处理结果
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
// 保存消息到数据库
try {
const base64Data = (reader.result as string).split(",")[1];
// 调用图像拓展API
const extendResponse = await fetch("/api/extend", {
await fetch("/api/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image_base64: base64Data,
prompt: "图片图像拓展处理",
}),
});
if (!extendResponse.ok) {
const error = await extendResponse.json();
throw new Error(error.error || "图像拓展处理失败");
}
const result = await extendResponse.json();
if (result.success && result.original_url) {
// 构造图片Markdown(使用与Gemini生成相同的格式)
const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`;
// 创建最终消息(前端显示用)
const finalMessage: ChatMessage = {
id: messageId,
role: "assistant",
parts: [{ type: "text", text: imageMarkdown }],
metadata: {
chatId: id,
message: {
id: messageId,
role: "assistant",
parts: [{ type: "text", text: imageMarkdown }],
attachments: [],
createdAt: new Date().toISOString(),
},
} as ChatMessage;
// 更新消息
setMessages((prev) =>
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
);
// 滚动到底部,显示处理结果
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
// 保存消息到数据库
try {
await fetch("/api/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chatId: id,
message: {
id: messageId,
role: "assistant",
parts: [{ type: "text", text: imageMarkdown }],
attachments: [],
createdAt: new Date().toISOString(),
},
}),
});
} catch (saveError) {
console.error("[Chat] Failed to save message:", saveError);
}
toast.success(`图像拓展完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
} else {
throw new Error(result.error || "图像拓展处理失败");
}
} catch (error) {
console.error("[图像拓展] 错误:", error);
toast.error(
getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", {
rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"),
})
);
// 移除加载消息
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
} finally {
console.log("[图像拓展] finally 块执行,重置 isProcessingExtend");
setIsProcessingExtend(false);
}),
});
} catch (saveError) {
console.error("[Chat] Failed to save message:", saveError);
}
};
reader.onerror = () => {
console.log("[图像拓展] reader.onerror 执行,重置 isProcessingExtend");
toast.error("图片读取失败");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingExtend(false);
};
reader.readAsDataURL(blob);
toast.success(`图像拓展完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
} else {
throw new Error(result?.error || "图像拓展处理失败");
}
} catch (error) {
console.error("[图像拓展] 错误:", error);
console.log("[图像拓展] 外层 catch 执行,重置 isProcessingExtend");
toast.error(
getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", {
rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"),
})
);
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
} finally {
console.log("[图像拓展] finally 块执行,重置 isProcessingExtend");
setIsProcessingExtend(false);
}
};

View File

@ -1205,6 +1205,7 @@ export function ImageAnnotator({
body: JSON.stringify({
image_url: originalImageUrl, // 直接发送图片URL
prompt: "画图标注-图像拓展处理", // 可选:添加提示词
includeBase64: false,
}),
});
@ -1228,9 +1229,7 @@ export function ImageAnnotator({
setProcessedImageUrl(null);
}
// 优先使用返回的base64,如果没有则使用thumbnail_url或original_url
if (result.base64) {
setProcessedImageBase64(result.base64);
}
setProcessedImageBase64(result.base64 || null);
setProcessType("extend");
toast.success(`图像拓展完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);

View File

@ -23,12 +23,14 @@ export interface ExecuteExtendJobParams {
messageId?: string | null;
xScale?: number;
yScale?: number;
includeBase64?: boolean;
skipGallerySave?: boolean;
sourceImageUrl?: string | null;
requestId?: string | null;
}
export interface ExecuteExtendJobResult {
image_base64: string;
image_base64?: string;
thumbnail_url: string;
original_url: string;
processing_time: number;
@ -221,8 +223,10 @@ export async function executeExtendJob(
prompt = "图像拓展处理",
chatId = null,
messageId = null,
includeBase64 = false,
skipGallerySave = false,
sourceImageUrl = imageUrl || null,
requestId = null,
} = params;
if (!imageUrl && !imageBase64) {
@ -250,8 +254,15 @@ export async function executeExtendJob(
}
const buffer = Buffer.from(await imageResponse.arrayBuffer());
const imageBase64Result = buffer.toString("base64");
const imageBase64Result = includeBase64 ? buffer.toString("base64") : undefined;
const uploadResult = await uploadBufferPairToS3(buffer);
console.log("[Extend Service] Result uploaded:", {
requestId,
includeBase64,
inputImageUrl: inputImageUrl.slice(0, 120),
extendedImageUrl: extendedImageUrl.slice(0, 120),
outputBytes: buffer.length,
});
let mediaAssetId: string | null = null;
let generatedImageId: string | null = null;

View File

@ -487,6 +487,7 @@ export const imageGenerationService = {
body: JSON.stringify({
image_url: imageUrl,
prompt: normalizedOptions.prompt || '图像拓展处理',
includeBase64: false,
...(typeof normalizedOptions.xScale === 'number'
? { xScale: normalizedOptions.xScale }
: {}),
@ -499,7 +500,7 @@ export const imageGenerationService = {
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || '图片扩展失败' };
return { success: false, error: data.error || data.detail || '图片扩展失败' };
} catch (error) {
console.error('[Canvas] 图片扩展失败:', error);
return { success: false, error: '图片扩展失败' };