feat: enhance flowgram editor with condition node handling, port normalization, and UI improvements
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
type FreeLayoutPluginContext,
|
||||
type FreeLayoutProps,
|
||||
type WorkflowNodeEntity,
|
||||
type WorkflowJSON,
|
||||
} from "@flowgram.ai/free-layout-editor"
|
||||
import { createFreeSnapPlugin } from "@flowgram.ai/free-snap-plugin"
|
||||
@@ -13,6 +15,10 @@ import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
|
||||
import { FlowgramNodeRenderer } from "./flowgram-node-renderer"
|
||||
import { buildFlowgramNodeRegistries } from "./flowgram-node-registries"
|
||||
import {
|
||||
normalizeConditionPortsForFlowgram,
|
||||
syncConditionBranchTargetsFromEdges,
|
||||
} from "./workflow-utils"
|
||||
|
||||
export function useFlowgramEditorProps({
|
||||
definition,
|
||||
@@ -26,79 +32,121 @@ export function useFlowgramEditorProps({
|
||||
onDefinitionChange?: (definition: AIWorkflowDefinition) => void
|
||||
}) {
|
||||
return useMemo<FreeLayoutProps>(
|
||||
() => ({
|
||||
background: true,
|
||||
readonly,
|
||||
initialData: definition as WorkflowJSON,
|
||||
nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs),
|
||||
fromNodeJSON(_node, json) {
|
||||
return json
|
||||
},
|
||||
toNodeJSON(_node, json) {
|
||||
return json
|
||||
},
|
||||
materials: {
|
||||
renderDefaultNode: FlowgramNodeRenderer,
|
||||
},
|
||||
nodeEngine: {
|
||||
enable: true,
|
||||
},
|
||||
history: {
|
||||
enable: !readonly,
|
||||
enableChangeNode: !readonly,
|
||||
},
|
||||
canDeleteNode: (_ctx, node) => {
|
||||
const type = String(node.flowNodeType ?? "")
|
||||
return type !== "start" && type !== "end"
|
||||
},
|
||||
canDeleteLine: () => !readonly,
|
||||
onContentChange: (ctx) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
onDefinitionChange?.(ctx.document.toJSON() as AIWorkflowDefinition)
|
||||
},
|
||||
onAllLayersRendered: (ctx) => {
|
||||
void ctx.tools.fitView(false)
|
||||
},
|
||||
getNodeDefaultRegistry(type) {
|
||||
return {
|
||||
type,
|
||||
meta: {
|
||||
defaultExpanded: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: () => [
|
||||
createMinimapPlugin({
|
||||
disableLayer: true,
|
||||
canvasStyle: {
|
||||
canvasWidth: 150,
|
||||
canvasHeight: 84,
|
||||
canvasPadding: 48,
|
||||
canvasBackground: "rgba(245, 245, 245, 1)",
|
||||
canvasBorderRadius: 8,
|
||||
viewportBackground: "rgba(235, 235, 235, 1)",
|
||||
viewportBorderRadius: 4,
|
||||
viewportBorderColor: "rgba(201, 201, 201, 1)",
|
||||
viewportBorderWidth: 1,
|
||||
viewportBorderDashLength: 2,
|
||||
nodeColor: "rgba(255, 255, 255, 1)",
|
||||
nodeBorderRadius: 2,
|
||||
nodeBorderWidth: 0.145,
|
||||
nodeBorderColor: "rgba(6, 7, 9, 0.10)",
|
||||
overlayColor: "rgba(255, 255, 255, 0)",
|
||||
},
|
||||
}),
|
||||
createFreeSnapPlugin({
|
||||
edgeColor: "#00B2B2",
|
||||
alignColor: "#00B2B2",
|
||||
edgeLineWidth: 1,
|
||||
alignLineWidth: 1,
|
||||
alignCrossWidth: 8,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
() => {
|
||||
const initialData = normalizeConditionPortsForFlowgram(definition)
|
||||
return {
|
||||
background: true,
|
||||
readonly,
|
||||
scroll: {
|
||||
disableScrollBar: true,
|
||||
},
|
||||
initialData: initialData as WorkflowJSON,
|
||||
nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs),
|
||||
fromNodeJSON(_node, json) {
|
||||
return json
|
||||
},
|
||||
toNodeJSON(_node, json) {
|
||||
return json
|
||||
},
|
||||
materials: {
|
||||
renderDefaultNode: FlowgramNodeRenderer,
|
||||
},
|
||||
nodeEngine: {
|
||||
enable: true,
|
||||
},
|
||||
history: {
|
||||
enable: !readonly,
|
||||
enableChangeNode: !readonly,
|
||||
},
|
||||
canDeleteNode: (_ctx, node) => {
|
||||
const type = String(node.flowNodeType ?? "")
|
||||
return type !== "start" && type !== "end"
|
||||
},
|
||||
canDeleteLine: () => !readonly,
|
||||
onContentChange: (ctx) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const next = normalizeConditionPortsForFlowgram(
|
||||
syncConditionBranchTargetsFromEdges(ctx.document.toJSON() as AIWorkflowDefinition)
|
||||
)
|
||||
onDefinitionChange?.(next)
|
||||
},
|
||||
onAllLayersRendered: (ctx) => {
|
||||
scrollToInitialNode(ctx)
|
||||
},
|
||||
getNodeDefaultRegistry(type) {
|
||||
return {
|
||||
type,
|
||||
meta: {
|
||||
defaultExpanded: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: () => [
|
||||
createMinimapPlugin({
|
||||
disableLayer: true,
|
||||
canvasStyle: {
|
||||
canvasWidth: 150,
|
||||
canvasHeight: 84,
|
||||
canvasPadding: 48,
|
||||
canvasBackground: "rgba(245, 245, 245, 1)",
|
||||
canvasBorderRadius: 8,
|
||||
viewportBackground: "rgba(235, 235, 235, 1)",
|
||||
viewportBorderRadius: 4,
|
||||
viewportBorderColor: "rgba(201, 201, 201, 1)",
|
||||
viewportBorderWidth: 1,
|
||||
viewportBorderDashLength: 2,
|
||||
nodeColor: "rgba(255, 255, 255, 1)",
|
||||
nodeBorderRadius: 2,
|
||||
nodeBorderWidth: 0.145,
|
||||
nodeBorderColor: "rgba(6, 7, 9, 0.10)",
|
||||
overlayColor: "rgba(255, 255, 255, 0)",
|
||||
},
|
||||
}),
|
||||
createFreeSnapPlugin({
|
||||
edgeColor: "#00B2B2",
|
||||
alignColor: "#00B2B2",
|
||||
edgeLineWidth: 1,
|
||||
alignLineWidth: 1,
|
||||
alignCrossWidth: 8,
|
||||
}),
|
||||
],
|
||||
}
|
||||
},
|
||||
[definition, nodeSpecs, onDefinitionChange, readonly]
|
||||
)
|
||||
}
|
||||
|
||||
function scrollToInitialNode(ctx: FreeLayoutPluginContext) {
|
||||
const nodes = ctx.document.getAllNodes()
|
||||
const startNode = nodes.find((node) => String(node.flowNodeType ?? "") === "start")
|
||||
const targetNode = startNode ?? findLeftTopNode(nodes)
|
||||
if (!targetNode) {
|
||||
return
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const viewport = ctx.playground.config.getViewport(false)
|
||||
void ctx.playground.scrollToView({
|
||||
bounds: targetNode.transform.bounds,
|
||||
scrollDelta: {
|
||||
x: Math.max(viewport.width / 2 - 250, 0),
|
||||
y: Math.max(viewport.height / 2 - 180, 0),
|
||||
},
|
||||
zoom: 1,
|
||||
scrollToCenter: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findLeftTopNode(nodes: WorkflowNodeEntity[]) {
|
||||
return [...nodes].sort((left, right) => {
|
||||
const leftBounds = left.transform.bounds
|
||||
const rightBounds = right.transform.bounds
|
||||
if (leftBounds.left !== rightBounds.left) {
|
||||
return leftBounds.left - rightBounds.left
|
||||
}
|
||||
return leftBounds.top - rightBounds.top
|
||||
})[0]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Field, type WorkflowNodeRegistry } from "@flowgram.ai/free-layout-editor"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
import {
|
||||
createConditionBranchID,
|
||||
normalizeNodeConfig,
|
||||
type WorkflowConditionBranch,
|
||||
} from "./workflow-utils"
|
||||
|
||||
export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): WorkflowNodeRegistry[] {
|
||||
const seen = new Set<string>()
|
||||
@@ -41,16 +48,20 @@ function FlowgramNodeForm({
|
||||
nodeType: string
|
||||
fallbackTitle: string
|
||||
}) {
|
||||
if (nodeType === "condition") {
|
||||
return <ConditionNodeForm fallbackTitle={fallbackTitle} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex w-full flex-col">
|
||||
<Field<string> name="title">
|
||||
{({ field }) => (
|
||||
<div className="truncate text-sm font-medium leading-5">
|
||||
<div className="border-b px-4 py-3 text-sm font-medium leading-5">
|
||||
{field.value || fallbackTitle}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">{nodeType}</div>
|
||||
<div className="px-4 py-3 text-xs text-muted-foreground">{nodeType}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -62,5 +73,94 @@ function defaultPortsForNodeType(type: string) {
|
||||
if (type === "end") {
|
||||
return [{ type: "input" as const }]
|
||||
}
|
||||
if (type === "condition") {
|
||||
return [{ type: "input" as const }]
|
||||
}
|
||||
return [{ type: "input" as const }, { type: "output" as const }]
|
||||
}
|
||||
|
||||
function ConditionNodeForm({ fallbackTitle }: { fallbackTitle: string }) {
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<Field<string> name="title">
|
||||
{({ field }) => (
|
||||
<div className="border-b px-4 py-3 text-sm font-medium leading-5">
|
||||
{field.value || fallbackTitle}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field<Record<string, unknown>> name="config">
|
||||
{({ field }) => {
|
||||
const config = normalizeNodeConfig(field.value)
|
||||
const branches = ensureConditionBranches(config.branches ?? [])
|
||||
const updateBranches = (nextBranches: WorkflowConditionBranch[]) => {
|
||||
field.onChange({
|
||||
...config,
|
||||
branches: ensureConditionBranches(nextBranches),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3">
|
||||
<div className="space-y-2">
|
||||
{branches.map((branch) => (
|
||||
<div
|
||||
key={branch.id}
|
||||
className="flex min-h-9 items-center gap-2 rounded-md bg-muted/40 px-3 text-xs hover:bg-muted/70"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{branch.name || (branch.default ? "默认分支" : branch.id)}
|
||||
</span>
|
||||
{branch.default ? (
|
||||
<span className="shrink-0 rounded bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||
else
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
data-port-id={branch.id}
|
||||
data-port-type="output"
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-full border border-[#4e40e5]/70 bg-background"
|
||||
>
|
||||
<span className="size-2 rounded-full bg-[#4e40e5]" />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-2 h-7 px-2 text-xs text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
updateBranches([
|
||||
...branches,
|
||||
{
|
||||
id: createConditionBranchID(branches),
|
||||
name: "新条件",
|
||||
targetNodeId: "",
|
||||
condition: { operator: "eq" },
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ensureConditionBranches(branches: WorkflowConditionBranch[]) {
|
||||
const normalized = branches.some((branch) => branch.default)
|
||||
? branches
|
||||
: [...branches, { id: "default", name: "默认分支", targetNodeId: "", default: true }]
|
||||
return [
|
||||
...normalized.filter((branch) => !branch.default),
|
||||
...normalized.filter((branch) => branch.default).slice(0, 1),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,54 +5,35 @@ import {
|
||||
WorkflowNodeRenderer,
|
||||
type WorkflowNodeProps,
|
||||
} from "@flowgram.ai/free-layout-editor"
|
||||
import {
|
||||
BotIcon,
|
||||
CircleStopIcon,
|
||||
DatabaseIcon,
|
||||
GitBranchIcon,
|
||||
MessageSquareTextIcon,
|
||||
SendIcon,
|
||||
UserRoundIcon,
|
||||
} from "lucide-react"
|
||||
import type { ComponentType } from "react"
|
||||
import { useLayoutEffect } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const iconByType: Record<string, ComponentType<{ className?: string }>> = {
|
||||
start: UserRoundIcon,
|
||||
conversation_understanding: BotIcon,
|
||||
reply_policy: MessageSquareTextIcon,
|
||||
condition: GitBranchIcon,
|
||||
knowledge_retrieve: DatabaseIcon,
|
||||
answerability_gate: GitBranchIcon,
|
||||
llm_reply: BotIcon,
|
||||
human_confirm: UserRoundIcon,
|
||||
create_ticket: MessageSquareTextIcon,
|
||||
handoff_to_human: UserRoundIcon,
|
||||
send_reply: SendIcon,
|
||||
end: CircleStopIcon,
|
||||
}
|
||||
|
||||
export function FlowgramNodeRenderer(props: WorkflowNodeProps) {
|
||||
const { selected, node, form } = useNodeRender()
|
||||
const nodeType = String(node.flowNodeType ?? "")
|
||||
const Icon = iconByType[nodeType] ?? BotIcon
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (nodeType !== "condition") return
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
node.ports.updateDynamicPorts()
|
||||
})
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
})
|
||||
|
||||
return (
|
||||
<WorkflowNodeRenderer
|
||||
node={props.node}
|
||||
className={cn(
|
||||
"w-[260px] rounded-md border bg-background shadow-sm transition-colors",
|
||||
selected ? "border-primary ring-2 ring-primary/15" : "border-border"
|
||||
"w-[360px] overflow-hidden rounded-lg border bg-background shadow-[0_2px_6px_rgba(0,0,0,0.04),0_4px_12px_rgba(0,0,0,0.02)] transition-colors",
|
||||
selected ? "border-[#4e40e5]" : "border-[rgba(6,7,9,0.15)]"
|
||||
)}
|
||||
style={{ padding: 0 }}
|
||||
portPrimaryColor="#4e40e5"
|
||||
portSecondaryColor="#d0d5dd"
|
||||
portBackgroundColor="#fff"
|
||||
>
|
||||
<div className="flex items-start gap-3 p-3">
|
||||
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border bg-muted">
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
{form?.render()}
|
||||
</div>
|
||||
{form?.render()}
|
||||
</WorkflowNodeRenderer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -303,4 +303,68 @@ describe("workflow definition mutations", () => {
|
||||
const deleted = deleteConditionBranch(updated, "condition_1", "default")
|
||||
assert.deepEqual(plain(deleted.nodes[0].data.config.branches.map((branch) => branch.id)), ["vip"])
|
||||
})
|
||||
|
||||
it("adds FlowGram source ports for condition edges without removing existing lines", async () => {
|
||||
const { normalizeConditionPortsForFlowgram } = await loadModule()
|
||||
const definition = {
|
||||
schemaVersion: 2,
|
||||
nodes: [
|
||||
workflowNode("condition_1", "condition", { x: 240, y: 0 }, {
|
||||
config: {
|
||||
branches: [
|
||||
{ id: "vip", name: "VIP", targetNodeId: "vip_reply", condition: { operator: "eq" } },
|
||||
{ id: "default", name: "默认", targetNodeId: "normal_reply", default: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
workflowNode("vip_reply", "llm_reply", { x: 520, y: 0 }),
|
||||
workflowNode("normal_reply", "llm_reply", { x: 520, y: 120 }),
|
||||
],
|
||||
edges: [
|
||||
workflowEdge("condition_1", "vip_reply"),
|
||||
workflowEdge("condition_1", "normal_reply"),
|
||||
],
|
||||
}
|
||||
|
||||
const next = normalizeConditionPortsForFlowgram(definition)
|
||||
|
||||
assert.deepEqual(plain(next.nodes[0].data.portKeys), ["vip", "default"])
|
||||
assert.deepEqual(plain(next.nodes[0].data.ports), ["vip", "default"])
|
||||
assert.deepEqual(plain(next.edges), [
|
||||
workflowEdge("condition_1", "vip_reply", { sourcePortID: "vip" }),
|
||||
workflowEdge("condition_1", "normal_reply", { sourcePortID: "default" }),
|
||||
])
|
||||
})
|
||||
|
||||
it("syncs branch targets from condition source ports while preserving unrelated edges", async () => {
|
||||
const { syncConditionBranchTargetsFromEdges } = await loadModule()
|
||||
const definition = {
|
||||
schemaVersion: 2,
|
||||
nodes: [
|
||||
workflowNode("condition_1", "condition", { x: 240, y: 0 }, {
|
||||
config: {
|
||||
branches: [
|
||||
{ id: "vip", name: "VIP", targetNodeId: "", condition: { operator: "eq" } },
|
||||
{ id: "default", name: "默认", targetNodeId: "", default: true },
|
||||
],
|
||||
},
|
||||
portKeys: ["vip", "default"],
|
||||
ports: ["vip", "default"],
|
||||
}),
|
||||
workflowNode("vip_reply", "llm_reply", { x: 520, y: 0 }),
|
||||
workflowNode("normal_reply", "llm_reply", { x: 520, y: 120 }),
|
||||
],
|
||||
edges: [
|
||||
workflowEdge("condition_1", "vip_reply", { sourcePortID: "vip" }),
|
||||
workflowEdge("condition_1", "normal_reply", { sourcePortID: "default" }),
|
||||
workflowEdge("vip_reply", "normal_reply"),
|
||||
],
|
||||
}
|
||||
|
||||
const next = syncConditionBranchTargetsFromEdges(definition)
|
||||
|
||||
assert.equal(next.nodes[0].data.config.branches[0].targetNodeId, "vip_reply")
|
||||
assert.equal(next.nodes[0].data.config.branches[1].targetNodeId, "normal_reply")
|
||||
assert.equal(next.edges.length, 3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,13 +166,16 @@ export function createWorkflowNodeFromSpec(
|
||||
position: WorkflowNodePosition
|
||||
): AIWorkflowDefinition["nodes"][number] {
|
||||
const id = uniqueNodeId(existingNodes, spec.type)
|
||||
const defaultConfig = spec.type === "condition"
|
||||
? { branches: [{ id: "default", name: "默认分支", targetNodeId: "", default: true }] }
|
||||
: {}
|
||||
return {
|
||||
id,
|
||||
type: spec.type,
|
||||
meta: { position },
|
||||
data: {
|
||||
title: spec.title || spec.type,
|
||||
config: {},
|
||||
config: defaultConfig,
|
||||
inputsValues: spec.defaultInputs ?? {},
|
||||
},
|
||||
}
|
||||
@@ -247,6 +250,68 @@ export function deleteConditionBranch(
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeConditionPortsForFlowgram(
|
||||
definition: AIWorkflowDefinition
|
||||
): AIWorkflowDefinition {
|
||||
return {
|
||||
...definition,
|
||||
nodes: definition.nodes.map((node) => {
|
||||
if (node.type !== "condition") {
|
||||
return node
|
||||
}
|
||||
const config = normalizeNodeConfig(node.data?.config)
|
||||
const branches = ensureConditionBranches(config.branches ?? [])
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...(node.data ?? {}),
|
||||
config: { ...config, branches },
|
||||
portKeys: branches.map((branch) => branch.id),
|
||||
ports: branches.map((branch) => branch.id),
|
||||
},
|
||||
}
|
||||
}),
|
||||
edges: definition.edges.map((edge) => {
|
||||
const source = definition.nodes.find((node) => node.id === edge.sourceNodeID)
|
||||
if (!source || source.type !== "condition" || edge.sourcePortID) {
|
||||
return edge
|
||||
}
|
||||
const branch = findConditionBranchForTarget(source, edge.targetNodeID)
|
||||
return branch ? { ...edge, sourcePortID: branch.id } : edge
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function syncConditionBranchTargetsFromEdges(
|
||||
definition: AIWorkflowDefinition
|
||||
): AIWorkflowDefinition {
|
||||
return {
|
||||
...definition,
|
||||
nodes: definition.nodes.map((node) => {
|
||||
if (node.type !== "condition") {
|
||||
return node
|
||||
}
|
||||
const config = normalizeNodeConfig(node.data?.config)
|
||||
const branches = ensureConditionBranches(config.branches ?? [])
|
||||
const nextBranches = branches.map((branch) => {
|
||||
const edge = definition.edges.find((item) => (
|
||||
item.sourceNodeID === node.id && item.sourcePortID === branch.id
|
||||
))
|
||||
return edge ? { ...branch, targetNodeId: edge.targetNodeID } : branch
|
||||
})
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...(node.data ?? {}),
|
||||
config: { ...config, branches: nextBranches },
|
||||
portKeys: nextBranches.map((branch) => branch.id),
|
||||
ports: nextBranches.map((branch) => branch.id),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeNodeConfig(config: unknown): WorkflowNodeConfig {
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||
return {}
|
||||
@@ -263,6 +328,31 @@ export function normalizeNodeConfig(config: unknown): WorkflowNodeConfig {
|
||||
} as WorkflowNodeConfig
|
||||
}
|
||||
|
||||
function ensureConditionBranches(branches: WorkflowConditionBranch[]) {
|
||||
if (branches.some((branch) => branch.default)) {
|
||||
return orderConditionBranches(branches)
|
||||
}
|
||||
return orderConditionBranches([
|
||||
...branches,
|
||||
{ id: "default", name: "默认分支", targetNodeId: "", default: true },
|
||||
])
|
||||
}
|
||||
|
||||
function orderConditionBranches(branches: WorkflowConditionBranch[]) {
|
||||
return [
|
||||
...branches.filter((branch) => !branch.default),
|
||||
...branches.filter((branch) => branch.default).slice(0, 1),
|
||||
]
|
||||
}
|
||||
|
||||
function findConditionBranchForTarget(
|
||||
node: AIWorkflowDefinition["nodes"][number],
|
||||
targetNodeId: string
|
||||
) {
|
||||
const branches = ensureConditionBranches(normalizeNodeConfig(node.data?.config).branches ?? [])
|
||||
return branches.find((branch) => branch.targetNodeId === targetNodeId)
|
||||
}
|
||||
|
||||
export function createConditionBranchID(existingBranches: WorkflowConditionBranch[]) {
|
||||
const existingIDs = new Set(existingBranches.map((branch) => branch.id))
|
||||
for (let index = 1; index < 10000; index++) {
|
||||
|
||||
Reference in New Issue
Block a user