136 lines
4.7 KiB
Go
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
|
|
}
|