refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aitooling "code.tczkiot.com/wlw/ai-agent/internal/ai/tooling"
|
||||
"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/dto/request"
|
||||
"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/toolx"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
|
||||
|
||||
@@ -79,13 +78,7 @@ func (s *aIAgentService) CreateAIAgent(req request.CreateAIAgentRequest, operato
|
||||
item.Status = enums.StatusOk
|
||||
item.SortNo = 0
|
||||
item.AuditFields = utils.BuildAuditFields(operator)
|
||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
if err := repositories.AIAgentRepository.Create(ctx.Tx, item); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.replaceWorkflowBindings(ctx.Tx, item.ID, req.WorkflowBindings, operator)
|
||||
return err
|
||||
}); err != nil {
|
||||
if err := repositories.AIAgentRepository.Create(sqls.DB(), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
@@ -105,6 +98,7 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
|
||||
}
|
||||
columns := map[string]any{
|
||||
"name": item.Name,
|
||||
"avatar": item.Avatar,
|
||||
"description": item.Description,
|
||||
"ai_config_id": item.AIConfigID,
|
||||
"max_steps": item.MaxSteps,
|
||||
@@ -121,8 +115,6 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
|
||||
"fallback_mode": item.FallbackMode,
|
||||
"fallback_message": item.FallbackMessage,
|
||||
"knowledge_ids": item.KnowledgeIDs,
|
||||
"skill_ids": item.SkillIDs,
|
||||
"allowed_mcp_tools": item.AllowedMCPTools,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
@@ -130,13 +122,7 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
|
||||
if item.RolloutPercent != current.RolloutPercent {
|
||||
columns["previous_rollout_percent"] = current.RolloutPercent
|
||||
}
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
if err := repositories.AIAgentRepository.Updates(ctx.Tx, req.ID, columns); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.replaceWorkflowBindings(ctx.Tx, req.ID, req.WorkflowBindings, operator)
|
||||
return err
|
||||
})
|
||||
return repositories.AIAgentRepository.Updates(sqls.DB(), req.ID, columns)
|
||||
}
|
||||
|
||||
func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) error {
|
||||
@@ -189,42 +175,25 @@ func (s *aIAgentService) PublishAIAgent(id int64, operator *dto.AuthPrincipal) (
|
||||
}
|
||||
|
||||
func (s *aIAgentService) validatePublishableAgent(db *gorm.DB, agent *models.AIAgent) error {
|
||||
if agent == nil || agent.AIConfigID <= 0 {
|
||||
if agent == nil {
|
||||
return errorsx.InvalidParam("ai agent is required before publishing")
|
||||
}
|
||||
platform, err := PlatformAIService.IsPlatform(context.Background())
|
||||
if err != nil {
|
||||
return errorsx.InvalidParam("failed to resolve AI model source")
|
||||
}
|
||||
if !platform && agent.AIConfigID <= 0 {
|
||||
return errorsx.InvalidParam("ai agent model configuration is required before publishing")
|
||||
}
|
||||
config := repositories.AIConfigRepository.Get(db, agent.AIConfigID)
|
||||
if config == nil || config.Status != enums.StatusOk {
|
||||
return errorsx.InvalidParam("ai agent model configuration is unavailable")
|
||||
if !platform {
|
||||
config := repositories.AIConfigRepository.Get(db, agent.AIConfigID)
|
||||
if config == nil || config.Status != enums.StatusOk {
|
||||
return errorsx.InvalidParam("ai agent model configuration is unavailable")
|
||||
}
|
||||
}
|
||||
if _, err := s.normalizeToolPolicy(agent.ToolPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
var mcpTools []request.AIAgentMCPToolRequest
|
||||
if raw := strings.TrimSpace(agent.AllowedMCPTools); raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &mcpTools); err != nil {
|
||||
return errorsx.InvalidParam("ai agent MCP tools are invalid")
|
||||
}
|
||||
}
|
||||
for _, id := range utils.SplitInt64s(agent.SkillIDs) {
|
||||
skill := repositories.SkillDefinitionRepository.Get(db, id)
|
||||
if skill == nil || skill.Status != enums.StatusOk {
|
||||
return errorsx.InvalidParam("bound Skill is unavailable")
|
||||
}
|
||||
}
|
||||
for _, item := range mcpTools {
|
||||
definition, err := aitooling.DefaultRegistry.Resolve(item.ToolCode)
|
||||
if err != nil || definition.InputSchema == nil {
|
||||
return errorsx.InvalidParam("ai agent MCP tool definition is unavailable")
|
||||
}
|
||||
if _, err := validateMCPToolRiskPolicy(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, binding := range s.ListEnabledWorkflowBindings(db, agent.ID) {
|
||||
if binding.Version == nil || binding.Version.Status != enums.StatusOk {
|
||||
return errorsx.InvalidParam("bound workflow version is unavailable")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -292,15 +261,30 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
|
||||
if exists := s.Take("name = ? AND id <> ?", name, id); exists != nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0006")
|
||||
}
|
||||
if req.AIConfigID <= 0 {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0010")
|
||||
avatar := strings.TrimSpace(req.Avatar)
|
||||
if len(avatar) > 1024 {
|
||||
return nil, errorsx.InvalidParam("ai agent avatar URL must not exceed 1024 characters")
|
||||
}
|
||||
aiConfig := AIConfigService.Get(req.AIConfigID)
|
||||
if aiConfig == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0009")
|
||||
platform, err := PlatformAIService.IsPlatform(context.Background())
|
||||
if err != nil {
|
||||
return nil, errorsx.InvalidParam("failed to resolve AI model source")
|
||||
}
|
||||
if aiConfig.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0011")
|
||||
if platform && req.AIConfigID <= 0 && id > 0 {
|
||||
if current := s.Get(id); current != nil {
|
||||
req.AIConfigID = current.AIConfigID
|
||||
}
|
||||
}
|
||||
if !platform {
|
||||
if req.AIConfigID <= 0 {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0010")
|
||||
}
|
||||
aiConfig := AIConfigService.Get(req.AIConfigID)
|
||||
if aiConfig == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0009")
|
||||
}
|
||||
if aiConfig.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0011")
|
||||
}
|
||||
}
|
||||
if req.MaxSteps == 0 {
|
||||
req.MaxSteps = 6
|
||||
@@ -344,28 +328,13 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
|
||||
return nil, errorsx.InvalidParam("ai agent rollout percent must be between 1 and 100")
|
||||
}
|
||||
|
||||
skillIDs, err := s.normalizeSkillIDs(req.SkillIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
knowledgeBaseIDs, err := s.normalizeKnowledgeBaseIDs(req.KnowledgeBaseIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mcpTools, err := s.normalizeMCPTools(req.MCPTools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mcpToolsJSON := ""
|
||||
if len(mcpTools) > 0 {
|
||||
buf, marshalErr := json.Marshal(mcpTools)
|
||||
if marshalErr != nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0021")
|
||||
}
|
||||
mcpToolsJSON = string(buf)
|
||||
}
|
||||
return &models.AIAgent{
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
AIConfigID: req.AIConfigID,
|
||||
MaxSteps: req.MaxSteps,
|
||||
@@ -382,15 +351,13 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
|
||||
FallbackMode: req.FallbackMode,
|
||||
FallbackMessage: strings.TrimSpace(req.FallbackMessage),
|
||||
KnowledgeIDs: utils.JoinInt64s(knowledgeBaseIDs),
|
||||
SkillIDs: utils.JoinInt64s(skillIDs),
|
||||
AllowedMCPTools: mcpToolsJSON,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type normalizedAIAgentToolPolicy struct {
|
||||
MaxTotalCalls int `json:"maxTotalCalls,omitempty"`
|
||||
MaxArgumentBytes int `json:"maxArgumentBytes,omitempty"`
|
||||
AllowedRiskLevels []string `json:"allowedRiskLevels,omitempty"`
|
||||
MaxTotalCalls int `json:"max_total_calls,omitempty"`
|
||||
MaxArgumentBytes int `json:"max_argument_bytes,omitempty"`
|
||||
AllowedRiskLevels []string `json:"allowed_risk_levels,omitempty"`
|
||||
}
|
||||
|
||||
func (s *aIAgentService) normalizeToolPolicy(raw string) (string, error) {
|
||||
@@ -403,10 +370,10 @@ func (s *aIAgentService) normalizeToolPolicy(raw string) (string, error) {
|
||||
return "", errorsx.InvalidParam("ai agent tool policy must be valid JSON")
|
||||
}
|
||||
if policy.MaxTotalCalls < 0 || policy.MaxTotalCalls > 8 {
|
||||
return "", errorsx.InvalidParam("ai agent tool policy maxTotalCalls must be between 1 and 8")
|
||||
return "", errorsx.InvalidParam("ai agent tool policy max_total_calls must be between 1 and 8")
|
||||
}
|
||||
if policy.MaxArgumentBytes < 0 || policy.MaxArgumentBytes > 64*1024 {
|
||||
return "", errorsx.InvalidParam("ai agent tool policy maxArgumentBytes must be between 1 and 65536")
|
||||
return "", errorsx.InvalidParam("ai agent tool policy max_argument_bytes must be between 1 and 65536")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(policy.AllowedRiskLevels))
|
||||
riskLevels := make([]string, 0, len(policy.AllowedRiskLevels))
|
||||
@@ -477,78 +444,6 @@ func (s *aIAgentService) normalizeTeamIDs(input []int64) ([]int64, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *aIAgentService) normalizeSkillIDs(input []int64) ([]int64, error) {
|
||||
ret := make([]int64, 0, len(input))
|
||||
seen := make(map[int64]struct{})
|
||||
for _, id := range input {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
continue
|
||||
}
|
||||
skill := SkillDefinitionService.Get(id)
|
||||
if skill == nil || skill.Status == enums.StatusDeleted {
|
||||
continue
|
||||
}
|
||||
if skill.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0056")
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ret = append(ret, id)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *aIAgentService) normalizeMCPTools(input []request.AIAgentMCPToolRequest) ([]request.AIAgentMCPToolRequest, error) {
|
||||
if len(input) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ret := make([]request.AIAgentMCPToolRequest, 0, len(input))
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range input {
|
||||
normalized, err := toolx.NormalizeMCPToolRequest(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if toolx.ResolveToolSourceType(normalized.ToolCode) != enums.ToolSourceTypeMCP {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0020")
|
||||
}
|
||||
if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized.RiskLevel = strings.ToLower(strings.TrimSpace(item.RiskLevel))
|
||||
normalized.RequireConfirmation = item.RequireConfirmation
|
||||
normalized, err = validateMCPToolRiskPolicy(normalized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := strings.TrimSpace(normalized.ToolCode)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
ret = append(ret, normalized)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func validateMCPToolRiskPolicy(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
|
||||
if policy, ok := toolx.GetTrustedMCPToolPolicy(item.ToolCode); ok {
|
||||
if item.RiskLevel != policy.RiskLevel || item.RequireConfirmation != policy.RequireConfirmation {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("system MCP tool risk policy cannot be changed")
|
||||
}
|
||||
return toolx.ApplyTrustedMCPToolPolicy(item), nil
|
||||
}
|
||||
if item.RiskLevel != aitooling.RiskLevelRead && item.RiskLevel != aitooling.RiskLevelWrite {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("MCP tool risk level must be read or write")
|
||||
}
|
||||
if item.RiskLevel == aitooling.RiskLevelWrite && !item.RequireConfirmation {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("write MCP tools must require confirmation")
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *aIAgentService) UpdateSort(ids []int64) error {
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
for i, id := range ids {
|
||||
|
||||
Reference in New Issue
Block a user