2026-06-15 10:12:41 +08:00
|
|
|
/**
|
|
|
|
|
* 工作流节点的前端类型与运行期规格。
|
|
|
|
|
*
|
|
|
|
|
* 节点「目录」(有哪些类型、各自的字段与约束)由后端 /api/node-types 提供,
|
|
|
|
|
* 通过 useNodeSpecs 拉取后用 toRuntimeSpec 转成带 React 组件(图标)的运行期规格。
|
|
|
|
|
* 本文件只保留:类型定义、默认图、图标/配色解析。新增节点类型改后端即可。
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as LucideIcons from "lucide-react";
|
|
|
|
|
import { Circle, type LucideIcon } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
import type { NodeSpecDto } from "@/lib/api";
|
|
|
|
|
|
|
|
|
|
export type WorkflowNodeType = "startCall" | "agentNode" | "endCall";
|
|
|
|
|
|
|
|
|
|
export type WorkflowNodeData = {
|
|
|
|
|
/** 节点显示名 */
|
|
|
|
|
name: string;
|
|
|
|
|
/** 开场白(仅 startCall) */
|
|
|
|
|
greeting?: string;
|
|
|
|
|
/** 节点提示词 */
|
|
|
|
|
prompt?: string;
|
|
|
|
|
/** 允许打断(仅 agentNode) */
|
|
|
|
|
allowInterrupt?: boolean;
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FieldSpec = {
|
|
|
|
|
key: string;
|
|
|
|
|
label: string;
|
|
|
|
|
type: "text" | "textarea" | "switch";
|
|
|
|
|
required?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 解析后的运行期节点规格(DTO + 解析出的 React 图标 + 派生句柄) */
|
|
|
|
|
export type RuntimeNodeSpec = {
|
|
|
|
|
type: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
description: string;
|
|
|
|
|
icon: LucideIcon;
|
|
|
|
|
accent: string;
|
|
|
|
|
addable: boolean;
|
|
|
|
|
/** 入边句柄(开始节点没有) */
|
|
|
|
|
hasTarget: boolean;
|
|
|
|
|
/** 出边句柄(结束节点没有) */
|
|
|
|
|
hasSource: boolean;
|
|
|
|
|
fields: FieldSpec[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 渐变 token → CSS 变量名(图标底色用),未知配色回落到 sky */
|
|
|
|
|
export const ACCENT_VAR: Record<string, string> = {
|
|
|
|
|
mint: "--gradient-mint",
|
|
|
|
|
sky: "--gradient-sky",
|
|
|
|
|
rose: "--gradient-rose",
|
|
|
|
|
peach: "--gradient-peach",
|
|
|
|
|
lavender: "--gradient-lavender",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function accentVar(accent: string): string {
|
|
|
|
|
return ACCENT_VAR[accent] ?? ACCENT_VAR.sky;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 按名字解析 Lucide 图标,找不到回落到 Circle(对齐 dograh resolveIcon)。 */
|
|
|
|
|
export function resolveIcon(name: string): LucideIcon {
|
|
|
|
|
const icons = LucideIcons as unknown as Record<string, LucideIcon>;
|
|
|
|
|
return icons[name] ?? Circle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 后端 DTO → 运行期规格。hasTarget/hasSource 由入/出边上限派生。 */
|
|
|
|
|
export function toRuntimeSpec(dto: NodeSpecDto): RuntimeNodeSpec {
|
|
|
|
|
return {
|
|
|
|
|
type: dto.name,
|
|
|
|
|
displayName: dto.displayName,
|
|
|
|
|
description: dto.description,
|
|
|
|
|
icon: resolveIcon(dto.icon),
|
|
|
|
|
accent: dto.accent,
|
|
|
|
|
addable: dto.addable,
|
|
|
|
|
hasTarget: dto.constraints.maxIncoming !== 0,
|
|
|
|
|
hasSource: dto.constraints.maxOutgoing !== 0,
|
|
|
|
|
fields: dto.fields.map((f) => ({
|
|
|
|
|
key: f.key,
|
|
|
|
|
label: f.label,
|
|
|
|
|
type: f.type,
|
|
|
|
|
required: f.required,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type NodeSpecMap = Record<string, RuntimeNodeSpec>;
|
|
|
|
|
|
|
|
|
|
export type WorkflowGraph = {
|
|
|
|
|
nodes: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
type: WorkflowNodeType;
|
|
|
|
|
position: { x: number; y: number };
|
|
|
|
|
data: WorkflowNodeData;
|
|
|
|
|
}>;
|
|
|
|
|
edges: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
source: string;
|
|
|
|
|
target: string;
|
2026-06-15 15:57:05 +08:00
|
|
|
data?: { condition?: string; label?: string; transition_speech?: string };
|
2026-06-15 10:12:41 +08:00
|
|
|
}>;
|
|
|
|
|
viewport?: { x: number; y: number; zoom: number };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 新建工作流的默认图:开始 → 智能体 → 结束 */
|
|
|
|
|
export function defaultGraph(): WorkflowGraph {
|
|
|
|
|
return {
|
|
|
|
|
nodes: [
|
|
|
|
|
{
|
|
|
|
|
id: "start",
|
|
|
|
|
type: "startCall",
|
|
|
|
|
position: { x: 80, y: 160 },
|
|
|
|
|
data: { name: "开始", greeting: "你好,我是 AI 视频助手,有什么可以帮你?" },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "agent-1",
|
|
|
|
|
type: "agentNode",
|
|
|
|
|
position: { x: 400, y: 160 },
|
|
|
|
|
data: { name: "智能体节点", prompt: "", allowInterrupt: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "end",
|
|
|
|
|
type: "endCall",
|
|
|
|
|
position: { x: 720, y: 160 },
|
|
|
|
|
data: { name: "结束", prompt: "感谢你的来电,再见!" },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
edges: [
|
|
|
|
|
{ id: "e-start-agent", source: "start", target: "agent-1", data: {} },
|
|
|
|
|
{ id: "e-agent-end", source: "agent-1", target: "end", data: {} },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
}
|