368 lines
11 KiB
TypeScript
368 lines
11 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;
|
|
sameModelPendingCount?: number;
|
|
sameModelRunningCount?: number;
|
|
concurrencyLimit?: number;
|
|
dispatchIntervalMs?: 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,
|
|
};
|
|
});
|
|
}
|