diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index eb67e224e..ab38bc3a6 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1662,6 +1662,56 @@ function buildAutoModeFallbackMessage(params: { ].join("\n"); } +function sanitizePromptOnlyUserIntent(text: string): string { + return String(text || "") + .replace(/先?分析(?:一下)?(?:这张)?(?:图片|图像|这张图)?/g, "") + .replace(/帮我|给我|先给我|只给我|仅给我/g, "") + .replace(/优化(?:一下)?提示词|提示词优化|提示词|prompt/gi, "") + .replace(/不要生图|先别生图|不需要生图|不要生成|别生成|不直接生成/g, "") + .replace(/[,。;:,.!!??]/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function buildPromptOnlyOptimizationSeed(params: { + text: string; + imageSummary?: string; +}): string { + const cleanedIntent = sanitizePromptOnlyUserIntent(params.text); + const summary = String(params.imageSummary || "").replace(/\s+/g, " ").trim(); + + if (cleanedIntent && summary) { + return `${cleanedIntent}。参考图关键特征:${summary}`; + } + if (summary) { + return `基于参考图生成一段可直接复现核心主体、场景、风格与光线特征的中文提示词。参考图关键特征:${summary}`; + } + if (cleanedIntent) { + return cleanedIntent; + } + return "基于当前参考图整理一段可直接使用的中文提示词。"; +} + +function buildPromptOnlyReply(params: { + userText: string; + imageSummary?: string; + optimizedPrompt: string; +}): string { + const demand = sanitizePromptOnlyUserIntent(params.userText) || "你希望我先分析这张图并整理可直接使用的提示词"; + const summary = String(params.imageSummary || "").replace(/\s+/g, " ").trim(); + + return [ + "【需求理解】", + `- ${demand}`, + "【图片分析】", + summary ? `- ${summary}` : "- 我已基于当前图片梳理主体、场景、风格与光线特征。", + "【优化后提示词】", + `- ${params.optimizedPrompt.replace(/\s+/g, " ").trim()}`, + "【使用建议】", + "- 本轮仅输出优化后的提示词,不直接执行生图。", + ].join("\n"); +} + function hasStructuredAgentSections(message?: string): boolean { if (!message) return false; return /(?:###\s*需求理解|【\s*需求理解\s*】|###\s*图片分析|【\s*图片分析\s*】|###\s*执行计划|【\s*执行计划\s*】|###\s*优化后提示词|【\s*优化后提示词\s*】)/.test( @@ -4042,6 +4092,133 @@ export async function POST(request: Request) { const streamId = generateUUID(); await createStreamId({ streamId, chatId: id }); + const wantsPromptOnlyFromCurrentImage = + shouldRunAgentConversation && + !smartAutoNeedsClarification && + requestResolvedMediaInput.currentMessageImageUrls.length > 0 && + ( + await import("@/lib/ai/smart-analyzer") + ).isExplicitPromptOptimizationRequest(requestTextContent); + + if (wantsPromptOnlyFromCurrentImage) { + let promptOnlyImageSummary = ""; + try { + const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer"); + const summaryTimeoutMs = getBudgetedTimeout({ + startedAt: requestStartedAt, + preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS, + minimumMs: 6_000, + }); + if (summaryTimeoutMs) { + const summaryAssetIds = Array.from( + new Set([ + ...extractMessageImageAssetIds(message.parts as any[]), + ...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]), + ]), + ); + const summary = await withOperationTimeout( + summarizeImageForExecutionReport({ + text: requestTextContent, + imageUrls: requestResolvedMediaInput.currentMessageImageUrls.slice(0, 4), + context: typeof autoContext === "string" ? autoContext : undefined, + preferredModelId: preferredAgentModelId, + assetIds: summaryAssetIds, + }), + "promptOnlyImageSummary", + summaryTimeoutMs, + ); + if (summary?.trim()) { + promptOnlyImageSummary = summary.trim(); + } + } + } catch (promptOnlySummaryError) { + console.warn("[Chat API] Prompt-only image summary failed:", promptOnlySummaryError); + } + + let promptOnlyOptimizedPrompt = smartAutoOptimizedPrompt?.trim() || ""; + if (!promptOnlyOptimizedPrompt) { + try { + const { optimizePromptWithLLM, optimizePrompt } = await import("@/lib/ai/prompt-optimizer"); + const promptSeed = buildPromptOnlyOptimizationSeed({ + text: requestTextContent, + imageSummary: promptOnlyImageSummary, + }); + const optimizedResult = await optimizePromptWithLLM({ + originalPrompt: promptSeed, + intent: "image_gen", + historyMessages: smartAutoHistoryMessagesFromDb, + context: typeof autoContext === "string" ? autoContext : undefined, + }).catch(() => + optimizePrompt({ + originalPrompt: promptSeed, + intent: "image_gen", + }), + ); + promptOnlyOptimizedPrompt = String(optimizedResult?.optimizedPrompt || "").trim(); + } catch (promptOnlyOptimizeError) { + console.warn("[Chat API] Prompt-only optimization failed:", promptOnlyOptimizeError); + } + } + + if (!promptOnlyOptimizedPrompt) { + promptOnlyOptimizedPrompt = buildPromptOnlyOptimizationSeed({ + text: requestTextContent, + imageSummary: promptOnlyImageSummary, + }); + } + + const promptOnlyMessage = buildPromptOnlyReply({ + userText: requestTextContent, + imageSummary: promptOnlyImageSummary, + optimizedPrompt: promptOnlyOptimizedPrompt, + }); + + const promptOnlyStream = createUIMessageStream({ + execute: async ({ writer }) => { + const messageId = generateUUID(); + writeAssistantTextChunk(writer, promptOnlyMessage, "prompt-only:reply", messageId); + + await saveMessages({ + messages: [ + { + id: messageId, + chatId: id, + role: "assistant", + parts: [{ type: "text", text: promptOnlyMessage }], + attachments: [], + createdAt: new Date(), + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }, + ], + }); + + try { + await upsertConversationWorkingState({ + userId: session.user.id, + chatId: id, + projectId: contextPayload?.projectId || null, + currentIntent: "vision", + lastMessageId: messageId, + lastTurnSummary: { + text: requestTextContent.slice(0, 500), + resolutionMode: "prompt_only", + confidence: 90, + optimizedPrompt: promptOnlyOptimizedPrompt.slice(0, 1000), + }, + }); + } catch (promptOnlyMemoryError) { + console.warn("[Chat API] Failed to persist prompt-only response state:", promptOnlyMemoryError); + } + }, + }); + + return createSseResponse(promptOnlyStream); + } + if (shouldRunAgentConversation && smartAutoNeedsClarification) { const hasCurrentImageInput = message.parts.some((part: any) => part?.type === "file" || part?.type === "image" || part?.type === "image_url" diff --git a/lib/ai/smart-analyzer.ts b/lib/ai/smart-analyzer.ts index 8f68813c7..58aa04a1e 100644 --- a/lib/ai/smart-analyzer.ts +++ b/lib/ai/smart-analyzer.ts @@ -46,6 +46,26 @@ function isLikelyAdvisoryOrPlanningRequest(text: string): boolean { ); } +export function isExplicitPromptOptimizationRequest(text: string): boolean { + const normalizedText = normalizeCompactText(text); + if (!normalizedText) return false; + + const asksPrompt = + /提示词|prompt|文生图提示|出图提示|描述词|咒语|关键词/.test(normalizedText); + if (!asksPrompt) return false; + + const asksAnalysis = + /分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|写出|给我|输出|优化|改写/.test( + normalizedText, + ); + const blocksExecution = + /不要生图|先别生图|不需要生图|不要生成|别生成|仅给|只给|先给我|先出提示词|先分析/.test( + normalizedText, + ); + + return asksAnalysis || blocksExecution; +} + function isExplicitVideoGenerationRequest(text: string): boolean { const normalizedText = normalizeCompactText(text); const hasStrongVideoMarkers = @@ -112,6 +132,8 @@ function applyDirectExecutionPolicy( hasAnyRequestText && isLikelyAdvisoryOrPlanningRequest(params.text); const likelyVisionRequest = hasAnyRequestText && isLikelyVisionRequest(params.text); const likelyGeneralChatTurn = hasAnyRequestText && isLikelyGeneralChatTurn(params.text); + const promptOptimizationOnlyRequest = + hasAnyRequestText && isExplicitPromptOptimizationRequest(params.text); if (explicitVideoGenerationRequest) { return { @@ -130,6 +152,19 @@ function applyDirectExecutionPolicy( } if (!hasUsableImage) { + if (promptOptimizationOnlyRequest) { + return { + ...result, + intent: "chat", + subIntent: undefined, + confidence: Math.max(result.confidence, 0.82), + needsClarification: false, + clarifyingQuestions: [], + conversationalReply: + result.conversationalReply || "收到,我先按你的要求整理提示词,不直接执行生成。", + }; + } + if (!explicitImageGenerationRequest && !explicitVideoGenerationRequest) { if (likelyAdvisoryRequest || likelyGeneralChatTurn || result.intent === "chat") { return { @@ -174,6 +209,19 @@ function applyDirectExecutionPolicy( const keepClarification = Boolean(result.needsClarification); const explicitEditRequest = hasAnyRequestText && isLikelyImageEditRequest(params.text); + if (promptOptimizationOnlyRequest) { + return { + ...baseResult, + intent: "vision", + subIntent: undefined, + confidence: Math.max(baseResult.confidence, 0.88), + needsClarification: false, + clarifyingQuestions: [], + conversationalReply: + baseResult.conversationalReply || "收到,我先分析这张图并整理可直接使用的优化提示词,不会直接生图。", + }; + } + if (!hasAnyRequestText) { return { ...baseResult, @@ -2102,6 +2150,14 @@ export function fallbackKeywordAnalysis( // === 图片编辑场景(需要有图片) === if (hasImage) { + if (isExplicitPromptOptimizationRequest(text)) { + return { + intent: "vision", + confidence: 0.92, + conversationalReply: "收到,我先分析这张图并给你整理优化后的提示词,不直接生图。", + }; + } + // 视频生成(图生视频/首尾帧等) if (isExplicitVideoGenerationRequest(text)) { return {