import type { AgentProjectToolDefinition, AgentProjectToolName, } from "@/lib/ai/tools-registry"; import type { AgentSpecialistContext, AgentSpecialistRoute, } from "@/lib/ai/specialists"; import { buildImageEditSpecialistRoute } from "@/lib/ai/image-edit-subagent"; import { buildImageGenerationSpecialistRoute } from "@/lib/ai/image-generation-subagent"; import { buildStoryboardSpecialistRoute } from "@/lib/video-agent/storyboard-subagent"; export interface AgentSkillRouteResult { primary: AgentSpecialistRoute; overlays: AgentSpecialistRoute[]; toolAllowlist: AgentProjectToolName[]; planningHints: string[]; reason: string; } function normalizeCompactText(text: string) { return String(text || "").toLowerCase().replace(/\s+/g, "").trim(); } function dedupeToolNames(toolNames: AgentProjectToolName[]) { return Array.from(new Set(toolNames)); } function isPureGreeting(text: string) { const normalized = normalizeCompactText(text); if (!normalized) return false; return /^(你好|您好|hello|hi|嗨|哈喽|在吗|早上好|中午好|下午好|晚上好|深夜好|谢谢|感谢|收到|好的|ok|okay|明白了|了解了)$/.test( normalized, ); } 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(), ); } function createRoute( route: AgentSpecialistRoute, toolAllowlist: AgentProjectToolName[], planningHints: string[], ): AgentSpecialistRoute { return { ...route, toolAllowlist: dedupeToolNames(toolAllowlist), planningHints, }; } export function routeAgentSkill( context: AgentSpecialistContext, ): AgentSkillRouteResult { const greetingTurn = isPureGreeting(context.text); const conversationRoute = createRoute( { domain: "conversation", specialistId: "conversation-coordinator", reason: greetingTurn ? "纯打招呼/寒暄" : "普通对话回合", planningHints: [], }, ["respondDirect"], [ "当前回合是纯对话,不要调用生图、生视频或修图工具。", "即使当前有图片,也只能先自然回复,不能直接执行。", ], ); const visionRoute = createRoute( { domain: "vision", specialistId: "vision-analyst", reason: "当前回合以看图分析为主", planningHints: [], }, [ "resolveTargetImage", "analyzeTargetImage", "analyzeImage", "respondDirect", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", ], [ "当前回合重点是识图、分析、解释或给建议,不要误切到生图。", "如有目标图,优先围绕目标图做分析,再给用户结论。", ], ); const imageEditRoute = createRoute( { domain: "image_edit", specialistId: "image-edit-specialist", reason: "当前回合是修图/抠图/增强类任务", planningHints: [], }, [ "resolveTargetImage", "editTargetImage", "editImage", "removeBackground", "enhanceImage", "extendImage", "resizeImage", "boxCutout", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", "publishResultToCanvas", "publishToCanvas", ], [ "当前回合必须收敛到修图链路,优先选择最具体的修图工具。", "除非用户明确要求生成新图,否则不要把修图误判成生图。", ], ); const imageGenerateRoute = createRoute( { domain: "image_generate", specialistId: "image-generate-specialist", reason: "当前回合是生图任务", planningHints: [], }, [ "resolveTargetImage", "generateNewImage", "generateImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", "publishResultToCanvas", "publishToCanvas", ], [ "当前回合是生成新图,聚焦生成链路,不要退回修图工具。", "如用户带参考图,仅把参考图作为风格/构图条件,不要误当纯分析。", ], ); const videoRoute = createRoute( { domain: "video_generate", specialistId: "video-specialist", reason: "当前回合是视频生成任务", planningHints: [], }, [ "generateVideo", "resolveTargetImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", ], [ "当前回合是视频任务,只规划视频生成相关动作。", "如果信息不足可以先澄清,但不要误切回图片生成。", ], ); const canvasOverlay = createRoute( { domain: "canvas", specialistId: "canvas-specialist", reason: "检测到画布上下文,需要优先绑定目标图并回传结果", planningHints: [], }, [ "resolveTargetImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage", "publishResultToCanvas", "publishToCanvas", ], [ "如果用户在无限画布中操作,优先绑定选中图或最近结果图。", "修图/生图结果优先同步回画布,避免只停留在聊天气泡。", ], ); let primary = conversationRoute; if (!greetingTurn) { switch (context.analysis.intent) { case "vision": primary = visionRoute; break; case "image_edit": primary = imageEditRoute; break; case "image_gen": primary = imageGenerateRoute; break; case "video_gen": primary = videoRoute; break; case "chat": default: primary = conversationRoute; break; } } const overlays = primary.domain === "conversation" ? [] : hasCanvasContext(context) || mentionsCanvas(context.text) ? [canvasOverlay] : []; const storyboardOverlay = buildStoryboardSpecialistRoute({ text: context.text, threadState: context.threadState, }); const imageEditOverlay = buildImageEditSpecialistRoute({ text: context.text, threadState: context.threadState, hasImage: context.hasImage, }); const imageGenerationOverlay = buildImageGenerationSpecialistRoute({ text: context.text, threadState: context.threadState, hasImage: context.hasImage, }); const mergedOverlays = [ ...overlays, ...(imageEditOverlay ? [imageEditOverlay] : []), ...(imageGenerationOverlay ? [imageGenerationOverlay] : []), ...(storyboardOverlay ? [storyboardOverlay] : []), ]; const toolAllowlist = dedupeToolNames([ ...(primary.toolAllowlist || []), ...mergedOverlays.flatMap((item) => item.toolAllowlist || []), ]); const planningHints = [ ...primary.planningHints, ...mergedOverlays.flatMap((item) => item.planningHints), ]; const reason = [primary.reason, ...mergedOverlays.map((item) => item.reason)] .filter(Boolean) .join(";"); return { primary, overlays: mergedOverlays, toolAllowlist, planningHints, reason, }; } export function filterToolsForSkillRoute( availableTools: AgentProjectToolDefinition[], route: AgentSkillRouteResult, ) { if (!route.toolAllowlist.length) return availableTools; const allowSet = new Set(route.toolAllowlist); return availableTools.filter((tool) => allowSet.has(tool.name)); }