diff --git a/internal/ai/skills/execution.go b/internal/ai/skills/execution.go index e0a55d4..61c7eae 100644 --- a/internal/ai/skills/execution.go +++ b/internal/ai/skills/execution.go @@ -1,57 +1,36 @@ package skills import ( - "bytes" "context" - "encoding/json" - "fmt" "strings" - "text/template" "time" "cs-agent/internal/ai" - "cs-agent/internal/ai/mcps" - "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" ) -type mcpToolExecutionConfig struct { - ServerCode string `json:"serverCode"` - ToolName string `json:"toolName"` - Arguments map[string]string `json:"arguments"` -} - func executeByPlan(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext) (string, *ExecutionTrace, error) { if plan == nil || plan.Skill == nil { return "", nil, nil } trace := &ExecutionTrace{ Status: "started", - ExecutionMode: string(plan.Skill.ExecutionMode), - } - switch plan.Skill.ExecutionMode { - case "", enums.SkillExecutionModePromptOnly: - replyText, err := executePromptOnly(ctx, plan, runtimeCtx, trace) - return replyText, trace, err - case enums.SkillExecutionModeMCPTool: - replyText, err := executeMCPTool(ctx, plan, runtimeCtx, trace) - return replyText, trace, err - default: - trace.Status = "invalid_execution_mode" - return "", trace, errorsx.InvalidParam("Skill执行模式不支持") + ExecutionMode: "content", } + replyText, err := executeContent(ctx, plan, runtimeCtx, trace) + return replyText, trace, err } -func executePromptOnly(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext, trace *ExecutionTrace) (string, error) { +func executeContent(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext, trace *ExecutionTrace) (string, error) { if plan == nil || plan.Skill == nil { return "", nil } if plan.AIConfig == nil { return "", errorsx.InvalidParam("Skill 关联的 AI 配置不可用") } - systemPrompt := strings.TrimSpace(plan.Skill.Prompt) + systemPrompt := strings.TrimSpace(plan.Skill.Content) if systemPrompt == "" { - return "", errorsx.InvalidParam("Skill Prompt 不能为空") + return "", errorsx.InvalidParam("Skill Content 不能为空") } userPrompt := strings.TrimSpace(runtimeCtx.UserMessage) if userPrompt == "" { @@ -81,168 +60,3 @@ func executePromptOnly(ctx context.Context, plan *ExecutionPlan, runtimeCtx Runt } return strings.TrimSpace(result.Content), nil } - -func executeMCPTool(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext, trace *ExecutionTrace) (string, error) { - cfg, err := parseMCPToolExecutionConfig(plan.Skill.ExecutionConfig) - if err != nil { - if trace != nil { - trace.Status = "config_error" - } - return "", err - } - arguments, err := buildToolArguments(cfg.Arguments, runtimeCtx) - if err != nil { - if trace != nil { - trace.Status = "argument_error" - } - return "", err - } - mcpTrace := &MCPExecutionTrace{ - Status: "started", - ServerCode: cfg.ServerCode, - ToolName: cfg.ToolName, - Arguments: arguments, - } - if trace != nil { - trace.MCP = mcpTrace - } - toolStartedAt := time.Now() - toolResult, err := mcps.Runtime.CallTool(ctx, cfg.ServerCode, cfg.ToolName, arguments) - mcpTrace.LatencyMs = time.Since(toolStartedAt).Milliseconds() - if err != nil { - mcpTrace.Status = "error" - mcpTrace.Error = err.Error() - if trace != nil { - trace.Status = "error" - } - return "", err - } - mcpTrace.Status = "ok" - mcpTrace.IsError = toolResult.IsError - mcpTrace.ContentItemCount = len(toolResult.Content) - mcpTrace.HasStructuredContent = toolResult.StructuredContent != nil - toolSummary := buildToolSummary(toolResult) - mcpTrace.ResultPreview = truncateTraceText(toolSummary, 500) - if strings.TrimSpace(toolSummary) == "" { - if trace != nil { - trace.Status = "empty_tool_result" - } - return "", errorsx.InvalidParam("MCP工具未返回有效结果") - } - systemPrompt := strings.TrimSpace(plan.Skill.Prompt) - if systemPrompt == "" { - systemPrompt = "你是客服技能助手。请依据工具结果准确回答用户问题,不要编造工具结果中不存在的事实。" - } - userPrompt := fmt.Sprintf("用户问题:%s\n\n工具结果:\n%s", strings.TrimSpace(runtimeCtx.UserMessage), toolSummary) - summaryTrace := &PromptTrace{Status: "started"} - mcpTrace.SummaryPrompt = summaryTrace - summaryStartedAt := time.Now() - result, err := ai.LLM.ChatWithConfig(ctx, plan.AIConfig, systemPrompt, userPrompt) - summaryTrace.LatencyMs = time.Since(summaryStartedAt).Milliseconds() - if err != nil { - summaryTrace.Status = "error" - summaryTrace.Error = err.Error() - if trace != nil { - trace.Status = "error" - } - return "", err - } - summaryTrace.Status = "ok" - summaryTrace.ModelName = result.ModelName - summaryTrace.PromptTokens = result.PromptTokens - summaryTrace.CompletionTokens = result.CompletionTokens - if trace != nil { - trace.Status = "ok" - } - return strings.TrimSpace(result.Content), nil -} - -func parseMCPToolExecutionConfig(raw string) (*mcpToolExecutionConfig, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil, errorsx.InvalidParam("ExecutionConfig不能为空") - } - cfg := &mcpToolExecutionConfig{} - if err := json.Unmarshal([]byte(raw), cfg); err != nil { - return nil, errorsx.InvalidParam("ExecutionConfig格式不合法") - } - if strings.TrimSpace(cfg.ServerCode) == "" { - return nil, errorsx.InvalidParam("ExecutionConfig.serverCode不能为空") - } - if strings.TrimSpace(cfg.ToolName) == "" { - return nil, errorsx.InvalidParam("ExecutionConfig.toolName不能为空") - } - return cfg, nil -} - -func buildToolArguments(templateArgs map[string]string, runtimeCtx RuntimeContext) (map[string]any, error) { - if len(templateArgs) == 0 { - return map[string]any{ - "query": strings.TrimSpace(runtimeCtx.UserMessage), - }, nil - } - data := map[string]any{ - "userMessage": strings.TrimSpace(runtimeCtx.UserMessage), - "conversationId": runtimeCtx.ConversationID, - "aiAgentId": runtimeCtx.AIAgentID, - "manualSkillCode": strings.TrimSpace(runtimeCtx.ManualSkillCode), - "intentCode": strings.TrimSpace(runtimeCtx.IntentCode), - } - ret := make(map[string]any, len(templateArgs)) - for key, value := range templateArgs { - rendered, err := renderTemplate(value, data) - if err != nil { - return nil, errorsx.InvalidParam("ExecutionConfig.arguments模板不合法") - } - ret[key] = rendered - } - return ret, nil -} - -func renderTemplate(raw string, data map[string]any) (string, error) { - tpl, err := template.New("skill_arg").Option("missingkey=zero").Parse(raw) - if err != nil { - return "", err - } - var buf bytes.Buffer - if err := tpl.Execute(&buf, data); err != nil { - return "", err - } - return strings.TrimSpace(buf.String()), nil -} - -func buildToolSummary(result *mcps.ToolCallResult) string { - if result == nil { - return "" - } - lines := make([]string, 0, len(result.Content)+2) - if result.StructuredContent != nil { - if data, err := json.Marshal(result.StructuredContent); err == nil { - lines = append(lines, string(data)) - } - } - for _, item := range result.Content { - if strings.TrimSpace(item.Text) != "" { - lines = append(lines, strings.TrimSpace(item.Text)) - continue - } - if item.Data != nil { - if data, err := json.Marshal(item.Data); err == nil { - lines = append(lines, string(data)) - } - } - } - return strings.TrimSpace(strings.Join(lines, "\n")) -} - -func truncateTraceText(raw string, limit int) string { - raw = strings.TrimSpace(raw) - if raw == "" || limit <= 0 { - return raw - } - runes := []rune(raw) - if len(runes) <= limit { - return raw - } - return strings.TrimSpace(string(runes[:limit])) + "..." -} diff --git a/internal/ai/skills/runtime.go b/internal/ai/skills/runtime.go index 2df0b6e..c1e9f12 100644 --- a/internal/ai/skills/runtime.go +++ b/internal/ai/skills/runtime.go @@ -77,7 +77,7 @@ func Execute(ctx context.Context, runtimeCtx RuntimeContext) (*ExecutionResult, } log := BuildRunLog(runtimeCtx, plan, trace, err) if strings.TrimSpace(replyText) != "" && strings.TrimSpace(log.MatchReason) == "" { - log.MatchReason = string(plan.Skill.ExecutionMode) + log.MatchReason = "content" } if writeErr := WriteRunLog(log); writeErr != nil && err == nil { err = writeErr diff --git a/internal/builders/skill_builder.go b/internal/builders/skill_builder.go index e82b9be..a834ee3 100644 --- a/internal/builders/skill_builder.go +++ b/internal/builders/skill_builder.go @@ -1,29 +1,38 @@ package builders import ( + "encoding/json" + "cs-agent/internal/models" "cs-agent/internal/pkg/dto/response" "cs-agent/internal/pkg/enums" ) func BuildSkillDefinitionResponse(item *models.SkillDefinition) response.SkillDefinitionResponse { + examples := make([]string, 0) + if raw := item.Examples; raw != "" { + _ = json.Unmarshal([]byte(raw), &examples) + } + allowedToolCodes := make([]string, 0) + if raw := item.AllowedToolCodes; raw != "" { + _ = json.Unmarshal([]byte(raw), &allowedToolCodes) + } return response.SkillDefinitionResponse{ - ID: item.ID, - Code: item.Code, - Name: item.Name, - Description: item.Description, - Prompt: item.Prompt, - ExecutionMode: string(item.ExecutionMode), - ExecutionModeName: enums.GetSkillExecutionModeLabel(item.ExecutionMode), - ExecutionConfig: item.ExecutionConfig, - Priority: item.Priority, - Status: int(item.Status), - StatusName: getSkillStatusName(item.Status), - Remark: item.Remark, - CreatedAt: item.CreatedAt, - UpdatedAt: item.UpdatedAt, - CreateUserName: item.CreateUserName, - UpdateUserName: item.UpdateUserName, + ID: item.ID, + Code: item.Code, + Name: item.Name, + Description: item.Description, + Content: item.Content, + Examples: examples, + AllowedToolCodes: allowedToolCodes, + Priority: item.Priority, + Status: int(item.Status), + StatusName: getSkillStatusName(item.Status), + Remark: item.Remark, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + CreateUserName: item.CreateUserName, + UpdateUserName: item.UpdateUserName, } } diff --git a/internal/controllers/console/skill_definition_controller.go b/internal/controllers/console/skill_definition_controller.go index f000a2c..6bd56d2 100644 --- a/internal/controllers/console/skill_definition_controller.go +++ b/internal/controllers/console/skill_definition_controller.go @@ -88,16 +88,19 @@ func (c *SkillDefinitionController) PostCreate() *web.JsonResult { } item := &models.SkillDefinition{ - Code: strings.TrimSpace(req.Code), - Name: strings.TrimSpace(req.Name), - Description: strings.TrimSpace(req.Description), - Prompt: strings.TrimSpace(req.Prompt), - ExecutionMode: normalizeExecutionMode(req.ExecutionMode), - ExecutionConfig: normalizeExecutionConfig(req.ExecutionConfig), - Priority: 0, - Status: enums.StatusOk, - Remark: strings.TrimSpace(req.Remark), - AuditFields: utils.BuildAuditFields(operator), + Code: strings.TrimSpace(req.Code), + Name: strings.TrimSpace(req.Name), + Description: strings.TrimSpace(req.Description), + Content: strings.TrimSpace(req.Content), + Examples: mustMarshalJSONStringArray(req.Examples), + AllowedToolCodes: mustMarshalJSONStringArray(req.AllowedToolCodes), + Priority: normalizeSkillPriority(req.Priority), + Status: enums.StatusOk, + Remark: strings.TrimSpace(req.Remark), + AuditFields: utils.BuildAuditFields(operator), + } + if item.Priority <= 0 { + item.Priority = services.SkillDefinitionService.NextPriority() } if err := services.SkillDefinitionService.Create(item); err != nil { return web.JsonError(err) @@ -132,16 +135,17 @@ func (c *SkillDefinitionController) PostUpdate() *web.JsonResult { } if err := services.SkillDefinitionService.Updates(req.ID, map[string]any{ - "code": strings.TrimSpace(req.Code), - "name": strings.TrimSpace(req.Name), - "description": strings.TrimSpace(req.Description), - "prompt": strings.TrimSpace(req.Prompt), - "execution_mode": normalizeExecutionMode(req.ExecutionMode), - "execution_config": normalizeExecutionConfig(req.ExecutionConfig), - "remark": strings.TrimSpace(req.Remark), - "update_user_id": operator.UserID, - "update_user_name": operator.Username, - "updated_at": time.Now(), + "code": strings.TrimSpace(req.Code), + "name": strings.TrimSpace(req.Name), + "description": strings.TrimSpace(req.Description), + "content": strings.TrimSpace(req.Content), + "examples": mustMarshalJSONStringArray(req.Examples), + "allowed_tool_codes": mustMarshalJSONStringArray(req.AllowedToolCodes), + "priority": resolveSkillPriorityForUpdate(req.Priority, item.Priority), + "remark": strings.TrimSpace(req.Remark), + "update_user_id": operator.UserID, + "update_user_name": operator.Username, + "updated_at": time.Now(), }); err != nil { return web.JsonError(err) } @@ -240,41 +244,65 @@ func (c *SkillDefinitionController) PostDebug_run() *web.JsonResult { func validateSkillDefinitionRequest(req request.CreateSkillDefinitionRequest) error { code := strings.TrimSpace(req.Code) name := strings.TrimSpace(req.Name) - prompt := strings.TrimSpace(req.Prompt) + content := strings.TrimSpace(req.Content) if code == "" { return errorsx.InvalidParam("Skill 编码不能为空") } if name == "" { return errorsx.InvalidParam("Skill 名称不能为空") } - mode := normalizeExecutionMode(req.ExecutionMode) - if prompt == "" { - return errorsx.InvalidParam("Prompt 不能为空") + if content == "" { + return errorsx.InvalidParam("Content 不能为空") } - switch mode { - case enums.SkillExecutionModePromptOnly: - case enums.SkillExecutionModeMCPTool: - configText := strings.TrimSpace(req.ExecutionConfig) - if configText == "" { - return errorsx.InvalidParam("MCP工具模式必须填写ExecutionConfig") - } - var payload map[string]any - if err := json.Unmarshal([]byte(configText), &payload); err != nil { - return errorsx.InvalidParam("ExecutionConfig 必须是合法JSON") - } - default: - return errorsx.InvalidParam("Skill 执行模式不合法") + if _, err := normalizeJSONStringArray(req.Examples); err != nil { + return err + } + if _, err := normalizeJSONStringArray(req.AllowedToolCodes); err != nil { + return err } return nil } -func normalizeExecutionMode(mode enums.SkillExecutionMode) enums.SkillExecutionMode { - if mode == "" { - return enums.SkillExecutionModePromptOnly +func normalizeSkillPriority(priority int) int { + if priority < 0 { + return 0 } - return mode + return priority } -func normalizeExecutionConfig(raw string) string { - return strings.TrimSpace(raw) +func resolveSkillPriorityForUpdate(input, current int) int { + input = normalizeSkillPriority(input) + if input <= 0 { + return current + } + return input +} + +func normalizeJSONStringArray(input []string) ([]string, error) { + ret := make([]string, 0, len(input)) + seen := make(map[string]struct{}) + for _, item := range input { + item = strings.TrimSpace(item) + if item == "" { + continue + } + if _, exists := seen[item]; exists { + continue + } + seen[item] = struct{}{} + ret = append(ret, item) + } + return ret, nil +} + +func mustMarshalJSONStringArray(input []string) string { + items, _ := normalizeJSONStringArray(input) + if len(items) == 0 { + return "[]" + } + buf, err := json.Marshal(items) + if err != nil { + return "[]" + } + return string(buf) } diff --git a/internal/models/models.go b/internal/models/models.go index ae17c98..c906ebc 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -906,16 +906,16 @@ type KnowledgeFeedback struct { // SkillDefinition 表示可由后台配置并参与运行时路由的 Skill 定义。 type SkillDefinition struct { - ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 主键。 - Code string `gorm:"type:varchar(100);not null;default:'';uniqueIndex"` // Code 为 Skill 的稳定唯一编码,供程序内部引用和路由判断使用,例如 refund_skill。 - Name string `gorm:"type:varchar(100);not null;default:'';index"` // Name 为 Skill 的展示名称,用于后台列表、配置页和人工选择场景。 - Description string `gorm:"type:varchar(255);not null;default:''"` // Description 为 Skill 的简要说明,用于描述该 Skill 的适用场景和职责边界。 - Prompt string `gorm:"type:longtext"` // Prompt 为 Skill 的核心提示词,在命中后注入模型上下文参与执行。 - ExecutionMode enums.SkillExecutionMode `gorm:"type:varchar(30);not null;default:'prompt_only'"` // ExecutionMode 为 Skill 执行模式。 - ExecutionConfig string `gorm:"type:text"` // ExecutionConfig 为 Skill 执行配置JSON。 - Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为 Skill 命中冲突时的优先级,数值越大优先级越高。 - Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 为 Skill 当前状态,使用全局通用状态:0启用 1禁用 2删除。 - Remark string `gorm:"type:text"` // Remark 为后台备注,用于记录配置说明、维护信息或内部协作信息。 + ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 主键。 + Code string `gorm:"type:varchar(100);not null;default:'';uniqueIndex"` // Code 为 Skill 的稳定唯一编码,供程序内部引用和路由判断使用,例如 refund_skill。 + Name string `gorm:"type:varchar(100);not null;default:'';index"` // Name 为 Skill 的展示名称,用于后台列表、配置页和人工选择场景。 + Description string `gorm:"type:varchar(255);not null;default:''"` // Description 为 Skill 的简要说明,用于描述该 Skill 的适用场景和职责边界。 + Content string `gorm:"type:longtext"` // Content 为 Skill 的主体文档,使用 Markdown 编写,供 Agent 理解任务目标、步骤和工具使用要求。 + Examples string `gorm:"type:text"` // Examples 为示例问法 JSON 数组字符串。 + AllowedToolCodes string `gorm:"type:text"` // AllowedToolCodes 为允许使用的工具编码 JSON 数组字符串。 + Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为 Skill 命中冲突时的优先级,数值越大优先级越高。 + Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 为 Skill 当前状态,使用全局通用状态:0启用 1禁用 2删除。 + Remark string `gorm:"type:text"` // Remark 为后台备注,用于记录配置说明、维护信息或内部协作信息。 AuditFields } diff --git a/internal/pkg/dto/request/skill_request.go b/internal/pkg/dto/request/skill_request.go index eeda8b2..30336db 100644 --- a/internal/pkg/dto/request/skill_request.go +++ b/internal/pkg/dto/request/skill_request.go @@ -1,7 +1,5 @@ package request -import "cs-agent/internal/pkg/enums" - type SkillDefinitionListRequest struct { Name string `json:"name"` Code string `json:"code"` @@ -9,13 +7,14 @@ type SkillDefinitionListRequest struct { } type CreateSkillDefinitionRequest struct { - Code string `json:"code"` - Name string `json:"name"` - Description string `json:"description"` - Prompt string `json:"prompt"` - ExecutionMode enums.SkillExecutionMode `json:"executionMode"` - ExecutionConfig string `json:"executionConfig"` - Remark string `json:"remark"` + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + Content string `json:"content"` + Examples []string `json:"examples"` + AllowedToolCodes []string `json:"allowedToolCodes"` + Priority int `json:"priority"` + Remark string `json:"remark"` } type UpdateSkillDefinitionRequest struct { diff --git a/internal/pkg/dto/response/skill_response.go b/internal/pkg/dto/response/skill_response.go index 10ef197..45b6a5a 100644 --- a/internal/pkg/dto/response/skill_response.go +++ b/internal/pkg/dto/response/skill_response.go @@ -3,22 +3,21 @@ package response import "time" type SkillDefinitionResponse struct { - ID int64 `json:"id"` - Code string `json:"code"` - Name string `json:"name"` - Description string `json:"description"` - Prompt string `json:"prompt"` - ExecutionMode string `json:"executionMode"` - ExecutionModeName string `json:"executionModeName"` - ExecutionConfig string `json:"executionConfig"` - Priority int `json:"priority"` - Status int `json:"status"` - StatusName string `json:"statusName"` - Remark string `json:"remark"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - CreateUserName string `json:"createUserName"` - UpdateUserName string `json:"updateUserName"` + ID int64 `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + Content string `json:"content"` + Examples []string `json:"examples"` + AllowedToolCodes []string `json:"allowedToolCodes"` + Priority int `json:"priority"` + Status int `json:"status"` + StatusName string `json:"statusName"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + CreateUserName string `json:"createUserName"` + UpdateUserName string `json:"updateUserName"` } type SkillDebugRunResponse struct {