fix: reduce 504 via async polling and chat keepalive
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-04 20:00:37 +08:00
parent bff8ffa100
commit 5e155ecf3c
7 changed files with 579 additions and 330 deletions

View File

@ -1661,8 +1661,11 @@ let globalStreamContext: ResumableStreamContext | null = null;
const inFlightChatRequests = new Map<string, number>();
const IN_FLIGHT_REQUEST_TTL_MS = 5 * 60 * 1000;
const DUPLICATE_REQUEST_WAIT_MS = 2 * 60 * 1000;
const SMART_AUTO_GRAPH_TIMEOUT_MS = 120000;
const SMART_AUTO_FALLBACK_TIMEOUT_MS = 120000;
const CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000;
const SMART_AUTO_GRAPH_TIMEOUT_MS = 35_000;
const SMART_AUTO_FALLBACK_TIMEOUT_MS = 20_000;
const SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS = 18_000;
const DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS = 12_000;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@ -1696,6 +1699,26 @@ async function withOperationTimeout<T>(
}
}
function getRemainingBudgetMs(startedAt: number, budgetMs: number) {
return Math.max(0, budgetMs - (Date.now() - startedAt));
}
function getBudgetedTimeout(params: {
startedAt: number;
preferredMs: number;
minimumMs?: number;
}) {
const minimumMs = params.minimumMs ?? 6_000;
const remainingMs = getRemainingBudgetMs(
params.startedAt,
CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS
);
if (remainingMs < minimumMs) {
return null;
}
return Math.min(params.preferredMs, remainingMs);
}
const getTokenlensCatalog = cache(
async (): Promise<ModelCatalog | undefined> => {
try {
@ -1764,6 +1787,7 @@ export async function POST(request: Request) {
autoContext?: string;
deepThinkingEnabled?: boolean;
} = requestBody;
const requestStartedAt = Date.now();
const now = Date.now();
for (const [key, startedAt] of inFlightChatRequests.entries()) {
@ -2040,6 +2064,17 @@ export async function POST(request: Request) {
// 2. 使用 LangGraph 驱动的自动模式工作流
const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph");
const autoGraphTimeoutMs = getBudgetedTimeout({
startedAt: requestStartedAt,
preferredMs: SMART_AUTO_GRAPH_TIMEOUT_MS,
minimumMs: 8_000,
});
if (!autoGraphTimeoutMs) {
throw new OperationTimeoutError(
"runAutoAgentGraph.preResponseBudget",
CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS
);
}
const autoGraphResult = await withOperationTimeout(
runAutoAgentGraph({
text: textContent,
@ -2051,7 +2086,7 @@ export async function POST(request: Request) {
preferredModelId: preferredAgentModelId,
}),
"runAutoAgentGraph",
SMART_AUTO_GRAPH_TIMEOUT_MS,
autoGraphTimeoutMs,
);
if (autoGraphResult.analysis) {
@ -2214,26 +2249,35 @@ export async function POST(request: Request) {
const isAutoTimeout = autoError instanceof OperationTimeoutError;
if (isAutoTimeout) {
console.warn(
`[Smart Auto Mode] ⏱️ Auto-agent timed out after ${SMART_AUTO_GRAPH_TIMEOUT_MS}ms, using local fallback`,
`[Smart Auto Mode] ⏱️ Auto-agent timed out or exceeded pre-response budget (${CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS}ms), using local fallback`,
);
}
try {
const { analyzeIntent } = await import("@/lib/ai/smart-analyzer");
const rescueAnalysis = await withOperationTimeout(
analyzeIntent({
text: autoTextForFallback,
hasImage: autoHasImageForFallback || smartAutoHistoryImages.length > 0,
imageUrls: smartAutoHistoryImages,
imageFromHistory: autoFromHistoryForFallback || smartAutoHistoryImages.length > 0,
historyMessages: [],
context: undefined,
preferredModelId: preferredAgentModelId,
}),
"fallbackAnalyzeIntent",
SMART_AUTO_FALLBACK_TIMEOUT_MS,
);
if (rescueAnalysis.conversationalReply?.trim()) {
smartAutoFriendlyMessage = rescueAnalysis.conversationalReply.trim();
const rescueTimeoutMs = getBudgetedTimeout({
startedAt: requestStartedAt,
preferredMs: SMART_AUTO_FALLBACK_TIMEOUT_MS,
minimumMs: 5_000,
});
if (rescueTimeoutMs) {
const rescueAnalysis = await withOperationTimeout(
analyzeIntent({
text: autoTextForFallback,
hasImage: autoHasImageForFallback || smartAutoHistoryImages.length > 0,
imageUrls: smartAutoHistoryImages,
imageFromHistory: autoFromHistoryForFallback || smartAutoHistoryImages.length > 0,
historyMessages: [],
context: undefined,
preferredModelId: preferredAgentModelId,
}),
"fallbackAnalyzeIntent",
rescueTimeoutMs,
);
if (rescueAnalysis.conversationalReply?.trim()) {
smartAutoFriendlyMessage = rescueAnalysis.conversationalReply.trim();
}
} else {
console.warn("[Smart Auto Mode] Skip rescue analysis due to pre-response budget");
}
} catch (rescueError) {
console.error("[Smart Auto Mode] Rescue analysis failed:", rescueError);
@ -2265,18 +2309,27 @@ export async function POST(request: Request) {
if (summaryImageUrls.length > 0) {
const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer");
const summary = await withOperationTimeout(
summarizeImageForExecutionReport({
text: autoTextForFallback,
imageUrls: summaryImageUrls,
context: typeof autoContext === "string" ? autoContext : undefined,
preferredModelId: preferredAgentModelId,
}),
"fallbackVisualSummary",
SMART_AUTO_FALLBACK_TIMEOUT_MS,
);
if (summary?.trim()) {
fallbackVisualSummary = summary.trim();
const fallbackSummaryTimeoutMs = getBudgetedTimeout({
startedAt: requestStartedAt,
preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS,
minimumMs: 5_000,
});
if (fallbackSummaryTimeoutMs) {
const summary = await withOperationTimeout(
summarizeImageForExecutionReport({
text: autoTextForFallback,
imageUrls: summaryImageUrls,
context: typeof autoContext === "string" ? autoContext : undefined,
preferredModelId: preferredAgentModelId,
}),
"fallbackVisualSummary",
fallbackSummaryTimeoutMs,
);
if (summary?.trim()) {
fallbackVisualSummary = summary.trim();
}
} else {
console.warn("[Smart Auto Mode] Skip fallback visual summary due to pre-response budget");
}
}
} catch (summaryError) {
@ -2496,8 +2549,12 @@ export async function POST(request: Request) {
.join(" ");
let summary: string | null = null;
const summaryTimeouts = [120000];
for (const timeoutMs of summaryTimeouts) {
const summaryTimeoutMs = getBudgetedTimeout({
startedAt: requestStartedAt,
preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS,
minimumMs: 6_000,
});
if (summaryTimeoutMs) {
summary = await withOperationTimeout(
summarizeImageForExecutionReport({
text: latestUserText,
@ -2506,9 +2563,10 @@ export async function POST(request: Request) {
preferredModelId: preferredAgentModelId,
}),
"enrichImageAnalysisSummary",
timeoutMs,
summaryTimeoutMs,
);
if (summary?.trim()) break;
} else {
console.warn("[Smart Auto Mode] Skip image summary enrichment due to pre-response budget");
}
if (summary?.trim()) {
@ -4158,11 +4216,27 @@ export async function POST(request: Request) {
}
// ========== 千问/FAL 视频处理结束 ==========
let stopStreamKeepalive: (() => void) | null = null;
const stream = createUIMessageStream({
execute: async ({ writer: dataStream }) => {
// 构建工具对象,只包含启用的工具
const tools: any = {};
const activeTools: string[] = [];
const stopDefaultKeepalive = startStreamKeepalive(
dataStream,
"chat-default",
DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS
);
stopStreamKeepalive = stopDefaultKeepalive;
safeWriteStreamChunk(
dataStream,
{
type: "data-keepalive",
data: JSON.stringify({ ts: Date.now(), label: "chat-default:init" }),
},
"chat-default:init"
);
try {
// 构建工具对象,只包含启用的工具
const tools: any = {};
const activeTools: string[] = [];
if (enabledTools.includes("getWeather")) {
tools.getWeather = getWeather;
@ -5049,16 +5123,21 @@ export async function POST(request: Request) {
},
});
result.consumeStream();
result.consumeStream();
dataStream.merge(
result.toUIMessageStream({
sendReasoning: true,
})
);
dataStream.merge(
result.toUIMessageStream({
sendReasoning: true,
})
);
} finally {
stopStreamKeepalive = stopDefaultKeepalive;
}
},
generateId: generateUUID,
onFinish: async ({ messages }) => {
stopStreamKeepalive?.();
stopStreamKeepalive = null;
await saveMessages({
messages: messages.map((currentMessage) => ({
id: currentMessage.id,
@ -5126,6 +5205,8 @@ export async function POST(request: Request) {
}
},
onError: () => {
stopStreamKeepalive?.();
stopStreamKeepalive = null;
return "Oops, an error occurred!";
},
});

View File

@ -8,6 +8,7 @@ import { ArrowLeft, Download, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsR
import { Input } from "@/components/ui/input";
import Image from "next/image";
import { toast } from "sonner";
import { requestLongTaskResult } from "@/lib/client/long-task";
import { formatBeijingTime } from "@/lib/utils";
type GalleryImage = {
@ -176,26 +177,20 @@ export default function GalleryPage() {
try {
const base64Data = (reader.result as string).split(",")[1];
// 调用变清晰API
const enhanceResponse = await fetch("/api/enhance", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
const result = await requestLongTaskResult({
endpoint: "/api/enhance",
payload: {
image_base64: base64Data,
prompt: `图片变清晰处理 - ${image.alt}`,
chatId: image.chatId,
messageId: image.messageId,
}),
},
pollingParams: {
prompt: `图片变清晰处理 - ${image.alt}`,
chatId: image.chatId,
messageId: image.messageId,
},
});
if (!enhanceResponse.ok) {
const error = await enhanceResponse.json();
throw new Error(error.error || error.detail || "变清晰失败");
}
const result = await enhanceResponse.json();
// 刷新相册列表以显示新图片
await fetchImages(pagination.page);

View File

@ -22,6 +22,8 @@ type EnhanceApiConfig = {
const AOS_MAX_RESOLUTION = 4096;
const ENHANCE_CREATE_TIMEOUT_MS = 120_000;
const ENHANCE_CREATE_RETRY_COUNT = 1;
const ENHANCE_SYNC_WAIT_TIMEOUT_MS = 20_000;
const ENHANCE_TASK_POLL_INTERVAL_MS = 1200;
function getAosFriendlyErrorMessage(detail: string): string | null {
const resolutionMatch = detail.match(/resolution excess,\s*width=(\d+),\s*height=(\d+)/i);
@ -380,6 +382,166 @@ async function pollTaskResult(
}
}
function isEnhanceTaskStillProcessing(error: unknown): boolean {
if (!(error instanceof Error)) return false;
return /轮询超时/i.test(error.message || "");
}
function buildEnhanceTaskPendingResponse(taskId: string) {
return NextResponse.json(
{
success: true,
processing: true,
status: "running",
taskId,
pollAfterMs: ENHANCE_TASK_POLL_INTERVAL_MS,
},
{ status: 202 }
);
}
async function finalizeEnhanceTaskResult(params: {
userId: string;
taskResult: any;
prompt?: string;
chatId?: string | null;
messageId?: string | null;
startedAt?: number;
}) {
if (params.taskResult?.status !== 200 || !params.taskResult?.data) {
throw new Error(params.taskResult?.message || "未知错误");
}
let enhancedImageBase64: string | null = null;
if (typeof params.taskResult.data.image_base64 === "string" && params.taskResult.data.image_base64) {
enhancedImageBase64 = params.taskResult.data.image_base64;
} else if (typeof params.taskResult.data.image === "string" && params.taskResult.data.image) {
const imageData = params.taskResult.data.image;
if (/^https?:\/\//i.test(imageData)) {
const imageResponse = await fetch(imageData);
if (!imageResponse.ok) {
throw new Error(`无法下载处理后的图片: ${imageResponse.status}`);
}
const downloadedBuffer = await imageResponse.arrayBuffer();
enhancedImageBase64 = Buffer.from(downloadedBuffer).toString("base64");
} else {
enhancedImageBase64 = imageData;
}
}
if (!enhancedImageBase64) {
throw new Error("轮询结果未包含图片数据");
}
const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
const finalPrompt = params.prompt?.trim() || "图片变清晰处理";
try {
await db.insert(generatedImage).values({
userId: params.userId,
chatId: params.chatId || null,
messageId: params.messageId || null,
thumbnailUrl: uploadResult.thumbnail,
originalUrl: uploadResult.original,
imageSize: "2K",
prompt: finalPrompt,
modelId: "enhance-1.0",
});
} catch (dbError) {
console.error("[Enhance] 保存到相册失败(已忽略):", dbError);
}
const processingTime =
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined;
return {
success: true,
image_base64: enhancedImageBase64,
thumbnail_url: uploadResult.thumbnail,
original_url: uploadResult.original,
processing_time: processingTime,
};
}
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const taskId = String(searchParams.get("taskId") || "").trim();
if (!taskId) {
return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
}
const prompt = searchParams.get("prompt") || "图片变清晰处理";
const chatId = searchParams.get("chatId");
const messageId = searchParams.get("messageId");
const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
if (!enhanceConfig?.value) {
return NextResponse.json(
{ error: "变清晰API配置未设置,请在系统设置中配置" },
{ status: 400 }
);
}
const config = enhanceConfig.value as EnhanceApiConfig;
if (config.useBackupApi) {
return NextResponse.json(
{ error: "当前启用了Real-ESRGAN备用模式,不支持 taskId 轮询" },
{ status: 400 }
);
}
if (!config.apiUrl || !config.apiKey) {
return NextResponse.json(
{ error: "变清晰API地址或密钥未配置" },
{ status: 400 }
);
}
const apiUrl = config.apiUrl.endsWith("/") ? config.apiUrl.slice(0, -1) : config.apiUrl;
let taskResult: any;
try {
taskResult = await pollTaskResult(
apiUrl,
config.apiKey,
taskId,
ENHANCE_TASK_POLL_INTERVAL_MS,
ENHANCE_SYNC_WAIT_TIMEOUT_MS
);
} catch (pollError) {
if (isEnhanceTaskStillProcessing(pollError)) {
return buildEnhanceTaskPendingResponse(taskId);
}
const detail = pollError instanceof Error ? pollError.message : "轮询失败";
return NextResponse.json(
{ error: "变清晰处理失败", detail },
{ status: 500 }
);
}
const responsePayload = await finalizeEnhanceTaskResult({
userId: session.user.id,
taskResult,
prompt,
chatId,
messageId,
});
return NextResponse.json(responsePayload);
} catch (error) {
console.error("[Enhance API][TaskStatus] 处理错误:", error);
return NextResponse.json(
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
{ status: 500 }
);
}
}
/**
* 图片变清晰API路由
* 从数据库读取变清晰API配置,调用外部API进行图片增强处理
@ -780,12 +942,21 @@ export async function POST(request: NextRequest) {
);
}
// 4. 轮询获取任务结果(根据官方示例)
console.log(`[Enhance] 开始轮询任务结果,task_id: ${taskId}`);
// 4. 轮询获取任务结果(短等待,超时后让前端继续轮询)
console.log(`[Enhance] 开始短轮询任务结果,task_id: ${taskId}`);
let taskResult: any;
try {
taskResult = await pollTaskResult(apiUrl, config.apiKey!, taskId);
taskResult = await pollTaskResult(
apiUrl,
config.apiKey!,
taskId,
ENHANCE_TASK_POLL_INTERVAL_MS,
ENHANCE_SYNC_WAIT_TIMEOUT_MS
);
} catch (pollError) {
if (isEnhanceTaskStillProcessing(pollError)) {
return buildEnhanceTaskPendingResponse(taskId);
}
const detail = pollError instanceof Error ? pollError.message : "轮询失败";
const friendlyMessage = getAosFriendlyErrorMessage(detail);
return NextResponse.json(
@ -793,154 +964,17 @@ export async function POST(request: NextRequest) {
{ status: friendlyMessage ? 400 : 500 }
);
}
const finalProcessingTime = (Date.now() - startTime) / 1000;
// 5. 处理返回结果
if (taskResult.status !== 200 || !taskResult.data) {
return NextResponse.json(
{ error: "变清晰处理失败", detail: taskResult.message || "未知错误" },
{ status: 500 }
);
}
// ✅ 记录原始输入图片大小(用于对比)
const originalImageSize = imageBuffer.length;
console.log(`[Enhance] 📊 原始输入图片大小: ${originalImageSize} bytes (${(originalImageSize / 1024 / 1024).toFixed(2)}MB)`);
// ✅ 打印外部API返回的完整数据结构(用于调试)
console.log(`[Enhance] 📋 外部API返回的taskResult.data结构:`, {
hasImageBase64: !!taskResult.data.image_base64,
hasImage: !!taskResult.data.image,
imageType: typeof taskResult.data.image,
imagePreview: typeof taskResult.data.image === "string" ? taskResult.data.image.substring(0, 100) : "非字符串",
status: taskResult.data.status,
progress: taskResult.data.progress,
state: taskResult.data.state,
});
let enhancedImageBase64: string;
if (taskResult.data.image_base64) {
// 返回的是base64
enhancedImageBase64 = taskResult.data.image_base64;
console.log(`[Enhance] ✅ API返回了image_base64,长度: ${enhancedImageBase64.length} 字符`);
} else if (taskResult.data.image) {
// 检查是否是URL(以http://或https://开头)
const imageData = taskResult.data.image;
console.log(`[Enhance] 📥 API返回了image字段,类型: ${typeof imageData}, 值: ${typeof imageData === "string" ? imageData.substring(0, 100) : "非字符串"}`);
if (typeof imageData === "string" && (imageData.startsWith("http://") || imageData.startsWith("https://"))) {
// 返回的是URL,需要下载
console.log(`[Enhance] 📥 从URL下载处理后的图片: ${imageData}`);
const imageResponse = await fetch(imageData);
if (!imageResponse.ok) {
return NextResponse.json(
{ error: "变清晰处理失败", detail: "无法下载处理后的图片" },
{ status: 500 }
);
}
const downloadedBuffer = await imageResponse.arrayBuffer();
enhancedImageBase64 = Buffer.from(downloadedBuffer).toString("base64");
const downloadedSize = downloadedBuffer.byteLength;
console.log(`[Enhance] ✅ 从URL下载完成,Base64长度: ${enhancedImageBase64.length} 字符,文件大小: ${downloadedSize} bytes`);
// ✅ 对比下载的图片和原图大小
if (Math.abs(downloadedSize - originalImageSize) < 100) {
console.warn(`[Enhance] ⚠️ 警告:下载的图片大小(${downloadedSize})和原图大小(${originalImageSize})几乎相同,可能外部API返回的是原图!`);
}
} else {
// 返回的是base64字符串(直接使用)
enhancedImageBase64 = imageData;
console.log(`[Enhance] ✅ API返回了base64字符串,长度: ${enhancedImageBase64.length} 字符`);
}
} else {
console.error(`[Enhance] ❌ 轮询结果未包含图片数据,taskResult.data:`, JSON.stringify(taskResult.data, null, 2));
return NextResponse.json(
{ error: "变清晰处理失败", detail: "轮询结果未包含图片数据" },
{ status: 500 }
);
}
if (!enhancedImageBase64) {
return NextResponse.json(
{ error: "变清晰处理失败", detail: "无法获取处理后的图片" },
{ status: 500 }
);
}
// ✅ 验证:检查处理后的图片大小(应该和原图不同)
const enhancedBuffer = Buffer.from(enhancedImageBase64, "base64");
const enhancedImageSize = enhancedBuffer.length;
console.log(`[Enhance] 📊 处理后的图片大小: ${enhancedImageSize} bytes (${(enhancedImageSize / 1024 / 1024).toFixed(2)}MB)`);
console.log(`[Enhance] 📊 处理后的图片Base64长度: ${enhancedImageBase64.length} 字符`);
// ✅ 对比:如果处理后的图片大小和原图完全相同,可能是返回了原图
if (Math.abs(enhancedImageSize - originalImageSize) < 100) {
console.warn(`[Enhance] ⚠️ 警告:处理后的图片大小(${enhancedImageSize})和原图大小(${originalImageSize})几乎相同,可能API返回的是原图!`);
} else {
const sizeDiff = ((enhancedImageSize - originalImageSize) / originalImageSize * 100).toFixed(1);
console.log(`[Enhance] ✅ 图片大小变化: ${sizeDiff}% (${sizeDiff.startsWith("-") ? "减小" : "增大"})`);
}
// 5. 上传处理后的图片到S3存储桶
let thumbnailUrl: string;
let originalUrl: string;
try {
// ✅ 确认:上传的是处理后的图片(enhancedImageBase64)
console.log(`[Enhance] 📤 准备上传处理后的图片到S3,Base64长度: ${enhancedImageBase64.length}`);
const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
thumbnailUrl = uploadResult.thumbnail;
originalUrl = uploadResult.original;
console.log(`[Enhance] ✅ Images uploaded to S3 successfully`);
console.log(`[Enhance] ✅ 处理后的原图URL: ${originalUrl}`);
console.log(`[Enhance] ✅ 处理后的缩略图URL: ${thumbnailUrl}`);
} catch (uploadError) {
console.error("[Enhance] ❌ Failed to upload to S3:", uploadError);
return NextResponse.json(
{
error: "图片上传到存储桶失败",
detail: uploadError instanceof Error ? uploadError.message : "未知错误"
},
{ status: 500 }
);
}
// 6. 保存图片记录到数据库(相册和生图记录)
try {
const [savedImage] = await db.insert(generatedImage).values({
const responsePayload = await finalizeEnhanceTaskResult({
userId,
chatId: chatId || null,
messageId: messageId || null,
thumbnailUrl,
originalUrl,
imageSize: "2K", // 默认尺寸
prompt: prompt || "图片变清晰处理",
modelId: "enhance-1.0", // 标记为变清晰处理
}).returning();
console.log(`[Enhance] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
} catch (dbError) {
console.error("[Enhance] ❌ 保存到数据库失败:", dbError);
// 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
// 如果希望严格保证数据一致性,可以取消注释下面的代码来返回错误
// return NextResponse.json(
// {
// error: "图片已处理但保存到相册失败",
// detail: dbError instanceof Error ? dbError.message : "未知错误"
// },
// { status: 500 }
// );
}
taskResult,
prompt,
chatId,
messageId,
startedAt: startTime,
});
// 7. 返回结果(包含S3 URL和Base64,前端可以选择使用)
return NextResponse.json({
success: true,
image_base64: enhancedImageBase64, // 保留Base64用于前端立即显示
thumbnail_url: thumbnailUrl, // S3缩略图URL
original_url: originalUrl, // S3原图URL
processing_time: finalProcessingTime,
});
return NextResponse.json(responsePayload);
} catch (error) {
console.error("[Enhance API] 处理错误:", error);
return NextResponse.json(

View File

@ -6,6 +6,8 @@ import { eq } from "drizzle-orm";
import { uploadFile } from "@/lib/storage/service";
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
const WATERMARK_POLL_INTERVAL_MS = 1200;
// 获取去水印API配置
async function getWatermarkRemoveConfig() {
@ -27,9 +29,13 @@ async function getWatermarkRemoveConfig() {
}
// 轮询获取处理结果
async function pollForResult(taskId: string, apiKey: string): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
const maxAttempts = 180; // 最多轮询180次(约3分钟)
const pollInterval = 1000; // 每次间隔1秒
async function pollForResult(
taskId: string,
apiKey: string,
timeoutMs = WATERMARK_SYNC_WAIT_TIMEOUT_MS
): Promise<{ success: boolean; imageUrl?: string; error?: string; pending?: boolean }> {
const maxAttempts = Math.max(1, Math.ceil(timeoutMs / WATERMARK_POLL_INTERVAL_MS));
const pollInterval = WATERMARK_POLL_INTERVAL_MS;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
@ -81,7 +87,128 @@ async function pollForResult(taskId: string, apiKey: string): Promise<{ success:
}
}
return { success: false, error: "去水印任务处理超时(服务端仍在处理中),请稍后重试" };
return {
success: false,
pending: true,
error: "去水印任务仍在处理中",
};
}
function buildWatermarkTaskPendingResponse(taskId: string) {
return NextResponse.json(
{
success: true,
processing: true,
status: "running",
taskId,
pollAfterMs: WATERMARK_POLL_INTERVAL_MS,
},
{ status: 202 }
);
}
async function finalizeWatermarkTaskResult(params: {
taskId: string;
userId: string;
startedAt?: number;
apiKey: string;
}): Promise<{
success: boolean;
original_url: string;
thumbnail_url: string;
processing_time?: number;
}> {
const pollResult = await pollForResult(params.taskId, params.apiKey, WATERMARK_SYNC_WAIT_TIMEOUT_MS);
if (pollResult.pending) {
throw new Error("WATERMARK_TASK_PENDING");
}
if (!pollResult.success || !pollResult.imageUrl) {
throw new Error(pollResult.error || "去水印处理失败");
}
const processedImageUrl = pollResult.imageUrl;
try {
const imageResponse = await fetch(processedImageUrl);
if (!imageResponse.ok) {
throw new Error("下载处理后的图片失败");
}
const imageBuffer = await imageResponse.arrayBuffer();
const filename = `watermark-removed-${Date.now()}.png`;
const uploadResult = await uploadFile(filename, imageBuffer);
try {
await db.insert(generatedImage).values({
userId: params.userId,
thumbnailUrl: uploadResult.url,
originalUrl: uploadResult.url,
imageSize: "2K",
prompt: "去水印处理",
modelId: "watermark-remove",
});
} catch (saveError) {
console.error("[WatermarkRemove] 保存到相册失败:", saveError);
}
return {
success: true,
original_url: uploadResult.url,
thumbnail_url: uploadResult.url,
processing_time:
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined,
};
} catch (uploadError) {
console.error("[WatermarkRemove] 上传结果失败,回退原始URL:", uploadError);
return {
success: true,
original_url: processedImageUrl,
thumbnail_url: processedImageUrl,
processing_time:
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined,
};
}
}
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const taskId = String(searchParams.get("taskId") || "").trim();
if (!taskId) {
return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
}
const config = await getWatermarkRemoveConfig();
if (!config?.apiKey) {
return NextResponse.json(
{ error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" },
{ status: 500 }
);
}
try {
const result = await finalizeWatermarkTaskResult({
taskId,
userId: session.user.id,
apiKey: config.apiKey,
});
return NextResponse.json(result);
} catch (error) {
if (error instanceof Error && error.message === "WATERMARK_TASK_PENDING") {
return buildWatermarkTaskPendingResponse(taskId);
}
throw error;
}
} catch (error) {
console.error("[WatermarkRemove][TaskStatus] 处理错误:", error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : "去水印处理失败" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
@ -212,66 +339,19 @@ export async function POST(request: NextRequest) {
);
}
// 轮询获取结果
const pollResult = await pollForResult(taskId, config.apiKey);
if (!pollResult.success) {
return NextResponse.json(
{ error: pollResult.error || "去水印处理失败" },
{ status: 500 }
);
}
// 下载处理后的图片并上传到S3
const processedImageUrl = pollResult.imageUrl!;
console.log("[WatermarkRemove] 处理完成,结果URL:", processedImageUrl);
try {
// 下载处理后的图片
const imageResponse = await fetch(processedImageUrl);
if (!imageResponse.ok) {
throw new Error("下载处理后的图片失败");
}
const imageBuffer = await imageResponse.arrayBuffer();
// 上传到S3
const filename = `watermark-removed-${Date.now()}.png`;
const uploadResult = await uploadFile(filename, imageBuffer);
const processingTime = (Date.now() - startTime) / 1000;
console.log(`[WatermarkRemove] 处理完成,耗时: ${processingTime}s`);
// 保存到相册/历史出图记录
try {
await db.insert(generatedImage).values({
userId: session.user.id,
thumbnailUrl: uploadResult.url,
originalUrl: uploadResult.url,
imageSize: "2K",
prompt: "去水印处理",
modelId: "watermark-remove",
});
console.log(`[WatermarkRemove] 已保存到相册: ${uploadResult.url.substring(0, 80)}...`);
} catch (saveError) {
console.error("[WatermarkRemove] 保存到相册失败:", saveError);
}
return NextResponse.json({
success: true,
original_url: uploadResult.url,
thumbnail_url: uploadResult.url,
processing_time: processingTime,
});
} catch (uploadError) {
console.error("[WatermarkRemove] 上传结果失败:", uploadError);
// 如果上传失败,直接返回原始URL
const processingTime = (Date.now() - startTime) / 1000;
return NextResponse.json({
success: true,
original_url: processedImageUrl,
thumbnail_url: processedImageUrl,
processing_time: processingTime,
const result = await finalizeWatermarkTaskResult({
taskId,
userId: session.user.id,
startedAt: startTime,
apiKey: config.apiKey,
});
return NextResponse.json(result);
} catch (error) {
if (error instanceof Error && error.message === "WATERMARK_TASK_PENDING") {
return buildWatermarkTaskPendingResponse(taskId);
}
throw error;
}
} catch (error) {
console.error("[WatermarkRemove] 处理错误:", error);

View File

@ -26,6 +26,7 @@ import type { UserType } from "@/app/(auth)/auth";
import { ChatSDKError } from "@/lib/errors";
import type { Attachment, ChatMessage } from "@/lib/types";
import type { AppUsage } from "@/lib/usage";
import { requestLongTaskResult } from "@/lib/client/long-task";
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
import { Artifact } from "@/components/artifact";
import { useDataStream } from "@/components/data-stream-provider";
@ -1229,25 +1230,21 @@ export function Chat({
try {
const base64Data = (reader.result as string).split(",")[1];
// 调用变清晰API
const enhanceResponse = await fetch("/api/enhance", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
const result = await requestLongTaskResult({
endpoint: "/api/enhance",
payload: {
image_base64: base64Data,
prompt: "图片变清晰处理",
}),
chatId: id,
messageId,
},
pollingParams: {
prompt: "图片变清晰处理",
chatId: id,
messageId,
},
});
if (!enhanceResponse.ok) {
const error = await enhanceResponse.json();
throw new Error(error.error || "变清晰处理失败");
}
const result = await enhanceResponse.json();
if (result.success && result.original_url) {
// 构造图片Markdown(使用与Gemini生成相同的格式)
const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`;
@ -1510,24 +1507,13 @@ export function Chat({
}, 100);
try {
// 调用去水印API
const watermarkRemoveResponse = await fetch("/api/watermark-remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
const result = await requestLongTaskResult({
endpoint: "/api/watermark-remove",
payload: {
image_url: imageUrl,
}),
},
});
if (!watermarkRemoveResponse.ok) {
const error = await watermarkRemoveResponse.json();
throw new Error(error.error || "去水印处理失败");
}
const result = await watermarkRemoveResponse.json();
if (result.success && result.original_url) {
// 构造图片Markdown(使用与Gemini生成相同的格式)
const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`;

View File

@ -25,6 +25,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { requestLongTaskResult } from "@/lib/client/long-task";
type Tool = "pen" | "arrow" | "circle" | "text";
@ -742,24 +743,13 @@ export function ImageAnnotator({
try {
console.log("[去水印] 使用原图URL:", originalImageUrl);
// 调用去水印API
const response = await fetch("/api/watermark-remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
const result = await requestLongTaskResult({
endpoint: "/api/watermark-remove",
payload: {
image_url: originalImageUrl,
}),
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "去水印失败");
}
const result = await response.json();
if (result.success && result.original_url) {
console.log("[去水印] API返回结果:", {
originalUrl: result.original_url?.substring(0, 100),
@ -805,25 +795,17 @@ export function ImageAnnotator({
const base64Data = await imageUrlToBase64(originalImageUrl);
console.log("[变清晰] 原图Base64长度:", base64Data.length, "字符");
// 调用变清晰API
const response = await fetch("/api/enhance", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
const result = await requestLongTaskResult({
endpoint: "/api/enhance",
payload: {
image_base64: base64Data,
prompt: "画图标注-变清晰处理", // 可选:添加提示词
}),
prompt: "画图标注-变清晰处理",
},
pollingParams: {
prompt: "画图标注-变清晰处理",
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "变清晰处理失败");
}
const result = await response.json();
if (result.success && result.image_base64) {
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图)
console.log("[变清晰] API返回结果:", {

91
lib/client/long-task.ts Normal file
View File

@ -0,0 +1,91 @@
type LongTaskResponse = {
success?: boolean;
processing?: boolean;
status?: string;
taskId?: string;
pollAfterMs?: number;
error?: string;
detail?: string;
[key: string]: any;
};
function isProcessingPayload(payload: LongTaskResponse | null | undefined) {
if (!payload) return false;
if (payload.processing === true) return true;
const status = String(payload.status || "").toLowerCase();
return status === "queued" || status === "running" || status === "pending";
}
function buildTaskError(payload: LongTaskResponse | null | undefined, fallback: string) {
const message = payload?.error || payload?.detail || fallback;
return new Error(String(message || fallback));
}
async function safeParseJson(response: Response) {
try {
return (await response.json()) as LongTaskResponse;
} catch {
return null;
}
}
export async function requestLongTaskResult(params: {
endpoint: string;
payload: Record<string, any>;
pollingParams?: Record<string, string | number | null | undefined>;
timeoutMs?: number;
}) {
const response = await fetch(params.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(params.payload),
});
const result = await safeParseJson(response);
if (!response.ok) {
throw buildTaskError(result, "请求失败");
}
if (!isProcessingPayload(result)) {
return result as LongTaskResponse;
}
const taskId = String(result?.taskId || "").trim();
if (!taskId) {
throw buildTaskError(result, "任务正在处理中,但缺少 taskId");
}
const pollingParams = new URLSearchParams();
pollingParams.set("taskId", taskId);
for (const [key, value] of Object.entries(params.pollingParams || {})) {
if (value === null || value === undefined || value === "") continue;
pollingParams.set(key, String(value));
}
const timeoutMs = Math.max(10_000, params.timeoutMs ?? 8 * 60 * 1000);
const startedAt = Date.now();
let pollAfterMs = Math.max(800, Number(result?.pollAfterMs || 1500));
while (Date.now() - startedAt < timeoutMs) {
await new Promise((resolve) => setTimeout(resolve, pollAfterMs));
const statusResponse = await fetch(`${params.endpoint}?${pollingParams.toString()}`, {
method: "GET",
cache: "no-store",
});
const statusPayload = await safeParseJson(statusResponse);
if (!statusResponse.ok) {
throw buildTaskError(statusPayload, "查询任务状态失败");
}
if (!isProcessingPayload(statusPayload)) {
return statusPayload as LongTaskResponse;
}
pollAfterMs = Math.max(800, Number(statusPayload?.pollAfterMs || pollAfterMs));
}
throw new Error("任务处理超时,请稍后再试");
}