Merge remote-tracking branch 'origin/main' into simple
This commit is contained in:
commit
0ef90956e2
5 changed files with 135 additions and 2 deletions
6
go.mod
6
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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
77
pkg/skills/loader_test.go
Normal file
77
pkg/skills/loader_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue