Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_undo.go
2026-09-14 01:11:56 +08:00

136 lines
4.7 KiB
Go

package app
import (
"encoding/hex"
"errors"
"strings"
"time"
"gorm.io/gorm"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
// UndoCloudAgentCanvas restores only the latest Agent canvas mutation. It is
// deliberately optimistic: the caller must prove that the canvas still has
// the snapshot produced by that mutation. Submitted generation tasks are never
// undone, cancelled, or refunded by this method.
func (s *Service) UndoCloudAgentCanvas(userID, runID, stepID, expectedSnapshotHash, reason string) (map[string]any, error) {
if strings.TrimSpace(userID) == "" || strings.TrimSpace(runID) == "" {
return nil, Unauthorized("请先登录")
}
expectedSnapshotHash = strings.TrimSpace(expectedSnapshotHash)
if len(expectedSnapshotHash) != 64 {
return nil, BadAuthRequest("需要有效的画布快照哈希")
}
if _, err := hex.DecodeString(expectedSnapshotHash); err != nil {
return nil, BadAuthRequest("需要有效的画布快照哈希")
}
stepID = strings.TrimSpace(stepID)
if len(stepID) > 160 {
return nil, BadAuthRequest("Agent 操作 ID 过长")
}
if len([]rune(reason)) > 2000 {
return nil, BadAuthRequest("撤销理由过长")
}
s.storageMu.Lock()
defer s.storageMu.Unlock()
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NotFound("Agent 运行不存在")
}
return nil, err
}
result := map[string]any{"accepted": false}
err = s.repo.MutateCloudAgent(userID, runID, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
mutation, err := repo.LatestCloudAgentCanvasMutation(userID, runID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return NotFound("当前 Agent 运行没有可撤销的画布变更")
}
return err
}
if stepID != "" && mutation.StepID != stepID {
return creationConflict("待撤销的 Agent 操作已不是当前运行的最新变更")
}
canvas, err := repo.CanvasProjectForUser(userID, mutation.CanvasID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return NotFound("画布不存在或无权访问")
}
return err
}
currentDoc, err := creationDocument(canvas.PayloadJSON)
if err != nil {
return err
}
currentHash := cloudAgentCanvasHash(currentDoc)
if mutation.Status == "undone" {
if currentHash == mutation.BeforeSnapshotHash && expectedSnapshotHash == currentHash {
result = map[string]any{"accepted": true, "snapshotHash": currentHash}
return nil
}
return creationConflict("该 Agent 变更已经撤销,但画布之后又发生了变化")
}
if mutation.Status == "not_undoable" {
return BadAuthRequest("该 Agent 画布变更未保留完整快照,无法撤销")
}
if mutation.Status != "applied" {
return creationConflict("该 Agent 变更当前不可撤销")
}
if mutation.HasSubmittedTask {
return BadAuthRequest("已提交的生成任务不能撤销;任务不会取消或退款")
}
if expectedSnapshotHash != currentHash || mutation.AfterSnapshotHash != currentHash {
return creationConflict("画布已发生后续变化,未执行撤销")
}
if mutation.BeforeJSON == "" {
return BadAuthRequest("该 Agent 画布变更未保留可撤销快照")
}
beforeDoc, err := creationDocument(mutation.BeforeJSON)
if err != nil {
return BadAuthRequest("该 Agent 画布变更的撤销快照无效")
}
if cloudAgentCanvasHash(beforeDoc) != mutation.BeforeSnapshotHash {
return BadAuthRequest("该 Agent 画布变更的撤销快照校验失败")
}
previous := canvas.PayloadJSON
canvas.PayloadJSON = mutation.BeforeJSON
if err := repo.CompareSaveCreationCanvas(canvas, previous); err != nil {
if errors.Is(err, repository.ErrCreationConflict) {
return creationConflict("画布已发生后续变化,未执行撤销")
}
return err
}
if err := repo.MarkCloudAgentCanvasMutationUndone(userID, runID, mutation.ID, time.Now().UTC()); err != nil {
if errors.Is(err, repository.ErrCreationConflict) {
return creationConflict("该 Agent 变更已经被处理,请重新读取画布")
}
return err
}
state, err := cloudAgentDecode(current)
if err != nil {
return err
}
state.event(runID, "canvas_undone", map[string]any{
"canvasId": mutation.CanvasID,
"stepId": mutation.StepID,
"snapshotHash": mutation.BeforeSnapshotHash,
"reason": strings.TrimSpace(reason),
})
if err := cloudAgentSave(current, &state); err != nil {
return err
}
result = map[string]any{"accepted": true, "snapshotHash": mutation.BeforeSnapshotHash}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}