From 252da5d7e1f30e1cc5ee2ea1bbac89d6287b4cdd Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 4 Jun 2025 10:50:52 +0800 Subject: [PATCH 01/17] refactor(ui): clean code --- .../workflow/node-processor/monitor_node.go | 5 +- .../workflow/node/ApplyNodeConfigForm.tsx | 8 +-- .../workflow/node/ConditionNodeConfigForm.tsx | 4 +- .../workflow/node/DeployNodeConfigForm.tsx | 6 +- .../workflow/node/MonitorNodeConfigForm.tsx | 8 +-- .../workflow/node/NotifyNodeConfigForm.tsx | 4 +- .../workflow/node/StartNodeConfigForm.tsx | 7 +-- .../workflow/node/UploadNodeConfigForm.tsx | 4 +- ui/src/domain/workflow.ts | 60 +++++++++++++++++-- 9 files changed, 72 insertions(+), 34 deletions(-) diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go index 4b875f26..86714d57 100644 --- a/internal/workflow/node-processor/monitor_node.go +++ b/internal/workflow/node-processor/monitor_node.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "fmt" "math" + "net" "net/http" "strconv" "strings" @@ -34,9 +35,9 @@ func (n *monitorNode) Process(ctx context.Context) error { nodeCfg := n.node.GetConfigForMonitor() - targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port) + targetAddr := net.JoinHostPort(nodeCfg.Host, fmt.Sprintf("%d", nodeCfg.Port)) if nodeCfg.Port == 0 { - targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host) + targetAddr = net.JoinHostPort(nodeCfg.Host, "443") } targetDomain := nodeCfg.Domain diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index ae56efc3..abbce8b4 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -28,7 +28,7 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele import CAProviderSelect from "@/components/provider/CAProviderSelect"; import Show from "@/components/Show"; import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForApply } from "@/domain/workflow"; +import { type WorkflowNodeConfigForApply, defaultNodeConfigForApply } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { useContactEmailsStore } from "@/stores/contact"; @@ -59,11 +59,7 @@ export type ApplyNodeConfigFormInstance = { const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { - return { - challengeType: "dns-01", - keyAlgorithm: "RSA2048", - skipBeforeExpiryDays: 30, - }; + return defaultNodeConfigForApply(); }; const ApplyNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 3cd92d7b..1a2794e7 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -4,7 +4,7 @@ import { Form, type FormInstance } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; -import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { type Expr, type WorkflowNodeConfigForCondition, defaultNodeConfigForCondition } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor"; @@ -29,7 +29,7 @@ export type ConditionNodeConfigFormInstance = { }; const initFormModel = (): ConditionNodeConfigFormFieldValues => { - return {}; + return defaultNodeConfigForCondition(); }; const ConditionNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index c0083298..96e50911 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx"; import Show from "@/components/Show"; import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow"; +import { type WorkflowNodeConfigForDeploy, WorkflowNodeType, defaultNodeConfigForDeploy } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; @@ -117,9 +117,7 @@ export type DeployNodeConfigFormInstance = { }; const initFormModel = (): DeployNodeConfigFormFieldValues => { - return { - skipOnLastSucceeded: true, - }; + return defaultNodeConfigForDeploy(); }; const DeployNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx index 883124f9..92eef42e 100644 --- a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx @@ -4,7 +4,7 @@ import { Alert, Form, type FormInstance, Input, InputNumber } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; -import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow"; +import { type WorkflowNodeConfigForMonitor, defaultNodeConfigForMonitor } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators"; @@ -25,11 +25,7 @@ export type MonitorNodeConfigFormInstance = { }; const initFormModel = (): MonitorNodeConfigFormFieldValues => { - return { - host: "", - port: 443, - requestPath: "/", - }; + return defaultNodeConfigForMonitor(); }; const MonitorNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx index 3c612df8..4473da6a 100644 --- a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx @@ -12,7 +12,7 @@ import NotificationProviderSelect from "@/components/provider/NotificationProvid import Show from "@/components/Show"; import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider"; import { notifyChannelsMap } from "@/domain/settings"; -import { type WorkflowNodeConfigForNotify } from "@/domain/workflow"; +import { type WorkflowNodeConfigForNotify, defaultNodeConfigForNotify } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { useNotifyChannelsStore } from "@/stores/notify"; @@ -41,7 +41,7 @@ export type NotifyNodeConfigFormInstance = { }; const initFormModel = (): NotifyNodeConfigFormFieldValues => { - return {}; + return defaultNodeConfigForNotify(); }; const NotifyNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/StartNodeConfigForm.tsx b/ui/src/components/workflow/node/StartNodeConfigForm.tsx index 7b3ba73d..7acdfddc 100644 --- a/ui/src/components/workflow/node/StartNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/StartNodeConfigForm.tsx @@ -6,7 +6,7 @@ import dayjs from "dayjs"; import { z } from "zod"; import Show from "@/components/Show"; -import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType } from "@/domain/workflow"; +import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType, defaultNodeConfigForStart } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { getNextCronExecutions, validCronExpression } from "@/utils/cron"; @@ -27,10 +27,7 @@ export type StartNodeConfigFormInstance = { }; const initFormModel = (): StartNodeConfigFormFieldValues => { - return { - trigger: WORKFLOW_TRIGGERS.AUTO, - triggerCron: "0 0 * * *", - }; + return defaultNodeConfigForStart(); }; const StartNodeConfigForm = forwardRef( diff --git a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx index 5dc16b0b..27898401 100644 --- a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { validateCertificate, validatePrivateKey } from "@/api/certificates"; import TextFileInput from "@/components/TextFileInput"; -import { type WorkflowNodeConfigForUpload } from "@/domain/workflow"; +import { type WorkflowNodeConfigForUpload, defaultNodeConfigForUpload } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { getErrMsg } from "@/utils/error"; @@ -27,7 +27,7 @@ export type UploadNodeConfigFormInstance = { }; const initFormModel = (): UploadNodeConfigFormFieldValues => { - return {}; + return defaultNodeConfigForUpload(); }; const UploadNodeConfigForm = forwardRef( diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 6ebb7c97..50a0be4a 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -133,6 +133,13 @@ export type WorkflowNodeConfigForStart = { triggerCron?: string; }; +export const defaultNodeConfigForStart = (): Partial => { + return { + trigger: WORKFLOW_TRIGGERS.AUTO, + triggerCron: "0 0 * * *", + }; +}; + export type WorkflowNodeConfigForApply = { domains: string; contactEmail: string; @@ -152,6 +159,14 @@ export type WorkflowNodeConfigForApply = { skipBeforeExpiryDays: number; }; +export const defaultNodeConfigForApply = (): Partial => { + return { + challengeType: "dns-01", + keyAlgorithm: "RSA2048", + skipBeforeExpiryDays: 30, + }; +}; + export type WorkflowNodeConfigForUpload = { certificateId: string; domains: string; @@ -159,6 +174,10 @@ export type WorkflowNodeConfigForUpload = { privateKey: string; }; +export const defaultNodeConfigForUpload = (): Partial => { + return {}; +}; + export type WorkflowNodeConfigForMonitor = { host: string; port: number; @@ -166,6 +185,13 @@ export type WorkflowNodeConfigForMonitor = { requestPath?: string; }; +export const defaultNodeConfigForMonitor = (): Partial => { + return { + port: 443, + requestPath: "/", + }; +}; + export type WorkflowNodeConfigForDeploy = { certificate: string; provider: string; @@ -174,6 +200,12 @@ export type WorkflowNodeConfigForDeploy = { skipOnLastSucceeded: boolean; }; +export const defaultNodeConfigForDeploy = (): Partial => { + return { + skipOnLastSucceeded: true, + }; +}; + export type WorkflowNodeConfigForNotify = { subject: string; message: string; @@ -186,10 +218,18 @@ export type WorkflowNodeConfigForNotify = { providerConfig?: Record; }; +export const defaultNodeConfigForNotify = (): Partial => { + return {}; +}; + export type WorkflowNodeConfigForCondition = { expression?: Expr; }; +export const defaultNodeConfigForCondition = (): Partial => { + return {}; +}; + export type WorkflowNodeConfigForBranch = never; export type WorkflowNodeConfigForEnd = never; @@ -243,15 +283,18 @@ type InitWorkflowOptions = { }; export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => { - const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode; - root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL }; + const root = newNode(WorkflowNodeType.Start, { + nodeConfig: { trigger: WORKFLOW_TRIGGERS.MANUAL }, + }); switch (options.template) { case "standard": { let current = root; - const applyNode = newNode(WorkflowNodeType.Apply); + const applyNode = newNode(WorkflowNodeType.Apply, { + nodeConfig: defaultNodeConfigForApply(), + }); current.next = applyNode; current = current.next; @@ -260,6 +303,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = current = current.next!.branches![1]; current.next = newNode(WorkflowNodeType.Notify, { nodeConfig: { + ...defaultNodeConfigForNotify(), subject: "[Certimate] Workflow Failure Alert!", message: "Your workflow run for the certificate application has failed. Please check the details.", } as WorkflowNodeConfigForNotify, @@ -268,8 +312,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = current = applyNode.next!.branches![0]; current.next = newNode(WorkflowNodeType.Deploy, { nodeConfig: { + ...defaultNodeConfigForDeploy(), certificate: `${applyNode.id}#certificate`, - skipOnLastSucceeded: true, } as WorkflowNodeConfigForDeploy, }); @@ -279,6 +323,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = current = current.next!.branches![1]; current.next = newNode(WorkflowNodeType.Notify, { nodeConfig: { + ...defaultNodeConfigForNotify(), subject: "[Certimate] Workflow Failure Alert!", message: "Your workflow run for the certificate deployment has failed. Please check the details.", } as WorkflowNodeConfigForNotify, @@ -290,7 +335,9 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = { let current = root; - const monitorNode = newNode(WorkflowNodeType.Monitor); + const monitorNode = newNode(WorkflowNodeType.Monitor, { + nodeConfig: defaultNodeConfigForMonitor(), + }); current.next = monitorNode; current = current.next; @@ -299,6 +346,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = current = current.next!.branches![1]; current.next = newNode(WorkflowNodeType.Notify, { nodeConfig: { + ...defaultNodeConfigForNotify(), subject: "[Certimate] Workflow Failure Alert!", message: "Your workflow run for the certificate monitoring has failed. Please check the details.", } as WorkflowNodeConfigForNotify, @@ -352,6 +400,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = } as WorkflowNodeConfigForCondition; current.next = newNode(WorkflowNodeType.Notify, { nodeConfig: { + ...defaultNodeConfigForNotify(), subject: "[Certimate] Certificate Expiry Alert!", message: "The certificate will expire soon. Please pay attention to your website.", } as WorkflowNodeConfigForNotify, @@ -380,6 +429,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = } as WorkflowNodeConfigForCondition; current.next = newNode(WorkflowNodeType.Notify, { nodeConfig: { + ...defaultNodeConfigForNotify(), subject: "[Certimate] Certificate Expiry Alert!", message: "The certificate has already expired. Please pay attention to your website.", } as WorkflowNodeConfigForNotify, From 59935df6b15fb80451b1fea71ae8a1bd196b5337 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 5 Jun 2025 20:24:43 +0800 Subject: [PATCH 02/17] fix: #766 --- .../deployer/providers/baiducloud-appblb/baiducloud_appblb.go | 1 + .../pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go index 3bb965ca..87d7ba65 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go @@ -285,6 +285,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c ClientToken: generateClientToken(), ListenerPort: uint16(cloudHttpsListenerPort), Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler, + CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds, AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel { if domain.Host == d.config.Domain { return bceappblb.AdditionalCertDomainsModel{ diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go index 0490b9ad..54e4a55d 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -283,6 +283,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ ClientToken: generateClientToken(), ListenerPort: uint16(cloudHttpsListenerPort), + CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds, AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel { if domain.Host == d.config.Domain { return bceblb.AdditionalCertDomainsModel{ From d58109f4bef3a4933065b11469dbbe637607e613 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 5 Jun 2025 20:28:50 +0800 Subject: [PATCH 03/17] feat: support wildcard domains on deployment to wangsu cdn --- .../pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go | 9 +++++++-- .../node/DeployNodeConfigFormWangsuCDNConfig.tsx | 2 +- ui/src/i18n/locales/zh/nls.workflow.nodes.json | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go index f889b996..3be2b119 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go @@ -6,11 +6,13 @@ import ( "fmt" "log/slog" "strconv" + "strings" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate" wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn" + sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice" ) type DeployerConfig struct { @@ -18,7 +20,7 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 网宿云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` - // 加速域名数组。 + // 加速域名数组(支持泛域名)。 Domains []string `json:"domains"` } @@ -80,7 +82,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE certId, _ := strconv.ParseInt(upres.CertId, 10, 64) batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{ CertificateId: certId, - DomainNames: d.config.Domains, + DomainNames: sliceutil.Map(d.config.Domains, func(domain string) string { + // "*.example.com" → ".example.com",适配网宿云 CDN 要求的泛域名格式 + return strings.TrimPrefix(domain, "*") + }), } batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq) d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp)) diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx index 36d663b5..7650894e 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx @@ -43,7 +43,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({ if (!v) return false; return String(v) .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => validDomainName(e)); + .every((e) => validDomainName(e, { allowWildcard: true })); }, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 9a8be8af..e138e350 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -795,7 +795,7 @@ "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see https://console.volcengine.com/tos", "workflow_node.deploy.form.wangsu_cdn_domains.label": "网宿云 CDN 加速域名", - "workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(多个值请用半角分号隔开)", + "workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(支持泛域名;多个值请用半角分号隔开)", "workflow_node.deploy.form.wangsu_cdn_domains.tooltip": "这是什么?请参阅 https://cdn.console.wangsu.com/v2/index/#/property/list", "workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title": "修改网宿云 CDN 加速域名", "workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder": "请输入网宿云 CDN 加速域名", From 43182de732fdab856b77192b065c08dc987ca3d6 Mon Sep 17 00:00:00 2001 From: leun Date: Sun, 8 Jun 2025 15:00:28 +0800 Subject: [PATCH 04/17] =?UTF-8?q?fix(docs):=20=E4=BF=AE=E5=A4=8D=E8=85=BE?= =?UTF-8?q?=E8=AE=AF=E4=BA=91=E6=96=87=E6=A1=A3=E9=93=BE=E6=8E=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E9=94=99=E8=AF=AF=E7=9A=84=20com.cn=20=E5=9F=9F?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将文档链接中的 cloud.tencent.com.cn 统一替换为正确的 cloud.tencent.com, 以避免链接跳转失败或文档加载异常的问题。 --- .../deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go | 2 +- .../deployer/providers/tencentcloud-clb/tencentcloud_clb.go | 2 +- .../deployer/providers/tencentcloud-cos/tencentcloud_cos.go | 2 +- .../providers/tencentcloud-ecdn/tencentcloud_ecdn.go | 2 +- .../tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go | 2 +- ui/src/i18n/locales/en/nls.workflow.nodes.json | 4 ++-- ui/src/i18n/locales/zh/nls.workflow.nodes.json | 6 +++--- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go index 1df67032..b815ebf8 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go @@ -136,7 +136,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } // 循环获取部署任务详情,等待任务状态变更 - // REF: https://cloud.tencent.com.cn/document/api/400/91658 + // REF: https://cloud.tencent.com/document/api/400/91658 for { select { case <-ctx.Done(): diff --git a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go index 5455e236..7179ee71 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go @@ -153,7 +153,7 @@ func (d *DeployerProvider) deployViaSslService(ctx context.Context, cloudCertId } // 循环获取部署任务详情,等待任务状态变更 - // REF: https://cloud.tencent.com.cn/document/api/400/91658 + // REF: https://cloud.tencent.com/document/api/400/91658 for { select { case <-ctx.Done(): diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go index 2aa6b2d0..537d2285 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go @@ -104,7 +104,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } // 循环获取部署任务详情,等待任务状态变更 - // REF: https://cloud.tencent.com.cn/document/api/400/91658 + // REF: https://cloud.tencent.com/document/api/400/91658 for { select { case <-ctx.Done(): diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index d1ba3ce4..c029c667 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -119,7 +119,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } // 循环获取部署任务详情,等待任务状态变更 - // REF: https://cloud.tencent.com.cn/document/api/400/91658 + // REF: https://cloud.tencent.com/document/api/400/91658 for { select { case <-ctx.Done(): diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go index 5b4dd8d3..db6617d5 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go @@ -106,7 +106,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } // 循环获取部署任务详情,等待任务状态变更 - // REF: https://cloud.tencent.com.cn/document/api/400/91658 + // REF: https://cloud.tencent.com/document/api/400/91658 for { select { case <-ctx.Done(): diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 88d3dc15..55a44141 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -682,11 +682,11 @@ "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "For more information, see https://www.tencentcloud.com/document/product/1007/36573", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.label": "Tencent Cloud resource type", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.placeholder": "Please enter Tencent Cloud resource type", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "For more information, see https://cloud.tencent.com.cn/document/product/400/91667", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "For more information, see https://cloud.tencent.com/document/product/400/91667", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.label": "Tencent Cloud resource IDs", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder": "Please enter Tencent Cloud resource IDs (separated by semicolons)", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid": "Please enter a valid Tencent Cloud resource ID", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "For more information, see https://cloud.tencent.com.cn/document/product/400/91667", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "For more information, see https://cloud.tencent.com/document/product/400/91667", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title": "Change Tencent Cloud resource IDs", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.placeholder": "Please enter Tencent Cloud resouce ID", "workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label": "Tencent Cloud VOD App ID", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 9a8be8af..a470c098 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -678,14 +678,14 @@ "workflow_node.deploy.form.tencentcloud_ssl_deploy.guide": "小贴士:由于腾讯云证书部署任务是异步的,此节点若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。", "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.label": "腾讯云云产品地域", "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.placeholder": "请输入腾讯云云产品地域(例如:ap-guangzhou)", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com.cn/document/product/400/41659", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.label": "腾讯云云产品资源类型", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.placeholder": "请输入腾讯云产品资源类型", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "这是什么?请参阅 https://cloud.tencent.com.cn/document/product/400/91667", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91667", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.label": "腾讯云云产品资源 ID", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder": "请输入腾讯云云产品资源 ID(多个值请用半角分号隔开)", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid": "请输入正确的腾讯云云产品资源 ID", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "这是什么?请参阅 https://cloud.tencent.com.cn/document/product/400/91667

注意与各产品本身的实例 ID 区分。", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91667

注意与各产品本身的实例 ID 区分。", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title": "修改腾讯云云产品资源 ID", "workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.placeholder": "请输入腾讯云云产品资源 ID", "workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label": "腾讯云云点播应用 ID", From bd26dfecb80eb6a790cb125ac8f4797b7e5b886b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Jun 2025 10:06:41 +0800 Subject: [PATCH 05/17] feat: improve workflow log --- internal/domain/workflow.go | 2 +- internal/workflow/node-processor/apply_node.go | 4 +++- internal/workflow/node-processor/deploy_node.go | 3 ++- internal/workflow/node-processor/monitor_node.go | 4 ++-- internal/workflow/node-processor/notify_node.go | 3 +-- internal/workflow/node-processor/upload_node.go | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index a2e049e2..55fe56ed 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -128,7 +128,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { CAProvider: maputil.GetString(n.Config, "caProvider"), CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"), CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"), - KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"), + KeyAlgorithm: maputil.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)), Nameservers: maputil.GetString(n.Config, "nameservers"), DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"), DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"), diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 8616fbd9..852d7b9e 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "log/slog" "strconv" "time" @@ -35,7 +36,8 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { } func (n *applyNode) Process(ctx context.Context) error { - n.logger.Info("ready to obtain certificiate ...") + nodeCfg := n.node.GetConfigForApply() + n.logger.Info("ready to obtain certificiate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index f89f4a1f..279893e8 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -33,7 +33,8 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { } func (n *deployNode) Process(ctx context.Context) error { - n.logger.Info("ready to deploy certificate ...") + nodeCfg := n.node.GetConfigForDeploy() + n.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go index 86714d57..d13e4247 100644 --- a/internal/workflow/node-processor/monitor_node.go +++ b/internal/workflow/node-processor/monitor_node.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "log/slog" "math" "net" "net/http" @@ -31,9 +32,8 @@ func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { } func (n *monitorNode) Process(ctx context.Context) error { - n.logger.Info("ready to monitor certificate ...") - nodeCfg := n.node.GetConfigForMonitor() + n.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg)) targetAddr := net.JoinHostPort(nodeCfg.Host, fmt.Sprintf("%d", nodeCfg.Port)) if nodeCfg.Port == 0 { diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index dabfd034..9d259c0a 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -28,9 +28,8 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { } func (n *notifyNode) Process(ctx context.Context) error { - n.logger.Info("ready to send notification ...") - nodeCfg := n.node.GetConfigForNotify() + n.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg)) if nodeCfg.Provider == "" { // Deprecated: v0.4.x 将废弃 diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 9431d31a..adbf46dd 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "log/slog" "strconv" "strings" "time" @@ -32,9 +33,8 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { } func (n *uploadNode) Process(ctx context.Context) error { - n.logger.Info("ready to upload certiticate ...") - nodeCfg := n.node.GetConfigForUpload() + n.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) From 84a3f3346ad88c9707c8161b88904005fa331040 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Jun 2025 19:11:04 +0800 Subject: [PATCH 06/17] fix: #769 --- internal/workflow/node-processor/apply_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 852d7b9e..291b604d 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -103,7 +103,7 @@ func (n *applyNode) Process(ctx context.Context) error { } // 保存 ARI 记录 - if applyResult.ARIReplaced { + if applyResult.ARIReplaced && lastOutput != nil { lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) if lastCertificate != nil { lastCertificate.ACMERenewed = true From 24fe82475721134412317c7c1a03943b689b4806 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Jun 2025 20:39:23 +0800 Subject: [PATCH 07/17] feat: allow skip notify nodes when all previous nodes were skipped --- internal/domain/workflow.go | 26 +++++----- internal/workflow/dispatcher/invoker.go | 1 + .../workflow/node-processor/apply_node.go | 29 ++++++----- .../workflow/node-processor/condition_node.go | 2 +- internal/workflow/node-processor/const.go | 1 + internal/workflow/node-processor/context.go | 49 +++++-------------- .../workflow/node-processor/deploy_node.go | 16 ++++-- .../workflow/node-processor/notify_node.go | 26 ++++++++++ .../workflow/node-processor/upload_node.go | 11 +++-- .../workflow/node/NotifyNodeConfigForm.tsx | 24 ++++++++- ui/src/domain/workflow.ts | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 6 +++ .../i18n/locales/zh/nls.workflow.nodes.json | 8 ++- 13 files changed, 126 insertions(+), 74 deletions(-) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 55fe56ed..256ad08d 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -106,12 +106,13 @@ type WorkflowNodeConfigForDeploy struct { } type WorkflowNodeConfigForNotify struct { - Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃 - Provider string `json:"provider"` // 通知提供商 - ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置 - Subject string `json:"subject"` // 通知主题 - Message string `json:"message"` // 通知内容 + Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃 + Provider string `json:"provider"` // 通知提供商 + ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID + ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置 + Subject string `json:"subject"` // 通知主题 + Message string `json:"message"` // 通知内容 + SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过 } type WorkflowNodeConfigForCondition struct { @@ -169,12 +170,13 @@ func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { return WorkflowNodeConfigForNotify{ - Channel: maputil.GetString(n.Config, "channel"), - Provider: maputil.GetString(n.Config, "provider"), - ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), - ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"), - Subject: maputil.GetString(n.Config, "subject"), - Message: maputil.GetString(n.Config, "message"), + Channel: maputil.GetString(n.Config, "channel"), + Provider: maputil.GetString(n.Config, "provider"), + ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), + ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"), + Subject: maputil.GetString(n.Config, "subject"), + Message: maputil.GetString(n.Config, "message"), + SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"), } } diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go index b6e4a4db..c1d1260e 100644 --- a/internal/workflow/dispatcher/invoker.go +++ b/internal/workflow/dispatcher/invoker.go @@ -112,6 +112,7 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow break } + // TODO: 优化可读性 if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition { current = nil diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 291b604d..96cbabfb 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -47,6 +47,7 @@ func (n *applyNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 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)) return nil } 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[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) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 - currentNodeConfig := n.node.GetConfigForApply() - lastNodeConfig := lastOutput.Node.GetConfigForApply() - if currentNodeConfig.Domains != lastNodeConfig.Domains { + thisNodeCfg := n.node.GetConfigForApply() + lastNodeCfg := lastOutput.Node.GetConfigForApply() + + if thisNodeCfg.Domains != lastNodeCfg.Domains { return false, "the configuration item 'Domains' changed" } - if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail { + if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail { return false, "the configuration item 'ContactEmail' changed" } - if currentNodeConfig.Provider != lastNodeConfig.Provider { + if thisNodeCfg.Provider != lastNodeCfg.Provider { return false, "the configuration item 'Provider' changed" } - if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { + if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { 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" } - if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider { + if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider { return false, "the configuration item 'CAProvider' changed" } - if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId { + if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId { 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" } - if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm { + if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm { return false, "the configuration item 'KeyAlgorithm' changed" } lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) if lastCertificate != nil { - renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 + renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { 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[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) } } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index d9e8126d..023f9e1a 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -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) { - variables := GetNodeOutputs(ctx) + variables := GetAllNodeOutputs(ctx) return expression.Eval(variables) } diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go index 62d2d56b..21bdf167 100644 --- a/internal/workflow/node-processor/const.go +++ b/internal/workflow/node-processor/const.go @@ -3,4 +3,5 @@ package nodeprocessor const ( outputKeyForCertificateValidity = "certificate.validity" outputKeyForCertificateDaysLeft = "certificate.daysLeft" + outputKeyForNodeSkipped = "node.skipped" ) diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go index 96c40487..d600554d 100644 --- a/internal/workflow/node-processor/context.go +++ b/internal/workflow/node-processor/context.go @@ -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 { 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 { container := getNodeOutputsContainer(ctx) if container == nil { - return nil + container = newNodeOutputsContainer() } container.RLock() @@ -69,22 +78,11 @@ func GetNodeOutput(ctx context.Context, nodeId string) map[string]any { 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) if container == nil { - return nil + container = newNodeOutputsContainer() } container.RLock() @@ -103,26 +101,3 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any { 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 -} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 279893e8..30a7c4e7 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strconv" "strings" "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 skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { + n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true) n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason)) return nil } else if reason != "" { @@ -97,6 +99,9 @@ func (n *deployNode) Process(ctx context.Context) error { return err } + // 记录中间结果 + n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false) + n.logger.Info("deployment completed") 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) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 - currentNodeConfig := n.node.GetConfigForDeploy() - lastNodeConfig := lastOutput.Node.GetConfigForDeploy() - if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { + thisNodeCfg := n.node.GetConfigForDeploy() + lastNodeCfg := lastOutput.Node.GetConfigForDeploy() + + if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { 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" } - if currentNodeConfig.SkipOnLastSucceeded { + if thisNodeCfg.SkipOnLastSucceeded { return true, "the certificate has already been deployed" } } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 9d259c0a..2cba06cf 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -2,7 +2,9 @@ package nodeprocessor import ( "context" + "fmt" "log/slog" + "strconv" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/notify" @@ -58,6 +60,12 @@ func (n *notifyNode) Process(ctx context.Context) error { 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{ Node: n.node, @@ -79,3 +87,21 @@ func (n *notifyNode) Process(ctx context.Context) error { n.logger.Info("notification completed") 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 +} diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index adbf46dd..c06d0e82 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -44,6 +44,7 @@ func (n *uploadNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 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)) return nil } 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[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) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 - currentNodeConfig := n.node.GetConfigForUpload() - lastNodeConfig := lastOutput.Node.GetConfigForUpload() - if strings.TrimSpace(currentNodeConfig.Certificate) != strings.TrimSpace(lastNodeConfig.Certificate) { + thisNodeCfg := n.node.GetConfigForUpload() + lastNodeCfg := lastOutput.Node.GetConfigForUpload() + + if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) { 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" } diff --git a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx index 4473da6a..09c6e02e 100644 --- a/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/NotifyNodeConfigForm.tsx @@ -2,7 +2,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } f import { useTranslation } from "react-i18next"; import { Link } from "react-router"; 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 { z } from "zod"; @@ -74,6 +74,7 @@ const NotifyNodeConfigForm = forwardRef + + + + {t("workflow_node.notify.form.strategy_config.label")} + + + +
+ + +
{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}
+ + + +
{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}
+
+
+
); } diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 50a0be4a..d9069d5b 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -216,6 +216,7 @@ export type WorkflowNodeConfigForNotify = { provider: string; providerAccessId: string; providerConfig?: Record; + skipOnAllPrevSkipped?: boolean; }; export const defaultNodeConfigForNotify = (): Partial => { diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 88d3dc15..742abcd7 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -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.guide": "
Supported variables:
  1. ${SUBJECT}: The subject of notification.
  2. ${MESSAGE}: The message of notification.

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.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.default_name": "End", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index e138e350..a1497782 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -845,7 +845,7 @@ "workflow_node.notify.form.subject.placeholder": "请输入通知主题", "workflow_node.notify.form.message.label": "通知内容", "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.button": "设置", "workflow_node.notify.form.provider.label": "通知渠道", @@ -877,6 +877,12 @@ "workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。", "workflow_node.notify.form.webhook_data.guide": "
支持的变量:
  1. ${SUBJECT}:通知主题。
  2. ${MESSAGE}:通知内容。

其他注意事项请前往授权管理页面查看。", "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.default_name": "结束", From 5e6d72963176eccda83433134d79ec9b51d220b7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Jun 2025 20:44:48 +0800 Subject: [PATCH 08/17] fix: #769 --- internal/repository/certificate.go | 19 ------------------- .../workflow/node-processor/apply_node.go | 4 ++-- internal/workflow/node-processor/processor.go | 1 - .../workflow/node-processor/upload_node.go | 2 +- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 290d5f9f..f5a345c7 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -77,25 +77,6 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo return r.castRecordToModel(records[0]) } -func (r *CertificateRepository) GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error) { - records, err := app.GetApp().FindRecordsByFilter( - domain.CollectionNameCertificate, - "workflowRunId={:workflowRunId} && deleted=null", - "-created", - 1, 0, - dbx.Params{"workflowRunId": workflowRunId}, - ) - if err != nil { - return nil, err - } - - if len(records) == 0 { - return nil, domain.ErrRecordNotFound - } - - return r.castRecordToModel(records[0]) -} - func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate) if err != nil { diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 96cbabfb..313d21a6 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -105,7 +105,7 @@ func (n *applyNode) Process(ctx context.Context) error { // 保存 ARI 记录 if applyResult.ARIReplaced && lastOutput != nil { - lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) + lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId) if lastCertificate != nil { lastCertificate.ACMERenewed = true n.certRepo.Save(ctx, lastCertificate) @@ -155,7 +155,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo return false, "the configuration item 'KeyAlgorithm' changed" } - lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) + lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId) if lastCertificate != nil { renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index d375883f..ec0a1d91 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -50,7 +50,6 @@ func (n *nodeOutputer) GetOutputs() map[string]any { type certificateRepository interface { GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) - GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index c06d0e82..95aac51d 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -93,7 +93,7 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl return false, "the configuration item 'PrivateKey' changed" } - lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) + lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId) if lastCertificate != nil { daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24) n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0) From a750592eb58f11a90a1a414ba2d373fcea5282c8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 9 Jun 2025 21:04:54 +0800 Subject: [PATCH 09/17] feat: duplicate workflow --- .../components/workflow/WorkflowEditModal.tsx | 122 ++++++++++++++++++ ui/src/domain/workflow.ts | 12 +- ui/src/i18n/locales/en/nls.workflow.json | 2 + ui/src/i18n/locales/zh/nls.workflow.json | 2 + ui/src/pages/workflows/WorkflowList.tsx | 44 ++++++- 5 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 ui/src/components/workflow/WorkflowEditModal.tsx diff --git a/ui/src/components/workflow/WorkflowEditModal.tsx b/ui/src/components/workflow/WorkflowEditModal.tsx new file mode 100644 index 00000000..76932ece --- /dev/null +++ b/ui/src/components/workflow/WorkflowEditModal.tsx @@ -0,0 +1,122 @@ +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useControllableValue } from "ahooks"; +import { Modal, notification } from "antd"; + +import ModalForm from "@/components/ModalForm"; +import { useTriggerElement, useZustandShallowSelector } from "@/hooks"; +import { getErrMsg } from "@/utils/error"; + +import WorkflowForm, { type WorkflowFormInstance, type WorkflowFormProps } from "./WorkflowForm"; + +export type WorkflowEditModalProps = { + data?: WorkflowFormProps["initialValues"]; + loading?: boolean; + open?: boolean; + usage?: WorkflowFormProps["usage"]; + scene: WorkflowFormProps["scene"]; + trigger?: React.ReactNode; + onOpenChange?: (open: boolean) => void; + afterSubmit?: (record: WorkflowModel) => void; +}; + +const WorkflowEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: WorkflowEditModalProps) => { + const { t } = useTranslation(); + + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { createWorkflow, updateWorkflow } = useWorkflowesStore(useZustandShallowSelector(["createWorkflow", "updateWorkflow"])); + + const [open, setOpen] = useControllableValue(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + + const formRef = useRef(null); + const [formPending, setFormPending] = useState(false); + + const handleOkClick = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + let values: WorkflowModel = formRef.current!.getFieldsValue(); + + if (scene === "add") { + if (data?.id) { + throw "Invalid props: `data`"; + } + + values = await createWorkflow(values); + } else if (scene === "edit") { + if (!data?.id) { + throw "Invalid props: `data`"; + } + + values = await updateWorkflow({ ...data, ...values }); + } else { + throw "Invalid props: `preset`"; + } + + afterSubmit?.(values); + setOpen(false); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; + } finally { + setFormPending(false); + } + }; + + const handleCancelClick = () => { + if (formPending) return; + + setOpen(false); + }; + + return ( + <> + {NotificationContextHolder} + + {triggerEl} + + setOpen(false)} + cancelButtonProps={{ disabled: formPending }} + cancelText={t("common.button.cancel")} + closable + confirmLoading={formPending} + destroyOnHidden + loading={loading} + okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")} + open={open} + title={t(`access.action.${scene}`)} + width={480} + onOk={handleOkClick} + onCancel={handleCancelClick} + > +
+ +
+
+ + ); +}; + +export default WorkflowEditModal; diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index d9069d5b..415123c4 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -509,18 +509,22 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} return node; }; -export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => { +type CloneNodeOptions = { + withCopySuffix?: boolean; +}; + +export const cloneNode = (sourceNode: WorkflowNode, { withCopySuffix }: CloneNodeOptions = { withCopySuffix: true }): WorkflowNode => { const { produce } = new Immer({ autoFreeze: false }); const deepClone = (node: WorkflowNode): WorkflowNode => { return produce(node, (draft) => { draft.id = nanoid(); if (draft.next) { - draft.next = cloneNode(draft.next); + draft.next = cloneNode(draft.next, { withCopySuffix }); } if (draft.branches) { - draft.branches = draft.branches.map((branch) => cloneNode(branch)); + draft.branches = draft.branches.map((branch) => cloneNode(branch, { withCopySuffix })); } return draft; @@ -528,7 +532,7 @@ export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => { }; const copyNode = produce(sourceNode, (draft) => { - draft.name = `${draft.name}-copy`; + draft.name = withCopySuffix ? `${draft.name}-copy` : draft.name; }); return deepClone(copyNode); }; diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index b086e25f..900a7f44 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -7,6 +7,8 @@ "workflow.action.create": "Create workflow", "workflow.action.edit": "Edit workflow", + "workflow.action.duplicate": "Duplicate workflow", + "workflow.action.duplicate.confirm": "Are you sure to duplicate this workflow?", "workflow.action.delete": "Delete workflow", "workflow.action.delete.confirm": "Are you sure to delete this workflow?", "workflow.action.enable": "Enable", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 9ff12aac..66e59ece 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -7,6 +7,8 @@ "workflow.action.create": "新建工作流", "workflow.action.edit": "编辑工作流", + "workflow.action.duplicate": "复制工作流", + "workflow.action.duplicate.confirm": "确定要复制此工作流吗?", "workflow.action.delete": "删除工作流", "workflow.action.delete.confirm": "确定要删除此工作流吗?", "workflow.action.enable": "启用", diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index e40bc894..dd12e53a 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -9,6 +9,7 @@ import { EditOutlined as EditOutlinedIcon, PlusOutlined as PlusOutlinedIcon, ReloadOutlined as ReloadOutlinedIcon, + SnippetsOutlined as SnippetsOutlinedIcon, StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; @@ -39,7 +40,7 @@ import { import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; -import { WORKFLOW_TRIGGERS, type WorkflowModel, isAllNodesValidated } from "@/domain/workflow"; +import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; @@ -219,6 +220,17 @@ const WorkflowList = () => { /> + +