mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-10 05:14:48 +08:00
fixed(go-agent): agent-publish (#16713)
## Summary This PR updates Go agent publish logic to persist the parent canvas update and canvas-version save in the same transaction. ## Changes: - Reuse SaveOrReplaceLatest semantics for published versions - Add SaveOrReplaceLatestTx for transactional publish flow - Keep canvas release update and version persistence atomic - Add a focused publish test covering canvas and released version state ## Tested: ``` bash build.sh --test -run 'Test(PublishAgentUpdatesCanvasAndReleasedVersion|UpdateAgentDSLCreatesAndReplacesDraftVersion| UpdateAgentDSLDoesNotOverwriteLatestReleasedVersion)$' ./internal/service ./internal/dao ``` <img width="1476" height="850" alt="image" src="https://github.com/user-attachments/assets/2c576581-1143-420b-8750-a77aa3c4292d" />
This commit is contained in:
@@ -141,73 +141,85 @@ func (dao *UserCanvasVersionDAO) SaveOrReplaceLatest(opts SaveOrReplaceLatestVer
|
||||
}
|
||||
var saved *entity.UserCanvasVersion
|
||||
if err := DB.Transaction(func(tx *gorm.DB) error {
|
||||
var parent struct {
|
||||
ID string
|
||||
}
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Table((&entity.UserCanvas{}).TableName()).
|
||||
Select("id").
|
||||
Where("id = ?", opts.UserCanvasID).
|
||||
Take(&parent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var latest entity.UserCanvasVersion
|
||||
err := tx.Where("user_canvas_id = ?", opts.UserCanvasID).
|
||||
Order("create_time DESC, id DESC").
|
||||
First(&latest).Error
|
||||
row, err := dao.SaveOrReplaceLatestTx(tx, opts)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
} else if opts.sameDSL(latest.DSL) {
|
||||
if !latest.Release || opts.Release {
|
||||
updates := map[string]interface{}{
|
||||
"dsl": opts.DSL,
|
||||
"release": opts.Release,
|
||||
}
|
||||
if opts.Title != nil {
|
||||
updates["title"] = opts.Title
|
||||
}
|
||||
if opts.Description != nil {
|
||||
updates["description"] = opts.Description
|
||||
}
|
||||
if err := tx.Model(&entity.UserCanvasVersion{}).
|
||||
Where("id = ?", latest.ID).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
latest.DSL = opts.DSL
|
||||
latest.Release = opts.Release
|
||||
if opts.Title != nil {
|
||||
latest.Title = opts.Title
|
||||
}
|
||||
if opts.Description != nil {
|
||||
latest.Description = opts.Description
|
||||
}
|
||||
saved = &latest
|
||||
return dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
|
||||
}
|
||||
}
|
||||
row := &entity.UserCanvasVersion{
|
||||
ID: opts.NewID,
|
||||
UserCanvasID: opts.UserCanvasID,
|
||||
Title: opts.Title,
|
||||
Description: opts.Description,
|
||||
Release: opts.Release,
|
||||
DSL: opts.DSL,
|
||||
}
|
||||
if err := tx.Create(row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
saved = row
|
||||
return dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
// SaveOrReplaceLatestTx inserts or refreshes a version using the caller's
|
||||
// transaction. Its write semantics match SaveOrReplaceLatest exactly.
|
||||
func (dao *UserCanvasVersionDAO) SaveOrReplaceLatestTx(tx *gorm.DB, opts SaveOrReplaceLatestVersionOptions) (*entity.UserCanvasVersion, error) {
|
||||
if opts.KeepUnpublished <= 0 {
|
||||
opts.KeepUnpublished = 20
|
||||
}
|
||||
var parent struct {
|
||||
ID string
|
||||
}
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Table((&entity.UserCanvas{}).TableName()).
|
||||
Select("id").
|
||||
Where("id = ?", opts.UserCanvasID).
|
||||
Take(&parent).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var latest entity.UserCanvasVersion
|
||||
err := tx.Where("user_canvas_id = ?", opts.UserCanvasID).
|
||||
Order("create_time DESC, id DESC").
|
||||
First(&latest).Error
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
} else if opts.sameDSL(latest.DSL) {
|
||||
if !latest.Release || opts.Release {
|
||||
updates := map[string]interface{}{
|
||||
"dsl": opts.DSL,
|
||||
"release": opts.Release,
|
||||
}
|
||||
if opts.Title != nil {
|
||||
updates["title"] = opts.Title
|
||||
}
|
||||
if opts.Description != nil {
|
||||
updates["description"] = opts.Description
|
||||
}
|
||||
if err := tx.Model(&entity.UserCanvasVersion{}).
|
||||
Where("id = ?", latest.ID).
|
||||
Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latest.DSL = opts.DSL
|
||||
latest.Release = opts.Release
|
||||
if opts.Title != nil {
|
||||
latest.Title = opts.Title
|
||||
}
|
||||
if opts.Description != nil {
|
||||
latest.Description = opts.Description
|
||||
}
|
||||
return &latest, dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
|
||||
}
|
||||
}
|
||||
row := &entity.UserCanvasVersion{
|
||||
ID: opts.NewID,
|
||||
UserCanvasID: opts.UserCanvasID,
|
||||
Title: opts.Title,
|
||||
Description: opts.Description,
|
||||
Release: opts.Release,
|
||||
DSL: opts.DSL,
|
||||
}
|
||||
if err := tx.Create(row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
|
||||
}
|
||||
|
||||
func (opts SaveOrReplaceLatestVersionOptions) sameDSL(dsl entity.JSONMap) bool {
|
||||
if opts.SameDSL != nil {
|
||||
return opts.SameDSL(dsl)
|
||||
|
||||
@@ -422,9 +422,7 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string,
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"release": false,
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
for _, key := range []string{"title", "avatar", "description", "permission", "canvas_type", "canvas_category"} {
|
||||
if value, ok := patch[key]; ok && value != nil {
|
||||
if key == "title" {
|
||||
@@ -543,10 +541,6 @@ type PublishAgentRequest struct {
|
||||
DSL entity.JSONMap `json:"dsl,omitempty"`
|
||||
}
|
||||
|
||||
// PublishAgent appends a new user_canvas_version row and marks the parent
|
||||
// canvas as released in a single transaction. Existing versions are never
|
||||
// overwritten (§2.9); the parent canvas DSL/title/description/release
|
||||
// fields are updated atomically with the new version row.
|
||||
func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string, req *PublishAgentRequest) (*entity.UserCanvasVersion, error) {
|
||||
canvasInstance, err := s.loadCanvasForUser(ctx, userID, canvasID)
|
||||
if err != nil {
|
||||
@@ -560,30 +554,33 @@ func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string
|
||||
dsl = dslpkg.NormalizeForCanvas(req.DSL)
|
||||
}
|
||||
if req.Title != nil {
|
||||
title = req.Title
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
title = &trimmed
|
||||
}
|
||||
if req.Description != nil {
|
||||
description = req.Description
|
||||
}
|
||||
}
|
||||
row := &entity.UserCanvasVersion{
|
||||
ID: utility.GenerateUUID(),
|
||||
UserCanvasID: canvasID,
|
||||
Title: title,
|
||||
Description: description,
|
||||
DSL: dsl,
|
||||
|
||||
canvasInstance.DSL = dsl
|
||||
canvasInstance.Title = title
|
||||
canvasInstance.Description = description
|
||||
canvasInstance.Release = true
|
||||
titleStr := ""
|
||||
if title != nil {
|
||||
titleStr = *title
|
||||
}
|
||||
opts := s.saveOrReplaceVersionOptions(ctx, userID, canvasID, dsl, titleStr, description, true)
|
||||
var row *entity.UserCanvasVersion
|
||||
if err = dao.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err = s.versionDAO.CreateTx(tx, row); err != nil {
|
||||
return fmt.Errorf("publish agent %s: insert version: %w", canvasID, err)
|
||||
}
|
||||
canvasInstance.DSL = dsl
|
||||
canvasInstance.Title = title
|
||||
canvasInstance.Description = description
|
||||
canvasInstance.Release = true
|
||||
if err = s.canvasDAO.UpdateTx(tx, canvasInstance); err != nil {
|
||||
if err := s.canvasDAO.UpdateTx(tx, canvasInstance); err != nil {
|
||||
return fmt.Errorf("publish agent %s: update parent: %w", canvasID, err)
|
||||
}
|
||||
saved, err := s.versionDAO.SaveOrReplaceLatestTx(tx, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("publish agent %s: save version: %w", canvasID, err)
|
||||
}
|
||||
row = saved
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -592,12 +589,16 @@ func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string
|
||||
}
|
||||
|
||||
func (s *AgentService) saveOrReplaceVersion(ctx context.Context, userID, canvasID string, dsl entity.JSONMap, title string, description *string, release bool) (*entity.UserCanvasVersion, error) {
|
||||
return s.versionDAO.SaveOrReplaceLatest(s.saveOrReplaceVersionOptions(ctx, userID, canvasID, dsl, title, description, release))
|
||||
}
|
||||
|
||||
func (s *AgentService) saveOrReplaceVersionOptions(ctx context.Context, userID, canvasID string, dsl entity.JSONMap, title string, description *string, release bool) dao.SaveOrReplaceLatestVersionOptions {
|
||||
nickname, err := s.userDAO.GetNicknameByID(ctx, userID)
|
||||
if err != nil || strings.TrimSpace(nickname) == "" {
|
||||
nickname = userID
|
||||
}
|
||||
versionTitle := buildVersionTitle(nickname, title, time.Now())
|
||||
return s.versionDAO.SaveOrReplaceLatest(dao.SaveOrReplaceLatestVersionOptions{
|
||||
return dao.SaveOrReplaceLatestVersionOptions{
|
||||
NewID: utility.GenerateUUID(),
|
||||
UserCanvasID: canvasID,
|
||||
Title: &versionTitle,
|
||||
@@ -611,7 +612,7 @@ func (s *AgentService) saveOrReplaceVersion(ctx context.Context, userID, canvasI
|
||||
dsl,
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildVersionTitle(userNickname, agentTitle string, ts time.Time) string {
|
||||
|
||||
@@ -1338,6 +1338,91 @@ func TestUpdateAgentDSLCreatesAndReplacesDraftVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishAgentUpdatesCanvasAndReleasedVersion(t *testing.T) {
|
||||
setupAgentSessionServiceTest(t)
|
||||
|
||||
if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"}).Error; err != nil {
|
||||
t.Fatalf("failed to seed user: %v", err)
|
||||
}
|
||||
initialDSL := entity.JSONMap{
|
||||
"components": map[string]any{},
|
||||
}
|
||||
if err := dao.DB.Create(&entity.UserCanvas{
|
||||
ID: "canvas-publish",
|
||||
UserID: "user-1",
|
||||
Title: sptr("Draft Agent"),
|
||||
CanvasCategory: "agent_canvas",
|
||||
DSL: initialDSL,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed to seed canvas: %v", err)
|
||||
}
|
||||
|
||||
description := "published description"
|
||||
publishTitle := " Published Agent "
|
||||
publishDSL := entity.JSONMap{
|
||||
"graph": map[string]any{
|
||||
"nodes": []any{map[string]any{"id": "begin"}},
|
||||
"edges": []any{},
|
||||
},
|
||||
"components": map[string]any{
|
||||
"begin": map[string]any{
|
||||
"obj": map[string]any{"component_name": "Begin"},
|
||||
},
|
||||
},
|
||||
}
|
||||
row, err := NewAgentService().PublishAgent(context.Background(), "user-1", "canvas-publish", &PublishAgentRequest{
|
||||
Title: &publishTitle,
|
||||
Description: &description,
|
||||
DSL: publishDSL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PublishAgent failed: %v", err)
|
||||
}
|
||||
if row == nil {
|
||||
t.Fatal("PublishAgent returned nil version")
|
||||
}
|
||||
if !row.Release {
|
||||
t.Fatal("published version release flag is false")
|
||||
}
|
||||
if row.Description == nil || *row.Description != description {
|
||||
t.Fatalf("published version description = %v", row.Description)
|
||||
}
|
||||
if row.Title == nil || !strings.HasPrefix(*row.Title, "owner_Published Agent_") {
|
||||
t.Fatalf("unexpected published version title: %v", row.Title)
|
||||
}
|
||||
|
||||
persisted, err := dao.NewUserCanvasDAO().GetByID("canvas-publish")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload canvas: %v", err)
|
||||
}
|
||||
if !persisted.Release {
|
||||
t.Fatal("publish did not mark canvas released")
|
||||
}
|
||||
if persisted.Title == nil || *persisted.Title != "Published Agent" {
|
||||
t.Fatalf("published canvas title = %v", persisted.Title)
|
||||
}
|
||||
if persisted.Description == nil || *persisted.Description != description {
|
||||
t.Fatalf("published canvas description = %v", persisted.Description)
|
||||
}
|
||||
if _, ok := persisted.DSL["graph"]; !ok {
|
||||
t.Fatalf("published canvas DSL was not persisted: %#v", persisted.DSL)
|
||||
}
|
||||
|
||||
versions, err := dao.NewUserCanvasVersionDAO().ListByCanvasID("canvas-publish")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list versions: %v", err)
|
||||
}
|
||||
if len(versions) != 1 {
|
||||
t.Fatalf("expected one published version, got %d", len(versions))
|
||||
}
|
||||
if versions[0].ID != row.ID {
|
||||
t.Fatalf("listed version id = %q, want %q", versions[0].ID, row.ID)
|
||||
}
|
||||
if !versions[0].Release {
|
||||
t.Fatal("listed version release flag is false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAgentDSLDoesNotOverwriteLatestReleasedVersion(t *testing.T) {
|
||||
setupAgentSessionServiceTest(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user