diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 16d7d13ad..27f53945a 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -672,13 +672,14 @@ function sanitizeAgentOutputSettingsSection(params: { const { message, userImageParams, hasUserCustomImageParams } = params; if (!message?.trim()) return message; - const outputSectionRegex = /###\s*输出设置[\s\S]*?(?=\n###\s*|$)/; + const outputSectionRegex = + /(?:###\s*输出设置|【\s*输出设置\s*】)[\s\S]*?(?=\n(?:###\s*|【\s*[^】]+\s*】)|$)/; if (!outputSectionRegex.test(message)) return message; if (!hasUserCustomImageParams) { return message.replace( outputSectionRegex, - "### 输出设置\n- 参数:按用户面板当前设置执行(未自动改写)" + "【输出设置】\n- 参数:按用户面板当前设置执行(未自动改写)" ); } @@ -701,7 +702,46 @@ function sanitizeAgentOutputSettingsSection(params: { } lines.push("- 其余参数:保持用户面板当前设置"); - return message.replace(outputSectionRegex, `### 输出设置\n${lines.join("\n")}`); + return message.replace(outputSectionRegex, `【输出设置】\n${lines.join("\n")}`); +} + +function normalizeAgentReportHeadingLine(line: string): string { + const trimmed = line.trim(); + if (!trimmed) return line; + + const markdownHeading = trimmed.match(/^#{1,6}\s*(.+?)\s*$/); + const plainTitle = markdownHeading?.[1]?.trim() || trimmed; + const normalizedTitle = plainTitle + .replace(/^[^\u4e00-\u9fa5A-Za-z0-9]+/, "") + .trim(); + + const knownTitles = [ + "需求理解", + "图片分析", + "执行计划", + "输出设置", + "脚本规划", + "分镜规划", + "执行说明", + "视频生成完成", + ]; + + if (knownTitles.some((title) => normalizedTitle === title)) { + return `【${normalizedTitle}】`; + } + + if (markdownHeading) { + return `【${normalizedTitle || plainTitle}】`; + } + return line; +} + +function normalizeAgentReportForDisplay(message?: string): string | undefined { + if (!message?.trim()) return message; + return message + .split("\n") + .map((line) => normalizeAgentReportHeadingLine(line)) + .join("\n"); } function guessImageMediaType(url: string): string { @@ -812,7 +852,7 @@ function extractMessageImageUrls(parts: any[]): string[] { function shouldEnrichImageAnalysisText(message?: string): boolean { if (!message) return false; - if (!/###\s*图片分析/.test(message)) return false; + if (!/(###\s*图片分析|【\s*图片分析\s*】)/.test(message)) return false; return /(基于当前上传图片处理|基于你选中的原图处理|基于上一轮图片继续处理)/.test(message); } @@ -820,12 +860,14 @@ function injectImageAnalysisSummary(message: string, summary: string): string { const normalizedSummary = summary.replace(/\s+/g, " ").trim(); if (!normalizedSummary) return message; const lines = message.split("\n"); - const analysisHeaderIndex = lines.findIndex((line) => /^###\s*图片分析/.test(line.trim())); + const analysisHeaderIndex = lines.findIndex((line) => + /^(###\s*图片分析|【\s*图片分析\s*】)/.test(line.trim()) + ); if (analysisHeaderIndex < 0) return message; for (let i = analysisHeaderIndex + 1; i < lines.length; i += 1) { const current = lines[i]?.trim() || ""; - if (/^###\s*/.test(current)) break; + if (/^(###\s*|【\s*[^】]+\s*】)/.test(current)) break; if (/^- /.test(current)) { lines[i] = `- ${normalizedSummary}`; return lines.join("\n"); @@ -837,6 +879,19 @@ function injectImageAnalysisSummary(message: string, summary: string): string { return lines.join("\n"); } +function ensureImageAnalysisSummarySection(message: string, summary: string): string { + const normalizedSummary = summary.replace(/\s+/g, " ").trim(); + if (!normalizedSummary) return message; + + if (/(###\s*图片分析|【\s*图片分析\s*】)/.test(message)) { + return injectImageAnalysisSummary(message, normalizedSummary); + } + + const trimmed = message.trim(); + if (!trimmed) return `【图片分析】\n- ${normalizedSummary}`; + return `${trimmed}\n【图片分析】\n- ${normalizedSummary}`; +} + /** * 自定义消息转换函数 * 注意:gemini-image 和 huanke-gemini 都需要 URL 格式(不是 Base64) @@ -1355,7 +1410,8 @@ let globalStreamContext: ResumableStreamContext | null = null; const inFlightChatRequests = new Map(); const IN_FLIGHT_REQUEST_TTL_MS = 5 * 60 * 1000; const DUPLICATE_REQUEST_WAIT_MS = 2 * 60 * 1000; -const SMART_AUTO_GRAPH_TIMEOUT_MS = 45000; +const SMART_AUTO_GRAPH_TIMEOUT_MS = 120000; +const SMART_AUTO_FALLBACK_TIMEOUT_MS = 120000; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -1905,7 +1961,7 @@ export async function POST(request: Request) { context: undefined, }), "fallbackAnalyzeIntent", - 15000, + SMART_AUTO_FALLBACK_TIMEOUT_MS, ); if (rescueAnalysis.conversationalReply?.trim()) { smartAutoFriendlyMessage = rescueAnalysis.conversationalReply.trim(); @@ -1947,7 +2003,7 @@ export async function POST(request: Request) { context: typeof autoContext === "string" ? autoContext : undefined, }), "fallbackVisualSummary", - 15000, + SMART_AUTO_FALLBACK_TIMEOUT_MS, ); if (summary?.trim()) { fallbackVisualSummary = summary.trim(); @@ -2138,44 +2194,59 @@ export async function POST(request: Request) { }); } - if ( - shouldRunAgentConversation && - !smartAutoNeedsClarification && - smartAutoFriendlyMessage && - shouldEnrichImageAnalysisText(smartAutoFriendlyMessage) - ) { - try { - const currentImageUrls = extractMessageImageUrls(message.parts as any[]); - const summaryImageUrls = - currentImageUrls.length > 0 ? currentImageUrls : smartAutoHistoryImages; + if (shouldRunAgentConversation && !smartAutoNeedsClarification) { + const currentImageUrls = extractMessageImageUrls(message.parts as any[]); + const summaryImageUrls = + currentImageUrls.length > 0 ? currentImageUrls : smartAutoHistoryImages; + const shouldRequireImageAnalysis = summaryImageUrls.length > 0; - if (summaryImageUrls.length > 0) { + if (shouldRequireImageAnalysis) { + try { const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer"); - const summary = await withOperationTimeout( - summarizeImageForExecutionReport({ - text: message.parts - .filter((part): part is { type: "text"; text: string } => part.type === "text") - .map((part) => part.text) - .join(" "), - imageUrls: summaryImageUrls, - context: typeof autoContext === "string" ? autoContext : undefined, - }), - "enrichImageAnalysisSummary", - 8000, - ); - if (summary?.trim()) { - smartAutoFriendlyMessage = injectImageAnalysisSummary( - smartAutoFriendlyMessage, - summary.trim(), + const latestUserText = message.parts + .filter((part): part is { type: "text"; text: string } => part.type === "text") + .map((part) => part.text) + .join(" "); + + let summary: string | null = null; + const summaryTimeouts = [120000]; + for (const timeoutMs of summaryTimeouts) { + summary = await withOperationTimeout( + summarizeImageForExecutionReport({ + text: latestUserText, + imageUrls: summaryImageUrls, + context: typeof autoContext === "string" ? autoContext : undefined, + }), + "enrichImageAnalysisSummary", + timeoutMs, ); - console.log("[Smart Auto Mode] ✅ Enriched image analysis summary"); + if (summary?.trim()) break; } + + if (summary?.trim()) { + const baseMessage = + smartAutoFriendlyMessage || + buildAutoModeFallbackMessage({ + text: latestUserText, + hasImage: true, + fromHistory: currentImageUrls.length === 0 && summaryImageUrls.length > 0, + imageSummary: summary.trim(), + }); + smartAutoFriendlyMessage = ensureImageAnalysisSummarySection(baseMessage, summary.trim()); + console.log("[Smart Auto Mode] ✅ Image analysis summary ready"); + } else if (smartAutoFriendlyMessage && shouldEnrichImageAnalysisText(smartAutoFriendlyMessage)) { + console.warn("[Smart Auto Mode] ⚠️ Image summary unavailable, keep original analysis text"); + } + } catch (enrichError) { + console.warn("[Smart Auto Mode] Skip image analysis enrichment:", enrichError); } - } catch (enrichError) { - console.warn("[Smart Auto Mode] Skip image analysis enrichment:", enrichError); } } + if (shouldRunAgentConversation && smartAutoFriendlyMessage) { + smartAutoFriendlyMessage = normalizeAgentReportForDisplay(smartAutoFriendlyMessage); + } + const session = await auth(); if (!session?.user) { diff --git a/lib/ai/auto-agent-graph.ts b/lib/ai/auto-agent-graph.ts index 553689fdb..a33471da0 100644 --- a/lib/ai/auto-agent-graph.ts +++ b/lib/ai/auto-agent-graph.ts @@ -105,7 +105,8 @@ async function withTimeoutFallback( const AGENT_VISION_MAX_IMAGES = 4; const AGENT_VISION_MAX_SIDE = 500; -const AGENT_VISION_FETCH_TIMEOUT_MS = 10_000; +const AGENT_OPERATION_TIMEOUT_MS = 120_000; +const AGENT_VISION_FETCH_TIMEOUT_MS = AGENT_OPERATION_TIMEOUT_MS; function isHttpUrl(value: string) { return /^https?:\/\//i.test(value); @@ -514,7 +515,10 @@ function buildStrategicFriendlyMessage( const isStructuredReply = /###\s*需求理解/.test(existingReply) || /###\s*图片分析/.test(existingReply) || - /###\s*执行计划/.test(existingReply); + /###\s*执行计划/.test(existingReply) || + /【\s*需求理解\s*】/.test(existingReply) || + /【\s*图片分析\s*】/.test(existingReply) || + /【\s*执行计划\s*】/.test(existingReply); if (isStructuredReply || existingReply.length >= 24) { return Promise.resolve(existingReply); } @@ -522,7 +526,7 @@ function buildStrategicFriendlyMessage( return withTimeoutFallback( generateAgentConversation(analysis, state), - 12000, + AGENT_OPERATION_TIMEOUT_MS, existingReply || null, ); } @@ -727,7 +731,7 @@ async function generateAgentConversation( ], temperature: 0.2, maxTokens: 360, - abortSignal: AbortSignal.timeout(12000), + abortSignal: AbortSignal.timeout(AGENT_OPERATION_TIMEOUT_MS), } as any); visualAnalysis = visionText.trim(); @@ -751,12 +755,12 @@ async function generateAgentConversation( 输出要求: - 不要说“看不到图片/无法访问图片”。 - 不暴露内部实现、模型路由、算法细节。 -- 输出 Markdown,控制在 8-14 行。 -- 有图片时必须按顺序输出以下 Markdown H3 标题(逐字一致): - ### 需求理解 - ### 图片分析 - ### 执行计划 - ### 输出设置 +- 输出控制在 8-14 行,标题行不要使用 # 号。 +- 有图片时必须按顺序输出以下标题(逐字一致): + 【需求理解】 + 【图片分析】 + 【执行计划】 + 【输出设置】 - “图片分析”必须基于视觉分析结果写具体观察,不允许写泛化空话。 - 只有明确拿到原图分辨率时,才能写具体像素尺寸;否则只写宽高比/清晰度目标。`; @@ -780,7 +784,7 @@ async function generateAgentConversation( prompt, temperature: 0.35, maxTokens: 520, - abortSignal: AbortSignal.timeout(12000), + abortSignal: AbortSignal.timeout(AGENT_OPERATION_TIMEOUT_MS), } as any); const cleaned = text.trim(); @@ -854,7 +858,7 @@ async function recommendImageParamsWithAgent( prompt: requestText, temperature: 0.15, maxTokens: 220, - abortSignal: AbortSignal.timeout(10000), + abortSignal: AbortSignal.timeout(AGENT_OPERATION_TIMEOUT_MS), } as any); const parsed = parseJsonBlock(text); @@ -939,7 +943,7 @@ async function recommendVideoPlanWithAgent( prompt, temperature: 0.2, maxTokens: 720, - abortSignal: AbortSignal.timeout(12000), + abortSignal: AbortSignal.timeout(AGENT_OPERATION_TIMEOUT_MS), } as any); const parsed = parseJsonBlock>(text); @@ -1011,7 +1015,7 @@ async function refineRouteWithAgent( ${candidatesText}`, temperature: 0.1, maxTokens: 120, - abortSignal: AbortSignal.timeout(8000), + abortSignal: AbortSignal.timeout(AGENT_OPERATION_TIMEOUT_MS), } as any); const parsed = parseJsonBlock<{ modelId?: string; reason?: string }>(text); @@ -1108,12 +1112,27 @@ const optimizeNode = async (state: AutoAgentStateType) => { if (state.analysis.intent !== "image_edit" && state.analysis.intent !== "image_gen") { return {}; } - const { optimizePrompt } = await import("./prompt-optimizer"); - const optimized = optimizePrompt({ - originalPrompt: state.text, - intent: state.analysis.intent, - subIntent: state.analysis.subIntent, - }); + const { optimizePrompt, optimizePromptWithLLM } = await import("./prompt-optimizer"); + const optimizedFromLlm = await withTimeoutFallback< + Awaited> | null + >( + optimizePromptWithLLM({ + originalPrompt: state.text, + intent: state.analysis.intent, + subIntent: state.analysis.subIntent, + historyMessages: state.historyMessages, + context: state.context || undefined, + }), + AGENT_OPERATION_TIMEOUT_MS, + null, + ); + const optimized = + optimizedFromLlm || + optimizePrompt({ + originalPrompt: state.text, + intent: state.analysis.intent, + subIntent: state.analysis.subIntent, + }); const mergedParams = sanitizeRecommendedImageParams( { ...optimized.recommendedImageParams, diff --git a/lib/ai/smart-analyzer.ts b/lib/ai/smart-analyzer.ts index 88d3e3fc5..6e0cf075b 100644 --- a/lib/ai/smart-analyzer.ts +++ b/lib/ai/smart-analyzer.ts @@ -240,7 +240,8 @@ const ANALYZER_SYSTEM_PROMPT = `你是一个友好的AI图片编辑助手。用 const ANALYZER_MAX_IMAGE_COUNT = 2; const ANALYZER_IMAGE_MAX_SIDE = 500; -const ANALYZER_IMAGE_FETCH_TIMEOUT_MS = 10000; +const ANALYZER_TIMEOUT_MS = 120000; +const ANALYZER_IMAGE_FETCH_TIMEOUT_MS = ANALYZER_TIMEOUT_MS; const ZHIPU_VISION_MODEL_CANDIDATES = [ "glm-4.6v-flash", "glm-4.6v", @@ -668,7 +669,7 @@ export async function summarizeImageForExecutionReport(params: { ], temperature: 0.1, maxTokens: 120, - abortSignal: AbortSignal.timeout(8000), + abortSignal: AbortSignal.timeout(ANALYZER_TIMEOUT_MS), } as any); const normalized = summaryText?.replace(/\s+/g, " ").trim(); @@ -971,7 +972,7 @@ ${historyText} ], temperature: 0.1, maxTokens: 360, - abortSignal: AbortSignal.timeout(12000), + abortSignal: AbortSignal.timeout(ANALYZER_TIMEOUT_MS), } : { model, @@ -979,7 +980,7 @@ ${historyText} prompt: userMessage, temperature: 0.1, maxTokens: 300, - abortSignal: AbortSignal.timeout(10000), + abortSignal: AbortSignal.timeout(ANALYZER_TIMEOUT_MS), }; const { text: responseText } = await generateText(requestPayload as any);