feat: enhance AI agent functionality with tool management and logging improvements

- Added AllowedToolCodes field to Context for managing tool access.
- Introduced ResumeSource in aiReplyTraceData to track resume points.
- Enhanced logging with additional fields: PlannedSkillName, SkillRouteTrace, InterruptType, ResumeSource, and FinalStatus.
- Implemented functions to parse and resolve allowed tool codes for agents and skills.
- Refactored skill definition creation and update logic to streamline request handling.
- Updated MCP tool catalog to include source type and integrated built-in tools.
- Improved UI components to display additional tool information and enhance user experience.
This commit is contained in:
mlogclub
2026-04-10 14:43:57 +08:00
parent 7411bac57d
commit 1370e4b675
23 changed files with 642 additions and 222 deletions
+2 -9
View File
@@ -7,7 +7,6 @@ import (
"time"
"cs-agent/internal/models"
"cs-agent/internal/pkg/config"
"cs-agent/internal/pkg/dto"
"cs-agent/internal/pkg/dto/request"
"cs-agent/internal/pkg/enums"
@@ -288,10 +287,6 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
if len(input) == 0 {
return nil, nil
}
cfg := config.Current()
if !cfg.MCP.Enabled {
return nil, errorsx.InvalidParam("系统未启用 MCP,不能配置 Direct Tool")
}
ret := make([]request.AIAgentMCPToolRequest, 0, len(input))
seen := make(map[string]struct{})
for _, item := range input {
@@ -299,10 +294,8 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
if err != nil {
return nil, err
}
serverCode := strings.TrimSpace(normalized.ServerCode)
server, ok := cfg.MCP.Servers[serverCode]
if !ok || !server.Enabled {
return nil, errorsx.InvalidParam("Direct Tool 绑定的 MCP 服务不存在或未启用")
if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil {
return nil, err
}
key := strings.TrimSpace(normalized.ToolCode)
if _, exists := seen[key]; exists {
@@ -1,7 +1,16 @@
package services
import (
"encoding/json"
"strings"
"time"
"cs-agent/internal/models"
"cs-agent/internal/pkg/dto"
"cs-agent/internal/pkg/dto/request"
"cs-agent/internal/pkg/enums"
"cs-agent/internal/pkg/errorsx"
"cs-agent/internal/pkg/utils"
"cs-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
@@ -86,3 +95,156 @@ func (s *skillDefinitionService) UpdatePriority(ids []int64) error {
func (s *skillDefinitionService) GetByCode(code string) *models.SkillDefinition {
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), code)
}
func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDefinitionRequest, operator *dto.AuthPrincipal) (*models.SkillDefinition, error) {
if operator == nil {
return nil, errorsx.Unauthorized("未登录或登录已过期")
}
normalized, err := s.normalizeSkillDefinitionRequest(req)
if err != nil {
return nil, err
}
if s.Take("code = ?", normalized.Code) != nil {
return nil, errorsx.InvalidParam("Skill 编码已存在")
}
item := &models.SkillDefinition{
Code: normalized.Code,
Name: normalized.Name,
Description: normalized.Description,
Content: normalized.Content,
Examples: mustMarshalSkillStringArray(normalized.Examples),
AllowedToolCodes: mustMarshalSkillStringArray(normalized.AllowedToolCodes),
Priority: normalized.Priority,
Status: enums.StatusOk,
Remark: normalized.Remark,
AuditFields: utils.BuildAuditFields(operator),
}
if item.Priority <= 0 {
item.Priority = s.NextPriority()
}
if err := repositories.SkillDefinitionRepository.Create(sqls.DB(), item); err != nil {
return nil, err
}
return item, nil
}
func (s *skillDefinitionService) UpdateSkillDefinition(req request.UpdateSkillDefinitionRequest, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.Unauthorized("未登录或登录已过期")
}
if req.ID <= 0 {
return errorsx.InvalidParam("Skill ID 不合法")
}
current := s.Get(req.ID)
if current == nil {
return errorsx.InvalidParam("Skill 不存在")
}
normalized, err := s.normalizeSkillDefinitionRequest(req.CreateSkillDefinitionRequest)
if err != nil {
return err
}
if exists := s.Take("code = ? AND id <> ?", normalized.Code, req.ID); exists != nil {
return errorsx.InvalidParam("Skill 编码已存在")
}
return repositories.SkillDefinitionRepository.Updates(sqls.DB(), req.ID, map[string]any{
"code": normalized.Code,
"name": normalized.Name,
"description": normalized.Description,
"content": normalized.Content,
"examples": mustMarshalSkillStringArray(normalized.Examples),
"allowed_tool_codes": mustMarshalSkillStringArray(normalized.AllowedToolCodes),
"priority": resolveSkillPriorityForService(normalized.Priority, current.Priority),
"remark": normalized.Remark,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
})
}
func (s *skillDefinitionService) normalizeSkillDefinitionRequest(req request.CreateSkillDefinitionRequest) (*request.CreateSkillDefinitionRequest, error) {
normalized := &request.CreateSkillDefinitionRequest{
Code: strings.TrimSpace(req.Code),
Name: strings.TrimSpace(req.Name),
Description: strings.TrimSpace(req.Description),
Content: strings.TrimSpace(req.Content),
Priority: normalizeSkillPriorityForService(req.Priority),
Remark: strings.TrimSpace(req.Remark),
}
if normalized.Code == "" {
return nil, errorsx.InvalidParam("Skill 编码不能为空")
}
if normalized.Name == "" {
return nil, errorsx.InvalidParam("Skill 名称不能为空")
}
if normalized.Content == "" {
return nil, errorsx.InvalidParam("Content 不能为空")
}
examples, err := normalizeSkillStringArray(req.Examples)
if err != nil {
return nil, err
}
allowedToolCodes, err := normalizeSkillStringArray(req.AllowedToolCodes)
if err != nil {
return nil, err
}
for _, toolCode := range allowedToolCodes {
if err := ToolCatalogService.ValidateMCPToolCode(toolCode); err != nil {
return nil, err
}
}
normalized.Examples = examples
normalized.AllowedToolCodes = allowedToolCodes
return normalized, nil
}
func normalizeSkillStringArray(input []string) ([]string, error) {
buf, err := json.Marshal(input)
if err != nil {
return nil, errorsx.InvalidParam("JSON 数组格式不合法")
}
var ret []string
if err := json.Unmarshal(buf, &ret); err != nil {
return nil, errorsx.InvalidParam("JSON 数组格式不合法")
}
normalized := make([]string, 0, len(ret))
seen := make(map[string]struct{}, len(ret))
for _, item := range ret {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
normalized = append(normalized, item)
}
return normalized, nil
}
func mustMarshalSkillStringArray(input []string) string {
items, err := normalizeSkillStringArray(input)
if err != nil || len(items) == 0 {
return "[]"
}
buf, err := json.Marshal(items)
if err != nil {
return "[]"
}
return string(buf)
}
func normalizeSkillPriorityForService(priority int) int {
if priority < 0 {
return 0
}
return priority
}
func resolveSkillPriorityForService(input, current int) int {
input = normalizeSkillPriorityForService(input)
if input <= 0 {
return current
}
return input
}
+23 -2
View File
@@ -23,6 +23,7 @@ type MCPToolCatalogItem struct {
ToolCode string
ServerCode string
ToolName string
SourceType string
Title string
Description string
InputSchema any
@@ -46,6 +47,14 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
}
slices.Sort(serverCodes)
ret := make([]MCPToolCatalogItem, 0)
ret = append(ret, MCPToolCatalogItem{
ToolCode: toolx.BuiltinCreateTicketConfirmToolCode,
ServerCode: toolx.BuiltinToolCatalogServerCode,
ToolName: toolx.BuiltinCreateTicketConfirmToolName,
SourceType: toolx.BuiltinToolCatalogServerCode,
Title: toolx.BuiltinCreateTicketConfirmToolTitle,
Description: toolx.BuiltinCreateTicketConfirmToolDescription,
})
for _, serverCode := range serverCodes {
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
if err != nil {
@@ -56,6 +65,7 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
ToolCode: toolx.BuildMCPToolCode(serverCode, item.Name),
ServerCode: serverCode,
ToolName: strings.TrimSpace(item.Name),
SourceType: "mcp",
Title: strings.TrimSpace(item.Title),
Description: strings.TrimSpace(item.Description),
InputSchema: item.InputSchema,
@@ -67,14 +77,25 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
}
func (s *toolCatalogService) ValidateMCPToolCode(toolCode string) error {
return s.ValidateToolCode(toolCode)
}
func (s *toolCatalogService) ValidateToolCode(toolCode string) error {
cfg := config.Current()
if !cfg.MCP.Enabled {
return errorsx.InvalidParam("MCP未启用")
toolCode = strings.TrimSpace(toolCode)
if toolCode == "" {
return errorsx.InvalidParam("toolCode不能为空")
}
if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
return nil
}
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
if serverCode == "" || toolName == "" {
return errorsx.InvalidParam("toolCode格式不合法")
}
if !cfg.MCP.Enabled {
return errorsx.InvalidParam("MCP未启用")
}
server, ok := cfg.MCP.Servers[serverCode]
if !ok || !server.Enabled {
return errorsx.InvalidParam("toolCode 绑定的 MCP 服务不存在或未启用")