mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-16 12:15:20 +00:00
feat(skills): add slash command activation
This commit is contained in:
1 parent
14b32bd219
commit
c99f7e1fa4
24 files changed
+978
-40
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user