mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
160 lines
5.0 KiB
Go
160 lines
5.0 KiB
Go
package engine
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/lyzrai/flow/pkg/models"
|
||
|
|
)
|
||
|
|
|
||
|
|
// n8nWorkflowJSON mirrors the raw n8n export format for unmarshalling.
|
||
|
|
type n8nWorkflowJSON struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
Nodes []models.NodeDef `json:"nodes"`
|
||
|
|
Connections map[string]struct {
|
||
|
|
Main []json.RawMessage `json:"main"`
|
||
|
|
} `json:"connections"`
|
||
|
|
Settings map[string]any `json:"settings"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type connectionTarget struct {
|
||
|
|
Node string `json:"node"`
|
||
|
|
Type string `json:"type"`
|
||
|
|
Index int `json:"index"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// n8nToFlow maps specific n8n node types to flow-native equivalents.
|
||
|
|
// Unmapped types are translated automatically via prefix replacement.
|
||
|
|
var n8nToFlow = map[string]string{
|
||
|
|
// All triggers → single flow trigger type
|
||
|
|
"n8n-nodes-base.webhook": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.scheduleTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.manualTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.formTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.activationTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.errorTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.executeWorkflowTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.n8nTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.sseTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.workflowTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.localFileTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.emailReadImap": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.rssFeedReadTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.evaluationTrigger": "flow-nodes-base.trigger",
|
||
|
|
|
||
|
|
"n8n-nodes-base.start": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.cron": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-base.interval": "flow-nodes-base.trigger",
|
||
|
|
|
||
|
|
"n8n-nodes-base.function": "flow-nodes-base.code",
|
||
|
|
"n8n-nodes-base.functionItem": "flow-nodes-base.code",
|
||
|
|
|
||
|
|
"n8n-nodes-base.spreadsheetFile": "flow-nodes-base.convertToFile",
|
||
|
|
"n8n-nodes-base.moveBinaryData": "flow-nodes-base.convertToFile",
|
||
|
|
"n8n-nodes-base.readBinaryFile": "flow-nodes-base.readWriteFile",
|
||
|
|
"n8n-nodes-base.readBinaryFiles": "flow-nodes-base.readWriteFile",
|
||
|
|
"n8n-nodes-base.writeBinaryFile": "flow-nodes-base.readWriteFile",
|
||
|
|
"n8n-nodes-base.readPdf": "flow-nodes-base.extractFromFile",
|
||
|
|
"n8n-nodes-base.htmlExtract": "flow-nodes-base.html",
|
||
|
|
"n8n-nodes-base.transform": "flow-nodes-base.set",
|
||
|
|
"n8n-nodes-base.noop": "flow-nodes-base.noOp",
|
||
|
|
|
||
|
|
"n8n-nodes-langchain.chatTrigger": "flow-nodes-base.trigger",
|
||
|
|
"n8n-nodes-langchain.mcpTrigger": "flow-nodes-base.trigger",
|
||
|
|
}
|
||
|
|
|
||
|
|
// TranslateNodeType converts an n8n node type string to its flow-native equivalent.
|
||
|
|
// Types already using the flow-nodes-base prefix are returned as-is.
|
||
|
|
func TranslateNodeType(n8nType string) string {
|
||
|
|
if strings.HasPrefix(n8nType, "flow-nodes-base.") {
|
||
|
|
return n8nType
|
||
|
|
}
|
||
|
|
|
||
|
|
cleaned := strings.TrimPrefix(n8nType, "@n8n/")
|
||
|
|
|
||
|
|
if flowType, ok := n8nToFlow[cleaned]; ok {
|
||
|
|
return flowType
|
||
|
|
}
|
||
|
|
|
||
|
|
if strings.HasPrefix(cleaned, "n8n-nodes-base.") {
|
||
|
|
return strings.Replace(cleaned, "n8n-nodes-base.", "flow-nodes-base.", 1)
|
||
|
|
}
|
||
|
|
if strings.HasPrefix(cleaned, "n8n-nodes-langchain.") {
|
||
|
|
return strings.Replace(cleaned, "n8n-nodes-langchain.", "flow-nodes-base.", 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
return cleaned
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseWorkflow parses raw n8n workflow JSON bytes into a WorkflowDefinition.
|
||
|
|
func ParseWorkflow(data []byte) (*models.WorkflowDefinition, error) {
|
||
|
|
var raw n8nWorkflowJSON
|
||
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to unmarshal workflow JSON: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(raw.Nodes) == 0 {
|
||
|
|
return nil, fmt.Errorf("workflow has no nodes")
|
||
|
|
}
|
||
|
|
|
||
|
|
for i := range raw.Nodes {
|
||
|
|
raw.Nodes[i].Type = TranslateNodeType(raw.Nodes[i].Type)
|
||
|
|
}
|
||
|
|
|
||
|
|
triggerCount := 0
|
||
|
|
for _, n := range raw.Nodes {
|
||
|
|
if n.Type == "flow-nodes-base.trigger" {
|
||
|
|
triggerCount++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if triggerCount == 0 {
|
||
|
|
return nil, fmt.Errorf("workflow must have exactly one trigger node (found 0)")
|
||
|
|
}
|
||
|
|
if triggerCount > 1 {
|
||
|
|
return nil, fmt.Errorf("workflow must have exactly one trigger node (found %d)", triggerCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
nodeNames := make(map[string]bool, len(raw.Nodes))
|
||
|
|
for _, n := range raw.Nodes {
|
||
|
|
nodeNames[n.Name] = true
|
||
|
|
}
|
||
|
|
|
||
|
|
var connections []models.ConnectionDef
|
||
|
|
|
||
|
|
for sourceName, connData := range raw.Connections {
|
||
|
|
if !nodeNames[sourceName] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
for outputIndex, rawTargets := range connData.Main {
|
||
|
|
var targets []connectionTarget
|
||
|
|
if err := json.Unmarshal(rawTargets, &targets); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, t := range targets {
|
||
|
|
if t.Node == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !nodeNames[t.Node] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
connections = append(connections, models.ConnectionDef{
|
||
|
|
SourceNode: sourceName,
|
||
|
|
SourceOutputIndex: outputIndex,
|
||
|
|
TargetNode: t.Node,
|
||
|
|
TargetInputIndex: t.Index,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return &models.WorkflowDefinition{
|
||
|
|
Name: raw.Name,
|
||
|
|
Nodes: raw.Nodes,
|
||
|
|
Connections: connections,
|
||
|
|
Settings: raw.Settings,
|
||
|
|
}, nil
|
||
|
|
}
|