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
@@ -251,6 +251,8 @@ func DefaultRegistry() *Registry {
InputSchema: []VariableSpec{
requiredInput("ticketDraft", "工单草稿", VariableTypeObject, "已经由客户确认的工单草稿内容。"),
requiredInput("confirmed", "已确认", VariableTypeBoolean, "客户是否已确认创建工单。"),
optionalInput("tagIds", "工单标签", VariableTypeIntegerArray, "创建工单时附加的标签 ID 列表。"),
optionalInput("assigneeId", "处理人", VariableTypeInteger, "创建工单后默认指派的客服用户 ID。"),
},
OutputSchema: []VariableSpec{
output("ticketId", "工单 ID", VariableTypeInteger, "创建成功后的工单内部编号。"),
@@ -2,6 +2,22 @@ package registry
import "testing"
func TestDefaultRegistryMarksOnlyRuntimeSupportedNodesExecutable(t *testing.T) {
registry := DefaultRegistry()
for _, nodeType := range []string{NodeTypeCreateTicket, NodeTypeHumanConfirm, NodeTypeSendReply, NodeTypeLLM} {
spec, ok := registry.Get(nodeType)
if !ok || !spec.Executable {
t.Fatalf("expected %s to be executable, got %#v", nodeType, spec)
}
}
for _, nodeType := range []string{NodeTypeHTTP, NodeTypeCode, NodeTypeLoop} {
spec, ok := registry.Get(nodeType)
if !ok || spec.Executable {
t.Fatalf("expected %s to be unavailable in server runtime, got %#v", nodeType, spec)
}
}
}
func TestDefaultRegistryVariablesHaveBusinessLabels(t *testing.T) {
for _, spec := range DefaultRegistry().List() {
for _, variable := range append(spec.InputSchema, spec.OutputSchema...) {
+44
View File
@@ -45,6 +45,8 @@ type NodeSpec struct {
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
Category string `json:"category"`
Executable bool `json:"executable"`
RiskLevel NodeRiskLevel `json:"riskLevel"`
Interruptible bool `json:"interruptible"`
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
@@ -68,12 +70,54 @@ func NewRegistry(specs ...NodeSpec) *Registry {
if spec.Type == "" {
continue
}
spec.Executable = IsExecutableNodeType(spec.Type)
if spec.Category == "" {
spec.Category = NodeCategory(spec.Type)
}
ret.specsByType[spec.Type] = spec
ret.specs = append(ret.specs, spec)
}
return ret
}
func IsExecutableNodeType(nodeType string) bool {
switch nodeType {
case NodeTypeStart,
NodeTypeConversationUnderstanding,
NodeTypeReplyPolicy,
NodeTypeKnowledgeRetrieve,
NodeTypeAnswerabilityGate,
NodeTypeCondition,
NodeTypeAnalyzeConversation,
NodeTypePrepareTicketDraft,
NodeTypeHumanConfirm,
NodeTypeCreateTicket,
NodeTypeLLMReply,
NodeTypeLLM,
NodeTypeSendReply,
NodeTypeHandoffToHuman,
NodeTypeEnd:
return true
default:
return false
}
}
func NodeCategory(nodeType string) string {
switch nodeType {
case NodeTypeStart, NodeTypeEnd:
return "trigger"
case NodeTypeCondition, NodeTypeMultiCondition, NodeTypeLoop, NodeTypeBlockStart, NodeTypeBlockEnd, NodeTypeContinue, NodeTypeBreak:
return "control"
case NodeTypeConversationUnderstanding, NodeTypeReplyPolicy, NodeTypeAnswerabilityGate, NodeTypeAnalyzeConversation, NodeTypeLLMReply, NodeTypeLLM, NodeTypeKnowledgeRetrieve:
return "ai"
case NodeTypePrepareTicketDraft, NodeTypeHumanConfirm, NodeTypeCreateTicket, NodeTypeHandoffToHuman, NodeTypeSendReply:
return "business"
default:
return "utility"
}
}
func (r *Registry) Get(nodeType string) (NodeSpec, bool) {
if r == nil {
return NodeSpec{}, false
@@ -82,6 +82,10 @@ func (v *definitionValidator) validateNodes() {
v.addError(field+".type", "unknown node type: "+node.Type)
continue
}
if !registry.IsExecutableNodeType(node.Type) {
v.addError(field+".type", "node type is not supported by the server runtime: "+node.Type)
continue
}
switch node.Type {
case registry.NodeTypeStart:
v.startNodeIDs = append(v.startNodeIDs, node.ID)
@@ -18,6 +18,23 @@ func TestValidateDefinitionAcceptsMinimalFlowGramStyleFlow(t *testing.T) {
}
}
func TestValidateDefinitionRejectsNodeMissingFromServerRuntime(t *testing.T) {
def := dsl.Definition{
Nodes: []dsl.Node{
node("start_1", "start", nil, nil),
node("http_1", "http", nil, nil),
node("end_1", "end", nil, nil),
},
Edges: []dsl.Edge{edge("start_1", "http_1"), edge("http_1", "end_1")},
}
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
if result.Valid || !hasValidationMessage(result, "not supported by the server runtime") {
t.Fatalf("expected unsupported-runtime error, got %#v", result.Errors)
}
}
func TestValidateDefinitionAcceptsOfficialFlowGramCondition(t *testing.T) {
def := dsl.Definition{
Nodes: []dsl.Node{