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/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
||||||
github.com/openai/openai-go/v3 v3.22.0
|
github.com/openai/openai-go/v3 v3.22.0
|
||||||
github.com/slack-go/slack v0.17.3
|
github.com/slack-go/slack v0.17.3
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tencent-connect/botgo v0.2.1
|
github.com/tencent-connect/botgo v0.2.1
|
||||||
golang.org/x/oauth2 v0.35.0
|
golang.org/x/oauth2 v0.35.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
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 (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,12 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||||
return
|
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) {
|
if !c.IsAllowed(m.Author.ID) {
|
||||||
logger.DebugCF("discord", "Message rejected by allowlist", map[string]any{
|
logger.DebugCF("discord", "Message rejected by allowlist", map[string]any{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package skills
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -11,6 +13,13 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxNameLength = 64
|
||||||
|
MaxDescriptionLength = 1024
|
||||||
|
)
|
||||||
|
|
||||||
type SkillMetadata struct {
|
type SkillMetadata struct {
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Description string `json:"description" yaml:"description"`
|
Description string `json:"description" yaml:"description"`
|
||||||
|
|
@ -23,6 +32,27 @@ type SkillInfo struct {
|
||||||
Description string `json:"description"`
|
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 {
|
type SkillsLoader struct {
|
||||||
workspace string
|
workspace string
|
||||||
workspaceSkills string // workspace skills (项目级别)
|
workspaceSkills string // workspace skills (项目级别)
|
||||||
|
|
@ -56,6 +86,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
metadata := sl.getSkillMetadata(skillFile)
|
metadata := sl.getSkillMetadata(skillFile)
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
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)
|
skills = append(skills, info)
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +126,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
metadata := sl.getSkillMetadata(skillFile)
|
metadata := sl.getSkillMetadata(skillFile)
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
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)
|
skills = append(skills, info)
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +165,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
metadata := sl.getSkillMetadata(skillFile)
|
metadata := sl.getSkillMetadata(skillFile)
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
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)
|
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
|
// Generate unique filename with UUID prefix to prevent conflicts
|
||||||
ext := filepath.Ext(filename)
|
|
||||||
safeName := SanitizeFilename(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
|
// Create HTTP request
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue