From 88c41f0b2b8cbe3ba9d6858e98b04a453a06d1f3 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Mon, 27 Jul 2026 17:09:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91=E4=B8=BB=E6=9C=BA=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cloud/provider.go | 12 +- backend/cloud/tencent.go | 193 ++++++++++++++++++ .../components/server/CloudServerImport.tsx | 2 - .../cloud-server-import/StepIndicator.tsx | 2 +- 4 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 backend/cloud/tencent.go diff --git a/backend/cloud/provider.go b/backend/cloud/provider.go index a0e96ba..a30f175 100644 --- a/backend/cloud/provider.go +++ b/backend/cloud/provider.go @@ -19,10 +19,10 @@ type CloudInstance struct { // CloudRegion 云平台区域(跨平台统一结构) type CloudRegion struct { - RegionID string `json:"region_id"` - LocalName string `json:"local_name"` - Endpoint string `json:"endpoint"` - Status string `json:"status"` + RegionID string `json:"region_id"` + LocalName string `json:"local_name"` + Endpoint string `json:"endpoint"` + Status string `json:"status"` } // Provider 云平台 Provider 接口 @@ -42,8 +42,8 @@ func NewProvider(provider, accessKey, secretKey string) Provider { switch provider { case "aliyun": return NewAliyunProvider(accessKey, secretKey) - // case "tencent": - // return NewTencentProvider(accessKey, secretKey) + case "tencent": + return NewTencentProvider(accessKey, secretKey) // 启用腾讯云 CVM 区域与实例同步。 // Logan.Li // case "huawei": // return NewHuaweiProvider(accessKey, secretKey) // case "aws": diff --git a/backend/cloud/tencent.go b/backend/cloud/tencent.go new file mode 100644 index 0000000..1b90463 --- /dev/null +++ b/backend/cloud/tencent.go @@ -0,0 +1,193 @@ +package cloud + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" +) + +// TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li +type TencentProvider struct{ accessKey, secretKey string } + +// tencentInstancesResponse 描述腾讯云 CVM 实例分页响应。 // Logan.Li +type tencentInstancesResponse struct { + Response struct { + TotalCount int `json:"TotalCount"` + InstanceSet []struct { + ID string `json:"InstanceId"` + Name string `json:"InstanceName"` + Type string `json:"InstanceType"` + State string `json:"InstanceState"` + Placement struct { + Zone string `json:"Zone"` + } `json:"Placement"` + Created string `json:"CreatedTime"` + OS string `json:"OsName"` + Private []string `json:"PrivateIpAddresses"` + Public []string `json:"PublicIpAddresses"` + CPU int `json:"CPU"` + Memory int `json:"Memory"` + } `json:"InstanceSet"` + } `json:"Response"` +} + +// NewTencentProvider 创建腾讯云 Provider。 // Logan.Li +func NewTencentProvider(accessKey, secretKey string) *TencentProvider { + return &TencentProvider{accessKey, secretKey} +} + +// Name 返回腾讯云厂商标识。 // Logan.Li +func (p *TencentProvider) Name() string { return "tencent" } + +// call 调用腾讯云 CVM API。 // Logan.Li +func (p *TencentProvider) call(action string, payload interface{}) ([]byte, error) { + return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, "ap-guangzhou", payload) +} + +// callInRegion 调用指定区域的腾讯云 CVM API,签名区域必须与请求区域一致。 // Logan.Li +func (p *TencentProvider) callInRegion(region, action string, payload interface{}) ([]byte, error) { + return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, region, payload) +} + +// callService 使用 TC3-HMAC-SHA256 调用腾讯云服务 API。 // Logan.Li +func (p *TencentProvider) callService(service, host, version, action, region string, payload interface{}) ([]byte, error) { + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("编码腾讯云请求失败: %w", err) + } + now := time.Now() + timestamp, date := now.Unix(), now.UTC().Format("2006-01-02") + canonicalHeaders := "content-type:application/json; charset=utf-8\nhost:" + host + "\n" + signedHeaders := "content-type;host" + canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + tencentSHA256Hex(string(body)) + credentialScope := date + "/" + service + "/tc3_request" + stringToSign := "TC3-HMAC-SHA256\n" + fmt.Sprint(timestamp) + "\n" + credentialScope + "\n" + tencentSHA256Hex(canonicalRequest) + secretDate := tencentHMACSHA256([]byte("TC3"+p.secretKey), date) + secretService := tencentHMACSHA256(secretDate, service) + signature := hex.EncodeToString(tencentHMACSHA256(tencentHMACSHA256(secretService, "tc3_request"), stringToSign)) + req, err := http.NewRequest(http.MethodPost, "https://"+host, strings.NewReader(string(body))) + if err != nil { + return nil, fmt.Errorf("创建腾讯云请求失败: %w", err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Host", host) + req.Header.Set("X-TC-Action", action) + req.Header.Set("X-TC-Version", version) + req.Header.Set("X-TC-Timestamp", fmt.Sprint(timestamp)) + if region != "" { + req.Header.Set("X-TC-Region", region) + } + req.Header.Set("Authorization", "TC3-HMAC-SHA256 Credential="+p.accessKey+"/"+credentialScope+", SignedHeaders="+signedHeaders+", Signature="+signature) + resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) + if err != nil { + return nil, fmt.Errorf("请求腾讯云 API 失败: %w", err) + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取腾讯云响应失败: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("腾讯云 API 返回 HTTP %d: %s", resp.StatusCode, string(data)) + } + var envelope struct { + Response struct { + Error *struct { + Code string `json:"Code"` + Message string `json:"Message"` + } `json:"Error"` + } `json:"Response"` + } + if json.Unmarshal(data, &envelope) == nil && envelope.Response.Error != nil { + return nil, fmt.Errorf("腾讯云 API 错误 %s: %s", envelope.Response.Error.Code, envelope.Response.Error.Message) + } + return data, nil +} + +// ListRegions 获取腾讯云 CVM 区域。 // Logan.Li +func (p *TencentProvider) ListRegions() ([]CloudRegion, error) { + body, err := p.call("DescribeRegions", map[string]string{}) + if err != nil { + return nil, err + } + var result struct { + Response struct { + RegionSet []struct { + Region string `json:"Region"` + Name string `json:"RegionName"` + } `json:"RegionSet"` + } `json:"Response"` + } + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("解析腾讯云区域响应失败: %w", err) + } + regions := make([]CloudRegion, 0, len(result.Response.RegionSet)) + for _, r := range result.Response.RegionSet { + regions = append(regions, CloudRegion{RegionID: r.Region, LocalName: r.Name, Endpoint: "https://cvm.tencentcloudapi.com", Status: "active"}) + } + sort.Slice(regions, func(i, j int) bool { return regions[i].RegionID < regions[j].RegionID }) + return regions, nil +} + +// ListInstances 获取腾讯云指定区域的 CVM 实例。 // Logan.Li +func (p *TencentProvider) ListInstances(regionID string) ([]CloudInstance, error) { + instances := []CloudInstance{} + const pageSize = 100 + for offset := 0; ; offset += pageSize { + // 腾讯云 CVM 区域由 X-TC-Region 公共请求头传递,请求体不重复传 Region。 // Logan.Li + body, err := p.callInRegion(regionID, "DescribeInstances", map[string]interface{}{ + "Limit": pageSize, + "Offset": offset, + }) + if err != nil { + return nil, err + } + var result tencentInstancesResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("解析腾讯云实例响应失败: %w", err) + } + for _, item := range result.Response.InstanceSet { + instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: item.Name, Hostname: item.Name, OSType: item.OS, Status: tencentState(item.State), RegionID: regionID, ZoneID: item.Placement.Zone, PrivateIP: tencentFirstString(item.Private), PublicIP: tencentFirstString(item.Public), InstanceType: item.Type, CPU: item.CPU, Memory: item.Memory * 1024, CreationTime: item.Created}) + } + if len(result.Response.InstanceSet) == 0 || len(instances) >= result.Response.TotalCount { + break + } + } + return instances, nil +} + +// tencentFirstString 获取 API 列表中的首个 IP。 // Logan.Li +func tencentFirstString(values []string) string { + if len(values) == 0 { + return "" + } + return values[0] +} + +// tencentState 统一腾讯云实例状态,便于复用导入逻辑。 // Logan.Li +func tencentState(state string) string { + if strings.EqualFold(state, "RUNNING") { + return "Running" + } + return state +} + +// tencentSHA256Hex 计算腾讯云 TC3 签名的 SHA-256 摘要。 // Logan.Li +func tencentSHA256Hex(value string) string { + sum := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sum[:]) +} + +// tencentHMACSHA256 计算腾讯云 TC3 签名的 HMAC-SHA256。 // Logan.Li +func tencentHMACSHA256(key []byte, value string) []byte { + mac := hmac.New(sha256.New, key) + _, _ = mac.Write([]byte(value)) + return mac.Sum(nil) +} diff --git a/frontend/src/components/server/CloudServerImport.tsx b/frontend/src/components/server/CloudServerImport.tsx index 53dbae5..4bf763f 100644 --- a/frontend/src/components/server/CloudServerImport.tsx +++ b/frontend/src/components/server/CloudServerImport.tsx @@ -13,7 +13,6 @@ import { import { Cloud, Key, - CheckCircle, ChevronRight, ChevronLeft, Check, RefreshCw, @@ -265,7 +264,6 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ { id: 1, title: t('server.cloudProvider'), icon: Cloud }, { id: 2, title: t('server.cloudAccount'), icon: Key }, { id: 3, title: t('server.selectRegion'), icon: Cloud }, - { id: 4, title: t('server.importConfirm'), icon: CheckCircle }, ]; // 导入凭证设置面板:Step 3/4 且未出结果时显示(Linux 默认 SSH,Windows 默认 RDP) // Logan.Li diff --git a/frontend/src/components/server/cloud-server-import/StepIndicator.tsx b/frontend/src/components/server/cloud-server-import/StepIndicator.tsx index 2f7eca9..7b21739 100644 --- a/frontend/src/components/server/cloud-server-import/StepIndicator.tsx +++ b/frontend/src/components/server/cloud-server-import/StepIndicator.tsx @@ -7,7 +7,7 @@ interface StepIndicatorProps { steps: StepDef[]; } -/** 顶部 4 步骤进度指示器(云厂商 / 账号 / 区域 / 导入确认) */ +/** 顶部 3 步骤进度指示器(云厂商 / 账号 / 区域)。 // Logan.Li */ export function StepIndicator({ step, steps }: StepIndicatorProps) { return (