refactor: replace skill code with skill ID across the application

- Updated skill handling to use skill IDs instead of skill codes in various components, services, and models.
- Modified tests to reflect changes in skill identification.
- Removed references to skill codes in favor of skill IDs for consistency and clarity.
- Updated localization files to remove skill code references and adjust error messages accordingly.
This commit is contained in:
mlogclub
2026-06-20 20:42:07 +08:00
parent 39c648f3c1
commit 541e4c5874
49 changed files with 258 additions and 341 deletions
+3 -5
View File
@@ -142,14 +142,12 @@ func getDefaultTeamIDs() string {
} }
func getDefaultSkillIDs() (string, error) { func getDefaultSkillIDs() (string, error) {
skillItem := repositories.SkillDefinitionRepository.Take( skillItem := repositories.SkillDefinitionRepository.FindOne(
sqls.DB(), sqls.DB(),
"code = ? AND status = ?", sqls.NewCnd().Where("status = ?", enums.StatusOk).Desc("id"),
skill.AfterSalesEscalationSkillCode,
enums.StatusOk,
) )
if skillItem == nil { if skillItem == nil {
return "", fmt.Errorf("default test skill not found: %s", skill.AfterSalesEscalationSkillCode) return "", fmt.Errorf("default test skill not found")
} }
return utils.JoinInt64s([]int64{skillItem.ID}), nil return utils.JoinInt64s([]int64{skillItem.ID}), nil
} }
-5
View File
@@ -5,10 +5,7 @@ import (
"agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/enums"
) )
const AfterSalesEscalationSkillCode = "after_sales_escalation_skill"
type SkillDefinitionSeed struct { type SkillDefinitionSeed struct {
Code string
Name string Name string
Description string Description string
Instruction string Instruction string
@@ -22,7 +19,6 @@ func SkillDefinitionSeeds(lang seedlang.Language) []SkillDefinitionSeed {
if lang == seedlang.English { if lang == seedlang.English {
return []SkillDefinitionSeed{ return []SkillDefinitionSeed{
{ {
Code: AfterSalesEscalationSkillCode,
Name: "After-sales Escalation", Name: "After-sales Escalation",
Description: "Handles incidents, complaints, after-sales follow-up, ticket creation, and human handoff requests. Match only when the user clearly needs after-sales intervention or escalation; do not match ordinary greetings, product introductions, or general inquiries.", Description: "Handles incidents, complaints, after-sales follow-up, ticket creation, and human handoff requests. Match only when the user clearly needs after-sales intervention or escalation; do not match ordinary greetings, product introductions, or general inquiries.",
Instruction: `You are the dedicated "After-sales Escalation" skill responsible for customer support requests that require escalation. Instruction: `You are the dedicated "After-sales Escalation" skill responsible for customer support requests that require escalation.
@@ -65,7 +61,6 @@ Response requirements:
} }
return []SkillDefinitionSeed{ return []SkillDefinitionSeed{
{ {
Code: AfterSalesEscalationSkillCode,
Name: "售后升级处理", Name: "售后升级处理",
Description: "处理报障、投诉、售后跟进、建单、转人工等升级诉求。只在用户明确需要售后介入或问题升级处理时命中,不处理普通问候、产品介绍或泛咨询。", Description: "处理报障、投诉、售后跟进、建单、转人工等升级诉求。只在用户明确需要售后介入或问题升级处理时命中,不处理普通问候、产品介绍或泛咨询。",
Instruction: `你是“售后升级处理”专项 Skill,负责承接需要升级处理的客服诉求。 Instruction: `你是“售后升级处理”专项 Skill,负责承接需要升级处理的客服诉求。
+2 -4
View File
@@ -6,13 +6,12 @@ import (
"agent-desk/internal/models" "agent-desk/internal/models"
"agent-desk/internal/repositories" "agent-desk/internal/repositories"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/mlogclub/simple/sqls" "github.com/mlogclub/simple/sqls"
) )
const AfterSalesEscalationSkillCode = seeds.AfterSalesEscalationSkillCode
type InitResult struct { type InitResult struct {
Created int Created int
Updated int Updated int
@@ -24,7 +23,7 @@ func Init(lang seedlang.Language) (*InitResult, error) {
for _, item := range seedItems { for _, item := range seedItems {
itemCopy := item itemCopy := item
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
existing := repositories.SkillDefinitionRepository.Take(ctx.Tx, "code = ?", itemCopy.Code) existing := repositories.SkillDefinitionRepository.Take(ctx.Tx, "name = ?", strings.TrimSpace(itemCopy.Name))
if existing != nil { if existing != nil {
if err := ctx.Tx.Model(existing).Updates(&itemCopy).Error; err != nil { if err := ctx.Tx.Model(existing).Updates(&itemCopy).Error; err != nil {
return err return err
@@ -50,7 +49,6 @@ func buildModels(lang seedlang.Language) []models.SkillDefinition {
items := make([]models.SkillDefinition, 0, len(seedItems)) items := make([]models.SkillDefinition, 0, len(seedItems))
for _, seed := range seedItems { for _, seed := range seedItems {
items = append(items, models.SkillDefinition{ items = append(items, models.SkillDefinition{
Code: seed.Code,
Name: seed.Name, Name: seed.Name,
Description: seed.Description, Description: seed.Description,
Instruction: seed.Instruction, Instruction: seed.Instruction,
@@ -13,7 +13,7 @@ func toSummary(summary *executor.RunResult) *Summary {
RunID: summary.RunID, RunID: summary.RunID,
Status: summary.Status, Status: summary.Status,
ReplyText: summary.ReplyText, ReplyText: summary.ReplyText,
PlannedSkillCode: strings.TrimSpace(summary.SelectedSkillCode), PlannedSkillID: summary.SelectedSkillID,
PlannedSkillName: strings.TrimSpace(summary.SelectedSkillName), PlannedSkillName: strings.TrimSpace(summary.SelectedSkillName),
PlanReason: strings.TrimSpace(summary.SkillRouteReason), PlanReason: strings.TrimSpace(summary.SkillRouteReason),
SkillRouteTrace: strings.TrimSpace(summary.SkillRouteTrace), SkillRouteTrace: strings.TrimSpace(summary.SkillRouteTrace),
+1 -1
View File
@@ -33,7 +33,7 @@ type Summary struct {
RunID string RunID string
Status string Status string
ReplyText string ReplyText string
PlannedSkillCode string PlannedSkillID int64
PlannedSkillName string PlannedSkillName string
PlanReason string PlanReason string
SkillRouteTrace string SkillRouteTrace string
+14 -7
View File
@@ -2,6 +2,7 @@ package runtime
import ( import (
"context" "context"
"fmt"
"strings" "strings"
applicationruntime "agent-desk/internal/ai/application/runtime" applicationruntime "agent-desk/internal/ai/application/runtime"
@@ -28,6 +29,12 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp
if aiConfig == nil { if aiConfig == nil {
return nil, errorsx.InvalidParamI18n("error.e0008") return nil, errorsx.InvalidParamI18n("error.e0008")
} }
skill := svc.SkillDefinitionService.Get(req.SkillDefinitionID)
if skill == nil || skill.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0054")
}
debugAgent := *aiAgent
debugAgent.SkillIDs = fmt.Sprintf("%d", skill.ID)
var conversation *models.Conversation var conversation *models.Conversation
if req.ConversationID > 0 { if req.ConversationID > 0 {
if conversation = svc.ConversationService.Get(req.ConversationID); conversation == nil { if conversation = svc.ConversationService.Get(req.ConversationID); conversation == nil {
@@ -45,13 +52,13 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp
summary, err := Service.Run(ctx, applicationruntime.Request{ summary, err := Service.Run(ctx, applicationruntime.Request{
Conversation: *conversation, Conversation: *conversation,
UserMessage: message, UserMessage: message,
AIAgent: *aiAgent, AIAgent: debugAgent,
AIConfig: *aiConfig, AIConfig: *aiConfig,
}) })
if err != nil { if err != nil {
return buildSkillDebugRunResponse(req, summary, nil), err return buildSkillDebugRunResponse(req, summary, skill), err
} }
return buildSkillDebugRunResponse(req, summary, nil), nil return buildSkillDebugRunResponse(req, summary, skill), nil
} }
func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) { func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) {
@@ -125,14 +132,14 @@ func buildSkillDebugRunResponse(req request.SkillDebugRunRequest, summary *appli
AIAgentID: req.AIAgentID, AIAgentID: req.AIAgentID,
} }
if skill != nil { if skill != nil {
resp.SkillCode = skill.Code resp.SkillDefinitionID = skill.ID
resp.SkillName = skill.Name resp.SkillName = skill.Name
} }
if summary == nil { if summary == nil {
return resp return resp
} }
if resp.SkillCode == "" { if resp.SkillDefinitionID <= 0 {
resp.SkillCode = strings.TrimSpace(summary.PlannedSkillCode) resp.SkillDefinitionID = summary.PlannedSkillID
} }
resp.ReplyText = summary.ReplyText resp.ReplyText = summary.ReplyText
resp.PlanReason = summary.PlanReason resp.PlanReason = summary.PlanReason
@@ -159,7 +166,7 @@ func buildSkillDebugResumeResponse(req request.SkillDebugResumeRequest, summary
if summary == nil { if summary == nil {
return resp return resp
} }
resp.SkillCode = strings.TrimSpace(summary.PlannedSkillCode) resp.SkillDefinitionID = summary.PlannedSkillID
resp.SkillName = strings.TrimSpace(summary.PlannedSkillName) resp.SkillName = strings.TrimSpace(summary.PlannedSkillName)
resp.ReplyText = summary.ReplyText resp.ReplyText = summary.ReplyText
resp.PlanReason = summary.PlanReason resp.PlanReason = summary.PlanReason
+1 -1
View File
@@ -213,7 +213,7 @@ func syncSkillSummaryFromCollector(summary *RunResult, collector *callbacks.Runt
return return
} }
trace := collector.Data.Skill trace := collector.Data.Skill
summary.SelectedSkillCode = strings.TrimSpace(trace.Code) summary.SelectedSkillID = trace.ID
summary.SelectedSkillName = strings.TrimSpace(trace.Name) summary.SelectedSkillName = strings.TrimSpace(trace.Name)
summary.SkillRouteReason = strings.TrimSpace(trace.RouteReason) summary.SkillRouteReason = strings.TrimSpace(trace.RouteReason)
summary.SkillRouteTrace = strings.TrimSpace(trace.RouteTrace) summary.SkillRouteTrace = strings.TrimSpace(trace.RouteTrace)
+1 -1
View File
@@ -33,7 +33,7 @@ type RunResult struct {
RunID string RunID string
Status string Status string
ReplyText string ReplyText string
SelectedSkillCode string SelectedSkillID int64
SelectedSkillName string SelectedSkillName string
SkillRouteReason string SkillRouteReason string
SkillRouteTrace string SkillRouteTrace string
+2 -2
View File
@@ -16,7 +16,7 @@ func BuildSelectedSkillActivationInstruction(skill *models.SkillDefinition) stri
} }
lines := []string{ lines := []string{
"当前命中的专项技能:", "当前命中的专项技能:",
fmt.Sprintf("- code: %s", strings.TrimSpace(skill.Code)), fmt.Sprintf("- id: %d", skill.ID),
fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)), fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)),
} }
if desc := strings.TrimSpace(skill.Description); desc != "" { if desc := strings.TrimSpace(skill.Description); desc != "" {
@@ -36,7 +36,7 @@ func BuildSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtime
} }
lines := []string{ lines := []string{
"当前命中的专项技能:", "当前命中的专项技能:",
fmt.Sprintf("- code: %s", strings.TrimSpace(skill.Code)), fmt.Sprintf("- id: %d", skill.ID),
fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)), fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)),
} }
if desc := strings.TrimSpace(skill.Description); desc != "" { if desc := strings.TrimSpace(skill.Description); desc != "" {
@@ -3,6 +3,7 @@ package callbacks
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strconv"
"strings" "strings"
"time" "time"
@@ -142,17 +143,19 @@ func (h *RuntimeTraceHandler) tryActivateSkill(argumentsInJSON string) {
if err := json.Unmarshal([]byte(strings.TrimSpace(argumentsInJSON)), &args); err != nil { if err := json.Unmarshal([]byte(strings.TrimSpace(argumentsInJSON)), &args); err != nil {
return return
} }
code := strings.TrimSpace(args.Skill) skillKey := strings.TrimSpace(args.Skill)
if code == "" { if skillKey == "" {
return return
} }
meta, ok := h.skillMetadataBy[code] meta, ok := h.skillMetadataBy[skillKey]
if !ok { if !ok {
meta = SkillMetadata{Code: code} if id, err := strconv.ParseInt(skillKey, 10, 64); err == nil {
meta = SkillMetadata{ID: id}
}
} }
buf, err := json.Marshal(map[string]any{ buf, err := json.Marshal(map[string]any{
"source": "eino_skill_tool", "source": "eino_skill_tool",
"skill": code, "skillId": skillKey,
}) })
routeTrace := "" routeTrace := ""
if err == nil { if err == nil {
@@ -42,18 +42,18 @@ func TestTryActivateSkill(t *testing.T) {
handler := &RuntimeTraceHandler{ handler := &RuntimeTraceHandler{
collector: collector, collector: collector,
skillMetadataBy: map[string]SkillMetadata{ skillMetadataBy: map[string]SkillMetadata{
"after_sales_escalation_skill": { "44": {
Code: "after_sales_escalation_skill", ID: 44,
Name: "售后升级", Name: "售后升级",
AllowedToolCodes: []string{"graph/handoff_to_human"}, AllowedToolCodes: []string{"graph/handoff_to_human"},
}, },
}, },
} }
handler.tryActivateSkill(`{"skill":"after_sales_escalation_skill"}`) handler.tryActivateSkill(`{"skill":"44"}`)
if collector.Data.Skill.Code != "after_sales_escalation_skill" { if collector.Data.Skill.ID != 44 {
t.Fatalf("unexpected skill code: %#v", collector.Data.Skill) t.Fatalf("unexpected skill id: %#v", collector.Data.Skill)
} }
if collector.Data.Skill.Name != "售后升级" { if collector.Data.Skill.Name != "售后升级" {
t.Fatalf("unexpected skill name: %#v", collector.Data.Skill) t.Fatalf("unexpected skill name: %#v", collector.Data.Skill)
@@ -52,7 +52,7 @@ func (c *RuntimeTraceCollector) SetSkillMiddleware(enabled bool, toolName string
} }
type SkillMetadata struct { type SkillMetadata struct {
Code string ID int64
Name string Name string
Description string Description string
AllowedToolCodes []string AllowedToolCodes []string
@@ -64,20 +64,20 @@ func (c *RuntimeTraceCollector) SetVisibleSkills(skills map[string]SkillMetadata
} }
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
codes := make([]string, 0, len(skills)) ids := make([]int64, 0, len(skills))
for code := range skills { for _, skill := range skills {
if code == "" { if skill.ID <= 0 {
continue continue
} }
codes = append(codes, code) ids = append(ids, skill.ID)
} }
c.Data.Skill.VisibleCodes = append([]string(nil), codes...) c.Data.Skill.VisibleIDs = append([]int64(nil), ids...)
} }
func (c *RuntimeTraceCollector) ActivateSkill(skill SkillMetadata, routeReason string, routeTrace string) { func (c *RuntimeTraceCollector) ActivateSkill(skill SkillMetadata, routeReason string, routeTrace string) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.Data.Skill.Code = skill.Code c.Data.Skill.ID = skill.ID
c.Data.Skill.Name = skill.Name c.Data.Skill.Name = skill.Name
c.Data.Skill.Description = skill.Description c.Data.Skill.Description = skill.Description
c.Data.Skill.AllowedToolCodes = append([]string(nil), skill.AllowedToolCodes...) c.Data.Skill.AllowedToolCodes = append([]string(nil), skill.AllowedToolCodes...)
@@ -152,7 +152,7 @@ type RuntimeTraceData struct {
} }
type SkillTraceData struct { type SkillTraceData struct {
Code string `json:"code,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
RouteReason string `json:"routeReason,omitempty"` RouteReason string `json:"routeReason,omitempty"`
@@ -161,7 +161,7 @@ type SkillTraceData struct {
FilteredToolCodes []string `json:"filteredToolCodes,omitempty"` FilteredToolCodes []string `json:"filteredToolCodes,omitempty"`
MiddlewareEnabled bool `json:"middlewareEnabled,omitempty"` MiddlewareEnabled bool `json:"middlewareEnabled,omitempty"`
MiddlewareToolName string `json:"middlewareToolName,omitempty"` MiddlewareToolName string `json:"middlewareToolName,omitempty"`
VisibleCodes []string `json:"visibleCodes,omitempty"` VisibleIDs []int64 `json:"visibleIds,omitempty"`
} }
type InterruptTraceContext struct { type InterruptTraceContext struct {
@@ -38,19 +38,19 @@ func NewAgentHandlerService(skillMiddleware *SkillMiddlewareService) *AgentHandl
func (s *AgentHandlerService) Build(ctx context.Context, input BuildAgentHandlersInput) ([]adk.ChatModelAgentMiddleware, error) { func (s *AgentHandlerService) Build(ctx context.Context, input BuildAgentHandlersInput) ([]adk.ChatModelAgentMiddleware, error) {
handlers := make([]adk.ChatModelAgentMiddleware, 0, 4) handlers := make([]adk.ChatModelAgentMiddleware, 0, 4)
skillMetadataByCode := buildRuntimeSkillMetadataMap(input.AIAgent) skillMetadataByID := buildRuntimeSkillMetadataMap(input.AIAgent)
toolMetadataBy := buildRuntimeTraceToolMetadata(input.DynamicToolDefinitions, input.StaticToolMetadata, len(skillMetadataByCode) > 0) toolMetadataBy := buildRuntimeTraceToolMetadata(input.DynamicToolDefinitions, input.StaticToolMetadata, len(skillMetadataByID) > 0)
traceSkillMetadata := make(map[string]einocallbacks.SkillMetadata, len(skillMetadataByCode)) traceSkillMetadata := make(map[string]einocallbacks.SkillMetadata, len(skillMetadataByID))
for code, item := range skillMetadataByCode { for id, item := range skillMetadataByID {
traceSkillMetadata[code] = einocallbacks.SkillMetadata{ traceSkillMetadata[id] = einocallbacks.SkillMetadata{
Code: item.Code, ID: item.ID,
Name: item.Name, Name: item.Name,
Description: item.Description, Description: item.Description,
AllowedToolCodes: append([]string(nil), item.AllowedToolCodes...), AllowedToolCodes: append([]string(nil), item.AllowedToolCodes...),
} }
} }
if input.Collector != nil { if input.Collector != nil {
if len(skillMetadataByCode) > 0 { if len(skillMetadataByID) > 0 {
input.Collector.SetSkillMiddleware(true, toolx.BuiltinSkill.Name) input.Collector.SetSkillMiddleware(true, toolx.BuiltinSkill.Name)
} }
input.Collector.SetVisibleSkills(traceSkillMetadata) input.Collector.SetVisibleSkills(traceSkillMetadata)
@@ -66,7 +66,7 @@ func (s *AgentHandlerService) Build(ctx context.Context, input BuildAgentHandler
} }
handlers = append(handlers, toolSearchHandler) handlers = append(handlers, toolSearchHandler)
} }
if len(skillMetadataByCode) > 0 { if len(skillMetadataByID) > 0 {
skillHandler, err := s.skillMiddleware.Build(ctx, input.AIAgent, input.InstructionToolDefinitions) skillHandler, err := s.skillMiddleware.Build(ctx, input.AIAgent, input.InstructionToolDefinitions)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings" "strings"
runtimeinstruction "agent-desk/internal/ai/runtime/instruction" runtimeinstruction "agent-desk/internal/ai/runtime/instruction"
@@ -17,7 +18,7 @@ import (
) )
type runtimeSkillMetadata struct { type runtimeSkillMetadata struct {
Code string ID int64
Name string Name string
Description string Description string
AllowedToolCodes []string AllowedToolCodes []string
@@ -25,7 +26,7 @@ type runtimeSkillMetadata struct {
type databaseSkillBackend struct { type databaseSkillBackend struct {
toolDefinitions []runtimetooling.MCPToolDefinition toolDefinitions []runtimetooling.MCPToolDefinition
skillsByCode map[string]models.SkillDefinition skillsByID map[string]models.SkillDefinition
order []string order []string
} }
@@ -36,18 +37,18 @@ func newDatabaseSkillBackend(aiAgent models.AIAgent, toolDefinitions []runtimeto
} }
ret := &databaseSkillBackend{ ret := &databaseSkillBackend{
toolDefinitions: append([]runtimetooling.MCPToolDefinition(nil), toolDefinitions...), toolDefinitions: append([]runtimetooling.MCPToolDefinition(nil), toolDefinitions...),
skillsByCode: make(map[string]models.SkillDefinition, len(visibleSkills)), skillsByID: make(map[string]models.SkillDefinition, len(visibleSkills)),
order: make([]string, 0, len(visibleSkills)), order: make([]string, 0, len(visibleSkills)),
} }
for _, item := range visibleSkills { for _, item := range visibleSkills {
code := strings.TrimSpace(item.Code) id := strconv.FormatInt(item.ID, 10)
if code == "" { if id == "" {
continue continue
} }
ret.skillsByCode[code] = item ret.skillsByID[id] = item
ret.order = append(ret.order, code) ret.order = append(ret.order, id)
} }
if len(ret.skillsByCode) == 0 { if len(ret.skillsByID) == 0 {
return nil, fmt.Errorf("no visible skills available") return nil, fmt.Errorf("no visible skills available")
} }
return ret, nil return ret, nil
@@ -58,13 +59,13 @@ func (b *databaseSkillBackend) List(_ context.Context) ([]einoskill.FrontMatter,
return nil, nil return nil, nil
} }
ret := make([]einoskill.FrontMatter, 0, len(b.order)) ret := make([]einoskill.FrontMatter, 0, len(b.order))
for _, code := range b.order { for _, id := range b.order {
item, ok := b.skillsByCode[code] item, ok := b.skillsByID[id]
if !ok { if !ok {
continue continue
} }
ret = append(ret, einoskill.FrontMatter{ ret = append(ret, einoskill.FrontMatter{
Name: strings.TrimSpace(item.Code), Name: strconv.FormatInt(item.ID, 10),
Description: skillListDescription(item), Description: skillListDescription(item),
}) })
} }
@@ -79,13 +80,13 @@ func (b *databaseSkillBackend) Get(_ context.Context, name string) (einoskill.Sk
if name == "" { if name == "" {
return einoskill.Skill{}, fmt.Errorf("skill name is empty") return einoskill.Skill{}, fmt.Errorf("skill name is empty")
} }
item, ok := b.skillsByCode[name] item, ok := b.skillsByID[name]
if !ok { if !ok {
return einoskill.Skill{}, fmt.Errorf("skill %q not found", name) return einoskill.Skill{}, fmt.Errorf("skill %q not found", name)
} }
return einoskill.Skill{ return einoskill.Skill{
FrontMatter: einoskill.FrontMatter{ FrontMatter: einoskill.FrontMatter{
Name: strings.TrimSpace(item.Code), Name: strconv.FormatInt(item.ID, 10),
Description: skillListDescription(item), Description: skillListDescription(item),
}, },
Content: runtimeinstruction.BuildSkillDocument(&item, filterSkillToolDefinitions(b.toolDefinitions, &item)), Content: runtimeinstruction.BuildSkillDocument(&item, filterSkillToolDefinitions(b.toolDefinitions, &item)),
@@ -105,7 +106,7 @@ func loadVisibleSkills(aiAgent models.AIAgent) []models.SkillDefinition {
ret := make([]models.SkillDefinition, 0, len(ids)) ret := make([]models.SkillDefinition, 0, len(ids))
for _, id := range ids { for _, id := range ids {
item, ok := byID[id] item, ok := byID[id]
if !ok || item.Status != enums.StatusOk || strings.TrimSpace(item.Code) == "" { if !ok || item.Status != enums.StatusOk || item.ID <= 0 {
continue continue
} }
ret = append(ret, item) ret = append(ret, item)
@@ -120,12 +121,12 @@ func buildRuntimeSkillMetadataMap(aiAgent models.AIAgent) map[string]runtimeSkil
} }
ret := make(map[string]runtimeSkillMetadata, len(visibleSkills)) ret := make(map[string]runtimeSkillMetadata, len(visibleSkills))
for _, item := range visibleSkills { for _, item := range visibleSkills {
code := strings.TrimSpace(item.Code) if item.ID <= 0 {
if code == "" {
continue continue
} }
ret[code] = runtimeSkillMetadata{ id := strconv.FormatInt(item.ID, 10)
Code: code, ret[id] = runtimeSkillMetadata{
ID: item.ID,
Name: strings.TrimSpace(item.Name), Name: strings.TrimSpace(item.Name),
Description: skillListDescription(item), Description: skillListDescription(item),
AllowedToolCodes: parseSkillToolWhitelist(item.ToolWhitelist), AllowedToolCodes: parseSkillToolWhitelist(item.ToolWhitelist),
@@ -145,7 +146,7 @@ func skillListDescription(item models.SkillDefinition) string {
if name := strings.TrimSpace(item.Name); name != "" { if name := strings.TrimSpace(item.Name); name != "" {
return name return name
} }
return strings.TrimSpace(item.Code) return fmt.Sprintf("Skill %d", item.ID)
} }
func parseSkillToolWhitelist(raw string) []string { func parseSkillToolWhitelist(raw string) []string {
@@ -19,7 +19,6 @@ func TestDatabaseSkillBackendListAndGet(t *testing.T) {
setupSkillBackendTestDB(t) setupSkillBackendTestDB(t)
createSkillDefinitionForTest(t, models.SkillDefinition{ createSkillDefinitionForTest(t, models.SkillDefinition{
ID: 1, ID: 1,
Code: "after_sales_escalation_skill",
Name: "售后升级", Name: "售后升级",
Description: "处理转人工和升级诉求", Description: "处理转人工和升级诉求",
Instruction: "请优先判断是否需要转人工。", Instruction: "请优先判断是否需要转人工。",
@@ -28,7 +27,6 @@ func TestDatabaseSkillBackendListAndGet(t *testing.T) {
}) })
createSkillDefinitionForTest(t, models.SkillDefinition{ createSkillDefinitionForTest(t, models.SkillDefinition{
ID: 2, ID: 2,
Code: "disabled_skill",
Name: "禁用技能", Name: "禁用技能",
Description: "不会被暴露", Description: "不会被暴露",
Instruction: "noop", Instruction: "noop",
@@ -47,15 +45,15 @@ func TestDatabaseSkillBackendListAndGet(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("List returned error: %v", err) t.Fatalf("List returned error: %v", err)
} }
if len(matters) != 1 || matters[0].Name != "after_sales_escalation_skill" { if len(matters) != 1 || matters[0].Name != "1" {
t.Fatalf("unexpected matters: %#v", matters) t.Fatalf("unexpected matters: %#v", matters)
} }
skill, err := backend.Get(context.Background(), "after_sales_escalation_skill") skill, err := backend.Get(context.Background(), "1")
if err != nil { if err != nil {
t.Fatalf("Get returned error: %v", err) t.Fatalf("Get returned error: %v", err)
} }
if skill.Name != "after_sales_escalation_skill" { if skill.Name != "1" {
t.Fatalf("unexpected skill name: %#v", skill) t.Fatalf("unexpected skill name: %#v", skill)
} }
if skill.Content == "" || !containsAll(skill.Content, "处理转人工和升级诉求", "graph/handoff_to_human") { if skill.Content == "" || !containsAll(skill.Content, "处理转人工和升级诉求", "graph/handoff_to_human") {
@@ -67,7 +65,6 @@ func TestHasVisibleSkills(t *testing.T) {
setupSkillBackendTestDB(t) setupSkillBackendTestDB(t)
createSkillDefinitionForTest(t, models.SkillDefinition{ createSkillDefinitionForTest(t, models.SkillDefinition{
ID: 3, ID: 3,
Code: "enabled_skill",
Name: "启用技能", Name: "启用技能",
Description: "可见", Description: "可见",
Instruction: "noop", Instruction: "noop",
@@ -75,7 +72,6 @@ func TestHasVisibleSkills(t *testing.T) {
}) })
createSkillDefinitionForTest(t, models.SkillDefinition{ createSkillDefinitionForTest(t, models.SkillDefinition{
ID: 4, ID: 4,
Code: "deleted_skill",
Name: "删除技能", Name: "删除技能",
Description: "不可见", Description: "不可见",
Instruction: "noop", Instruction: "noop",
@@ -15,7 +15,7 @@ import (
"github.com/cloudwego/eino/schema" "github.com/cloudwego/eino/schema"
) )
const activeSkillRunLocalKey = "runtime_active_skill_code" const activeSkillRunLocalKey = "runtime_active_skill_id"
type RuntimeToolFilterMiddleware struct { type RuntimeToolFilterMiddleware struct {
*adk.BaseChatModelAgentMiddleware *adk.BaseChatModelAgentMiddleware
@@ -72,7 +72,7 @@ func (m *RuntimeToolFilterMiddleware) WrapInvokableToolCall(_ context.Context, e
return result, err return result, err
} }
if strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinSkill.Code { if strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinSkill.Code {
_ = m.setActiveSkill(ctx, skillCodeFromArguments(argumentsInJSON)) _ = m.setActiveSkill(ctx, skillIDFromArguments(argumentsInJSON))
return result, nil return result, nil
} }
if strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearch.Code { if strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearch.Code {
@@ -90,7 +90,7 @@ func (m *RuntimeToolFilterMiddleware) WrapInvokableToolCall(_ context.Context, e
} }
func (m *RuntimeToolFilterMiddleware) blockToolCall(metadata einocallbacks.ToolMetadata, argumentsInJSON string, activeSkill einocallbacks.SkillMetadata) error { func (m *RuntimeToolFilterMiddleware) blockToolCall(metadata einocallbacks.ToolMetadata, argumentsInJSON string, activeSkill einocallbacks.SkillMetadata) error {
err := fmt.Errorf("tool %s is not allowed for active skill %s", strings.TrimSpace(metadata.ToolCode), strings.TrimSpace(activeSkill.Code)) err := fmt.Errorf("tool %s is not allowed for active skill %d", strings.TrimSpace(metadata.ToolCode), activeSkill.ID)
if m.collector != nil { if m.collector != nil {
m.collector.AddToolItem(einocallbacks.ToolTraceItem{ m.collector.AddToolItem(einocallbacks.ToolTraceItem{
ToolCode: strings.TrimSpace(metadata.ToolCode), ToolCode: strings.TrimSpace(metadata.ToolCode),
@@ -106,12 +106,12 @@ func (m *RuntimeToolFilterMiddleware) blockToolCall(metadata einocallbacks.ToolM
return err return err
} }
func (m *RuntimeToolFilterMiddleware) setActiveSkill(ctx context.Context, skillCode string) error { func (m *RuntimeToolFilterMiddleware) setActiveSkill(ctx context.Context, skillID string) error {
skillCode = strings.TrimSpace(skillCode) skillID = strings.TrimSpace(skillID)
if skillCode == "" { if skillID == "" {
return nil return nil
} }
return adk.SetRunLocalValue(ctx, activeSkillRunLocalKey, skillCode) return adk.SetRunLocalValue(ctx, activeSkillRunLocalKey, skillID)
} }
func (m *RuntimeToolFilterMiddleware) resolveActiveSkill(ctx context.Context) (einocallbacks.SkillMetadata, bool) { func (m *RuntimeToolFilterMiddleware) resolveActiveSkill(ctx context.Context) (einocallbacks.SkillMetadata, bool) {
@@ -122,15 +122,15 @@ func (m *RuntimeToolFilterMiddleware) resolveActiveSkill(ctx context.Context) (e
if err != nil || !found { if err != nil || !found {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
code, ok := value.(string) skillID, ok := value.(string)
if !ok { if !ok {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
code = strings.TrimSpace(code) skillID = strings.TrimSpace(skillID)
if code == "" { if skillID == "" {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
skill, ok := m.skillMetadataBy[code] skill, ok := m.skillMetadataBy[skillID]
if !ok { if !ok {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
@@ -179,12 +179,12 @@ func resolveActiveSkillMetadata(ctx context.Context, skills map[string]einocallb
if err != nil || !found { if err != nil || !found {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
code, ok := value.(string) skillID, ok := value.(string)
if !ok { if !ok {
return einocallbacks.SkillMetadata{}, false return einocallbacks.SkillMetadata{}, false
} }
code = strings.TrimSpace(code) skillID = strings.TrimSpace(skillID)
skill, ok := skills[code] skill, ok := skills[skillID]
if !ok || len(skill.AllowedToolCodes) == 0 { if !ok || len(skill.AllowedToolCodes) == 0 {
return skill, false return skill, false
} }
@@ -330,7 +330,7 @@ func resolveRuntimeToolMetadata(toolName string, toolMetadataByName map[string]e
return metadata, ok return metadata, ok
} }
func skillCodeFromArguments(argumentsInJSON string) string { func skillIDFromArguments(argumentsInJSON string) string {
var args struct { var args struct {
Skill string `json:"skill"` Skill string `json:"skill"`
} }
+1 -1
View File
@@ -26,7 +26,7 @@ func TestSummaryPrimaryToolCodePrefersToolSearchTarget(t *testing.T) {
} }
func TestToRunLogFinalAction(t *testing.T) { func TestToRunLogFinalAction(t *testing.T) {
if got := toRunLogFinalAction(&applicationruntime.Summary{PlannedSkillCode: "refund", ReplyText: "ok"}); got != "skill" { if got := toRunLogFinalAction(&applicationruntime.Summary{PlannedSkillID: 44, ReplyText: "ok"}); got != "skill" {
t.Fatalf("expected skill final action, got %q", got) t.Fatalf("expected skill final action, got %q", got)
} }
+6 -6
View File
@@ -46,7 +46,7 @@ func (s *replyRunLogService) Write(input replyRunLogInput) {
AIConfigID: input.AIAgent.AIConfigID, AIConfigID: input.AIAgent.AIConfigID,
UserMessage: strings.TrimSpace(input.Question), UserMessage: strings.TrimSpace(input.Question),
PlannedAction: plannedAction, PlannedAction: plannedAction,
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(input.Summary)), PlannedSkillID: summaryPlannedSkillID(input.Summary),
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(input.Summary)), PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(input.Summary)),
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(input.Summary)), SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(input.Summary)),
ToolSearchTrace: extractToolSearchTrace(input.Summary), ToolSearchTrace: extractToolSearchTrace(input.Summary),
@@ -90,7 +90,7 @@ func buildRunLogPlan(summary *applicationruntime.Summary) (plannedAction, planne
if summary == nil { if summary == nil {
return "", "", "" return "", "", ""
} }
if skillCode := strings.TrimSpace(summaryPlannedSkillCode(summary)); skillCode != "" { if summaryPlannedSkillID(summary) > 0 {
reason := strings.TrimSpace(summary.PlanReason) reason := strings.TrimSpace(summary.PlanReason)
if reason == "" { if reason == "" {
reason = "skill_selected" reason = "skill_selected"
@@ -138,7 +138,7 @@ func toRunLogFinalAction(summary *applicationruntime.Summary) string {
if summary == nil { if summary == nil {
return "" return ""
} }
if skillCode := strings.TrimSpace(summaryPlannedSkillCode(summary)); skillCode != "" && strings.TrimSpace(summary.ReplyText) != "" { if summaryPlannedSkillID(summary) > 0 && strings.TrimSpace(summary.ReplyText) != "" {
return "skill" return "skill"
} }
if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" && strings.TrimSpace(summary.ReplyText) != "" { if graphToolCode := firstGraphToolCode(summary); graphToolCode != "" && strings.TrimSpace(summary.ReplyText) != "" {
@@ -167,11 +167,11 @@ func buildRunLogReplyText(summary *applicationruntime.Summary) string {
return strings.TrimSpace(summary.ReplyText) return strings.TrimSpace(summary.ReplyText)
} }
func summaryPlannedSkillCode(summary *applicationruntime.Summary) string { func summaryPlannedSkillID(summary *applicationruntime.Summary) int64 {
if summary == nil { if summary == nil {
return "" return 0
} }
return strings.TrimSpace(summary.PlannedSkillCode) return summary.PlannedSkillID
} }
func summaryPlannedSkillName(summary *applicationruntime.Summary) string { func summaryPlannedSkillName(summary *applicationruntime.Summary) string {
+2 -2
View File
@@ -72,8 +72,8 @@ func TestResolveReplyTimeout(t *testing.T) {
func TestBuildRunLogPlan(t *testing.T) { func TestBuildRunLogPlan(t *testing.T) {
summary := &applicationruntime.Summary{ summary := &applicationruntime.Summary{
PlannedSkillCode: "faq_router", PlannedSkillID: 44,
PlanReason: "manual", PlanReason: "manual",
} }
action, toolCode, reason := buildRunLogPlan(summary) action, toolCode, reason := buildRunLogPlan(summary)
if action != "skill" || toolCode != "" || reason != "manual" { if action != "skill" || toolCode != "" || reason != "manual" {
+5 -2
View File
@@ -16,8 +16,11 @@ var newCandidateLoader = func() *candidateLoader {
type candidateLoader struct { type candidateLoader struct {
} }
func (l *candidateLoader) findManualSkillDefinition(skillCode string) *models.SkillDefinition { func (l *candidateLoader) findManualSkillDefinition(skillDefinitionID int64) *models.SkillDefinition {
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), skillCode) if skillDefinitionID <= 0 {
return nil
}
return repositories.SkillDefinitionRepository.Get(sqls.DB(), skillDefinitionID)
} }
func (l *candidateLoader) loadCandidateSkills(aiAgent models.AIAgent) []models.SkillDefinition { func (l *candidateLoader) loadCandidateSkills(aiAgent models.AIAgent) []models.SkillDefinition {
+7 -11
View File
@@ -11,12 +11,11 @@ import (
func TestBuildRunLogMatchedPlan(t *testing.T) { func TestBuildRunLogMatchedPlan(t *testing.T) {
log := BuildRunLog( log := BuildRunLog(
RuntimeContext{ RuntimeContext{
AIAgent: models.AIAgent{ID: 22}, AIAgent: models.AIAgent{ID: 22},
AIConfig: models.AIConfig{ID: 33}, AIConfig: models.AIConfig{ID: 33},
ConversationID: 11, ConversationID: 11,
ManualSkillCode: "manual_refund", ManualSkillDefinitionID: 44,
IntentCode: "refund", UserMessage: "我要退款",
UserMessage: "我要退款",
}, },
&ExecutionPlan{ &ExecutionPlan{
AIAgent: models.AIAgent{ID: 22}, AIAgent: models.AIAgent{ID: 22},
@@ -25,10 +24,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
ModelName: "gpt-test", ModelName: "gpt-test",
Provider: enums.AIProviderOpenAI, Provider: enums.AIProviderOpenAI,
}, },
Skill: &models.SkillDefinition{ Skill: &models.SkillDefinition{ID: 44},
ID: 44,
Code: "refund_skill",
},
MatchReason: "llm_route", MatchReason: "llm_route",
}, },
&ExecutionTrace{Status: "ok"}, &ExecutionTrace{Status: "ok"},
@@ -41,7 +37,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
if log.ConversationID != 11 || log.AIAgentID != 22 || log.AIConfigID != 33 { if log.ConversationID != 11 || log.AIAgentID != 22 || log.AIConfigID != 33 {
t.Fatalf("unexpected ids in run log: %#v", log) t.Fatalf("unexpected ids in run log: %#v", log)
} }
if !log.Matched || !log.FinalSelected || log.SkillCode != "refund_skill" { if !log.Matched || !log.FinalSelected || log.SkillDefinitionID != 44 {
t.Fatalf("expected matched skill log, got %#v", log) t.Fatalf("expected matched skill log, got %#v", log)
} }
if log.MatchReason != "llm_route" { if log.MatchReason != "llm_route" {
+9 -23
View File
@@ -2,13 +2,10 @@ package skills
import ( import (
"context" "context"
"strings"
"agent-desk/internal/models" "agent-desk/internal/models"
"agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx" "agent-desk/internal/pkg/errorsx"
"github.com/mlogclub/simple/common/strs"
) )
type intentTriggerConfig struct { type intentTriggerConfig struct {
@@ -18,45 +15,34 @@ type intentTriggerConfig struct {
// MatchSkill 对单个 SkillDefinition 执行命中判断。 // MatchSkill 对单个 SkillDefinition 执行命中判断。
func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefinition, string, *RouteTrace, error) { func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefinition, string, *RouteTrace, error) {
loader := newCandidateLoader() loader := newCandidateLoader()
if strs.IsNotBlank(ctx.ManualSkillCode) { if ctx.ManualSkillDefinitionID > 0 {
skill := loader.findManualSkillDefinition(ctx.ManualSkillCode) skill := loader.findManualSkillDefinition(ctx.ManualSkillDefinitionID)
if skill == nil || skill.Status != enums.StatusOk { if skill == nil || skill.Status != enums.StatusOk {
return nil, "", nil, errorsx.InvalidParamI18n("error.e0054") return nil, "", nil, errorsx.InvalidParamI18n("error.e0054")
} }
return skill, "manual_skill_code", &RouteTrace{ return skill, "manual_skill_id", &RouteTrace{
Status: "manual_selected", Status: "manual_selected",
SelectedSkillCode: skill.Code, SelectedSkillID: skill.ID,
}, nil }, nil
} }
candidates := loader.loadCandidateSkills(ctx.AIAgent) candidates := loader.loadCandidateSkills(ctx.AIAgent)
trace := &RouteTrace{ trace := &RouteTrace{
Status: "started", Status: "started",
CandidateSkillCodes: make([]string, 0, len(candidates)), CandidateSkillIDs: make([]int64, 0, len(candidates)),
} }
for _, item := range candidates { for _, item := range candidates {
trace.CandidateSkillCodes = append(trace.CandidateSkillCodes, item.Code) trace.CandidateSkillIDs = append(trace.CandidateSkillIDs, item.ID)
} }
if len(candidates) == 0 { if len(candidates) == 0 {
trace.Status = "no_candidate" trace.Status = "no_candidate"
return nil, "no_enabled_skill_bound", trace, nil return nil, "no_enabled_skill_bound", trace, nil
} }
intentCode := strings.TrimSpace(ctx.IntentCode)
if intentCode != "" {
for _, item := range candidates {
if strings.EqualFold(strings.TrimSpace(item.Code), intentCode) {
trace.Status = "intent_selected"
trace.SelectedSkillCode = item.Code
return &item, "intent_code", trace, nil
}
}
}
selected, routeTrace, err := routeSkillWithLLM(execCtx, ctx, candidates) selected, routeTrace, err := routeSkillWithLLM(execCtx, ctx, candidates)
if routeTrace != nil { if routeTrace != nil {
trace.Status = routeTrace.Status trace.Status = routeTrace.Status
trace.SelectedSkillCode = routeTrace.SelectedSkillCode trace.SelectedSkillID = routeTrace.SelectedSkillID
trace.RawDecision = routeTrace.RawDecision trace.RawDecision = routeTrace.RawDecision
trace.LatencyMs = routeTrace.LatencyMs trace.LatencyMs = routeTrace.LatencyMs
trace.Error = routeTrace.Error trace.Error = routeTrace.Error
+13 -6
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@@ -13,10 +14,10 @@ import (
"github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/common/strs"
) )
const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillCode,或者返回 NONE。 const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillId,或者返回 NONE。
只有当用户问题与 Skill 的职责边界明确匹配时才选择; 只有当用户问题与 Skill 的职责边界明确匹配时才选择;
如果不明确、信息不足、多个 Skill 都不够确定,就返回 NONE。 如果不明确、信息不足、多个 Skill 都不够确定,就返回 NONE。
输出只能是 skillCode 或 NONE,不能输出其他内容。` 输出只能是 skillId 或 NONE,不能输出其他内容。`
func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) { func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) {
trace := &RouteTrace{Status: "started"} trace := &RouteTrace{Status: "started"}
@@ -43,10 +44,16 @@ func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidate
trace.Status = "not_matched" trace.Status = "not_matched"
return nil, trace, nil return nil, trace, nil
} }
selectedID, parseErr := strconv.ParseInt(decision, 10, 64)
if parseErr != nil || selectedID <= 0 {
trace.Status = "invalid_decision"
trace.Error = fmt.Sprintf("invalid route decision: %s", decision)
return nil, trace, nil
}
for _, item := range candidates { for _, item := range candidates {
if strings.EqualFold(item.Code, decision) { if item.ID == selectedID {
trace.Status = "llm_selected" trace.Status = "llm_selected"
trace.SelectedSkillCode = item.Code trace.SelectedSkillID = item.ID
return &item, trace, nil return &item, trace, nil
} }
} }
@@ -62,14 +69,14 @@ func buildSkillRoutePrompt(userMessage string, candidates []models.SkillDefiniti
lines = append(lines, "") lines = append(lines, "")
lines = append(lines, "候选 Skills") lines = append(lines, "候选 Skills")
for _, item := range candidates { for _, item := range candidates {
line := fmt.Sprintf("- skillCode=%s; name=%s; description=%s", strings.TrimSpace(item.Code), strings.TrimSpace(item.Name), strings.TrimSpace(item.Description)) line := fmt.Sprintf("- skillId=%d; name=%s; description=%s", item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description))
if examples := parseSkillExamples(item.Examples); len(examples) > 0 { if examples := parseSkillExamples(item.Examples); len(examples) > 0 {
line += "; examples=" + strings.Join(examples, " | ") line += "; examples=" + strings.Join(examples, " | ")
} }
lines = append(lines, line) lines = append(lines, line)
} }
lines = append(lines, "") lines = append(lines, "")
lines = append(lines, "请只输出一个 skillCode 或 NONE。") lines = append(lines, "请只输出一个 skillId 或 NONE。")
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }
+5 -5
View File
@@ -18,7 +18,7 @@ func TestParseSkillExamples(t *testing.T) {
} }
func TestNormalizeRouteDecision(t *testing.T) { func TestNormalizeRouteDecision(t *testing.T) {
if got := normalizeRouteDecision("```refund_skill```\n补充说明"); got != "refund_skill" { if got := normalizeRouteDecision("```44```\n补充说明"); got != "44" {
t.Fatalf("unexpected normalized decision: %q", got) t.Fatalf("unexpected normalized decision: %q", got)
} }
if got := normalizeRouteDecision(" none "); got != "NONE" { if got := normalizeRouteDecision(" none "); got != "NONE" {
@@ -29,20 +29,20 @@ func TestNormalizeRouteDecision(t *testing.T) {
func TestBuildSkillRoutePrompt(t *testing.T) { func TestBuildSkillRoutePrompt(t *testing.T) {
prompt := buildSkillRoutePrompt("我要申请退款", []models.SkillDefinition{ prompt := buildSkillRoutePrompt("我要申请退款", []models.SkillDefinition{
{ {
Code: "refund_skill", ID: 44,
Name: "退款处理", Name: "退款处理",
Description: "负责退款和退货相关问题", Description: "负责退款和退货相关问题",
Examples: `["退款进度","退货运费"]`, Examples: `["退款进度","退货运费"]`,
}, },
}) })
if !strings.Contains(prompt, "skillCode=refund_skill") { if !strings.Contains(prompt, "skillId=44") {
t.Fatalf("expected prompt to include skill code, got %q", prompt) t.Fatalf("expected prompt to include skill id, got %q", prompt)
} }
if !strings.Contains(prompt, "examples=退款进度 | 退货运费") { if !strings.Contains(prompt, "examples=退款进度 | 退货运费") {
t.Fatalf("expected prompt to include examples, got %q", prompt) t.Fatalf("expected prompt to include examples, got %q", prompt)
} }
if !strings.Contains(prompt, "请只输出一个 skillCode 或 NONE。") { if !strings.Contains(prompt, "请只输出一个 skillId 或 NONE。") {
t.Fatalf("expected prompt to include output constraint, got %q", prompt) t.Fatalf("expected prompt to include output constraint, got %q", prompt)
} }
} }
+6 -8
View File
@@ -19,13 +19,12 @@ type RunLogService struct{}
// Build 根据执行计划与运行结果构建 Skill 运行日志。 // Build 根据执行计划与运行结果构建 Skill 运行日志。
func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog { func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog {
log := &models.SkillRunLog{ log := &models.SkillRunLog{
ConversationID: ctx.ConversationID, ConversationID: ctx.ConversationID,
AIAgentID: ctx.AIAgent.ID, AIAgentID: ctx.AIAgent.ID,
ManualSkillCode: ctx.ManualSkillCode, ManualSkillID: ctx.ManualSkillDefinitionID,
IntentCode: ctx.IntentCode, UserMessage: ctx.UserMessage,
UserMessage: ctx.UserMessage, TraceData: s.buildTraceData(trace),
TraceData: s.buildTraceData(trace), CreatedAt: time.Now(),
CreatedAt: time.Now(),
} }
if plan != nil { if plan != nil {
log.AIConfigID = plan.AIConfig.ID log.AIConfigID = plan.AIConfig.ID
@@ -34,7 +33,6 @@ func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *Ex
if plan.Skill != nil { if plan.Skill != nil {
log.SkillDefinitionID = plan.Skill.ID log.SkillDefinitionID = plan.Skill.ID
log.SkillCode = plan.Skill.Code
log.Matched = true log.Matched = true
log.FinalSelected = true log.FinalSelected = true
log.MatchReason = plan.MatchReason log.MatchReason = plan.MatchReason
+11 -12
View File
@@ -4,12 +4,11 @@ import "agent-desk/internal/models"
// RuntimeContext 表示一次 Skill 运行的输入上下文。 // RuntimeContext 表示一次 Skill 运行的输入上下文。
type RuntimeContext struct { type RuntimeContext struct {
AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。 AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。 AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
UserMessage string // UserMessage 为当前用户输入。 UserMessage string // UserMessage 为当前用户输入。
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。 ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
ManualSkillCode string // ManualSkillCode 为显式指定的 Skill 编码 ManualSkillDefinitionID int64 // ManualSkillDefinitionID 为显式指定的 Skill 定义ID
IntentCode string // IntentCode 为上游识别出的意图编码。
} }
// ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。 // ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。
@@ -35,12 +34,12 @@ type ExecutionTrace struct {
} }
type RouteTrace struct { type RouteTrace struct {
Status string `json:"status"` Status string `json:"status"`
CandidateSkillCodes []string `json:"candidateSkillCodes,omitempty"` CandidateSkillIDs []int64 `json:"candidateSkillIds,omitempty"`
SelectedSkillCode string `json:"selectedSkillCode,omitempty"` SelectedSkillID int64 `json:"selectedSkillId,omitempty"`
RawDecision string `json:"rawDecision,omitempty"` RawDecision string `json:"rawDecision,omitempty"`
LatencyMs int64 `json:"latencyMs,omitempty"` LatencyMs int64 `json:"latencyMs,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type PromptTrace struct { type PromptTrace struct {
+1 -1
View File
@@ -22,7 +22,7 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse {
AIConfigID: item.AIConfigID, AIConfigID: item.AIConfigID,
UserMessage: item.UserMessage, UserMessage: item.UserMessage,
PlannedAction: item.PlannedAction, PlannedAction: item.PlannedAction,
PlannedSkillCode: item.PlannedSkillCode, PlannedSkillID: item.PlannedSkillID,
PlannedSkillName: item.PlannedSkillName, PlannedSkillName: item.PlannedSkillName,
SkillRouteTrace: item.SkillRouteTrace, SkillRouteTrace: item.SkillRouteTrace,
ToolSearchTrace: item.ToolSearchTrace, ToolSearchTrace: item.ToolSearchTrace,
-1
View File
@@ -19,7 +19,6 @@ func BuildSkillDefinitionResponse(item *models.SkillDefinition) response.SkillDe
} }
return response.SkillDefinitionResponse{ return response.SkillDefinitionResponse{
ID: item.ID, ID: item.ID,
Code: item.Code,
Name: item.Name, Name: item.Name,
Description: item.Description, Description: item.Description,
Instruction: item.Instruction, Instruction: item.Instruction,
@@ -25,7 +25,7 @@ func AgentRunLogAnyList(ctx *gin.Context) {
params.QueryFilter{ParamName: "requestId"}, params.QueryFilter{ParamName: "requestId"},
params.QueryFilter{ParamName: "aiAgentId"}, params.QueryFilter{ParamName: "aiAgentId"},
params.QueryFilter{ParamName: "plannedAction"}, params.QueryFilter{ParamName: "plannedAction"},
params.QueryFilter{ParamName: "plannedSkillCode", Op: params.Like}, params.QueryFilter{ParamName: "plannedSkillId"},
params.QueryFilter{ParamName: "graphToolCode"}, params.QueryFilter{ParamName: "graphToolCode"},
params.QueryFilter{ParamName: "interruptType"}, params.QueryFilter{ParamName: "interruptType"},
params.QueryFilter{ParamName: "resumeSource"}, params.QueryFilter{ParamName: "resumeSource"},
@@ -215,7 +215,6 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
if skill := services.SkillDefinitionService.Get(id); skill != nil { if skill := services.SkillDefinitionService.Get(id); skill != nil {
ret.Skills = append(ret.Skills, response.AIAgentSkillResponse{ ret.Skills = append(ret.Skills, response.AIAgentSkillResponse{
ID: skill.ID, ID: skill.ID,
Code: skill.Code,
Name: skill.Name, Name: skill.Name,
}) })
} }
@@ -28,7 +28,6 @@ func SkillDefinitionAnyList(ctx *gin.Context) {
cnd := params.NewPagedSqlCnd(ctx, cnd := params.NewPagedSqlCnd(ctx,
params.QueryFilter{ParamName: "status"}, params.QueryFilter{ParamName: "status"},
params.QueryFilter{ParamName: "name", Op: params.Like}, params.QueryFilter{ParamName: "name", Op: params.Like},
params.QueryFilter{ParamName: "code", Op: params.Like},
).Desc("id") ).Desc("id")
if _, ok := params.Get(ctx, "status"); !ok { if _, ok := params.Get(ctx, "status"); !ok {
cnd.Where("status <> ?", enums.StatusDeleted) cnd.Where("status <> ?", enums.StatusDeleted)
+24 -27
View File
@@ -818,37 +818,34 @@ type KnowledgeFeedback struct {
// SkillDefinition 表示可由后台配置并参与运行时路由的 Skill 定义。 // SkillDefinition 表示可由后台配置并参与运行时路由的 Skill 定义。
type SkillDefinition struct { type SkillDefinition struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 主键。 ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 主键。
Code string `gorm:"type:varchar(100);not null;default:'';uniqueIndex"` // Code 为 Skill 的稳定唯一编码,供程序内部引用和路由判断使用,例如 refund_skill Name string `gorm:"type:varchar(100);not null;default:'';index"` // Name 为 Skill 的展示名称,用于后台列表、配置页和人工选择场景
Name string `gorm:"type:varchar(100);not null;default:'';index"` // Name 为 Skill 的展示名称,用于后台列表、配置页和人工选择场景 Description string `gorm:"type:varchar(255);not null;default:''"` // Description 为 Skill 的简要说明,用于描述该 Skill 的适用场景和职责边界
Description string `gorm:"type:varchar(255);not null;default:''"` // Description 为 Skill 的简要说明,用于描述该 Skill 的适用场景和职责边界 Instruction string `gorm:"type:longtext"` // Instruction 为 Skill 的主体说明文档存储字段,使用 Markdown 编写,供 Agent 理解任务目标、步骤和工具使用要求
Instruction string `gorm:"type:longtext"` // Instruction 为 Skill 的主体说明文档存储字段,使用 Markdown 编写,供 Agent 理解任务目标、步骤和工具使用要求 Examples string `gorm:"type:text"` // Examples 为示例问法 JSON 数组字符串
Examples string `gorm:"type:text"` // Examples 为示例问法 JSON 数组字符串。 ToolWhitelist string `gorm:"type:text"` // ToolWhitelist 为允许使用的工具编码 JSON 数组字符串。
ToolWhitelist string `gorm:"type:text"` // ToolWhitelist 为允许使用的工具编码 JSON 数组字符串 Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 为 Skill 当前状态,使用全局通用状态:0启用 1禁用 2删除
Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 为 Skill 当前状态,使用全局通用状态:0启用 1禁用 2删除 Remark string `gorm:"type:text"` // Remark 为后台备注,用于记录配置说明、维护信息或内部协作信息
Remark string `gorm:"type:text"` // Remark 为后台备注,用于记录配置说明、维护信息或内部协作信息。
AuditFields AuditFields
} }
// SkillRunLog 表示一次 Skill 运行过程的审计日志。 // SkillRunLog 表示一次 Skill 运行过程的审计日志。
type SkillRunLog struct { type SkillRunLog struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 运行日志主键。 ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为 Skill 运行日志主键。
ConversationID int64 `gorm:"type:bigint;not null;default:0;index"` // ConversationID 为关联会话ID,无会话上下文时为0。 ConversationID int64 `gorm:"type:bigint;not null;default:0;index"` // ConversationID 为关联会话ID,无会话上下文时为0。
AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为本次运行所属的 AI Agent ID。 AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为本次运行所属的 AI Agent ID。
AIConfigID int64 `gorm:"type:bigint;not null;default:0;index"` // AIConfigID 为本次运行实际使用的 AI 配置ID。 AIConfigID int64 `gorm:"type:bigint;not null;default:0;index"` // AIConfigID 为本次运行实际使用的 AI 配置ID。
SkillDefinitionID int64 `gorm:"type:bigint;not null;default:0;index"` // SkillDefinitionID 为最终命中的 Skill 定义ID,未命中时为0。 SkillDefinitionID int64 `gorm:"type:bigint;not null;default:0;index"` // SkillDefinitionID 为最终命中的 Skill 定义ID,未命中时为0。
SkillCode string `gorm:"type:varchar(100);not null;default:'';index"` // SkillCode 为最终命中的 Skill 编码,未命中时为空 ManualSkillID int64 `gorm:"type:bigint;not null;default:0;index"` // ManualSkillID 为本次请求显式指定的 Skill 定义ID
ManualSkillCode string `gorm:"type:varchar(100);not null;default:'';index"` // ManualSkillCode 为本次请求显式指定的 Skill 编码 UserMessage string `gorm:"type:longtext"` // UserMessage 为本次请求的用户输入内容
IntentCode string `gorm:"type:varchar(100);not null;default:'';index"` // IntentCode 为上游传入的意图编码 Matched bool `gorm:"not null;default:false;index"` // Matched 表示本次请求是否命中了 Skill
UserMessage string `gorm:"type:longtext"` // UserMessage 为本次请求的用户输入内容 MatchReason string `gorm:"type:varchar(500);not null;default:''"` // MatchReason 为命中或未命中的原因说明
Matched bool `gorm:"not null;default:false;index"` // Matched 表示本次请求是否命中了 Skill。 FinalSelected bool `gorm:"not null;default:false;index"` // FinalSelected 表示该日志记录的 Skill 是否为最终选中的执行 Skill。
MatchReason string `gorm:"type:varchar(500);not null;default:''"` // MatchReason 为命中或未命中的原因说明 UsedModel string `gorm:"type:varchar(100);not null;default:''"` // UsedModel 为本次实际调用的模型名称
FinalSelected bool `gorm:"not null;default:false;index"` // FinalSelected 表示该日志记录的 Skill 是否为最终选中的执行 Skill UsedProvider enums.AIProvider `gorm:"type:varchar(50);not null;default:''"` // UsedProvider 为本次实际调用的模型供应商
UsedModel string `gorm:"type:varchar(100);not null;default:''"` // UsedModel 为本次实际调用的模型名称 ErrorMessage string `gorm:"type:text"` // ErrorMessage 为运行过程中的错误信息
UsedProvider enums.AIProvider `gorm:"type:varchar(50);not null;default:''"` // UsedProvider 为本次实际调用的模型供应商 TraceData string `gorm:"type:text"` // TraceData 为 Skill 执行链路追踪数据JSON
ErrorMessage string `gorm:"type:text"` // ErrorMessage 为运行过程中的错误信息 CreatedAt time.Time `gorm:"type:datetime;not null;index"` // CreatedAt 为运行日志创建时间
TraceData string `gorm:"type:text"` // TraceData 为 Skill 执行链路追踪数据JSON。
CreatedAt time.Time `gorm:"type:datetime;not null;index"` // CreatedAt 为运行日志创建时间。
} }
// AgentRunLog 表示一次客服 Agent 自动运行的总链路日志。 // AgentRunLog 表示一次客服 Agent 自动运行的总链路日志。
@@ -861,7 +858,7 @@ type AgentRunLog struct {
AIConfigID int64 `gorm:"type:bigint;not null;default:0;index"` AIConfigID int64 `gorm:"type:bigint;not null;default:0;index"`
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"` PlannedSkillID int64 `gorm:"type:bigint;not null;default:0;index"`
PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"` PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"`
SkillRouteTrace string `gorm:"type:text"` SkillRouteTrace string `gorm:"type:text"`
ToolSearchTrace string `gorm:"type:text"` ToolSearchTrace string `gorm:"type:text"`
+4 -6
View File
@@ -2,12 +2,10 @@ package request
type SkillDefinitionListRequest struct { type SkillDefinitionListRequest struct {
Name string `json:"name"` Name string `json:"name"`
Code string `json:"code"`
Status int `json:"status"` Status int `json:"status"`
} }
type CreateSkillDefinitionRequest struct { type CreateSkillDefinitionRequest struct {
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Instruction string `json:"instruction"` Instruction string `json:"instruction"`
@@ -35,10 +33,10 @@ type UpdateSkillDefinitionStatusRequest struct {
} }
type SkillDebugRunRequest struct { type SkillDebugRunRequest struct {
AIAgentID int64 `json:"aiAgentId"` AIAgentID int64 `json:"aiAgentId"`
ConversationID int64 `json:"conversationId"` ConversationID int64 `json:"conversationId"`
SkillCode string `json:"skillCode"` SkillDefinitionID int64 `json:"skillDefinitionId"`
UserMessage string `json:"userMessage"` UserMessage string `json:"userMessage"`
} }
type SkillDebugResumeRequest struct { type SkillDebugResumeRequest struct {
-1
View File
@@ -12,7 +12,6 @@ type AIAgentTeamResponse struct {
type AIAgentSkillResponse struct { type AIAgentSkillResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
} }
+19 -20
View File
@@ -4,7 +4,6 @@ import "time"
type SkillDefinitionResponse struct { type SkillDefinitionResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Instruction string `json:"instruction"` Instruction string `json:"instruction"`
@@ -20,24 +19,24 @@ type SkillDefinitionResponse struct {
} }
type SkillDebugRunResponse struct { type SkillDebugRunResponse struct {
SkillCode string `json:"skillCode"` SkillDefinitionID int64 `json:"skillDefinitionId"`
SkillName string `json:"skillName"` SkillName string `json:"skillName"`
ReplyText string `json:"replyText"` ReplyText string `json:"replyText"`
PlanReason string `json:"planReason"` PlanReason string `json:"planReason"`
SkillRouteTrace string `json:"skillRouteTrace"` SkillRouteTrace string `json:"skillRouteTrace"`
ToolWhitelist []string `json:"toolWhitelist"` ToolWhitelist []string `json:"toolWhitelist"`
ExposedToolCodes []string `json:"exposedToolCodes"` ExposedToolCodes []string `json:"exposedToolCodes"`
InvokedToolCodes []string `json:"invokedToolCodes"` InvokedToolCodes []string `json:"invokedToolCodes"`
ToolSearchTrace string `json:"toolSearchTrace"` ToolSearchTrace string `json:"toolSearchTrace"`
GraphToolTrace string `json:"graphToolTrace"` GraphToolTrace string `json:"graphToolTrace"`
GraphToolCode string `json:"graphToolCode"` GraphToolCode string `json:"graphToolCode"`
InterruptType string `json:"interruptType"` InterruptType string `json:"interruptType"`
CheckPointID string `json:"checkPointId"` CheckPointID string `json:"checkPointId"`
Interrupted bool `json:"interrupted"` Interrupted bool `json:"interrupted"`
TraceData string `json:"traceData"` TraceData string `json:"traceData"`
ErrorMessage string `json:"errorMessage"` ErrorMessage string `json:"errorMessage"`
ConversationID int64 `json:"conversationId"` ConversationID int64 `json:"conversationId"`
AIAgentID int64 `json:"aiAgentId"` AIAgentID int64 `json:"aiAgentId"`
} }
type AgentRunLogResponse struct { type AgentRunLogResponse struct {
@@ -49,7 +48,7 @@ type AgentRunLogResponse struct {
AIConfigID int64 `json:"aiConfigId"` AIConfigID int64 `json:"aiConfigId"`
UserMessage string `json:"userMessage"` UserMessage string `json:"userMessage"`
PlannedAction string `json:"plannedAction"` PlannedAction string `json:"plannedAction"`
PlannedSkillCode string `json:"plannedSkillCode"` PlannedSkillID int64 `json:"plannedSkillId"`
PlannedSkillName string `json:"plannedSkillName"` PlannedSkillName string `json:"plannedSkillName"`
SkillRouteTrace string `json:"skillRouteTrace"` SkillRouteTrace string `json:"skillRouteTrace"`
ToolSearchTrace string `json:"toolSearchTrace"` ToolSearchTrace string `json:"toolSearchTrace"`
+3 -3
View File
@@ -54,8 +54,8 @@ error.e0053: "Skill not found."
error.e0054: "Skill not found or not enabled." error.e0054: "Skill not found or not enabled."
error.e0055: "Enter a Skill name." error.e0055: "Enter a Skill name."
error.e0056: "Skill is not enabled." error.e0056: "Skill is not enabled."
error.e0057: "Enter a Skill code." error.e0057: "Skill is required."
error.e0058: "This Skill code is already in use." error.e0058: "This Skill already exists."
error.e0059: "Web channel position must be left or right." error.e0059: "Web channel position must be left or right."
error.e0060: "Invalid web channel configuration." error.e0060: "Invalid web channel configuration."
error.e0061: "AI Agent ID is required." error.e0061: "AI Agent ID is required."
@@ -68,7 +68,7 @@ error.e0067: "Invalid index status."
error.e0068: "openKfID is required." error.e0068: "openKfID is required."
error.e0069: "This openKfId is already used by another channel." error.e0069: "This openKfId is already used by another channel."
error.e0070: "Server code is required." error.e0070: "Server code is required."
error.e0071: "Skill code is required." error.e0071: "Skill definition ID is required."
error.e0072: "Ticket is required." error.e0072: "Ticket is required."
error.e0073: "The MCP server bound to this tool code does not exist or is not enabled." error.e0073: "The MCP server bound to this tool code does not exist or is not enabled."
error.e0074: "Tool code is required." error.e0074: "Tool code is required."
+3 -3
View File
@@ -54,8 +54,8 @@ error.e0053: "Skill 不存在"
error.e0054: "Skill 不存在或未启用" error.e0054: "Skill 不存在或未启用"
error.e0055: "Skill 名称不能为空" error.e0055: "Skill 名称不能为空"
error.e0056: "Skill 未启用" error.e0056: "Skill 未启用"
error.e0057: "Skill 编码不能为空" error.e0057: "Skill 参数不能为空"
error.e0058: "Skill 编码已存在" error.e0058: "Skill 已存在"
error.e0059: "Web渠道配置 position 只能为 left 或 right" error.e0059: "Web渠道配置 position 只能为 left 或 right"
error.e0060: "Web渠道配置不合法" error.e0060: "Web渠道配置不合法"
error.e0061: "aiAgentId不能为空" error.e0061: "aiAgentId不能为空"
@@ -68,7 +68,7 @@ error.e0067: "indexStatus参数不合法"
error.e0068: "openKfID不能为空" error.e0068: "openKfID不能为空"
error.e0069: "openKfId 已被其他渠道使用" error.e0069: "openKfId 已被其他渠道使用"
error.e0070: "serverCode不能为空" error.e0070: "serverCode不能为空"
error.e0071: "skillCode不能为空" error.e0071: "skillDefinitionId不能为空"
error.e0072: "ticket 不能为空" error.e0072: "ticket 不能为空"
error.e0073: "toolCode 绑定的 MCP 服务不存在或未启用" error.e0073: "toolCode 绑定的 MCP 服务不存在或未启用"
error.e0074: "toolCode不能为空" error.e0074: "toolCode不能为空"
@@ -101,10 +101,6 @@ func (r *skillDefinitionRepository) Delete(db *gorm.DB, id int64) {
db.Delete(&models.SkillDefinition{}, "id = ?", id) db.Delete(&models.SkillDefinition{}, "id = ?", id)
} }
func (r *skillDefinitionRepository) GetByCode(db *gorm.DB, code string) *models.SkillDefinition {
return r.FindOne(db, sqls.NewCnd().Where("code = ?", code))
}
func (r *skillDefinitionRepository) GetByIDs(db *gorm.DB, ids []int64) map[int64]models.SkillDefinition { func (r *skillDefinitionRepository) GetByIDs(db *gorm.DB, ids []int64) map[int64]models.SkillDefinition {
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
@@ -76,10 +76,6 @@ func (s *skillDefinitionService) Delete(id int64) {
repositories.SkillDefinitionRepository.Delete(sqls.DB(), id) repositories.SkillDefinitionRepository.Delete(sqls.DB(), id)
} }
func (s *skillDefinitionService) GetByCode(code string) *models.SkillDefinition {
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), code)
}
func (s *skillDefinitionService) GetByIDs(ids []int64) map[int64]models.SkillDefinition { func (s *skillDefinitionService) GetByIDs(ids []int64) map[int64]models.SkillDefinition {
return repositories.SkillDefinitionRepository.GetByIDs(sqls.DB(), ids) return repositories.SkillDefinitionRepository.GetByIDs(sqls.DB(), ids)
} }
@@ -92,11 +88,7 @@ func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDe
if err != nil { if err != nil {
return nil, err return nil, err
} }
if s.Take("code = ?", normalized.Code) != nil {
return nil, errorsx.InvalidParamI18n("error.e0058")
}
item := &models.SkillDefinition{ item := &models.SkillDefinition{
Code: normalized.Code,
Name: normalized.Name, Name: normalized.Name,
Description: normalized.Description, Description: normalized.Description,
Instruction: normalized.Instruction, Instruction: normalized.Instruction,
@@ -127,11 +119,7 @@ func (s *skillDefinitionService) UpdateSkillDefinition(req request.UpdateSkillDe
if err != nil { if err != nil {
return err return err
} }
if exists := s.Take("code = ? AND id <> ?", normalized.Code, req.ID); exists != nil {
return errorsx.InvalidParamI18n("error.e0058")
}
return repositories.SkillDefinitionRepository.Updates(sqls.DB(), req.ID, map[string]any{ return repositories.SkillDefinitionRepository.Updates(sqls.DB(), req.ID, map[string]any{
"code": normalized.Code,
"name": normalized.Name, "name": normalized.Name,
"description": normalized.Description, "description": normalized.Description,
"instruction": normalized.Instruction, "instruction": normalized.Instruction,
@@ -146,15 +134,11 @@ func (s *skillDefinitionService) UpdateSkillDefinition(req request.UpdateSkillDe
func (s *skillDefinitionService) normalizeSkillDefinitionRequest(req request.CreateSkillDefinitionRequest) (*request.CreateSkillDefinitionRequest, error) { func (s *skillDefinitionService) normalizeSkillDefinitionRequest(req request.CreateSkillDefinitionRequest) (*request.CreateSkillDefinitionRequest, error) {
normalized := &request.CreateSkillDefinitionRequest{ normalized := &request.CreateSkillDefinitionRequest{
Code: strings.TrimSpace(req.Code),
Name: strings.TrimSpace(req.Name), Name: strings.TrimSpace(req.Name),
Description: strings.TrimSpace(req.Description), Description: strings.TrimSpace(req.Description),
Instruction: strings.TrimSpace(req.Instruction), Instruction: strings.TrimSpace(req.Instruction),
Remark: strings.TrimSpace(req.Remark), Remark: strings.TrimSpace(req.Remark),
} }
if normalized.Code == "" {
return nil, errorsx.InvalidParamI18n("error.e0057")
}
if normalized.Name == "" { if normalized.Name == "" {
return nil, errorsx.InvalidParamI18n("error.e0055") return nil, errorsx.InvalidParamI18n("error.e0055")
} }
+1 -1
View File
@@ -24,7 +24,7 @@ func (s *skillRuntimeService) DebugRun(ctx context.Context, req request.SkillDeb
if req.AIAgentID <= 0 { if req.AIAgentID <= 0 {
return nil, errorsx.InvalidParamI18n("error.e0061") return nil, errorsx.InvalidParamI18n("error.e0061")
} }
if strings.TrimSpace(req.SkillCode) == "" { if req.SkillDefinitionID <= 0 {
return nil, errorsx.InvalidParamI18n("error.e0071") return nil, errorsx.InvalidParamI18n("error.e0071")
} }
if strings.TrimSpace(req.UserMessage) == "" { if strings.TrimSpace(req.UserMessage) == "" {
@@ -122,7 +122,7 @@ export function AgentRunLogDetailDialog({
title={t("agentRunLog.planningStage")} title={t("agentRunLog.planningStage")}
lines={[ lines={[
`plannedAction: ${activeLog.plannedAction || "-"}`, `plannedAction: ${activeLog.plannedAction || "-"}`,
`plannedSkillCode: ${activeLog.plannedSkillCode || "-"}`, `plannedSkillId: ${activeLog.plannedSkillId || "-"}`,
`plannedSkillName: ${activeLog.plannedSkillName || "-"}`, `plannedSkillName: ${activeLog.plannedSkillName || "-"}`,
`graphToolCode: ${activeLog.graphToolCode || "-"}`, `graphToolCode: ${activeLog.graphToolCode || "-"}`,
`recommendedAction: ${activeLog.recommendedAction || "-"}`, `recommendedAction: ${activeLog.recommendedAction || "-"}`,
+9 -4
View File
@@ -239,14 +239,19 @@ export default function DashboardAgentRunLogsPage() {
</div> </div>
<div className="min-w-0 text-sm"> <div className="min-w-0 text-sm">
{item.plannedSkillCode || item.graphToolCode || item.plannedToolCode ? ( {item.plannedSkillId || item.graphToolCode || item.plannedToolCode ? (
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<div className="truncate font-medium"> <div className="truncate font-medium">
{item.plannedSkillCode || item.graphToolCode || item.plannedToolCode} {item.plannedSkillName ||
(item.plannedSkillId
? `Skill #${item.plannedSkillId}`
: "") ||
item.graphToolCode ||
item.plannedToolCode}
</div> </div>
{item.plannedSkillName ? ( {item.plannedSkillId ? (
<div className="truncate text-xs text-muted-foreground"> <div className="truncate text-xs text-muted-foreground">
{item.plannedSkillName} Skill #{item.plannedSkillId}
</div> </div>
) : item.handoffReason ? ( ) : item.handoffReason ? (
<div className="truncate text-xs text-muted-foreground"> <div className="truncate text-xs text-muted-foreground">
@@ -33,7 +33,7 @@ import { useI18n } from "@/i18n/provider"
type DebugDialogProps = { type DebugDialogProps = {
open: boolean open: boolean
skillCode: string skillDefinitionId: number
skillName: string skillName: string
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
} }
@@ -96,7 +96,7 @@ function ResultBlock({
export function DebugDialog({ export function DebugDialog({
open, open,
skillCode, skillDefinitionId,
skillName, skillName,
onOpenChange, onOpenChange,
}: DebugDialogProps) { }: DebugDialogProps) {
@@ -106,9 +106,9 @@ export function DebugDialog({
return ( return (
<DebugDialogBody <DebugDialogBody
key={skillCode} key={skillDefinitionId}
open={open} open={open}
skillCode={skillCode} skillDefinitionId={skillDefinitionId}
skillName={skillName} skillName={skillName}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
/> />
@@ -117,12 +117,12 @@ export function DebugDialog({
function DebugDialogBody({ function DebugDialogBody({
open, open,
skillCode, skillDefinitionId,
skillName, skillName,
onOpenChange, onOpenChange,
}: DebugDialogProps) { }: DebugDialogProps) {
const t = useI18n() const t = useI18n()
const formId = `skill-debug-form-${skillCode}` const formId = `skill-debug-form-${skillDefinitionId}`
const [running, setRunning] = useState(false) const [running, setRunning] = useState(false)
const [resuming, setResuming] = useState(false) const [resuming, setResuming] = useState(false)
const [aiAgents, setAiAgents] = useState<AIAgent[]>([]) const [aiAgents, setAiAgents] = useState<AIAgent[]>([])
@@ -198,7 +198,7 @@ function DebugDialogBody({
async function onSubmit(values: DebugForm) { async function onSubmit(values: DebugForm) {
const payload: SkillDebugRunPayload = { const payload: SkillDebugRunPayload = {
aiAgentId: Number(values.aiAgentId), aiAgentId: Number(values.aiAgentId),
skillCode, skillDefinitionId,
userMessage: values.userMessage.trim(), userMessage: values.userMessage.trim(),
} }
const conversationId = Number(values.conversationId) const conversationId = Number(values.conversationId)
@@ -256,7 +256,7 @@ function DebugDialogBody({
<ProjectDialog <ProjectDialog
open={open} open={open}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
title={t("skillDefinition.debugTitle", { name: skillName || skillCode })} title={t("skillDefinition.debugTitle", { name: skillName })}
description={t("skillDefinition.debugDescription")} description={t("skillDefinition.debugDescription")}
size="xl" size="xl"
allowFullscreen allowFullscreen
@@ -322,7 +322,7 @@ function DebugDialogBody({
<Field> <Field>
<FieldLabel>Skill</FieldLabel> <FieldLabel>Skill</FieldLabel>
<FieldContent> <FieldContent>
<Input value={skillCode} disabled /> <Input value={skillName} disabled />
</FieldContent> </FieldContent>
</Field> </Field>
<Field> <Field>
@@ -359,7 +359,7 @@ function DebugDialogBody({
</CardHeader> </CardHeader>
<CardContent className="space-y-3 text-sm"> <CardContent className="space-y-3 text-sm">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge variant="outline">{result?.skillCode || skillCode}</Badge> <Badge variant="outline">{result?.skillName || skillName}</Badge>
{result?.graphToolCode ? ( {result?.graphToolCode ? (
<Badge variant="secondary">{result.graphToolCode}</Badge> <Badge variant="secondary">{result.graphToolCode}</Badge>
) : null} ) : null}
@@ -522,7 +522,7 @@ function DebugDialogBody({
</CardHeader> </CardHeader>
<CardContent className="space-y-3 text-sm"> <CardContent className="space-y-3 text-sm">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge variant="outline">{resumeResult.skillCode || skillCode}</Badge> <Badge variant="outline">{resumeResult.skillName || skillName}</Badge>
{resumeResult.graphToolCode ? ( {resumeResult.graphToolCode ? (
<Badge variant="secondary">{resumeResult.graphToolCode}</Badge> <Badge variant="secondary">{resumeResult.graphToolCode}</Badge>
) : null} ) : null}
@@ -37,7 +37,6 @@ type SkillEditDialogProps = {
}; };
const emptyForm: EditForm = { const emptyForm: EditForm = {
code: "",
name: "", name: "",
description: "", description: "",
instruction: "", instruction: "",
@@ -47,21 +46,15 @@ const emptyForm: EditForm = {
function createSkillFormSchema(t: TFunction) { function createSkillFormSchema(t: TFunction) {
return z.object({ return z.object({
code: z name: z.string().trim().min(1, t("skillDefinition.nameRequired")),
.string() description: z.string().trim(),
.trim() instruction: z.string().trim().min(1, t("skillDefinition.instructionRequired")),
.min(1, t("skillDefinition.codeRequired")) examplesText: z.string().trim(),
.regex(/^[a-zA-Z0-9_-]+$/, t("skillDefinition.codeInvalid")), remark: z.string().trim(),
name: z.string().trim().min(1, t("skillDefinition.nameRequired")),
description: z.string().trim(),
instruction: z.string().trim().min(1, t("skillDefinition.instructionRequired")),
examplesText: z.string().trim(),
remark: z.string().trim(),
}); });
} }
type EditForm = { type EditForm = {
code: string;
name: string; name: string;
description: string; description: string;
instruction: string; instruction: string;
@@ -75,7 +68,6 @@ function buildForm(item: SkillDefinition | null): EditForm {
} }
return { return {
code: item.code,
name: item.name, name: item.name,
description: item.description ?? "", description: item.description ?? "",
instruction: item.instruction ?? "", instruction: item.instruction ?? "",
@@ -89,7 +81,6 @@ function buildPayload(
toolWhitelist: string[], toolWhitelist: string[],
): CreateSkillDefinitionPayload { ): CreateSkillDefinitionPayload {
return { return {
code: form.code.trim(),
name: form.name.trim(), name: form.name.trim(),
description: form.description.trim(), description: form.description.trim(),
instruction: form.instruction.trim(), instruction: form.instruction.trim(),
@@ -276,32 +267,18 @@ function SkillEditDialogBody({
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="space-y-4" className="space-y-4"
> >
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <Field data-invalid={!!errors.name}>
<Field data-invalid={!!errors.code}> <FieldLabel htmlFor="skill-name">{t("skillDefinition.name")}</FieldLabel>
<FieldLabel htmlFor="skill-code">{t("skillDefinition.code")}</FieldLabel> <FieldContent>
<FieldContent> <Input
<Input id="skill-name"
id="skill-code" placeholder={t("skillDefinition.namePlaceholder")}
placeholder={t("skillDefinition.codePlaceholder")} aria-invalid={!!errors.name}
aria-invalid={!!errors.code} {...register("name")}
{...register("code")} />
/> <FieldError errors={[errors.name]} />
<FieldError errors={[errors.code]} /> </FieldContent>
</FieldContent> </Field>
</Field>
<Field data-invalid={!!errors.name}>
<FieldLabel htmlFor="skill-name">{t("skillDefinition.name")}</FieldLabel>
<FieldContent>
<Input
id="skill-name"
placeholder={t("skillDefinition.namePlaceholder")}
aria-invalid={!!errors.name}
{...register("name")}
/>
<FieldError errors={[errors.name]} />
</FieldContent>
</Field>
</div>
<Field data-invalid={!!errors.description}> <Field data-invalid={!!errors.description}>
<FieldLabel htmlFor="skill-description">{t("skillDefinition.description")}</FieldLabel> <FieldLabel htmlFor="skill-description">{t("skillDefinition.description")}</FieldLabel>
+1 -11
View File
@@ -73,14 +73,6 @@ export default function DashboardSkillsPage() {
trim: true, trim: true,
className: "w-full sm:w-72", className: "w-full sm:w-72",
}, },
{
name: "code",
label: t("skillDefinition.filterCode"),
placeholder: t("skillDefinition.filterCode"),
defaultValue: "",
trim: true,
className: "w-full sm:w-56",
},
{ {
name: "status", name: "status",
label: t("skillDefinition.allStatus"), label: t("skillDefinition.allStatus"),
@@ -108,7 +100,6 @@ export default function DashboardSkillsPage() {
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div className="font-medium">{item.name}</div> <div className="font-medium">{item.name}</div>
<Badge variant="outline">{item.code}</Badge>
<Badge variant="secondary"> <Badge variant="secondary">
{t("skillDefinition.whitelistCount", { {t("skillDefinition.whitelistCount", {
count: item.toolWhitelist.length, count: item.toolWhitelist.length,
@@ -185,7 +176,6 @@ export default function DashboardSkillsPage() {
fetchList={(query) => fetchList={(query) =>
fetchSkillDefinitions({ fetchSkillDefinitions({
name: typeof query.name === "string" ? query.name : undefined, name: typeof query.name === "string" ? query.name : undefined,
code: typeof query.code === "string" ? query.code : undefined,
status: typeof query.status === "number" ? query.status : undefined, status: typeof query.status === "number" ? query.status : undefined,
page: Number(query.page), page: Number(query.page),
limit: Number(query.limit), limit: Number(query.limit),
@@ -252,7 +242,7 @@ export default function DashboardSkillsPage() {
/> />
<DebugDialog <DebugDialog
open={debugDialogOpen} open={debugDialogOpen}
skillCode={debuggingItem?.code ?? ""} skillDefinitionId={debuggingItem?.id ?? 0}
skillName={debuggingItem?.name ?? ""} skillName={debuggingItem?.name ?? ""}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) setDebuggingItem(null); if (!open) setDebuggingItem(null);
+4 -6
View File
@@ -232,7 +232,7 @@ export type AIAgent = {
knowledgeIds: number[] knowledgeIds: number[]
knowledgeBaseNames: string[] knowledgeBaseNames: string[]
skillIds: number[] skillIds: number[]
skills: { id: number; code: string; name: string }[] skills: { id: number; name: string }[]
directTools: { directTools: {
toolCode: string toolCode: string
serverCode: string serverCode: string
@@ -292,7 +292,6 @@ export type UpdateAdminQuickReplyPayload = CreateAdminQuickReplyPayload & {
export type SkillDefinition = { export type SkillDefinition = {
id: number id: number
code: string
name: string name: string
description: string description: string
instruction: string instruction: string
@@ -308,7 +307,6 @@ export type SkillDefinition = {
} }
export type CreateSkillDefinitionPayload = { export type CreateSkillDefinitionPayload = {
code: string
name: string name: string
description: string description: string
instruction: string instruction: string
@@ -324,7 +322,7 @@ export type UpdateSkillDefinitionPayload = CreateSkillDefinitionPayload & {
export type SkillDebugRunPayload = { export type SkillDebugRunPayload = {
aiAgentId: number aiAgentId: number
conversationId?: number conversationId?: number
skillCode: string skillDefinitionId: number
userMessage: string userMessage: string
} }
@@ -336,7 +334,7 @@ export type SkillDebugResumePayload = {
} }
export type SkillDebugRunResult = { export type SkillDebugRunResult = {
skillCode: string skillDefinitionId: number
skillName: string skillName: string
replyText: string replyText: string
planReason: string planReason: string
@@ -415,7 +413,7 @@ export type AgentRunLog = {
aiConfigId: number aiConfigId: number
userMessage: string userMessage: string
plannedAction: string plannedAction: string
plannedSkillCode: string plannedSkillId: number
plannedSkillName: string plannedSkillName: string
skillRouteTrace: string skillRouteTrace: string
toolSearchTrace: string toolSearchTrace: string
-5
View File
@@ -1612,7 +1612,6 @@
"refresh": "Refresh", "refresh": "Refresh",
"new": "New", "new": "New",
"filterName": "Filter by name", "filterName": "Filter by name",
"filterCode": "Filter by code",
"searchStatus": "Search statuses", "searchStatus": "Search statuses",
"emptyStatus": "No matching statuses", "emptyStatus": "No matching statuses",
"query": "Search", "query": "Search",
@@ -1621,8 +1620,6 @@
"actions": "Actions", "actions": "Actions",
"loadingRows": "Loading skills...", "loadingRows": "Loading skills...",
"emptyRows": "No matching skills", "emptyRows": "No matching skills",
"codeRequired": "Enter a skill code.",
"codeInvalid": "Skill codes can contain letters, numbers, underscores, and hyphens only.",
"nameRequired": "Enter a skill name.", "nameRequired": "Enter a skill name.",
"instructionRequired": "Enter skill instructions.", "instructionRequired": "Enter skill instructions.",
"editTitle": "Edit", "editTitle": "Edit",
@@ -1632,8 +1629,6 @@
"save": "Save", "save": "Save",
"create": "Create", "create": "Create",
"loading": "Loading...", "loading": "Loading...",
"code": "Code",
"codePlaceholder": "Example: refund_skill",
"name": "Name", "name": "Name",
"namePlaceholder": "Example: Refund Handling", "namePlaceholder": "Example: Refund Handling",
"description": "Description", "description": "Description",
-5
View File
@@ -1613,7 +1613,6 @@
"refresh": "刷新", "refresh": "刷新",
"new": "新建", "new": "新建",
"filterName": "按名称筛选", "filterName": "按名称筛选",
"filterCode": "按编码筛选",
"searchStatus": "搜索状态", "searchStatus": "搜索状态",
"emptyStatus": "未找到状态", "emptyStatus": "未找到状态",
"query": "查询", "query": "查询",
@@ -1622,8 +1621,6 @@
"actions": "操作", "actions": "操作",
"loadingRows": "正在加载 Skill...", "loadingRows": "正在加载 Skill...",
"emptyRows": "没有匹配的 Skill", "emptyRows": "没有匹配的 Skill",
"codeRequired": "Skill 编码不能为空",
"codeInvalid": "Skill 编码仅支持字母、数字、下划线和中划线",
"nameRequired": "Skill 名称不能为空", "nameRequired": "Skill 名称不能为空",
"instructionRequired": "技能说明不能为空", "instructionRequired": "技能说明不能为空",
"editTitle": "编辑", "editTitle": "编辑",
@@ -1633,8 +1630,6 @@
"save": "保存", "save": "保存",
"create": "创建", "create": "创建",
"loading": "加载中...", "loading": "加载中...",
"code": "编码",
"codePlaceholder": "例如:refund_skill",
"name": "名称", "name": "名称",
"namePlaceholder": "例如:退款处理", "namePlaceholder": "例如:退款处理",
"description": "描述", "description": "描述",