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:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter"
|
||||
@@ -17,14 +18,16 @@ import (
|
||||
)
|
||||
|
||||
type AgentFactory struct {
|
||||
chatModelFactory *ChatModelFactory
|
||||
toolFactory *ToolFactory
|
||||
chatModelFactory *ChatModelFactory
|
||||
toolFactory *ToolFactory
|
||||
instructionAssembler *InstructionAssembler
|
||||
}
|
||||
|
||||
func NewAgentFactory() *AgentFactory {
|
||||
return &AgentFactory{
|
||||
chatModelFactory: NewChatModelFactory(),
|
||||
toolFactory: NewToolFactory(),
|
||||
chatModelFactory: NewChatModelFactory(),
|
||||
toolFactory: NewToolFactory(),
|
||||
instructionAssembler: NewInstructionAssembler(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +97,35 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD
|
||||
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
||||
`))
|
||||
}
|
||||
if len(appendixParts) == 0 {
|
||||
return baseInstruction
|
||||
projectRoot, _ := os.Getwd()
|
||||
return NewInstructionAssembler().Build(InstructionAssemblerInput{
|
||||
ProjectRoot: projectRoot,
|
||||
AgentInstruction: baseInstruction,
|
||||
SkillInstruction: firstAppendixPart(appendixParts),
|
||||
ToolAppendices: remainingAppendixParts(appendixParts),
|
||||
})
|
||||
}
|
||||
|
||||
func firstAppendixPart(parts []string) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
if baseInstruction == "" {
|
||||
return strings.Join(appendixParts, "\n\n")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
|
||||
func remainingAppendixParts(parts []string) []string {
|
||||
if len(parts) <= 1 {
|
||||
return nil
|
||||
}
|
||||
return baseInstruction + "\n\n" + strings.Join(appendixParts, "\n\n")
|
||||
ret := make([]string, 0, len(parts)-1)
|
||||
for _, item := range parts[1:] {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildSelectedSkillInstruction(skill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) string {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type InstructionAssembler struct {
|
||||
governanceInstruction string
|
||||
}
|
||||
|
||||
type InstructionAssemblerInput struct {
|
||||
AgentInstruction string
|
||||
SkillInstruction string
|
||||
ToolAppendices []string
|
||||
ProjectRoot string
|
||||
ProjectInstruction string
|
||||
}
|
||||
|
||||
var (
|
||||
projectInstructionOnce sync.Once
|
||||
projectInstructionText string
|
||||
)
|
||||
|
||||
func NewInstructionAssembler() *InstructionAssembler {
|
||||
return &InstructionAssembler{
|
||||
governanceInstruction: strings.TrimSpace(`
|
||||
你正在一个有明确工程约束的客服系统中工作。
|
||||
执行时必须严格遵守当前注入的项目规则、Agent 规则和技能规则。
|
||||
如果存在工具白名单限制,只能调用当前允许的工具;信息不足时优先追问,不要伪造事实或跳过必要确认。
|
||||
`),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InstructionAssembler) Build(input InstructionAssemblerInput) string {
|
||||
parts := make([]string, 0, 5)
|
||||
projectInstruction := strings.TrimSpace(input.ProjectInstruction)
|
||||
if projectInstruction == "" {
|
||||
projectInstruction = loadProjectInstruction(input.ProjectRoot)
|
||||
}
|
||||
if projectInstruction != "" {
|
||||
parts = append(parts, buildInstructionSection("项目级规则", projectInstruction))
|
||||
}
|
||||
if a != nil && strings.TrimSpace(a.governanceInstruction) != "" {
|
||||
parts = append(parts, buildInstructionSection("系统治理规则", a.governanceInstruction))
|
||||
}
|
||||
if agentInstruction := strings.TrimSpace(input.AgentInstruction); agentInstruction != "" {
|
||||
parts = append(parts, buildInstructionSection("Agent 规则", agentInstruction))
|
||||
}
|
||||
if skillInstruction := strings.TrimSpace(input.SkillInstruction); skillInstruction != "" {
|
||||
parts = append(parts, buildInstructionSection("当前技能上下文", skillInstruction))
|
||||
}
|
||||
if appendix := buildToolAppendix(input.ToolAppendices); appendix != "" {
|
||||
parts = append(parts, buildInstructionSection("工具补充规则", appendix))
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
||||
}
|
||||
|
||||
func buildInstructionSection(title, body string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
body = strings.TrimSpace(body)
|
||||
if body == "" {
|
||||
return ""
|
||||
}
|
||||
if title == "" {
|
||||
return body
|
||||
}
|
||||
return title + ":\n" + body
|
||||
}
|
||||
|
||||
func buildToolAppendix(input []string) string {
|
||||
if len(input) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(input))
|
||||
for _, item := range input {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, item)
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
||||
}
|
||||
|
||||
func loadProjectInstruction(projectRoot string) string {
|
||||
// TODO 这里还要读取工程目录中的AGENTS.md吗?
|
||||
projectInstructionOnce.Do(func() {
|
||||
candidates := []string{
|
||||
"AGENTS.md",
|
||||
}
|
||||
projectRoot = strings.TrimSpace(projectRoot)
|
||||
if projectRoot != "" {
|
||||
candidates = append([]string{filepath.Join(projectRoot, "AGENTS.md")}, candidates...)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(candidate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
projectInstructionText = strings.TrimSpace(string(data))
|
||||
if projectInstructionText != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return projectInstructionText
|
||||
}
|
||||
@@ -32,10 +32,14 @@ func (f *ToolFactory) BuildMCPTools(aiAgent *models.AIAgent) ([]impladapter.MCPT
|
||||
if toolCode == "" {
|
||||
toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName)
|
||||
}
|
||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||
if serverCode == "" || toolName == "" {
|
||||
continue
|
||||
}
|
||||
definition := impladapter.MCPToolDefinition{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: strings.TrimSpace(item.ServerCode),
|
||||
ToolName: strings.TrimSpace(item.ToolName),
|
||||
ServerCode: serverCode,
|
||||
ToolName: toolName,
|
||||
Title: strings.TrimSpace(item.Title),
|
||||
Description: strings.TrimSpace(item.Description),
|
||||
FixedArgs: cloneStringMap(item.Arguments),
|
||||
|
||||
@@ -21,10 +21,17 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
||||
Tools: make([]einotool.BaseTool, 0, len(r.tools)),
|
||||
ToolCodes: make(map[string]string),
|
||||
}
|
||||
allowedToolCodes := makeAllowedToolCodeSet(ctx.AllowedToolCodes)
|
||||
for _, toolDef := range r.tools {
|
||||
if toolDef == nil || !toolDef.Enabled(ctx) {
|
||||
continue
|
||||
}
|
||||
toolCode := strings.TrimSpace(toolDef.Code())
|
||||
if len(allowedToolCodes) > 0 {
|
||||
if _, ok := allowedToolCodes[toolCode]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tool, err := toolDef.Build(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -33,7 +40,6 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
||||
continue
|
||||
}
|
||||
toolName := strings.TrimSpace(toolDef.Name())
|
||||
toolCode := strings.TrimSpace(toolDef.Code())
|
||||
if toolName == "" || toolCode == "" {
|
||||
continue
|
||||
}
|
||||
@@ -42,3 +48,18 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func makeAllowedToolCodeSet(input []string) map[string]struct{} {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
ret := make(map[string]struct{}, len(input))
|
||||
for _, item := range input {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
ret[item] = struct{}{}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Conversation *models.Conversation
|
||||
AIAgent *models.AIAgent
|
||||
AIConfig *models.AIConfig
|
||||
UserMessage *models.Message
|
||||
Conversation *models.Conversation
|
||||
AIAgent *models.AIAgent
|
||||
AIConfig *models.AIConfig
|
||||
UserMessage *models.Message
|
||||
AllowedToolCodes []string
|
||||
}
|
||||
|
||||
type ToolSet struct {
|
||||
|
||||
@@ -36,6 +36,7 @@ type aiReplyTraceData struct {
|
||||
RecheckMs int64 `json:"recheckMs,omitempty"`
|
||||
CommitMs int64 `json:"commitMs,omitempty"`
|
||||
FinalAction string `json:"finalAction,omitempty"`
|
||||
ResumeSource string `json:"resumeSource,omitempty"`
|
||||
ReplySent bool `json:"replySent,omitempty"`
|
||||
ReplyMessageID int64 `json:"replyMessageId,omitempty"`
|
||||
Runtime json.RawMessage `json:"runtime,omitempty"`
|
||||
@@ -162,6 +163,7 @@ func (s *aiReplyService) resumePendingInterrupt(ctx context.Context, conversatio
|
||||
return fmt.Errorf("ai config is nil")
|
||||
}
|
||||
runtimeStartedAt := time.Now()
|
||||
trace.ResumeSource = "pending_interrupt"
|
||||
summary, err := Service.Resume(ctx, ResumeRequest{
|
||||
Conversation: &conversation,
|
||||
AIAgent: &aiAgent,
|
||||
@@ -327,9 +329,14 @@ func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message
|
||||
UserMessage: strings.TrimSpace(question),
|
||||
PlannedAction: plannedAction,
|
||||
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
|
||||
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)),
|
||||
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)),
|
||||
PlannedToolCode: plannedToolCode,
|
||||
PlanReason: planReason,
|
||||
InterruptType: firstInterruptType(summary),
|
||||
ResumeSource: runLogResumeSource(trace),
|
||||
FinalAction: toRunLogFinalAction(summary),
|
||||
FinalStatus: runLogFinalStatus(summary),
|
||||
ReplyText: buildRunLogReplyText(summary),
|
||||
ErrorMessage: errorMessage,
|
||||
LatencyMs: time.Since(startedAt).Milliseconds(),
|
||||
@@ -422,6 +429,34 @@ func summaryPlannedSkillCode(summary *Summary) string {
|
||||
return strings.TrimSpace(summary.PlannedSkillCode)
|
||||
}
|
||||
|
||||
func summaryPlannedSkillName(summary *Summary) string {
|
||||
if summary == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(summary.PlannedSkillName)
|
||||
}
|
||||
|
||||
func summarySkillRouteTrace(summary *Summary) string {
|
||||
if summary == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(summary.SkillRouteTrace)
|
||||
}
|
||||
|
||||
func runLogResumeSource(trace *aiReplyTraceData) string {
|
||||
if trace == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(trace.ResumeSource)
|
||||
}
|
||||
|
||||
func runLogFinalStatus(summary *Summary) string {
|
||||
if summary == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(summary.Status)
|
||||
}
|
||||
|
||||
func (s *aiReplyService) incrementAIReplyRounds(conversationID int64, nextRounds int, aiAgentName string) error {
|
||||
return repositories.ConversationRepository.Updates(sqls.DB(), conversationID, map[string]any{
|
||||
"ai_reply_rounds": nextRounds,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"cs-agent/internal/ai/runtime/tools"
|
||||
"cs-agent/internal/ai/skills"
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/toolx"
|
||||
)
|
||||
|
||||
var Service = newService()
|
||||
@@ -89,10 +90,11 @@ func (s *service) prepareToolsForRun(req *Request) error {
|
||||
return nil
|
||||
}
|
||||
toolSet, err := s.registry.Resolve(registry.Context{
|
||||
Conversation: req.Conversation,
|
||||
AIAgent: req.AIAgent,
|
||||
AIConfig: req.AIConfig,
|
||||
UserMessage: req.UserMessage,
|
||||
Conversation: req.Conversation,
|
||||
AIAgent: req.AIAgent,
|
||||
AIConfig: req.AIConfig,
|
||||
UserMessage: req.UserMessage,
|
||||
AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -200,3 +202,76 @@ func cloneSkillDefinition(item *models.SkillDefinition) *models.SkillDefinition
|
||||
clone := *item
|
||||
return &clone
|
||||
}
|
||||
|
||||
func parseSkillAllowedToolCodes(skill *models.SkillDefinition) []string {
|
||||
if skill == nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(skill.AllowedToolCodes)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var items []string
|
||||
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
||||
return nil
|
||||
}
|
||||
ret := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string {
|
||||
if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" {
|
||||
return nil
|
||||
}
|
||||
items, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ret := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
toolCode := strings.TrimSpace(item.ToolCode)
|
||||
if toolCode == "" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, toolCode)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string {
|
||||
agentAllowed := parseAgentAllowedToolCodes(aiAgent)
|
||||
skillAllowed := parseSkillAllowedToolCodes(skill)
|
||||
switch {
|
||||
case len(agentAllowed) == 0:
|
||||
return skillAllowed
|
||||
case len(skillAllowed) == 0:
|
||||
return agentAllowed
|
||||
default:
|
||||
skillSet := make(map[string]struct{}, len(skillAllowed))
|
||||
for _, item := range skillAllowed {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
skillSet[item] = struct{}{}
|
||||
}
|
||||
ret := make([]string, 0, len(agentAllowed))
|
||||
for _, item := range agentAllowed {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := skillSet[item]; ok {
|
||||
ret = append(ret, item)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/dto"
|
||||
"cs-agent/internal/pkg/dto/request"
|
||||
"cs-agent/internal/pkg/toolx"
|
||||
"cs-agent/internal/services"
|
||||
|
||||
componenttool "github.com/cloudwego/eino/components/tool"
|
||||
@@ -20,8 +21,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
||||
CreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||
CreateTicketConfirmToolCode = toolx.BuiltinCreateTicketConfirmToolCode
|
||||
CreateTicketConfirmToolName = toolx.BuiltinCreateTicketConfirmToolName
|
||||
)
|
||||
|
||||
type CreateTicketConfirmState struct {
|
||||
|
||||
@@ -18,9 +18,14 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse {
|
||||
UserMessage: item.UserMessage,
|
||||
PlannedAction: item.PlannedAction,
|
||||
PlannedSkillCode: item.PlannedSkillCode,
|
||||
PlannedSkillName: item.PlannedSkillName,
|
||||
SkillRouteTrace: item.SkillRouteTrace,
|
||||
PlannedToolCode: item.PlannedToolCode,
|
||||
PlanReason: item.PlanReason,
|
||||
InterruptType: item.InterruptType,
|
||||
ResumeSource: item.ResumeSource,
|
||||
FinalAction: item.FinalAction,
|
||||
FinalStatus: item.FinalStatus,
|
||||
ReplyText: item.ReplyText,
|
||||
ErrorMessage: item.ErrorMessage,
|
||||
LatencyMs: item.LatencyMs,
|
||||
|
||||
@@ -174,12 +174,29 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
||||
if toolCode == "" {
|
||||
toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName)
|
||||
}
|
||||
serverCode := strings.TrimSpace(tool.ServerCode)
|
||||
toolName := strings.TrimSpace(tool.ToolName)
|
||||
if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||
serverCode = toolx.BuiltinToolCatalogServerCode
|
||||
toolName = toolx.BuiltinCreateTicketConfirmToolName
|
||||
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
}
|
||||
title := strings.TrimSpace(tool.Title)
|
||||
if title == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||
title = toolx.BuiltinCreateTicketConfirmToolTitle
|
||||
}
|
||||
description := strings.TrimSpace(tool.Description)
|
||||
if description == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||
description = toolx.BuiltinCreateTicketConfirmToolDescription
|
||||
}
|
||||
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: strings.TrimSpace(tool.ServerCode),
|
||||
ToolName: strings.TrimSpace(tool.ToolName),
|
||||
Title: strings.TrimSpace(tool.Title),
|
||||
Description: strings.TrimSpace(tool.Description),
|
||||
ServerCode: serverCode,
|
||||
ToolName: toolName,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Arguments: tool.Arguments,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (c *MCPController) AnyCatalog() *web.JsonResult {
|
||||
ToolCode: item.ToolCode,
|
||||
ServerCode: item.ServerCode,
|
||||
ToolName: item.ToolName,
|
||||
SourceType: item.SourceType,
|
||||
Title: item.Title,
|
||||
Description: item.Description,
|
||||
InputSchema: item.InputSchema,
|
||||
|
||||
@@ -2,18 +2,13 @@ package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cs-agent/internal/builders"
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/constants"
|
||||
"cs-agent/internal/pkg/dto/request"
|
||||
"cs-agent/internal/pkg/dto/response"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
"cs-agent/internal/pkg/utils"
|
||||
"cs-agent/internal/services"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
@@ -80,29 +75,8 @@ func (c *SkillDefinitionController) PostCreate() *web.JsonResult {
|
||||
if err := params.ReadJSON(c.Ctx, &req); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
if err := validateSkillDefinitionRequest(req); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
if services.SkillDefinitionService.Take("code = ?", strings.TrimSpace(req.Code)) != nil {
|
||||
return web.JsonErrorMsg("Skill 编码已存在")
|
||||
}
|
||||
|
||||
item := &models.SkillDefinition{
|
||||
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 {
|
||||
item, err := services.SkillDefinitionService.CreateSkillDefinition(req, operator)
|
||||
if err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
return web.JsonData(builders.BuildSkillDefinitionResponse(item))
|
||||
@@ -118,35 +92,7 @@ func (c *SkillDefinitionController) PostUpdate() *web.JsonResult {
|
||||
if err := params.ReadJSON(c.Ctx, &req); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
if req.ID <= 0 {
|
||||
return web.JsonErrorMsg("Skill ID 不合法")
|
||||
}
|
||||
if err := validateSkillDefinitionRequest(req.CreateSkillDefinitionRequest); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
|
||||
item := services.SkillDefinitionService.Get(req.ID)
|
||||
if item == nil {
|
||||
return web.JsonErrorMsg("Skill 不存在")
|
||||
}
|
||||
exists := services.SkillDefinitionService.Take("code = ? AND id <> ?", strings.TrimSpace(req.Code), req.ID)
|
||||
if exists != nil {
|
||||
return web.JsonErrorMsg("Skill 编码已存在")
|
||||
}
|
||||
|
||||
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),
|
||||
"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 {
|
||||
if err := services.SkillDefinitionService.UpdateSkillDefinition(req, operator); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
return web.JsonSuccess()
|
||||
@@ -240,75 +186,3 @@ func (c *SkillDefinitionController) PostDebug_run() *web.JsonResult {
|
||||
}
|
||||
return web.JsonData(resp)
|
||||
}
|
||||
|
||||
func validateSkillDefinitionRequest(req request.CreateSkillDefinitionRequest) error {
|
||||
code := strings.TrimSpace(req.Code)
|
||||
name := strings.TrimSpace(req.Name)
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if code == "" {
|
||||
return errorsx.InvalidParam("Skill 编码不能为空")
|
||||
}
|
||||
if name == "" {
|
||||
return errorsx.InvalidParam("Skill 名称不能为空")
|
||||
}
|
||||
if content == "" {
|
||||
return errorsx.InvalidParam("Content 不能为空")
|
||||
}
|
||||
if _, err := normalizeJSONStringArray(req.Examples); err != nil {
|
||||
return err
|
||||
}
|
||||
allowedToolCodes, err := normalizeJSONStringArray(req.AllowedToolCodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, toolCode := range allowedToolCodes {
|
||||
if err := services.ToolCatalogService.ValidateMCPToolCode(toolCode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSkillPriority(priority int) int {
|
||||
if priority < 0 {
|
||||
return 0
|
||||
}
|
||||
return priority
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -950,9 +950,14 @@ type AgentRunLog struct {
|
||||
UserMessage string `gorm:"type:longtext"`
|
||||
PlannedAction string `gorm:"type:varchar(30);not null;default:'';index"`
|
||||
PlannedSkillCode string `gorm:"type:varchar(100);not null;default:'';index"`
|
||||
PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"`
|
||||
SkillRouteTrace string `gorm:"type:text"`
|
||||
PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"`
|
||||
PlanReason string `gorm:"type:varchar(500);not null;default:''"`
|
||||
InterruptType string `gorm:"type:varchar(50);not null;default:'';index"`
|
||||
ResumeSource string `gorm:"type:varchar(50);not null;default:'';index"`
|
||||
FinalAction string `gorm:"type:varchar(30);not null;default:'';index"`
|
||||
FinalStatus string `gorm:"type:varchar(30);not null;default:'';index"`
|
||||
ReplyText string `gorm:"type:longtext"`
|
||||
ErrorMessage string `gorm:"type:text"`
|
||||
LatencyMs int64 `gorm:"type:bigint;not null;default:0"`
|
||||
|
||||
@@ -69,6 +69,7 @@ type MCPToolCatalogResponse struct {
|
||||
ToolCode string `json:"toolCode"`
|
||||
ServerCode string `json:"serverCode"`
|
||||
ToolName string `json:"toolName"`
|
||||
SourceType string `json:"sourceType"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
InputSchema any `json:"inputSchema"`
|
||||
|
||||
@@ -46,9 +46,14 @@ type AgentRunLogResponse struct {
|
||||
UserMessage string `json:"userMessage"`
|
||||
PlannedAction string `json:"plannedAction"`
|
||||
PlannedSkillCode string `json:"plannedSkillCode"`
|
||||
PlannedSkillName string `json:"plannedSkillName"`
|
||||
SkillRouteTrace string `json:"skillRouteTrace"`
|
||||
PlannedToolCode string `json:"plannedToolCode"`
|
||||
PlanReason string `json:"planReason"`
|
||||
InterruptType string `json:"interruptType"`
|
||||
ResumeSource string `json:"resumeSource"`
|
||||
FinalAction string `json:"finalAction"`
|
||||
FinalStatus string `json:"finalStatus"`
|
||||
ReplyText string `json:"replyText"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
LatencyMs int64 `json:"latencyMs"`
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package toolx
|
||||
|
||||
const (
|
||||
BuiltinToolCatalogServerCode = "builtin"
|
||||
BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
||||
BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
||||
BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认"
|
||||
BuiltinCreateTicketConfirmToolDescription = "当用户明确要求创建工单,且标题和描述已经整理清楚后调用。工具会先向用户确认,确认后才真正创建工单。"
|
||||
)
|
||||
@@ -35,23 +35,32 @@ func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgen
|
||||
toolName := strings.TrimSpace(item.ToolName)
|
||||
if toolCode != "" {
|
||||
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
|
||||
if parsedServerCode == "" || parsedToolName == "" {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 格式不合法")
|
||||
if parsedServerCode != "" && parsedToolName != "" {
|
||||
if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 serverCode 不一致")
|
||||
}
|
||||
if toolName != "" && !strings.EqualFold(toolName, parsedToolName) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 toolName 不一致")
|
||||
}
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
} else {
|
||||
serverCode = ""
|
||||
toolName = ""
|
||||
}
|
||||
if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 serverCode 不一致")
|
||||
}
|
||||
if toolName != "" && !strings.EqualFold(toolName, parsedToolName) {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 toolName 不一致")
|
||||
}
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
} else {
|
||||
toolCode = BuildMCPToolCode(serverCode, toolName)
|
||||
}
|
||||
if toolCode == "" || serverCode == "" || toolName == "" {
|
||||
if toolCode == "" {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode、serverCode 和 toolName 不能为空")
|
||||
}
|
||||
if parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
}
|
||||
if serverCode == "" && toolName == "" && strings.Contains(toolCode, "/") && !strings.HasPrefix(toolCode, "builtin/") {
|
||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 格式不合法")
|
||||
}
|
||||
ret := request.AIAgentMCPToolRequest{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: serverCode,
|
||||
|
||||
@@ -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,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 服务不存在或未启用")
|
||||
|
||||
@@ -229,8 +229,8 @@ export default function DashboardAgentRunLogsPage() {
|
||||
<TableHead className="w-[180px]">时间</TableHead>
|
||||
<TableHead>用户问题</TableHead>
|
||||
<TableHead className="w-[120px]">规划动作</TableHead>
|
||||
<TableHead className="w-[180px]">Skill / Tool</TableHead>
|
||||
<TableHead className="w-[120px]">最终动作</TableHead>
|
||||
<TableHead className="w-[220px]">Skill / Tool</TableHead>
|
||||
<TableHead className="w-[140px]">最终状态</TableHead>
|
||||
<TableHead className="w-[110px] text-right">耗时</TableHead>
|
||||
<TableHead className="w-[96px] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
@@ -265,17 +265,29 @@ export default function DashboardAgentRunLogsPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{item.plannedSkillCode || item.plannedToolCode ? (
|
||||
<Badge variant="outline">
|
||||
{item.plannedSkillCode || item.plannedToolCode}
|
||||
</Badge>
|
||||
<div className="space-y-1">
|
||||
<Badge variant="outline">
|
||||
{item.plannedSkillCode || item.plannedToolCode}
|
||||
</Badge>
|
||||
{item.plannedSkillName ? (
|
||||
<div className="line-clamp-1 text-xs text-muted-foreground">
|
||||
{item.plannedSkillName}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={actionBadgeVariant(item.finalAction)}>
|
||||
{item.finalAction || "-"}
|
||||
</Badge>
|
||||
<div className="space-y-1">
|
||||
<Badge variant={actionBadgeVariant(item.finalAction)}>
|
||||
{item.finalAction || "-"}
|
||||
</Badge>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{item.finalStatus || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-sm text-muted-foreground">
|
||||
{item.latencyMs} ms
|
||||
@@ -342,14 +354,19 @@ export default function DashboardAgentRunLogsPage() {
|
||||
lines={[
|
||||
`plannedAction: ${activeLog.plannedAction || "-"}`,
|
||||
`plannedSkillCode: ${activeLog.plannedSkillCode || "-"}`,
|
||||
`plannedSkillName: ${activeLog.plannedSkillName || "-"}`,
|
||||
`plannedToolCode: ${activeLog.plannedToolCode || "-"}`,
|
||||
`planReason: ${activeLog.planReason || "-"}`,
|
||||
`skillRouteTrace: ${activeLog.skillRouteTrace || "-"}`,
|
||||
]}
|
||||
/>
|
||||
<InfoBlock
|
||||
title="执行结果"
|
||||
lines={[
|
||||
`finalAction: ${activeLog.finalAction || "-"}`,
|
||||
`finalStatus: ${activeLog.finalStatus || "-"}`,
|
||||
`interruptType: ${activeLog.interruptType || "-"}`,
|
||||
`resumeSource: ${activeLog.resumeSource || "-"}`,
|
||||
`latencyMs: ${activeLog.latencyMs} ms`,
|
||||
`createdAt: ${formatDateTime(activeLog.createdAt)}`,
|
||||
]}
|
||||
|
||||
@@ -59,6 +59,16 @@ import {
|
||||
import { getEnumOptions } from "@/lib/enums";
|
||||
import { FieldDescription } from "@base-ui/react";
|
||||
|
||||
type DirectToolItem = CreateAIAgentPayload["directTools"][number];
|
||||
|
||||
type DirectToolOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
meta: DirectToolItem;
|
||||
sourceType: string;
|
||||
groupLabel: string;
|
||||
};
|
||||
|
||||
type EditDialogProps = {
|
||||
open: boolean;
|
||||
saving: boolean;
|
||||
@@ -231,18 +241,16 @@ function EditDialogBody({
|
||||
const [knowledgeToAdd, setKnowledgeToAdd] = useState("");
|
||||
const [teamToAdd, setTeamToAdd] = useState("");
|
||||
const [skillToAdd, setSkillToAdd] = useState("");
|
||||
const [directToolServerCodeToAdd, setDirectToolServerCodeToAdd] = useState("");
|
||||
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("");
|
||||
const [directToolToAdd, setDirectToolToAdd] = useState("");
|
||||
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]);
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]);
|
||||
const [skills, setSkills] = useState<SkillDefinition[]>([]);
|
||||
const [directTools, setDirectTools] = useState<
|
||||
CreateAIAgentPayload["directTools"]
|
||||
>([]);
|
||||
const [directToolOptions, setDirectToolOptions] = useState<
|
||||
{ value: string; label: string; meta: CreateAIAgentPayload["directTools"][number] }[]
|
||||
>([]);
|
||||
const [directTools, setDirectTools] = useState<DirectToolItem[]>([]);
|
||||
const [directToolOptions, setDirectToolOptions] = useState<DirectToolOption[]>(
|
||||
[],
|
||||
);
|
||||
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -256,7 +264,7 @@ function EditDialogBody({
|
||||
setKnowledgeToAdd("");
|
||||
setTeamToAdd("");
|
||||
setSkillToAdd("");
|
||||
setDirectToolServerCodeToAdd("");
|
||||
setDirectToolGroupToAdd("");
|
||||
setDirectToolToAdd("");
|
||||
return;
|
||||
}
|
||||
@@ -271,7 +279,7 @@ function EditDialogBody({
|
||||
setKnowledgeToAdd("");
|
||||
setTeamToAdd("");
|
||||
setSkillToAdd("");
|
||||
setDirectToolServerCodeToAdd("");
|
||||
setDirectToolGroupToAdd("");
|
||||
setDirectToolToAdd("");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
@@ -349,6 +357,11 @@ function EditDialogBody({
|
||||
catalog.map((tool) => ({
|
||||
value: tool.toolCode,
|
||||
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
|
||||
sourceType: tool.sourceType,
|
||||
groupLabel:
|
||||
tool.sourceType === "builtin"
|
||||
? "内置工具"
|
||||
: tool.serverCode,
|
||||
meta: {
|
||||
toolCode: tool.toolCode,
|
||||
serverCode: tool.serverCode,
|
||||
@@ -446,21 +459,21 @@ function EditDialogBody({
|
||||
() =>
|
||||
directToolOptions.filter(
|
||||
(option) =>
|
||||
option.meta.serverCode === directToolServerCodeToAdd &&
|
||||
option.groupLabel === directToolGroupToAdd &&
|
||||
!directTools.some((tool) => tool.toolCode === option.value),
|
||||
),
|
||||
[directToolOptions, directToolServerCodeToAdd, directTools],
|
||||
[directToolOptions, directToolGroupToAdd, directTools],
|
||||
);
|
||||
|
||||
const directToolServerOptions = useMemo(
|
||||
const directToolGroupOptions = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Map(
|
||||
directToolOptions.map((option) => [
|
||||
option.meta.serverCode,
|
||||
option.groupLabel,
|
||||
{
|
||||
value: option.meta.serverCode,
|
||||
label: option.meta.serverCode,
|
||||
value: option.groupLabel,
|
||||
label: option.groupLabel,
|
||||
},
|
||||
]),
|
||||
).values(),
|
||||
@@ -468,15 +481,16 @@ function EditDialogBody({
|
||||
[directToolOptions],
|
||||
);
|
||||
|
||||
const directToolsGroupedByServer = useMemo(() => {
|
||||
const groups = new Map<
|
||||
string,
|
||||
CreateAIAgentPayload["directTools"]
|
||||
>();
|
||||
const directToolsGrouped = useMemo(() => {
|
||||
const groups = new Map<string, DirectToolItem[]>();
|
||||
for (const tool of directTools) {
|
||||
const current = groups.get(tool.serverCode) ?? [];
|
||||
const groupLabel =
|
||||
tool.serverCode === "builtin" || tool.toolCode.startsWith("builtin/")
|
||||
? "内置工具"
|
||||
: tool.serverCode || "未分组";
|
||||
const current = groups.get(groupLabel) ?? [];
|
||||
current.push(tool);
|
||||
groups.set(tool.serverCode, current);
|
||||
groups.set(groupLabel, current);
|
||||
}
|
||||
return Array.from(groups.entries());
|
||||
}, [directTools]);
|
||||
@@ -585,7 +599,7 @@ function EditDialogBody({
|
||||
}
|
||||
return [...prev, option.meta];
|
||||
});
|
||||
setDirectToolServerCodeToAdd(option.meta.serverCode);
|
||||
setDirectToolGroupToAdd(option.groupLabel);
|
||||
setDirectToolToAdd("");
|
||||
}
|
||||
|
||||
@@ -852,22 +866,22 @@ function EditDialogBody({
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-muted/10 p-4">
|
||||
<div className="mb-1 text-sm font-medium">Direct MCP Tools</div>
|
||||
<div className="mb-1 text-sm font-medium">Direct Tools</div>
|
||||
<div className="mb-4 text-xs text-muted-foreground">
|
||||
用于低风险、原子化的实时查询。工具来自统一的 MCP Tool Catalog。
|
||||
用于低风险、原子化的实时查询。可选择 MCP 工具,也可选择系统内置工具。
|
||||
</div>
|
||||
<Field>
|
||||
<FieldContent className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-52">
|
||||
<OptionCombobox
|
||||
value={directToolServerCodeToAdd}
|
||||
options={directToolServerOptions}
|
||||
placeholder="选择 MCP Server"
|
||||
searchPlaceholder="搜索 MCP Server"
|
||||
emptyText="没有可用的 MCP Server"
|
||||
value={directToolGroupToAdd}
|
||||
options={directToolGroupOptions}
|
||||
placeholder="选择工具分组"
|
||||
searchPlaceholder="搜索工具分组"
|
||||
emptyText="没有可用的工具分组"
|
||||
onChange={(value) => {
|
||||
setDirectToolServerCodeToAdd(value);
|
||||
setDirectToolGroupToAdd(value);
|
||||
setDirectToolToAdd("");
|
||||
}}
|
||||
/>
|
||||
@@ -876,7 +890,7 @@ function EditDialogBody({
|
||||
<OptionCombobox
|
||||
value={directToolToAdd}
|
||||
options={addableDirectToolOptions}
|
||||
placeholder="选择该 Server 下的 Direct Tool"
|
||||
placeholder="选择 Direct Tool"
|
||||
searchPlaceholder="搜索 Direct Tool"
|
||||
emptyText="没有可添加的 Direct Tool"
|
||||
onChange={handleAddDirectTool}
|
||||
@@ -886,7 +900,7 @@ function EditDialogBody({
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={
|
||||
!directToolServerCodeToAdd || !directToolToAdd
|
||||
!directToolGroupToAdd || !directToolToAdd
|
||||
}
|
||||
onClick={() => handleAddDirectTool(directToolToAdd)}
|
||||
>
|
||||
@@ -897,16 +911,16 @@ function EditDialogBody({
|
||||
<div className="space-y-3">
|
||||
{directTools.length === 0 ? (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
不配置 Direct Tool 时,Agent 不会直接访问 MCP,只能通过 Skill 间接调用。
|
||||
不配置 Direct Tool 时,Agent 不会直接调用外部或内置工具,只会依赖知识库、Skill 和普通回复。
|
||||
</span>
|
||||
) : (
|
||||
directToolsGroupedByServer.map(([serverCode, tools]) => (
|
||||
directToolsGrouped.map(([groupLabel, tools]) => (
|
||||
<div
|
||||
key={serverCode}
|
||||
key={groupLabel}
|
||||
className="rounded-md border p-3"
|
||||
>
|
||||
<div className="mb-2 text-xs font-medium text-muted-foreground">
|
||||
{serverCode}
|
||||
{groupLabel}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tools.map((tool) => {
|
||||
@@ -921,6 +935,11 @@ function EditDialogBody({
|
||||
className="gap-1 pr-1"
|
||||
>
|
||||
{tool.title || catalogItem?.title || value}
|
||||
<span className="text-[10px] text-muted-foreground/80">
|
||||
{catalogItem?.sourceType === "builtin"
|
||||
? "内置"
|
||||
: tool.serverCode || "MCP"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -333,6 +333,7 @@ export type MCPToolCatalogItem = {
|
||||
toolCode: string
|
||||
serverCode: string
|
||||
toolName: string
|
||||
sourceType: string
|
||||
title: string
|
||||
description: string
|
||||
inputSchema: unknown
|
||||
@@ -362,9 +363,14 @@ export type AgentRunLog = {
|
||||
userMessage: string
|
||||
plannedAction: string
|
||||
plannedSkillCode: string
|
||||
plannedSkillName: string
|
||||
skillRouteTrace: string
|
||||
plannedToolCode: string
|
||||
planReason: string
|
||||
interruptType: string
|
||||
resumeSource: string
|
||||
finalAction: string
|
||||
finalStatus: string
|
||||
replyText: string
|
||||
errorMessage: string
|
||||
latencyMs: number
|
||||
|
||||
Reference in New Issue
Block a user