diff --git a/go.mod b/go.mod index e57de7951..cb05fcd0e 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,17 @@ require ( github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/openai/openai-go/v3 v3.22.0 github.com/slack-go/slack v0.17.3 + github.com/stretchr/testify v1.11.1 github.com/tencent-connect/botgo v0.2.1 golang.org/x/oauth2 v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) + require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index e65c99eec..cb8014086 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -140,6 +140,12 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag return } + if err := c.session.ChannelTyping(m.ChannelID); err != nil { + logger.ErrorCF("discord", "Failed to send typing indicator", map[string]any{ + "error": err.Error(), + }) + } + // 检查白名单,避免为被拒绝的用户下载附件和转录 if !c.IsAllowed(m.Author.ID) { logger.DebugCF("discord", "Message rejected by allowlist", map[string]any{ diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index b76884db1..1bba848f8 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -2,7 +2,9 @@ package skills import ( "encoding/json" + "errors" "fmt" + "log/slog" "os" "path/filepath" "regexp" @@ -11,6 +13,13 @@ import ( "gopkg.in/yaml.v3" ) +var namePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$`) + +const ( + MaxNameLength = 64 + MaxDescriptionLength = 1024 +) + type SkillMetadata struct { Name string `json:"name" yaml:"name"` Description string `json:"description" yaml:"description"` @@ -23,6 +32,27 @@ type SkillInfo struct { Description string `json:"description"` } +func (info SkillInfo) validate() error { + var errs error + if info.Name == "" { + errs = errors.Join(errs, errors.New("name is required")) + } else { + if len(info.Name) > MaxNameLength { + errs = errors.Join(errs, fmt.Errorf("name exceeds %d characters", MaxNameLength)) + } + if !namePattern.MatchString(info.Name) { + errs = errors.Join(errs, errors.New("name must be alphanumeric with hyphens")) + } + } + + if info.Description == "" { + errs = errors.Join(errs, errors.New("description is required")) + } else if len(info.Description) > MaxDescriptionLength { + errs = errors.Join(errs, fmt.Errorf("description exceeds %d character", MaxDescriptionLength)) + } + return errs +} + type SkillsLoader struct { workspace string workspaceSkills string // workspace skills (项目级别) @@ -56,6 +86,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from workspace", "name", info.Name, "error", err) + continue } skills = append(skills, info) } @@ -91,6 +126,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from global", "name", info.Name, "error", err) + continue } skills = append(skills, info) } @@ -125,6 +165,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from builtin", "name", info.Name, "error", err) + continue } skills = append(skills, info) } diff --git a/pkg/skills/loader_test.go b/pkg/skills/loader_test.go new file mode 100644 index 000000000..e0e7109cf --- /dev/null +++ b/pkg/skills/loader_test.go @@ -0,0 +1,77 @@ +package skills + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSkillsInfoValidate(t *testing.T) { + testcases := []struct { + name string + skillName string + description string + wantErr bool + errContains []string + }{ + { + name: "valid-skill", + skillName: "valid-skill", + description: "a valid skill description", + wantErr: false, + }, + { + name: "empty-name", + skillName: "", + description: "description without name", + wantErr: true, + errContains: []string{"name is required"}, + }, + { + name: "empty-description", + skillName: "skill-without-description", + description: "", + wantErr: true, + errContains: []string{"description is required"}, + }, + { + name: "empty-both", + skillName: "", + description: "", + wantErr: true, + errContains: []string{"name is required", "description is required"}, + }, + { + name: "name-with-spaces", + skillName: "skill with spaces", + description: "invalid name with spaces", + wantErr: true, + errContains: []string{"name must be alphanumeric with hyphens"}, + }, + { + name: "name-with-underscore", + skillName: "skill_underscore", + description: "invalid name with underscore", + wantErr: true, + errContains: []string{"name must be alphanumeric with hyphens"}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + info := SkillInfo{ + Name: tc.skillName, + Description: tc.description, + } + err := info.validate() + if tc.wantErr { + assert.Error(t, err) + for _, msg := range tc.errContains { + assert.ErrorContains(t, err, msg) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/utils/media.go b/pkg/utils/media.go index 6345da8fc..2b184f2ec 100644 --- a/pkg/utils/media.go +++ b/pkg/utils/media.go @@ -73,9 +73,8 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { } // Generate unique filename with UUID prefix to prevent conflicts - ext := filepath.Ext(filename) safeName := SanitizeFilename(filename) - localPath := filepath.Join(mediaDir, uuid.New().String()[:8]+"_"+safeName+ext) + localPath := filepath.Join(mediaDir, uuid.New().String()[:8]+"_"+safeName) // Create HTTP request req, err := http.NewRequest("GET", url, nil)