chore: expand canvas log hygiene coverage

This commit is contained in:
Your Name 2026-05-26 22:02:17 +08:00
parent 91a98083a1
commit fadcbbc206
7 changed files with 284 additions and 258 deletions

View File

@ -1,25 +1,27 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { callQwen3Vl } from "@/lib/ai/qwen3-vl"; 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) { export async function POST(request: NextRequest) {
console.log("[Auto-title] API called"); logger.debug("request_received");
try { try {
const session = await auth(); const session = await auth();
if (!session?.user?.id) { if (!session?.user?.id) {
console.log("[Auto-title] Unauthorized"); logger.warn("unauthorized");
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
const { imageUrl } = await request.json(); const { imageUrl } = await request.json();
console.log("[Auto-title] Image URL:", imageUrl);
if (!imageUrl) { if (!imageUrl) {
return NextResponse.json({ error: "Image URL is required" }, { status: 400 }); return NextResponse.json({ error: "Image URL is required" }, { status: 400 });
} }
console.log("[Auto-title] 使用硬编码模型: qwen3-vl"); logger.debug("model_selected", { model: "qwen3-vl" });
// 构造 Prompt // 构造 Prompt
const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。"; const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。";
@ -35,11 +37,11 @@ export async function POST(request: NextRequest) {
title = title.replace(/[\s\p{P}\p{S}]+/gu, ""); title = title.replace(/[\s\p{P}\p{S}]+/gu, "");
if (title.length > 4) title = title.slice(0, 4); if (title.length > 4) title = title.slice(0, 4);
if (title.length < 2) title = "新画布"; 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 }); return NextResponse.json({ title });
} catch (error) { } catch (error) {
console.error("[Auto-title] Error:", error); logger.error("auto_title_failed", error);
// 失败时返回默认标题,不报错中断流程 // 失败时返回默认标题,不报错中断流程
return NextResponse.json({ title: "新画布" }); return NextResponse.json({ title: "新画布" });
} }

View File

@ -17,6 +17,7 @@ import {
type DetailKimiGenerateOptions, type DetailKimiGenerateOptions,
} from "@/lib/ai/detail-agent-kimi-profile"; } from "@/lib/ai/detail-agent-kimi-profile";
import { inlineVisionImages } from "@/lib/ai/vision-inline"; import { inlineVisionImages } from "@/lib/ai/vision-inline";
import { createLogger } from "@/lib/observability/logger";
import { filterReachableRemoteUrls } from "@/lib/net/url-access"; import { filterReachableRemoteUrls } from "@/lib/net/url-access";
import { import {
buildDetailUiProtocol, buildDetailUiProtocol,
@ -47,6 +48,7 @@ const DETAIL_PLAN_INLINE_TIMEOUT_MS = 4000;
const DETAIL_PLAN_INLINE_RETRIES = 1; const DETAIL_PLAN_INLINE_RETRIES = 1;
const DETAIL_PLAN_MAIN_TIMEOUT_MS = 45000; const DETAIL_PLAN_MAIN_TIMEOUT_MS = 45000;
const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 10000; const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 10000;
const logger = createLogger("canvas-detail-plan");
function isAbortError(error: unknown): boolean { function isAbortError(error: unknown): boolean {
const target = error as { const target = error as {
@ -1774,7 +1776,7 @@ async function runDualDirectorPlanPreflight(params: {
candidateScreens, candidateScreens,
}; };
} catch (error) { } catch (error) {
console.error("[Canvas Detail Plan] preflight error:", error); logger.warn("preflight_failed", error);
return defaultResult; return defaultResult;
} }
} }
@ -1808,19 +1810,17 @@ export async function POST(request: Request) {
const bundleImageUrls = bundleImageCheck.kept.slice(0, 3); const bundleImageUrls = bundleImageCheck.kept.slice(0, 3);
const modelImageUrl = modelImageCheck.kept[0] || ""; const modelImageUrl = modelImageCheck.kept[0] || "";
if (referenceImageCheck.dropped.length > 0) { if (referenceImageCheck.dropped.length > 0) {
console.warn("[Canvas Detail Plan] Dropping unreachable referenceImages:", { logger.warn("reference_images_dropped", {
count: referenceImageCheck.dropped.length, count: referenceImageCheck.dropped.length,
urls: referenceImageCheck.dropped.slice(0, 6),
}); });
} }
if (bundleImageCheck.dropped.length > 0) { if (bundleImageCheck.dropped.length > 0) {
console.warn("[Canvas Detail Plan] Dropping unreachable bundleImageUrls:", { logger.warn("bundle_images_dropped", {
count: bundleImageCheck.dropped.length, count: bundleImageCheck.dropped.length,
urls: bundleImageCheck.dropped.slice(0, 3),
}); });
} }
if (modelImageUrlRaw && !modelImageUrl) { if (modelImageUrlRaw && !modelImageUrl) {
console.warn("[Canvas Detail Plan] modelImageUrl unreachable, ignore this run:", modelImageUrlRaw); logger.warn("model_image_dropped");
} }
const effectiveReferenceImageNotes = const effectiveReferenceImageNotes =
referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : []; referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : [];
@ -1935,7 +1935,7 @@ export async function POST(request: Request) {
isLocalMoonshot: Boolean(localDetailAgentResolved), isLocalMoonshot: Boolean(localDetailAgentResolved),
}); });
console.log("[Canvas Detail Plan] Planner model selected:", { logger.info("planner_model_selected", {
id: selectedModelRef.id, id: selectedModelRef.id,
displayName: selectedModelRef.displayName, displayName: selectedModelRef.displayName,
modelId: selectedModelRef.modelId, modelId: selectedModelRef.modelId,
@ -2030,10 +2030,9 @@ export async function POST(request: Request) {
} }
} catch (error) { } catch (error) {
if (isReferenceImageDownloadError(error) || isTransientNetworkError(error)) { if (isReferenceImageDownloadError(error) || isTransientNetworkError(error)) {
console.warn( logger.warn("vision_input_preprocessing_failed", error, {
"[Canvas Detail Plan] Vision input preprocessing failed, retry without reference images:", referenceImageCount: referenceImages.length,
error });
);
imageInputs = []; imageInputs = [];
} else { } else {
throw error; throw error;
@ -2125,10 +2124,9 @@ export async function POST(request: Request) {
if (!shouldRetryWithoutImages) { if (!shouldRetryWithoutImages) {
throw firstError; throw firstError;
} }
console.warn( logger.warn("planner_retry_without_reference_images", firstError, {
"[Canvas Detail Plan] Primary planning with reference images failed, retrying without reference images:", imageInputCount: imageInputs.length,
firstError });
);
const retryResult = await runPlanner([]); const retryResult = await runPlanner([]);
text = String(retryResult?.text || ""); text = String(retryResult?.text || "");
} }
@ -2141,7 +2139,7 @@ export async function POST(request: Request) {
isTransientNetworkError(error) || isTransientNetworkError(error) ||
/operationtimeouterror|timed out|timeout/i.test(message) /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) const fallbackReason = /invalid scheme/i.test(message)
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。" ? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
: isReferenceImageDownloadError(error) : isReferenceImageDownloadError(error)
@ -2384,7 +2382,7 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true, data: response }); return NextResponse.json({ success: true, data: response });
} catch (error) { } catch (error) {
console.error("[Canvas Detail Plan] Error:", error); logger.error("detail_plan_failed", error);
return NextResponse.json( return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : "详情规划失败" }, { success: false, error: error instanceof Error ? error.message : "详情规划失败" },
{ status: 500 }, { status: 500 },

View File

@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { z } from "zod"; import { z } from "zod";
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { callQwen3Vl } from "@/lib/ai/qwen3-vl"; import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
import { createLogger } from "@/lib/observability/logger";
import sharp from "sharp"; import sharp from "sharp";
export const runtime = "nodejs"; export const runtime = "nodejs";
@ -13,6 +14,7 @@ const requestSchema = z.object({
const OCR_IMAGE_FETCH_TIMEOUT_MS = 15000; const OCR_IMAGE_FETCH_TIMEOUT_MS = 15000;
const OCR_IMAGE_MAX_SIDE = 1400; const OCR_IMAGE_MAX_SIDE = 1400;
const logger = createLogger("canvas-text-detect");
interface OcrItem { interface OcrItem {
id: string; id: string;
@ -145,7 +147,7 @@ export async function POST(request: Request) {
return NextResponse.json({ success: false, error: "仅支持 URL 图片,不允许 base64 传输" }, { status: 400 }); 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 = [ const prompt = [
"你是图片文字识别助手。", "你是图片文字识别助手。",
"请识别图片中可见文字并按阅读顺序输出 JSON。", "请识别图片中可见文字并按阅读顺序输出 JSON。",
@ -172,7 +174,9 @@ export async function POST(request: Request) {
throw error; 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 dataUrl = await buildVisionDataUrl(imageUrl);
const fallbackResult = await callQwen3Vl({ const fallbackResult = await callQwen3Vl({
imageUrl: dataUrl, imageUrl: dataUrl,
@ -205,7 +209,7 @@ export async function POST(request: Request) {
}, },
}); });
} catch (error) { } catch (error) {
console.error("[Canvas Text Detect] Error:", error); logger.error("text_detect_failed", error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@ -32,6 +32,7 @@ import {
type SceneFusionImageGenConfig, type SceneFusionImageGenConfig,
} from "@/lib/scene-fusion/output-capabilities"; } from "@/lib/scene-fusion/output-capabilities";
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency"; import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
import { createLogger } from "@/lib/observability/logger";
// 请求体 Schema // 请求体 Schema
const sceneFusionRequestSchema = z.object({ const sceneFusionRequestSchema = z.object({
@ -70,6 +71,7 @@ const SCENE_FUSION_RANDOM_BG_PROMPT_TAIL =
"硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。"; "硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。";
let sceneFusionRoundRobinCursor = 0; let sceneFusionRoundRobinCursor = 0;
type SceneFusionModelCooldown = Map<string, number>; type SceneFusionModelCooldown = Map<string, number>;
const logger = createLogger("scene-fusion");
interface SceneFusionModelCandidate { interface SceneFusionModelCandidate {
model: { model: {
@ -304,8 +306,7 @@ async function getAverageHashBits(
hashCache.set(imageUrl, hashBits); hashCache.set(imageUrl, hashBits);
return hashBits; return hashBits;
} catch (error) { } catch (error) {
console.warn("[SceneFusion] Echo hash compute skipped:", { logger.warn("echo_hash_compute_skipped", {
imageUrl: imageUrl.slice(0, 120),
error: error instanceof Error ? error.message : "unknown", error: error instanceof Error ? error.message : "unknown",
}); });
return null; return null;
@ -357,9 +358,12 @@ async function detectEchoedProductResult(
const distance = hammingDistanceBits(productHash, generatedHash); const distance = hammingDistanceBits(productHash, generatedHash);
if (distance <= SCENE_FUSION_ECHO_HASH_THRESHOLD) { if (distance <= SCENE_FUSION_ECHO_HASH_THRESHOLD) {
console.warn( logger.warn("echo_guard_hit", {
`[SceneFusion][${params.requestId}] Echo-guard hit: scene#${params.sceneIndex + 1}, model=${params.modelName}, distance=${distance}` requestId: params.requestId,
); sceneIndex: params.sceneIndex + 1,
modelName: params.modelName,
distance,
});
return { return {
tooSimilar: true, tooSimilar: true,
reason: "hash_too_close", reason: "hash_too_close",
@ -603,7 +607,8 @@ async function reviewSceneFusionResult(params: {
suspectedEcho !== true && suspectedEcho !== true &&
score >= 0.5; score >= 0.5;
console.log(`[SceneFusion][${params.requestId}] Review result:`, { logger.info("review_result", {
requestId: params.requestId,
sceneIndex: params.sceneIndex + 1, sceneIndex: params.sceneIndex + 1,
candidate: params.modelName, candidate: params.modelName,
passed, passed,
@ -621,7 +626,8 @@ async function reviewSceneFusionResult(params: {
model, model,
}; };
} catch (error) { } catch (error) {
console.warn(`[SceneFusion][${params.requestId}] Review skipped:`, { logger.warn("review_skipped", {
requestId: params.requestId,
sceneIndex: params.sceneIndex + 1, sceneIndex: params.sceneIndex + 1,
candidate: params.modelName, candidate: params.modelName,
error: error instanceof Error ? error.message : "unknown", error: error instanceof Error ? error.message : "unknown",
@ -745,10 +751,10 @@ async function enrichRandomBackgroundScenesWithAgent(params: {
scene.customPrompt = mergeCustomPrompt(scene.customPrompt, `场景建议:${generatedPrompt}`); scene.customPrompt = mergeCustomPrompt(scene.customPrompt, `场景建议:${generatedPrompt}`);
} }
} catch (error) { } catch (error) {
console.warn( logger.warn("random_prompt_enhance_skipped", error, {
`[SceneFusion][${params.requestId}] Random scene prompt enhance skipped: scene=${scene.id}`, requestId: params.requestId,
error sceneId: scene.id,
); });
} }
} }
}); });
@ -809,7 +815,7 @@ async function enrichSceneLabelsWithAgent(params: {
meta.productModel = productDetected.model; meta.productModel = productDetected.model;
} }
} catch (error) { } catch (error) {
console.warn(`[SceneFusion][${params.requestId}] Product detect skipped:`, error); logger.warn("product_detect_skipped", error, { requestId: params.requestId });
} }
const unresolvedIndexes = clonedScenes const unresolvedIndexes = clonedScenes
@ -858,10 +864,10 @@ async function enrichSceneLabelsWithAgent(params: {
metaScene.model = targetDetected.model; metaScene.model = targetDetected.model;
} }
} catch (error) { } catch (error) {
console.warn( logger.warn("scene_target_detect_skipped", error, {
`[SceneFusion][${params.requestId}] Scene target detect skipped: scene=${scene.id}`, requestId: params.requestId,
error sceneId: scene.id,
); });
} }
} }
}); });
@ -957,7 +963,7 @@ async function getSceneFusionConfig() {
}; };
} }
} catch (error) { } catch (error) {
console.error("[SceneFusion] Failed to load config:", error); logger.error("config_load_failed", error);
} }
return { return {
@ -1000,9 +1006,10 @@ async function preprocessProductImageWithRmbg(
const candidateUrls = buildPreRmbgCandidateUrls(request); const candidateUrls = buildPreRmbgCandidateUrls(request);
let lastReason = "no_candidate_url"; let lastReason = "no_candidate_url";
console.log( logger.info("pre_rmbg_started", {
`[SceneFusion][${requestId}] Pre-RMBG start: ${productImageUrl.substring(0, 120)}..., candidates=${candidateUrls.join(" | ")}` requestId,
); candidateCount: candidateUrls.length,
});
for (const rmbgUrl of candidateUrls) { for (const rmbgUrl of candidateUrls) {
const controller = new AbortController(); const controller = new AbortController();
@ -1026,7 +1033,8 @@ async function preprocessProductImageWithRmbg(
if (!response.ok) { if (!response.ok) {
lastReason = `request_failed_${response.status}`; lastReason = `request_failed_${response.status}`;
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate failed: ${rmbgUrl}`, { logger.warn("pre_rmbg_candidate_failed", {
requestId,
status: response.status, status: response.status,
detail: payload?.detail || payload?.error || "unknown", detail: payload?.detail || payload?.error || "unknown",
}); });
@ -1036,7 +1044,8 @@ async function preprocessProductImageWithRmbg(
const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim(); const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim();
if (!cleanedUrl || cleanedUrl === productImageUrl) { if (!cleanedUrl || cleanedUrl === productImageUrl) {
lastReason = cleanedUrl ? "same_as_source" : "empty_result"; 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, reason: lastReason,
}); });
continue; continue;
@ -1049,7 +1058,7 @@ async function preprocessProductImageWithRmbg(
}; };
} catch (error) { } catch (error) {
lastReason = error instanceof Error ? error.message : "exception"; 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 { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
@ -1073,7 +1082,7 @@ async function createThumbnail(buffer: Buffer): Promise<Buffer> {
.toBuffer(); .toBuffer();
return thumbnailBuffer; return thumbnailBuffer;
} catch (error) { } catch (error) {
console.error("[SceneFusion] Failed to create thumbnail:", error); logger.error("thumbnail_create_failed", error);
return buffer; return buffer;
} }
} }
@ -1098,10 +1107,10 @@ async function uploadBase64ToS3(base64Data: string, prefix: string = "scene-fusi
const filename = generateFilename(`${prefix}-ref`, "png"); const filename = generateFilename(`${prefix}-ref`, "png");
const result = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)); 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; return result.url;
} catch (error) { } catch (error) {
console.error("[SceneFusion] Failed to upload reference image:", error); logger.error("reference_image_upload_failed", error, { prefix });
throw error; throw error;
} }
} }
@ -1114,7 +1123,7 @@ async function downloadAndUploadToS3(
prefix: string = "scene-fusion" prefix: string = "scene-fusion"
): Promise<{ thumbnail: string; original: string }> { ): Promise<{ thumbnail: string; original: string }> {
try { try {
console.log("[SceneFusion] Downloading image from:", imageUrl.substring(0, 100) + "..."); logger.debug("download_upload_started", { prefix });
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
if (!response.ok) { if (!response.ok) {
@ -1122,7 +1131,7 @@ async function downloadAndUploadToS3(
} }
const buffer = Buffer.from(await response.arrayBuffer()); 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); const thumbnailBuffer = await createThumbnail(buffer);
@ -1130,20 +1139,20 @@ async function downloadAndUploadToS3(
// 上传原图 // 上传原图
const originalFilename = generateFilename(`${prefix}-orig`, "png"); const originalFilename = generateFilename(`${prefix}-orig`, "png");
const originalResult = await uploadFile(originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)); 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 thumbnailFilename = generateFilename(`${prefix}-thumb`, "jpg");
const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer; const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer;
const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer); const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer);
console.log(`[SceneFusion] Uploaded thumbnail to: ${thumbnailResult.url}`); logger.debug("thumbnail_uploaded", { prefix, bytes: thumbnailBuffer.byteLength });
return { return {
original: originalResult.url, original: originalResult.url,
thumbnail: thumbnailResult.url, thumbnail: thumbnailResult.url,
}; };
} catch (error) { } catch (error) {
console.error("[SceneFusion] Failed to download/upload image:", error); logger.error("download_upload_failed", error, { prefix });
throw error; throw error;
} }
} }
@ -1229,7 +1238,7 @@ async function saveSceneFusionImageRecord(params: {
} }
} }
} catch (assetError) { } 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: logger.debug("alibaba_request_prepared", {
- Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)} modelId,
- Product URL: ${productImageUrl} size: size || "auto",
- Scene URL: ${sceneImageUrl} productImageCount: productImageUrl ? 1 : 0,
- Size: ${size || 'auto'} sceneImageCount: sceneImageUrl ? 1 : 0,
- Prompt: ${prompt} promptLength: prompt.length,
`); });
const response = await fetch(baseURL, { const response = await fetch(baseURL, {
method: "POST", method: "POST",
@ -1337,23 +1346,35 @@ async function callAlibabaImageAPI(
const data = await response.json(); const data = await response.json();
// 打印完整响应用于调试 logger.debug("alibaba_response_received", {
console.log(`[SceneFusion][${modelId}] Full API Response:`, JSON.stringify(data, null, 2)); modelId,
ok: response.ok,
code: data?.code ?? null,
hasOutput: Boolean(data?.output),
});
if (!response.ok || data.code) { 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}`)); throw new Error(data.message || (data.code ? `Error ${data.code}: ${data.message}` : `API request failed: ${response.status}`));
} }
// 提取生成的图片 URL // 提取生成的图片 URL
const choices = data.output?.choices; const choices = data.output?.choices;
if (!choices || choices.length === 0) { 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"); throw new Error("No images in response");
} }
const imageContent = choices[0]?.message?.content; 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) { if (!imageContent || imageContent.length === 0) {
throw new Error("No image content in response"); throw new Error("No image content in response");
@ -1364,25 +1385,25 @@ async function callAlibabaImageAPI(
for (const item of imageContent) { for (const item of imageContent) {
if (item.image) { if (item.image) {
generatedImageUrl = item.image; generatedImageUrl = item.image;
console.log(`[SceneFusion] Found image URL in content item`); logger.debug("alibaba_image_url_found", { modelId });
break; break;
} }
} }
if (!generatedImageUrl) { 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"); throw new Error("No image URL in response");
} }
// 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图) // 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图)
if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) { if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) {
console.warn(`[SceneFusion] ⚠️ WARNING: API returned same URL as input!`); logger.warn("alibaba_returned_input_url", { modelId });
console.warn(`[SceneFusion] - Generated URL: ${generatedImageUrl}`);
console.warn(`[SceneFusion] - Product URL: ${productImageUrl}`);
console.warn(`[SceneFusion] - Scene URL: ${sceneImageUrl}`);
} }
console.log("[SceneFusion] Generated image URL:", generatedImageUrl.substring(0, 100) + "..."); logger.debug("alibaba_image_generated", { modelId });
return generatedImageUrl; return generatedImageUrl;
} }
@ -1468,8 +1489,11 @@ async function processSceneWithProvider(
sceneMode sceneMode
); );
console.log(`[SceneFusion] Built prompt: ${prompt}`); logger.debug("provider_prompt_built", {
console.log(`[SceneFusion] Provider type: ${provider.type}`); providerType: provider.type,
promptLength: prompt.length,
sceneMode,
});
// 根据 provider 类型选择不同的处理逻辑 // 根据 provider 类型选择不同的处理逻辑
switch (provider.type) { switch (provider.type) {
@ -1541,7 +1565,7 @@ async function processWithGemini(
aspectRatio?: string, aspectRatio?: string,
imageSize?: string imageSize?: string
): Promise<{ thumbnail: string; original: string; prompt: 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 // 创建 Gemini provider
const geminiProvider = createGeminiImage({ const geminiProvider = createGeminiImage({
@ -1589,7 +1613,7 @@ async function processWithRuxaImage(
aspectRatio?: string, aspectRatio?: string,
imageSize?: string imageSize?: string
): Promise<{ thumbnail: string; original: string; prompt: 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({ const ruxaProvider = createRuxaImage({
baseURL: provider.baseUrl, baseURL: provider.baseUrl,
@ -1629,7 +1653,7 @@ async function processWithFalImage(
aspectRatio?: string, aspectRatio?: string,
imageSize?: string imageSize?: string
): Promise<{ thumbnail: string; original: string; prompt: 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({ const falProvider = createFalImage({
baseURL: provider.baseUrl, baseURL: provider.baseUrl,
@ -1669,7 +1693,7 @@ async function processWithOpenAIImage(
aspectRatio?: string, aspectRatio?: string,
imageSize?: string imageSize?: string
): Promise<{ thumbnail: string; original: string; prompt: 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({ const openAIImageProvider = createOpenAIImage({
baseURL: provider.baseUrl, baseURL: provider.baseUrl,
@ -1712,7 +1736,7 @@ async function processWithVolcanoVision(
aspectRatio?: string, aspectRatio?: string,
imageSize?: string imageSize?: string
): Promise<{ thumbnail: string; original: string; prompt: 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 // 创建 Volcano Vision provider
const volcanoProvider = createVolcanoVision({ const volcanoProvider = createVolcanoVision({
@ -1753,7 +1777,7 @@ async function processWithVolcanoVision(
* 格式: ![thumb|||originalUrl|||size](thumbnailUrl) * 格式: ![thumb|||originalUrl|||size](thumbnailUrl)
*/ */
function parseImageFromMarkdown(markdown: string): { thumbnail: string; original: string } { 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 });
// 匹配 ![thumb|||originalUrl|||size](thumbnailUrl) 格式 // 匹配 ![thumb|||originalUrl|||size](thumbnailUrl) 格式
const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/); const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
@ -1805,7 +1829,7 @@ async function processScene(
sceneMode sceneMode
); );
console.log(`[SceneFusion] Built prompt: ${prompt}`); logger.debug("legacy_prompt_built", { promptLength: prompt.length, sceneMode });
// 调用阿里生图 API // 调用阿里生图 API
const generatedImageUrl = await callAlibabaImageAPI( const generatedImageUrl = await callAlibabaImageAPI(
@ -1890,14 +1914,15 @@ async function processSceneWithDiversity(
const attemptCandidates = const attemptCandidates =
orderedHealthyCandidates.length > 0 ? orderedHealthyCandidates : orderedFallbackCandidates; orderedHealthyCandidates.length > 0 ? orderedHealthyCandidates : orderedFallbackCandidates;
console.log( logger.debug("candidate_order_resolved", {
`[SceneFusion][${params.requestId}] Scene#${params.sceneIndex + 1} candidate order: ${attemptCandidates requestId: params.requestId,
.map( sceneIndex: params.sceneIndex + 1,
(candidate) => candidates: attemptCandidates.map((candidate) => ({
`${candidate.model.displayName}/${candidate.provider.type}/score=${getSceneFusionCandidatePriority(candidate)}` modelId: candidate.model.modelId,
) providerType: candidate.provider.type,
.join(" -> ")}` score: getSceneFusionCandidatePriority(candidate),
); })),
});
const failureReasons: string[] = []; const failureReasons: string[] = [];
@ -1909,9 +1934,12 @@ async function processSceneWithDiversity(
imageSize: params.imageSize, imageSize: params.imageSize,
}); });
if (normalizedOutputParams.changed) { if (normalizedOutputParams.changed) {
console.log( logger.info("candidate_output_params_normalized", {
`[SceneFusion][${params.requestId}] Candidate ${candidate.model.displayName} output params normalized: ${normalizedOutputParams.changes.join(", ")}` requestId: params.requestId,
); sceneIndex: params.sceneIndex + 1,
modelId: candidate.model.modelId,
changes: normalizedOutputParams.changes,
});
} }
const modelSlot = await acquireCanvasImageModelSlot({ const modelSlot = await acquireCanvasImageModelSlot({
@ -2000,17 +2028,19 @@ async function processSceneWithDiversity(
markSceneFusionCandidateFailed(candidate, params.modelCooldown); markSceneFusionCandidateFailed(candidate, params.modelCooldown);
// Batch processing can fail because of per-image inputs, review gates, or echo checks. // 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. // Keep these failures local to this request so they do not auto-disable shared models.
const logCandidateFailure = isSceneFusionReviewRejectError(errorMessage) const failureMeta = {
? console.warn
: console.error;
logCandidateFailure("[SceneFusion] Candidate failed, try next:", {
requestId: params.requestId, requestId: params.requestId,
sceneIndex: params.sceneIndex, sceneIndex: params.sceneIndex,
attempt: attemptIndex + 1, attempt: attemptIndex + 1,
model: candidate.model.modelId, model: candidate.model.modelId,
provider: candidate.provider.type, provider: candidate.provider.type,
error: errorMessage, 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"); 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); 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. 获取生图模型池(固定多样性模式) // 4. 获取生图模型池(固定多样性模式)
const allModels = await getEnabledAiModelsWithProvider(); const allModels = await getEnabledAiModelsWithProvider();
const candidateModels = buildSceneFusionCandidatePool(allModels, config.modelId); const candidateModels = buildSceneFusionCandidatePool(allModels, config.modelId);
if (config.modelId && !candidateModels.some((candidate) => candidate.model.id === config.modelId)) { if (config.modelId && !candidateModels.some((candidate) => candidate.model.id === config.modelId)) {
console.warn( logger.warn("preferred_model_unavailable", { requestId, modelId: config.modelId });
`[SceneFusion] Preferred model ${config.modelId} not available, fallback to full diversity pool`
);
} }
if (candidateModels.length === 0) { if (candidateModels.length === 0) {
@ -2093,37 +2124,39 @@ export async function POST(request: Request) {
); );
} }
console.log( logger.info("candidate_pool_resolved", {
`[SceneFusion] Diversity candidates: ${candidateModels requestId,
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`) candidateCount: candidateModels.length,
.join(", ")}` providers: [...new Set(candidateModels.map((candidate) => candidate.provider.type))],
); });
const unsupportedCandidates = candidateModels.filter( const unsupportedCandidates = candidateModels.filter(
(candidate) => !canProcessSceneFusionProviderType(candidate.provider.type) (candidate) => !canProcessSceneFusionProviderType(candidate.provider.type)
); );
if (unsupportedCandidates.length > 0) { if (unsupportedCandidates.length > 0) {
console.warn( logger.warn("unsupported_candidates_present", {
`[SceneFusion] Skip unsupported providers for scene fusion: ${unsupportedCandidates requestId,
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`) count: unsupportedCandidates.length,
.join(", ")}` providers: [...new Set(unsupportedCandidates.map((candidate) => candidate.provider.type))],
); });
} }
const nonVisionCandidates = candidateModels.filter( const nonVisionCandidates = candidateModels.filter(
(candidate) => !candidate.model.supportsVision (candidate) => !candidate.model.supportsVision
); );
if (nonVisionCandidates.length > 0) { if (nonVisionCandidates.length > 0) {
console.warn( logger.warn("non_vision_candidates_present", {
`[SceneFusion] Models without supportsVision are included for failover diversity: ${nonVisionCandidates requestId,
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`) count: nonVisionCandidates.length,
.join(", ")}` });
);
} }
const requestOffset = getNextSceneFusionRoundRobinOffset( const requestOffset = getNextSceneFusionRoundRobinOffset(
Math.max(1, candidateModels.length) Math.max(1, candidateModels.length)
); );
console.log( logger.debug("round_robin_offset_resolved", {
`[SceneFusion][${requestId}] Round-robin offset=${requestOffset}, scenes=${scenes.length}, candidates=${candidateModels.length}` requestId,
); requestOffset,
sceneCount: scenes.length,
candidateCount: candidateModels.length,
});
// 5. 预处理:将 Base64 图片上传到 S3 // 5. 预处理:将 Base64 图片上传到 S3
let productImageUrl = productImage; let productImageUrl = productImage;
@ -2138,9 +2171,12 @@ export async function POST(request: Request) {
shouldForcePreRmbg ? true : config.preRmbgEnabled shouldForcePreRmbg ? true : config.preRmbgEnabled
); );
const fusionProductImageUrl = preprocessedProduct.productImageUrl; const fusionProductImageUrl = preprocessedProduct.productImageUrl;
console.log( logger.info("pre_rmbg_resolved", {
`[SceneFusion][${requestId}] Pre-RMBG result: used=${preprocessedProduct.usedPreRmbg}, provider=${preprocessedProduct.provider || "-"}, reason=${preprocessedProduct.reason || "-"}` requestId,
); used: preprocessedProduct.usedPreRmbg,
provider: preprocessedProduct.provider || null,
reason: preprocessedProduct.reason || null,
});
const processedScenes = await Promise.all( const processedScenes = await Promise.all(
scenes.map(async (scene) => { scenes.map(async (scene) => {
@ -2175,17 +2211,18 @@ export async function POST(request: Request) {
const imageHashCache = new Map<string, string>(); const imageHashCache = new Map<string, string>();
const modelCooldown: SceneFusionModelCooldown = new Map(); const modelCooldown: SceneFusionModelCooldown = new Map();
console.log( logger.info("agent_labels_resolved", {
`[SceneFusion][${requestId}] Agent labels: product=${resolvedProductName || "(none)"}; targets=${resolvedScenes requestId,
.map((scene) => `${scene.id}:${scene.targetName || "-"}`) hasProductName: Boolean(resolvedProductName),
.join(", ")}` targetCount: resolvedScenes.filter((scene) => Boolean(scene.targetName)).length,
); sceneCount: resolvedScenes.length,
});
if (resolvedSceneMode === "random-bg") { if (resolvedSceneMode === "random-bg") {
console.log( logger.debug("random_bg_prompts_resolved", {
`[SceneFusion][${requestId}] Random-bg prompts: ${resolvedScenes requestId,
.map((scene) => `${scene.id}:${(scene.customPrompt || "").slice(0, 80) || "-"}`) sceneCount: resolvedScenes.length,
.join(" | ")}` promptCount: resolvedScenes.filter((scene) => Boolean(scene.customPrompt)).length,
); });
} }
// 6. 创建流式响应,每完成一张图就立即发送给前端 // 6. 创建流式响应,每完成一张图就立即发送给前端
@ -2199,7 +2236,7 @@ export async function POST(request: Request) {
try { try {
controller.close(); controller.close();
} catch (closeError) { } catch (closeError) {
console.warn("[SceneFusion] Stream close skipped:", closeError); logger.warn("stream_close_skipped", closeError, { requestId });
} }
}; };
const enqueue = (payload: Record<string, unknown>) => { const enqueue = (payload: Record<string, unknown>) => {
@ -2209,7 +2246,7 @@ export async function POST(request: Request) {
return true; return true;
} catch (enqueueError) { } catch (enqueueError) {
streamClosed = true; streamClosed = true;
console.warn("[SceneFusion] Stream enqueue skipped:", enqueueError); logger.warn("stream_enqueue_skipped", enqueueError, { requestId });
return false; return false;
} }
}; };
@ -2258,27 +2295,16 @@ export async function POST(request: Request) {
const resolvedMetaScene = const resolvedMetaScene =
enriched.meta.sceneTargets.find((item) => item.id === resolvedScene.id) || null; enriched.meta.sceneTargets.find((item) => item.id === resolvedScene.id) || null;
console.log( logger.info("scene_processing_started", {
`[SceneFusion] ========== Processing scene ${currentIndex + 1}/${processedScenes.length} ==========` requestId,
); sceneId: scene.id,
console.log(`[SceneFusion] Scene ID: ${scene.id}`); sceneIndex: currentIndex + 1,
console.log( total: processedScenes.length,
`[SceneFusion] Product Image URL: ${fusionProductImageUrl.substring(0, 80)}...` hasTargetName: Boolean(resolvedScene.targetName),
); hasPositionHint: Boolean(resolvedMetaScene?.positionHint),
console.log( hasReplaceCue: Boolean(resolvedMetaScene?.replaceCue),
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...` hasCustomPrompt: Boolean(resolvedScene.customPrompt),
); });
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)"}`
);
try { try {
const { result, candidate, attempts } = await processSceneWithDiversity({ const { result, candidate, attempts } = await processSceneWithDiversity({
@ -2333,9 +2359,13 @@ export async function POST(request: Request) {
modelId: candidate.model.modelId, modelId: candidate.model.modelId,
sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl], sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl],
}); });
console.log( logger.info("scene_saved", {
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...` requestId,
); sceneId: scene.id,
modelId: candidate.model.modelId,
providerType: candidate.provider.type,
attempts,
});
successCount += 1; successCount += 1;
if (!enqueue({ if (!enqueue({
@ -2356,7 +2386,11 @@ export async function POST(request: Request) {
break; break;
} }
} catch (error) { } 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; failCount += 1;
if (!enqueue({ if (!enqueue({
type: "result", type: "result",
@ -2375,7 +2409,12 @@ export async function POST(request: Request) {
await Promise.all(workers); 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({ enqueue({
type: "done", type: "done",
summary: { summary: {
@ -2386,7 +2425,7 @@ export async function POST(request: Request) {
}); });
} catch (error) { } catch (error) {
console.error("[SceneFusion] Stream error:", error); logger.error("stream_failed", error, { requestId });
// 发送错误消息 // 发送错误消息
if (!streamClosed) { if (!streamClosed) {
enqueue({ enqueue({
@ -2408,7 +2447,7 @@ export async function POST(request: Request) {
}, },
}); });
} catch (error) { } catch (error) {
console.error("[SceneFusion] Unexpected error:", error); logger.error("request_failed", error);
return NextResponse.json( return NextResponse.json(
{ error: error instanceof Error ? error.message : "处理失败" }, { error: error instanceof Error ? error.message : "处理失败" },
{ status: 500 } { status: 500 }

View File

@ -55,6 +55,7 @@ import {
import { requestLongTaskResult } from "@/lib/client/long-task"; import { requestLongTaskResult } from "@/lib/client/long-task";
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import { buildImageProxyUrl } from "@/lib/image-delivery"; import { buildImageProxyUrl } from "@/lib/image-delivery";
import { createClientLogger } from "@/lib/observability/client-logger";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AngleEditDialog } from "./angle-edit-dialog"; import { AngleEditDialog } from "./angle-edit-dialog";
import { SceneFusionView } from "./scene-fusion-view"; import { SceneFusionView } from "./scene-fusion-view";
@ -64,6 +65,8 @@ import { Slider } from "./ui/slider";
type Tool = "pen" | "arrow" | "circle" | "text"; type Tool = "pen" | "arrow" | "circle" | "text";
const logger = createClientLogger("image-annotator");
type DrawingAction = { type DrawingAction = {
type: Tool; type: Tool;
color: string; color: string;
@ -315,17 +318,14 @@ export function ImageAnnotator({
const localImage = await localPreviewPromise; const localImage = await localPreviewPromise;
setImage(localImage); setImage(localImage);
} catch (previewError) { } catch (previewError) {
console.warn( logger.warn("local_preview_failed", previewError);
"[ImageAnnotator] Local preview failed, fallback to uploaded url:",
previewError
);
const uploadedImage = await loadImageFromSource(uploadResult.url); const uploadedImage = await loadImageFromSource(uploadResult.url);
setImage(uploadedImage); setImage(uploadedImage);
} }
toast.success("图片上传成功"); toast.success("图片上传成功");
} catch (error) { } catch (error) {
console.error("[ImageAnnotator] 上传图片失败:", error); logger.error("upload_failed", error);
const message = const message =
error instanceof Error ? error.message : "上传失败,请重试"; error instanceof Error ? error.message : "上传失败,请重试";
toast.error(message); toast.error(message);
@ -517,7 +517,7 @@ export function ImageAnnotator({
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark); if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
}) })
.catch((err) => { .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); setSelectedAspectRatio(null);
} catch (error) { } catch (error) {
if (cancelled) return; if (cancelled) return;
console.error("初始化图片加载失败:", error); logger.error("initial_image_load_failed", error);
setImage(null); setImage(null);
setIsLoading(false); setIsLoading(false);
toast.error("当前图片加载失败,请重试"); toast.error("当前图片加载失败,请重试");
@ -892,9 +892,8 @@ export function ImageAnnotator({
setIsRemovingBg(true); setIsRemovingBg(true);
try { try {
// 使用原图URL获取原图数据,而不是canvas的预览图 // 使用原图URL获取原图数据,而不是canvas的预览图
console.log("[抠图] 使用原图URL获取数据:", originalImageUrl);
const base64Data = await imageUrlToBase64(originalImageUrl); const base64Data = await imageUrlToBase64(originalImageUrl);
console.log("[抠图] 原图Base64长度:", base64Data.length, "字符"); logger.debug("rmbg_source_prepared", { base64Length: base64Data.length });
// 调用抠图API // 调用抠图API
const response = await fetch("/api/rmbg", { const response = await fetch("/api/rmbg", {
@ -917,23 +916,18 @@ export function ImageAnnotator({
if (result.success && result.image_base64) { if (result.success && result.image_base64) {
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
console.log("[抠图] API返回结果:", { logger.info("rmbg_completed", {
hasOriginalUrl: !!result.original_url, hasOriginalUrl: !!result.original_url,
hasThumbnailUrl: !!result.thumbnail_url, hasThumbnailUrl: !!result.thumbnail_url,
originalUrl: result.original_url?.substring(0, 100), base64Length: String(result.image_base64 || "").length,
thumbnailUrl: result.thumbnail_url?.substring(0, 100),
}); });
if (result.original_url) { if (result.original_url) {
console.log("[抠图] ✅ 使用原图URL:", result.original_url);
setProcessedImageUrl(result.original_url); setProcessedImageUrl(result.original_url);
} else if (result.thumbnail_url) { } else if (result.thumbnail_url) {
console.warn( logger.warn("rmbg_original_url_missing", { hasThumbnailUrl: true });
"[抠图] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
result.thumbnail_url
);
setProcessedImageUrl(result.thumbnail_url); setProcessedImageUrl(result.thumbnail_url);
} else { } else {
console.warn("[抠图] ⚠️ 未找到图片URL"); logger.warn("rmbg_image_url_missing");
setProcessedImageUrl(null); setProcessedImageUrl(null);
} }
setProcessedImageBase64(result.image_base64); setProcessedImageBase64(result.image_base64);
@ -946,7 +940,7 @@ export function ImageAnnotator({
throw new Error(result.error || result.detail || "抠图处理失败"); throw new Error(result.error || result.detail || "抠图处理失败");
} }
} catch (error) { } catch (error) {
console.error("[抠图] 错误:", error); logger.error("rmbg_failed", error);
toast.error( toast.error(
error instanceof Error ? error.message : "抠图失败,请稍后重试" error instanceof Error ? error.message : "抠图失败,请稍后重试"
); );
@ -961,8 +955,6 @@ export function ImageAnnotator({
setIsRemovingWatermark(true); setIsRemovingWatermark(true);
try { try {
console.log("[去水印] 使用原图URL:", originalImageUrl);
const result = await requestLongTaskResult({ const result = await requestLongTaskResult({
endpoint: "/api/watermark-remove", endpoint: "/api/watermark-remove",
payload: { payload: {
@ -971,9 +963,9 @@ export function ImageAnnotator({
}); });
if (result.success && result.original_url) { if (result.success && result.original_url) {
console.log("[去水印] API返回结果:", { logger.info("watermark_remove_completed", {
originalUrl: result.original_url?.substring(0, 100), hasOriginalUrl: !!result.original_url,
thumbnailUrl: result.thumbnail_url?.substring(0, 100), hasThumbnailUrl: !!result.thumbnail_url,
}); });
setProcessedImageUrl(result.original_url); setProcessedImageUrl(result.original_url);
@ -988,7 +980,7 @@ export function ImageAnnotator({
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
} catch { } catch {
console.warn("[去水印] 无法获取Base64,仅使用URL"); logger.warn("watermark_remove_base64_fetch_failed");
} }
setProcessType("watermark"); setProcessType("watermark");
@ -999,7 +991,7 @@ export function ImageAnnotator({
throw new Error(result.error || "去水印处理失败"); throw new Error(result.error || "去水印处理失败");
} }
} catch (error) { } catch (error) {
console.error("[去水印] 错误:", error); logger.error("watermark_remove_failed", error);
toast.error( toast.error(
error instanceof Error ? error.message : "去水印失败,请稍后重试" error instanceof Error ? error.message : "去水印失败,请稍后重试"
); );
@ -1015,9 +1007,8 @@ export function ImageAnnotator({
setIsEnhancing(true); setIsEnhancing(true);
try { try {
// 使用原图URL获取原图数据,而不是canvas的预览图 // 使用原图URL获取原图数据,而不是canvas的预览图
console.log("[变清晰] 使用原图URL获取数据:", originalImageUrl);
const base64Data = await imageUrlToBase64(originalImageUrl); const base64Data = await imageUrlToBase64(originalImageUrl);
console.log("[变清晰] 原图Base64长度:", base64Data.length, "字符"); logger.debug("enhance_source_prepared", { base64Length: base64Data.length });
const result = await requestLongTaskResult({ const result = await requestLongTaskResult({
endpoint: "/api/enhance", endpoint: "/api/enhance",
@ -1032,40 +1023,28 @@ export function ImageAnnotator({
if (result.success && result.image_base64) { if (result.success && result.image_base64) {
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图) // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图)
console.log("[变清晰] API返回结果:", { logger.info("enhance_completed", {
hasOriginalUrl: !!result.original_url, hasOriginalUrl: !!result.original_url,
hasThumbnailUrl: !!result.thumbnail_url, hasThumbnailUrl: !!result.thumbnail_url,
originalUrl: result.original_url,
thumbnailUrl: result.thumbnail_url,
base64Length: result.image_base64?.length || 0, base64Length: result.image_base64?.length || 0,
}); });
// ✅ 验证:检查返回的URL是否是处理后的图片 // ✅ 验证:检查返回的URL是否是处理后的图片
if (result.original_url) { if (result.original_url) {
console.log("[变清晰] ✅ 使用处理后的原图URL:", result.original_url);
console.log(
"[变清晰] ✅ URL应该包含 'enhance-orig' 标识,确认是处理后的图片"
);
// 验证URL格式(应该包含enhance-orig) // 验证URL格式(应该包含enhance-orig)
if ( if (
!result.original_url.includes("enhance-orig") && !result.original_url.includes("enhance-orig") &&
!result.original_url.includes("enhance-orig") !result.original_url.includes("enhance-orig")
) { ) {
console.warn( logger.warn("enhance_result_marker_missing");
"[变清晰] ⚠️ 警告:URL不包含 'enhance-orig' 标识,可能不是处理后的图片!"
);
} }
setProcessedImageUrl(result.original_url); setProcessedImageUrl(result.original_url);
} else if (result.thumbnail_url) { } else if (result.thumbnail_url) {
console.warn( logger.warn("enhance_original_url_missing", { hasThumbnailUrl: true });
"[变清晰] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
result.thumbnail_url
);
setProcessedImageUrl(result.thumbnail_url); setProcessedImageUrl(result.thumbnail_url);
} else { } else {
console.warn("[变清晰] ⚠️ 未找到图片URL"); logger.warn("enhance_image_url_missing");
setProcessedImageUrl(null); setProcessedImageUrl(null);
} }
setProcessedImageBase64(result.image_base64); setProcessedImageBase64(result.image_base64);
@ -1078,7 +1057,7 @@ export function ImageAnnotator({
throw new Error(result.error || result.detail || "变清晰处理失败"); throw new Error(result.error || result.detail || "变清晰处理失败");
} }
} catch (error) { } catch (error) {
console.error("[变清晰] 错误:", error); logger.error("enhance_failed", error);
toast.error( toast.error(
error instanceof Error ? error.message : "变清晰失败,请稍后重试" error instanceof Error ? error.message : "变清晰失败,请稍后重试"
); );
@ -1359,7 +1338,7 @@ export function ImageAnnotator({
const finalResultUrl = result.original_url || dataUrl; const finalResultUrl = result.original_url || dataUrl;
if (result.original_url) { if (result.original_url) {
console.log("[尺寸调整] 原图URL:", result.original_url); logger.info("resize_completed", { hasOriginalUrl: true });
setProcessedImageUrl(result.original_url); setProcessedImageUrl(result.original_url);
} else { } else {
setProcessedImageUrl(null); setProcessedImageUrl(null);
@ -1372,7 +1351,7 @@ export function ImageAnnotator({
throw new Error(result.error || "尺寸调整处理失败"); throw new Error(result.error || "尺寸调整处理失败");
} }
} catch (error) { } catch (error) {
console.error("[尺寸调整] 错误:", error); logger.error("resize_failed", error);
toast.error( toast.error(
error instanceof Error ? error.message : "尺寸调整失败,请稍后重试" error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"
); );
@ -1388,8 +1367,6 @@ export function ImageAnnotator({
setIsExtending(true); setIsExtending(true);
try { try {
// 直接使用原图URL,而不是转换为base64 // 直接使用原图URL,而不是转换为base64
console.log("[图像拓展] 使用原图URL:", originalImageUrl);
// 调用图像拓展API(直接发送URL) // 调用图像拓展API(直接发送URL)
const response = await fetch("/api/extend", { const response = await fetch("/api/extend", {
method: "POST", method: "POST",
@ -1413,15 +1390,17 @@ export function ImageAnnotator({
if (result.success) { if (result.success) {
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
if (result.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); setProcessedImageUrl(result.original_url);
} else if (result.thumbnail_url) { } else if (result.thumbnail_url) {
console.warn( logger.warn("extend_original_url_missing", { hasThumbnailUrl: true });
"[图像拓展] ⚠️ 未找到original_url,使用thumbnail_url作为备选"
);
setProcessedImageUrl(result.thumbnail_url); setProcessedImageUrl(result.thumbnail_url);
} else { } else {
console.warn("[图像拓展] ⚠️ 未找到图片URL"); logger.warn("extend_image_url_missing");
setProcessedImageUrl(null); setProcessedImageUrl(null);
} }
// 优先使用返回的base64,如果没有则使用thumbnail_url或original_url // 优先使用返回的base64,如果没有则使用thumbnail_url或original_url
@ -1435,7 +1414,7 @@ export function ImageAnnotator({
throw new Error(result.error || result.detail || "图像拓展处理失败"); throw new Error(result.error || result.detail || "图像拓展处理失败");
} }
} catch (error) { } catch (error) {
console.error("[图像拓展] 错误:", error); logger.error("extend_failed", error);
toast.error( toast.error(
error instanceof Error ? error.message : "图像拓展失败,请稍后重试" error instanceof Error ? error.message : "图像拓展失败,请稍后重试"
); );
@ -1507,9 +1486,7 @@ export function ImageAnnotator({
} }
// ✅ 验证:确认下载的是处理后的图片URL // ✅ 验证:确认下载的是处理后的图片URL
console.log("[下载] 📥 开始下载,processType:", processType); logger.debug("download_started", { processType });
console.log("[下载] 📥 processedImageUrl:", processedImageUrl);
console.log("[下载] 📥 这个URL应该是处理后的图片,不是原图!");
// 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识) // 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识)
let downloadUrl = processedImageUrl; let downloadUrl = processedImageUrl;
@ -1526,7 +1503,7 @@ export function ImageAnnotator({
.replace(/-thumb\./g, "-orig.") .replace(/-thumb\./g, "-orig.")
.replace(/\/thumb\//g, "/orig/") .replace(/\/thumb\//g, "/orig/")
.replace(/thumbnail/g, "original"); .replace(/thumbnail/g, "original");
console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl); logger.warn("download_thumbnail_url_rewritten", { processType });
} }
// 如果转换后的URL仍然包含thumb标识,尝试更激进的转换 // 如果转换后的URL仍然包含thumb标识,尝试更激进的转换
@ -1538,18 +1515,20 @@ export function ImageAnnotator({
if (pathname.includes("thumb")) { if (pathname.includes("thumb")) {
const newPathname = pathname.replace(/thumb/g, "orig"); const newPathname = pathname.replace(/thumb/g, "orig");
downloadUrl = downloadUrl.replace(pathname, newPathname); downloadUrl = downloadUrl.replace(pathname, newPathname);
console.warn("[下载] ⚠️ 二次转换原图URL:", downloadUrl); logger.warn("download_thumbnail_url_rewritten_again", { processType });
} }
} catch (urlError) { } catch (urlError) {
// 如果URL解析失败,尝试简单的字符串替换 // 如果URL解析失败,尝试简单的字符串替换
downloadUrl = downloadUrl.replace(/thumb/g, "orig"); 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是正确的 // ✅ 验证:确认下载URL是正确的
console.log("[下载] ✅ 最终下载URL:", downloadUrl); logger.debug("download_url_resolved", { processType });
console.log("[下载] ✅ 这个URL应该指向处理后的图片(不是原图)");
// 如果是变清晰,验证URL应该包含enhance相关标识 // 如果是变清晰,验证URL应该包含enhance相关标识
if ( if (
@ -1557,9 +1536,7 @@ export function ImageAnnotator({
!downloadUrl.includes("enhance") && !downloadUrl.includes("enhance") &&
!downloadUrl.includes("1767358346590") !downloadUrl.includes("1767358346590")
) { ) {
console.warn( logger.warn("download_enhance_marker_missing");
"[下载] ⚠️ 警告:变清晰下载URL不包含 'enhance' 标识,可能下载了错误的图片!"
);
} }
// 从原图URL下载(原图已上传到S3) // 从原图URL下载(原图已上传到S3)
@ -1570,7 +1547,7 @@ export function ImageAnnotator({
); );
} }
const blob = await response.blob(); 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 url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
@ -1582,7 +1559,7 @@ export function ImageAnnotator({
toast.success("原图下载成功"); toast.success("原图下载成功");
} catch (error) { } catch (error) {
console.error("[下载] 错误:", error); logger.error("download_failed", error);
toast.error( toast.error(
`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}` `下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`
); );
@ -1615,7 +1592,7 @@ export function ImageAnnotator({
} }
return uploadResult.url; return uploadResult.url;
} catch (error) { } catch (error) {
console.error("上传图片失败:", error); logger.error("server_upload_failed", error);
throw error; throw error;
} }
}; };

View File

@ -10,6 +10,12 @@ const files = [
"lib/ai/providers/gemini-image.ts", "lib/ai/providers/gemini-image.ts",
"lib/ai/providers/volcano-vision.ts", "lib/ai/providers/volcano-vision.ts",
"lib/ai/providers/qianwen-video.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; const consolePattern = /\bconsole\.(log|warn|error|info|debug)\b/g;

View File

@ -14,7 +14,7 @@ import type {
EnhanceProOutputFormat, EnhanceProOutputFormat,
EnhanceProSubjectDetection, EnhanceProSubjectDetection,
} from '@/lib/enhance-pro/config'; } from '@/lib/enhance-pro/config';
import { createLogger } from '@/lib/logger'; import { createLogger } from '@/lib/observability/logger';
const log = createLogger("Canvas"); const log = createLogger("Canvas");
@ -267,7 +267,7 @@ export const imageGenerationService = {
} }
}; };
} catch (error) { } catch (error) {
console.error('[Canvas] 图片上传失败:', error); logCatch(error, "image_upload");
return { success: false, error: '图片上传失败' }; return { success: false, error: '图片上传失败' };
} }
}, },
@ -374,7 +374,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '局部编辑提交失败' }; return { success: false, error: data.error || '局部编辑提交失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 局部编辑提交失败:', error); logCatch(error, "local_edit_submit");
return { success: false, error: '局部编辑提交失败' }; return { success: false, error: '局部编辑提交失败' };
} }
}, },
@ -403,7 +403,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '查询生图任务失败' }; return { success: false, error: data.error || '查询生图任务失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 查询生图任务失败:', error); logCatch(error, "image_task_fetch", { taskId });
return { success: false, error: '查询生图任务失败' }; return { success: false, error: '查询生图任务失败' };
} }
}, },
@ -422,7 +422,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '抠图失败' }; return { success: false, error: data.error || '抠图失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 抠图失败:', error); logCatch(error, "remove_background");
return { success: false, error: '抠图失败' }; return { success: false, error: '抠图失败' };
} }
}, },
@ -458,7 +458,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '框选抠图失败' }; return { success: false, error: data.error || '框选抠图失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 框选抠图失败:', error); logCatch(error, "box_cutout");
return { success: false, error: '框选抠图失败' }; return { success: false, error: '框选抠图失败' };
} }
}, },
@ -477,7 +477,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '去水印失败' }; return { success: false, error: data.error || '去水印失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 去水印失败:', error); logCatch(error, "remove_watermark");
return { success: false, error: '去水印失败' }; return { success: false, error: '去水印失败' };
} }
}, },
@ -504,7 +504,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '图片增强失败' }; return { success: false, error: data.error || '图片增强失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 图片增强失败:', error); logCatch(error, "enhance_image");
return { success: false, error: '图片增强失败' }; return { success: false, error: '图片增强失败' };
} }
}, },
@ -559,7 +559,7 @@ export const imageGenerationService = {
}, },
}; };
} catch (error) { } catch (error) {
console.error('[Canvas] 图像增强 Pro 失败:', error); logCatch(error, "enhance_image_pro");
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) { if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
return { return {
success: false, success: false,
@ -612,7 +612,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || data.detail || '图片扩展失败' }; return { success: false, error: data.error || data.detail || '图片扩展失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 图片扩展失败:', error); logCatch(error, "extend_image");
return { success: false, error: '图片扩展失败' }; return { success: false, error: '图片扩展失败' };
} }
}, },
@ -658,7 +658,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '图层拆分失败' }; return { success: false, error: data.error || '图层拆分失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 图层拆分失败:', error); logCatch(error, "split_image_layers");
return { success: false, error: '图层拆分失败' }; return { success: false, error: '图层拆分失败' };
} }
}, },
@ -714,7 +714,7 @@ export const imageGenerationService = {
} }
return { success: false, error: data.error || '尺寸调整失败' }; return { success: false, error: data.error || '尺寸调整失败' };
} catch (error) { } catch (error) {
console.error('[Canvas] 尺寸调整失败:', error); logCatch(error, "resize_image", { width, height });
return { success: false, error: '尺寸调整失败' }; return { success: false, error: '尺寸调整失败' };
} }
}, },
@ -796,7 +796,7 @@ export const imageGenerationService = {
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error('[Canvas] 文字识别失败:', error); logCatch(error, "detect_canvas_text");
return { success: false, error: '文字识别失败' }; return { success: false, error: '文字识别失败' };
} }
}, },
@ -826,7 +826,7 @@ export const imageGenerationService = {
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error('[Canvas] 文案建议生成失败:', error); logCatch(error, "text_suggestions");
return { success: false, error: '文案建议生成失败' }; return { success: false, error: '文案建议生成失败' };
} }
}, },
@ -845,7 +845,7 @@ export const imageGenerationService = {
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error('[Canvas] 文字替换失败:', error); logCatch(error, "edit_canvas_text", { itemCount: params.items.length });
return { success: false, error: '文字替换失败' }; return { success: false, error: '文字替换失败' };
} }
}, },