From 37038469a1fe1cec6b546353237cfb506414a0be Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 13:08:26 +0800 Subject: [PATCH 1/9] fix: incompatible node ids --- migrations/1756296000_cm0.4.0_migrate.go | 17 +++++++++++++++++ ui/src/domain/workflow.ts | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/migrations/1756296000_cm0.4.0_migrate.go b/migrations/1756296000_cm0.4.0_migrate.go index 51c91879..5e3500d9 100644 --- a/migrations/1756296000_cm0.4.0_migrate.go +++ b/migrations/1756296000_cm0.4.0_migrate.go @@ -1304,6 +1304,23 @@ func init() { } } } + + // normalize field `nodeId` in collection `workflow`, `workflow_run`, `workflow_output`, `workflow_logs` + for i := 0; i < 3; i++ { + app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"-', '"id":"')`).Execute() + app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"_', '"id":"')`).Execute() + app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"-', '"id":"')`).Execute() + app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"_', '"id":"')`).Execute() + + app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"-', '"id":"')`).Execute() + app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"_', '"id":"')`).Execute() + + app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute() + app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '_%'`).Execute() + + app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute() + app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '_%'`).Execute() + } } tracer.Printf("done") diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 84f3c3d3..158ed402 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -174,7 +174,9 @@ export const defaultNodeConfigForBizNotify = (): Partial { - return nanoid(); + return nanoid() + .replace(/^[_-]+/g, "") + .replace(/[_-]+$/g, ""); }; export const newNode = (type: WorkflowNodeType, { i18n = getI18n() }: { i18n?: ReturnType }): WorkflowNode => { From 34749ae6e6dfafc5348cda437360c3ba2d4ae0e3 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 16:12:19 +0800 Subject: [PATCH 2/9] fix: could not activate workflow again --- ui/src/pages/dashboard/Dashboard.tsx | 6 ++++++ ui/src/pages/workflows/WorkflowList.tsx | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index dece63cc..618c3124 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -268,6 +268,12 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty width: 48, render: (_, __, index) => index + 1, }, + { + key: "id", + title: "ID", + width: 160, + render: (_, record) => {record.id}, + }, { key: "name", title: t("workflow.props.name"), diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index c6d1ef7a..0eceb7a7 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -313,7 +313,7 @@ const WorkflowList = () => { const handleRecordActiveChange = async (workflow: WorkflowModel) => { try { - if (!workflow.enabled && !workflow.graphContent) { + if (!workflow.enabled && !workflow.hasContent) { message.warning(t("workflow.action.enable.errmsg.unpublished")); return; } @@ -326,7 +326,8 @@ const WorkflowList = () => { setTableData((prev) => { return prev.map((item) => { if (item.id === workflow.id) { - return resp; + item.enabled = resp.enabled; + item.updated = resp.updated; } return item; }); From 6b741da6544fcb011081184f148f1c2b299496af Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 17:38:24 +0800 Subject: [PATCH 3/9] chore(ui): improve i18n --- ...BizDeployNodeConfigFieldsProviderLocal.tsx | 42 ++++++++++--------- .../BizDeployNodeConfigFieldsProviderSSH.tsx | 42 ++++++++++--------- .../i18n/locales/en/nls.workflow.nodes.json | 40 ++++++++++-------- .../i18n/locales/zh/nls.workflow.nodes.json | 42 ++++++++++--------- 4 files changed, 89 insertions(+), 77 deletions(-) diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx index 8bbd6fc8..e12ca8fd 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx @@ -234,16 +234,6 @@ const BizDeployNodeConfigFieldsProviderLocal = () => { /> - - - - { > + + + + + + }) return z .object({ format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.local_format.placeholder")), + keyPath: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), certPath: z .string() - .min(1, t("workflow_node.deploy.form.local_cert_path.tooltip")) + .min(1, t("workflow_node.deploy.form.local_cert_path.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), certPathForServerOnly: z .string() @@ -423,10 +429,6 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), - keyPath: z - .string() - .max(256, t("common.errmsg.string_max", { max: 256 })) - .nullish(), pfxPassword: z .string() .max(64, t("common.errmsg.string_max", { max: 256 })) @@ -460,7 +462,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.keyPath?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_key_path.tooltip"), + message: t("workflow_node.deploy.form.local_key_path.placeholder"), path: ["keyPath"], }); } @@ -472,7 +474,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.pfxPassword?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_pfx_password.tooltip"), + message: t("workflow_node.deploy.form.local_pfx_password.placeholder"), path: ["pfxPassword"], }); } @@ -484,7 +486,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksAlias?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_alias.tooltip"), + message: t("workflow_node.deploy.form.local_jks_alias.placeholder"), path: ["jksAlias"], }); } @@ -492,7 +494,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksKeypass?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_keypass.tooltip"), + message: t("workflow_node.deploy.form.local_jks_keypass.placeholder"), path: ["jksKeypass"], }); } @@ -500,7 +502,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksStorepass?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_storepass.tooltip"), + message: t("workflow_node.deploy.form.local_jks_storepass.placeholder"), path: ["jksStorepass"], }); } diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx index b29e74ab..a6a57f68 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx @@ -266,7 +266,7 @@ const BizDeployNodeConfigFieldsProviderSSH = () => { - - { > + + + + + + }) return z .object({ format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.ssh_format.placeholder")), - certPath: z - .string() - .min(1, t("workflow_node.deploy.form.ssh_cert_path.tooltip")) - .max(256, t("common.errmsg.string_max", { max: 256 })), keyPath: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), + certPath: z + .string() + .min(1, t("workflow_node.deploy.form.ssh_cert_path.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), certPathForServerOnly: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) @@ -502,7 +504,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.keyPath?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_key_path.tooltip"), + message: t("workflow_node.deploy.form.ssh_key_path.placeholder"), path: ["keyPath"], }); } @@ -514,7 +516,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.pfxPassword?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_pfx_password.tooltip"), + message: t("workflow_node.deploy.form.ssh_pfx_password.placeholder"), path: ["pfxPassword"], }); } @@ -526,7 +528,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksAlias?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_alias.tooltip"), + message: t("workflow_node.deploy.form.ssh_jks_alias.placeholder"), path: ["jksAlias"], }); } @@ -534,7 +536,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksKeypass?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_keypass.tooltip"), + message: t("workflow_node.deploy.form.ssh_jks_keypass.placeholder"), path: ["jksKeypass"], }); } @@ -542,7 +544,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) if (!values.jksStorepass?.trim()) { ctx.addIssue({ code: "custom", - message: t("workflow_node.deploy.form.ssh_jks_storepass.tooltip"), + message: t("workflow_node.deploy.form.ssh_jks_storepass.placeholder"), path: ["jksStorepass"], }); } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index e1d81920..03aa09be 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -612,17 +612,19 @@ "workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", "workflow_node.deploy.form.local_format.option.pfx.label": "PFX (*.pfx, *.p12)", "workflow_node.deploy.form.local_format.option.jks.label": "JKS (*.jks)", - "workflow_node.deploy.form.local_cert_path.label": "Certificate file saving path", - "workflow_node.deploy.form.local_cert_path.placeholder": "Please enter saving path for certificate file", - "workflow_node.deploy.form.local_cert_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.local_key_path.label": "Certificate's private key file saving path", - "workflow_node.deploy.form.local_key_path.placeholder": "Please enter saving path for certificate's private key file", + "workflow_node.deploy.form.local_key_path.label": "Private key file path", + "workflow_node.deploy.form.local_key_path.placeholder": "Please enter the local path for private key file", "workflow_node.deploy.form.local_key_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.local_servercert_path.label": "Server certificate file saving path (Optional)", - "workflow_node.deploy.form.local_servercert_path.placeholder": "Please enter saving path for server certificate file", + "workflow_node.deploy.form.local_cert_path.label": "Certificate file path", + "workflow_node.deploy.form.local_cert_path.placeholder": "Please enter the local path for certificate file", + "workflow_node.deploy.form.local_cert_path.help": "Notes: It should include the full file path, not just the directory.", + "workflow_node.deploy.form.local_fullchaincert_path.label": "Bundled fullchain certificate file path", + "workflow_node.deploy.form.local_fullchaincert_path.placeholder": "Please enter the local path for bundled fullchain certificate", + "workflow_node.deploy.form.local_servercert_path.label": "Server certificate file path (Optional)", + "workflow_node.deploy.form.local_servercert_path.placeholder": "Please enter the local path for server certificate file", "workflow_node.deploy.form.local_servercert_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.local_intermediacert_path.label": "Intermediate CA certificate file saving path (Optional)", - "workflow_node.deploy.form.local_intermediacert_path.placeholder": "Please enter saving path for intermediate CA certificate file", + "workflow_node.deploy.form.local_intermediacert_path.label": "Intermediate CA certificate file path (Optional)", + "workflow_node.deploy.form.local_intermediacert_path.placeholder": "Please enter the local path for intermediate CA certificate file", "workflow_node.deploy.form.local_intermediacert_path.help": "Notes: It should include the full file path, not just the directory.", "workflow_node.deploy.form.local_pfx_password.label": "PFX password", "workflow_node.deploy.form.local_pfx_password.placeholder": "Please enter PFX password", @@ -690,17 +692,19 @@ "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", "workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX (*.pfx, *.p12)", "workflow_node.deploy.form.ssh_format.option.jks.label": "JKS (*.jks)", - "workflow_node.deploy.form.ssh_cert_path.label": "Certificate file uploading path", - "workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter uploading path for certificate file", - "workflow_node.deploy.form.ssh_cert_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.ssh_key_path.label": "Certificate's private key file uploading path", - "workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter uploading path for certificate's private key file", + "workflow_node.deploy.form.ssh_key_path.label": "Private key file path", + "workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter the remote path for private key file", "workflow_node.deploy.form.ssh_key_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.ssh_servercert_path.label": "Server certificate file uploading path (Optional)", - "workflow_node.deploy.form.ssh_servercert_path.placeholder": "Please enter uploading path for server certificate file", + "workflow_node.deploy.form.ssh_cert_path.label": "Certificate file path", + "workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter the remote path for certificate", + "workflow_node.deploy.form.ssh_cert_path.help": "Notes: It should include the full file path, not just the directory.", + "workflow_node.deploy.form.ssh_fullchaincert_path.label": "Bundled fullchain certificate file path", + "workflow_node.deploy.form.ssh_fullchaincert_path.placeholder": "Please enter the remote path for bundled fullchain certificate", + "workflow_node.deploy.form.ssh_servercert_path.label": "Server certificate file path (Optional)", + "workflow_node.deploy.form.ssh_servercert_path.placeholder": "Please enter the remote path for server certificate file", "workflow_node.deploy.form.ssh_servercert_path.help": "Notes: It should include the full file path, not just the directory.", - "workflow_node.deploy.form.ssh_intermediacert_path.label": "Intermediate CA certificate file uploading path (Optional)", - "workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "Please enter uploading path for intermediate CA certificate file", + "workflow_node.deploy.form.ssh_intermediacert_path.label": "Intermediate CA certificate file path (Optional)", + "workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "Please enter the remote path for intermediate CA certificate file", "workflow_node.deploy.form.ssh_intermediacert_path.help": "Notes: It should include the full file path, not just the directory.", "workflow_node.deploy.form.ssh_pfx_password.label": "PFX password", "workflow_node.deploy.form.ssh_pfx_password.placeholder": "Please enter PFX password", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index d5dcfa17..242a1957 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -610,17 +610,19 @@ "workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)", "workflow_node.deploy.form.local_format.option.pfx.label": "PFX 格式(*.pfx, *.p12)", "workflow_node.deploy.form.local_format.option.jks.label": "JKS 格式(*.jks)", - "workflow_node.deploy.form.local_cert_path.label": "证书文件保存路径", - "workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件保存路径", - "workflow_node.deploy.form.local_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", - "workflow_node.deploy.form.local_key_path.label": "证书私钥文件保存路径", - "workflow_node.deploy.form.local_key_path.placeholder": "请输入证书私钥文件保存路径", + "workflow_node.deploy.form.local_key_path.label": "私钥文件路径", + "workflow_node.deploy.form.local_key_path.placeholder": "请输入私钥文件本地路径", "workflow_node.deploy.form.local_key_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", - "workflow_node.deploy.form.local_servercert_path.label": "服务器证书文件保存路径(可选)", - "workflow_node.deploy.form.local_servercert_path.placeholder": "请输入服务器证书文件保存路径", + "workflow_node.deploy.form.local_cert_path.label": "证书文件路径", + "workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件本地路径", + "workflow_node.deploy.form.local_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", + "workflow_node.deploy.form.local_fullchaincert_path.label": "证书链文件路径", + "workflow_node.deploy.form.local_fullchaincert_path.placeholder": "请输入证书链文件本地路径", + "workflow_node.deploy.form.local_servercert_path.label": "服务器证书文件路径(可选)", + "workflow_node.deploy.form.local_servercert_path.placeholder": "请输入服务器证书文件本地路径", "workflow_node.deploy.form.local_servercert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不会保存服务器证书。", - "workflow_node.deploy.form.local_intermediacert_path.label": "中间证书文件保存路径(可选)", - "workflow_node.deploy.form.local_intermediacert_path.placeholder": "请输入中间证书文件保存路径", + "workflow_node.deploy.form.local_intermediacert_path.label": "中间证书文件路径(可选)", + "workflow_node.deploy.form.local_intermediacert_path.placeholder": "请输入中间证书文件本地路径", "workflow_node.deploy.form.local_intermediacert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不会保存中间证书。", "workflow_node.deploy.form.local_pfx_password.label": "PFX 导出密码", "workflow_node.deploy.form.local_pfx_password.placeholder": "请输入 PFX 导出密码", @@ -688,19 +690,21 @@ "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)", "workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX 格式(*.pfx, *.p12)", "workflow_node.deploy.form.ssh_format.option.jks.label": "JKS 格式(*.jks)", - "workflow_node.deploy.form.ssh_cert_path.label": "证书文件上传路径", - "workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件上传路径", - "workflow_node.deploy.form.ssh_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", - "workflow_node.deploy.form.ssh_key_path.label": "证书私钥文件上传路径", - "workflow_node.deploy.form.ssh_key_path.placeholder": "请输入证书私钥文件上传路径", + "workflow_node.deploy.form.ssh_key_path.label": "私钥文件路径", + "workflow_node.deploy.form.ssh_key_path.placeholder": "请输入私钥文件远程路径", "workflow_node.deploy.form.ssh_key_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", - "workflow_node.deploy.form.ssh_pfx_password.label": "PFX 导出密码", - "workflow_node.deploy.form.ssh_servercert_path.label": "服务器证书文件上传路径(可选)", - "workflow_node.deploy.form.ssh_servercert_path.placeholder": "请输入服务器证书文件上传路径", + "workflow_node.deploy.form.ssh_cert_path.label": "证书文件路径", + "workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件远程路径", + "workflow_node.deploy.form.ssh_cert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。", + "workflow_node.deploy.form.ssh_fullchaincert_path.label": "证书链文件路径", + "workflow_node.deploy.form.ssh_fullchaincert_path.placeholder": "请输入证书链文件远程路径", + "workflow_node.deploy.form.ssh_servercert_path.label": "服务器证书文件路径(可选)", + "workflow_node.deploy.form.ssh_servercert_path.placeholder": "请输入服务器证书文件远程路径", "workflow_node.deploy.form.ssh_servercert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不上传服务器证书。", - "workflow_node.deploy.form.ssh_intermediacert_path.label": "中间证书文件上传路径(可选)", - "workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "请输入中间证书文件上传路径", + "workflow_node.deploy.form.ssh_intermediacert_path.label": "中间证书文件路径(可选)", + "workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "请输入中间证书文件远程路径", "workflow_node.deploy.form.ssh_intermediacert_path.help": "注意:路径需包含完整的文件名,而不是只有目录。不填写时将不上传中间证书。", + "workflow_node.deploy.form.ssh_pfx_password.label": "PFX 导出密码", "workflow_node.deploy.form.ssh_pfx_password.placeholder": "请输入 PFX 导出密码", "workflow_node.deploy.form.ssh_pfx_password.tooltip": "这是什么?请参阅 https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files", "workflow_node.deploy.form.ssh_jks_alias.label": "JKS 别名", From 6f0d468be9190853b389cf307f5d12f32e8298c5 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 18:12:41 +0800 Subject: [PATCH 4/9] fix: could not register acme external account --- internal/certapply/account.go | 20 +++++++++++++++++--- internal/certapply/config.go | 6 +++--- internal/domain/expr/expr.go | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/certapply/account.go b/internal/certapply/account.go index f609dfce..34d53cea 100644 --- a/internal/certapply/account.go +++ b/internal/certapply/account.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "errors" "fmt" + "strings" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" @@ -70,10 +71,23 @@ func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccou var regres *registration.Resource var regerr error if legoClient.GetExternalAccountRequired() { + if config.EABKid == "" { + return nil, errors.New("missing or invalid eab kid") + } + if config.EABHmacKey == "" { + return nil, errors.New("missing or invalid eab hmac key") + } + + // patch, see https://github.com/go-acme/lego/issues/2634 + keyId := strings.TrimSpace(config.EABKid) + keyEncoded := strings.TrimSpace(config.EABHmacKey) + keyEncoded = strings.ReplaceAll(strings.ReplaceAll(keyEncoded, "+", "-"), "/", "_") + keyEncoded = strings.TrimRight(keyEncoded, "=") + regres, regerr = legoClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, - Kid: config.EABKid, - HmacEncoded: config.EABHmacEncoded, + Kid: keyId, + HmacEncoded: keyEncoded, }) } else { regres, regerr = legoClient.Registration.Register(registration.RegisterOptions{ @@ -81,7 +95,7 @@ func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccou }) } if regerr != nil { - return nil, fmt.Errorf("failed to register acme account: %w", err) + return nil, fmt.Errorf("failed to register acme account: %w", regerr) } account.ACMEAccount = ®res.Body diff --git a/internal/certapply/config.go b/internal/certapply/config.go index a1eaeb73..8b36d778 100644 --- a/internal/certapply/config.go +++ b/internal/certapply/config.go @@ -34,7 +34,7 @@ type ACMEConfig struct { CAProvider domain.CAProviderType CADirUrl string EABKid string - EABHmacEncoded string + EABHmacKey string CertifierKeyType certcrypto.KeyType } @@ -93,12 +93,12 @@ func NewACMEConfig(options *ACMEConfigOptions) (*ACMEConfig, error) { ca.CADirUrl = endpoint } - eab := &domain.AccessConfigForACMEExternalAccountBinding{} + eab := domain.AccessConfigForACMEExternalAccountBinding{} if err := xmaps.Populate(caAccessConfig, &eab); err != nil { return nil, err } ca.EABKid = eab.EabKid - ca.EABHmacEncoded = eab.EabHmacKey + ca.EABHmacKey = eab.EabHmacKey return ca, nil } diff --git a/internal/domain/expr/expr.go b/internal/domain/expr/expr.go index 755a876c..e42a3967 100644 --- a/internal/domain/expr/expr.go +++ b/internal/domain/expr/expr.go @@ -53,7 +53,7 @@ func (e *EvalResult) GetFloat64() (float64, error) { floatValue, err := strconv.ParseFloat(stringValue, 64) if err != nil { - return 0, fmt.Errorf("failed to parse float64: %v", err) + return 0, fmt.Errorf("failed to parse float64: %w", err) } return floatValue, nil } From 09e938e77fbbe979afaca22a6ee0564d6b0167fb Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 20:56:47 +0800 Subject: [PATCH 5/9] chore(ui): improve i18n --- ui/src/components/access/AccessEditDrawer.tsx | 4 +++- ui/src/components/workflow/designer/forms/_shared.tsx | 2 +- ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.workflow.json | 1 - ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.workflow.json | 1 - 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/src/components/access/AccessEditDrawer.tsx b/ui/src/components/access/AccessEditDrawer.tsx index df5a2c26..75a9e050 100644 --- a/ui/src/components/access/AccessEditDrawer.tsx +++ b/ui/src/components/access/AccessEditDrawer.tsx @@ -29,7 +29,7 @@ export interface AccessEditDrawerProps { const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => { const { t } = useTranslation(); - const { notification } = App.useApp(); + const { message, notification } = App.useApp(); const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"])); @@ -57,6 +57,8 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge try { await formInst.validateFields(); } catch (err) { + message.warning(t("common.errmsg.form_invalid")); + setFormPending(false); throw err; } diff --git a/ui/src/components/workflow/designer/forms/_shared.tsx b/ui/src/components/workflow/designer/forms/_shared.tsx index 63e23df4..e99cf638 100644 --- a/ui/src/components/workflow/designer/forms/_shared.tsx +++ b/ui/src/components/workflow/designer/forms/_shared.tsx @@ -84,7 +84,7 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true, try { await formInst.validateFields(); } catch (err) { - message.warning(t("workflow.detail.design.drawer.errmsg.invalid_form")); + message.warning(t("common.errmsg.form_invalid")); setFormPending(false); throw err; diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 829ef007..daa4fe6c 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -44,6 +44,7 @@ "common.errmsg.port_invalid": "Please enter a valid port", "common.errmsg.ip_invalid": "Please enter a valid IP address", "common.errmsg.url_invalid": "Please enter a valid URL", + "common.errmsg.form_invalid": "Please check the form content", "common.notifier.bark": "Bark", "common.notifier.dingtalk": "DingTalk", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 7f3e42bb..0deeadeb 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -76,7 +76,6 @@ "workflow.detail.design.drawer.node_id.label": "Node ID: ", "workflow.detail.design.drawer.disabled.on.tooltip": "Disable", "workflow.detail.design.drawer.disabled.off.tooltip": "Enable", - "workflow.detail.design.drawer.errmsg.invalid_form": "Please check the form content", "workflow.detail.design.action.publish.button": "Publish", "workflow.detail.design.action.publish.modal.title": "Publish changes", "workflow.detail.design.action.publish.modal.content": "Are you sure to publish your changes?", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 5bfe67f3..731ef9e3 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -44,6 +44,7 @@ "common.errmsg.port_invalid": "请输入正确的端口号", "common.errmsg.ip_invalid": "请输入正确的 IP 地址", "common.errmsg.url_invalid": "请输入正确的 URL 地址", + "common.errmsg.form_invalid": "请检查表单内容", "common.notifier.bark": "Bark", "common.notifier.dingtalk": "钉钉", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 0667095d..67e5948b 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -76,7 +76,6 @@ "workflow.detail.design.drawer.node_id.label": "节点 ID:", "workflow.detail.design.drawer.disabled.on.tooltip": "禁用", "workflow.detail.design.drawer.disabled.off.tooltip": "启用", - "workflow.detail.design.drawer.errmsg.invalid_form": "请检查表单内容", "workflow.detail.design.action.publish.button": "发布更改", "workflow.detail.design.action.publish.modal.title": "发布更改", "workflow.detail.design.action.publish.modal.content": "确定要发布更改吗?", From 1b0f652de3ed3a512420b75bf587f60d80ebecd3 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 21:04:43 +0800 Subject: [PATCH 6/9] chore: improve logging --- internal/workflow/engine/engine.go | 2 +- internal/workflow/engine/executor_bizapply.go | 14 +++++++------- internal/workflow/engine/executor_bizdeploy.go | 4 ++-- internal/workflow/engine/executor_bizmonitor.go | 2 +- internal/workflow/engine/executor_biznotify.go | 2 +- internal/workflow/engine/executor_bizupload.go | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/workflow/engine/engine.go b/internal/workflow/engine/engine.go index 210c800c..39a13143 100644 --- a/internal/workflow/engine/engine.go +++ b/internal/workflow/engine/engine.go @@ -180,7 +180,7 @@ func (we *workflowEngine) executeNode(wfCtx *WorkflowContext, node *Node) error }) } if _, err := we.wfoutputRepo.Save(execCtx.ctx, output); err != nil { - we.syslog.Warn("failed to save node output") + we.syslog.Error("failed to save node output", slog.Any("error", err)) } } diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go index 2d6c1921..7f8e16f4 100644 --- a/internal/workflow/engine/executor_bizapply.go +++ b/internal/workflow/engine/executor_bizapply.go @@ -87,7 +87,7 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe // 解析证书 certX509, err := xcert.ParseCertificateFromPEM(obtainResp.FullChainCertificate) if err != nil { - ne.logger.Warn("failed to parse certificate, may be the CA responded error") + ne.logger.Warn("could not parse certificate, may be the CA responded error") return execRes, err } @@ -106,7 +106,7 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe } certificate.PopulateFromX509(certX509) if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil { - ne.logger.Warn("failed to save certificate") + ne.logger.Warn("could not save certificate") return execRes, err } else { ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) @@ -232,7 +232,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod } legoConfig, err := certapply.NewACMEConfig(legoOptions) if err != nil { - ne.logger.Warn("failed to initialize acme config") + ne.logger.Warn("could not initialize acme config") return nil, err } else { ne.logger.Info("acme config initialized", slog.String("acmeDirUrl", legoConfig.CADirUrl)) @@ -242,7 +242,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod // 注意此步骤仍需在主进程中进行,以保证并发安全 legoUser, err := certapply.NewACMEAccountWithSingleFlight(legoConfig, nodeCfg.ContactEmail) if err != nil { - ne.logger.Warn("failed to initialize acme account") + ne.logger.Warn("could not initialize acme account") return nil, err } else { ne.logger.Info("acme account initialized", slog.String("acmeAcctUrl", legoUser.ACMEAcctUrl)) @@ -320,7 +320,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod Request: obtainReq, }) if err != nil { - ne.logger.Warn("failed to obtain certificate") + ne.logger.Warn("could not obtain certificate") return nil, err } @@ -339,14 +339,14 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod return nil }) if err != nil { - ne.logger.Warn("failed to initialize acme client") + ne.logger.Warn("could not initialize acme client") return nil, err } // 执行申请证书请求 obtainResp, err := legoClient.ObtainCertificate(execCtx.ctx, obtainReq) if err != nil { - ne.logger.Warn("failed to obtain certificate") + ne.logger.Warn("could not obtain certificate") return nil, err } diff --git a/internal/workflow/engine/executor_bizdeploy.go b/internal/workflow/engine/executor_bizdeploy.go index 9f8b98b8..1f05e5f5 100644 --- a/internal/workflow/engine/executor_bizdeploy.go +++ b/internal/workflow/engine/executor_bizdeploy.go @@ -43,7 +43,7 @@ func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx if len(s) == 2 { certificate, err := ne.certificateRepo.GetById(execCtx.ctx, s[1]) if err != nil { - ne.logger.Warn("failed to get input certificate") + ne.logger.Warn("could not get input certificate") return execRes, err } @@ -89,7 +89,7 @@ func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx PrivateKey: inputCertificate.PrivateKey, } if _, err := deployClient.DeployCertificate(execCtx.ctx, deployReq); err != nil { - ne.logger.Warn("failed to deploy certificate") + ne.logger.Warn("could not deploy certificate") return execRes, err } diff --git a/internal/workflow/engine/executor_bizmonitor.go b/internal/workflow/engine/executor_bizmonitor.go index d4616e01..b3304e71 100644 --- a/internal/workflow/engine/executor_bizmonitor.go +++ b/internal/workflow/engine/executor_bizmonitor.go @@ -67,7 +67,7 @@ func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeE } if err != nil { - ne.logger.Warn("failed to monitor certificate") + ne.logger.Warn("could not retrieve certificate") return execRes, err } else { if len(certs) == 0 { diff --git a/internal/workflow/engine/executor_biznotify.go b/internal/workflow/engine/executor_biznotify.go index e1410fae..5ba43d72 100644 --- a/internal/workflow/engine/executor_biznotify.go +++ b/internal/workflow/engine/executor_biznotify.go @@ -49,7 +49,7 @@ func (ne *bizNotifyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx Message: nodeCfg.Message, } if _, err := notifyClient.SendNotification(execCtx.ctx, notifyReq); err != nil { - ne.logger.Warn("failed to send notification") + ne.logger.Warn("could not send notification") return execRes, err } diff --git a/internal/workflow/engine/executor_bizupload.go b/internal/workflow/engine/executor_bizupload.go index aa8a5eef..c62cea79 100644 --- a/internal/workflow/engine/executor_bizupload.go +++ b/internal/workflow/engine/executor_bizupload.go @@ -63,7 +63,7 @@ func (ne *bizUploadNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeEx } certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil { - ne.logger.Warn("failed to save certificate") + ne.logger.Warn("could not save certificate") return execRes, err } else { ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) From 299e2202fd2a21ba43bcf42663b595ed8f254a5f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 21:18:14 +0800 Subject: [PATCH 7/9] fix: #906 --- .../i18n/locales/en/nls.workflow.nodes.json | 8 +- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- ui/src/utils/cron.ts | 91 ++++++++++++++++++- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 03aa09be..60ed9ca9 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -11,10 +11,10 @@ "workflow_node.start.form.trigger.placeholder": "Please select trigger", "workflow_node.start.form.trigger.option.scheduled.label": "Scheduled", "workflow_node.start.form.trigger.option.manual.label": "Manual", - "workflow_node.start.form.trigger_cron.label": "Cron expression", - "workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression", - "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression", - "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments.", + "workflow_node.start.form.trigger_cron.label": "CRON expression", + "workflow_node.start.form.trigger_cron.placeholder": "Please enter CRON expression", + "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid CRON expression", + "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments, in standard crontab rules.", "workflow_node.start.form.trigger_cron.help": "Expected execution time for the last 5 times (the actual time zone is based on the server):", "workflow_node.start.form.trigger_cron.guide": "If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 242a1957..e214f6e9 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -11,10 +11,10 @@ "workflow_node.start.form.trigger.placeholder": "请选择触发方式", "workflow_node.start.form.trigger.option.scheduled.label": "定时触发", "workflow_node.start.form.trigger.option.manual.label": "手动触发", - "workflow_node.start.form.trigger_cron.label": "Cron 表达式", - "workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式", - "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", - "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", + "workflow_node.start.form.trigger_cron.label": "CRON 表达式", + "workflow_node.start.form.trigger_cron.placeholder": "请输入 CRON 表达式", + "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 CRON 表达式", + "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,使用 crontab 标准语法规则。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", "workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):", "workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts index 46d806e3..8e3dd7e3 100644 --- a/ui/src/utils/cron.ts +++ b/ui/src/utils/cron.ts @@ -4,7 +4,16 @@ export const validCronExpression = (expr: string): boolean => { try { CronExpressionParser.parse(expr); - if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 + // pocketbase 后端仅支持标准 crontab 形式的表达式 + // 这里转译了来自 pocketbase 的 golang 代码来验证 + const segments = expr.trim().split(" "); + if (segments.length !== 5) return false; + parseCronSegment(segments[0], 0, 59); + parseCronSegment(segments[1], 0, 23); + parseCronSegment(segments[2], 1, 31); + parseCronSegment(segments[3], 1, 12); + parseCronSegment(segments[4], 0, 6); + return true; } catch { return false; @@ -19,3 +28,83 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => { return cron.take(times).map((date) => date.toDate()); }; + +// transpile from: +// https://github.com/pocketbase/pocketbase/blob/5d964c1b1d020f425299b32df03ecf44e0a0502e/tools/cron/schedule.go#L141-L218 +function parseCronSegment(segment: string, min: number, max: number): Set { + const slots = new Set(); + + const list = segment.split(","); + for (const p of list) { + const stepParts = p.split("/"); + + let step: number; + switch (stepParts.length) { + case 1: + { + step = 1; + } + break; + case 2: + { + const parsedStep = parseInt(stepParts[1], 10); + if (isNaN(parsedStep) || parsedStep < 1 || parsedStep > max) { + throw new Error(`invalid segment step boundary - the step must be between 1 and the ${max}`); + } + step = parsedStep; + } + break; + + default: + throw new Error("invalid segment step format - must be in the format */n or 1-30/n"); + } + + let rangeMin: number, rangeMax: number; + if (stepParts[0] === "*") { + rangeMin = min; + rangeMax = max; + } else { + const rangeParts = stepParts[0].split("-"); + switch (rangeParts.length) { + case 1: + { + if (step !== 1) { + throw new Error("invalid segment step - step > 1 could be used only with the wildcard or range format"); + } + const parsed = parseInt(rangeParts[0], 10); + if (isNaN(parsed) || parsed < min || parsed > max) { + throw new Error("invalid segment value - must be between the min and max of the segment"); + } + rangeMin = parsed; + rangeMax = rangeMin; + } + break; + + case 2: + { + const parsedMin = parseInt(rangeParts[0], 10); + if (isNaN(parsedMin) || parsedMin < min || parsedMin > max) { + throw new Error(`invalid segment range minimum - must be between ${min} and ${max}`); + } + rangeMin = parsedMin; + + const parsedMax = parseInt(rangeParts[1], 10); + if (isNaN(parsedMax) || parsedMax < rangeMin || parsedMax > max) { + throw new Error(`invalid segment range maximum - must be between ${rangeMin} and ${max}`); + } + rangeMax = parsedMax; + } + break; + + default: + throw new Error("invalid segment range format - the range must have 1 or 2 parts"); + } + } + + for (let i = rangeMin; i <= rangeMax; i += step) { + slots.add(i); + } + } + + return slots; +} From d9a1facbe1f9b5b782f7d682d097733de5ae8b82 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 21:36:47 +0800 Subject: [PATCH 8/9] chore: improve logging --- internal/workflow/dispatcher/dispatcher.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index 3802d10c..b53189e6 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -330,8 +330,8 @@ func (wd *workflowDispatcher) tryNextAsync() { if hasSameWorkflowTask { wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because tasks that belonging to the same workflow already exists", pendingRunId)) - } else if len(wd.processingTasks) >= wd.concurrency { - wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because the maximum concurrency limit has been reached", pendingRunId)) + } else if len(wd.processingTasks) >= wd.concurrency && wd.concurrency > 0 { + wd.syslog.Warn(fmt.Sprintf("workflow run #%s is pending, because the maximum concurrency (limit: %d) has been reached", pendingRunId, wd.concurrency)) } else { wd.taskMtx.RUnlock() wd.taskMtx.Lock() From 66149e0a510940abeed8f8cabfa9566df6d4fee5 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 5 Sep 2025 21:50:25 +0800 Subject: [PATCH 9/9] fix: #848 --- pkg/utils/filepath/path.go | 33 +++++++++++++++++++++++++++++++++ pkg/utils/ssh/io.go | 7 ++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 pkg/utils/filepath/path.go diff --git a/pkg/utils/filepath/path.go b/pkg/utils/filepath/path.go new file mode 100644 index 00000000..48d1e132 --- /dev/null +++ b/pkg/utils/filepath/path.go @@ -0,0 +1,33 @@ +package filepath + +import ( + stdfilepath "path/filepath" + "strings" +) + +// 与标准库中的 [filepath.Dir] 类似,但会尝试保留原有的路径分隔符。 +// +// 入参: +// - path: 文件路径。 +// +// 出参: +// - 目录路径。 +func Dir(path string) string { + const SEP_WIN = "\\" + const SEP_UNIX = "/" + + sep := SEP_UNIX + if strings.Contains(path, SEP_WIN) && !strings.Contains(path, SEP_UNIX) { + sep = SEP_WIN + } + + dir := stdfilepath.Dir(path) + + if sep != SEP_UNIX && strings.Contains(dir, SEP_UNIX) { + dir = strings.ReplaceAll(dir, SEP_UNIX, sep) + } else if sep != SEP_WIN && strings.Contains(dir, SEP_WIN) { + dir = strings.ReplaceAll(dir, SEP_WIN, sep) + } + + return dir +} diff --git a/pkg/utils/ssh/io.go b/pkg/utils/ssh/io.go index 7261e58f..ff1d7753 100644 --- a/pkg/utils/ssh/io.go +++ b/pkg/utils/ssh/io.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "os" - "path/filepath" "github.com/pkg/sftp" "github.com/povsister/scp" "golang.org/x/crypto/ssh" + + xfilepath "github.com/certimate-go/certimate/pkg/utils/filepath" ) // 与 [WriteRemote] 类似,但写入的是字符串内容。 @@ -97,7 +98,7 @@ func writeRemoteWithSFTP(sshCli *ssh.Client, path string, data []byte) error { } defer sftpCli.Close() - if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil { + if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } @@ -122,7 +123,7 @@ func removeRemoteWithSFTP(sshCli *ssh.Client, path string) error { } defer sftpCli.Close() - if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil { + if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) }