chore: govern angle edit api logging

This commit is contained in:
Your Name 2026-05-27 12:59:30 +08:00
parent 0768f84802
commit 36f3de8321
2 changed files with 97 additions and 64 deletions

View File

@ -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<Buffer> {
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<Buffer> {
})
.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 }

View File

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