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 AgentProjectToolName,
} 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 {
getAiModelAgentCodingModelId,
isAiModelMultimodal,
@ -140,6 +148,7 @@ export interface AutoAgentInput {
historyMessages?: HistoryMessage[];
context?: string;
preferredModelId?: string;
threadState?: AgentThreadState | null;
}
export interface AutoAgentResult {
@ -151,6 +160,8 @@ export interface AutoAgentResult {
recommendedImageParams?: AgentRecommendedImageParams;
videoPlan?: AgentVideoPlan;
route?: RouteResult;
threadState?: AgentThreadState | null;
workflowDecision?: HumanConfirmationGateDecision;
}
const AutoAgentState = Annotation.Root({
@ -168,6 +179,7 @@ const AutoAgentState = Annotation.Root({
historyMessages: Annotation<HistoryMessage[]>,
context: Annotation<string | null>,
preferredModelId: Annotation<string | null>,
threadState: Annotation<AgentThreadState | null>,
analysis: Annotation<AnalysisResult | null>,
availableTools: Annotation<AgentProjectToolDefinition[]>,
plan: Annotation<AgentExecutionPlan | null>,
@ -177,6 +189,7 @@ const AutoAgentState = Annotation.Root({
recommendedImageParams: Annotation<AgentRecommendedImageParams | null>,
videoPlan: Annotation<AgentVideoPlan | null>,
route: Annotation<RouteResult | null>,
workflowDecision: Annotation<HumanConfirmationGateDecision | null>,
});
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 {
const raw = process.env[name];
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
@ -1789,6 +1849,25 @@ const analyzeNode = async (state: AutoAgentStateType) => {
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) => {
if (!state.analysis) return {};
const baseAvailableTools = await getEnabledAgentToolDefinitionsForContext({
@ -1960,14 +2039,19 @@ let compiledGraph: ReturnType<typeof buildGraph> | null = null;
function buildGraph() {
return new StateGraph(AutoAgentState)
.addNode("confirmationGate", confirmationGateNode)
.addNode("analyze", analyzeNode)
.addNode("plan", planNode)
.addNode("planner", planNode)
.addNode("prepareExecution", prepareExecutionNode)
.addNode("optimize", optimizeNode)
.addNode("routeModel", routeNode)
.addEdge(START, "analyze")
.addEdge("analyze", "plan")
.addEdge("plan", "prepareExecution")
.addEdge(START, "confirmationGate")
.addConditionalEdges("confirmationGate", shouldBypassPlanner, {
analyze: "analyze",
route: "routeModel",
})
.addEdge("analyze", "planner")
.addEdge("planner", "prepareExecution")
.addConditionalEdges("prepareExecution", shouldOptimize, {
optimize: "optimize",
route: "routeModel",
@ -2001,6 +2085,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
historyMessages: params.historyMessages ?? [],
context: params.context ?? null,
preferredModelId: params.preferredModelId ?? null,
threadState: normalizeAgentThreadState(params.threadState),
analysis: null,
availableTools: [],
plan: null,
@ -2010,6 +2095,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
recommendedImageParams: null,
videoPlan: null,
route: null,
workflowDecision: null,
});
return {
@ -2021,5 +2107,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
recommendedImageParams: result.recommendedImageParams ?? undefined,
videoPlan: result.videoPlan ?? 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);
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} 个子模型`);
@ -263,7 +291,7 @@ async function resolvePreferredVideoParentModelId(preferredModelRef?: string) {
const models = await getEnabledAiModelsWithProvider();
const matched = models.find(
(item) =>
item.provider.type === "volcano-video" &&
(item.provider.type === "volcano-video" || item.provider.type === "doubao-video") &&
(item.model.id === normalizedRef || item.model.modelId === normalizedRef),
);
@ -289,12 +317,14 @@ export async function executeDirectVideoGeneration(
const fallbackParentModel =
allModels.find(
(item) =>
item.provider.type === "volcano-video" &&
(item.provider.type === "volcano-video" || item.provider.type === "doubao-video") &&
!item.model.isSubModel &&
item.model.modelId === "volcano-video-master",
) ||
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;