refactor: update AI agent handling to maintain active published revision and improve toast messages

This commit is contained in:
mlogclub
2026-07-28 10:26:10 +08:00
parent cca3b558a3
commit 7f9ff18743
6 changed files with 160 additions and 6 deletions
+10 -2
View File
@@ -113,8 +113,16 @@ func (s *agentRevisionService) ResolvePublishedSnapshot(agent models.AIAgent, co
if err := json.Unmarshal([]byte(revision.Definition), &definition); err != nil {
return nil, errorsx.InvalidParam("published Agent revision is invalid")
}
if definition.Agent.AIConfigID > 0 && definition.Agent.AIConfigID != config.ID {
return nil, errorsx.InvalidParam("published agent model config no longer matches")
publishedConfigID := definition.Agent.AIConfigID
if publishedConfigID <= 0 {
publishedConfigID = definition.Model.ConfigID
}
if publishedConfigID > 0 && publishedConfigID != config.ID {
publishedConfig := repositories.AIConfigRepository.Get(sqls.DB(), publishedConfigID)
if publishedConfig == nil || publishedConfig.Status != enums.StatusOk {
return nil, errorsx.InvalidParam("published agent model config is unavailable")
}
snapshot.AIConfig = *publishedConfig
}
applyRevisionAgentSnapshot(&snapshot.Agent, definition.Agent)
snapshot.WorkflowBindings = append([]AgentRevisionWorkflowBinding(nil), definition.WorkflowBindings...)
@@ -18,7 +18,7 @@ func TestAgentRevisionServiceRestoresPublishedSnapshotAndKeepsAPIKey(t *testing.
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&models.AgentRevision{}); err != nil {
if err := db.AutoMigrate(&models.AgentRevision{}, &models.AIConfig{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
sqls.SetDB(db)
@@ -48,3 +48,59 @@ func TestAgentRevisionServiceRestoresPublishedSnapshotAndKeepsAPIKey(t *testing.
t.Fatalf("model snapshot not restored safely: %#v", snapshot.AIConfig)
}
}
func TestAgentRevisionServiceUsesPublishedModelConfigAfterDraftConfigChanges(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+strings.ReplaceAll(t.Name(), "/", "_")+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&models.AgentRevision{}, &models.AIConfig{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
sqls.SetDB(db)
publishedConfig := &models.AIConfig{
Name: "published", Status: enums.StatusOk, Provider: enums.AIProviderOpenAI,
ModelType: enums.AIModelTypeLLM, ModelName: "current-published-model", APIKey: "published-secret",
}
draftConfig := &models.AIConfig{
Name: "draft", Status: enums.StatusOk, Provider: enums.AIProviderOpenAI,
ModelType: enums.AIModelTypeLLM, ModelName: "draft-model", APIKey: "draft-secret",
}
if err := db.Create(publishedConfig).Error; err != nil {
t.Fatalf("create published config: %v", err)
}
if err := db.Create(draftConfig).Error; err != nil {
t.Fatalf("create draft config: %v", err)
}
definition := agentRevisionDefinition{
Agent: agentRevisionAgent{AIConfigID: publishedConfig.ID, SystemPrompt: "published instruction"},
Model: agentRevisionModel{
ConfigID: publishedConfig.ID, Provider: string(enums.AIProviderOpenAI),
ModelType: string(enums.AIModelTypeLLM), ModelName: "snapshotted-published-model",
},
}
data, err := json.Marshal(definition)
if err != nil {
t.Fatalf("marshal definition: %v", err)
}
revision := &models.AgentRevision{AgentID: 7, Revision: 1, Status: enums.StatusOk, Definition: string(data)}
if err := db.Create(revision).Error; err != nil {
t.Fatalf("create revision: %v", err)
}
snapshot, err := AgentRevisionService.ResolvePublishedSnapshot(
models.AIAgent{ID: 7, AIConfigID: draftConfig.ID, PublishedRevisionID: revision.ID},
*draftConfig,
)
if err != nil {
t.Fatalf("ResolvePublishedSnapshot: %v", err)
}
if snapshot.AIConfig.ID != publishedConfig.ID {
t.Fatalf("model config id = %d, want published config %d", snapshot.AIConfig.ID, publishedConfig.ID)
}
if snapshot.AIConfig.ModelName != "snapshotted-published-model" || snapshot.AIConfig.APIKey != "published-secret" {
t.Fatalf("published model snapshot not restored safely: %#v", snapshot.AIConfig)
}
}
-1
View File
@@ -123,7 +123,6 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
"knowledge_ids": item.KnowledgeIDs,
"skill_ids": item.SkillIDs,
"allowed_mcp_tools": item.AllowedMCPTools,
"published_revision_id": 0,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
@@ -0,0 +1,80 @@
package services
import (
"strings"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func TestUpdateAIAgentKeepsPublishedRevisionActive(t *testing.T) {
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_",
SingularTable: true,
},
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() {
sqlDB, dbErr := db.DB()
if dbErr == nil {
_ = sqlDB.Close()
}
})
if err := db.AutoMigrate(&models.AIConfig{}, &models.AIAgent{}, &models.AIAgentWorkflowBinding{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
sqls.SetDB(db)
config := &models.AIConfig{
Name: "test", Status: enums.StatusOk, Provider: enums.AIProviderOpenAI,
ModelType: enums.AIModelTypeLLM, ModelName: "test-model",
}
if err := db.Create(config).Error; err != nil {
t.Fatalf("create ai config: %v", err)
}
agent := &models.AIAgent{
Name: "published agent", Status: enums.StatusOk, AIConfigID: config.ID,
ServiceMode: enums.IMConversationServiceModeAIFirst, HandoffMode: enums.AIAgentHandoffModeWaitPool,
FallbackMode: enums.AIAgentFallbackModeNoAnswer, RolloutPercent: 100, PublishedRevisionID: 18,
}
if err := db.Create(agent).Error; err != nil {
t.Fatalf("create ai agent: %v", err)
}
err = AIAgentService.UpdateAIAgent(request.UpdateAIAgentRequest{
ID: agent.ID,
CreateAIAgentRequest: request.CreateAIAgentRequest{
Name: "updated draft", AIConfigID: config.ID,
ServiceMode: enums.IMConversationServiceModeAIFirst,
HandoffMode: enums.AIAgentHandoffModeWaitPool,
FallbackMode: enums.AIAgentFallbackModeNoAnswer,
RolloutPercent: 100,
},
}, &dto.AuthPrincipal{UserID: 1, Username: "admin"})
if err != nil {
t.Fatalf("UpdateAIAgent: %v", err)
}
var updated models.AIAgent
if err := db.First(&updated, agent.ID).Error; err != nil {
t.Fatalf("get updated ai agent: %v", err)
}
if updated.PublishedRevisionID != agent.PublishedRevisionID {
t.Fatalf("published revision id = %d, want %d", updated.PublishedRevisionID, agent.PublishedRevisionID)
}
if updated.Name != "updated draft" {
t.Fatalf("draft name = %q, want updated draft", updated.Name)
}
}
@@ -58,3 +58,8 @@ test("publishing an AI Agent saves the current form before publishing", () => {
assert.ok(publishIndex > saveIndex)
assert.match(publishFunction, /Agent 配置已保存并发布/)
})
test("saving a published AI Agent keeps the active revision online", () => {
assert.match(configWorkbenchSource, /配置已保存,当前已发布版本继续生效/)
assert.match(configWorkbenchSource, /已发布版本正在生效;再次发布后应用当前配置/)
})
@@ -390,7 +390,11 @@ export function AIAgentConfigWorkbench({
const payload = buildPayload()
if (agent) {
await updateAIAgent({ id: agent.id, ...payload })
toast.success("Agent 配置已保存")
toast.success(
agent.publishedRevisionId > 0
? "配置已保存,当前已发布版本继续生效"
: "Agent 配置已保存",
)
await loadData()
} else {
const created = await createAIAgent(payload)
@@ -828,7 +832,9 @@ export function AIAgentConfigWorkbench({
)}
/>
<span>
{agentPublished ? "Agent 已发布;再次发布会保存当前配置" : "发布时会先保存当前配置"}
{agentPublished
? "已发布版本正在生效;再次发布后应用当前配置"
: "发布时会先保存当前配置"}
</span>
</div>
<div className="flex items-center gap-2">