import type { AIWorkflowDefinition, AIWorkflowNodeSpec, AIWorkflowValue } from "@/lib/api/admin" export type WorkflowNodePosition = { x: number y: number } export type WorkflowVariableType = | "string" | "number" | "integer" | "boolean" | "object" | "array" | "array" | "array" | "any" export type WorkflowVariableValueOption = { value: unknown label: string description?: string } export type WorkflowVariableSpec = { name: string label?: string type: WorkflowVariableType required?: boolean description?: string operators?: string[] valueOptions?: WorkflowVariableValueOption[] } export type WorkflowValue = AIWorkflowValue export type WorkflowVariableSelector = Extract export type WorkflowCondition = { expression?: string left?: WorkflowValue operator?: string right?: unknown } export type WorkflowConditionBranch = { id: string name?: string targetNodeId: string condition?: WorkflowCondition default?: boolean } export type WorkflowNodeConfig = Record & { branches?: WorkflowConditionBranch[] } export type WorkflowVariableRef = { nodeId: string nodeName: string field: string label?: string type: string description: string operators?: string[] valueOptions?: WorkflowVariableValueOption[] } export type WorkflowVariableOption = { value: string label: string subtitle: string description: string } export type WorkflowNodeSpec = AIWorkflowNodeSpec export type WorkflowDraftValidation = { valid: boolean errors: string[] } export type WorkflowNode = AIWorkflowDefinition["nodes"][number] export type WorkflowNodeData = WorkflowNode["data"] export type WorkflowEdge = AIWorkflowDefinition["edges"][number] export function createRefValue(nodeId: string, field: string): WorkflowVariableSelector { return { type: "ref", content: [nodeId, field] } } export function isRefValue(value: WorkflowValue | undefined): value is WorkflowVariableSelector { return value?.type === "ref" && Array.isArray(value.content) && value.content.length >= 2 } export function refNodeId(value: WorkflowValue | undefined): string { return isRefValue(value) ? value.content[0] : "" } export function refField(value: WorkflowValue | undefined): string { return isRefValue(value) ? value.content[1] : "" } export function buildVariableOption(variable: WorkflowVariableRef): WorkflowVariableOption { const ref = `${variable.nodeId}.${variable.field}` const label = `${variable.nodeName} / ${variable.label || variable.field}` return { value: ref, label, subtitle: [ref, variable.type].filter(Boolean).join(" · "), description: variable.description, } } export function getNodeTitle( node: AIWorkflowDefinition["nodes"][number] | undefined, specs: AIWorkflowNodeSpec[] = [] ) { if (!node) { return "" } return node.data?.title || specs.find((item) => item.type === node.type)?.title || node.type || node.id } export function validateWorkflowDefinition( definition: AIWorkflowDefinition, nodeSpecs: AIWorkflowNodeSpec[] = [] ): WorkflowDraftValidation { const errors: string[] = [] const nodes = definition.nodes ?? [] const edges = definition.edges ?? [] const startNodes = nodes.filter((node) => node.type === "start") const endNodes = nodes.filter((node) => node.type === "end") if (startNodes.length !== 1) { errors.push("workflow must contain exactly one start node") } if (endNodes.length === 0) { errors.push("workflow must contain at least one end node") } const nodeIds = new Set() for (const node of nodes) { if (!node.id?.trim()) { errors.push("node id is required") continue } if (nodeIds.has(node.id)) { errors.push(`duplicate node id: ${node.id}`) } nodeIds.add(node.id) if (!node.type?.trim()) { errors.push(`node type is required: ${node.id}`) } } for (const edge of edges) { if (!nodeIds.has(edge.sourceNodeID)) { errors.push(`edge source node does not exist: ${edge.sourceNodeID}`) } if (!nodeIds.has(edge.targetNodeID)) { errors.push(`edge target node does not exist: ${edge.targetNodeID}`) } } const specByType = new Map(nodeSpecs.map((spec) => [spec.type, spec])) for (const node of nodes) { const spec = specByType.get(node.type) if (!spec) { continue } const inputsValues = node.data?.inputsValues ?? {} for (const input of spec.inputSchema ?? []) { if (input.required && !inputsValues[input.name]) { errors.push(`${getNodeTitle(node, nodeSpecs)} missing required input: ${input.label || input.name}`) } } } return { valid: errors.length === 0, errors } } export function createWorkflowNodeFromSpec( spec: AIWorkflowNodeSpec, existingNodes: Pick[], 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: defaultConfig, inputsValues: spec.defaultInputs ?? {}, }, } } export function updateWorkflowNodeData( definition: AIWorkflowDefinition, nodeId: string, data: WorkflowNodeData ): AIWorkflowDefinition { return { ...definition, nodes: definition.nodes.map((node) => ( node.id === nodeId ? { ...node, data } : node )), } } export function deleteWorkflowNode( definition: AIWorkflowDefinition, nodeId: string ): AIWorkflowDefinition { const node = definition.nodes.find((item) => item.id === nodeId) if (!node || node.type === "start") { return definition } return { ...definition, nodes: definition.nodes.filter((item) => item.id !== nodeId), edges: definition.edges.filter((edge) => ( edge.sourceNodeID !== nodeId && edge.targetNodeID !== nodeId )), } } export function upsertConditionBranch( definition: AIWorkflowDefinition, nodeId: string, branch: WorkflowConditionBranch ): AIWorkflowDefinition { const node = definition.nodes.find((item) => item.id === nodeId) if (!node) { return definition } const config = normalizeNodeConfig(node.data?.config) const branches = config.branches ?? [] const nextBranches = branches.some((item) => item.id === branch.id) ? branches.map((item) => (item.id === branch.id ? branch : item)) : [...branches, branch] return updateWorkflowNodeData(definition, nodeId, { ...(node.data ?? {}), config: { ...config, branches: nextBranches }, }) } export function deleteConditionBranch( definition: AIWorkflowDefinition, nodeId: string, branchId: string ): AIWorkflowDefinition { const node = definition.nodes.find((item) => item.id === nodeId) if (!node) { return definition } const config = normalizeNodeConfig(node.data?.config) const nextDefinition = updateWorkflowNodeData(definition, nodeId, { ...(node.data ?? {}), config: { ...config, branches: (config.branches ?? []).filter((branch) => branch.id !== branchId), }, }) return { ...nextDefinition, edges: nextDefinition.edges.filter((edge) => !(edge.sourceNodeID === nodeId && edge.sourcePortID === branchId)), } } export function normalizeConditionPortsForFlowgram( definition: AIWorkflowDefinition ): AIWorkflowDefinition { const branchIdsByNodeId = new Map>() for (const node of definition.nodes) { if (node.type !== "condition") { continue } const config = normalizeNodeConfig(node.data?.config) const branches = ensureConditionBranches(config.branches ?? []) branchIdsByNodeId.set(node.id, new Set(branches.map((branch) => branch.id))) } 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.filter((edge) => { if (!edge.sourcePortID) { return true } const branchIds = branchIdsByNodeId.get(edge.sourceNodeID) return !branchIds || branchIds.has(edge.sourcePortID) }).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 {} } const record = config as Record const branches = Array.isArray(record.branches) ? record.branches .map(normalizeConditionBranch) .filter((branch): branch is WorkflowConditionBranch => branch !== null) : undefined return { ...record, ...(branches ? { branches } : {}), } 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++) { const id = `branch_${index}` if (!existingIDs.has(id)) { return id } } return `branch_${Date.now()}` } export function getAvailableVariables( definition: AIWorkflowDefinition, nodeId: string, nodeSpecs: AIWorkflowNodeSpec[] ): WorkflowVariableRef[] { const ancestorIds = collectAncestorNodeIds(definition, nodeId) const specByType = new Map(nodeSpecs.map((spec) => [spec.type, spec])) const ret: WorkflowVariableRef[] = [] for (const id of ancestorIds) { const node = definition.nodes.find((item) => item.id === id) if (!node) { continue } const spec = specByType.get(node.type) for (const output of spec?.outputSchema ?? []) { ret.push({ nodeId: node.id, nodeName: getNodeTitle(node, nodeSpecs), field: output.name, label: output.label, type: output.type, description: output.description || "", operators: output.operators, valueOptions: output.valueOptions, }) } } return ret } function collectAncestorNodeIds(definition: AIWorkflowDefinition, nodeId: string): string[] { const incoming = new Map() for (const edge of definition.edges ?? []) { const list = incoming.get(edge.targetNodeID) ?? [] list.push(edge.sourceNodeID) incoming.set(edge.targetNodeID, list) } const result: string[] = [] const seen = new Set() const visit = (id: string) => { for (const source of incoming.get(id) ?? []) { if (seen.has(source)) { continue } seen.add(source) visit(source) result.push(source) } } visit(nodeId) return result } function uniqueNodeId(existingNodes: Pick[], nodeType: string) { const normalizedType = nodeType.replace(/[^a-zA-Z0-9_]/g, "_") || "node" const existingIDs = new Set(existingNodes.map((node) => node.id)) for (let index = 1; index < 10000; index++) { const id = `${normalizedType}_${index}` if (!existingIDs.has(id)) { return id } } return `${normalizedType}_${Date.now()}` } function normalizeConditionBranch(value: unknown): WorkflowConditionBranch | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null } const record = value as Record const id = typeof record.id === "string" ? record.id : "" const targetNodeId = typeof record.targetNodeId === "string" ? record.targetNodeId : "" if (!id) { return null } return { id, name: typeof record.name === "string" ? record.name : undefined, targetNodeId, default: record.default === true, condition: normalizeCondition(record.condition), } } function normalizeCondition(value: unknown): WorkflowCondition | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { return undefined } const record = value as Record return { expression: typeof record.expression === "string" ? record.expression : undefined, left: isWorkflowValue(record.left) ? record.left : undefined, operator: typeof record.operator === "string" ? record.operator : undefined, right: record.right, } } function isWorkflowValue(value: unknown): value is WorkflowValue { if (!value || typeof value !== "object" || Array.isArray(value)) { return false } const type = (value as Record).type return type === "ref" || type === "constant" || type === "template" }