fix(agent): tolerate non-json analyzer replies and cap graph step latency
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-02-19 02:59:50 +08:00
parent 794ab44fe8
commit ee2026a973
2 changed files with 71 additions and 3 deletions

View File

@ -85,6 +85,24 @@ function compactText(text: string, max = 48) {
return `${normalized.slice(0, max)}...`;
}
async function withTimeoutFallback<T>(
promise: Promise<T>,
timeoutMs: number,
fallbackValue: T,
): Promise<T> {
let timeoutId: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<T>((resolve) => {
timeoutId = setTimeout(() => resolve(fallbackValue), timeoutMs);
}),
]);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
const AGENT_VISION_MAX_IMAGES = 4;
const AGENT_VISION_MAX_SIDE = 500;
const AGENT_VISION_FETCH_TIMEOUT_MS = 10_000;
@ -484,6 +502,7 @@ function buildStrategicFriendlyMessage(
state: AutoAgentStateType,
): Promise<string | null> {
const fallbackQuestion = analysis.clarifyingQuestions?.[0];
const existingReply = analysis.conversationalReply?.trim();
if (analysis.needsClarification) {
return Promise.resolve(
@ -491,7 +510,21 @@ function buildStrategicFriendlyMessage(
);
}
return generateAgentConversation(analysis, state);
if (existingReply) {
const isStructuredReply =
/###\s*需求理解/.test(existingReply) ||
/###\s*图片分析/.test(existingReply) ||
/###\s*执行计划/.test(existingReply);
if (isStructuredReply || existingReply.length >= 24) {
return Promise.resolve(existingReply);
}
}
return withTimeoutFallback(
generateAgentConversation(analysis, state),
12000,
existingReply || null,
);
}
type EnabledModelWithProvider = Awaited<
@ -694,6 +727,7 @@ async function generateAgentConversation(
],
temperature: 0.2,
maxTokens: 360,
abortSignal: AbortSignal.timeout(12000),
} as any);
visualAnalysis = visionText.trim();
@ -746,6 +780,7 @@ async function generateAgentConversation(
prompt,
temperature: 0.35,
maxTokens: 520,
abortSignal: AbortSignal.timeout(12000),
} as any);
const cleaned = text.trim();
@ -819,6 +854,7 @@ async function recommendImageParamsWithAgent(
prompt: requestText,
temperature: 0.15,
maxTokens: 220,
abortSignal: AbortSignal.timeout(10000),
} as any);
const parsed = parseJsonBlock<AgentRecommendedImageParams>(text);
@ -903,6 +939,7 @@ async function recommendVideoPlanWithAgent(
prompt,
temperature: 0.2,
maxTokens: 720,
abortSignal: AbortSignal.timeout(12000),
} as any);
const parsed = parseJsonBlock<Record<string, unknown>>(text);
@ -974,6 +1011,7 @@ async function refineRouteWithAgent(
${candidatesText}`,
temperature: 0.1,
maxTokens: 120,
abortSignal: AbortSignal.timeout(8000),
} as any);
const parsed = parseJsonBlock<{ modelId?: string; reason?: string }>(text);

View File

@ -373,7 +373,21 @@ function parseAnalysisResultFromModelText(
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
console.warn("[Smart Analyzer] Model response missing JSON:", responseText.slice(0, 240));
return null;
const fallback = fallbackKeywordAnalysis(userText, hasImage);
const conversationalReply = responseText.trim();
if (!conversationalReply) {
return null;
}
return normalizeIntentForImageContext(
{
...fallback,
confidence: Math.max(fallback.confidence, 0.82),
needsClarification: false,
conversationalReply,
},
userText,
hasImage,
);
}
try {
@ -386,7 +400,21 @@ function parseAnalysisResultFromModelText(
"raw=",
jsonMatch[0].slice(0, 240),
);
return null;
const fallback = fallbackKeywordAnalysis(userText, hasImage);
const conversationalReply = responseText.trim();
if (!conversationalReply) {
return null;
}
return normalizeIntentForImageContext(
{
...fallback,
confidence: Math.max(fallback.confidence, 0.8),
needsClarification: false,
conversationalReply,
},
userText,
hasImage,
);
}
}
@ -902,6 +930,7 @@ ${historyText}
],
temperature: 0.1,
maxTokens: 360,
abortSignal: AbortSignal.timeout(12000),
}
: {
model,
@ -909,6 +938,7 @@ ${historyText}
prompt: userMessage,
temperature: 0.1,
maxTokens: 300,
abortSignal: AbortSignal.timeout(10000),
};
const { text: responseText } = await generateText(requestPayload as any);