Files
heicode-mananger/heicode/controller/heicode_cloud_deploy.go
T
chenchenandClaude Opus 4.8 56eef58b3c feat(agent): project-folder artifacts, local-edit revisions, cloud-deploy control plane
完成统一方案 v0.1 剩余客户端要求(#10/#11/#12)+ 对接文档。

- #10 项目文件夹产物(§12):Manager 解析 runtime 的 markdown 多文件 artifact 成项目文件树,
  新增 .../artifacts/{id}/manifest、/files/{path}、/archive 三接口(按需解析,zip 打包)。
- #11 本地修改 revision 协议(§12.7):新模型 AgentArtifactRevision + 迁移;
  .../local-edits、/local-edits/batch、/revisions;base_revision 冲突检测返回
  ARTIFACT_REVISION_CONFLICT;Manager 持有 accepted 基线,回调 Runtime(审计事件)。
- #12 云部署控制面(§18):新模型 AgentCloudDeployment + 迁移;
  GET /api/heicode/deployment-targets;.../tasks/{id}/deployments(创建/列表);
  生产环境进 waiting_approval;客户端只传 resource_binding_id(禁 inline secret);
  真实云执行留 executor=pending_worker,等 Deploy Worker 接入。
- 文档:新增 docs/integration/heicode-desktop-unified-api.md(取代旧 sub-agile 文档,
  覆盖 capabilities/统一任务路由/display_status/项目文件夹/本地修改/云部署/客户端约束/错误码)。

验证:go build ./... + go test(controller/router/model/middleware)全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:23:02 +08:00

140 lines
5.2 KiB
Go

package controller
import (
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// Cloud deployment control plane (unified spec §18). Manager validates,
// gates approval, resolves credentials and audits; a Deploy Worker (not yet
// connected) executes provider provisioning. Until a worker is wired, records
// rest in deployment_requested / waiting_approval with executor=pending_worker.
var cloudDeployTargets = map[string]bool{"azure": true, "aliyun": false, "aws": false}
var cloudDeployEnvironments = map[string]bool{"preview": true, "production": true}
// HeicodeDeploymentTargets lists deployable cloud targets (unified spec §18.1).
func HeicodeDeploymentTargets(c *gin.Context) {
common.ApiSuccess(c, gin.H{
"targets": []gin.H{
{"id": "azure", "name": "Azure", "enabled": true, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true, "regions": []string{"eastasia", "southeastasia", "westus"}},
{"id": "aliyun", "name": "阿里云", "enabled": false, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true},
{"id": "aws", "name": "AWS", "enabled": false, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true},
},
})
}
type cloudDeployRequest struct {
ArtifactID string `json:"artifact_id"`
ArtifactRevision int `json:"artifact_revision"`
Target string `json:"target"`
Environment string `json:"environment"`
Region string `json:"region"`
DeploymentMode string `json:"deployment_mode"`
ResourceBindingID int `json:"resource_binding_id"`
Options map[string]any `json:"options"`
}
// HeicodeCreateDeployment validates and records a cloud deployment request
// (unified spec §18.3/§18.4). Production deployments enter waiting_approval.
func HeicodeCreateDeployment(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
var req cloudDeployRequest
if err := c.ShouldBindJSON(&req); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
req.Target = strings.ToLower(strings.TrimSpace(req.Target))
req.Environment = strings.ToLower(strings.TrimSpace(req.Environment))
if strings.TrimSpace(req.ArtifactID) == "" {
agentError(c, "POLICY_REJECTED", "artifact_id is required")
return
}
if !cloudDeployTargets[req.Target] {
agentError(c, "DEPLOY_TARGET_DISABLED", "deployment target is not available")
return
}
if !cloudDeployEnvironments[req.Environment] {
agentError(c, "POLICY_REJECTED", "environment must be preview or production")
return
}
// Client must reference a credential binding, never inline secrets (§17.6).
if req.ResourceBindingID > 0 && model.DB != nil {
uid, _ := strconv.Atoi(strings.TrimSpace(record.Plan.UserContext.UserID))
var binding model.ResourceBinding
if err := model.DB.Where("id = ? AND user_id = ?", req.ResourceBindingID, uid).First(&binding).Error; err != nil {
agentError(c, "RESOURCE_BINDING_INVALID", "resource_binding_id not found or not owned by user")
return
}
}
status := "deployment_requested"
if req.Environment == "production" {
status = "waiting_approval"
}
now := time.Now().UnixMilli()
runID := "deploy_" + common.GetUUID()[:12]
payload, _ := common.Marshal(req)
row := &model.AgentCloudDeployment{
DeployRunID: runID,
DeploymentID: record.DeploymentID,
UserID: record.Plan.UserContext.UserID,
ArtifactID: strings.TrimSpace(req.ArtifactID),
ArtifactRevision: req.ArtifactRevision,
Target: req.Target,
Environment: req.Environment,
Region: strings.TrimSpace(req.Region),
ResourceBindingID: req.ResourceBindingID,
Status: status,
PayloadJSON: string(payload),
CreatedAtMs: now,
UpdatedAtMs: now,
}
if err := model.InsertAgentCloudDeployment(row); err != nil {
common.SysLog("HeicodeCreateDeployment: " + err.Error())
agentError(c, "DEPLOY_PERSIST_FAILED", "failed to record deployment request")
return
}
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "deployment.requested",
SchemaVersion: 1,
UserID: record.Plan.UserContext.UserID,
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agentNow(),
}, "manager_cloud_deploy", runID, "", "ok")
common.ApiSuccess(c, gin.H{
"deployment_run_id": runID,
"deployment_id": record.DeploymentID,
"target": req.Target,
"environment": req.Environment,
"status": status,
"executor": "pending_worker",
})
}
// HeicodeListDeployments lists cloud deployments for a task.
func HeicodeListDeployments(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
rows, err := model.ListAgentCloudDeploymentsByDeployment(record.DeploymentID)
if err != nil {
agentError(c, "DEPLOY_QUERY_FAILED", "failed to query deployments")
return
}
common.ApiSuccess(c, gin.H{"items": rows, "total": len(rows)})
}