返回 DeepSeek-Reasonix
slash_resolution.go
根目录 / internal / control / slash_resolution.go
1 package control
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/command"
8 "reasonix/internal/skill"
9 )
10
11 const (
12 DocsSlashName = "docs"
13 ReasonixDocsSlashName = "reasonix:docs"
14 )
15
16 // SlashCommandOwner identifies the runtime owner of a short slash name. Custom
17 // commands win over skills, matching Controller dispatch. A single plugin skill
18 // may own a compatible short name even though only its package-qualified name is
19 // shown in discovery surfaces.
20 type SlashCommandOwner uint8
21
22 const (
23 SlashOwnerBuiltin SlashCommandOwner = iota
24 SlashOwnerSkill
25 SlashOwnerCustom
26 )
27
28 // ResolveSlashCommandOwner is the shared precedence decision used by dispatch
29 // and every slash-command discovery surface.
30 func ResolveSlashCommandOwner(name string, commands []command.Command, skills []skill.Skill) SlashCommandOwner {
31 name = strings.TrimPrefix(strings.TrimSpace(name), "/")
32 if name == "" {
33 return SlashOwnerBuiltin
34 }
35 for _, cmd := range commands {
36 if cmd.Name == name {
37 return SlashOwnerCustom
38 }
39 }
40 if _, ok := skill.ResolveSlashSkill(skills, name); ok {
41 return SlashOwnerSkill
42 }
43 return SlashOwnerBuiltin
44 }
45
46 // ResolvedBuiltinSlashName returns the user-facing name for a built-in slash
47 // command. Its short name remains the default; when a compatible custom command
48 // or skill owns that short name, the built-in stays reachable through Reasonix's
49 // explicit namespace.
50 func ResolvedBuiltinSlashName(name string, commands []command.Command, skills []skill.Skill) string {
51 name = strings.TrimPrefix(strings.TrimSpace(name), "/")
52 if ResolveSlashCommandOwner(name, commands, skills) == SlashOwnerBuiltin {
53 return name
54 }
55 return QualifiedBuiltinSlashName(name, commands, skills)
56 }
57
58 // QualifiedBuiltinSlashName returns a deterministic Reasonix-owned fallback
59 // that does not displace any existing custom command or skill. The preferred
60 // form is /reasonix:<name>; progressively more explicit names are used only if
61 // a user already owns that spelling too.
62 func QualifiedBuiltinSlashName(name string, commands []command.Command, skills []skill.Skill) string {
63 name = strings.TrimPrefix(strings.TrimSpace(name), "/")
64 candidates := []string{"reasonix:" + name, "reasonix:builtin:" + name}
65 for _, candidate := range candidates {
66 if ResolveSlashCommandOwner(candidate, commands, skills) == SlashOwnerBuiltin {
67 return candidate
68 }
69 }
70 for suffix := 2; ; suffix++ {
71 candidate := fmt.Sprintf("reasonix:builtin:%s:%d", name, suffix)
72 if ResolveSlashCommandOwner(candidate, commands, skills) == SlashOwnerBuiltin {
73 return candidate
74 }
75 }
76 }
77
78 // IsBuiltinDocsSlash reports whether name should execute the embedded Reasonix
79 // documentation command. The short name is built-in only when unoccupied; the
80 // qualified fallback is selected without displacing an existing command or skill.
81 func IsBuiltinDocsSlash(name string, commands []command.Command, skills []skill.Skill) bool {
82 name = strings.TrimPrefix(strings.TrimSpace(name), "/")
83 if name == DocsSlashName {
84 return ResolveSlashCommandOwner(name, commands, skills) == SlashOwnerBuiltin
85 }
86 return name == QualifiedBuiltinSlashName(DocsSlashName, commands, skills)
87 }
88
88 lines GO