fix agent video routing failures
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-03-13 10:28:00 +08:00
parent b75dfa9295
commit 6f0e49bfb9
2 changed files with 126 additions and 8 deletions

View File

@ -9,6 +9,14 @@ import {
type AgentProjectToolDefinition, type AgentProjectToolDefinition,
type AgentProjectToolName, type AgentProjectToolName,
} from "@/lib/ai/tools-registry"; } from "@/lib/ai/tools-registry";
import {
runHumanConfirmationGate,
type HumanConfirmationGateDecision,
} from "@/lib/ai/runtime/middlewares/human-confirmation-gate";
import {
normalizeAgentThreadState,
type AgentThreadState,
} from "@/lib/ai/runtime/agent-thread-state";
import { import {
getAiModelAgentCodingModelId, getAiModelAgentCodingModelId,
isAiModelMultimodal, isAiModelMultimodal,
@ -140,6 +148,7 @@ export interface AutoAgentInput {
historyMessages?: HistoryMessage[]; historyMessages?: HistoryMessage[];
context?: string; context?: string;
preferredModelId?: string; preferredModelId?: string;
threadState?: AgentThreadState | null;
} }
export interface AutoAgentResult { export interface AutoAgentResult {
@ -151,6 +160,8 @@ export interface AutoAgentResult {
recommendedImageParams?: AgentRecommendedImageParams; recommendedImageParams?: AgentRecommendedImageParams;
videoPlan?: AgentVideoPlan; videoPlan?: AgentVideoPlan;
route?: RouteResult; route?: RouteResult;
threadState?: AgentThreadState | null;
workflowDecision?: HumanConfirmationGateDecision;
} }
const AutoAgentState = Annotation.Root({ const AutoAgentState = Annotation.Root({
@ -168,6 +179,7 @@ const AutoAgentState = Annotation.Root({
historyMessages: Annotation<HistoryMessage[]>, historyMessages: Annotation<HistoryMessage[]>,
context: Annotation<string | null>, context: Annotation<string | null>,
preferredModelId: Annotation<string | null>, preferredModelId: Annotation<string | null>,
threadState: Annotation<AgentThreadState | null>,
analysis: Annotation<AnalysisResult | null>, analysis: Annotation<AnalysisResult | null>,
availableTools: Annotation<AgentProjectToolDefinition[]>, availableTools: Annotation<AgentProjectToolDefinition[]>,
plan: Annotation<AgentExecutionPlan | null>, plan: Annotation<AgentExecutionPlan | null>,
@ -177,6 +189,7 @@ const AutoAgentState = Annotation.Root({
recommendedImageParams: Annotation<AgentRecommendedImageParams | null>, recommendedImageParams: Annotation<AgentRecommendedImageParams | null>,
videoPlan: Annotation<AgentVideoPlan | null>, videoPlan: Annotation<AgentVideoPlan | null>,
route: Annotation<RouteResult | null>, route: Annotation<RouteResult | null>,
workflowDecision: Annotation<HumanConfirmationGateDecision | null>,
}); });
type AutoAgentStateType = typeof AutoAgentState.State; type AutoAgentStateType = typeof AutoAgentState.State;
@ -425,6 +438,53 @@ function prepareExecutionFromPlan(params: {
}; };
} }
function buildWorkflowGateBypassPayload(
state: AutoAgentStateType,
decision: HumanConfirmationGateDecision,
) {
const analysis: AnalysisResult = {
intent: "chat",
confidence: 0.98,
needsClarification: decision.action === "awaiting_confirmation",
clarifyingQuestions:
decision.action === "awaiting_confirmation"
? ["请先确认或修改当前待确认流程,我再继续执行。"]
: [],
conversationalReply: decision.friendlyMessage,
};
const plan: AgentExecutionPlan = {
goal: createPlanGoalSummary(state.text, state.hasImage),
nextAction: "respond",
intent: "chat",
targetSource: state.targetResolutionMode,
preferredCapability: "respond",
responseMode: "direct_answer",
shouldUseHistoryImage: false,
shouldUseStructuredReply: false,
confidence: 0.98,
toolCalls: [
{
name: "respondDirect",
reason: "当前工作流处于人工确认门控阶段,先处理用户确认或修改意图。",
},
],
};
const execution = prepareExecutionFromPlan({
plan,
availableTools: [],
});
return {
analysis,
plan,
execution,
friendlyMessage: decision.friendlyMessage || null,
workflowDecision: decision,
};
}
function getEnvInt(name: string, fallback: number, min: number, max: number): number { function getEnvInt(name: string, fallback: number, min: number, max: number): number {
const raw = process.env[name]; const raw = process.env[name];
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN; const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
@ -1789,6 +1849,25 @@ const analyzeNode = async (state: AutoAgentStateType) => {
return { analysis }; return { analysis };
}; };
const confirmationGateNode = async (state: AutoAgentStateType) => {
const workflowDecision = runHumanConfirmationGate({
text: state.text,
threadState: state.threadState,
});
if (!workflowDecision.shouldBypassPlanner) {
return {
workflowDecision,
};
}
return buildWorkflowGateBypassPayload(state, workflowDecision);
};
const shouldBypassPlanner = (state: AutoAgentStateType) => {
return state.workflowDecision?.shouldBypassPlanner ? "route" : "analyze";
};
const planNode = async (state: AutoAgentStateType) => { const planNode = async (state: AutoAgentStateType) => {
if (!state.analysis) return {}; if (!state.analysis) return {};
const baseAvailableTools = await getEnabledAgentToolDefinitionsForContext({ const baseAvailableTools = await getEnabledAgentToolDefinitionsForContext({
@ -1960,14 +2039,19 @@ let compiledGraph: ReturnType<typeof buildGraph> | null = null;
function buildGraph() { function buildGraph() {
return new StateGraph(AutoAgentState) return new StateGraph(AutoAgentState)
.addNode("confirmationGate", confirmationGateNode)
.addNode("analyze", analyzeNode) .addNode("analyze", analyzeNode)
.addNode("plan", planNode) .addNode("planner", planNode)
.addNode("prepareExecution", prepareExecutionNode) .addNode("prepareExecution", prepareExecutionNode)
.addNode("optimize", optimizeNode) .addNode("optimize", optimizeNode)
.addNode("routeModel", routeNode) .addNode("routeModel", routeNode)
.addEdge(START, "analyze") .addEdge(START, "confirmationGate")
.addEdge("analyze", "plan") .addConditionalEdges("confirmationGate", shouldBypassPlanner, {
.addEdge("plan", "prepareExecution") analyze: "analyze",
route: "routeModel",
})
.addEdge("analyze", "planner")
.addEdge("planner", "prepareExecution")
.addConditionalEdges("prepareExecution", shouldOptimize, { .addConditionalEdges("prepareExecution", shouldOptimize, {
optimize: "optimize", optimize: "optimize",
route: "routeModel", route: "routeModel",
@ -2001,6 +2085,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
historyMessages: params.historyMessages ?? [], historyMessages: params.historyMessages ?? [],
context: params.context ?? null, context: params.context ?? null,
preferredModelId: params.preferredModelId ?? null, preferredModelId: params.preferredModelId ?? null,
threadState: normalizeAgentThreadState(params.threadState),
analysis: null, analysis: null,
availableTools: [], availableTools: [],
plan: null, plan: null,
@ -2010,6 +2095,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
recommendedImageParams: null, recommendedImageParams: null,
videoPlan: null, videoPlan: null,
route: null, route: null,
workflowDecision: null,
}); });
return { return {
@ -2021,5 +2107,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
recommendedImageParams: result.recommendedImageParams ?? undefined, recommendedImageParams: result.recommendedImageParams ?? undefined,
videoPlan: result.videoPlan ?? undefined, videoPlan: result.videoPlan ?? undefined,
route: result.route ?? undefined, route: result.route ?? undefined,
threadState: result.threadState ?? undefined,
workflowDecision: result.workflowDecision ?? undefined,
}; };
} }

View File

@ -74,7 +74,35 @@ async function selectBestSubModel(
const subModels = await getSubModelsByParentModelId(parentModelId); const subModels = await getSubModelsByParentModelId(parentModelId);
if (subModels.length === 0) { if (subModels.length === 0) {
throw new Error("没有可用的视频生成子模型"); const allModels = await getEnabledAiModelsWithProvider();
const directModel = allModels.find(
(item) =>
item.model.id === parentModelId &&
(item.provider.type === "volcano-video" || item.provider.type === "doubao-video"),
);
if (!directModel) {
throw new Error("没有可用的视频生成子模型");
}
const directCapabilities = directModel.model.capabilities as VideoModelCapabilities | undefined;
const supportedModes = getSupportedModes(directCapabilities, directModel.model.modelId);
if (!supportedModes.includes(mode)) {
throw new Error(`模型 ${directModel.model.displayName} 不支持 ${mode} 模式`);
}
return {
model: directModel,
capabilities:
directCapabilities ||
({
supportedModes,
supportedResolutions: ["480p", "720p", "1080p"],
supportedRatios: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"],
durationRange: { min: 2, max: 12 },
maxReferenceImages: 4,
} as VideoModelCapabilities),
};
} }
console.log(`[Smart Model Selection] 找到 ${subModels.length} 个子模型`); console.log(`[Smart Model Selection] 找到 ${subModels.length} 个子模型`);
@ -263,7 +291,7 @@ async function resolvePreferredVideoParentModelId(preferredModelRef?: string) {
const models = await getEnabledAiModelsWithProvider(); const models = await getEnabledAiModelsWithProvider();
const matched = models.find( const matched = models.find(
(item) => (item) =>
item.provider.type === "volcano-video" && (item.provider.type === "volcano-video" || item.provider.type === "doubao-video") &&
(item.model.id === normalizedRef || item.model.modelId === normalizedRef), (item.model.id === normalizedRef || item.model.modelId === normalizedRef),
); );
@ -289,12 +317,14 @@ export async function executeDirectVideoGeneration(
const fallbackParentModel = const fallbackParentModel =
allModels.find( allModels.find(
(item) => (item) =>
item.provider.type === "volcano-video" && (item.provider.type === "volcano-video" || item.provider.type === "doubao-video") &&
!item.model.isSubModel && !item.model.isSubModel &&
item.model.modelId === "volcano-video-master", item.model.modelId === "volcano-video-master",
) || ) ||
allModels.find( allModels.find(
(item) => item.provider.type === "volcano-video" && !item.model.isSubModel, (item) =>
(item.provider.type === "volcano-video" || item.provider.type === "doubao-video") &&
!item.model.isSubModel,
); );
const parentModelId = preferredParentModelId || fallbackParentModel?.model.id; const parentModelId = preferredParentModelId || fallbackParentModel?.model.id;