feat(agent): enforce visual analysis and extend auto timeouts to 120s
This commit is contained in:
parent
55b2c836b4
commit
e5c4b3780c
@ -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<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 = 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) {
|
||||
|
||||
@ -105,7 +105,8 @@ async function withTimeoutFallback<T>(
|
||||
|
||||
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<AgentRecommendedImageParams>(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<Record<string, unknown>>(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<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 optimized =
|
||||
optimizedFromLlm ||
|
||||
optimizePrompt({
|
||||
originalPrompt: state.text,
|
||||
intent: state.analysis.intent,
|
||||
subIntent: state.analysis.subIntent,
|
||||
});
|
||||
const mergedParams = sanitizeRecommendedImageParams(
|
||||
{
|
||||
...optimized.recommendedImageParams,
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user