feat(discord): Add typing heartbeat and fix safety guard for scoped npm packages
This commit is contained in:
parent
e60728cf96
commit
2dafadf079
5 changed files with 69 additions and 8 deletions
|
|
@ -8,3 +8,8 @@ config/
|
|||
*.md
|
||||
LICENSE
|
||||
assets/
|
||||
docker-compose.yml
|
||||
workspace/
|
||||
picoclaw-workspace/
|
||||
config/picoclaw-workspace/
|
||||
tmp/
|
||||
|
|
|
|||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -44,3 +44,10 @@ tasks/
|
|||
|
||||
# Added by goreleaser init:
|
||||
dist/
|
||||
|
||||
# Local Dev
|
||||
docker-compose.yml
|
||||
workspace/
|
||||
picoclaw-workspace/
|
||||
config/picoclaw-workspace/
|
||||
tmp/
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ RUN make build
|
|||
FROM alpine:3.23
|
||||
|
||||
# Install runtime essentials
|
||||
RUN apk add --no-cache ca-certificates tzdata curl
|
||||
RUN apk add --no-cache ca-certificates tzdata curl nodejs npm
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
|
@ -25,6 +26,7 @@ type DiscordChannel struct {
|
|||
config config.DiscordConfig
|
||||
transcriber *voice.GroqTranscriber
|
||||
ctx context.Context
|
||||
typingTasks sync.Map
|
||||
}
|
||||
|
||||
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
|
||||
|
|
@ -100,6 +102,18 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
|||
return fmt.Errorf("channel ID is empty")
|
||||
}
|
||||
|
||||
// Stop typing indicator heartbeat for this channel
|
||||
if stop, ok := c.typingTasks.Load(channelID); ok {
|
||||
if stopChan, ok := stop.(chan struct{}); ok {
|
||||
select {
|
||||
case <-stopChan:
|
||||
default:
|
||||
close(stopChan)
|
||||
}
|
||||
}
|
||||
c.typingTasks.Delete(channelID)
|
||||
}
|
||||
|
||||
runes := []rune(msg.Content)
|
||||
if len(runes) == 0 {
|
||||
return nil
|
||||
|
|
@ -286,11 +300,33 @@ 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(),
|
||||
})
|
||||
// Start typing indicator heartbeat
|
||||
stopTyping := make(chan struct{})
|
||||
if old, ok := c.typingTasks.Load(m.ChannelID); ok {
|
||||
if oldChan, ok := old.(chan struct{}); ok {
|
||||
select {
|
||||
case <-oldChan:
|
||||
default:
|
||||
close(oldChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.typingTasks.Store(m.ChannelID, stopTyping)
|
||||
|
||||
go func(chID string, stop chan struct{}) {
|
||||
// Send initial typing
|
||||
_ = c.session.ChannelTyping(chID)
|
||||
ticker := time.NewTicker(7 * time.Second) // Discord typing lasts ~10s
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = c.session.ChannelTyping(chID)
|
||||
}
|
||||
}
|
||||
}(m.ChannelID, stopTyping)
|
||||
|
||||
// 检查白名单,避免为被拒绝的用户下载附件和转录
|
||||
if !c.IsAllowed(m.Author.ID) {
|
||||
|
|
|
|||
|
|
@ -181,10 +181,23 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
pathPattern := regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`)
|
||||
matches := pathPattern.FindAllString(cmd, -1)
|
||||
// Match absolute paths: Unix (starts with / after space/start/quotes) or Windows (X:\)
|
||||
// This regex is careful not to match scoped packages like @mastra/core
|
||||
pathPattern := regexp.MustCompile(`(?:\s|^|["'|&;])(/[^\s\"'|&;]+)|([A-Za-z]:\\[^\\\"'|&;]+)`)
|
||||
matches := pathPattern.FindAllStringSubmatch(cmd, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
raw := ""
|
||||
if match[1] != "" {
|
||||
raw = match[1] // Unix path
|
||||
} else if match[2] != "" {
|
||||
raw = match[2] // Windows path
|
||||
}
|
||||
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, raw := range matches {
|
||||
p, err := filepath.Abs(raw)
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue