refactor: update AI agent handling to maintain active published revision and improve toast messages
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user