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": "通知主题",