chore: govern chat api logging

This commit is contained in:
Your Name 2026-05-27 09:43:43 +08:00
parent f2d4f9e698
commit cc52f414af
10 changed files with 375 additions and 580 deletions

View File

@ -1,3 +1,4 @@
import { createLogger } from "@/lib/observability/logger";
import { getModelGenerationCreditCost } from "@/lib/ai/image-model-pricing";
import {
consumeUserPoints,
@ -7,6 +8,9 @@ import {
getUserById,
} from "@/lib/db/queries";
const chatApiLogger = createLogger("chat.billing");
const MODEL_UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@ -50,7 +54,7 @@ export async function consumeGeneratedVideoPoints(params: {
const userData = await getUserById({ id: params.userId }).catch(() => undefined);
if (userData?.isHighVip) {
console.log(`[Points] Skip video points for VIP user=${params.userId}`);
chatApiLogger.info(`[Points] Skip video points for VIP user=${params.userId}`);
return;
}
@ -89,19 +93,13 @@ export async function consumeGeneratedVideoPoints(params: {
});
if (!consumeResult.ok) {
console.warn(
`[Points] Failed to consume video points. user=${params.userId} model=${billingModel.id} cost=${unitCost}`,
);
chatApiLogger.warn(`[Points] Failed to consume video points. user=${params.userId} model=${billingModel.id} cost=${unitCost}`);
} else if (consumeResult.skipped) {
console.log(
`[Points] Skipped ${unitCost} video points for VIP user=${params.userId} model=${billingModel.id}`,
);
chatApiLogger.info(`[Points] Skipped ${unitCost} video points for VIP user=${params.userId} model=${billingModel.id}`);
} else {
console.log(
`[Points] Consumed ${unitCost} points for video. balance=${consumeResult.balance}`,
);
chatApiLogger.info(`[Points] Consumed ${unitCost} points for video. balance=${consumeResult.balance}`);
}
} catch (error) {
console.error("[Points] Failed to consume points after video generation:", error);
chatApiLogger.error("[Points] Failed to consume points after video generation:", error);
}
}

View File

@ -1,3 +1,4 @@
import { createLogger } from "@/lib/observability/logger";
import { generateText } from "ai";
import { fetchImageBufferFromUrl } from "@/lib/ai/rmbg/service";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
@ -10,6 +11,9 @@ import { isVideoProviderType } from "./video-models";
import type { AgentPresetMessageImagePart } from "./agent-scene-workflow";
import type { PostRequestBody } from "../schema";
const chatApiLogger = createLogger("chat.detailReplacement");
export async function createAgentImageThumbnailBuffer(buffer: Buffer): Promise<Buffer> {
const sharp = (await import("sharp")).default;
return sharp(buffer, { failOn: "none" })
@ -91,7 +95,7 @@ export async function resolveReplacementReferenceGeometry(
aspectRatio: inferClosestReplacementAspectRatio(width, height),
};
} catch (error) {
console.warn("[Chat API] Failed to resolve replacement reference geometry:", {
chatApiLogger.warn("[Chat API] Failed to resolve replacement reference geometry:", {
referenceImageUrl,
error: error instanceof Error ? error.message : "unknown",
});
@ -318,7 +322,7 @@ export function parseReplacementScenePlan(
return scenePlan;
} catch (error) {
console.warn("[Chat API] Failed to parse replacement scene plan JSON:", error);
chatApiLogger.warn("[Chat API] Failed to parse replacement scene plan JSON:", error);
return null;
}
}
@ -583,7 +587,7 @@ export function parseReplacementReferenceDecision(
};
}
} catch (error) {
console.warn("[Chat API] Failed to parse replacement reference decision JSON:", error);
chatApiLogger.warn("[Chat API] Failed to parse replacement reference decision JSON:", error);
}
}
@ -694,7 +698,7 @@ export async function analyzeReplacementReferenceImage(params: {
}
}
} catch (error) {
console.warn("[Chat API] Configured Qwen-VL reference analysis failed:", error);
chatApiLogger.warn("[Chat API] Configured Qwen-VL reference analysis failed:", error);
}
}
@ -717,7 +721,7 @@ export async function analyzeReplacementReferenceImage(params: {
};
}
} catch (error) {
console.warn("[Chat API] Fallback Qwen3-VL reference analysis failed:", error);
chatApiLogger.warn("[Chat API] Fallback Qwen3-VL reference analysis failed:", error);
}
return {
@ -747,7 +751,7 @@ export async function resolveReplacementScenePlanningRuntime(params: {
};
}
} catch (error) {
console.warn("[Chat API] Failed to resolve local Moonshot scene planning model:", error);
chatApiLogger.warn("[Chat API] Failed to resolve local Moonshot scene planning model:", error);
}
const configuredModels = params.models || (await getEnabledAiModelsWithProvider());
@ -777,7 +781,7 @@ export async function resolveReplacementScenePlanningRuntime(params: {
configuredVisionModel.model.modelId,
};
} catch (error) {
console.warn("[Chat API] Failed to resolve configured replacement scene planning model:", {
chatApiLogger.warn("[Chat API] Failed to resolve configured replacement scene planning model:", {
modelId: configuredVisionModel.model.id,
error,
});
@ -811,7 +815,7 @@ export async function analyzeReplacementExecutionPlan(params: {
retries: 2,
loggerPrefix: "[Chat API] Replacement scene plan",
}).catch((error) => {
console.warn("[Chat API] Failed to inline replacement scene planning images:", error);
chatApiLogger.warn("[Chat API] Failed to inline replacement scene planning images:", error);
return { inlineDataUrls: [], skipped: [] };
});
const effectiveVisionImages =
@ -926,7 +930,7 @@ export async function analyzeReplacementExecutionPlan(params: {
};
}
} catch (error) {
console.warn("[Chat API] Replacement scene planning analysis failed:", {
chatApiLogger.warn("[Chat API] Replacement scene planning analysis failed:", {
referenceLabel: params.referenceLabel,
referenceImageUrl: params.referenceImageUrl,
error,

View File

@ -1,7 +1,11 @@
import { createLogger } from "@/lib/observability/logger";
import { NextResponse } from "next/server";
import { getRequestErrorMessage } from "@/lib/request-error";
import { generateUUID } from "@/lib/utils";
const chatApiLogger = createLogger("chat.errors");
const DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000;
export class OperationTimeoutError extends Error {
@ -124,7 +128,7 @@ export function createChatApiFailureResponse(
const failure = resolveChatApiFailure(error);
const traceId = generateUUID();
console.error("[Chat API] Unhandled request failure:", {
chatApiLogger.error("[Chat API] Unhandled request failure:", {
traceId,
vercelId,
category: failure.category,

View File

@ -1,8 +1,12 @@
import { createLogger } from "@/lib/observability/logger";
import { isFalGptImage2EditModelId } from "@/lib/ai/providers/fal-image";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import type { ChatMessage } from "@/lib/types";
import { convertMessagesWithImages } from "./image-media";
const chatApiLogger = createLogger("chat.imageFailover");
export const IMAGE_FAILOVER_PROVIDER_ALLOWLIST = new Set([
"alibaba-image",
"bfl-image",
@ -130,9 +134,7 @@ export function buildImageFallbackCandidates(
}
if (isImageFallbackModelCoolingDown(item.model.id)) {
const cooldown = imageFailoverModelCooldown.get(item.model.id);
console.log(
`[Chat API] ⏭ Skip configured image fallback on cooldown: ${item.model.displayName} (${providerType}) - ${cooldown?.reason || "cooldown"}`
);
chatApiLogger.info(`[Chat API] ⏭ Skip configured image fallback on cooldown: ${item.model.displayName} (${providerType}) - ${cooldown?.reason || "cooldown"}`);
return false;
}
return true;
@ -243,9 +245,7 @@ export function getImageCrossProviderCandidates(
if (options.currentProviderId && m.provider.id === options.currentProviderId) return false;
if (isImageFallbackModelCoolingDown(m.model.id)) {
const cooldown = imageFailoverModelCooldown.get(m.model.id);
console.log(
`[Chat API] ⏭ Skip image failover model on cooldown: ${m.model.displayName} (${m.provider.type}) - ${cooldown?.reason || "cooldown"}`
);
chatApiLogger.info(`[Chat API] ⏭ Skip image failover model on cooldown: ${m.model.displayName} (${m.provider.type}) - ${cooldown?.reason || "cooldown"}`);
return false;
}
return true;

View File

@ -1,3 +1,4 @@
import { createLogger } from "@/lib/observability/logger";
import { convertToModelMessages } from "ai";
import { inlineVisionImages } from "@/lib/ai/vision-inline";
import { extractImageUrlsFromText } from "@/lib/image-url";
@ -6,6 +7,9 @@ import { uploadFile } from "@/lib/storage/service";
import type { ChatMessage } from "@/lib/types";
import { ANNOTATION_SYSTEM_PROMPT, hasAnnotatedImage } from "./message-preparation";
const chatApiLogger = createLogger("chat.imageMedia");
export function guessImageMediaType(url: string): string {
const cleanUrl = url.split("?")[0]?.toLowerCase() || "";
if (cleanUrl.endsWith(".jpg") || cleanUrl.endsWith(".jpeg")) return "image/jpeg";
@ -458,7 +462,7 @@ export async function convertMessagesWithImages(
retries: 2,
loggerPrefix: target.loggerPrefix,
}).catch((error) => {
console.warn(`${target.loggerPrefix} failed to inline message images:`, error);
chatApiLogger.warn(`${target.loggerPrefix} failed to inline message images:`, error);
return { inlineDataUrls: [], skipped: [] };
});
@ -479,7 +483,7 @@ export async function convertMessagesWithImages(
return sourceMessages;
}
console.log(`${target.loggerPrefix} inlined chat message images:`, {
chatApiLogger.info(`${target.loggerPrefix} inlined chat message images:`, {
total: uniqueUrls.length,
inlined: inlineMap.size,
providerType: target.providerType,
@ -575,7 +579,7 @@ export async function convertMessagesWithImages(
// 🆕 检测是否有标注图片
const hasAnnotation = hasAnnotatedImage(messages);
if (hasAnnotation) {
console.log("[Message Convert] 🎨 检测到标注图片,将自动添加系统提示");
chatApiLogger.info("[Message Convert] 🎨 检测到标注图片,将自动添加系统提示");
}
// 对于 Gemini,需要特殊处理图片
@ -596,7 +600,7 @@ export async function convertMessagesWithImages(
if (hasAnnotation && message.role === "user" && !hasAddedAnnotationPrompt) {
textContent = ANNOTATION_SYSTEM_PROMPT + "\n\n用户请求:" + textContent;
hasAddedAnnotationPrompt = true;
console.log("[Message Convert] 🎨 已注入标注提示词");
chatApiLogger.info("[Message Convert] 🎨 已注入标注提示词");
}
content.push({
@ -612,7 +616,7 @@ export async function convertMessagesWithImages(
url: part.url,
},
});
console.log("[Message Convert] Image kept as URL format:", part.url.substring(0, 80) + "...");
chatApiLogger.info("[Message Convert] Image kept as URL format:", part.url.substring(0, 80) + "...");
}
}
@ -622,7 +626,7 @@ export async function convertMessagesWithImages(
type: "text",
text: ANNOTATION_SYSTEM_PROMPT,
});
console.log("[Message Convert] 🎨 已在消息开头注入标注提示词");
chatApiLogger.info("[Message Convert] 🎨 已在消息开头注入标注提示词");
}
convertedMessages.push({
@ -862,7 +866,7 @@ export async function normalizeAssistantImageMarkdown(
uploadedUrls.push(uploadedUrl);
rebuilt += `![thumb|||${uploadedUrl}|||${imageSize}](${uploadedUrl})`;
} catch (error) {
console.error("[Chat API] Failed to upload inline data URL image:", error);
chatApiLogger.error("[Chat API] Failed to upload inline data URL image:", error);
rebuilt += matchText;
}
@ -883,7 +887,7 @@ export async function normalizeAssistantImageMarkdown(
uploadedUrls.push(uploadedUrl);
normalizedText = normalizedText.replace(rawDataUrl, uploadedUrl);
} catch (error) {
console.error("[Chat API] Failed to normalize raw data URL image:", error);
chatApiLogger.error("[Chat API] Failed to normalize raw data URL image:", error);
}
}
}
@ -917,14 +921,14 @@ export async function normalizeAssistantImageMarkdown(
try {
hostedThumbnailUrl = await ensureHostedImageUrl(thumbnailUrl);
} catch (error) {
console.error("[Chat API] Failed to normalize remote thumbnail URL:", error);
chatApiLogger.error("[Chat API] Failed to normalize remote thumbnail URL:", error);
}
if (originalUrl && originalUrl !== thumbnailUrl) {
try {
hostedOriginalUrl = await ensureHostedImageUrl(originalUrl);
} catch (error) {
console.error("[Chat API] Failed to normalize remote original URL:", error);
chatApiLogger.error("[Chat API] Failed to normalize remote original URL:", error);
hostedOriginalUrl = hostedThumbnailUrl || originalUrl;
}
} else {
@ -938,7 +942,7 @@ export async function normalizeAssistantImageMarkdown(
uploadedUrls.push(finalOriginalUrl);
rebuilt += `![thumb|||${finalOriginalUrl}|||${parsedImageSize || imageSize}](${finalThumbnailUrl})`;
} catch (error) {
console.error("[Chat API] Failed to parse remote image markdown metadata:", error);
chatApiLogger.error("[Chat API] Failed to parse remote image markdown metadata:", error);
rebuilt += matchText;
}
@ -952,9 +956,7 @@ export async function normalizeAssistantImageMarkdown(
const hasMarkdownImage = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/i.test(normalizedText);
if (!hasMarkdownImage && uploadedUrls.length > 0) {
const uniqueUrls = [...new Set(uploadedUrls)];
console.log(
`[Chat API] Converted ${uniqueUrls.length} image link(s) to hosted link(s)`
);
chatApiLogger.info(`[Chat API] Converted ${uniqueUrls.length} image link(s) to hosted link(s)`);
return uniqueUrls
.map((url) => `![thumb|||${url}|||${imageSize}](${url})`)
.join("\n");

View File

@ -1,16 +1,20 @@
import { createLogger } from "@/lib/observability/logger";
import {
analyzeOptimizedPromptMerge,
buildOptimizedPromptPreview,
} from "@/lib/ai/runtime/optimized-prompt";
import type { ChatMessage } from "@/lib/types";
const chatApiLogger = createLogger("chat.messagePreparation");
/**
* 将图片 URL 转换为 base64 格式
* 用于将 S3 URL 的图片转换为 base64 发送给 Gemini API
*/
export async function convertImageUrlToBase64(url: string): Promise<string> {
try {
console.log("[Image Convert] Fetching image from:", url);
chatApiLogger.info("[Image Convert] Fetching image from:", url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
@ -24,10 +28,10 @@ export async function convertImageUrlToBase64(url: string): Promise<string> {
const contentType = response.headers.get("content-type") || "image/jpeg";
const dataUrl = `data:${contentType};base64,${base64}`;
console.log("[Image Convert] Converted to base64, size:", base64.length);
chatApiLogger.info("[Image Convert] Converted to base64, size:", base64.length);
return dataUrl;
} catch (error) {
console.error("[Image Convert] Failed:", error);
chatApiLogger.error("[Image Convert] Failed:", error);
throw error;
}
}
@ -113,7 +117,7 @@ export function applyOptimizedPromptToLatestUserMessage(
.join("\n\n")
.trim()
: `${hiddenPromptPrefix}${prompt}`.trim();
console.log("[Prompt Merge] Final image prompt prepared:", {
chatApiLogger.info("[Prompt Merge] Final image prompt prepared:", {
original: normalizedOriginal.substring(0, 120),
optimized: prompt.substring(0, 120),
merged: finalPrompt.substring(0, 180),

View File

@ -1,3 +1,4 @@
import { createLogger } from "@/lib/observability/logger";
import { JsonToSseTransformStream } from "ai";
import { extractImageUrlsFromText } from "@/lib/image-url";
import type {
@ -7,6 +8,9 @@ import type {
} from "@/lib/ai/context-memory/types";
import { generateUUID } from "@/lib/utils";
const chatApiLogger = createLogger("chat.stream");
export type StreamWriter = {
write: (chunk: any) => void;
};
@ -20,7 +24,7 @@ export function safeWriteStreamChunk(
writer.write(chunk);
return true;
} catch (error) {
console.warn(`[Chat API] Stream write skipped (${label})`, error);
chatApiLogger.warn(`[Chat API] Stream write skipped (${label})`, error);
return false;
}
}

View File

@ -1,3 +1,4 @@
import { createLogger } from "@/lib/observability/logger";
import { after } from "next/server";
import {
getAiModelById,
@ -10,6 +11,9 @@ import {
} from "@/lib/ai/model-classification";
import type { StoryboardModelCandidate } from "@/lib/video-agent/types";
const chatApiLogger = createLogger("chat.videoModels");
export type VideoModeType =
| "text-to-video"
| "first-frame"
@ -260,12 +264,12 @@ export function scheduleVideoArchive(params: {
videoMetadata: nextMetadata as any,
});
console.log("[Chat API] 🕒 已登记延迟归档视频:", {
chatApiLogger.info("[Chat API] 🕒 已登记延迟归档视频:", {
messageId,
archiveAfterAt: new Date(archiveAfterAtMs).toISOString(),
});
} catch (error) {
console.error("[Chat API] ❌ 登记视频延迟归档失败(保留临时地址):", {
chatApiLogger.error("[Chat API] ❌ 登记视频延迟归档失败(保留临时地址):", {
messageId,
temporaryVideoUrl,
error,

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,19 @@
import { readFileSync } from "node:fs";
const files = [
"app/(chat)/api/chat/route.ts",
"app/(chat)/api/chat/_lib/agent-output.ts",
"app/(chat)/api/chat/_lib/agent-scene-workflow.ts",
"app/(chat)/api/chat/_lib/billing.ts",
"app/(chat)/api/chat/_lib/detail-replacement.ts",
"app/(chat)/api/chat/_lib/errors.ts",
"app/(chat)/api/chat/_lib/image-failover.ts",
"app/(chat)/api/chat/_lib/image-media.ts",
"app/(chat)/api/chat/_lib/message-preparation.ts",
"app/(chat)/api/chat/_lib/model-resolver.ts",
"app/(chat)/api/chat/_lib/persistence.ts",
"app/(chat)/api/chat/_lib/stream.ts",
"app/(chat)/api/chat/_lib/video-models.ts",
"lib/storage/service.ts",
"app/(chat)/api/files/upload/route.ts",
"app/(auth)/login/page.tsx",