chore: expand canvas log hygiene coverage
This commit is contained in:
parent
91a98083a1
commit
fadcbbc206
@ -1,25 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
|
||||
const logger = createLogger("canvas-auto-title");
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
console.log("[Auto-title] API called");
|
||||
logger.debug("request_received");
|
||||
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
console.log("[Auto-title] Unauthorized");
|
||||
logger.warn("unauthorized");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { imageUrl } = await request.json();
|
||||
console.log("[Auto-title] Image URL:", imageUrl);
|
||||
|
||||
if (!imageUrl) {
|
||||
return NextResponse.json({ error: "Image URL is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log("[Auto-title] 使用硬编码模型: qwen3-vl");
|
||||
logger.debug("model_selected", { model: "qwen3-vl" });
|
||||
|
||||
// 构造 Prompt
|
||||
const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。";
|
||||
@ -35,11 +37,11 @@ export async function POST(request: NextRequest) {
|
||||
title = title.replace(/[\s\p{P}\p{S}]+/gu, "");
|
||||
if (title.length > 4) title = title.slice(0, 4);
|
||||
if (title.length < 2) title = "新画布";
|
||||
console.log("[Auto-title] Generated title:", title, "model:", model);
|
||||
logger.info("title_generated", { model, titleLength: title.length });
|
||||
return NextResponse.json({ title });
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Auto-title] Error:", error);
|
||||
logger.error("auto_title_failed", error);
|
||||
// 失败时返回默认标题,不报错中断流程
|
||||
return NextResponse.json({ title: "新画布" });
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
type DetailKimiGenerateOptions,
|
||||
} from "@/lib/ai/detail-agent-kimi-profile";
|
||||
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import { filterReachableRemoteUrls } from "@/lib/net/url-access";
|
||||
import {
|
||||
buildDetailUiProtocol,
|
||||
@ -47,6 +48,7 @@ const DETAIL_PLAN_INLINE_TIMEOUT_MS = 4000;
|
||||
const DETAIL_PLAN_INLINE_RETRIES = 1;
|
||||
const DETAIL_PLAN_MAIN_TIMEOUT_MS = 45000;
|
||||
const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 10000;
|
||||
const logger = createLogger("canvas-detail-plan");
|
||||
|
||||
function isAbortError(error: unknown): boolean {
|
||||
const target = error as {
|
||||
@ -1774,7 +1776,7 @@ async function runDualDirectorPlanPreflight(params: {
|
||||
candidateScreens,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[Canvas Detail Plan] preflight error:", error);
|
||||
logger.warn("preflight_failed", error);
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
@ -1808,19 +1810,17 @@ export async function POST(request: Request) {
|
||||
const bundleImageUrls = bundleImageCheck.kept.slice(0, 3);
|
||||
const modelImageUrl = modelImageCheck.kept[0] || "";
|
||||
if (referenceImageCheck.dropped.length > 0) {
|
||||
console.warn("[Canvas Detail Plan] Dropping unreachable referenceImages:", {
|
||||
logger.warn("reference_images_dropped", {
|
||||
count: referenceImageCheck.dropped.length,
|
||||
urls: referenceImageCheck.dropped.slice(0, 6),
|
||||
});
|
||||
}
|
||||
if (bundleImageCheck.dropped.length > 0) {
|
||||
console.warn("[Canvas Detail Plan] Dropping unreachable bundleImageUrls:", {
|
||||
logger.warn("bundle_images_dropped", {
|
||||
count: bundleImageCheck.dropped.length,
|
||||
urls: bundleImageCheck.dropped.slice(0, 3),
|
||||
});
|
||||
}
|
||||
if (modelImageUrlRaw && !modelImageUrl) {
|
||||
console.warn("[Canvas Detail Plan] modelImageUrl unreachable, ignore this run:", modelImageUrlRaw);
|
||||
logger.warn("model_image_dropped");
|
||||
}
|
||||
const effectiveReferenceImageNotes =
|
||||
referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : [];
|
||||
@ -1935,7 +1935,7 @@ export async function POST(request: Request) {
|
||||
isLocalMoonshot: Boolean(localDetailAgentResolved),
|
||||
});
|
||||
|
||||
console.log("[Canvas Detail Plan] Planner model selected:", {
|
||||
logger.info("planner_model_selected", {
|
||||
id: selectedModelRef.id,
|
||||
displayName: selectedModelRef.displayName,
|
||||
modelId: selectedModelRef.modelId,
|
||||
@ -2030,10 +2030,9 @@ export async function POST(request: Request) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (isReferenceImageDownloadError(error) || isTransientNetworkError(error)) {
|
||||
console.warn(
|
||||
"[Canvas Detail Plan] Vision input preprocessing failed, retry without reference images:",
|
||||
error
|
||||
);
|
||||
logger.warn("vision_input_preprocessing_failed", error, {
|
||||
referenceImageCount: referenceImages.length,
|
||||
});
|
||||
imageInputs = [];
|
||||
} else {
|
||||
throw error;
|
||||
@ -2125,10 +2124,9 @@ export async function POST(request: Request) {
|
||||
if (!shouldRetryWithoutImages) {
|
||||
throw firstError;
|
||||
}
|
||||
console.warn(
|
||||
"[Canvas Detail Plan] Primary planning with reference images failed, retrying without reference images:",
|
||||
firstError
|
||||
);
|
||||
logger.warn("planner_retry_without_reference_images", firstError, {
|
||||
imageInputCount: imageInputs.length,
|
||||
});
|
||||
const retryResult = await runPlanner([]);
|
||||
text = String(retryResult?.text || "");
|
||||
}
|
||||
@ -2141,7 +2139,7 @@ export async function POST(request: Request) {
|
||||
isTransientNetworkError(error) ||
|
||||
/operationtimeouterror|timed out|timeout/i.test(message)
|
||||
) {
|
||||
console.error("[Canvas Detail Plan] Primary planning aborted, fallback applied:", error);
|
||||
logger.error("primary_planning_fallback_applied", error);
|
||||
const fallbackReason = /invalid scheme/i.test(message)
|
||||
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
|
||||
: isReferenceImageDownloadError(error)
|
||||
@ -2384,7 +2382,7 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ success: true, data: response });
|
||||
} catch (error) {
|
||||
console.error("[Canvas Detail Plan] Error:", error);
|
||||
logger.error("detail_plan_failed", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "详情规划失败" },
|
||||
{ status: 500 },
|
||||
|
||||
@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import sharp from "sharp";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@ -13,6 +14,7 @@ const requestSchema = z.object({
|
||||
|
||||
const OCR_IMAGE_FETCH_TIMEOUT_MS = 15000;
|
||||
const OCR_IMAGE_MAX_SIDE = 1400;
|
||||
const logger = createLogger("canvas-text-detect");
|
||||
|
||||
interface OcrItem {
|
||||
id: string;
|
||||
@ -145,7 +147,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ success: false, error: "仅支持 URL 图片,不允许 base64 传输" }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log("[Canvas Text Detect] 使用硬编码模型: qwen3-vl");
|
||||
logger.debug("model_selected", { model: "qwen3-vl" });
|
||||
const prompt = [
|
||||
"你是图片文字识别助手。",
|
||||
"请识别图片中可见文字并按阅读顺序输出 JSON。",
|
||||
@ -172,7 +174,9 @@ export async function POST(request: Request) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.warn("[Canvas Text Detect] URL 拉图超时,切换 data URL 兜底");
|
||||
logger.warn("vision_url_fallback", {
|
||||
reason: error instanceof Error ? error.message : "unknown",
|
||||
});
|
||||
const dataUrl = await buildVisionDataUrl(imageUrl);
|
||||
const fallbackResult = await callQwen3Vl({
|
||||
imageUrl: dataUrl,
|
||||
@ -205,7 +209,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Canvas Text Detect] Error:", error);
|
||||
logger.error("text_detect_failed", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
type SceneFusionImageGenConfig,
|
||||
} from "@/lib/scene-fusion/output-capabilities";
|
||||
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
|
||||
// 请求体 Schema
|
||||
const sceneFusionRequestSchema = z.object({
|
||||
@ -70,6 +71,7 @@ const SCENE_FUSION_RANDOM_BG_PROMPT_TAIL =
|
||||
"硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。";
|
||||
let sceneFusionRoundRobinCursor = 0;
|
||||
type SceneFusionModelCooldown = Map<string, number>;
|
||||
const logger = createLogger("scene-fusion");
|
||||
|
||||
interface SceneFusionModelCandidate {
|
||||
model: {
|
||||
@ -304,8 +306,7 @@ async function getAverageHashBits(
|
||||
hashCache.set(imageUrl, hashBits);
|
||||
return hashBits;
|
||||
} catch (error) {
|
||||
console.warn("[SceneFusion] Echo hash compute skipped:", {
|
||||
imageUrl: imageUrl.slice(0, 120),
|
||||
logger.warn("echo_hash_compute_skipped", {
|
||||
error: error instanceof Error ? error.message : "unknown",
|
||||
});
|
||||
return null;
|
||||
@ -357,9 +358,12 @@ async function detectEchoedProductResult(
|
||||
|
||||
const distance = hammingDistanceBits(productHash, generatedHash);
|
||||
if (distance <= SCENE_FUSION_ECHO_HASH_THRESHOLD) {
|
||||
console.warn(
|
||||
`[SceneFusion][${params.requestId}] Echo-guard hit: scene#${params.sceneIndex + 1}, model=${params.modelName}, distance=${distance}`
|
||||
);
|
||||
logger.warn("echo_guard_hit", {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex + 1,
|
||||
modelName: params.modelName,
|
||||
distance,
|
||||
});
|
||||
return {
|
||||
tooSimilar: true,
|
||||
reason: "hash_too_close",
|
||||
@ -603,7 +607,8 @@ async function reviewSceneFusionResult(params: {
|
||||
suspectedEcho !== true &&
|
||||
score >= 0.5;
|
||||
|
||||
console.log(`[SceneFusion][${params.requestId}] Review result:`, {
|
||||
logger.info("review_result", {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex + 1,
|
||||
candidate: params.modelName,
|
||||
passed,
|
||||
@ -621,7 +626,8 @@ async function reviewSceneFusionResult(params: {
|
||||
model,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`[SceneFusion][${params.requestId}] Review skipped:`, {
|
||||
logger.warn("review_skipped", {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex + 1,
|
||||
candidate: params.modelName,
|
||||
error: error instanceof Error ? error.message : "unknown",
|
||||
@ -745,10 +751,10 @@ async function enrichRandomBackgroundScenesWithAgent(params: {
|
||||
scene.customPrompt = mergeCustomPrompt(scene.customPrompt, `场景建议:${generatedPrompt}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[SceneFusion][${params.requestId}] Random scene prompt enhance skipped: scene=${scene.id}`,
|
||||
error
|
||||
);
|
||||
logger.warn("random_prompt_enhance_skipped", error, {
|
||||
requestId: params.requestId,
|
||||
sceneId: scene.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -809,7 +815,7 @@ async function enrichSceneLabelsWithAgent(params: {
|
||||
meta.productModel = productDetected.model;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[SceneFusion][${params.requestId}] Product detect skipped:`, error);
|
||||
logger.warn("product_detect_skipped", error, { requestId: params.requestId });
|
||||
}
|
||||
|
||||
const unresolvedIndexes = clonedScenes
|
||||
@ -858,10 +864,10 @@ async function enrichSceneLabelsWithAgent(params: {
|
||||
metaScene.model = targetDetected.model;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[SceneFusion][${params.requestId}] Scene target detect skipped: scene=${scene.id}`,
|
||||
error
|
||||
);
|
||||
logger.warn("scene_target_detect_skipped", error, {
|
||||
requestId: params.requestId,
|
||||
sceneId: scene.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -957,7 +963,7 @@ async function getSceneFusionConfig() {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Failed to load config:", error);
|
||||
logger.error("config_load_failed", error);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1000,9 +1006,10 @@ async function preprocessProductImageWithRmbg(
|
||||
const candidateUrls = buildPreRmbgCandidateUrls(request);
|
||||
let lastReason = "no_candidate_url";
|
||||
|
||||
console.log(
|
||||
`[SceneFusion][${requestId}] Pre-RMBG start: ${productImageUrl.substring(0, 120)}..., candidates=${candidateUrls.join(" | ")}`
|
||||
);
|
||||
logger.info("pre_rmbg_started", {
|
||||
requestId,
|
||||
candidateCount: candidateUrls.length,
|
||||
});
|
||||
|
||||
for (const rmbgUrl of candidateUrls) {
|
||||
const controller = new AbortController();
|
||||
@ -1026,7 +1033,8 @@ async function preprocessProductImageWithRmbg(
|
||||
|
||||
if (!response.ok) {
|
||||
lastReason = `request_failed_${response.status}`;
|
||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate failed: ${rmbgUrl}`, {
|
||||
logger.warn("pre_rmbg_candidate_failed", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
detail: payload?.detail || payload?.error || "unknown",
|
||||
});
|
||||
@ -1036,7 +1044,8 @@ async function preprocessProductImageWithRmbg(
|
||||
const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim();
|
||||
if (!cleanedUrl || cleanedUrl === productImageUrl) {
|
||||
lastReason = cleanedUrl ? "same_as_source" : "empty_result";
|
||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate invalid result: ${rmbgUrl}`, {
|
||||
logger.warn("pre_rmbg_candidate_invalid", {
|
||||
requestId,
|
||||
reason: lastReason,
|
||||
});
|
||||
continue;
|
||||
@ -1049,7 +1058,7 @@ async function preprocessProductImageWithRmbg(
|
||||
};
|
||||
} catch (error) {
|
||||
lastReason = error instanceof Error ? error.message : "exception";
|
||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate exception: ${rmbgUrl}`, error);
|
||||
logger.warn("pre_rmbg_candidate_exception", error, { requestId });
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
@ -1073,7 +1082,7 @@ async function createThumbnail(buffer: Buffer): Promise<Buffer> {
|
||||
.toBuffer();
|
||||
return thumbnailBuffer;
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Failed to create thumbnail:", error);
|
||||
logger.error("thumbnail_create_failed", error);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@ -1098,10 +1107,10 @@ async function uploadBase64ToS3(base64Data: string, prefix: string = "scene-fusi
|
||||
|
||||
const filename = generateFilename(`${prefix}-ref`, "png");
|
||||
const result = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
console.log(`[SceneFusion] Uploaded reference image to: ${result.url}`);
|
||||
logger.info("reference_image_uploaded", { prefix, bytes: buffer.byteLength });
|
||||
return result.url;
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Failed to upload reference image:", error);
|
||||
logger.error("reference_image_upload_failed", error, { prefix });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -1114,15 +1123,15 @@ async function downloadAndUploadToS3(
|
||||
prefix: string = "scene-fusion"
|
||||
): Promise<{ thumbnail: string; original: string }> {
|
||||
try {
|
||||
console.log("[SceneFusion] Downloading image from:", imageUrl.substring(0, 100) + "...");
|
||||
|
||||
logger.debug("download_upload_started", { prefix });
|
||||
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download image: ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
console.log(`[SceneFusion] Downloaded image, size: ${buffer.length} bytes`);
|
||||
logger.debug("image_downloaded", { prefix, bytes: buffer.length });
|
||||
|
||||
// 创建缩略图
|
||||
const thumbnailBuffer = await createThumbnail(buffer);
|
||||
@ -1130,20 +1139,20 @@ async function downloadAndUploadToS3(
|
||||
// 上传原图
|
||||
const originalFilename = generateFilename(`${prefix}-orig`, "png");
|
||||
const originalResult = await uploadFile(originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
console.log(`[SceneFusion] Uploaded original to: ${originalResult.url}`);
|
||||
logger.debug("original_uploaded", { prefix, bytes: buffer.byteLength });
|
||||
|
||||
// 上传缩略图
|
||||
const thumbnailFilename = generateFilename(`${prefix}-thumb`, "jpg");
|
||||
const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer;
|
||||
const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer);
|
||||
console.log(`[SceneFusion] Uploaded thumbnail to: ${thumbnailResult.url}`);
|
||||
logger.debug("thumbnail_uploaded", { prefix, bytes: thumbnailBuffer.byteLength });
|
||||
|
||||
return {
|
||||
original: originalResult.url,
|
||||
thumbnail: thumbnailResult.url,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Failed to download/upload image:", error);
|
||||
logger.error("download_upload_failed", error, { prefix });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -1229,7 +1238,7 @@ async function saveSceneFusionImageRecord(params: {
|
||||
}
|
||||
}
|
||||
} catch (assetError) {
|
||||
console.error("[SceneFusion] ⚠️ MediaAsset 注册失败:", assetError);
|
||||
logger.error("media_asset_register_failed", assetError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1318,13 +1327,13 @@ async function callAlibabaImageAPI(
|
||||
},
|
||||
};
|
||||
|
||||
console.log(`[SceneFusion][${modelId}] Request Debug:
|
||||
- Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)}
|
||||
- Product URL: ${productImageUrl}
|
||||
- Scene URL: ${sceneImageUrl}
|
||||
- Size: ${size || 'auto'}
|
||||
- Prompt: ${prompt}
|
||||
`);
|
||||
logger.debug("alibaba_request_prepared", {
|
||||
modelId,
|
||||
size: size || "auto",
|
||||
productImageCount: productImageUrl ? 1 : 0,
|
||||
sceneImageCount: sceneImageUrl ? 1 : 0,
|
||||
promptLength: prompt.length,
|
||||
});
|
||||
|
||||
const response = await fetch(baseURL, {
|
||||
method: "POST",
|
||||
@ -1337,23 +1346,35 @@ async function callAlibabaImageAPI(
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 打印完整响应用于调试
|
||||
console.log(`[SceneFusion][${modelId}] Full API Response:`, JSON.stringify(data, null, 2));
|
||||
logger.debug("alibaba_response_received", {
|
||||
modelId,
|
||||
ok: response.ok,
|
||||
code: data?.code ?? null,
|
||||
hasOutput: Boolean(data?.output),
|
||||
});
|
||||
|
||||
if (!response.ok || data.code) {
|
||||
console.error("[SceneFusion] API Error Response:", JSON.stringify(data, null, 2));
|
||||
logger.error("alibaba_response_error", {
|
||||
modelId,
|
||||
status: response.status,
|
||||
code: data?.code ?? null,
|
||||
message: data?.message ?? null,
|
||||
});
|
||||
throw new Error(data.message || (data.code ? `Error ${data.code}: ${data.message}` : `API request failed: ${response.status}`));
|
||||
}
|
||||
|
||||
// 提取生成的图片 URL
|
||||
const choices = data.output?.choices;
|
||||
if (!choices || choices.length === 0) {
|
||||
console.error("[SceneFusion] No choices in response, output:", data.output);
|
||||
logger.error("alibaba_no_choices", { modelId, hasOutput: Boolean(data?.output) });
|
||||
throw new Error("No images in response");
|
||||
}
|
||||
|
||||
const imageContent = choices[0]?.message?.content;
|
||||
console.log(`[SceneFusion] Image content array:`, JSON.stringify(imageContent, null, 2));
|
||||
logger.debug("alibaba_image_content_received", {
|
||||
modelId,
|
||||
contentCount: Array.isArray(imageContent) ? imageContent.length : 0,
|
||||
});
|
||||
|
||||
if (!imageContent || imageContent.length === 0) {
|
||||
throw new Error("No image content in response");
|
||||
@ -1364,25 +1385,25 @@ async function callAlibabaImageAPI(
|
||||
for (const item of imageContent) {
|
||||
if (item.image) {
|
||||
generatedImageUrl = item.image;
|
||||
console.log(`[SceneFusion] Found image URL in content item`);
|
||||
logger.debug("alibaba_image_url_found", { modelId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!generatedImageUrl) {
|
||||
console.error("[SceneFusion] No image URL found in content:", imageContent);
|
||||
logger.error("alibaba_image_url_missing", {
|
||||
modelId,
|
||||
contentCount: Array.isArray(imageContent) ? imageContent.length : 0,
|
||||
});
|
||||
throw new Error("No image URL in response");
|
||||
}
|
||||
|
||||
// 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图)
|
||||
if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) {
|
||||
console.warn(`[SceneFusion] ⚠️ WARNING: API returned same URL as input!`);
|
||||
console.warn(`[SceneFusion] - Generated URL: ${generatedImageUrl}`);
|
||||
console.warn(`[SceneFusion] - Product URL: ${productImageUrl}`);
|
||||
console.warn(`[SceneFusion] - Scene URL: ${sceneImageUrl}`);
|
||||
logger.warn("alibaba_returned_input_url", { modelId });
|
||||
}
|
||||
|
||||
console.log("[SceneFusion] Generated image URL:", generatedImageUrl.substring(0, 100) + "...");
|
||||
logger.debug("alibaba_image_generated", { modelId });
|
||||
return generatedImageUrl;
|
||||
}
|
||||
|
||||
@ -1468,8 +1489,11 @@ async function processSceneWithProvider(
|
||||
sceneMode
|
||||
);
|
||||
|
||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
||||
console.log(`[SceneFusion] Provider type: ${provider.type}`);
|
||||
logger.debug("provider_prompt_built", {
|
||||
providerType: provider.type,
|
||||
promptLength: prompt.length,
|
||||
sceneMode,
|
||||
});
|
||||
|
||||
// 根据 provider 类型选择不同的处理逻辑
|
||||
switch (provider.type) {
|
||||
@ -1541,7 +1565,7 @@ async function processWithGemini(
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
console.log(`[SceneFusion][Gemini] Processing with model: ${modelId}`);
|
||||
logger.info("provider_processing", { providerType: "gemini", modelId });
|
||||
|
||||
// 创建 Gemini provider
|
||||
const geminiProvider = createGeminiImage({
|
||||
@ -1589,7 +1613,7 @@ async function processWithRuxaImage(
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
console.log(`[SceneFusion][RUXA] Processing with model: ${modelId}`);
|
||||
logger.info("provider_processing", { providerType: "ruxa-image", modelId });
|
||||
|
||||
const ruxaProvider = createRuxaImage({
|
||||
baseURL: provider.baseUrl,
|
||||
@ -1629,7 +1653,7 @@ async function processWithFalImage(
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
console.log(`[SceneFusion][FAL] Processing with model: ${modelId}`);
|
||||
logger.info("provider_processing", { providerType: "fal-image", modelId });
|
||||
|
||||
const falProvider = createFalImage({
|
||||
baseURL: provider.baseUrl,
|
||||
@ -1669,7 +1693,7 @@ async function processWithOpenAIImage(
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
console.log(`[SceneFusion][OpenAI Image] Processing with model: ${modelId}`);
|
||||
logger.info("provider_processing", { providerType: "openai-image", modelId });
|
||||
|
||||
const openAIImageProvider = createOpenAIImage({
|
||||
baseURL: provider.baseUrl,
|
||||
@ -1712,7 +1736,7 @@ async function processWithVolcanoVision(
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
console.log(`[SceneFusion][Volcano] Processing with model: ${modelId}`);
|
||||
logger.info("provider_processing", { providerType: "volcano-vision", modelId });
|
||||
|
||||
// 创建 Volcano Vision provider
|
||||
const volcanoProvider = createVolcanoVision({
|
||||
@ -1753,7 +1777,7 @@ async function processWithVolcanoVision(
|
||||
* 格式: 
|
||||
*/
|
||||
function parseImageFromMarkdown(markdown: string): { thumbnail: string; original: string } {
|
||||
console.log(`[SceneFusion] Parsing markdown: ${markdown.substring(0, 200)}...`);
|
||||
logger.debug("markdown_parse_started", { markdownLength: markdown.length });
|
||||
|
||||
// 匹配  格式
|
||||
const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
|
||||
@ -1805,7 +1829,7 @@ async function processScene(
|
||||
sceneMode
|
||||
);
|
||||
|
||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
||||
logger.debug("legacy_prompt_built", { promptLength: prompt.length, sceneMode });
|
||||
|
||||
// 调用阿里生图 API
|
||||
const generatedImageUrl = await callAlibabaImageAPI(
|
||||
@ -1890,14 +1914,15 @@ async function processSceneWithDiversity(
|
||||
const attemptCandidates =
|
||||
orderedHealthyCandidates.length > 0 ? orderedHealthyCandidates : orderedFallbackCandidates;
|
||||
|
||||
console.log(
|
||||
`[SceneFusion][${params.requestId}] Scene#${params.sceneIndex + 1} candidate order: ${attemptCandidates
|
||||
.map(
|
||||
(candidate) =>
|
||||
`${candidate.model.displayName}/${candidate.provider.type}/score=${getSceneFusionCandidatePriority(candidate)}`
|
||||
)
|
||||
.join(" -> ")}`
|
||||
);
|
||||
logger.debug("candidate_order_resolved", {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex + 1,
|
||||
candidates: attemptCandidates.map((candidate) => ({
|
||||
modelId: candidate.model.modelId,
|
||||
providerType: candidate.provider.type,
|
||||
score: getSceneFusionCandidatePriority(candidate),
|
||||
})),
|
||||
});
|
||||
|
||||
const failureReasons: string[] = [];
|
||||
|
||||
@ -1909,9 +1934,12 @@ async function processSceneWithDiversity(
|
||||
imageSize: params.imageSize,
|
||||
});
|
||||
if (normalizedOutputParams.changed) {
|
||||
console.log(
|
||||
`[SceneFusion][${params.requestId}] Candidate ${candidate.model.displayName} output params normalized: ${normalizedOutputParams.changes.join(", ")}`
|
||||
);
|
||||
logger.info("candidate_output_params_normalized", {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex + 1,
|
||||
modelId: candidate.model.modelId,
|
||||
changes: normalizedOutputParams.changes,
|
||||
});
|
||||
}
|
||||
|
||||
const modelSlot = await acquireCanvasImageModelSlot({
|
||||
@ -2000,17 +2028,19 @@ async function processSceneWithDiversity(
|
||||
markSceneFusionCandidateFailed(candidate, params.modelCooldown);
|
||||
// Batch processing can fail because of per-image inputs, review gates, or echo checks.
|
||||
// Keep these failures local to this request so they do not auto-disable shared models.
|
||||
const logCandidateFailure = isSceneFusionReviewRejectError(errorMessage)
|
||||
? console.warn
|
||||
: console.error;
|
||||
logCandidateFailure("[SceneFusion] Candidate failed, try next:", {
|
||||
const failureMeta = {
|
||||
requestId: params.requestId,
|
||||
sceneIndex: params.sceneIndex,
|
||||
attempt: attemptIndex + 1,
|
||||
model: candidate.model.modelId,
|
||||
provider: candidate.provider.type,
|
||||
error: errorMessage,
|
||||
});
|
||||
};
|
||||
if (isSceneFusionReviewRejectError(errorMessage)) {
|
||||
logger.warn("candidate_review_rejected", failureMeta);
|
||||
} else {
|
||||
logger.error("candidate_failed", failureMeta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2060,7 +2090,16 @@ export async function POST(request: Request) {
|
||||
|
||||
// 计算输出尺寸
|
||||
const size = calculateSize(aspectRatio || "auto", imageSize || "2K");
|
||||
console.log(`[SceneFusion] Output settings: aspectRatio=${aspectRatio || 'auto'}, imageSize=${imageSize || '2K'}, calculated size=${size || 'auto'}`);
|
||||
logger.info("request_received", {
|
||||
requestId,
|
||||
aspectRatio: aspectRatio || "auto",
|
||||
imageSize: imageSize || "2K",
|
||||
size: size || "auto",
|
||||
sceneCount: scenes.length,
|
||||
sceneMode: resolvedSceneMode,
|
||||
hasProductName: Boolean(productName),
|
||||
preferredModelId: config.modelId || null,
|
||||
});
|
||||
|
||||
// 检查场景数量限制
|
||||
const effectiveMaxScenes = Math.min(config.maxScenes, SCENE_FUSION_MAX_ITEMS);
|
||||
@ -2071,19 +2110,11 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[SceneFusion] Processing ${scenes.length} scenes for user ${session.user.id}`);
|
||||
console.log(`[SceneFusion] Product name: ${productName || '(not specified)'}`);
|
||||
console.log(`[SceneFusion] Scene mode: ${resolvedSceneMode}`);
|
||||
console.log(`[SceneFusion] Using default prompt: ${config.defaultPrompt.substring(0, 50)}...`);
|
||||
console.log(`[SceneFusion] Preferred model ID: ${config.modelId || '(none)'}`);
|
||||
|
||||
// 4. 获取生图模型池(固定多样性模式)
|
||||
const allModels = await getEnabledAiModelsWithProvider();
|
||||
const candidateModels = buildSceneFusionCandidatePool(allModels, config.modelId);
|
||||
if (config.modelId && !candidateModels.some((candidate) => candidate.model.id === config.modelId)) {
|
||||
console.warn(
|
||||
`[SceneFusion] Preferred model ${config.modelId} not available, fallback to full diversity pool`
|
||||
);
|
||||
logger.warn("preferred_model_unavailable", { requestId, modelId: config.modelId });
|
||||
}
|
||||
|
||||
if (candidateModels.length === 0) {
|
||||
@ -2093,37 +2124,39 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[SceneFusion] Diversity candidates: ${candidateModels
|
||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
||||
.join(", ")}`
|
||||
);
|
||||
logger.info("candidate_pool_resolved", {
|
||||
requestId,
|
||||
candidateCount: candidateModels.length,
|
||||
providers: [...new Set(candidateModels.map((candidate) => candidate.provider.type))],
|
||||
});
|
||||
const unsupportedCandidates = candidateModels.filter(
|
||||
(candidate) => !canProcessSceneFusionProviderType(candidate.provider.type)
|
||||
);
|
||||
if (unsupportedCandidates.length > 0) {
|
||||
console.warn(
|
||||
`[SceneFusion] Skip unsupported providers for scene fusion: ${unsupportedCandidates
|
||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
||||
.join(", ")}`
|
||||
);
|
||||
logger.warn("unsupported_candidates_present", {
|
||||
requestId,
|
||||
count: unsupportedCandidates.length,
|
||||
providers: [...new Set(unsupportedCandidates.map((candidate) => candidate.provider.type))],
|
||||
});
|
||||
}
|
||||
const nonVisionCandidates = candidateModels.filter(
|
||||
(candidate) => !candidate.model.supportsVision
|
||||
);
|
||||
if (nonVisionCandidates.length > 0) {
|
||||
console.warn(
|
||||
`[SceneFusion] Models without supportsVision are included for failover diversity: ${nonVisionCandidates
|
||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
||||
.join(", ")}`
|
||||
);
|
||||
logger.warn("non_vision_candidates_present", {
|
||||
requestId,
|
||||
count: nonVisionCandidates.length,
|
||||
});
|
||||
}
|
||||
const requestOffset = getNextSceneFusionRoundRobinOffset(
|
||||
Math.max(1, candidateModels.length)
|
||||
);
|
||||
console.log(
|
||||
`[SceneFusion][${requestId}] Round-robin offset=${requestOffset}, scenes=${scenes.length}, candidates=${candidateModels.length}`
|
||||
);
|
||||
logger.debug("round_robin_offset_resolved", {
|
||||
requestId,
|
||||
requestOffset,
|
||||
sceneCount: scenes.length,
|
||||
candidateCount: candidateModels.length,
|
||||
});
|
||||
|
||||
// 5. 预处理:将 Base64 图片上传到 S3
|
||||
let productImageUrl = productImage;
|
||||
@ -2138,9 +2171,12 @@ export async function POST(request: Request) {
|
||||
shouldForcePreRmbg ? true : config.preRmbgEnabled
|
||||
);
|
||||
const fusionProductImageUrl = preprocessedProduct.productImageUrl;
|
||||
console.log(
|
||||
`[SceneFusion][${requestId}] Pre-RMBG result: used=${preprocessedProduct.usedPreRmbg}, provider=${preprocessedProduct.provider || "-"}, reason=${preprocessedProduct.reason || "-"}`
|
||||
);
|
||||
logger.info("pre_rmbg_resolved", {
|
||||
requestId,
|
||||
used: preprocessedProduct.usedPreRmbg,
|
||||
provider: preprocessedProduct.provider || null,
|
||||
reason: preprocessedProduct.reason || null,
|
||||
});
|
||||
|
||||
const processedScenes = await Promise.all(
|
||||
scenes.map(async (scene) => {
|
||||
@ -2175,17 +2211,18 @@ export async function POST(request: Request) {
|
||||
const imageHashCache = new Map<string, string>();
|
||||
const modelCooldown: SceneFusionModelCooldown = new Map();
|
||||
|
||||
console.log(
|
||||
`[SceneFusion][${requestId}] Agent labels: product=${resolvedProductName || "(none)"}; targets=${resolvedScenes
|
||||
.map((scene) => `${scene.id}:${scene.targetName || "-"}`)
|
||||
.join(", ")}`
|
||||
);
|
||||
logger.info("agent_labels_resolved", {
|
||||
requestId,
|
||||
hasProductName: Boolean(resolvedProductName),
|
||||
targetCount: resolvedScenes.filter((scene) => Boolean(scene.targetName)).length,
|
||||
sceneCount: resolvedScenes.length,
|
||||
});
|
||||
if (resolvedSceneMode === "random-bg") {
|
||||
console.log(
|
||||
`[SceneFusion][${requestId}] Random-bg prompts: ${resolvedScenes
|
||||
.map((scene) => `${scene.id}:${(scene.customPrompt || "").slice(0, 80) || "-"}`)
|
||||
.join(" | ")}`
|
||||
);
|
||||
logger.debug("random_bg_prompts_resolved", {
|
||||
requestId,
|
||||
sceneCount: resolvedScenes.length,
|
||||
promptCount: resolvedScenes.filter((scene) => Boolean(scene.customPrompt)).length,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 创建流式响应,每完成一张图就立即发送给前端
|
||||
@ -2199,7 +2236,7 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
controller.close();
|
||||
} catch (closeError) {
|
||||
console.warn("[SceneFusion] Stream close skipped:", closeError);
|
||||
logger.warn("stream_close_skipped", closeError, { requestId });
|
||||
}
|
||||
};
|
||||
const enqueue = (payload: Record<string, unknown>) => {
|
||||
@ -2209,7 +2246,7 @@ export async function POST(request: Request) {
|
||||
return true;
|
||||
} catch (enqueueError) {
|
||||
streamClosed = true;
|
||||
console.warn("[SceneFusion] Stream enqueue skipped:", enqueueError);
|
||||
logger.warn("stream_enqueue_skipped", enqueueError, { requestId });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@ -2258,27 +2295,16 @@ export async function POST(request: Request) {
|
||||
const resolvedMetaScene =
|
||||
enriched.meta.sceneTargets.find((item) => item.id === resolvedScene.id) || null;
|
||||
|
||||
console.log(
|
||||
`[SceneFusion] ========== Processing scene ${currentIndex + 1}/${processedScenes.length} ==========`
|
||||
);
|
||||
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
||||
console.log(
|
||||
`[SceneFusion] Product Image URL: ${fusionProductImageUrl.substring(0, 80)}...`
|
||||
);
|
||||
console.log(
|
||||
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`
|
||||
);
|
||||
console.log(
|
||||
`[SceneFusion] Target Name: ${resolvedScene.targetName || "(not specified)"}`
|
||||
);
|
||||
if (resolvedMetaScene?.positionHint || resolvedMetaScene?.replaceCue) {
|
||||
console.log(
|
||||
`[SceneFusion] Target Hints: position=${resolvedMetaScene?.positionHint || "-"}, cue=${resolvedMetaScene?.replaceCue || "-"}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[SceneFusion] Custom Prompt: ${resolvedScene.customPrompt || "(none)"}`
|
||||
);
|
||||
logger.info("scene_processing_started", {
|
||||
requestId,
|
||||
sceneId: scene.id,
|
||||
sceneIndex: currentIndex + 1,
|
||||
total: processedScenes.length,
|
||||
hasTargetName: Boolean(resolvedScene.targetName),
|
||||
hasPositionHint: Boolean(resolvedMetaScene?.positionHint),
|
||||
hasReplaceCue: Boolean(resolvedMetaScene?.replaceCue),
|
||||
hasCustomPrompt: Boolean(resolvedScene.customPrompt),
|
||||
});
|
||||
|
||||
try {
|
||||
const { result, candidate, attempts } = await processSceneWithDiversity({
|
||||
@ -2333,9 +2359,13 @@ export async function POST(request: Request) {
|
||||
modelId: candidate.model.modelId,
|
||||
sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl],
|
||||
});
|
||||
console.log(
|
||||
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
||||
);
|
||||
logger.info("scene_saved", {
|
||||
requestId,
|
||||
sceneId: scene.id,
|
||||
modelId: candidate.model.modelId,
|
||||
providerType: candidate.provider.type,
|
||||
attempts,
|
||||
});
|
||||
|
||||
successCount += 1;
|
||||
if (!enqueue({
|
||||
@ -2356,7 +2386,11 @@ export async function POST(request: Request) {
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error);
|
||||
logger.error("scene_processing_failed", error, {
|
||||
requestId,
|
||||
sceneId: scene.id,
|
||||
sceneIndex: currentIndex + 1,
|
||||
});
|
||||
failCount += 1;
|
||||
if (!enqueue({
|
||||
type: "result",
|
||||
@ -2375,7 +2409,12 @@ export async function POST(request: Request) {
|
||||
await Promise.all(workers);
|
||||
|
||||
// 发送完成消息
|
||||
console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`);
|
||||
logger.info("request_completed", {
|
||||
requestId,
|
||||
total: processedScenes.length,
|
||||
success: successCount,
|
||||
failed: failCount,
|
||||
});
|
||||
enqueue({
|
||||
type: "done",
|
||||
summary: {
|
||||
@ -2386,7 +2425,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Stream error:", error);
|
||||
logger.error("stream_failed", error, { requestId });
|
||||
// 发送错误消息
|
||||
if (!streamClosed) {
|
||||
enqueue({
|
||||
@ -2408,7 +2447,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[SceneFusion] Unexpected error:", error);
|
||||
logger.error("request_failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "处理失败" },
|
||||
{ status: 500 }
|
||||
|
||||
@ -55,6 +55,7 @@ import {
|
||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import { createClientLogger } from "@/lib/observability/client-logger";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AngleEditDialog } from "./angle-edit-dialog";
|
||||
import { SceneFusionView } from "./scene-fusion-view";
|
||||
@ -64,6 +65,8 @@ import { Slider } from "./ui/slider";
|
||||
|
||||
type Tool = "pen" | "arrow" | "circle" | "text";
|
||||
|
||||
const logger = createClientLogger("image-annotator");
|
||||
|
||||
type DrawingAction = {
|
||||
type: Tool;
|
||||
color: string;
|
||||
@ -315,17 +318,14 @@ export function ImageAnnotator({
|
||||
const localImage = await localPreviewPromise;
|
||||
setImage(localImage);
|
||||
} catch (previewError) {
|
||||
console.warn(
|
||||
"[ImageAnnotator] Local preview failed, fallback to uploaded url:",
|
||||
previewError
|
||||
);
|
||||
logger.warn("local_preview_failed", previewError);
|
||||
const uploadedImage = await loadImageFromSource(uploadResult.url);
|
||||
setImage(uploadedImage);
|
||||
}
|
||||
|
||||
toast.success("图片上传成功");
|
||||
} catch (error) {
|
||||
console.error("[ImageAnnotator] 上传图片失败:", error);
|
||||
logger.error("upload_failed", error);
|
||||
const message =
|
||||
error instanceof Error ? error.message : "上传失败,请重试";
|
||||
toast.error(message);
|
||||
@ -517,7 +517,7 @@ export function ImageAnnotator({
|
||||
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[ImageAnnotator] Failed to fetch site settings:", err);
|
||||
logger.warn("site_settings_fetch_failed", err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -559,7 +559,7 @@ export function ImageAnnotator({
|
||||
setSelectedAspectRatio(null);
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
console.error("初始化图片加载失败:", error);
|
||||
logger.error("initial_image_load_failed", error);
|
||||
setImage(null);
|
||||
setIsLoading(false);
|
||||
toast.error("当前图片加载失败,请重试");
|
||||
@ -892,9 +892,8 @@ export function ImageAnnotator({
|
||||
setIsRemovingBg(true);
|
||||
try {
|
||||
// 使用原图URL获取原图数据,而不是canvas的预览图
|
||||
console.log("[抠图] 使用原图URL获取数据:", originalImageUrl);
|
||||
const base64Data = await imageUrlToBase64(originalImageUrl);
|
||||
console.log("[抠图] 原图Base64长度:", base64Data.length, "字符");
|
||||
logger.debug("rmbg_source_prepared", { base64Length: base64Data.length });
|
||||
|
||||
// 调用抠图API
|
||||
const response = await fetch("/api/rmbg", {
|
||||
@ -917,23 +916,18 @@ export function ImageAnnotator({
|
||||
|
||||
if (result.success && result.image_base64) {
|
||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
||||
console.log("[抠图] API返回结果:", {
|
||||
logger.info("rmbg_completed", {
|
||||
hasOriginalUrl: !!result.original_url,
|
||||
hasThumbnailUrl: !!result.thumbnail_url,
|
||||
originalUrl: result.original_url?.substring(0, 100),
|
||||
thumbnailUrl: result.thumbnail_url?.substring(0, 100),
|
||||
base64Length: String(result.image_base64 || "").length,
|
||||
});
|
||||
if (result.original_url) {
|
||||
console.log("[抠图] ✅ 使用原图URL:", result.original_url);
|
||||
setProcessedImageUrl(result.original_url);
|
||||
} else if (result.thumbnail_url) {
|
||||
console.warn(
|
||||
"[抠图] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
|
||||
result.thumbnail_url
|
||||
);
|
||||
logger.warn("rmbg_original_url_missing", { hasThumbnailUrl: true });
|
||||
setProcessedImageUrl(result.thumbnail_url);
|
||||
} else {
|
||||
console.warn("[抠图] ⚠️ 未找到图片URL");
|
||||
logger.warn("rmbg_image_url_missing");
|
||||
setProcessedImageUrl(null);
|
||||
}
|
||||
setProcessedImageBase64(result.image_base64);
|
||||
@ -946,7 +940,7 @@ export function ImageAnnotator({
|
||||
throw new Error(result.error || result.detail || "抠图处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[抠图] 错误:", error);
|
||||
logger.error("rmbg_failed", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "抠图失败,请稍后重试"
|
||||
);
|
||||
@ -961,8 +955,6 @@ export function ImageAnnotator({
|
||||
|
||||
setIsRemovingWatermark(true);
|
||||
try {
|
||||
console.log("[去水印] 使用原图URL:", originalImageUrl);
|
||||
|
||||
const result = await requestLongTaskResult({
|
||||
endpoint: "/api/watermark-remove",
|
||||
payload: {
|
||||
@ -971,9 +963,9 @@ export function ImageAnnotator({
|
||||
});
|
||||
|
||||
if (result.success && result.original_url) {
|
||||
console.log("[去水印] API返回结果:", {
|
||||
originalUrl: result.original_url?.substring(0, 100),
|
||||
thumbnailUrl: result.thumbnail_url?.substring(0, 100),
|
||||
logger.info("watermark_remove_completed", {
|
||||
hasOriginalUrl: !!result.original_url,
|
||||
hasThumbnailUrl: !!result.thumbnail_url,
|
||||
});
|
||||
|
||||
setProcessedImageUrl(result.original_url);
|
||||
@ -988,7 +980,7 @@ export function ImageAnnotator({
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
} catch {
|
||||
console.warn("[去水印] 无法获取Base64,仅使用URL");
|
||||
logger.warn("watermark_remove_base64_fetch_failed");
|
||||
}
|
||||
setProcessType("watermark");
|
||||
|
||||
@ -999,7 +991,7 @@ export function ImageAnnotator({
|
||||
throw new Error(result.error || "去水印处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[去水印] 错误:", error);
|
||||
logger.error("watermark_remove_failed", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "去水印失败,请稍后重试"
|
||||
);
|
||||
@ -1015,9 +1007,8 @@ export function ImageAnnotator({
|
||||
setIsEnhancing(true);
|
||||
try {
|
||||
// 使用原图URL获取原图数据,而不是canvas的预览图
|
||||
console.log("[变清晰] 使用原图URL获取数据:", originalImageUrl);
|
||||
const base64Data = await imageUrlToBase64(originalImageUrl);
|
||||
console.log("[变清晰] 原图Base64长度:", base64Data.length, "字符");
|
||||
logger.debug("enhance_source_prepared", { base64Length: base64Data.length });
|
||||
|
||||
const result = await requestLongTaskResult({
|
||||
endpoint: "/api/enhance",
|
||||
@ -1032,40 +1023,28 @@ export function ImageAnnotator({
|
||||
|
||||
if (result.success && result.image_base64) {
|
||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图)
|
||||
console.log("[变清晰] API返回结果:", {
|
||||
logger.info("enhance_completed", {
|
||||
hasOriginalUrl: !!result.original_url,
|
||||
hasThumbnailUrl: !!result.thumbnail_url,
|
||||
originalUrl: result.original_url,
|
||||
thumbnailUrl: result.thumbnail_url,
|
||||
base64Length: result.image_base64?.length || 0,
|
||||
});
|
||||
|
||||
// ✅ 验证:检查返回的URL是否是处理后的图片
|
||||
if (result.original_url) {
|
||||
console.log("[变清晰] ✅ 使用处理后的原图URL:", result.original_url);
|
||||
console.log(
|
||||
"[变清晰] ✅ URL应该包含 'enhance-orig' 标识,确认是处理后的图片"
|
||||
);
|
||||
|
||||
// 验证URL格式(应该包含enhance-orig)
|
||||
if (
|
||||
!result.original_url.includes("enhance-orig") &&
|
||||
!result.original_url.includes("enhance-orig")
|
||||
) {
|
||||
console.warn(
|
||||
"[变清晰] ⚠️ 警告:URL不包含 'enhance-orig' 标识,可能不是处理后的图片!"
|
||||
);
|
||||
logger.warn("enhance_result_marker_missing");
|
||||
}
|
||||
|
||||
setProcessedImageUrl(result.original_url);
|
||||
} else if (result.thumbnail_url) {
|
||||
console.warn(
|
||||
"[变清晰] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
|
||||
result.thumbnail_url
|
||||
);
|
||||
logger.warn("enhance_original_url_missing", { hasThumbnailUrl: true });
|
||||
setProcessedImageUrl(result.thumbnail_url);
|
||||
} else {
|
||||
console.warn("[变清晰] ⚠️ 未找到图片URL");
|
||||
logger.warn("enhance_image_url_missing");
|
||||
setProcessedImageUrl(null);
|
||||
}
|
||||
setProcessedImageBase64(result.image_base64);
|
||||
@ -1078,7 +1057,7 @@ export function ImageAnnotator({
|
||||
throw new Error(result.error || result.detail || "变清晰处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[变清晰] 错误:", error);
|
||||
logger.error("enhance_failed", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "变清晰失败,请稍后重试"
|
||||
);
|
||||
@ -1359,7 +1338,7 @@ export function ImageAnnotator({
|
||||
const finalResultUrl = result.original_url || dataUrl;
|
||||
|
||||
if (result.original_url) {
|
||||
console.log("[尺寸调整] 原图URL:", result.original_url);
|
||||
logger.info("resize_completed", { hasOriginalUrl: true });
|
||||
setProcessedImageUrl(result.original_url);
|
||||
} else {
|
||||
setProcessedImageUrl(null);
|
||||
@ -1372,7 +1351,7 @@ export function ImageAnnotator({
|
||||
throw new Error(result.error || "尺寸调整处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[尺寸调整] 错误:", error);
|
||||
logger.error("resize_failed", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"
|
||||
);
|
||||
@ -1388,8 +1367,6 @@ export function ImageAnnotator({
|
||||
setIsExtending(true);
|
||||
try {
|
||||
// 直接使用原图URL,而不是转换为base64
|
||||
console.log("[图像拓展] 使用原图URL:", originalImageUrl);
|
||||
|
||||
// 调用图像拓展API(直接发送URL)
|
||||
const response = await fetch("/api/extend", {
|
||||
method: "POST",
|
||||
@ -1413,15 +1390,17 @@ export function ImageAnnotator({
|
||||
if (result.success) {
|
||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
||||
if (result.original_url) {
|
||||
console.log("[图像拓展] 原图URL:", result.original_url);
|
||||
logger.info("extend_completed", {
|
||||
hasOriginalUrl: true,
|
||||
hasThumbnailUrl: !!result.thumbnail_url,
|
||||
hasBase64: !!result.base64,
|
||||
});
|
||||
setProcessedImageUrl(result.original_url);
|
||||
} else if (result.thumbnail_url) {
|
||||
console.warn(
|
||||
"[图像拓展] ⚠️ 未找到original_url,使用thumbnail_url作为备选"
|
||||
);
|
||||
logger.warn("extend_original_url_missing", { hasThumbnailUrl: true });
|
||||
setProcessedImageUrl(result.thumbnail_url);
|
||||
} else {
|
||||
console.warn("[图像拓展] ⚠️ 未找到图片URL");
|
||||
logger.warn("extend_image_url_missing");
|
||||
setProcessedImageUrl(null);
|
||||
}
|
||||
// 优先使用返回的base64,如果没有则使用thumbnail_url或original_url
|
||||
@ -1435,7 +1414,7 @@ export function ImageAnnotator({
|
||||
throw new Error(result.error || result.detail || "图像拓展处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[图像拓展] 错误:", error);
|
||||
logger.error("extend_failed", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "图像拓展失败,请稍后重试"
|
||||
);
|
||||
@ -1507,9 +1486,7 @@ export function ImageAnnotator({
|
||||
}
|
||||
|
||||
// ✅ 验证:确认下载的是处理后的图片URL
|
||||
console.log("[下载] 📥 开始下载,processType:", processType);
|
||||
console.log("[下载] 📥 processedImageUrl:", processedImageUrl);
|
||||
console.log("[下载] 📥 这个URL应该是处理后的图片,不是原图!");
|
||||
logger.debug("download_started", { processType });
|
||||
|
||||
// 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识)
|
||||
let downloadUrl = processedImageUrl;
|
||||
@ -1526,7 +1503,7 @@ export function ImageAnnotator({
|
||||
.replace(/-thumb\./g, "-orig.")
|
||||
.replace(/\/thumb\//g, "/orig/")
|
||||
.replace(/thumbnail/g, "original");
|
||||
console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl);
|
||||
logger.warn("download_thumbnail_url_rewritten", { processType });
|
||||
}
|
||||
|
||||
// 如果转换后的URL仍然包含thumb标识,尝试更激进的转换
|
||||
@ -1538,18 +1515,20 @@ export function ImageAnnotator({
|
||||
if (pathname.includes("thumb")) {
|
||||
const newPathname = pathname.replace(/thumb/g, "orig");
|
||||
downloadUrl = downloadUrl.replace(pathname, newPathname);
|
||||
console.warn("[下载] ⚠️ 二次转换原图URL:", downloadUrl);
|
||||
logger.warn("download_thumbnail_url_rewritten_again", { processType });
|
||||
}
|
||||
} catch (urlError) {
|
||||
// 如果URL解析失败,尝试简单的字符串替换
|
||||
downloadUrl = downloadUrl.replace(/thumb/g, "orig");
|
||||
console.warn("[下载] ⚠️ 使用简单替换转换原图URL:", downloadUrl);
|
||||
logger.warn("download_thumbnail_url_string_rewrite", {
|
||||
processType,
|
||||
reason: urlError instanceof Error ? urlError.message : "unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 验证:确认下载URL是正确的
|
||||
console.log("[下载] ✅ 最终下载URL:", downloadUrl);
|
||||
console.log("[下载] ✅ 这个URL应该指向处理后的图片(不是原图)");
|
||||
logger.debug("download_url_resolved", { processType });
|
||||
|
||||
// 如果是变清晰,验证URL应该包含enhance相关标识
|
||||
if (
|
||||
@ -1557,9 +1536,7 @@ export function ImageAnnotator({
|
||||
!downloadUrl.includes("enhance") &&
|
||||
!downloadUrl.includes("1767358346590")
|
||||
) {
|
||||
console.warn(
|
||||
"[下载] ⚠️ 警告:变清晰下载URL不包含 'enhance' 标识,可能下载了错误的图片!"
|
||||
);
|
||||
logger.warn("download_enhance_marker_missing");
|
||||
}
|
||||
|
||||
// 从原图URL下载(原图已上传到S3)
|
||||
@ -1570,7 +1547,7 @@ export function ImageAnnotator({
|
||||
);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
console.log("[下载] 下载的图片大小:", blob.size, "bytes");
|
||||
logger.info("download_blob_ready", { bytes: blob.size, processType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
@ -1582,7 +1559,7 @@ export function ImageAnnotator({
|
||||
|
||||
toast.success("原图下载成功");
|
||||
} catch (error) {
|
||||
console.error("[下载] 错误:", error);
|
||||
logger.error("download_failed", error);
|
||||
toast.error(
|
||||
`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`
|
||||
);
|
||||
@ -1615,7 +1592,7 @@ export function ImageAnnotator({
|
||||
}
|
||||
return uploadResult.url;
|
||||
} catch (error) {
|
||||
console.error("上传图片失败:", error);
|
||||
logger.error("server_upload_failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@ -10,6 +10,12 @@ const files = [
|
||||
"lib/ai/providers/gemini-image.ts",
|
||||
"lib/ai/providers/volcano-vision.ts",
|
||||
"lib/ai/providers/qianwen-video.ts",
|
||||
"services/canvas/index.ts",
|
||||
"components/image-annotator.tsx",
|
||||
"app/api/canvas/auto-title/route.ts",
|
||||
"app/api/canvas/text-detect/route.ts",
|
||||
"app/api/canvas/detail-plan/route.ts",
|
||||
"app/api/scene-fusion/route.ts",
|
||||
];
|
||||
|
||||
const consolePattern = /\bconsole\.(log|warn|error|info|debug)\b/g;
|
||||
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
EnhanceProOutputFormat,
|
||||
EnhanceProSubjectDetection,
|
||||
} from '@/lib/enhance-pro/config';
|
||||
import { createLogger } from '@/lib/logger';
|
||||
import { createLogger } from '@/lib/observability/logger';
|
||||
|
||||
const log = createLogger("Canvas");
|
||||
|
||||
@ -267,7 +267,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图片上传失败:', error);
|
||||
logCatch(error, "image_upload");
|
||||
return { success: false, error: '图片上传失败' };
|
||||
}
|
||||
},
|
||||
@ -374,7 +374,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '局部编辑提交失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 局部编辑提交失败:', error);
|
||||
logCatch(error, "local_edit_submit");
|
||||
return { success: false, error: '局部编辑提交失败' };
|
||||
}
|
||||
},
|
||||
@ -403,7 +403,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '查询生图任务失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 查询生图任务失败:', error);
|
||||
logCatch(error, "image_task_fetch", { taskId });
|
||||
return { success: false, error: '查询生图任务失败' };
|
||||
}
|
||||
},
|
||||
@ -422,7 +422,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '抠图失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 抠图失败:', error);
|
||||
logCatch(error, "remove_background");
|
||||
return { success: false, error: '抠图失败' };
|
||||
}
|
||||
},
|
||||
@ -458,7 +458,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '框选抠图失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 框选抠图失败:', error);
|
||||
logCatch(error, "box_cutout");
|
||||
return { success: false, error: '框选抠图失败' };
|
||||
}
|
||||
},
|
||||
@ -477,7 +477,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '去水印失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 去水印失败:', error);
|
||||
logCatch(error, "remove_watermark");
|
||||
return { success: false, error: '去水印失败' };
|
||||
}
|
||||
},
|
||||
@ -504,7 +504,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '图片增强失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图片增强失败:', error);
|
||||
logCatch(error, "enhance_image");
|
||||
return { success: false, error: '图片增强失败' };
|
||||
}
|
||||
},
|
||||
@ -559,7 +559,7 @@ export const imageGenerationService = {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图像增强 Pro 失败:', error);
|
||||
logCatch(error, "enhance_image_pro");
|
||||
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
|
||||
return {
|
||||
success: false,
|
||||
@ -612,7 +612,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || data.detail || '图片扩展失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图片扩展失败:', error);
|
||||
logCatch(error, "extend_image");
|
||||
return { success: false, error: '图片扩展失败' };
|
||||
}
|
||||
},
|
||||
@ -658,7 +658,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '图层拆分失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 图层拆分失败:', error);
|
||||
logCatch(error, "split_image_layers");
|
||||
return { success: false, error: '图层拆分失败' };
|
||||
}
|
||||
},
|
||||
@ -714,7 +714,7 @@ export const imageGenerationService = {
|
||||
}
|
||||
return { success: false, error: data.error || '尺寸调整失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 尺寸调整失败:', error);
|
||||
logCatch(error, "resize_image", { width, height });
|
||||
return { success: false, error: '尺寸调整失败' };
|
||||
}
|
||||
},
|
||||
@ -796,7 +796,7 @@ export const imageGenerationService = {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 文字识别失败:', error);
|
||||
logCatch(error, "detect_canvas_text");
|
||||
return { success: false, error: '文字识别失败' };
|
||||
}
|
||||
},
|
||||
@ -826,7 +826,7 @@ export const imageGenerationService = {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 文案建议生成失败:', error);
|
||||
logCatch(error, "text_suggestions");
|
||||
return { success: false, error: '文案建议生成失败' };
|
||||
}
|
||||
},
|
||||
@ -845,7 +845,7 @@ export const imageGenerationService = {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 文字替换失败:', error);
|
||||
logCatch(error, "edit_canvas_text", { itemCount: params.items.length });
|
||||
return { success: false, error: '文字替换失败' };
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user