18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
197 lines
8.0 KiB
Go
197 lines
8.0 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
|
|
|
|
"github.com/mlogclub/simple/sqls"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var AgentRevisionService = newAgentRevisionService()
|
|
|
|
func newAgentRevisionService() *agentRevisionService {
|
|
return &agentRevisionService{}
|
|
}
|
|
|
|
type agentRevisionService struct{}
|
|
|
|
func (s *agentRevisionService) Get(id int64) *models.AgentRevision {
|
|
if id <= 0 {
|
|
return nil
|
|
}
|
|
return repositories.AgentRevisionRepository.Get(sqls.DB(), id)
|
|
}
|
|
|
|
func (s *agentRevisionService) FindByAgentID(agentID int64) []models.AgentRevision {
|
|
return repositories.AgentRevisionRepository.FindByAgentID(sqls.DB(), agentID)
|
|
}
|
|
|
|
type agentRevisionDefinition struct {
|
|
Agent agentRevisionAgent `json:"agent"`
|
|
Model agentRevisionModel `json:"model"`
|
|
}
|
|
|
|
// agentRevisionModel deliberately excludes APIKey. A revision must capture
|
|
// reproducible routing/model parameters without duplicating credentials.
|
|
type agentRevisionModel struct {
|
|
ConfigID int64 `json:"config_id"`
|
|
Provider string `json:"provider"`
|
|
BaseURL string `json:"base_url"`
|
|
ModelType string `json:"model_type"`
|
|
ModelName string `json:"model_name"`
|
|
MaxContextTokens int `json:"max_context_tokens"`
|
|
MaxOutputTokens int `json:"max_output_tokens"`
|
|
TimeoutMS int `json:"timeout_ms"`
|
|
MaxRetryCount int `json:"max_retry_count"`
|
|
}
|
|
|
|
type agentRevisionAgent struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
AIConfigID int64 `json:"ai_config_id"`
|
|
MaxSteps int `json:"max_steps"`
|
|
ContextWindow int `json:"context_window"`
|
|
ToolPolicy string `json:"tool_policy"`
|
|
KnowledgePolicy string `json:"knowledge_policy"`
|
|
ServiceMode int `json:"service_mode"`
|
|
SystemPrompt string `json:"system_prompt"`
|
|
WelcomeMessage string `json:"welcome_message"`
|
|
ReplyTimeoutSeconds int `json:"reply_timeout_seconds"`
|
|
TeamIDs string `json:"team_ids"`
|
|
HandoffMode int `json:"handoff_mode"`
|
|
FallbackMode int `json:"fallback_mode"`
|
|
FallbackMessage string `json:"fallback_message"`
|
|
KnowledgeIDs string `json:"knowledge_ids"`
|
|
}
|
|
|
|
// AgentRevisionSnapshot is the immutable runtime configuration restored from
|
|
// a published revision. Model credentials deliberately remain on the current
|
|
// AIConfig so credential rotation does not require republishing every Agent.
|
|
type AgentRevisionSnapshot struct {
|
|
Revision models.AgentRevision
|
|
Agent models.AIAgent
|
|
AIConfig models.AIConfig
|
|
}
|
|
|
|
// ResolvePublishedSnapshot restores an immutable published Agent revision.
|
|
func (s *agentRevisionService) ResolvePublishedSnapshot(agent models.AIAgent, config models.AIConfig) (*AgentRevisionSnapshot, error) {
|
|
if agent.PublishedRevisionID <= 0 {
|
|
return nil, errorsx.InvalidParam("Agent is not published")
|
|
}
|
|
revision := repositories.AgentRevisionRepository.Get(sqls.DB(), agent.PublishedRevisionID)
|
|
if revision == nil || revision.AgentID != agent.ID || revision.Status != enums.StatusOk {
|
|
return nil, errorsx.InvalidParam("published Agent revision does not exist")
|
|
}
|
|
snapshot := &AgentRevisionSnapshot{Revision: *revision, Agent: agent, AIConfig: config}
|
|
if strings.TrimSpace(revision.Definition) == "" {
|
|
return nil, errorsx.InvalidParam("published Agent revision definition is empty")
|
|
}
|
|
definition := agentRevisionDefinition{}
|
|
if err := json.Unmarshal([]byte(revision.Definition), &definition); err != nil {
|
|
return nil, errorsx.InvalidParam("published Agent revision is invalid")
|
|
}
|
|
publishedConfigID := definition.Agent.AIConfigID
|
|
if publishedConfigID <= 0 {
|
|
publishedConfigID = definition.Model.ConfigID
|
|
}
|
|
if !config.Platform && 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)
|
|
if !config.Platform {
|
|
applyRevisionModelSnapshot(&snapshot.AIConfig, definition.Model)
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func applyRevisionAgentSnapshot(agent *models.AIAgent, definition agentRevisionAgent) {
|
|
if agent == nil {
|
|
return
|
|
}
|
|
agent.Name = definition.Name
|
|
agent.Description = definition.Description
|
|
agent.AIConfigID = definition.AIConfigID
|
|
agent.MaxSteps = definition.MaxSteps
|
|
agent.ContextWindow = definition.ContextWindow
|
|
agent.ToolPolicy = definition.ToolPolicy
|
|
agent.KnowledgePolicy = definition.KnowledgePolicy
|
|
agent.ServiceMode = enums.IMConversationServiceMode(definition.ServiceMode)
|
|
agent.SystemPrompt = definition.SystemPrompt
|
|
agent.WelcomeMessage = definition.WelcomeMessage
|
|
agent.ReplyTimeoutSeconds = definition.ReplyTimeoutSeconds
|
|
agent.TeamIDs = definition.TeamIDs
|
|
agent.HandoffMode = enums.AIAgentHandoffMode(definition.HandoffMode)
|
|
agent.FallbackMode = enums.AIAgentFallbackMode(definition.FallbackMode)
|
|
agent.FallbackMessage = definition.FallbackMessage
|
|
agent.KnowledgeIDs = definition.KnowledgeIDs
|
|
}
|
|
|
|
func applyRevisionModelSnapshot(config *models.AIConfig, definition agentRevisionModel) {
|
|
if config == nil || definition.ConfigID <= 0 {
|
|
return
|
|
}
|
|
config.Provider = enums.AIProvider(definition.Provider)
|
|
config.BaseURL = definition.BaseURL
|
|
config.ModelType = enums.AIModelType(definition.ModelType)
|
|
config.ModelName = definition.ModelName
|
|
config.MaxContextTokens = definition.MaxContextTokens
|
|
config.MaxOutputTokens = definition.MaxOutputTokens
|
|
config.TimeoutMS = definition.TimeoutMS
|
|
config.MaxRetryCount = definition.MaxRetryCount
|
|
}
|
|
|
|
func (s *agentRevisionService) PublishSnapshot(db *gorm.DB, agent *models.AIAgent, operator *dto.AuthPrincipal) (*models.AgentRevision, error) {
|
|
return s.publishSnapshot(db, agent, operator)
|
|
}
|
|
|
|
func (s *agentRevisionService) publishSnapshot(db *gorm.DB, agent *models.AIAgent, operator *dto.AuthPrincipal) (*models.AgentRevision, error) {
|
|
model := agentRevisionModel{ConfigID: agent.AIConfigID}
|
|
if config := repositories.AIConfigRepository.Get(db, agent.AIConfigID); config != nil {
|
|
model = agentRevisionModel{
|
|
ConfigID: config.ID, Provider: string(config.Provider), BaseURL: config.BaseURL, ModelType: string(config.ModelType),
|
|
ModelName: config.ModelName, MaxContextTokens: config.MaxContextTokens, MaxOutputTokens: config.MaxOutputTokens,
|
|
TimeoutMS: config.TimeoutMS, MaxRetryCount: config.MaxRetryCount,
|
|
}
|
|
}
|
|
definition := agentRevisionDefinition{
|
|
Agent: agentRevisionAgent{
|
|
Name: agent.Name, Description: agent.Description, AIConfigID: agent.AIConfigID,
|
|
MaxSteps: agent.MaxSteps, ContextWindow: agent.ContextWindow,
|
|
ToolPolicy: agent.ToolPolicy, KnowledgePolicy: agent.KnowledgePolicy, ServiceMode: int(agent.ServiceMode), SystemPrompt: agent.SystemPrompt,
|
|
WelcomeMessage: agent.WelcomeMessage, ReplyTimeoutSeconds: agent.ReplyTimeoutSeconds, TeamIDs: agent.TeamIDs, HandoffMode: int(agent.HandoffMode),
|
|
FallbackMode: int(agent.FallbackMode), FallbackMessage: agent.FallbackMessage, KnowledgeIDs: agent.KnowledgeIDs,
|
|
},
|
|
Model: model,
|
|
}
|
|
data, err := json.Marshal(definition)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
hash := sha256.Sum256(data)
|
|
item := &models.AgentRevision{
|
|
AgentID: agent.ID, Revision: repositories.AgentRevisionRepository.MaxRevisionByAgentID(db, agent.ID) + 1,
|
|
Status: enums.StatusOk, Definition: string(data), DefinitionHash: hex.EncodeToString(hash[:]),
|
|
PublishedAt: &now, PublishedByID: operator.UserID, PublishedByName: operator.Username, AuditFields: utils.BuildAuditFields(operator),
|
|
}
|
|
if err := repositories.AgentRevisionRepository.Create(db, item); err != nil {
|
|
return nil, err
|
|
}
|
|
return item, nil
|
|
}
|