diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go
index 292c34f7..c2136d20 100644
--- a/internal/deployer/providers.go
+++ b/internal/deployer/providers.go
@@ -74,6 +74,7 @@ import (
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos"
+ pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maputil"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
@@ -1003,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
+ case domain.DeployProviderTypeWangsuCDNPro:
+ {
+ access := domain.AccessConfigForWangsu{}
+ 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.DeployProviderTypeWangsuCDNPro:
+ deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ AccessKeySecret: access.AccessKeySecret,
+ Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"),
+ Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
+ CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
+ WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"),
+ })
+ return deployer, err
+
+ default:
+ break
+ }
+ }
+
case domain.DeployProviderTypeWebhook:
{
access := domain.AccessConfigForWebhook{}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 0d3528ab..9e419eaa 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -228,6 +228,11 @@ type AccessConfigForVolcEngine struct {
SecretAccessKey string `json:"secretAccessKey"`
}
+type AccessConfigForWangsu struct {
+ AccessKeyId string `json:"accessKeyId"`
+ AccessKeySecret string `json:"accessKeySecret"`
+}
+
type AccessConfigForWebhook struct {
Url string `json:"url"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 18ee73b9..668612f7 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -61,6 +61,7 @@ const (
AccessProviderTypeUpyun = AccessProviderType("upyun")
AccessProviderTypeVercel = AccessProviderType("vercel")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
+ AccessProviderTypeWangsu = AccessProviderType("wangsu")
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
@@ -212,5 +213,6 @@ const (
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
+ DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro")
DeployProviderTypeWebhook = DeployProviderType("webhook")
)
diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go
new file mode 100644
index 00000000..c5ac15b9
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go
@@ -0,0 +1,276 @@
+package wangsucdnpro
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "log/slog"
+ "regexp"
+ "time"
+
+ "github.com/alibabacloud-go/tea/tea"
+ xerrors "github.com/pkg/errors"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/utils/certutil"
+ wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn"
+)
+
+type DeployerConfig struct {
+ // 网宿云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 网宿云 AccessKeySecret。
+ AccessKeySecret string `json:"accessKeySecret"`
+ // 网宿云环境。
+ Environment string `json:"environment"`
+ // 加速域名(支持泛域名)。
+ Domain string `json:"domain"`
+ // 证书 ID。
+ // 选填。
+ CertificateId string `json:"certificateId,omitempty"`
+ // Webhook ID。
+ // 选填。
+ WebhookId string `json:"webhookId,omitempty"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *wangsucdn.Client
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to create sdk client")
+ }
+
+ 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.Domain == "" {
+ return nil, errors.New("config `domain` is required")
+ }
+
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPem)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询已部署加速域名的详情
+ getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain)
+ d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'")
+ }
+
+ // 生成网宿云证书参数
+ encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix())
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to encrypt private key")
+ }
+ certificateNewVersionInfo := &wangsucdn.CertificateVersion{
+ PrivateKey: tea.String(encryptedPrivateKey),
+ Certificate: tea.String(certPem),
+ IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{
+ CommonName: tea.String(certX509.Subject.CommonName),
+ SubjectAlternativeNames: &certX509.DNSNames,
+ },
+ }
+
+ // 网宿云证书 URL 中包含证书 ID 及版本号
+ // 格式:
+ // http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33
+ // http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5
+ var wangsuCertUrl string
+ var wangsuCertId, wangsuCertVer string
+
+ // 如果原证书 ID 为空,则创建证书;否则更新证书。
+ timestamp := time.Now().Unix()
+ if d.config.CertificateId == "" {
+ // 创建证书
+ createCertificateReq := &wangsucdn.CreateCertificateRequest{
+ Timestamp: timestamp,
+ Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
+ AutoRenew: tea.String("Off"),
+ NewVersion: certificateNewVersionInfo,
+ }
+ createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
+ d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'")
+ }
+
+ wangsuCertUrl = createCertificateResp.CertificateUrl
+ d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
+
+ wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
+ if len(wangsuCertIdMatches) > 1 {
+ wangsuCertId = wangsuCertIdMatches[1]
+ }
+
+ wangsuCertVer = "1"
+ } else {
+ // 更新证书
+ updateCertificateReq := &wangsucdn.UpdateCertificateRequest{
+ Timestamp: timestamp,
+ Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
+ AutoRenew: tea.String("Off"),
+ NewVersion: certificateNewVersionInfo,
+ }
+ updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
+ d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'")
+ }
+
+ wangsuCertUrl = updateCertificateResp.CertificateUrl
+ d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
+
+ wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
+ if len(wangsuCertIdMatches) > 1 {
+ wangsuCertId = wangsuCertIdMatches[1]
+ }
+
+ wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl)
+ if len(wangsuCertVerMatches) > 1 {
+ wangsuCertVer = wangsuCertVerMatches[1]
+ }
+ }
+
+ // 创建部署任务
+ // REF: https://www.wangsu.com/document/api-doc/27034
+ createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{
+ Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
+ Target: tea.String(d.config.Environment),
+ Actions: &[]wangsucdn.DeploymentTaskAction{
+ {
+ Action: tea.String("deploy_cert"),
+ CertificateId: tea.String(wangsuCertId),
+ Version: tea.String(wangsuCertVer),
+ },
+ },
+ }
+ if d.config.WebhookId != "" {
+ createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId)
+ }
+ createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq)
+ d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'")
+ }
+
+ // 循环获取部署任务详细信息,等待任务状态变更
+ // REF: https://www.wangsu.com/document/api-doc/27038
+ var wangsuTaskId string
+ wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
+ if len(wangsuTaskMatches) > 1 {
+ wangsuTaskId = wangsuTaskMatches[1]
+ }
+ for {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+
+ getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)
+ d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
+ if err != nil {
+ return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'")
+ }
+
+ if getDeploymentTaskDetailResp.Status == "failed" {
+ return nil, errors.New("unexpected deployment task status")
+ } else if getDeploymentTaskDetailResp.Status == "succeeded" {
+ break
+ }
+
+ d.logger.Info("waiting for deployment task completion ...")
+ time.Sleep(time.Second * 15)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) {
+ if accessKeyId == "" {
+ return nil, errors.New("invalid wangsu access key id")
+ }
+
+ if accessKeySecret == "" {
+ return nil, errors.New("invalid wangsu access key secret")
+ }
+
+ return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil
+}
+
+func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) {
+ date := time.Unix(timestamp, 0).UTC()
+ dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT")
+
+ mac := hmac.New(sha256.New, []byte(secretKey))
+ mac.Write([]byte(dateStr))
+ aesivkey := mac.Sum(nil)
+ aesivkeyHex := hex.EncodeToString(aesivkey)
+
+ if len(aesivkeyHex) != 64 {
+ return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex))
+ }
+ ivHex := aesivkeyHex[:32]
+ keyHex := aesivkeyHex[32:64]
+
+ iv, err := hex.DecodeString(ivHex)
+ if err != nil {
+ return "", fmt.Errorf("failed to decode iv: %w", err)
+ }
+
+ key, err := hex.DecodeString(keyHex)
+ if err != nil {
+ return "", fmt.Errorf("failed to decode key: %w", err)
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return "", err
+ }
+
+ plainBytes := []byte(privkeyPem)
+ padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize
+ if padlen > 0 {
+ paddata := bytes.Repeat([]byte{byte(padlen)}, padlen)
+ plainBytes = append(plainBytes, paddata...)
+ }
+
+ encBytes := make([]byte, len(plainBytes))
+ mode := cipher.NewCBCEncrypter(block, iv)
+ mode.CryptBlocks(encBytes, plainBytes)
+
+ return base64.StdEncoding.EncodeToString(encBytes), nil
+}
diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go
new file mode 100644
index 00000000..25dd7b1e
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go
@@ -0,0 +1,90 @@
+package wangsucdnpro_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fAccessKeySecret string
+ fEnvironment string
+ fDomain string
+ fCertificateId string
+ fWebhookId string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
+ flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+ flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
+ flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./wangsu_cdnpro_test.go -args \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\
+ --CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-id"
+*/
+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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
+ fmt.Sprintf("ENVIRONMENT: %v", fEnvironment),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
+ fmt.Sprintf("WEBHOOKID: %v", fWebhookId),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ AccessKeySecret: fAccessKeySecret,
+ Environment: fEnvironment,
+ Domain: fDomain,
+ CertificateId: fCertificateId,
+ WebhookId: fWebhookId,
+ })
+ 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)
+ })
+}
diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/api.go b/internal/pkg/vendors/wangsu-sdk/cdn/api.go
new file mode 100644
index 00000000..fd96ba2f
--- /dev/null
+++ b/internal/pkg/vendors/wangsu-sdk/cdn/api.go
@@ -0,0 +1,58 @@
+package cdn
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/go-resty/resty/v2"
+)
+
+func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
+ resp := &CreateCertificateResponse{}
+ r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) {
+ r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
+ })
+ if err != nil {
+ return resp, err
+ }
+
+ resp.CertificateUrl = r.Header().Get("Location")
+ return resp, err
+}
+
+func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
+ resp := &UpdateCertificateResponse{}
+ r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) {
+ r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
+ })
+ if err != nil {
+ return resp, err
+ }
+
+ resp.CertificateUrl = r.Header().Get("Location")
+ return resp, err
+}
+
+func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) {
+ resp := &GetHostnameDetailResponse{}
+ _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp)
+ return resp, err
+}
+
+func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {
+ resp := &CreateDeploymentTaskResponse{}
+ r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp)
+ if err != nil {
+ return resp, err
+ }
+
+ resp.DeploymentTaskUrl = r.Header().Get("Location")
+ return resp, err
+}
+
+func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {
+ resp := &GetDeploymentTaskDetailResponse{}
+ _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", url.PathEscape(hostname)), nil, resp)
+ return resp, err
+}
diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/client.go b/internal/pkg/vendors/wangsu-sdk/cdn/client.go
new file mode 100644
index 00000000..e1831960
--- /dev/null
+++ b/internal/pkg/vendors/wangsu-sdk/cdn/client.go
@@ -0,0 +1,20 @@
+package cdn
+
+import (
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
+)
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKey, secretKey string) *Client {
+ return &Client{client: openapi.NewClient(accessKey, secretKey)}
+}
+
+func (c *Client) WithTimeout(timeout time.Duration) *Client {
+ c.client.WithTimeout(timeout)
+ return c
+}
diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/models.go b/internal/pkg/vendors/wangsu-sdk/cdn/models.go
new file mode 100644
index 00000000..0126418a
--- /dev/null
+++ b/internal/pkg/vendors/wangsu-sdk/cdn/models.go
@@ -0,0 +1,107 @@
+package cdn
+
+import (
+ "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
+)
+
+type baseResponse struct {
+ RequestId *string `json:"-"`
+ Code *string `json:"code,omitempty"`
+ Message *string `json:"message,omitempty"`
+}
+
+var _ openapi.Result = (*baseResponse)(nil)
+
+func (r *baseResponse) SetRequestId(requestId string) {
+ r.RequestId = &requestId
+}
+
+type CertificateVersion struct {
+ Comments *string `json:"comments,omitempty"`
+ PrivateKey *string `json:"privateKey,omitempty"`
+ Certificate *string `json:"certificate,omitempty"`
+ ChainCert *string `json:"chainCert,omitempty"`
+ IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"`
+}
+
+type CertificateVersionIdentificationInfo struct {
+ Country *string `json:"country,omitempty"`
+ State *string `json:"state,omitempty"`
+ City *string `json:"city,omitempty"`
+ Company *string `json:"company,omitempty"`
+ Department *string `json:"department,omitempty"`
+ CommonName *string `json:"commonName,omitempty" required:"true"`
+ Email *string `json:"email,omitempty"`
+ SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"`
+}
+
+type CreateCertificateRequest struct {
+ Timestamp int64 `json:"-"`
+ Name *string `json:"name,omitempty" required:"true"`
+ Description *string `json:"description,omitempty"`
+ AutoRenew *string `json:"autoRenew,omitempty"`
+ ForceRenew *bool `json:"forceRenew,omitempty"`
+ NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
+}
+
+type CreateCertificateResponse struct {
+ baseResponse
+ CertificateUrl string `json:"-"`
+}
+
+type UpdateCertificateRequest struct {
+ Timestamp int64 `json:"-"`
+ Name *string `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ AutoRenew *string `json:"autoRenew,omitempty"`
+ ForceRenew *bool `json:"forceRenew,omitempty"`
+ NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
+}
+
+type UpdateCertificateResponse struct {
+ baseResponse
+ CertificateUrl string `json:"-"`
+}
+
+type HostnameProperty struct {
+ PropertyId string `json:"propertyId"`
+ Version int32 `json:"version"`
+ CertificateId *string `json:"certificateId,omitempty"`
+}
+
+type GetHostnameDetailResponse struct {
+ baseResponse
+ Hostname string `json:"hostname"`
+ PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"`
+ PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"`
+}
+
+type DeploymentTaskAction struct {
+ Action *string `json:"action,omitempty" required:"true"`
+ PropertyId *string `json:"propertyId,omitempty"`
+ CertificateId *string `json:"certificateId,omitempty"`
+ Version *string `json:"version,omitempty"`
+}
+
+type CreateDeploymentTaskRequest struct {
+ Name *string `json:"name,omitempty"`
+ Target *string `json:"target,omitempty" required:"true"`
+ Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"`
+ Webhook *string `json:"webhook,omitempty"`
+}
+
+type CreateDeploymentTaskResponse struct {
+ baseResponse
+ DeploymentTaskUrl string `json:"-"`
+}
+
+type GetDeploymentTaskDetailResponse struct {
+ baseResponse
+ Target string `json:"target"`
+ Actions []DeploymentTaskAction `json:"actions"`
+ Status string `json:"status"`
+ StatusDetails string `json:"statusDetails"`
+ SubmissionTime string `json:"submissionTime"`
+ FinishTime string `json:"finishTime"`
+ ApiRequestId string `json:"apiRequestId"`
+}
diff --git a/internal/pkg/vendors/wangsu-sdk/openapi/client.go b/internal/pkg/vendors/wangsu-sdk/openapi/client.go
new file mode 100644
index 00000000..6492aba8
--- /dev/null
+++ b/internal/pkg/vendors/wangsu-sdk/openapi/client.go
@@ -0,0 +1,187 @@
+package openapi
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+)
+
+type Client struct {
+ accessKey string
+ secretKey string
+
+ client *resty.Client
+}
+
+type Result interface {
+ SetRequestId(requestId string)
+}
+
+func NewClient(accessKey, secretKey string) *Client {
+ client := resty.New().
+ SetBaseURL("https://open.chinanetcenter.com").
+ SetHeader("Host", "open.chinanetcenter.com").
+ SetHeader("Accept", "application/json").
+ SetHeader("Content-Type", "application/json").
+ SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
+ // Step 1: Get request method
+ method := req.Method
+ method = strings.ToUpper(method)
+
+ // Step 2: Get request path
+ path := "/"
+ if req.URL != nil {
+ path = req.URL.Path
+ }
+
+ // Step 3: Get unencoded query string
+ queryString := ""
+ if method != http.MethodPost && req.URL != nil {
+ queryString = req.URL.RawQuery
+
+ s, err := url.QueryUnescape(queryString)
+ if err != nil {
+ return err
+ }
+
+ queryString = s
+ }
+
+ // Step 4: Get canonical headers & signed headers
+ canonicalHeaders := "" +
+ "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" +
+ "host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n"
+ signedHeaders := "content-type;host"
+
+ // Step 5: Get request payload
+ payload := ""
+ if method != http.MethodGet && req.Body != nil {
+ reader, err := req.GetBody()
+ if err != nil {
+ return err
+ }
+
+ defer reader.Close()
+
+ payloadb, err := io.ReadAll(reader)
+ if err != nil {
+ return err
+ }
+
+ payload = string(payloadb)
+ }
+ hashedPayload := sha256.Sum256([]byte(payload))
+ hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:]))
+
+ // Step 6: Get timestamp
+ var reqtime time.Time
+ timestampString := req.Header.Get("x-cnc-timestamp")
+ if timestampString == "" {
+ reqtime = time.Now().UTC()
+ timestampString = fmt.Sprintf("%d", reqtime.Unix())
+ } else {
+ timestamp, err := strconv.ParseInt(timestampString, 10, 64)
+ if err != nil {
+ return err
+ }
+ reqtime = time.Unix(timestamp, 0).UTC()
+ }
+
+ // Step 7: Get canonical request string
+ canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex)
+ hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
+ hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:]))
+
+ // Step 8: String to sign
+ const SignAlgorithmHeader = "CNC-HMAC-SHA256"
+ stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex)
+ hmac := hmac.New(sha256.New, []byte(secretKey))
+ hmac.Write([]byte(stringToSign))
+ sign := hmac.Sum(nil)
+ signHex := strings.ToLower(hex.EncodeToString(sign))
+
+ // Step 9: Add headers to request
+ req.Header.Set("x-cnc-accessKey", accessKey)
+ req.Header.Set("x-cnc-timestamp", timestampString)
+ req.Header.Set("x-cnc-auth-method", "AKSK")
+ req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex))
+ req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
+
+ return nil
+ })
+
+ return &Client{
+ accessKey: accessKey,
+ secretKey: secretKey,
+ client: client,
+ }
+}
+
+func (c *Client) WithTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) sendRequest(method string, path string, params interface{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
+ req := c.client.R()
+ req.Method = method
+ req.URL = path
+ if strings.EqualFold(method, http.MethodGet) {
+ qs := make(map[string]string)
+ if params != nil {
+ temp := make(map[string]any)
+ jsonb, _ := json.Marshal(params)
+ json.Unmarshal(jsonb, &temp)
+ for k, v := range temp {
+ if v != nil {
+ qs[k] = fmt.Sprintf("%v", v)
+ }
+ }
+ }
+
+ req = req.SetQueryParams(qs)
+ } else {
+ req = req.SetBody(params)
+ }
+
+ for _, fn := range configureReq {
+ fn(req)
+ }
+
+ resp, err := req.Send()
+ if err != nil {
+ return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err)
+ } else if resp.IsError() {
+ return resp, fmt.Errorf("wangsu api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
+ }
+
+ return resp, nil
+}
+
+func (c *Client) SendRequestWithResult(method string, path string, params interface{}, result Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
+ resp, err := c.sendRequest(method, path, params, configureReq...)
+ if err != nil {
+ if resp != nil {
+ json.Unmarshal(resp.Body(), &result)
+ result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
+ }
+ return resp, err
+ }
+
+ if err := json.Unmarshal(resp.Body(), &result); err != nil {
+ return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err)
+ }
+
+ result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
+ return resp, nil
+}
diff --git a/migrations/1744192800_upgrade.go b/migrations/1744192800_upgrade.go
new file mode 100644
index 00000000..83e83ee6
--- /dev/null
+++ b/migrations/1744192800_upgrade.go
@@ -0,0 +1,91 @@
+package migrations
+
+import (
+ "github.com/pocketbase/pocketbase/core"
+ m "github.com/pocketbase/pocketbase/migrations"
+)
+
+func init() {
+ m.Register(func(app core.App) error {
+ collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
+ if err != nil {
+ return err
+ }
+
+ // update field
+ if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
+ "hidden": false,
+ "id": "hwy7m03o",
+ "maxSelect": 1,
+ "name": "provider",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "1panel",
+ "acmehttpreq",
+ "akamai",
+ "aliyun",
+ "aws",
+ "azure",
+ "baiducloud",
+ "baishan",
+ "baotapanel",
+ "byteplus",
+ "buypass",
+ "cachefly",
+ "cdnfly",
+ "cloudflare",
+ "cloudns",
+ "cmcccloud",
+ "ctcccloud",
+ "cucccloud",
+ "desec",
+ "dnsla",
+ "dogecloud",
+ "dynv6",
+ "edgio",
+ "fastly",
+ "gname",
+ "gcore",
+ "godaddy",
+ "goedge",
+ "googletrustservices",
+ "huaweicloud",
+ "jdcloud",
+ "k8s",
+ "letsencrypt",
+ "letsencryptstaging",
+ "local",
+ "namecheap",
+ "namedotcom",
+ "namesilo",
+ "ns1",
+ "porkbun",
+ "powerdns",
+ "qiniu",
+ "qingcloud",
+ "rainyun",
+ "safeline",
+ "ssh",
+ "sslcom",
+ "tencentcloud",
+ "ucloud",
+ "upyun",
+ "vercel",
+ "volcengine",
+ "wangsu",
+ "webhook",
+ "westcn",
+ "zerossl"
+ ]
+ }`)); err != nil {
+ return err
+ }
+
+ return app.Save(collection)
+ }, func(app core.App) error {
+ return nil
+ })
+}
diff --git a/ui/public/imgs/providers/wangsu.svg b/ui/public/imgs/providers/wangsu.svg
new file mode 100644
index 00000000..276ec1cc
--- /dev/null
+++ b/ui/public/imgs/providers/wangsu.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index d5906434..bffb1f49 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -51,6 +51,7 @@ import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
import AccessFormVercelConfig from "./AccessFormVercelConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
+import AccessFormWangsuConfig from "./AccessFormWangsuConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
@@ -229,6 +230,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.VOLCENGINE:
return ;
+ case ACCESS_PROVIDERS.WANGSU:
+ return ;
case ACCESS_PROVIDERS.WEBHOOK:
return ;
case ACCESS_PROVIDERS.WESTCN:
diff --git a/ui/src/components/access/AccessFormWangsuConfig.tsx b/ui/src/components/access/AccessFormWangsuConfig.tsx
new file mode 100644
index 00000000..f9676829
--- /dev/null
+++ b/ui/src/components/access/AccessFormWangsuConfig.tsx
@@ -0,0 +1,76 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type AccessConfigForWangsu } from "@/domain/access";
+
+type AccessFormWangsuConfigFieldValues = Nullish;
+
+export type AccessFormWangsuConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormWangsuConfigFieldValues;
+ onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormWangsuConfigFieldValues => {
+ return {
+ accessKeyId: "",
+ accessKeySecret: "",
+ };
+};
+
+const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ accessKeyId: z
+ .string()
+ .min(1, t("access.form.wangsu_access_key_id.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 }))
+ .trim(),
+ accessKeySecret: z
+ .string()
+ .min(1, t("access.form.wangsu_access_key_secret.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 }))
+ .trim(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormWangsuConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
index cd1a0fe7..5565c985 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
@@ -81,6 +81,7 @@ import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolc
import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx";
import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx";
import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx";
+import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx";
import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx";
type DeployNodeConfigFormFieldValues = Partial;
@@ -302,6 +303,8 @@ const DeployNodeConfigForm = forwardRef;
case DEPLOY_PROVIDERS.VOLCENGINE_TOS:
return ;
+ case DEPLOY_PROVIDERS.WANGSU_CDNPRO:
+ return ;
case DEPLOY_PROVIDERS.WEBHOOK:
return ;
}
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx
new file mode 100644
index 00000000..90bdb064
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx
@@ -0,0 +1,107 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Select } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { validDomainName } from "@/utils/validators";
+
+type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{
+ environment: string;
+ domain: string;
+ certificateId?: string;
+ webhookId?: string;
+}>;
+
+export type DeployNodeConfigFormBaishanCDNConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void;
+};
+
+const ENVIRONMENT_PRODUCTION = "production" as const;
+const ENVIRONMENT_STAGING = "stating" as const;
+
+const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => {
+ return {
+ environment: ENVIRONMENT_PRODUCTION,
+ };
+};
+
+const DeployNodeConfigFormBaishanCDNConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormBaishanCDNConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ resourceType: z.union([z.literal(ENVIRONMENT_PRODUCTION), z.literal(ENVIRONMENT_STAGING)], {
+ message: t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder"),
+ }),
+ domain: z
+ .string({ message: t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder") })
+ .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
+ certificateId: z.string().nullish(),
+ webhookId: z.string().nullish(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+
+
+
+
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormBaishanCDNConfig;
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index 86d644fa..3015e4d0 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -47,6 +47,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForUpyun
| AccessConfigForVercel
| AccessConfigForVolcEngine
+ | AccessConfigForWangsu
| AccessConfigForWebhook
| AccessConfigForWestcn
| AccessConfigForZeroSSL
@@ -268,6 +269,11 @@ export type AccessConfigForVolcEngine = {
secretAccessKey: string;
};
+export type AccessConfigForWangsu = {
+ accessKeyId: string;
+ accessKeySecret: string;
+};
+
export type AccessConfigForWebhook = {
url: string;
allowInsecureConnections?: boolean;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index c101b0e3..9c59a30e 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -50,6 +50,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
UPYUN: "upyun",
VERCEL: "vercel",
VOLCENGINE: "volcengine",
+ WANGSU: "wangsu",
WEBHOOK: "webhook",
WESTCN: "westcn",
ZEROSSL: "zerossl",
@@ -99,6 +100,7 @@ export const accessProvidersMap: Maphttps://www.volcengine.com/docs/6291/216571",
+ "access.form.wangsu_access_key_id.label": "Wangsu Cloud AccessKeyId",
+ "access.form.wangsu_access_key_id.placeholder": "Please enter Wangsu Cloud AccessKeyId",
+ "access.form.wangsu_access_key_id.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775",
+ "access.form.wangsu_access_key_secret.label": "Wangsu Cloud AccessKeySecret",
+ "access.form.wangsu_access_key_secret.placeholder": "Please enter Wangsu Cloud AccessKeySecret",
+ "access.form.wangsu_access_key_secret.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775",
"access.form.webhook_url.label": "Webhook URL",
"access.form.webhook_url.placeholder": "Please enter Webhook URL",
"access.form.webhook_allow_insecure_conns.label": "Insecure SSL/TLS connections",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index 5df6c3b3..76680957 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -125,6 +125,8 @@
"provider.volcengine.imagex": "Volcengine - ImageX",
"provider.volcengine.live": "Volcengine - Live",
"provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)",
+ "provider.wangsu": "Wangsu Cloud",
+ "provider.wangsu.cdnpro": "Wangsu Cloud - CDN Pro",
"provider.webhook": "Webhook",
"provider.westcn": "West.cn",
"provider.zerossl": "ZeroSSL",
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index d6728544..4ac796b3 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -95,7 +95,7 @@
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider_access.label": "Host provider authorization",
"workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider",
- "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.",
+ "workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.",
"workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.provider_access.guide_for_local": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.",
"workflow_node.deploy.form.certificate.label": "Certificate",
@@ -269,11 +269,11 @@
"workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain",
"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name",
"workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see https://console.bce.baidu.com/cdn",
- "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan CDN domain",
- "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan CDN domain name",
+ "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan Cloud CDN domain",
+ "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan Cloud CDN domain name",
"workflow_node.deploy.form.baishan_cdn_domain.tooltip": "For more information, see https://cdnx.console.baishan.com",
- "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan CDN certificate ID (Optional",
- "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan CDN certificate ID",
+ "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan Cloud CDN certificate ID (Optional)",
+ "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan Cloud CDN certificate ID",
"workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip": "For more information, see https://cdnx.console.baishan.com/#/cdn/cert",
"workflow_node.deploy.form.baotapanel_console_auto_restart.label": "Auto restart after deployment",
"workflow_node.deploy.form.baotapanel_site_type.label": "aaPanel site type",
@@ -639,6 +639,19 @@
"workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain",
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name",
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "Wangsu Cloud environment",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "Please select Wangsu Cloud environment",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "Production environment",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "Staging environment",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "Wangsu Cloud CDN domain",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "Please enter Wangsu Cloud CDN domain name",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/properties",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "Wangsu Cloud CDN certificate ID (Optional)",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "Please enter Wangsu Cloud CDN certificate ID",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "Wangsu Cloud CDN Webhook ID (Optional)",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "Please enter Wangsu Cloud CDN Webhook ID",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate",
"workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)",
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data",
"workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL.
Supported variables:
${DOMAIN}: The primary domain of the certificate (CommonName).
${DOMAINS}: The domain list of the certificate (SubjectAltNames).
${CERTIFICATE}: The PEM format content of the certificate file.
${PRIVATE_KEY}: The PEM format content of the private key file.",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index 6022fa15..bf068260 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -298,6 +298,12 @@
"access.form.volcengine_secret_access_key.label": "火山引擎 SecretAccessKey",
"access.form.volcengine_secret_access_key.placeholder": "请输入火山引擎 SecretAccessKey",
"access.form.volcengine_secret_access_key.tooltip": "这是什么?请参阅 https://www.volcengine.com/docs/6291/216571",
+ "access.form.wangsu_access_key_id.label": "网宿云 AccessKeyId",
+ "access.form.wangsu_access_key_id.placeholder": "请输入网宿科技 AccessKeyId",
+ "access.form.wangsu_access_key_id.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775",
+ "access.form.wangsu_access_key_secret.label": "网宿科技 AccessKeySecret",
+ "access.form.wangsu_access_key_secret.placeholder": "请输入网宿科技 AccessKeySecret",
+ "access.form.wangsu_access_key_secret.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775",
"access.form.webhook_url.label": "Webhook 回调地址",
"access.form.webhook_url.placeholder": "请输入 Webhook 回调地址",
"access.form.webhook_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index 34ef3fb2..d6be56a2 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -125,6 +125,8 @@
"provider.volcengine.imagex": "火山引擎 - 图片服务 ImageX",
"provider.volcengine.live": "火山引擎 - 视频直播 Live",
"provider.volcengine.tos": "火山引擎 - 对象存储 TOS",
+ "provider.wangsu": "网宿云",
+ "provider.wangsu.cdnpro": "网宿云 - CDN Pro",
"provider.webhook": "Webhook",
"provider.westcn": "西部数码",
"provider.zerossl": "ZeroSSL",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index 7e50cc5b..5fcb201d 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -94,7 +94,7 @@
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
"workflow_node.deploy.form.provider_access.label": "主机提供商授权",
"workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权",
- "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书,注意与申请阶段所需的 DNS 提供商相区分。",
+ "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API,注意与申请阶段所需的 DNS 提供商相区分。",
"workflow_node.deploy.form.provider_access.button": "新建",
"workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:如果你正在使用 Docker 运行 Certimate,“本地”指的是容器内而非宿主机。",
"workflow_node.deploy.form.certificate.label": "待部署证书",
@@ -638,6 +638,19 @@
"workflow_node.deploy.form.volcengine_tos_domain.label": "火山引擎 TOS 自定义域名",
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名",
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see https://console.volcengine.com/tos",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "网宿云环境",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "请选择网宿云环境",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "生产环境",
+ "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "演练环境",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "网宿云 CDN Pro 加速域名",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "请输入网宿云 CDN Pro 加速域名(支持泛域名)",
+ "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/properties",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "网宿云 CDN Pro 原证书 ID(可选)",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "请输入网宿云 CDN Pro 原证书 ID",
+ "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate
不填写时,将上传新证书;否则,将替换原证书。",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "网宿云 CDN Pro 部署任务 Webhook ID(可选)",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID",
+ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate",
"workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(JSON 格式)",
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据",
"workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。
支持的变量:
${DOMAIN}:证书的主域名(即 CommonName)
${DOMAINS}:证书的多域名列表(即 SubjectAltNames)
${CERTIFICATE}:证书文件 PEM 格式内容
${PRIVATE_KEY}:私钥文件 PEM 格式内容",