153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
import "server-only";
|
||
|
||
import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
|
||
import type { AgentExecutionPlan, AgentPlannedToolCall } from "@/lib/ai/auto-agent-graph";
|
||
import type { AgentSpecialistRoute } from "@/lib/ai/specialists";
|
||
import type { AgentProjectToolDefinition } from "@/lib/ai/tools-registry";
|
||
import { getStoryboardPreferencePlanningHints } from "@/lib/video-agent/storyboard-preference-memory";
|
||
|
||
function normalizeCompactText(text: string) {
|
||
return String(text || "").toLowerCase().replace(/\s+/g, "").trim();
|
||
}
|
||
|
||
export function mentionsStoryboardConcept(text: string): boolean {
|
||
const normalized = normalizeCompactText(text);
|
||
if (!normalized) return false;
|
||
return /分镜|镜头图|镜头草案|镜头预览|storyboard|宫格|四宫格|六宫格|九宫格|拼图视频|分镜视频|高清图/.test(
|
||
normalized
|
||
);
|
||
}
|
||
|
||
export function hasStoryboardWorkflow(threadState?: AgentThreadState | null): boolean {
|
||
return threadState?.workflowState?.type === "storyboard_video";
|
||
}
|
||
|
||
export function buildStoryboardSpecialistRoute(params: {
|
||
text: string;
|
||
threadState?: AgentThreadState | null;
|
||
}): AgentSpecialistRoute | null {
|
||
const hasWorkflow = hasStoryboardWorkflow(params.threadState);
|
||
const mentionsStoryboard = mentionsStoryboardConcept(params.text);
|
||
if (!hasWorkflow && !mentionsStoryboard) return null;
|
||
|
||
const workflowSummary = params.threadState?.workflowState?.summary;
|
||
const workflowStatus = params.threadState?.workflowState?.status;
|
||
const hasPendingConfirmation = workflowStatus === "awaiting_user_confirmation";
|
||
const preferenceHints = getStoryboardPreferencePlanningHints(params.threadState);
|
||
|
||
const planningHints = [
|
||
hasWorkflow
|
||
? "当前回合与一个 storyboard workflow 强相关,优先理解为分镜流程里的确认、修改、重生或继续执行。"
|
||
: "当前回合明显在处理分镜/宫格任务,不要按普通单图生图或普通视频问答理解。",
|
||
"如果用户提到第N镜头、最后一格、倒数第二张、高清图重生,优先解释为对 storyboard 的局部修改。",
|
||
hasPendingConfirmation
|
||
? "当前任务仍处于待确认阶段,除非用户明确确认,否则不要推进到视频执行。"
|
||
: "如果用户目标是先做分镜图或宫格图,优先保证输出适合后续切片与连续镜头生成。",
|
||
...preferenceHints,
|
||
workflowSummary ? `当前 workflow 摘要:${workflowSummary}` : "",
|
||
].filter(Boolean);
|
||
|
||
return {
|
||
domain: "storyboard",
|
||
specialistId: "storyboard-subagent",
|
||
reason: hasWorkflow ? "当前回合命中 storyboard 工作流" : "当前回合命中分镜/宫格任务",
|
||
planningHints,
|
||
toolAllowlist: [
|
||
"respondDirect",
|
||
"generateImage",
|
||
"generateVideo",
|
||
"useSelectedCanvasImage",
|
||
"useLatestResultImage",
|
||
"useOriginalUploadImage",
|
||
"publishToCanvas",
|
||
],
|
||
};
|
||
}
|
||
|
||
function hasTool(
|
||
availableTools: AgentProjectToolDefinition[],
|
||
name: AgentPlannedToolCall["name"],
|
||
) {
|
||
return availableTools.some((tool) => tool.name === name);
|
||
}
|
||
|
||
function upsertTool(
|
||
toolCalls: AgentPlannedToolCall[],
|
||
name: AgentPlannedToolCall["name"],
|
||
reason: string,
|
||
options?: { prepend?: boolean },
|
||
) {
|
||
const existing = toolCalls.find((tool) => tool.name === name);
|
||
if (existing) return toolCalls;
|
||
const nextTool = { name, reason } satisfies AgentPlannedToolCall;
|
||
return options?.prepend ? [nextTool, ...toolCalls] : [...toolCalls, nextTool];
|
||
}
|
||
|
||
export function refinePlanWithStoryboardSubagent(params: {
|
||
text: string;
|
||
plan: AgentExecutionPlan;
|
||
availableTools: AgentProjectToolDefinition[];
|
||
threadState?: AgentThreadState | null;
|
||
}): AgentExecutionPlan {
|
||
const { text, plan, availableTools, threadState } = params;
|
||
const normalized = normalizeCompactText(text);
|
||
const hasWorkflow = hasStoryboardWorkflow(threadState);
|
||
const status = threadState?.workflowState?.status;
|
||
let toolCalls = [...plan.toolCalls];
|
||
|
||
const wantsGridImage =
|
||
/四宫格|六宫格|九宫格|宫格图|分镜图|镜头图|storyboard/.test(normalized);
|
||
const wantsStoryboardVideo =
|
||
/分镜视频|按分镜生成视频|按这个分镜生成|图生视频|镜头视频/.test(normalized);
|
||
|
||
if (hasWorkflow && status === "awaiting_user_confirmation") {
|
||
if (hasTool(availableTools, "respondDirect")) {
|
||
toolCalls = [{ name: "respondDirect", reason: "storyboard 子代理要求先处理确认/修改,不直接切换普通工具链" }];
|
||
}
|
||
return {
|
||
...plan,
|
||
nextAction: "respond",
|
||
intent: "chat",
|
||
responseMode: "direct_answer",
|
||
preferredCapability: "respond",
|
||
shouldUseStructuredReply: false,
|
||
toolCalls,
|
||
};
|
||
}
|
||
|
||
if (wantsGridImage && plan.nextAction === "generate_image") {
|
||
if (hasTool(availableTools, "publishToCanvas")) {
|
||
toolCalls = upsertTool(
|
||
toolCalls,
|
||
"publishToCanvas",
|
||
"storyboard 子代理要求把分镜图结果同步到画布,便于后续切片和确认"
|
||
);
|
||
}
|
||
return {
|
||
...plan,
|
||
toolCalls,
|
||
shouldUseStructuredReply: true,
|
||
preferredCapability: "generate_image",
|
||
};
|
||
}
|
||
|
||
if ((wantsStoryboardVideo || hasWorkflow) && plan.nextAction === "generate_video") {
|
||
if (hasTool(availableTools, "useLatestResultImage")) {
|
||
toolCalls = upsertTool(
|
||
toolCalls,
|
||
"useLatestResultImage",
|
||
"storyboard 子代理优先绑定最近的分镜图/镜头预览作为视频输入",
|
||
{ prepend: true }
|
||
);
|
||
}
|
||
return {
|
||
...plan,
|
||
toolCalls,
|
||
shouldUseStructuredReply: true,
|
||
preferredCapability: "generate_video",
|
||
};
|
||
}
|
||
|
||
return plan;
|
||
}
|