返回 DeepSeek-Reasonix
skill_health.go
根目录 / internal / doctor / skill_health.go
1 package doctor
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/config"
8 "reasonix/internal/skill"
9 "reasonix/internal/tool"
10 _ "reasonix/internal/tool/builtin" // Initialize compile-time tool identities.
11 )
12
13 // SkillHealthOptions configures skill/MCP capability diagnostics for doctor.
14 type SkillHealthOptions struct {
15 Skills []skill.Skill
16 Tools []tool.ContractEntry
17 Bindings []tool.MCPBinding
18 Plugins []config.PluginEntry
19 // FailedServers maps MCP server name → host-proven failure reason.
20 FailedServers map[string]string
21 // CacheMismatch lists MCP servers whose schema cache fingerprint mismatched.
22 CacheMismatch []string
23 }
24
25 // CollectSkillHealthWarnings returns human-readable skill/MCP health warnings.
26 func CollectSkillHealthWarnings(opts SkillHealthOptions) []string {
27 var out []string
28 for _, diagnostic := range skill.CheckToolReferences(opts.Skills, skill.ToolReferenceOptions{Known: tool.KnownToolNames(), Registered: opts.Tools, Bindings: opts.Bindings}) {
29 out = append(out, diagnostic.Message)
30 }
31 for _, d := range skill.CheckMCPRequirements(opts.Skills, opts.Plugins, opts.FailedServers) {
32 out = append(out, d.Message)
33 }
34
35 // Detect require skills with identical trigger sets (ambiguous conflicts).
36 requireTriggers := map[string][]string{} // key=sorted triggers → skill names
37
38 for _, sk := range opts.Skills {
39 name := sk.Name
40 desc := strings.TrimSpace(sk.Description)
41 if desc == "" || strings.Contains(desc, "no description") || desc == "(no description)" {
42 out = append(out, fmt.Sprintf("skill %q has a missing or placeholder description", name))
43 }
44 // Trigger / negative-trigger conflicts.
45 neg := map[string]bool{}
46 for _, n := range sk.NegativeTriggers {
47 neg[strings.ToLower(strings.TrimSpace(n))] = true
48 }
49 for _, tr := range sk.Triggers {
50 if neg[strings.ToLower(strings.TrimSpace(tr))] {
51 out = append(out, fmt.Sprintf("skill %q trigger %q also appears in negative-triggers", name, tr))
52 }
53 }
54 // auto-use require with missing dependencies.
55 if strings.EqualFold(sk.AutoUse, "require") {
56 key := strings.Join(normalizedTriggers(sk.Triggers), "|")
57 if key != "" {
58 requireTriggers[key] = append(requireTriggers[key], name)
59 }
60 }
61 // The parser drops illegal profiles values from Profiles but preserves
62 // them in InvalidProfiles precisely so this check can reach them.
63 for _, p := range sk.InvalidProfiles {
64 out = append(out, fmt.Sprintf("skill %q has illegal profiles value %q (valid: economy, balanced, delivery)", name, p))
65 }
66 }
67
68 for key, names := range requireTriggers {
69 if len(names) > 1 {
70 out = append(out, fmt.Sprintf("multiple require skills share identical triggers [%s]: %s", key, strings.Join(names, ", ")))
71 }
72 }
73
74 for _, srv := range opts.CacheMismatch {
75 out = append(out, fmt.Sprintf("MCP server %q schema cache fingerprint mismatched; tools may be stale until reconnect", srv))
76 }
77 for srv, reason := range opts.FailedServers {
78 out = append(out, fmt.Sprintf("MCP server %q is in a host-failed state: %s", srv, reason))
79 }
80 return out
81 }
82
83 func normalizedTriggers(in []string) []string {
84 out := make([]string, 0, len(in))
85 for _, t := range in {
86 t = strings.ToLower(strings.TrimSpace(t))
87 if t != "" {
88 out = append(out, t)
89 }
90 }
91 // sort-like: simple insertion for small lists
92 for i := 1; i < len(out); i++ {
93 j := i
94 for j > 0 && out[j] < out[j-1] {
95 out[j], out[j-1] = out[j-1], out[j]
96 j--
97 }
98 }
99 return out
100 }
101
101 lines GO