fix: improve agent image intent handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-11 20:31:17 +08:00
parent 3ebe7db37d
commit dd4426ded7
9 changed files with 413 additions and 60 deletions

View File

@ -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,

View File

@ -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>,
): 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<string, number> = {
一: 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<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> {
const output: Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> = {};
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<AgentExecutionPlan> {
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<Record<string, unknown>>(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<ReturnType<typeof optimizePromptWithLLM>> | 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<Awaited<ReturnType<typeof optimizePromptWithLLM>> | 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,

View File

@ -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 {

View File

@ -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",

View File

@ -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",

View File

@ -579,8 +579,9 @@ export async function optimizePromptWithLLM(params: {
subIntent?: string;
historyMessages?: HistoryMessage[];
context?: string;
preferredModelId?: string;
}): Promise<OptimizeResult> {
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 响应

View File

@ -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";

View File

@ -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,
)
);
}

View File

@ -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",