From 36f3de8321e49eeb2e81cce576c6a21003fd7d7a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 May 2026 12:59:30 +0800 Subject: [PATCH] chore: govern angle edit api logging --- app/api/angle-edit/route.ts | 160 ++++++++++++++++++++-------------- scripts/check-log-hygiene.mjs | 1 + 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/app/api/angle-edit/route.ts b/app/api/angle-edit/route.ts index d6a7fb659..92901de6f 100644 --- a/app/api/angle-edit/route.ts +++ b/app/api/angle-edit/route.ts @@ -9,6 +9,9 @@ import { auth } from "@/app/(auth)/auth"; import { uploadFile } from "@/lib/storage/service"; import { db } from "@/lib/db"; import { generatedImage } from "@/lib/db/schema"; +import { createLogger } from "@/lib/observability/logger"; + +const logger = createLogger("angle-edit-api"); // 角度编辑API配置类型 type AngleEditConfig = { @@ -30,7 +33,7 @@ export const maxDuration = 180; async function createThumbnail(buffer: Buffer): Promise { try { const sharp = await import("sharp"); - + const thumbnailBuffer = await sharp.default(buffer) .resize(500, null, { fit: "inside", @@ -38,11 +41,14 @@ async function createThumbnail(buffer: Buffer): Promise { }) .jpeg({ quality: 80 }) .toBuffer(); - - console.log(`[Angle-Edit] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`); + + logger.debug("thumbnail_created", { + originalBytes: buffer.length, + thumbnailBytes: thumbnailBuffer.length, + }); return thumbnailBuffer; } catch (error) { - console.error("[Angle-Edit] Failed to create thumbnail:", error); + logger.warn("thumbnail_creation_failed", error); return buffer; } } @@ -57,43 +63,48 @@ async function uploadBase64ToS3(base64Data: string, prefix: string = "angle-edit const randomStr = Math.random().toString(36).slice(2, 8); const thumbnailFilename = `${prefix}-thumb-${timestamp}-${randomStr}.png`; const originalFilename = `${prefix}-orig-${timestamp}-${randomStr}.png`; - - console.log(`[Angle-Edit] Original size: ${buffer.length} bytes`); - + + logger.debug("source_image_prepared", { originalBytes: buffer.length }); + const thumbnailBuffer = await createThumbnail(buffer); - + const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( - thumbnailBuffer.byteOffset, + thumbnailBuffer.byteOffset, thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength ) as ArrayBuffer; const originalArrayBuffer = buffer.buffer.slice( - buffer.byteOffset, + buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; - - console.log(`[Angle-Edit] Uploading thumbnail and original to S3...`); + + logger.debug("storage_upload_started", { + hasOriginal: originalArrayBuffer.byteLength > 0, + hasThumbnail: thumbnailArrayBuffer.byteLength > 0, + prefix, + }); const [thumbnailResult, originalResult] = await Promise.all([ uploadFile(thumbnailFilename, thumbnailArrayBuffer), uploadFile(originalFilename, originalArrayBuffer), ]); - - console.log(`[Angle-Edit] Uploaded to S3:`); - console.log(` - Thumbnail: ${thumbnailResult.url}`); - console.log(` - Original: ${originalResult.url}`); - + + logger.info("storage_upload_completed", { + originalUrl: originalResult.url, + thumbnailUrl: thumbnailResult.url, + }); + return { thumbnail: thumbnailResult.url, original: originalResult.url, }; } catch (error) { - console.error("[Angle-Edit] Failed to upload to S3:", error); + logger.error("storage_upload_failed", error); throw new Error(`图片上传失败: ${error instanceof Error ? error.message : "未知错误"}`); } } export async function POST(request: NextRequest) { const startTime = Date.now(); - + try { // 1. 验证用户登录 const session = await auth(); @@ -101,8 +112,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } - console.log("[Angle-Edit] ===== 收到请求 ====="); - console.log("[Angle-Edit] 用户ID:", session.user.id); + logger.info("request_received", { authenticated: true }); // 2. 从数据库获取配置 const angleEditConfig = await getSystemSetting({ key: "angle_edit_config" }); @@ -124,7 +134,11 @@ export async function POST(request: NextRequest) { ); } - console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 1); + logger.debug("config_loaded", { + enabled: Boolean(config.enabled), + hasApiKey: Boolean(config.falApiKey), + loraScale: config.loraScale || 1, + }); // 3. 解析请求 const body = await request.json(); @@ -132,9 +146,9 @@ export async function POST(request: NextRequest) { image_url, image_base64, // 数值参数 (官方 API 格式) - horizontal_angle, // 0-360 度 - vertical_angle, // -30 到 90 度 - zoom, // 0-10 + horizontal_angle, // 0-360 度 + vertical_angle, // -30 到 90 度 + zoom, // 0-10 lora_scale, } = body; @@ -148,11 +162,14 @@ export async function POST(request: NextRequest) { const finalVerticalAngle = vertical_angle ?? 0; const finalZoom = zoom ?? 5; - console.log("[Angle-Edit] 角度参数:"); - console.log(` - 水平角度: ${finalHorizontalAngle}°`); - console.log(` - 垂直角度: ${finalVerticalAngle}°`); - console.log(` - 缩放: ${finalZoom}`); - console.log(` - LoRA强度: ${finalLoraScale}`); + logger.info("request_parameters_normalized", { + hasBase64Input: Boolean(image_base64), + hasUrlInput: Boolean(image_url), + horizontalAngle: finalHorizontalAngle, + loraScale: finalLoraScale, + verticalAngle: finalVerticalAngle, + zoom: finalZoom, + }); // 4. 准备图片输入 let imageInput: string; @@ -164,10 +181,12 @@ export async function POST(request: NextRequest) { } // 5. 动态导入 fal.ai SDK 并调用官方多角度 API - console.log("[Angle-Edit] 调用 fal.ai API (qwen-image-edit-2511-multiple-angles)..."); - + logger.info("provider_call_started", { + modelId: "fal-ai/qwen-image-edit-2511-multiple-angles", + }); + const fal = await import("@fal-ai/serverless-client"); - + fal.config({ credentials: config.falApiKey, }); @@ -176,20 +195,20 @@ export async function POST(request: NextRequest) { // 文档: https://fal.ai/models/fal-ai/qwen-image-edit-2511-multiple-angles const result = await fal.subscribe("fal-ai/qwen-image-edit-2511-multiple-angles", { input: { - image_urls: [imageInput], // 官方使用 image_urls (数组) - horizontal_angle: finalHorizontalAngle, // 水平旋转角度 0-360 - vertical_angle: finalVerticalAngle, // 垂直角度 -30 到 90 - zoom: finalZoom, // 缩放 0-10 - lora_scale: finalLoraScale, // LoRA 强度 + image_urls: [imageInput], // 官方使用 image_urls (数组) + horizontal_angle: finalHorizontalAngle, // 水平旋转角度 0-360 + vertical_angle: finalVerticalAngle, // 垂直角度 -30 到 90 + zoom: finalZoom, // 缩放 0-10 + lora_scale: finalLoraScale, // LoRA 强度 num_inference_steps: config.inferenceSteps || 28, - guidance_scale: config.guidanceScale || 4.5, // 官方默认 4.5 + guidance_scale: config.guidanceScale || 4.5, // 官方默认 4.5 enable_safety_checker: true, output_format: "png", }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { - console.log("[Angle-Edit] 处理进度..."); + logger.debug("provider_progress", { status: update.status }); } }, }); @@ -202,8 +221,11 @@ export async function POST(request: NextRequest) { } const outputImageUrl = outputImage.url; - console.log("[Angle-Edit] 输出图片URL:", outputImageUrl); - console.log("[Angle-Edit] API 构建的提示词:", apiResult.prompt); + logger.info("provider_call_completed", { + hasGeneratedPrompt: Boolean(apiResult.prompt), + imageCount: apiResult.images?.length ?? 0, + outputImageUrl, + }); // 7. 下载图片并上传到我们的 S3 const imageResponse = await fetch(outputImageUrl); @@ -214,19 +236,25 @@ export async function POST(request: NextRequest) { const imageBase64Result = imageBuffer.toString("base64"); const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit"); - console.log("[Angle-Edit] 已上传到S3:", s3Result.original); + logger.info("result_image_persisted", { + originalUrl: s3Result.original, + thumbnailUrl: s3Result.thumbnail, + }); // 8. 保存到数据库 const processingTime = (Date.now() - startTime) / 1000; - - const [savedImage] = await db.insert(generatedImage).values({ - userId: session.user.id, - thumbnailUrl: s3Result.thumbnail, - originalUrl: s3Result.original, - prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`, - modelId: "qwen-image-edit-2511-multiple-angles", - createdAt: new Date(), - }).returning(); + + const [savedImage] = await db + .insert(generatedImage) + .values({ + userId: session.user.id, + thumbnailUrl: s3Result.thumbnail, + originalUrl: s3Result.original, + prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`, + modelId: "qwen-image-edit-2511-multiple-angles", + createdAt: new Date(), + }) + .returning(); try { const sourceAssets = @@ -272,10 +300,12 @@ export async function POST(request: NextRequest) { } } } catch (assetError) { - console.error("[Angle-Edit] ⚠️ MediaAsset 注册失败:", assetError); + logger.warn("media_asset_registration_failed", assetError); } - console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒"); + logger.info("request_completed", { + processingSeconds: Number(processingTime.toFixed(2)), + }); // 9. 返回结果 return NextResponse.json({ @@ -293,19 +323,21 @@ export async function POST(request: NextRequest) { }); } catch (error) { - console.error("[Angle-Edit] ❌ 错误:", error); - + logger.error("request_failed", error); + // 输出详细的错误信息 if (error && typeof error === "object" && "status" in error) { const apiError = error as { status?: number; body?: unknown; message?: string }; - console.error("[Angle-Edit] API 错误详情:"); - console.error(" - Status:", apiError.status); - console.error(" - Body:", JSON.stringify(apiError.body, null, 2)); - console.error(" - Message:", apiError.message); - + logger.warn("provider_error_summary", { + bodyType: typeof apiError.body, + hasBody: apiError.body != null, + message: apiError.message, + status: apiError.status, + }); + if (apiError.status === 403) { return NextResponse.json( - { + { error: "API 访问被拒绝 (403 Forbidden)。请检查 API Key 是否正确,以及是否有权限访问该模型。", details: apiError.body, }, @@ -313,9 +345,9 @@ export async function POST(request: NextRequest) { ); } } - + return NextResponse.json( - { + { error: error instanceof Error ? error.message : "角度编辑处理失败", }, { status: 500 } diff --git a/scripts/check-log-hygiene.mjs b/scripts/check-log-hygiene.mjs index 401f8f55c..a0cd87fd6 100644 --- a/scripts/check-log-hygiene.mjs +++ b/scripts/check-log-hygiene.mjs @@ -31,6 +31,7 @@ const files = [ "app/api/canvas/text-detect/route.ts", "app/api/canvas/detail-plan/route.ts", "app/api/scene-fusion/route.ts", + "app/api/angle-edit/route.ts", "app/api/pick-object/route.ts", "app/api/canvas/world/upload/route.ts", "app/api/canvas/3d/tasks/route.ts",