From dd4426ded73d9a44a31e115f801da964eb2ee03c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 11 May 2026 20:31:17 +0800 Subject: [PATCH] fix: improve agent image intent handling --- app/(chat)/api/chat/route.ts | 1 + lib/ai/auto-agent-graph.ts | 258 ++++++++++++++++++--- lib/ai/conversation-action-resolver.ts | 11 +- lib/ai/image-edit-subagent.ts | 31 ++- lib/ai/image-generation-subagent.ts | 28 ++- lib/ai/prompt-optimizer.ts | 16 +- lib/ai/runtime/intent-arbiter.ts | 35 ++- lib/ai/runtime/task-boundary-classifier.ts | 28 ++- lib/ai/smart-analyzer.ts | 65 +++++- 9 files changed, 413 insertions(+), 60 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index aa7386471..99e344506 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -6463,6 +6463,7 @@ export async function POST(request: Request) { }) .filter(Boolean) as Array<{ role: "user" | "assistant"; content: string }>, context: effectiveAutoContextText || undefined, + preferredModelId: preferredAgentModelId, }).catch(() => optimizePrompt({ originalPrompt: promptSeed, diff --git a/lib/ai/auto-agent-graph.ts b/lib/ai/auto-agent-graph.ts index 1caca068c..8a4c982eb 100644 --- a/lib/ai/auto-agent-graph.ts +++ b/lib/ai/auto-agent-graph.ts @@ -284,6 +284,30 @@ function defaultToolForAction(action: AgentPlanNextAction): AgentProjectToolName } } +function candidateToolsForAction(action: AgentPlanNextAction): AgentProjectToolName[] { + switch (action) { + case "analyze_image": + return ["analyzeTargetImage", "analyzeImage"]; + case "edit_image": + return ["editTargetImage", "editImage"]; + case "generate_image": + return ["generateNewImage", "generateImage"]; + case "generate_video": + return ["generateVideo"]; + case "clarify": + case "respond": + default: + return ["respondDirect"]; + } +} + +function pickAvailableToolForAction( + action: AgentPlanNextAction, + availableNames: Set, +): AgentProjectToolName | undefined { + return candidateToolsForAction(action).find((name) => availableNames.has(name)); +} + function mapIntentToAction(intent: AnalysisResult["intent"]): AgentPlanNextAction { switch (intent) { case "vision": @@ -395,9 +419,26 @@ function sanitizePlannedToolCalls( }); } - if (sanitized.length > 0) return sanitized; + if (sanitized.length > 0) { + const hasPrimaryActionTool = sanitized.some((tool) => + candidateToolsForAction(fallbackAction).includes(tool.name), + ); + const fallbackTool = pickAvailableToolForAction(fallbackAction, availableNames); + if ( + !hasPrimaryActionTool && + fallbackTool && + fallbackAction !== "respond" && + fallbackAction !== "clarify" + ) { + sanitized.push({ + name: fallbackTool, + reason: "补齐本轮主执行工具,避免只解析目标图后停住。", + }); + } + return sanitized; + } - const fallbackTool = defaultToolForAction(fallbackAction); + const fallbackTool = pickAvailableToolForAction(fallbackAction, availableNames) || defaultToolForAction(fallbackAction); if ( (fallbackAction === "analyze_image" || fallbackAction === "edit_image") && options?.targetSource && @@ -698,6 +739,59 @@ function normalizeAspectRatio(value?: string): string | undefined { return allowed.has(raw) ? raw : undefined; } +function parseChineseCountToken(token?: string): number | undefined { + const normalized = String(token || "").trim().toLowerCase(); + if (!normalized) return undefined; + const numeric = Number(normalized); + if (Number.isFinite(numeric) && numeric >= 1) return Math.round(numeric); + const map: Record = { + 一: 1, + 二: 2, + 两: 2, + 三: 3, + 四: 4, + 五: 5, + 六: 6, + 七: 7, + 八: 8, + 九: 9, + 十: 10, + }; + return map[normalized]; +} + +function inferExplicitImageCountFromText(text: string): number | undefined { + const normalized = String(text || "").toLowerCase().replace(/\s+/g, ""); + const match = + normalized.match(/([1-9]\d?|一|二|两|三|四|五|六|七|八|九|十)张/) || + normalized.match(/([1-9]\d?|一|二|两|三|四|五|六|七|八|九|十)(?:个|组)?(?:版本|方案|变体)/); + const parsed = parseChineseCountToken(match?.[1]); + if (parsed && parsed >= 1) return Math.min(parsed, 12); + if (/多张/.test(normalized)) return 3; + return undefined; +} + +function inferExplicitAspectRatioFromText(text: string): string | undefined { + const normalized = String(text || "").toLowerCase().replace(/\s+/g, ""); + const explicitRatio = normalized.match(/(21:9|16:9|9:16|5:4|4:5|4:3|3:4|3:2|2:3|1:1)/)?.[1]; + if (explicitRatio) return explicitRatio; + if (/竖图|竖版|竖屏|手机壁纸|9比16/.test(normalized)) return "9:16"; + if (/横图|横版|横屏|宽屏|banner|16比9/.test(normalized)) return "16:9"; + if (/方图|方形|头像|logo|电商主图|1比1/.test(normalized)) return "1:1"; + return undefined; +} + +function extractExplicitImageParamsFromText( + text: string, +): Pick { + const output: Pick = {}; + const aspectRatio = inferExplicitAspectRatioFromText(text); + const imageCount = inferExplicitImageCountFromText(text); + if (aspectRatio) output.aspectRatio = aspectRatio; + if (imageCount) output.imageCount = imageCount; + return output; +} + function normalizeImageSize(value?: string): string | undefined { if (!value) return undefined; const normalized = value.trim().toUpperCase(); @@ -964,9 +1058,8 @@ function heuristicImageParams( if (analysis.intent === "image_edit") { params.imageCount = 1; } else { - const explicitCountMatch = normalized.match(/([1-9]\d?)张/); - const explicitCount = explicitCountMatch ? Number(explicitCountMatch[1]) : 0; - if (explicitCount >= 1) { + const explicitCount = inferExplicitImageCountFromText(text); + if (explicitCount && explicitCount >= 1) { params.imageCount = Math.min(explicitCount, 12); } else if (/多张|四张|4张|三张|3张/.test(normalized)) { params.imageCount = /四张|4张/.test(normalized) ? 4 : 3; @@ -977,7 +1070,10 @@ function heuristicImageParams( } } - if (/横图|横版|宽屏|封面|banner|海报/.test(normalized)) { + const explicitAspectRatio = inferExplicitAspectRatioFromText(text); + if (explicitAspectRatio) { + params.aspectRatio = explicitAspectRatio; + } else if (/横图|横版|宽屏|封面|banner|海报/.test(normalized)) { params.aspectRatio = "16:9"; } else if (/竖图|竖版|手机壁纸|全身|海报竖版/.test(normalized)) { params.aspectRatio = "9:16"; @@ -1313,6 +1409,59 @@ function sanitizeAgentPlan( }; } +async function refinePlanWithSkillRoute(params: { + text: string; + analysis: AnalysisResult; + plan: AgentExecutionPlan; + availableTools: AgentProjectToolDefinition[]; + skillRoute?: AgentSkillRouteResult; + hasImage: boolean; + hasCanvasSelection: boolean; + targetResolutionMode: AgentTargetResolutionMode; + mediaSource: "current_message" | "canvas_selection" | "history" | "none"; + selectedImageCount: number; + threadState: AgentThreadState | null; +}): Promise { + if (!params.skillRoute) return params.plan; + + const { refinePlanWithSpecialist } = await import("./specialists"); + let refined = params.plan; + for (const specialist of [params.skillRoute.primary, ...params.skillRoute.overlays]) { + refined = refinePlanWithSpecialist(specialist, { + text: params.text, + analysis: params.analysis, + plan: refined, + availableTools: params.availableTools, + hasImage: params.hasImage, + hasCanvasSelection: params.hasCanvasSelection, + targetResolutionMode: params.targetResolutionMode, + mediaSource: params.mediaSource, + selectedImageCount: params.selectedImageCount, + threadState: params.threadState, + }); + } + return refined; +} + +function shouldUseLightweightPlanner( + state: AutoAgentStateType, + analysis: AnalysisResult, +): boolean { + if (process.env.AGENT_FORCE_LLM_PLANNER === "true") return false; + if (analysis.needsClarification) return false; + if (analysis.confidence < 0.9) return false; + if (analysis.intent === "image_gen") return true; + if (analysis.intent === "video_gen") return true; + if ( + analysis.intent === "image_edit" && + state.targetResolutionMode !== "clarification" && + (state.hasImage || state.imageFromHistory || state.historyImageCount > 0 || state.selectedImageCount > 0) + ) { + return true; + } + return false; +} + async function buildExecutionPlanWithAgent( state: AutoAgentStateType, analysis: AnalysisResult, @@ -1328,6 +1477,28 @@ async function buildExecutionPlanWithAgent( availableTools, }); + if (shouldUseLightweightPlanner(state, analysis)) { + const refinedFallbackPlan = await refinePlanWithSkillRoute({ + text: state.text, + analysis, + plan: fallbackPlan, + availableTools, + skillRoute, + hasImage: state.hasImage, + hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0, + targetResolutionMode: state.targetResolutionMode, + mediaSource: state.mediaSource, + selectedImageCount: state.selectedImageCount, + threadState: state.threadState, + }); + console.log("[Auto Agent] Lightweight planner used:", { + intent: analysis.intent, + confidence: analysis.confidence, + nextAction: refinedFallbackPlan.nextAction, + }); + return refinedFallbackPlan; + } + try { const { getEnabledAiModelsWithProvider, @@ -1445,23 +1616,19 @@ async function buildExecutionPlanWithAgent( const parsed = parseJsonBlock>(text); let planned = sanitizeAgentPlan(parsed, fallbackPlan, availableTools); - if (skillRoute) { - const { refinePlanWithSpecialist } = await import("./specialists"); - for (const specialist of [skillRoute.primary, ...skillRoute.overlays]) { - planned = refinePlanWithSpecialist(specialist, { - text: state.text, - analysis, - plan: planned, - availableTools, - hasImage: state.hasImage, - hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0, - targetResolutionMode: state.targetResolutionMode, - mediaSource: state.mediaSource, - selectedImageCount: state.selectedImageCount, - threadState: state.threadState, - }); - } - } + planned = await refinePlanWithSkillRoute({ + text: state.text, + analysis, + plan: planned, + availableTools, + skillRoute, + hasImage: state.hasImage, + hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0, + targetResolutionMode: state.targetResolutionMode, + mediaSource: state.mediaSource, + selectedImageCount: state.selectedImageCount, + threadState: state.threadState, + }); console.log( `[Auto Agent] Planner model: ${plannerModelConfig.model.displayName} (${plannerModelConfig.provider.type})`, ); @@ -2017,6 +2184,18 @@ const shouldOptimize = (state: AutoAgentStateType) => { return state.execution.shouldOptimizePrompt ? "optimize" : "route"; }; +function shouldUseLlmPromptOptimizer(state: AutoAgentStateType): boolean { + if (!state.analysis || !state.execution?.shouldApplyImagePipeline) return false; + if (process.env.AGENT_LLM_PROMPT_OPTIMIZER_ENABLED === "false") return false; + if ( + state.analysis.intent === "image_gen" && + process.env.AGENT_IMAGE_GEN_LLM_OPTIMIZER_ENABLED !== "true" + ) { + return false; + } + return true; +} + const optimizeNode = async (state: AutoAgentStateType) => { if (!state.analysis || !state.execution) { return {}; @@ -2053,19 +2232,20 @@ const optimizeNode = async (state: AutoAgentStateType) => { return {}; } 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 optimizedFromLlm = shouldUseLlmPromptOptimizer(state) + ? await withTimeoutFallback> | null>( + optimizePromptWithLLM({ + originalPrompt: state.text, + intent: state.analysis.intent, + subIntent: state.analysis.subIntent, + historyMessages: state.historyMessages, + context: state.context || undefined, + preferredModelId: state.preferredModelId || undefined, + }), + AGENT_OPERATION_TIMEOUT_MS, + null, + ) + : null; const optimized = optimizedFromLlm || optimizePrompt({ @@ -2073,16 +2253,20 @@ const optimizeNode = async (state: AutoAgentStateType) => { intent: state.analysis.intent, subIntent: state.analysis.subIntent, }); + const heuristicParams = heuristicImageParams(state.text, state.analysis); + const explicitTextParams = extractExplicitImageParamsFromText(state.text); const mergedParams = sanitizeRecommendedImageParams( { + ...heuristicParams, ...optimized.recommendedImageParams, + ...explicitTextParams, negativePrompt: optimized.negativePrompt && optimized.negativePrompt.trim().length > 0 ? optimized.negativePrompt : optimized.recommendedImageParams?.negativePrompt, }, state.analysis.intent, - ) ?? heuristicImageParams(state.text, state.analysis); + ) ?? heuristicParams; return { optimizedPrompt: optimized.optimizedPrompt, diff --git a/lib/ai/conversation-action-resolver.ts b/lib/ai/conversation-action-resolver.ts index 8ef59e5bc..4686cca8b 100644 --- a/lib/ai/conversation-action-resolver.ts +++ b/lib/ai/conversation-action-resolver.ts @@ -139,6 +139,7 @@ export function resolveConversationAction(params: { confirmedPrompt?: string; hasImageInputForPromptCarryForward: boolean; }) { + const normalizedText = String(params.text || "").toLowerCase().replace(/\s+/g, ""); const requestedGrid = detectRequestedStoryboardGrid(params.text); const shouldContinuePendingGrid = Boolean(params.pendingGridConfirmation) && @@ -163,8 +164,16 @@ export function resolveConversationAction(params: { } satisfies ConversationActionResolution; } + const mentionsGenerationFollowup = mentionsImageGenerationFollowup(params.text); + const explicitFreshImageGeneration = + !/不要生图|先别生图|不需要生图|不要生成|别生成/.test(normalizedText) && + /生图|出图|生成.*(?:图|图片|海报|头像|插画|场景|主图|产品图)|画.*(?:图|图片|海报|头像|插画|小猫|猫咪|人物|角色)|做.*(?:图|图片|海报|头像|主图|产品图)/.test( + normalizedText, + ); const hasImageEditFollowup = params.imageWorkflowMemory?.lastIntent === "image_edit" && + !mentionsGenerationFollowup && + !explicitFreshImageGeneration && (mentionsImageEditFollowup(params.text) || Boolean(params.imageWorkflowMemory?.lastResultImageUrls?.length)); @@ -187,7 +196,7 @@ export function resolveConversationAction(params: { const hasImageGenerationFollowup = params.imageWorkflowMemory?.lastIntent === "image_gen" && - (mentionsImageGenerationFollowup(params.text) || promptCarryForwardRequested); + (mentionsGenerationFollowup || promptCarryForwardRequested); if (hasImageGenerationFollowup) { return { diff --git a/lib/ai/image-edit-subagent.ts b/lib/ai/image-edit-subagent.ts index ebc61f7fc..89f856731 100644 --- a/lib/ai/image-edit-subagent.ts +++ b/lib/ai/image-edit-subagent.ts @@ -62,6 +62,8 @@ export function buildImageEditSpecialistRoute(params: { planningHints, toolAllowlist: [ "respondDirect", + "resolveTargetImage", + "editTargetImage", "editImage", "removeBackground", "enhanceImage", @@ -71,6 +73,7 @@ export function buildImageEditSpecialistRoute(params: { "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", + "publishResultToCanvas", "publishToCanvas", ], }; @@ -111,7 +114,14 @@ export function refinePlanWithImageEditSubagent(params: { } let toolCalls = [...params.plan.toolCalls]; - if (hasTool(params.availableTools, "useLatestResultImage")) { + if (hasTool(params.availableTools, "resolveTargetImage")) { + toolCalls = upsertTool( + toolCalls, + "resolveTargetImage", + "image-edit 子代理优先绑定上一轮结果图作为连续修图目标", + { prepend: true } + ); + } else if (hasTool(params.availableTools, "useLatestResultImage")) { toolCalls = upsertTool( toolCalls, "useLatestResultImage", @@ -119,15 +129,26 @@ export function refinePlanWithImageEditSubagent(params: { { prepend: true } ); } - if (hasTool(params.availableTools, "editImage")) { + if (hasTool(params.availableTools, "editTargetImage")) { + toolCalls = upsertTool( + toolCalls, + "editTargetImage", + "image-edit 子代理要求优先走连续修图执行链路" + ); + } else if (hasTool(params.availableTools, "editImage")) { toolCalls = upsertTool( toolCalls, "editImage", - "image-edit 子代理要求优先走连续修图执行链路", - { prepend: true } + "image-edit 子代理要求优先走连续修图执行链路" ); } - if (hasTool(params.availableTools, "publishToCanvas")) { + if (hasTool(params.availableTools, "publishResultToCanvas")) { + toolCalls = upsertTool( + toolCalls, + "publishResultToCanvas", + "image-edit 子代理建议把修图结果同步回画布" + ); + } else if (hasTool(params.availableTools, "publishToCanvas")) { toolCalls = upsertTool( toolCalls, "publishToCanvas", diff --git a/lib/ai/image-generation-subagent.ts b/lib/ai/image-generation-subagent.ts index 7f046af70..3fcaa332b 100644 --- a/lib/ai/image-generation-subagent.ts +++ b/lib/ai/image-generation-subagent.ts @@ -85,10 +85,13 @@ export function buildImageGenerationSpecialistRoute(params: { planningHints, toolAllowlist: [ "respondDirect", + "resolveTargetImage", + "generateNewImage", "generateImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", + "publishResultToCanvas", "publishToCanvas", ], }; @@ -137,10 +140,29 @@ export function refinePlanWithImageGenerationSubagent(params: { } let toolCalls = [...params.plan.toolCalls]; - if (hasTool(params.availableTools, "generateImage")) { - toolCalls = [{ name: "generateImage", reason: "image-generation 子代理要求优先走连续生图执行链路" }]; + const targetResolverTools = toolCalls.filter((tool) => + ["resolveTargetImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage"].includes( + tool.name, + ), + ); + if (hasTool(params.availableTools, "generateNewImage")) { + toolCalls = [ + ...targetResolverTools, + { name: "generateNewImage", reason: "image-generation 子代理要求优先走连续生图执行链路" }, + ]; + } else if (hasTool(params.availableTools, "generateImage")) { + toolCalls = [ + ...targetResolverTools, + { name: "generateImage", reason: "image-generation 子代理要求优先走连续生图执行链路" }, + ]; } - if (hasTool(params.availableTools, "publishToCanvas")) { + if (hasTool(params.availableTools, "publishResultToCanvas")) { + toolCalls = upsertTool( + toolCalls, + "publishResultToCanvas", + "image-generation 子代理建议把连续生图结果同步到画布" + ); + } else if (hasTool(params.availableTools, "publishToCanvas")) { toolCalls = upsertTool( toolCalls, "publishToCanvas", diff --git a/lib/ai/prompt-optimizer.ts b/lib/ai/prompt-optimizer.ts index 679c41866..10b01aca0 100644 --- a/lib/ai/prompt-optimizer.ts +++ b/lib/ai/prompt-optimizer.ts @@ -579,8 +579,9 @@ export async function optimizePromptWithLLM(params: { subIntent?: string; historyMessages?: HistoryMessage[]; context?: string; + preferredModelId?: string; }): Promise { - const { originalPrompt, intent, subIntent, historyMessages, context } = params; + const { originalPrompt, intent, subIntent, historyMessages, context, preferredModelId } = params; console.log("[Prompt Optimizer LLM] Input:", { originalPrompt, @@ -611,6 +612,16 @@ export async function optimizePromptWithLLM(params: { const agentModel = await getAgentAiModelWithProvider(); const isReasoningCompatibleProvider = (providerType: string) => ["zhipu", "alibaba", "openai", "custom", "local", "volcano-multimodal", "volcano-vision"].includes(providerType); + const normalizedPreferredModelId = String(preferredModelId || "").trim(); + const preferredOptimizerModel = normalizedPreferredModelId + ? allModels.find( + (m) => + (m.model.id === normalizedPreferredModelId || m.model.modelId === normalizedPreferredModelId) && + m.model.isEnabled && + !m.model.isSubModel && + isReasoningCompatibleProvider(m.provider.type) + ) + : undefined; const usableAgentModel = agentModel && isReasoningCompatibleProvider(agentModel.provider.type) ? agentModel @@ -621,7 +632,7 @@ export async function optimizePromptWithLLM(params: { const qwenModel = allModels.find( (m) => m.provider.type === "alibaba" && m.model.supportsVision ); - const selectedOptimizerModel = usableAgentModel || zhipuModel || qwenModel; + const selectedOptimizerModel = preferredOptimizerModel || usableAgentModel || zhipuModel || qwenModel; const agentCodingModelId = selectedOptimizerModel && usableAgentModel && @@ -677,6 +688,7 @@ export async function optimizePromptWithLLM(params: { prompt: finalUserMessage, temperature: getCompatibleTextGenerationTemperature(selectedOptimizerModel, 0.2), maxTokens: 200, + abortSignal: AbortSignal.timeout(12_000), } as any); // 解析 JSON 响应 diff --git a/lib/ai/runtime/intent-arbiter.ts b/lib/ai/runtime/intent-arbiter.ts index a1e257222..5dff3053d 100644 --- a/lib/ai/runtime/intent-arbiter.ts +++ b/lib/ai/runtime/intent-arbiter.ts @@ -78,14 +78,42 @@ function isExplicitVideoGenerationRequest(text: string): boolean { function isExplicitImageGenerationRequest(text: string): boolean { const normalizedText = normalizeCompactText(text); if (!normalizedText || isLikelyAdvisoryOrPlanningRequest(text)) return false; - return /重新生成|再来一张|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( - normalizedText, + if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalizedText)) { + return false; + } + + const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)"; + const visualTarget = + "(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)"; + const createVisualTarget = new RegExp( + `(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`, + ); + const drawSubject = new RegExp( + `(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, + ); + const continueVariants = new RegExp( + `(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`, + ); + + return ( + /重新生成|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|生图|出图/.test( + normalizedText, + ) || + createVisualTarget.test(normalizedText) || + drawSubject.test(normalizedText) || + continueVariants.test(normalizedText) || + /参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test( + normalizedText, + ) || + /帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( + normalizedText, + ) ); } function isLikelyImageEditRequest(text: string): boolean { const normalizedText = String(text || "").toLowerCase(); - return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( + return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( normalizedText, ); } @@ -94,6 +122,7 @@ function inferDefaultImageEditSubIntent(text: string): string { const lowerText = String(text || "").toLowerCase(); if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background"; if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background"; + if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend"; if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer"; if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust"; if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint"; diff --git a/lib/ai/runtime/task-boundary-classifier.ts b/lib/ai/runtime/task-boundary-classifier.ts index 96f3b3f9e..a4dddf4a2 100644 --- a/lib/ai/runtime/task-boundary-classifier.ts +++ b/lib/ai/runtime/task-boundary-classifier.ts @@ -54,9 +54,33 @@ function isLikelyVideoExecutionRequest(text: string): boolean { function isLikelyImageExecutionRequest(text: string): boolean { const normalized = normalizeCompactText(text); if (!normalized) return false; + if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalized)) { + return false; + } - return /生图|生成图片|出图|画一张|帮我画|做图|做一张图|重新生成|再来一张|执行生成|开始生成|生成出来|生成一只|生成一个|生成一张|生成一幅|生成一版|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( - normalized, + const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)"; + const visualTarget = + "(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)"; + const createVisualTarget = new RegExp( + `(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`, + ); + const drawSubject = new RegExp( + `(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, + ); + const continueVariants = new RegExp( + `(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`, + ); + + return ( + /生图|生成图片|出图|画一张|帮我画|做图|做一张图|重新生成|再来一张|执行生成|开始生成|生成出来|生成一只|生成一个|生成一张|生成一幅|生成一版|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( + normalized, + ) || + createVisualTarget.test(normalized) || + drawSubject.test(normalized) || + continueVariants.test(normalized) || + /参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test( + normalized, + ) ); } diff --git a/lib/ai/smart-analyzer.ts b/lib/ai/smart-analyzer.ts index e3e735dd5..2825ee8f8 100644 --- a/lib/ai/smart-analyzer.ts +++ b/lib/ai/smart-analyzer.ts @@ -33,6 +33,7 @@ function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent const lowerText = text.toLowerCase(); if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background"; if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background"; + if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend"; if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer"; if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust"; if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint"; @@ -65,11 +66,11 @@ export function isExplicitPromptOptimizationRequest(text: string): boolean { if (alsoRequestsExecution) return false; const asksAnalysis = - /分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|写出|给我|输出|优化|改写/.test( + /分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|写|撰写|写出|给我|输出|优化|改写/.test( normalizedText, ); const asksPromptQuestion = - /提示词应该是什么|什么提示词|提示词是什么|怎么写提示词|如何写提示词|怎么描述|如何描述|怎么表达|如何表达|给我一版提示词|给我一套提示词|适合.*提示词|用于生图的提示词|生成这样的图片.*提示词/.test( + /提示词应该是什么|什么提示词|提示词是什么|怎么写提示词|如何写提示词|怎么描述|如何描述|怎么表达|如何表达|给我一版提示词|给我一套提示词|帮我写.*提示词|写一版.*提示词|适合.*提示词|用于生图.*提示词|生成这样的图片.*提示词/.test( normalizedText, ); const blocksExecution = @@ -95,17 +96,48 @@ function isExplicitVideoGenerationRequest(text: string): boolean { ); } -function isExplicitImageGenerationRequest(text: string): boolean { +export function isExplicitImageGenerationRequest(text: string): boolean { const normalizedText = normalizeCompactText(text); if (isLikelyAdvisoryOrPlanningRequest(text)) return false; - return /重新生成|再来一张|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( - normalizedText, + if (isExplicitPromptOptimizationRequest(text)) return false; + if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalizedText)) { + return false; + } + + const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)"; + const visualTarget = + "(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)"; + const createVisualTarget = new RegExp( + `(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`, + ); + const drawSubject = new RegExp( + `(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`, + ); + const continueVariants = new RegExp( + `(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`, + ); + const referenceGeneration = + /参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test( + normalizedText, + ); + + return ( + /重新生成|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|生图|出图/.test( + normalizedText, + ) || + createVisualTarget.test(normalizedText) || + drawSubject.test(normalizedText) || + continueVariants.test(normalizedText) || + referenceGeneration || + /帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test( + normalizedText, + ) ); } function isLikelyImageEditRequest(text: string): boolean { const normalizedText = text.toLowerCase(); - return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( + return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test( normalizedText, ); } @@ -1859,6 +1891,16 @@ export async function analyzeIntent(params: { * 🆕 现在返回低置信度,强制走LLM分析以获取自然对话回复 */ function quickKeywordMatch(text: string, hasImage: boolean): AnalysisResult { + if (isExplicitPromptOptimizationRequest(text)) { + return { + intent: hasImage ? "vision" : "chat", + confidence: 0.95, + conversationalReply: hasImage + ? "收到,我先分析这张图并整理可直接使用的优化提示词,不会直接生图。" + : "收到,我先按你的要求整理提示词,不直接执行生成。", + }; + } + if (isExplicitVideoGenerationRequest(text)) { return { intent: "video_gen", @@ -1876,6 +1918,15 @@ function quickKeywordMatch(text: string, hasImage: boolean): AnalysisResult { conversationalReply: hasImage ? "收到,我会结合参考图直接帮你生成。" : "好的,我来直接帮你生成这张图。", + }; + } + + if (hasImage && isLikelyImageEditRequest(text) && !isLikelyAdvisoryOrPlanningRequest(text)) { + return { + intent: "image_edit", + subIntent: inferDefaultImageEditSubIntent(text), + confidence: 0.92, + conversationalReply: "收到,我会按你的目标直接处理这张图。", }; } @@ -2306,7 +2357,7 @@ export function fallbackKeywordAnalysis( } // 图像拓展 - if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大/.test(lowerText)) { + if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版/.test(lowerText)) { return { intent: "image_edit", subIntent: "extend",