feat: pass canvas slot context to agent planner

This commit is contained in:
134 2026-06-23 14:03:02 +08:00
parent ac16dc5e64
commit de457b5dc3
6 changed files with 103 additions and 13 deletions

View File

@ -1480,8 +1480,12 @@ export async function POST(request: Request) {
mediaSource: requestResolvedMediaInput.source,
hasCanvasSelection:
Boolean(effectiveContextPayload?.selectedNodeIds?.length) ||
requestResolvedMediaInput.selectedImageUrls.length > 0,
requestResolvedMediaInput.selectedImageUrls.length > 0 ||
getSelectedSlots(requestCanvasAgentContext).length > 0,
selectedImageCount: requestResolvedMediaInput.selectedImageUrls.length,
canvasAgentContext: requestCanvasAgentContext,
selectedSlotCount: requestCanvasAgentContext?.slots.length || 0,
hasSelectedAgentSlot: getSelectedSlots(requestCanvasAgentContext).length > 0,
historyImageCount:
requestResolvedMediaInput.historyAssistantImageUrls.length +
requestResolvedMediaInput.historyUserImageUrls.length,

View File

@ -5,6 +5,7 @@ import { getCompatibleTextGenerationTemperature } from "@/lib/ai/model-compat";
import type { AnalysisResult } from "@/lib/ai/smart-analyzer";
import type { RouteResult } from "@/lib/ai/model-router";
import type { AgentSkillRouteResult } from "@/lib/ai/skill-router";
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
import {
getEnabledAgentToolDefinitionsForContext,
type AgentProjectToolDefinition,
@ -144,6 +145,9 @@ export interface AutoAgentInput {
mediaSource?: "current_message" | "canvas_selection" | "history" | "none";
hasCanvasSelection?: boolean;
selectedImageCount?: number;
canvasAgentContext?: CanvasAgentContext | null;
selectedSlotCount?: number;
hasSelectedAgentSlot?: boolean;
historyImageCount?: number;
currentUploadImageCount?: number;
historyMessages?: HistoryMessage[];
@ -175,6 +179,9 @@ const AutoAgentState = Annotation.Root({
mediaSource: Annotation<"current_message" | "canvas_selection" | "history" | "none">,
hasCanvasSelection: Annotation<boolean>,
selectedImageCount: Annotation<number>,
canvasAgentContext: Annotation<CanvasAgentContext | null>,
selectedSlotCount: Annotation<number>,
hasSelectedAgentSlot: Annotation<boolean>,
historyImageCount: Annotation<number>,
currentUploadImageCount: Annotation<number>,
historyMessages: Annotation<HistoryMessage[]>,
@ -1431,6 +1438,8 @@ async function refinePlanWithSkillRoute(params: {
targetResolutionMode: AgentTargetResolutionMode;
mediaSource: "current_message" | "canvas_selection" | "history" | "none";
selectedImageCount: number;
selectedSlotCount: number;
hasSelectedAgentSlot: boolean;
threadState: AgentThreadState | null;
}): Promise<AgentExecutionPlan> {
if (!params.skillRoute) return params.plan;
@ -1448,6 +1457,8 @@ async function refinePlanWithSkillRoute(params: {
targetResolutionMode: params.targetResolutionMode,
mediaSource: params.mediaSource,
selectedImageCount: params.selectedImageCount,
selectedSlotCount: params.selectedSlotCount,
hasSelectedAgentSlot: params.hasSelectedAgentSlot,
threadState: params.threadState,
});
}
@ -1500,6 +1511,8 @@ async function buildExecutionPlanWithAgent(
targetResolutionMode: state.targetResolutionMode,
mediaSource: state.mediaSource,
selectedImageCount: state.selectedImageCount,
selectedSlotCount: state.selectedSlotCount,
hasSelectedAgentSlot: state.hasSelectedAgentSlot,
threadState: state.threadState,
});
console.log("[Auto Agent] Lightweight planner used:", {
@ -1604,6 +1617,26 @@ async function buildExecutionPlanWithAgent(
`当前上传图数量:${state.currentUploadImageCount}`,
`历史图数量:${state.historyImageCount}`,
`画布选中图数量:${state.selectedImageCount}`,
`画布选中 Agent 槽位数量:${state.selectedSlotCount}`,
state.hasSelectedAgentSlot
? "画布中当前选中的空 Agent Slot 是有效生成目标;如果用户要求在这里生成/填充,不要因为没有图片而澄清。"
: "",
state.canvasAgentContext?.slots?.length
? `画布槽位摘要:${state.canvasAgentContext.slots
.slice(0, 6)
.map((slot) =>
[
slot.nodeId,
slot.slotType,
slot.label || "未命名槽位",
slot.isSelected ? "selected" : "",
slot.fillPolicy,
]
.filter(Boolean)
.join("/")
)
.join(";")}`
: "",
`基础分析:intent=${analysis.intent}; subIntent=${analysis.subIntent || "none"}; confidence=${analysis.confidence}`,
skillRoute
? `专家路由:${skillRoute.primary.specialistId}|原因:${skillRoute.reason}`
@ -1638,6 +1671,8 @@ async function buildExecutionPlanWithAgent(
targetResolutionMode: state.targetResolutionMode,
mediaSource: state.mediaSource,
selectedImageCount: state.selectedImageCount,
selectedSlotCount: state.selectedSlotCount,
hasSelectedAgentSlot: state.hasSelectedAgentSlot,
threadState: state.threadState,
});
console.log(
@ -2115,9 +2150,13 @@ const shouldBypassPlanner = (state: AutoAgentStateType) => {
const planNode = async (state: AutoAgentStateType) => {
if (!state.analysis) return {};
const hasCanvasOperationTarget =
state.hasCanvasSelection ||
state.selectedImageCount > 0 ||
state.hasSelectedAgentSlot;
const baseAvailableTools = await getEnabledAgentToolDefinitionsForContext({
hasImage: state.hasImage,
hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0,
hasCanvasSelection: hasCanvasOperationTarget,
hasHistoryImage: state.historyImageCount > 0 || state.imageFromHistory,
hasCurrentUpload: state.currentUploadImageCount > 0,
});
@ -2126,10 +2165,12 @@ const planNode = async (state: AutoAgentStateType) => {
text: state.text,
analysis: state.analysis,
hasImage: state.hasImage,
hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0,
hasCanvasSelection: hasCanvasOperationTarget,
targetResolutionMode: state.targetResolutionMode,
mediaSource: state.mediaSource,
selectedImageCount: state.selectedImageCount,
selectedSlotCount: state.selectedSlotCount,
hasSelectedAgentSlot: state.hasSelectedAgentSlot,
threadState: state.threadState,
});
const availableTools = filterToolsForSkillRoute(baseAvailableTools, skillRoute);
@ -2343,6 +2384,9 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
mediaSource: params.mediaSource ?? "none",
hasCanvasSelection: params.hasCanvasSelection ?? false,
selectedImageCount: params.selectedImageCount ?? 0,
canvasAgentContext: params.canvasAgentContext ?? null,
selectedSlotCount: params.selectedSlotCount ?? 0,
hasSelectedAgentSlot: params.hasSelectedAgentSlot ?? false,
historyImageCount: params.historyImageCount ?? 0,
currentUploadImageCount: params.currentUploadImageCount ?? 0,
historyMessages: params.historyMessages ?? [],

View File

@ -0,0 +1,23 @@
import type { AgentTargetResolutionMode } from "@/lib/ai/auto-agent-graph";
export type CanvasAgentRoutingContext = {
hasCanvasSelection?: boolean;
selectedImageCount?: number;
selectedSlotCount?: number;
hasSelectedAgentSlot?: boolean;
mediaSource?: "current_message" | "canvas_selection" | "history" | "none";
targetResolutionMode?: AgentTargetResolutionMode;
};
export function hasCanvasAgentRoutingContext(
context: CanvasAgentRoutingContext
) {
return (
Boolean(context.hasCanvasSelection) ||
(context.selectedImageCount || 0) > 0 ||
(context.selectedSlotCount || 0) > 0 ||
Boolean(context.hasSelectedAgentSlot) ||
context.mediaSource === "canvas_selection" ||
context.targetResolutionMode === "explicit_selection"
);
}

View File

@ -6,6 +6,7 @@ import type {
AgentSpecialistContext,
AgentSpecialistRoute,
} from "@/lib/ai/specialists";
import { hasCanvasAgentRoutingContext } from "@/lib/ai/canvas-agent-routing";
import { buildImageEditSpecialistRoute } from "@/lib/ai/image-edit-subagent";
import { buildImageGenerationSpecialistRoute } from "@/lib/ai/image-generation-subagent";
import { buildStoryboardSpecialistRoute } from "@/lib/video-agent/storyboard-subagent";
@ -34,15 +35,6 @@ function isPureGreeting(text: string) {
);
}
function hasCanvasContext(context: AgentSpecialistContext) {
return (
context.hasCanvasSelection ||
(context.selectedImageCount || 0) > 0 ||
context.mediaSource === "canvas_selection" ||
context.targetResolutionMode === "explicit_selection"
);
}
function mentionsCanvas(text: string) {
return /画布|发布到画布|同步到画布|发送到画布|放到画布/.test(
String(text || "").toLowerCase(),
@ -220,7 +212,7 @@ export function routeAgentSkill(
const overlays =
primary.domain === "conversation"
? []
: hasCanvasContext(context) || mentionsCanvas(context.text)
: hasCanvasAgentRoutingContext(context) || mentionsCanvas(context.text)
? [canvasOverlay]
: [];

View File

@ -54,6 +54,8 @@ export interface AgentSpecialistContext {
targetResolutionMode: AgentTargetResolutionMode;
mediaSource?: "current_message" | "canvas_selection" | "history" | "none";
selectedImageCount?: number;
selectedSlotCount?: number;
hasSelectedAgentSlot?: boolean;
threadState?: AgentThreadState | null;
}

View File

@ -26,6 +26,7 @@ import {
getCanvasToolbarItem,
} from "@/lib/canvas-toolbar-config";
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
import { hasCanvasAgentRoutingContext } from "@/lib/ai/canvas-agent-routing";
import {
CANVAS_LOCAL_EDIT_REGION_ANNOTATION_SOURCE,
CANVAS_NODE_NOTE_ANNOTATION_SOURCE,
@ -227,6 +228,30 @@ test.describe("canvas chat pure helpers", () => {
).toBe(true);
});
test("treats selected agent slots as canvas routing context", () => {
expect(
hasCanvasAgentRoutingContext({
hasCanvasSelection: false,
targetResolutionMode: "none",
mediaSource: "none",
selectedImageCount: 0,
selectedSlotCount: 1,
hasSelectedAgentSlot: true,
})
).toBe(true);
expect(
hasCanvasAgentRoutingContext({
hasCanvasSelection: false,
targetResolutionMode: "none",
mediaSource: "none",
selectedImageCount: 0,
selectedSlotCount: 0,
hasSelectedAgentSlot: false,
})
).toBe(false);
});
test("normalizes model-planned empty slot creation", () => {
const outcome = normalizeCanvasAgentModelPlannerDecision({
decision: {