ai-chatbot/lib/ai/skill-router.ts
2026-06-24 00:17:03 +08:00

274 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
AgentProjectToolDefinition,
AgentProjectToolName,
} from "@/lib/ai/tools-registry";
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";
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 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",
"removeWatermark",
"enhanceImage",
"extendImage",
"resizeImage",
"angleEditImage",
"localEditImage",
"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"
? []
: hasCanvasAgentRoutingContext(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));
}