From 9cdc59b2726ce5701bd01e25e36f1d0d30ea8b82 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Thu, 22 May 2025 17:09:14 +0800 Subject: [PATCH] refactor code --- internal/domain/expr.go | 126 +++++++++---- internal/domain/workflow.go | 13 +- .../workflow/node-processor/apply_node.go | 10 +- internal/workflow/node-processor/const.go | 6 + .../workflow/node-processor/inspect_node.go | 176 +++++++++++------- .../workflow/node-processor/upload_node.go | 8 +- .../workflow/node/ConditionNode.tsx | 27 +-- .../workflow/node/ConditionNodeConfigForm.tsx | 26 +-- .../components/workflow/node/InspectNode.tsx | 2 +- .../workflow/node/InspectNodeConfigForm.tsx | 22 ++- ui/src/domain/workflow.ts | 52 ++++-- ui/src/i18n/locales/en/nls.workflow.json | 6 +- .../i18n/locales/en/nls.workflow.nodes.json | 4 + ui/src/i18n/locales/zh/nls.workflow.json | 6 +- .../i18n/locales/zh/nls.workflow.nodes.json | 4 + 15 files changed, 312 insertions(+), 176 deletions(-) create mode 100644 internal/workflow/node-processor/const.go diff --git a/internal/domain/expr.go b/internal/domain/expr.go index 9d1a744e..01730e3d 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr.go @@ -3,6 +3,7 @@ package domain import ( "encoding/json" "fmt" + "strconv" ) type Value any @@ -11,6 +12,7 @@ type ( ComparisonOperator string LogicalOperator string ValueType string + ExprType string ) const ( @@ -29,6 +31,12 @@ const ( Number ValueType = "number" String ValueType = "string" Boolean ValueType = "boolean" + + ConstExprType ExprType = "const" + VarExprType ExprType = "var" + CompareExprType ExprType = "compare" + LogicalExprType ExprType = "logical" + NotExprType ExprType = "not" ) type EvalResult struct { @@ -40,14 +48,40 @@ func (e *EvalResult) GetFloat64() (float64, error) { if e.Type != Number { return 0, fmt.Errorf("type mismatch: %s", e.Type) } - switch v := e.Value.(type) { - case int: - return float64(v), nil - case float64: - return v, nil - default: - return 0, fmt.Errorf("unsupported type: %T", v) + + stringValue, ok := e.Value.(string) + if !ok { + return 0, fmt.Errorf("value is not a string: %v", e.Value) } + + floatValue, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse float64: %v", err) + } + return floatValue, nil +} + +func (e *EvalResult) GetBool() (bool, error) { + if e.Type != Boolean { + return false, fmt.Errorf("type mismatch: %s", e.Type) + } + + strValue, ok := e.Value.(string) + if ok { + if strValue == "true" { + return true, nil + } else if strValue == "false" { + return false, nil + } + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + boolValue, ok := e.Value.(bool) + if !ok { + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + return boolValue, nil } func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { @@ -232,9 +266,17 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) && other.Value.(bool), + Value: left && right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -247,9 +289,17 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) || other.Value.(bool), + Value: left || right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -260,9 +310,13 @@ func (e *EvalResult) Not() (*EvalResult, error) { if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } + boolValue, err := e.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: !e.Value.(bool), + Value: !boolValue, }, nil } @@ -272,9 +326,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) == other.Value.(bool), + Value: left == right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -282,17 +344,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { } type Expr interface { - GetType() string + GetType() ExprType Eval(variables map[string]map[string]any) (*EvalResult, error) } type ConstExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` Value Value `json:"value"` ValueType ValueType `json:"valueType"` } -func (c ConstExpr) GetType() string { return c.Type } +func (c ConstExpr) GetType() ExprType { return c.Type } func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { return &EvalResult{ @@ -302,11 +364,11 @@ func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error } type VarExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` Selector WorkflowNodeIOValueSelector `json:"selector"` } -func (v VarExpr) GetType() string { return v.Type } +func (v VarExpr) GetType() ExprType { return v.Type } func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { @@ -330,13 +392,13 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) } type CompareExpr struct { - Type string `json:"type"` // compare + Type ExprType `json:"type"` // compare Op ComparisonOperator `json:"op"` Left Expr `json:"left"` Right Expr `json:"right"` } -func (c CompareExpr) GetType() string { return c.Type } +func (c CompareExpr) GetType() ExprType { return c.Type } func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) @@ -369,13 +431,13 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err } type LogicalExpr struct { - Type string `json:"type"` // logical + Type ExprType `json:"type"` // logical Op LogicalOperator `json:"op"` Left Expr `json:"left"` Right Expr `json:"right"` } -func (l LogicalExpr) GetType() string { return l.Type } +func (l LogicalExpr) GetType() ExprType { return l.Type } func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := l.Left.Eval(variables) @@ -398,11 +460,11 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, err } type NotExpr struct { - Type string `json:"type"` // not - Expr Expr `json:"expr"` + Type ExprType `json:"type"` // not + Expr Expr `json:"expr"` } -func (n NotExpr) GetType() string { return n.Type } +func (n NotExpr) GetType() ExprType { return n.Type } func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { inner, err := n.Expr.Eval(variables) @@ -413,7 +475,7 @@ func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) } type rawExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` } func MarshalExpr(e Expr) ([]byte, error) { @@ -427,31 +489,31 @@ func UnmarshalExpr(data []byte) (Expr, error) { } switch typ.Type { - case "const": + case ConstExprType: var e ConstExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case "var": + case VarExprType: var e VarExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case "compare": + case CompareExprType: var e CompareExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToCompareExpr() - case "logical": + case LogicalExprType: var e LogicalExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToLogicalExpr() - case "not": + case NotExprType: var e NotExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err @@ -463,7 +525,7 @@ func UnmarshalExpr(data []byte) (Expr, error) { } type CompareExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Op ComparisonOperator `json:"op"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` @@ -487,7 +549,7 @@ func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) { } type LogicalExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Op LogicalOperator `json:"op"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` @@ -511,7 +573,7 @@ func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { } type NotExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Expr json.RawMessage `json:"expr"` } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 8f6522a5..afa379a8 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -88,8 +88,10 @@ type WorkflowNodeConfigForCondition struct { } type WorkflowNodeConfigForInspect struct { + Host string `json:"host"` // 主机 Domain string `json:"domain"` // 域名 Port string `json:"port"` // 端口 + Path string `json:"path"` // 路径 } type WorkflowNodeConfigForUpload struct { @@ -134,9 +136,14 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { } func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { + host := maputil.GetString(n.Config, "host") + if host == "" { + return WorkflowNodeConfigForInspect{} + } + domain := maputil.GetString(n.Config, "domain") if domain == "" { - return WorkflowNodeConfigForInspect{} + domain = host } port := maputil.GetString(n.Config, "port") @@ -144,9 +151,13 @@ func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { port = "443" } + path := maputil.GetString(n.Config, "path") + return WorkflowNodeConfigForInspect{ Domain: domain, Port: port, + Host: host, + Path: path, } } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 7ace68ef..e5cc7274 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -100,8 +100,8 @@ func (n *applyNode) Process(ctx context.Context) error { } // 添加中间结果 - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) n.logger.Info("apply completed") @@ -146,9 +146,9 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24) + + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24)) return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go new file mode 100644 index 00000000..c1af01c9 --- /dev/null +++ b/internal/workflow/node-processor/const.go @@ -0,0 +1,6 @@ +package nodeprocessor + +const ( + outputCertificateValidatedKey = "certificate.validated" + outputCertificateDaysLeftKey = "certificate.daysLeft" +) diff --git a/internal/workflow/node-processor/inspect_node.go b/internal/workflow/node-processor/inspect_node.go index 6c6bea6a..a8661f37 100644 --- a/internal/workflow/node-processor/inspect_node.go +++ b/internal/workflow/node-processor/inspect_node.go @@ -3,9 +3,12 @@ package nodeprocessor import ( "context" "crypto/tls" + "crypto/x509" "fmt" "math" "net" + "net/http" + "strings" "time" "github.com/usual2970/certimate/internal/domain" @@ -26,13 +29,13 @@ func NewInspectNode(node *domain.WorkflowNode) *inspectNode { } func (n *inspectNode) Process(ctx context.Context) error { - n.logger.Info("enter inspect website certificate node ...") + n.logger.Info("entering inspect certificate node...") nodeConfig := n.node.GetConfigForInspect() err := n.inspect(ctx, nodeConfig) if err != nil { - n.logger.Warn("inspect website certificate failed: " + err.Error()) + n.logger.Warn("inspect certificate failed: " + err.Error()) return err } @@ -40,18 +43,35 @@ func (n *inspectNode) Process(ctx context.Context) error { } func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error { - // 定义重试参数 maxRetries := 3 retryInterval := 2 * time.Second - var cert *tls.Certificate var lastError error + var certInfo *x509.Certificate - domainWithPort := nodeConfig.Domain + ":" + nodeConfig.Port + host := nodeConfig.Host + + port := nodeConfig.Port + if port == "" { + port = "443" + } + + domain := nodeConfig.Domain + if domain == "" { + domain = host + } + + path := nodeConfig.Path + if path != "" && !strings.HasPrefix(path, "/") { + path = "/" + path + } + + targetAddr := fmt.Sprintf("%s:%s", host, port) + n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain)) for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { - n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, domainWithPort)) + n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr)) select { case <-ctx.Done(): return ctx.Err() @@ -60,30 +80,65 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod } } - dialer := &net.Dialer{ - Timeout: 10 * time.Second, + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + }).DialContext, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: domain, // Set SNI to domain for proper certificate selection + }, + ForceAttemptHTTP2: false, + DisableKeepAlives: true, } - conn, err := tls.DialWithDialer(dialer, "tcp", domainWithPort, &tls.Config{ - InsecureSkipVerify: true, // Allow self-signed certificates - }) + client := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + scheme := "https" + urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr) + if path != "" { + urlStr = urlStr + path + } + + req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil) if err != nil { - lastError = fmt.Errorf("failed to connect to %s: %w", domainWithPort, err) + lastError = fmt.Errorf("failed to create HTTP request: %w", err) + n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error())) + continue + } + + if domain != host { + req.Host = domain + } + + req.Header.Set("User-Agent", "CertificateValidator/1.0") + req.Header.Set("Accept", "*/*") + + resp, err := client.Do(req) + if err != nil { + lastError = fmt.Errorf("HTTP request failed: %w", err) n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error())) continue } - // Get certificate information - certInfo := conn.ConnectionState().PeerCertificates[0] - conn.Close() - - // Certificate information retrieved successfully - cert = &tls.Certificate{ - Certificate: [][]byte{certInfo.Raw}, - Leaf: certInfo, + if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { + resp.Body.Close() + lastError = fmt.Errorf("no TLS certificates received in HTTP response") + n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error())) + continue } + + certInfo = resp.TLS.PeerCertificates[0] + resp.Body.Close() + lastError = nil - n.logger.Info(fmt.Sprintf("Successfully retrieved certificate information for %s", domainWithPort)) + n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr)) break } @@ -91,69 +146,46 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError) } - certInfo := cert.Leaf - now := time.Now() - - isValid := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) - - // Check domain matching - domainMatch := false - if len(certInfo.DNSNames) > 0 { - for _, dnsName := range certInfo.DNSNames { - if matchDomain(nodeConfig.Domain, dnsName) { - domainMatch = true - break - } + if certInfo == nil { + outputs := map[string]any{ + outputCertificateValidatedKey: "false", + outputCertificateDaysLeftKey: "0", } - } else if matchDomain(nodeConfig.Domain, certInfo.Subject.CommonName) { - domainMatch = true + n.setOutputs(outputs) + return nil } - isValid = isValid && domainMatch + now := time.Now() + + isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) + + domainMatch := true + if err := certInfo.VerifyHostname(domain); err != nil { + domainMatch = false + } + + isValid := isValidTime && domainMatch daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24) - // Set node outputs - outputs := map[string]any{ - "certificate.validated": isValid, - "certificate.daysLeft": daysRemaining, + isValidStr := "false" + if isValid { + isValidStr = "true" } + + outputs := map[string]any{ + outputCertificateValidatedKey: isValidStr, + outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)), + } + n.setOutputs(outputs) + n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d", + targetAddr, domain, isValidStr, int(daysRemaining))) + return nil } func (n *inspectNode) setOutputs(outputs map[string]any) { n.outputs = outputs } - -func matchDomain(requestDomain, certDomain string) bool { - if requestDomain == certDomain { - return true - } - - if len(certDomain) > 2 && certDomain[0] == '*' && certDomain[1] == '.' { - - wildcardSuffix := certDomain[1:] - requestDomainLen := len(requestDomain) - suffixLen := len(wildcardSuffix) - - if requestDomainLen > suffixLen && requestDomain[requestDomainLen-suffixLen:] == wildcardSuffix { - remainingPart := requestDomain[:requestDomainLen-suffixLen] - if len(remainingPart) > 0 && !contains(remainingPart, '.') { - return true - } - } - } - - return false -} - -func contains(s string, c byte) bool { - for i := 0; i < len(s); i++ { - if s[i] == c { - return true - } - } - return false -} diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 7fbb1515..ab86807e 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -69,8 +69,8 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) n.logger.Info("upload completed") @@ -91,8 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id) if lastCertificate != nil { - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(lastCertificate.ExpireAt).Hours()/24)) return true, "the certificate has already been uploaded" } } diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 417db4af..bcd58c77 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow"; +import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow"; import { produce } from "immer"; import { useWorkflowStore } from "@/stores/workflow"; import { useZustandShallowSelector } from "@/hooks"; @@ -32,7 +32,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const selectors = condition.leftSelector.split("#"); const t = selectors[2] as WorkflowNodeIoValueType; const left: Expr = { - type: "var", + type: ExprType.Var, selector: { id: selectors[0], name: selectors[1], @@ -40,27 +40,10 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP }, }; - let value: Value = condition.rightValue; - switch (t) { - case "boolean": - if (value === "true") { - value = true; - } else if (value === "false") { - value = false; - } - break; - case "number": - value = parseInt(value as string); - break; - case "string": - value = value as string; - break; - } - - const right: Expr = { type: "const", value: value, valueType: t }; + const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t }; return { - type: "compare", + type: ExprType.Compare, op: condition.operator, left, right, @@ -77,7 +60,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP for (let i = 1; i < values.conditions.length; i++) { expr = { - type: "logical", + type: ExprType.Logical, op: values.logicalOperator, left: expr, right: createComparisonExpr(values.conditions[i]), diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 81022a28..9cbb56cc 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -14,6 +14,7 @@ import { WorkflowNode, workflowNodeIOOptions, WorkflowNodeIoValueType, + ExprType, } from "@/domain/workflow"; import { FormInstance } from "antd"; import { useZustandShallowSelector } from "@/hooks"; @@ -58,7 +59,7 @@ const initFormModel = (): ConditionNodeConfigFormFieldValues => { rightValue: "", }, ], - logicalOperator: "and", + logicalOperator: LogicalOperator.And, }; }; @@ -67,10 +68,10 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { if (!expr) return initFormModel(); const conditions: ConditionItem[] = []; - let logicalOp: LogicalOperator = "and"; + let logicalOp: LogicalOperator = LogicalOperator.And; const extractComparisons = (expr: Expr): void => { - if (expr.type === "compare") { + if (expr.type === ExprType.Compare) { // 确保左侧是变量,右侧是常量 if (isVarExpr(expr.left) && isConstExpr(expr.right)) { conditions.push({ @@ -79,7 +80,7 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { rightValue: String(expr.right.value), }); } - } else if (expr.type === "logical") { + } else if (expr.type === ExprType.Logical) { logicalOp = expr.op; extractComparisons(expr.left); extractComparisons(expr.right); @@ -304,25 +305,18 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { const type = typeStr as WorkflowNodeIoValueType; const left: Expr = { - type: "var", + type: ExprType.Var, selector: { id, name, type }, }; - let rightValue: any = condition.rightValue; - if (type === "number") { - rightValue = Number(condition.rightValue); - } else if (type === "boolean") { - rightValue = condition.rightValue === "true"; - } - const right: Expr = { - type: "const", - value: rightValue, + type: ExprType.Const, + value: condition.rightValue, valueType: type, }; return { - type: "compare", + type: ExprType.Compare, op: condition.operator, left, right, @@ -339,7 +333,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { for (let i = 1; i < values.conditions.length; i++) { expr = { - type: "logical", + type: ExprType.Logical, op: values.logicalOperator, left: expr, right: createComparisonExpr(values.conditions[i]), diff --git a/ui/src/components/workflow/node/InspectNode.tsx b/ui/src/components/workflow/node/InspectNode.tsx index fa4324e2..0d038894 100644 --- a/ui/src/components/workflow/node/InspectNode.tsx +++ b/ui/src/components/workflow/node/InspectNode.tsx @@ -39,7 +39,7 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => { const config = (node.config as WorkflowNodeConfigForInspect) ?? {}; return ( - {config.domain ?? ""} + {config.host ?? ""} ); }, [node]); diff --git a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx index ea9573e5..2d7d83b0 100644 --- a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { type WorkflowNodeConfigForInspect } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; -import { validDomainName, validPortNumber } from "@/utils/validators"; +import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators"; type InspectNodeConfigFormFieldValues = Partial; @@ -29,6 +29,8 @@ const initFormModel = (): InspectNodeConfigFormFieldValues => { return { domain: "", port: "443", + path: "", + host: "", }; }; @@ -37,12 +39,14 @@ const InspectNodeConfigForm = forwardRef validDomainName(val), { - message: t("workflow_node.inspect.form.domain.placeholder"), + host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), { + message: t("workflow_node.inspect.form.host.placeholder"), }), + domain: z.string().optional(), port: z.string().refine((val) => validPortNumber(val), { message: t("workflow_node.inspect.form.port.placeholder"), }), + path: z.string().optional(), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm({ @@ -70,13 +74,21 @@ const InspectNodeConfigForm = forwardRef - - + + + + + + + + + + ); } diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 6b951b49..5a3e9821 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -67,7 +67,7 @@ const workflowNodeTypeDefaultInputs: Map = n name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -82,7 +82,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -93,7 +93,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -104,7 +104,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -161,6 +161,8 @@ export type WorkflowNodeConfigForUpload = { export type WorkflowNodeConfigForInspect = { domain: string; port: string; + host: string; + path: string; }; export type WorkflowNodeConfigForDeploy = { @@ -200,14 +202,20 @@ export type WorkflowNodeIO = { valueSelector?: WorkflowNodeIOValueSelector; }; +export const VALUE_TYPES = Object.freeze({ + STRING: "string", + NUMBER: "number", + BOOLEAN: "boolean", +} as const); + +export type WorkflowNodeIoValueType = (typeof VALUE_TYPES)[keyof typeof VALUE_TYPES]; + export type WorkflowNodeIOValueSelector = { id: string; name: string; type: WorkflowNodeIoValueType; }; -export type WorkflowNodeIoValueType = "string" | "number" | "boolean"; - type WorkflowNodeIOOptions = { label: string; value: string; @@ -224,12 +232,12 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => { switch (output.type) { case "certificate": rs.options.push({ - label: `${node.name} - ${output.label} - 是否有效`, + label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.is_validated.label")}`, value: `${node.id}#${output.name}.validated#boolean`, }); rs.options.push({ - label: `${node.name} - ${output.label} - 剩余天数`, + label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.days_left.label")}`, value: `${node.id}#${output.name}.daysLeft#number`, }); break; @@ -254,22 +262,34 @@ export type Value = string | number | boolean; export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; -export type LogicalOperator = "and" | "or" | "not"; +export enum LogicalOperator { + And = "and", + Or = "or", + Not = "not", +} -export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType }; -export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector }; -export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr }; -export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr }; -export type NotExpr = { type: "not"; expr: Expr }; +export enum ExprType { + Const = "const", + Var = "var", + Compare = "compare", + Logical = "logical", + Not = "not", +} + +export type ConstExpr = { type: ExprType.Const; value: string; valueType: WorkflowNodeIoValueType }; +export type VarExpr = { type: ExprType.Var; selector: WorkflowNodeIOValueSelector }; +export type CompareExpr = { type: ExprType.Compare; op: ComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: ExprType.Logical; op: LogicalOperator; left: Expr; right: Expr }; +export type NotExpr = { type: ExprType.Not; expr: Expr }; export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; export const isConstExpr = (expr: Expr): expr is ConstExpr => { - return expr.type === "const"; + return expr.type === ExprType.Const; }; export const isVarExpr = (expr: Expr): expr is VarExpr => { - return expr.type === "var"; + return expr.type === ExprType.Var; }; // #endregion diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index b4f9d7e6..cdf722a0 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -53,5 +53,9 @@ "workflow.detail.orchestration.action.run": "Run", "workflow.detail.orchestration.action.run.confirm": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?", "workflow.detail.orchestration.action.run.prompt": "Running... Please check the history later", - "workflow.detail.runs.tab": "History runs" + "workflow.detail.runs.tab": "History runs", + + "workflow.variables.is_validated.label": "Is valid", + "workflow.variables.days_left.label": "Days left", + "workflow.variables.certificate.label": "Certificate" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 2555c36e..b70e38de 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -806,6 +806,10 @@ "workflow_node.inspect.form.domain.placeholder": "Please enter domain name", "workflow_node.inspect.form.port.label": "Port", "workflow_node.inspect.form.port.placeholder": "Please enter port", + "workflow_node.inspect.form.host.label": "Host", + "workflow_node.inspect.form.host.placeholder": "Please enter host", + "workflow_node.inspect.form.path.label": "Path", + "workflow_node.inspect.form.path.placeholder": "Please enter path", "workflow_node.notify.label": "Notification", "workflow_node.notify.form.subject.label": "Subject", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 46cdc228..e86e796a 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -53,5 +53,9 @@ "workflow.detail.orchestration.action.run": "执行", "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", - "workflow.detail.runs.tab": "执行历史" + "workflow.detail.runs.tab": "执行历史", + + "workflow.variables.is_validated.label": "是否有效", + "workflow.variables.days_left.label": "剩余天数", + "workflow.variables.certificate.label": "证书" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 206daeeb..722568fe 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -805,6 +805,10 @@ "workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名", "workflow_node.inspect.form.port.label": "端口号", "workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号", + "workflow_node.inspect.form.host.label": "Host", + "workflow_node.inspect.form.host.placeholder": "请输入 Host", + "workflow_node.inspect.form.path.label": "Path", + "workflow_node.inspect.form.path.placeholder": "请输入 Path", "workflow_node.notify.label": "推送通知", "workflow_node.notify.form.subject.label": "通知主题",