fix: reduce 504 via async polling and chat keepalive
This commit is contained in:
parent
bff8ffa100
commit
5e155ecf3c
@ -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!";
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = ``;
|
||||
@ -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 = ``;
|
||||
|
||||
@ -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
91
lib/client/long-task.ts
Normal 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("任务处理超时,请稍后再试");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user