feat: new deployment provider: baotawaf site

This commit is contained in:
Fu Diwei 2025-05-20 21:16:28 +08:00
parent 4ad08d983a
commit 591df58992
22 changed files with 628 additions and 8 deletions

View File

@ -35,6 +35,7 @@ import (
pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn" pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn"
pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console" pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console"
pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site" pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site"
pBaotaWAFSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-site"
pBunnyCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/bunny-cdn" pBunnyCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/bunny-cdn"
pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn" pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly" pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly"
@ -475,6 +476,29 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
} }
} }
case domain.DeploymentProviderTypeBaotaWAFSite:
{
access := domain.AccessConfigForBaotaWAF{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.DeploymentProviderTypeBaotaWAFSite:
deployer, err := pBaotaWAFSite.NewDeployer(&pBaotaWAFSite.DeployerConfig{
ApiUrl: access.ApiUrl,
ApiKey: access.ApiKey,
AllowInsecureConnections: access.AllowInsecureConnections,
SiteName: maputil.GetString(options.ProviderServiceConfig, "siteName"),
SitePort: maputil.GetOrDefaultInt32(options.ProviderServiceConfig, "sitePort", 443),
})
return deployer, err
default:
break
}
}
case domain.DeploymentProviderTypeBunnyCDN: case domain.DeploymentProviderTypeBunnyCDN:
{ {
access := domain.AccessConfigForBunny{} access := domain.AccessConfigForBunny{}

View File

@ -67,6 +67,12 @@ type AccessConfigForBaotaPanel struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
} }
type AccessConfigForBaotaWAF struct {
ApiUrl string `json:"apiUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBytePlus struct { type AccessConfigForBytePlus struct {
AccessKey string `json:"accessKey"` AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"` SecretKey string `json:"secretKey"`

View File

@ -19,6 +19,7 @@ const (
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
AccessProviderTypeBaishan = AccessProviderType("baishan") AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel") AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf")
AccessProviderTypeBytePlus = AccessProviderType("byteplus") AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeBunny = AccessProviderType("bunny") AccessProviderTypeBunny = AccessProviderType("bunny")
AccessProviderTypeBuypass = AccessProviderType("buypass") AccessProviderTypeBuypass = AccessProviderType("buypass")
@ -190,6 +191,7 @@ const (
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn") DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console") DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site") DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
DeploymentProviderTypeBaotaWAFSite = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-site")
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn") DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn") DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly) DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)

View File

@ -0,0 +1,151 @@
package baotapanelwaf
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 堡塔云 WAF 地址。
ApiUrl string `json:"apiUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站名称。
SiteName string `json:"siteName"`
// 网站 SSL 端口。
// 零值时默认为 443。
SitePort int32 `json:"sitePort,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
if d.config.SitePort == 0 {
d.config.SitePort = 443
}
// 遍历获取网站列表,获取网站 ID
// REF: https://support.huaweicloud.com/api-waf/ListHost.html
siteId := ""
getSitListPage := int32(1)
getSitListPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getSiteListReq := &btsdk.GetSiteListRequest{
SiteName: typeutil.ToPtr(d.config.SiteName),
Page: typeutil.ToPtr(getSitListPage),
PageSize: typeutil.ToPtr(getSitListPageSize),
}
getSiteListResp, err := d.sdkClient.GetSiteList(getSiteListReq)
d.logger.Debug("sdk request 'bt.GetSiteList'", slog.Any("request", getSiteListReq), slog.Any("response", getSiteListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.GetSiteList': %w", err)
}
if getSiteListResp.Result != nil && getSiteListResp.Result.List != nil {
for _, siteItem := range getSiteListResp.Result.List {
if siteItem.SiteName == d.config.SiteName {
siteId = siteItem.SiteId
break
}
}
}
if getSiteListResp.Result == nil || len(getSiteListResp.Result.List) < int(getSitListPageSize) {
break
} else {
getSitListPage++
}
}
if siteId == "" {
return nil, errors.New("site not found")
}
// 修改站点配置
modifySiteReq := &btsdk.ModifySiteRequest{
SiteId: siteId,
Type: typeutil.ToPtr("openCert"),
Server: &btsdk.SiteServerInfo{
ListenSSLPort: typeutil.ToPtr(d.config.SitePort),
SSL: &btsdk.SiteServerSSLInfo{
IsSSL: typeutil.ToPtr(int32(1)),
FullChain: typeutil.ToPtr(certPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
},
},
}
modifySiteResp, err := d.sdkClient.ModifySite(modifySiteReq)
d.logger.Debug("sdk request 'bt.ModifySite'", slog.Any("request", modifySiteReq), slog.Any("response", modifySiteResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ModifySite': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid baota api url")
}
if apiKey == "" {
return nil, errors.New("invalid baota api key")
}
client := btsdk.NewClient(apiUrl, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@ -0,0 +1,81 @@
package baotapanelwaf_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAOTAWAFSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
flag.Int64Var(&fSitePort, argsPrefix+"SITEPORT", 0, "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_site_test.go -args \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name"\
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITEPORT=443
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("SITENAME: %v", fSiteName),
fmt.Sprintf("SITEPORT: %v", fSitePort),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
SiteName: fSiteName,
SitePort: int32(fSitePort),
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,19 @@
package btwaf
func (c *Client) GetSiteList(req *GetSiteListRequest) (*GetSiteListResponse, error) {
resp := &GetSiteListResponse{}
err := c.sendRequestWithResult("/wafmastersite/get_site_list", req, resp)
return resp, err
}
func (c *Client) ModifySite(req *ModifySiteRequest) (*ModifySiteResponse, error) {
resp := &ModifySiteResponse{}
err := c.sendRequestWithResult("/wafmastersite/modify_site", req, resp)
return resp, err
}
func (c *Client) ConfigSetSSL(req *ConfigSetSSLRequest) (*ConfigSetSSLResponse, error) {
resp := &ConfigSetSSLResponse{}
err := c.sendRequestWithResult("/config/set_cert", req, resp)
return resp, err
}

View File

@ -0,0 +1,77 @@
package btwaf
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
client *resty.Client
}
func NewClient(apiHost, apiKey string) *Client {
client := resty.New().
SetBaseURL(strings.TrimRight(apiHost, "/") + "/api").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
keyMd5 := md5.Sum([]byte(apiKey))
keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:]))
signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex))
signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:]))
req.Header.Set("waf_request_time", timestamp)
req.Header.Set("waf_request_token", signMd5Hex)
return nil
})
return &Client{
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) WithTLSConfig(config *tls.Config) *Client {
c.client.SetTLSClientConfig(config)
return c
}
func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) {
req := c.client.R().
SetHeader("Content-Type", "application/json").
SetBody(params)
resp, err := req.Post(path)
if err != nil {
return resp, fmt.Errorf("baota api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(path string, params interface{}, result BaseResponse) error {
resp, err := c.sendRequest(path, params)
if err != nil {
return err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("baota api error: failed to unmarshal response: %w", err)
} else if errcode := result.GetCode(); errcode != 0 {
return fmt.Errorf("baota api error: code='%d'", errcode)
}
return nil
}

View File

@ -0,0 +1,67 @@
package btwaf
type BaseResponse interface {
GetCode() int32
}
type baseResponse struct {
Code *int32 `json:"code,omitempty"`
}
func (r *baseResponse) GetCode() int32 {
if r.Code != nil {
return *r.Code
}
return 0
}
type GetSiteListRequest struct {
Page *int32 `json:"p,omitempty"`
PageSize *int32 `json:"p_size,omitempty"`
SiteName *string `json:"site_name,omitempty"`
}
type GetSiteListResponse struct {
baseResponse
Result *struct {
List []*struct {
SiteId string `json:"site_id"`
SiteName string `json:"site_name"`
Type string `json:"types"`
Status int32 `json:"status"`
CreateTime int64 `json:"create_time"`
UpdateTime int64 `json:"update_time"`
} `json:"list"`
Total int32 `json:"total"`
} `json:"res,omitempty"`
}
type SiteServerInfo struct {
ListenSSLPort *int32 `json:"listen_ssl_port,omitempty"`
SSL *SiteServerSSLInfo `json:"ssl,omitempty"`
}
type SiteServerSSLInfo struct {
IsSSL *int32 `json:"is_ssl,omitempty"`
FullChain *string `json:"full_chain,omitempty"`
PrivateKey *string `json:"private_key,omitempty"`
}
type ModifySiteRequest struct {
SiteId string `json:"site_id"`
Type *string `json:"types,omitempty"`
Server *SiteServerInfo `json:"server,omitempty"`
}
type ModifySiteResponse struct {
baseResponse
}
type ConfigSetSSLRequest struct {
CertContent string `json:"certContent"`
KeyContent string `json:"keyContent"`
}
type ConfigSetSSLResponse struct {
baseResponse
}

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@ import AccessFormAzureConfig from "./AccessFormAzureConfig";
import AccessFormBaiduCloudConfig from "./AccessFormBaiduCloudConfig"; import AccessFormBaiduCloudConfig from "./AccessFormBaiduCloudConfig";
import AccessFormBaishanConfig from "./AccessFormBaishanConfig"; import AccessFormBaishanConfig from "./AccessFormBaishanConfig";
import AccessFormBaotaPanelConfig from "./AccessFormBaotaPanelConfig"; import AccessFormBaotaPanelConfig from "./AccessFormBaotaPanelConfig";
import AccessFormBaotaWAFConfig from "./AccessFormBaotaWAFConfig";
import AccessFormBunnyConfig from "./AccessFormBunnyConfig"; import AccessFormBunnyConfig from "./AccessFormBunnyConfig";
import AccessFormBytePlusConfig from "./AccessFormBytePlusConfig"; import AccessFormBytePlusConfig from "./AccessFormBytePlusConfig";
import AccessFormCacheFlyConfig from "./AccessFormCacheFlyConfig"; import AccessFormCacheFlyConfig from "./AccessFormCacheFlyConfig";
@ -196,6 +197,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormBaishanConfig {...nestedFormProps} />; return <AccessFormBaishanConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.BAOTAPANEL: case ACCESS_PROVIDERS.BAOTAPANEL:
return <AccessFormBaotaPanelConfig {...nestedFormProps} />; return <AccessFormBaotaPanelConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.BAOTAWAF:
return <AccessFormBaotaWAFConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.BUNNY: case ACCESS_PROVIDERS.BUNNY:
return <AccessFormBunnyConfig {...nestedFormProps} />; return <AccessFormBunnyConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.BYTEPLUS: case ACCESS_PROVIDERS.BYTEPLUS:

View File

@ -27,11 +27,7 @@ const AccessFormBaotaPanelConfig = ({ form: formInst, formName, disabled, initia
const formSchema = z.object({ const formSchema = z.object({
apiUrl: z.string().url(t("common.errmsg.url_invalid")), apiUrl: z.string().url(t("common.errmsg.url_invalid")),
apiKey: z apiKey: z.string().nonempty(t("access.form.baotapanel_api_key.placeholder")).trim(),
.string()
.min(1, t("access.form.baotapanel_api_key.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
allowInsecureConnections: z.boolean().nullish(), allowInsecureConnections: z.boolean().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);

View File

@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForBaotaWAF } from "@/domain/access";
type AccessFormBaotaWAFConfigFieldValues = Nullish<AccessConfigForBaotaWAF>;
export type AccessFormBaotaWAFConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormBaotaWAFConfigFieldValues;
onValuesChange?: (values: AccessFormBaotaWAFConfigFieldValues) => void;
};
const initFormModel = (): AccessFormBaotaWAFConfigFieldValues => {
return {
apiUrl: "http://<your-host-addr>:8379/",
apiKey: "",
};
};
const AccessFormBaotaWAFConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormBaotaWAFConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
apiUrl: z.string().url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.baotawaf_api_key.placeholder")).trim(),
allowInsecureConnections: z.boolean().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="apiUrl" label={t("access.form.baotawaf_api_url.label")} rules={[formRule]}>
<Input placeholder={t("access.form.baotawaf_api_url.placeholder")} />
</Form.Item>
<Form.Item
name="apiKey"
label={t("access.form.baotawaf_api_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.baotawaf_api_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.baotawaf_api_key.placeholder")} />
</Form.Item>
<Form.Item name="allowInsecureConnections" label={t("access.form.baotawaf_allow_insecure_conns.label")} rules={[formRule]}>
<Switch
checkedChildren={t("access.form.baotawaf_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.baotawaf_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</Form>
);
};
export default AccessFormBaotaWAFConfig;

View File

@ -42,6 +42,7 @@ import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaidu
import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig"; import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig";
import DeployNodeConfigFormBaotaPanelConsoleConfig from "./DeployNodeConfigFormBaotaPanelConsoleConfig"; import DeployNodeConfigFormBaotaPanelConsoleConfig from "./DeployNodeConfigFormBaotaPanelConsoleConfig";
import DeployNodeConfigFormBaotaPanelSiteConfig from "./DeployNodeConfigFormBaotaPanelSiteConfig"; import DeployNodeConfigFormBaotaPanelSiteConfig from "./DeployNodeConfigFormBaotaPanelSiteConfig";
import DeployNodeConfigFormBaotaWAFSiteConfig from "./DeployNodeConfigFormBaotaWAFSiteConfig";
import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNConfig.tsx"; import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNConfig.tsx";
import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig"; import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig";
import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig"; import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig";
@ -237,6 +238,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormBaotaPanelConsoleConfig {...nestedFormProps} />; return <DeployNodeConfigFormBaotaPanelConsoleConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.BAOTAPANEL_SITE: case DEPLOYMENT_PROVIDERS.BAOTAPANEL_SITE:
return <DeployNodeConfigFormBaotaPanelSiteConfig {...nestedFormProps} />; return <DeployNodeConfigFormBaotaPanelSiteConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE:
return <DeployNodeConfigFormBaotaWAFSiteConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.BUNNY_CDN: case DEPLOYMENT_PROVIDERS.BUNNY_CDN:
return <DeployNodeConfigFormBunnyCDNConfig {...nestedFormProps} />; return <DeployNodeConfigFormBunnyCDNConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN: case DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN:

View File

@ -0,0 +1,78 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, InputNumber } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validPortNumber } from "@/utils/validators";
type DeployNodeConfigFormBaotaWAFSiteConfigFieldValues = Nullish<{
siteName: string;
sitePort: number;
}>;
export type DeployNodeConfigFormBaotaWAFSiteConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormBaotaWAFSiteConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormBaotaWAFSiteConfigFieldValues) => void;
};
const initFormModel = (): DeployNodeConfigFormBaotaWAFSiteConfigFieldValues => {
return {
siteName: "",
sitePort: 443,
};
};
const DeployNodeConfigFormBaotaWAFSiteConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormBaotaWAFSiteConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
siteName: z.string().nonempty(t("workflow_node.deploy.form.baotawaf_site_name.placeholder")).trim(),
sitePort: z.preprocess(
(v) => Number(v),
z
.number()
.int(t("workflow_node.deploy.form.baotawaf_site_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="siteName"
label={t("workflow_node.deploy.form.baotawaf_site_name.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baotawaf_site_name.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baotawaf_site_name.placeholder")} />
</Form.Item>
<Form.Item name="sitePort" label={t("workflow_node.deploy.form.baotawaf_site_port.label")} rules={[formRule]}>
<InputNumber className="w-full" placeholder={t("access.form.ssh_port.placeholder")} min={1} max={65535} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormBaotaWAFSiteConfig;

View File

@ -15,6 +15,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForBaiduCloud | AccessConfigForBaiduCloud
| AccessConfigForBaishan | AccessConfigForBaishan
| AccessConfigForBaotaPanel | AccessConfigForBaotaPanel
| AccessConfigForBaotaWAF
| AccessConfigForBunny | AccessConfigForBunny
| AccessConfigForBytePlus | AccessConfigForBytePlus
| AccessConfigForCacheFly | AccessConfigForCacheFly
@ -123,6 +124,12 @@ export type AccessConfigForBaotaPanel = {
allowInsecureConnections?: boolean; allowInsecureConnections?: boolean;
}; };
export type AccessConfigForBaotaWAF = {
apiUrl: string;
apiKey: string;
allowInsecureConnections?: boolean;
};
export type AccessConfigForBunny = { export type AccessConfigForBunny = {
apiKey: string; apiKey: string;
}; };

View File

@ -13,6 +13,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
BAIDUCLOUD: "baiducloud", BAIDUCLOUD: "baiducloud",
BAISHAN: "baishan", BAISHAN: "baishan",
BAOTAPANEL: "baotapanel", BAOTAPANEL: "baotapanel",
BAOTAWAF: "baotawaf",
BUNNY: "bunny", BUNNY: "bunny",
BYTEPLUS: "byteplus", BYTEPLUS: "byteplus",
BUYPASS: "buypass", BUYPASS: "buypass",
@ -124,6 +125,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.SAFELINE, "provider.safeline", "/imgs/providers/safeline.svg", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS.SAFELINE, "provider.safeline", "/imgs/providers/safeline.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS["1PANEL"], "provider.1panel", "/imgs/providers/1panel.svg", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS["1PANEL"], "provider.1panel", "/imgs/providers/1panel.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BAOTAPANEL, "provider.baotapanel", "/imgs/providers/baotapanel.svg", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS.BAOTAPANEL, "provider.baotapanel", "/imgs/providers/baotapanel.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BAOTAWAF, "provider.baotawaf", "/imgs/providers/baotawaf.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.RATPANEL, "provider.ratpanel", "/imgs/providers/ratpanel.png", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS.RATPANEL, "provider.ratpanel", "/imgs/providers/ratpanel.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.CDNFLY, "provider.cdnfly", "/imgs/providers/cdnfly.png", [ACCESS_USAGES.HOSTING]], [ACCESS_PROVIDERS.CDNFLY, "provider.cdnfly", "/imgs/providers/cdnfly.png", [ACCESS_USAGES.HOSTING]],
@ -371,6 +373,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`, BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`,
BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`, BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`,
BAOTAPANEL_SITE: `${ACCESS_PROVIDERS.BAOTAPANEL}-site`, BAOTAPANEL_SITE: `${ACCESS_PROVIDERS.BAOTAPANEL}-site`,
BAOTAWAF_SITE: `${ACCESS_PROVIDERS.BAOTAWAF}-site`,
BUNNY_CDN: `${ACCESS_PROVIDERS.BUNNY}-cdn`, BUNNY_CDN: `${ACCESS_PROVIDERS.BUNNY}-cdn`,
BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`, BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`,
CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`, CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`,
@ -547,6 +550,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE, "provider.baotapanel.console", DEPLOYMENT_CATEGORIES.OTHER], [DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE, "provider.baotapanel.console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.RATPANEL_SITE, "provider.ratpanel.site", DEPLOYMENT_CATEGORIES.WEBSITE], [DEPLOYMENT_PROVIDERS.RATPANEL_SITE, "provider.ratpanel.site", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.RATPANEL_CONSOLE, "provider.ratpanel.console", DEPLOYMENT_CATEGORIES.OTHER], [DEPLOYMENT_PROVIDERS.RATPANEL_CONSOLE, "provider.ratpanel.console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE, "provider.baotawaf.site", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL], [DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS], [DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS],
].map(([type, name, category, builtin]) => [ ].map(([type, name, category, builtin]) => [

View File

@ -112,6 +112,14 @@
"access.form.baotapanel_allow_insecure_conns.label": "Insecure SSL/TLS connections", "access.form.baotapanel_allow_insecure_conns.label": "Insecure SSL/TLS connections",
"access.form.baotapanel_allow_insecure_conns.switch.on": "Allow", "access.form.baotapanel_allow_insecure_conns.switch.on": "Allow",
"access.form.baotapanel_allow_insecure_conns.switch.off": "Disallow", "access.form.baotapanel_allow_insecure_conns.switch.off": "Disallow",
"access.form.baotawaf_api_url.label": "aaWAF URL",
"access.form.baotawaf_api_url.placeholder": "Please enter aaWAF URL",
"access.form.baotawaf_api_key.label": "aaWAF API key",
"access.form.baotawaf_api_key.placeholder": "Please enter aaWAF API key",
"access.form.baotawaf_api_key.tooltip": "For more information, see <a href=\"https://github.com/aaPanel/aaWAF/blob/main/API.md\" target=\"_blank\">https://github.com/aaPanel/aaWAF/blob/main/API.md</a>",
"access.form.baotawaf_allow_insecure_conns.label": "Insecure SSL/TLS connections",
"access.form.baotawaf_allow_insecure_conns.switch.on": "Allow",
"access.form.baotawaf_allow_insecure_conns.switch.off": "Disallow",
"access.form.byteplus_access_key.label": "BytePlus AccessKey", "access.form.byteplus_access_key.label": "BytePlus AccessKey",
"access.form.byteplus_access_key.placeholder": "Please enter BytePlus AccessKey", "access.form.byteplus_access_key.placeholder": "Please enter BytePlus AccessKey",
"access.form.byteplus_access_key.tooltip": "For more information, see <a href=\"https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys\" target=\"_blank\">https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys</a>", "access.form.byteplus_access_key.tooltip": "For more information, see <a href=\"https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys\" target=\"_blank\">https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys</a>",

View File

@ -42,6 +42,8 @@
"provider.baotapanel": "aaPanel (aka BaoTaPanel)", "provider.baotapanel": "aaPanel (aka BaoTaPanel)",
"provider.baotapanel.console": "aaPanel (aka BaoTaPanel) - Console", "provider.baotapanel.console": "aaPanel (aka BaoTaPanel) - Console",
"provider.baotapanel.site": "aaPanel (aka BaoTaPanel) - Website", "provider.baotapanel.site": "aaPanel (aka BaoTaPanel) - Website",
"provider.baotawaf": "aaWAF (aka BaotaWAF)",
"provider.baotawaf.site": "aaWAF (aka BaotaWAF) - Website",
"provider.bunny": "Bunny", "provider.bunny": "Bunny",
"provider.bunny.cdn": "Bunny - CDN (Content Delivery Network)", "provider.bunny.cdn": "Bunny - CDN (Content Delivery Network)",
"provider.byteplus": "BytePlus", "provider.byteplus": "BytePlus",

View File

@ -342,9 +342,14 @@
"workflow_node.deploy.form.baotapanel_site_names.label": "aaPanel site names", "workflow_node.deploy.form.baotapanel_site_names.label": "aaPanel site names",
"workflow_node.deploy.form.baotapanel_site_names.placeholder": "Please enter aaPanel site names (separated by semicolons)", "workflow_node.deploy.form.baotapanel_site_names.placeholder": "Please enter aaPanel site names (separated by semicolons)",
"workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "Please enter a valid aaPanel site name", "workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "Please enter a valid aaPanel site name",
"workflow_node.deploy.form.baotapanel_site_names.tooltip": "Usually equal to the websites domain name.", "workflow_node.deploy.form.baotapanel_site_names.tooltip": "You can find it on aaPanel WebUI.",
"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title": "Change aaPanel site names", "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title": "Change aaPanel site names",
"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "Please enter aaPanel site name", "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "Please enter aaPanel site name",
"workflow_node.deploy.form.baotawaf_site_name.label": "aaWAF site name",
"workflow_node.deploy.form.baotawaf_site_name.placeholder": "Please enter aaWAF site name",
"workflow_node.deploy.form.baotawaf_site_name.tooltip": "You can find it on aaWAF WebUI.",
"workflow_node.deploy.form.baotawaf_site_port.label": "aaWAF site SSL port",
"workflow_node.deploy.form.baotawaf_site_port.placeholder": "Please enter aaWAF SSL port",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label": "Bunny CDN pull zone ID", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.label": "Bunny CDN pull zone ID",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder": "Please enter Bunny CDN pull zone ID", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder": "Please enter Bunny CDN pull zone ID",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip": "What is this? See <a href=\"https://dash.bunny.net/cdn\" target=\"_blank\">https://dash.bunny.net/cdn</a>", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip": "What is this? See <a href=\"https://dash.bunny.net/cdn\" target=\"_blank\">https://dash.bunny.net/cdn</a>",

View File

@ -103,6 +103,14 @@
"access.form.baotapanel_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", "access.form.baotapanel_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
"access.form.baotapanel_allow_insecure_conns.switch.on": "允许", "access.form.baotapanel_allow_insecure_conns.switch.on": "允许",
"access.form.baotapanel_allow_insecure_conns.switch.off": "不允许", "access.form.baotapanel_allow_insecure_conns.switch.off": "不允许",
"access.form.baotawaf_api_url.label": "堡塔云 WAF URL",
"access.form.baotawaf_api_url.placeholder": "请输入堡塔云 WAF URL",
"access.form.baotawaf_api_key.label": "堡塔云 WAF 接口密钥",
"access.form.baotawaf_api_key.placeholder": "请输入 堡塔云 WAF 接口密钥",
"access.form.baotawaf_api_key.tooltip": "这是什么?请参阅 <a href=\"https://github.com/aaPanel/aaWAF/blob/main/API.md\" target=\"_blank\">https://github.com/aaPanel/aaWAF/blob/main/API.md</a>",
"access.form.baotawaf_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
"access.form.baotawaf_allow_insecure_conns.switch.on": "允许",
"access.form.baotawaf_allow_insecure_conns.switch.off": "不允许",
"access.form.bunny_api_key.label": "Bunny API Key", "access.form.bunny_api_key.label": "Bunny API Key",
"access.form.bunny_api_key.placeholder": "请输入 Bunny API Key", "access.form.bunny_api_key.placeholder": "请输入 Bunny API Key",
"access.form.bunny_api_key.tooltip": "这是什么?请参阅 <a href=\"https://docs.bunny.net/reference/bunnynet-api-overview\" target=\"_blank\">https://docs.bunny.net/reference/bunnynet-api-overview</a>", "access.form.bunny_api_key.tooltip": "这是什么?请参阅 <a href=\"https://docs.bunny.net/reference/bunnynet-api-overview\" target=\"_blank\">https://docs.bunny.net/reference/bunnynet-api-overview</a>",

View File

@ -42,6 +42,8 @@
"provider.baotapanel": "宝塔面板", "provider.baotapanel": "宝塔面板",
"provider.baotapanel.console": "宝塔面板 - 面板", "provider.baotapanel.console": "宝塔面板 - 面板",
"provider.baotapanel.site": "宝塔面板 - 网站", "provider.baotapanel.site": "宝塔面板 - 网站",
"provider.baotawaf": "堡塔云 WAF",
"provider.baotawaf.site": "堡塔云 WAF - 网站",
"provider.bunny": "Bunny", "provider.bunny": "Bunny",
"provider.bunny.cdn": "Bunny - 内容分发网络 CDN", "provider.bunny.cdn": "Bunny - 内容分发网络 CDN",
"provider.byteplus": "BytePlus", "provider.byteplus": "BytePlus",

View File

@ -337,13 +337,18 @@
"workflow_node.deploy.form.baotapanel_site_type.option.other.label": "其他", "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.label": "宝塔面板网站名称",
"workflow_node.deploy.form.baotapanel_site_name.placeholder": "请输入宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_name.placeholder": "请输入宝塔面板网站名称",
"workflow_node.deploy.form.baotapanel_site_name.tooltip": "请登录宝塔面板查看", "workflow_node.deploy.form.baotapanel_site_name.tooltip": "请登录宝塔面板查看",
"workflow_node.deploy.form.baotapanel_site_names.label": "宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_names.label": "宝塔面板网站名称",
"workflow_node.deploy.form.baotapanel_site_names.placeholder": "请输入宝塔面板网站名称(多个值请用半角分号隔开)", "workflow_node.deploy.form.baotapanel_site_names.placeholder": "请输入宝塔面板网站名称(多个值请用半角分号隔开)",
"workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "请输入正确的宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "请输入正确的宝塔面板网站名称",
"workflow_node.deploy.form.baotapanel_site_names.tooltip": "通常为网站域名。", "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.title": "修改宝塔面板网站名称",
"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "请输入宝塔面板网站名称", "workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "请输入宝塔面板网站名称",
"workflow_node.deploy.form.baotawaf_site_name.label": "堡塔云 WAF 网站名称",
"workflow_node.deploy.form.baotawaf_site_name.placeholder": "请输入堡塔云 WAF 网站名称",
"workflow_node.deploy.form.baotawaf_site_name.tooltip": "请登录堡塔云 WAF 面板查看。",
"workflow_node.deploy.form.baotawaf_site_port.label": "堡塔云 WAF 网站 SSL 端口",
"workflow_node.deploy.form.baotawaf_site_port.placeholder": "请输入堡塔云 WAF 网站 SSL 端口",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label": "Bunny CDN 拉取区域 ID", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.label": "Bunny CDN 拉取区域 ID",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder": "请输入 Bunny CDN 拉取区域 ID", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder": "请输入 Bunny CDN 拉取区域 ID",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip": "这是什么?请参阅 <a href=\"https://dash.bunny.net/cdn\" target=\"_blank\">https://dash.bunny.net/cdn</a>", "workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip": "这是什么?请参阅 <a href=\"https://dash.bunny.net/cdn\" target=\"_blank\">https://dash.bunny.net/cdn</a>",