From c451bf5e030770c3f052ccb186977678b916b1bc Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 18 Feb 2025 16:14:54 +0800 Subject: [PATCH] feat: support multiple sites on deployment to baotapanel site --- internal/deployer/providers.go | 8 +- .../baotapanel-site/baotapanel_site.go | 83 ++++++++-- .../baotapanel-site/baotapanel_site_test.go | 4 + internal/pkg/vendors/btpanel-sdk/api.go | 26 +++ internal/pkg/vendors/btpanel-sdk/client.go | 4 +- internal/pkg/vendors/btpanel-sdk/models.go | 37 ++++- .../workflow/node/ApplyNodeConfigForm.tsx | 1 + ...loyNodeConfigFormAliyunCASDeployConfig.tsx | 1 + ...ployNodeConfigFormBaotaPanelSiteConfig.tsx | 148 ++++++++++++++++-- ...eConfigFormTencentCloudSSLDeployConfig.tsx | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 10 ++ .../i18n/locales/zh/nls.workflow.nodes.json | 10 ++ 12 files changed, 293 insertions(+), 40 deletions(-) diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 9b2bd4f7..34e3ef0c 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -258,9 +258,11 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeBaotaPanelSite: deployer, err := pBaotaPanelSite.NewWithLogger(&pBaotaPanelSite.BaotaPanelSiteDeployerConfig{ - ApiUrl: access.ApiUrl, - ApiKey: access.ApiKey, - SiteName: maps.GetValueAsString(options.ProviderDeployConfig, "siteName"), + ApiUrl: access.ApiUrl, + ApiKey: access.ApiKey, + SiteType: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "siteType", "other"), + SiteName: maps.GetValueAsString(options.ProviderDeployConfig, "siteName"), + SiteNames: slices.Filter(strings.Split(maps.GetValueAsString(options.ProviderDeployConfig, "siteNames"), ";"), func(s string) bool { return s != "" }), }, logger) return deployer, logger, err diff --git a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go index 6fffa670..01cabdde 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go +++ b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go @@ -3,12 +3,14 @@ import ( "context" "errors" + "fmt" "net/url" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/logger" + "github.com/usual2970/certimate/internal/pkg/utils/slices" btsdk "github.com/usual2970/certimate/internal/pkg/vendors/btpanel-sdk" ) @@ -17,8 +19,12 @@ type BaotaPanelSiteDeployerConfig struct { ApiUrl string `json:"apiUrl"` // 宝塔面板接口密钥。 ApiKey string `json:"apiKey"` - // 站点名称。 - SiteName string `json:"siteName"` + // 站点类型。 + SiteType string `json:"siteType"` + // 站点名称(单个)。 + SiteName string `json:"siteName,omitempty"` + // 站点名称(多个)。 + SiteNames []string `json:"siteNames,omitempty"` } type BaotaPanelSiteDeployer struct { @@ -55,22 +61,65 @@ func NewWithLogger(config *BaotaPanelSiteDeployerConfig, logger logger.Logger) ( } func (d *BaotaPanelSiteDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { - if d.config.SiteName == "" { - return nil, errors.New("config `siteName` is required") - } + switch d.config.SiteType { + case "php": + { + if d.config.SiteName == "" { + return nil, errors.New("config `siteName` is required") + } - // 设置站点 SSL 证书 - siteSetSSLReq := &btsdk.SiteSetSSLRequest{ - SiteName: d.config.SiteName, - Type: "0", - PrivateKey: privkeyPem, - Certificate: certPem, - } - siteSetSSLResp, err := d.sdkClient.SiteSetSSL(siteSetSSLReq) - if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request 'bt.SiteSetSSL'") - } else { - d.logger.Logt("已设置站点 SSL 证书", siteSetSSLResp) + // 设置站点 SSL 证书 + siteSetSSLReq := &btsdk.SiteSetSSLRequest{ + SiteName: d.config.SiteName, + Type: "0", + Certificate: certPem, + PrivateKey: privkeyPem, + } + siteSetSSLResp, err := d.sdkClient.SiteSetSSL(siteSetSSLReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'bt.SiteSetSSL'") + } else { + d.logger.Logt("已设置站点证书", siteSetSSLResp) + } + } + + case "other": + { + if len(d.config.SiteNames) == 0 { + return nil, errors.New("config `siteNames` is required") + } + + // 上传证书 + sslCertSaveCertReq := &btsdk.SSLCertSaveCertRequest{ + Certificate: certPem, + PrivateKey: privkeyPem, + } + sslCertSaveCertResp, err := d.sdkClient.SSLCertSaveCert(sslCertSaveCertReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'bt.SSLCertSaveCert'") + } else { + d.logger.Logt("已上传证书", sslCertSaveCertResp) + } + + // 设置站点证书 + sslSetBatchCertToSiteReq := &btsdk.SSLSetBatchCertToSiteRequest{ + BatchInfo: slices.Map(d.config.SiteNames, func(siteName string) *btsdk.SSLSetBatchCertToSiteRequestBatchInfo { + return &btsdk.SSLSetBatchCertToSiteRequestBatchInfo{ + SiteName: siteName, + SSLHash: sslCertSaveCertResp.SSLHash, + } + }), + } + sslSetBatchCertToSiteResp, err := d.sdkClient.SSLSetBatchCertToSite(sslSetBatchCertToSiteReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'bt.SSLSetBatchCertToSite'") + } else { + d.logger.Logt("已设置站点证书", sslSetBatchCertToSiteResp) + } + } + + default: + return nil, fmt.Errorf("unsupported site type: %s", d.config.SiteType) } return &deployer.DeployResult{}, nil diff --git a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site_test.go b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site_test.go index ad45b56c..d3cc52cd 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site_test.go +++ b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site_test.go @@ -16,6 +16,7 @@ var ( fInputKeyPath string fApiUrl string fApiKey string + fSiteType string fSiteName string ) @@ -26,6 +27,7 @@ func init() { flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") + flag.StringVar(&fSiteType, argsPrefix+"SITETYPE", "", "") flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "") } @@ -37,6 +39,7 @@ Shell command to run this test: --CERTIMATE_DEPLOYER_BAOTAPANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CERTIMATE_DEPLOYER_BAOTAPANELSITE_APIURL="http://127.0.0.1:8888" \ --CERTIMATE_DEPLOYER_BAOTAPANELSITE_APIKEY="your-api-key" \ + --CERTIMATE_DEPLOYER_BAOTAPANELSITE_SITETYPE="php" \ --CERTIMATE_DEPLOYER_BAOTAPANELSITE_SITENAME="your-site-name" */ func TestDeploy(t *testing.T) { @@ -49,6 +52,7 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APIURL: %v", fApiUrl), fmt.Sprintf("APIKEY: %v", fApiKey), + fmt.Sprintf("SITETYPE: %v", fSiteType), fmt.Sprintf("SITENAME: %v", fSiteName), }, "\n")) diff --git a/internal/pkg/vendors/btpanel-sdk/api.go b/internal/pkg/vendors/btpanel-sdk/api.go index f197e081..b4efa433 100644 --- a/internal/pkg/vendors/btpanel-sdk/api.go +++ b/internal/pkg/vendors/btpanel-sdk/api.go @@ -42,3 +42,29 @@ func (c *Client) SystemServiceAdmin(req *SystemServiceAdminRequest) (*SystemServ } return &result, nil } + +func (c *Client) SSLCertSaveCert(req *SSLCertSaveCertRequest) (*SSLCertSaveCertResponse, error) { + params := make(map[string]any) + jsonData, _ := json.Marshal(req) + json.Unmarshal(jsonData, ¶ms) + + result := SSLCertSaveCertResponse{} + err := c.sendRequestWithResult("/ssl/cert/save_cert", params, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +func (c *Client) SSLSetBatchCertToSite(req *SSLSetBatchCertToSiteRequest) (*SSLSetBatchCertToSiteResponse, error) { + params := make(map[string]any) + jsonData, _ := json.Marshal(req) + json.Unmarshal(jsonData, ¶ms) + + result := SSLSetBatchCertToSiteResponse{} + err := c.sendRequestWithResult("/ssl?action=SetBatchCertToSite", params, &result) + if err != nil { + return nil, err + } + return &result, nil +} diff --git a/internal/pkg/vendors/btpanel-sdk/client.go b/internal/pkg/vendors/btpanel-sdk/client.go index 8134feca..32f3eba8 100644 --- a/internal/pkg/vendors/btpanel-sdk/client.go +++ b/internal/pkg/vendors/btpanel-sdk/client.go @@ -74,10 +74,10 @@ func (c *Client) sendRequestWithResult(path string, params map[string]any, resul if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("baota api error: failed to parse response: %w", err) } else if errstatus := result.GetStatus(); errstatus != nil && !*errstatus { - if result.GetMsg() == nil { + if result.GetMessage() == nil { return fmt.Errorf("baota api error: unknown error") } else { - return fmt.Errorf("baota api error: %s", *result.GetMsg()) + return fmt.Errorf("baota api error: %s", *result.GetMessage()) } } diff --git a/internal/pkg/vendors/btpanel-sdk/models.go b/internal/pkg/vendors/btpanel-sdk/models.go index f733d67a..8625e539 100644 --- a/internal/pkg/vendors/btpanel-sdk/models.go +++ b/internal/pkg/vendors/btpanel-sdk/models.go @@ -2,20 +2,20 @@ package btpanelsdk type BaseResponse interface { GetStatus() *bool - GetMsg() *string + GetMessage() *string } type baseResponse struct { - Status *bool `json:"status,omitempty"` - Msg *string `json:"msg,omitempty"` + Status *bool `json:"status,omitempty"` + Message *string `json:"msg,omitempty"` } func (r *baseResponse) GetStatus() *bool { return r.Status } -func (r *baseResponse) GetMsg() *string { - return r.Msg +func (r *baseResponse) GetMessage() *string { + return r.Message } type ConfigSavePanelSSLRequest struct { @@ -46,3 +46,30 @@ type SystemServiceAdminRequest struct { type SystemServiceAdminResponse struct { baseResponse } + +type SSLCertSaveCertRequest struct { + PrivateKey string `json:"key"` + Certificate string `json:"csr"` +} + +type SSLCertSaveCertResponse struct { + baseResponse + SSLHash string `json:"ssl_hash"` +} + +type SSLSetBatchCertToSiteRequest struct { + BatchInfo []*SSLSetBatchCertToSiteRequestBatchInfo `json:"BatchInfo"` +} + +type SSLSetBatchCertToSiteRequestBatchInfo struct { + SSLHash string `json:"ssl_hash"` + SiteName string `json:"siteName"` + CertName string `json:"certName"` +} + +type SSLSetBatchCertToSiteResponse struct { + baseResponse + TotalCount int32 `json:"total"` + SuccessCount int32 `json:"success"` + FailedCount int32 `json:"faild"` +} diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 5a0472dd..7b43cad2 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -70,6 +70,7 @@ const ApplyNodeConfigForm = forwardRef { + if (!v) return false; return String(v) .split(MULTIPLE_INPUT_DELIMITER) .every((e) => validDomainName(e, { allowWildcard: true })); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx index 05bc632c..065f752c 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx @@ -44,6 +44,7 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({ .nonempty(t("workflow_node.deploy.form.aliyun_cas_deploy_region.placeholder")) .trim(), resourceIds: z.string({ message: t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.placeholder") }).refine((v) => { + if (!v) return false; return String(v) .split(MULTIPLE_INPUT_DELIMITER) .every((e) => /^[1-9]\d*$/.test(e)); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx index aa891245..f62379bb 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormBaotaPanelSiteConfig.tsx @@ -1,10 +1,19 @@ +import { memo } from "react"; import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; +import { FormOutlined as FormOutlinedIcon } from "@ant-design/icons"; +import { Button, Form, type FormInstance, Input, Select, Space } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import ModalForm from "@/components/ModalForm"; +import MultipleInput from "@/components/MultipleInput"; +import Show from "@/components/Show"; +import { useAntdForm } from "@/hooks"; + type DeployNodeConfigFormBaotaPanelSiteConfigFieldValues = Nullish<{ - siteName: string; + siteType: string; + siteName?: string; + siteNames?: string; }>; export type DeployNodeConfigFormBaotaPanelSiteConfigProps = { @@ -15,8 +24,17 @@ export type DeployNodeConfigFormBaotaPanelSiteConfigProps = { onValuesChange?: (values: DeployNodeConfigFormBaotaPanelSiteConfigFieldValues) => void; }; +const SITE_TYPE_PHP = "php"; +const SITE_TYPE_OTHER = "other"; + +const MULTIPLE_INPUT_DELIMITER = ";"; + const initFormModel = (): DeployNodeConfigFormBaotaPanelSiteConfigFieldValues => { - return {}; + return { + siteType: SITE_TYPE_OTHER, + siteName: "", + siteNames: "", + }; }; const DeployNodeConfigFormBaotaPanelSiteConfig = ({ @@ -29,13 +47,32 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({ const { t } = useTranslation(); const formSchema = z.object({ + siteType: z.union([z.literal(SITE_TYPE_PHP), z.literal(SITE_TYPE_OTHER)], { + message: t("workflow_node.deploy.form.baotapanel_site_type.placeholder"), + }), siteName: z - .string({ message: t("workflow_node.deploy.form.baotapanel_site_name.placeholder") }) - .nonempty(t("workflow_node.deploy.form.baotapanel_site_name.placeholder")) - .trim(), + .string() + .nullish() + .refine((v) => { + if (fieldSiteType !== SITE_TYPE_PHP) return true; + return !!v?.trim(); + }, t("workflow_node.deploy.form.baotapanel_site_name.placeholder")), + siteNames: z + .string() + .nullish() + .refine((v) => { + if (fieldSiteType !== SITE_TYPE_OTHER) return true; + if (!v) return false; + return String(v) + .split(MULTIPLE_INPUT_DELIMITER) + .every((e) => !!e.trim()); + }, t("workflow_node.deploy.form.baotapanel_site_names.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); + const fieldSiteType = Form.useWatch("siteType", formInst); + const fieldSiteNames = Form.useWatch("siteNames", formInst); + const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -49,16 +86,101 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({ name={formName} onValuesChange={handleFormChange} > - } - > - + + + + + } + > + + + + + + } + > + + + { + formInst.setFieldValue("siteNames", e.target.value); + }} + /> + + + + + } + onChange={(value) => { + formInst.setFieldValue("siteNames", value); + }} + /> + + + ); }; +const SiteNamesModalInput = memo(({ value, trigger, onChange }: { value?: string; trigger?: React.ReactNode; onChange?: (value: string) => void }) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + siteNames: z.array(z.string()).refine((v) => { + return v.every((e) => !!e?.trim()); + }, t("workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeDeployConfigFormBaotaPanelSiteNamesModalInput", + initialValues: { siteNames: value?.split(MULTIPLE_INPUT_DELIMITER) }, + onSubmit: (values) => { + onChange?.( + values.siteNames + .map((e) => e.trim()) + .filter((e) => !!e) + .join(MULTIPLE_INPUT_DELIMITER) + ); + }, + }); + + return ( + + + + + + ); +}); + export default DeployNodeConfigFormBaotaPanelSiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx index 14266151..67cdbf11 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx @@ -48,6 +48,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({ .nonempty(t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_type.placeholder")) .trim(), resourceIds: z.string({ message: t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder") }).refine((v) => { + if (!v) return false; return String(v) .split(MULTIPLE_INPUT_DELIMITER) .every((e) => /^[A-Za-z0-9._-]+$/.test(e)); diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index ae79074f..6bf12328 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -194,9 +194,19 @@ "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan CDN domain name", "workflow_node.deploy.form.baishan_cdn_domain.tooltip": "For more information, see https://cdnx.console.baishan.com", "workflow_node.deploy.form.baotapanel_console_auto_restart.label": "Auto restart after deployment", + "workflow_node.deploy.form.baotapanel_site_type.label": "BaoTa Panel site type", + "workflow_node.deploy.form.baotapanel_site_type.placeholder": "Please select BaoTa Panel site type", + "workflow_node.deploy.form.baotapanel_site_type.option.php.label": "PHP sites", + "workflow_node.deploy.form.baotapanel_site_type.option.other.label": "Other sites", "workflow_node.deploy.form.baotapanel_site_name.label": "BaoTa Panel site name", "workflow_node.deploy.form.baotapanel_site_name.placeholder": "Please enter BaoTa Panel site name", "workflow_node.deploy.form.baotapanel_site_name.tooltip": "Usually equal to the website domain name.", + "workflow_node.deploy.form.baotapanel_site_names.label": "BaoTa Panel site names", + "workflow_node.deploy.form.baotapanel_site_names.placeholder": "Please enter BaoTa Panel site names (separated by semicolons)", + "workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "Please enter a valid BaoTa Panel site name", + "workflow_node.deploy.form.baotapanel_site_names.tooltip": "Usually equal to the websites domain name.", + "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title": "Change BaoTa Panel site names", + "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "Please enter BaoTa Panel site name", "workflow_node.deploy.form.byteplus_cdn_domain.label": "BytePlus CDN domain", "workflow_node.deploy.form.byteplus_cdn_domain.placeholder": "Please enter BytePlus CDN domain name", "workflow_node.deploy.form.byteplus_cdn_domain.tooltip": "For more information, see https://console.byteplus.com/cdn", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index ad886a02..e4b341e7 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -194,9 +194,19 @@ "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "请输入白山云 CDN 加速域名", "workflow_node.deploy.form.baishan_cdn_domain.tooltip": "这是什么?请参阅 https://cdnx.console.baishan.com", "workflow_node.deploy.form.baotapanel_console_auto_restart.label": "部署后自动重启面板服务", + "workflow_node.deploy.form.baotapanel_site_type.label": "宝塔面板网站类型", + "workflow_node.deploy.form.baotapanel_site_type.placeholder": "请选择宝塔面板网站类型", + "workflow_node.deploy.form.baotapanel_site_type.option.php.label": "PHP", + "workflow_node.deploy.form.baotapanel_site_type.option.other.label": "其他", "workflow_node.deploy.form.baotapanel_site_name.label": "宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_name.placeholder": "请输入宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_name.tooltip": "通常为网站域名。", + "workflow_node.deploy.form.baotapanel_site_names.label": "宝塔面板网站名称", + "workflow_node.deploy.form.baotapanel_site_names.placeholder": "请输入宝塔面板网站名称(多个值请用半角分号隔开)", + "workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "请输入正确的宝塔面板网站名称", + "workflow_node.deploy.form.baotapanel_site_names.tooltip": "通常为网站域名。", + "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title": "修改宝塔面板网站名称", + "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "请输入宝塔面板网站名称", "workflow_node.deploy.form.byteplus_cdn_domain.label": "BytePlus CDN 域名(支持泛域名)", "workflow_node.deploy.form.byteplus_cdn_domain.placeholder": "请输入 BytePlus CDN 域名", "workflow_node.deploy.form.byteplus_cdn_domain.tooltip": "这是什么?请参阅 https://console.byteplus.com/cdn",