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 { 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: "新画布" });
}

View File

@ -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 },

View File

@ -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,

View File

@ -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(
* 格式: ![thumb|||originalUrl|||size](thumbnailUrl)
*/
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) 格式
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 }

View File

@ -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;
}
};

View File

@ -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;

View File

@ -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: '文字替换失败' };
}
},