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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter"
|
einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter"
|
||||||
@@ -17,14 +18,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AgentFactory struct {
|
type AgentFactory struct {
|
||||||
chatModelFactory *ChatModelFactory
|
chatModelFactory *ChatModelFactory
|
||||||
toolFactory *ToolFactory
|
toolFactory *ToolFactory
|
||||||
|
instructionAssembler *InstructionAssembler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentFactory() *AgentFactory {
|
func NewAgentFactory() *AgentFactory {
|
||||||
return &AgentFactory{
|
return &AgentFactory{
|
||||||
chatModelFactory: NewChatModelFactory(),
|
chatModelFactory: NewChatModelFactory(),
|
||||||
toolFactory: NewToolFactory(),
|
toolFactory: NewToolFactory(),
|
||||||
|
instructionAssembler: NewInstructionAssembler(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +97,35 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD
|
|||||||
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
if len(appendixParts) == 0 {
|
projectRoot, _ := os.Getwd()
|
||||||
return baseInstruction
|
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.TrimSpace(parts[0])
|
||||||
return strings.Join(appendixParts, "\n\n")
|
}
|
||||||
|
|
||||||
|
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 {
|
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 == "" {
|
if toolCode == "" {
|
||||||
toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName)
|
toolCode = toolx.BuildMCPToolCode(item.ServerCode, item.ToolName)
|
||||||
}
|
}
|
||||||
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
|
if serverCode == "" || toolName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
definition := impladapter.MCPToolDefinition{
|
definition := impladapter.MCPToolDefinition{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
ServerCode: strings.TrimSpace(item.ServerCode),
|
ServerCode: serverCode,
|
||||||
ToolName: strings.TrimSpace(item.ToolName),
|
ToolName: toolName,
|
||||||
Title: strings.TrimSpace(item.Title),
|
Title: strings.TrimSpace(item.Title),
|
||||||
Description: strings.TrimSpace(item.Description),
|
Description: strings.TrimSpace(item.Description),
|
||||||
FixedArgs: cloneStringMap(item.Arguments),
|
FixedArgs: cloneStringMap(item.Arguments),
|
||||||
|
|||||||
@@ -21,10 +21,17 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
|||||||
Tools: make([]einotool.BaseTool, 0, len(r.tools)),
|
Tools: make([]einotool.BaseTool, 0, len(r.tools)),
|
||||||
ToolCodes: make(map[string]string),
|
ToolCodes: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
allowedToolCodes := makeAllowedToolCodeSet(ctx.AllowedToolCodes)
|
||||||
for _, toolDef := range r.tools {
|
for _, toolDef := range r.tools {
|
||||||
if toolDef == nil || !toolDef.Enabled(ctx) {
|
if toolDef == nil || !toolDef.Enabled(ctx) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
toolCode := strings.TrimSpace(toolDef.Code())
|
||||||
|
if len(allowedToolCodes) > 0 {
|
||||||
|
if _, ok := allowedToolCodes[toolCode]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
tool, err := toolDef.Build(ctx)
|
tool, err := toolDef.Build(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -33,7 +40,6 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
toolName := strings.TrimSpace(toolDef.Name())
|
toolName := strings.TrimSpace(toolDef.Name())
|
||||||
toolCode := strings.TrimSpace(toolDef.Code())
|
|
||||||
if toolName == "" || toolCode == "" {
|
if toolName == "" || toolCode == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -42,3 +48,18 @@ func (r *Registry) Resolve(ctx Context) (*ToolSet, error) {
|
|||||||
}
|
}
|
||||||
return ret, nil
|
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 {
|
type Context struct {
|
||||||
Conversation *models.Conversation
|
Conversation *models.Conversation
|
||||||
AIAgent *models.AIAgent
|
AIAgent *models.AIAgent
|
||||||
AIConfig *models.AIConfig
|
AIConfig *models.AIConfig
|
||||||
UserMessage *models.Message
|
UserMessage *models.Message
|
||||||
|
AllowedToolCodes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolSet struct {
|
type ToolSet struct {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type aiReplyTraceData struct {
|
|||||||
RecheckMs int64 `json:"recheckMs,omitempty"`
|
RecheckMs int64 `json:"recheckMs,omitempty"`
|
||||||
CommitMs int64 `json:"commitMs,omitempty"`
|
CommitMs int64 `json:"commitMs,omitempty"`
|
||||||
FinalAction string `json:"finalAction,omitempty"`
|
FinalAction string `json:"finalAction,omitempty"`
|
||||||
|
ResumeSource string `json:"resumeSource,omitempty"`
|
||||||
ReplySent bool `json:"replySent,omitempty"`
|
ReplySent bool `json:"replySent,omitempty"`
|
||||||
ReplyMessageID int64 `json:"replyMessageId,omitempty"`
|
ReplyMessageID int64 `json:"replyMessageId,omitempty"`
|
||||||
Runtime json.RawMessage `json:"runtime,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")
|
return fmt.Errorf("ai config is nil")
|
||||||
}
|
}
|
||||||
runtimeStartedAt := time.Now()
|
runtimeStartedAt := time.Now()
|
||||||
|
trace.ResumeSource = "pending_interrupt"
|
||||||
summary, err := Service.Resume(ctx, ResumeRequest{
|
summary, err := Service.Resume(ctx, ResumeRequest{
|
||||||
Conversation: &conversation,
|
Conversation: &conversation,
|
||||||
AIAgent: &aiAgent,
|
AIAgent: &aiAgent,
|
||||||
@@ -327,9 +329,14 @@ func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message
|
|||||||
UserMessage: strings.TrimSpace(question),
|
UserMessage: strings.TrimSpace(question),
|
||||||
PlannedAction: plannedAction,
|
PlannedAction: plannedAction,
|
||||||
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
|
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
|
||||||
|
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)),
|
||||||
|
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)),
|
||||||
PlannedToolCode: plannedToolCode,
|
PlannedToolCode: plannedToolCode,
|
||||||
PlanReason: planReason,
|
PlanReason: planReason,
|
||||||
|
InterruptType: firstInterruptType(summary),
|
||||||
|
ResumeSource: runLogResumeSource(trace),
|
||||||
FinalAction: toRunLogFinalAction(summary),
|
FinalAction: toRunLogFinalAction(summary),
|
||||||
|
FinalStatus: runLogFinalStatus(summary),
|
||||||
ReplyText: buildRunLogReplyText(summary),
|
ReplyText: buildRunLogReplyText(summary),
|
||||||
ErrorMessage: errorMessage,
|
ErrorMessage: errorMessage,
|
||||||
LatencyMs: time.Since(startedAt).Milliseconds(),
|
LatencyMs: time.Since(startedAt).Milliseconds(),
|
||||||
@@ -422,6 +429,34 @@ func summaryPlannedSkillCode(summary *Summary) string {
|
|||||||
return strings.TrimSpace(summary.PlannedSkillCode)
|
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 {
|
func (s *aiReplyService) incrementAIReplyRounds(conversationID int64, nextRounds int, aiAgentName string) error {
|
||||||
return repositories.ConversationRepository.Updates(sqls.DB(), conversationID, map[string]any{
|
return repositories.ConversationRepository.Updates(sqls.DB(), conversationID, map[string]any{
|
||||||
"ai_reply_rounds": nextRounds,
|
"ai_reply_rounds": nextRounds,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"cs-agent/internal/ai/runtime/tools"
|
"cs-agent/internal/ai/runtime/tools"
|
||||||
"cs-agent/internal/ai/skills"
|
"cs-agent/internal/ai/skills"
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Service = newService()
|
var Service = newService()
|
||||||
@@ -89,10 +90,11 @@ func (s *service) prepareToolsForRun(req *Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
toolSet, err := s.registry.Resolve(registry.Context{
|
toolSet, err := s.registry.Resolve(registry.Context{
|
||||||
Conversation: req.Conversation,
|
Conversation: req.Conversation,
|
||||||
AIAgent: req.AIAgent,
|
AIAgent: req.AIAgent,
|
||||||
AIConfig: req.AIConfig,
|
AIConfig: req.AIConfig,
|
||||||
UserMessage: req.UserMessage,
|
UserMessage: req.UserMessage,
|
||||||
|
AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -200,3 +202,76 @@ func cloneSkillDefinition(item *models.SkillDefinition) *models.SkillDefinition
|
|||||||
clone := *item
|
clone := *item
|
||||||
return &clone
|
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/models"
|
||||||
"cs-agent/internal/pkg/dto"
|
"cs-agent/internal/pkg/dto"
|
||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
|
"cs-agent/internal/pkg/toolx"
|
||||||
"cs-agent/internal/services"
|
"cs-agent/internal/services"
|
||||||
|
|
||||||
componenttool "github.com/cloudwego/eino/components/tool"
|
componenttool "github.com/cloudwego/eino/components/tool"
|
||||||
@@ -20,8 +21,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation"
|
CreateTicketConfirmToolCode = toolx.BuiltinCreateTicketConfirmToolCode
|
||||||
CreateTicketConfirmToolName = "create_ticket_with_confirmation"
|
CreateTicketConfirmToolName = toolx.BuiltinCreateTicketConfirmToolName
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateTicketConfirmState struct {
|
type CreateTicketConfirmState struct {
|
||||||
|
|||||||
@@ -18,9 +18,14 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse {
|
|||||||
UserMessage: item.UserMessage,
|
UserMessage: item.UserMessage,
|
||||||
PlannedAction: item.PlannedAction,
|
PlannedAction: item.PlannedAction,
|
||||||
PlannedSkillCode: item.PlannedSkillCode,
|
PlannedSkillCode: item.PlannedSkillCode,
|
||||||
|
PlannedSkillName: item.PlannedSkillName,
|
||||||
|
SkillRouteTrace: item.SkillRouteTrace,
|
||||||
PlannedToolCode: item.PlannedToolCode,
|
PlannedToolCode: item.PlannedToolCode,
|
||||||
PlanReason: item.PlanReason,
|
PlanReason: item.PlanReason,
|
||||||
|
InterruptType: item.InterruptType,
|
||||||
|
ResumeSource: item.ResumeSource,
|
||||||
FinalAction: item.FinalAction,
|
FinalAction: item.FinalAction,
|
||||||
|
FinalStatus: item.FinalStatus,
|
||||||
ReplyText: item.ReplyText,
|
ReplyText: item.ReplyText,
|
||||||
ErrorMessage: item.ErrorMessage,
|
ErrorMessage: item.ErrorMessage,
|
||||||
LatencyMs: item.LatencyMs,
|
LatencyMs: item.LatencyMs,
|
||||||
|
|||||||
@@ -174,12 +174,29 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
|||||||
if toolCode == "" {
|
if toolCode == "" {
|
||||||
toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName)
|
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{
|
ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
ServerCode: strings.TrimSpace(tool.ServerCode),
|
ServerCode: serverCode,
|
||||||
ToolName: strings.TrimSpace(tool.ToolName),
|
ToolName: toolName,
|
||||||
Title: strings.TrimSpace(tool.Title),
|
Title: title,
|
||||||
Description: strings.TrimSpace(tool.Description),
|
Description: description,
|
||||||
Arguments: tool.Arguments,
|
Arguments: tool.Arguments,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func (c *MCPController) AnyCatalog() *web.JsonResult {
|
|||||||
ToolCode: item.ToolCode,
|
ToolCode: item.ToolCode,
|
||||||
ServerCode: item.ServerCode,
|
ServerCode: item.ServerCode,
|
||||||
ToolName: item.ToolName,
|
ToolName: item.ToolName,
|
||||||
|
SourceType: item.SourceType,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
InputSchema: item.InputSchema,
|
InputSchema: item.InputSchema,
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ package console
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cs-agent/internal/builders"
|
"cs-agent/internal/builders"
|
||||||
"cs-agent/internal/models"
|
|
||||||
"cs-agent/internal/pkg/constants"
|
"cs-agent/internal/pkg/constants"
|
||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
"cs-agent/internal/pkg/dto/response"
|
"cs-agent/internal/pkg/dto/response"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
"cs-agent/internal/pkg/errorsx"
|
|
||||||
"cs-agent/internal/pkg/utils"
|
|
||||||
"cs-agent/internal/services"
|
"cs-agent/internal/services"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
@@ -80,29 +75,8 @@ func (c *SkillDefinitionController) PostCreate() *web.JsonResult {
|
|||||||
if err := params.ReadJSON(c.Ctx, &req); err != nil {
|
if err := params.ReadJSON(c.Ctx, &req); err != nil {
|
||||||
return web.JsonError(err)
|
return web.JsonError(err)
|
||||||
}
|
}
|
||||||
if err := validateSkillDefinitionRequest(req); err != nil {
|
item, err := services.SkillDefinitionService.CreateSkillDefinition(req, operator)
|
||||||
return web.JsonError(err)
|
if err != nil {
|
||||||
}
|
|
||||||
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 {
|
|
||||||
return web.JsonError(err)
|
return web.JsonError(err)
|
||||||
}
|
}
|
||||||
return web.JsonData(builders.BuildSkillDefinitionResponse(item))
|
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 {
|
if err := params.ReadJSON(c.Ctx, &req); err != nil {
|
||||||
return web.JsonError(err)
|
return web.JsonError(err)
|
||||||
}
|
}
|
||||||
if req.ID <= 0 {
|
if err := services.SkillDefinitionService.UpdateSkillDefinition(req, operator); err != nil {
|
||||||
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 {
|
|
||||||
return web.JsonError(err)
|
return web.JsonError(err)
|
||||||
}
|
}
|
||||||
return web.JsonSuccess()
|
return web.JsonSuccess()
|
||||||
@@ -240,75 +186,3 @@ func (c *SkillDefinitionController) PostDebug_run() *web.JsonResult {
|
|||||||
}
|
}
|
||||||
return web.JsonData(resp)
|
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"`
|
UserMessage string `gorm:"type:longtext"`
|
||||||
PlannedAction string `gorm:"type:varchar(30);not null;default:'';index"`
|
PlannedAction string `gorm:"type:varchar(30);not null;default:'';index"`
|
||||||
PlannedSkillCode string `gorm:"type:varchar(100);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"`
|
PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"`
|
||||||
PlanReason string `gorm:"type:varchar(500);not null;default:''"`
|
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"`
|
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"`
|
ReplyText string `gorm:"type:longtext"`
|
||||||
ErrorMessage string `gorm:"type:text"`
|
ErrorMessage string `gorm:"type:text"`
|
||||||
LatencyMs int64 `gorm:"type:bigint;not null;default:0"`
|
LatencyMs int64 `gorm:"type:bigint;not null;default:0"`
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type MCPToolCatalogResponse struct {
|
|||||||
ToolCode string `json:"toolCode"`
|
ToolCode string `json:"toolCode"`
|
||||||
ServerCode string `json:"serverCode"`
|
ServerCode string `json:"serverCode"`
|
||||||
ToolName string `json:"toolName"`
|
ToolName string `json:"toolName"`
|
||||||
|
SourceType string `json:"sourceType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
InputSchema any `json:"inputSchema"`
|
InputSchema any `json:"inputSchema"`
|
||||||
|
|||||||
@@ -46,9 +46,14 @@ type AgentRunLogResponse struct {
|
|||||||
UserMessage string `json:"userMessage"`
|
UserMessage string `json:"userMessage"`
|
||||||
PlannedAction string `json:"plannedAction"`
|
PlannedAction string `json:"plannedAction"`
|
||||||
PlannedSkillCode string `json:"plannedSkillCode"`
|
PlannedSkillCode string `json:"plannedSkillCode"`
|
||||||
|
PlannedSkillName string `json:"plannedSkillName"`
|
||||||
|
SkillRouteTrace string `json:"skillRouteTrace"`
|
||||||
PlannedToolCode string `json:"plannedToolCode"`
|
PlannedToolCode string `json:"plannedToolCode"`
|
||||||
PlanReason string `json:"planReason"`
|
PlanReason string `json:"planReason"`
|
||||||
|
InterruptType string `json:"interruptType"`
|
||||||
|
ResumeSource string `json:"resumeSource"`
|
||||||
FinalAction string `json:"finalAction"`
|
FinalAction string `json:"finalAction"`
|
||||||
|
FinalStatus string `json:"finalStatus"`
|
||||||
ReplyText string `json:"replyText"`
|
ReplyText string `json:"replyText"`
|
||||||
ErrorMessage string `json:"errorMessage"`
|
ErrorMessage string `json:"errorMessage"`
|
||||||
LatencyMs int64 `json:"latencyMs"`
|
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)
|
toolName := strings.TrimSpace(item.ToolName)
|
||||||
if toolCode != "" {
|
if toolCode != "" {
|
||||||
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
|
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
|
||||||
if parsedServerCode == "" || parsedToolName == "" {
|
if parsedServerCode != "" && parsedToolName != "" {
|
||||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 格式不合法")
|
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 {
|
} else {
|
||||||
toolCode = BuildMCPToolCode(serverCode, toolName)
|
toolCode = BuildMCPToolCode(serverCode, toolName)
|
||||||
}
|
}
|
||||||
if toolCode == "" || serverCode == "" || toolName == "" {
|
if toolCode == "" {
|
||||||
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode、serverCode 和 toolName 不能为空")
|
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{
|
ret := request.AIAgentMCPToolRequest{
|
||||||
ToolCode: toolCode,
|
ToolCode: toolCode,
|
||||||
ServerCode: serverCode,
|
ServerCode: serverCode,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/config"
|
|
||||||
"cs-agent/internal/pkg/dto"
|
"cs-agent/internal/pkg/dto"
|
||||||
"cs-agent/internal/pkg/dto/request"
|
"cs-agent/internal/pkg/dto/request"
|
||||||
"cs-agent/internal/pkg/enums"
|
"cs-agent/internal/pkg/enums"
|
||||||
@@ -288,10 +287,6 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
|
|||||||
if len(input) == 0 {
|
if len(input) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
cfg := config.Current()
|
|
||||||
if !cfg.MCP.Enabled {
|
|
||||||
return nil, errorsx.InvalidParam("系统未启用 MCP,不能配置 Direct Tool")
|
|
||||||
}
|
|
||||||
ret := make([]request.AIAgentMCPToolRequest, 0, len(input))
|
ret := make([]request.AIAgentMCPToolRequest, 0, len(input))
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
for _, item := range input {
|
for _, item := range input {
|
||||||
@@ -299,10 +294,8 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
serverCode := strings.TrimSpace(normalized.ServerCode)
|
if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil {
|
||||||
server, ok := cfg.MCP.Servers[serverCode]
|
return nil, err
|
||||||
if !ok || !server.Enabled {
|
|
||||||
return nil, errorsx.InvalidParam("Direct Tool 绑定的 MCP 服务不存在或未启用")
|
|
||||||
}
|
}
|
||||||
key := strings.TrimSpace(normalized.ToolCode)
|
key := strings.TrimSpace(normalized.ToolCode)
|
||||||
if _, exists := seen[key]; exists {
|
if _, exists := seen[key]; exists {
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
"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"
|
"cs-agent/internal/repositories"
|
||||||
|
|
||||||
"github.com/mlogclub/simple/sqls"
|
"github.com/mlogclub/simple/sqls"
|
||||||
@@ -86,3 +95,156 @@ func (s *skillDefinitionService) UpdatePriority(ids []int64) error {
|
|||||||
func (s *skillDefinitionService) GetByCode(code string) *models.SkillDefinition {
|
func (s *skillDefinitionService) GetByCode(code string) *models.SkillDefinition {
|
||||||
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), code)
|
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
|
ToolCode string
|
||||||
ServerCode string
|
ServerCode string
|
||||||
ToolName string
|
ToolName string
|
||||||
|
SourceType string
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
InputSchema any
|
InputSchema any
|
||||||
@@ -46,6 +47,14 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
}
|
}
|
||||||
slices.Sort(serverCodes)
|
slices.Sort(serverCodes)
|
||||||
ret := make([]MCPToolCatalogItem, 0)
|
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 {
|
for _, serverCode := range serverCodes {
|
||||||
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
tools, err := mcps.Runtime.ListTools(ctx, serverCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,6 +65,7 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
ToolCode: toolx.BuildMCPToolCode(serverCode, item.Name),
|
ToolCode: toolx.BuildMCPToolCode(serverCode, item.Name),
|
||||||
ServerCode: serverCode,
|
ServerCode: serverCode,
|
||||||
ToolName: strings.TrimSpace(item.Name),
|
ToolName: strings.TrimSpace(item.Name),
|
||||||
|
SourceType: "mcp",
|
||||||
Title: strings.TrimSpace(item.Title),
|
Title: strings.TrimSpace(item.Title),
|
||||||
Description: strings.TrimSpace(item.Description),
|
Description: strings.TrimSpace(item.Description),
|
||||||
InputSchema: item.InputSchema,
|
InputSchema: item.InputSchema,
|
||||||
@@ -67,14 +77,25 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *toolCatalogService) ValidateMCPToolCode(toolCode string) error {
|
func (s *toolCatalogService) ValidateMCPToolCode(toolCode string) error {
|
||||||
|
return s.ValidateToolCode(toolCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *toolCatalogService) ValidateToolCode(toolCode string) error {
|
||||||
cfg := config.Current()
|
cfg := config.Current()
|
||||||
if !cfg.MCP.Enabled {
|
toolCode = strings.TrimSpace(toolCode)
|
||||||
return errorsx.InvalidParam("MCP未启用")
|
if toolCode == "" {
|
||||||
|
return errorsx.InvalidParam("toolCode不能为空")
|
||||||
|
}
|
||||||
|
if toolCode == toolx.BuiltinCreateTicketConfirmToolCode {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
serverCode, toolName := toolx.SplitMCPToolCode(toolCode)
|
||||||
if serverCode == "" || toolName == "" {
|
if serverCode == "" || toolName == "" {
|
||||||
return errorsx.InvalidParam("toolCode格式不合法")
|
return errorsx.InvalidParam("toolCode格式不合法")
|
||||||
}
|
}
|
||||||
|
if !cfg.MCP.Enabled {
|
||||||
|
return errorsx.InvalidParam("MCP未启用")
|
||||||
|
}
|
||||||
server, ok := cfg.MCP.Servers[serverCode]
|
server, ok := cfg.MCP.Servers[serverCode]
|
||||||
if !ok || !server.Enabled {
|
if !ok || !server.Enabled {
|
||||||
return errorsx.InvalidParam("toolCode 绑定的 MCP 服务不存在或未启用")
|
return errorsx.InvalidParam("toolCode 绑定的 MCP 服务不存在或未启用")
|
||||||
|
|||||||
@@ -229,8 +229,8 @@ export default function DashboardAgentRunLogsPage() {
|
|||||||
<TableHead className="w-[180px]">时间</TableHead>
|
<TableHead className="w-[180px]">时间</TableHead>
|
||||||
<TableHead>用户问题</TableHead>
|
<TableHead>用户问题</TableHead>
|
||||||
<TableHead className="w-[120px]">规划动作</TableHead>
|
<TableHead className="w-[120px]">规划动作</TableHead>
|
||||||
<TableHead className="w-[180px]">Skill / Tool</TableHead>
|
<TableHead className="w-[220px]">Skill / Tool</TableHead>
|
||||||
<TableHead className="w-[120px]">最终动作</TableHead>
|
<TableHead className="w-[140px]">最终状态</TableHead>
|
||||||
<TableHead className="w-[110px] text-right">耗时</TableHead>
|
<TableHead className="w-[110px] text-right">耗时</TableHead>
|
||||||
<TableHead className="w-[96px] text-right">操作</TableHead>
|
<TableHead className="w-[96px] text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -265,17 +265,29 @@ export default function DashboardAgentRunLogsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
{item.plannedSkillCode || item.plannedToolCode ? (
|
{item.plannedSkillCode || item.plannedToolCode ? (
|
||||||
<Badge variant="outline">
|
<div className="space-y-1">
|
||||||
{item.plannedSkillCode || item.plannedToolCode}
|
<Badge variant="outline">
|
||||||
</Badge>
|
{item.plannedSkillCode || item.plannedToolCode}
|
||||||
|
</Badge>
|
||||||
|
{item.plannedSkillName ? (
|
||||||
|
<div className="line-clamp-1 text-xs text-muted-foreground">
|
||||||
|
{item.plannedSkillName}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={actionBadgeVariant(item.finalAction)}>
|
<div className="space-y-1">
|
||||||
{item.finalAction || "-"}
|
<Badge variant={actionBadgeVariant(item.finalAction)}>
|
||||||
</Badge>
|
{item.finalAction || "-"}
|
||||||
|
</Badge>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{item.finalStatus || "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-sm text-muted-foreground">
|
<TableCell className="text-right text-sm text-muted-foreground">
|
||||||
{item.latencyMs} ms
|
{item.latencyMs} ms
|
||||||
@@ -342,14 +354,19 @@ export default function DashboardAgentRunLogsPage() {
|
|||||||
lines={[
|
lines={[
|
||||||
`plannedAction: ${activeLog.plannedAction || "-"}`,
|
`plannedAction: ${activeLog.plannedAction || "-"}`,
|
||||||
`plannedSkillCode: ${activeLog.plannedSkillCode || "-"}`,
|
`plannedSkillCode: ${activeLog.plannedSkillCode || "-"}`,
|
||||||
|
`plannedSkillName: ${activeLog.plannedSkillName || "-"}`,
|
||||||
`plannedToolCode: ${activeLog.plannedToolCode || "-"}`,
|
`plannedToolCode: ${activeLog.plannedToolCode || "-"}`,
|
||||||
`planReason: ${activeLog.planReason || "-"}`,
|
`planReason: ${activeLog.planReason || "-"}`,
|
||||||
|
`skillRouteTrace: ${activeLog.skillRouteTrace || "-"}`,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<InfoBlock
|
<InfoBlock
|
||||||
title="执行结果"
|
title="执行结果"
|
||||||
lines={[
|
lines={[
|
||||||
`finalAction: ${activeLog.finalAction || "-"}`,
|
`finalAction: ${activeLog.finalAction || "-"}`,
|
||||||
|
`finalStatus: ${activeLog.finalStatus || "-"}`,
|
||||||
|
`interruptType: ${activeLog.interruptType || "-"}`,
|
||||||
|
`resumeSource: ${activeLog.resumeSource || "-"}`,
|
||||||
`latencyMs: ${activeLog.latencyMs} ms`,
|
`latencyMs: ${activeLog.latencyMs} ms`,
|
||||||
`createdAt: ${formatDateTime(activeLog.createdAt)}`,
|
`createdAt: ${formatDateTime(activeLog.createdAt)}`,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ import {
|
|||||||
import { getEnumOptions } from "@/lib/enums";
|
import { getEnumOptions } from "@/lib/enums";
|
||||||
import { FieldDescription } from "@base-ui/react";
|
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 = {
|
type EditDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
@@ -231,18 +241,16 @@ function EditDialogBody({
|
|||||||
const [knowledgeToAdd, setKnowledgeToAdd] = useState("");
|
const [knowledgeToAdd, setKnowledgeToAdd] = useState("");
|
||||||
const [teamToAdd, setTeamToAdd] = useState("");
|
const [teamToAdd, setTeamToAdd] = useState("");
|
||||||
const [skillToAdd, setSkillToAdd] = useState("");
|
const [skillToAdd, setSkillToAdd] = useState("");
|
||||||
const [directToolServerCodeToAdd, setDirectToolServerCodeToAdd] = useState("");
|
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("");
|
||||||
const [directToolToAdd, setDirectToolToAdd] = useState("");
|
const [directToolToAdd, setDirectToolToAdd] = useState("");
|
||||||
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]);
|
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([]);
|
||||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||||
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]);
|
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([]);
|
||||||
const [skills, setSkills] = useState<SkillDefinition[]>([]);
|
const [skills, setSkills] = useState<SkillDefinition[]>([]);
|
||||||
const [directTools, setDirectTools] = useState<
|
const [directTools, setDirectTools] = useState<DirectToolItem[]>([]);
|
||||||
CreateAIAgentPayload["directTools"]
|
const [directToolOptions, setDirectToolOptions] = useState<DirectToolOption[]>(
|
||||||
>([]);
|
[],
|
||||||
const [directToolOptions, setDirectToolOptions] = useState<
|
);
|
||||||
{ value: string; label: string; meta: CreateAIAgentPayload["directTools"][number] }[]
|
|
||||||
>([]);
|
|
||||||
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]);
|
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -256,7 +264,7 @@ function EditDialogBody({
|
|||||||
setKnowledgeToAdd("");
|
setKnowledgeToAdd("");
|
||||||
setTeamToAdd("");
|
setTeamToAdd("");
|
||||||
setSkillToAdd("");
|
setSkillToAdd("");
|
||||||
setDirectToolServerCodeToAdd("");
|
setDirectToolGroupToAdd("");
|
||||||
setDirectToolToAdd("");
|
setDirectToolToAdd("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -271,7 +279,7 @@ function EditDialogBody({
|
|||||||
setKnowledgeToAdd("");
|
setKnowledgeToAdd("");
|
||||||
setTeamToAdd("");
|
setTeamToAdd("");
|
||||||
setSkillToAdd("");
|
setSkillToAdd("");
|
||||||
setDirectToolServerCodeToAdd("");
|
setDirectToolGroupToAdd("");
|
||||||
setDirectToolToAdd("");
|
setDirectToolToAdd("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -349,6 +357,11 @@ function EditDialogBody({
|
|||||||
catalog.map((tool) => ({
|
catalog.map((tool) => ({
|
||||||
value: tool.toolCode,
|
value: tool.toolCode,
|
||||||
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
|
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
|
||||||
|
sourceType: tool.sourceType,
|
||||||
|
groupLabel:
|
||||||
|
tool.sourceType === "builtin"
|
||||||
|
? "内置工具"
|
||||||
|
: tool.serverCode,
|
||||||
meta: {
|
meta: {
|
||||||
toolCode: tool.toolCode,
|
toolCode: tool.toolCode,
|
||||||
serverCode: tool.serverCode,
|
serverCode: tool.serverCode,
|
||||||
@@ -446,21 +459,21 @@ function EditDialogBody({
|
|||||||
() =>
|
() =>
|
||||||
directToolOptions.filter(
|
directToolOptions.filter(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.meta.serverCode === directToolServerCodeToAdd &&
|
option.groupLabel === directToolGroupToAdd &&
|
||||||
!directTools.some((tool) => tool.toolCode === option.value),
|
!directTools.some((tool) => tool.toolCode === option.value),
|
||||||
),
|
),
|
||||||
[directToolOptions, directToolServerCodeToAdd, directTools],
|
[directToolOptions, directToolGroupToAdd, directTools],
|
||||||
);
|
);
|
||||||
|
|
||||||
const directToolServerOptions = useMemo(
|
const directToolGroupOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Array.from(
|
Array.from(
|
||||||
new Map(
|
new Map(
|
||||||
directToolOptions.map((option) => [
|
directToolOptions.map((option) => [
|
||||||
option.meta.serverCode,
|
option.groupLabel,
|
||||||
{
|
{
|
||||||
value: option.meta.serverCode,
|
value: option.groupLabel,
|
||||||
label: option.meta.serverCode,
|
label: option.groupLabel,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
).values(),
|
).values(),
|
||||||
@@ -468,15 +481,16 @@ function EditDialogBody({
|
|||||||
[directToolOptions],
|
[directToolOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const directToolsGroupedByServer = useMemo(() => {
|
const directToolsGrouped = useMemo(() => {
|
||||||
const groups = new Map<
|
const groups = new Map<string, DirectToolItem[]>();
|
||||||
string,
|
|
||||||
CreateAIAgentPayload["directTools"]
|
|
||||||
>();
|
|
||||||
for (const tool of directTools) {
|
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);
|
current.push(tool);
|
||||||
groups.set(tool.serverCode, current);
|
groups.set(groupLabel, current);
|
||||||
}
|
}
|
||||||
return Array.from(groups.entries());
|
return Array.from(groups.entries());
|
||||||
}, [directTools]);
|
}, [directTools]);
|
||||||
@@ -585,7 +599,7 @@ function EditDialogBody({
|
|||||||
}
|
}
|
||||||
return [...prev, option.meta];
|
return [...prev, option.meta];
|
||||||
});
|
});
|
||||||
setDirectToolServerCodeToAdd(option.meta.serverCode);
|
setDirectToolGroupToAdd(option.groupLabel);
|
||||||
setDirectToolToAdd("");
|
setDirectToolToAdd("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,22 +866,22 @@ function EditDialogBody({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border bg-muted/10 p-4">
|
<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">
|
<div className="mb-4 text-xs text-muted-foreground">
|
||||||
用于低风险、原子化的实时查询。工具来自统一的 MCP Tool Catalog。
|
用于低风险、原子化的实时查询。可选择 MCP 工具,也可选择系统内置工具。
|
||||||
</div>
|
</div>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldContent className="space-y-3">
|
<FieldContent className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-52">
|
<div className="w-52">
|
||||||
<OptionCombobox
|
<OptionCombobox
|
||||||
value={directToolServerCodeToAdd}
|
value={directToolGroupToAdd}
|
||||||
options={directToolServerOptions}
|
options={directToolGroupOptions}
|
||||||
placeholder="选择 MCP Server"
|
placeholder="选择工具分组"
|
||||||
searchPlaceholder="搜索 MCP Server"
|
searchPlaceholder="搜索工具分组"
|
||||||
emptyText="没有可用的 MCP Server"
|
emptyText="没有可用的工具分组"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setDirectToolServerCodeToAdd(value);
|
setDirectToolGroupToAdd(value);
|
||||||
setDirectToolToAdd("");
|
setDirectToolToAdd("");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -876,7 +890,7 @@ function EditDialogBody({
|
|||||||
<OptionCombobox
|
<OptionCombobox
|
||||||
value={directToolToAdd}
|
value={directToolToAdd}
|
||||||
options={addableDirectToolOptions}
|
options={addableDirectToolOptions}
|
||||||
placeholder="选择该 Server 下的 Direct Tool"
|
placeholder="选择 Direct Tool"
|
||||||
searchPlaceholder="搜索 Direct Tool"
|
searchPlaceholder="搜索 Direct Tool"
|
||||||
emptyText="没有可添加的 Direct Tool"
|
emptyText="没有可添加的 Direct Tool"
|
||||||
onChange={handleAddDirectTool}
|
onChange={handleAddDirectTool}
|
||||||
@@ -886,7 +900,7 @@ function EditDialogBody({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={
|
disabled={
|
||||||
!directToolServerCodeToAdd || !directToolToAdd
|
!directToolGroupToAdd || !directToolToAdd
|
||||||
}
|
}
|
||||||
onClick={() => handleAddDirectTool(directToolToAdd)}
|
onClick={() => handleAddDirectTool(directToolToAdd)}
|
||||||
>
|
>
|
||||||
@@ -897,16 +911,16 @@ function EditDialogBody({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{directTools.length === 0 ? (
|
{directTools.length === 0 ? (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
不配置 Direct Tool 时,Agent 不会直接访问 MCP,只能通过 Skill 间接调用。
|
不配置 Direct Tool 时,Agent 不会直接调用外部或内置工具,只会依赖知识库、Skill 和普通回复。
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
directToolsGroupedByServer.map(([serverCode, tools]) => (
|
directToolsGrouped.map(([groupLabel, tools]) => (
|
||||||
<div
|
<div
|
||||||
key={serverCode}
|
key={groupLabel}
|
||||||
className="rounded-md border p-3"
|
className="rounded-md border p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-2 text-xs font-medium text-muted-foreground">
|
<div className="mb-2 text-xs font-medium text-muted-foreground">
|
||||||
{serverCode}
|
{groupLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{tools.map((tool) => {
|
{tools.map((tool) => {
|
||||||
@@ -921,6 +935,11 @@ function EditDialogBody({
|
|||||||
className="gap-1 pr-1"
|
className="gap-1 pr-1"
|
||||||
>
|
>
|
||||||
{tool.title || catalogItem?.title || value}
|
{tool.title || catalogItem?.title || value}
|
||||||
|
<span className="text-[10px] text-muted-foreground/80">
|
||||||
|
{catalogItem?.sourceType === "builtin"
|
||||||
|
? "内置"
|
||||||
|
: tool.serverCode || "MCP"}
|
||||||
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ export type MCPToolCatalogItem = {
|
|||||||
toolCode: string
|
toolCode: string
|
||||||
serverCode: string
|
serverCode: string
|
||||||
toolName: string
|
toolName: string
|
||||||
|
sourceType: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
inputSchema: unknown
|
inputSchema: unknown
|
||||||
@@ -362,9 +363,14 @@ export type AgentRunLog = {
|
|||||||
userMessage: string
|
userMessage: string
|
||||||
plannedAction: string
|
plannedAction: string
|
||||||
plannedSkillCode: string
|
plannedSkillCode: string
|
||||||
|
plannedSkillName: string
|
||||||
|
skillRouteTrace: string
|
||||||
plannedToolCode: string
|
plannedToolCode: string
|
||||||
planReason: string
|
planReason: string
|
||||||
|
interruptType: string
|
||||||
|
resumeSource: string
|
||||||
finalAction: string
|
finalAction: string
|
||||||
|
finalStatus: string
|
||||||
replyText: string
|
replyText: string
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
latencyMs: number
|
latencyMs: number
|
||||||
|
|||||||
Reference in New Issue
Block a user