feat: implement business node specifications and enhance workflow editor with node specs support

This commit is contained in:
mlogclub
2026-08-16 21:27:45 +08:00
parent 16a0d6f5bd
commit ad971370cf
17 changed files with 397 additions and 22 deletions
@@ -1,10 +1,11 @@
"use client"
import { useEffect, useRef } from "react"
import { useCallback, useEffect, useRef } from "react"
import type { AIWorkflowDefinition } from "@/lib/api/admin"
import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
const MESSAGE_SOURCE = "agent-desk"
const EMPTY_NODE_SPECS: AIWorkflowNodeSpec[] = []
type EditorMessage =
| {
@@ -20,31 +21,36 @@ type EditorMessage =
export function OfficialWorkflowEditor({
documentKey,
definition,
nodeSpecs = EMPTY_NODE_SPECS,
onDefinitionChange,
readonly = false,
}: {
documentKey: string
definition: AIWorkflowDefinition
nodeSpecs?: AIWorkflowNodeSpec[]
onDefinitionChange: (definition: AIWorkflowDefinition) => void
readonly?: boolean
}) {
const frameRef = useRef<HTMLIFrameElement>(null)
const definitionRef = useRef(definition)
definitionRef.current = definition
useEffect(() => {
definitionRef.current = definition
}, [definition])
function loadDocument() {
const loadDocument = useCallback(() => {
frameRef.current?.contentWindow?.postMessage(
{
source: MESSAGE_SOURCE,
type: "workflow:load",
documentKey,
document: definitionRef.current,
nodeSpecs,
readonly,
},
window.location.origin
)
}
}, [documentKey, nodeSpecs, readonly])
useEffect(() => {
const handleMessage = (event: MessageEvent<EditorMessage>) => {
@@ -63,11 +69,11 @@ export function OfficialWorkflowEditor({
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [documentKey, onDefinitionChange, readonly])
}, [loadDocument, onDefinitionChange])
useEffect(() => {
loadDocument()
}, [documentKey, readonly])
}, [loadDocument])
return (
<iframe
@@ -8,14 +8,19 @@ import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { OptionCombobox } from "@/components/option-combobox"
import {
createAIWorkflow,
fetchAIWorkflow,
fetchAIWorkflowDefaultDefinition,
fetchAIWorkflowNodeSpecs,
fetchAIWorkflowTemplates,
publishAIWorkflow,
updateAIWorkflow,
type AIWorkflow,
type AIWorkflowDefinition,
type AIWorkflowNodeSpec,
type AIWorkflowTemplate,
} from "@/lib/api/admin"
import { OfficialWorkflowEditor } from "./official-workflow-editor"
@@ -59,6 +64,9 @@ export function WorkflowWorkbench({
const [description, setDescription] = useState("")
const [definition, setDefinition] =
useState<AIWorkflowDefinition>(emptyDefinition)
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [templates, setTemplates] = useState<AIWorkflowTemplate[]>([])
const [selectedTemplate, setSelectedTemplate] = useState("")
const [saving, setSaving] = useState(false)
const [dirty, setDirty] = useState(false)
const [loaded, setLoaded] = useState(false)
@@ -71,6 +79,12 @@ export function WorkflowWorkbench({
const load = useCallback(async () => {
setLoaded(false)
const [specs, availableTemplates] = await Promise.all([
fetchAIWorkflowNodeSpecs(),
fetchAIWorkflowTemplates(),
])
setNodeSpecs(specs)
setTemplates(availableTemplates)
if (!workflowID) {
setDefinition(await fetchAIWorkflowDefaultDefinition())
setLoaded(true)
@@ -356,6 +370,29 @@ export function WorkflowWorkbench({
)}
</div>
<div className="flex shrink-0 gap-2">
{!active ? (
<OptionCombobox
value={selectedTemplate}
placeholder="选择流程模板"
searchPlaceholder="搜索流程模板"
emptyText="暂无流程模板"
triggerClassName="w-48"
options={templates.map((template) => ({
value: template.code,
label: template.name,
description: template.description,
}))}
onChange={(code) => {
const template = templates.find((item) => item.code === code)
if (!template) return
setSelectedTemplate(code)
setDefinition(structuredClone(template.definition))
if (!name.trim()) setName(template.name)
if (!description.trim()) setDescription(template.description)
setDirty(true)
}}
/>
) : null}
<Button
variant="outline"
disabled={saving}
@@ -377,8 +414,13 @@ export function WorkflowWorkbench({
<div className="min-h-0 flex-1 overflow-hidden">
{loaded ? (
<OfficialWorkflowEditor
documentKey={workflowID ? `workflow-${workflowID}` : "new"}
documentKey={
workflowID
? `workflow-${workflowID}`
: `new-${selectedTemplate || "blank"}`
}
definition={definition}
nodeSpecs={nodeSpecs}
onDefinitionChange={handleDefinitionChange}
/>
) : (
+2
View File
@@ -450,6 +450,8 @@ export type AIWorkflowNodeSpec = {
title: string
description: string
icon: string
category: "trigger" | "control" | "ai" | "business" | "utility"
executable: boolean
riskLevel: "low" | "medium" | "high"
interruptible: boolean
requiresConfirmationPredecessor: boolean