feat: allow skip notify nodes when all previous nodes were skipped

This commit is contained in:
Fu Diwei 2025-06-09 20:39:23 +08:00
parent 84a3f3346a
commit 24fe824757
13 changed files with 126 additions and 74 deletions

View File

@ -112,6 +112,7 @@ type WorkflowNodeConfigForNotify struct {
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置 ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
Subject string `json:"subject"` // 通知主题 Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容 Message string `json:"message"` // 通知内容
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
} }
type WorkflowNodeConfigForCondition struct { type WorkflowNodeConfigForCondition struct {
@ -175,6 +176,7 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"), ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
Subject: maputil.GetString(n.Config, "subject"), Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"), Message: maputil.GetString(n.Config, "message"),
SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"),
} }
} }

View File

@ -112,6 +112,7 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
break break
} }
// TODO: 优化可读性 // TODO: 优化可读性
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition { if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
current = nil current = nil

View File

@ -47,6 +47,7 @@ func (n *applyNode) Process(ctx context.Context) error {
// 检测是否可以跳过本次执行 // 检测是否可以跳过本次执行
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
n.logger.Info(fmt.Sprintf("skip this application, because %s", reason)) n.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
return nil return nil
} else if reason != "" { } else if reason != "" {
@ -112,6 +113,7 @@ func (n *applyNode) Process(ctx context.Context) error {
} }
// 记录中间结果 // 记录中间结果
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
@ -122,39 +124,40 @@ func (n *applyNode) Process(ctx context.Context) error {
func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
if lastOutput != nil && lastOutput.Succeeded { if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
currentNodeConfig := n.node.GetConfigForApply() thisNodeCfg := n.node.GetConfigForApply()
lastNodeConfig := lastOutput.Node.GetConfigForApply() lastNodeCfg := lastOutput.Node.GetConfigForApply()
if currentNodeConfig.Domains != lastNodeConfig.Domains {
if thisNodeCfg.Domains != lastNodeCfg.Domains {
return false, "the configuration item 'Domains' changed" return false, "the configuration item 'Domains' changed"
} }
if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail { if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail {
return false, "the configuration item 'ContactEmail' changed" return false, "the configuration item 'ContactEmail' changed"
} }
if currentNodeConfig.Provider != lastNodeConfig.Provider { if thisNodeCfg.Provider != lastNodeCfg.Provider {
return false, "the configuration item 'Provider' changed" return false, "the configuration item 'Provider' changed"
} }
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
return false, "the configuration item 'ProviderAccessId' changed" return false, "the configuration item 'ProviderAccessId' changed"
} }
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) { if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
return false, "the configuration item 'ProviderConfig' changed" return false, "the configuration item 'ProviderConfig' changed"
} }
if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider { if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider {
return false, "the configuration item 'CAProvider' changed" return false, "the configuration item 'CAProvider' changed"
} }
if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId { if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId {
return false, "the configuration item 'CAProviderAccessId' changed" return false, "the configuration item 'CAProviderAccessId' changed"
} }
if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) { if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) {
return false, "the configuration item 'CAProviderConfig' changed" return false, "the configuration item 'CAProviderConfig' changed"
} }
if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm { if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm {
return false, "the configuration item 'KeyAlgorithm' changed" return false, "the configuration item 'KeyAlgorithm' changed"
} }
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
if lastCertificate != nil { if lastCertificate != nil {
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24
expirationTime := time.Until(lastCertificate.ExpireAt) expirationTime := time.Until(lastCertificate.ExpireAt)
if expirationTime > renewalInterval { if expirationTime > renewalInterval {
daysLeft := int(expirationTime.Hours() / 24) daysLeft := int(expirationTime.Hours() / 24)
@ -162,7 +165,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10)
return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, currentNodeConfig.SkipBeforeExpiryDays) return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, thisNodeCfg.SkipBeforeExpiryDays)
} }
} }
} }

View File

@ -47,6 +47,6 @@ func (n *conditionNode) Process(ctx context.Context) error {
} }
func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) { func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) {
variables := GetNodeOutputs(ctx) variables := GetAllNodeOutputs(ctx)
return expression.Eval(variables) return expression.Eval(variables)
} }

View File

@ -3,4 +3,5 @@ package nodeprocessor
const ( const (
outputKeyForCertificateValidity = "certificate.validity" outputKeyForCertificateValidity = "certificate.validity"
outputKeyForCertificateDaysLeft = "certificate.daysLeft" outputKeyForCertificateDaysLeft = "certificate.daysLeft"
outputKeyForNodeSkipped = "node.skipped"
) )

View File

@ -25,6 +25,15 @@ func newNodeOutputsContainer() *nodeOutputsContainer {
} }
} }
// 获取节点输出容器
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
value := ctx.Value(nodeOutputsKey)
if value == nil {
return nil
}
return value.(*nodeOutputsContainer)
}
// 添加节点输出到上下文 // 添加节点输出到上下文
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context { func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
container := getNodeOutputsContainer(ctx) container := getNodeOutputsContainer(ctx)
@ -50,7 +59,7 @@ func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) co
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any { func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
container := getNodeOutputsContainer(ctx) container := getNodeOutputsContainer(ctx)
if container == nil { if container == nil {
return nil container = newNodeOutputsContainer()
} }
container.RLock() container.RLock()
@ -69,22 +78,11 @@ func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
return outputCopy return outputCopy
} }
// 获取特定节点的特定输出项
func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) {
output := GetNodeOutput(ctx, nodeId)
if output == nil {
return nil, false
}
value, exists := output[key]
return value, exists
}
// 获取所有节点输出 // 获取所有节点输出
func GetNodeOutputs(ctx context.Context) map[string]map[string]any { func GetAllNodeOutputs(ctx context.Context) map[string]map[string]any {
container := getNodeOutputsContainer(ctx) container := getNodeOutputsContainer(ctx)
if container == nil { if container == nil {
return nil container = newNodeOutputsContainer()
} }
container.RLock() container.RLock()
@ -103,26 +101,3 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
return allOutputs return allOutputs
} }
// 获取节点输出容器
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
value := ctx.Value(nodeOutputsKey)
if value == nil {
return nil
}
return value.(*nodeOutputsContainer)
}
// 检查节点是否有输出
func HasNodeOutput(ctx context.Context, nodeId string) bool {
container := getNodeOutputsContainer(ctx)
if container == nil {
return false
}
container.RLock()
defer container.RUnlock()
_, exists := container.outputs[nodeId]
return exists
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"strconv"
"strings" "strings"
"github.com/usual2970/certimate/internal/deployer" "github.com/usual2970/certimate/internal/deployer"
@ -59,6 +60,7 @@ func (n *deployNode) Process(ctx context.Context) error {
// 检测是否可以跳过本次执行 // 检测是否可以跳过本次执行
if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) { if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason)) n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason))
return nil return nil
} else if reason != "" { } else if reason != "" {
@ -97,6 +99,9 @@ func (n *deployNode) Process(ctx context.Context) error {
return err return err
} }
// 记录中间结果
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
n.logger.Info("deployment completed") n.logger.Info("deployment completed")
return nil return nil
} }
@ -104,16 +109,17 @@ func (n *deployNode) Process(ctx context.Context) error {
func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
if lastOutput != nil && lastOutput.Succeeded { if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
currentNodeConfig := n.node.GetConfigForDeploy() thisNodeCfg := n.node.GetConfigForDeploy()
lastNodeConfig := lastOutput.Node.GetConfigForDeploy() lastNodeCfg := lastOutput.Node.GetConfigForDeploy()
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
return false, "the configuration item 'ProviderAccessId' changed" return false, "the configuration item 'ProviderAccessId' changed"
} }
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) { if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
return false, "the configuration item 'ProviderConfig' changed" return false, "the configuration item 'ProviderConfig' changed"
} }
if currentNodeConfig.SkipOnLastSucceeded { if thisNodeCfg.SkipOnLastSucceeded {
return true, "the certificate has already been deployed" return true, "the certificate has already been deployed"
} }
} }

View File

@ -2,7 +2,9 @@ package nodeprocessor
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"strconv"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/notify" "github.com/usual2970/certimate/internal/notify"
@ -58,6 +60,12 @@ func (n *notifyNode) Process(ctx context.Context) error {
return nil return nil
} }
// 检测是否可以跳过本次执行
if skippable := n.checkCanSkip(ctx); skippable {
n.logger.Info(fmt.Sprintf("skip this notification, because all the previous nodes have been skipped"))
return nil
}
// 初始化通知器 // 初始化通知器
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
Node: n.node, Node: n.node,
@ -79,3 +87,21 @@ func (n *notifyNode) Process(ctx context.Context) error {
n.logger.Info("notification completed") n.logger.Info("notification completed")
return nil return nil
} }
func (n *notifyNode) checkCanSkip(ctx context.Context) (_skip bool) {
thisNodeCfg := n.node.GetConfigForNotify()
if !thisNodeCfg.SkipOnAllPrevSkipped {
return false
}
prevNodeOutputs := GetAllNodeOutputs(ctx)
for _, nodeOutput := range prevNodeOutputs {
if nodeOutput[outputKeyForNodeSkipped] != nil {
if nodeOutput[outputKeyForNodeSkipped].(string) != strconv.FormatBool(true) {
return false
}
}
}
return true
}

View File

@ -44,6 +44,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
// 检测是否可以跳过本次执行 // 检测是否可以跳过本次执行
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason)) n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
return nil return nil
} else if reason != "" { } else if reason != "" {
@ -71,6 +72,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
} }
// 记录中间结果 // 记录中间结果
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
@ -81,12 +83,13 @@ func (n *uploadNode) Process(ctx context.Context) error {
func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
if lastOutput != nil && lastOutput.Succeeded { if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
currentNodeConfig := n.node.GetConfigForUpload() thisNodeCfg := n.node.GetConfigForUpload()
lastNodeConfig := lastOutput.Node.GetConfigForUpload() lastNodeCfg := lastOutput.Node.GetConfigForUpload()
if strings.TrimSpace(currentNodeConfig.Certificate) != strings.TrimSpace(lastNodeConfig.Certificate) {
if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) {
return false, "the configuration item 'Certificate' changed" return false, "the configuration item 'Certificate' changed"
} }
if strings.TrimSpace(currentNodeConfig.PrivateKey) != strings.TrimSpace(lastNodeConfig.PrivateKey) { if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) {
return false, "the configuration item 'PrivateKey' changed" return false, "the configuration item 'PrivateKey' changed"
} }

View File

@ -2,7 +2,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } f
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router"; import { Link } from "react-router";
import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons"; import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
import { Button, Divider, Form, type FormInstance, Input, Select, Typography } from "antd"; import { Button, Divider, Flex, Form, type FormInstance, Input, Select, Switch, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -74,6 +74,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
.string({ message: t("workflow_node.notify.form.provider_access.placeholder") }) .string({ message: t("workflow_node.notify.form.provider_access.placeholder") })
.nonempty(t("workflow_node.notify.form.provider_access.placeholder")), .nonempty(t("workflow_node.notify.form.provider_access.placeholder")),
providerConfig: z.any().nullish(), providerConfig: z.any().nullish(),
skipOnAllPrevSkipped: z.boolean().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({ const { form: formInst, formProps } = useAntdForm({
@ -281,6 +282,27 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
{nestedFormEl} {nestedFormEl}
</Show> </Show>
<Divider size="small">
<Typography.Text className="text-xs font-normal" type="secondary">
{t("workflow_node.notify.form.strategy_config.label")}
</Typography.Text>
</Divider>
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item label={t("workflow_node.notify.form.skip_on_all_prev_skipped.label")}>
<Flex align="center" gap={8} wrap="wrap">
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}</div>
<Form.Item name="skipOnAllPrevSkipped" noStyle rules={[formRule]}>
<Switch
checkedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.on")}
unCheckedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.off")}
/>
</Form.Item>
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}</div>
</Flex>
</Form.Item>
</Form>
</Form> </Form>
); );
} }

View File

@ -216,6 +216,7 @@ export type WorkflowNodeConfigForNotify = {
provider: string; provider: string;
providerAccessId: string; providerAccessId: string;
providerConfig?: Record<string, unknown>; providerConfig?: Record<string, unknown>;
skipOnAllPrevSkipped?: boolean;
}; };
export const defaultNodeConfigForNotify = (): Partial<WorkflowNodeConfigForNotify> => { export const defaultNodeConfigForNotify = (): Partial<WorkflowNodeConfigForNotify> => {

View File

@ -878,6 +878,12 @@
"workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.", "workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.", "workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string", "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
"workflow_node.notify.form.strategy_config.label": "Strategy settings",
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "Silent behavior",
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "If all the previous nodes were skipped, ",
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": " to notify.",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "skip",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "not skip",
"workflow_node.end.label": "End", "workflow_node.end.label": "End",
"workflow_node.end.default_name": "End", "workflow_node.end.default_name": "End",

View File

@ -845,7 +845,7 @@
"workflow_node.notify.form.subject.placeholder": "请输入通知主题", "workflow_node.notify.form.subject.placeholder": "请输入通知主题",
"workflow_node.notify.form.message.label": "通知内容", "workflow_node.notify.form.message.label": "通知内容",
"workflow_node.notify.form.message.placeholder": "请输入通知内容", "workflow_node.notify.form.message.placeholder": "请输入通知内容",
"workflow_node.notify.form.channel.label": "通知渠道(废弃,请使用「通知渠道授权」字段)", "workflow_node.notify.form.channel.label": "通知渠道(即将废弃,请使用「通知渠道授权」字段)",
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道", "workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
"workflow_node.notify.form.channel.button": "设置", "workflow_node.notify.form.channel.button": "设置",
"workflow_node.notify.form.provider.label": "通知渠道", "workflow_node.notify.form.provider.label": "通知渠道",
@ -877,6 +877,12 @@
"workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。", "workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。", "workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串", "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
"workflow_node.notify.form.strategy_config.label": "执行策略",
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "静默行为",
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "当前序申请、上传、部署等节点均已跳过执行时,",
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": "此通知节点。",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "跳过",
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "不跳过",
"workflow_node.end.label": "结束", "workflow_node.end.label": "结束",
"workflow_node.end.default_name": "结束", "workflow_node.end.default_name": "结束",