完成统一方案 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>
178 lines
5.6 KiB
Go
178 lines
5.6 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
)
|
|
|
|
// Local-edit revision protocol (unified spec §12.7). Clients upload edits to a
|
|
// project artifact; Manager stores them as a new accepted revision and is the
|
|
// owner of the baseline. Runtime reads new revisions via Manager, never the
|
|
// client's local files directly.
|
|
|
|
type localEditChange struct {
|
|
Op string `json:"op"`
|
|
Path string `json:"path"`
|
|
BaseContentHash string `json:"base_content_hash"`
|
|
ContentHash string `json:"content_hash"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type localEditRequest struct {
|
|
EditID string `json:"edit_id"`
|
|
BaseArtifactID string `json:"base_artifact_id"`
|
|
BaseProjectRevision int `json:"base_project_revision"`
|
|
BaseRevision int `json:"base_revision"`
|
|
BaseContentHash string `json:"base_content_hash"`
|
|
ContentHash string `json:"content_hash"`
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
ChangeSummary string `json:"change_summary"`
|
|
Changes []localEditChange `json:"changes"`
|
|
}
|
|
|
|
func hashString(s string) string {
|
|
sum := sha256.Sum256([]byte(s))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// HeicodeArtifactLocalEdit stores a client local edit (single file or batch) as
|
|
// a new artifact revision, rejecting edits based on a stale baseline.
|
|
func HeicodeArtifactLocalEdit(c *gin.Context) {
|
|
record, ok := requireAuthenticatedUserAgentDeployment(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
artifactID := strings.TrimSpace(c.Param("artifact_id"))
|
|
if artifactID == "" {
|
|
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
|
|
return
|
|
}
|
|
var req localEditRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
agentError(c, "POLICY_REJECTED", err.Error())
|
|
return
|
|
}
|
|
|
|
latest, has := model.LatestAgentArtifactRevision(artifactID)
|
|
currentRev := 1
|
|
if has {
|
|
currentRev = latest.Revision
|
|
}
|
|
// Conflict: client edited on top of a baseline that is no longer current.
|
|
if req.BaseRevision > 0 && req.BaseRevision != currentRev {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"error": gin.H{
|
|
"code": "ARTIFACT_REVISION_CONFLICT",
|
|
"message": "本地产物基于旧版本修改,请先同步最新版本后再提交。",
|
|
"retryable": false,
|
|
},
|
|
"data": gin.H{
|
|
"current_project_revision": currentRev,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
contentHash := strings.TrimSpace(req.ContentHash)
|
|
if contentHash == "" {
|
|
if len(req.Changes) > 0 {
|
|
joined := ""
|
|
for _, ch := range req.Changes {
|
|
joined += ch.Path + "\n" + ch.Content + "\n"
|
|
}
|
|
contentHash = hashString(joined)
|
|
} else {
|
|
contentHash = hashString(req.Content)
|
|
}
|
|
}
|
|
|
|
payload, _ := common.Marshal(req)
|
|
newRev := currentRev + 1
|
|
rev := &model.AgentArtifactRevision{
|
|
ArtifactID: artifactID,
|
|
DeploymentID: record.DeploymentID,
|
|
UserID: record.Plan.UserContext.UserID,
|
|
Revision: newRev,
|
|
ProjectRevision: newRev,
|
|
Source: "client_local_edit",
|
|
BaseContentHash: strings.TrimSpace(req.BaseContentHash),
|
|
ContentHash: contentHash,
|
|
ChangeSummary: strings.TrimSpace(req.ChangeSummary),
|
|
CreatedBy: "user",
|
|
Status: "received",
|
|
PayloadJSON: string(payload),
|
|
CreatedAtMs: time.Now().UnixMilli(),
|
|
}
|
|
if err := model.InsertAgentArtifactRevision(rev); err != nil {
|
|
common.SysLog("HeicodeArtifactLocalEdit: " + err.Error())
|
|
agentError(c, "REVISION_PERSIST_FAILED", "failed to store artifact revision")
|
|
return
|
|
}
|
|
notifyRuntimeArtifactEdit(record, rev)
|
|
|
|
common.ApiSuccess(c, gin.H{
|
|
"artifact_id": artifactID,
|
|
"revision": newRev,
|
|
"project_revision": newRev,
|
|
"source": "client_local_edit",
|
|
"status": "received",
|
|
"content_hash": contentHash,
|
|
"created_by": "user",
|
|
})
|
|
}
|
|
|
|
// HeicodeArtifactRevisions lists the revision history for a project artifact.
|
|
func HeicodeArtifactRevisions(c *gin.Context) {
|
|
if _, ok := requireAuthenticatedUserAgentDeployment(c); !ok {
|
|
return
|
|
}
|
|
artifactID := strings.TrimSpace(c.Param("artifact_id"))
|
|
if artifactID == "" {
|
|
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
|
|
return
|
|
}
|
|
rows, err := model.ListAgentArtifactRevisions(artifactID)
|
|
if err != nil {
|
|
common.SysLog("HeicodeArtifactRevisions: " + err.Error())
|
|
agentError(c, "REVISION_QUERY_FAILED", "failed to query revisions")
|
|
return
|
|
}
|
|
current := 1
|
|
if latest, ok := model.LatestAgentArtifactRevision(artifactID); ok {
|
|
current = latest.Revision
|
|
}
|
|
common.ApiSuccess(c, gin.H{
|
|
"artifact_id": artifactID,
|
|
"current_project_revision": current,
|
|
"items": rows,
|
|
"total": len(rows),
|
|
})
|
|
}
|
|
|
|
// notifyRuntimeArtifactEdit best-effort notifies the runtime that a new local
|
|
// edit revision is available (unified spec §12.7.4). The runtime reads the
|
|
// revision via Manager; here we only emit the audit/notification side.
|
|
func notifyRuntimeArtifactEdit(record agentDeploymentRecord, rev *model.AgentArtifactRevision) {
|
|
recordAgentAuditEvent(agentEvent{
|
|
EventID: "evt_" + common.GetUUID()[:12],
|
|
Event: "artifact.local_edit_received",
|
|
SchemaVersion: 1,
|
|
UserID: record.Plan.UserContext.UserID,
|
|
ChannelID: record.Plan.UserContext.ChannelID,
|
|
BindingScope: firstPlanBindingScope(record.Plan),
|
|
DeploymentID: record.DeploymentID,
|
|
CorrelationID: record.Plan.Metadata.CorrelationID,
|
|
OccurredAt: agentNow(),
|
|
}, "manager_artifact_edit", rev.ArtifactID, "", "ok")
|
|
}
|