完成统一方案 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>
140 lines
5.2 KiB
Go
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)})
|
|
}
|