feat(skills): add slash command activation

This commit is contained in:
Goon committed 2026-05-24 16:39:44 +07:00
1 parent 14b32bd219
commit c99f7e1fa4
24 files changed
+978 -40

No files matched your search

+49 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"strings"
"sync"
"time"
@@ -130,8 +131,17 @@ type DatabaseConfig struct {
// SkillsConfig configures the skills storage system.
type SkillsConfig struct {
StorageDir string `json:"storage_dir,omitempty"` // directory for skill content (default: dataDir/skills-store/)
MaxUploadSizeMB int `json:"max_upload_size_mb,omitempty"` // per-file ZIP upload limit
StorageDir string `json:"storage_dir,omitempty"` // directory for skill content (default: dataDir/skills-store/)
MaxUploadSizeMB int `json:"max_upload_size_mb,omitempty"` // per-file ZIP upload limit
SlashCommands SkillSlashCommandConfig `json:"slash_commands,omitempty"`
}
// SkillSlashCommandConfig controls explicit slash-command skill activation.
type SkillSlashCommandConfig struct {
Enabled *bool `json:"enabled,omitempty"`
SuggestNotFound *bool `json:"suggest_not_found,omitempty"`
PartialMatching bool `json:"partial_matching,omitempty"`
Prefix string `json:"prefix,omitempty"`
}
const (
@@ -139,7 +149,13 @@ const (
MinSkillMaxUploadSizeMB = 1
MaxSkillMaxUploadSizeMB = 500
SkillMaxUploadSizeSystemConfigKey = "skills.max_upload_size_mb"
DefaultSkillSlashCommandPrefix = "/"
SkillMaxUploadSizeSystemConfigKey = "skills.max_upload_size_mb"
SkillSlashCommandsEnabledSystemConfigKey = "skills.slash_commands.enabled"
SkillSlashSuggestNotFoundSystemConfigKey = "skills.slash_commands.suggest_not_found"
SkillSlashPartialMatchingSystemConfigKey = "skills.slash_commands.partial_matching"
SkillSlashCommandPrefixSystemConfigKey = "skills.slash_commands.prefix"
)
func ClampSkillMaxUploadSizeMB(value int) int {
@@ -163,6 +179,36 @@ func (c SkillsConfig) EffectiveMaxUploadSizeBytes() int64 {
return int64(c.EffectiveMaxUploadSizeMB()) << 20
}
func (c SkillSlashCommandConfig) EffectiveEnabled() bool {
if c.Enabled == nil {
return true
}
return *c.Enabled
}
func (c SkillSlashCommandConfig) EffectiveSuggestNotFound() bool {
if c.SuggestNotFound == nil {
return true
}
return *c.SuggestNotFound
}
func (c SkillSlashCommandConfig) EffectivePartialMatching() bool {
return c.PartialMatching
}
func (c SkillSlashCommandConfig) EffectivePrefix() string {
prefix := strings.TrimSpace(c.Prefix)
if prefix == "" {
return DefaultSkillSlashCommandPrefix
}
runes := []rune(prefix)
if len(runes) != 1 || runes[0] == ' ' || runes[0] == '\t' || runes[0] == '\n' || runes[0] == '\r' {
return DefaultSkillSlashCommandPrefix
}
return prefix
}
// AgentBinding maps a channel/peer pattern to a specific agent.
// Matching TS AgentBinding from config/types.agents.ts.
type AgentBinding struct {
+24
View File
@@ -63,6 +63,15 @@ func isLoopbackGatewayHost(host string) bool {
return err == nil && addr.IsLoopback()
}
func parseEnvBool(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
// Default returns a Config with sensible defaults.
func Default() *Config {
return &Config{
@@ -242,6 +251,21 @@ func (c *Config) applyEnvOverrides() {
c.Skills.MaxUploadSizeMB = ClampSkillMaxUploadSizeMB(mb)
}
}
envBoolPtr := func(key string, dst **bool) {
if v := os.Getenv(key); v != "" {
b := parseEnvBool(v)
*dst = &b
}
}
envBool := func(key string, dst *bool) {
if v := os.Getenv(key); v != "" {
*dst = parseEnvBool(v)
}
}
envBoolPtr("GOCLAW_SKILLS_SLASH_COMMANDS_ENABLED", &c.Skills.SlashCommands.Enabled)
envBoolPtr("GOCLAW_SKILLS_SLASH_COMMANDS_SUGGEST_NOT_FOUND", &c.Skills.SlashCommands.SuggestNotFound)
envBool("GOCLAW_SKILLS_SLASH_COMMANDS_PARTIAL_MATCHING", &c.Skills.SlashCommands.PartialMatching)
envStr("GOCLAW_SKILLS_SLASH_COMMANDS_PREFIX", &c.Skills.SlashCommands.Prefix)
// Database
envStr("GOCLAW_POSTGRES_DSN", &c.Database.PostgresDSN)
+84
View File
@@ -27,6 +27,18 @@ func TestDefault_SensibleDefaults(t *testing.T) {
if cfg.Skills.EffectiveMaxUploadSizeMB() != DefaultSkillMaxUploadSizeMB {
t.Fatalf("default skill upload max: got %d, want %d", cfg.Skills.EffectiveMaxUploadSizeMB(), DefaultSkillMaxUploadSizeMB)
}
if !cfg.Skills.SlashCommands.EffectiveEnabled() {
t.Fatal("slash commands should default enabled")
}
if !cfg.Skills.SlashCommands.EffectiveSuggestNotFound() {
t.Fatal("slash command suggestions should default enabled")
}
if cfg.Skills.SlashCommands.EffectivePartialMatching() {
t.Fatal("slash command partial matching should default disabled")
}
if cfg.Skills.SlashCommands.EffectivePrefix() != "/" {
t.Fatalf("slash command prefix = %q, want /", cfg.Skills.SlashCommands.EffectivePrefix())
}
}
@@ -129,6 +141,78 @@ func TestLoad_SkillsMaxUploadSizeFromFileAndEnv(t *testing.T) {
}
}
func TestLoad_SkillSlashCommandsFromFileEnvAndSystemConfig(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json5")
os.WriteFile(cfgPath, []byte(`{
"skills": {
"slash_commands": {
"enabled": false,
"suggest_not_found": false,
"partial_matching": true,
"prefix": "!"
}
}
}`), 0644)
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("load error: %v", err)
}
if cfg.Skills.SlashCommands.EffectiveEnabled() {
t.Fatal("file enabled override should be false")
}
if cfg.Skills.SlashCommands.EffectiveSuggestNotFound() {
t.Fatal("file suggestion override should be false")
}
if !cfg.Skills.SlashCommands.EffectivePartialMatching() {
t.Fatal("file partial matching override should be true")
}
if cfg.Skills.SlashCommands.EffectivePrefix() != "!" {
t.Fatalf("file prefix = %q, want !", cfg.Skills.SlashCommands.EffectivePrefix())
}
t.Setenv("GOCLAW_SKILLS_SLASH_COMMANDS_ENABLED", "true")
t.Setenv("GOCLAW_SKILLS_SLASH_COMMANDS_SUGGEST_NOT_FOUND", "true")
t.Setenv("GOCLAW_SKILLS_SLASH_COMMANDS_PARTIAL_MATCHING", "false")
t.Setenv("GOCLAW_SKILLS_SLASH_COMMANDS_PREFIX", "#")
cfg, err = Load(cfgPath)
if err != nil {
t.Fatalf("load with env error: %v", err)
}
if !cfg.Skills.SlashCommands.EffectiveEnabled() {
t.Fatal("env enabled override should be true")
}
if !cfg.Skills.SlashCommands.EffectiveSuggestNotFound() {
t.Fatal("env suggestion override should be true")
}
if cfg.Skills.SlashCommands.EffectivePartialMatching() {
t.Fatal("env partial matching override should be false")
}
if cfg.Skills.SlashCommands.EffectivePrefix() != "#" {
t.Fatalf("env prefix = %q, want #", cfg.Skills.SlashCommands.EffectivePrefix())
}
cfg.ApplySystemConfigs(map[string]string{
"skills.slash_commands.enabled": "false",
"skills.slash_commands.suggest_not_found": "false",
"skills.slash_commands.partial_matching": "true",
"skills.slash_commands.prefix": "%",
})
if cfg.Skills.SlashCommands.EffectiveEnabled() {
t.Fatal("system enabled override should be false")
}
if cfg.Skills.SlashCommands.EffectiveSuggestNotFound() {
t.Fatal("system suggestion override should be false")
}
if !cfg.Skills.SlashCommands.EffectivePartialMatching() {
t.Fatal("system partial matching override should be true")
}
if cfg.Skills.SlashCommands.EffectivePrefix() != "%" {
t.Fatalf("system prefix = %q, want %%", cfg.Skills.SlashCommands.EffectivePrefix())
}
}
func TestSkillsMaxUploadSizeClampAndSystemConfigOverlay(t *testing.T) {
cfg := Default()
cfg.Skills.MaxUploadSizeMB = 0
+4
View File
@@ -77,6 +77,10 @@ func (c *Config) ApplySystemConfigs(configs map[string]string) {
// Skills
integer(SkillMaxUploadSizeSystemConfigKey, &c.Skills.MaxUploadSizeMB)
c.Skills.MaxUploadSizeMB = ClampSkillMaxUploadSizeMB(c.Skills.MaxUploadSizeMB)
boolean(SkillSlashCommandsEnabledSystemConfigKey, &c.Skills.SlashCommands.Enabled)
boolean(SkillSlashSuggestNotFoundSystemConfigKey, &c.Skills.SlashCommands.SuggestNotFound)
boolValue(SkillSlashPartialMatchingSystemConfigKey, &c.Skills.SlashCommands.PartialMatching)
str(SkillSlashCommandPrefixSystemConfigKey, &c.Skills.SlashCommands.Prefix)
// TTS
str("tts.provider", &c.Tts.Provider)