// Package dsl 负责把客户端导出的 JSON DSL 解析并组装为可调度的 Task。 package dsl import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "github.com/sundynix/sundynix-shared/contract" ) // ParseAndAssemble 校验 DSL 结构并生成共享契约中的 Task。 func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) { if len(raw) == 0 { return nil, errors.New("empty dsl") } // 轻量结构校验:至少要能解析为对象。 var probe map[string]json.RawMessage if err := json.Unmarshal(raw, &probe); err != nil { return nil, errors.New("invalid dsl json: " + err.Error()) } // 拓扑校验:前置拦截明确的结构错误(重复/空 id、悬挂边), // 避免坏图进编排后被 compose 静默跳过(dangling edge 会被 nodeByID 检查悄悄丢弃)。 if err := validateTopology(raw); err != nil { return nil, err } return &contract.Task{ ID: newID(), Graph: raw, Meta: map[string]any{}, }, nil } // validateTopology 做保守的拓扑校验:只拦不含糊的结构错误,对空图/非标准图(如报告任务的 // {topic:...})与无法按图结构解析的载荷一律宽松放过,杜绝误伤合法提交。 // - 节点 id 不得为空、不得重复 // - 每条边的 source/target 不得为空、且必须指向存在的节点(悬挂边直接拒) func validateTopology(raw json.RawMessage) error { var g struct { Nodes []struct { ID string `json:"id"` } `json:"nodes"` Edges []struct { Source string `json:"source"` Target string `json:"target"` } `json:"edges"` } if err := json.Unmarshal(raw, &g); err != nil { return nil // 非标准图结构(字段类型不匹配)→ 跳过拓扑校验,不误伤 } ids := make(map[string]bool, len(g.Nodes)) for i, n := range g.Nodes { if n.ID == "" { return fmt.Errorf("dsl 校验失败:节点[%d] 缺少 id", i) } if ids[n.ID] { return fmt.Errorf("dsl 校验失败:节点 id 重复:%s", n.ID) } ids[n.ID] = true } for i, e := range g.Edges { if e.Source == "" || e.Target == "" { return fmt.Errorf("dsl 校验失败:边[%d] 的 source/target 不能为空", i) } if !ids[e.Source] { return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的源节点:%s", i, e.Source) } if !ids[e.Target] { return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的目标节点:%s", i, e.Target) } } return nil } func newID() string { var b [8]byte _, _ = rand.Read(b[:]) return "task_" + hex.EncodeToString(b[:]) }