ai-chatbot/components/canvas/chat-panel/canvas-agent-stream.ts

366 lines
10 KiB
TypeScript

import type { CanvasViewOperationPlan } from "@/types/canvas/agent-protocol";
export type CanvasMediaStreamPayload = {
messageId?: string;
imageUrls?: string[];
videoUrls?: string[];
prompt?: string;
sourceImageUrls?: string[];
agentPresetId?: string;
agentPresetName?: string;
agentCategoryId?: string;
agentCategoryName?: string;
agentExecutionSnapshotId?: string;
mediaItemIds?: string[];
handledCanvasMediaItemIds?: string[];
};
export type CanvasOperationStreamPayload = {
projectId?: string;
operationId?: string;
operation?: string;
status?: "applied" | "failed" | "shadowed";
createdNodeIds?: string[];
createdNodeIdsByMediaItemId?: Record<string, string>;
updatedNodeIds?: string[];
createdEdgeIds?: string[];
canvasRevision?: string;
anchorNodeId?: string;
handledCanvasMediaItemIds?: string[];
mediaItemIds?: string[];
errorCode?: string;
errorMessage?: string;
fallbackToCanvasMedia?: boolean;
message?: string;
};
export type CanvasViewOperationStreamPayload = CanvasViewOperationPlan;
export function normalizeCanvasStreamIdList(value: unknown) {
return Array.isArray(value)
? value.map((id) => String(id || "").trim()).filter(Boolean)
: [];
}
export function normalizeCanvasAgentClientBoolean(
value: unknown,
fallback: boolean
) {
if (typeof value !== "string") return fallback;
const normalized = value.trim().toLowerCase();
if (!normalized) return fallback;
if (["1", "true", "yes", "on", "enabled"].includes(normalized)) return true;
if (["0", "false", "no", "off", "disabled"].includes(normalized)) return false;
return fallback;
}
export function isCanvasAgentViewOperationClientEnabled(value: unknown) {
return normalizeCanvasAgentClientBoolean(value, true);
}
export function getCanvasOperationHandledMediaItemIds(
payload: CanvasOperationStreamPayload
) {
if (payload.status === "failed" || payload.status === "shadowed") return [];
return normalizeCanvasStreamIdList(payload.handledCanvasMediaItemIds);
}
export function getCanvasMediaPayloadHandledItemIds(payload: CanvasMediaStreamPayload) {
return normalizeCanvasStreamIdList(payload.handledCanvasMediaItemIds);
}
export function shouldSkipCanvasMediaPayloadForHandledItems(params: {
payload: CanvasMediaStreamPayload;
handledCanvasMediaItemIds: ReadonlySet<string>;
}) {
const mediaItemIds = normalizeCanvasStreamIdList(params.payload.mediaItemIds);
return (
mediaItemIds.length > 0 &&
mediaItemIds.every((id) => params.handledCanvasMediaItemIds.has(id))
);
}
function normalizeCanvasStreamMediaUrls(value: unknown) {
return Array.isArray(value)
? value
.map((url) => String(url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
: [];
}
export function filterCanvasMediaPayloadForUnhandledItems(params: {
payload: CanvasMediaStreamPayload;
handledCanvasMediaItemIds: ReadonlySet<string>;
}): CanvasMediaStreamPayload {
const mediaItemIds = normalizeCanvasStreamIdList(params.payload.mediaItemIds);
if (mediaItemIds.length === 0 || params.handledCanvasMediaItemIds.size === 0) {
return params.payload;
}
const imageUrls = normalizeCanvasStreamMediaUrls(params.payload.imageUrls);
const videoUrls = normalizeCanvasStreamMediaUrls(params.payload.videoUrls);
const mediaItems = [
...imageUrls.map((url) => ({ type: "image" as const, url })),
...videoUrls.map((url) => ({ type: "video" as const, url })),
];
if (mediaItems.length === 0 || mediaItems.length !== mediaItemIds.length) {
return params.payload;
}
const unhandledItems = mediaItems
.map((item, index) => ({ ...item, mediaItemId: mediaItemIds[index]! }))
.filter((item) => !params.handledCanvasMediaItemIds.has(item.mediaItemId));
return {
...params.payload,
imageUrls: unhandledItems
.filter((item) => item.type === "image")
.map((item) => item.url),
videoUrls: unhandledItems
.filter((item) => item.type === "video")
.map((item) => item.url),
mediaItemIds: unhandledItems.map((item) => item.mediaItemId),
};
}
export function shouldRetryCanvasViewOperationAfterRefresh(
payload: CanvasViewOperationStreamPayload,
currentProjectId?: string
) {
const operation = String(payload?.operation || "").trim();
if (!operation) return false;
const targetProjectId = String(payload?.projectId || "").trim();
const normalizedCurrentProjectId = String(currentProjectId || "").trim();
if (
targetProjectId &&
normalizedCurrentProjectId &&
targetProjectId !== normalizedCurrentProjectId
) {
return false;
}
if (normalizeCanvasStreamIdList(payload.nodeIds).length > 0) return true;
if (String(payload.anchorNodeId || "").trim()) return true;
return (
operation === "focus_node" ||
operation === "focus_nodes" ||
operation === "focus_selection" ||
operation === "focus_latest_result" ||
operation === "fit_all"
);
}
export type ImageGenerationStatusStreamPayload = {
phase?: "queued" | "processing" | "finalizing" | "failed" | "completed";
providerType?: string;
modelName?: string;
imageCount?: number;
queuePosition?: number;
sameModelRunningCount?: number;
concurrencyLimit?: number;
estimatedWaitMs?: number;
};
export type AgentTargetLockRefPreview = {
url: string;
title?: string;
nodeId?: string;
assetId?: string;
source:
| "current_message"
| "canvas_selection"
| "canvas_context"
| "history_user_upload"
| "history_assistant_result"
| "working_memory";
role: "upload" | "selected" | "reference" | "history_result" | "working_memory";
isSelected?: boolean;
positionLabel?: string;
};
export type AgentTargetLockStreamPayload = {
stage?: "locked" | "clarify" | "unresolved";
resolutionMode?:
| "explicit_selection"
| "current_upload"
| "text_reference"
| "working_memory"
| "latest_result"
| "clarification"
| "none";
confidence?: number;
reason?: string;
needsClarification?: boolean;
clarificationMessage?: string;
targetCount?: number;
candidateCount?: number;
targets?: AgentTargetLockRefPreview[];
candidates?: AgentTargetLockRefPreview[];
};
export type AgentProgressStreamPayload = {
stage?: "planning" | "clarify" | "executing" | "completed" | "failed";
currentStep?: "lock_target" | "analyze" | "select_tool" | "execute" | "finalize";
title?: string;
message?: string;
toolName?: string;
capability?: string;
};
export type AgentProgressStepState =
| "upcoming"
| "current"
| "completed"
| "failed";
export type AgentProgressNodeStep = {
key: NonNullable<AgentProgressStreamPayload["currentStep"]>;
label: string;
state: AgentProgressStepState;
};
export const AGENT_PROGRESS_STEPS: Array<{
key: NonNullable<AgentProgressStreamPayload["currentStep"]>;
label: string;
}> = [
{ key: "lock_target", label: "理解用户需求" },
{ key: "analyze", label: "分析使用工具" },
{ key: "select_tool", label: "确认执行工具" },
{ key: "execute", label: "执行中" },
{ key: "finalize", label: "整理结果" },
];
export function getAgentTargetLockTitle(payload?: AgentTargetLockStreamPayload | null) {
if (payload?.stage === "clarify") return "需要确认目标";
if (payload?.stage === "locked") return "当前锁定目标";
return "待锁定目标";
}
export function getAgentTargetResolutionLabel(
mode?: AgentTargetLockStreamPayload["resolutionMode"]
) {
switch (mode) {
case "explicit_selection":
return "画布选中";
case "current_upload":
return "当前上传";
case "text_reference":
return "文本定位";
case "working_memory":
return "工作记忆";
case "latest_result":
return "最新结果";
case "clarification":
return "待确认";
case "none":
return "未解析";
default:
return "自动解析";
}
}
export function getAgentProgressFallbackTitle(payload?: AgentProgressStreamPayload | null) {
switch (payload?.stage) {
case "clarify":
return "需要补充信息";
case "executing":
return "正在生成结果";
case "completed":
return "创作完成";
case "failed":
return "创作失败";
default:
return "正在理解需求";
}
}
export function getAgentProgressActionLabel(
payload?: AgentProgressStreamPayload | null
) {
switch (payload?.capability) {
case "analyze":
return "图片分析";
case "generic_edit":
return "修图";
case "remove_background":
return "抠图";
case "remove_watermark":
return "去水印";
case "enhance":
return "图像增强";
case "resize":
return "尺寸调整";
case "extend":
return "扩图";
case "angle_variant":
return "多视角";
case "local_edit":
return "局部编辑";
case "box_cutout":
return "框选抠图";
case "generate_image":
return "生图";
case "generate_video":
return "视频生成";
default:
return "工具";
}
}
export function getAgentProgressStepLabel(
stepKey: NonNullable<AgentProgressStreamPayload["currentStep"]>,
payload?: AgentProgressStreamPayload | null
) {
const actionLabel = getAgentProgressActionLabel(payload);
switch (stepKey) {
case "lock_target":
return "理解用户需求";
case "analyze":
return payload?.capability === "analyze" ? "分析图片" : "分析使用工具";
case "select_tool":
return actionLabel === "工具" ? "确认执行工具" : `确认${actionLabel}工具`;
case "execute":
return actionLabel === "工具" ? "执行中" : `${actionLabel}中`;
case "finalize":
return "整理结果";
default:
return "处理中";
}
}
export function buildAgentProgressNodeSteps(
payload?: AgentProgressStreamPayload | null
): AgentProgressNodeStep[] {
const currentStep = payload?.currentStep || "lock_target";
const currentStepIndex = Math.max(
0,
AGENT_PROGRESS_STEPS.findIndex((step) => step.key === currentStep)
);
return AGENT_PROGRESS_STEPS.map((step, index) => {
let state: AgentProgressStepState = "upcoming";
if (payload?.stage === "failed") {
state =
index < currentStepIndex
? "completed"
: index === currentStepIndex
? "failed"
: "upcoming";
} else if (payload?.stage === "completed") {
state = "completed";
} else {
state =
index < currentStepIndex
? "completed"
: index === currentStepIndex
? "current"
: "upcoming";
}
return {
key: step.key,
label: getAgentProgressStepLabel(step.key, payload),
state,
};
});
}