新版本流程编辑器

This commit is contained in:
mlogclub
2026-07-27 19:04:15 +08:00
parent d33960961a
commit e072141953
281 changed files with 25145 additions and 3405 deletions
+16 -4
View File
@@ -5,10 +5,11 @@ import "encoding/json"
const SchemaVersion = 2
type Definition struct {
SchemaVersion int `json:"schemaVersion"`
Nodes []Node `json:"nodes"`
Annotations []Node `json:"annotations,omitempty"`
Edges []Edge `json:"edges"`
SchemaVersion int `json:"schemaVersion,omitempty"`
Nodes []Node `json:"nodes"`
Annotations []Node `json:"annotations,omitempty"`
Edges []Edge `json:"edges"`
GlobalVariable json.RawMessage `json:"globalVariable,omitempty"`
}
type Node struct {
@@ -79,6 +80,17 @@ type Condition struct {
Right any `json:"right,omitempty"`
}
type FlowGramConditionItem struct {
Key string `json:"key"`
Value FlowGramCondition `json:"value"`
}
type FlowGramCondition struct {
Left Value `json:"left"`
Operator string `json:"operator"`
Right Value `json:"right"`
}
func RefValue(nodeID string, field string) Value {
return Value{Type: ValueTypeRef, Content: []string{nodeID, field}}
}
+42
View File
@@ -9,6 +9,18 @@ const (
NodeTypeKnowledgeRetrieve = "knowledge_retrieve"
NodeTypeAnswerabilityGate = "answerability_gate"
NodeTypeLLMReply = "llm_reply"
NodeTypeLLM = "llm"
NodeTypeHTTP = "http"
NodeTypeCode = "code"
NodeTypeVariable = "variable"
NodeTypeMultiCondition = "multi-condition"
NodeTypeLoop = "loop"
NodeTypeBlockStart = "block-start"
NodeTypeBlockEnd = "block-end"
NodeTypeComment = "comment"
NodeTypeContinue = "continue"
NodeTypeBreak = "break"
NodeTypeGroup = "group"
NodeTypeCondition = "condition"
NodeTypeAnalyzeConversation = "analyze_conversation"
NodeTypePrepareTicketDraft = "prepare_ticket_draft"
@@ -297,9 +309,39 @@ func DefaultRegistry() *Registry {
output("status", "结束状态", VariableTypeString, "工作流执行结束时的状态。"),
},
},
NodeSpec{
Type: NodeTypeLLM,
Title: "LLM",
Description: "Call the large language model and generate responses.",
RiskLevel: NodeRiskLevelLow,
OutputSchema: []VariableSpec{
output("result", "Result", VariableTypeString, "The generated model response."),
},
},
officialNodeSpec(NodeTypeHTTP, "HTTP", "Send an HTTP request."),
officialNodeSpec(NodeTypeCode, "Code", "Run JavaScript code."),
officialNodeSpec(NodeTypeVariable, "Variable", "Assign workflow variables."),
officialNodeSpec(NodeTypeMultiCondition, "Multi Condition", "Route through multiple condition branches."),
officialNodeSpec(NodeTypeLoop, "Loop", "Iterate over an array in a sub-canvas."),
officialNodeSpec(NodeTypeBlockStart, "Block Start", "Start a container block."),
officialNodeSpec(NodeTypeBlockEnd, "Block End", "End a container block."),
officialNodeSpec(NodeTypeComment, "Comment", "Add a canvas annotation."),
officialNodeSpec(NodeTypeContinue, "Continue", "Continue the current loop."),
officialNodeSpec(NodeTypeBreak, "Break", "Break the current loop."),
officialNodeSpec(NodeTypeGroup, "Group", "Group related workflow nodes."),
)
}
func officialNodeSpec(nodeType string, title string, description string) NodeSpec {
return NodeSpec{
Type: nodeType,
Title: title,
Description: description,
Icon: "",
RiskLevel: NodeRiskLevelLow,
}
}
func requiredInput(name string, label string, variableType VariableType, description string) VariableSpec {
return VariableSpec{Name: name, Label: label, Type: variableType, Required: true, Description: description}
}
@@ -257,6 +257,10 @@ func (v *definitionValidator) validateConditions() {
if strings.TrimSpace(node.Type) != registry.NodeTypeCondition {
continue
}
if rawConditions, ok := node.Data.Extra["conditions"]; ok {
v.validateFlowGramConditions(index, node, rawConditions)
continue
}
field := fmt.Sprintf("nodes[%d].config.branches", index)
config := dsl.ConditionConfig{}
if len(node.Data.Config) > 0 {
@@ -307,6 +311,72 @@ func (v *definitionValidator) validateConditions() {
}
}
func (v *definitionValidator) validateFlowGramConditions(index int, node dsl.Node, raw json.RawMessage) {
field := fmt.Sprintf("nodes[%d].data.conditions", index)
var conditions []dsl.FlowGramConditionItem
if err := json.Unmarshal(raw, &conditions); err != nil {
v.addError(field, "condition data must be valid JSON")
return
}
if len(conditions) == 0 {
v.addError(field, "condition node must include at least one condition")
return
}
seenKeys := make(map[string]struct{}, len(conditions))
for conditionIndex, item := range conditions {
itemField := fmt.Sprintf("%s[%d]", field, conditionIndex)
key := strings.TrimSpace(item.Key)
if key == "" {
v.addError(itemField+".key", "condition key is required")
} else if _, exists := seenKeys[key]; exists {
v.addError(itemField+".key", "duplicate condition key: "+key)
}
seenKeys[key] = struct{}{}
if !v.hasConditionPortEdge(strings.TrimSpace(node.ID), key) {
v.addError(itemField+".key", "condition output port must have an outgoing edge: "+key)
}
v.validateFlowGramCondition(itemField+".value", strings.TrimSpace(node.ID), item.Value)
}
if !v.hasConditionPortEdge(strings.TrimSpace(node.ID), "else") {
v.addError(field, "condition else port must have an outgoing edge")
}
}
func (v *definitionValidator) validateFlowGramCondition(field string, sourceNodeID string, condition dsl.FlowGramCondition) {
operator := strings.TrimSpace(condition.Operator)
if !isSupportedConditionOperator(operator) {
v.addError(field+".operator", "unsupported condition operator: "+operator)
return
}
sourceSelectorNodeID, sourceField, ok := condition.Left.Ref()
sourceSelectorNodeID = strings.TrimSpace(sourceSelectorNodeID)
sourceField = strings.TrimSpace(sourceField)
if !ok || sourceSelectorNodeID == "" || sourceField == "" {
v.addError(field+".left", "condition left variable is required")
return
}
if _, exists := v.nodesByID[sourceSelectorNodeID]; !exists {
v.addError(field+".left", "condition source node does not exist: "+sourceSelectorNodeID)
return
}
if sourceNodeID != "" && !v.hasPath(sourceSelectorNodeID, sourceNodeID, make(map[string]struct{})) && sourceSelectorNodeID != sourceNodeID {
v.addError(field+".left", "condition source node is not available before branch: "+sourceSelectorNodeID)
}
if !conditionOperatorWithoutRight(operator) && condition.Right.Type == "" {
v.addError(field+".right", "condition comparison value is required")
}
}
func (v *definitionValidator) hasConditionPortEdge(sourceID string, sourcePortID string) bool {
for _, edge := range v.def.Edges {
if strings.TrimSpace(edge.SourceNodeID) == sourceID &&
strings.TrimSpace(edge.SourcePortID) == sourcePortID {
return true
}
}
return false
}
func (v *definitionValidator) validateKnowledgeRetrieveConfigs() {
for index, node := range v.def.Nodes {
if strings.TrimSpace(node.Type) != registry.NodeTypeKnowledgeRetrieve {
@@ -18,6 +18,46 @@ func TestValidateDefinitionAcceptsMinimalFlowGramStyleFlow(t *testing.T) {
}
}
func TestValidateDefinitionAcceptsOfficialFlowGramCondition(t *testing.T) {
def := dsl.Definition{
Nodes: []dsl.Node{
node("start_0", "start", nil, nil),
{
ID: "condition_0",
Type: "condition",
Data: dsl.NodeData{
Title: "Condition",
Extra: map[string]json.RawMessage{
"conditions": mustJSON([]dsl.FlowGramConditionItem{
{
Key: "if_0",
Value: dsl.FlowGramCondition{
Left: dsl.RefValue("start_0", "query"),
Operator: "contains",
Right: dsl.ConstantValue("hello"),
},
},
}),
},
},
},
node("matched_end", "end", nil, nil),
node("else_end", "end", nil, nil),
},
Edges: []dsl.Edge{
edge("start_0", "condition_0"),
portEdge("condition_0", "matched_end", "if_0"),
portEdge("condition_0", "else_end", "else"),
},
}
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
if !result.Valid {
t.Fatalf("expected official FlowGram condition to be valid, got %#v", result.Errors)
}
}
func TestValidateDefinitionRejectsMissingStart(t *testing.T) {
def := minimalDefinition()
def.Nodes = []dsl.Node{